# 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": 3, "projects": [ { "id": "site-web", "name": "Refonte du site web", "color": "#3b82f6", "tags": ["client", "web"], "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 `3`. Sert à détecter un fichier trop ancien au chargement. | | `projects` | tableau | Les projets, dans l'ordre d'affichage des couloirs. | Un fichier en version `2` — celle d'avant les tags — se charge sans rien demander : ses projets reçoivent une liste de tags vide, et il est réécrit en version `3` à 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. | | `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. - `tags` est 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). ## 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** — `Client` et `client` sont 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](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. ## 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.