feat: subscribable iCal calendar feed (#107 Phase A; Phase B dropped) #188
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/107-calendar-feed"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Implements Phase A of #107. Phase B (Google Calendar OAuth) is intentionally dropped — see the investigation below; the owner opted to replace it with emailed calendar invites (separate follow-up). Fourth and last of the v3.6.0 (Scheduling) milestone.
Phase A — the feed
Calendar export was one-shot per session (
GET /sessions/{id}/calendar.ics) and never reflected reschedules/cancellations. Now each user gets a subscribable personal feed.GET /calendar/feed/{token}.ics— unauthenticated (token-based), rate-limited,text/calendar. Lists the user's sessions across all their campaigns (90-day trailing window).users.calendar_feed_token— opaque per-user token (mirrorsanalytics_share_token); regenerating revokes the old URL. Managed viaGET/POST/DELETE /api/me/calendar-feed.app/services/calendar_service.py— a shared multi-VEVENT builder: stableUIDper session, UTCDTSTART/DTEND(end_timeor +4h),SEQUENCE/LAST-MODIFIEDso clients refresh on change, andSTATUS:CANCELLEDfor cancelled sessions (subscribers remove them). The existing per-session download now reuses this builder.sessions.updated_at— added (it didn't exist;server_default=now(),onupdate=now()), which drivesSEQUENCE.src/api/calendar.js+ a "Calendar subscription" card on Profile (feed URL + copy, generate/regenerate/revoke).Acceptance criteria (Phase A)
SEQUENCE/LAST-MODIFIEDso clients update.STATUS:CANCELLED.Phase B — investigation & decision (dropped)
Requested by the issue's checklist. Findings:
calendar.eventssensitive scope requires either Google verification (video demo, ~10-day review) or the "unverified app" warning + a 100-user lifetime cap that can't be reset.Conclusion: Google OAuth push is poor cost/benefit for a self-hosted tool — heavy per-instance setup + verification friction to fix only Google's refresh lag. Dropped. The owner chose emailed iCalendar invites (
METHOD:REQUEST/CANCELon the existing confirmation/cancellation emails) as the near-instant, provider-agnostic, no-OAuth replacement — filed as a follow-up.Sources: Google sensitive-scope verification, unverified apps / 100-user cap, ICS refresh rates.
Verification
Backend (dev-host harness): migration applies from base and
downgrade -1 → upgrade headround-trips (both new columns drop/recreate); 430 passed (9 new); ruff clean.Frontend (node:20): lint clean, 169 tests (20 new), build succeeds.
Deploy note: migration
c0d1e2f3a4b5runs with the v3.6.0 deploy (backup-first), after #98's and #104's.🤖 Generated with Claude Code
Calendar export was one-shot per-session (GET /sessions/{id}/calendar.ics) and never updated on reschedule/cancel. Add a subscribable per-user feed so a group can subscribe once in Google/Apple/Outlook and see all their confirmed sessions, kept current. Backend (migration c0d1e2f3a4b5, down_revision b9c0d1e2f3a4): - users.calendar_feed_token (nullable, unique) — an opaque per-user token, mirroring the campaign analytics_share_token pattern; regenerating it revokes the old URL. - sessions.updated_at (server_default now, onupdate now) — did not exist before; drives the feed's SEQUENCE/LAST-MODIFIED so calendar clients refresh on change. - app/services/calendar_service.py: a shared multi-VEVENT ICS builder. Stable UID per session, DTSTART/DTEND in UTC (end_time or +4h), SEQUENCE = int(updated_at.timestamp()), STATUS:CANCELLED for cancelled sessions (so subscribers remove them). Covers all campaigns the user belongs to, within a 90-day trailing window. The per-session download now reuses this builder. - GET /calendar/feed/{token}.ics — unauthenticated, rate-limited, text/calendar. - GET/POST/DELETE /api/me/calendar-feed — view / generate-or-rotate / revoke. Frontend: src/api/calendar.js + a "Calendar subscription" card on Profile (feed URL + copy, generate/regenerate/revoke). Verified via the dev-host harness: migration applies from base and downgrade -1 + upgrade head round-trips (both new columns drop/recreate); backend 430 passed (9 new), ruff clean; frontend lint clean, 169 tests (20 new), build succeeds. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>