Frise multi-projets : outil de planification macro
Une frise chronologique unique où plusieurs projets s'empilent en couloirs pliables, pour voir d'un coup d'œil où en est chacun et comment ils se situent les uns par rapport aux autres. Ce n'est pas un outil de suivi quotidien : une phase se compte en semaines, et il n'y a ni sous-tâches, ni tickets, ni dépendances entre phases. - Manipulation directe des barres (glisser, redimensionner), accrochées au lundi - Couloirs pliables : plié, un projet devient une barre segmentée par phase - Planning de référence figeable, avec barre fantôme et calcul de dérive - En-tête à trois bandes : année, mois, numéro de semaine ISO 8601 - Frise qui s'élargit au défilement, pour planifier dans un futur encore vide - Micro-serveur Python (bibliothèque standard) exposant GET/PUT sur /api/data, avec sauvegarde horodatée avant chaque écriture - Zéro build : modules ES natifs, aucune dépendance à installer 50 tests unitaires sur la logique métier (node --test, sans dépendance). Vérifié dans Chromium et Firefox. Les arbitrages de conception et surtout leurs raisons sont consignés dans docs/decisions.md — notamment l'abandon des dépendances entre phases, de la File System Access API, du SVG et des niveaux de zoom. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
220
docs/decisions.md
Normal file
220
docs/decisions.md
Normal file
@@ -0,0 +1,220 @@
|
||||
# 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](https://markwhen.com) 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 `scrollLeft` de 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.
|
||||
123
docs/modele-donnees.md
Normal file
123
docs/modele-donnees.md
Normal file
@@ -0,0 +1,123 @@
|
||||
# Modèle de données
|
||||
|
||||
Tout le planning tient dans un seul fichier JSON, par défaut `data/projets.json`.
|
||||
|
||||
Le fichier est écrit indenté sur 2 espaces, avec les clés dans un ordre stable, pour rester lisible
|
||||
dans un diff git et éditable à la main.
|
||||
|
||||
## Structure
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 2,
|
||||
"projects": [
|
||||
{
|
||||
"id": "site-web",
|
||||
"name": "Refonte du site web",
|
||||
"color": "#3b82f6",
|
||||
"collapsed": false,
|
||||
"hidden": false,
|
||||
"baselineDate": "2026-06-12",
|
||||
"phases": [
|
||||
{
|
||||
"id": "cadrage",
|
||||
"name": "Cadrage",
|
||||
"start": "2026-08-01",
|
||||
"end": "2026-08-20",
|
||||
"status": "done",
|
||||
"milestone": false,
|
||||
"notes": "Ateliers avec les trois pôles.",
|
||||
"baseline": { "start": "2026-07-25", "end": "2026-08-10" }
|
||||
},
|
||||
{
|
||||
"id": "livraison",
|
||||
"name": "Mise en ligne",
|
||||
"start": "2026-11-02",
|
||||
"end": "2026-11-02",
|
||||
"status": "todo",
|
||||
"milestone": true,
|
||||
"notes": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Racine
|
||||
|
||||
| Champ | Type | Description |
|
||||
|---|---|---|
|
||||
| `version` | entier | Version du format. Vaut `2`. Sert à détecter un fichier trop ancien au chargement. |
|
||||
| `projects` | tableau | Les projets, dans l'ordre d'affichage des couloirs. |
|
||||
|
||||
## Projet
|
||||
|
||||
| Champ | Type | Description |
|
||||
|---|---|---|
|
||||
| `id` | chaîne | Identifiant parlant, dérivé du nom (`Refonte du site web` → `site-web`). Unique dans le fichier. |
|
||||
| `name` | chaîne | Nom affiché. Non vide. |
|
||||
| `color` | chaîne | Couleur du couloir, en hexadécimal `#rrggbb`. |
|
||||
| `collapsed` | booléen | `true` si le couloir est plié. |
|
||||
| `hidden` | booléen | `true` si le projet est masqué de la frise. Ses données restent intactes. |
|
||||
| `baselineDate` | chaîne ou absent | Date à laquelle la référence a été figée. Absent si elle ne l'a jamais été. |
|
||||
| `phases` | tableau | Les phases, triées par date de début croissante. |
|
||||
|
||||
## Phase
|
||||
|
||||
| Champ | Type | Description |
|
||||
|---|---|---|
|
||||
| `id` | chaîne | Identifiant parlant, unique **au sein de son projet**. |
|
||||
| `name` | chaîne | Nom affiché. Non vide. |
|
||||
| `start` | chaîne | Date de début, `AAAA-MM-JJ`. |
|
||||
| `end` | chaîne | Date de fin **incluse**, `AAAA-MM-JJ`. |
|
||||
| `status` | chaîne | `todo`, `doing`, `done` ou `blocked`. |
|
||||
| `milestone` | booléen | `true` pour un jalon. |
|
||||
| `notes` | chaîne | Texte libre, éventuellement vide. |
|
||||
| `baseline` | objet ou absent | Dates de référence : `{ "start": …, "end": … }`. Absent tant que la référence n'a pas été figée. |
|
||||
|
||||
## Règles de validation
|
||||
|
||||
Appliquées par `js/model.js` et couvertes par `tests/model.test.js`.
|
||||
|
||||
- `end` est postérieure ou égale à `start`.
|
||||
- Un jalon (`milestone: true`) a nécessairement `start === end`. Cocher « jalon » sur une phase de
|
||||
plusieurs jours ramène `end` sur `start`.
|
||||
- Les dates suivent strictement `AAAA-MM-JJ` et doivent exister réellement dans le calendrier
|
||||
(`2026-02-30` est rejetée).
|
||||
- `id` de projet unique dans le fichier ; `id` de phase unique dans son projet.
|
||||
- Un `id` est engendré à partir du nom : minuscules, accents retirés, tout ce qui n'est ni lettre ni
|
||||
chiffre remplacé par un tiret. En cas de collision, un suffixe numérique est ajouté (`cadrage-2`).
|
||||
Un `id` ne change jamais si le nom est modifié ensuite — il identifie, il ne décrit pas.
|
||||
- `status` fait partie des quatre valeurs autorisées ; toute autre valeur est ramenée à `todo`.
|
||||
- `color` est un hexadécimal `#rrggbb` valide.
|
||||
|
||||
Un fichier invalide n'est jamais réparé en silence : le chargement échoue avec un message qui pointe
|
||||
le projet et la phase fautifs, pour que le fichier puisse être corrigé à la main.
|
||||
|
||||
## Dates : conventions
|
||||
|
||||
Les dates sont manipulées comme des **chaînes `AAAA-MM-JJ`**, pas comme des objets `Date`. Cela évite
|
||||
tous les pièges de fuseau horaire : `new Date("2026-08-01")` est interprétée en UTC alors que
|
||||
`new Date(2026, 7, 1)` l'est en heure locale, ce qui décale d'un jour selon le fuseau. Les
|
||||
conversions n'ont lieu que dans les helpers de calcul de `model.js`.
|
||||
|
||||
`end` est **incluse** : une phase du `2026-08-01` au `2026-08-01` dure un jour.
|
||||
|
||||
Tout se compte en **jours calendaires**, week-ends et jours fériés compris.
|
||||
|
||||
Les numéros de semaine affichés dans l'en-tête suivent l'**ISO 8601** : la semaine
|
||||
appartient à l'année où tombe son jeudi. `semaineISO()` renvoie donc le numéro *et*
|
||||
l'année correspondante, qui peut différer de celle de la date.
|
||||
|
||||
## Bornes et dérive
|
||||
|
||||
- Les **bornes d'un projet** vont de la plus petite `start` à la plus grande `end` de ses phases. Un
|
||||
projet sans phase n'a pas de bornes et s'affiche comme un couloir vide.
|
||||
- **Figer la référence** copie, pour chaque phase du projet, ses `start` et `end` courantes dans son
|
||||
champ `baseline`, et inscrit la date du jour dans `baselineDate`. L'opération est rejouable : la
|
||||
refiger après un arbitrage assumé repart d'une base propre.
|
||||
- La **dérive** d'un projet est l'écart, en jours, entre la plus grande `end` actuelle et la plus
|
||||
grande `end` de référence. Elle est affichée arrondie en semaines. Une phase créée après le figeage
|
||||
n'a pas de `baseline` et ne compte pas dans la référence, mais compte dans les dates actuelles :
|
||||
ajouter une phase en fin de projet crée donc bien une dérive, ce qui est le comportement voulu.
|
||||
Reference in New Issue
Block a user