# 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 `