Files
echo-du-huit/docs/solutions/2026-07-17-cli-de-selection-des-evenements-oear-solution.md
T

208 lines
12 KiB
Markdown

# 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:<uid>` (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, `<li class="liste-vide">` **dans** le `<ul>` (un état vide est un
contenu, pas une erreur — et un `<ul>` ne peut contenir que des `<li>`).
## 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.<api> 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]