On Call mode — a junior doctor's shift hub - #2608
Conversation
Captures the agreed design for a seventeenth app mode giving a junior doctor one place for shift-operational knowledge: orientation manuals, role-based contacts, referral pathways, teaching, an escalation playbook, and site logistics. Records the decisions and what each rules out, notably: - Content is owner-scoped private data edited in the app, not committed into the repository. - v1 is owner-only. Cohort sharing is deferred to its own project: the app has two visibility states, owner-only and fully public, and the site has no login wall, so the only available share would publish internal service information to the open internet. - The playbook links to the owner's own uploaded guidelines and never authors clinical guidance in the app's voice; a scenario with no local guideline says so rather than substituting a generated answer. - Every entry carries a last-checked date and shows an unmissable stale state after twelve months. - No rosters, no hosted recordings, no patient information. Also records the migration-timing constraint: merging reaches the live clinical database within seconds, so the migration merges only inside an approved window with auto-merge unarmed. The user-experience section is still to be written and will follow the design-system system of record and the phone chrome contract. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LL9gE67N6yorjPcssuK4bC
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Team Run ID: 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 |
|
This pull request has been ignored for the connected project Preview Branches by Supabase. |
Both were written from the general pattern rather than from the files, and both were wrong in a way that would have cost time in the plan: - The offline contacts cache reuses the existing `createBrowserStore` helper, following `saved-registry-storage.ts`, rather than introducing a storage mechanism the app does not need. - Reads resolve the caller through `publicAccessContext`, which already handles the anonymous case for the registry routes; writes require an authenticated user. The owner still never comes from the request body. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LL9gE67N6yorjPcssuK4bC
The design section was written from the repository's own design-system system of record and phone chrome contract rather than from general practice, which corrected two things the earlier draft had wrong: - Mode homes live on the shared home with the mode path redirecting to it. The draft had On Call owning a real home. Following the convention removes work rather than adding it. - Six navigation labels are not automatically allowed; they need recorded density evidence. The spec now carries the fallback in advance, so nobody makes a fresh decision mid-build. The section commits On Call to borrowing the existing design language rather than inventing one: no new colours, type sizes, spacing or motion; the shared in-page navigation header rather than a second phone header; 48px tap targets, never 44. It also specifies the states that carry safety weight — stale entries signalled by more than colour, the offline banner announced through a screen-reader-only region, a faulted search that renders no count, and a playbook scenario with no linked local guideline rendering an empty state rather than a substitute. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LL9gE67N6yorjPcssuK4bC
CI triageCI failed on this PR. Automated classification of the 2 failed job(s):
Compared with main CI run #15613 (success). That run's conclusion is an aggregate and did not exercise Classification is evidence routing, not permission to ignore a failure. Exact quarantined Playwright identities remain governed by the flake ledger. |
`Static PR checks` went red on `check:repo-awareness-snapshot`: adding a documentation file leaves the committed snapshot behind the repository. Regenerated with `npm run snapshot:repo-awareness` rather than edited by hand, which is what the failure message asks for. Verified locally: the gate now reports the snapshot in step, and the four documentation gates that run after it in the same job — index coverage, inventory, script references and links — all pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LL9gE67N6yorjPcssuK4bC
Fourteen tasks, seventy steps, each ending in an independently testable deliverable and a commit. Written test-first: every task states the failing test, the command that proves it fails, the implementation, and the command that proves it passes. The order is deliberate. The domain model and staleness come first because they are pure and every later task depends on their names; the table and the API follow; mode registration comes next because the typecheck then enumerates the remaining registries itself rather than relying on a hand-written list; the six section pages come last, built on Contacts as the reference implementation. Two things the plan settles in advance rather than leaving to the build: whether six navigation labels fit a phone bar is answered by recorded density evidence, with the fallback already chosen; and the migration's merge timing is carried in the global constraints, because merging reaches the live clinical database within seconds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LL9gE67N6yorjPcssuK4bC
…nth staleness Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…xt month When a verification date falls on February 29, adding 12 months via setUTCMonth silently rolls into March 1 (February 2025 has no 29th), causing that entry to stay fresh one day longer than all others. Capture day-of-month, detect overflow by comparing after the shift, and clamp back to the last day of the intended month using setUTCDate(0). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds the migration for on_call_entries (owner-scoped, RLS enabled, service_role only), mirrors it into supabase/schema.sql, indexes the new table in the codebase map, and adds the migration contract test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LL9gE67N6yorjPcssuK4bC
…tory Two plan tasks in one commit. They were built in parallel by separate agents and are separable in principle, but the pre-commit hook refuses a path-limited commit while the sibling documentation inputs it regenerates are still outstanding, and splitting them would have meant stashing one agent's work — the operation that had already caused one silent revert earlier in this run. Mode registration. `on-call` joins the sixteen existing modes in every map keyed by mode id: the definition and search configuration, the shared-home copy, the mode glyph and accent, the universal-search context, the command surface, and the site-map generator. Most of these are exhaustive by type, so the compiler enumerated the work rather than a hand-written list. The secondary-navigation registry takes an empty placeholder for now; the next task fills in its six destinations. The glyph is a new key rather than a reused one, because mode glyphs must be pairwise unique. Four tests holding complete sixteen-mode lists now hold seventeen. Entry repository. Maps a row to the domain entry and back, and fetches an owner's entries with the owner predicate on the same fluent chain as the table reference — the shape the owner-scope gate reads to prove a query is scoped. Fetching without an owner throws rather than returning rows, and a row whose details do not match its section renders without them rather than as a half-parsed shape. `on_call_entries` is also declared in the Supabase types file, which is curated by hand in this repository: there is no regeneration script, it carries a hand-written compatibility note, and the comparable registry table is already declared there. Regeneration could not have produced this entry anyway, since the table does not reach the live database until the migration merges. Verified: whole-project typecheck clean, 177 tests across the mode registration suites, 3 for the repository. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LL9gE67N6yorjPcssuK4bC
…le emptying the list Navigation. On Call's six destinations join the secondary bar — Contacts, Playbook, Referrals, Orientation, Teaching, Logistics, in that order, because Contacts is the page a shift actually opens. Each carries an icon, since that map is exhaustive by type. The mode joins the phone sheet's Care group and the sidebar's pinnable and more-modes lists; without the first of those it would have been silently unreachable on a phone, which is exactly what that test exists to catch. The three shell routing helpers now recognise the namespace. The density profile is `extended`, the one Therapy Compass uses, and carries a comment recording that six labels is one more than any existing adopted mode and that the phone-bar density evidence is owed once the routes exist. The fallback, if six do not fit, is already decided in the plan rather than left to be invented later. Also adds `desktopSearchPlacement: "hero"`, which the plan omitted and every sibling mode sets. Subtitle handling. The database permits an empty subtitle, but the entry schema required a non-empty string, and the parse runs inside a map over every row — so one blank subtitle rejected the entire fetch and would have left an on-call doctor with no numbers at all rather than one untidy row. Empty subtitles now normalise to absent. The parse still throws on a genuinely malformed row rather than silently dropping it: degrade conservatively, do not guess. One test is knowingly red until the search page exists: search-results-band-adoption asserts that a mode declaring a result list must mount the shared band, which stops a failed search reporting "0 matches". On Call declares the list; the band arrives with its search page. Silencing it by weakening the declaration would defeat the check. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LL9gE67N6yorjPcssuK4bC
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_2a0fa906-ac24-4382-b1cd-fe40b62a4f7c) |
Add GET/POST /api/on-call/entries, PATCH/DELETE /api/on-call/entries/[id], and POST /api/on-call/entries/[id]/verify. Every write scopes on owner_id from the validated session (never body/query), a PATCH/DELETE/ verify for an id the caller does not own 404s identically to a missing id, and demo mode serves a synthetic non-clinical fixture set without touching Supabase. Adds a dedicated "on_call" rate-limit bucket and indexes the new route area in docs/codebase-index.md. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LL9gE67N6yorjPcssuK4bC
…ject partial saves Routes. Ten route files under /on-call: the redirect stub, layout, loading, the six section pages, and a minimal search page. All six sections render through one module rather than six divergent shells, following the Sources mode's factoring, and each mounts the shared in-page navigation header rather than a hand-built second phone header. The search page is deliberately minimal for now; its content and the shared results band arrive with the search task. Two registrations the plan did not anticipate, both required rather than discretionary. The section routes are declared as information pages, without which the shared mode bar double-mounts alongside our own header in the same slot. And the eight new routes are declared in the design-system adoption contract, a static gate that fails closed on an undeclared production route; they inherit the existing catalogue surface's proof, so no new evidence was needed. Partial saves. Editing an entry now requires the complete entry. Previously a save that omitted a field silently reset it to a default and returned success — worst of all for the last-checked date, which is the record that an entry was confirmed correct and the thing the twelve-month staleness design rests on. A partial body is now rejected outright. Defaults remain correct on create, where a new entry that omits a subtitle genuinely has none, and are dangerous only on update. The results-band adoption test stays knowingly red until the search page mounts the band. Verified: typecheck and lint clean; 111 tests across route reachability, loading contract, in-page navigation, nav slot ownership, viewport fill and redirect behaviour; 11 on the API contract; owner scope green on both phases. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LL9gE67N6yorjPcssuK4bC
Contacts is the page the mode exists for, so it is designed for one hand in a corridor and the desktop view is that widened rather than a separate layout. Each role is one row, the whole row is the tap target at 48px, and the number is a tel: link, so ringing someone is a single tap. Entries overdue for checking collect into a group at the top rather than being buried. Freshness is shown with an icon and the words "checked" plus a date, never by colour alone — colour may reinforce the meaning but must not carry it, because it fails for a colourblind reader and in high-contrast mode. An entry never checked says so plainly instead of showing a date it does not have. Offline. The entry set is cached in the browser on a successful load and rendered when the network fails, behind a banner naming the date the copy was taken: a cached number with no age is worse than no number. The cache clears on sign-out through the existing sign-out path, so a hospital's internal numbers — some of them personal — do not outlive the session on a shared machine. Every storage read and write is wrapped, because a browser with site data blocked throws on access and that must not reach render. The row primitive is deliberately section-agnostic; the remaining five sections reuse it rather than growing five variants. One inference to flag: the design says contacts group by area, but the contact shape carries tags rather than an explicit area, so grouping uses the first tag. That is recorded in a comment at the call site. Verified: 13 contacts tests, 13 store tests; typecheck, lint, design-system contract, type scale and icon scale all clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LL9gE67N6yorjPcssuK4bC
…ealth-n17dvm # Conflicts: # data/repo-awareness-snapshot.json
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7e7c0227d4
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
Sections. Playbook, Referrals, Orientation, Teaching and Logistics, all reusing the row and freshness primitives rather than growing five variants. The Playbook carries the mode's one real safety rule and now has a test holding it: escalation steps are administrative facts the owner typed — who to call, when, on what number — and every clinical statement is a link to one of the owner's own documents, shown with its title and date. A scenario with no linked guideline renders an empty state pointing at a document search, and the test asserts no clinical instruction language appears on that card. A page that fills that gap would be worse than one that admits it. Referrals labels what a service accepts and excludes as text rather than colour-coded chips alone. Orientation attributes a pinned summary to the owner, above the real document, so it never reads as the manual's words. Teaching orders by next occurrence and marks a recording link as leaving the app. Editing. A sheet with per-section fields, validation surfaced on the field, deletion guarded by a dialog that names the entry rather than saying "Confirm", and a one-tap "still correct" action. Saving sends the complete entry, which is what the API now requires. Build fix. `updateOnCallEntrySchema` moves out of the route file into `src/lib/on-call/api-schemas.ts`. A Next.js route module may export only the framework's own names, and the extra export failed CI's production build with TS2344. The comment claiming it was inert was wrong. Worth recording why it got through: `tsc --noEmit` passes it happily and only a real build catches it, so a "typecheck clean" report is weaker evidence than it sounds. Also adds the On Call row to the navigation density coverage. Deferring that did not merely owe evidence later — it failed three assertions immediately, which I should have run rather than assumed. Verified: 88 tests across the editor, sections, routes, API contract and navigation contract; a clean production build listing all ten routes and three endpoints. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LL9gE67N6yorjPcssuK4bC
…docs Search (`/on-call/search`) runs entirely over the browser cache, so it works with no signal — the same constraint the sections were built to. It mounts `SearchResultsHeaderBand`, which the mode's `resultsSurface: "results-band"` declaration requires: without it a failed search reports "0 matches" instead of saying the search itself failed. The printable card (`/on-call/card`) is the paper fallback for a phone with a flat battery. `card-selection` decides what reaches it: entries marked on-card, excluding personal entries and — deliberately — excluding stale ones. A number nobody has confirmed in twelve months should not be carried into a night shift on a piece of paper that cannot show it is stale. Docs and counts follow the seventeenth mode: the codebase index gains a real On Call section (modules, routes, storage model, and the explicit statement that the mode carries no app-authored clinical content), and the mode counts in CLAUDE.md and the index move to 17. Two of the count strings became count-free instead, which is how the 15-versus-16 drift recorded in the 2026-09-02 audit happened. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LL9gE67N6yorjPcssuK4bC
…ealth-n17dvm The only conflict was data/repo-awareness-snapshot.json, which is generated: regenerated with `npm run snapshot:repo-awareness` rather than hand-resolved. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LL9gE67N6yorjPcssuK4bC
Four committed contract tests went red. None was a flake; each names a
repository rule the new mode had stepped outside of.
- The essentials card mounted `InPageNavHeader` itself. Every mode registers
its claim on the phone header's addon slot in one `*-nav-header.tsx`
sibling, so the pages competing for the single collapse owner can be read
off a list of files rather than found by grep. The card's header is now
`OnCallCardNavHeader`, beside the section header.
- `on_call_entries` was missing from the tenancy guard's explicit list of
owner-scoped tables reachable from API routes. That list is the reviewable
record of the tenant boundary; a new owner-scoped table has to appear in it.
- Two routes built `{ error, issues }` envelopes by hand. This repository has
one public error envelope (`apiErrorPayloadSchema`), so the raw Zod issue
list now stays server-side and the two distinct messages carry which half of
the contract failed.
- The shared-composer routing test still asserted "On Call has no search route
by design" — true when it was written, before `/on-call/search` existed.
The code was right and the expectation was stale.
Three further suites (privacy readiness, clinical hazard controls, RAG plan
parity) were red only because this container held a shallow 114-commit clone
and they resolve real commit SHAs. They pass unchanged once the clone has full
history; nothing in them needed fixing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LL9gE67N6yorjPcssuK4bC
A fresh reviewer read Tasks 12 and 13 against their briefs. These are its findings, in the order they matter. Search showed phone numbers with no freshness marker, while every section row carries one. That made search the single surface where a number nobody has confirmed in over a year looked current — the exact failure the whole freshness system exists to prevent. Result rows now carry the same badge. Search matched case-sensitively. Every haystack handed to `rankCatalogRecords` must already be normalised, because matching runs against lower-cased query terms; ours were raw. Orientation, whose content lives in `body` alone, was effectively unsearchable. Mutation-tested: two of the three new cases go red when the normalisation is removed. The third asserts ordering, because a cased title is still found by the fuzzy fallback — being found is not proof, ranking first is. The count line read "3 on Call": with no `resultNoun`, the band lower-cases the mode's label and calls it the noun. The existing test asserted only the digit and sailed past it, so the new one asserts the phrase. An owner with nothing added yet was told to "check the spelling" — blaming them for a typo they did not make, and hiding the one action that helps. Nothing to search is not a failed search. The printable card said "No number on file" against playbook steps and orientation notes, which have no number by design; it now prints the body, which is their content. It also asserted "nothing is flagged" while the first fetch was still running, and re-read the clock on every render. The card's only entry point disappeared whenever Contacts was empty, stranding a page that can hold flagged referral and logistics entries. The filter sheet printed its own result count even when the search had faulted, reintroducing one layer down the false "0 matches" the band exists to suppress. Task 13 shipped with no component test: nothing proved a stale entry stays off the printed page. Nine cases now do, including never-verified and the exact twelve-month boundary. Two fixtures authored clinical content — a temperature threshold and an escalation rule. This mode carries none, and a fixture is how such content usually enters a repository: it gets copied into a seed, then a demo, then a screenshot. They now carry the search term and no clinical claim. Also removed: two unreachable `default:` arms (a seventh section should be a compile error, not silently unsearchable), an unread route parameter and a needless cast. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LL9gE67N6yorjPcssuK4bC
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_86b717f8-e604-4eea-b232-bb52dacc8c17) |
…ealth-n17dvm Three conflicts. Two were generated files (data/repo-awareness-snapshot.json, docs/site-map.md) and were regenerated with the repo's own tooling rather than hand-resolved. The third was the mode-homes paragraph in docs/codebase-index.md, which #2614 rewrote on main to consolidate Documents and Medications onto the shared home. Main's rewrite is the newer structure, so it wins; the On Call facts are re-applied on top of it — seventeen modes, and /on-call as a thirteenth consolidated redirect stub whose section pages live one level down. Full offline suite on the merged tree: 1 failed, 15,126 passed. The one failure is the stale drift manifest, unchanged and still blocking. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LL9gE67N6yorjPcssuK4bC
The whole-branch review found that only Contacts was wired to the entry store. The other five list components — Playbook, Referrals, Orientation, Teaching, Logistics — were imported by their own tests and by nothing else in `src/`. Every one of those five pages said "no entries yet" no matter what the owner had saved, offered no way to add anything, and the Playbook's "no local guideline linked" state, which exists so the app never substitutes guidance it cannot source, was unreachable. A playbook entry would appear in search and be absent from its own page. Each section id now resolves to its list component through one switch with no `default` arm, so a seventh section is a compile error rather than another silently blank page — which is exactly how this went unnoticed. The editor, whose field map was already keyed by section, is mounted for all six, and the five gain a per-row edit control and the same "still correct today" action Contacts has. `useOnCallLinkedDocuments` resolves the ids the Playbook and Orientation link to; a failed fetch yields an empty map, so those sections say "no local guideline linked" rather than claiming one exists. Five further defects, each found by the same review: An account switch left the previous user's entries cached. Sign-out cleared them; switching accounts did not, and a shared ward computer switches accounts without ever signing out — so user A's ward numbers and personal lines rendered for user B. `onCallEntryFreshness` failed open on a date it could not parse: every comparison against NaN is false, so the overdue branch fell through and the entry reported as freshly checked, which put it on the printed card. A date we cannot read is not evidence anyone checked it; it now fails to stale. A browser blocking site data reported an empty hub after a completely successful fetch, because entries were only ever read back out of localStorage. The fetch result is now held in memory as the fallback for that browser. A stale referral showed no marker until someone expanded its disclosure — the one surface in the mode where staleness was hidden by default. The badge moved to the collapsed header. Three cards hand-rolled the shared `cardSurface` recipe and dropped `forced-colors:border`, so in Windows High Contrast their edges disappeared. Also: `createOnCallEntrySchema` was declared twice, so create and update could have drifted apart; the last test fixture authoring clinical content (a scenario stating a trigger and an escalation rule) is now content-free, like the search fixtures; search no longer claims the owner has nothing while the first fetch is still running; and `OnCallLinkedDocument` moved into the domain layer, because `src/lib` may not import from `src/components`. Full offline suite: 1 failed, 15,141 passed. The one failure is the stale drift manifest, unchanged. Production build exits 0 with all nine On Call pages and three endpoints. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LL9gE67N6yorjPcssuK4bC
CI's "Static PR checks" caught this: the committed snapshot was behind the repository. The merge from main resolved this generated file by regenerating it, but the regeneration ran before the later documentation edits, and the merge left duplicate entries for docs/codebase-index.md and docs/site-map.md in the process. Regenerated with `npm run snapshot:repo-awareness`, which drops the duplicates and re-pins the captured revision. `check:repo-awareness-snapshot`, along with sitemap:check, docs:check-index, docs:check-links, check:design-system-adoption and check:outstanding-issues, all pass locally. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LL9gE67N6yorjPcssuK4bC
CI's Lighthouse budget failed the home page: desktop LCP 1004 ms against a 786 ms baseline, +27.8%. That baseline was last refreshed at #2399 and the Lighthouse job is path-scoped, so it had been skipped on every recent push to main — main's green said nothing about it, and the change could not be attributed from CI alone. Measured both trees instead, on this machine, same Chromium, same byte-identical node_modules: main desktop-root LCP 772 ms route / 244.0 KiB gzip this PR desktop-root LCP 940 ms route / 265.3 KiB gzip So it reproduced, and it was this branch's. Twenty-one kilobytes for a seventeenth mode is not a seventeenth mode's worth of code. The cause: `src/app/layout.tsx` mounts the Supabase auth provider on every page, and that provider clears the On Call cache on sign-out and on an account switch. It did so by importing `clearOnCallEntryCache` from `entry-store`, which pulls in `entry-model` and its six per-section Zod schemas. Anyone opening the home page downloaded the entire On Call domain model so that signing out could remove one localStorage key. The key, its change event and the clear function now live in `entry-cache-keys.ts`, which imports nothing. `entry-store` re-exports them, so no call site changed. after desktop-root LCP 873 ms route / 244.2 KiB gzip +0.2 KiB against main, which is what a seventeenth mode should cost. `tests/on-call-root-bundle-isolation.test.ts` is the guard, because nothing else would notice the import creeping back: it is one line, it typechecks, and every other test passes. The test also pins the new module to zero imports — anything added there is added to every page in the app. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LL9gE67N6yorjPcssuK4bC
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_d9bcf7a2-a8fb-4491-8049-d2a1a7e136f2) |
Two browser checks failed. Both were real, and together they exposed that five of the mode's six pages could not be reached from anywhere in the app. `ui-mode-nav-density` failed at all nine width points. Not because six labels clipped — the diagnosis the spec and I had both prepared for — but because the bar was never there to measure. Every On Call section route is an information page (`isInformationPage`), so `PageSecondaryNavigation` returns null on all six, and the destinations the mode registered in the shared header bar could never be drawn. The registration described navigation the app does not render. Removing it made the real defect visible: `route-reachability` immediately named `/on-call/playbook`, `/referrals`, `/orientation`, `/education` and `/logistics` as orphan routes. They always had been. The registry had been satisfying that guard with links nothing ever painted, so a reader could open Contacts and have no way to reach the Playbook except by typing the URL. So On Call leaves the mode-nav registry — it is the ninth mode that registers nothing — and gains an in-flow section strip in `OnCallSectionPage`: six links, current one marked `aria-current="page"`, scrolling inside its own container so the page never scrolls sideways. Deliberately in flow and not a second sticky header: `OnCallNavHeader` is this mode's single phone-header collapse owner and AGENTS.md forbids stacking another fixed bar under it. The six links are written out one by one rather than mapped. The reachability guard resolves `<Link href="/literal">` statically, so an interpolated path — or one passed through a wrapper component's prop, which I tried first — leaves the routes reading as orphans again. Verbose, but the verbosity is the point. `ui-smoke`'s mode menu still counted 16 modes. Verified: `ui-mode-nav-density` 57 passed, the mode-menu smoke test passed, and the offline suite is 1 failed / 15,147 passed — the one failure being the stale drift manifest, unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LL9gE67N6yorjPcssuK4bC
The section strip lists its six links one by one, so the array it used to map over is no longer read. The pre-push static guard caught it; my own eslint run had not, because I checked before switching from the mapped form. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LL9gE67N6yorjPcssuK4bC
|
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_e6abd411-e7ad-45f0-9726-70dc3e5ae505) |
The final block of "app mode menu supports keyboard navigation without removed prototype modes" focused the Answer option and pressed Tab without waiting for the menu's own open-time autofocus to land, and without asserting focus had arrived. Opening the menu focuses the search box on a requestAnimationFrame. When that frame lands after Playwright's .focus() call, Tab fires from the search box instead of the option. Measured behaviour in that state: focus moves to the first option, which is inside the mode-menu wrapper, so the wrapper's onBlur never sees a focus-leave and the menu correctly stays open. The assertion then times out ten seconds later with no indication of why. On desktop only a Tab keydown on an option itself closes the menu. Wait for the autofocus to settle, then prove the option holds focus before pressing the key whose behaviour depends on it. The same test's first block already guards both; only this block omitted them. Verified the search box takes focus on reopen 6/6, so the settle-wait is a sound synchronisation point rather than another race. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LL9gE67N6yorjPcssuK4bC
…m' into claude/on-call-mode-health-n17dvm
…m' into claude/on-call-mode-health-n17dvm
Resolves the conflict in tests/ui-smoke.spec.ts. Main independently reached the same fix for the app mode menu Tab assertion - the same two focus preconditions, the same reopenedAnswerMode binding - so only the explanatory comment differed. Kept main's wording; the assertions are identical on both sides. supabase/drift-manifest.json is the operator-supplied regeneration from CI run 33888898898, which replayed supabase/schema.sql on a Docker-capable runner. Verified before landing: its schema_sha256 (a94a6471...) equals the value tests/drift-detection.test.ts computes from this tree's schema.sql, and its snapshot genuinely carries on_call_entries rather than only a bumped hash. tests/drift-detection.test.ts now passes 21/21. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LL9gE67N6yorjPcssuK4bC
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_6506d643-5e6a-4ce6-8a1b-c0b9d38a1aa9) |
|
Correction: I was wrong to attribute the LCP regression to #2600My previous comment blamed the desktop The measurement. Two production builds, same machine, same browser binary, same measurement harness, differing only in whether #2600 is present:
A 0.2 ms difference, the same LCP element, and the same phase split. #2600 costs nothing measurable on this route, and neither does On Call. What the phase breakdown also shows: the LCP element on desktop What the CI failure most likely is. The budget triggers only when a metric exceeds both +20% and +100 ms, so against a 786 ms baseline the trigger sits at ~943 ms. CI measured 962 ms — about 19 ms, or 2%, past the trigger. Earlier heads on this same branch passed the same gate. A route sitting that close to its own trigger will cross it on a slow runner without any code changing, and Caveat, stated rather than glossed: the local browser here is HeadlessChrome 141; CI pins 151, and What I am doing. Re-running the failed job once — the single re-run this PR has not yet spent, now justified by evidence rather than hope. Worth recording separately: desktop Generated by Claude Code |
… fifteen SPEC §3 and DECISIONS §C5 both still said "fifteen modes". The registry in src/lib/app-modes.ts declares seventeen: sources landed in #2510 and on-call in #2608. The 2026-09-02 repository audit confirmed both strings (L81/L94) and deliberately left them for the design-system owner rather than correcting them in a documentation-only commit. The claim these two lines make is unchanged and still holds — three semantically-named identity families cover every mode, with no fourth family — so only the count moves. No inbound link targets the C5 heading, and no test or script pins either phrase. Historical records keep their original numbers: the audit findings and the superseded plans under docs/superpowers/ describe what was true when they were written. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LL9gE67N6yorjPcssuK4bC
Summary
Adds a seventeenth app mode,
on-call, giving a junior doctor one place for the operational knowledge a shift needs: orientation manuals, role-based contacts, referral pathways, teaching, an escalation playbook, and site logistics.All fourteen planned tasks have landed: the domain model and twelve-month freshness rule, the
on_call_entriestable, the owner-scoped repository and API routes, registration across every mode-keyed map, navigation, the route tree and shared section shell, the offline entry cache, all six section UIs, in-app entry editing, cross-section search, the printable essentials card, and the generated artefacts and docs.Design and plan travel with the code:
docs/superpowers/specs/2026-09-04-on-call-mode-design.mdanddocs/superpowers/plans/2026-09-04-on-call-mode.md.Decisions worth a reviewer's attention:
public.on_call_entriesfollows theclinical_registry_recordstemplate —owner_id NOT NULL, service-role-only RLS, application-layer owner scoping on the same fluent chain socheck:owner-scopecan prove it. This table has no public state by design and must never gain one.What the reviews changed
Two independent reviews ran against this branch, and both found real defects. Recording them because the diff alone does not show what was nearly shipped.
The whole-branch review found that only Contacts was wired to the entry store. The five other list components were imported by their own tests and by nothing else in
src/, so those five pages reported "no entries yet" regardless of what the owner had saved, offered no way to add anything, and left the Playbook's "no local guideline linked" state unreachable — an entry would appear in search and be absent from its own page. Each section id now resolves to its list component through one exhaustive switch with nodefaultarm, so a seventh section is a compile error rather than another silently blank page. The same review also caught an account switch that left the previous user's cached entries in place (sign-out cleared them; switching accounts did not, and a shared ward computer switches accounts without signing out), a freshness check that failed open on an unparseable date and so printed such an entry on the card, a storage-blocked browser reporting an empty hub after a successful fetch, a stale referral whose marker was hidden until its disclosure was expanded, and three cards that hand-rolled the sharedcardSurfacerecipe withoutforced-colors:border.The per-task review found search matching case-sensitively — every haystack handed to
rankCatalogRecordsmust already be normalised, and Orientation carries its content inbodyalone, so it was effectively unsearchable — plus a result count reading "3 on Call", an empty account told to "check the spelling", and the card claiming "No number on file" against playbook steps that have no number by design.Two test fixtures authored clinical content (a temperature threshold, an escalation rule). Both are now content-free: a fixture is how such content usually enters a repository.
Navigation: On Call is not a mode-nav mode
Worth flagging because an earlier revision of this branch got it wrong in a way that hid a real defect.
All six On Call routes are information pages, and
PageSecondaryNavigationreturns null for those, soRegistryModeNavnever mounts on any of them. The branch initially registered On Call inMODE_NAV_ADOPTED_MODESwith a six-itemextendeddensity profile, and the phone density spec then failed — not because six labels clipped, but because the bar was never in the DOM to measure. A four-label fallback would have failed identically.So the registration is removed:
"on-call": []in the secondary-navigation registry, no density profile, no icon entries. Removing it exposed what the registration had been masking — the registry was satisfyingroute-reachabilitywith links nothing ever drew, so five On Call pages had never been reachable in the app. They are now reached by six literal<Link href="/on-call/...">elements in the section switcher, literal because the guard resolves hrefs statically and neither an interpolated path nor a wrapper component takinghrefas a prop counts.Verification
npm run verify:pr-localFull offline unit suite on the current head: 16,800 passed, 0 failed across 1,145 files. Lint and whole-project typecheck clean. A real production build exits 0 and emits all nine On Call pages (
/on-call, six sections,/on-call/search,/on-call/card) plus the three API routes; typecheck alone does not catch Route Handler export violations, so the build is run rather than inferred.supabase/drift-manifest.jsonhas been regenerated and committed.npm run drift:manifestneeds the digest-pinned Postgres image, and the authoring environment's egress policy denies the registry blob fetch from every mirror tried as well as the Actions artifact download, so the manifest came from the CI job that regenerates it on a Docker-capable runner (run 33888898898) and was supplied by the operator. It was checked before landing rather than trusted: itsschema_sha256is byte-equal to the valuetests/drift-detection.test.tsderives from this tree'ssupabase/schema.sql, and its snapshot genuinely carrieson_call_entriesrather than only a bumped hash — updating the hash alone would have made the gate compare the live clinical database against a baseline without the new table, which is green and wrong.tests/drift-detection.test.tspasses 21/21.Three suites (
privacy-readiness-contract,clinical-hazard-controls,rag-plan-package-parity) initially failed in the authoring environment because its clone was shallow and they resolve real commit SHAs. They pass unchanged with full history; nothing in them needed fixing.Browser coverage runs in CI, which executes the same suite. One fix here is worth naming: the app mode menu keyboard test raced its own menu. Opening the menu schedules the search autofocus in a
requestAnimationFrame; focusing an option before that frame ran let the autofocus steal focus back into the search box, where Tab is not a dismiss key, so the menu stayed open and the assertion timed out ten seconds later. Measured in a browser rather than guessed: from the search box, Tab moves focus to the first option, which is inside the menu wrapper, so the wrapper'sonBlurnever sees a focus-leave. The test now waits for the autofocus to settle and asserts the option holds focus before pressing the key whose behaviour depends on it. Main reached the same fix independently, which is what the merge conflict intests/ui-smoke.spec.tswas; the assertions were identical on both sides and only the comment differed.Known and deliberately deferred:
linked_document_idscarries no foreign key, so a deleted document leaves an id pointing at nothing. The behaviour is conservative — it reads as "no local guideline linked" rather than a dangling reference — and adding the constraint means a second migration on a PR that already carries one.Risk and rollout
Merge decision: merge only inside an approved window, per AGENTS.md "Supabase project safety". Auto-merge must not be armed on a migration-bearing PR; it was armed earlier on this PR and the repository owner has since disabled it.
Clinical Governance Preflight
Clinical KB Database(sjrfecxgysukkwxsowpy)How each was satisfied: the playbook and orientation summaries are specified and tested to link source documents rather than restate them, and an unresolvable link reads as "no local guideline linked" rather than promoting a scenario to "has guidance"; the mode stores no patient information and the spec records that as a boundary; the Supabase target is unchanged; the API resolves the owner from the validated session and never from a request body, with the service-role client confined to the server, and the cached entries are now cleared on an account switch as well as on sign-out; demo fixtures are obviously synthetic and every test fixture was rewritten to carry no clinical claim; stale entries surface loudly, are excluded from the printable card, and an unreadable date fails to stale; and no clinical decision-support behaviour is introduced — this mode is operational reference and is specified never to produce clinical guidance of its own.
🤖 Generated with Claude Code
https://claude.ai/code/session_01LL9gE67N6yorjPcssuK4bC