diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md index 1f2064d..e8d0efa 100644 --- a/docs/DECISIONS.md +++ b/docs/DECISIONS.md @@ -218,3 +218,62 @@ gracieuse, jamais d'exception, aucun faux positif). Deux cartes de même clé **additive** : la liste est rendue immédiatement, l'agenda ne la bloque jamais. Fetch de l'agenda **sans** `credentials` (public). Aucune modification de `manifest.json` (`host_permissions` couvre déjà `ville-cugnaux.fr`). + +## 2026-08-13 — [T2] Écriture du statut : REPORT + If-Match, garde-fou, annulable + +**Résolution paresseuse de la ressource, jamais d'URL devinée.** Au moment +d'écrire, un `REPORT` (`calendar-query` filtrée sur l'`UID`) rend `href` + `ETag` ++ `calendar-data`, puis on `PUT` avec `If-Match`. Mesuré : le nom du fichier +`.ics` **ne correspond pas** à l'UID (cf. RECHERCHE.md §1), l'approche +« `.ics` » aurait produit une panne partielle. `If-Match` transforme +l'écriture concurrente en **412** (« recharge la liste ») au lieu d'un écrasement +silencieux. 0 réponse → `introuvable`, plusieurs `href` → `ambigu` : on n'écrit +jamais dans une ressource choisie au hasard. + +**Garde-fou de non-régression.** L'aller-retour `ical.js` pourrait abîmer une +propriété exotique. Avant tout `PUT`, on re-parse l'ICS **avant** et **après**, +on retire `CATEGORIES` des deux arbres jCal et on compare : différence ailleurs → +**aucun PUT**, message honnête. La comparaison est **structurelle**, donc le +repliement de ligne et l'espacement peuvent changer librement — une **valeur**, +non. On écrit dans l'agenda réel d'une association : un tag non posé se repose, +un événement abîmé se répare à la main. + +**Ni `SEQUENCE` ni `DTSTAMP`/`LAST-MODIFIED` ne sont bumpés** : le garde-fou +l'interdit, et poser une catégorie n'est pas une modification de planification. +La synchro des autres clients passe par l'ETag/ctag. + +**Le CSRF est un confort, pas une condition** : `/csrftoken` rend 200, mais un +jeton **invalide** passe quand même sur DAV (mesuré). On envoie l'en-tête +`requesttoken` quand on l'a, et un échec de récupération n'est **pas fatal** : +le serveur tranche. + +**Le tag `soumis` est posé au clic « Créer » et reste annulable.** Rien ne prouve +qu'un formulaire modéré a été envoyé ; le clic est le meilleur signal disponible, +donc le statut porte toujours « Annuler l'envoi » et le bouton « Créer » +disparaît (c'est le doublon d'annonce qu'on combat). **Un échec d'écriture +n'empêche pas d'ouvrir le formulaire** : le tag est un confort d'équipe, remplir +le formulaire est la mission. + +**Jamais d'affichage optimiste** : le statut affiché ne change qu'après +confirmation du serveur, et ce sont les catégories **renvoyées par +l'aller-retour** qui sont conservées. Mentir sur un état que d'autres bénévoles +lisent est pire que ne rien afficher. Seule la **ligne concernée** est re-rendue : +recharger la liste effacerait les badges « publié » déjà posés. + +**Les ignorés sont grisés et repoussés en fin de liste, jamais masqués** : un +ignoré invisible est un ignoré qu'on ne peut plus dé-ignorer. + +**Orthographe du tag : `mairie:ignoré`, avec l'accent** — ces tags sont lus par +des humains dans Nextcloud et l'ICS est de l'UTF-8. À re-constater au premier +aller-retour réel : si Nextcloud renormalise, basculer en ASCII et le noter. + +**Limite assumée v1** : une ressource = un tag, donc un récurrent porte le même +statut pour toutes ses occurrences (cohérent avec le report des récurrences en T3). + +**Limite honnête du garde-fou** : il compare `parse(ics)` à +`parse(toString(parse(ics)))`. Il attrape donc ce que la **sérialisation** perd, +pas ce que le **parsing** perdrait des deux côtés à la fois. Vérifié à +l'écriture : `ical.js` round-trippe sans dommage les formes exotiques d'un vrai +calendrier (paramètre quoté à virgule, propriété inconnue, `ATTACH` binaire, +`EXDATE` multiple, accent sur la frontière de repliement) — ces cas sont figés en +test. Le vrai arbitre reste le test humain « rien d'autre n'a bougé ». diff --git a/docs/RECHERCHE.md b/docs/RECHERCHE.md index 2507533..29a893b 100644 --- a/docs/RECHERCHE.md +++ b/docs/RECHERCHE.md @@ -31,6 +31,33 @@ Document de référence pour l'implémentation. Toutes les valeurs ci-dessous so > être enregistrés (`ICAL.TimezoneService.register`) **avant** `toJSDate()`, > sinon l'offset est faux pour les TZID non standards (ex. `Africa/Lagos`). +### Écriture CalDAV — **mesuré le 2026-08-13** (spike navigateur, T2) + +Mesures faites depuis la console de la page d'extension, session Nextcloud +ouverte. La séquence complète a été jouée, `PUT` **no-op** compris (contenu +reposé à l'identique : rien modifié). + +| Étape | Résultat mesuré | +|---|---| +| `GET /csrftoken` | **200**, corps `{"token":"…=:…="}` (le repli `data-requesttoken` n'a pas servi) | +| `REPORT` sur la collection (`calendar-query` filtrée sur `UID`) | **207**, en-têtes `Depth: 1` + `Content-Type: application/xml; charset=utf-8` | +| `PUT` avec `If-Match` | **204** → **le partage est en écriture** | +| `REPORT` avec un `requesttoken` **invalide** | **207 quand même** : l'endpoint DAV **n'exige pas** le jeton CSRF | + +- **Collection** = URL du calendrier **sans** `?export` (d'où `URL_COLLECTION` + dérivée dans `nextcloud.js`). +- **Préfixes réels** du `multistatus` : `d:` → `DAV:`, `cal:` → + `urn:ietf:params:xml:ns:caldav` (plus `s:`, `cs:`, `oc:`, `nc:`). Ce sont des + **préfixes**, pas un contrat : sélectionner **par namespace** + (`getElementsByTagNameNS`), jamais par préfixe. +- ⚠️ **Le nom de la ressource n'est PAS l'UID** : l'événement d'UID + `db2d146b-8e18-…` vit dans `…/00D03211-DA2E-4D06-AAAE-6E8C2355BA2B.ics`. + Déduire l'URL en `.ics` **aurait échoué** — d'où le `REPORT` préalable. +- `href` du `multistatus` = chemin **absolu depuis la racine** + (`/remote.php/dav/…`) → à résoudre contre la collection. +- `getetag` est rendu **avec ses guillemets** (`"8ee26a8f…"`) : `If-Match` le + reprend **verbatim**. + --- ## 2. Cible : formulaire mairie (Gravity Forms, form id = 6)