docs: capitalize CalDAV feed reading solution (T1)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
b27bb478bf
commit
b359902c69
@@ -0,0 +1,117 @@
|
|||||||
|
# Solution : [T1] Lire les événements via CalDAV (session navigateur)
|
||||||
|
|
||||||
|
## Problème résolu
|
||||||
|
Donner à l'extension sa matière première : le flux iCalendar **brut** du
|
||||||
|
calendrier Nextcloud de l'Atelier du Huit, en **réutilisant la session du
|
||||||
|
navigateur** (cookies), sans jamais stocker de mot de passe. Frontière T1
|
||||||
|
tenue : on s'arrête au **flux brut vérifié** + une preuve de vie (comptage des
|
||||||
|
`VEVENT`). Le parsing ical.js, le dédoublonnage par UID, les RRULE/fuseaux et le
|
||||||
|
filtrage « à venir » sont **hors T1** (tâche T2).
|
||||||
|
|
||||||
|
Le vrai enjeu n'était pas « fetcher une URL » mais **savoir qu'on tient bien un
|
||||||
|
calendrier** : Nextcloud peut renvoyer une page de login en **HTTP 200 HTML**,
|
||||||
|
donc `response.ok` ne suffit pas à garantir la validité du flux.
|
||||||
|
|
||||||
|
## Approche choisie
|
||||||
|
`GET …/?export` + `fetch` direct depuis la **page** de l'extension
|
||||||
|
(`credentials:"include"`), validation du flux par **sniffing du corps**
|
||||||
|
(`trimStart().startsWith("BEGIN:VCALENDAR")`). Transport **isolé dans son propre
|
||||||
|
module** `extension/nextcloud.js`, pur de toute présentation : il renvoie un
|
||||||
|
*type* d'erreur, pas un message.
|
||||||
|
|
||||||
|
Pourquoi pas les alternatives :
|
||||||
|
- **CalDAV REPORT + `time-range`** (filtre serveur, expansion RRULE) : corps XML
|
||||||
|
à construire/maintenir, multi-statut 207, sémantique d'expansion à valider —
|
||||||
|
surdimensionné pour 473 événements. Le `?export` brut suffit pour T1.
|
||||||
|
- **Couche réseau centralisée dans le background** : utile plus tard (lecture
|
||||||
|
CalDAV des statuts, cache), mais sur-ingénierie tant qu'il n'y a qu'un
|
||||||
|
consommateur. On garde le déplacement **peu coûteux** grâce au module isolé.
|
||||||
|
|
||||||
|
## Décisions clés
|
||||||
|
- **Résultat discriminé** `{ ok:true, flux } | { ok:false, erreur }` avec
|
||||||
|
`erreur ∈ {"non-connecte","reseau","serveur"}`. Le mapping type → message FR
|
||||||
|
actionnable vit dans `liste.js`, pas dans le transport. Séparation nette
|
||||||
|
données / présentation.
|
||||||
|
- **`fetchImpl` injecté et obligatoire** (convention « params obligatoires par
|
||||||
|
défaut ») : le module est testable **sans réseau réel** (Bun fabrique des
|
||||||
|
`Response`), et le wiring navigateur reste un détail de l'appelant.
|
||||||
|
- **Ordre des branches** : 401/403 traités **avant** `!reponse.ok` (sinon le
|
||||||
|
générique « serveur » les masquerait), puis sniffing du corps en dernier.
|
||||||
|
- **Détection « non connecté » par sniffing**, pas par `redirect:"manual"` : le
|
||||||
|
corps après redirection-login (200 HTML) est déjà détecté ; inutile de
|
||||||
|
complexifier. 401/403 captés en amont pour le cas réel.
|
||||||
|
- **`manifest.json` intact** : `host_permissions` couvrait déjà
|
||||||
|
`https://atelier-huit.frama.space/*`. Changement purement additif.
|
||||||
|
- **Constatation terrain (testée contre le vrai serveur)** : sans cookie,
|
||||||
|
Nextcloud répond **401** (`Sabre\DAV\Exception\NotAuthenticated`, 0 redirect),
|
||||||
|
**pas** un 200 HTML de login. Le sniffing du corps reste une ceinture-et-
|
||||||
|
bretelles utile (cas redirection possible selon config), mais la branche
|
||||||
|
réellement empruntée « pas connecté » est le **401**.
|
||||||
|
|
||||||
|
## Patterns à réutiliser
|
||||||
|
**Transport injectable, pur, à résultat typé** (testable sans réseau) :
|
||||||
|
```js
|
||||||
|
export async function recupererFluxCalendrier(fetchImpl) {
|
||||||
|
let reponse;
|
||||||
|
try { reponse = await fetchImpl(URL_CALENDRIER, { credentials: "include" }); }
|
||||||
|
catch { return { ok: false, erreur: "reseau" }; } // fetch lève = réseau
|
||||||
|
if (reponse.status === 401 || reponse.status === 403)
|
||||||
|
return { ok: false, erreur: "non-connecte" }; // session refusée
|
||||||
|
if (!reponse.ok) return { ok: false, erreur: "serveur" };
|
||||||
|
const flux = await reponse.text();
|
||||||
|
if (!flux.trimStart().startsWith("BEGIN:VCALENDAR")) // login 200 HTML
|
||||||
|
return { ok: false, erreur: "non-connecte" };
|
||||||
|
return { ok: true, flux };
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Comptage ancré ligne** (ne compte pas une occurrence dans une `DESCRIPTION`,
|
||||||
|
tolère le CRLF de la RFC 5545) :
|
||||||
|
```js
|
||||||
|
export function compterEvenements(flux) {
|
||||||
|
return (flux.match(/^BEGIN:VEVENT\s*$/gm) ?? []).length;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Wrapper `fetch` côté page** (voir piège ci-dessous) :
|
||||||
|
```js
|
||||||
|
const res = await recupererFluxCalendrier((u, o) => fetch(u, o));
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rendu d'état reconstruit à chaque tentative** : `etat.replaceChildren()` avant
|
||||||
|
de re-rendre → pas d'empilement de messages/boutons/listeners quand on clique
|
||||||
|
*Réessayer* plusieurs fois. Région `#etat` en `role="status"
|
||||||
|
aria-live="polite"` pour l'accessibilité.
|
||||||
|
|
||||||
|
**Tests Bun important un module ES du navigateur** : `import { … } from
|
||||||
|
"./extension/nextcloud.js"`, `fetchImpl` remplacé par
|
||||||
|
`async () => new Response(corps, { status })` ou un `throw`. Aucun jsdom, aucun
|
||||||
|
réseau.
|
||||||
|
|
||||||
|
## Pièges à éviter
|
||||||
|
- **`response.ok` ne prouve pas un calendrier** : la page de login peut arriver
|
||||||
|
en **200 HTML**. Toujours valider le *contenu* (`BEGIN:VCALENDAR`), pas que le
|
||||||
|
statut. C'est le piège central de la tâche.
|
||||||
|
- **`fetch` nu détaché de `window`** lève « Illegal invocation » dans le
|
||||||
|
navigateur. Passer un wrapper `(u, o) => fetch(u, o)` (ou `fetch.bind(window)`),
|
||||||
|
jamais `fetch` nu en argument.
|
||||||
|
- **Regex non ancrée** : `/BEGIN:VEVENT/g` compterait une occurrence apparaissant
|
||||||
|
dans une `DESCRIPTION`. Ancrer `^…$` + flag `m`.
|
||||||
|
- **Ordre des branches d'erreur** : tester 401/403 avant le `!ok` générique,
|
||||||
|
sinon « non-connecte » serait noyé dans « serveur ».
|
||||||
|
- **BOM / espaces en tête** du flux : `trimStart()` avant la comparaison
|
||||||
|
`startsWith`, sinon un VCALENDAR valide précédé d'un BOM serait rejeté.
|
||||||
|
- **0 VEVENT n'est pas une erreur** : un calendrier valide mais vide est un
|
||||||
|
succès (« 0 événement lu »), à ne pas confondre avec un échec.
|
||||||
|
- **CSS mort** : la classe `.liste-vide` (squelette) n'est plus utilisée depuis
|
||||||
|
que `liste.js` orchestre `#etat`. Signalée P3 en review, **laissée
|
||||||
|
volontairement** car T2 (rendu détaillé de la liste) la réemploiera. À nettoyer
|
||||||
|
si T2 ne la reprend pas.
|
||||||
|
- **Warning `MISSING_DATA_COLLECTION_PERMISSIONS`** (web-ext) : préexistant sur
|
||||||
|
`manifest.json` (commit squelette), **non touché** par T1, hors périmètre — pas
|
||||||
|
une régression. À traiter dans une tâche dédiée au manifest.
|
||||||
|
|
||||||
|
## Tags
|
||||||
|
tags: [firefox-extension, manifest-v3, webextension, vanilla-js, caldav, ical,
|
||||||
|
nextcloud, fetch, session-cookies, bun-test, dependency-injection, error-handling,
|
||||||
|
low-tech]
|
||||||
Reference in New Issue
Block a user