From ced420180be8e23a9b76baab1dab651644d1a024 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 12 Nov 2026 04:53:11 +0000 Subject: [PATCH 01/10] Fix the 10 to 12 November PR review findings (D-159) Design: New referral chip phone-only on the ward top (D-116); Home's Waiting in ED strip scrolls sideways on a tablet instead of cutting words; chip rows let the page scroll (pan-x pan-y, D-144 follow-up); DESIGN-SYSTEM's page header rule names the v6 quick-link chip row. Eight rows then Show more on the Night view, Capacity intake and closed-beds cards, Leave and handover lists; Leave on the band hero; icon-only Print and Export on phones; kit control sizes; shorter transport choices; outage catch-up labels say what they open. Behaviour: one ready count with named holds everywhere (D-115); one ended Form 5A/5B wording helper; uptime raises the sign-in or workspace issue before failing; a trusted proxy list other than * is ignored with a warning; Home's loading hold uses opacity and yields to focus instead of visibility hidden. Records D-159 and fixes the D-79 boundary sentence. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxPPxoiThczWiMzXnymuUN --- .github/workflows/backend-uptime.yml | 15 +- docs/FACTS.md | 4 +- docs/design/DESIGN-SYSTEM.md | 42 +- docs/hosting.md | 4 + docs/ward-flow/OWNER-RULINGS.md | 642 +++++++++--------- docs/ward-flow/decisions.md | 15 +- .../SETTINGS-AND-SECRETS-INVENTORY.md | 49 +- .../alerts/use-broadcast-audience.ts | 15 +- .../board/ward-board-detail-panel.tsx | 4 +- .../ward-management/board/ward-peek.tsx | 9 +- .../capacity/capacity-closed-beds-review.tsx | 33 +- .../capacity/capacity-intake-windows-card.tsx | 25 +- .../capacity/service-capacity-tracker.ts | 7 +- .../community/community-demo-cohort.ts | 4 +- .../community/community-screen-modals.tsx | 4 +- .../team-2026/midland-top.module.css | 3 +- .../coordinator/home-cards-settled.ts | 5 +- .../coordinator/home-hero.module.css | 24 +- .../coordinator/home.module.css | 10 +- .../coordinator/shortlist-panel.tsx | 15 +- .../discharges/discharge-departure-field.tsx | 4 +- .../ward-management/handover-own-work.tsx | 25 +- .../handover/handover-open-promises.tsx | 35 +- .../leave/leave-screen.module.css | 14 - .../ward-management/leave/leave-screen.tsx | 93 ++- .../legal-forms/legal-form-card.tsx | 9 +- .../legal-forms/legal-form-progression.ts | 18 + .../legal-forms/legal-forms-derivations.ts | 11 +- .../legal-forms/legal-forms-export.ts | 10 +- .../legal-forms/legal-forms-screen.tsx | 34 +- .../movements/movements-list-tools.tsx | 12 +- .../movements/movements-screen.tsx | 20 +- .../night/night-view-screen.tsx | 72 +- .../night/night-view.module.css | 2 +- .../patients/patient-glance.tsx | 17 +- .../quick-bar/ward-quick-bar.module.css | 7 +- .../referrals/referral-no-reply-flag.tsx | 9 +- .../search/global-search-preview.tsx | 5 +- .../search/ward-smart-search.ts | 5 +- .../shell-next/next-phone-menu-top.tsx | 4 +- .../shell-next/next-rail-body.tsx | 14 +- .../shell/ward-phone-menu-top.tsx | 4 +- .../ward-management/shell/ward-rail.tsx | 14 +- .../shell/ward-service-bed-alerts.ts | 14 +- .../statistics/statistics-capacity-chart.tsx | 9 +- .../statistics/statistics-screen.tsx | 1 + .../statistics/statistics-service-screen.tsx | 1 + .../ward-management/tools/ward-bed-finder.tsx | 17 +- .../ward-management/tools/ward-bed-summary.ts | 11 +- .../tools/ward-tool-call-log.tsx | 10 +- .../tools/ward-tool-figures.tsx | 8 +- .../tools/ward-tool-find-bed.tsx | 13 +- .../tools/ward-tool-messages.tsx | 21 +- .../ward-admission-checklist.ts | 4 +- .../ward-management/ward-bed-states.ts | 10 +- .../ward-global-search.module.css | 42 +- .../ward-list-export-button.tsx | 7 +- .../ward-list-print-button.tsx | 6 +- .../ward/ward-intake-window-dialog.tsx | 59 +- .../ward/ward-outage-catch-up-card.tsx | 15 +- .../ward/ward-screen-derivations.ts | 19 +- .../ward/ward-screen-top-bar.tsx | 4 +- .../ward/ward-under-18-notice.module.css | 9 + .../ward/ward-under-18-notice.tsx | 11 +- .../pop-search/search-preview.tsx | 5 +- .../ward-redesign/shell/preview-rail-body.tsx | 14 +- src/components/wf/hero-top.module.css | 6 +- src/components/wf/phone-rows.module.css | 6 +- src/lib/demo-code-tries.ts | 29 +- ...sideways-rows-scroll-sideways-only.test.ts | 8 +- tests/front-door-pass.test.ts | 23 +- tests/ward-audit-24-4-counts.dom.test.tsx | 10 +- tests/ward-audit-24-4-counts.test.ts | 6 +- tests/ward-audit-24-4-wording.dom.test.tsx | 2 +- .../ward-audit-m118-ward-fit-surfaces.test.ts | 7 +- tests/ward-audit-m80-scheduled-jobs.test.ts | 19 + tests/ward-audit-planned-reservations.test.ts | 2 + .../ward-audit-remediation-statistics.test.ts | 2 +- tests/ward-board-left-ward.dom.test.tsx | 2 +- .../ward-consolidated-census-20261104.test.ts | 2 +- tests/ward-count-breakdown.test.ts | 2 +- .../ward-d115-named-holds-everywhere.test.ts | 107 +++ ...rd-discharge-record-departure.dom.test.tsx | 2 +- ...ard-executive-statistics-projector.test.ts | 9 +- tests/ward-figures-agree.test.ts | 20 +- .../ward-home-cards-pending-reachable.test.ts | 25 + ...d-legal-form-ended-cto-glance.dom.test.tsx | 28 + .../ward-legal-form-ended-cto-wording.test.ts | 53 ++ tests/ward-never-build-drift-guard.test.ts | 1 + .../ward-outage-catch-up-screens.dom.test.tsx | 2 +- tests/ward-search-answers.test.ts | 4 +- tests/ward-service-bed-alerts.test.ts | 2 +- tests/ward-tools-sheet-helpers.test.ts | 38 +- tests/ward-tools-sheet.dom.test.tsx | 2 +- tests/ward-workflow-audit-regressions.test.ts | 10 +- 95 files changed, 1434 insertions(+), 672 deletions(-) create mode 100644 tests/ward-d115-named-holds-everywhere.test.ts create mode 100644 tests/ward-home-cards-pending-reachable.test.ts create mode 100644 tests/ward-legal-form-ended-cto-glance.dom.test.tsx create mode 100644 tests/ward-legal-form-ended-cto-wording.test.ts diff --git a/.github/workflows/backend-uptime.yml b/.github/workflows/backend-uptime.yml index 0f6eb59ab6..ba3cfd7e43 100644 --- a/.github/workflows/backend-uptime.yml +++ b/.github/workflows/backend-uptime.yml @@ -244,13 +244,17 @@ jobs: exit 0 fi # Hidden database or a monitor account that is not set up: neither raise nor close a - # backend-down issue. A hidden database check must not look green (A7-01). A rate-limited - # or unset workspace check stays a warning only; the monitor-not-set-up issue covers it. + # backend-down issue. A hidden database check must not look green (A7-01), but it is + # reported after any sign-in or workspace failure has raised its issue. A rate-limited or + # unset workspace check stays a warning only; the monitor-not-set-up issue covers it. + hidden="" if [ "$HEALTH" = "unknown" ]; then - echo "::error::Uptime check did not see the database: WARD_FLOW_UPTIME_TOKEN is missing or does not match the backend's value." - exit 1 + hidden="::error::Uptime check did not see the database: WARD_FLOW_UPTIME_TOKEN is missing or does not match the backend's value." + fi + if [ "$state" = "unknown" ]; then + if [ -n "$hidden" ]; then echo "$hidden"; exit 1; fi + exit 0 fi - if [ "$state" = "unknown" ]; then exit 0; fi if [ -z "$open" ]; then gh label create "$LABEL" --color B60205 --description "Hosted backend not answering" --force case "$state" in @@ -264,4 +268,5 @@ jobs: gh issue create --label "$LABEL" --title "Shared backend is not answering" --body "@$OWNER The hosted Ward Flow backend ($BACKEND_URL/healthz) did not answer three times in a row ($run). Shared mode will not load or save until it is back. Check the ward-flow-backend service in Railway. This issue closes itself when the backend answers again." ;; esac fi + if [ -n "$hidden" ]; then echo "$hidden"; fi exit 1 diff --git a/docs/FACTS.md b/docs/FACTS.md index 453ba8874b..facc74efb7 100644 --- a/docs/FACTS.md +++ b/docs/FACTS.md @@ -13,8 +13,8 @@ green. Quote a fact instead of restating it, and it updates itself when the sour | `migration-count` | 18 | Numbered database migrations | `backend/ward-flow/migrations/` | | `ward-route-count` | 61 | `page.tsx` routes under `/ward-flow` | `src/app/ward-flow/` | | `app-route-count` | 65 | Pages and route handlers: the Ward Flow routes plus sign-in and the `/mockups` digest | `src/app/ward-flow/, src/app/ward-flow-sign-in/, src/app/mockups/` | -| `latest-decision` | D-158 | Newest entry in the decision log | `docs/ward-flow/decisions.md` | -| `next-decision` | D-159 | Number to use for the next decision | `docs/ward-flow/decisions.md` | +| `latest-decision` | D-159 | Newest entry in the decision log | `docs/ward-flow/decisions.md` | +| `next-decision` | D-160 | Number to use for the next decision | `docs/ward-flow/decisions.md` | | `node-engine` | >=24.22.1 <25 | Supported Node.js range | `package.json` | | `backend-env-vars` | 33 names, [listed below](#backend-env-vars) | Environment variables the shared backend reads | `backend/ward-flow/*.mjs` | diff --git a/docs/design/DESIGN-SYSTEM.md b/docs/design/DESIGN-SYSTEM.md index 6b9f4cc338..6af40a1b34 100644 --- a/docs/design/DESIGN-SYSTEM.md +++ b/docs/design/DESIGN-SYSTEM.md @@ -116,25 +116,25 @@ The shell is the same on every page. Copy it from the master's newest screens (t Build with `src/components/wf`. Never copy a primitive into a screen, and never write one-off `.tag`, `.chip` or `.pill` CSS. The master's Components tab shows each part in each state. -| Need | Use | Rules | -| --------------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Page header | `Hero` (band), `CheckingFoot` | Title is the answer, five words at most. One strip of figures. At most one on-hero action; on phone the rest go in "…" (Q153). Eyebrow hidden on phone unless it adds facts. | -| Figures | `Stat` strip | Mono, tabular. Keep a strip on one row where it fits; let a title column shrink first (Q290, Q293). | -| A labelled state | `Badge`, `StatusGlyph` | Small tinted tag or glyph. Counts use the count badge. | -| A group of content | `Card` | Only where it is a separate object. Kit card heads keep their title whole (Q237). | -| Actions | `Button` | One filled button per row or panel; the rest are quiet. Unavailable for now: `aria-disabled` with `disabledReason`. Never usable by this desk: not shown. Unwired: "Not wired in this prototype." | -| Choosing one of a few | `choice` (segments, radio group) | Never the browser's own drop-down for a kit choice (Q319). | -| Search | `SearchField` | "/" jumps to it where a page has one search. | -| Dates and times | `DateField` (DD/MM/YYYY), `TimePresets` | Never the browser's date picker. Never default a written time to now (Q120). | -| Long lists | `ShowMoreRow` | About eight rows, then "Show 8 more" / "Show fewer". Never a small scrolling box. | -| Pop-ups and panels | `Sheet`, `Dialog`, `requestConfirm` | See section 5. Leaving with unsaved work: "Leave this page?", "Leave without saving", "Keep editing" (focus on Keep editing). Never `confirm`, `alert` or `prompt`. | -| Activity lists | `Feed`, `SinceYouLookedFold` | One timeline per record. | -| Charts | `Chart` | To scale, from existing figures only, data tokens, Geist axes. | -| Names | `FormalName` | Patients surname first as recorded ("Jenkinwood, Sarah"); staff as written ("Dr S. Chen", Q226). Always in full, wrapping. | -| Missing facts | `MissingValue` | "Not recorded" in ink-3; amber only where the gap blocks the next step. | -| Phone folds and rows | `PhoneFold`, `phoneRows` | Chip rows put the count first and slide with an edge fade. | -| Live changes | `LiveAnnouncer`, `live` | One polite region per page, batched, act-now changes only. | -| Formatting | `format.ts` (`clk`, `dur`, `durMinutes`) | Never re-implement a formatter. | +| Need | Use | Rules | +| --------------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Page header | `Hero` (band), `CheckingFoot` | Title is the answer, five words at most. One strip of figures. At most one on-hero button, the next step's light button; page tools sit in one row of quick-link chips under it, which scrolls sideways on a phone (page tops v6, D-116, D-159). Anything past that goes in "…" (Q153). Eyebrow hidden on phone unless it adds facts. | +| Figures | `Stat` strip | Mono, tabular. Keep a strip on one row where it fits; let a title column shrink first (Q290, Q293). | +| A labelled state | `Badge`, `StatusGlyph` | Small tinted tag or glyph. Counts use the count badge. | +| A group of content | `Card` | Only where it is a separate object. Kit card heads keep their title whole (Q237). | +| Actions | `Button` | One filled button per row or panel; the rest are quiet. Unavailable for now: `aria-disabled` with `disabledReason`. Never usable by this desk: not shown. Unwired: "Not wired in this prototype." | +| Choosing one of a few | `choice` (segments, radio group) | Never the browser's own drop-down for a kit choice (Q319). | +| Search | `SearchField` | "/" jumps to it where a page has one search. | +| Dates and times | `DateField` (DD/MM/YYYY), `TimePresets` | Never the browser's date picker. Never default a written time to now (Q120). | +| Long lists | `ShowMoreRow` | About eight rows, then "Show 8 more" / "Show fewer". Never a small scrolling box. | +| Pop-ups and panels | `Sheet`, `Dialog`, `requestConfirm` | See section 5. Leaving with unsaved work: "Leave this page?", "Leave without saving", "Keep editing" (focus on Keep editing). Never `confirm`, `alert` or `prompt`. | +| Activity lists | `Feed`, `SinceYouLookedFold` | One timeline per record. | +| Charts | `Chart` | To scale, from existing figures only, data tokens, Geist axes. | +| Names | `FormalName` | Patients surname first as recorded ("Jenkinwood, Sarah"); staff as written ("Dr S. Chen", Q226). Always in full, wrapping. | +| Missing facts | `MissingValue` | "Not recorded" in ink-3; amber only where the gap blocks the next step. | +| Phone folds and rows | `PhoneFold`, `phoneRows` | Chip rows put the count first and slide with an edge fade. | +| Live changes | `LiveAnnouncer`, `live` | One polite region per page, batched, act-now changes only. | +| Formatting | `format.ts` (`clk`, `dur`, `durMinutes`) | Never re-implement a formatter. | ### Icons and status marks @@ -249,7 +249,7 @@ The shell, tokens and kit keep Ward Flow feeling like one product. The variation - The shell: sidebar, app bar, band hero frame, phone dock, toasts, footer. - Tokens: no new colours, sizes, radii or fonts. Tone semantics (red is act now). - Kit primitives: buttons, badges, choices, fields, sheets, dialogs, Show more. -- House rules: 12px floor, 40px phone controls with a 48px cover, 30px desktop tabs, no coloured side edges, one filled button per panel, one on-hero action. +- House rules: 12px floor, 40px phone controls with a 48px cover, 30px desktop tabs, no coloured side edges, one filled button per panel, one on-hero button plus one quick-link chip row (D-159). - Words, names, times, safety wording and synthetic data. - The five-second answer in the hero title. @@ -282,7 +282,7 @@ Each page may take **one signature part**: a bespoke visual that shows its own d - [ ] Tokens only; kit parts only; no one-off pills. - [ ] Icons from the table in section 4 at 1.75 stroke; status marks from `StatusGlyph`, no typed symbols, and only where they change the next action. - [ ] No coloured side edges; no text under 12px; phone controls 40px; desktop tabs 30px. -- [ ] One on-hero action, one filled button per panel, eight rows then Show more. +- [ ] One on-hero button plus one quick-link chip row (D-159), one filled button per panel, eight rows then Show more. - [ ] Words from section 6; patient names surname first, staff names as written, both in full; times as typed. - [ ] At most one signature part, inside the limits in section 8. - [ ] Synthetic data only; no names or typed text in URLs, screenshots or logs. diff --git a/docs/hosting.md b/docs/hosting.md index f2a001205f..88034747fe 100644 --- a/docs/hosting.md +++ b/docs/hosting.md @@ -110,6 +110,10 @@ Backend settings (Railway dashboard): headers and the backend cannot be reached directly. Reverify those conditions before relying on per-address sign-in limits; prefer explicit proxy socket addresses, or no forwarded-header trust when unverified. This document does not establish the current deployed ingress boundary. + On the web app (Next) service the same setting only gates the demo-code try limit, and only `*` + applies there: the session route has no socket address to match, so a list of IP addresses is + ignored with one `ward_front_door_config_warning` log line and the D-147 per-address keying + stays. - Recorded watch paths: `/backend/ward-flow/**`, `/src/components/ward-management/**` and `/src/lib/**` (the engine bundles both, including the synthetic-data guard), `/package.json`, `/package-lock.json` and `/tsconfig.json`. The maintained source contract in diff --git a/docs/ward-flow/OWNER-RULINGS.md b/docs/ward-flow/OWNER-RULINGS.md index 368a3c6c78..9239cf1fc8 100644 --- a/docs/ward-flow/OWNER-RULINGS.md +++ b/docs/ward-flow/OWNER-RULINGS.md @@ -9,7 +9,7 @@ Scanned `docs/ward-flow/decisions.md`, `docs/ward-flow/owner-*.md` and `docs/his One file matches `*owner*` but not `owner-*` and is deliberately excluded: `how-to-write-to-the-owner.md` — it is instructions for writing TO the owner, not a record of what he ruled. -**720 rulings/items extracted, across 43 of 44 files.** +**721 rulings/items extracted, across 43 of 44 files.** **0 file(s) UNPARSED** — no recognised ruling structure found; listed, not dropped. See below. **1 file(s) recorded as holding no ruling of their own**, each with its reason. See below. @@ -32,7 +32,7 @@ line, in the order discovered — confirm which instance you mean before citing --- -## ID index — 470 distinct IDs +## ID index — 471 distinct IDs So an ID like `D-9` or `O-17.1` can be looked up directly, without knowing which file it lives in. Bare numbered rulings (files that number "1., 2., 3. …" with no owner-issued letter code) are @@ -72,263 +72,264 @@ safe global key across 30 files. | `D-1` ⚠️ **3 occurrences in 3 files — not unique** | | | | ↳ | `../history/archive/dated-notes/owner-decisions-2026-09-12-four-answers.md`:16 | D-1 · Legal forms: split the list in two | | ↳ | `../history/archive/dated-notes/owner-decisions-2026-09-1x.md`:85 | D-1 — 🔴 The screen is called **Patient**, not "Patient Now" | -| ↳ | `decisions.md`:187 | D-1. Ward Flow is local only, with no linked repository (Replaced by D-27) | +| ↳ | `decisions.md`:188 | D-1. Ward Flow is local only, with no linked repository (Replaced by D-27) | | `D-2` ⚠️ **3 occurrences in 3 files — not unique** | | | | ↳ | `../history/archive/dated-notes/owner-decisions-2026-09-12-four-answers.md`:57 | D-2 · Raise the three unbindable table thresholds | | ↳ | `../history/archive/dated-notes/owner-decisions-2026-09-1x.md`:108 | D-2 — 🔴 Communication: BUILD IT. The system must be able to tell someone something. | -| ↳ | `decisions.md`:197 | D-2. Railway is disconnected | +| ↳ | `decisions.md`:198 | D-2. Railway is disconnected | | `D-3` ⚠️ **2 occurrences in 2 files — not unique** | | | | ↳ | `../history/archive/dated-notes/owner-decisions-2026-09-1x.md`:215 | D-3 — 🔴 The 12px floor is ADOPTED. Screen by screen, not as a sweep. | -| ↳ | `decisions.md`:208 | D-3. The PsychSift and Database code is being retired from Ward Flow | +| ↳ | `decisions.md`:209 | D-3. The PsychSift and Database code is being retired from Ward Flow | | `D-3b` | `../history/archive/dated-notes/owner-decisions-2026-09-12-four-answers.md`:77 | D-3b · The data-conditional absence sweep is DEFERRED | | `D-4` ⚠️ **4 occurrences in 3 files — not unique** | | | | ↳ | `../history/archive/dated-notes/owner-decisions-2026-09-12-four-answers.md`:97 | D-4 · The deliberate sub-12px exception stands if its reasoning is sound | | ↳ | `../history/archive/dated-notes/owner-decisions-2026-09-1x.md`:263 | D-4 · WARD LEAD RULING, not the owner's — the Access record's false assurance | | ↳ | `../history/archive/dated-notes/owner-decisions-2026-09-1x.md`:390 | D-4 ADDENDUM, 2026-09-11 — 🔴 the OTHER half of that sentence, found by Lane C after the fix | -| ↳ | `decisions.md`:216 | D-4. Synthetic patients only | +| ↳ | `decisions.md`:217 | D-4. Synthetic patients only | | `D-5` ⚠️ **4 occurrences in 3 files — not unique** | | | | ↳ | `../history/archive/dated-notes/owner-decisions-2026-09-12-routing-and-nine-flags.md`:15 | D-5 · 🔴 A NEW STANDING RULE: PROTECTED-WORK DELETIONS COME TO WARD VERIFIER FIRST | | ↳ | `../history/archive/dated-notes/owner-decisions-2026-09-1x.md`:340 | D-5 · WARD LEAD RULING, not the owner's — gender reaches BOTH bed gates or neither | | ↳ | `../history/archive/dated-notes/owner-decisions-2026-09-1x.md`:431 | D-5 MEASUREMENT, 2026-09-11 — the wiring task handed back, and the arithmetic is starker than I expected | -| ↳ | `decisions.md`:224 | D-5. Parallel threads in separate worktrees, folded one at a time | +| ↳ | `decisions.md`:225 | D-5. Parallel threads in separate worktrees, folded one at a time | | `D-6` ⚠️ **3 occurrences in 3 files — not unique** | | | | ↳ | `../history/archive/dated-notes/owner-decisions-2026-09-12-routing-and-nine-flags.md`:52 | D-6 · 🟢 THE `--t-0` COLLISION — amend the brief in the same change | | ↳ | `../history/archive/dated-notes/owner-decisions-2026-09-1x.md`:472 | D-6 · WARD LEAD RULING — "Not recorded" and "Not yet recorded" are BOTH used, deliberately | -| ↳ | `decisions.md`:235 | D-6. Aboriginal cultural safety review deferred | +| ↳ | `decisions.md`:236 | D-6. Aboriginal cultural safety review deferred | | `D-7` ⚠️ **3 occurrences in 3 files — not unique** | | | | ↳ | `../history/archive/dated-notes/owner-decisions-2026-09-12-routing-and-nine-flags.md`:72 | D-7 · 🟢 THE REMAINING SEVEN ARE FLAGGED TO WARD LEAD | | ↳ | `../history/archive/dated-notes/owner-decisions-2026-09-1x.md`:499 | D-7 · WARD LEAD RULING — the Patient screen SHOWS gender, and the addition is logged as an addition | -| ↳ | `decisions.md`:243 | D-7. The first four milestones, in this order | +| ↳ | `decisions.md`:244 | D-7. The first four milestones, in this order | | `D-8` ⚠️ **3 occurrences in 3 files — not unique** | | | | ↳ | `../history/archive/dated-notes/owner-decisions-2026-09-12-fold-and-authorisation.md`:9 | D-8 · 🔴 "FOLD INTO MAIN" ALWAYS MEANS THE LOCAL WARD FLOW LINE | | ↳ | `../history/archive/dated-notes/owner-decisions-2026-09-1x.md`:545 | D-8 · WARD LEAD RULING — the announced marker says "invented NAMES", never "invented people" | -| ↳ | `decisions.md`:255 | D-8. Josh's standing rulings, numbered R1 to R16, for every session and tool | +| ↳ | `decisions.md`:256 | D-8. Josh's standing rulings, numbered R1 to R16, for every session and tool | | `D-9` ⚠️ **3 occurrences in 3 files — not unique** | | | | ↳ | `../history/archive/dated-notes/owner-decisions-2026-09-12-fold-and-authorisation.md`:51 | D-9 · 🟢 THE MENTAL HEALTH ACT AUTHORISATION GATE IS OVERRIDABLE — with the owner's clinical reason | | ↳ | `../history/archive/dated-notes/owner-decisions-2026-09-1x.md`:589 | D-9 · WARD LEAD RULING — the referral board's table MAY scroll sideways; the missing thing is the affordance | -| ↳ | `decisions.md`:287 | D-9. Josh's rulings of 25 September, 11:43 to 12:00 (R17 to R21) | +| ↳ | `decisions.md`:288 | D-9. Josh's rulings of 25 September, 11:43 to 12:00 (R17 to R21) | | `D-10` ⚠️ **3 occurrences in 3 files — not unique** | | | | ↳ | `../history/archive/dated-notes/owner-decisions-2026-09-12-fold-and-authorisation.md`:91 | D-10 · ⏸️ THE SMALL-TEXT RAISE IS DEFERRED — with the triggers written down, because a deferral without one is a drop | | ↳ | `../history/archive/dated-notes/owner-decisions-2026-09-1x.md`:618 | D-10 · WARD LEAD RULING — the overflow affordance is a SENTENCE first and a measured border second, not the standard's shade | -| ↳ | `decisions.md`:301 | D-10. Gate build type check (R22) | +| ↳ | `decisions.md`:302 | D-10. Gate build type check (R22) | | `D-11` ⚠️ **3 occurrences in 3 files — not unique** | | | | ↳ | `../history/archive/dated-notes/owner-decisions-2026-09-12-fold-and-authorisation.md`:129 | D-11 · 🔴 TWO LEDGERS IS CORRECT AND INTENDED — WARD FLOW TASKS GO ON THE WARD FLOW LEDGER, AND THE MAIN PROJECT'S IS DISREGARDED FOR THIS PROJECT | | ↳ | `../history/archive/dated-notes/owner-decisions-2026-09-1x.md`:685 | D-11 · A-1 — the half-written referral history WARNS, and is never stored | -| ↳ | `decisions.md`:310 | D-11. Rulings of 25 September, 12:41 (R23 to R25) | +| ↳ | `decisions.md`:311 | D-11. Rulings of 25 September, 12:41 (R23 to R25) | | `D-12` ⚠️ **2 occurrences in 2 files — not unique** | | | | ↳ | `../history/archive/dated-notes/owner-decisions-2026-09-1x.md`:704 | D-12 · A-2 — the referrer is the RECORDED SOURCE, and a person only if the record carries one | -| ↳ | `decisions.md`:326 | D-12. Rulings of 25 September, 12:43 (R26 to R31) | +| ↳ | `decisions.md`:327 | D-12. Rulings of 25 September, 12:43 (R26 to R31) | | `D-13` ⚠️ **2 occurrences in 2 files — not unique** | | | | ↳ | `../history/archive/dated-notes/owner-decisions-2026-09-1x.md`:720 | D-13 · A-3 and A-4 — both resolve FOR THE CODE; the drawings change | -| ↳ | `decisions.md`:345 | D-13. Tooling tests in batch gates (R32) | +| ↳ | `decisions.md`:346 | D-13. Tooling tests in batch gates (R32) | | `D-14` ⚠️ **2 occurrences in 2 files — not unique** | | | | ↳ | `../history/archive/dated-notes/owner-decisions-2026-09-1x.md`:739 | D-14 · A-6 and A-8 — ADD THE LINK, AND MAKE THE GUARD ENFORCE IN THE SAME CHANGE. Never one without the other. | -| ↳ | `decisions.md`:355 | D-14. Standing defaults: decide small things without asking | +| ↳ | `decisions.md`:356 | D-14. Standing defaults: decide small things without asking | | `D-15` ⚠️ **2 occurrences in 2 files — not unique** | | | | ↳ | `../history/archive/dated-notes/owner-decisions-2026-09-1x.md`:778 | D-15 · A-7 — the figure NAMES ITS OWN CLOCK, or it does not ship | -| ↳ | `decisions.md`:367 | D-15. Rulings of 25 September, 13:26 (R33 to R35) | +| ↳ | `decisions.md`:368 | D-15. Rulings of 25 September, 13:26 (R33 to R35) | | `D-16` ⚠️ **2 occurrences in 2 files — not unique** | | | | ↳ | `../history/archive/dated-notes/owner-decisions-2026-09-1x.md`:799 | D-16 · WARD LEAD RULING — the unwired primary actions use §8.6's EXISTING sentence; nothing new is invented | -| ↳ | `decisions.md`:380 | D-16. Legal overrides and form receipts, 25 September 13:51 UTC | +| ↳ | `decisions.md`:381 | D-16. Legal overrides and form receipts, 25 September 13:51 UTC | | `D-17` ⚠️ **2 occurrences in 2 files — not unique** | | | | ↳ | `../history/archive/dated-notes/owner-decisions-2026-09-1x.md`:843 | D-17 · WARD LEAD RULING — A-8: FD-23 wins. The Patient screen never shows where else a patient has been. | -| ↳ | `decisions.md`:393 | D-17. Morning roll-up time is Josh's own default (25 September) | +| ↳ | `decisions.md`:394 | D-17. Morning roll-up time is Josh's own default (25 September) | | `D-18` ⚠️ **2 occurrences in 2 files — not unique** | | | | ↳ | `../history/archive/dated-notes/owner-decisions-2026-09-1x.md`:878 | D-18 · WARD LEAD RULING — the referral query contract carries MODEL values; and `gp` is a question for the owner, not a mapping for me | -| ↳ | `decisions.md`:404 | D-18. The privacy lock stops saving only for typed text (25 September) | +| ↳ | `decisions.md`:405 | D-18. The privacy lock stops saving only for typed text (25 September) | | `D-19` ⚠️ **2 occurrences in 2 files — not unique** | | | | ↳ | `../history/archive/dated-notes/owner-decisions-2026-09-1x.md`:922 | D-19 · WARD LEAD RULING — "something is already open for this person" is PERMITTED; "where" never is | -| ↳ | `decisions.md`:415 | D-19. Rulings of 25 September, evening (walkthrough, leave beds, PsychSift, gender display) | +| ↳ | `decisions.md`:416 | D-19. Rulings of 25 September, evening (walkthrough, leave beds, PsychSift, gender display) | | `D-20` ⚠️ **3 occurrences in 2 files — not unique** | | | | ↳ | `../history/archive/dated-notes/owner-decisions-2026-09-1x.md`:967 | D-20 · WARD LEAD RULING — the drawing wins; the vocabulary anchor was never the heading | | ↳ | `../history/archive/dated-notes/owner-decisions-2026-09-1x.md`:1010 | D-20-REVISED · the drawing renames BOTH, so neither side is taken whole | -| ↳ | `decisions.md`:445 | D-20. One high-contrast colour check retired (26 September) | +| ↳ | `decisions.md`:446 | D-20. One high-contrast colour check retired (26 September) | | `D-21` ⚠️ **2 occurrences in 2 files — not unique** | | | | ↳ | `../history/archive/dated-notes/owner-decisions-2026-09-1x.md`:1058 | D-21 · WARD LEAD RULING — tasks 10 and 12 are CLOSED, not deferred, and not built as callerless modules | -| ↳ | `decisions.md`:458 | D-21. Coordinator bed board: all nine recommended answers (26 September) | +| ↳ | `decisions.md`:459 | D-21. Coordinator bed board: all nine recommended answers (26 September) | | `D-22` ⚠️ **2 occurrences in 2 files — not unique** | | | | ↳ | `../history/archive/dated-notes/owner-decisions-2026-09-1x.md`:1084 | D-22 · SHAPE 2 — the landmark label and the heading stop being two strings | -| ↳ | `decisions.md`:470 | D-22. No legal limits in hour rules; the rest are labelled defaults (26 September) | +| ↳ | `decisions.md`:471 | D-22. No legal limits in hour rules; the rest are labelled defaults (26 September) | | `D-23` ⚠️ **2 occurrences in 2 files — not unique** | | | | ↳ | `../history/archive/dated-notes/owner-decisions-2026-09-1x.md`:1121 | D-23 · The bed board passes an empty leave-bed list into three capacity figures, and says nothing | -| ↳ | `decisions.md`:486 | D-23. Leave-bed fields kept; patient search stops matching by ward (26 September) | +| ↳ | `decisions.md`:487 | D-23. Leave-bed fields kept; patient search stops matching by ward (26 September) | | `D-24` ⚠️ **2 occurrences in 2 files — not unique** | | | | ↳ | `../history/archive/dated-notes/owner-decisions-2026-09-1x.md`:1142 | D-24 · The raw `1.2` line-height — token it, and accept that community no longer matches | -| ↳ | `decisions.md`:500 | D-24. Hour-rules morning list: all A (26 September) | +| ↳ | `decisions.md`:501 | D-24. Hour-rules morning list: all A (26 September) | | `D-25` ⚠️ **2 occurrences in 2 files — not unique** | | | | ↳ | `../history/archive/dated-notes/owner-decisions-2026-09-1x.md`:1161 | D-25 · Command's `deadlineLine` sentence is not built — and this DEFERS the R-5 question rather than answering it | -| ↳ | `decisions.md`:524 | D-25. The lone-patient check counts free beds on both matching paths (26 September) | +| ↳ | `decisions.md`:525 | D-25. The lone-patient check counts free beds on both matching paths (26 September) | | `D-26` ⚠️ **2 occurrences in 2 files — not unique** | | | | ↳ | `../history/archive/dated-notes/owner-decisions-2026-09-1x.md`:1184 | D-26 · `edPressure` becomes a projection of `edHomeSummaries` — the two screens stop being able to disagree | -| ↳ | `decisions.md`:532 | D-26. Select fold checks by changed risk (27 September) | +| ↳ | `decisions.md`:533 | D-26. Select fold checks by changed risk (27 September) | | `D-27` ⚠️ **2 occurrences in 2 files — not unique** | | | | ↳ | `../history/archive/dated-notes/owner-decisions-2026-09-1x.md`:1213 | D-27 · The word for a free bed is READY — and the ruling is about the WORD, not the identifier | -| ↳ | `decisions.md`:550 | D-27. Ward Flow uses its dedicated repository (28 September) | -| `D-28` | `decisions.md`:563 | D-28. Design Modernization & Anti-Box Architecture (30 September 2026) | -| `D-29` | `decisions.md`:577 | D-29. Act time limits shown as a labelled synthetic demo (4 October 2026) | +| ↳ | `decisions.md`:551 | D-27. Ward Flow uses its dedicated repository (28 September) | +| `D-28` | `decisions.md`:564 | D-28. Design Modernization & Anti-Box Architecture (30 September 2026) | +| `D-29` | `decisions.md`:578 | D-29. Act time limits shown as a labelled synthetic demo (4 October 2026) | | `D-30` ⚠️ **2 occurrences in 2 files — not unique** | | | | ↳ | `../history/archive/dated-notes/owner-decisions-2026-09-1x.md`:1238 | D-30 · `Unit.forensic` is a WARD flag, and every rendered string says so | -| ↳ | `decisions.md`:592 | D-30. Central Coordinator Approval for Inter-Ward Bed Transfers (6 October 2026) | +| ↳ | `decisions.md`:593 | D-30. Central Coordinator Approval for Inter-Ward Bed Transfers (6 October 2026) | | `D-31` ⚠️ **2 occurrences in 2 files — not unique** | | | | ↳ | `../history/archive/dated-notes/owner-decisions-2026-09-1x.md`:1367 | D-31 · The plan's disposition — delete one half, re-derive the other | -| ↳ | `decisions.md`:599 | D-31. Gender-Diverse Bed Placement Policy & Clinical Override Safety (6 October 2026) | +| ↳ | `decisions.md`:600 | D-31. Gender-Diverse Bed Placement Policy & Clinical Override Safety (6 October 2026) | | `D-32` ⚠️ **2 occurrences in 2 files — not unique** | | | | ↳ | `../history/archive/dated-notes/owner-decisions-2026-09-1x.md`:1289 | D-32 · D-6 is restated: ONE WORDING PER STATE, and the states are named | -| ↳ | `decisions.md`:606 | D-32. FIFO Waiting Time with Clinical Triage Badging vs Algorithmic Urgency Sorting (6 October 2026) | +| ↳ | `decisions.md`:607 | D-32. FIFO Waiting Time with Clinical Triage Badging vs Algorithmic Urgency Sorting (6 October 2026) | | `D-33` ⚠️ **2 occurrences in 2 files — not unique** | | | | ↳ | `../history/archive/dated-notes/owner-decisions-2026-09-1x.md`:1322 | D-33 · "Nothing outstanding" does three jobs, and only one of them may keep the words | -| ↳ | `decisions.md`:613 | D-33. Separation of Local Ward Shift Notes from Statewide Coordinator Telemetry (6 October 2026) | +| ↳ | `decisions.md`:614 | D-33. Separation of Local Ward Shift Notes from Statewide Coordinator Telemetry (6 October 2026) | | `D-34` ⚠️ **2 occurrences in 2 files — not unique** | | | | ↳ | `../history/archive/dated-notes/owner-decisions-2026-09-1x.md`:1347 | D-34 · A hypothetical destination row may not use obligation vocabulary at all | -| ↳ | `decisions.md`:620 | D-34. Acute Medical Deterioration Bed Release and Escalation Protocol (6 October 2026) | +| ↳ | `decisions.md`:621 | D-34. Acute Medical Deterioration Bed Release and Escalation Protocol (6 October 2026) | | `D-35` ⚠️ **2 occurrences in 2 files — not unique** | | | | ↳ | `../history/archive/dated-notes/owner-decisions-2026-09-1x.md`:1388 | D-35 · The ED pressure strip does not narrow to the selected service, and #6 is CLOSED | -| ↳ | `decisions.md`:627 | D-35. Deferral of Regional Multi-Leg Transport & RFDS Repatriation (6 October 2026) | +| ↳ | `decisions.md`:628 | D-35. Deferral of Regional Multi-Leg Transport & RFDS Repatriation (6 October 2026) | | `D-36` ⚠️ **2 occurrences in 2 files — not unique** | | | | ↳ | `../history/archive/dated-notes/owner-decisions-2026-09-1x.md`:1405 | D-36 · _Referrals into this ward_ / _into the team_ — PRESENTATION, with a condition | -| ↳ | `decisions.md`:634 | D-36. Affirmation of External Governance & Clinical Safety Hard Gates (6 October 2026) (Replaced by D-87) | +| ↳ | `decisions.md`:635 | D-36. Affirmation of External Governance & Clinical Safety Hard Gates (6 October 2026) (Replaced by D-87) | | `D-37` ⚠️ **2 occurrences in 2 files — not unique** | | | | ↳ | `../history/archive/dated-notes/owner-decisions-2026-09-1x.md`:1411 | D-37 · _Clinically ready, not yet gone_ — PRESENTATION, and the heading must name its subject | -| ↳ | `decisions.md`:649 | D-37. External Governance Hard Gate Operational Postures & Resolution Roadmap (6 October 2026) (Replaced by D-87) | +| ↳ | `decisions.md`:650 | D-37. External Governance Hard Gate Operational Postures & Resolution Roadmap (6 October 2026) (Replaced by D-87) | | `D-38` ⚠️ **2 occurrences in 2 files — not unique** | | | | ↳ | `../history/archive/dated-notes/owner-decisions-2026-09-1x.md`:1417 | D-38 · _People currently in a hospital bed_ on community team statistics — an aggregate that identifies at small N | -| ↳ | `decisions.md`:676 | D-38. Leave, absent without leave and CTO on the Patient page (9 October 2026) | +| ↳ | `decisions.md`:677 | D-38. Leave, absent without leave and CTO on the Patient page (9 October 2026) | | `D-39` ⚠️ **2 occurrences in 2 files — not unique** | | | | ↳ | `../history/archive/dated-notes/owner-decisions-2026-09-1x.md`:1437 | D-39 · _Contacts_ and _Time to first contact_ — HELD, build neither | -| ↳ | `decisions.md`:694 | D-39. Patients are identified by UMRN, never by a WF number (9 October 2026) | +| ↳ | `decisions.md`:695 | D-39. Patients are identified by UMRN, never by a WF number (9 October 2026) | | `D-40` ⚠️ **2 occurrences in 2 files — not unique** | | | | ↳ | `../history/archive/dated-notes/owner-decisions-2026-09-1x.md`:1445 | D-40 · An empty check array never claims reconciliation — on either shell surface | -| ↳ | `decisions.md`:738 | D-40. A second community treatment order keeps the first (9 October 2026) | +| ↳ | `decisions.md`:739 | D-40. A second community treatment order keeps the first (9 October 2026) | | `D-41` ⚠️ **2 occurrences in 2 files — not unique** | | | | ↳ | `../history/archive/dated-notes/owner-decisions-2026-09-1x.md`:1478 | D-41 · `corridorCounts` is extended, and its POPULATION changes | -| ↳ | `decisions.md`:749 | D-41. Delays board filters narrow the table and state the hidden count (9 October 2026) | +| ↳ | `decisions.md`:750 | D-41. Delays board filters narrow the table and state the hidden count (9 October 2026) | | `D-42` ⚠️ **2 occurrences in 2 files — not unique** | | | | ↳ | `../history/archive/dated-notes/owner-decisions-2026-09-1x.md`:1490 | D-42 · _Referrals into this ward_ renders bare counts. No share. | -| ↳ | `decisions.md`:762 | D-42. 12px typography floor with no uppercase exception reaffirmed in Design System v9 (9–10 October 2026) | +| ↳ | `decisions.md`:763 | D-42. 12px typography floor with no uppercase exception reaffirmed in Design System v9 (9–10 October 2026) | | `D-43` ⚠️ **2 occurrences in 2 files — not unique** | | | | ↳ | `../history/archive/dated-notes/owner-decisions-2026-09-1x.md`:1724 | D-43 · Ward Lead ruling — the bed board's ordinal contradiction is a LANE TASK | -| ↳ | `decisions.md`:768 | D-43. Colour lives in glyphs and the hero; red means act now with a thin 1px edge, never a fill (9 October 2026) | +| ↳ | `decisions.md`:769 | D-43. Colour lives in glyphs and the hero; red means act now with a thin 1px edge, never a fill (9 October 2026) | | `D-44` ⚠️ **2 occurrences in 2 files — not unique** | | | | ↳ | `../history/archive/dated-notes/owner-decisions-2026-09-1x.md`:1791 | D-44 · The community scope note is reworded and the comparison panel stays | -| ↳ | `decisions.md`:775 | D-44. Compact sizing, white cards and smaller text retained over expression-test card tints (9 October 2026) | +| ↳ | `decisions.md`:776 | D-44. Compact sizing, white cards and smaller text retained over expression-test card tints (9 October 2026) | | `D-45` ⚠️ **2 occurrences in 2 files — not unique** | | | | ↳ | `../history/archive/dated-notes/owner-decisions-2026-09-1x.md`:1803 | D-45 · 🔴 THE SEED DECISION — the fixture gains BOTH, as ONE change, after the statistics screens | -| ↳ | `decisions.md`:781 | D-45. Phone is its own 390×844 design, never a desktop reflow (9 October 2026) | +| ↳ | `decisions.md`:782 | D-45. Phone is its own 390×844 design, never a desktop reflow (9 October 2026) | | `D-46` ⚠️ **2 occurrences in 2 files — not unique** | | | | ↳ | `../history/archive/dated-notes/owner-decisions-2026-09-1x.md`:1984 | D-46 · A guard that fails whenever the machine is busy | -| ↳ | `decisions.md`:788 | D-46. Every page has a focal part, a part of its own, a page-specific hero and command-bar action rules (9 October 2026) | -| `D-47` | `decisions.md`:794 | D-47. Glass stays about 80% opaque with the top sheen kept (10 October 2026) | -| `D-48` | `decisions.md`:800 | D-48. Figures stay in Geist Mono with tabular digits while chart axes use Geist Sans (10 October 2026) | -| `D-49` | `decisions.md`:806 | D-49. Ward Flow 44px default row target (dense 36px, touch 52px) vs retired 48px PsychSift tap knob (10 October 2026) | -| `D-50` | `decisions.md`:812 | D-50. Neutral T1 urgency pill vs Form 1A amber statutory warning badge (10 October 2026) | -| `D-51` | `decisions.md`:818 | D-51. Delays filters hide rows with a stated hidden count while other queues highlight by default (10 October 2026) | -| `D-52` | `decisions.md`:825 | D-52. 10 October page review rulings: time-axis overdue zone, 15px/20px strip figures, 8px people pips and incremental shared parts (10 October 2026) | -| `D-53` | `decisions.md`:831 | D-53. Elevate design language and Glare mode adopted for Design System v9 (10 October 2026) | -| `D-54` | `decisions.md`:838 | D-54. Tier time thresholds remain synthetic demo values requiring clinical sign-off (10 October 2026) | -| `D-55` | `decisions.md`:846 | D-55. Supabase is Ward Flow's database and sign-in provider, and Azure is retired (15 October 2026) | -| `D-56` | `decisions.md`:852 | D-56. Josh signs the clinical values that have a recorded source, for the prototype (15 October 2026) | -| `D-57` | `decisions.md`:858 | D-57. Twenty Top 25 items move from the showcase into the real screens, each behind its own flag (15 October 2026) | -| `D-58` | `decisions.md`:885 | D-58. Josh signs three more clinical values that have a national source, for the prototype (15 October 2026) | -| `D-59` | `decisions.md`:891 | D-59. Each role sees only what it needs, from one access table (15 October 2026) | -| `D-60` | `decisions.md`:897 | D-60. Criteria led discharge, for voluntary patients only (15 October 2026) | -| `D-61` | `decisions.md`:906 | D-61. Tasks and Alerts are one queue called Alerts (16 October 2026) | -| `D-62` | `decisions.md`:912 | D-62. The one Alerts queue is signed off and its flag removed (16 October 2026) | -| `D-63` | `decisions.md`:918 | D-63. Josh signs the remaining clinical values and the criteria list, for the prototype (16 October 2026) | -| `D-64` | `decisions.md`:926 | D-64. Criteria led discharge shows on the boards and warns at departure (16 October 2026) | -| `D-65` | `decisions.md`:933 | D-65. Criteria led discharge on in production, typed patient specific criteria, and the AWOL bed hold review paused (17 October 2026) | -| `D-66` | `decisions.md`:949 | D-66. Granted roles work without a second approval (17 October 2026) | -| `D-67` | `decisions.md`:957 | D-67. Recover synthetic form drafts within the browser session (17 October 2026) | -| `D-68` | `decisions.md`:964 | D-68. Bed meeting TV content and access (19 October 2026) | -| `D-69` | `decisions.md`:972 | D-69. A referral with no legal status: "Needs an involuntary-capable bed until status is set." (20 October 2026) | -| `D-70` | `decisions.md`:979 | D-70. No computed Mental Health Act periods: show only the recorded due time (20 October 2026) | -| `D-71` | `decisions.md`:985 | D-71. Audit 1 clinical safety fixes: no suggested wards, wait-order queues, recorded facts only (20 October 2026) | -| `D-72` | `decisions.md`:998 | D-72. Seven product decisions for the 21 October fix plan (21 October 2026) | -| `D-73` | `decisions.md`:1014 | D-73. Batch merge safeguards (21 October 2026) | -| `D-74` | `decisions.md`:1030 | D-74. Owner answers to the 21 October open questions (21 October 2026) | -| `D-75` | `decisions.md`:1051 | D-75. UX audit follow-up, page caching, community gaps and the extra-travel bed hold (21 October 2026) | -| `D-76` | `decisions.md`:1064 | D-76. Community referral gaps: the full move journey and asking more wards (21 October 2026) | -| `D-77` | `decisions.md`:1073 | D-77. The ten bigger UX changes approved for build (22 October 2026) | -| `D-78` | `decisions.md`:1093 | D-78. One decline list for wards and coordinators (22 October 2026) | -| `D-79` | `decisions.md`:1104 | D-79. Wards see the full referral, with name and UMRN, before they answer (23 October 2026) | -| `D-80` | `decisions.md`:1115 | D-80. Role access plan version 2: all 17 recommendations (23 October 2026) | -| `D-81` | `decisions.md`:1130 | D-81. Pulled, with a pull time limit, for a bed kept for an incoming patient (24 October 2026) | -| `D-82` | `decisions.md`:1137 | D-82. A PR may raise a phone size limit by up to 5 KB per screen without asking (26 October 2026) | -| `D-83` | `decisions.md`:1145 | D-83. Patient names stay in full on every screen, copy and TV; privacy work must not make the app less useful (26 October 2026) | -| `D-84` | `decisions.md`:1156 | D-84. The live app stays on Railway Singapore for now, and there is no server-side TV account yet (26 October 2026) | -| `D-85` | `decisions.md`:1164 | D-85. "Held" only for a bed kept for its own patient; "pulled" for an incoming bed; "held up" for a delay (26 October 2026) | -| `D-86` | `decisions.md`:1171 | D-86. No second approver for role grants, at pilot or otherwise (27 October 2026) | -| `D-87` | `decisions.md`:1178 | D-87. D-36 and D-37 are replaced by the real-data gate (27 October 2026) | -| `D-88` | `decisions.md`:1190 | D-88. The public demo opens with a demo code once the front door is on (27 October 2026) | -| `D-89` | `decisions.md`:1197 | D-89. Redesign mock-up copies of large screens may pass the 1,500-line cap (30 October 2026) | -| `D-90` | `decisions.md`:1205 | D-90. Help gets How do I…, Show me runs everywhere, and Help is built without a separate mock-up step (30 October 2026) | -| `D-91` | `decisions.md`:1216 | D-91. One design home for the redesign, and Josh's answers on Help, Settings, search and notifications (31 October 2026) | -| `D-92` | `decisions.md`:1251 | D-92. Help goes live before its mock-up review, one search box on Capacity, and referral rows on Home open the panel (31 October 2026) | -| `D-93` | `decisions.md`:1261 | D-93. Legal form times show only as typed, never as a countdown (31 October 2026) | -| `D-94` | `decisions.md`:1269 | D-94. Design rulings of 31 October 2026 | -| `D-95` | `decisions.md`:1282 | D-95. One page scroll, and a click outside closes a drawer (3 November 2026) | -| `D-96` | `decisions.md`:1292 | D-96. A named bed is pulled, and its patient is not also waiting (8 November 2026) | -| `D-97` | `decisions.md`:1302 | D-97. Owner answers on seven open items: N21, N25, N26, N29, N40, N41 and WF-B008 (8 November 2026) | -| `D-98` | `decisions.md`:1315 | D-98. Friction pass: Quick bar, Patient peek and Ward peek (8 November 2026) | -| `D-99` | `decisions.md`:1324 | D-99. "Real details" in the referral drawer's sample patients (25 September 2026) | -| `D-100` | `decisions.md`:1330 | D-100. "Under a form" means one rule everywhere (9 November 2026) | -| `D-101` | `decisions.md`:1337 | D-101. Clinically ready for discharge, with one delay reason (9 November 2026) | -| `D-102` | `decisions.md`:1346 | D-102. Report-a-problem screenshots are blurred, and patient-table exports carry no names (9 November 2026) | -| `D-103` | `decisions.md`:1355 | D-103. Any coordinator on shift may mark a bed override reviewed (9 November 2026) | -| `D-104` | `decisions.md`:1362 | D-104. Care plans and safety flags: five defaults (9 November 2026) | -| `D-105` | `decisions.md`:1374 | D-105. Optional clerk details on the patient record (8 November 2026) | -| `D-106` | `decisions.md`:1381 | D-106. Clinical values from the 9 November 2026 documentation audit (9 November 2026) | -| `D-107` | `decisions.md`:1393 | D-107. Working rules and documentation from the 9 November 2026 audit (9 November 2026) | -| `D-108` | `decisions.md`:1407 | D-108. Six bed-flow features: keep D-93 and D-72, ED long-wait next steps (9 November 2026) | -| `D-109` | `decisions.md`:1416 | D-109. Home's bed alert line tests the whole number shown (9 November 2026) | -| `D-110` | `decisions.md`:1423 | D-110. ED and referral flow: neutral fit line, clearance timing and alternatives to admission (9 November 2026) | -| `D-111` | `decisions.md`:1433 | D-111. Review and audit plan: first guards and four open answers (9 November 2026) | -| `D-112` | `decisions.md`:1447 | D-112. Phone bottom bar: the floating bar (9 November 2026) | -| `D-113` | `decisions.md`:1456 | D-113. Second patient identifier on accept and Mark done; no initials search (9 November 2026) | -| `D-114` | `decisions.md`:1466 | D-114. Allow pull_request_target for three workflows only (9 November 2026) | -| `D-115` | `decisions.md`:1474 | D-115. One count on every screen, and a missing patient shown everywhere (9 November 2026) | -| `D-116` | `decisions.md`:1488 | D-116. Page tops: the PR #1581 recommendations stand (9 November 2026) | -| `D-117` | `decisions.md`:1505 | D-117. Care plans follow-up: frequent presenter count, tribunal date and ward flags (10 November 2026) | -| `D-118` | `decisions.md`:1514 | D-118. Page tops: Referrals and Ward phone limits up to 8 KB above main (10 November 2026) | -| `D-119` | `decisions.md`:1520 | D-119. Page tops: phone limits raised to measured sizes, trim after merge (10 November 2026) | -| `D-120` | `decisions.md`:1527 | D-120. Discharge and capacity values: pull limit, transport delay and discharge date slip (9 November 2026) | -| `D-121` | `decisions.md`:1536 | D-121. Working rules, guards and your PC from the 10 November 2026 audit (10 November 2026) | -| `D-122` | `decisions.md`:1550 | D-122. Clinical and privacy answers from the 10 November 2026 audit (10 November 2026) | -| `D-123` | `decisions.md`:1563 | D-123. Master audit list answers: two clinical fixes and five working rules (10 November 2026) | -| `D-124` | `decisions.md`:1577 | D-124. Offline saves whose record changed are refused and shown (10 November 2026) | -| `D-125` | `decisions.md`:1584 | D-125. Top 10 audit follow-ups approved (10 November 2026) | -| `D-126` | `decisions.md`:1596 | D-126. Ranked lists: neutral orders approved (10 November 2026) | -| `D-127` | `decisions.md`:1615 | D-127. Neutral wording for a community treatment order at arrival (10 November 2026) | -| `D-128` | `decisions.md`:1622 | D-128. Final master audit: nine answers (10 November 2026) | -| `D-129` | `decisions.md`:1637 | D-129. Master audit findings: every recommendation approved (10 November 2026) | -| `D-130` | `decisions.md`:1687 | D-130. Audit follow-ups: every remaining recommendation approved (10 November 2026) | -| `D-131` | `decisions.md`:1703 | D-131. Hard-case demo data allowed, and the movement diagram follows the engine (10 November 2026) | -| `D-132` | `decisions.md`:1712 | D-132. Audit fixes group 1: docs and CI guards (10 November 2026) | -| `D-133` | `decisions.md`:1722 | D-133. Batch 1 (ED and referral flow): three answers (10 November 2026) | -| `D-134` | `decisions.md`:1732 | D-134. Ward arrival and beds batch: how to hold, advocate line, night list and the feature freeze (10 November 2026) | -| `D-135` | `decisions.md`:1755 | D-135. Batch 4, outages and pilot control: pack age and plan answers (10 November 2026) | -| `D-136` | `decisions.md`:1764 | D-136. Feature batch 3 (shift change and follow-up): two signed values (10 November 2026) | -| `D-137` | `decisions.md`:1776 | D-137. Feature build batch 5: count leak first, TV change mark and QR library (10 November 2026) | -| `D-138` | `decisions.md`:1792 | D-138. Phone alert urgency by kind (10 November 2026) | -| `D-139` | `decisions.md`:1798 | D-139. Phone Statistics ward list A to Z, and leave return times as typed (10 November 2026) | -| `D-140` | `decisions.md`:1808 | D-140. Feature list adopted, and Wave 1 begins (10 November 2026) | -| `D-141` | `decisions.md`:1821 | D-141. Batch 1 (ED and referral flow) design approved, with eleven answers (11 November 2026) | -| `D-142` | `decisions.md`:1839 | D-142. Batch 4: design review and all twelve answers approved, D-80 amended for site features (11 November 2026) | -| `D-143` | `decisions.md`:1851 | D-143. MHERT is MHERL in the glossary; SMHMR deferred (11 November 2026) | -| `D-144` | `decisions.md`:1859 | D-144. Chip rows move only sideways on a phone (11 November 2026) | -| `D-145` | `decisions.md`:1865 | D-145. Repository review: fixes, features and code health approved (11 November 2026) | -| `D-146` | `decisions.md`:1879 | D-146. Performance audit fixes and the phone speed target (11 November 2026) | -| `D-147` | `decisions.md`:1885 | D-147. Demo-code tries keep per-address keying until a trusted proxy is set (11 November 2026) | -| `D-148` | `decisions.md`:1891 | D-148. Closing a bed stays one tap, and its toast offers a reason (11 November 2026) | -| `D-149` | `decisions.md`:1898 | D-149. A record whose only links are withdrawn referrals can be archived (11 November 2026) | -| `D-150` | `decisions.md`:1909 | D-150. The signed-in site runs on real Perth time (11 November 2026) | -| `D-151` | `decisions.md`:1917 | D-151. A bed tile says when a change is waiting to save (11 November 2026) | -| `D-152` | `decisions.md`:1924 | D-152. Returned to ED prompt sees every return in shared mode (11 November 2026) | -| `D-153` | `decisions.md`:1932 | D-153. Keep the drawn-time settle check, and defer a separate agent GitHub identity (11 November 2026) | -| `D-154` | `decisions.md`:1940 | D-154. The community desk's ED heads-up reads a narrow scoped list (11 November 2026) | -| `D-155` | `decisions.md`:1949 | D-155. Check the live clock offset before clearing it (11 November 2026) | -| `D-156` | `decisions.md`:1956 | D-156. Reset the synthetic live world after the clock fix (11 November 2026) | -| `D-157` | `decisions.md`:1964 | D-157. Legal forms Print and Export include people admitted on a form (12 November 2026) | -| `D-158` | `decisions.md`:1970 | D-158. A This-shift staff request is refused once that shift has ended (12 November 2026) | +| ↳ | `decisions.md`:789 | D-46. Every page has a focal part, a part of its own, a page-specific hero and command-bar action rules (9 October 2026) | +| `D-47` | `decisions.md`:795 | D-47. Glass stays about 80% opaque with the top sheen kept (10 October 2026) | +| `D-48` | `decisions.md`:801 | D-48. Figures stay in Geist Mono with tabular digits while chart axes use Geist Sans (10 October 2026) | +| `D-49` | `decisions.md`:807 | D-49. Ward Flow 44px default row target (dense 36px, touch 52px) vs retired 48px PsychSift tap knob (10 October 2026) | +| `D-50` | `decisions.md`:813 | D-50. Neutral T1 urgency pill vs Form 1A amber statutory warning badge (10 October 2026) | +| `D-51` | `decisions.md`:819 | D-51. Delays filters hide rows with a stated hidden count while other queues highlight by default (10 October 2026) | +| `D-52` | `decisions.md`:826 | D-52. 10 October page review rulings: time-axis overdue zone, 15px/20px strip figures, 8px people pips and incremental shared parts (10 October 2026) | +| `D-53` | `decisions.md`:832 | D-53. Elevate design language and Glare mode adopted for Design System v9 (10 October 2026) | +| `D-54` | `decisions.md`:839 | D-54. Tier time thresholds remain synthetic demo values requiring clinical sign-off (10 October 2026) | +| `D-55` | `decisions.md`:847 | D-55. Supabase is Ward Flow's database and sign-in provider, and Azure is retired (15 October 2026) | +| `D-56` | `decisions.md`:853 | D-56. Josh signs the clinical values that have a recorded source, for the prototype (15 October 2026) | +| `D-57` | `decisions.md`:859 | D-57. Twenty Top 25 items move from the showcase into the real screens, each behind its own flag (15 October 2026) | +| `D-58` | `decisions.md`:886 | D-58. Josh signs three more clinical values that have a national source, for the prototype (15 October 2026) | +| `D-59` | `decisions.md`:892 | D-59. Each role sees only what it needs, from one access table (15 October 2026) | +| `D-60` | `decisions.md`:898 | D-60. Criteria led discharge, for voluntary patients only (15 October 2026) | +| `D-61` | `decisions.md`:907 | D-61. Tasks and Alerts are one queue called Alerts (16 October 2026) | +| `D-62` | `decisions.md`:913 | D-62. The one Alerts queue is signed off and its flag removed (16 October 2026) | +| `D-63` | `decisions.md`:919 | D-63. Josh signs the remaining clinical values and the criteria list, for the prototype (16 October 2026) | +| `D-64` | `decisions.md`:927 | D-64. Criteria led discharge shows on the boards and warns at departure (16 October 2026) | +| `D-65` | `decisions.md`:934 | D-65. Criteria led discharge on in production, typed patient specific criteria, and the AWOL bed hold review paused (17 October 2026) | +| `D-66` | `decisions.md`:950 | D-66. Granted roles work without a second approval (17 October 2026) | +| `D-67` | `decisions.md`:958 | D-67. Recover synthetic form drafts within the browser session (17 October 2026) | +| `D-68` | `decisions.md`:965 | D-68. Bed meeting TV content and access (19 October 2026) | +| `D-69` | `decisions.md`:973 | D-69. A referral with no legal status: "Needs an involuntary-capable bed until status is set." (20 October 2026) | +| `D-70` | `decisions.md`:980 | D-70. No computed Mental Health Act periods: show only the recorded due time (20 October 2026) | +| `D-71` | `decisions.md`:986 | D-71. Audit 1 clinical safety fixes: no suggested wards, wait-order queues, recorded facts only (20 October 2026) | +| `D-72` | `decisions.md`:999 | D-72. Seven product decisions for the 21 October fix plan (21 October 2026) | +| `D-73` | `decisions.md`:1015 | D-73. Batch merge safeguards (21 October 2026) | +| `D-74` | `decisions.md`:1031 | D-74. Owner answers to the 21 October open questions (21 October 2026) | +| `D-75` | `decisions.md`:1052 | D-75. UX audit follow-up, page caching, community gaps and the extra-travel bed hold (21 October 2026) | +| `D-76` | `decisions.md`:1065 | D-76. Community referral gaps: the full move journey and asking more wards (21 October 2026) | +| `D-77` | `decisions.md`:1074 | D-77. The ten bigger UX changes approved for build (22 October 2026) | +| `D-78` | `decisions.md`:1094 | D-78. One decline list for wards and coordinators (22 October 2026) | +| `D-79` | `decisions.md`:1105 | D-79. Wards see the full referral, with name and UMRN, before they answer (23 October 2026) | +| `D-80` | `decisions.md`:1116 | D-80. Role access plan version 2: all 17 recommendations (23 October 2026) | +| `D-81` | `decisions.md`:1131 | D-81. Pulled, with a pull time limit, for a bed kept for an incoming patient (24 October 2026) | +| `D-82` | `decisions.md`:1138 | D-82. A PR may raise a phone size limit by up to 5 KB per screen without asking (26 October 2026) | +| `D-83` | `decisions.md`:1146 | D-83. Patient names stay in full on every screen, copy and TV; privacy work must not make the app less useful (26 October 2026) | +| `D-84` | `decisions.md`:1157 | D-84. The live app stays on Railway Singapore for now, and there is no server-side TV account yet (26 October 2026) | +| `D-85` | `decisions.md`:1165 | D-85. "Held" only for a bed kept for its own patient; "pulled" for an incoming bed; "held up" for a delay (26 October 2026) | +| `D-86` | `decisions.md`:1172 | D-86. No second approver for role grants, at pilot or otherwise (27 October 2026) | +| `D-87` | `decisions.md`:1179 | D-87. D-36 and D-37 are replaced by the real-data gate (27 October 2026) | +| `D-88` | `decisions.md`:1191 | D-88. The public demo opens with a demo code once the front door is on (27 October 2026) | +| `D-89` | `decisions.md`:1198 | D-89. Redesign mock-up copies of large screens may pass the 1,500-line cap (30 October 2026) | +| `D-90` | `decisions.md`:1206 | D-90. Help gets How do I…, Show me runs everywhere, and Help is built without a separate mock-up step (30 October 2026) | +| `D-91` | `decisions.md`:1217 | D-91. One design home for the redesign, and Josh's answers on Help, Settings, search and notifications (31 October 2026) | +| `D-92` | `decisions.md`:1252 | D-92. Help goes live before its mock-up review, one search box on Capacity, and referral rows on Home open the panel (31 October 2026) | +| `D-93` | `decisions.md`:1262 | D-93. Legal form times show only as typed, never as a countdown (31 October 2026) | +| `D-94` | `decisions.md`:1270 | D-94. Design rulings of 31 October 2026 | +| `D-95` | `decisions.md`:1283 | D-95. One page scroll, and a click outside closes a drawer (3 November 2026) | +| `D-96` | `decisions.md`:1293 | D-96. A named bed is pulled, and its patient is not also waiting (8 November 2026) | +| `D-97` | `decisions.md`:1303 | D-97. Owner answers on seven open items: N21, N25, N26, N29, N40, N41 and WF-B008 (8 November 2026) | +| `D-98` | `decisions.md`:1316 | D-98. Friction pass: Quick bar, Patient peek and Ward peek (8 November 2026) | +| `D-99` | `decisions.md`:1325 | D-99. "Real details" in the referral drawer's sample patients (25 September 2026) | +| `D-100` | `decisions.md`:1331 | D-100. "Under a form" means one rule everywhere (9 November 2026) | +| `D-101` | `decisions.md`:1338 | D-101. Clinically ready for discharge, with one delay reason (9 November 2026) | +| `D-102` | `decisions.md`:1347 | D-102. Report-a-problem screenshots are blurred, and patient-table exports carry no names (9 November 2026) | +| `D-103` | `decisions.md`:1356 | D-103. Any coordinator on shift may mark a bed override reviewed (9 November 2026) | +| `D-104` | `decisions.md`:1363 | D-104. Care plans and safety flags: five defaults (9 November 2026) | +| `D-105` | `decisions.md`:1375 | D-105. Optional clerk details on the patient record (8 November 2026) | +| `D-106` | `decisions.md`:1382 | D-106. Clinical values from the 9 November 2026 documentation audit (9 November 2026) | +| `D-107` | `decisions.md`:1394 | D-107. Working rules and documentation from the 9 November 2026 audit (9 November 2026) | +| `D-108` | `decisions.md`:1408 | D-108. Six bed-flow features: keep D-93 and D-72, ED long-wait next steps (9 November 2026) | +| `D-109` | `decisions.md`:1417 | D-109. Home's bed alert line tests the whole number shown (9 November 2026) | +| `D-110` | `decisions.md`:1424 | D-110. ED and referral flow: neutral fit line, clearance timing and alternatives to admission (9 November 2026) | +| `D-111` | `decisions.md`:1434 | D-111. Review and audit plan: first guards and four open answers (9 November 2026) | +| `D-112` | `decisions.md`:1448 | D-112. Phone bottom bar: the floating bar (9 November 2026) | +| `D-113` | `decisions.md`:1457 | D-113. Second patient identifier on accept and Mark done; no initials search (9 November 2026) | +| `D-114` | `decisions.md`:1467 | D-114. Allow pull_request_target for three workflows only (9 November 2026) | +| `D-115` | `decisions.md`:1475 | D-115. One count on every screen, and a missing patient shown everywhere (9 November 2026) | +| `D-116` | `decisions.md`:1489 | D-116. Page tops: the PR #1581 recommendations stand (9 November 2026) | +| `D-117` | `decisions.md`:1506 | D-117. Care plans follow-up: frequent presenter count, tribunal date and ward flags (10 November 2026) | +| `D-118` | `decisions.md`:1515 | D-118. Page tops: Referrals and Ward phone limits up to 8 KB above main (10 November 2026) | +| `D-119` | `decisions.md`:1521 | D-119. Page tops: phone limits raised to measured sizes, trim after merge (10 November 2026) | +| `D-120` | `decisions.md`:1528 | D-120. Discharge and capacity values: pull limit, transport delay and discharge date slip (9 November 2026) | +| `D-121` | `decisions.md`:1537 | D-121. Working rules, guards and your PC from the 10 November 2026 audit (10 November 2026) | +| `D-122` | `decisions.md`:1551 | D-122. Clinical and privacy answers from the 10 November 2026 audit (10 November 2026) | +| `D-123` | `decisions.md`:1564 | D-123. Master audit list answers: two clinical fixes and five working rules (10 November 2026) | +| `D-124` | `decisions.md`:1578 | D-124. Offline saves whose record changed are refused and shown (10 November 2026) | +| `D-125` | `decisions.md`:1585 | D-125. Top 10 audit follow-ups approved (10 November 2026) | +| `D-126` | `decisions.md`:1597 | D-126. Ranked lists: neutral orders approved (10 November 2026) | +| `D-127` | `decisions.md`:1616 | D-127. Neutral wording for a community treatment order at arrival (10 November 2026) | +| `D-128` | `decisions.md`:1623 | D-128. Final master audit: nine answers (10 November 2026) | +| `D-129` | `decisions.md`:1638 | D-129. Master audit findings: every recommendation approved (10 November 2026) | +| `D-130` | `decisions.md`:1688 | D-130. Audit follow-ups: every remaining recommendation approved (10 November 2026) | +| `D-131` | `decisions.md`:1704 | D-131. Hard-case demo data allowed, and the movement diagram follows the engine (10 November 2026) | +| `D-132` | `decisions.md`:1713 | D-132. Audit fixes group 1: docs and CI guards (10 November 2026) | +| `D-133` | `decisions.md`:1723 | D-133. Batch 1 (ED and referral flow): three answers (10 November 2026) | +| `D-134` | `decisions.md`:1733 | D-134. Ward arrival and beds batch: how to hold, advocate line, night list and the feature freeze (10 November 2026) | +| `D-135` | `decisions.md`:1756 | D-135. Batch 4, outages and pilot control: pack age and plan answers (10 November 2026) | +| `D-136` | `decisions.md`:1765 | D-136. Feature batch 3 (shift change and follow-up): two signed values (10 November 2026) | +| `D-137` | `decisions.md`:1777 | D-137. Feature build batch 5: count leak first, TV change mark and QR library (10 November 2026) | +| `D-138` | `decisions.md`:1793 | D-138. Phone alert urgency by kind (10 November 2026) | +| `D-139` | `decisions.md`:1799 | D-139. Phone Statistics ward list A to Z, and leave return times as typed (10 November 2026) | +| `D-140` | `decisions.md`:1809 | D-140. Feature list adopted, and Wave 1 begins (10 November 2026) | +| `D-141` | `decisions.md`:1822 | D-141. Batch 1 (ED and referral flow) design approved, with eleven answers (11 November 2026) | +| `D-142` | `decisions.md`:1840 | D-142. Batch 4: design review and all twelve answers approved, D-80 amended for site features (11 November 2026) | +| `D-143` | `decisions.md`:1852 | D-143. MHERT is MHERL in the glossary; SMHMR deferred (11 November 2026) | +| `D-144` | `decisions.md`:1860 | D-144. Chip rows move only sideways on a phone (11 November 2026) | +| `D-145` | `decisions.md`:1866 | D-145. Repository review: fixes, features and code health approved (11 November 2026) | +| `D-146` | `decisions.md`:1880 | D-146. Performance audit fixes and the phone speed target (11 November 2026) | +| `D-147` | `decisions.md`:1886 | D-147. Demo-code tries keep per-address keying until a trusted proxy is set (11 November 2026) | +| `D-148` | `decisions.md`:1892 | D-148. Closing a bed stays one tap, and its toast offers a reason (11 November 2026) | +| `D-149` | `decisions.md`:1899 | D-149. A record whose only links are withdrawn referrals can be archived (11 November 2026) | +| `D-150` | `decisions.md`:1910 | D-150. The signed-in site runs on real Perth time (11 November 2026) | +| `D-151` | `decisions.md`:1918 | D-151. A bed tile says when a change is waiting to save (11 November 2026) | +| `D-152` | `decisions.md`:1925 | D-152. Returned to ED prompt sees every return in shared mode (11 November 2026) | +| `D-153` | `decisions.md`:1933 | D-153. Keep the drawn-time settle check, and defer a separate agent GitHub identity (11 November 2026) | +| `D-154` | `decisions.md`:1941 | D-154. The community desk's ED heads-up reads a narrow scoped list (11 November 2026) | +| `D-155` | `decisions.md`:1950 | D-155. Check the live clock offset before clearing it (11 November 2026) | +| `D-156` | `decisions.md`:1957 | D-156. Reset the synthetic live world after the clock fix (11 November 2026) | +| `D-157` | `decisions.md`:1965 | D-157. Legal forms Print and Export include people admitted on a form (12 November 2026) | +| `D-158` | `decisions.md`:1971 | D-158. A This-shift staff request is refused once that shift has ended (12 November 2026) | +| `D-159` | `decisions.md`:1977 | D-159. Fixes from the 10 to 12 November PR review (12 November 2026) | | `D9-1` | `../history/archive/dated-notes/ward-flow-phase-9-decisions.md`:62 | D9-1 — The wait becomes prominent, and its ceiling is removed; it never outranks urgency (SESSION) | | `D9-2` | `../history/archive/dated-notes/ward-flow-phase-9-decisions.md`:102 | D9-2 — Declaring escalation records it and marks the screens, and changes nothing else (SESSION, and partly blocked) | | `D9-3` | `../history/archive/dated-notes/ward-flow-phase-9-decisions.md`:126 | D9-3 — The retrospective is one person's journey replayed, plus the ward prediction track record last (SESSION) | @@ -637,166 +638,167 @@ safe global key across 30 files. **Title:** Ward Flow — decision log -158 item(s): - -- `D-1` — line 187, heading: "D-1. Ward Flow is local only, with no linked repository (Replaced by D-27)" -- `D-2` — line 197, heading: "D-2. Railway is disconnected" -- `D-3` — line 208, heading: "D-3. The PsychSift and Database code is being retired from Ward Flow" -- `D-4` — line 216, heading: "D-4. Synthetic patients only" -- `D-5` — line 224, heading: "D-5. Parallel threads in separate worktrees, folded one at a time" -- `D-6` — line 235, heading: "D-6. Aboriginal cultural safety review deferred" -- `D-7` — line 243, heading: "D-7. The first four milestones, in this order" -- `D-8` — line 255, heading: "D-8. Josh's standing rulings, numbered R1 to R16, for every session and tool" -- `D-9` — line 287, heading: "D-9. Josh's rulings of 25 September, 11:43 to 12:00 (R17 to R21)" -- `D-10` — line 301, heading: "D-10. Gate build type check (R22)" -- `D-11` — line 310, heading: "D-11. Rulings of 25 September, 12:41 (R23 to R25)" -- `D-12` — line 326, heading: "D-12. Rulings of 25 September, 12:43 (R26 to R31)" -- `D-13` — line 345, heading: "D-13. Tooling tests in batch gates (R32)" -- `D-14` — line 355, heading: "D-14. Standing defaults: decide small things without asking" -- `D-15` — line 367, heading: "D-15. Rulings of 25 September, 13:26 (R33 to R35)" -- `D-16` — line 380, heading: "D-16. Legal overrides and form receipts, 25 September 13:51 UTC" -- `D-17` — line 393, heading: "D-17. Morning roll-up time is Josh's own default (25 September)" -- `D-18` — line 404, heading: "D-18. The privacy lock stops saving only for typed text (25 September)" -- `D-19` — line 415, heading: "D-19. Rulings of 25 September, evening (walkthrough, leave beds, PsychSift, gender display)" -- `D-20` — line 445, heading: "D-20. One high-contrast colour check retired (26 September)" -- `D-21` — line 458, heading: "D-21. Coordinator bed board: all nine recommended answers (26 September)" -- `D-22` — line 470, heading: "D-22. No legal limits in hour rules; the rest are labelled defaults (26 September)" -- `D-23` — line 486, heading: "D-23. Leave-bed fields kept; patient search stops matching by ward (26 September)" -- `D-24` — line 500, heading: "D-24. Hour-rules morning list: all A (26 September)" -- `D-25` — line 524, heading: "D-25. The lone-patient check counts free beds on both matching paths (26 September)" -- `D-26` — line 532, heading: "D-26. Select fold checks by changed risk (27 September)" -- `D-27` — line 550, heading: "D-27. Ward Flow uses its dedicated repository (28 September)" -- `D-28` — line 563, heading: "D-28. Design Modernization & Anti-Box Architecture (30 September 2026)" -- `D-29` — line 577, heading: "D-29. Act time limits shown as a labelled synthetic demo (4 October 2026)" -- `D-30` — line 592, heading: "D-30. Central Coordinator Approval for Inter-Ward Bed Transfers (6 October 2026)" -- `D-31` — line 599, heading: "D-31. Gender-Diverse Bed Placement Policy & Clinical Override Safety (6 October 2026)" -- `D-32` — line 606, heading: "D-32. FIFO Waiting Time with Clinical Triage Badging vs Algorithmic Urgency Sorting (6 October 2026)" -- `D-33` — line 613, heading: "D-33. Separation of Local Ward Shift Notes from Statewide Coordinator Telemetry (6 October 2026)" -- `D-34` — line 620, heading: "D-34. Acute Medical Deterioration Bed Release and Escalation Protocol (6 October 2026)" -- `D-35` — line 627, heading: "D-35. Deferral of Regional Multi-Leg Transport & RFDS Repatriation (6 October 2026)" -- `D-36` — line 634, heading: "D-36. Affirmation of External Governance & Clinical Safety Hard Gates (6 October 2026) (Replaced by D-87)" -- `D-37` — line 649, heading: "D-37. External Governance Hard Gate Operational Postures & Resolution Roadmap (6 October 2026) (Replaced by D-87)" -- `D-38` — line 676, heading: "D-38. Leave, absent without leave and CTO on the Patient page (9 October 2026)" -- `D-39` — line 694, heading: "D-39. Patients are identified by UMRN, never by a WF number (9 October 2026)" -- `D-40` — line 738, heading: "D-40. A second community treatment order keeps the first (9 October 2026)" -- `D-41` — line 749, heading: "D-41. Delays board filters narrow the table and state the hidden count (9 October 2026)" -- `D-42` — line 762, heading: "D-42. 12px typography floor with no uppercase exception reaffirmed in Design System v9 (9–10 October 2026)" -- `D-43` — line 768, heading: "D-43. Colour lives in glyphs and the hero; red means act now with a thin 1px edge, never a fill (9 October 2026)" -- `D-44` — line 775, heading: "D-44. Compact sizing, white cards and smaller text retained over expression-test card tints (9 October 2026)" -- `D-45` — line 781, heading: "D-45. Phone is its own 390×844 design, never a desktop reflow (9 October 2026)" -- `D-46` — line 788, heading: "D-46. Every page has a focal part, a part of its own, a page-specific hero and command-bar action rules (9 October 2026)" -- `D-47` — line 794, heading: "D-47. Glass stays about 80% opaque with the top sheen kept (10 October 2026)" -- `D-48` — line 800, heading: "D-48. Figures stay in Geist Mono with tabular digits while chart axes use Geist Sans (10 October 2026)" -- `D-49` — line 806, heading: "D-49. Ward Flow 44px default row target (dense 36px, touch 52px) vs retired 48px PsychSift tap knob (10 October 2026)" -- `D-50` — line 812, heading: "D-50. Neutral T1 urgency pill vs Form 1A amber statutory warning badge (10 October 2026)" -- `D-51` — line 818, heading: "D-51. Delays filters hide rows with a stated hidden count while other queues highlight by default (10 October 2026)" -- `D-52` — line 825, heading: "D-52. 10 October page review rulings: time-axis overdue zone, 15px/20px strip figures, 8px people pips and incremental shared parts (10 October 2026)" -- `D-53` — line 831, heading: "D-53. Elevate design language and Glare mode adopted for Design System v9 (10 October 2026)" -- `D-54` — line 838, heading: "D-54. Tier time thresholds remain synthetic demo values requiring clinical sign-off (10 October 2026)" -- `D-55` — line 846, heading: "D-55. Supabase is Ward Flow's database and sign-in provider, and Azure is retired (15 October 2026)" -- `D-56` — line 852, heading: "D-56. Josh signs the clinical values that have a recorded source, for the prototype (15 October 2026)" -- `D-57` — line 858, heading: "D-57. Twenty Top 25 items move from the showcase into the real screens, each behind its own flag (15 October 2026)" -- `D-58` — line 885, heading: "D-58. Josh signs three more clinical values that have a national source, for the prototype (15 October 2026)" -- `D-59` — line 891, heading: "D-59. Each role sees only what it needs, from one access table (15 October 2026)" -- `D-60` — line 897, heading: "D-60. Criteria led discharge, for voluntary patients only (15 October 2026)" -- `D-61` — line 906, heading: "D-61. Tasks and Alerts are one queue called Alerts (16 October 2026)" -- `D-62` — line 912, heading: "D-62. The one Alerts queue is signed off and its flag removed (16 October 2026)" -- `D-63` — line 918, heading: "D-63. Josh signs the remaining clinical values and the criteria list, for the prototype (16 October 2026)" -- `D-64` — line 926, heading: "D-64. Criteria led discharge shows on the boards and warns at departure (16 October 2026)" -- `D-65` — line 933, heading: "D-65. Criteria led discharge on in production, typed patient specific criteria, and the AWOL bed hold review paused (17 October 2026)" -- `D-66` — line 949, heading: "D-66. Granted roles work without a second approval (17 October 2026)" -- `D-67` — line 957, heading: "D-67. Recover synthetic form drafts within the browser session (17 October 2026)" -- `D-68` — line 964, heading: "D-68. Bed meeting TV content and access (19 October 2026)" -- `D-69` — line 972, heading: "D-69. A referral with no legal status: "Needs an involuntary-capable bed until status is set." (20 October 2026)" -- `D-70` — line 979, heading: "D-70. No computed Mental Health Act periods: show only the recorded due time (20 October 2026)" -- `D-71` — line 985, heading: "D-71. Audit 1 clinical safety fixes: no suggested wards, wait-order queues, recorded facts only (20 October 2026)" -- `D-72` — line 998, heading: "D-72. Seven product decisions for the 21 October fix plan (21 October 2026)" -- `D-73` — line 1014, heading: "D-73. Batch merge safeguards (21 October 2026)" -- `D-74` — line 1030, heading: "D-74. Owner answers to the 21 October open questions (21 October 2026)" -- `D-75` — line 1051, heading: "D-75. UX audit follow-up, page caching, community gaps and the extra-travel bed hold (21 October 2026)" -- `D-76` — line 1064, heading: "D-76. Community referral gaps: the full move journey and asking more wards (21 October 2026)" -- `D-77` — line 1073, heading: "D-77. The ten bigger UX changes approved for build (22 October 2026)" -- `D-78` — line 1093, heading: "D-78. One decline list for wards and coordinators (22 October 2026)" -- `D-79` — line 1104, heading: "D-79. Wards see the full referral, with name and UMRN, before they answer (23 October 2026)" -- `D-80` — line 1115, heading: "D-80. Role access plan version 2: all 17 recommendations (23 October 2026)" -- `D-81` — line 1130, heading: "D-81. Pulled, with a pull time limit, for a bed kept for an incoming patient (24 October 2026)" -- `D-82` — line 1137, heading: "D-82. A PR may raise a phone size limit by up to 5 KB per screen without asking (26 October 2026)" -- `D-83` — line 1145, heading: "D-83. Patient names stay in full on every screen, copy and TV; privacy work must not make the app less useful (26 October 2026)" -- `D-84` — line 1156, heading: "D-84. The live app stays on Railway Singapore for now, and there is no server-side TV account yet (26 October 2026)" -- `D-85` — line 1164, heading: "D-85. "Held" only for a bed kept for its own patient; "pulled" for an incoming bed; "held up" for a delay (26 October 2026)" -- `D-86` — line 1171, heading: "D-86. No second approver for role grants, at pilot or otherwise (27 October 2026)" -- `D-87` — line 1178, heading: "D-87. D-36 and D-37 are replaced by the real-data gate (27 October 2026)" -- `D-88` — line 1190, heading: "D-88. The public demo opens with a demo code once the front door is on (27 October 2026)" -- `D-89` — line 1197, heading: "D-89. Redesign mock-up copies of large screens may pass the 1,500-line cap (30 October 2026)" -- `D-90` — line 1205, heading: "D-90. Help gets How do I…, Show me runs everywhere, and Help is built without a separate mock-up step (30 October 2026)" -- `D-91` — line 1216, heading: "D-91. One design home for the redesign, and Josh's answers on Help, Settings, search and notifications (31 October 2026)" -- `D-92` — line 1251, heading: "D-92. Help goes live before its mock-up review, one search box on Capacity, and referral rows on Home open the panel (31 October 2026)" -- `D-93` — line 1261, heading: "D-93. Legal form times show only as typed, never as a countdown (31 October 2026)" -- `D-94` — line 1269, heading: "D-94. Design rulings of 31 October 2026" -- `D-95` — line 1282, heading: "D-95. One page scroll, and a click outside closes a drawer (3 November 2026)" -- `D-96` — line 1292, heading: "D-96. A named bed is pulled, and its patient is not also waiting (8 November 2026)" -- `D-97` — line 1302, heading: "D-97. Owner answers on seven open items: N21, N25, N26, N29, N40, N41 and WF-B008 (8 November 2026)" -- `D-98` — line 1315, heading: "D-98. Friction pass: Quick bar, Patient peek and Ward peek (8 November 2026)" -- `D-99` — line 1324, heading: "D-99. "Real details" in the referral drawer's sample patients (25 September 2026)" -- `D-100` — line 1330, heading: "D-100. "Under a form" means one rule everywhere (9 November 2026)" -- `D-101` — line 1337, heading: "D-101. Clinically ready for discharge, with one delay reason (9 November 2026)" -- `D-102` — line 1346, heading: "D-102. Report-a-problem screenshots are blurred, and patient-table exports carry no names (9 November 2026)" -- `D-103` — line 1355, heading: "D-103. Any coordinator on shift may mark a bed override reviewed (9 November 2026)" -- `D-104` — line 1362, heading: "D-104. Care plans and safety flags: five defaults (9 November 2026)" -- `D-105` — line 1374, heading: "D-105. Optional clerk details on the patient record (8 November 2026)" -- `D-106` — line 1381, heading: "D-106. Clinical values from the 9 November 2026 documentation audit (9 November 2026)" -- `D-107` — line 1393, heading: "D-107. Working rules and documentation from the 9 November 2026 audit (9 November 2026)" -- `D-108` — line 1407, heading: "D-108. Six bed-flow features: keep D-93 and D-72, ED long-wait next steps (9 November 2026)" -- `D-109` — line 1416, heading: "D-109. Home's bed alert line tests the whole number shown (9 November 2026)" -- `D-110` — line 1423, heading: "D-110. ED and referral flow: neutral fit line, clearance timing and alternatives to admission (9 November 2026)" -- `D-111` — line 1433, heading: "D-111. Review and audit plan: first guards and four open answers (9 November 2026)" -- `D-112` — line 1447, heading: "D-112. Phone bottom bar: the floating bar (9 November 2026)" -- `D-113` — line 1456, heading: "D-113. Second patient identifier on accept and Mark done; no initials search (9 November 2026)" -- `D-114` — line 1466, heading: "D-114. Allow pull_request_target for three workflows only (9 November 2026)" -- `D-115` — line 1474, heading: "D-115. One count on every screen, and a missing patient shown everywhere (9 November 2026)" -- `D-116` — line 1488, heading: "D-116. Page tops: the PR #1581 recommendations stand (9 November 2026)" -- `D-117` — line 1505, heading: "D-117. Care plans follow-up: frequent presenter count, tribunal date and ward flags (10 November 2026)" -- `D-118` — line 1514, heading: "D-118. Page tops: Referrals and Ward phone limits up to 8 KB above main (10 November 2026)" -- `D-119` — line 1520, heading: "D-119. Page tops: phone limits raised to measured sizes, trim after merge (10 November 2026)" -- `D-120` — line 1527, heading: "D-120. Discharge and capacity values: pull limit, transport delay and discharge date slip (9 November 2026)" -- `D-121` — line 1536, heading: "D-121. Working rules, guards and your PC from the 10 November 2026 audit (10 November 2026)" -- `D-122` — line 1550, heading: "D-122. Clinical and privacy answers from the 10 November 2026 audit (10 November 2026)" -- `D-123` — line 1563, heading: "D-123. Master audit list answers: two clinical fixes and five working rules (10 November 2026)" -- `D-124` — line 1577, heading: "D-124. Offline saves whose record changed are refused and shown (10 November 2026)" -- `D-125` — line 1584, heading: "D-125. Top 10 audit follow-ups approved (10 November 2026)" -- `D-126` — line 1596, heading: "D-126. Ranked lists: neutral orders approved (10 November 2026)" -- `D-127` — line 1615, heading: "D-127. Neutral wording for a community treatment order at arrival (10 November 2026)" -- `D-128` — line 1622, heading: "D-128. Final master audit: nine answers (10 November 2026)" -- `D-129` — line 1637, heading: "D-129. Master audit findings: every recommendation approved (10 November 2026)" -- `D-130` — line 1687, heading: "D-130. Audit follow-ups: every remaining recommendation approved (10 November 2026)" -- `D-131` — line 1703, heading: "D-131. Hard-case demo data allowed, and the movement diagram follows the engine (10 November 2026)" -- `D-132` — line 1712, heading: "D-132. Audit fixes group 1: docs and CI guards (10 November 2026)" -- `D-133` — line 1722, heading: "D-133. Batch 1 (ED and referral flow): three answers (10 November 2026)" -- `D-134` — line 1732, heading: "D-134. Ward arrival and beds batch: how to hold, advocate line, night list and the feature freeze (10 November 2026)" -- `D-135` — line 1755, heading: "D-135. Batch 4, outages and pilot control: pack age and plan answers (10 November 2026)" -- `D-136` — line 1764, heading: "D-136. Feature batch 3 (shift change and follow-up): two signed values (10 November 2026)" -- `D-137` — line 1776, heading: "D-137. Feature build batch 5: count leak first, TV change mark and QR library (10 November 2026)" -- `D-138` — line 1792, heading: "D-138. Phone alert urgency by kind (10 November 2026)" -- `D-139` — line 1798, heading: "D-139. Phone Statistics ward list A to Z, and leave return times as typed (10 November 2026)" -- `D-140` — line 1808, heading: "D-140. Feature list adopted, and Wave 1 begins (10 November 2026)" -- `D-141` — line 1821, heading: "D-141. Batch 1 (ED and referral flow) design approved, with eleven answers (11 November 2026)" -- `D-142` — line 1839, heading: "D-142. Batch 4: design review and all twelve answers approved, D-80 amended for site features (11 November 2026)" -- `D-143` — line 1851, heading: "D-143. MHERT is MHERL in the glossary; SMHMR deferred (11 November 2026)" -- `D-144` — line 1859, heading: "D-144. Chip rows move only sideways on a phone (11 November 2026)" -- `D-145` — line 1865, heading: "D-145. Repository review: fixes, features and code health approved (11 November 2026)" -- `D-146` — line 1879, heading: "D-146. Performance audit fixes and the phone speed target (11 November 2026)" -- `D-147` — line 1885, heading: "D-147. Demo-code tries keep per-address keying until a trusted proxy is set (11 November 2026)" -- `D-148` — line 1891, heading: "D-148. Closing a bed stays one tap, and its toast offers a reason (11 November 2026)" -- `D-149` — line 1898, heading: "D-149. A record whose only links are withdrawn referrals can be archived (11 November 2026)" -- `D-150` — line 1909, heading: "D-150. The signed-in site runs on real Perth time (11 November 2026)" -- `D-151` — line 1917, heading: "D-151. A bed tile says when a change is waiting to save (11 November 2026)" -- `D-152` — line 1924, heading: "D-152. Returned to ED prompt sees every return in shared mode (11 November 2026)" -- `D-153` — line 1932, heading: "D-153. Keep the drawn-time settle check, and defer a separate agent GitHub identity (11 November 2026)" -- `D-154` — line 1940, heading: "D-154. The community desk's ED heads-up reads a narrow scoped list (11 November 2026)" -- `D-155` — line 1949, heading: "D-155. Check the live clock offset before clearing it (11 November 2026)" -- `D-156` — line 1956, heading: "D-156. Reset the synthetic live world after the clock fix (11 November 2026)" -- `D-157` — line 1964, heading: "D-157. Legal forms Print and Export include people admitted on a form (12 November 2026)" -- `D-158` — line 1970, heading: "D-158. A This-shift staff request is refused once that shift has ended (12 November 2026)" +159 item(s): + +- `D-1` — line 188, heading: "D-1. Ward Flow is local only, with no linked repository (Replaced by D-27)" +- `D-2` — line 198, heading: "D-2. Railway is disconnected" +- `D-3` — line 209, heading: "D-3. The PsychSift and Database code is being retired from Ward Flow" +- `D-4` — line 217, heading: "D-4. Synthetic patients only" +- `D-5` — line 225, heading: "D-5. Parallel threads in separate worktrees, folded one at a time" +- `D-6` — line 236, heading: "D-6. Aboriginal cultural safety review deferred" +- `D-7` — line 244, heading: "D-7. The first four milestones, in this order" +- `D-8` — line 256, heading: "D-8. Josh's standing rulings, numbered R1 to R16, for every session and tool" +- `D-9` — line 288, heading: "D-9. Josh's rulings of 25 September, 11:43 to 12:00 (R17 to R21)" +- `D-10` — line 302, heading: "D-10. Gate build type check (R22)" +- `D-11` — line 311, heading: "D-11. Rulings of 25 September, 12:41 (R23 to R25)" +- `D-12` — line 327, heading: "D-12. Rulings of 25 September, 12:43 (R26 to R31)" +- `D-13` — line 346, heading: "D-13. Tooling tests in batch gates (R32)" +- `D-14` — line 356, heading: "D-14. Standing defaults: decide small things without asking" +- `D-15` — line 368, heading: "D-15. Rulings of 25 September, 13:26 (R33 to R35)" +- `D-16` — line 381, heading: "D-16. Legal overrides and form receipts, 25 September 13:51 UTC" +- `D-17` — line 394, heading: "D-17. Morning roll-up time is Josh's own default (25 September)" +- `D-18` — line 405, heading: "D-18. The privacy lock stops saving only for typed text (25 September)" +- `D-19` — line 416, heading: "D-19. Rulings of 25 September, evening (walkthrough, leave beds, PsychSift, gender display)" +- `D-20` — line 446, heading: "D-20. One high-contrast colour check retired (26 September)" +- `D-21` — line 459, heading: "D-21. Coordinator bed board: all nine recommended answers (26 September)" +- `D-22` — line 471, heading: "D-22. No legal limits in hour rules; the rest are labelled defaults (26 September)" +- `D-23` — line 487, heading: "D-23. Leave-bed fields kept; patient search stops matching by ward (26 September)" +- `D-24` — line 501, heading: "D-24. Hour-rules morning list: all A (26 September)" +- `D-25` — line 525, heading: "D-25. The lone-patient check counts free beds on both matching paths (26 September)" +- `D-26` — line 533, heading: "D-26. Select fold checks by changed risk (27 September)" +- `D-27` — line 551, heading: "D-27. Ward Flow uses its dedicated repository (28 September)" +- `D-28` — line 564, heading: "D-28. Design Modernization & Anti-Box Architecture (30 September 2026)" +- `D-29` — line 578, heading: "D-29. Act time limits shown as a labelled synthetic demo (4 October 2026)" +- `D-30` — line 593, heading: "D-30. Central Coordinator Approval for Inter-Ward Bed Transfers (6 October 2026)" +- `D-31` — line 600, heading: "D-31. Gender-Diverse Bed Placement Policy & Clinical Override Safety (6 October 2026)" +- `D-32` — line 607, heading: "D-32. FIFO Waiting Time with Clinical Triage Badging vs Algorithmic Urgency Sorting (6 October 2026)" +- `D-33` — line 614, heading: "D-33. Separation of Local Ward Shift Notes from Statewide Coordinator Telemetry (6 October 2026)" +- `D-34` — line 621, heading: "D-34. Acute Medical Deterioration Bed Release and Escalation Protocol (6 October 2026)" +- `D-35` — line 628, heading: "D-35. Deferral of Regional Multi-Leg Transport & RFDS Repatriation (6 October 2026)" +- `D-36` — line 635, heading: "D-36. Affirmation of External Governance & Clinical Safety Hard Gates (6 October 2026) (Replaced by D-87)" +- `D-37` — line 650, heading: "D-37. External Governance Hard Gate Operational Postures & Resolution Roadmap (6 October 2026) (Replaced by D-87)" +- `D-38` — line 677, heading: "D-38. Leave, absent without leave and CTO on the Patient page (9 October 2026)" +- `D-39` — line 695, heading: "D-39. Patients are identified by UMRN, never by a WF number (9 October 2026)" +- `D-40` — line 739, heading: "D-40. A second community treatment order keeps the first (9 October 2026)" +- `D-41` — line 750, heading: "D-41. Delays board filters narrow the table and state the hidden count (9 October 2026)" +- `D-42` — line 763, heading: "D-42. 12px typography floor with no uppercase exception reaffirmed in Design System v9 (9–10 October 2026)" +- `D-43` — line 769, heading: "D-43. Colour lives in glyphs and the hero; red means act now with a thin 1px edge, never a fill (9 October 2026)" +- `D-44` — line 776, heading: "D-44. Compact sizing, white cards and smaller text retained over expression-test card tints (9 October 2026)" +- `D-45` — line 782, heading: "D-45. Phone is its own 390×844 design, never a desktop reflow (9 October 2026)" +- `D-46` — line 789, heading: "D-46. Every page has a focal part, a part of its own, a page-specific hero and command-bar action rules (9 October 2026)" +- `D-47` — line 795, heading: "D-47. Glass stays about 80% opaque with the top sheen kept (10 October 2026)" +- `D-48` — line 801, heading: "D-48. Figures stay in Geist Mono with tabular digits while chart axes use Geist Sans (10 October 2026)" +- `D-49` — line 807, heading: "D-49. Ward Flow 44px default row target (dense 36px, touch 52px) vs retired 48px PsychSift tap knob (10 October 2026)" +- `D-50` — line 813, heading: "D-50. Neutral T1 urgency pill vs Form 1A amber statutory warning badge (10 October 2026)" +- `D-51` — line 819, heading: "D-51. Delays filters hide rows with a stated hidden count while other queues highlight by default (10 October 2026)" +- `D-52` — line 826, heading: "D-52. 10 October page review rulings: time-axis overdue zone, 15px/20px strip figures, 8px people pips and incremental shared parts (10 October 2026)" +- `D-53` — line 832, heading: "D-53. Elevate design language and Glare mode adopted for Design System v9 (10 October 2026)" +- `D-54` — line 839, heading: "D-54. Tier time thresholds remain synthetic demo values requiring clinical sign-off (10 October 2026)" +- `D-55` — line 847, heading: "D-55. Supabase is Ward Flow's database and sign-in provider, and Azure is retired (15 October 2026)" +- `D-56` — line 853, heading: "D-56. Josh signs the clinical values that have a recorded source, for the prototype (15 October 2026)" +- `D-57` — line 859, heading: "D-57. Twenty Top 25 items move from the showcase into the real screens, each behind its own flag (15 October 2026)" +- `D-58` — line 886, heading: "D-58. Josh signs three more clinical values that have a national source, for the prototype (15 October 2026)" +- `D-59` — line 892, heading: "D-59. Each role sees only what it needs, from one access table (15 October 2026)" +- `D-60` — line 898, heading: "D-60. Criteria led discharge, for voluntary patients only (15 October 2026)" +- `D-61` — line 907, heading: "D-61. Tasks and Alerts are one queue called Alerts (16 October 2026)" +- `D-62` — line 913, heading: "D-62. The one Alerts queue is signed off and its flag removed (16 October 2026)" +- `D-63` — line 919, heading: "D-63. Josh signs the remaining clinical values and the criteria list, for the prototype (16 October 2026)" +- `D-64` — line 927, heading: "D-64. Criteria led discharge shows on the boards and warns at departure (16 October 2026)" +- `D-65` — line 934, heading: "D-65. Criteria led discharge on in production, typed patient specific criteria, and the AWOL bed hold review paused (17 October 2026)" +- `D-66` — line 950, heading: "D-66. Granted roles work without a second approval (17 October 2026)" +- `D-67` — line 958, heading: "D-67. Recover synthetic form drafts within the browser session (17 October 2026)" +- `D-68` — line 965, heading: "D-68. Bed meeting TV content and access (19 October 2026)" +- `D-69` — line 973, heading: "D-69. A referral with no legal status: "Needs an involuntary-capable bed until status is set." (20 October 2026)" +- `D-70` — line 980, heading: "D-70. No computed Mental Health Act periods: show only the recorded due time (20 October 2026)" +- `D-71` — line 986, heading: "D-71. Audit 1 clinical safety fixes: no suggested wards, wait-order queues, recorded facts only (20 October 2026)" +- `D-72` — line 999, heading: "D-72. Seven product decisions for the 21 October fix plan (21 October 2026)" +- `D-73` — line 1015, heading: "D-73. Batch merge safeguards (21 October 2026)" +- `D-74` — line 1031, heading: "D-74. Owner answers to the 21 October open questions (21 October 2026)" +- `D-75` — line 1052, heading: "D-75. UX audit follow-up, page caching, community gaps and the extra-travel bed hold (21 October 2026)" +- `D-76` — line 1065, heading: "D-76. Community referral gaps: the full move journey and asking more wards (21 October 2026)" +- `D-77` — line 1074, heading: "D-77. The ten bigger UX changes approved for build (22 October 2026)" +- `D-78` — line 1094, heading: "D-78. One decline list for wards and coordinators (22 October 2026)" +- `D-79` — line 1105, heading: "D-79. Wards see the full referral, with name and UMRN, before they answer (23 October 2026)" +- `D-80` — line 1116, heading: "D-80. Role access plan version 2: all 17 recommendations (23 October 2026)" +- `D-81` — line 1131, heading: "D-81. Pulled, with a pull time limit, for a bed kept for an incoming patient (24 October 2026)" +- `D-82` — line 1138, heading: "D-82. A PR may raise a phone size limit by up to 5 KB per screen without asking (26 October 2026)" +- `D-83` — line 1146, heading: "D-83. Patient names stay in full on every screen, copy and TV; privacy work must not make the app less useful (26 October 2026)" +- `D-84` — line 1157, heading: "D-84. The live app stays on Railway Singapore for now, and there is no server-side TV account yet (26 October 2026)" +- `D-85` — line 1165, heading: "D-85. "Held" only for a bed kept for its own patient; "pulled" for an incoming bed; "held up" for a delay (26 October 2026)" +- `D-86` — line 1172, heading: "D-86. No second approver for role grants, at pilot or otherwise (27 October 2026)" +- `D-87` — line 1179, heading: "D-87. D-36 and D-37 are replaced by the real-data gate (27 October 2026)" +- `D-88` — line 1191, heading: "D-88. The public demo opens with a demo code once the front door is on (27 October 2026)" +- `D-89` — line 1198, heading: "D-89. Redesign mock-up copies of large screens may pass the 1,500-line cap (30 October 2026)" +- `D-90` — line 1206, heading: "D-90. Help gets How do I…, Show me runs everywhere, and Help is built without a separate mock-up step (30 October 2026)" +- `D-91` — line 1217, heading: "D-91. One design home for the redesign, and Josh's answers on Help, Settings, search and notifications (31 October 2026)" +- `D-92` — line 1252, heading: "D-92. Help goes live before its mock-up review, one search box on Capacity, and referral rows on Home open the panel (31 October 2026)" +- `D-93` — line 1262, heading: "D-93. Legal form times show only as typed, never as a countdown (31 October 2026)" +- `D-94` — line 1270, heading: "D-94. Design rulings of 31 October 2026" +- `D-95` — line 1283, heading: "D-95. One page scroll, and a click outside closes a drawer (3 November 2026)" +- `D-96` — line 1293, heading: "D-96. A named bed is pulled, and its patient is not also waiting (8 November 2026)" +- `D-97` — line 1303, heading: "D-97. Owner answers on seven open items: N21, N25, N26, N29, N40, N41 and WF-B008 (8 November 2026)" +- `D-98` — line 1316, heading: "D-98. Friction pass: Quick bar, Patient peek and Ward peek (8 November 2026)" +- `D-99` — line 1325, heading: "D-99. "Real details" in the referral drawer's sample patients (25 September 2026)" +- `D-100` — line 1331, heading: "D-100. "Under a form" means one rule everywhere (9 November 2026)" +- `D-101` — line 1338, heading: "D-101. Clinically ready for discharge, with one delay reason (9 November 2026)" +- `D-102` — line 1347, heading: "D-102. Report-a-problem screenshots are blurred, and patient-table exports carry no names (9 November 2026)" +- `D-103` — line 1356, heading: "D-103. Any coordinator on shift may mark a bed override reviewed (9 November 2026)" +- `D-104` — line 1363, heading: "D-104. Care plans and safety flags: five defaults (9 November 2026)" +- `D-105` — line 1375, heading: "D-105. Optional clerk details on the patient record (8 November 2026)" +- `D-106` — line 1382, heading: "D-106. Clinical values from the 9 November 2026 documentation audit (9 November 2026)" +- `D-107` — line 1394, heading: "D-107. Working rules and documentation from the 9 November 2026 audit (9 November 2026)" +- `D-108` — line 1408, heading: "D-108. Six bed-flow features: keep D-93 and D-72, ED long-wait next steps (9 November 2026)" +- `D-109` — line 1417, heading: "D-109. Home's bed alert line tests the whole number shown (9 November 2026)" +- `D-110` — line 1424, heading: "D-110. ED and referral flow: neutral fit line, clearance timing and alternatives to admission (9 November 2026)" +- `D-111` — line 1434, heading: "D-111. Review and audit plan: first guards and four open answers (9 November 2026)" +- `D-112` — line 1448, heading: "D-112. Phone bottom bar: the floating bar (9 November 2026)" +- `D-113` — line 1457, heading: "D-113. Second patient identifier on accept and Mark done; no initials search (9 November 2026)" +- `D-114` — line 1467, heading: "D-114. Allow pull_request_target for three workflows only (9 November 2026)" +- `D-115` — line 1475, heading: "D-115. One count on every screen, and a missing patient shown everywhere (9 November 2026)" +- `D-116` — line 1489, heading: "D-116. Page tops: the PR #1581 recommendations stand (9 November 2026)" +- `D-117` — line 1506, heading: "D-117. Care plans follow-up: frequent presenter count, tribunal date and ward flags (10 November 2026)" +- `D-118` — line 1515, heading: "D-118. Page tops: Referrals and Ward phone limits up to 8 KB above main (10 November 2026)" +- `D-119` — line 1521, heading: "D-119. Page tops: phone limits raised to measured sizes, trim after merge (10 November 2026)" +- `D-120` — line 1528, heading: "D-120. Discharge and capacity values: pull limit, transport delay and discharge date slip (9 November 2026)" +- `D-121` — line 1537, heading: "D-121. Working rules, guards and your PC from the 10 November 2026 audit (10 November 2026)" +- `D-122` — line 1551, heading: "D-122. Clinical and privacy answers from the 10 November 2026 audit (10 November 2026)" +- `D-123` — line 1564, heading: "D-123. Master audit list answers: two clinical fixes and five working rules (10 November 2026)" +- `D-124` — line 1578, heading: "D-124. Offline saves whose record changed are refused and shown (10 November 2026)" +- `D-125` — line 1585, heading: "D-125. Top 10 audit follow-ups approved (10 November 2026)" +- `D-126` — line 1597, heading: "D-126. Ranked lists: neutral orders approved (10 November 2026)" +- `D-127` — line 1616, heading: "D-127. Neutral wording for a community treatment order at arrival (10 November 2026)" +- `D-128` — line 1623, heading: "D-128. Final master audit: nine answers (10 November 2026)" +- `D-129` — line 1638, heading: "D-129. Master audit findings: every recommendation approved (10 November 2026)" +- `D-130` — line 1688, heading: "D-130. Audit follow-ups: every remaining recommendation approved (10 November 2026)" +- `D-131` — line 1704, heading: "D-131. Hard-case demo data allowed, and the movement diagram follows the engine (10 November 2026)" +- `D-132` — line 1713, heading: "D-132. Audit fixes group 1: docs and CI guards (10 November 2026)" +- `D-133` — line 1723, heading: "D-133. Batch 1 (ED and referral flow): three answers (10 November 2026)" +- `D-134` — line 1733, heading: "D-134. Ward arrival and beds batch: how to hold, advocate line, night list and the feature freeze (10 November 2026)" +- `D-135` — line 1756, heading: "D-135. Batch 4, outages and pilot control: pack age and plan answers (10 November 2026)" +- `D-136` — line 1765, heading: "D-136. Feature batch 3 (shift change and follow-up): two signed values (10 November 2026)" +- `D-137` — line 1777, heading: "D-137. Feature build batch 5: count leak first, TV change mark and QR library (10 November 2026)" +- `D-138` — line 1793, heading: "D-138. Phone alert urgency by kind (10 November 2026)" +- `D-139` — line 1799, heading: "D-139. Phone Statistics ward list A to Z, and leave return times as typed (10 November 2026)" +- `D-140` — line 1809, heading: "D-140. Feature list adopted, and Wave 1 begins (10 November 2026)" +- `D-141` — line 1822, heading: "D-141. Batch 1 (ED and referral flow) design approved, with eleven answers (11 November 2026)" +- `D-142` — line 1840, heading: "D-142. Batch 4: design review and all twelve answers approved, D-80 amended for site features (11 November 2026)" +- `D-143` — line 1852, heading: "D-143. MHERT is MHERL in the glossary; SMHMR deferred (11 November 2026)" +- `D-144` — line 1860, heading: "D-144. Chip rows move only sideways on a phone (11 November 2026)" +- `D-145` — line 1866, heading: "D-145. Repository review: fixes, features and code health approved (11 November 2026)" +- `D-146` — line 1880, heading: "D-146. Performance audit fixes and the phone speed target (11 November 2026)" +- `D-147` — line 1886, heading: "D-147. Demo-code tries keep per-address keying until a trusted proxy is set (11 November 2026)" +- `D-148` — line 1892, heading: "D-148. Closing a bed stays one tap, and its toast offers a reason (11 November 2026)" +- `D-149` — line 1899, heading: "D-149. A record whose only links are withdrawn referrals can be archived (11 November 2026)" +- `D-150` — line 1910, heading: "D-150. The signed-in site runs on real Perth time (11 November 2026)" +- `D-151` — line 1918, heading: "D-151. A bed tile says when a change is waiting to save (11 November 2026)" +- `D-152` — line 1925, heading: "D-152. Returned to ED prompt sees every return in shared mode (11 November 2026)" +- `D-153` — line 1933, heading: "D-153. Keep the drawn-time settle check, and defer a separate agent GitHub identity (11 November 2026)" +- `D-154` — line 1941, heading: "D-154. The community desk's ED heads-up reads a narrow scoped list (11 November 2026)" +- `D-155` — line 1950, heading: "D-155. Check the live clock offset before clearing it (11 November 2026)" +- `D-156` — line 1957, heading: "D-156. Reset the synthetic live world after the clock fix (11 November 2026)" +- `D-157` — line 1965, heading: "D-157. Legal forms Print and Export include people admitted on a form (12 November 2026)" +- `D-158` — line 1971, heading: "D-158. A This-shift staff request is refused once that shift has ended (12 November 2026)" +- `D-159` — line 1977, heading: "D-159. Fixes from the 10 to 12 November PR review (12 November 2026)" ### `../history/archive/dated-notes/ward-management-decisions.md` (2026-08-18 – 2026-10-24 (mixed, see file)) diff --git a/docs/ward-flow/decisions.md b/docs/ward-flow/decisions.md index 5f63e60b5b..7ae8a7a4e6 100644 --- a/docs/ward-flow/decisions.md +++ b/docs/ward-flow/decisions.md @@ -181,6 +181,7 @@ Checked 9 November 2026 (D-107 audit). "Partly replaced" means part of the entry | [D-156](#d-156-reset-the-synthetic-live-world-after-the-clock-fix-11-november-2026) | Post-release reset of the live synthetic world withdrawn | Withdrawn 12 November 2026 | | [D-157](#d-157-legal-forms-print-and-export-include-people-admitted-on-a-form-12-november-2026) | Legal forms Print and Export include people admitted on a form | Active | | [D-158](#d-158-a-this-shift-staff-request-is-refused-once-that-shift-has-ended-12-november-2026) | A This-shift staff request is refused once that shift has ended | Active | +| [D-159](#d-159-fixes-from-the-10-to-12-november-pr-review-12-november-2026) | Fixes from the 10 to 12 November PR review | Active | --- @@ -1109,7 +1110,7 @@ is inferred. 2. **Never other destinations.** The ward never sees which other wards or teams the patient was referred to (FD-23, and D-17 in `archive/dated-notes/owner-decisions-2026-09-1x.md`, not the D-17 in this log). 3. **Fit lines split.** The card shows the patient's checks on one line and the ward's own state, such as a stale bed count or no free bed, on another, so a ward problem no longer reads as the patient failing. - **What it replaces:** Josh's 26 September 2026 rule that a ward sees no identity before it accepts, and the part of D-68 item 3 that kept the name and UMRN off the accept card until acceptance. D-39 and D-74 item 5 stand: no `WF-...` journey number or referral number shows on the card. -- **Boundary:** synthetic data only. with synthetic referral forms and documents for the seeded journeys. Showing identity before acceptance needs the privacy review before any real patient data. Being built in PR #588, still a draft when this was recorded. This authorises no deployment, provider configuration change or spending. +- **Boundary:** Synthetic data only, with synthetic referral forms and documents for the seeded journeys. Showing identity before acceptance needs the privacy review before any real patient data. Being built in PR #588, still a draft when this was recorded. This authorises no deployment, provider configuration change or spending. - **Why:** A ward cannot make a safe decision on a referral it cannot read. The engine already held about half the fields, and community teams already saw them. ## D-80. Role access plan version 2: all 17 recommendations (23 October 2026) @@ -1972,3 +1973,15 @@ is inferred. - **Date:** 12 November 2026, 8:44 AM to 8:45 AM AWST (00:44 to 00:45 UTC). **Decided by:** Josh, ruling on the two open review threads on PR #1623. Numbered D-158 because D-157 records the Legal forms ruling from the same answers, and D-155 and D-156 are already on main via PR #1626. - **Decision:** A `this_shift` staff request whose chosen shift has already ended is refused, using the Staff needed sheet's existing save error, with the reason "This shift has already ended. Choose this shift or next shift again". "Next shift" late-save behaviour is unchanged: a Next-shift request that lands just after the shift changes is still kept. - **Boundary:** synthetic data only. No signed clinical value or clinical rule. Approves no merge or release. + +## D-159. Fixes from the 10 to 12 November PR review (12 November 2026) + +- **Date:** 12 November 2026, 12:04 PM AWST (04:04 UTC). **Decided by:** Josh, "Yes to all" in the Ward Flow project thread "Review recent PRs for design regressions", answering its three numbered questions: fix the review's findings in one PR, keep v6's quick-link chip rows as the rule, and refresh the master design's Home and Ward tabs from main. +- **Decision:** + 1. **Page header rule:** a page top has at most one on-hero button, the next step's light button, plus one row of quick-link chips under it that scrolls sideways on a phone (page tops v6, D-116). DESIGN-SYSTEM.md says so in place of "one on-hero action". + 2. **New referral chip:** phone-only on the ward top, as D-116 says; off the phone the bar's New menu is the way in. + 3. **Chip rows (D-144 follow-up):** a sideways chip row still never slides up and down, but a finger that starts on it scrolls the page (`touch-action: pan-x pan-y`, `overflow-y: hidden`). + 4. **Home's Waiting in ED strip on a tablet:** cells keep their whole words and the strip scrolls sideways when the departments do not fit. + 5. **The other review findings** are fixed as recommended: eight rows then Show more on the new lists, Leave on the band hero, one phone rule for Print and Export, kit control sizes, shorter transport choices, honest outage catch-up labels, one ready count (D-115), one ended-form wording, the uptime issue raised before the run fails, a trusted proxy list other than `*` ignored, and Home's loading cards no longer hiding the page. + 6. **MHERL wording:** the synthetic community example reads "joint assessment with the WA Police co-response team, referred through MHERL", so the phone line refers rather than attends. Josh may reword it. +- **Boundary:** synthetic data only. No signed clinical value, time limit or clinical rule changes. Approves no release. diff --git a/docs/ward-flow/governance/SETTINGS-AND-SECRETS-INVENTORY.md b/docs/ward-flow/governance/SETTINGS-AND-SECRETS-INVENTORY.md index 17f5e72ffc..c0b192f799 100644 --- a/docs/ward-flow/governance/SETTINGS-AND-SECRETS-INVENTORY.md +++ b/docs/ward-flow/governance/SETTINGS-AND-SECRETS-INVENTORY.md @@ -31,30 +31,31 @@ that is not in a table below, so the list cannot fall behind the code. ## Web app (Railway web service) -| Setting | Secret | Purpose | Read by | Checked on service | -| ------------------------------------------- | ------ | ------------------------------------------------------------ | -------------------------------------- | ------------------ | -| `NEXT_PUBLIC_DEMO_MODE` | No | Public demo switch | `src/instrumentation.ts` | | -| `NEXT_PUBLIC_SITE_URL` | No | The site's own address | `src` | | -| `NEXT_PUBLIC_WARD_API_BASE_URL` | No | Backend address for the shared workspace | `src` | | -| `NEXT_PUBLIC_WARD_AUTH_PROVIDER` | No | Sign-in provider (blank means Supabase, D-55) | `src/lib/security-headers.ts` | | -| `NEXT_PUBLIC_WARD_COORDINATOR_EMAIL` | No | Bootstrap coordinator sign-in hint | `src/components/ward-management` | | -| `NEXT_PUBLIC_WARD_DATA_MODE` | No | Data mode; drafts are kept only in `prototype` | `ward-session-drafts.ts` | | -| `NEXT_PUBLIC_WARD_DEMO_URL` | No | Link to the public demo | `src` | | -| `NEXT_PUBLIC_WARD_ENVIRONMENT` | No | Which environment flags apply | `ward-feature-flags.ts` | | -| `NEXT_PUBLIC_WARD_FLAGS` | No | Feature flags | `ward-feature-flags.ts` | | -| `NEXT_PUBLIC_WARD_OPEN_SIGNUP` | No | Whether the front door offers sign-up | `front-door/use-front-door-auth.ts` | | -| `NEXT_PUBLIC_WARD_SHARED_ENABLED` | No | Shared workspace switch | `src` | | -| `NEXT_PUBLIC_WARD_SUPABASE_PUBLISHABLE_KEY` | No | Supabase publishable key (public by design) | `ward-auth-session.ts` | | -| `NEXT_PUBLIC_WARD_SUPABASE_URL` | No | Supabase project address | `ward-auth-session.ts` | | -| `NEXT_PUBLIC_WARD_SUPPORT_EMAIL` | No | Report-a-problem support address | `help/ward-help-report.ts` | | -| `WARD_FRONT_DOOR` | No | Front door (pass) switch | `src/proxy.ts`, `api/session/route.ts` | | -| `WARD_DEMO_CODE` | Yes | The demo code people type at the front door | `src/lib/front-door-pass.ts` | | -| `WARD_PASS_SECRET` | Yes | Signs the `wf_pass` cookie | `src/lib/front-door-pass.ts` | | -| `SENTRY_DSN` | Yes | Server error reporting, off when blank | `src/lib/sentry-server.ts` | | -| `SENTRY_ENVIRONMENT` | No | Label on error reports | `src/lib/sentry-server.ts` | | -| `SENTRY_ENABLE_LOGS` | No | Sends warn and error log lines to Sentry Logs unless `false` | `src/lib/sentry-server.ts` | | -| `SENTRY_RELEASE` | No | Release label on error reports | `src/lib/sentry-server.ts` | | -| `SUPABASE_SERVICE_ROLE_KEY` | Yes | **Must never be set on the web app.** Read only to refuse it | `src/instrumentation.ts` | | +| Setting | Secret | Purpose | Read by | Checked on service | +| ------------------------------------------- | ------ | ------------------------------------------------------------------------------------------ | -------------------------------------- | ------------------ | +| `NEXT_PUBLIC_DEMO_MODE` | No | Public demo switch | `src/instrumentation.ts` | | +| `NEXT_PUBLIC_SITE_URL` | No | The site's own address | `src` | | +| `NEXT_PUBLIC_WARD_API_BASE_URL` | No | Backend address for the shared workspace | `src` | | +| `NEXT_PUBLIC_WARD_AUTH_PROVIDER` | No | Sign-in provider (blank means Supabase, D-55) | `src/lib/security-headers.ts` | | +| `NEXT_PUBLIC_WARD_COORDINATOR_EMAIL` | No | Bootstrap coordinator sign-in hint | `src/components/ward-management` | | +| `NEXT_PUBLIC_WARD_DATA_MODE` | No | Data mode; drafts are kept only in `prototype` | `ward-session-drafts.ts` | | +| `NEXT_PUBLIC_WARD_DEMO_URL` | No | Link to the public demo | `src` | | +| `NEXT_PUBLIC_WARD_ENVIRONMENT` | No | Which environment flags apply | `ward-feature-flags.ts` | | +| `NEXT_PUBLIC_WARD_FLAGS` | No | Feature flags | `ward-feature-flags.ts` | | +| `NEXT_PUBLIC_WARD_OPEN_SIGNUP` | No | Whether the front door offers sign-up | `front-door/use-front-door-auth.ts` | | +| `NEXT_PUBLIC_WARD_SHARED_ENABLED` | No | Shared workspace switch | `src` | | +| `NEXT_PUBLIC_WARD_SUPABASE_PUBLISHABLE_KEY` | No | Supabase publishable key (public by design) | `ward-auth-session.ts` | | +| `NEXT_PUBLIC_WARD_SUPABASE_URL` | No | Supabase project address | `ward-auth-session.ts` | | +| `NEXT_PUBLIC_WARD_SUPPORT_EMAIL` | No | Report-a-problem support address | `help/ward-help-report.ts` | | +| `WARD_FRONT_DOOR` | No | Front door (pass) switch | `src/proxy.ts`, `api/session/route.ts` | | +| `WARD_DEMO_CODE` | Yes | The demo code people type at the front door | `src/lib/front-door-pass.ts` | | +| `WARD_PASS_SECRET` | Yes | Signs the `wf_pass` cookie | `src/lib/front-door-pass.ts` | | +| `WARD_TRUSTED_PROXY_ADDRESSES` | No | Demo-code try limit: only `*` applies here; an IP list is ignored with one warning (D-147) | `src/lib/demo-code-tries.ts` | | +| `SENTRY_DSN` | Yes | Server error reporting, off when blank | `src/lib/sentry-server.ts` | | +| `SENTRY_ENVIRONMENT` | No | Label on error reports | `src/lib/sentry-server.ts` | | +| `SENTRY_ENABLE_LOGS` | No | Sends warn and error log lines to Sentry Logs unless `false` | `src/lib/sentry-server.ts` | | +| `SENTRY_RELEASE` | No | Release label on error reports | `src/lib/sentry-server.ts` | | +| `SUPABASE_SERVICE_ROLE_KEY` | Yes | **Must never be set on the web app.** Read only to refuse it | `src/instrumentation.ts` | | ## Backend (Railway backend service) diff --git a/src/components/ward-management/alerts/use-broadcast-audience.ts b/src/components/ward-management/alerts/use-broadcast-audience.ts index 7339b08d0d..d278ecfd3f 100644 --- a/src/components/ward-management/alerts/use-broadcast-audience.ts +++ b/src/components/ward-management/alerts/use-broadcast-audience.ts @@ -21,17 +21,18 @@ import { broadcastKind, type BroadcastAlert, type BroadcastTargetScope } from ". import type { LiveBedCall } from "./broadcast-composer"; export function useBroadcastAudience( - state: Pick, + state: Pick, initial: { templateId: string; scope: BroadcastTargetScope }, wardsOnly = false, ) { - const { units, admissions, bedReleases } = state; + const { units, admissions, bedReleases, movements, referrals } = state; const groups = useMemo(() => audienceGroups(units), [units]); - // The wards Capacity shows with a bed ready now. - const wardsWithBedNow = useMemo( - () => units.filter((unit) => readyBedsNow(unit, admissions, bedReleases ?? []) > 0).map((unit) => unit.id), - [units, admissions, bedReleases], - ); + // The wards Capacity shows with a bed ready now: a bed named for an open journey or accepted + // referral is not ready (D-115 item 3). + const wardsWithBedNow = useMemo(() => { + const holds = [...movements, ...referrals]; + return units.filter((unit) => readyBedsNow(unit, admissions, bedReleases ?? [], holds) > 0).map((unit) => unit.id); + }, [units, admissions, bedReleases, movements, referrals]); const [audience, setAudience] = useState(() => templateAudience(initial.templateId, initial.scope, audienceGroups(units)), ); diff --git a/src/components/ward-management/board/ward-board-detail-panel.tsx b/src/components/ward-management/board/ward-board-detail-panel.tsx index e4f511f6e7..868148dbe2 100644 --- a/src/components/ward-management/board/ward-board-detail-panel.tsx +++ b/src/components/ward-management/board/ward-board-detail-panel.tsx @@ -715,6 +715,6 @@ export function TransportWaitField({ } const TRANSPORT_WAIT_ITEMS: { id: "" | "waiting" | "collected"; label: string }[] = [ - { id: "waiting", label: "No, still waiting for transport" }, - { id: "collected", label: "Yes, gone with transport" }, + { id: "waiting", label: "Still waiting" }, + { id: "collected", label: "Gone with transport" }, ]; diff --git a/src/components/ward-management/board/ward-peek.tsx b/src/components/ward-management/board/ward-peek.tsx index d7caa01ae8..9411c40e55 100644 --- a/src/components/ward-management/board/ward-peek.tsx +++ b/src/components/ward-management/board/ward-peek.tsx @@ -25,17 +25,20 @@ export function WardPeekButton({ label: string; variant?: "pri" | "sec"; }) { - const { units, admissions, bedReleases, referrals } = useWardFlowSelect( + const { units, admissions, bedReleases, movements, referrals } = useWardFlowSelect( "units", "admissions", "bedReleases", + "movements", "referrals", ); const canOpen = useCanOpenHref(); const unit = units.find((candidate) => candidate.id === unitId); if (!unit || !canOpen(href)) return null; - // Same rule as Search's ready count, without pulling Search's code into every board. - const ready = unit.allocatable && unit.empty ? readyBedsNow(unit, admissions, [], referrals) : null; + // Same rule as Search's ready count, without pulling Search's code into every board. The named + // holds are open journeys and accepted referrals, as Home and the rail count them (D-115 item 3). + const ready = + unit.allocatable && unit.empty ? readyBedsNow(unit, admissions, [], [...movements, ...referrals]) : null; const inUse = wardBedsInUse(unit, admissions, bedReleases); const site = siteByCode(unit.siteCode)?.name; return ( diff --git a/src/components/ward-management/capacity/capacity-closed-beds-review.tsx b/src/components/ward-management/capacity/capacity-closed-beds-review.tsx index 9b7c38ec4f..eeb2ec249a 100644 --- a/src/components/ward-management/capacity/capacity-closed-beds-review.tsx +++ b/src/components/ward-management/capacity/capacity-closed-beds-review.tsx @@ -1,6 +1,9 @@ +"use client"; + import Link from "next/link"; +import { useState } from "react"; -import { buttonClass, Card, CardHead, StatusGlyph } from "@/components/wf"; +import { buttonClass, Card, CardHead, ShowMoreRow, foldHidden, foldLimit } from "@/components/wf"; import { SpelledCodes } from "@/components/ward-management/ward-abbreviations"; import { formatInstantWithDay, type Instant } from "@/components/ward-management/ward-clock"; import type { Unit } from "@/components/ward-management/ward-model"; @@ -10,11 +13,15 @@ import { unitHref } from "@/components/ward-management/shell/ward-facade"; import { closedBedsPastReview } from "../ward-bed-closure-review"; import styles from "./capacity-page.module.css"; +/** About eight rows, then the kit's Show more (DESIGN-SYSTEM.md section 4). */ +const REVIEW_ROWS = 8; + /** * Item 6 (10 Nov 2026): "Closed beds past review". Every bed still closed whose chosen review time * has passed, so a bed never stays closed by accident. A prompt only: reopening is the ward's tap on * its own page, so each row links there. Wards in the neutral service-then-name order. Hidden when - * no closed bed is past its review, as nothing then needs a look. + * no closed bed is past its review, as nothing then needs a look. A desk that opens no ward reads the + * same plain "review was" line with no warning (fewer warnings; the night list is neutral, D-134). */ export function ClosedBedsPastReview({ units, @@ -27,13 +34,16 @@ export function ClosedBedsPastReview({ executive?: boolean; }) { const canOpen = useCanOpenHref(); + const [showAll, setShowAll] = useState(false); const rows = closedBedsPastReview(units, now); if (rows.length === 0) return null; + const hidden = foldHidden(rows.length, REVIEW_ROWS); + const shown = showAll || hidden === 0 ? rows : rows.slice(0, foldLimit(rows.length, REVIEW_ROWS)); return ( -
    - {rows.map(({ unit, mark }) => { +
      + {shown.map(({ unit, mark }) => { const href = unitHref(unit.id); return (
    • @@ -54,15 +64,20 @@ export function ClosedBedsPastReview({ > Open ward - ) : ( - - Overdue - - )} + ) : null}
    • ); })}
    + {hidden > 0 ? ( + setShowAll((all) => !all)} + expanded={showAll} + controls="capacity-closed-review-list" + > + {showAll ? "Show fewer" : `Show ${hidden} more`} + + ) : null} ); } diff --git a/src/components/ward-management/capacity/capacity-intake-windows-card.tsx b/src/components/ward-management/capacity/capacity-intake-windows-card.tsx index 75d32c8a52..b73050ce19 100644 --- a/src/components/ward-management/capacity/capacity-intake-windows-card.tsx +++ b/src/components/ward-management/capacity/capacity-intake-windows-card.tsx @@ -1,4 +1,8 @@ -import { Card, CardHead } from "@/components/wf"; +"use client"; + +import { useState } from "react"; + +import { Card, CardHead, ShowMoreRow, foldHidden, foldLimit } from "@/components/wf"; import { SpelledCodes } from "@/components/ward-management/ward-abbreviations"; import { formatInstant, type Instant } from "@/components/ward-management/ward-clock"; import type { Unit } from "@/components/ward-management/ward-model"; @@ -6,6 +10,9 @@ import type { Unit } from "@/components/ward-management/ward-model"; import { tonightIntakeWindowList } from "./ward-intake-window-list"; import styles from "./capacity-intake-windows-card.module.css"; +/** About eight rows, then the kit's Show more (DESIGN-SYSTEM.md section 4). */ +const INTAKE_ROWS = 8; + /** * Feature item 13 (10 Nov 2026): "Tonight's intake windows" on the Capacity Overview, so night staff * see which wards said when they can take an admission before ringing round. Information only: the @@ -22,12 +29,15 @@ export function CapacityIntakeWindowsCard({ onWard?: (unitId: string) => void; }) { const { set, notSetCount } = tonightIntakeWindowList(units, now); + const [showAll, setShowAll] = useState(false); + const hidden = foldHidden(set.length, INTAKE_ROWS); + const shown = showAll || hidden === 0 ? set : set.slice(0, foldLimit(set.length, INTAKE_ROWS)); return ( {set.length > 0 ? ( -
      - {set.map(({ unit, intake }) => ( +
        + {shown.map(({ unit, intake }) => (
      • {onWard ? (
      ) : null} + {hidden > 0 ? ( + setShowAll((all) => !all)} + expanded={showAll} + controls="capacity-intake-windows-list" + > + {showAll ? "Show fewer" : `Show ${hidden} more`} + + ) : null}

      {notSetCount === 0 ? "Every ward has set a window." diff --git a/src/components/ward-management/capacity/service-capacity-tracker.ts b/src/components/ward-management/capacity/service-capacity-tracker.ts index f9b73758ea..aeaa5dafa3 100644 --- a/src/components/ward-management/capacity/service-capacity-tracker.ts +++ b/src/components/ward-management/capacity/service-capacity-tracker.ts @@ -1,6 +1,6 @@ import { currentLegalDueAt } from "@/components/ward-management/legal-forms/legal-form-progression"; import { type Instant, isClockBreached } from "@/components/ward-management/ward-clock"; -import { readyBedsNow } from "@/components/ward-management/ward-bed-states"; +import { readyBedsNow, type ReservationReferral } from "@/components/ward-management/ward-bed-states"; import { isWaitingInEd } from "@/components/ward-management/ward-ed-presence"; import { elapsedOpenMinutes, isDetainedUnderTheAct } from "@/components/ward-management/ed/ed-home-derivations"; import { @@ -342,8 +342,11 @@ export function trackServiceBedCapacity( accessTargetMinutes?: number, /** Admissions, so a live pull counts as a bed in use (`ward-occupancy.ts`). */ admissions: readonly Admission[] = [], + /** Accepted referrals; with `movements` they are the named holds a ready bed leaves out (D-115). */ + referrals: readonly ReservationReferral[] = [], ): StatewideCapacityReport { const releases = [...bedReleases]; + const holds: readonly ReservationReferral[] = [...movements, ...referrals]; const serviceTrackings: ServiceCapacityTracking[] = []; for (const service of HEALTH_SERVICES) { @@ -376,7 +379,7 @@ export function trackServiceBedCapacity( for (const u of siteUnits) { // Audit 24-4 NA2-08: the one ready figure every screen prints (`readyBedsNow`), not the // pull gate `openBedsNow`, which also drops beds still being made ready. - siteFree += readyBedsNow(u, admissions, releases); + siteFree += readyBedsNow(u, admissions, releases, holds); siteTotal += u.beds; // Occupied is the board's own buckets (Josh, 25 Sept 2026: the rail matches the board): the // Occupied and Pulled boxes, through the one shared count every screen reads diff --git a/src/components/ward-management/community/community-demo-cohort.ts b/src/components/ward-management/community/community-demo-cohort.ts index 51e239bfe0..531d50647d 100644 --- a/src/components/ward-management/community/community-demo-cohort.ts +++ b/src/components/ward-management/community/community-demo-cohort.ts @@ -131,9 +131,9 @@ export const DEMO_COMMUNITY_REFERRALS: DemoReferral[] = [ patientDetails: "Male 45y", legalStatus: "Form 1A MHA", legalStatusTone: "danger", - origin: "MHERL / WA Police co-response", + origin: "MHERL referral, WA Police co-response", clinicalSummary: - "Origin: MHERL / WA Police co-response. Severe paranoid persecutory delusions, barricaded in residence, medication non-adherence x3 months. Community mental health crisis team dispatched for joint assessment.", + "Origin: MHERL referral, WA Police co-response. Severe paranoid persecutory delusions, barricaded in residence, medication non-adherence x3 months. Community mental health crisis team dispatched for joint assessment.", primaryActionLabel: "Accept & allocate", secActionLabel: "Dispatch crisis team", declineActionLabel: "Decline", diff --git a/src/components/ward-management/community/community-screen-modals.tsx b/src/components/ward-management/community/community-screen-modals.tsx index eeda0c1b83..f31fa147b7 100644 --- a/src/components/ward-management/community/community-screen-modals.tsx +++ b/src/components/ward-management/community/community-screen-modals.tsx @@ -299,8 +299,8 @@ export function CommunityScreenNoteModal({ view }: { view: CommunityScreenView }

      - Example deployment (synthetic): Urgent joint assessment with MHERL / WA Police co-response, - travelling from {teamConfig.campus} to a catchment residence. + Example deployment (synthetic): Urgent joint assessment with the WA Police co-response team, + referred through MHERL, travelling from {teamConfig.campus} to a catchment residence.

      {/* Column heads for the rows below. The rows are list items, so these are a visual diff --git a/src/components/ward-management/night/night-view-screen.tsx b/src/components/ward-management/night/night-view-screen.tsx index 26400b9026..71536148c0 100644 --- a/src/components/ward-management/night/night-view-screen.tsx +++ b/src/components/ward-management/night/night-view-screen.tsx @@ -2,11 +2,24 @@ import { Ambulance, BedSingle, CalendarClock, Clock, Phone } from "lucide-react"; import Link from "next/link"; -import { useMemo } from "react"; +import { useMemo, useState } from "react"; -import { Card, CardHead, EmptyState, Hero, HeroFigures, HeroTop, LiveChip, StatusGlyph } from "@/components/wf"; +import { + Card, + CardHead, + EmptyState, + Hero, + HeroFigures, + HeroTop, + LiveChip, + ShowMoreRow, + StatusGlyph, + foldHidden, + foldLimit, +} from "@/components/wf"; import { movementHref, unitHref } from "@/components/ward-management/shell/ward-facade"; import { WardPrototypeFooter } from "@/components/ward-management/shell/ward-prototype-footer"; +import { useServiceScope } from "@/components/ward-management/shell/ward-service-store"; import { formatInstant, formatInstantWithDay, minuteOfDay } from "@/components/ward-management/ward-clock"; import { useWardFlowClock, useWardFlowSelect, useWardNetwork } from "@/components/ward-management/ward-flow-provider"; import { @@ -17,7 +30,11 @@ import { nightReadyBeds, } from "@/components/ward-management/night-view-figures"; import { usePatientOf } from "@/components/ward-management/ward-patient-name"; -import { unitHealthService } from "@/components/ward-management/ward-service-scope"; +import { + healthServiceAcronym, + movementBelongsToService, + unitHealthService, +} from "@/components/ward-management/ward-service-scope"; import styles from "./night-view.module.css"; @@ -26,6 +43,24 @@ const WAITING_HREF = "/ward-flow/waiting"; const MOVEMENTS_HREF = "/ward-flow/movements"; const ON_CALL_HREF = "/ward-flow/on-call"; +/** About eight rows, then the kit's Show more (DESIGN-SYSTEM.md section 4). */ +const NIGHT_ROWS = 8; + +/** A list's fold: the rows to draw and the Show more row, if folding hides two or more. */ +function useFold(items: readonly T[], id: string) { + const [open, setOpen] = useState(false); + const hidden = foldHidden(items.length, NIGHT_ROWS); + return { + rows: open || hidden === 0 ? items : items.slice(0, foldLimit(items.length, NIGHT_ROWS)), + more: + hidden === 0 ? null : ( + setOpen((was) => !was)} expanded={open} controls={id}> + {open ? "Show fewer" : `Show ${hidden} more`} + + ), + }; +} + /** * Night view (feature plan item 29): the coordinator's overnight screen. Beds ready, ED waits by * Step, who is on the way, tonight's intake windows and the key on-call lines, on one page. Every @@ -43,13 +78,24 @@ export function NightViewScreen() { const now = useWardFlowClock(); const network = useWardNetwork(); const patientOf = usePatientOf(); + // The chosen service scopes each figure exactly as Home's matching figure: ED waits and journeys + // follow it; beds stay network-wide (a placement may use any ward) and say so, in Home's words. + const service = useServiceScope(); + const networkWord = service === null ? "" : " network-wide"; + const scopedMovements = useMemo( + () => + service === null ? movements : movements.filter((movement) => movementBelongsToService(movement, service, units)), + [movements, units, service], + ); const beds = useMemo( () => nightReadyBeds(units, admissions, bedReleases, movements, referrals), [units, admissions, bedReleases, movements, referrals], ); - const waits = useMemo(() => nightEdWaits(movements, now), [movements, now]); - const onTheWay = useMemo(() => nightOnTheWay(movements, now), [movements, now]); + const waits = useMemo(() => nightEdWaits(scopedMovements, now), [scopedMovements, now]); + const onTheWay = useMemo(() => nightOnTheWay(scopedMovements, now), [scopedMovements, now]); + const bedRows = useFold(beds.wards, "night-beds-list"); + const movingRows = useFold(onTheWay, "night-moving-list"); const contacts = useMemo(() => nightContacts(network.sites, minuteOfDay(now)), [network.sites, now]); const unitName = (id: string | undefined) => units.find((unit) => unit.id === id)?.name; const atStep = waits.waiting - waits.underFirstStep; @@ -59,7 +105,7 @@ export function NightViewScreen() { } > Beds ready now} icon={BedSingle} iconPlain - meta={`${beds.total} ready`} + meta={`${beds.total} ready${networkWord}`} action={ Capacity @@ -109,8 +155,8 @@ export function NightViewScreen() { {beds.wards.length === 0 ? ( ) : ( -
        - {beds.wards.map(({ unit, ready }) => ( +
          + {bedRows.rows.map(({ unit, ready }) => (
        • @@ -128,6 +174,7 @@ export function NightViewScreen() { ))}
        )} + {bedRows.more} {beds.wardsWithNone > 0 ? (

        {beds.wardsWithNone} {beds.wardsWithNone === 1 ? "ward has" : "wards have"} no bed ready. Wards are in a @@ -189,8 +236,8 @@ export function NightViewScreen() { {onTheWay.length === 0 ? ( ) : ( -

          - {onTheWay.map((movement) => { +
            + {movingRows.rows.map((movement) => { const eta = movement.transport?.estimatedAt; return (
          • @@ -208,6 +255,7 @@ export function NightViewScreen() { })}
          )} + {movingRows.more} diff --git a/src/components/ward-management/night/night-view.module.css b/src/components/ward-management/night/night-view.module.css index 1f392d0aa5..0a9161faf1 100644 --- a/src/components/ward-management/night/night-view.module.css +++ b/src/components/ward-management/night/night-view.module.css @@ -80,7 +80,7 @@ a.rowTitle { .headLink { display: inline-flex; align-items: center; - min-height: 32px; + min-height: var(--wf-h-md); color: var(--wf-accent-ink); font: 500 var(--wf-fs-13) / 18px var(--wf-font); text-decoration: underline; diff --git a/src/components/ward-management/patients/patient-glance.tsx b/src/components/ward-management/patients/patient-glance.tsx index 7e44391532..cd7e47e629 100644 --- a/src/components/ward-management/patients/patient-glance.tsx +++ b/src/components/ward-management/patients/patient-glance.tsx @@ -17,8 +17,9 @@ import { observationSummary } from "@/components/ward-management/ward-observatio import { riskFlagLabel } from "@/components/ward-management/referrals/referral-risk-flags"; import type { FormTimeLeft } from "@/components/ward-management/legal-forms/legal-form-time-left"; import { - formProgression, + FORM_RECORDED_ENDED, formProgressionPhrase, + legalFormEnd, } from "@/components/ward-management/legal-forms/legal-form-progression"; import type { PatientMode } from "./patient-mode"; import { interpreterRequired } from "./patient-flight-header"; @@ -123,15 +124,23 @@ function legalTile({ patient, movement, formTimeLeft, mode, onOpenForms, recordC } // Audit S-6: a referral or transport form that ended at arrival or at the examination is never // shown as in force; the band names it as ended and the inpatient order still to record. - const progression = formProgression(movement); - if (progression) + // A form recorded as ended is read before the neutral 5A/5B arrival wording (as the Legal forms page). + const end = legalFormEnd(movement); + if (end?.kind === "progressed") return { ...base, value: status ?? "Not recorded", quiet: status === undefined, - sub: `${formProgressionPhrase(progression)} · Form ${progression.nextCode} to record`, + sub: `${formProgressionPhrase(end.progression)} · Form ${end.progression.nextCode} to record`, tone: "warning", }; + if (end?.kind === "recorded" && movement?.legalForm) + return { + ...base, + value: status ?? "Not recorded", + quiet: status === undefined, + sub: `Form ${movement.legalForm.code} ${FORM_RECORDED_ENDED.toLowerCase()}`, + }; const form = movement?.legalForm ? `Form ${movement.legalForm.code}` : cto ? `Form ${cto.form}` : undefined; return { ...base, diff --git a/src/components/ward-management/quick-bar/ward-quick-bar.module.css b/src/components/ward-management/quick-bar/ward-quick-bar.module.css index 9b4c440246..8a9f9e4022 100644 --- a/src/components/ward-management/quick-bar/ward-quick-bar.module.css +++ b/src/components/ward-management/quick-bar/ward-quick-bar.module.css @@ -117,9 +117,10 @@ takes it back), so the row has nothing to drag up and down on iPhone Safari. */ margin: -4px -16px; padding: 4px 24px 4px 16px; - /* D-144: on a phone this row only ever moves sideways. A finger on it pans the row, never the - row or page up and down (iPhone still slid the row vertically after every layout fix). */ - touch-action: pan-x; + /* D-144 and D-159: the row itself moves only sideways (overflow-y hidden), but a finger + that starts on it can still scroll the page up and down. */ + touch-action: pan-x pan-y; + overscroll-behavior-x: contain; scroll-padding-left: 16px; /* A control focused from the keyboard scrolls clear of the right-hand fade. */ scroll-padding-right: 48px; diff --git a/src/components/ward-management/referrals/referral-no-reply-flag.tsx b/src/components/ward-management/referrals/referral-no-reply-flag.tsx index f173f202df..5a341add14 100644 --- a/src/components/ward-management/referrals/referral-no-reply-flag.tsx +++ b/src/components/ward-management/referrals/referral-no-reply-flag.tsx @@ -60,15 +60,14 @@ export function ReferralNotOpenedTag({ const unitIds = referralNotOpenedUnitIds(referral, now); if (!unitIds.length) return null; const wards = unitIds.map((id) => flow.units.find((unit) => unit.id === id)?.name ?? "A ward"); + const why = `Not opened by ${wards.join(", ")} after ${NOT_OPENED_TAG_MINUTES} minutes`; return ( - + {NOT_OPENED_TAG} + {/* The tooltip is mouse-only, so keyboard and screen reader users get the wards here. */} + : {why} ); } diff --git a/src/components/ward-management/search/global-search-preview.tsx b/src/components/ward-management/search/global-search-preview.tsx index 49e0b0d5c4..7032417a83 100644 --- a/src/components/ward-management/search/global-search-preview.tsx +++ b/src/components/ward-management/search/global-search-preview.tsx @@ -508,7 +508,10 @@ function WardPreview({ href: string; onOpen: PreviewProps["onOpen"]; }) { - const ready = unitReadyBedCount(unit); + // The ready count Search lists, with the same pulls and named holds (D-115 item 3). Outside the + // provider nothing is held, as before. + const flow = useOptionalWardFlow(); + const ready = unitReadyBedCount(unit, flow?.admissions ?? [], flow ? [...flow.movements, ...flow.referrals] : []); const incoming = movements.filter((movement) => isOpen(movement) && movement.acceptedUnitId === unit.id); const referred = movements.filter( (movement) => isOpen(movement) && !movement.acceptedUnitId && movement.referredUnitIds.includes(unit.id), diff --git a/src/components/ward-management/search/ward-smart-search.ts b/src/components/ward-management/search/ward-smart-search.ts index 2b0aeda8e4..0c9d1ac280 100644 --- a/src/components/ward-management/search/ward-smart-search.ts +++ b/src/components/ward-management/search/ward-smart-search.ts @@ -209,8 +209,9 @@ type TaskSearchResult = { */ export function unitReadyBedCount( unit: Unit, - admissions: readonly Admission[] = [], - referrals: readonly ReservationReferral[] = [], + admissions: readonly Admission[], + /** The named holds: open journeys and accepted referrals (D-115 item 3). */ + referrals: readonly ReservationReferral[], ): number | null { if (unit.allocatable && unit.empty) { return readyBedsNow(unit, admissions, [], referrals); diff --git a/src/components/ward-management/shell-next/next-phone-menu-top.tsx b/src/components/ward-management/shell-next/next-phone-menu-top.tsx index 80181b48dc..58c3ab42fd 100644 --- a/src/components/ward-management/shell-next/next-phone-menu-top.tsx +++ b/src/components/ward-management/shell-next/next-phone-menu-top.tsx @@ -34,12 +34,13 @@ export function NextPhoneMenuTop({ onGo: () => void; }) { const network = useWardNetwork(); - const { units, bedReleases, movements, configuration, admissions } = useWardFlowSelect( + const { units, bedReleases, movements, configuration, admissions, referrals } = useWardFlowSelect( "units", "bedReleases", "movements", "configuration", "admissions", + "referrals", ); const now = useWardFlowClock(); const access = useEffectiveWardAccess(); @@ -54,6 +55,7 @@ export function NextPhoneMenuTop({ now, configuration.edAccessTargetMinutes, admissions, + referrals, ); const rows = deskCapacityRows(access, units, admissions ?? [], bedReleases, bedAlerts); const overLine = overLineLabel(rows); diff --git a/src/components/ward-management/shell-next/next-rail-body.tsx b/src/components/ward-management/shell-next/next-rail-body.tsx index 0ba40a44b4..01bc5e56e3 100644 --- a/src/components/ward-management/shell-next/next-rail-body.tsx +++ b/src/components/ward-management/shell-next/next-rail-body.tsx @@ -282,12 +282,13 @@ const NO_BED_ALERTS: ServiceBedAlertsSummary = { export function NextRailBody({ onOpenOperatorModal }: WardRailProps = {}) { const network = useWardNetwork(); const pathname = usePathname() ?? ""; - const { units, bedReleases, movements, configuration, admissions } = useWardFlowSelect( + const { units, bedReleases, movements, configuration, admissions, referrals } = useWardFlowSelect( "units", "bedReleases", "movements", "configuration", "admissions", + "referrals", ); const now = useWardFlowClock(); const accessTargetMinutes = configuration.edAccessTargetMinutes; @@ -295,7 +296,16 @@ export function NextRailBody({ onOpenOperatorModal }: WardRailProps = {}) { // A phone never draws the rail (below), so it skips the bed alert figures too. const bedAlerts = phone ? NO_BED_ALERTS - : deriveServiceBedAlerts(units, bedReleases, network.sites, movements, now, accessTargetMinutes, admissions); + : deriveServiceBedAlerts( + units, + bedReleases, + network.sites, + movements, + now, + accessTargetMinutes, + admissions, + referrals, + ); // Nav order follows the signed-in person, not the route (device audit item 6). // ROLE BRANCH (bed_manager): Capacity counts the desk's own site's ready beds (`preview-nav-counts.ts`). const { counts, shellRole: role } = useNextNavCounts(); diff --git a/src/components/ward-management/shell/ward-phone-menu-top.tsx b/src/components/ward-management/shell/ward-phone-menu-top.tsx index e46a923686..a293f7d8d0 100644 --- a/src/components/ward-management/shell/ward-phone-menu-top.tsx +++ b/src/components/ward-management/shell/ward-phone-menu-top.tsx @@ -23,12 +23,13 @@ export function PhoneMenuTop({ onGo: () => void; }) { const network = useWardNetwork(); - const { units, bedReleases, movements, configuration, admissions } = useWardFlowSelect( + const { units, bedReleases, movements, configuration, admissions, referrals } = useWardFlowSelect( "units", "bedReleases", "movements", "configuration", "admissions", + "referrals", ); const now = useWardFlowClock(); const access = useEffectiveWardAccess(); @@ -43,6 +44,7 @@ export function PhoneMenuTop({ now, configuration.edAccessTargetMinutes, admissions, + referrals, ); const rows = deskCapacityRows(access, units, admissions ?? [], bedReleases, bedAlerts); const overLine = overLineLabel(rows); diff --git a/src/components/ward-management/shell/ward-rail.tsx b/src/components/ward-management/shell/ward-rail.tsx index fc77833074..d1a3a3c4ca 100644 --- a/src/components/ward-management/shell/ward-rail.tsx +++ b/src/components/ward-management/shell/ward-rail.tsx @@ -252,12 +252,13 @@ const NO_BED_ALERTS: ServiceBedAlertsSummary = { export function WardRail({ onOpenOperatorModal }: WardRailProps = {}) { const network = useWardNetwork(); const pathname = usePathname() ?? ""; - const { units, bedReleases, movements, configuration, admissions } = useWardFlowSelect( + const { units, bedReleases, movements, configuration, admissions, referrals } = useWardFlowSelect( "units", "bedReleases", "movements", "configuration", "admissions", + "referrals", ); const now = useWardFlowClock(); const accessTargetMinutes = configuration.edAccessTargetMinutes; @@ -265,7 +266,16 @@ export function WardRail({ onOpenOperatorModal }: WardRailProps = {}) { // A phone never draws the rail (below), so it skips the bed alert figures too. const bedAlerts = phone ? NO_BED_ALERTS - : deriveServiceBedAlerts(units, bedReleases, network.sites, movements, now, accessTargetMinutes, admissions); + : deriveServiceBedAlerts( + units, + bedReleases, + network.sites, + movements, + now, + accessTargetMinutes, + admissions, + referrals, + ); // Nav order follows the signed-in person, not the route (device audit item 6). const { counts, shellRole: role } = useWardNavCounts(); const { open, appearance: storedAppearance, glare } = useWardDocumentPreferences(); diff --git a/src/components/ward-management/shell/ward-service-bed-alerts.ts b/src/components/ward-management/shell/ward-service-bed-alerts.ts index 6dda9f067f..1b12ab1c68 100644 --- a/src/components/ward-management/shell/ward-service-bed-alerts.ts +++ b/src/components/ward-management/shell/ward-service-bed-alerts.ts @@ -18,6 +18,7 @@ import type { Instant } from "@/components/ward-management/ward-clock"; import { CLINICAL_VALUES } from "@/components/ward-management/ward-clinical-values"; import { bedsPendingPreparation } from "@/components/ward-management/ward-bed-availability"; import type { Admission } from "@/components/ward-management/ward-admissions"; +import type { ReservationReferral } from "@/components/ward-management/ward-bed-states"; import { OCCUPANCY_ALERT_PERCENT, OCCUPANCY_CRITICAL_PERCENT, @@ -137,9 +138,20 @@ export function deriveServiceBedAlerts( accessTargetMinutes?: number, /** Admissions, so a live pull counts as a bed in use (`ward-occupancy.ts`). */ admissions: readonly Admission[] = [], + /** Accepted referrals; with `movements` they are the named holds Free leaves out (D-115 item 3). */ + referrals: readonly ReservationReferral[] = [], ): ServiceBedAlertsSummary { // Leverage the comprehensive backend tracker - const report = trackServiceBedCapacity(units, bedReleases, movements, now, sites, accessTargetMinutes, admissions); + const report = trackServiceBedCapacity( + units, + bedReleases, + movements, + now, + sites, + accessTargetMinutes, + admissions, + referrals, + ); const codePriority: Record = { black: 4, diff --git a/src/components/ward-management/statistics/statistics-capacity-chart.tsx b/src/components/ward-management/statistics/statistics-capacity-chart.tsx index b685103d6b..36d2b1adb3 100644 --- a/src/components/ward-management/statistics/statistics-capacity-chart.tsx +++ b/src/components/ward-management/statistics/statistics-capacity-chart.tsx @@ -19,7 +19,7 @@ import { } from "@/components/wf"; import { BED_ALERT_THRESHOLD_PERCENT } from "../shell/ward-service-bed-alerts"; import { bedsPendingPreparation } from "../ward-bed-availability"; -import { BED_STATE_LABELS, bedStates, readyBedsNow } from "../ward-bed-states"; +import { BED_STATE_LABELS, bedStates, readyBedsNow, type ReservationReferral } from "../ward-bed-states"; import type { Admission } from "../ward-admissions"; import { siteByCode } from "../ward-sites"; import type { BedRelease, HealthService, LeaveBed, Unit } from "../ward-model"; @@ -59,6 +59,7 @@ type CapacityInput = CapacityOptions & bedReleases?: never; admissions?: never; leaveBeds?: never; + holds?: never; } | { aggregate?: undefined; @@ -66,6 +67,8 @@ type CapacityInput = CapacityOptions & bedReleases: BedRelease[]; admissions?: readonly Admission[]; leaveBeds?: readonly LeaveBed[]; + /** D-115 item 3: open journeys and accepted referrals, so a named bed shows Pulled, not Ready. */ + holds?: readonly ReservationReferral[]; } ); @@ -143,9 +146,9 @@ export function StatisticsCapacityChart(props: CapacityInput) { hospital: siteByCode(unit.siteCode)?.name ?? unit.siteCode, service: siteByCode(unit.siteCode)?.service ?? "Service not recorded", beds: unit.beds, - states: bedStates(unit, props.admissions ?? [], props.bedReleases!, props.leaveBeds ?? []), + states: bedStates(unit, props.admissions ?? [], props.bedReleases!, props.leaveBeds ?? [], props.holds ?? []), pending: bedsPendingPreparation(unit.id, props.bedReleases!), - available: readyBedsNow(unit, props.admissions ?? [], props.bedReleases!), + available: readyBedsNow(unit, props.admissions ?? [], props.bedReleases!, props.holds ?? []), })); return ( ), occupancy: ( diff --git a/src/components/ward-management/statistics/statistics-service-screen.tsx b/src/components/ward-management/statistics/statistics-service-screen.tsx index f14b1761f6..b4255638bf 100644 --- a/src/components/ward-management/statistics/statistics-service-screen.tsx +++ b/src/components/ward-management/statistics/statistics-service-screen.tsx @@ -491,6 +491,7 @@ function OperationalServiceScreen({ serviceId }: { serviceId: string }) { bedReleases={bedReleases} admissions={admissions} leaveBeds={leaveBeds} + holds={namedHolds} initialGroup="ward" scopeLabel={`in ${service}`} title="Ward capacity" diff --git a/src/components/ward-management/tools/ward-bed-finder.tsx b/src/components/ward-management/tools/ward-bed-finder.tsx index 05a61f98cd..273fb36495 100644 --- a/src/components/ward-management/tools/ward-bed-finder.tsx +++ b/src/components/ward-management/tools/ward-bed-finder.tsx @@ -6,7 +6,7 @@ import { ArrowUpRight } from "lucide-react"; import { useWardFlowSelect } from "../ward-flow-provider"; import { bedsPendingPreparation } from "../ward-bed-availability"; -import { readyBedsNow } from "../ward-bed-states"; +import { readyBedsNow, type ReservationReferral } from "../ward-bed-states"; import type { Admission } from "../ward-admissions"; import { designationSummary, lockedBedsFree, openBedsFree } from "../ward-bed-designation"; import { formatInstantWithDay, type Instant } from "../ward-clock"; @@ -67,6 +67,8 @@ export function findBeds( admissions: readonly Admission[], releases: BedRelease[], filters: BedFinderFilters, + /** D-115 item 3: the named holds (open journeys and accepted referrals), so a named bed is not offered. */ + holds: readonly ReservationReferral[], ): BedFinderRow[] { if (!filters.legalStatus) return []; const involuntary = filters.legalStatus === "involuntary"; @@ -84,7 +86,7 @@ export function findBeds( // pulled for an incoming patient; the older min(allocatable, empty) still offered it. The // data does not say which beds are empty, so each kind is capped on its own rather than // giving the cap to locked beds first. - const ready = readyBedsNow(unit, admissions, releases); + const ready = readyBedsNow(unit, admissions, releases, holds); const free = filters.bed === "locked" ? Math.min(lockedBedsFree(unit), ready) @@ -283,12 +285,19 @@ function BedFinderRowText({ row, now }: { row: BedFinderRow; now: Instant }) { ); } function OperationalBedFinder({ onNavigate }: { onNavigate: () => void }) { - const { units, admissions, bedReleases, now } = useWardFlowSelect("units", "admissions", "bedReleases", "now"); + const { units, admissions, bedReleases, movements, referrals, now } = useWardFlowSelect( + "units", + "admissions", + "bedReleases", + "movements", + "referrals", + "now", + ); // Access audit (31 Oct 2026): a ward links to its page only for a desk that may open that page, // by the same role table the menu reads. Another desk still reads the row, as plain text. const canOpen = useCanOpenHref(); const [filters, setFilters] = useState(DEFAULT_BED_FINDER_FILTERS); - const rows = findBeds(units, admissions, bedReleases, filters); + const rows = findBeds(units, admissions, bedReleases, filters, [...movements, ...referrals]); const set = (key: K) => (value: BedFinderFilters[K]) => diff --git a/src/components/ward-management/tools/ward-bed-summary.ts b/src/components/ward-management/tools/ward-bed-summary.ts index ac6f3cdee3..f4b99d7f84 100644 --- a/src/components/ward-management/tools/ward-bed-summary.ts +++ b/src/components/ward-management/tools/ward-bed-summary.ts @@ -10,7 +10,10 @@ import type { useWardFlow } from "../ward-flow-provider"; type Flow = ReturnType; -export type BedSummaryInput = Pick & { +export type BedSummaryInput = Pick< + Flow, + "units" | "admissions" | "movements" | "referrals" | "bedReleases" | "leaveBeds" +> & { now: number; thresholds: DueSoonThresholds; network: WardNetwork; @@ -29,7 +32,9 @@ export type BedSummaryInput = Pick(HEALTH_SERVICES.map((service) => [service, 0])); let readyTotal = 0; let pendingTotal = 0; @@ -39,7 +44,7 @@ export function bedSummaryText(input: BedSummaryInput): string { let heldUp = 0; for (const unit of units) { const capacity = capacityBreakdown(unit, bedReleases, leaveBeds, now); - const readyHere = readyBedsNow(unit, admissions, bedReleases); + const readyHere = readyBedsNow(unit, admissions, bedReleases, holds); const service = unitHealthService(unit); readyTotal += readyHere; if (service) ready.set(service, (ready.get(service) ?? 0) + readyHere); diff --git a/src/components/ward-management/tools/ward-tool-call-log.tsx b/src/components/ward-management/tools/ward-tool-call-log.tsx index 6e8208592c..7b819a4439 100644 --- a/src/components/ward-management/tools/ward-tool-call-log.tsx +++ b/src/components/ward-management/tools/ward-tool-call-log.tsx @@ -47,7 +47,13 @@ function laterAt(step: LaterStep, now: number): number { * browser only; it never changes a ward's figures, which only the ward confirms. */ export function ToolCallLog() { - const { units, admissions, bedReleases } = useWardFlowSelect("units", "admissions", "bedReleases"); + const { units, admissions, bedReleases, movements, referrals } = useWardFlowSelect( + "units", + "admissions", + "bedReleases", + "movements", + "referrals", + ); const now = useWardFlowClock(); const log = useCallLog(); @@ -101,7 +107,7 @@ export function ToolCallLog() { } // The same Ready count as the Capacity page, never the raw ward observation (review, 8 Nov 2026). - const free = ward ? readyBedsNow(ward, admissions, bedReleases) : 0; + const free = ward ? readyBedsNow(ward, admissions, bedReleases, [...movements, ...referrals]) : 0; const detail: Record = { "as-shown": ward ? `The board shows ${free} free` : "The board is right", later: "One should free up today", diff --git a/src/components/ward-management/tools/ward-tool-figures.tsx b/src/components/ward-management/tools/ward-tool-figures.tsx index aaa4276b9d..3142f4dc9d 100644 --- a/src/components/ward-management/tools/ward-tool-figures.tsx +++ b/src/components/ward-management/tools/ward-tool-figures.tsx @@ -35,10 +35,11 @@ export function ToolFigures({ onNavigate }: { onNavigate: () => void }) { } function OperationalFigures({ onNavigate }: { onNavigate: () => void }) { - const { units, admissions, movements, bedReleases, leaveBeds } = useWardFlowSelect( + const { units, admissions, movements, referrals, bedReleases, leaveBeds } = useWardFlowSelect( "units", "admissions", "movements", + "referrals", "bedReleases", "leaveBeds", ); @@ -46,7 +47,10 @@ function OperationalFigures({ onNavigate }: { onNavigate: () => void }) { const occupied = occupiedBeds(units, admissions, bedReleases, leaveBeds); const ed = openEdJourneys(movements, now); const figures: Figure[] = [ - { label: "Beds ready", value: String(totalReadyBedsNow(units, admissions, bedReleases)) }, + { + label: "Beds ready", + value: String(totalReadyBedsNow(units, admissions, bedReleases, [...movements, ...referrals])), + }, { label: "Occupancy", value: occupied.beds ? `${occupancyPercent(occupied)}%` : "—" }, { label: "Open ED movements", value: String(ed.total) }, { label: "Waiting in ED", value: String(ed.stillInEd) }, diff --git a/src/components/ward-management/tools/ward-tool-find-bed.tsx b/src/components/ward-management/tools/ward-tool-find-bed.tsx index 0ef6e795f3..37a54474ee 100644 --- a/src/components/ward-management/tools/ward-tool-find-bed.tsx +++ b/src/components/ward-management/tools/ward-tool-find-bed.tsx @@ -199,8 +199,17 @@ function FilterRow({ label, children }: { label: string; children: ReactNode }) } function OperationalResults({ filters, onNavigate }: { filters: Filters; onNavigate: () => void }) { - const { units, admissions, bedReleases } = useWardFlowSelect("units", "admissions", "bedReleases"); - const rows: FoundRow[] = findBeds(units, admissions, bedReleases, { ...filters, home: "any" }) + const { units, admissions, bedReleases, movements, referrals } = useWardFlowSelect( + "units", + "admissions", + "bedReleases", + "movements", + "referrals", + ); + const rows: FoundRow[] = findBeds(units, admissions, bedReleases, { ...filters, home: "any" }, [ + ...movements, + ...referrals, + ]) .filter((row) => row.free > 0) .map((row) => ({ id: row.unit.id, diff --git a/src/components/ward-management/tools/ward-tool-messages.tsx b/src/components/ward-management/tools/ward-tool-messages.tsx index aa6d386d20..8e1dafaa1b 100644 --- a/src/components/ward-management/tools/ward-tool-messages.tsx +++ b/src/components/ward-management/tools/ward-tool-messages.tsx @@ -99,7 +99,9 @@ const listByService = (counts: ReadonlyMap) => /** The figures `bedSummaryText` counts, from the same derivations, plus occupancy and the longest wait. */ function operationalFigures(input: BedSummaryInput) { - const { units, admissions, movements, bedReleases, leaveBeds, now, thresholds, network } = input; + const { units, admissions, movements, referrals, bedReleases, leaveBeds, now, thresholds, network } = input; + // D-115 item 3: a bed named for an open journey or accepted referral is not ready. + const holds = [...movements, ...referrals]; const ready = new Map(HEALTH_SERVICES.map((service) => [service, 0])); let readyTotal = 0; let pending = 0; @@ -108,7 +110,7 @@ function operationalFigures(input: BedSummaryInput) { let heldUp = 0; for (const unit of units) { const capacity = capacityBreakdown(unit, bedReleases, leaveBeds, now); - const readyHere = readyBedsNow(unit, admissions, bedReleases); + const readyHere = readyBedsNow(unit, admissions, bedReleases, holds); const service = unitHealthService(unit); readyTotal += readyHere; if (service) ready.set(service, (ready.get(service) ?? 0) + readyHere); @@ -380,10 +382,11 @@ function MessagesView({ } function OperationalMessages() { - const { units, admissions, movements, bedReleases, leaveBeds } = useWardFlowSelect( + const { units, admissions, movements, referrals, bedReleases, leaveBeds } = useWardFlowSelect( "units", "admissions", "movements", + "referrals", "bedReleases", "leaveBeds", ); @@ -391,7 +394,17 @@ function OperationalMessages() { const thresholds = useDueSoonThresholds(); const network = useWardNetwork(); const [choice, setChoice] = useState("summary"); - const input: BedSummaryInput = { units, admissions, movements, bedReleases, leaveBeds, now, thresholds, network }; + const input: BedSummaryInput = { + units, + admissions, + movements, + referrals, + bedReleases, + leaveBeds, + now, + thresholds, + network, + }; const message = choice === "summary" ? summaryMessage(bedSummaryText(input)) : operationalMessage(choice, input); return ; } diff --git a/src/components/ward-management/ward-admission-checklist.ts b/src/components/ward-management/ward-admission-checklist.ts index 469fc49da0..bfbb533e23 100644 --- a/src/components/ward-management/ward-admission-checklist.ts +++ b/src/components/ward-management/ward-admission-checklist.ts @@ -123,8 +123,8 @@ function advocacyLine( record === undefined ? "Not recorded" : record.outcome === "told" - ? `Told ${record.who ?? ""}${when}` - : `Not applicable · ${record.reason ?? ""}`, + ? `${["Told", record.who?.trim()].filter(Boolean).join(" ")}${when}` + : ["Not applicable", record.reason?.trim()].filter(Boolean).join(" · "), ...(subject ? { supportNotification: { occasion: subject.occasion, subjectId: subject.subjectId } } : {}), }, ]; diff --git a/src/components/ward-management/ward-bed-states.ts b/src/components/ward-management/ward-bed-states.ts index 5160c78851..bd9a8aa957 100644 --- a/src/components/ward-management/ward-bed-states.ts +++ b/src/components/ward-management/ward-bed-states.ts @@ -93,12 +93,16 @@ export function bedStates( * bed board, Handover and Statistics read this, and the bed flow, Capacity and the TV read the same * box through `bedStates`. New pulls use the same reservation partition through * `pulledBedAvailability`; physical arrivals keep their separate recording rules. + * + * `referrals` is required (D-115 item 3): the named holds, open journeys and accepted referrals + * (`[...movements, ...referrals]`), so a bed named for an incoming patient is off Ready on every + * screen. Pass `[]` only where the caller truly holds no named reservation. */ export function readyBedsNow( unit: Unit, admissions: readonly Admission[], bedReleases: readonly BedRelease[], - referrals: readonly ReservationReferral[] = [], + referrals: readonly ReservationReferral[], ): number { return bedStates(unit, admissions, [...bedReleases], [], referrals).ready; } @@ -111,7 +115,7 @@ export function readyBedsByWard( units: readonly Unit[], admissions: readonly Admission[], bedReleases: readonly BedRelease[], - referrals: readonly ReservationReferral[] = [], + referrals: readonly ReservationReferral[], ): { unit: Unit; ready: number }[] { return units .map((unit) => ({ unit, ready: readyBedsNow(unit, admissions, bedReleases, referrals) })) @@ -123,7 +127,7 @@ export function totalReadyBedsNow( units: readonly Unit[], admissions: readonly Admission[], bedReleases: readonly BedRelease[], - referrals: readonly ReservationReferral[] = [], + referrals: readonly ReservationReferral[], ): number { return readyBedsByWard(units, admissions, bedReleases, referrals).reduce((sum, ward) => sum + ward.ready, 0); } diff --git a/src/components/ward-management/ward-global-search.module.css b/src/components/ward-management/ward-global-search.module.css index 473a78141a..0231957e4a 100644 --- a/src/components/ward-management/ward-global-search.module.css +++ b/src/components/ward-management/ward-global-search.module.css @@ -1512,7 +1512,8 @@ a.askChip { outline-offset: 2px; } - /* Go to always fits the width: four equal columns, so it never scrolls sideways. */ + /* Go to always fits the width, so it never scrolls sideways. The five 54px columns are set in + the "Glass rows" block below; this is the fallback grid and the shared reset. */ .popup[data-start] .groupItem[data-group="goto"] .groupList { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); @@ -1616,17 +1617,18 @@ a.askChip { inset: -20px -4px -4px; /* 48px tall; grows into the gap above, never over the first alert */ } + /* The kit's phone pill size (40px face; HOW-WE-WORK §6). */ .popup[data-start] .askChip, .popup[data-start] .commandBtn { - height: 32px; - gap: 7px; - padding: 0 12px; + height: var(--wf-phone-pill-h); + gap: var(--wf-phone-pill-gap); + padding: 0 var(--wf-phone-pill-px); border: 0; - border-radius: 999px; + border-radius: var(--wf-r-pill); background: color-mix(in srgb, var(--wf-surface) 55%, transparent); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--wf-ink-1) 8%, transparent); color: var(--wf-ink-1); - font: 500 var(--wf-fs-13) / 1 var(--wf-font); + font: 500 var(--wf-phone-pill-fs) / 1 var(--wf-font); } .popup[data-start] .commandBtn { @@ -1635,7 +1637,7 @@ a.askChip { .popup[data-start] .askChip .kindCount { color: var(--wf-ink-3); - font: 600 var(--wf-fs-12) / 1 var(--wf-mono); + font: 600 var(--wf-phone-pill-count-fs) / 1 var(--wf-mono); opacity: 1; } @@ -1648,7 +1650,7 @@ a.askChip { } .popup[data-start] .groupItem[data-group="goto"] .option { - gap: 8px; + gap: var(--wf-s-2); padding: 2px 0; } @@ -1657,7 +1659,7 @@ a.askChip { align-self: center; width: 54px; height: 54px; - border-radius: 16px; + border-radius: var(--wf-r-lg); background: color-mix(in srgb, var(--wf-accent-tint) 78%, transparent); box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--wf-accent) 9%, transparent); color: var(--wf-accent); @@ -1750,9 +1752,9 @@ a.askChip { @media (max-width: 22.4375rem) { .popup[data-start] .groupItem[data-group="goto"] .groupList { - grid-template-columns: repeat(5, auto); - justify-content: space-between; - gap: 0 8px; + grid-template-columns: repeat(5, minmax(0, 1fr)); + justify-content: stretch; + gap: 0 var(--wf-s-1); } .popup[data-start] .groupItem[data-group="goto"] .itemIconBox { @@ -1760,12 +1762,20 @@ a.askChip { height: 48px; } - /* 12 px is the text floor (D-3). Each column is as wide as its tile or label, whichever is - wider, and the spare room goes between them, so "Movements" fits inside the 14 px edge. */ + /* 12 px is the text floor (D-3). Five equal columns share the width, and a label longer than + its column is cut with an ellipsis (the full name stays the option's accessible name), so + neighbouring labels never collide at 320px. */ + .popup[data-start] .groupItem[data-group="goto"] .resultContent { + overflow: hidden; + } + .popup[data-start] .groupItem[data-group="goto"] .resultTitle { - max-width: none; - font-size: 12px; + max-width: 100%; + overflow: hidden; + font-size: var(--wf-fs-12); letter-spacing: -0.02em; + text-overflow: ellipsis; + white-space: nowrap; } } diff --git a/src/components/ward-management/ward-list-export-button.tsx b/src/components/ward-management/ward-list-export-button.tsx index d7efe687a0..93ab078be3 100644 --- a/src/components/ward-management/ward-list-export-button.tsx +++ b/src/components/ward-management/ward-list-export-button.tsx @@ -14,12 +14,15 @@ import { DownloadRefusedError } from "./ward-download"; export function ListExportButton({ load, variant = "sec", + iconOnly = false, className, disabled, testId, }: { load: () => Promise; variant?: "sec" | "ghost" | "onHero"; + /** The download icon alone, keeping its spoken name: the one phone rule beside an icon-only Print. */ + iconOnly?: boolean; className?: string; disabled?: boolean; testId?: string; @@ -30,8 +33,8 @@ export function ListExportButton({ <> {/* Always present, so a screen reader announces the note when it arrives. Visually hidden while empty, so it takes no gap in a flex row and the button sits flush with the row's right edge. */} diff --git a/src/components/ward-management/ward-list-print-button.tsx b/src/components/ward-management/ward-list-print-button.tsx index 4b1d8d63f6..450512e895 100644 --- a/src/components/ward-management/ward-list-print-button.tsx +++ b/src/components/ward-management/ward-list-print-button.tsx @@ -6,7 +6,8 @@ import { Button } from "@/components/wf"; /** * A list's Print button: the browser's own print of the page as it shows, as Discharges, the call * sheet and the chronology already do. The page's print styles drop the controls; names print in - * full, as on screen (D-102 keeps names out of downloaded files only). + * full, as on screen (D-102 keeps names out of downloaded files only). The kit's default size (36px), + * as the Export beside it. */ export function ListPrintButton({ variant = "sec", @@ -15,14 +16,13 @@ export function ListPrintButton({ testId, }: { variant?: "sec" | "ghost"; - /** The printer alone, named "Print this list", where a narrow card head has no room for the word. */ + /** The printer alone, named "Print this list": the one phone rule on Movements, Leave and Legal forms. */ iconOnly?: boolean; disabled?: boolean; testId?: string; }) { const shared = { variant, - size: "sm", icon: Printer, disabled, "aria-label": "Print this list", diff --git a/src/components/ward-management/ward/ward-intake-window-dialog.tsx b/src/components/ward-management/ward/ward-intake-window-dialog.tsx index f4d5e9d654..229b1e0074 100644 --- a/src/components/ward-management/ward/ward-intake-window-dialog.tsx +++ b/src/components/ward-management/ward/ward-intake-window-dialog.tsx @@ -2,7 +2,7 @@ import { useId, useRef, useState } from "react"; -import { Dialog, Field, StateLine, TextInput } from "@/components/wf"; +import { Dialog, Field, StateLine, TIME_FIELD_ERROR, TimeField } from "@/components/wf"; import { intakeWindowFromClockText } from "../ward-intake-window"; import { formatInstant, type Instant } from "../ward-clock"; @@ -28,8 +28,15 @@ export function WardIntakeWindowDialog({ role: Extract; onClose: () => void; }) { + // Each time is the kit TimeField's "HH:MM" (or "" while unfinished), plus the text as typed, so + // an empty box can be told from an unfinished or wrong one. const [fromText, setFromText] = useState(""); const [toText, setToText] = useState(""); + const [fromTyped, setFromTyped] = useState(""); + const [toTyped, setToTyped] = useState(""); + // A box's error shows once the person has left it, not while they are still typing. + const [fromLeft, setFromLeft] = useState(false); + const [toLeft, setToLeft] = useState(false); // A refused or unconfirmed save keeps the dialog and both typed times, and says why here. const [refused, setRefused] = useState(null); // Counts edits and saves, so an earlier save that lands late cannot close over newer typing. @@ -41,12 +48,16 @@ export function WardIntakeWindowDialog({ const { run } = useUndoableStep({ dispatch, rejections, now: at, routeRole: role }); const intake = intakeWindowFromClockText(fromText, toText, at); + const fromWrong = fromTyped.trim() !== "" && fromText === ""; + const toWrong = toTyped.trim() !== "" && toText === ""; const problem = - fromText.trim() === "" || toText.trim() === "" - ? "Type both times." - : intake === undefined - ? "Type each time as 21:00 or 2100, and make them different." - : undefined; + fromWrong || toWrong + ? TIME_FIELD_ERROR + : fromText === "" || toText === "" + ? "Type both times." + : intake === undefined + ? "Make the two times different." + : undefined; const send = (next: { from: Instant; to: Instant } | null) => { setRefused(null); @@ -95,29 +106,37 @@ export function WardIntakeWindowDialog({ data-testid="ward-intake-window-dialog" >
          - - + { + onValueChange={(time) => { draft.current += 1; - setFromText(event.target.value); + setFromText(time); }} + onTextChange={setFromTyped} + onBlur={() => setFromLeft(true)} data-testid="ward-intake-window-from" /> - - + { + onValueChange={(time) => { draft.current += 1; - setToText(event.target.value); + setToText(time); }} + onTextChange={setToTyped} + onBlur={() => setToLeft(true)} data-testid="ward-intake-window-to" /> diff --git a/src/components/ward-management/ward/ward-outage-catch-up-card.tsx b/src/components/ward-management/ward/ward-outage-catch-up-card.tsx index 45f08ff2aa..f2b841cc74 100644 --- a/src/components/ward-management/ward/ward-outage-catch-up-card.tsx +++ b/src/components/ward-management/ward/ward-outage-catch-up-card.tsx @@ -8,6 +8,7 @@ import type { Unit } from "@/components/ward-management/ward-model"; import { useRouteRole } from "@/components/ward-management/ward-role-gate"; import { savedAtToast } from "@/components/ward-management/ward-saved-at-toast"; import { useWardOutageCatchUp } from "@/components/ward-management/use-outage-catch-up"; +import { bedsNotConfirmedSinceOutage } from "@/components/ward-management/ward-outage-catch-up"; import { useWardCommandCompletion, wardCommandFailure } from "./use-ward-command-completion"; import styles from "./ward-outage-catch-up-card.module.css"; @@ -47,28 +48,30 @@ export function WardOutageCatchUpCard({ const toast = useOptionalToast(); if (catchUp === null || catchUp.caughtUp || routeRole === "bed_manager") return null; const { outage } = catchUp; + // The beds the record says are still unconfirmed, not simply every bed on the ward. + const unconfirmed = bedsNotConfirmedSinceOutage(unit, outage).length; const by = WARD_FLOW_ROLE_LABELS[outage.by]?.toLowerCase() ?? "the coordinator"; const rows = [ { id: "admissions", title: "Admissions", sub: "People admitted on paper", - action: "Add admission", + action: "Open arrivals", run: actions.onAdmissions, }, { id: "discharges", title: "Discharges", sub: "People who left on paper", - action: "Record discharge", + action: "Open discharges", run: actions.onDischarges, }, - { id: "moves", title: "Moves", sub: "Bed or ward moves on paper", action: "Record move", run: actions.onMoves }, + { id: "moves", title: "Moves", sub: "Bed or ward moves on paper", action: "Open bed board", run: actions.onMoves }, { id: "beds", title: "Bed changes", sub: "Beds closed, opened or cleaned", - action: "Change a bed", + action: "Open bed list", run: actions.onBedChanges, }, ]; @@ -123,7 +126,9 @@ export function WardOutageCatchUpCard({
          - {unit.beds} {unit.beds === 1 ? "bed" : "beds"} not confirmed since the outage + {unconfirmed > 0 + ? `${unconfirmed} ${unconfirmed === 1 ? "bed" : "beds"} not confirmed since the outage` + : "Confirm the beds since the outage"} ) : null} - {message ? ( -

          - {message} -

          - ) : null} + {/* Always mounted, so screen readers hear the message when it arrives. */} +

          + {message ?? ""} +

          ); } diff --git a/src/components/ward-redesign/pop-search/search-preview.tsx b/src/components/ward-redesign/pop-search/search-preview.tsx index 19a0f9f1a1..644ed213a3 100644 --- a/src/components/ward-redesign/pop-search/search-preview.tsx +++ b/src/components/ward-redesign/pop-search/search-preview.tsx @@ -478,7 +478,10 @@ function WardPreview({ href: string; onOpen: PreviewProps["onOpen"]; }) { - const ready = unitReadyBedCount(unit); + // The ready count Search lists, with the same pulls and named holds (D-115 item 3). Outside the + // provider nothing is held, as before. + const flow = useOptionalWardFlow(); + const ready = unitReadyBedCount(unit, flow?.admissions ?? [], flow ? [...flow.movements, ...flow.referrals] : []); const incoming = movements.filter((movement) => isOpen(movement) && movement.acceptedUnitId === unit.id); const referred = movements.filter( (movement) => isOpen(movement) && !movement.acceptedUnitId && movement.referredUnitIds.includes(unit.id), diff --git a/src/components/ward-redesign/shell/preview-rail-body.tsx b/src/components/ward-redesign/shell/preview-rail-body.tsx index 5589619af3..c2427d8dd5 100644 --- a/src/components/ward-redesign/shell/preview-rail-body.tsx +++ b/src/components/ward-redesign/shell/preview-rail-body.tsx @@ -262,12 +262,13 @@ const NO_BED_ALERTS: ServiceBedAlertsSummary = { export function PreviewRailBody({ onOpenOperatorModal }: WardRailProps = {}) { const network = useWardNetwork(); const pathname = usePathname() ?? ""; - const { units, bedReleases, movements, configuration, admissions } = useWardFlowSelect( + const { units, bedReleases, movements, configuration, admissions, referrals } = useWardFlowSelect( "units", "bedReleases", "movements", "configuration", "admissions", + "referrals", ); const now = useWardFlowClock(); const accessTargetMinutes = configuration.edAccessTargetMinutes; @@ -275,7 +276,16 @@ export function PreviewRailBody({ onOpenOperatorModal }: WardRailProps = {}) { // A phone never draws the rail (below), so it skips the bed alert figures too. const bedAlerts = phone ? NO_BED_ALERTS - : deriveServiceBedAlerts(units, bedReleases, network.sites, movements, now, accessTargetMinutes, admissions); + : deriveServiceBedAlerts( + units, + bedReleases, + network.sites, + movements, + now, + accessTargetMinutes, + admissions, + referrals, + ); // Nav order follows the signed-in person, not the route (device audit item 6). // ROLE BRANCH (bed_manager): Capacity counts the desk's own site's ready beds (`preview-nav-counts.ts`). const { counts, shellRole: role } = usePreviewNavCounts(); diff --git a/src/components/wf/hero-top.module.css b/src/components/wf/hero-top.module.css index c2d945b85f..ea85f595b4 100644 --- a/src/components/wf/hero-top.module.css +++ b/src/components/wf/hero-top.module.css @@ -553,9 +553,9 @@ a.cellBody { padding: 4px 24px 4px 16px; overflow-x: auto; overflow-y: hidden; - /* D-144: on a phone this row only ever moves sideways. A finger on it pans the row, never the - row or page up and down (iPhone still slid the row vertically after every layout fix). */ - touch-action: pan-x; + /* D-144 and D-159: the row itself moves only sideways (overflow-y hidden), but a finger + that starts on it can still scroll the page up and down. */ + touch-action: pan-x pan-y; overscroll-behavior-x: contain; scroll-padding-left: 16px; /* A chip focused from the keyboard scrolls clear of the right-hand fade. */ diff --git a/src/components/wf/phone-rows.module.css b/src/components/wf/phone-rows.module.css index 878a8c0353..61a8bf35c7 100644 --- a/src/components/wf/phone-rows.module.css +++ b/src/components/wf/phone-rows.module.css @@ -24,9 +24,9 @@ margin-block: -8px; overflow-x: auto; overflow-y: hidden; - /* D-144: on a phone this row only ever moves sideways. A finger on it pans the row, never the - row or page up and down (iPhone still slid the row vertically after every layout fix). */ - touch-action: pan-x; + /* D-144 and D-159: the row itself moves only sideways (overflow-y hidden), but a finger + that starts on it can still scroll the page up and down. */ + touch-action: pan-x pan-y; overscroll-behavior-x: contain; scrollbar-width: none; -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 28px), transparent); diff --git a/src/lib/demo-code-tries.ts b/src/lib/demo-code-tries.ts index 7f162ab581..fcef1e3bb0 100644 --- a/src/lib/demo-code-tries.ts +++ b/src/lib/demo-code-tries.ts @@ -5,6 +5,8 @@ import { isIP } from "node:net"; +import { logger } from "@/lib/logger"; + /** * A few wrong tries per address, then a pause, so a demo code cannot be guessed by brute force. The * address comes from a trusted hop only (A4-02), the same rule as the backend's `clientAddress`. A @@ -20,16 +22,35 @@ const ADDRESS_TABLE_LIMIT = 5_000; const wrongTries = new Map(); let allWrongTries = { count: 0, since: 0 }; +let warnedIgnoredProxyList = false; + /** * Proxies whose forwarding headers may be trusted, from `WARD_TRUSTED_PROXY_ADDRESSES`. * Each entry must be a literal IP address or `*`; anything else is ignored, as on the backend. * `*` requires verified proxy-only ingress that overwrites those headers and prevents direct access. + * + * On the Next service only `*` means anything (#1615, 12 November 2026): the session route has no + * socket address to match a listed IP against, so a list without `*` would key every caller as + * `"unknown"` and ten wrong tries would refuse every presenter. Such a list is ignored, with one + * warning per process, and the D-147 no-proxy keying applies. The backend still honours IP lists. */ -export function trustedProxyAddressesFromEnv(env: NodeJS.Dict = process.env): string[] { - return (env.WARD_TRUSTED_PROXY_ADDRESSES ?? "") +export function trustedProxyAddressesFromEnv( + env: NodeJS.Dict = process.env, + warn: (message: string, context: Record) => void = logger.warn, +): string[] { + const listed = (env.WARD_TRUSTED_PROXY_ADDRESSES ?? "") .split(",") .map((entry) => entry.trim()) .filter((address) => address === "*" || isIP(address) !== 0); + if (listed.includes("*")) return ["*"]; + if (listed.length > 0 && !warnedIgnoredProxyList) { + warnedIgnoredProxyList = true; + warn("ward_front_door_config_warning", { + setting: "WARD_TRUSTED_PROXY_ADDRESSES", + detail: "The Next service has no socket address, so only * applies here; the IP list is ignored.", + }); + } + return []; } /** @@ -41,7 +62,8 @@ export function trustedProxyAddressesFromEnv(env: NodeJS.Dict { }); // D-144 (Josh, 11 Nov 2026): on a phone the quick-link chip rows move only sideways under a finger. + // D-159 (Josh, 12 Nov 2026): the row stays sideways-only, but a finger that starts on it can still + // scroll the page, so the touch action allows both pans and the row's own overflow-y stays hidden. it.each([ "wf/hero-top.module.css", "ward-management/quick-bar/ward-quick-bar.module.css", "wf/phone-rows.module.css", "ward-management/coordinator/home-hero.module.css", - ])("%s locks its phone chip row to sideways touch", (file) => { + ])("%s keeps its phone chip row sideways and lets the page scroll from it", (file) => { const css = readFileSync(join(ROOT, "components", file), "utf8").replace(/\/\*[\s\S]*?\*\//g, ""); - expect(css).toMatch(/touch-action\s*:\s*pan-x/); + expect(css).toMatch(/touch-action\s*:\s*pan-x pan-y\s*;/); + expect(css).not.toMatch(/touch-action\s*:\s*pan-x\s*;/); + expect(css).toMatch(/overscroll-behavior-x\s*:\s*contain/); }); }); diff --git a/tests/front-door-pass.test.ts b/tests/front-door-pass.test.ts index 0751364173..030938923a 100644 --- a/tests/front-door-pass.test.ts +++ b/tests/front-door-pass.test.ts @@ -491,11 +491,17 @@ describe("POST /api/session", () => { expect(edgeAddress(dual, [])).toBe("192.0.2.40"); expect(edgeAddress(forwardedOnly, [])).toBe("192.0.2.41"); expect(edgeAddress(new Headers(), [])).toBe("unknown"); - expect(trustedProxyAddressesFromEnv({ WARD_TRUSTED_PROXY_ADDRESSES: "10.0.0.2, *, not-an-ip" })).toEqual([ - "10.0.0.2", + // #1615: on the Next service only `*` applies; an IP-only list is dropped with one warning. + const warn = vi.fn(); + expect(trustedProxyAddressesFromEnv({ WARD_TRUSTED_PROXY_ADDRESSES: "10.0.0.2, *, not-an-ip" }, warn)).toEqual([ "*", ]); - expect(trustedProxyAddressesFromEnv({})).toEqual([]); + expect(trustedProxyAddressesFromEnv({}, warn)).toEqual([]); + expect(warn).not.toHaveBeenCalled(); + expect(trustedProxyAddressesFromEnv({ WARD_TRUSTED_PROXY_ADDRESSES: "10.0.0.2, not-an-ip" }, warn)).toEqual([]); + expect(trustedProxyAddressesFromEnv({ WARD_TRUSTED_PROXY_ADDRESSES: "10.0.0.3" }, warn)).toEqual([]); + expect(warn).toHaveBeenCalledTimes(1); + expect(warn.mock.calls[0]?.[0]).toBe("ward_front_door_config_warning"); vi.stubEnv("WARD_FRONT_DOOR", "required"); vi.stubEnv("WARD_PASS_SECRET", SECRET); vi.stubEnv("WARD_DEMO_CODE", "WARD-7Q4K-SYNTH"); @@ -520,17 +526,18 @@ describe("POST /api/session", () => { expect((await POST(post({ demoCode: "ward 7q4k synth" }, "192.0.2.72"))).status).toBe(204); }); - it("ignores a caller-sent address once a trusted proxy is configured but the socket is not it (A4-02)", async () => { + it("ignores an IP-only trusted proxy list on the Next route, so one guesser cannot lock everyone out (#1615)", async () => { vi.stubEnv("WARD_FRONT_DOOR", "required"); vi.stubEnv("WARD_PASS_SECRET", SECRET); vi.stubEnv("WARD_DEMO_CODE", "WARD-7Q4K-SYNTH"); vi.stubEnv("NEXT_PUBLIC_WARD_SHARED_ENABLED", "false"); vi.stubEnv("WARD_TRUSTED_PROXY_ADDRESSES", "10.0.0.2"); for (let attempt = 0; attempt < 10; attempt += 1) - expect((await POST(post({ demoCode: "WRONG" }, `198.51.100.${attempt}`))).status).toBe(401); - // Rotating X-Real-IP does not open a new bucket: every request shares "unknown". - expect((await POST(post({ demoCode: "WRONG" }, "192.0.2.99"))).status).toBe(429); - expect((await POST(post({ demoCode: "ward 7q4k synth" }, "192.0.2.98"))).status).toBe(429); + expect((await POST(post({ demoCode: "WRONG" }, "192.0.2.80"))).status).toBe(401); + expect((await POST(post({ demoCode: "WRONG" }, "192.0.2.80"))).status).toBe(429); + // The route has no socket, so the list falls back to D-147 keying: other callers keep their budget. + expect((await POST(post({ demoCode: "WRONG" }, "192.0.2.81"))).status).toBe(401); + expect((await POST(post({ demoCode: "ward 7q4k synth" }, "192.0.2.82"))).status).toBe(204); }); it("refuses wrong codes once tries across all addresses reach the cap, but still opens with the right code", async () => { diff --git a/tests/ward-audit-24-4-counts.dom.test.tsx b/tests/ward-audit-24-4-counts.dom.test.tsx index 1550f029ce..0c453a2705 100644 --- a/tests/ward-audit-24-4-counts.dom.test.tsx +++ b/tests/ward-audit-24-4-counts.dom.test.tsx @@ -49,14 +49,16 @@ function eventsToSplit(): WardFlowEvent[] { state = next; events.push(event); const now = nowFor(state, step); - if (oldReady(state, now) !== totalReadyBedsNow(state.units, state.admissions, state.bedReleases)) return events; + const holds = [...state.movements, ...state.referrals]; + if (oldReady(state, now) !== totalReadyBedsNow(state.units, state.admissions, state.bedReleases, holds)) + return events; } } throw new Error("no fuzz run reached a split between the old and shared ready formulas"); } function ReadyProbe({ events }: { events: WardFlowEvent[] }) { - const { units, admissions, bedReleases, leaveBeds, dispatch } = useWardFlow(); + const { units, admissions, bedReleases, leaveBeds, movements, referrals, dispatch } = useWardFlow(); const now = useWardFlowClock(); const old = units.reduce( (sum, unit) => sum + capacityBreakdown(unit, [...bedReleases], leaveBeds, now).availableNow, @@ -64,7 +66,9 @@ function ReadyProbe({ events }: { events: WardFlowEvent[] }) { ); return ( <> - {totalReadyBedsNow(units, admissions, bedReleases)} + + {totalReadyBedsNow(units, admissions, bedReleases, [...movements, ...referrals])} + {old} diff --git a/tests/ward-audit-24-4-counts.test.ts b/tests/ward-audit-24-4-counts.test.ts index b746b60b4d..5a456fa92e 100644 --- a/tests/ward-audit-24-4-counts.test.ts +++ b/tests/ward-audit-24-4-counts.test.ts @@ -54,7 +54,10 @@ describe("audit 24-4: Handover held and header bed alerts read the shared bed st if (oldHeld !== pulled) oldHeldSplit += 1; const siteUnits = state.units.filter((unit) => siteCodes.has(unit.siteCode)); - const ready = totalReadyBedsNow(siteUnits, state.admissions, state.bedReleases); + const ready = totalReadyBedsNow(siteUnits, state.admissions, state.bedReleases, [ + ...state.movements, + ...state.referrals, + ]); const alerts = deriveServiceBedAlerts( state.units, state.bedReleases, @@ -63,6 +66,7 @@ describe("audit 24-4: Handover held and header bed alerts read the shared bed st now, undefined, state.admissions, + state.referrals, ); expect(alerts.totalFreeBeds, label).toBe(ready); const preparing = siteUnits.reduce((sum, unit) => sum + bedsPendingPreparation(unit.id, state.bedReleases), 0); diff --git a/tests/ward-audit-24-4-wording.dom.test.tsx b/tests/ward-audit-24-4-wording.dom.test.tsx index f0c8799260..ea6f4c4cd9 100644 --- a/tests/ward-audit-24-4-wording.dom.test.tsx +++ b/tests/ward-audit-24-4-wording.dom.test.tsx @@ -87,7 +87,7 @@ describe("audit 24-4 wording", () => { ); act(() => fireEvent.click(screen.getByTestId("ward-open-bed-board"))); const unit = seen!.units.find((candidate) => candidate.id === "scgh-adult-open")!; - const ready = readyBedsNow(unit, seen!.admissions, seen!.bedReleases); + const ready = readyBedsNow(unit, seen!.admissions, seen!.bedReleases, []); const preparing = bedsPendingPreparation(unit.id, seen!.bedReleases); const head = screen.getAllByText( (_, element) => element?.tagName === "SPAN" && /^\d+ ready of \d+/u.test(element.textContent ?? ""), diff --git a/tests/ward-audit-m118-ward-fit-surfaces.test.ts b/tests/ward-audit-m118-ward-fit-surfaces.test.ts index d22094732b..aaccf3b5ed 100644 --- a/tests/ward-audit-m118-ward-fit-surfaces.test.ts +++ b/tests/ward-audit-m118-ward-fit-surfaces.test.ts @@ -251,7 +251,7 @@ describe("M118: the Tools bed finder is a narrower quick filter", () => { it("never lists a ward the engine refuses on the three rules it does check (cohort, authorisation, sex)", () => { for (const person of PEOPLE.filter((candidate) => candidate.gender === candidate.sex)) { - const listed = findBeds(units, [], [], filtersFor(person, person.sex)); + const listed = findBeds(units, [], [], filtersFor(person, person.sex), []); for (const row of listed) { const gates = eligibility(asMovement(person), row.unit, NOW).gates; const where = `${row.unit.id} ${JSON.stringify(person)}`; @@ -268,11 +268,12 @@ describe("M118: the Tools bed finder is a narrower quick filter", () => { const forensic: Unit = { ...base, id: `${base.id}-m118-forensic`, forensic: true }; const plain: Unit = { ...base, id: `${base.id}-m118-plain`, forensic: false }; const person = PEOPLE.find((candidate) => candidate.cohort === "Adult" && candidate.gender === candidate.sex)!; - const listed = findBeds([forensic, plain], [], [], filtersFor(person, "any")).map((row) => row.unit.id); + const listed = findBeds([forensic, plain], [], [], filtersFor(person, "any"), []).map((row) => row.unit.id); expect(listed).toEqual([plain.id]); expect(gate(eligibility(asMovement(person), forensic, NOW).gates, "forensic").pass).toBe(false); for (const person of PEOPLE.filter((candidate) => candidate.gender === candidate.sex)) - for (const row of findBeds(units, [], [], filtersFor(person, person.sex))) expect(row.unit.forensic).toBe(false); + for (const row of findBeds(units, [], [], filtersFor(person, person.sex), [])) + expect(row.unit.forensic).toBe(false); }); it("says on screen which fit rules the quick filter leaves out", () => { diff --git a/tests/ward-audit-m80-scheduled-jobs.test.ts b/tests/ward-audit-m80-scheduled-jobs.test.ts index cad7d27d4e..b3f1173131 100644 --- a/tests/ward-audit-m80-scheduled-jobs.test.ts +++ b/tests/ward-audit-m80-scheduled-jobs.test.ts @@ -218,6 +218,25 @@ describe("M72: the 10-minute uptime check, each failure injected offline", () => expect(workspaceOnly.gh).not.toMatch(/issue (?:create|close|comment)/u); }); + // #1613, 12 Nov 2026: a hidden database check must not swallow a sign-in or workspace outage. + it("still raises the sign-in or workspace issue, then fails, when the database check is hidden", async () => { + for (const [workspace, title] of [ + ["signin", "Ward Flow sign-in is not answering"], + ["workspace", "Signed-in staff cannot load the shared workspace"], + ] as const) { + const result = await runStep(RAISE_STEP, { + HEALTH: "unknown", + WORKSPACE: workspace, + WORKSPACE_DETAIL: "HTTP 503", + OPEN_ISSUE: "", + }); + expect(result.code).toBe(1); + expect(result.gh.match(/issue create/gu)).toHaveLength(1); + expect(result.gh).toContain(`--title ${title}`); + expect(result.log).toMatch(/::error::Uptime check did not see the database: WARD_FLOW_UPTIME_TOKEN/u); + } + }); + const gapRun = (env: Record) => runStep(GAP_STEP, { OPEN_ISSUE: "", diff --git a/tests/ward-audit-planned-reservations.test.ts b/tests/ward-audit-planned-reservations.test.ts index c6e0eec6d6..7ae77cfea2 100644 --- a/tests/ward-audit-planned-reservations.test.ts +++ b/tests/ward-audit-planned-reservations.test.ts @@ -106,6 +106,7 @@ describe("planned conversion consumes only unreserved open capacity", () => { before.units.find((u) => u.id === UNIT)!, before.admissions, before.bedReleases, + [], ); expect(ready()).toBeGreaterThan(0); for (let step = 0; ready() > 0 && step < before.units.find((u) => u.id === UNIT)!.beds; step++) { @@ -181,6 +182,7 @@ describe("planned conversion consumes only unreserved open capacity", () => { before.units.find((u) => u.id === UNIT)!, before.admissions, before.bedReleases, + [], ), ).toBe(pulls === 4 ? 1 : 0); const after = wardFlowReducer(before, convert(booking.plannedAdmissionId)); diff --git a/tests/ward-audit-remediation-statistics.test.ts b/tests/ward-audit-remediation-statistics.test.ts index fa62fe951e..9b2a0bc143 100644 --- a/tests/ward-audit-remediation-statistics.test.ts +++ b/tests/ward-audit-remediation-statistics.test.ts @@ -29,7 +29,7 @@ describe("audit remediation: statistics use recorded populations", () => { const admissions = [ { ...state.admissions[0]!, unitId: unit.id, state: "pulled" as const, movementId: state.movements[0]!.id }, ]; - expect(readyBedsNow(unit, admissions, [])).toBe(2); + expect(readyBedsNow(unit, admissions, [], [])).toBe(2); expect(networkCapacity([unit], [], admissions).ready).toBe(2); const sites = siteFigures({ units: [unit], diff --git a/tests/ward-board-left-ward.dom.test.tsx b/tests/ward-board-left-ward.dom.test.tsx index cfeb36fd9f..b2951e61d0 100644 --- a/tests/ward-board-left-ward.dom.test.tsx +++ b/tests/ward-board-left-ward.dom.test.tsx @@ -87,7 +87,7 @@ describe("ward board: Left the ward, waiting for transport (item 18)", () => { .getAllByRole("radio") .filter((radio) => (radio as HTMLInputElement).checked), ).toHaveLength(0); - fireEvent.click(within(transport).getByRole("radio", { name: "No, still waiting for transport" })); + fireEvent.click(within(transport).getByRole("radio", { name: "Still waiting" })); fireEvent.click(proceed); expect(screen.getAllByRole("button", { name: /^Collected/ })).toHaveLength(before + 1); }); diff --git a/tests/ward-consolidated-census-20261104.test.ts b/tests/ward-consolidated-census-20261104.test.ts index 8f72a2f27e..35ad9bba18 100644 --- a/tests/ward-consolidated-census-20261104.test.ts +++ b/tests/ward-consolidated-census-20261104.test.ts @@ -145,7 +145,7 @@ describe("consolidated capacity and scope joins", () => { const admissions = [ { ...seed.admissions[0]!, state: "pulled" as const, unitId: unit.id, movementId: "WF-live-pull" as const }, ]; - expect(unitReadyBedCount(unit, admissions)).toBe(1); + expect(unitReadyBedCount(unit, admissions, [])).toBe(1); const results = searchWardFlow({ query: "ready beds", units: [unit], movements: [], patients: [], admissions }); expect(results.wards[0]?.readyBeds).toBe(1); }); diff --git a/tests/ward-count-breakdown.test.ts b/tests/ward-count-breakdown.test.ts index 2724e3bb05..d976465aa2 100644 --- a/tests/ward-count-breakdown.test.ts +++ b/tests/ward-count-breakdown.test.ts @@ -129,7 +129,7 @@ describe("count breakdown: scope", () => { for (const p of pulled) expect(own.has(p.key)).toBe(true); // Beds are not people: a ward desk sees the network's ready beds as counts (D-115), so a bed // breakdown lists wards and numbers, never a person. - for (const row of wardReadyBreakdownRows(readyBedsByWard(scoped.units, scoped.admissions, scoped.bedReleases))) + for (const row of wardReadyBreakdownRows(readyBedsByWard(scoped.units, scoped.admissions, scoped.bedReleases, []))) expect(row.count).toBeGreaterThan(0); }); diff --git a/tests/ward-d115-named-holds-everywhere.test.ts b/tests/ward-d115-named-holds-everywhere.test.ts new file mode 100644 index 0000000000..640a80b26d --- /dev/null +++ b/tests/ward-d115-named-holds-everywhere.test.ts @@ -0,0 +1,107 @@ +import { describe, expect, it } from "vitest"; + +import { trackServiceBedCapacity } from "@/components/ward-management/capacity/service-capacity-tracker"; +import { unitReadyBedCount } from "@/components/ward-management/search/ward-smart-search"; +import { deriveServiceBedAlerts } from "@/components/ward-management/shell/ward-service-bed-alerts"; +import { bedSummaryText } from "@/components/ward-management/tools/ward-bed-summary"; +import { DEFAULT_BED_FINDER_FILTERS, findBeds } from "@/components/ward-management/tools/ward-bed-finder"; +import { readyBedsNow, totalReadyBedsNow } from "@/components/ward-management/ward-bed-states"; +import { DEFAULT_DUE_SOON_THRESHOLDS } from "@/components/ward-management/ward-clock"; +import { seedWardFlowState, wardFlowReducer } from "@/components/ward-management/ward-flow-reducer"; +import { activeWardNetwork, NOW_ANCHOR, wardSites } from "@/components/ward-management/ward-sites"; + +/** + * D-115 item 3 (Josh, 9 Nov 2026), follow-up review 12 Nov 2026: a bed named for an open journey or + * an accepted referral is not ready on any screen. The Tools sheet (Key figures, Copy bed summary, + * Find a bed, Call log), the rail's service alerts, Search and the ward peek used to count it as + * ready. Synthetic seed data only. + */ +// The ward board's "Place here": RF-005 accepted into rph-older-adult bed 1 (as the one-number test). +const seed = seedWardFlowState(); +const state = wardFlowReducer(seed, { + type: "ACCEPT_REFERRAL", + role: "ward", + now: NOW_ANCHOR, + referralId: "RF-005", + destinationKind: "psychiatric_ward", + unitId: "rph-older-adult", + actingUnitId: "rph-older-adult", + bedNumber: 1, +} as Parameters[1]); +const holds = [...state.movements, ...state.referrals]; +const named = state.units.filter( + (unit) => + readyBedsNow(unit, state.admissions, state.bedReleases, holds) < + readyBedsNow(unit, state.admissions, state.bedReleases, []), +); + +describe("D-115 item 3: a named bed is not ready on the Tools sheet, rail or Search", () => { + it("the accepted referral names an otherwise ready bed, so the case is reached", () => { + expect(state.rejections.length, "the acceptance was taken").toBe(seed.rejections.length); + expect(named.map((unit) => unit.id)).toContain("rph-older-adult"); + }); + + it("Copy bed summary states the ready count with the named holds left out", () => { + const ready = totalReadyBedsNow(state.units, state.admissions, state.bedReleases, holds); + expect(ready).toBeLessThan(totalReadyBedsNow(state.units, state.admissions, state.bedReleases, [])); + const text = bedSummaryText({ + units: state.units, + admissions: state.admissions, + movements: state.movements, + referrals: state.referrals, + bedReleases: state.bedReleases, + leaveBeds: state.leaveBeds, + now: NOW_ANCHOR, + thresholds: DEFAULT_DUE_SOON_THRESHOLDS, + network: activeWardNetwork(), + }); + expect(text).toContain(`Ready beds: ${ready} of `); + }); + + it("Find a bed caps each ward at the ready count with the named holds left out", () => { + for (const unit of named) { + const filters = { ...DEFAULT_BED_FINDER_FILTERS, cohort: unit.cohort, legalStatus: "voluntary" as const }; + const row = findBeds(state.units, state.admissions, state.bedReleases, filters, holds).find( + (candidate) => candidate.unit.id === unit.id, + ); + expect(row?.free, unit.id).toBe(readyBedsNow(unit, state.admissions, state.bedReleases, holds)); + } + }); + + it("the rail's service alerts and the capacity tracker leave the named beds out of Free", () => { + const siteCodes = new Set(wardSites.map((site) => site.code)); + const inNetwork = state.units.filter((unit) => siteCodes.has(unit.siteCode)); + const ready = totalReadyBedsNow(inNetwork, state.admissions, state.bedReleases, holds); + const alerts = deriveServiceBedAlerts( + state.units, + state.bedReleases, + wardSites, + state.movements, + NOW_ANCHOR, + undefined, + state.admissions, + state.referrals, + ); + expect(alerts.totalFreeBeds).toBe(ready); + const report = trackServiceBedCapacity( + state.units, + state.bedReleases, + state.movements, + NOW_ANCHOR, + wardSites, + undefined, + state.admissions, + state.referrals, + ); + const free = report.services.reduce((sum, service) => sum + service.remainingBeds, 0); + expect(free).toBe(ready); + }); + + it("Search's ward ready count leaves the named beds out", () => { + for (const unit of named) { + expect(unitReadyBedCount(unit, state.admissions, holds), unit.id).toBe( + readyBedsNow(unit, state.admissions, [], holds), + ); + } + }); +}); diff --git a/tests/ward-discharge-record-departure.dom.test.tsx b/tests/ward-discharge-record-departure.dom.test.tsx index ea814fa38e..403b98f744 100644 --- a/tests/ward-discharge-record-departure.dom.test.tsx +++ b/tests/ward-discharge-record-departure.dom.test.tsx @@ -178,7 +178,7 @@ describe("Record discharge asks about transport while it is still to come (item // Nothing is chosen for them: the departure waits for an answer. expect(record).toBeDisabled(); const transport = screen.getByTestId("ward-discharge-departure-transport"); - fireEvent.click(within(transport).getByRole("radio", { name: "No, still waiting for transport" })); + fireEvent.click(within(transport).getByRole("radio", { name: "Still waiting" })); expect(record).toBeEnabled(); const waitingBefore = screen.getByTestId("transport-probe").textContent!.split("|")[1]!; fireEvent.click(record); diff --git a/tests/ward-executive-statistics-projector.test.ts b/tests/ward-executive-statistics-projector.test.ts index ed5751e875..6e43a7e9a9 100644 --- a/tests/ward-executive-statistics-projector.test.ts +++ b/tests/ward-executive-statistics-projector.test.ts @@ -248,7 +248,10 @@ describe("the inactive full reporting bundle", () => { const sourceWard = state.units.find((unit) => unit.id === ward.id)!; // These are public ward inventory facts already used by the permitted bed finder. // Never infer free locked/open capacity from the physical designation split. - const finderReady = readyBedsNow(sourceWard, state.admissions, state.bedReleases); + const finderReady = readyBedsNow(sourceWard, state.admissions, state.bedReleases, [ + ...state.movements, + ...state.referrals, + ]); expect(ward).toMatchObject({ isClosed: sourceWard.isClosed === true, sexDesignation: sourceWard.sexDesignation, @@ -1042,7 +1045,9 @@ describe("the executive Network aggregate boundary", () => { pulled: occupancy.pulled, inUse: occupancy.inUse, }); - expect(projected.beds.ready).toBe(totalReadyBedsNow(state.units, state.admissions, state.bedReleases)); + expect(projected.beds.ready).toBe( + totalReadyBedsNow(state.units, state.admissions, state.bedReleases, [...state.movements, ...state.referrals]), + ); // The seed's linked movement family has protected information. Its large waiting figure // must no longer remain exact beside that family; unsuppressed source parity is checked below. expect(waitingInEds(state.movements, NOW_ANCHOR).total).toBeGreaterThan(0); diff --git a/tests/ward-figures-agree.test.ts b/tests/ward-figures-agree.test.ts index df187fb9c1..a4c8795bac 100644 --- a/tests/ward-figures-agree.test.ts +++ b/tests/ward-figures-agree.test.ts @@ -56,12 +56,15 @@ describe("figures that more than one screen prints agree after random actions", const thresholds = dueSoonThresholdsOf(state.configuration); // Beds ready. - const ready = totalReadyBedsNow(state.units, state.admissions, state.bedReleases); - expect(bedsReadySummary(state.units, state.admissions, state.bedReleases, now).total, at).toBe(ready); + // D-115 item 3: a bed named for an open journey or accepted referral is not ready anywhere. + const holds = [...state.movements, ...state.referrals]; + const ready = totalReadyBedsNow(state.units, state.admissions, state.bedReleases, holds); + expect(bedsReadySummary(state.units, state.admissions, state.bedReleases, now, holds).total, at).toBe(ready); const summary = bedSummaryText({ units: state.units, admissions: state.admissions, movements: state.movements, + referrals: state.referrals, bedReleases: state.bedReleases, leaveBeds: state.leaveBeds, now, @@ -69,11 +72,14 @@ describe("figures that more than one screen prints agree after random actions", network, }); expect(summary, at).toContain(`Ready beds: ${ready} of `); - for (const row of findBeds(state.units, state.admissions, state.bedReleases, { - ...DEFAULT_BED_FINDER_FILTERS, - legalStatus: "voluntary", - })) { - expect(row.free, at).toBe(readyBedsNow(row.unit, state.admissions, state.bedReleases)); + for (const row of findBeds( + state.units, + state.admissions, + state.bedReleases, + { ...DEFAULT_BED_FINDER_FILTERS, legalStatus: "voluntary" }, + holds, + )) { + expect(row.free, at).toBe(readyBedsNow(row.unit, state.admissions, state.bedReleases, holds)); } const old = state.units.reduce( (sum, unit) => sum + capacityBreakdown(unit, state.bedReleases, state.leaveBeds, now).availableNow, diff --git a/tests/ward-home-cards-pending-reachable.test.ts b/tests/ward-home-cards-pending-reachable.test.ts new file mode 100644 index 0000000000..dac1b821ce --- /dev/null +++ b/tests/ward-home-cards-pending-reachable.test.ts @@ -0,0 +1,25 @@ +import { readFileSync } from "node:fs"; + +import { describe, expect, it } from "vitest"; + +const strip = (css: string) => css.replace(/\/\*[\s\S]*?\*\//gu, ""); +const home = strip(readFileSync("src/components/ward-management/coordinator/home.module.css", "utf8")); + +// #1631 (12 Nov 2026): while Home waits for its two early cards (layout shift on Home, 11 Nov 2026), +// the columns stay in place but must not leave the accessibility tree or the keyboard order. +describe("Home: the columns held for the early cards stay reachable", () => { + const pendingRules = [...home.matchAll(/([^{}]*data-cards-pending[^{}]*)\{([^}]*)\}/gu)]; + + it("holds the columns invisible with opacity, keeping their space", () => { + expect(pendingRules.length).toBeGreaterThan(0); + expect(pendingRules.some(([, , body]) => /opacity:\s*0/u.test(body ?? ""))).toBe(true); + }); + + it("never hides them with visibility or display, which screen readers and the keyboard skip", () => { + for (const [, , body] of pendingRules) expect(body).not.toMatch(/visibility:\s*hidden|display:\s*none/u); + }); + + it("shows them as soon as focus moves inside", () => { + for (const [, selector] of pendingRules) expect(selector).toMatch(/:not\(:focus-within\)/u); + }); +}); diff --git a/tests/ward-legal-form-ended-cto-glance.dom.test.tsx b/tests/ward-legal-form-ended-cto-glance.dom.test.tsx new file mode 100644 index 0000000000..2678636387 --- /dev/null +++ b/tests/ward-legal-form-ended-cto-glance.dom.test.tsx @@ -0,0 +1,28 @@ +// Review follow-up (12 Nov 2026): the patient band names an arrived Form 5A or 5B that was then +// recorded as ended as ended, never "On Form 5A at arrival". Synthetic record only. +import "@testing-library/jest-dom/vitest"; +import { cleanup, render, screen } from "@testing-library/react"; +import { afterEach, describe, expect, it } from "vitest"; + +import { PatientGlance } from "@/components/ward-management/patients/patient-glance"; +import type { Movement } from "@/components/ward-management/ward-model"; + +afterEach(cleanup); + +describe("patient band, an ended community treatment order after arrival", () => { + it("says the form was recorded as ended", () => { + const movement = { + id: "ZZ-SYNTH-CTO", + openedAt: 0, + stage: "arrived", + legalStatus: "Involuntary inpatient", + legalForm: { code: "5A", endedBy: "revoked" }, + closure: { outcome: "arrived" }, + } as unknown as Movement; + render(); + const legal = screen.getByTestId("ward-patient-glance-legal"); + expect(legal).toHaveTextContent("Form 5A recorded as ended"); + expect(legal).not.toHaveTextContent("On Form 5A at arrival"); + expect(legal).not.toHaveTextContent("in force"); + }); +}); diff --git a/tests/ward-legal-form-ended-cto-wording.test.ts b/tests/ward-legal-form-ended-cto-wording.test.ts new file mode 100644 index 0000000000..30e157241b --- /dev/null +++ b/tests/ward-legal-form-ended-cto-wording.test.ts @@ -0,0 +1,53 @@ +import { describe, expect, it } from "vitest"; + +import { + FORM_RECORDED_ENDED, + formProgression, + legalFormEnd, +} from "@/components/ward-management/legal-forms/legal-form-progression"; +import { legalDeadlineText } from "@/components/ward-management/legal-forms/legal-forms-derivations"; +import { legalEndAsTyped } from "@/components/ward-management/legal-forms/legal-forms-export"; +import type { Movement } from "@/components/ward-management/ward-model"; + +/** + * Review follow-up (12 Nov 2026): a patient arrives on a community treatment order (Form 5A or 5B) + * and the order is then recorded as ended. #1621 made the Legal forms line check the recorded end + * before the neutral D-127 arrival wording; every other output now reads the same order through + * `legalFormEnd`, so none says "On Form 5A at arrival" for an ended order. Synthetic records only. + */ +const NOW = 10 * 60; + +function arrivedOn(code: string, endedBy?: "revoked"): Movement { + return { + id: "ZZ-SYNTH-CTO", + openedAt: 0, + stage: "arrived", + legalStatus: "Involuntary inpatient", + legalForm: { code, ...(endedBy ? { endedBy } : {}) }, + closure: { at: NOW, outcome: "arrived", reason: "arrived" }, + } as unknown as Movement; +} + +describe("an arrived community treatment order recorded as ended", () => { + for (const code of ["5A", "5B"]) { + it(`Form ${code}: the recorded end wins over the arrival wording`, () => { + const ended = arrivedOn(code, "revoked"); + expect(formProgression(ended)?.ended).toBe(false); + expect(legalFormEnd(ended)).toEqual({ kind: "recorded" }); + expect(legalDeadlineText(ended, NOW)).toBe("Form ended"); + expect(legalEndAsTyped(ended, NOW, new Date("2026-10-19T00:00:00Z"))).toBe(FORM_RECORDED_ENDED); + }); + + it(`Form ${code}: still neutral at arrival when no end is recorded (D-127)`, () => { + const open = arrivedOn(code); + expect(legalFormEnd(open)).toMatchObject({ kind: "progressed", progression: { ended: false } }); + expect(legalEndAsTyped(open, NOW, new Date("2026-10-19T00:00:00Z"))).toBe(`On Form ${code} at arrival`); + }); + } + + it("a form that ended at arrival still reads as ended at arrival, even when also recorded as ended", () => { + const ended = arrivedOn("4C", "revoked"); + expect(legalFormEnd(ended)).toMatchObject({ kind: "progressed", progression: { ended: true } }); + expect(legalEndAsTyped(ended, NOW, new Date("2026-10-19T00:00:00Z"))).toBe("Form 4C ended at arrival"); + }); +}); diff --git a/tests/ward-never-build-drift-guard.test.ts b/tests/ward-never-build-drift-guard.test.ts index 5825a1998f..54663872c8 100644 --- a/tests/ward-never-build-drift-guard.test.ts +++ b/tests/ward-never-build-drift-guard.test.ts @@ -297,6 +297,7 @@ describe("never-build drift guard", () => { units: state.units, admissions: state.admissions, movements: state.movements, + referrals: state.referrals, bedReleases: state.bedReleases, leaveBeds: state.leaveBeds, now: NOW_ANCHOR, diff --git a/tests/ward-outage-catch-up-screens.dom.test.tsx b/tests/ward-outage-catch-up-screens.dom.test.tsx index 185756dc5d..348e963089 100644 --- a/tests/ward-outage-catch-up-screens.dom.test.tsx +++ b/tests/ward-outage-catch-up-screens.dom.test.tsx @@ -237,7 +237,7 @@ describe("item 25: catch up after the outage", () => { expect(card).toHaveTextContent( `Since your pack at ${formatInstant(OUTAGE.from - 30)}, enter admissions, discharges, moves and bed changes done on paper.`, ); - for (const action of ["Add admission", "Record discharge", "Record move", "Change a bed", "Beds still correct"]) + for (const action of ["Open arrivals", "Open discharges", "Open bed board", "Open bed list", "Beds still correct"]) expect(within(card).getByRole("button", { name: action })).toBeInTheDocument(); expect(within(card).getByTestId("ward-catch-up-beds")).toHaveTextContent( `${ward.beds} beds not confirmed since the outage`, diff --git a/tests/ward-search-answers.test.ts b/tests/ward-search-answers.test.ts index 642a8249cc..3bd04880c6 100644 --- a/tests/ward-search-answers.test.ts +++ b/tests/ward-search-answers.test.ts @@ -287,7 +287,9 @@ describe("beds and ready beds", () => { expect(isReadyBedQuery("bed")).toBe(false); const results = search("free beds"); expect(results.readyBedsOnly).toBe(true); - const expected = state.units.filter((unit) => (unitReadyBedCount(unit) ?? 0) > 0).map((unit) => unit.id); + const expected = state.units + .filter((unit) => (unitReadyBedCount(unit, [], [...state.movements, ...state.referrals]) ?? 0) > 0) + .map((unit) => unit.id); if (expected.length === 0) throw new Error("fixture drifted: no ward has a ready bed"); expect(results.wards.map((result) => result.unit.id)).toEqual(expected.slice(0, 40)); }); diff --git a/tests/ward-service-bed-alerts.test.ts b/tests/ward-service-bed-alerts.test.ts index 8d06104882..2f46d23136 100644 --- a/tests/ward-service-bed-alerts.test.ts +++ b/tests/ward-service-bed-alerts.test.ts @@ -140,7 +140,7 @@ describe("deriveServiceBedAlerts", () => { const releases: BedRelease[] = []; const summary = deriveServiceBedAlerts([unit], releases, [site]); const row = summary.services[0]; - expect(row.freeBeds).toBe(readyBedsNow(unit, [], releases)); + expect(row.freeBeds).toBe(readyBedsNow(unit, [], releases, [])); expect(row.totalBeds).toBe(unit.beds); // The board's occupied bucket, not total minus free (Josh, 25 Sept 2026: the rail matches the board). expect(row.occupiedBeds).toBe(unitCapacity(unit, releases).occupied); diff --git a/tests/ward-tools-sheet-helpers.test.ts b/tests/ward-tools-sheet-helpers.test.ts index cb35f9b582..af453864da 100644 --- a/tests/ward-tools-sheet-helpers.test.ts +++ b/tests/ward-tools-sheet-helpers.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from "vitest"; import { wardAdmissions } from "@/components/ward-management/ward-admissions-seed"; -import { bedReleases, leaveBeds, wardMovements } from "@/components/ward-management/ward-movements"; +import { bedReleases, leaveBeds, referrals, wardMovements } from "@/components/ward-management/ward-movements"; import { activeWardNetwork, allUnits, NOW_ANCHOR } from "@/components/ward-management/ward-sites"; import { DEFAULT_DUE_SOON_THRESHOLDS } from "@/components/ward-management/ward-clock"; import { readyBedsNow, totalReadyBedsNow } from "@/components/ward-management/ward-bed-states"; @@ -14,6 +14,7 @@ const seed = { units: allUnits(), admissions: wardAdmissions, movements: wardMovements, + referrals, bedReleases, leaveBeds, now: NOW_ANCHOR, @@ -21,11 +22,14 @@ const seed = { network: activeWardNetwork(), }; +// D-115 item 3: the named holds every ready figure leaves out. +const holds = [...wardMovements, ...referrals]; + describe("Tools sheet, Copy bed summary", () => { // NA3-05 (audit 26 Oct 2026): the figure Home, Capacity and the rail print, not the older // `capacityBreakdown().availableNow` this test used to compare against. it("states the same ready count as every other screen, with total beds", () => { - const ready = totalReadyBedsNow(seed.units, wardAdmissions, bedReleases); + const ready = totalReadyBedsNow(seed.units, wardAdmissions, bedReleases, holds); const beds = seed.units.reduce((sum, unit) => sum + unit.beds, 0); expect(bedSummaryText(seed)).toContain(`Ready beds: ${ready} of ${beds}`); }); @@ -43,32 +47,34 @@ const VOLUNTARY = { ...DEFAULT_BED_FINDER_FILTERS, legalStatus: "voluntary" } as describe("Tools sheet, Find a bed", () => { it("lists nothing until a legal status is chosen", () => { expect(DEFAULT_BED_FINDER_FILTERS.legalStatus).toBeUndefined(); - expect(findBeds(seed.units, wardAdmissions, bedReleases, DEFAULT_BED_FINDER_FILTERS)).toEqual([]); + expect(findBeds(seed.units, wardAdmissions, bedReleases, DEFAULT_BED_FINDER_FILTERS, holds)).toEqual([]); }); it("lists every open ward of the chosen age group, with its Ready count and recorded time", () => { - const matches = findBeds(seed.units, wardAdmissions, bedReleases, VOLUNTARY); + const matches = findBeds(seed.units, wardAdmissions, bedReleases, VOLUNTARY, holds); expect(matches.length).toBeGreaterThan(0); expect(matches).toHaveLength(seed.units.filter((unit) => !unit.isClosed && unit.cohort === "Adult").length); for (const match of matches) { expect(match.unit.cohort).toBe("Adult"); expect(match.unit.isClosed).not.toBe(true); - expect(match.free).toBe(readyBedsNow(match.unit, wardAdmissions, bedReleases)); + expect(match.free).toBe(readyBedsNow(match.unit, wardAdmissions, bedReleases, holds)); expect(match.recordedAt).toBe(match.unit.allocatable.confirmedAt); } }); it("keeps involuntary patients to authorised wards and locked requests to free locked beds", () => { - const matches = findBeds(seed.units, wardAdmissions, bedReleases, { - ...DEFAULT_BED_FINDER_FILTERS, - legalStatus: "involuntary", - bed: "locked", - }); + const matches = findBeds( + seed.units, + wardAdmissions, + bedReleases, + { ...DEFAULT_BED_FINDER_FILTERS, legalStatus: "involuntary", bed: "locked" }, + holds, + ); expect(matches.length).toBeGreaterThan(0); for (const match of matches) { expect(match.unit.authorised).toBe(true); expect(match.free).toBe( - Math.min(lockedBedsFree(match.unit), readyBedsNow(match.unit, wardAdmissions, bedReleases)), + Math.min(lockedBedsFree(match.unit), readyBedsNow(match.unit, wardAdmissions, bedReleases, holds)), ); } }); @@ -76,7 +82,7 @@ describe("Tools sheet, Find a bed", () => { it("never shows more beds than the board's Ready figure", () => { const ward = seed.units.find((unit) => !unit.isClosed && unit.cohort === "Adult" && unit.allocatable.value > 1)!; const crowded = { ...ward, empty: { ...ward.empty, value: 1 } }; - const [match] = findBeds([crowded], [], bedReleases, VOLUNTARY); + const [match] = findBeds([crowded], [], bedReleases, VOLUNTARY, []); expect(match?.free).toBe(1); const mixed = { ...ward, @@ -84,21 +90,21 @@ describe("Tools sheet, Find a bed", () => { allocatableLocked: 2, empty: { ...ward.empty, value: 2 }, }; - expect(findBeds([mixed], [], bedReleases, { ...VOLUNTARY, bed: "open" })[0]?.free).toBe(2); + expect(findBeds([mixed], [], bedReleases, { ...VOLUNTARY, bed: "open" }, [])[0]?.free).toBe(2); // Every ward in scope is listed, so a ward with no Ready bed shows 0 rather than dropping out. const none = { ...ward, empty: { ...ward.empty, value: 0 } }; - expect(findBeds([none], [], bedReleases, VOLUNTARY)[0]?.free).toBe(0); + expect(findBeds([none], [], bedReleases, VOLUNTARY, [])[0]?.free).toBe(0); }); it("labels wards in the home service without moving them up the list", () => { const home = unitHealthService(seed.units[0]!)!; - const matches = findBeds(seed.units, wardAdmissions, bedReleases, { ...VOLUNTARY, home }); + const matches = findBeds(seed.units, wardAdmissions, bedReleases, { ...VOLUNTARY, home }, holds); expect(matches.some((match) => match.home)).toBe(true); for (const match of matches) expect(match.home).toBe(match.service === home); const names = matches.map((match) => match.unit.name); expect(names).toEqual([...names].sort((a, b) => a.localeCompare(b, "en-AU"))); expect(matches.map((match) => match.unit.id)).toEqual( - findBeds(seed.units, wardAdmissions, bedReleases, VOLUNTARY).map((match) => match.unit.id), + findBeds(seed.units, wardAdmissions, bedReleases, VOLUNTARY, holds).map((match) => match.unit.id), ); }); }); diff --git a/tests/ward-tools-sheet.dom.test.tsx b/tests/ward-tools-sheet.dom.test.tsx index 25bbca8254..04ed3d8a7d 100644 --- a/tests/ward-tools-sheet.dom.test.tsx +++ b/tests/ward-tools-sheet.dom.test.tsx @@ -165,7 +165,7 @@ describe("Tools sheet mockup (Tools review, 15 Oct 2026)", () => { it.each([6, 7])("lists all %i wards in scope, with no shortlist", (count) => { function MatchingWards() { const flow = useWardFlow(); - const template = findBeds(flow.units, [], [], { ...DEFAULT_BED_FINDER_FILTERS, legalStatus: "voluntary" })[0] + const template = findBeds(flow.units, [], [], { ...DEFAULT_BED_FINDER_FILTERS, legalStatus: "voluntary" }, [])[0] ?.unit; if (!template) throw new Error("the fixture needs one matching adult ward"); const units = Array.from({ length: count }, (_, index) => ({ diff --git a/tests/ward-workflow-audit-regressions.test.ts b/tests/ward-workflow-audit-regressions.test.ts index 3d0d5b3b86..930cd5e058 100644 --- a/tests/ward-workflow-audit-regressions.test.ts +++ b/tests/ward-workflow-audit-regressions.test.ts @@ -111,7 +111,10 @@ describe("workflow audit regressions", () => { }); const unit = state.units.find((u) => u.id === "rph-adult-secure")!; const before = takeLastLookSnapshot(state, now, DEFAULT_DUE_SOON_THRESHOLDS); - const readyBefore = readyBedsNow(unit, state.admissions, state.bedReleases); + const readyBefore = readyBedsNow(unit, state.admissions, state.bedReleases, [ + ...state.movements, + ...state.referrals, + ]); state = send(state, { type: "CONFIRM_CAPACITY", role: "ward", @@ -123,7 +126,10 @@ describe("workflow audit regressions", () => { }); const currentUnit = state.units.find((u) => u.id === unit.id)!; const after = takeLastLookSnapshot(state, now + 1, DEFAULT_DUE_SOON_THRESHOLDS); - const readyAfter = readyBedsNow(currentUnit, state.admissions, state.bedReleases); + const readyAfter = readyBedsNow(currentUnit, state.admissions, state.bedReleases, [ + ...state.movements, + ...state.referrals, + ]); expect(readyAfter - readyBefore).toBe(1); expect(after.readyByUnit[unit.id]).toBe(readyAfter); expect(changesSinceLastLook(JSON.parse(JSON.stringify(before)), after, state)?.bedsFreed).toEqual([ From 037d282e4c9d25fa2b1590d1ac1385542b8e0833 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 12 Nov 2026 05:43:38 +0000 Subject: [PATCH 02/10] Keep the full suite green after the D-159 fixes The intake hours hint reads HH:MM (no invented hour figure), the review-engine test raises the ward referral from the bed's own actions now the desktop ward top has no New referral chip, and the Home hold guard joins the source-text list: it reads CSS because jsdom cannot compute the hold. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxPPxoiThczWiMzXnymuUN --- .../ward/ward-intake-window-dialog.tsx | 14 ++------------ tests/source-text-tests.json | 1 + tests/ward-review-engine.dom.test.tsx | 4 +++- 3 files changed, 6 insertions(+), 13 deletions(-) diff --git a/src/components/ward-management/ward/ward-intake-window-dialog.tsx b/src/components/ward-management/ward/ward-intake-window-dialog.tsx index 229b1e0074..8bfdda8844 100644 --- a/src/components/ward-management/ward/ward-intake-window-dialog.tsx +++ b/src/components/ward-management/ward/ward-intake-window-dialog.tsx @@ -106,12 +106,7 @@ export function WardIntakeWindowDialog({ data-testid="ward-intake-window-dialog" >
          - + { @@ -123,12 +118,7 @@ export function WardIntakeWindowDialog({ data-testid="ward-intake-window-from" /> - + { diff --git a/tests/source-text-tests.json b/tests/source-text-tests.json index a17ee3e364..710ec0e683 100644 --- a/tests/source-text-tests.json +++ b/tests/source-text-tests.json @@ -135,6 +135,7 @@ "tests/ward-governance-claims.test.ts", "tests/ward-handover-no-typed-patient-rows.test.ts", "tests/ward-handover.dom.test.tsx", + "tests/ward-home-cards-pending-reachable.test.ts", "tests/ward-hub-route.test.ts", "tests/ward-inbox-classification.test.ts", "tests/ward-inline-form-holds.test.ts", diff --git a/tests/ward-review-engine.dom.test.tsx b/tests/ward-review-engine.dom.test.tsx index a701a3c01a..0a2dfe61c7 100644 --- a/tests/ward-review-engine.dom.test.tsx +++ b/tests/ward-review-engine.dom.test.tsx @@ -136,7 +136,9 @@ describe("review caller regressions", () => { ); expect(occupied).toBeDefined(); fireEvent.click(occupied!); - fireEvent.click(screen.getAllByRole("button", { name: /^New referral$/ }).at(-1)!); + // D-159: off the phone the ward top has no New referral chip; the bed's own actions raise it. + fireEvent.click(screen.getAllByRole("button", { name: /^More$/ }).at(-1)!); + fireEvent.click(screen.getByRole("menuitem", { name: /^Raise ward referral/ })); expect(routing.dispatch.mock.calls.filter(([event]) => event.type === "RECEIVE_REFERRAL")).toEqual([]); expect(routing.push).toHaveBeenCalledWith(expect.stringMatching(/^\/ward-flow\/referrals\/new\?category=ward/)); }); From 8e06394aa9e4f21be73cb38d7f39a704f6b66ee1 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 12 Nov 2026 05:50:44 +0000 Subject: [PATCH 03/10] Refresh journey source-review digests after the D-159 fixes Reviewed the three changed sources against their event prose: the shortlist ended-form wording, the Legal forms phone Print and Export head and the shorter transport choice labels change no dispatch or event behaviour, and no journey prose quotes the old labels. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01GxPPxoiThczWiMzXnymuUN --- docs/ward-flow/journey/actions.json | 22 +++++++++---------- docs/ward-flow/journey/machines.json | 2 +- .../journey/ward-journey-explorer.html | 2 +- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/ward-flow/journey/actions.json b/docs/ward-flow/journey/actions.json index 320ab76d68..d431577c8b 100644 --- a/docs/ward-flow/journey/actions.json +++ b/docs/ward-flow/journey/actions.json @@ -949,7 +949,7 @@ { "path": "src/components/ward-management/coordinator/shortlist-panel.tsx", "line": 844, - "sha256": "6f864c8eff0bea759c1460ed1e8ebaf9bb4dc7d80aa3d515d0788709c109f9d0" + "sha256": "7f7692869cb4a53cfc7afe12449e068f78c995f9ef338be6e4ccdf1baf870cfe" }, { "path": "src/components/ward-management/ward-flow-referral-send-events.ts", @@ -1206,7 +1206,7 @@ { "path": "src/components/ward-management/coordinator/shortlist-panel.tsx", "line": 886, - "sha256": "6f864c8eff0bea759c1460ed1e8ebaf9bb4dc7d80aa3d515d0788709c109f9d0" + "sha256": "7f7692869cb4a53cfc7afe12449e068f78c995f9ef338be6e4ccdf1baf870cfe" }, { "path": "src/components/ward-management/ward-flow-referral-send-events.ts", @@ -3094,7 +3094,7 @@ { "path": "src/components/ward-management/legal-forms/legal-forms-screen.tsx", "line": 376, - "sha256": "06b9403c02ff16aace7df7690c0ca9f195bf1a674e79aaa9f0274b332fcc06b1" + "sha256": "381f09f6b932ae6752767f1e74e0978629c3ff442e1b0132c9d79275ef100762" }, { "path": "src/components/ward-management/ward-flow-record-events.ts", @@ -3240,7 +3240,7 @@ { "path": "src/components/ward-management/legal-forms/legal-forms-screen.tsx", "line": 1321, - "sha256": "06b9403c02ff16aace7df7690c0ca9f195bf1a674e79aaa9f0274b332fcc06b1" + "sha256": "381f09f6b932ae6752767f1e74e0978629c3ff442e1b0132c9d79275ef100762" }, { "path": "src/components/ward-management/ward-flow-record-legal-events.ts", @@ -3396,7 +3396,7 @@ { "path": "src/components/ward-management/board/ward-board-detail-panel.tsx", "line": 268, - "sha256": "403fc9988b2da12a35f0efbd2c4ea67e47eacd5a00bef6d74f9d05f0eb27b961" + "sha256": "12c0928971208fa4f68884904edd25f77b950c8e5e7476e5c51f921ab718dd0a" }, { "path": "src/components/ward-management/board/ward-board.tsx", @@ -3475,7 +3475,7 @@ { "path": "src/components/ward-management/board/ward-board-detail-panel.tsx", "line": 461, - "sha256": "403fc9988b2da12a35f0efbd2c4ea67e47eacd5a00bef6d74f9d05f0eb27b961" + "sha256": "12c0928971208fa4f68884904edd25f77b950c8e5e7476e5c51f921ab718dd0a" }, { "path": "src/components/ward-management/board/ward-board.tsx", @@ -3554,7 +3554,7 @@ { "path": "src/components/ward-management/board/ward-board-detail-panel.tsx", "line": 493, - "sha256": "403fc9988b2da12a35f0efbd2c4ea67e47eacd5a00bef6d74f9d05f0eb27b961" + "sha256": "12c0928971208fa4f68884904edd25f77b950c8e5e7476e5c51f921ab718dd0a" }, { "path": "src/components/ward-management/board/ward-board.tsx", @@ -5637,7 +5637,7 @@ { "path": "src/components/ward-management/board/ward-board-detail-panel.tsx", "line": 524, - "sha256": "403fc9988b2da12a35f0efbd2c4ea67e47eacd5a00bef6d74f9d05f0eb27b961" + "sha256": "12c0928971208fa4f68884904edd25f77b950c8e5e7476e5c51f921ab718dd0a" }, { "path": "src/components/ward-management/ward-flow-bed-events.ts", @@ -5706,7 +5706,7 @@ { "path": "src/components/ward-management/board/ward-board-detail-panel.tsx", "line": 566, - "sha256": "403fc9988b2da12a35f0efbd2c4ea67e47eacd5a00bef6d74f9d05f0eb27b961" + "sha256": "12c0928971208fa4f68884904edd25f77b950c8e5e7476e5c51f921ab718dd0a" }, { "path": "src/components/ward-management/ward-flow-bed-events.ts", @@ -5869,7 +5869,7 @@ { "path": "src/components/ward-management/board/ward-board-detail-panel.tsx", "line": 401, - "sha256": "403fc9988b2da12a35f0efbd2c4ea67e47eacd5a00bef6d74f9d05f0eb27b961" + "sha256": "12c0928971208fa4f68884904edd25f77b950c8e5e7476e5c51f921ab718dd0a" }, { "path": "src/components/ward-management/patients/patient-now-screen.tsx", @@ -6239,7 +6239,7 @@ { "path": "src/components/ward-management/legal-forms/legal-forms-screen.tsx", "line": 542, - "sha256": "06b9403c02ff16aace7df7690c0ca9f195bf1a674e79aaa9f0274b332fcc06b1" + "sha256": "381f09f6b932ae6752767f1e74e0978629c3ff442e1b0132c9d79275ef100762" }, { "path": "src/components/ward-management/ward-flow-record-legal-events.ts", diff --git a/docs/ward-flow/journey/machines.json b/docs/ward-flow/journey/machines.json index d17de25cf0..5927b1b8df 100644 --- a/docs/ward-flow/journey/machines.json +++ b/docs/ward-flow/journey/machines.json @@ -1744,7 +1744,7 @@ { "path": "src/components/ward-management/legal-forms/legal-forms-screen.tsx", "line": 1, - "sha256": "06b9403c02ff16aace7df7690c0ca9f195bf1a674e79aaa9f0274b332fcc06b1" + "sha256": "381f09f6b932ae6752767f1e74e0978629c3ff442e1b0132c9d79275ef100762" }, { "path": "src/components/ward-management/legal-forms/legal-forms-sheets.tsx", diff --git a/docs/ward-flow/journey/ward-journey-explorer.html b/docs/ward-flow/journey/ward-journey-explorer.html index f9a6efaa39..f871b10d2f 100644 --- a/docs/ward-flow/journey/ward-journey-explorer.html +++ b/docs/ward-flow/journey/ward-journey-explorer.html @@ -1284,7 +1284,7 @@

          What to do here