L'accueil ne prend pas en compte les agendas importés #1

Open
opened 2026-08-28 19:56:58 +01:00 by maxime · 5 comments
Owner

Description

Sur la page d'accueil, le bloc « À venir » n'affiche que les événements de l'agenda familial. Les agendas externes ajoutés depuis le panneau « Agendas » (D33, D34) n'y apparaissent jamais, alors qu'ils sont bien visibles dans l'emploi du temps.

Reproduction

  1. Emploi du temps → panneau « Agendas » → ajouter un abonnement à un flux ICS externe contenant un événement dans les prochains jours.
  2. Vérifier que l'événement apparaît bien dans la grille de l'emploi du temps.
  3. Aller sur /accueil.
  4. L'événement est absent de « À venir ».

Cause
ex_app/lib/src/routes/summary.ts ne lit qu'une seule collection : calendarByKind(family.id, CalendarKind.EVENTS). L'exclusion est délibérée pour les deux agendas projetés — les échéances de tâches et les repas ont déjà leur bloc sur l'accueil, les remonter ici mettrait la même tâche dans deux colonnes de la même page (D31). Un abonnement n'est ni l'un ni l'autre : rien d'autre sur l'accueil ne le montre.

Ce qu'il faut prévoir

  • Un abonnement se lit comme le compte de service, jamais comme le membre connecté (D34). Le chemin actuel passe req.user à upcomingEvents.
  • Il peut y en avoir plusieurs. « À venir » est borné à 5 événements : la fusion doit trier par date sur l'ensemble avant de couper, pas agenda par agenda.
  • Chaque agenda lu est un aller-retour CalDAV de plus sur une route qui se veut bornée en temps. Décider si tous les abonnements sont lus, ou seulement ceux que le panneau « Agendas » laisse visibles.
  • calendarUnreadable est aujourd'hui un booléen pour une seule source. Avec plusieurs, il faut trancher ce que veut dire « l'un des agendas n'a pas répondu » sur un accueil qui doit rester lisible.

Plan d'action

La conception est dans ce commentaire ; le bilan du développement dans celui-ci.

  • contracts/calendar.ts : exporter isProjection, qui nomme l'exclusion de D31 au lieu de la coder en dur dans la synthèse.
  • services/calendar.ts : upcomingEvents prend readonly CalendarRef[] — lecture en parallèle, filtre du passé, tri global, coupe à 5 en dernier, unreadable vrai dès qu'un agenda n'a pas répondu.
  • routes/summary.ts : lire calendarsOf(family.id) privé de ses projections, un refOf par agenda (le compte de service lit les abonnements, D33/D34).
  • contracts/summary.ts : faire voyager les agendas de la famille avec la synthèse, comme EventPeriod.calendars (D26).
  • HomeEventsCard : avertissement au-dessus de la liste quand un agenda a échoué mais que d'autres ont répondu ; état vide réservé au cas où il n'y a rien à montrer ; texte revu (« l'agenda familial » n'existe plus au singulier).
  • HomeEventsCard : colorer la pastille avec l'agenda d'origine de l'événement plutôt qu'avec la couleur de la famille.
  • Tests backend : créer services/calendar.test.ts (il n'y en a pas) — tri et coupe sur l'ensemble et non agenda par agenda, lecteur imposé pour un abonnement, échec partiel qui garde les événements lus.
  • Tests frontend : stores/summary.test.ts et la carte — échec partiel avec événements, couleur par agenda.
  • Le filtre compare la fin de l'événement et non son début : ce qui est en cours reste affiché (retour de revue).
  • e2e : reprendre le montage de calendar-subscription.spec.ts (hostIcsFeed, refreshSubscriptions) et vérifier l'événement de l'abonnement sur /accueil ; mettre unreachable-calendar.spec.ts au nouveau texte.
  • Non prévu, découvert en route : la lecture part de la veille — un événement toute-la-journée du jour même n'était jamais rendu par le REPORT.

Recette

  • make lint typecheck test
  • make build-frontend, puis vérifier que le bundle est bien une IIFE
  • cd e2e && npm test — 94 tests verts
  • /code-review, retours déposés en commentaire du ticket
  • Trancher le retour de revue : un événement déjà commencé est désormais gardé tant qu'il n'est pas fini, et affiché « En cours »
  • Revue technique personnelle
  • Mise à jour du wiki (décision, schéma, interface, périmètre) — Architecture D31 : l'exclusion porte sur les projections ; UX : la carte « À venir » et l'échec partiel
  • Revue fonctionnelle

Plan de MEP

  • Rien à migrer : aucune table, aucune colonne, aucune route nouvelle — appinfo/info.xml est inchangé.
  • Livraison applicative simple : image reconstruite avec le bundle frontend, make build-frontend avant la construction de l'image.
  • Rien à purger côté client : la synthèse n'est ni mise en cache par le service worker ni stockée en préférence.
  • Effet immédiat au premier chargement de l'accueil après la livraison ; un abonnement déjà présent apparaît sans geste de la part des familles.
## Description Sur la page d'accueil, le bloc « À venir » n'affiche que les événements de l'agenda familial. Les agendas externes ajoutés depuis le panneau « Agendas » (D33, D34) n'y apparaissent jamais, alors qu'ils sont bien visibles dans l'emploi du temps. **Reproduction** 1. Emploi du temps → panneau « Agendas » → ajouter un abonnement à un flux ICS externe contenant un événement dans les prochains jours. 2. Vérifier que l'événement apparaît bien dans la grille de l'emploi du temps. 3. Aller sur `/accueil`. 4. L'événement est absent de « À venir ». **Cause** `ex_app/lib/src/routes/summary.ts` ne lit qu'une seule collection : `calendarByKind(family.id, CalendarKind.EVENTS)`. L'exclusion est délibérée pour les deux agendas projetés — les échéances de tâches et les repas ont déjà leur bloc sur l'accueil, les remonter ici mettrait la même tâche dans deux colonnes de la même page (D31). Un abonnement n'est ni l'un ni l'autre : rien d'autre sur l'accueil ne le montre. **Ce qu'il faut prévoir** - Un abonnement se lit **comme le compte de service**, jamais comme le membre connecté (D34). Le chemin actuel passe `req.user` à `upcomingEvents`. - Il peut y en avoir plusieurs. « À venir » est borné à 5 événements : la fusion doit trier par date sur l'ensemble avant de couper, pas agenda par agenda. - Chaque agenda lu est un aller-retour CalDAV de plus sur une route qui se veut bornée en temps. Décider si tous les abonnements sont lus, ou seulement ceux que le panneau « Agendas » laisse visibles. - `calendarUnreadable` est aujourd'hui un booléen pour une seule source. Avec plusieurs, il faut trancher ce que veut dire « l'un des agendas n'a pas répondu » sur un accueil qui doit rester lisible. ## Plan d'action La conception est [dans ce commentaire](https://git.lozach.eu/maxime/OrganisateurFamilial/issues/1#issuecomment-84) ; le bilan du développement [dans celui-ci](https://git.lozach.eu/maxime/OrganisateurFamilial/issues/1#issuecomment-88). - [x] `contracts/calendar.ts` : exporter `isProjection`, qui nomme l'exclusion de D31 au lieu de la coder en dur dans la synthèse. - [x] `services/calendar.ts` : `upcomingEvents` prend `readonly CalendarRef[]` — lecture en parallèle, filtre du passé, tri global, coupe à 5 en dernier, `unreadable` vrai dès qu'un agenda n'a pas répondu. - [x] `routes/summary.ts` : lire `calendarsOf(family.id)` privé de ses projections, un `refOf` par agenda (le compte de service lit les abonnements, D33/D34). - [x] `contracts/summary.ts` : faire voyager les agendas de la famille avec la synthèse, comme `EventPeriod.calendars` (D26). - [x] `HomeEventsCard` : avertissement au-dessus de la liste quand un agenda a échoué mais que d'autres ont répondu ; état vide réservé au cas où il n'y a rien à montrer ; texte revu (« l'agenda familial » n'existe plus au singulier). - [x] `HomeEventsCard` : colorer la pastille avec l'agenda d'origine de l'événement plutôt qu'avec la couleur de la famille. - [x] Tests backend : créer `services/calendar.test.ts` (il n'y en a pas) — tri et coupe sur l'ensemble et non agenda par agenda, lecteur imposé pour un abonnement, échec partiel qui garde les événements lus. - [x] Tests frontend : `stores/summary.test.ts` et la carte — échec partiel avec événements, couleur par agenda. - [x] Le filtre compare la fin de l'événement et non son début : ce qui est en cours reste affiché (retour de revue). - [x] e2e : reprendre le montage de `calendar-subscription.spec.ts` (`hostIcsFeed`, `refreshSubscriptions`) et vérifier l'événement de l'abonnement sur `/accueil` ; mettre `unreachable-calendar.spec.ts` au nouveau texte. - [x] Non prévu, découvert en route : la lecture part de la veille — un événement toute-la-journée du jour même n'était jamais rendu par le `REPORT`. ## Recette - [x] `make lint typecheck test` - [x] `make build-frontend`, puis vérifier que le bundle est bien une IIFE - [x] `cd e2e && npm test` — 94 tests verts - [x] `/code-review`, retours déposés en commentaire du ticket - [x] Trancher le retour de revue : un événement déjà commencé est désormais gardé tant qu'il n'est pas fini, et affiché « En cours » - [x] Revue technique personnelle - [x] Mise à jour du wiki (décision, schéma, interface, périmètre) — `Architecture` D31 : l'exclusion porte sur les projections ; `UX` : la carte « À venir » et l'échec partiel - [x] Revue fonctionnelle ## Plan de MEP - Rien à migrer : aucune table, aucune colonne, aucune route nouvelle — `appinfo/info.xml` est inchangé. - Livraison applicative simple : image reconstruite avec le bundle frontend, `make build-frontend` avant la construction de l'image. - Rien à purger côté client : la synthèse n'est ni mise en cache par le service worker ni stockée en préférence. - Effet immédiat au premier chargement de l'accueil après la livraison ; un abonnement déjà présent apparaît sans geste de la part des familles.
maxime changed title from L'acceuil ne prends pas en compte les plannings importé to L'accueil ne prends pas en compte les plannings importé 2026-08-28 19:57:36 +01:00
maxime added this to the 1.0.x milestone 2026-08-28 20:02:51 +01:00
claudeagent changed title from L'accueil ne prends pas en compte les plannings importé to L'accueil ne prend pas en compte les agendas importés 2026-08-30 14:07:44 +01:00
Collaborator

Conception

Ce que l'accueil lit désormais

L'agenda des événements et les abonnements, jamais les deux agendas projetés. La règle
que D31 pose n'est pas « l'accueil ne lit qu'un agenda », c'est « l'accueil ne montre pas
deux fois la même chose » : les échéances de tâches et les repas ont déjà leur bloc sur la
page, un abonnement n'en a aucun.

Le code dit aujourd'hui calendarByKind(family.id, EVENTS), ce qui code en dur une seule
sortie possible. Il dira calendarsOf(family.id) filtré sur un prédicat exporté depuis
contracts/calendar.ts :

/** Why: ce que cette application écrit ailleurs et projette ici. Voir D31. */
export function isProjection(calendar: Pick<FamilyCalendar, 'kind'>): boolean {
	return calendar.kind === CalendarKind.TASKS || calendar.kind === CalendarKind.MEALS
}

Ainsi un cinquième type d'agenda ajouté demain est lu par défaut plutôt qu'oublié en
silence, et l'exclusion reste nommée là où elle se décide.

Qui lit

Rien de nouveau côté authentification : refOf pose déjà readerId = compte de service
pour un abonnement (D33), et objectsBetween lit avec calendar.readerId ?? userId. Il
suffit de construire un CalendarRef par agenda avec refOf — l'agenda d'événements
continue d'être lu comme le membre, à travers le partage Team, ce qui reste chargé de sens
(D31).

Fusion et coupe

upcomingEvents passe de CalendarRef | null à readonly CalendarRef[] :

  1. lecture en parallèle des agendas, comme eventsBetween ;
  2. filtre du passé (un événement toute la journée en cours n'est pas passé — inchangé) ;
  3. tri par instant sur l'ensemble ;
  4. slice(limit) en dernier.

Deux conséquences dans le corps de la fonction :

  • le tableau vide se comporte comme le null d'aujourd'hui : { events: [], unreadable: false } ;
  • le retour anticipé sur unreadable disparaît. Avec plusieurs sources il faut garder ce
    qui a été lu et signaler l'échec ; c'est exactement ce que eventsBetween fait déjà.

Le coût réseau : tous les abonnements sont lus

Les lectures sont parallèles : l'accueil coûte le plus lent des REPORT, pas leur somme —
le contrat que eventsBetween tient déjà pour les trois agendas de la grille. Le nombre est
borné par ailleurs : MAX_SUBSCRIPTIONS_PER_FAMILY = 5 (D33), donc six collections au pire.

Tous les abonnements, pas seulement ceux que le panneau laisse visibles. Le masquage est
une préférence de navigateur (organisateur_familial:timetable-hidden-calendars, par
famille et par appareil, D27) : le serveur ne la connaît pas, et c'est une préférence de
l'emploi du temps, pas de l'accueil — elle répond à une grille chargée, pas à cinq
lignes. Un abonnement est de toute façon affiché par défaut (isShownByDefault).

Deux alternatives écartées :

  • envoyer les jetons masqués dans la requête — une préférence de navigateur dans une URL,
    et une réponse qui varie selon une clé du poste appelant ;
  • filtrer côté client après réception — la coupe à 5 est faite par le serveur, l'accueil
    afficherait donc moins de 5 événements sans que rien ne dise pourquoi.

calendarUnreadable

Le champ garde son nom et change de définition : « au moins un des agendas lus n'a pas
répondu ». C'est déjà le sens de EventPeriod.unreadable pour une période (D26), et lequel
a échoué n'est pas dit : la conséquence est la même, et nommer un agenda deviendrait faux
dès qu'un second tombe.

Sur l'écran, en revanche, le message ne peut plus remplacer le contenu. Il passe
aujourd'hui par le empty de HomeCard, ce qui est juste tant qu'un échec veut dire zéro
événement ; avec plusieurs sources, un échec partiel coexiste avec des événements lus. Deux
cas :

  • aucun événement et injoignable → l'état vide d'aujourd'hui, texte revu ;
  • des événements et injoignable → un avertissement au-dessus de la liste, la liste reste.

Le texte « L'agenda familial est momentanément injoignable. » ne décrit plus rien de juste
(il n'y a plus « l'agenda »). Proposition alignée sur la bannière de l'emploi du temps :
« Un agenda est injoignable : il peut manquer des événements. » Le sélecteur
.home-card__empty--warning d'unreachable-calendar.spec.ts teste le texte actuel et suit.

La pastille de couleur

HomeEventsCard peint son marqueur avec --family-color. Un événement venu d'un abonnement
s'afficherait donc aux couleurs de la famille, alors que la grille le distingue.

Proposition : faire voyager les agendas avec la synthèse (HomeSummary.calendars, comme
EventPeriod.calendars le fait pour la grille — même raison qu'en D26 : un client qui les
cacherait dessinerait une légende périmée) et peindre le marqueur avec la couleur de
l'agenda de l'événement, que event.calendarId désigne déjà. Coût : une lecture SQLite,
aucun appel réseau de plus.

C'est la seule partie du ticket qui dépasse la correction stricte du bug. Si elle est
écartée, il faut l'assumer : un événement externe restera indiscernable d'un événement
familial sur l'accueil.

Ce qui ne bouge pas

  • Aucune table, aucune colonne, aucune route : appinfo/info.xml est inchangé.
  • L'en-tête X-NC-CalDAV-Webcal-Caching: On est déjà envoyé sur toutes les requêtes DAV
    (D33) — sans lui un REPORT sur un abonnement répond 207 vide, indiscernable de « rien
    dans cette période ».
  • Un abonnement qui n'a pas encore été rafraîchi par son RefreshWebcalJob est vide, pas
    injoignable. L'accueil dira « rien de prévu », ce qui est correct.
## Conception ### Ce que l'accueil lit désormais L'agenda des événements **et les abonnements**, jamais les deux agendas projetés. La règle que D31 pose n'est pas « l'accueil ne lit qu'un agenda », c'est « l'accueil ne montre pas deux fois la même chose » : les échéances de tâches et les repas ont déjà leur bloc sur la page, un abonnement n'en a aucun. Le code dit aujourd'hui `calendarByKind(family.id, EVENTS)`, ce qui code en dur une seule sortie possible. Il dira `calendarsOf(family.id)` filtré sur un prédicat exporté depuis `contracts/calendar.ts` : ```ts /** Why: ce que cette application écrit ailleurs et projette ici. Voir D31. */ export function isProjection(calendar: Pick<FamilyCalendar, 'kind'>): boolean { return calendar.kind === CalendarKind.TASKS || calendar.kind === CalendarKind.MEALS } ``` Ainsi un cinquième type d'agenda ajouté demain est lu par défaut plutôt qu'oublié en silence, et l'exclusion reste nommée là où elle se décide. ### Qui lit Rien de nouveau côté authentification : `refOf` pose déjà `readerId = compte de service` pour un abonnement (D33), et `objectsBetween` lit avec `calendar.readerId ?? userId`. Il suffit de construire un `CalendarRef` par agenda avec `refOf` — l'agenda d'événements continue d'être lu comme le membre, à travers le partage Team, ce qui reste chargé de sens (D31). ### Fusion et coupe `upcomingEvents` passe de `CalendarRef | null` à `readonly CalendarRef[]` : 1. lecture en parallèle des agendas, comme `eventsBetween` ; 2. filtre du passé (un événement toute la journée en cours n'est pas passé — inchangé) ; 3. tri par instant **sur l'ensemble** ; 4. `slice(limit)` en dernier. Deux conséquences dans le corps de la fonction : - le tableau vide se comporte comme le `null` d'aujourd'hui : `{ events: [], unreadable: false }` ; - le retour anticipé sur `unreadable` disparaît. Avec plusieurs sources il faut garder ce qui a été lu **et** signaler l'échec ; c'est exactement ce que `eventsBetween` fait déjà. ### Le coût réseau : tous les abonnements sont lus Les lectures sont parallèles : l'accueil coûte le plus lent des `REPORT`, pas leur somme — le contrat que `eventsBetween` tient déjà pour les trois agendas de la grille. Le nombre est borné par ailleurs : `MAX_SUBSCRIPTIONS_PER_FAMILY = 5` (D33), donc six collections au pire. **Tous les abonnements, pas seulement ceux que le panneau laisse visibles.** Le masquage est une préférence de navigateur (`organisateur_familial:timetable-hidden-calendars`, par famille et par appareil, D27) : le serveur ne la connaît pas, et c'est une préférence de **l'emploi du temps**, pas de l'accueil — elle répond à une grille chargée, pas à cinq lignes. Un abonnement est de toute façon affiché par défaut (`isShownByDefault`). Deux alternatives écartées : - **envoyer les jetons masqués dans la requête** — une préférence de navigateur dans une URL, et une réponse qui varie selon une clé du poste appelant ; - **filtrer côté client après réception** — la coupe à 5 est faite par le serveur, l'accueil afficherait donc moins de 5 événements sans que rien ne dise pourquoi. ### `calendarUnreadable` Le champ garde son nom et change de définition : « au moins un des agendas lus n'a pas répondu ». C'est déjà le sens de `EventPeriod.unreadable` pour une période (D26), et lequel a échoué n'est pas dit : la conséquence est la même, et nommer un agenda deviendrait faux dès qu'un second tombe. Sur l'écran, en revanche, le message ne peut plus **remplacer** le contenu. Il passe aujourd'hui par le `empty` de `HomeCard`, ce qui est juste tant qu'un échec veut dire zéro événement ; avec plusieurs sources, un échec partiel coexiste avec des événements lus. Deux cas : - aucun événement et injoignable → l'état vide d'aujourd'hui, texte revu ; - des événements et injoignable → un avertissement **au-dessus** de la liste, la liste reste. Le texte « L'agenda familial est momentanément injoignable. » ne décrit plus rien de juste (il n'y a plus « l'agenda »). Proposition alignée sur la bannière de l'emploi du temps : « Un agenda est injoignable : il peut manquer des événements. » Le sélecteur `.home-card__empty--warning` d'`unreachable-calendar.spec.ts` teste le texte actuel et suit. ### La pastille de couleur `HomeEventsCard` peint son marqueur avec `--family-color`. Un événement venu d'un abonnement s'afficherait donc aux couleurs de la famille, alors que la grille le distingue. Proposition : faire voyager les agendas avec la synthèse (`HomeSummary.calendars`, comme `EventPeriod.calendars` le fait pour la grille — même raison qu'en D26 : un client qui les cacherait dessinerait une légende périmée) et peindre le marqueur avec la couleur de l'agenda de l'événement, que `event.calendarId` désigne déjà. Coût : une lecture SQLite, aucun appel réseau de plus. C'est la seule partie du ticket qui dépasse la correction stricte du bug. Si elle est écartée, il faut l'assumer : un événement externe restera indiscernable d'un événement familial sur l'accueil. ### Ce qui ne bouge pas - Aucune table, aucune colonne, aucune route : `appinfo/info.xml` est inchangé. - L'en-tête `X-NC-CalDAV-Webcal-Caching: On` est déjà envoyé sur toutes les requêtes DAV (D33) — sans lui un `REPORT` sur un abonnement répond 207 vide, indiscernable de « rien dans cette période ». - Un abonnement qui n'a pas encore été rafraîchi par son `RefreshWebcalJob` est vide, pas injoignable. L'accueil dira « rien de prévu », ce qui est correct.
Collaborator

Développement terminé — branche fix/accueil-agendas-importes

Cinq commits, dans cet ordre :

Commit Ce qu'il porte
refactor(calendars): name the payload conversion once presentCalendar, au lieu d'un familyId retiré à la main dans trois payloads
fix(summary): the home reads the family's external agendas too le backend : isProjection, upcomingEvents sur plusieurs agendas, la route, services/calendar.test.ts (11 tests, le fichier n'existait pas)
fix(summary): an event on the home says which agenda it came from le front : couleur par agenda, avertissement au-dessus de la liste, et l'e2e
docs(skill): a REPORT starting mid-day loses that day's all-day events la mesure ci-dessous, dans references/icalendar.md
chore(e2e): format offline.spec.ts make lint échouait sur main avant cette branche

Ce qui a été mesuré en route, et corrigé ici

L'accueil n'affichait aucun événement « toute la journée » du jour même, y compris ceux
de l'agenda de la famille. Nextcloud 34 indexe un VEVENT toute-la-journée sans DTEND —
ce dont un flux externe est plein — comme une occurrence de durée nulle, et le REPORT ne
garde un objet que tant que sa dernière occurrence est strictement après le début de la
période demandée. Demander à partir de « maintenant » répondait donc 207 sans les
événements du jour, sans erreur ni avertissement. Mesuré sur la même collection :

Période demandée Objets rendus
2026-08-24T00:00Z → 2026-08-30T23:59Z (la semaine) 1
2026-08-30T00:00Z → 2026-09-29T16:00Z (l'accueil) 0

L'accueil lit maintenant à partir de la veille et filtre le passé lui-même. Une journée
entière de marge et non un instant : cette occurrence est datée dans le fuseau du serveur et
peut tomber de part et d'autre de minuit UTC.

Retour de revue à trancher

services/calendar.ts — un événement toute-la-journée sur plusieurs jours, commencé avant
aujourd'hui, n'apparaît pas sur l'accueil alors qu'il est en cours.
Le filtre compare la
date de début de l'événement au début du jour, pas sa fin. Un flux de vacances scolaires
portant un seul VEVENT du 17/10 au 02/11 est donc absent de « À venir » le 20/10 — et
c'est exactement la forme qu'a un flux d'établissement scolaire.

Le comportement est antérieur à ce ticket (le commentaire du code prétend déjà l'inverse :
« an all-day event in progress is not past. It is kept until it ends »), mais il devient
visible maintenant que l'accueil lit des flux externes. Deux lectures défendables :

  1. « À venir » veut dire qui commence bientôt — les vacances déjà commencées n'y ont pas
    leur place, et il faut corriger le commentaire plutôt que le code ;
  2. « À venir » veut dire ce qui occupe les trente prochains jours — il faut alors comparer
    event.end pour un événement toute-la-journée, et les vacances en cours s'affichent.

Rien n'est changé pour l'instant : c'est un choix de produit, pas un correctif évident.

Portes mécaniques

  • make lint typecheck test — vert (378 tests backend, 342 front)
  • make build-frontend + vérification IIFE du bundle — vert
  • cd e2e && npm test — 94 tests, tous verts (dont les deux nouvelles étapes de
    calendar-subscription.spec.ts)
  • /code-review niveau high — un seul retour, celui ci-dessus

Wiki

  • Architecture / D31 : la règle d'exclusion devient « rien que la page montre déjà sous
    son propre titre », avec les conséquences de la lecture multi-agendas et la mesure
    ci-dessus.
  • UX : ce que montre la carte « À venir », la couleur par agenda, et le fait qu'un échec
    partiel s'affiche au-dessus de la liste sans la remplacer.

La branche n'est ni rebasée sur main ni poussée.

## Développement terminé — branche `fix/accueil-agendas-importes` Cinq commits, dans cet ordre : | Commit | Ce qu'il porte | |---|---| | `refactor(calendars): name the payload conversion once` | `presentCalendar`, au lieu d'un `familyId` retiré à la main dans trois payloads | | `fix(summary): the home reads the family's external agendas too` | le backend : `isProjection`, `upcomingEvents` sur plusieurs agendas, la route, `services/calendar.test.ts` (11 tests, le fichier n'existait pas) | | `fix(summary): an event on the home says which agenda it came from` | le front : couleur par agenda, avertissement au-dessus de la liste, et l'e2e | | `docs(skill): a REPORT starting mid-day loses that day's all-day events` | la mesure ci-dessous, dans `references/icalendar.md` | | `chore(e2e): format offline.spec.ts` | `make lint` échouait sur `main` avant cette branche | ### Ce qui a été mesuré en route, et corrigé ici **L'accueil n'affichait aucun événement « toute la journée » du jour même**, y compris ceux de l'agenda de la famille. Nextcloud 34 indexe un `VEVENT` toute-la-journée sans `DTEND` — ce dont un flux externe est plein — comme une occurrence de durée nulle, et le `REPORT` ne garde un objet que tant que sa dernière occurrence est **strictement après** le début de la période demandée. Demander à partir de « maintenant » répondait donc 207 sans les événements du jour, sans erreur ni avertissement. Mesuré sur la même collection : | Période demandée | Objets rendus | |---|---| | `2026-08-24T00:00Z → 2026-08-30T23:59Z` (la semaine) | 1 | | `2026-08-30T00:00Z → 2026-09-29T16:00Z` (l'accueil) | 0 | L'accueil lit maintenant à partir de la veille et filtre le passé lui-même. Une journée entière de marge et non un instant : cette occurrence est datée dans le fuseau du serveur et peut tomber de part et d'autre de minuit UTC. ### Retour de revue à trancher `services/calendar.ts` — **un événement toute-la-journée sur plusieurs jours, commencé avant aujourd'hui, n'apparaît pas sur l'accueil alors qu'il est en cours.** Le filtre compare la date de *début* de l'événement au début du jour, pas sa fin. Un flux de vacances scolaires portant un seul `VEVENT` du 17/10 au 02/11 est donc absent de « À venir » le 20/10 — et c'est exactement la forme qu'a un flux d'établissement scolaire. Le comportement est antérieur à ce ticket (le commentaire du code prétend déjà l'inverse : « an all-day event in progress is not past. It is kept until it ends »), mais il devient visible maintenant que l'accueil lit des flux externes. Deux lectures défendables : 1. « À venir » veut dire *qui commence bientôt* — les vacances déjà commencées n'y ont pas leur place, et il faut corriger le commentaire plutôt que le code ; 2. « À venir » veut dire *ce qui occupe les trente prochains jours* — il faut alors comparer `event.end` pour un événement toute-la-journée, et les vacances en cours s'affichent. Rien n'est changé pour l'instant : c'est un choix de produit, pas un correctif évident. ### Portes mécaniques - `make lint typecheck test` — vert (378 tests backend, 342 front) - `make build-frontend` + vérification IIFE du bundle — vert - `cd e2e && npm test` — **94 tests, tous verts** (dont les deux nouvelles étapes de `calendar-subscription.spec.ts`) - `/code-review` niveau *high* — un seul retour, celui ci-dessus ### Wiki - `Architecture` / D31 : la règle d'exclusion devient « rien que la page montre déjà sous son propre titre », avec les conséquences de la lecture multi-agendas et la mesure ci-dessus. - `UX` : ce que montre la carte « À venir », la couleur par agenda, et le fait qu'un échec partiel s'affiche au-dessus de la liste sans la remplacer. La branche n'est ni rebasée sur `main` ni poussée.
Collaborator

Retour de revue tranché : un événement en cours reste sur l'accueil

Arbitrage retenu : « À venir » veut dire ce qui occupe les trente prochains jours. Le
filtre compare donc la fin de l'événement, et non plus son début.

Commit fix(summary): an event under way is still what the family has on.

  • Un événement sans fin du tout garde l'ancienne règle, sur son début : c'est tout ce dont
    on dispose pour le juger, et la plupart des entrées toute-la-journée d'un flux n'ont pas
    de DTEND.
  • eventEnd est explicite sur ce que vaut la valeur : le DTEND d'un événement
    toute-la-journée désigne le lendemain du dernier jour. La comparaison est stricte, sinon
    les vacances disparaissent le matin de leur dernier jour. Trois tests bornent exactement
    ça : la veille du dernier jour, le dernier jour, le lendemain.
  • Un événement chronométré en cours (une réunion de 11 h à 13 h à midi) est gardé lui aussi
    — même règle, appliquée sans exception.
  • La carte affiche « En cours » au lieu de la date de début pour un tel événement : « 17
    août » sous « Les 30 prochains jours » se lirait comme un bug.

Le flux servi par la suite e2e portait un seul événement d'une journée, que l'ancienne règle
traitait correctement — il ne pouvait donc rien détecter ici. Il porte maintenant une
seconde entrée commencée il y a trois jours et finissant demain, vérifiée sur l'accueil.

Portes rejouées : make lint typecheck test vert (382 tests backend, 342 front),
bundle IIFE vérifié, cd e2e && npm test 94/94 vert.

Wiki : la règle est dans D31 (Architecture) et dans UX.

## Retour de revue tranché : un événement en cours reste sur l'accueil Arbitrage retenu : « À venir » veut dire *ce qui occupe les trente prochains jours*. Le filtre compare donc la **fin** de l'événement, et non plus son début. Commit `fix(summary): an event under way is still what the family has on`. - Un événement sans fin du tout garde l'ancienne règle, sur son début : c'est tout ce dont on dispose pour le juger, et la plupart des entrées toute-la-journée d'un flux n'ont pas de `DTEND`. - `eventEnd` est explicite sur ce que vaut la valeur : le `DTEND` d'un événement toute-la-journée désigne le lendemain du dernier jour. La comparaison est stricte, sinon les vacances disparaissent le matin de leur dernier jour. Trois tests bornent exactement ça : la veille du dernier jour, le dernier jour, le lendemain. - Un événement chronométré en cours (une réunion de 11 h à 13 h à midi) est gardé lui aussi — même règle, appliquée sans exception. - La carte affiche « En cours » au lieu de la date de début pour un tel événement : « 17 août » sous « Les 30 prochains jours » se lirait comme un bug. Le flux servi par la suite e2e portait un seul événement d'une journée, que l'ancienne règle traitait correctement — il ne pouvait donc rien détecter ici. Il porte maintenant une seconde entrée commencée il y a trois jours et finissant demain, vérifiée sur l'accueil. **Portes rejouées** : `make lint typecheck test` vert (382 tests backend, 342 front), bundle IIFE vérifié, `cd e2e && npm test` **94/94** vert. Wiki : la règle est dans D31 (`Architecture`) et dans `UX`.
Collaborator

Revue de la branche complète, avant fusion

/code-review niveau high sur main...HEAD (6 commits). Deux retours, un seul demandant
une décision.

1. Le wiki mentait sur les rappels — corrigé

Le paragraphe de D31 réécrit par cette branche s'intitulait « The home and the reminders
leave out the projections ». Faux : seul l'accueil a changé. services/reminder.ts:117 lit
toujours calendarByKind(family.id, CalendarKind.EVENTS), donc un événement venu d'un
abonnement ne déclenche aucun rappel
. Quelqu'un consultant D31 pour le savoir aurait eu la
mauvaise réponse.

D31 est corrigé : l'accueil exclut les projections, les rappels lisent l'agenda d'événements
seul — et pour une autre raison, qui vaut aussi pour un abonnement : personne ici n'a planifié
ce que porte un flux, et un flux de quelques centaines d'entrées notifierait une famille pour
chacune d'elles. La page dit désormais que c'est une question ouverte, pas un oubli.

Si tu veux qu'un abonnement notifie, c'est un ticket à part — il faudra choisir quoi
notifier dans un flux qu'on ne contrôle pas.

2. Un événement long en cours peut occuper les cinq places — à trancher, plus tard

Conséquence directe de l'arbitrage d'hier : les événements sont triés par date de début et
l'accueil en garde cinq. Un événement commencé il y a longtemps et pas terminé passe donc
devant tout le reste, pour toute sa durée.

Un flux d'établissement portant un unique VEVENT « Année scolaire » du 1ᵉʳ septembre au
31 décembre trusterait la première ligne de « À venir » pendant quatre mois, avec « En cours »
pour seule information. Deux entrées de ce genre et il ne reste que trois places pour ce qui
arrive vraiment.

Rien n'est changé : trier ce qui est en cours après ce qui est à venir, ou borner la durée
au-delà de laquelle un événement ne remonte plus, sont deux choix de produit et aucun n'est
évidemment le bon. À voir si le cas se présente sur un vrai flux.

Ce que la revue a vérifié sans rien trouver

  • Le compte lecteur par agenda (refOf), inchangé et déjà couvert par D33.
  • eventEnd sur des valeurs mal formées : un DTEND horaire sur un événement
    toute-la-journée produit NaN, ce que le garde intercepte pour retomber sur le début.
  • L'ordre filter puis slice : la coupe à 5 se fait bien après l'élimination du passé.
  • Un échec partiel : les événements lus sont conservés, unreadable remonte quand même.
  • Le service worker ne met pas la synthèse en cache — rien à purger à la livraison.
  • Une famille sans aucun agenda : aucun appel réseau, aucune exception.
  • Les classes CSS ajoutées (.home-events__notice) ne sont partagées avec aucune autre
    colonne, conformément à l'invariant d'AGENTS.md.

État

Branche prête à être fusionnée en fast-forward sur main (qui n'a pas bougé). Portes toutes
vertes au dernier passage : make lint typecheck test, bundle IIFE, cd e2e && npm test
94/94.

## Revue de la branche complète, avant fusion `/code-review` niveau *high* sur `main...HEAD` (6 commits). Deux retours, un seul demandant une décision. ### 1. Le wiki mentait sur les rappels — corrigé Le paragraphe de D31 réécrit par cette branche s'intitulait « The home **and the reminders** leave out the projections ». Faux : seul l'accueil a changé. `services/reminder.ts:117` lit toujours `calendarByKind(family.id, CalendarKind.EVENTS)`, donc **un événement venu d'un abonnement ne déclenche aucun rappel**. Quelqu'un consultant D31 pour le savoir aurait eu la mauvaise réponse. D31 est corrigé : l'accueil exclut les projections, les rappels lisent l'agenda d'événements seul — et pour une autre raison, qui vaut aussi pour un abonnement : personne ici n'a planifié ce que porte un flux, et un flux de quelques centaines d'entrées notifierait une famille pour chacune d'elles. La page dit désormais que c'est une question ouverte, pas un oubli. **Si tu veux qu'un abonnement notifie, c'est un ticket à part** — il faudra choisir quoi notifier dans un flux qu'on ne contrôle pas. ### 2. Un événement long en cours peut occuper les cinq places — à trancher, plus tard Conséquence directe de l'arbitrage d'hier : les événements sont triés par date de début et l'accueil en garde cinq. Un événement commencé il y a longtemps et pas terminé passe donc **devant tout le reste**, pour toute sa durée. Un flux d'établissement portant un unique `VEVENT` « Année scolaire » du 1ᵉʳ septembre au 31 décembre trusterait la première ligne de « À venir » pendant quatre mois, avec « En cours » pour seule information. Deux entrées de ce genre et il ne reste que trois places pour ce qui arrive vraiment. Rien n'est changé : trier ce qui est en cours *après* ce qui est à venir, ou borner la durée au-delà de laquelle un événement ne remonte plus, sont deux choix de produit et aucun n'est évidemment le bon. À voir si le cas se présente sur un vrai flux. ### Ce que la revue a vérifié sans rien trouver - Le compte lecteur par agenda (`refOf`), inchangé et déjà couvert par D33. - `eventEnd` sur des valeurs mal formées : un `DTEND` horaire sur un événement toute-la-journée produit `NaN`, ce que le garde intercepte pour retomber sur le début. - L'ordre `filter` puis `slice` : la coupe à 5 se fait bien après l'élimination du passé. - Un échec partiel : les événements lus sont conservés, `unreadable` remonte quand même. - Le service worker ne met pas la synthèse en cache — rien à purger à la livraison. - Une famille sans aucun agenda : aucun appel réseau, aucune exception. - Les classes CSS ajoutées (`.home-events__notice`) ne sont partagées avec aucune autre colonne, conformément à l'invariant d'`AGENTS.md`. ### État Branche prête à être fusionnée en fast-forward sur `main` (qui n'a pas bougé). Portes toutes vertes au dernier passage : `make lint typecheck test`, bundle IIFE, `cd e2e && npm test` 94/94.
Collaborator

Fusionné sur main

Fast-forward des 6 commits, branche fix/accueil-agendas-importes supprimée en local.
main est 6 commits en avance sur origin/main : rien n'est poussé.

b6d1d82 fix(summary): an event under way is still what the family has on
02dfd2c docs(skill): a REPORT starting mid-day loses that day's all-day events
762ebe3 chore(e2e): format offline.spec.ts
1685e18 fix(summary): an event on the home says which agenda it came from
2515ba9 fix(summary): the home reads the family's external agendas too
4794c14 refactor(calendars): name the payload conversion once

Ticket en step/done, jalon 1.0.x. Reste la revue fonctionnelle dans l'application et la
livraison — le Plan de MEP la décrit : rien à migrer, image reconstruite avec le bundle
frontend, effet au premier chargement de l'accueil.

## Fusionné sur `main` Fast-forward des 6 commits, branche `fix/accueil-agendas-importes` supprimée en local. `main` est **6 commits en avance sur `origin/main`** : rien n'est poussé. ``` b6d1d82 fix(summary): an event under way is still what the family has on 02dfd2c docs(skill): a REPORT starting mid-day loses that day's all-day events 762ebe3 chore(e2e): format offline.spec.ts 1685e18 fix(summary): an event on the home says which agenda it came from 2515ba9 fix(summary): the home reads the family's external agendas too 4794c14 refactor(calendars): name the payload conversion once ``` Ticket en `step/done`, jalon `1.0.x`. Reste la revue fonctionnelle dans l'application et la livraison — le `Plan de MEP` la décrit : rien à migrer, image reconstruite avec le bundle frontend, effet au premier chargement de l'accueil.
Sign in to join this conversation.
No description provided.