feat: Google Calendar sync for single-user self-hosting #123

Closed
opened 2026-03-27 23:02:50 +00:00 by barrettruth · 2 comments
barrettruth commented 2026-03-27 23:02:50 +00:00

Tracker

Google Calendar sync is still the root feature, but the implementation direction has changed from the original bidirectional March sketch. The v1 stack is now a pull-only, read-only Google sync model shared by Google Tasks and Google Calendar.

This tracker supersedes the earlier bidirectional wording on this issue. Keep the old comments as historical context only; do not implement Calendar push/write sync in this stack.

Topological implementation stack

  1. #444 - shared sync-source state and provider-agnostic read-only imported-task guard.
  2. #445 - migrate Google Tasks onto the multi-sync/read-only model first.
  3. #446 - add Google Calendar source discovery and selection.
  4. #447 - map Google Calendar event payloads to read-only Delta task rows and metadata.
  5. #448 - implement manual Google Calendar pull sync.
  6. #449 - surface imported-source indicators and read-only actions across UI surfaces.
  7. #450 - harden tests, fixtures, docs, and operator smoke coverage.

Post-v1 / deliberately deferred tracker: #451.

Core product decisions

  • Google Calendar v1 is pull-only.
  • Google Tasks remains pull-only and should be refactored into the same read-only model before Calendar pull lands.
  • Imported Google Tasks and Google Calendar events become normal Delta task rows, but read-only to the user.
  • Imported Google rows appear everywhere normal tasks appear.
  • Read-only enforcement must live at the core mutation layer, not only frontend controls.
  • Only provider sync engines may mutate imported task fields.
  • Attempted user edits should show a warning/yellow status message using existing warning palette/tokens.
  • Use generic source indicators rather than provider-specific one-off UI.
  • Task detail can show fuller source/attribute information; dense views use compact indicators decided through visual iteration.
  • Google Calendar imported rows should expose open in Google Calendar where task details/actions are available.
  • Disconnecting Google hard-removes imported Google Tasks/Calendar rows, external links, and sync source state by default. The UI copy must make that destructive behavior explicit.

Sync/storage decisions

  • Keep Google account OAuth/tokens in the existing encrypted integration config path from #290.
  • Add durable per-source sync-state storage instead of growing the shared Google metadata blob.
  • Source state should support Google Task lists, Google calendars, and future CalDAV/Apple-style sources.
  • Product routes can stay explicit, e.g. Google Tasks pull and Google Calendar pull/source routes, while internal sync primitives should be modular/provider-agnostic.
  • Use simple read-only result summaries: seen, created, updated, cancelled, skipped, duplicate skipped, and errors.
  • Google Calendar v1 is manual-only, matching current Google Tasks. Scheduled/background sync is required later but not part of this stack.
  • On Calendar 410 Gone, clear the affected calendar sync token, full-resync during the same manual pull, and report that full resync happened.

Calendar source selection decisions

  • Calendar selection is configurable.
  • Default to preselecting all visible Google calendars.
  • Show hidden calendars in the same list, unchecked, with a compact [hidden] attribute.
  • Include calendars with event-detail access, including read-only calendars.
  • Exclude freeBusyReader calendars because they do not expose event details.
  • Map each selected Google calendar to a Delta category using the Google calendar name.
  • Seed a category color from Google only when the local category has no existing Delta color.
  • Do not add a large knob surface.

Calendar import decisions

  • Import all selected-calendar history; no initial time-window setting and no pruning policy in v1.
  • Import all Google event types by default; do not filter eventTypes.
  • Tentative canonical external id: {calendarId}:{event.id}.
  • Store calendarId, eventId, iCalUID, etag, updated, sequence, eventType, status, visibility, transparency, calendar/source attributes, and raw useful payload in metadata.
  • Skip likely .ics duplicates by matching iCalUID; count them as duplicate skipped. Do not auto-link and do not build a conflict workflow for this edge case in v1.
  • Preserve Google all-day exclusive end-date semantics to avoid off-by-one bugs.
  • Port effective timezone into task fields where it affects display/recurrence, and preserve original Google time payloads in metadata.
  • Best-effort convert supported Google description formatting into Tiptap JSON; preserve the raw description in metadata.
  • Map Meet/conference links into meetingUrl.
  • Preserve conference, attendee, attachment, organizer/creator, reminder, source, and raw provider payloads in metadata for later features.
  • Hidden-detail private events import as read-only tasks titled private event with [private] source metadata.
  • Transparent/free events import and preserve transparency; Calendar surfaces should render them more quietly later instead of hiding them.

Recurrence decisions

  • Google recurring masters and exceptions map into Delta's existing RRULE/master-exception model.
  • Cancelled recurring instances become exdates on the master.
  • If a materialized exception row already exists for a cancelled occurrence, suppress/cancel it so it does not render.
  • Completion-based Delta recurrence is not exported to Google because v1 has no Google writes.

Explicit non-goals for this stack

  • No Google Calendar writes, push sync, attendee invitations, or delete/update calls.
  • No bidirectional conflict-resolution workflow.
  • No scheduled/background sync in v1.
  • No attachment UI, attendee UI, reminder UI, raw JSON editor, free/busy native rendering, or final icon perfection in this stack.
  • No broad configurable sync-timing knobs in this stack.

Deferred design tracker

#451 tracks the post-Google-sync backlog and open questions, including attachments, link sharing, raw provider JSON view/editing, native free/busy rendering, final icon/source-indicator polish, scheduled sync, merge/conflict policy, recurring UI sync semantics, auto-sync, and whether configurable sync times should exist.

Acceptance criteria for this tracker

  • #444 through #450 land in topological order unless a later issue is deliberately split further.
  • Google Tasks and Google Calendar share the read-only imported-task model.
  • A self-hosted owner can connect Google, configure Calendar sources, manually pull Google Tasks/Calendar, see imported rows everywhere, and get clear read-only feedback on attempted edits.
  • Docs describe the operator setup, scopes, source selection, manual sync behavior, destructive disconnect cleanup, preserved metadata, and v1 non-goals.
  • Tests/fixtures cover the edge cases listed in #450.
## Tracker Google Calendar sync is still the root feature, but the implementation direction has changed from the original bidirectional March sketch. The v1 stack is now a pull-only, read-only Google sync model shared by Google Tasks and Google Calendar. This tracker supersedes the earlier bidirectional wording on this issue. Keep the old comments as historical context only; do not implement Calendar push/write sync in this stack. ## Topological implementation stack 1. #444 - shared sync-source state and provider-agnostic read-only imported-task guard. 2. #445 - migrate Google Tasks onto the multi-sync/read-only model first. 3. #446 - add Google Calendar source discovery and selection. 4. #447 - map Google Calendar event payloads to read-only Delta task rows and metadata. 5. #448 - implement manual Google Calendar pull sync. 6. #449 - surface imported-source indicators and read-only actions across UI surfaces. 7. #450 - harden tests, fixtures, docs, and operator smoke coverage. Post-v1 / deliberately deferred tracker: #451. ## Core product decisions - Google Calendar v1 is pull-only. - Google Tasks remains pull-only and should be refactored into the same read-only model before Calendar pull lands. - Imported Google Tasks and Google Calendar events become normal Delta task rows, but read-only to the user. - Imported Google rows appear everywhere normal tasks appear. - Read-only enforcement must live at the core mutation layer, not only frontend controls. - Only provider sync engines may mutate imported task fields. - Attempted user edits should show a warning/yellow status message using existing warning palette/tokens. - Use generic source indicators rather than provider-specific one-off UI. - Task detail can show fuller source/attribute information; dense views use compact indicators decided through visual iteration. - Google Calendar imported rows should expose `open in Google Calendar` where task details/actions are available. - Disconnecting Google hard-removes imported Google Tasks/Calendar rows, external links, and sync source state by default. The UI copy must make that destructive behavior explicit. ## Sync/storage decisions - Keep Google account OAuth/tokens in the existing encrypted integration config path from #290. - Add durable per-source sync-state storage instead of growing the shared Google metadata blob. - Source state should support Google Task lists, Google calendars, and future CalDAV/Apple-style sources. - Product routes can stay explicit, e.g. Google Tasks pull and Google Calendar pull/source routes, while internal sync primitives should be modular/provider-agnostic. - Use simple read-only result summaries: `seen`, `created`, `updated`, `cancelled`, `skipped`, `duplicate skipped`, and `errors`. - Google Calendar v1 is manual-only, matching current Google Tasks. Scheduled/background sync is required later but not part of this stack. - On Calendar `410 Gone`, clear the affected calendar sync token, full-resync during the same manual pull, and report that full resync happened. ## Calendar source selection decisions - Calendar selection is configurable. - Default to preselecting all visible Google calendars. - Show hidden calendars in the same list, unchecked, with a compact `[hidden]` attribute. - Include calendars with event-detail access, including read-only calendars. - Exclude `freeBusyReader` calendars because they do not expose event details. - Map each selected Google calendar to a Delta category using the Google calendar name. - Seed a category color from Google only when the local category has no existing Delta color. - Do not add a large knob surface. ## Calendar import decisions - Import all selected-calendar history; no initial time-window setting and no pruning policy in v1. - Import all Google event types by default; do not filter `eventTypes`. - Tentative canonical external id: `{calendarId}:{event.id}`. - Store `calendarId`, `eventId`, `iCalUID`, `etag`, `updated`, `sequence`, `eventType`, status, visibility, transparency, calendar/source attributes, and raw useful payload in metadata. - Skip likely `.ics` duplicates by matching `iCalUID`; count them as `duplicate skipped`. Do not auto-link and do not build a conflict workflow for this edge case in v1. - Preserve Google all-day exclusive end-date semantics to avoid off-by-one bugs. - Port effective timezone into task fields where it affects display/recurrence, and preserve original Google time payloads in metadata. - Best-effort convert supported Google description formatting into Tiptap JSON; preserve the raw description in metadata. - Map Meet/conference links into `meetingUrl`. - Preserve conference, attendee, attachment, organizer/creator, reminder, source, and raw provider payloads in metadata for later features. - Hidden-detail private events import as read-only tasks titled `private event` with `[private]` source metadata. - Transparent/free events import and preserve `transparency`; Calendar surfaces should render them more quietly later instead of hiding them. ## Recurrence decisions - Google recurring masters and exceptions map into Delta's existing RRULE/master-exception model. - Cancelled recurring instances become `exdates` on the master. - If a materialized exception row already exists for a cancelled occurrence, suppress/cancel it so it does not render. - Completion-based Delta recurrence is not exported to Google because v1 has no Google writes. ## Explicit non-goals for this stack - No Google Calendar writes, push sync, attendee invitations, or delete/update calls. - No bidirectional conflict-resolution workflow. - No scheduled/background sync in v1. - No attachment UI, attendee UI, reminder UI, raw JSON editor, free/busy native rendering, or final icon perfection in this stack. - No broad configurable sync-timing knobs in this stack. ## Deferred design tracker #451 tracks the post-Google-sync backlog and open questions, including attachments, link sharing, raw provider JSON view/editing, native free/busy rendering, final icon/source-indicator polish, scheduled sync, merge/conflict policy, recurring UI sync semantics, auto-sync, and whether configurable sync times should exist. ## Acceptance criteria for this tracker - #444 through #450 land in topological order unless a later issue is deliberately split further. - Google Tasks and Google Calendar share the read-only imported-task model. - A self-hosted owner can connect Google, configure Calendar sources, manually pull Google Tasks/Calendar, see imported rows everywhere, and get clear read-only feedback on attempted edits. - Docs describe the operator setup, scopes, source selection, manual sync behavior, destructive disconnect cleanup, preserved metadata, and v1 non-goals. - Tests/fixtures cover the edge cases listed in #450.
barrettruth commented 2026-03-29 17:44:27 +00:00

Status update: sync engine and mapper are built, not yet wired to UI.

Done:

  • google-calendar.ts: REST API client (list/create/update/delete events, list/create calendars)
  • google-calendar-mapper.ts: bidirectional field mapping (Google Event <-> Delta Task), 36 tests passing
  • google-calendar-sync.ts: pull phase (Google->Delta), push phase (Delta->Google), field-level merge, sync token management, auto-creates "delta" calendar
  • OAuth callback updated to handle incremental calendar.events scope
  • Google Calendar integration row in settings (connect via OAuth)

Remaining:

  • Wire sync engine to automation recipe / cron trigger
  • UI for sync status in status bar
  • Conflict resolution UX (currently auto-merges by field timestamp)
Status update: sync engine and mapper are built, not yet wired to UI. **Done:** - `google-calendar.ts`: REST API client (list/create/update/delete events, list/create calendars) - `google-calendar-mapper.ts`: bidirectional field mapping (Google Event <-> Delta Task), 36 tests passing - `google-calendar-sync.ts`: pull phase (Google->Delta), push phase (Delta->Google), field-level merge, sync token management, auto-creates "delta" calendar - OAuth callback updated to handle incremental `calendar.events` scope - Google Calendar integration row in settings (connect via OAuth) **Remaining:** - Wire sync engine to automation recipe / cron trigger - UI for sync status in status bar - Conflict resolution UX (currently auto-merges by field timestamp)
barrettruth added this to the v0.1.0 milestone 2026-05-10 20:16:30 +00:00
barrettruth changed title from feat: Google Calendar bidirectional sync to feat: Google Calendar sync for single-user self-hosting 2026-05-11 18:30:06 +00:00
Owner

Completed by the merged Google read-only sync stack: #454 through #460 landed #444 through #450 in order, with Google Tasks and Google Calendar sharing sync_sources, read-only mutation guards, manual Calendar source/pull flows, UI source indicators, docs, tests, and the operator smoke path. #451 remains the intentionally deferred post-v1 backlog.

Completed by the merged Google read-only sync stack: #454 through #460 landed #444 through #450 in order, with Google Tasks and Google Calendar sharing sync_sources, read-only mutation guards, manual Calendar source/pull flows, UI source indicators, docs, tests, and the operator smoke path. #451 remains the intentionally deferred post-v1 backlog.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
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
barrettruth/delta#123
No description provided.