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