Files
echo-du-huit/docs/DECISIONS.md
T

12 KiB

Décisions d'architecture

Choix actés et leur pourquoi. But : éviter aux futurs agents de relitiger ce qui est déjà tranché. Format léger (date — décision — pourquoi).

2026-06-29 — Extension Firefox plutôt que CLI

Première piste : un CLI (Bun) qui fetch l'ICS et auto-soumet le formulaire. Abandonnée au profit d'une extension Firefox.

Pourquoi :

  • La revue manuelle avant envoi est le vrai besoin (descriptions à enrichir, formulaire modéré) → pré-remplir + envoyer soi-même bat l'auto-submit.
  • L'extension réutilise la session du navigateur pour Nextcloud → aucun mot de passe à stocker.
  • Pas de gestion des tokens CSRF : le formulaire les porte lui-même.

2026-06-29 — v1 = page autonome de l'extension

Plutôt que d'injecter des boutons dans le calendrier Nextcloud (SPA Vue, DOM fragile, casse à chaque mise à jour), la v1 est une page propre à l'extension. L'injection dans le calendrier reste un bonus ultérieur.

2026-06-29 — Statut dans le tag CATEGORIES (CalDAV)

L'intention (ignoré / soumis) est écrite dans l'événement Nextcloud, pas dans un stockage local.

Pourquoi : partagé entre bénévoles, durable, visible dans le calendrier. Un state.json local serait privé à une machine et invisible des autres.

Le statut publié n'est pas un tag : il est dérivé de l'agenda public de la mairie (match titre + date), ce qui donne aussi le lien direct.

2026-06-29 — Image ajoutée manuellement

Un content script ne peut pas remplir un <input type=file> (sécurité navigateur). L'extension facilite le geste (affiche l'image à glisser) mais ne l'automatise pas. L'affiche vient souvent de la newsletter Brevo, hors calendrier.

2026-06-30 — Parseur iCalendar (T1) : contrat Event figé + ical.js vendoré

Le flux brut est transformé en Event[] par une fonction pure parserEvenements(flux, aujourdhui) (extension/evenements.js), via ical.js v2.2.1 vendoré (approche 3 du brainstorm : pas de build, dépendance en .js).

Contrat Event figé (ne pas rouvrir sans accord) : { uid, titre, description, lieu, debut: Date, fin: Date, categories: string[], journeeEntiere: boolean }. Champs texte coercés en "" si absents (l'aval suppose des chaînes). Le défaut « café » du lieu est appliqué en aval, pas ici.

Décisions tranchées :

  • uid seul, pas de href/ETag. Le transport actuel (?export) renvoie un ICS concaténé sans href/ETag par événement. L'UID est la clé stable ; T2 résoudra l'adressage CalDAV par UID au moment du PUT. Le contrat n'est pas alourdi.
  • journeeEntiere dès T1 (drapeau porté, comportement « heure exigée » différé à T3).
  • Cas limites traités « au plus simple », différés à T3 : pas d'expansion RRULE (récurrents pris au DTSTART maître + filtre futur → un récurrent au maître passé disparaît, trou assumé et documenté), exceptions d'occurrence (RECURRENCE-ID) ignorées, journées entières au minuit local.
  • Tri & message « 0 événement futur » hors parseur : le parseur renvoie les événements dans l'ordre du flux, non triés (responsabilité de la liste).

2026-06-29 — Pré-remplissage par content script, pas par URL

Gravity Forms n'accepte ?input_X= que si chaque champ est configuré « population dynamique » côté mairie (improbable). On remplit donc le DOM par id (ids confirmés dans RECHERCHE.md).

2026-07-14 — [T1] Liste : couture page → content script

La liste dépose l'événement choisi dans storage.local puis ouvre le formulaire mairie (extension/transfert.js). Ce que la tâche sœur (« formater les données pour le formulaire mairie ») doit lire :

Clé unique evenement-en-attente, PAS evenement:<uid> (révise la couture « clé = uid » annoncée par la tâche sœur, qui avait un trou). Le content script s'exécute sur le site de la mairie : il n'a aucun canal pour apprendre un uid, le fragment d'URL ayant été écarté (pas de pollution d'une URL tierce). Une clé connue d'avance le rend lisible sans transporter quoi que ce soit.

  • Pas de résidus : un seul slot, écrasé au clic suivant (le dernier clic gagne — limite assumée, le geste réel est séquentiel et le formulaire est modéré).
  • On n'efface pas à la lecture : le formulaire survit à un rechargement.
  • Le dépôt est await avant tabs.create : sinon le content script peut lire avant l'écriture.

debut/fin sérialisés en ISO 8601 (le payload n'est pas un Event). On ne parie pas sur le structured clone : le contrat reste vrai quel que soit le moteur, le content script réhydrate avec new Date(iso).

⚠️ Si journeeEntiere est vrai, l'instant ISO n'est PAS significatif. Une DTSTART;VALUE=DATE est une date flottante (RFC 5545) : elle n'a pas de fuseau. ical.js la matérialise à minuit local, donc l'ISO produit dépend de la machine (2026-07-10 devient ...T15:00:00Z depuis Tokyo). Le consommateur doit tester journeeEntiere d'abord et n'en lire que les composantes locales (getFullYear/getMonth/getDate) — jamais l'heure, jamais une conversion de fuseau. C'est pour la même raison que la liste formate les journées entières sans timeZone (presentation.js) : forcer Europe/Paris faisait glisser la date au jour précédent depuis Tokyo ou Auckland.

⚠️ fin est un DTEND EXCLUSIF (RFC 5545) — ne pas l'afficher/soumettre tel quel. Le payload porte la valeur brute du parseur. Pour une journée entière, un événement du 18 porte fin = le 19, et un festival du 18 au 20 porte fin = le 21 : le dernier jour réel est fin moins un jour (composantes locales, jamais « -24h »). Concerne directement la tâche sœur, qui remplit #input_6_32 (Date fin) depuis DTEND : sans ce recul d'un jour, elle soumettra une date fausse à la mairie. La liste applique déjà la règle (presentation.js, veilleDe). Autre cas mesuré : DTEND absent → le parseur pose fin === debut (il n'y a alors pas de plage à afficher, ni de fin à soumettre).

TZ=Europe/Paris sur bun test est PORTEUR — ne pas le retirer. Le runner de Bun force TZ=UTC quand TZ est absente (vérifié : bun -e lit le fuseau système, le runner non). Sans le pin, les tests ne tournent donc pas dans le fuseau des bénévoles. Corollaire pour l'écriture des tests : un fixture de date flottante (journée entière, DTSTART sans Z ni TZID) se construit en composantes locales (new Date(2026, 6, 10)), jamais en instant absolu (new Date("2026-07-10T00:00:00+02:00")) — ce dernier n'est juste que sur une machine à +02:00 et ment partout ailleurs.

Défaut café = affichage seulement ; lieu stocké brut ("" si absent). Les deux avals du parseur ont chacun le leur : la liste affiche l'adresse pour ne pas montrer une ligne vide (cas majoritaire), le content script décide s'il pose aussi les coordonnées (input_18). Écrire l'adresse dans le payload détruirait le signal « pas de lieu » dont il a besoin.

2026-07-17 — [T1] Pré-remplissage : content scripts classiques + globalThis

Le formulaire mairie est pré-rempli par deux content scripts (config.js, formulaire-valeurs.js, formulaire-mairie.js), classiques — pas des modules ESM — qui communiquent par des namespaces globalThis (EchoConfig, EchoFormulaire).

Pourquoi pas ESM : un content script n'est pas un module (import y lève une SyntaxError), et le import() dynamique y est cassé côté Firefox (bugs ouverts 1803950 / 1536094). Restait à convertir la couture transfert.js, figée le 2026-07-14 : non.

⚠️ config.js et formulaire-valeurs.js n'ont NI import NI export, et c'est délibéré — ne pas « corriger ». Un fichier sans les deux est valide à la fois comme script classique (content script, background) et comme module ESM (import "./config.js" depuis presentation.js et depuis bun test). C'est ce qui permet à presentation.js de partager l'adresse du café sans build. Le jour où quelqu'un y ajoute un export, les content scripts cassent silencieusement.

Corollaire : l'ordre des tableaux js du manifest EST la dépendance (config → valeurs → mairie), testé dans extension.test.ts.

CLE_EVENEMENT_EN_ATTENTE est dupliquée dans formulaire-valeurs.js (l'original vit dans transfert.js, en ESM, inatteignable depuis un content script classique). Un test de dérive garantit l'égalité des deux.

Config (a) : défauts committés + config.local.json optionnel. Les défauts (thème, tarifs, café) sont dans config.js ; seuls organisateur et email sont personnels et vivent dans config.local.json (gitignoré). Le fichier n'est pas requis : un clone frais doit marcher, il laisserait sinon l'extension cassée par défaut. Absent ou invalide → défauts nus, manque signalé dans le bandeau, jamais d'exception.

C'est le background qui lit config.local.json, puis le passe par le storage. Un content script ne pourrait le fetch(runtime.getURL(...)) que si le fichier était en web_accessible_resources — ce qui exposerait l'email à la page de la mairie. Le fichier étant optionnel, il ne peut pas non plus être listé dans un tableau js (Firefox refuse d'installer si un fichier déclaré manque) : fetch + repli est la seule voie.

On ne remplit que ce qu'on sait ; le reste est dit, jamais inventé. Pas d'heure « 00:00 » pour une journée entière, pas de lat|lng deviné pour un lieu hors café, pas d'organisateur par défaut. Ce qui manque part dans le bandeau (aCompleter). En particulier, input_6_35 (accessibilité) est laissé intact : RECHERCHE.md le dit « fixe », mais la machine ne sait pas si le lieu est accessible — cocher au hasard sur un formulaire modéré serait exactement le « champ rempli faux » qu'on veut éviter.

Séparation calcul / DOM : formulaire-valeurs.js est pur et testé (les dates sont le vrai risque, invisible sous TZ=Europe/Paris) ; formulaire-mairie.js est fin, impur et non testé unitairement — aucun harnais DOM dans le repo, et happy-dom n'émulerait de toute façon pas Gravity Forms. C'est le test humain qui tranche.

2026-07-17 — [T2] Publication mairie : dérivée, paginée, seam pur/impur

Le statut publié est dérivé de l'agenda public de la mairie (pas de tag), en retrouvant l'événement Nextcloud par date locale de Cugnaux + titre normalisé. Il donne aussi le lien direct (lu dans la carte).

Corrections factuelles vérifiées en phase plan :

  • L'agenda est PAGINÉ : .../agenda/page/N/?f=1&…. Un seul fetch « période » ne suffit pas → on boucle jusqu'à une page vide (plafond de sécurité anti-boucle).
  • Le slug porte des suffixes WordPress (« Atelier dessin » → /agenda/atelier-dessin-2/) : on matche sur date + titre, jamais sur le slug. Le lien est lu dans la carte, jamais reconstruit.
  • <time datetime="YYYY-MM-DD"> est une date nue (pas d'heure) → comparer à la date locale de Cugnaux du debut, jamais à une conversion de fuseau. Journée entière = date flottante → composantes locales (comme presentation.js).

Seam extraction impure / matching pur (même logique que formulaire-valeurs.js vs formulaire-mairie.js) : DOMParser est absent de Bun, donc extraireCartes(html) (agenda-mairie.js) est impur et non testé unitairement (test humain sur l'agenda réel) ; le matching (publication.js) et la boucle de pagination (via impls injectées) sont purs et testés à fond — c'est le vrai risque produit.

Biais PRÉCISION v1 : match STRICT (date identique + titre normalisé identique). Titre proche mais non strictement identique le même jour → on n'affirme pas « publié » (faux négatif bénin ; le formulaire modéré rattrape un doublon éventuel). Dans le doute — agenda injoignable, HTTP ≠ 200, DOMParser qui lève, structure HTML changée — la liste s'affiche sans badge (dégradation gracieuse, jamais d'exception, aucun faux positif). Deux cartes de même clé (même jour + même titre normalisé, rare) : la dernière indexée l'emporte.

Périmètre v1 = badge « publié » + lien uniquement. L'annotation est additive : la liste est rendue immédiatement, l'agenda ne la bloque jamais. Fetch de l'agenda sans credentials (public). Aucune modification de manifest.json (host_permissions couvre déjà ville-cugnaux.fr).