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 `