Skip to content

feat(update-meeting): create a linked schedule sharing one Zoom meeting - #554

Merged
tunile943 merged 4 commits into
masterfrom
feat/linked-schedule-update-api
Aug 23, 2026
Merged

feat(update-meeting): create a linked schedule sharing one Zoom meeting#554
tunile943 merged 4 commits into
masterfrom
feat/linked-schedule-update-api

Conversation

@tunile943

@tunile943 tunile943 commented Aug 23, 2026

Copy link
Copy Markdown
Member

@coderabbitai summary

Description

Third PR of the linked meeting modes work, stacked on #552. PR 1 added the linkedToMid family model and PR 2 taught Zoom and Google Calendar to name a family; this one is the first write path that creates a family. PUT /api/update/meeting gains an optional linkedSchedule block that adds a second weekly schedule — a different mode on different weekdays — to an existing recurring meeting, sharing the family's one Zoom meeting instead of minting a second, and fans the family's new name out to the existing member's Zoom topic and Google Calendar events.

No UI posts this block yet (PR 6), and POST /api/write/meeting's two-row create is PR 4 — this PR is API + tests only, and every existing request shape is untouched (linkedSchedule absent ⇒ byte-identical control flow).

  • frontend/util/meetings/meetingValidation.ts: linkedScheduleBlockSchema / linkedScheduleSchema ({ mid, modeType, room, zoomRoom, recurrencePattern }), parsed separately from meetingSchema for the same reason editScopeSchema is — these keys describe a whole other Meeting row and must never be spread into the anchor's update data. Deliberately narrow: the schema carries only the mode, the room(s) that mode needs, and the weekdays. Everything the two schedules must agree on — title, description, email, group, calType, time of day, duration, interval, where the series ends — is derived server-side from the anchor and never read from the payload, because a family whose rows disagree on any of them has no single-series representation on Zoom (isSharedZoomScheduleCompatible) and would silently stop reaching Zoom at all. Zod-level rules mirror meetingSchema's room requirements (Hybrid needs both rooms, In Person needs a room) and reject a non-weekly pattern rather than coercing it.
    • The title de-sync decision carried over from PR 2's review: accepted, not guarded. The derivation is a create-time snapshot. Editing either row's title/description/email/group/calType afterwards propagates to neither the sibling nor the family's external name (buildLinkedScheduleLabel builds each member's name from that member's own title), so the two events can end up reading differently; nothing detects or reconciles it, and re-saving both rows is the fix. The schedule fields are the ones that would actually break Zoom, and those are re-checked on every write. Recorded as a RULE: in the schema's own comment, with a TODO(linked-schedules PR6) if the form later grows a combined submit.
  • frontend/app/api/update/meeting/route.ts: handleLinkedScheduleCreate, modeled on handleScopedEdit minus the parent trim — the anchor row is read, never written.
    • Validation (all 400s): anchor not recurring; anchor not weekly; family already at LINKED_SCHEDULE_CAP (canLinkSchedule); a mode the family already runs (availableModesFor); no weekdays at all; weekdays overlapping any existing member's (claimedDaysFor — disjoint days are a hard requirement, since Zoom holds the family as ONE union of weekdays and a day claimed twice silently collapses into a single occurrence); the requested days producing no occurrence inside the anchor's series; a prospective family isSharedZoomScheduleCompatible rejects; and linkedSchedule combined with a non-all editScope. Plus one that isn't in the plan: a payload that also edits the anchor's own fields is refused (submitsAnchorEdits) rather than 200'd and applied nowhere — this branch never writes the anchor, so a silently-dropped edit would be the worst outcome. The comparison covers only what the form can actually edit (creator/group are placeholders), and resolves a count-bounded pattern's endDate the same way the whole-series path does before comparing, so resubmitting the meeting's own stored values is correctly not an edit.
    • Derived series (deriveLinkedScheduleStart): the anchor's ET wall-clock time of day and duration re-anchored onto the first date the anchor's own pattern meets on the requested weekdays, searched from today or the series start, whichever is later — a schedule added to a series that began years ago starts now, not retroactively (a backdated Google series is fabricated history, and a count-bounded anchor could otherwise resolve an already-passed end date, i.e. a row born dead). Searching against the anchor's pattern keeps an every-other-week family in one week phase. recurrencePattern.startDate is ET-midnight-anchored per the existing invariant, so calculateEndDateFromOccurrences reads the weekday off the right day for an evening meeting. A DST spring-forward gap surfaces as a 400 with convertETToUTC's own message, not a 500.
    • Zoom identity is inherited whole, never re-provisioned. inheritsZoom copies zid/zoomLink/zoomPasscode/zoomInvitation/zoomHost/zoomManaged/zoomTopic from the anchor (a pinned topic stays pinned for every member; a null one keeps meaning "auto, recompute"). An In-Person linked row gets none of it. The In-Person-anchor case is the inverse: there is no Zoom meeting to inherit, so the Zoom-bearing linked row provisions one (resolveZoomHost + createZoomMeeting with the family, so it is born holding the union schedule and the family name) and becomes the family's zid holder — which is precisely why the family is keyed on linkedToMid and not on zid. Host capacity is consumed only in that case; an inherited zid re-uses the family's existing booking, mirroring handleScopedEdit.
    • Transaction and conflicts: one $transaction + lockResourceClaims over the linked row's own room/zoomRoom and the inherited zoomHost (the whole pool only when this row provisions). room/zoomRoom are conflict-checked against the whole calendar and do not exclude the anchor — the anchor's occurrences are live bookings of that room, and the two schedules only avoid each other by weekday, which says nothing about the room. zoomHost does exclude the family, by excludeZid as well as by mid. 409 + confirmOverride retry, unchanged shape. Response gains linkedMid.
    • after(...) fan-out — this closes PR 2's deferred calendar gap. syncLinkedSchedule publishes the new row's own calendar events through the same path a split-off row gets (plus the provisioning branch above). syncLinkedScheduleFamily then does what handleScopedEdit never had to: PATCHes the shared Zoom meeting for the widened union schedule and new topic, and calls republishMeetingCalendars on every pre-existing member so their events pick up the new title instead of advertising the old single-schedule name until something else happened to touch them. The Zoom PATCH is keyed on the family holding a zid at all, not on the new schedule needing one — an In-Person member adds no days to the recurrence but does name itself in the family's topic. Its result is persisted either way, so a holder carrying a stale zoomSyncStatus: 'error' doesn't keep the calendar's ⚠ badge after a PATCH that actually succeeded. Both syncs share one linkedFamilyLoader, pinned to the family's zid rather than whichever caller resolves it first — an In-Person linked row would otherwise pin it to null and drop split children and legacy zid rows from the union, silently re-narrowing Zoom's weekly_days (the bug fixed in Editing one row of a shared Zoom meeting narrows the unioned Zoom recurrence #513).
    • syncScopedParentCalendarrepublishMeetingCalendars: same function, now with two callers (a scoped edit's parent, and every family member a new schedule just joined). Rename only.
  • frontend/util/meetings/resourceOverlap.ts: FindConflictsOptions.excludeZid, honored by findResourceConflicts, getPoolHostLoads and findResourceConflictRows. One Zoom meeting is ONE real booking of its host no matter how many rows point at it (a linked family, a scoped edit's split children, a legacy zid group), so a zoomHost check for a candidate joining that meeting must not report it as colliding with itself. Spelled { OR: [{ zid: null }, { zid: { not } }] } rather than a bare not, because a SQL inequality never matches NULL and the Zoom-free rows are exactly the ones the check must keep seeing.
  • frontend/services/googleCalendar.ts: PR 2's TODO(linked-schedules PR3) at buildEventTitle retired — it now points at this PR's fan-out, with a narrowed TODO(linked-schedules PR5) for the remaining half (removing a linked schedule is a plain row soft-delete, which still leaves the survivor's events on the two-schedule name until it is next written; the delete-route cleanup is PR 5's).
  • frontend/tests/integration/update-meeting-linked-schedule.test.ts (22 tests) and frontend/tests/unit/linkedScheduleSchema.test.ts (9): every 400 rule and the editScope combination; 409-writes-nothing then confirmOverride retries; Zoom identity inherited for Remote-on-Hybrid and not inherited for In-Person-on-Hybrid; the In-Person-anchor provisioning case incl. an exhausted pool leaving the row unpublished rather than half-published; client-supplied fields that aren't the schedule's own being ignored; the family-wide republish (Zoom topic + anchor's calendar events) and the stale-error clear; an evening count-bounded anchor's occurrence counting; a schedule added to an already-running series starting now; a Zoom-free schedule still leaving every row of the shared booking in the union; and the family's own zid not being a host conflict.

Testing

  • cd frontend && yarn test:all — green (lint, lint:css, typecheck, unit, component, integration, e2e), run against a clear port 3000.
  • yarn test:integration --testPathPattern update-meeting-linked-schedule — the 22 route tests above.
  • yarn test:unit --testPathPattern linkedScheduleSchema — the 9 schema tests.
  • Pre-existing-test sweep (pr_convention.md): the existing update-meeting-route.test.ts / update-meeting-scoped-edit.test.ts suites still pass unchanged, which is the assertion that a request without linkedSchedule reaches exactly the same code as before; syncScopedParentCalendar's rename is covered by yarn typecheck.
  • Reviewer check: git diff feat/linked-zoom-topic...feat/linked-schedule-update-api -- frontend/app/api/update/meeting/route.ts — confirm handleLinkedScheduleCreate never writes the anchor row, and that no derived topic is written back into Meeting.zoomTopic.
  • Reviewer check: POST a linkedSchedule block against a local recurring Hybrid meeting (e.g. Mon–Fri) with modeType: "Remote" on Saturday and confirm one Zoom meeting is shared, the topic reads "<title> - Hybrid Mon-Fri - Zoom Only Sat", and both rows' Google Calendar events carry that same title.

Area(s) Touched

Product areas

  • auth — sign-in, sessions, NextAuth, role-based access
  • admin — /admin shell (Diagnostics, Users, Import, Export tabs)
  • docs — /docs Resources page (rendering, navigation, search)
  • ui/ux — frontend layout/styling not tied to a specific integration

Integrations

  • google-calendar — Google Calendar sync
  • zoom-api — Zoom meeting/host sync

Engineering

  • security — auth hardening, data exposure, dependency/security scanning
  • testing — test suite (Jest/Playwright) changes
  • github-actions — workflows, Dependabot config, other .github/ CI tooling
  • cleanup — refactor or dead-code removal, no behavior change
  • documentation — docs/ or README changes only

Pre-merge Checklist

  • Code follows current formatting conventions and passes lint (yarn lint).
  • Comments are appropriate — only where genuinely non-obvious, not restating what the code already says.
  • All test suites pass, both run locally and green in GitHub CI. Do not merge if any test suite is failing.
  • A test suite was added or updated for the feature/fix in this PR. Double-check this if you change a feature and did not update the test suites.
  • Only files relevant to this change are committed — no stray or unrelated files.
  • If this branch has a merge conflict with master, master was merged into this branch first (not the other way around).
  • The rest of this PR description (Description, Testing) is filled out, not left as template placeholders.

@vercel

vercel Bot commented Aug 23, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
ithaca-recovery Ready Ready Preview Aug 23, 2026 6:01pm

@coderabbitai

coderabbitai Bot commented Aug 23, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 3e5bfa92-6054-42d8-aa22-3cf33c859088

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@tunile943 tunile943 changed the title feat/linked schedule update api feat(update-meeting): create a linked schedule sharing one Zoom meeting Aug 23, 2026
@github-actions github-actions Bot added scope: google-calendar Google Calendar sync (services/googleCalendar.ts) scope: testing Test suite (Jest/Playwright) changes scope: zoom-api Zoom meeting/host sync (services/zoom.ts) labels Aug 23, 2026
@tunile943
tunile943 force-pushed the feat/linked-zoom-topic branch from bd6b8b9 to 209dc3e Compare August 23, 2026 17:51
Base automatically changed from feat/linked-zoom-topic to master August 23, 2026 17:59
A linkedSchedule block adds a second weekly schedule -- another mode on other
weekdays -- to a recurring meeting, inheriting the family's one Zoom meeting
instead of minting a second, and fans the family's new name out to the existing
member's Zoom topic and Google Calendar events.
…schedule joins

An In-Person linked schedule adds no days to the shared Zoom recurrence but does
name itself in the family's topic, so the PATCH is keyed on the family holding a
zid at all, not on the new schedule needing one.
… linked schedule

The linked row's pattern is now ET-midnight-anchored and never starts before today, the family
loader and the zoomHost conflict check are keyed on the family's zid, and an anchor edit sent
alongside a linkedSchedule is refused rather than silently dropped.
@tunile943
tunile943 force-pushed the feat/linked-schedule-update-api branch from af241bb to 52ab738 Compare August 23, 2026 18:00
@tunile943
tunile943 merged commit 9e9456c into master Aug 23, 2026
16 checks passed
@tunile943
tunile943 deleted the feat/linked-schedule-update-api branch August 23, 2026 18:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: google-calendar Google Calendar sync (services/googleCalendar.ts) scope: testing Test suite (Jest/Playwright) changes scope: zoom-api Zoom meeting/host sync (services/zoom.ts)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant