feat(update-meeting): create a linked schedule sharing one Zoom meeting - #554
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
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. Comment |
bd6b8b9 to
209dc3e
Compare
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.
af241bb to
52ab738
Compare
@coderabbitai summary
Description
Third PR of the linked meeting modes work, stacked on #552. PR 1 added the
linkedToMidfamily 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/meetinggains an optionallinkedScheduleblock 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 (linkedScheduleabsent ⇒ byte-identical control flow).linkedScheduleBlockSchema/linkedScheduleSchema({ mid, modeType, room, zoomRoom, recurrencePattern }), parsed separately frommeetingSchemafor the same reasoneditScopeSchemais — 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 mirrormeetingSchema's room requirements (Hybrid needs both rooms, In Person needs a room) and reject a non-weeklypattern rather than coercing it.buildLinkedScheduleLabelbuilds 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 aRULE:in the schema's own comment, with aTODO(linked-schedules PR6)if the form later grows a combined submit.handleLinkedScheduleCreate, modeled onhandleScopedEditminus the parent trim — the anchor row is read, never written.weekly; family already atLINKED_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 familyisSharedZoomScheduleCompatiblerejects; andlinkedSchedulecombined with a non-alleditScope. 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/groupare placeholders), and resolves a count-bounded pattern'sendDatethe same way the whole-series path does before comparing, so resubmitting the meeting's own stored values is correctly not an edit.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.startDateis ET-midnight-anchored per the existing invariant, socalculateEndDateFromOccurrencesreads the weekday off the right day for an evening meeting. A DST spring-forward gap surfaces as a 400 withconvertETToUTC's own message, not a 500.inheritsZoomcopieszid/zoomLink/zoomPasscode/zoomInvitation/zoomHost/zoomManaged/zoomTopicfrom 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+createZoomMeetingwith 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 onlinkedToMidand not onzid. Host capacity is consumed only in that case; an inherited zid re-uses the family's existing booking, mirroringhandleScopedEdit.$transaction+lockResourceClaimsover the linked row's ownroom/zoomRoomand the inheritedzoomHost(the whole pool only when this row provisions).room/zoomRoomare 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.zoomHostdoes exclude the family, byexcludeZidas well as by mid. 409 +confirmOverrideretry, unchanged shape. Response gainslinkedMid.after(...)fan-out — this closes PR 2's deferred calendar gap.syncLinkedSchedulepublishes the new row's own calendar events through the same path a split-off row gets (plus the provisioning branch above).syncLinkedScheduleFamilythen does whathandleScopedEditnever had to: PATCHes the shared Zoom meeting for the widened union schedule and new topic, and callsrepublishMeetingCalendarson 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 stalezoomSyncStatus: 'error'doesn't keep the calendar's ⚠ badge after a PATCH that actually succeeded. Both syncs share onelinkedFamilyLoader, pinned to the family's zid rather than whichever caller resolves it first — an In-Person linked row would otherwise pin it tonulland drop split children and legacy zid rows from the union, silently re-narrowing Zoom'sweekly_days(the bug fixed in Editing one row of a shared Zoom meeting narrows the unioned Zoom recurrence #513).syncScopedParentCalendar→republishMeetingCalendars: same function, now with two callers (a scoped edit's parent, and every family member a new schedule just joined). Rename only.FindConflictsOptions.excludeZid, honored byfindResourceConflicts,getPoolHostLoadsandfindResourceConflictRows. 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 azoomHostcheck for a candidate joining that meeting must not report it as colliding with itself. Spelled{ OR: [{ zid: null }, { zid: { not } }] }rather than a barenot, because a SQL inequality never matches NULL and the Zoom-free rows are exactly the ones the check must keep seeing.TODO(linked-schedules PR3)atbuildEventTitleretired — it now points at this PR's fan-out, with a narrowedTODO(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).editScopecombination; 409-writes-nothing thenconfirmOverrideretries; 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.pr_convention.md): the existingupdate-meeting-route.test.ts/update-meeting-scoped-edit.test.tssuites still pass unchanged, which is the assertion that a request withoutlinkedSchedulereaches exactly the same code as before;syncScopedParentCalendar's rename is covered byyarn typecheck.git diff feat/linked-zoom-topic...feat/linked-schedule-update-api -- frontend/app/api/update/meeting/route.ts— confirmhandleLinkedScheduleCreatenever writes the anchor row, and that no derived topic is written back intoMeeting.zoomTopic.linkedScheduleblock against a local recurring Hybrid meeting (e.g. Mon–Fri) withmodeType: "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
Integrations
Engineering
Pre-merge Checklist
yarn lint).