Skip to content

feat: ✨ add to calendar on schedule#539

Open
kylebtran wants to merge 5 commits into
mainfrom
kbt/google-calendar-write
Open

feat: ✨ add to calendar on schedule#539
kylebtran wants to merge 5 commits into
mainfrom
kbt/google-calendar-write

Conversation

@kylebtran

@kylebtran kylebtran commented May 21, 2026

Copy link
Copy Markdown
Member

Description

Replaces the "Add to Calendar" prefilled-link flow with real Google Calendar API writes, and wires schedule commits / meeting deletion to fan out to every responding member's primary calendar.

OAuth

  • Adds the calendar.events.owned scope to the Google login route so we can insert / patch / delete events on the user's behalf (in addition to the existing read-only scope).

Schema

  • New meeting_google_calendar_events table (migration 0018) keyed on (meeting_id, member_id). Stores google_calendar_id, google_event_id, a last_synced_snapshot jsonb (date, fromTime, toTime), and updated_at. Cascades on meeting / member delete.

Server actions (src/server/actions/availability/google/calendar/action.ts)

  • addOrUpdateMeetingGoogleCalendarEvent — idempotent per-member upsert. Uses a deterministic event ID (sha256(meetingId:memberId) → base32hex) so retries / races collapse to one event. Patches the tracked event if we have one, falls back to insert, recovers from 404 (deleted) and 409 (already exists), and persists the snapshot.
  • removeMeetingGoogleCalendarEvent — tolerates 404/410 and clears the tracking row.
  • syncMeetingToAllMemberCalendars / unsyncMeetingFromAllMemberCalendars — fan out across members who responded to the meeting and currently have a valid Google refresh token, with bounded concurrency (5) and a retry/backoff helper that only retries 5xx + Google rate-limit 403s.
  • loadMergedScheduledInterval — merges contiguous 15-min blocks into the single {date, fromTime, toTime} snapshot the calendar event model uses.
  • addMeetingToMyGoogleCalendar — single-member action invoked from the new button.

Schedule commit (commitMeetingSchedule)

  • After the DB transaction commits, calls syncMeetingToAllMemberCalendars (non-empty schedule) or unsyncMeetingFromAllMemberCalendars (host cleared it) so previously-synced events don't linger. Fan-out runs post-commit so a Google API failure can't roll back the local schedule, and the FanOutOutcome is surfaced in the UI snackbar.

Archive / delete (archiveMeeting)

  • Returns a typed ArchiveMeetingResult with the unsync FanOutOutcome. DeleteModal now uses a new showWarning snackbar variant to tell the host when some member calendars couldn't be cleaned up (e.g. revoked token), instead of silently succeeding.

Per-user button (add-to-calendar-button.tsx)

  • Replaces the old getGoogleCalendarPrefilledLink button. Derives one of four label states from the stored snapshot vs the current merged interval:
    • add → "Add to Calendar"
    • in_sync → "Added to Calendar" (disabled, check icon)
    • drifted → "Update Calendar Event" (refresh icon) when the schedule moved after the user already synced
    • non_contiguous → button is hidden, since a multi-interval schedule can't be represented as one Google event
  • Helpers live in src/lib/google-calendar/snapshot.ts so the state machine is unit-testable.

Page loading

  • /availability/[slug] loads availabilities, scheduled blocks, merged interval, and the per-user snapshot in parallel via Promise.all.

Recording/Screenshots

Before

After

Test Plan

  1. Sign in with Google; re-consent so the new calendar.events.owned scope is granted.
  2. Create a meeting, have a couple of accounts respond, then as the host schedule a contiguous block.
    • Expect: every responding member with a valid Google session gets the event on their primary calendar; snackbar shows N added / N skipped / N failed.
    • DB: one row per (meeting_id, member_id) in meeting_google_calendar_events with a matching last_synced_snapshot.
  3. As a non-host responding member, open the meeting page:
    • Before host schedules → no button.
    • After host schedules → "Added to Calendar" (disabled, check) if the fan-out reached you, otherwise "Add to Calendar".
  4. Host moves the schedule to a different time. Reload the meeting page as a member who'd previously synced → button now reads "Update Calendar Event"; click it, verify the Google event moves and the button flips back to "Added to Calendar".
  5. Host schedules a non-contiguous selection (two separate blocks) → button is hidden for everyone; no rows written.
  6. Host unschedules (clears all blocks) → previously-synced events are removed from member calendars and tracking rows are deleted.
  7. Host archives the meeting → events removed from member calendars; if you revoke one member's Google session first, expect the warning snackbar listing the partial failure count instead of a success toast.
  8. Re-clicking "Add to Calendar" rapidly should not create duplicate events (deterministic event ID + 409 recovery).

Issues

  • Closes #

Future Follow-Up

  • Support days-of-the-week meetings. The current write path assumes a single {date, fromTime, toTime} snapshot produced by loadMergedScheduledIntervalFor / mergeContiguousTimeBlocks, which returns null for any schedule that spans more than one date or has gaps. For weekly / recurring meetings we'd need to either (a) emit a Google RRULE recurring event with the appropriate BYDAY/UNTIL and store the recurrence in last_synced_snapshot, or (b) keep one tracked event per occurrence (extend meeting_google_calendar_events to be keyed on (meeting_id, member_id, occurrence_key) and adapt the fan-out + drift detection accordingly). The button's non_contiguous state and the schedule-commit fan-out are the two places to teach about the new shape.

@kylebtran kylebtran marked this pull request as ready for review May 21, 2026 12:04

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 issues found across 19 files

Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.

Re-trigger cubic

Comment thread src/server/actions/meeting/schedule/action.ts Outdated
Comment thread src/server/actions/availability/google/calendar/action.ts Outdated
Comment thread src/server/actions/availability/google/calendar/action.ts
Comment thread src/server/actions/meeting/archive/action.ts Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5 issues found across 21 files

Tip: cubic can generate docs of your entire codebase and keep them up to date. Try it here.

Re-trigger cubic

Comment thread src/server/actions/meeting/schedule/action.ts Outdated
Comment thread src/server/actions/availability/google/calendar/action.ts Outdated
Comment thread src/server/actions/meeting/archive/action.ts
Comment thread src/lib/auth/google.ts Outdated
Comment thread src/server/actions/meeting/schedule/action.ts Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant