16 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 :
uidseul, pas dehref/ETag. Le transport actuel (?export) renvoie un ICS concaténé sanshref/ETagpar événement. L'UIDest la clé stable ; T2 résoudra l'adressage CalDAV par UID au moment du PUT. Le contrat n'est pas alourdi.journeeEntieredè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
awaitavanttabs.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 dudebut, jamais à une conversion de fuseau. Journée entière = date flottante → composantes locales (commepresentation.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).
2026-08-13 — [T2] Écriture du statut : REPORT + If-Match, garde-fou, annulable
Résolution paresseuse de la ressource, jamais d'URL devinée. Au moment
d'écrire, un REPORT (calendar-query filtrée sur l'UID) rend href + ETag
calendar-data, puis onPUTavecIf-Match. Mesuré : le nom du fichier.icsne correspond pas à l'UID (cf. RECHERCHE.md §1), l'approche «<uid>.ics» aurait produit une panne partielle.If-Matchtransforme l'écriture concurrente en 412 (« recharge la liste ») au lieu d'un écrasement silencieux. 0 réponse →introuvable, plusieurshref→ambigu: on n'écrit jamais dans une ressource choisie au hasard.
Garde-fou de non-régression. L'aller-retour ical.js pourrait abîmer une
propriété exotique. Avant tout PUT, on re-parse l'ICS avant et après,
on retire CATEGORIES des deux arbres jCal et on compare : différence ailleurs →
aucun PUT, message honnête. La comparaison est structurelle, donc le
repliement de ligne et l'espacement peuvent changer librement — une valeur,
non. On écrit dans l'agenda réel d'une association : un tag non posé se repose,
un événement abîmé se répare à la main.
Ni SEQUENCE ni DTSTAMP/LAST-MODIFIED ne sont bumpés : le garde-fou
l'interdit, et poser une catégorie n'est pas une modification de planification.
La synchro des autres clients passe par l'ETag/ctag.
Le CSRF est un confort, pas une condition : /csrftoken rend 200, mais un
jeton invalide passe quand même sur DAV (mesuré). On envoie l'en-tête
requesttoken quand on l'a, et un échec de récupération n'est pas fatal :
le serveur tranche.
Le tag soumis est posé au clic « Créer » et reste annulable. Rien ne prouve
qu'un formulaire modéré a été envoyé ; le clic est le meilleur signal disponible,
donc le statut porte toujours « Annuler l'envoi » et le bouton « Créer »
disparaît (c'est le doublon d'annonce qu'on combat). Un échec d'écriture
n'empêche pas d'ouvrir le formulaire : le tag est un confort d'équipe, remplir
le formulaire est la mission.
Jamais d'affichage optimiste : le statut affiché ne change qu'après confirmation du serveur, et ce sont les catégories renvoyées par l'aller-retour qui sont conservées. Mentir sur un état que d'autres bénévoles lisent est pire que ne rien afficher. Seule la ligne concernée est re-rendue : recharger la liste effacerait les badges « publié » déjà posés.
Les ignorés sont grisés et repoussés en fin de liste, jamais masqués : un ignoré invisible est un ignoré qu'on ne peut plus dé-ignorer.
Orthographe du tag : mairie:ignoré, avec l'accent — ces tags sont lus par
des humains dans Nextcloud et l'ICS est de l'UTF-8. À re-constater au premier
aller-retour réel : si Nextcloud renormalise, basculer en ASCII et le noter.
Limite assumée v1 : une ressource = un tag, donc un récurrent porte le même statut pour toutes ses occurrences (cohérent avec le report des récurrences en T3).
Limite honnête du garde-fou : il compare parse(ics) à
parse(toString(parse(ics))). Il attrape donc ce que la sérialisation perd,
pas ce que le parsing perdrait des deux côtés à la fois. Vérifié à
l'écriture : ical.js round-trippe sans dommage les formes exotiques d'un vrai
calendrier (paramètre quoté à virgule, propriété inconnue, ATTACH binaire,
EXDATE multiple, accent sur la frontière de repliement) — ces cas sont figés en
test. Le vrai arbitre reste le test humain « rien d'autre n'a bougé ».