Section 16 de decisions.md : pourquoi des étiquettes libres plutôt qu'un référentiel, pourquoi un filtre conjonctif plutôt que disjonctif, pourquoi il n'est pas enregistré alors que le masquage par l'œil l'est — avec un tableau qui oppose les deux —, pourquoi la couleur d'un tag est calculée et non choisie, et pourquoi un tag trop long est tronqué là où une date invalide bloque tout. Le README décrit l'usage : saisie des tags, filtre cumulatif, portée limitée à la session. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
15 KiB
Décisions de conception
Ce document garde la trace des arbitrages et surtout de leurs raisons, pour éviter de refaire les mêmes débats dans six mois.
1. Pourquoi un outil sur-mesure
Les suites complètes (Redmine, Taiga, OpenProject) exigent un serveur applicatif et une base de données à administrer — hors de proportion avec le besoin. Les mini-outils « un seul fichier HTML » sont des Kanban sans dates.
Markwhen est le concurrent sérieux : timeline en couloirs engendrée depuis du texte markdown-ish, open source, self-hostable, avec extension VS Code et vue Gantt. Il couvre une bonne partie du besoin.
Deux choses manquent, et ce sont elles qui justifient ce projet :
- la manipulation directe des barres (glisser, redimensionner) ;
- le suivi de dérive face à un planning de référence figé.
Si ces deux besoins disparaissaient, il faudrait sérieusement envisager d'abandonner cet outil au profit de Markwhen.
2. Pas de dépendances entre phases
Une première version du cahier des charges prévoyait des dépendances fin→début avec propagation en cascade. Abandonné.
Le coût est très supérieur à ce qu'il paraît : graphe orienté, détection de cycles, et surtout une sémantique de propagation à trancher — la poussée est-elle une contrainte dure ou souple, la marge existante est-elle conservée, avancer un prédécesseur tire-t-il ses successeurs en arrière, que faire d'une date imposée de l'extérieur. Chacune de ces questions a plusieurs réponses défendables, et se tromper produit un outil qui déplace des barres dans le dos de l'utilisateur.
En regard, le bénéfice réel en planification macro est faible. Le besoin concret — « ce projet glisse d'un mois » — se traite en décalant les phases concernées à la main, ce qui prend quelques secondes sur un projet de cinq ou six phases.
Conséquence heureuse : sans flèches à tracer entre les barres, le SVG perd sa raison d'être (voir décision 4).
3. Micro-serveur Python plutôt que File System Access API
La première approche envisagée était d'écrire directement dans le fichier depuis le navigateur, via
showOpenFilePicker / createWritable, pour se passer de tout back-end.
Sauf que cette API exige un contexte sécurisé : elle ne fonctionne pas depuis file://. Il
fallait donc de toute façon lancer un serveur statique. Le « zéro back-end » était perdu d'avance,
tout en payant trois prix : limitation aux navigateurs Chromium, re-autorisation du sélecteur de
fichier à chaque session, et maintien d'un chemin de repli export/import pour Firefox et Safari,
doublé d'un miroir localStorage.
Puisqu'une commande doit être lancée dans tous les cas, autant qu'elle serve à quelque chose. Un
serveur de la bibliothèque standard, GET et PUT sur /api/data, tient en une centaine de lignes
— soit moins de code que l'API navigateur plus son repli plus son miroir. Et il fonctionne
partout, sans sélecteur à réautoriser.
Bénéfice supplémentaire : le serveur peut écrire une sauvegarde horodatée à chaque écriture, ce que la File System Access API ne permettait pas simplement.
4. DOM et CSS plutôt que SVG
Le SVG s'imposait tant qu'il fallait tracer des flèches courbes entre des barres. Les dépendances supprimées, il ne reste que des rectangles, du texte et des losanges.
Des div positionnées en absolu font tout cela plus simplement : troncature du texte par
text-overflow, curseurs de redimensionnement, styles de survol, transitions, tout est natif en CSS.
En SVG il faudrait mesurer le texte à la main et réimplémenter des comportements que le navigateur
offre gratuitement. Les losanges de jalon se font par une rotation CSS de 45°.
5. Identifiants parlants plutôt qu'UUID
Le fichier de données est censé rester ouvrable dans un éditeur de texte et versionné dans git. Des UUID rendent les deux pénibles : impossible de savoir quelle phase on lit, et un diff illisible.
site-web et cadrage coûtent la même chose à manipuler et gardent le fichier compréhensible. La
contrepartie — gérer les collisions à la création — est traitée par un suffixe numérique.
Un identifiant ne change jamais quand le nom est modifié : il identifie, il ne décrit pas. Renommer une phase ne doit pas invalider les références qui pointent vers elle.
6. Statut plutôt que pourcentage d'avancement
Un pourcentage d'avancement sur une phase macro de plusieurs semaines est presque toujours un chiffre
inventé, et il donne une fausse impression de précision. Quatre statuts — à venir, en cours, terminé,
bloqué — se saisissent en un clic et disent l'essentiel. blocked en particulier porte une
information qu'un pourcentage ne peut pas exprimer.
7. Jours calendaires
Gérer les jours ouvrés impose de faire passer chaque calcul de date par un helper, et soulève aussitôt la question des congés et des jours fériés — donc un calendrier à tenir à jour. Sur des phases qui se comptent en semaines et en mois, l'écart est dans le bruit de l'estimation.
8. Le mode plié est en lecture seule
Un couloir plié affiche une barre unique segmentée par phase. On aurait pu rendre cette barre glissable pour décaler tout le projet en bloc.
Écarté pour une raison de lisibilité du geste : la même barre représenterait tantôt une phase, tantôt un projet entier, et un glissement de quelques pixels déplacerait alors plusieurs mois de travail d'un coup, sans que l'utilisateur voie ce qui bouge. Le mode plié sert à regarder ; pour modifier, on déplie.
9. Accroche à la semaine
Au zoom trimestre, un jour représente quelques pixels : viser une date précise à la souris devient un exercice de patience. L'accroche au lundi rend le geste franc et correspond à la granularité réelle d'une planification macro.
Les dates exactes restent saisissables au clavier dans le panneau de détail, ce qui couvre les cas où une phase doit démarrer un jeudi précis.
10. Zéro build
Modules ES natifs, chargés directement par le navigateur. Pas de package.json, pas de bundler, pas
de node_modules. Le fichier qu'on lit dans l'éditeur est exactement celui qu'exécute le navigateur,
ce qui rend le débogage direct et l'outil reprenable dans plusieurs années sans exhumer une chaîne de
compilation obsolète.
Node n'est utilisé que pour lancer les tests, via son lanceur intégré (node --test), sans aucune
dépendance.
11. Un en-tête à trois bandes, la semaine pour grain le plus fin
L'en-tête affichait une seule rangée de graduations dont le contenu changeait selon le zoom : des quantièmes en vue semaine, des noms de mois en vue trimestre. Il fallait deviner l'unité affichée, et l'année n'apparaissait qu'au zoom le plus large.
Trois bandes empilées — année, mois, numéro de semaine ISO — lèvent l'ambiguïté : les trois repères sont toujours là, quel que soit le zoom. Chaque cellule est un bloc couvrant exactement son intervalle, ce qui centre le libellé sur la période qu'il désigne au lieu de le laisser flotter après un trait.
Le jour a disparu : sur des phases qui se comptent en semaines, il n'apportait rien et encombrait l'affichage. Le zoom ne change donc plus l'unité, seulement la largeur des mêmes semaines.
Les numéros suivent l'ISO 8601 : la semaine appartient à l'année où tombe son jeudi.
C'est pourquoi semaineISO() renvoie l'année en même temps que le numéro — afficher
« S53 » sous un bandeau « 2027 » serait faux, alors que le 1er janvier 2027 est bien en
semaine 53 de 2026.
12. Défilement natif sur les deux axes plutôt que synchronisé en JavaScript
La colonne des libellés était un conteneur séparé, recalé sur le défilement vertical de
la frise par un translateY appliqué à chaque événement scroll. Deux défauts : un
décalage visible d'une image sur les défilements rapides, et deux systèmes de coordonnées
à tenir cohérents.
Tout tient désormais dans un unique conteneur défilant, découpé en grille 2×2 : l'en-tête
est sticky top, la colonne des libellés sticky left, et le coin les deux à la fois. Le
navigateur gère seul le défilement des deux axes. Le code de synchronisation a disparu.
13. Menu contextuel plutôt que prompt() et confirm()
Les actions d'un projet passaient par un prompt() listant des numéros d'action à saisir
au clavier, et les suppressions par un confirm(). C'était rapide à écrire et pénible à
utiliser : rien n'indiquait qu'il fallait cliquer sur le nom du projet, et ces boîtes
natives bloquent le fil d'exécution.
Un bouton ⋯ explicite ouvre maintenant un vrai menu, navigable au clavier, où les
actions indisponibles sont grisées plutôt qu'absentes — « Retirer la référence » reste
visible quand aucune référence n'est figée, ce qui apprend que la fonction existe.
Les dialogues s'appuient sur l'élément <dialog> natif, qui fournit le voile, le piège à
focus, la fermeture par Échap et la restauration du focus sans code supplémentaire. Sur
une action destructrice, le focus initial se pose sur « Annuler » : un appui réflexe sur
Entrée ne doit rien détruire.
Une exception assumée : ajouter une phase ne demande pas son nom dans un dialogue. La phase est créée immédiatement à la suite de la précédente, et le panneau de détail s'ouvre avec le nom présélectionné — l'utilisateur tape simplement par-dessus. Un dialogue de plus n'aurait rien apporté.
14. Une seule échelle, pas de zoom
Trois niveaux de zoom — semaine, mois, trimestre — étaient proposés. Retirés.
Depuis que l'en-tête empile l'année, le mois et le numéro de semaine (décision 11), les trois repères utiles sont lisibles en permanence. Le zoom ne résolvait plus qu'un problème qu'il avait lui-même créé : savoir quelle unité on regardait. Il restait un réglage de plus à comprendre, un état de plus à gérer, et un chemin de code de plus à tester.
L'échelle unique est de 6 pixels par jour, soit 42 pixels par semaine : assez pour que « S32 » tienne dans sa cellule, assez serré pour qu'une année tienne dans une fenêtre de portable.
15. Une frise qui s'élargit au défilement
La fenêtre affichée se déduisait des bornes des phases existantes, avec une marge fixe. Conséquence gênante : impossible d'aller regarder un futur vide pour y planifier. La frise s'arrêtait là où s'arrêtaient les données.
La fenêtre est désormais un état de vue distinct des données. Elle démarre autour des phases existantes, puis s'élargit de six mois dès que le défilement passe à moins de 400 pixels d'un bord. On peut ainsi partir vers 2031, y poser une phase, et revenir.
Deux subtilités :
- Élargir vers la gauche déplace l'origine de l'échelle, donc tout le contenu se
décale vers la droite. On compense
scrollLeftde la même quantité, sans quoi la vue sauterait en arrière à chaque élargissement. Un drapeau empêche l'ajustement de relancer le gestionnaire de défilement. - La fenêtre ne rétrécit jamais, même si les phases se resserrent : le terrain déjà exploré doit rester atteignable.
L'élargissement s'arrête à dix ans de part et d'autre des données. Ce n'est donc pas infini au sens strict — c'est un garde-fou : rien n'est virtualisé, et un défilement prolongé ferait autrement enfler le DOM sans limite. À l'échelle d'une planification macro, dix ans dépassent largement l'horizon utile.
Corollaire : le recentrage est instantané, jamais animé. Sur une frise qui peut atteindre plusieurs dizaines de milliers de pixels, une animation serait lente, et le gestionnaire de défilement l'interromprait en élargissant la fenêtre au passage près d'un bord.
Attention en modifiant centrerSur : scrollLeft porte sur la grille entière, dont la
première colonne est celle des libellés. Comme elle est sticky, elle masque en
permanence les premiers pixels de la vue, et le centre utile des couloirs est décalé
d'une demi-largeur de colonne par rapport au centre de la frise.
16. Tags libres et filtre conjonctif
Passé une dizaine de projets, la frise devient un mur : on veut n'y voir que les projets clients, ou que ceux d'un pôle. D'où des tags sur le projet, et un filtre.
Étiquettes libres, pas de référentiel. Aucune liste de tags autorisés n'est tenue quelque part : les tags disponibles sont ceux que portent les projets, et un tag disparaît de la barre de filtres dès qu'il n'orne plus rien. Un référentiel séparé aurait exigé une interface pour le gérer, et se serait désynchronisé. La contrepartie — la dérive orthographique, « client », « Clients », « client final » — est traitée par deux gardes : la comparaison ignore la casse, et le dialogue d'un projet propose en un clic les tags déjà employés ailleurs.
Le filtre exige tous les tags cochés, pas au moins un. Cocher un tag de plus resserre donc toujours la sélection : on part du tout et on élague, sans jamais voir la frise se repeupler en cochant. La disjonction se comporte à l'inverse, et il faut alors relire la liste des tags actifs pour savoir dans quel sens la prochaine case va jouer.
Le filtre n'est pas enregistré. C'est un état de vue, comme la fenêtre temporelle
(décision 15) : un filtre qui survivrait au rechargement rouvrirait le planning amputé sans
qu'on se souvienne pourquoi. Le masquage par l'œil (hidden), lui, est bien enregistré —
et les deux ne font pas la même chose :
| Filtre par tags | Œil (hidden) |
|
|---|---|---|
| Portée | La vue entière, libellé compris | Les barres seules, le libellé reste grisé |
| Durée de vie | La session | Enregistré dans le fichier |
| Question posée | « Je ne regarde que les projets clients » | « Ce projet existe, mais il encombre ma frise » |
La couleur d'un tag est calculée, pas choisie : une teinte HSL dérivée du nom par hachage. Rien à stocker, rien à saisir, et le même tag garde sa couleur partout. Des collisions de teinte sont possibles et sans gravité — le nom est toujours écrit à côté.
Un tag trop long est tronqué, pas refusé. Contrairement à une date invalide, qui bloque le chargement en nommant le fautif, la longueur d'un tag est cosmétique : refuser d'ouvrir un planning entier pour un libellé bavard serait disproportionné. La limite est donc appliquée en normalisant, ce qui la rend vraie par construction — aussi bien pour un fichier édité à la main que pour une saisie dans le dialogue.
Créer un projet alors qu'un filtre est actif préremplit ses tags avec ceux du filtre : sans cela, le nouveau venu naîtrait invisible, hors de la vue depuis laquelle on vient de le demander.