feat(calendar): Calendar-Integration Read (Phase 6 von #360) #370

Closed
opened 2026-05-20 22:50:56 +02:00 by admin-mrrm · 0 comments
Owner

Ziel

Phase 6 von Epic #360. Calendar-Integration (Read-Only): externe Kalender als blocked-Slots in den Day-View einbinden. Der Planner soll wissen, wann der User schon belegt ist, damit Phase 7 (Planner v1) reale Slots vergeben kann.

v0/v1-Scope ist bewusst Read-Only: ICS-URL-Abos (Outlook/Google/Apple liefern ICS-Exports), kein OAuth, kein Schreiben.

Scope

Schema

calendar_subscriptions:

  • id, ownerSub, name, url (ICS), lastSyncAt, createdAt

calendar_events:

  • id, ownerSub, subscriptionId, uid (ICS UID), title, startsAt, endsAt, allDay, location, createdAt, updatedAt
  • UNIQUE(subscriptionId, uid) für Idempotenz

Service

CalendarSyncService.syncOne(subscriptionId):

  • Fetched ICS-URL
  • Parsed Events (nur VEVENT, kein VTODO)
  • Upsert nach UID
  • Setzt subscription.lastSyncAt

CalendarSyncService.syncAll(): iteriert alle Subscriptions; wird per Cron getriggert (Default alle 15 Min, via CALENDAR_SYNC_CRON + CALENDAR_SYNC_DISABLED Env).

CalendarService.eventsForRange(ownerSub, from, to): liefert die Events im Zeitfenster, sortiert nach startsAt.

API

  • POST /calendar/subscriptions, GET /calendar/subscriptions, PATCH /calendar/subscriptions/:id, DELETE /calendar/subscriptions/:id
  • GET /calendar/events?from=ISO&to=ISO — Events im Range

Day-View-Integration

GET /planner/today liefert zusätzlich zu items (Candidates) auch events (Calendar-Events des Tages). Format:

{
  date: 'YYYY-MM-DD',
  items: [...],   // Candidates wie bisher
  events: [
    { id, title, startsAt, endsAt, allDay, location }
  ]
}

Tests

  • Unit CalendarSyncService: ICS-Parsing (Fixture-Datei), Upsert-Idempotenz, lastSyncAt-Update
  • Unit CalendarService.eventsForRange: Range-Filter
  • Integration: Subscription anlegen → Sync triggern (mit Stub-ICS) → GET /calendar/events liefert geparste Events; GET /planner/today liefert events neben items

Out of scope (folgt später)

  • OAuth-Flows (Outlook/Google) → eigene Phase
  • Bidirektionales Schreiben (Plan-Slots als CalEvents) → Out-of-scope von Epic #360
  • Recurring Events (RRULE) — v1 expandiert nur VEVENT mit DTSTART/DTEND, kein Master-Override-Handling
  • Planner-Conflict-Detection — kommt in Phase 7 (Planner v1)

Bezug

  • Epic: #360
  • Phase 5 (Planner v0): #368
  • Blockt: Phase 7 (Planner v1)
## Ziel Phase 6 von Epic #360. **Calendar-Integration (Read-Only):** externe Kalender als blocked-Slots in den Day-View einbinden. Der Planner soll wissen, *wann der User schon belegt ist*, damit Phase 7 (Planner v1) reale Slots vergeben kann. v0/v1-Scope ist bewusst **Read-Only**: ICS-URL-Abos (Outlook/Google/Apple liefern ICS-Exports), kein OAuth, kein Schreiben. ## Scope ### Schema `calendar_subscriptions`: - `id`, `ownerSub`, `name`, `url` (ICS), `lastSyncAt`, `createdAt` `calendar_events`: - `id`, `ownerSub`, `subscriptionId`, `uid` (ICS UID), `title`, `startsAt`, `endsAt`, `allDay`, `location`, `createdAt`, `updatedAt` - UNIQUE(subscriptionId, uid) für Idempotenz ### Service `CalendarSyncService.syncOne(subscriptionId)`: - Fetched ICS-URL - Parsed Events (nur VEVENT, kein VTODO) - Upsert nach UID - Setzt `subscription.lastSyncAt` `CalendarSyncService.syncAll()`: iteriert alle Subscriptions; wird per Cron getriggert (Default alle 15 Min, via `CALENDAR_SYNC_CRON` + `CALENDAR_SYNC_DISABLED` Env). `CalendarService.eventsForRange(ownerSub, from, to)`: liefert die Events im Zeitfenster, sortiert nach `startsAt`. ### API - `POST /calendar/subscriptions`, `GET /calendar/subscriptions`, `PATCH /calendar/subscriptions/:id`, `DELETE /calendar/subscriptions/:id` - `GET /calendar/events?from=ISO&to=ISO` — Events im Range ### Day-View-Integration `GET /planner/today` liefert zusätzlich zu `items` (Candidates) auch `events` (Calendar-Events des Tages). Format: ```ts { date: 'YYYY-MM-DD', items: [...], // Candidates wie bisher events: [ { id, title, startsAt, endsAt, allDay, location } ] } ``` ### Tests - Unit `CalendarSyncService`: ICS-Parsing (Fixture-Datei), Upsert-Idempotenz, lastSyncAt-Update - Unit `CalendarService.eventsForRange`: Range-Filter - Integration: Subscription anlegen → Sync triggern (mit Stub-ICS) → GET /calendar/events liefert geparste Events; GET /planner/today liefert events neben items ## Out of scope (folgt später) - OAuth-Flows (Outlook/Google) → eigene Phase - Bidirektionales Schreiben (Plan-Slots als CalEvents) → Out-of-scope von Epic #360 - Recurring Events (RRULE) — v1 expandiert nur VEVENT mit DTSTART/DTEND, kein Master-Override-Handling - Planner-Conflict-Detection — kommt in Phase 7 (Planner v1) ## Bezug - Epic: #360 - Phase 5 (Planner v0): #368 ✅ - Blockt: Phase 7 (Planner v1)
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
admin-mrrm/mrrmlabapp#370
No description provided.