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) <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user