From b2361fcfb92f33bc7d8844427dda4bbf23f6d2db Mon Sep 17 00:00:00 2001 From: Pierre Martin Date: Fri, 17 Jul 2026 12:37:37 +0200 Subject: [PATCH] docs: capitalize the event list and form seam solution (T1) Co-Authored-By: Claude Opus 4.8 --- ...-selection-des-evenements-oear-solution.md | 207 ++++++++++++++++++ 1 file changed, 207 insertions(+) create mode 100644 docs/solutions/2026-07-17-cli-de-selection-des-evenements-oear-solution.md diff --git a/docs/solutions/2026-07-17-cli-de-selection-des-evenements-oear-solution.md b/docs/solutions/2026-07-17-cli-de-selection-des-evenements-oear-solution.md new file mode 100644 index 0000000..f6246c0 --- /dev/null +++ b/docs/solutions/2026-07-17-cli-de-selection-des-evenements-oear-solution.md @@ -0,0 +1,207 @@ +# Solution : [T1] Page liste : afficher les événements + bouton Créer + +## Problème résolu +Câbler le parseur (`evenements.js`, figé en T1 précédente mais **branché nulle +part**) à la page : afficher les événements futurs triés, et poser la **couture** +page → content script pour le pré-remplissage du formulaire mairie (tâche sœur). + +Sous un ticket d'apparence UI se cachaient deux décisions structurantes : +1. **Où vivent le fetch et le parsing ?** L'énoncé disait « passage background + (CalDAV+parsing) → page », mais le code réel fetchait **déjà dans la page**. + La question n'était donc pas *comment transporter*, mais **faut-il introduire + ce transport**. +2. **Que transmet le bouton « Créer » ?** Le remplissage appartient à la tâche + sœur ; T1 devait poser la couture, pas le remplissage. + +Le coût d'une erreur ici n'était pas dans T1 mais dans T2 (statuts CalDAV) et +dans la tâche content script, qui héritent du chemin de données choisi. + +## Approche choisie +**A + H1** : fetch et parsing **restent dans la page** ; `storage.local` sous une +**clé unique** pour la seule frontière irréductible (page → content script). + +Pourquoi pas les alternatives : +- **Background fetch+parse par message (B)** : introduit une **frontière de + sérialisation gratuite**. Firefox préserve les `Date` (structured clone), + Chrome les JSON-ise : faire reposer le contrat `Event` fraîchement figé sur un + détail du moteur = dette silencieuse. Zéro besoin actuel — `background.js` + garde son unique rôle (ouvrir l'onglet) et **n'a pas été modifié**. +- **Background → `storage.session` (C)** : le storage JSON-ise → `debut`/`fin` + deviennent des chaînes, réhydratation partout, contrat entamé. Plus + invalidation/fraîcheur à gérer. Sur-ingénierie pour une liste ouverte à la demande. +- **Clé `evenement:` (couture annoncée par la tâche sœur)** : **inutilisable + en l'état** — le content script s'exécute sur le site mairie et n'a **aucun + canal** pour apprendre un `uid` (le fragment d'URL a été écarté). La couture + nommée par la tâche sœur avait un trou ; T1 l'a révisée (voir `DECISIONS.md`). +- **`tabs.sendMessage` au chargement** : course page/content script à arbitrer, + et un F5 sur le formulaire (modéré, on y revient) perd le contexte. + +Le principe : **n'introduire une frontière que quand elle sert quelqu'un**. Le +background reste disponible ; quand T2 aura un vrai besoin d'orchestration, il +sera introduit **pour une raison**, pas par anticipation. + +## Décisions clés +Actées en détail dans `docs/DECISIONS.md` (2026-07-14) — **à lire avant la tâche +sœur ou T2**. En résumé : +- **Modules purs extraits** (`presentation.js`, `transfert.js`) plutôt que tout + dans `liste.js` : ce dernier touche `document` dès l'import, donc l'importer + dans Bun plante — y enterrer tri/formatage/défaut aurait rendu de la **logique + métier réelle** intestable. Chaque module existe parce qu'il rend testable une + décision métier (l'ordre, le mensonge « 00:00 », le contrat de couture). +- **Clé unique `evenement-en-attente`**, pas indexée par uid ; **on n'efface pas + à la lecture** (le formulaire survit à un F5) ; le dernier clic gagne (assumé : + le geste réel est séquentiel). +- **Sérialisation ISO explicite** : on ne parie pas sur le structured clone. +- **Défaut café = affichage seulement**, `lieu` stocké **brut** (`""` si absent) : + écrire l'adresse dans le payload **détruirait le signal « pas de lieu »** dont + le content script a besoin pour décider s'il pose aussi les coordonnées. +- **`await` le dépôt AVANT `tabs.create`** : sinon course — c'est précisément ce + que la couture achète (« la donnée attend le content script »). +- **Permission `storage` seule** ; `"tabs"` correctement écartée (`tabs.create` + n'en a pas besoin — confirmé en Firefox réel). + +## Patterns à réutiliser + +**Injecter la dépendance impure, garder le module pur** (convention du repo : +`fetchImpl`, `aujourdhui`, et maintenant `storage`) — paramètre **obligatoire**, +testable sans navigateur : +```js +export async function deposerEvenement(storage, evenement) { + await storage.set({ [CLE_EVENEMENT_EN_ATTENTE]: serialiserPourFormulaire(evenement) }); +} +// appel réel : deposerEvenement(browser.storage.local, evenement) +``` + +**Le point d'entrée injecte le « maintenant »** — la pureté du parseur est +préservée jusqu'au dernier moment : +```js +rendreEvenements(trierParDebut(parserEvenements(res.flux, new Date()))); +``` + +**Tout handler `async` de clic doit attraper** — sinon la promesse rejette en +silence et le bouton paraît **mort** (voir Pièges) : +```js +creer.addEventListener("click", async () => { + try { await creerSurSiteMairie(evenement); } + catch (erreur) { rendreEchecCreation(erreur); } // console.error + message NOMMANT l'erreur +}); +``` +Closure sur l'événement de la carte : ni `dataset`, ni re-lookup par uid. + +**Formater une date flottante SANS `timeZone`, un instant AVEC** — les deux +formatteurs coexistent délibérément dans `presentation.js` : +```js +const FORMAT_JOUR_PARIS = new Intl.DateTimeFormat("fr-FR", { timeZone: "Europe/Paris", dateStyle: "full" }); +// Sans timeZone, DÉLIBÉRÉMENT : une journée entière est une date *flottante* +// (RFC 5545). ical.js la matérialise à minuit local ; la relire en local +// restitue le bon jour partout, alors que la convertir vers Paris la ferait +// glisser au jour précédent depuis Tokyo. +const FORMAT_JOUR_FLOTTANT = new Intl.DateTimeFormat("fr-FR", { dateStyle: "full" }); +``` + +**Reculer d'un jour un DTEND de journée entière, en composantes locales** : +```js +// DTEND est EXCLUSIF (RFC 5545) : un événement du 18 porte DTEND=19. +// Jamais « -24h » : la date est flottante, un changement d'heure ferait basculer le jour. +const veilleDe = (date) => new Date(date.getFullYear(), date.getMonth(), date.getDate() - 1); +``` + +**Tester ce que le fuseau épinglé cache — en sous-processus.** `TZ=Europe/Paris` +est justement le seul fuseau où une journée entière mal formatée reste invisible. +`Intl` fige le fuseau à l'import : seul un autre processus peut le changer. +```ts +describe.each(["Asia/Tokyo", "Pacific/Auckland", "America/New_York", "Europe/Paris"])( + "sous TZ=%s", (fuseau) => { + test("journée entière → la date ne glisse pas d'un jour", async () => { + expect(await formaterDansLeFuseau(fuseau)).toContain(JOUR_ATTENDU); + }); + }); +// Bun.spawn(["bun", "-e", …], { env: { ...process.env, TZ: fuseau } }) +``` +**Le fixture traverse la frontière de processus en code source** (une constante +unique interpolée : `JOURNEE_ENTIERE_DEBUT.join(", ")`) → les deux tests ne +peuvent plus diverger **par construction**. C'est ce qui a corrigé la *cause* du +faux vert, pas son symptôme. + +**Assertions tolérantes sur les sorties `Intl`** (`toContain` / `not.toMatch`, +jamais l'égalité stricte) : la ponctuation d'ICU varie entre versions de Bun. On +teste l'intention (bon jour, heure présente/absente, bon fuseau), pas le typographe. + +**Rendu DOM sûr** : `textContent` exclusivement (jamais `innerHTML` — les titres +viennent d'une source externe), `replaceChildren()` pour rester rejouable au +Réessayer, `
  • ` **dans** le `
      ` (un état vide est un +contenu, pas une erreur — et un `
        ` ne peut contenir que des `
      • `). + +## Pièges à éviter + +- **🔴 Date flottante vs instant absolu : la famille de bugs de ce ticket.** Une + `DTSTART;VALUE=DATE` n'a **pas** de fuseau. Cette confusion a produit **trois** + défauts distincts, chacun invisible sous `TZ=Europe/Paris` : + 1. `formaterDate` forçait `Europe/Paris` sur les journées entières → la date + **glissait d'un jour** depuis Tokyo (trou du **plan**, pas de l'exécution) ; + 2. un fixture de test déclarait un instant absolu (`"...T00:00+02:00"`) pour + décrire une date flottante → **faux vert** ; + 3. *(préexistant, hors T1)* `evenements.test.ts:141` mêle un `AUJOURDHUI` + absolu à un `DTSTART` flottant → rouge sous Auckland. + + **Règle** : un fixture de date flottante se construit en **composantes locales** + (`new Date(2026, 6, 10)`), jamais en instant absolu — ce dernier n'est juste + que sur une machine à +02:00 et **ment partout ailleurs**. Côté consommateur : + tester `journeeEntiere` **d'abord**, ne lire que les composantes locales. + +- **🔴 Un handler `async` non attrapé = un bouton silencieusement mort.** + `() => creerSurSiteMairie(evenement)` : la promesse n'était ni attendue ni + attrapée → **toute** défaillance devenait un rejet silencieux. Aucun onglet, + aucun message, **rien à diagnostiquer**. Le plan avait différé l'UI d'erreur + (« non demandé en T1 ») : le terrain a démontré que ce report rendait + l'anomalie **invisible ET indiagnosticable**, et a coûté **deux passes de + test**. Afficher le détail technique est **volontaire** : c'est ce qui a fait + passer la panne d'« indiagnosticable » à « nommée en 3 secondes ». + **Leçon générale : la remontée d'erreur d'un geste utilisateur n'est pas du + confort différable — c'est l'instrument qui rend le reste diagnosticable.** + +- **🔴 Ajouter une clé au `manifest.json` impose Décharger + Charger.** Firefox + relit les **scripts** de la page à chaque ouverture, mais le **manifest** + seulement **à l'installation**. On se retrouve avec le code le plus récent et + les **permissions figées de l'ancien**. Symptôme : `browser. is undefined` + alors que le manifest déclare la permission. **F5 et « Recharger » ne réparent + rien.** Ce piège a coûté deux passes ; **T2 (écriture CalDAV) y retombera** en + ajoutant sa permission. Documenté dans `README.md` § Installation (dev). + +- **`bun test` nu force `TZ=UTC`** quand `TZ` est absente (le runner, pas `bun -e` + qui lit le fuseau système). Sans le pin `TZ=Europe/Paris` de `package.json`, + les tests ne tournent **pas** dans le fuseau des bénévoles. **Le pin est + PORTEUR — ne pas le retirer.** Corollaire : un test qui ne passe **que** + grâce au pin cache un bug de fuseau (cf. le sous-processus ci-dessus). + +- **Un fuseau épinglé masque autant qu'il stabilise.** `Europe/Paris` est + précisément le fuseau où les bugs de date flottante sont invisibles. Épingler + pour le déterminisme **et** tester ailleurs pour la vérité. + +- **`DTEND` exclusif** : sans le recul d'un jour, on annonce une date **FAUSSE** + (pas juste laide). ⚠️ **La tâche sœur doit l'appliquer avant de remplir + `#input_6_32`**, sinon elle soumettra une date fausse à la mairie. + Autre cas : **`DTEND` absent → le parseur pose `fin === debut`** (pas de plage + à afficher : « de 10:30 à 10:30 » serait absurde). + +- **Ne pas coder « `debut` → `fin` » naïvement** : c'était faux **4 fois sur 6** + (DTEND absent, franchit minuit, journée entière, festival). Mesurer les formes + réelles **sur le vrai parseur** avant de coder. + +- **La liste est incomplète par construction** : les récurrents au DTSTART maître + passé (atelier hebdo, permanence) n'y figurent pas — trou hérité, comblé par + [T3]. **Aucun avertissement UI** : un avertissement permanent que personne ne + peut lever devient du bruit. + +- **Le bouton ouvre un formulaire VIDE** tant que la tâche sœur n'est pas faite. + État intermédiaire **honnête et démontrable** (`about:debugging` → Storage → + `evenement-en-attente`), pas un bug. Ne pas « finir le travail » en douce : + cela casserait un découpage déjà tranché. + +## Tags +tags: [firefox-extension, manifest-v3, webextension, vanilla-js, storage-local, +content-script, seam, icalendar, rfc5545, floating-date, dtend-exclusive, +timezone, intl, date-formatting, pure-function, dependency-injection, bun-test, +cross-timezone-testing, test-fixtures, false-green, async-error-handling, +silent-failure, dom-security, textcontent, serialization, contract-design, +yagni, low-tech]