📝(docs) reorganize docs in docs/
This commit is contained in:
@@ -1,196 +0,0 @@
|
||||
# Plan : Découpage en plusieurs PRs
|
||||
|
||||
## Contexte
|
||||
|
||||
- **24 commits** depuis `main`
|
||||
- **158 fichiers** modifiés (+16,943 / -10,848 lignes)
|
||||
- Travail accumulé sans découpage en PRs
|
||||
|
||||
---
|
||||
|
||||
## Stratégie
|
||||
|
||||
Créer **5-6 branches** depuis `main`, chacune avec des commits logiques,
|
||||
puis créer une PR pour chaque branche.
|
||||
|
||||
**Approche technique :**
|
||||
1. Rester sur `poc/event-calendar` (branche actuelle de travail)
|
||||
2. Pour chaque PR : créer une nouvelle branche depuis `main`, copier les
|
||||
fichiers pertinents depuis `poc/event-calendar`, commiter
|
||||
|
||||
---
|
||||
|
||||
## Découpage proposé (6 PRs)
|
||||
|
||||
### PR 1 : Backend - Invitations CalDAV avec emails
|
||||
**Branche** : `feat/caldav-invitations`
|
||||
|
||||
**Fichiers :**
|
||||
- `docker/sabredav/src/AttendeeNormalizerPlugin.php`
|
||||
- `docker/sabredav/src/HttpCallbackIMipPlugin.php`
|
||||
- `docker/sabredav/server.php`
|
||||
- `docker/sabredav/sql/pgsql.calendars.sql`
|
||||
- `src/backend/core/services/calendar_invitation_service.py`
|
||||
- `src/backend/core/api/viewsets_caldav.py`
|
||||
- `src/backend/core/templates/emails/calendar_invitation*.html/txt`
|
||||
- `src/backend/calendars/settings.py`
|
||||
- `env.d/development/backend.defaults`
|
||||
- `env.d/development/caldav.defaults`
|
||||
- `compose.yaml`
|
||||
|
||||
**Description PR** : Ajout du scheduling CalDAV (iTIP) avec envoi d'emails
|
||||
pour les invitations, mises à jour et annulations.
|
||||
|
||||
---
|
||||
|
||||
### PR 2 : Frontend - Refactoring CalDavService et helpers
|
||||
**Branche** : `refactor/caldav-service`
|
||||
|
||||
**Fichiers :**
|
||||
- `features/calendar/services/dav/CalDavService.ts`
|
||||
- `features/calendar/services/dav/EventCalendarAdapter.ts`
|
||||
- `features/calendar/services/dav/caldav-helpers.ts`
|
||||
- `features/calendar/services/dav/helpers/*.ts`
|
||||
- `features/calendar/services/dav/types/*.ts`
|
||||
- `features/calendar/services/dav/constants.ts`
|
||||
- `features/calendar/services/dav/__tests__/*.ts`
|
||||
|
||||
**Description PR** : Refactoring du service CalDAV avec extraction des
|
||||
helpers, meilleure gestion des types et ajout de tests.
|
||||
|
||||
---
|
||||
|
||||
### PR 3 : Frontend - Composant Scheduler (EventModal, handlers)
|
||||
**Branche** : `feat/scheduler-component`
|
||||
|
||||
**Dépend de** : PR 2
|
||||
|
||||
**Fichiers :**
|
||||
- `features/calendar/components/scheduler/*`
|
||||
- `features/calendar/components/RecurrenceEditor.tsx`
|
||||
- `features/calendar/components/RecurrenceEditor.scss`
|
||||
- `features/calendar/components/AttendeesInput.tsx`
|
||||
- `features/calendar/components/AttendeesInput.scss`
|
||||
- `features/calendar/contexts/CalendarContext.tsx`
|
||||
- `pages/calendar.tsx`
|
||||
- `pages/calendar.scss`
|
||||
|
||||
**Description PR** : Nouveau composant Scheduler avec EventModal pour
|
||||
la création/édition d'événements, gestion des récurrences et des invités.
|
||||
|
||||
---
|
||||
|
||||
### PR 4 : Frontend - Refactoring CalendarList modulaire
|
||||
**Branche** : `refactor/calendar-list`
|
||||
|
||||
**Fichiers :**
|
||||
- `features/calendar/components/calendar-list/*`
|
||||
- `features/calendar/components/LeftPanel.tsx`
|
||||
- `features/calendar/components/MiniCalendar.tsx`
|
||||
- `features/calendar/components/MiniCalendar.scss`
|
||||
- `features/calendar/components/CreateCalendarModal.tsx`
|
||||
- `features/calendar/components/CalendarList.scss`
|
||||
- `features/calendar/components/index.ts`
|
||||
|
||||
**Description PR** : Refactoring de CalendarList en composants modulaires
|
||||
(CalendarItemMenu, CalendarListItem, CalendarModal, DeleteConfirmModal).
|
||||
|
||||
---
|
||||
|
||||
### PR 5 : Frontend - Support i18n et locales
|
||||
**Branche** : `feat/calendar-i18n`
|
||||
|
||||
**Fichiers :**
|
||||
- `features/calendar/hooks/useCalendarLocale.ts`
|
||||
- `features/i18n/*` (si modifié)
|
||||
- `src/frontend/apps/e2e/__tests__/calendar-locale.test.ts`
|
||||
|
||||
**Description PR** : Ajout du support des locales pour le calendrier
|
||||
avec tests e2e.
|
||||
|
||||
---
|
||||
|
||||
### PR 6 : Frontend - Nettoyage code mort
|
||||
**Branche** : `chore/remove-dead-code`
|
||||
|
||||
**Fichiers supprimés :**
|
||||
- `features/ui/components/breadcrumbs/`
|
||||
- `features/ui/components/circular-progress/`
|
||||
- `features/ui/components/infinite-scroll/`
|
||||
- `features/ui/components/info/`
|
||||
- `features/ui/components/responsive/`
|
||||
- `features/forms/components/RhfInput.tsx`
|
||||
- `hooks/useCopyToClipboard.tsx`
|
||||
- `utils/useLayout.tsx`
|
||||
- `features/calendar/components/EventModalDeprecated.tsx`
|
||||
- `features/calendar/components/EventModalAdapter.tsx`
|
||||
- `features/calendar/hooks/useEventModal.tsx`
|
||||
- `features/calendar/hooks/useCreateEventModal.tsx`
|
||||
- `src/frontend/packages/open-calendar/` (package entier)
|
||||
|
||||
**Description PR** : Suppression du code mort et des composants inutilisés.
|
||||
|
||||
---
|
||||
|
||||
## Ordre de merge recommandé
|
||||
|
||||
```
|
||||
1. PR 1 (Backend invitations) - indépendante
|
||||
2. PR 2 (CalDavService) - indépendante
|
||||
3. PR 6 (Dead code) - indépendante
|
||||
4. PR 5 (i18n) - indépendante
|
||||
5. PR 4 (CalendarList) - après PR 6
|
||||
6. PR 3 (Scheduler) - après PR 2, PR 4
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Étapes d'exécution
|
||||
|
||||
Pour chaque PR :
|
||||
|
||||
```bash
|
||||
# 1. Créer la branche depuis main
|
||||
git checkout main
|
||||
git pull origin main
|
||||
git checkout -b <branch-name>
|
||||
|
||||
# 2. Copier les fichiers depuis poc/event-calendar
|
||||
git checkout poc/event-calendar -- <fichiers>
|
||||
|
||||
# 3. Vérifier et commiter
|
||||
git add .
|
||||
git commit -m "..."
|
||||
|
||||
# 4. Pousser et créer la PR
|
||||
git push -u origin <branch-name>
|
||||
gh pr create --title "..." --body "..."
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Fichiers à exclure des PRs
|
||||
|
||||
- `CLAUDE.md` (fichier local)
|
||||
- `IMPLEMENTATION_CHECKLIST.md`, `README_RECURRENCE.md`, etc.
|
||||
(documentation temporaire à supprimer ou consolider)
|
||||
|
||||
---
|
||||
|
||||
## Vérification
|
||||
|
||||
Avant chaque PR :
|
||||
```bash
|
||||
cd src/frontend/apps/calendars
|
||||
yarn tsc --noEmit # Types OK
|
||||
yarn lint # Lint OK
|
||||
yarn test # Tests OK
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Faisabilité
|
||||
|
||||
**Oui, c'est tout à fait possible.** La stratégie `git checkout <branch> -- <files>`
|
||||
permet de récupérer des fichiers spécifiques d'une branche sans perdre
|
||||
l'historique de travail. Chaque PR sera autonome et reviewable indépendamment.
|
||||
195
docs/invitations.md
Normal file
195
docs/invitations.md
Normal file
@@ -0,0 +1,195 @@
|
||||
# Invitations
|
||||
|
||||
How event invitations work end-to-end: creating, sending, responding,
|
||||
updating, and cancelling.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
Frontend (EventModal)
|
||||
→ CalDAV proxy (Django)
|
||||
→ SabreDAV (stores event, detects attendees)
|
||||
→ HttpCallbackIMipPlugin (HTTP POST to Django)
|
||||
→ CalendarInvitationService (sends email)
|
||||
→ Attendee receives email
|
||||
→ RSVP link or iTIP client response
|
||||
→ RSVPView (Django) or CalDAV REPLY
|
||||
→ PARTSTAT updated in event
|
||||
→ Organizer notified
|
||||
```
|
||||
|
||||
## Creating an event with attendees
|
||||
|
||||
1. User adds attendees via `AttendeesSection` in EventModal
|
||||
2. `useEventForm.toIcsEvent()` serializes the event with `ATTENDEE`
|
||||
and `ORGANIZER` properties
|
||||
3. `CalDavService.createEvent()` sends a PUT to CalDAV through the
|
||||
Django proxy
|
||||
4. The proxy (`CalDAVProxyView`) injects an
|
||||
`X-CalDAV-Callback-URL` header pointing back to Django
|
||||
|
||||
The resulting `.ics` contains:
|
||||
|
||||
```ics
|
||||
BEGIN:VEVENT
|
||||
UID:abc-123
|
||||
SUMMARY:Team Meeting
|
||||
DTSTART:20260301T140000Z
|
||||
DTEND:20260301T150000Z
|
||||
ORGANIZER;CN=Alice:mailto:alice@example.com
|
||||
ATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:bob@example.com
|
||||
SEQUENCE:0
|
||||
END:VEVENT
|
||||
```
|
||||
|
||||
## SabreDAV processing
|
||||
|
||||
When SabreDAV receives the event, three plugins run in order:
|
||||
|
||||
1. **CalendarSanitizerPlugin** (priority 85) — strips inline binary
|
||||
attachments (Outlook signatures), truncates oversized fields,
|
||||
enforces max resource size (1 MB default)
|
||||
2. **AttendeeNormalizerPlugin** (priority 90) — lowercases emails,
|
||||
deduplicates attendees keeping the highest-priority PARTSTAT
|
||||
(ACCEPTED > TENTATIVE > DECLINED > NEEDS-ACTION)
|
||||
3. **iMip scheduling** — detects attendees and creates a REQUEST
|
||||
message for each one
|
||||
|
||||
The scheduling message is routed by **HttpCallbackIMipPlugin**, which
|
||||
POSTs to Django:
|
||||
|
||||
```
|
||||
POST /api/v1.0/caldav-scheduling-callback/
|
||||
X-Api-Key: <shared secret>
|
||||
X-CalDAV-Sender: alice@example.com
|
||||
X-CalDAV-Recipient: bob@example.com
|
||||
X-CalDAV-Method: REQUEST
|
||||
Content-Type: text/calendar
|
||||
|
||||
<serialized VCALENDAR>
|
||||
```
|
||||
|
||||
## Sending invitation emails
|
||||
|
||||
`CalDAVSchedulingCallbackView` receives the callback and delegates to
|
||||
`CalendarInvitationService.send_invitation()`.
|
||||
|
||||
Steps:
|
||||
|
||||
1. **Parse** — `ICalendarParser.parse()` extracts UID, summary,
|
||||
dates, organizer, attendee, location, description, sequence number
|
||||
2. **Template selection** based on method and sequence:
|
||||
| Method | Sequence | Template |
|
||||
|--------|----------|----------|
|
||||
| REQUEST | 0 | `calendar_invitation.html` |
|
||||
| REQUEST | >0 | `calendar_invitation_update.html` |
|
||||
| CANCEL | any | `calendar_invitation_cancel.html` |
|
||||
| REPLY | any | `calendar_invitation_reply.html` |
|
||||
3. **RSVP tokens** — for REQUEST emails, generates signed URLs:
|
||||
```
|
||||
/rsvp/?token=<signed>&action=accepted
|
||||
/rsvp/?token=<signed>&action=tentative
|
||||
/rsvp/?token=<signed>&action=declined
|
||||
```
|
||||
Tokens are signed with `django.core.signing.Signer(salt="rsvp")`
|
||||
and contain `{uid, email, organizer}`.
|
||||
4. **ICS attachment** — if `CALENDAR_ITIP_ENABLED=True`, the
|
||||
attachment includes `METHOD:REQUEST` for iTIP-aware clients
|
||||
(Outlook, Apple Mail). If False (default), the METHOD is stripped
|
||||
and web RSVP links are used instead.
|
||||
5. **Send** — multipart email with HTML + plain text + ICS attachment.
|
||||
Reply-To is set to the organizer's email.
|
||||
|
||||
## Responding to invitations
|
||||
|
||||
Two paths:
|
||||
|
||||
### Web RSVP (default)
|
||||
|
||||
Attendee clicks Accept / Maybe / Decline link in the email.
|
||||
|
||||
`RSVPView` handles `GET /rsvp/?token=...&action=accepted`:
|
||||
|
||||
1. Unsigns the token (salt="rsvp")
|
||||
2. Finds the event in the organizer's CalDAV calendar by UID
|
||||
3. Checks the event is not in the past (recurring events are never
|
||||
considered past)
|
||||
4. Updates the attendee's `PARTSTAT` to ACCEPTED / TENTATIVE / DECLINED
|
||||
5. PUTs the updated event back to CalDAV
|
||||
6. Renders a confirmation page
|
||||
|
||||
The PUT triggers SabreDAV to generate a REPLY message, which flows
|
||||
back through HttpCallbackIMipPlugin → Django → organizer email.
|
||||
|
||||
### iTIP client response
|
||||
|
||||
When `CALENDAR_ITIP_ENABLED=True`, email clients like Outlook or
|
||||
Apple Calendar show native Accept/Decline buttons. The client sends
|
||||
an iTIP REPLY directly to the CalDAV server, which triggers the same
|
||||
callback flow.
|
||||
|
||||
## Updating an event
|
||||
|
||||
When an event with attendees is modified:
|
||||
|
||||
1. `CalDavService.updateEvent()` increments the `SEQUENCE` number
|
||||
2. SabreDAV detects the change and creates REQUEST messages with the
|
||||
updated sequence
|
||||
3. Attendees receive an update email
|
||||
(`calendar_invitation_update.html`)
|
||||
|
||||
## Cancelling an event
|
||||
|
||||
When an event with attendees is deleted:
|
||||
|
||||
1. SabreDAV creates CANCEL messages for each attendee
|
||||
2. Attendees receive a cancellation email
|
||||
(`calendar_invitation_cancel.html`)
|
||||
|
||||
## Configuration
|
||||
|
||||
| Setting | Default | Description |
|
||||
|---------|---------|-------------|
|
||||
| `CALDAV_URL` | `http://caldav:80` | Internal CalDAV server URL |
|
||||
| `CALDAV_INBOUND_API_KEY` | None | API key for callbacks from CalDAV |
|
||||
| `CALDAV_OUTBOUND_API_KEY` | None | API key for requests to CalDAV |
|
||||
| `CALDAV_CALLBACK_BASE_URL` | None | Internal URL for CalDAV→Django (Docker: `http://backend:8000`) |
|
||||
| `CALENDAR_ITIP_ENABLED` | False | Use iTIP METHOD headers in ICS attachments |
|
||||
| `CALENDAR_INVITATION_FROM_EMAIL` | `DEFAULT_FROM_EMAIL` | Sender address for invitation emails |
|
||||
| `APP_URL` | `""` | Base URL for RSVP links in emails |
|
||||
|
||||
## Key files
|
||||
|
||||
| Area | Path |
|
||||
|------|------|
|
||||
| Attendee UI | `src/frontend/.../event-modal-sections/AttendeesSection.tsx` |
|
||||
| Event form | `src/frontend/.../scheduler/hooks/useEventForm.ts` |
|
||||
| CalDAV client | `src/frontend/.../services/dav/CalDavService.ts` |
|
||||
| CalDAV proxy | `src/backend/core/api/viewsets_caldav.py` |
|
||||
| Scheduling callback | `src/backend/core/api/viewsets_caldav.py` (`CalDAVSchedulingCallbackView`) |
|
||||
| RSVP handler | `src/backend/core/api/viewsets_rsvp.py` |
|
||||
| Email service | `src/backend/core/services/calendar_invitation_service.py` |
|
||||
| ICS parser | `src/backend/core/services/calendar_invitation_service.py` (`ICalendarParser`) |
|
||||
| Email templates | `src/backend/core/templates/emails/calendar_invitation*.html` |
|
||||
| SabreDAV sanitizer | `docker/sabredav/src/CalendarSanitizerPlugin.php` |
|
||||
| SabreDAV attendee dedup | `docker/sabredav/src/AttendeeNormalizerPlugin.php` |
|
||||
| SabreDAV callback plugin | `docker/sabredav/src/HttpCallbackIMipPlugin.php` |
|
||||
|
||||
## Future: Messages mail client integration
|
||||
|
||||
La Suite includes a Messages mail client (based on an open-source
|
||||
webmail). Future integration would allow:
|
||||
|
||||
- **Inline RSVP** — render Accept/Decline buttons directly in the
|
||||
Messages UI when an email contains a `text/calendar` attachment with
|
||||
`METHOD:REQUEST`
|
||||
- **Calendar preview** — show event details (date, time, location)
|
||||
extracted from the ICS attachment without opening the full calendar
|
||||
- **Auto-add to calendar** — accepted events automatically appear in
|
||||
the user's Calendars calendar via a shared CalDAV backend
|
||||
- **Status sync** — PARTSTAT changes in Messages propagate to
|
||||
Calendars and vice versa
|
||||
|
||||
This requires Messages to support iTIP processing
|
||||
(`CALENDAR_ITIP_ENABLED=True`) and share the same CalDAV/auth
|
||||
infrastructure.
|
||||
107
docs/recurrence.md
Normal file
107
docs/recurrence.md
Normal file
@@ -0,0 +1,107 @@
|
||||
# Recurring Events
|
||||
|
||||
Recurring events follow the iCalendar RFC 5545 RRULE standard. No backend
|
||||
changes are needed — CalDAV (SabreDAV) handles recurrence natively.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
RecurrenceEditor (React)
|
||||
-> IcsRecurrenceRule (ts-ics)
|
||||
-> RRULE string (RFC 5545)
|
||||
-> .ics file (CalDAV)
|
||||
-> SabreDAV server
|
||||
```
|
||||
|
||||
## RecurrenceEditor Component
|
||||
|
||||
Located at
|
||||
`src/frontend/apps/calendars/src/features/calendar/components/RecurrenceEditor.tsx`
|
||||
|
||||
```tsx
|
||||
import { RecurrenceEditor } from '@/features/calendar/components/RecurrenceEditor';
|
||||
|
||||
const [recurrence, setRecurrence] = useState<IcsRecurrenceRule>();
|
||||
|
||||
<RecurrenceEditor value={recurrence} onChange={setRecurrence} />
|
||||
```
|
||||
|
||||
Include in the event object:
|
||||
|
||||
```typescript
|
||||
const event: IcsEvent = {
|
||||
// ...other fields
|
||||
recurrenceRule: recurrence,
|
||||
};
|
||||
```
|
||||
|
||||
### Supported patterns
|
||||
|
||||
| Pattern | RRULE |
|
||||
|---------|-------|
|
||||
| Every day | `FREQ=DAILY` |
|
||||
| Every 3 days | `FREQ=DAILY;INTERVAL=3` |
|
||||
| Every Monday | `FREQ=WEEKLY;BYDAY=MO` |
|
||||
| Mon/Wed/Fri | `FREQ=WEEKLY;BYDAY=MO,WE,FR` |
|
||||
| Every 2 weeks on Thu | `FREQ=WEEKLY;INTERVAL=2;BYDAY=TH` |
|
||||
| 15th of each month | `FREQ=MONTHLY;BYMONTHDAY=15` |
|
||||
| March 15 yearly | `FREQ=YEARLY;BYMONTH=3;BYMONTHDAY=15` |
|
||||
| 10 occurrences | append `;COUNT=10` |
|
||||
| Until a date | append `;UNTIL=20251231T235959Z` |
|
||||
|
||||
### Not yet supported
|
||||
|
||||
- `BYSETPOS` (e.g. "1st Monday of month", "last Friday")
|
||||
- Edit single instance vs series (needs RECURRENCE-ID UI)
|
||||
- Visual preview of recurrence pattern
|
||||
|
||||
### Date validation
|
||||
|
||||
The component warns about edge cases:
|
||||
- Feb 30/31 — "February has at most 29 days"
|
||||
- Feb 29 — "Only exists in leap years"
|
||||
- Day 31 on 30-day months — shown as warning
|
||||
|
||||
### Translations
|
||||
|
||||
Supported: English, French, Dutch. Keys are in
|
||||
`src/frontend/apps/calendars/src/features/i18n/translations.json`
|
||||
under `calendar.recurrence.*`.
|
||||
|
||||
## IcsRecurrenceRule interface (ts-ics)
|
||||
|
||||
```typescript
|
||||
interface IcsRecurrenceRule {
|
||||
frequency: 'DAILY' | 'WEEKLY' | 'MONTHLY' | 'YEARLY';
|
||||
interval?: number;
|
||||
count?: number;
|
||||
until?: IcsDateObject;
|
||||
byDay?: { day: 'MO'|'TU'|'WE'|'TH'|'FR'|'SA'|'SU' }[];
|
||||
byMonthDay?: number[];
|
||||
byMonth?: number[];
|
||||
}
|
||||
```
|
||||
|
||||
## How CalDAV handles it
|
||||
|
||||
1. RRULE is stored as a property in the VEVENT inside the `.ics` file
|
||||
2. SabreDAV expands recurring instances when clients query date ranges
|
||||
3. Individual instance modifications use RECURRENCE-ID (not yet in UI)
|
||||
|
||||
Example `.ics`:
|
||||
|
||||
```ics
|
||||
BEGIN:VEVENT
|
||||
UID:abc-123
|
||||
SUMMARY:Weekly Team Meeting
|
||||
DTSTART:20260125T140000Z
|
||||
RRULE:FREQ=WEEKLY;BYDAY=MO;UNTIL=20261231T235959Z
|
||||
END:VEVENT
|
||||
```
|
||||
|
||||
## References
|
||||
|
||||
- [RFC 5545 — iCalendar](https://datatracker.ietf.org/doc/html/rfc5545)
|
||||
- [RRULE spec](https://icalendar.org/iCalendar-RFC-5545/3-8-5-3-recurrence-rule.html)
|
||||
- [ts-ics](https://github.com/Neuvernetzung/ts-ics)
|
||||
- [SabreDAV](https://sabre.io/dav/)
|
||||
Reference in New Issue
Block a user