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 |
|
| `syncToCalendar` | `bool` | non | `false` | Opt-in pour la sync Zimbra |
|
||||||
| `calendarEventUid` | `string` | oui | `null` | UID du VEVENT dans Zimbra |
|
| `calendarEventUid` | `string` | oui | `null` | UID du VEVENT dans Zimbra |
|
||||||
| `calendarTodoUid` | `string` | oui | `null` | UID du VTODO 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`
|
### Nouvelle entité `TaskRecurrence`
|
||||||
|
|
||||||
| Champ | Type | Nullable | Description |
|
| Champ | Type | Nullable | Description |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `id` | `int` | non | PK auto-increment |
|
| `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) |
|
| `interval` | `int` | non | Tous les X (jours/semaines/mois/ans) |
|
||||||
| `daysOfWeek` | `json` | oui | Jours de la semaine pour hebdo, ex: `["monday","wednesday"]` |
|
| `daysOfWeek` | `json` | oui | Jours de la semaine pour hebdo, ex: `["monday","wednesday"]` |
|
||||||
| `dayOfMonth` | `int` | oui | Jour du mois pour mensuel, ex: `15` |
|
| `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 |
|
| `id` | `int` | non | PK auto-increment |
|
||||||
| `serverUrl` | `string` | non | URL CalDAV Zimbra |
|
| `serverUrl` | `string` | non | URL CalDAV Zimbra |
|
||||||
| `username` | `string` | non | Compte Zimbra |
|
| `username` | `string` | non | Compte Zimbra |
|
||||||
| `password` | `string` | non | Mot de passe (chiffré en BDD via sodium) |
|
| `encryptedPassword` | `string` | non | Mot de passe chiffré via `TokenEncryptor` (même pattern que `GiteaConfiguration`) |
|
||||||
| `calendarPath` | `string` | non | Chemin du calendrier, ex: `/Calendar/` |
|
| `calendarPath` | `string` | non | Chemin complet du calendrier, ex: `/dav/user@domain.com/Calendar/` |
|
||||||
| `enabled` | `bool` | non | Activer/désactiver la sync (default false) |
|
| `enabled` | `bool` | non | Activer/désactiver la sync (default false) |
|
||||||
|
|
||||||
## Service CalDAV
|
## 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`.
|
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
|
#### Méthodes
|
||||||
|
|
||||||
- `createEvent(Task): string` — crée un VEVENT (créneau planifié), retourne l'UID
|
- `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
|
- `deleteTodo(string $uid): void` — supprime le VTODO par UID
|
||||||
- `testConnection(): bool` — teste la connexion CalDAV
|
- `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
|
BEGIN:VEVENT
|
||||||
UID:{calendarEventUid}
|
UID:{calendarEventUid}
|
||||||
SUMMARY:[PROJET-NUM] Titre de la tâche
|
SUMMARY:[PROJET-NUM] Titre de la tâche
|
||||||
DTSTART:{scheduledStart}
|
DTSTART:{scheduledStart en UTC, format 20260319T140000Z}
|
||||||
DTEND:{scheduledEnd}
|
DTEND:{scheduledEnd en UTC}
|
||||||
DESCRIPTION:{description}\n\nLesstime: {url}
|
DESCRIPTION:{description}\n\nLesstime: {url}
|
||||||
RRULE:{rrule si récurrence}
|
RRULE:{rrule si récurrence}
|
||||||
END:VEVENT
|
END:VEVENT
|
||||||
|
END:VCALENDAR
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Format VTODO (deadline)
|
**VTODO (deadline)** :
|
||||||
|
|
||||||
```
|
```
|
||||||
|
BEGIN:VCALENDAR
|
||||||
|
VERSION:2.0
|
||||||
|
PRODID:-//Lesstime//CalDAV//EN
|
||||||
BEGIN:VTODO
|
BEGIN:VTODO
|
||||||
UID:{calendarTodoUid}
|
UID:{calendarTodoUid}
|
||||||
SUMMARY:[PROJET-NUM] Titre de la tâche (deadline)
|
SUMMARY:[PROJET-NUM] Titre de la tâche (deadline)
|
||||||
DUE:{deadline}
|
DUE:{deadline en UTC}
|
||||||
DESCRIPTION:{description}\n\nLesstime: {url}
|
DESCRIPTION:{description}\n\nLesstime: {url}
|
||||||
END:VTODO
|
END:VTODO
|
||||||
|
END:VCALENDAR
|
||||||
```
|
```
|
||||||
|
|
||||||
Pas de RRULE sur le VTODO — il suit la tâche courante uniquement.
|
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
|
### 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
|
### Matrice d'actions
|
||||||
|
|
||||||
@@ -120,7 +146,9 @@ Un **Doctrine Entity Listener** sur `Task` (postPersist, postUpdate, preRemove)
|
|||||||
|
|
||||||
- Timeout CalDAV : 5 secondes
|
- 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
|
- 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é
|
- 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
|
## 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` :
|
4. Quand la tâche passe en statut `isFinal` :
|
||||||
- La tâche est archivée automatiquement (`archived = true`)
|
- La tâche est archivée automatiquement (`archived = true`)
|
||||||
- Les événements Zimbra sont **conservés** (historique)
|
- 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 :
|
- Une nouvelle tâche est créée avec :
|
||||||
- Même titre, description, assigné, tags, projet, groupe, effort, priorité
|
- 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)
|
- 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)
|
- 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
|
- `calendarEventUid` pointant vers le même VEVENT récurrent
|
||||||
- Nouveau `calendarTodoUid` (nouvelle deadline)
|
- 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)
|
5. Si `maxOccurrences` ou `endDate` atteint, la récurrence s'arrête (pas de nouvelle tâche créée)
|
||||||
|
|
||||||
### Calcul de la prochaine date
|
### 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?
|
- `create-task-recurrence` — paramètres : taskId, type, interval, daysOfWeek?, dayOfMonth?, weekOfMonth?, endDate?, maxOccurrences?
|
||||||
- `update-task-recurrence` — paramètres : recurrenceId, + mêmes champs optionnels
|
- `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
|
## Dépendances PHP
|
||||||
|
|
||||||
- `sabre/vobject` — génération/parsing ICS (VEVENT, VTODO, RRULE)
|
- `sabre/vobject` — génération/parsing ICS (VEVENT, VTODO, RRULE)
|
||||||
- `symfony/http-client` — requêtes HTTP CalDAV (PUT, DELETE, PROPFIND)
|
- `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