Skip to content

feat(sync): derive shared-schedule labels for Zoom and Google Calendar from the linked family - #552

Merged
tunile943 merged 10 commits into
masterfrom
feat/linked-zoom-topic
Aug 23, 2026
Merged

feat(sync): derive shared-schedule labels for Zoom and Google Calendar from the linked family#552
tunile943 merged 10 commits into
masterfrom
feat/linked-zoom-topic

Conversation

@tunile943

@tunile943 tunile943 commented Aug 23, 2026

Copy link
Copy Markdown
Member

@coderabbitai summary

Description

Second PR of the linked meeting modes work, and the first consumer of PR 1's linkedToMid model. A meeting the group runs as two co-existing weekly schedules now carries one name across both external services"One Day at a Time - Hybrid Mon-Fri - Zoom Only Sat" — on its shared Zoom meeting and on every family member's Google Calendar event. The label is not Zoom-specific, so it is built once and both services call it, rather than the segment logic living twice.

Behavior-neutral for every existing single-schedule meeting (the almost-all case is asserted byte-for-byte in both services' tests), and immediately correct for the three legacy pairs PR 1 just backfilled.

  • frontend/util/meetings/linkedSchedules.ts: the shared label builder and its supporting pieces.
    • buildLinkedScheduleLabel(baseTitle, meeting, family, singleScheduleSuffix?) — pure text, no knowledge of Zoom, Google or zid. Segments follow LINKED_SCHEDULE_MODES' fixed [Hybrid, In Person, Remote] order (never row-creation order, so the name is stable no matter which member triggered the write), each rendered as ${modeLabel} ${formatDayColumn(pattern)} reusing the exports' own Day-column formatter. LINKED_SCHEDULE_MODE_LABEL is the one mode→label map both services now share.
    • resolveFamilyRows(meeting, family) — reconciles the in-flight row with its pre-edit copy still sitting in the DB-loaded family, so a caller may pass either the whole family or only the siblings.
    • isDetachedSplitChild(row) — a "this occurrence" split-off child is a one-off, not a schedule; it is excluded from the label, so one detached child whose mode was later edited can't inject a bogus "- Zoom Only One-time" segment into every sharing row's name.
    • getZoomScheduleFamily(tx, mid, zid) — the rows the family's single Zoom meeting must account for, unioned by mid from two sources that are each incomplete alone: the linkedToMid family (the only way to reach a Zoom-free In-Person member) and every other live row sharing the zid (scoped-edit split children and any legacy zid group the backfill didn't adopt — dropping them would re-narrow the union schedule fixed in Editing one row of a shared Zoom meeting narrows the unioned Zoom recurrence #513).
    • linkedFamilyLoader(tx, mid) — a once-per-request reader caching the in-flight promise (a scoped edit runs its parent and child after() syncs concurrently), so the Zoom write and the Google Calendar writes in one request read the same rows and cannot disagree.
  • frontend/services/zoom.ts: zoomTopicFor(meeting, family) keeps its pinned-zoomTopic early return and otherwise delegates to the shared builder with ZOOM_SINGLE_TOPIC_SUFFIX (Hybrid/Remote only — an in-person meeting has no Zoom meeting to name). buildZoomMeetingBody/updateZoomMeeting/createZoomMeeting take the full family instead of "sibling rows sharing this zid", and buildZoomRecurrence/nextOccurrenceStart filter it through isZoomBearing internally — a standalone correctness hardening, since unioning an In-Person member's weekdays into weekly_days would advertise Zoom occurrences for a schedule that never meets online. Nothing writes the derived topic back into Meeting.zoomTopic: a null column keeps meaning "auto, recompute from the current family", which is what makes the topic recomputable rather than pin-once.
  • frontend/services/googleCalendar.ts: buildEventTitle(meeting, family) calls the same builder, retiring the private modeTitleSuffix map. buildEventBody/createCalendarEvent/updateCalendarEvent/reconcileMeetingCalendars gain a trailing, defaulted family parameter — trailing because most call sites (resume, suspension, delete rewrites) have no family concept and an extra positional argument they'd all pass [] to is only a chance to get the order wrong. Google has no zoomTopic equivalent, so there is no verbatim-name escape hatch; every member's event gets the full union title, mirroring the one shared Zoom meeting even though each schedule keeps its own event with its own dates and RRULE.
  • frontend/app/api/update/meeting/route.ts, update/meeting/sync/route.ts, write/meeting/route.ts: one linkedFamilyLoader per request, replacing the two zid-only findMany calls, and its result feeds both the Zoom write and the calType/Zoom-Room calendar writes in that same request. handleScopedEdit shares a single loader between syncScopedParentCalendar and syncSplitMeeting, which start concurrently.
  • frontend/app/api/update/meeting/resume/route.ts, delete/meeting/route.ts, util/meetings/suspension.ts: the paths that rewrite or recreate calendar events without an accompanying edit — resume, pre-created resume series, and a scoped delete's survivor rewrite — now load the family too. Without it, those writes would quietly rename a family member's event back to its own mode alone, and it would stay wrong until the row was next fully edited. createPendingResumeSeries uses the lazy loader so its "no upcoming occurrence" early returns stay genuine no-ops, query included.
  • frontend/app/api/retrieve/meeting/[id]/route.ts: zoomScheduleDiverged's inline detached-child filter now calls isDetachedSplitChild, so the divergence signal and the family label can never disagree about what counts as a detached one-off. Same predicate, no behavior change.
  • frontend/scripts/zoom-scan.ts: the monthly horizon-extension scan passes the representative's family rather than its raw zid group, so the scan can never rename a linked family's Zoom meeting back to a single schedule's name.
  • frontend/tests/unit/linkedSchedules.test.ts, zoom.test.ts, googleCalendar.test.ts: 29 new tests across the three files — fixed segment order regardless of family order, all three family shapes (Hybrid+Remote / Hybrid+In Person / In Person+Remote), Daily collapsing, the in-flight-vs-stored row reconciliation, detached split children excluded but recurring tail splits kept, the In-Person exclusion from Zoom's recurrence union, pinned zoomTopic short-circuit, and single-schedule output asserted byte-identical to today in both services. Integration suites' Zoom/Calendar mocks and fixtures updated for the new signatures.
  • docs/02-handoff/technical-decisions.md: the adopted-legacy-Zoom bullet now describes the derived topic as coming from the linked family, and records that the Zoom topic and calendar title are the same string by construction.

Known gaps, deliberately deferred to PR 3

Both are carried forward from this branch's review and marked TODO(linked-schedules PR3) at buildEventTitle:

  • Calendar fan-out. The union title is recomputed only for the row being written, so adding or removing a family member leaves the other members' calendar events on their previous title until each is itself edited or retry-synced. PR 3's after(...) must republish every member's events alongside its family-wide updateZoomMeeting.
  • Title de-sync between members. The label uses each row's own title as its base, so two members' names agree only while their title columns do. PR 3 derives the linked row's title from the anchor and decides there whether a direct edit of a linked row's title is rejected, propagated, or accepted.

Testing

  • cd frontend && yarn lint — clean.
  • cd frontend && yarn typecheck — clean (this is what catches every un-updated call site of the six changed signatures).
  • cd frontend && yarn test:unit — green, including the 29 new label/topic/title tests.
  • cd frontend && yarn test:integration — green; the route suites exercise the new family lookups against a real Postgres.
  • Reviewed against the pre-existing suites for assertions on the old contract (pr_convention.md sweep): tests/unit/zoom.test.ts's suffix-only topic expectations, write-meeting-route.test.ts's createZoomMeeting call assertions, and the updateZoomMeeting sibling-array assertions in update-meeting-route.test.ts / update-meeting-sync-route.test.ts all updated to the family signature rather than left asserting the old one.
  • Reviewer check: git diff feat/linked-schedules-model...feat/linked-zoom-topic -- frontend/services — confirm no code path writes a derived topic back into Meeting.zoomTopic, which is what keeps NULL meaning "auto".

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 5:52pm

@tunile943 tunile943 changed the title feat/linked zoom topic feat(sync): derive shared-schedule labels for Zoom and Google Calendar from the linked family Aug 23, 2026
@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: 7a4458e4-1b02-4b81-ae32-af986b3f6fa2

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.

@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
Base automatically changed from feat/linked-schedules-model to master August 23, 2026 17:50
A family served by one Zoom meeting now names each mode with its own days
("... - Hybrid Mon-Fri - Zoom Only Sat"), and the recurrence union skips members
that never meet online. Single-schedule meetings keep the exact name they have
today, and a pinned zoomTopic is still returned verbatim.
The create/update/retry-sync routes and the monthly scan now load the family
(linkedToMid, unioned with the rows sharing the zid) instead of zid siblings
alone, so an In-Person member reaches the Zoom name while split children and
uncovered legacy zid groups still reach the union schedule.
…nkedFamily

The lookup is a family query, not a Zoom API call, so keeping it in
services/zoom.ts would have hidden it behind that module's jest mock in every
route integration test. The retry-sync assertion now names the whole family it
receives rather than siblings alone.
…Google Calendar

Both services named a family's schedules with their own copy of the segment logic and mode
labels; the builder now lives in util/meetings/linkedSchedules.ts, so a family's Zoom topic and
its calendar event titles can never drift apart. Each service keeps its own single-schedule
suffix map, since a Zoom topic never says "In Person" and a calendar event always has.
…whole family

The three routes that already load the linked family for Zoom now reuse that one lookup for
their Google Calendar writes, so a family's calendar events and its Zoom meeting always carry
the same name. Two integration tests that never drained their background sync are drained
explicitly -- the extra lookup made them leak calls into the next test's assertions.
Locks in the calendar event title's lone-meeting suffix byte-for-byte alongside the family
name every member's event now carries, and covers the shared builder and resolveFamilyRows
directly rather than only through the two services.
… name

A "this occurrence" split-off inherits the parent's zid, so it reaches the family the shared
label is built from; edited to a different mode afterwards it added a bogus segment
("... - Zoom Only One-time") to every sharing row's Zoom topic and calendar title. The
predicate the retrieve route already filtered divergence by is now shared and applied to the
label only -- buildZoomRecurrence must keep seeing those rows to judge whether Zoom's schedule
can be represented at all.
…d scoped delete

Suspend-then-resume, a scheduled resume's pre-created series, and a scoped delete's surviving
parent each rewrote a linked family member's Google Calendar event from its own mode alone,
undoing the union title and leaving it wrong until that row was next fully edited. All three
now load the family the same way the edit paths do.
…ckground syncs

The parent and split-child syncs start concurrently and read a near-identical row set, so they
now share the request's loader -- which caches its in-flight promise rather than the resolved
value, or concurrent callers would each pay for the query. Also states the calendar's
single-schedule suffix map at its call site, as the Zoom side already did, and records the
missing family-wide calendar fan-out as a TODO.
@tunile943
tunile943 force-pushed the feat/linked-zoom-topic branch from bd6b8b9 to 209dc3e Compare August 23, 2026 17:51
@tunile943
tunile943 enabled auto-merge (squash) August 23, 2026 17:58
@tunile943
tunile943 disabled auto-merge August 23, 2026 17:59
@tunile943
tunile943 merged commit 92b5a3e into master Aug 23, 2026
20 of 28 checks passed
@tunile943
tunile943 deleted the feat/linked-zoom-topic branch August 23, 2026 17:59
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