diff --git a/docs/solutions/2026-06-30-fetch-parse-le-flux-ics-oea8-solution.md b/docs/solutions/2026-06-30-fetch-parse-le-flux-ics-oea8-solution.md new file mode 100644 index 0000000..964b386 --- /dev/null +++ b/docs/solutions/2026-06-30-fetch-parse-le-flux-ics-oea8-solution.md @@ -0,0 +1,153 @@ +# Solution : [T1] Parser les événements iCalendar (version minimale) + +## Problème résolu +Transformer le flux iCalendar **brut** (déjà récupéré par `nextcloud.js` en T1 +précédente) en une liste d'objets `Event` exploitables par tout l'aval de +l'extension (liste, pré-remplissage du formulaire mairie, statut CalDAV en T2). + +Enjeu central : **cette tâche FIGE la forme de l'objet `Event`**. Le coût d'un +mauvais contrat n'est pas dans T1, il est dans la refonte de la liste + du form + +du statut le jour où T3 voudra ajouter la robustesse. La **forme prime sur le +choix du parseur**. + +Frontière T1 tenue : on parse, on dédoublonne, on filtre le futur, on lit +`CATEGORIES`. La robustesse (expansion `RRULE`, heure exigée pour les journées +entières, durcissement des entrées dégénérées) est **différée à T3**, « traitée +au plus simple » et **documentée**. + +## Approche choisie +**Approche 3 du brainstorm** : `ical.js` (Mozilla) **vendoré en `.js`** + forme +`Event` figée qui **anticipe T3** en portant dès maintenant le drapeau +`journeeEntiere`. T1 *remplit* le contrat ; le *comportement* des cas limites +reste différé. + +Pourquoi pas les alternatives : +- **Parseur maison (regex)** : iCalendar est piégeux (line-folding à 75 octets, + échappements `\,` `\n` `\;`, `TZID`, `VALUE=DATE`). Le réinventer = dette, et + T3 (RRULE, fuseaux) deviendrait un enfer maison. `RECHERCHE.md` le déconseille + explicitement. +- **`ical.js` + forme minimale stricte (approche 2)** : la forme n'exprimerait + pas la journée entière → T3 devrait **rouvrir le contrat figé**, exactement le + coût que T1 doit éviter. L'ajout d'un seul booléen rend le contrat durable pour + un coût marginal. + +Contrainte structurante respectée : **pas de build, pas de runtime**. `ical.js` +v2.2.1 est vendoré tel quel (build ESM, export *default* = namespace `ICAL`), +importé par une page propre de l'extension (`type=module`) — donc aucun +changement de `manifest.json` (pas de `web_accessible_resources`). + +## Décisions clés +- **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, jamais `null`). Le défaut « café » du lieu est appliqué + **en aval**, pas dans le parseur. +- **`uid` seul, pas de `href`/`ETag`.** Le transport actuel (`?export`) renvoie + un ICS concaténé sans `href`/`ETag` par événement. L'`UID` est la clé stable ; + T2 résoudra l'adressage CalDAV par UID au moment du PUT. Le contrat n'est pas + alourdi d'une préoccupation de transport. +- **Fonction pure à dépendance injectée** : `parserEvenements(flux, aujourdhui)`. + `aujourdhui` est **obligatoire** (pas de défaut) → tests déterministes du + filtre futur. Convention du repo « params obligatoires par défaut ». +- **Enregistrement des VTIMEZONE embarqués** avant toute conversion de date — + **non négociable** : sans lui, `toJSDate()` calcule un mauvais offset pour les + `TZID` non standards (prouvé par le test Africa/Lagos vs Europe/Paris). +- **Passer par `ICAL.Event`** (pas lire `DTEND` en brut) : il dérive `endDate` + depuis `DURATION` ou `DTSTART` quand `DTEND` est absent, et expose `startDate.isDate` + pour la journée entière. +- **Cas limites « 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 via `isRecurrenceException()` ; 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). +- **Périmètre : parseur NON câblé en prod en T1.** `liste.js` reste sur + `compterEvenements` (preuve de vie). Le câblage UI appartient à la tâche + d'affichage — éviter un demi-câblage jetable et un changement d'UX silencieux. + Conséquence : changement **purement additif**, rollback trivial. + +## Patterns à réutiliser +**Enregistrement des fuseaux embarqués avant conversion de dates** (le piège +n°1 du parsing iCalendar multi-fuseaux) : +```js +composant.getAllSubcomponents("vtimezone").forEach((vt) => { + const tzid = vt.getFirstPropertyValue("tzid"); + if (tzid && !ICAL.TimezoneService.has(tzid)) { + ICAL.TimezoneService.register(vt); // idempotent (garde !has) + } +}); +``` + +**Dédup par UID en un seul passage O(n), maître conservé** : +```js +const parUid = new Map(); +for (const vevent of composant.getAllSubcomponents("vevent")) { + const ev = new ICAL.Event(vevent); + if (ev.isRecurrenceException()) continue; // RECURRENCE-ID non développés en T1 + if (parUid.has(ev.uid)) continue; // garde le premier rencontré + parUid.set(ev.uid, toEvent(ev, vevent)); +} +``` + +**Mapping robuste via `ICAL.Event`** (coercition `null → ""`, `endDate` dérivé) : +```js +{ + uid: ev.uid, + titre: ev.summary ?? "", + description: ev.description ?? "", + lieu: ev.location ?? "", + debut: ev.startDate.toJSDate(), + fin: ev.endDate.toJSDate(), // gère DTEND absent / DURATION + journeeEntiere: ev.startDate.isDate === true, +} +``` + +**Lecture `CATEGORIES` tolérante aux deux formes** (valeurs séparées par virgule +ET/OU propriétés répétées) : +```js +vevent.getAllProperties("categories").flatMap((prop) => prop.getValues()); +``` + +**Filtre futur inclusif au minuit local** (aujourd'hui inclus) : +```js +const seuil = new Date(aujourdhui); seuil.setHours(0, 0, 0, 0); +return [...parUid.values()].filter((e) => e.debut.getTime() >= seuil.getTime()); +``` + +**Tests Bun avec fixtures ICS inline** (jamais l'export réel = données perso des +bénévoles) : helpers `vcalendar(...)` / `vevent(...)` qui joignent les lignes en +`\r\n` (CRLF de la RFC 5545), `aujourdhui` figé pour le déterminisme. Un +`VTIMEZONE` Paris et un Lagos inline prouvent l'enregistrement des fuseaux. + +## Pièges à éviter +- **VTIMEZONE non enregistré → offset faux.** Le piège central : `toJSDate()` + produit silencieusement une mauvaise heure. Toujours enregistrer les VTIMEZONE + embarqués avant conversion. Couvert par le test Africa/Lagos (UTC+1) ≠ + Europe/Paris (UTC+2 l'été). +- **Lire `DTEND` en brut** : il est souvent absent. Passer par + `ICAL.Event.endDate` qui le dérive (durée / start), sinon plantage. +- **`summary`/`description`/`location` peuvent renvoyer `null`** : toujours + coercer en `""` — l'aval (form, liste) suppose des chaînes. +- **Récurrents au DTSTART maître passé disparaissent** (pas d'expansion RRULE en + T1). **Trou produit assumé et documenté**, comblé en T3. Or les récurrents + (atelier hebdo, permanence) sont souvent ceux qu'on veut publier → priorité T3. +- **Pureté nuancée par un état global** (signalé P3 en review) : + `TimezoneService.register` mute un registre **global** partagé. Idempotent et + bénin, mais si deux flux portent le même `TZID` avec des définitions + différentes, **la première enregistrée gagne** pour la session. Le commentaire + le note ; ne pas qualifier la fonction de « pure » sans nuance. +- **Entrées dégénérées non durcies en T1** (constats Inzaghi, hors périmètre, + pas des anomalies) : flux chaîne vide `""` ou non-ICS → `ICAL.parse` lève ; + VEVENT sans `DTSTART` → lève ; VEVENT sans `UID` → clé `null` (collision). Le + transport renvoie toujours un VCALENDAR valide ; à durcir en T3 si besoin. +- **Ne pas committer l'export réel du calendrier** : données personnelles des + bénévoles. Fixtures inline uniquement. +- **Ne pas câbler le parseur dans `liste.js` en T1** : tentation de « finir le + travail », mais le rendu des cartes appartient à la tâche d'affichage. Un + demi-câblage serait jetable et changerait l'UX en douce. + +## Tags +tags: [firefox-extension, manifest-v3, webextension, vanilla-js, ical, ical.js, +icalendar, caldav, nextcloud, vendoring, esm, timezone, vtimezone, rrule, +recurrence, pure-function, dependency-injection, bun-test, contract-design, low-tech]