# 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.