macsync/docs/modules/ical.md

93 lines
3.6 KiB
Markdown
Raw Normal View History

# iCal (`ICalSync`)
## Purpose
Sync the user's calendars and events with the server, and apply web-initiated
event edits back into Calendar.app.
## Direction
Bidirectional. Inbound: EventKit -> server. Outbound:
`icloud.calendar_send_queue` -> EventKit save/remove.
## OS surface
EventKit (`EKEventStore`). `Package.swift:73-77` adds
`linkedFramework("EventKit")`. Requires Calendars (full access) — module
surfaces `.calendarAccessRequired` if denied
(`@packages/ical/Sources/ICalSync/SyncManager.swift:103-105`).
## Files
- `Reader.swift``CalendarReader.shared`; owns the shared `EKEventStore`
(consumed by the Sender at `SyncManager.swift:57`).
- `APIClient.swift`:
- `syncCalendars(_:)` -> `POST /client/ical/calendars` (line 108)
- `syncEvents(_:)` -> `POST /client/ical/events` (line 122)
- `getPendingSends()` -> `GET /client/ical/send-queue/pending` (line 137)
- `reportSendResult(...)` -> `POST /client/ical/send-queue/:id/result` (line 165)
- `getStats()` -> `GET /client/ical/stats` (line 173)
- `Sender.swift``CalendarSender(eventStore:)`; the `apply(item)` closure is
`SyncManager.swift:62-64`.
- `SyncManager.swift` — overrides `didStartSync` / `willStopSync` to start /
stop the send queue (`SyncManager.swift:67-73`). Authorization hooks (lines
97-106) gate cycles. Event batch size 200 (line 52).
## Timing
- Read interval: **300s** (`@packages/ical/Sources/ICalSync/SyncManager.swift:80`).
- Outbound poll interval: **60s** (`@packages/ical/Sources/ICalSync/SyncManager.swift:61`).
- Event batch size: 200.
## Server surface
- Entity tables: `icloud.calendars`, `icloud.events`,
`icloud.calendar_send_queue` (`app/server.ts:44-45, 51`).
- Allowed send-queue actions: `create_event`, `update_event`, `delete_event`
(`src/server/src/entities/calendarSendItem/types.ts:24`).
- Client routes (`src/server/src/surfaces/client/ical.ts`):
- `GET /client/ical/stats` (line 55)
- `POST /client/ical/calendars` (line 59)
- `POST /client/ical/events` (line 65)
- `GET /client/ical/send-queue/pending` (line 71)
- `POST /client/ical/send-queue/:id/result` (line 89)
- Web routes (`src/server/src/surfaces/my/calendar.ts`):
- `GET /my/calendar/stats` (line 46)
- `GET /my/calendar/calendars` (line 49)
- `GET /my/calendar/events` (line 57)
- `POST /my/calendar/events` (line 80)
- `PUT /my/calendar/events/:id` (line 90)
- `DELETE /my/calendar/events/:id` (line 102)
- Admin enqueue: `POST /admin/calendar-send-queue/enqueue`
(`src/server/src/surfaces/admin/calendar-send-queue.ts:16`).
## Web surface
- Tab: `/calendar` (`web/src/App.tsx:60`).
- API helpers: `web/src/api/calendar.ts`.
- UI is intentionally minimal (list + form) per
`~/.claude/plans/magical-tumbling-peach.md:180`.
## EventKit snippets
Reader fetches via `EKEventStore.events(matching:)` over a date predicate;
writer saves with `EKEventStore.save(_:span:)` /
`EKEventStore.remove(_:span:)`. The Sender layer maps the JSON payload's
recurrence/alarm fields onto `EKRecurrenceRule` / `EKAlarm`.
## Known limitations
- Last-write-wins on simultaneous edits (see
[known-limitations](../known-limitations.md#conflict-resolution-last-write-wins)).
- Calendar source (iCloud / on-device / Exchange) is captured but the
web UI does not let the user pick which source to create new events on; it
uses the default.
## Tests (`@packages/ical/Tests/ICalSyncTests/`)
- `ICalSyncTests.swift` — Codable round-trips, payload shape (hermetic).
- `SenderTests.swift``CalendarSender.apply` dispatch logic against a fake
EventKit interface (hermetic).
Not covered: real EventKit save/remove (needs the OS), authorization prompts.