Une phase avait quatre lignes de texte brut ; un projet n'avait rien. Les deux
manques se tiennent : la frise ne dit pas qui pilote ni sur quel budget, et ce
contexte finissait dans les notes de la première phase venue, où il ne survit
pas à sa suppression.
Le bloc de notes est le même dans les deux panneaux et prend toute la hauteur
restante : les autres champs ont une taille dictée par leur contenu, une note
fait ce qu'on a à dire. Il s'ouvre sur l'aperçu quand la note existe, sur la
saisie quand elle est vide.
Les cases à cocher se cliquent dans l'aperçu — seul geste de l'aperçu qui touche
aux données. La bascule réécrit la seule ligne visée plutôt que de régénérer la
note, qui verrait sinon sa mise en forme normalisée. Inertes dans la vue par
mois, en lecture seule (décision 24).
La barre d'outils écrit par `document.execCommand('insertText')` et non en
affectant `value`, qui viderait la pile d'annulation du navigateur : `Ctrl+Z`
doit continuer de marcher. Le DOM est bâti nœud par nœud, sans un `innerHTML`.
Les infobulles aplatissent le markdown — `title` ne connaît que le texte —, et
celles d'un jalon et d'un libellé de projet les affichent désormais.
Le format passe en version 4 : le validateur reconstruit chaque projet champ par
champ, un binaire antérieur effacerait les notes de projet à la première
sauvegarde.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
10 KiB
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
{
"version": 4,
"projects": [
{
"id": "site-web",
"name": "Refonte du site web",
"color": "#3b82f6",
"tags": ["client", "web"],
"collapsed": false,
"hidden": false,
"notes": "## Contexte\n\nPiloté par **Marie D.**\n\n- [x] cadrage validé\n- [ ] recette",
"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.\n\nPérimètre arrêté le **8 juillet**.",
"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 4. Sert à détecter un fichier trop ancien au chargement. |
projects |
tableau | Les projets, dans l'ordre d'affichage des couloirs. |
Un fichier plus ancien se charge sans rien demander et est réécrit au format courant à la première
sauvegarde : une version 2 — celle d'avant les tags — voit ses projets recevoir une liste de tags
vide, une version 3 — celle d'avant les notes de projet — des notes vides.
Un fichier écrit par une version plus récente est en revanche refusé. C'est la raison d'être du numéro : le validateur reconstruit chaque projet champ par champ et laisse tomber ce qu'il ne connaît pas, si bien qu'un binaire antérieur ouvrirait sans broncher un fichier plus riche que lui et en effacerait les champs inconnus à la première sauvegarde.
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. |
tags |
tableau | Étiquettes libres servant à filtrer la frise. Trié, sans doublon, éventuellement vide. |
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. |
notes |
chaîne | Texte libre en markdown, éventuellement vide. Ce que la frise ne sait pas dire : contexte, interlocuteurs, décisions. |
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 en markdown, é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.
endest postérieure ou égale àstart.- Un jalon (
milestone: true) a nécessairementstart === end. Cocher « jalon » sur une phase de plusieurs jours ramèneendsurstart. - Les dates suivent strictement
AAAA-MM-JJet doivent exister réellement dans le calendrier (2026-02-30est rejetée). idde projet unique dans le fichier ;idde phase unique dans son projet.- Un
idest 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). Unidne change jamais si le nom est modifié ensuite — il identifie, il ne décrit pas. statusfait partie des quatre valeurs autorisées ; toute autre valeur est ramenée àtodo.colorest un hexadécimal#rrggbbvalide.tagsest un tableau de chaînes. Le tableau lui-même et le type de ses éléments sont vérifiés — un tag mal typé serait un tag qu'on croit poser et qui ne filtre rien. Leur contenu, en revanche, est normalisé sans erreur (voir ci-dessous).notes, de projet comme de phase, est ramenée à""si ce n'est pas une chaîne. Le champ n'engage aucun calcul, contrairement aux dates : bloquer le chargement d'un planning entier pour lui serait disproportionné.
Tags
Une étiquette libre, posée sur un projet, sans liste fermée à tenir à jour : les tags disponibles sont simplement ceux que portent les projets.
À l'enregistrement, normaliserTags() nettoie la liste :
- espaces de bord retirés, espaces internes réduits à un seul ;
- tags vides écartés ;
- doublons fusionnés à la casse près —
Clientetclientsont le même tag, et c'est la première graphie rencontrée qui est conservée ; - tag tronqué au-delà de 24 caractères (
MAX_LONGUEUR_TAG) ; - tri alphabétique, pour que ressaisir les mêmes tags dans un autre ordre ne produise aucun diff git.
Les accents comptent : éditeur et editeur restent deux tags distincts. C'est la différence
avec fabriquerId(), qui les retire parce qu'un identifiant doit tenir dans une URL — un tag, lui,
n'est jamais qu'affiché.
La couleur d'un tag n'est pas stockée : elle se déduit du nom par un hachage
(teinteTag() → une teinte HSL entre 0 et 359). Elle est donc stable d'une session à l'autre et
identique partout où le tag apparaît, sans rien avoir à gérer. Deux tags peuvent tomber sur des
teintes voisines : la couleur aide à repérer, elle ne porte pas d'information à elle seule — le nom
est toujours écrit à côté.
Le filtre, lui, n'est pas dans le fichier : c'est un état de vue, au même titre que la fenêtre temporelle. Voir decisions.md, section 16.
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.
Notes
Un projet et une phase portent chacun un champ notes, en markdown. Rien n'est stocké d'autre
que le texte source : le rendu est refait à l'affichage par js/markdown.js (analyse) et
js/notes.js (mise en DOM). Un fichier reste donc lisible et modifiable à la main, et une note
écrite hors de l'outil s'affiche sans conversion.
Le markdown reconnu est un sous-ensemble, choisi pour ce qu'on écrit dans une note de suivi :
| Syntaxe | Rendu |
|---|---|
# Titre à ###### Titre |
Titres, décalés de deux niveaux sous celui du panneau |
**gras**, *italique*, _italique_ |
Emphase |
`code` et blocs ``` |
Code littéral |
- item, * item, 1. item |
Listes, imbricables à l'indentation |
- [ ] item, - [x] item |
Cases à cocher, cliquables dans le panneau |
[texte](https://…) |
Lien |
> cité |
Citation, réanalysée (elle peut contenir une liste) |
--- |
Séparateur |
\* |
Le caractère littéral qui suit la contre-oblique |
Trois écarts assumés au markdown standard :
- Un retour à la ligne en est un. Le standard recolle les lignes d'un même paragraphe et exige deux espaces en fin de ligne pour un vrai retour ; c'est un piège invisible dans un champ de saisie, où l'on écrit souvent une petite liste à la volée.
- Ce qui n'est pas reconnu reste du texte.
**grassans fermeture s'affiche tel quel plutôt que d'avaler la suite : une note à moitié écrite est l'état normal d'une note. - Ni tableaux, ni images, ni HTML brut. Les deux premiers ne tiennent pas dans un volet latéral ; le troisième rouvrirait la porte à l'injection que la construction du DOM nœud par nœud ferme.
Les liens ne sont suivis que si leur schéma est http, https ou mailto — liste blanche, et
non liste noire. Un planning s'échange, et rien ne garantit que la note affichée a été écrite par
celui qui la lit. Un lien refusé n'est pas effacé : son libellé redevient du texte.
Voir decisions.md, section 25.
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 grandeendde 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
startetendcourantes dans son champbaseline, et inscrit la date du jour dansbaselineDate. 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
endactuelle et la plus grandeendde référence. Elle est affichée arrondie en semaines. Une phase créée après le figeage n'a pas debaselineet 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.