Repository navigation
Add out of office and reassignment to Scheduling - #334
Merged
Merged
Conversation
…change Replacing a host reset the meeting link even when the meeting was someone else's: Zoom never gives a link back on update, so a collective booking lost it for good, and a new host who sorted first started a second meeting. Meetings now stay with whoever runs them, and only reset when that host leaves. A booking going to another host and back reused a deleted event's key, and Graph's answer to a repeated transactionId is undocumented, so events now get a new key each time they're deleted. Removing an event goes by the ledger, so an event on a calendar that's stopped being the write target still goes. Co-hosts' events now get a Meet link made by someone else's event. Slots for moving a booking come from the hosts it has, not who the event type would pick today.
Choosing another host for a booking has to keep to what it was routed with, and that was dropped once a host had won. It's now kept with the booking, encrypted, and goes when the attendee's details do. An empty value means nothing narrowed it, and null means it can't be known, so a later choice never mistakes one for the other.
Hosts can be marked as away, for part of a day or whole dates, one at a time, as a list or by attributes. They aren't offered during it, joining a running group session included, and a pool passes over them. Existing bookings are never touched: they're listed as conflicts, and the host's told how many. Bookers see when the host's back and any message they left. Reasons can say someone's on medical leave, so they stay out of webhooks, logs, calendars and anything a booker sees, and notes are encrypted. An entry puts a private event on the host's calendar, through the same ledger as bookings, which is why deleting one is soft: the row that removes the event belongs to it. put by your own id lets an HR tool sync without keeping ours, and webhooks say when time away is created, changed, starts and ends. Hosts get a Time off tab, and the dashboard an Out of office page.
Any kind of booking can now go to another host, keeping its id and time: a one-on-one, a pool, either role of a collective, a whole group session from any of its seats, a hold, a request waiting for approval, and for the record one that's already started. The target is claimed before the source lets go, so a lost race leaves the booking as it was, and nothing can override a host who already has something at that time. Their hours, calendar, limits and time off can be gone past, with a note saying why, which is encrypted and goes when the attendee's details do. auto picks the fairest who's free and keeps to what the booking was routed with. Candidates say who could take it, and why not. Whoever's leaving and joining are told apart, the attendee gets an update with the same invitation, and going to someone and straight back says nothing stale. rescheduleBooking's host goes through the same notices, webhook and fairness. The dashboard's booking page gets a Reassign dialog, with MCP tools and CLI commands for this and out of office.
A bulk reassignment moves up to 5,000 bookings in the background, each in its own transaction, so one that can't move never holds up the rest. Nothing's ever cancelled: a booking nobody can take stays where it is, and its item says so. Choose by host, out of office entry, ids or series, narrowed by time, event type or calendar conflict. Send them all to one host, try some hosts first and then the fairest who's free or leave the rest, or say who each goes to. A preview shows who each would go to without moving anything, working down them in order so two can't be promised the same free hour, and a change to it goes back as assignments. A run can be cancelled, and undone as a new run that skips anything that's moved on since. Each move's outcome commits with the move. Hosts get one email per run and a scheduling.reassignment.completed webhook carries ids and counts. Out of office entries can start a run with existing_bookings: reassign.
Out of office entries can name delegates, in order, who can be booked in the host's place during that time, inside the same event type. They're candidates only for a meeting that touches the time away, decided in isBookable so slots and booking can't disagree, and they still have to fit how a booking's routed: a pool's filter, what narrowed it, and not having been taken out by name. It doesn't chain, and required hosts just block. A delegate ranks no better than the person they're covering, or their own low count would make them take everything. A group now picks its host by time. A bulk run from an entry tries its delegates first. When someone comes back, their count in each pool moves up to the lowest of their peers, once, so they don't take nearly every booking until they've caught up. A host can name their own cover from their page, from people they share an event type with, and sees only names.
A manager session gives a team lead a hosted, embeddable page for the hosts a customer names, or everyone matching some attributes whenever it's asked: who's away, what needs cover, and moving it, one at a time or many at once. There's no team table: who manages whom is the customer's to say when they mint one. Its reach is checked before anything's read and again with the booking locked, and someone else's looks like it doesn't exist. It sees times, event types and hosts, never attendees' details. A run from it says who each goes to, since the fairest who's free could be on another team, and the worker has no session to ask. Hosts can give their own bookings to someone free who takes that event type, once the environment turns on host_reassignment. They see names only and can't go past anyone's availability. /host/bookings no longer returns contact details, metadata or other hosts.
A country's public holidays, or a customer's own ICS feed, applied to some hosts: by name, or everyone matching some attributes at each sync. A worker turns each into ordinary out of office entries, one per host and day in that host's zone, so everything that knows about being away already knows about these. Delegates and what happens to existing bookings come from the calendar. A deleted entry stays as a tombstone, so a host who opts out of a date isn't given it back. A date that's gone from the feed, or a host who's left, isn't one: nobody chose that. A failed, partial or empty read keeps what's there. A feed's URL can hold a token, so it's encrypted with WEBHOOK_SECRETS_KEY, goes through the pinned transport, never follows a redirect and never appears in a log or response, and sandboxes don't read them. Holiday names are unscreened, so they reach no email, entry or booker. Countries use Google's public ICS feeds, which need no key. Those mark every holiday as free, so they can't go through the check for busy time. Regional holidays apply unless `regions` narrows them.
Main gave hosts their own locations, guests on bookings, details on calendars and hosts moving their own bookings, so this reconciles them with reassignment: - A host's own office or link follows them when a booking or a whole session goes to someone else: the new host's takes its place, or it's left blank, and it never keeps the old host's address. A meeting on someone else's account still stands. - Booking through a delegate uses the delegate's own location. - /host/bookings keeps the no-show and session fields main's page reads. - Main's listRescheduleSlots replaces listBookingSlots, which answered the same question. - This branch's migrations are regenerated as one on top of main's: none had been applied anywhere but a local database. - New operations are documented for agents and listed in the agent surface.
Team pages and hosts: - An assignment's source has to be on the team, as its target does. - Undoing from a team page only gives back moves between its own people. - A host's or manager's session can't start moving bookings by changing an entry. Routing: - A preferred host still has to fit how a booking was routed, unless `any_host` says otherwise, for bookings and whole sessions. - rescheduleBooking can't change the host of an event type with reassignment off. Holidays: - A feed that's suddenly empty, or too long to expand, is a failed read and keeps what's there. - A recurring closure lasts as long as its first occurrence. - A change to a calendar's cover or bookings policy reaches entries already made, without undoing a host's own change since (settings_changed_at, since every sync moves updated_at). Also makes a Zoom test deterministic: it assumed which of two ids from one millisecond sorted first.
Main's understoodRow only claims the notification kinds it lists, so these three would have waited for good.
The dashboard ran availability checks on Node, against the rule that time is only
worked out in Bun. It now opens the team page for them, the way Reschedule opens the
manage page: reassigning, its candidates, a run's preview and out of office's dates.
- createDashboardSession: a manager session for a member, reaching every host of the
environment for an hour, acting as them. `{}` means that only with a member behind
it, and nothing else can mint it. Opening it is in the audit log.
- The team page gained what the dashboard's dialogs had: part days, several covers, a
message, notes, the calendar toggle, reasons, notes and telling attendees on moves,
and "starting before". It takes a booking or an entry to start from.
- A preview from a team page no longer lists every host as preferred, which failed
past 20 of them.
- A test refuses the functions that compute time in the dashboard's code.
- Clearing the token from the address bar keeps the query string.
Holds: a hold's remaining time is read after the target's locks, so waiting for them
comes out of it, and one that ran out meanwhile can't move.
Notifications: a change only supersedes what's pending for hosts it touches. Going A
to B to C tells A and C and not B. Going back to A tells nobody.
Out of office: a PATCH that changes cover and starts a run uses the new cover.
- A holiday calendar changed or deleted while its feed is being read: the sync checks again before making anything, and never overwrites the next_sync_at a change asked for. Deleting now counts as a settings change. - any_host can name someone taken off the event type, without putting them back on it. - "Stays" in a run's preview takes a proposed booking out of it (chosenTarget). - A preview works out whose time someone's covering per booking, from who it could go to, as the run does. - A preview only counts other event types' proposals against a host limit, not an event type's. - Two overlapping PUTs with one external id: the second tries again and updates. - Undoing an undo stays in a team's reach: managedRunWhere walks back to the run that started it.
- A worker that lost its lease while waiting on a lock could move an item the next worker also moved. Taking the item is now a compare-and-set on pending in the move's own transaction, and a move that loses rolls back, so an item moves once. A run cancelled mid-move now keeps that booking where it was, too. - A PUT sending an entry to another host starts a run for that host's bookings. - A holiday feed with an event missing its date is a failed read, rather than a good feed that takes holidays away.
- A team or host session moving an entry made with existing_bookings: reassign started its run, which could send bookings past the session's reach. Session edits start no run (startsNoRun): what's now in the way is reported, and moving it is its own step. - A run moved a booking rescheduled out of its selection. The selection's conditions are asked again with the booking locked, and it's reported as skipped_changed. - A queued run wrote its note about someone erased since. A move writes no note on a booking whose attendee is erased, read with a share lock in the order erasing takes them. - Undoing part of a collective change dropped the attendee's notice. It only goes if no other host is still waiting to hear of the change. Also fixes a deadlock race test that passed the wrong parameter, so its reschedules were always refused and it wasn't racing anything. It now does, and still doesn't deadlock.
- Undoing a group session whose first seat cancelled did nothing: another live seat stands for the session. - A host in a pool staffed by a filter had nobody to name as cover: listPossibleDelegates asks who takes each event type, as routing does, and someone taken off by name stays off. - A team page asked to override outside_coverage, which the API refuses: that's any_host. - A request whose time to approve has run out no longer counts as a conflict. - reassignment_enabled is in the dashboard's event type form.
- A holiday calendar's cover who'd since been deleted stopped every sync: they're left out. - A group seat with no claim left could move, and get a claim that never ended: it's hold_expired. - Changing a calendar's write target left the out of office block on the old one, with the old times: it goes.
Brings in fairer pools, slot diagnostics and calendar delegation. Conflicts were additive apart from rankCandidates, which keeps main's lowestShare and this branch's atLeastRatio. This branch's migration is regenerated on top of main's, as 0130. Fixes from reading the merge: - explainSlot goes by what booking checks: someone covering is only free while they are, and a running session can't be joined while its host is away. - A booking changing hands gives back the share it added, now kept on the host's row, so that happens with the row still there, for reassigning and for rescheduling to another host. Round 8: - A run cancelled while it's being worked out got pending items for good: expanding takes the run's lock, as cancelling does. - A far-off booking made a preview call a host's calendar unverified for nearer ones: each booking is judged to its own end (calendarVerifiedUntil).
- create_scheduling_out_of_office is marked SENDS, and says who it emails: it can reach hosts and attendees. - Candidates list everything in someone's way (reasons), so a team page can ask for every override it needs, rather than only finding out about the next one when the first fails. - A preview doesn't propose someone disabled, whom the run would refuse. - Candidates' from_host works by uid as well as id when deciding whose meeting it is.
- A holiday sync that outlasted its lease overwrote a newer one: the lease is checked before reconciling and before finishing. - Deleting a holiday calendar left a holiday still running west of UTC: an entry's end decides, not UTC's date. One that's started isn't gone from a feed, since feeds are read from today. - A preview treated a host someone had just been moved off as still busy: it tracks what's freed, in claims and limits, as the run would find them by then.
- The dashboard couldn't make an ICS holiday feed, having nothing to encrypt its address with. - A host who lost a booking heard nothing if it was cancelled before the email went. They do now, since a cancellation only reaches the hosts it has by then. - A team page only ever showed the first 100 entries of time away: it pages through them now. Bookings say when they're only the soonest 100. Also from the last gate: for bookings at one time, a preview went by booking id and a run by item id, so a run could disagree with its preview. A run goes by booking id too, which also makes that round's test stable.
Brings in attendee limits and Risk's pricing. This branch's migration is regenerated on top of main's, as 0131. - A booking waiting on a delegate's lock could land after their cover was taken back. Changing cover also takes the delegates' rows, and a booking reads it again with theirs locked. - A preview went one event type at a time where a run goes by time, so it could use up a shared limit differently: it goes in the run's order.
Brings in event type descriptions, booking history and cancellation reasons. This branch's migration is regenerated on top of main's, as 0134. A team page's moves appear in the new booking history, so its actor kinds gain manager_session. Erasing and redacting clear both a cancellation's reason and what a booking was routed with. - A long list of regions was cut at a name's length, so Quebec on Canada's Boxing Day didn't match. - Re-reading cover with a delegate locked counted their cover of someone unrelated: it keeps to the people it was found through. Also fixes round 4's dateless-event test, which wrapped its event in a second VEVENT.
One pass per booking, with both columns in one compare-and-set, rather than two passes over the same rows.
Brings in localized notifications. This branch's emails join the catalog in all 16 languages, with counts as "Bookings that came to you: 2", since it has no plurals. Its migration is regenerated on top of main's, as 0135. - A host's session could read why a colleague couldn't take a booking, from the refusal. - A session's new host heard nothing if the seat carrying the news cancelled: another seat does. - A preview only counted fairness up, where a run also takes it off whoever a booking leaves.
Brings in the hosted MCP server and usage guardrails. This branch's migration is regenerated on top of main's, as 0136. - Deleting an entry only checked whose it was before waiting for the host's lock: it asks again once it has it, so a session can't delete what's just become someone else's. - A first read of a holiday feed missed a holiday still running west of UTC: feeds are read from the day before, and a host's own date decides. - A PATCH with a new reassign target was ignored unless existing_bookings was repeated. Also restores three holiday tests an edit in round 13 deleted by mistake.
Brings in delegation's plan restriction. - An entry deleted before its run got there still moved its bookings: a deleted entry selects nothing. - A PATCH of only time_zone was accepted and ignored: on whole dates it applies, on times it's refused.
Brings in plans and fees for Scheduling. API-minted team pages go by the plan's host portal feature, as a host's page does, and /slots keeps main's rate limit. This branch's migration is regenerated on top of main's, as 0137. Privacy and permissions: - A manager session's response kept its link, with the token in it, in request logs. - Out of office, reassignments and holiday calendars kept their bodies in request logs: reasons, notes, and feed URLs that can hold tokens. They're excluded as bookings are, with a test per path. - A scheduling:create key could move bookings by creating time away or a holiday calendar with existing_bookings: reassign. That now needs scheduling:update.
- Reading attendees for a note before locking the booking was the opposite of erasing's order, and could deadlock it: it's read after the booking's lock. - A PUT that waited on a lock while its entry was deleted updated the deleted row and reported success: it reads again once it has the lock, and makes a new one. - A run started by time away silently stopped at 5,000 bookings: the cap is on what's asked for, and expanding takes it all.
- A host who lost a booking that was then cancelled heard, but with no CANCEL attachment, so the invitation stayed on their calendar. - Retrying a run with its Idempotency-Key failed if what it named had been deleted since: a replay comes before anything that can have changed. - A run's report was purged 395 days after it was made, though its bookings can outlive that: it waits for their rows.
- A dashboard team page kept reaching every host for up to an hour after its member lost access. The API asks on every /manager/* request whether they still can (memberStillHolds), and refuses as it does any session that's over. - An update that waited on a host's lock could change an entry that had moved to another host without holding theirs: it reads again, and locks them.
Brings in main's reminders work (#329): notifications now take their policy from notification-policy.ts, their copy from reminder-copy.ts and recordChange from booking-record.ts; the `reassigned` kind joins each. The branch's migration is regenerated on top of main's, as 0139. - A run an entry started no longer moves the first host's bookings after the entry was given to someone else. - A group session's former host gets a calendar cancellation for every seat, since each seat's invitation has a UID of its own.
- A former host's news about a session belongs to one seat. A seat cancelling first no longer swaps in another seat's booking, which sent two cancellations for the seat still standing and none for the one that held the invitation. A new host still hears through a seat that stands. - A holiday feed that turns up empty is refused from the same day the read starts, so a holiday still running west of UTC isn't taken away.
- A run's digest asks each booking's event type whether its hosts hear, as a booking moved by itself does, in both directions: an event type can silence it, and can turn it on against the environment's choice. What's silenced isn't counted either. - Saying the same thing again about an entry (an HR sync's PUT, say) no longer emails the host again about its conflicts. A change of host, times or `existing_bookings` still does.
- Writes to an entry hold whoever's covering as they are once the write has waited for its host, not as they were read before. A booking with a delegate holds only that delegate, so missing one let it commit on cover that had gone. Edits, upserts and deletes share `lockEntrySettled`. - A group booking trying one host after another uses `inSavepoint`, so a failed attempt doesn't keep its subtransaction until commit. - A candidate only counts as able to make the meeting with a calendar that's written to, which is what `setCalendarState` needs.
- A ban ends a dashboard member's team page: `memberStillHolds` now checks it, expiry included, as well as their role. - Holiday calendars count their settings by revision, and each entry says which one it reflects. Comparing timestamps couldn't tell an entry a sync wrote from old settings just after they changed from someone's own edit, so those entries kept stale cover for good. Someone's edit is stamped with the revision it was made against, so it still stays. The branch's migration is regenerated: `settings_changed_at` becomes `settings_revision`, and entries gain `source_revision`. Both tables are new on this branch, so there's no existing data to move.
- A session's new host gets an invitation per seat, as the old host and any group's host do. With only one, that seat cancelling took their only invitation away while the session went on. A seat standing in for another isn't needed any more, so `seatStillStanding` goes. - The team page's "who's leaving" follows the booking's hosts, so a second move doesn't send the host who has just left.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Hosts can now be marked away, and bookings can move to someone else one at a time or in bulk.
hosts,host_filter), with preset or custom reasons, a public message, andexternal_idupserts for syncing an HR tool. Bookers can't book a host who's away, group joins included, and the reason never reaches them.already_claimed.managersession and hosted page (/t/[sessionId]), scoped by host list or attribute filter, plus a host portal "Time off" tab and an optional setting, off by default, for hosts reassigning their own bookings./host/bookingsreturning more than its comment said.How it was tested
bun run verifyis green on this commit (79 of 79 tasks). The scheduling package has 1,144 tests on real Postgres, including races for booking against entry, reassign against reassign, reschedule and cancel, and a run against a reschedule.Not verified, or needs a person
requestIdstill need a real account. The plan asked for this before merging; Bailey chose to merge and run it against production afterwards.reassignment_enabledcheckbox.Behaviour worth knowing
/host/bookingsreturns less than it did. This is the unversioned client API that the hosted pages use.regionsnarrows it.notify.hosts: digest, the default) sends no calendar invitations or cancellations.eachdoes, one per seat of a session.memberStillHoldsre-checks role and ban on each request, not SSO or 2FA requirements.Review
27 rounds of Codex review. Round 27 reported nothing. No finding was rejected: all were real and fixed.
Merge of origin/main before review: reconciled per-host locations (a host's own office follows them on reassign,
including group sessions),
/host/bookingskeeps main'sno_show_atandgroup_session,listRescheduleSlotsreplaces
listBookingSlots, migrations regenerated as one on top of main's.Round 1: 9 findings, all real, all fixed
checkManagedRunalso checksfrom_host_id(manager.ts).undoReassignmenttakesonlyHosts; only moves between team members go back.SessionOutOfOfficeChange, without those fields.any_host, for bookings and sessions.rescheduleBookingignoredreassignment_enabledwhen changing host: refused.Note on round 1: the first version of the settings fix compared against
updated_at, which every sync moves; its testcaught that, hence
settings_changed_at.Merge of origin/main after round 1 (#315, #317): no conflicts and no new migrations, but main's
understoodRow(notifications.ts) would have left
reassigned,reassignment_summaryandout_of_office_conflictsunclaimed for good.8 tests failed on it; added the three kinds. Old workers leaving those rows alone during the rollout is what it's for.
Round 2: 4 findings, all real, all fixed
claimTarget'sttlOf).Reschedule opens the manage page (
createDashboardSession,openTeamPageAction). The team page gained what the dialogshad. A first draft called the API from the dashboard's server, dropped unapplied:
apps/web/AGENTS.mdsays it nevercalls the public API. Dashboard loses "move bookings" as a checkbox on adding time off: it's its own step there.
Checked in a browser: dashboard Reassign -> team page -> booking moved, history says "Dashboard member"; Move bookings from
an entry -> preview -> run (it stayed, since nobody was free, as the preview said). That found the query string being dropped
with the token (client-api.ts), a picker left open after a move, and the away host offered as their own cover.
Round 3: 7 findings, all real, all fixed
next_sync_at.any_hostrefused someone taken off the event type: it locks their row without needing it current, and never puts them back.chosenTarget).event_typelimit: onlyhostlimits count those.managedRunWherewalks the chain.Round 4: 3 findings, all real, all fixed
pending, and a move that loses rolls back.Round 5: 4 findings, all real, all fixed
existing_bookings: reassignstarted its run, reaching hosts past the session's scope: session edits passstartsNoRun, on team and host pages alike.stillSelected), reported asskipped_changed.Round 6 (the first run was killed, exit 144, and wrote nothing; retried): 5 findings, all real, all fixed
liveSeats).listPossibleDelegatesasks who takes each event type, as routing does.outside_coverage, which the API refuses: that'sany_host(overridesFor).liveBookingchecks it.reassignment_enabledwasn't in the dashboard's event type form: it is now.Round 7: 3 findings, all real, all fixed
hold_expired.Round 8: 2 findings, all real, all fixed. Merged origin/main (fairer pools, slot diagnostics, calendar delegation)
rankCandidates(main'slowestShareand ouratLeastRatio). Migration regenerated on top of main's.explainSlotdidn't know about cover or joining during time away, so it disagreed with booking;assignFairlynow gives back a share kept on the host row, so a booking changing hands does that with the row still there.Round 9: 4 findings (3 P2, 1 P3), all real, all fixed
create_scheduling_out_of_officesaid it reached nobody, and it can email hosts and attendees:SENDS, and the description says so.reasons, all of them, and the page asks for each.host_unavailable.from_hostby uid missed whose meeting it was: resolved to an id first.Round 10: 3 findings, all real, all fixed
The gate after round 10 caught a flaky test of that round's, which came from a real disagreement: for bookings at one time a preview went by booking id and the run by item id. The run now goes by booking id too.
Round 11: 3 findings, all real, all fixed
schedulingCalendarDeps().lostItBeforeItEnded).after,limitand Show more. Bookings say when they're only the soonest 100; moving a host's covers the rest.Round 12: 2 findings, all real, all fixed. Merged origin/main (attendee limits, Risk pricing); migration regenerated on top of main's
coverAsItStands). What's left: a delegate covering two people at once, where one's outside the booking's routing and the other's cover goes while it waits, still gets it.Round 13: 2 findings, all real, all fixed. Merged origin/main (event type descriptions, booking history, cancellation reasons); migration regenerated
manager_sessionadded to the booking history's actor kinds, since a team page's moves appear there. Itsdetailsholdreason_codeand host ids, and never the note.The gate after round 13: main's cancellation reasons and this branch's routing both re-encrypt booking rows, and two passes over them broke main's test of erasing between read and write. One pass, one compare-and-set.
Round 14: 3 findings, all real, all fixed. Merged origin/main (localized notifications); migration regenerated
namesOnly.seatStillStanding).Round 15: 3 findings, all real, all fixed. Merged origin/main (hosted MCP, usage guardrails); migration regenerated
reassigntarget was ignored unlessexisting_bookingswas repeated.Round 16: 2 findings, all real, all fixed. Merged origin/main (delegation's plan). (The migration needed nothing this time.)
time_zonewas accepted and ignored: on whole dates it applies, on times it's refused.Round 17: 3 findings, all P1, all real, all fixed. Merged origin/main (plans and fees for Scheduling); migration regenerated
scheduling:createkey could move bookings by creating time away or a holiday calendar withexisting_bookings: reassign: that now needsscheduling:update(apiKeyHasPermissions, as broadcasts do)./slotskeeps main's new rate limit. API-minted manager sessions go by the plan's host portal feature, as a host's page does.Round 18: 3 findings, all real, all fixed
Round 19: 3 findings, all real, all fixed
Round 20: 2 findings (1 P1, 1 P2), all real, all fixed
/manager/*request (memberStillHolds), and refuses as it does any session that's over. By role only: it doesn't re-apply the organization's SSO and two-factor requirements.Round 21: 2 findings (1 P1, 1 P2), all real, all fixed. Merged origin/main (reminders)
selectionConditionsnow compares the entry's host with the selection's and selects nothing when they differ. Test: entry moved from A to B before the worker ran leaves A's booking with A.notify.hosts: digest) carries no cancellations,eachdoes (documented inpackages/scheduling/AGENTS.md). Test: three seats give three "No longer yours" with three distinct UIDs.Round 22: 2 findings (2 P2), all real, all fixed
seatStillStandingsubstituted it). Who lost a seat is now told about that seat; substitution is only for the new host. Test: two seats, one cancels, two CANCELs with each seat's UID.earliestDay(now). Test: Los Angeles host, feed emptied at 01:00 UTC the next day, entry kept.Round 23: 2 findings (2 P2), all real, all fixed
notifications.reassigned.email_hostboth ways.hostsToldOfRunandreassignmentCountsForHostnow resolve the policy per event type, and silenced bookings are not counted. Tests: silenced by the event type, enabled by it against the environment, and a mixed run counts 1.PUTupserts queued a new conflicts email each time because the dedupe key usedupdatedAt. Unchanged host, times andexisting_bookingsnow queue nothing. Test: three identical PUTs give one notice, a changed end gives a second.Round 24: 3 findings (3 P2), all real, all fixed
updateOutOfOfficeread cover before waiting for the host lock and only re-read it if the host changed, so a booking with a newly named delegate could commit on cover the edit was removing.lockEntrySettlednow re-reads after each wait until nothing is missing;putOutOfOfficeanddeleteOutOfOfficeuse it too. Test: edit waits for the host, cover is replaced, edit must then wait for the new delegate.transaction, which rolls back without releasing, againstpackages/scheduling/AGENTS.md. NowinSavepoint. No behaviour test: it only changes what Postgres retains.meeting_provider_missingcounted a connected calendar even when it was not a write target, thoughsetCalendarStatewrites only to those. Now requires a write target or the Zoom calendar. Test: simulated calendar with its write target turned off reports true.Round 25: 2 findings (1 P1, 1 P2), all real, all fixed
memberStillHoldschecked only the role, so a banned member kept a working dashboard manager session until it expired. It now checks the ban and its expiry. Test: banned and banned-until-later give 404, an expired ban gives 200.updatedAtwithsettingsChangedAt, so an entry written from old settings just after a change kept stale cover permanently. Replaced withsettings_revisionon the calendar andsource_revisionon entries; an edit is stamped with the revision it was made against. Tests: an entry newer than the change but from an older revision catches up; an edit after the change stays. Unshipped migration regenerated.Round 26: 2 findings (2 P2), all real, all fixed
seatStillStandingis removed. Test: two seats, one cancels after delivery, only its UID is cancelled.sourceHostderives it from the booking's hosts. Test: unit test of the helper; not driven in a browser.Round 27: nothing reported. Clean.
After the PR opened: CI
testfailed on a 5 second timeout in "a rule that goes on forever can't take the worker with it". The test spends a rule's whole cycle allowance on purpose (2.4 s locally). Test-only change: a 30 second timeout. No behaviour changed, so no further review round.Merge of origin/main before merging (expo-kloq, 17 commits)
Clean, and touches none of this branch's code, so no further review round. Gate green on the merge commit.