From bd010728313e772199b421a61d64577b7fc2e08f Mon Sep 17 00:00:00 2001 From: Matthieu Date: Thu, 19 Mar 2026 09:33:18 +0100 Subject: [PATCH] docs : address spec review findings for Zimbra calendar integration - Use TokenEncryptor for password (align with GiteaConfiguration) - Replace Entity Listener with API Platform Processor for CalDAV sync - Add calendarSyncError field for persistent error tracking - Add validation rules for date fields - Fix ICS format (VCALENDAR wrapper, UTC timezone) - Add task number generation for recurring task auto-creation - Add optimistic locking on TaskRecurrence - Clear calendar UIDs on archived tasks - Add API filters for date fields - Document i18n for daysOfWeek - Clarify MCP tool behavior and known limitations Co-Authored-By: Claude Opus 4.6 (1M context) --- .../2026-03-19-zimbra-calendar-design.md | 68 ++++++++++++++++--- 1 file changed, 57 insertions(+), 11 deletions(-) diff --git a/docs/superpowers/specs/2026-03-19-zimbra-calendar-design.md b/docs/superpowers/specs/2026-03-19-zimbra-calendar-design.md index 3e8c1b8..ed5b12c 100644 --- a/docs/superpowers/specs/2026-03-19-zimbra-calendar-design.md +++ b/docs/superpowers/specs/2026-03-19-zimbra-calendar-design.md @@ -27,13 +27,21 @@ Permettre de synchroniser les tâches Lesstime vers un calendrier Zimbra OVH via | `syncToCalendar` | `bool` | non | `false` | Opt-in pour la sync Zimbra | | `calendarEventUid` | `string` | oui | `null` | UID du VEVENT dans Zimbra | | `calendarTodoUid` | `string` | oui | `null` | UID du VTODO dans Zimbra | +| `calendarSyncError` | `string` | oui | `null` | Dernière erreur de sync CalDAV (null = OK) | + +#### Règles de validation + +- `scheduledEnd` requiert `scheduledStart` (et vice versa) — les deux ou aucun +- `scheduledEnd` doit être après `scheduledStart` +- `syncToCalendar = true` sans aucune date → ignoré silencieusement (pas de sync) +- `deadline` est indépendant des dates planifiées (peut exister seul) ### Nouvelle entité `TaskRecurrence` | Champ | Type | Nullable | Description | |---|---|---|---| | `id` | `int` | non | PK auto-increment | -| `type` | `string` | non | Enum : `daily`, `weekly`, `monthly`, `yearly` | +| `type` | `RecurrenceType` (PHP enum) | non | Enum backed string : `daily`, `weekly`, `monthly`, `yearly` | | `interval` | `int` | non | Tous les X (jours/semaines/mois/ans) | | `daysOfWeek` | `json` | oui | Jours de la semaine pour hebdo, ex: `["monday","wednesday"]` | | `dayOfMonth` | `int` | oui | Jour du mois pour mensuel, ex: `15` | @@ -54,8 +62,8 @@ Permettre de synchroniser les tâches Lesstime vers un calendrier Zimbra OVH via | `id` | `int` | non | PK auto-increment | | `serverUrl` | `string` | non | URL CalDAV Zimbra | | `username` | `string` | non | Compte Zimbra | -| `password` | `string` | non | Mot de passe (chiffré en BDD via sodium) | -| `calendarPath` | `string` | non | Chemin du calendrier, ex: `/Calendar/` | +| `encryptedPassword` | `string` | non | Mot de passe chiffré via `TokenEncryptor` (même pattern que `GiteaConfiguration`) | +| `calendarPath` | `string` | non | Chemin complet du calendrier, ex: `/dav/user@domain.com/Calendar/` | | `enabled` | `bool` | non | Activer/désactiver la sync (default false) | ## Service CalDAV @@ -64,6 +72,8 @@ Permettre de synchroniser les tâches Lesstime vers un calendrier Zimbra OVH via Dépendances : `sabre/vobject` pour la génération ICS, requêtes HTTP via `Symfony\Contracts\HttpClient`. +Le service utilise la `ZimbraConfiguration` pour construire l'URL CalDAV complète : `{serverUrl}{calendarPath}{uid}.ics`. Le mot de passe est déchiffré via `TokenEncryptor` avant chaque requête. L'authentification CalDAV se fait via HTTP Basic Auth. + #### Méthodes - `createEvent(Task): string` — crée un VEVENT (créneau planifié), retourne l'UID @@ -74,28 +84,40 @@ Dépendances : `sabre/vobject` pour la génération ICS, requêtes HTTP via `Sym - `deleteTodo(string $uid): void` — supprime le VTODO par UID - `testConnection(): bool` — teste la connexion CalDAV -#### Format VEVENT (créneau planifié) +#### Format ICS + +Toutes les dates sont envoyées en **UTC** (suffixe `Z`). Les composants sont wrappés dans un document iCalendar complet : + +**VEVENT (créneau planifié)** : ``` +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Lesstime//CalDAV//EN BEGIN:VEVENT UID:{calendarEventUid} SUMMARY:[PROJET-NUM] Titre de la tâche -DTSTART:{scheduledStart} -DTEND:{scheduledEnd} +DTSTART:{scheduledStart en UTC, format 20260319T140000Z} +DTEND:{scheduledEnd en UTC} DESCRIPTION:{description}\n\nLesstime: {url} RRULE:{rrule si récurrence} END:VEVENT +END:VCALENDAR ``` -#### Format VTODO (deadline) +**VTODO (deadline)** : ``` +BEGIN:VCALENDAR +VERSION:2.0 +PRODID:-//Lesstime//CalDAV//EN BEGIN:VTODO UID:{calendarTodoUid} SUMMARY:[PROJET-NUM] Titre de la tâche (deadline) -DUE:{deadline} +DUE:{deadline en UTC} DESCRIPTION:{description}\n\nLesstime: {url} END:VTODO +END:VCALENDAR ``` Pas de RRULE sur le VTODO — il suit la tâche courante uniquement. @@ -104,7 +126,11 @@ Pas de RRULE sur le VTODO — il suit la tâche courante uniquement. ### Déclenchement -Un **Doctrine Entity Listener** sur `Task` (postPersist, postUpdate, preRemove) appelle le `CalDavService`. +Un **API Platform State Processor** (`TaskCalendarProcessor`) qui décore le persist/remove processor. La sync CalDAV est appelée **après** le flush en BDD, jamais pendant la transaction. Cela garantit : +- La tâche est sauvegardée même si Zimbra est down +- Pas de blocage de transaction DB par les appels HTTP + +Pour les **MCP tools**, le `CalDavService` doit être appelé explicitement après le `flush()` dans chaque tool qui modifie les champs liés au calendrier (create-task, update-task, delete-task). ### Matrice d'actions @@ -120,7 +146,9 @@ Un **Doctrine Entity Listener** sur `Task` (postPersist, postUpdate, preRemove) - Timeout CalDAV : 5 secondes - En cas d'échec : la tâche est quand même sauvegardée en BDD, un toast d'erreur est affiché côté frontend +- L'erreur est persistée dans `calendarSyncError` (visible dans l'UI comme indicateur rouge) - Les UIDs CalDAV restent `null` si la création a échoué +- En cas de succès après un échec précédent, `calendarSyncError` est remis à `null` ## Tâches récurrentes @@ -132,13 +160,15 @@ Un **Doctrine Entity Listener** sur `Task` (postPersist, postUpdate, preRemove) 4. Quand la tâche passe en statut `isFinal` : - La tâche est archivée automatiquement (`archived = true`) - Les événements Zimbra sont **conservés** (historique) + - Les `calendarEventUid` et `calendarTodoUid` de la tâche archivée sont **vidés** (null) pour éviter toute modification accidentelle de l'événement Zimbra depuis une tâche archivée - Une nouvelle tâche est créée avec : - Même titre, description, assigné, tags, projet, groupe, effort, priorité + - Nouveau `number` généré via `findMaxNumberByProjectForUpdate` (même pattern transactionnel que `TaskNumberProcessor`) - Statut réinitialisé au premier statut (position la plus basse) - Dates recalculées selon le pattern de récurrence (prochaine date selon le pattern, indépendamment de quand la tâche a été terminée) - `calendarEventUid` pointant vers le même VEVENT récurrent - Nouveau `calendarTodoUid` (nouvelle deadline) - - `occurrenceCount` incrémenté + - `occurrenceCount` incrémenté sur `TaskRecurrence` (avec lock optimiste `@ORM\Version` pour éviter les doublons en cas de concurrence) 5. Si `maxOccurrences` ou `endDate` atteint, la récurrence s'arrête (pas de nouvelle tâche créée) ### Calcul de la prochaine date @@ -224,9 +254,25 @@ Accessible uniquement `ROLE_ADMIN`. - `create-task-recurrence` — paramètres : taskId, type, interval, daysOfWeek?, dayOfMonth?, weekOfMonth?, endDate?, maxOccurrences? - `update-task-recurrence` — paramètres : recurrenceId, + mêmes champs optionnels -- `delete-task-recurrence` — paramètres : recurrenceId — supprime la récurrence et l'événement récurrent Zimbra si existant +- `delete-task-recurrence` — paramètres : recurrenceId — supprime la récurrence, nullifie la relation sur la tâche active, et supprime l'événement récurrent Zimbra si existant + +## API Filters + +Ajouter sur `Task` les filtres API Platform suivants : +- `DateFilter` sur `scheduledStart`, `scheduledEnd`, `deadline` (pour le tri et filtrage par plage de dates) +- `BooleanFilter` sur `syncToCalendar` +- `OrderFilter` sur `scheduledStart`, `deadline` + +### Valeurs stockées en JSON (i18n) + +Les `daysOfWeek` dans `TaskRecurrence` sont stockés en anglais (`monday`, `tuesday`...) — les labels traduits sont gérés uniquement côté frontend via i18n. ## Dépendances PHP - `sabre/vobject` — génération/parsing ICS (VEVENT, VTODO, RRULE) - `symfony/http-client` — requêtes HTTP CalDAV (PUT, DELETE, PROPFIND) + +## Limitations connues + +- Sync synchrone : si Zimbra est lent, chaque sauvegarde de tâche peut prendre jusqu'à 5s. Migration vers Symfony Messenger possible à l'avenir si nécessaire. +- Pas de sync bidirectionnelle : les modifications faites directement dans Zimbra ne sont pas reflétées dans Lesstime.