[Scheduling] Two-way calendar sync (iCal feed + Google Calendar) #107

Closed
opened 2026-07-14 19:50:01 +00:00 by claude-bot · 0 comments
Contributor

Context / Motivation

Calendar export today is minimal and one-shot:

  • Backend: GET /sessions/{session_id}/calendar.ics (download_ics, webapp/backend/app/routers/sessions.py:600, helper _build_ics at :569) — a per-session, member-authenticated, one-shot download of a single VEVENT (end defaults to start + 4h). Not tokenized, not a subscription feed.
  • Frontend: a client-side Google Calendar template link (googleCalendarUrl, webapp/frontend/src/pages/SessionDetail.jsx:66-76).

Neither updates when a session is rescheduled or cancelled. Users of competing schedulers cite two-way calendar sync as the top switching blocker.

Spec

Phase A — subscribable per-user iCal feed (read-only sync; shippable alone)

  • GET /calendar/feed/{token}.ics — unauthenticated-by-token subscription feed (webcal-compatible), covering confirmed sessions across all campaigns the user belongs to.
  • Token: per-user, random, stored hashed or as an opaque column on users; revocable/regenerable from the Profile page (pattern precedent: campaign.analytics_share_token, see webapp/backend/app/schemas/campaign.py:164).
  • Feed honors campaign timezone (campaign.timezone, models/campaign.py:41): emit UTC DTSTART with correct instants; stable UID per session (session UUID) and SEQUENCE/LAST-MODIFIED bumps on reschedule so calendar clients update. Cancelled sessions emit STATUS:CANCELLED (or drop out of the feed).
  • Reuse/extend _build_ics into a proper multi-VEVENT builder; keep the existing per-session download working.
  • Use end_time when set, else the same +4h default.
  • Profile UI: show feed URL, copy button, regenerate (revokes old token).

Phase B — Google Calendar API integration (true push sync; the bulk)

  • Per-user OAuth (incremental consent, calendar.events scope): on session confirm/reschedule/cancel, create/update/delete an event in the user's Google Calendar.
  • Store google_event_id per (user, session); handle token refresh, revocation (Google-side and app-side disconnect), and API errors with retry via a Celery task (follow the task patterns in webapp/backend/app/tasks/reminder_tasks.py).
  • Admin toggle + credential entry (Google Cloud client id/secret) in Admin settings, encrypted at rest like other secrets (_ENCRYPTED_KEYS pattern in webapp/backend/app/services/settings_service.py + AES-256-GCM in app/crypto.py); document the Google Cloud setup for self-hosters.
  • Investigation checklist (do first, may conclude "feed-only is enough"):
    • OAuth consent screen verification requirements for a self-hosted redirect URI (each instance has its own domain — does every self-hoster need their own Google Cloud project? Almost certainly yes; document it.)
    • Quota limits for Calendar API at hobby scale
    • "Unverified app" warning friction for small instances
    • Whether webcal subscription (Phase A) already satisfies most users given Google's feed-refresh latency (~12-24h) is the main drawback
  • Ship Phase B only if the checklist outcome justifies it; otherwise close with a written conclusion and keep Phase A.

Out of scope

  • Reading users' calendars for availability (pairs with the standing-availability issue later).
  • Outlook/CalDAV push integrations (the iCal feed covers them read-only).

Acceptance criteria

  • Phase A: subscribing in Google Calendar / Apple Calendar shows all confirmed sessions; rescheduling updates the event (SEQUENCE bump verified); cancelling removes/cancels it; regenerating the token invalidates the old URL (test 404/403); feed requires no session cookie.
  • Phase B (if pursued): confirm/reschedule/cancel round-trips to Google within one task run; disconnect removes stored tokens; failures retry without duplicate events.

References

  • webapp/backend/app/routers/sessions.py:569 (_build_ics), :600 (download_ics)
  • webapp/frontend/src/pages/SessionDetail.jsx:66-76 (googleCalendarUrl)
  • webapp/backend/app/models/campaign.py:41, models/session.py:89-92
  • webapp/backend/app/services/settings_service.py (_ENCRYPTED_KEYS), webapp/backend/app/crypto.py (AES-256-GCM settings encryption)

Filed from the July 2026 full-project review.

## Context / Motivation Calendar export today is minimal and one-shot: - Backend: `GET /sessions/{session_id}/calendar.ics` (`download_ics`, `webapp/backend/app/routers/sessions.py:600`, helper `_build_ics` at `:569`) — a **per-session, member-authenticated, one-shot download** of a single VEVENT (end defaults to start + 4h). Not tokenized, not a subscription feed. - Frontend: a client-side Google Calendar template link (`googleCalendarUrl`, `webapp/frontend/src/pages/SessionDetail.jsx:66-76`). Neither updates when a session is rescheduled or cancelled. Users of competing schedulers cite two-way calendar sync as the top switching blocker. ## Spec **Phase A — subscribable per-user iCal feed (read-only sync; shippable alone)** - `GET /calendar/feed/{token}.ics` — unauthenticated-by-token subscription feed (webcal-compatible), covering **confirmed** sessions across all campaigns the user belongs to. - Token: per-user, random, stored hashed or as an opaque column on `users`; revocable/regenerable from the Profile page (pattern precedent: `campaign.analytics_share_token`, see `webapp/backend/app/schemas/campaign.py:164`). - Feed honors campaign timezone (`campaign.timezone`, `models/campaign.py:41`): emit UTC `DTSTART` with correct instants; stable `UID` per session (session UUID) and `SEQUENCE`/`LAST-MODIFIED` bumps on reschedule so calendar clients update. Cancelled sessions emit `STATUS:CANCELLED` (or drop out of the feed). - Reuse/extend `_build_ics` into a proper multi-VEVENT builder; keep the existing per-session download working. - Use `end_time` when set, else the same +4h default. - Profile UI: show feed URL, copy button, regenerate (revokes old token). **Phase B — Google Calendar API integration (true push sync; the bulk)** - Per-user OAuth (incremental consent, `calendar.events` scope): on session confirm/reschedule/cancel, create/update/delete an event in the user's Google Calendar. - Store `google_event_id` per `(user, session)`; handle token refresh, revocation (Google-side and app-side disconnect), and API errors with retry via a Celery task (follow the task patterns in `webapp/backend/app/tasks/reminder_tasks.py`). - Admin toggle + credential entry (Google Cloud client id/secret) in Admin settings, encrypted at rest like other secrets (`_ENCRYPTED_KEYS` pattern in `webapp/backend/app/services/settings_service.py` + AES-256-GCM in `app/crypto.py`); document the Google Cloud setup for self-hosters. - **Investigation checklist (do first, may conclude "feed-only is enough"):** - [ ] OAuth consent screen verification requirements for a self-hosted redirect URI (each instance has its own domain — does every self-hoster need their own Google Cloud project? Almost certainly yes; document it.) - [ ] Quota limits for Calendar API at hobby scale - [ ] "Unverified app" warning friction for small instances - [ ] Whether webcal subscription (Phase A) already satisfies most users given Google's feed-refresh latency (~12-24h) is the main drawback - Ship Phase B only if the checklist outcome justifies it; otherwise close with a written conclusion and keep Phase A. ## Out of scope - Reading users' calendars for availability (pairs with the standing-availability issue later). - Outlook/CalDAV push integrations (the iCal feed covers them read-only). ## Acceptance criteria - **Phase A:** subscribing in Google Calendar / Apple Calendar shows all confirmed sessions; rescheduling updates the event (SEQUENCE bump verified); cancelling removes/cancels it; regenerating the token invalidates the old URL (test 404/403); feed requires no session cookie. - **Phase B (if pursued):** confirm/reschedule/cancel round-trips to Google within one task run; disconnect removes stored tokens; failures retry without duplicate events. ## References - `webapp/backend/app/routers/sessions.py:569` (`_build_ics`), `:600` (`download_ics`) - `webapp/frontend/src/pages/SessionDetail.jsx:66-76` (`googleCalendarUrl`) - `webapp/backend/app/models/campaign.py:41`, `models/session.py:89-92` - `webapp/backend/app/services/settings_service.py` (`_ENCRYPTED_KEYS`), `webapp/backend/app/crypto.py` (AES-256-GCM settings encryption) _Filed from the July 2026 full-project review._
rbrooks referenced this issue from a commit 2026-07-17 22:29:39 +00:00
Sign in to join this conversation.
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
rbrooks/Quest-Board#107
No description provided.