Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/FACTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-153 | Newest entry in the decision log | `docs/ward-flow/decisions.md` |
| `next-decision` | D-154 | Number to use for the next decision | `docs/ward-flow/decisions.md` |
| `latest-decision` | D-154 | Newest entry in the decision log | `docs/ward-flow/decisions.md` |
| `next-decision` | D-155 | 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` |

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# 11 Nov 2026, community desk ED heads-up (PR #1632, D-154)

Changed on top of `main` at `0b657ad` (just after #1606 merged). Each raise is under 1 KB, well within the per-screen allowance in HOW-WE-WORK section 6:

- Home `/ward-flow`: 587.7 to 588.3
- ED `/ward-flow/ed`: 543.4 to 543.9
- Movement `/ward-flow/movements/WF-001`: 618.7 to 619.0
- Capacity `/ward-flow/capacity`: 665.7 to 665.9
- Bed meeting `/ward-flow/bed-meeting`: 600.1 to 600.7
- Statistics `/ward-flow/statistics`: 640.5 to 641.0
- Referrals, Ward and ED site: unchanged

Hosted CI (job 130831009655) measured, in KB: Home 588.1, Referrals 594.0, ED 543.7, Ward 625.1, Movement 618.8, Capacity 665.7, ED site 676.1, Bed meeting 600.5 and Statistics 640.8. Each changed limit is the measurement plus 0.2.

Why it changed: the scoped workspace builder (`ward-access-scope.ts`), which every screen loads, now builds the community desk's narrow ED list (D-154). So it imports the heads-up list builder and the small episode team list, and the access shell gains one context. That costs about 0.3 to 0.6 KB a screen.

Trim tried: building the list only on the community desk's Home card would keep it off other screens. That would mean the card reads the whole workspace again, which is the exposure D-154 removes. The scoped view has to carry the list, so it is built where the scope is built.
602 changes: 302 additions & 300 deletions docs/ward-flow/OWNER-RULINGS.md

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions docs/ward-flow/decisions.md
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ Checked 9 November 2026 (D-107 audit). "Partly replaced" means part of the entry
| [D-147](#d-147-demo-code-tries-keep-per-address-keying-until-a-trusted-proxy-is-set-11-november-2026) | Demo-code tries keep per-address keying until a trusted proxy is set | Active |
| [D-152](#d-152-returned-to-ed-prompt-sees-every-return-in-shared-mode-11-november-2026) | Returned to ED prompt sees every return in shared mode | Active |
| [D-153](#d-153-keep-the-drawn-time-settle-check-and-defer-a-separate-agent-github-identity-11-november-2026) | Keep the drawn-time settle check, and defer a separate agent GitHub identity | Active |
| [D-154](#d-154-the-community-desks-ed-heads-up-reads-a-narrow-scoped-list-11-november-2026) | The community desk's ED heads-up reads a narrow scoped list | Active |

---

Expand Down Expand Up @@ -1879,3 +1880,12 @@ is inferred.
1. **Phone release gate settle check:** the shared helper `waitForAccessibleReadiness` (`tests/helpers/ward-release-gate.ts`, changed in PR #1607) stays as it is. It counts a 100 ms tick against its 5-second settle budget only when the browser drew a frame in that tick, or while fonts load. A cold WebKit page held by D-137 can pause drawing after its first frame. Finite motion, or geometry that keeps changing while the browser draws, still fails after 5 seconds, and the test timeout still bounds a renderer that never recovers. The helper ignores infinite animations and the running Undo availability bar, so those can carry on while the page settles.
2. **A9-01, a separate GitHub identity for the agents:** deferred until after the pilot. Agents keep publishing as they do now.
- **Boundary:** synthetic data only. Changes no clinical value or rule, weakens no release gate check, and approves no merge, release or settings change.

## D-154. The community desk's ED heads-up reads a narrow scoped list (11 November 2026)

- **Date:** 11 November 2026 (`2026-11-11`). **Decided by:** Josh, choosing "Yes, in a follow-up PR" on a decision card in the Ward Flow project thread "Batch 1: ED and referral flow", then "Yes I approve" in the project chat (15:43 UTC) when asked again whether to build it. Numbered D-154 because D-153 belongs to the counts thread's docs PR and D-145 to D-152 belong to other threads.
- **Decision:** the item 46 "Your patients in ED" card (D-133, D-141) now works on a community team desk. That desk's movements hold only the journeys its own referrals touch, so the card was empty there. When `scopeWorkspace` cuts a community team desk's view, it now finds that team's open-episode patients who are in an ED in the whole workspace, and adds `communityEdArrivals` beside the scoped records. The card reads that list on a community desk and its own movements anywhere else.
- **Role access, kept narrow:** each item is the UMRN, the ED's name and the time the stay was recorded, keyed by the journey id, and nothing else. The ED journey and the person's record stay out of the desk's movements and patients. Only a community desk scoped to one team gets the list; every other desk's view has none.
- **Episode list:** the mock-up community episodes' team and status moved to `patients/community-episode-teams.ts`, the one place they are typed. The patient record mock-ups spread them in. That keeps the record mock-ups out of the shared access code.
- **Not covered:** shared mode. A shared community desk's copy comes from the server's `scopeWardFlowState`, which does not send this list yet, so the card stays empty there, as before. Sending it the way D-152 sends `diversionReturns` is a separate change.
- **Boundary:** synthetic data only. Display only. No event, stored field or signed clinical value changes, and this approves no merge or release.
12 changes: 6 additions & 6 deletions script-budget.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
{
"$comment": "Phone first-load script budgets in KB gzipped, checked by scripts/check-script-budget.mjs after the production build. Set on 16 October 2026 at the measured size plus about 10 KB, and lowered the same day after the rail and search left a phone's first load. Lower a budget when a route shrinks. Raise one only with a reason in the PR. Raise and lowering history: docs/script-budget-history.md up to 4 November 2026, then one file per PR in docs/script-budget-history/ (see its README), not here.",
"budgets": {
"/ward-flow": 587.7,
"/ward-flow": 588.3,
"/ward-flow/referrals": 594.3,
"/ward-flow/ed": 543.4,
"/ward-flow/ed": 543.9,
"/ward-flow/ward/rph-adult-secure": 625.4,
"/ward-flow/movements/WF-001": 618.7,
"/ward-flow/capacity": 665.7,
"/ward-flow/movements/WF-001": 619.0,
"/ward-flow/capacity": 665.9,
"/ward-flow/ed/arm-ed": 678.1,
"/ward-flow/bed-meeting": 600.1,
"/ward-flow/statistics": 640.5
"/ward-flow/bed-meeting": 600.7,
"/ward-flow/statistics": 641.0
}
}
18 changes: 7 additions & 11 deletions src/components/ward-management/home-team-heads-up-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,15 @@ import { useId, useMemo } from "react";
import { Hospital } from "lucide-react";

import { Card, CardBody, CardHead } from "@/components/wf";
import { recordExtrasFor } from "@/components/ward-management/patients/patient-record-extras";
import { communityEpisodeTeamsOf } from "@/components/ward-management/patients/community-episode-teams";
import { useCommunityEdArrivals } from "@/components/ward-management/shell/ward-access";
import { formatInstantWithDay, type Instant } from "@/components/ward-management/ward-clock";
import { useOptionalWardFlowSelect } from "@/components/ward-management/ward-flow-provider";
import type { CommunityTeam } from "./community/community-derivations";
import {
homeTeamHeadsUps,
homeTeamHeadsUpWho,
type CommunityEpisodesOf,
type UmrnOf,
} from "./community/home-team-heads-up";
import { homeTeamHeadsUps, homeTeamHeadsUpWho, type UmrnOf } from "./community/home-team-heads-up";
import styles from "./out-of-area/return-early-alerts.module.css";
import headsUpStyles from "./community/home-team-heads-up.module.css";

/** The patient record's community team episodes (the record's own list; mock-up data for now). */
const recordedEpisodes: CommunityEpisodesOf = (patientId) => recordExtrasFor(patientId)?.community;

/**
* Home team heads-up (item 46, D-133): this team's patients with an open episode who are in an
* emergency department now. The UMRN (D-141), the ED name and the time only: no name, reason,
Expand All @@ -33,8 +26,11 @@ export function HomeTeamHeadsUpCard({ team, now }: { team: CommunityTeam; now: I
const byId = new Map((patients ?? []).map((patient) => [patient.id as string, patient.umrn]));
return (patientId) => byId.get(patientId);
}, [patients]);
// D-154: a community team desk gets its list from its scoped view, since the ED stays are not
// in its movements. Any other desk (a statewide coordinator previewing the page) reads its own.
const projected = useCommunityEdArrivals();
if (!source) return null;
const items = homeTeamHeadsUps(source.movements, team, recordedEpisodes, now, umrnOf);
const items = projected ?? homeTeamHeadsUps(source.movements, team, communityEpisodeTeamsOf, now, umrnOf);
if (items.length === 0) return null;
return (
<Card aria-labelledby={headingId} data-testid="ward-community-heads-up">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { CommunityEpisode, CommunityEpisodesOf } from "@/components/ward-management/community/home-team-heads-up";

/**
* MOCK-UP ONLY, like `patient-record-extras.ts`. The team and the open or ended status of each
* invented community team episode, and nothing else. This is the only place they are typed:
* the record extras spread these entries into their fuller mock-ups.
*
* The list is kept apart from the extras so the community desk's scoped view (D-154) can test
* "open episode with this team" without loading the record mock-ups into every screen.
*/
export const COMMUNITY_EPISODE_TEAMS = {
"PT-001": [{ status: "active", team: "Midland Community Team" }],
"PT-003": [{ status: "ended", team: "Bentley Community Team" }],
"PT-036": [{ status: "active", team: "Midland Community Team" }],
"PT-002": [{ status: "active", team: "Midland Community Team" }],
} as const satisfies Record<string, readonly CommunityEpisode[]>;

const EPISODES: Readonly<Record<string, readonly CommunityEpisode[]>> = COMMUNITY_EPISODE_TEAMS;

/** The patient's community team episodes, team and status only, or `undefined` when none is recorded. */
export const communityEpisodeTeamsOf: CommunityEpisodesOf = (patientId) => EPISODES[patientId];
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
*/

import { siteByCode } from "../ward-sites";
import { COMMUNITY_EPISODE_TEAMS } from "./community-episode-teams";

/** Site names come from the data layer, never typed a second time here. */
const GRAYLANDS = siteByCode("GRY")?.name ?? "Graylands";
Expand Down Expand Up @@ -240,8 +241,7 @@ const PATIENT_RECORD_EXTRAS: Record<string, RecordExtras> = {
episodes: [],
community: [
{
status: "active",
team: "Midland Community Team",
...COMMUNITY_EPISODE_TEAMS["PT-001"][0],
program: "Case management",
since: "Jun 2026",
caseManager: { name: "Okafor, Lin", role: "Nurse, case manager", phone: "08 0000 3124" },
Expand Down Expand Up @@ -373,8 +373,7 @@ const PATIENT_RECORD_EXTRAS: Record<string, RecordExtras> = {
],
community: [
{
status: "ended",
team: "Bentley Community Team",
...COMMUNITY_EPISODE_TEAMS["PT-003"][0],
program: "Case management",
since: "Mar 2025",
ended: "Jan 2026",
Expand Down Expand Up @@ -520,8 +519,7 @@ const PATIENT_RECORD_EXTRAS: Record<string, RecordExtras> = {
],
community: [
{
status: "active",
team: "Midland Community Team",
...COMMUNITY_EPISODE_TEAMS["PT-036"][0],
program: "Case management",
since: "Apr 2024",
caseManager: { name: "Pereira, Dan", role: "Social worker, case manager", phone: "08 0000 3120" },
Expand Down Expand Up @@ -710,8 +708,7 @@ const PATIENT_RECORD_EXTRAS: Record<string, RecordExtras> = {
],
community: [
{
status: "active",
team: "Midland Community Team",
...COMMUNITY_EPISODE_TEAMS["PT-002"][0],
program: "Case management",
since: "Nov 2024",
caseManager: { name: "Tran, Minh", role: "Clinical nurse, case manager", phone: "08 0000 3114" },
Expand Down
18 changes: 17 additions & 1 deletion src/components/ward-management/shell/ward-access.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import {
smallNumberParts,
smallNumberText,
} from "@/components/ward-management/ward-access-scope";
import type { HomeTeamHeadsUp } from "@/components/ward-management/community/home-team-heads-up";
import type { AuditReview } from "@/components/ward-management/ward-audit";
import { isBreakGlassReviewId } from "@/components/ward-management/ward-break-glass-review-id";
import type { RecordRead, WardRecordActor } from "@/components/ward-management/ward-discharge-records";
Expand Down Expand Up @@ -153,6 +154,17 @@ export function useIsDatesOnlyStay(): (admissionId: string | undefined) => boole
return useCallback((admissionId) => admissionId !== undefined && ids.has(admissionId), [ids]);
}

/** D-154: a community desk's own patients in ED now, or `undefined` on any other desk. */
const CommunityEdArrivalsContext = createContext<readonly HomeTeamHeadsUp[] | undefined>(undefined);

/**
* The community team desk's patients with an open episode who are in an ED now (UMRN, ED name and
* time only, D-154), or `undefined` when this desk is not a community team's.
*/
export function useCommunityEdArrivals(): readonly HomeTeamHeadsUp[] | undefined {
return useContext(CommunityEdArrivalsContext);
}

/**
* Whether a movement or stay belongs to a person this desk sees only through break glass. The
* patient link is read here, inside the archived D-14 allowlist, so callers never touch it themselves.
Expand Down Expand Up @@ -328,6 +340,7 @@ export function WardAccessScope({ children }: { children: ReactNode }) {
scoped: WardFlowContextValue;
glass: ReadonlySet<string>;
datesOnly: ReadonlySet<string>;
edArrivals?: readonly HomeTeamHeadsUp[];
}>(() => {
if (accessIsWholeNetwork(access) && activeGrants.length === 0)
return { scoped: context, glass: NO_IDS, datesOnly: NO_IDS };
Expand Down Expand Up @@ -473,6 +486,7 @@ export function WardAccessScope({ children }: { children: ReactNode }) {
return {
glass: breakGlassPatientIds,
datesOnly: scoped.datesOnlyAdmissionIds,
edArrivals: baseScoped.communityEdArrivals,
scoped: {
...rest,
dispatch,
Expand Down Expand Up @@ -565,7 +579,9 @@ export function WardAccessScope({ children }: { children: ReactNode }) {
<WardFlowOverride value={steadyScoped}>
<BreakGlassContext.Provider value={value.glass}>
<DatesOnlyStayContext.Provider value={value.datesOnly}>
<DowntimePackBoundary identity={packIdentity}>{children}</DowntimePackBoundary>
<CommunityEdArrivalsContext.Provider value={value.edArrivals}>
<DowntimePackBoundary identity={packIdentity}>{children}</DowntimePackBoundary>
</CommunityEdArrivalsContext.Provider>
</DatesOnlyStayContext.Provider>
</BreakGlassContext.Provider>
</WardFlowOverride>
Expand Down
23 changes: 23 additions & 0 deletions src/components/ward-management/ward-access-scope.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import { communityDischargeCriteria } from "./ward-discharge-criteria";
*/
import { communityTeamOptions } from "@/components/ward-management/referrals/referral-destination-options";
import { communityTeamSlug } from "@/components/ward-management/community/community-derivations";
import { homeTeamHeadsUps, type HomeTeamHeadsUp } from "@/components/ward-management/community/home-team-heads-up";
import { communityEpisodeTeamsOf } from "@/components/ward-management/patients/community-episode-teams";
import { CLINICAL_VALUES } from "@/components/ward-management/ward-clinical-values";
import { withWardReadmission } from "@/components/ward-management/ward-readmission-copy";
import type { Instant } from "@/components/ward-management/ward-clock";
Expand Down Expand Up @@ -90,6 +92,12 @@ export type ScopedWorkspace<T extends ScopableWorkspace> = T & {
readonly breakGlassPatientIds: ReadonlySet<string>;
/** Stays sent as ward and dates only, so nothing hung off them (leave, a release) goes too. */
readonly datesOnlyAdmissionIds: ReadonlySet<string>;
/**
* D-154: a community team desk's own patients in an emergency department now, for the Home
* team heads-up. Present only on a community desk. Each item is the UMRN, the ED's name and the
* time, and nothing else; the ED stay itself stays out of `movements`.
*/
readonly communityEdArrivals?: readonly HomeTeamHeadsUp[];
};

function activeGrantIds(grants: readonly BreakGlassGrant[] | undefined, now: Instant): Set<string> {
Expand Down Expand Up @@ -924,6 +932,7 @@ export function scopeWorkspace<T extends ScopableWorkspace>(
let referrals: Referral[] = [];
let admissions: Admission[] = [];
const datesOnlyAdmissionIds = new Set<string>();
let communityEdArrivals: HomeTeamHeadsUp[] | undefined;
let anonymise = false;
let officer = false;

Expand Down Expand Up @@ -1164,6 +1173,19 @@ export function scopeWorkspace<T extends ScopableWorkspace>(
.map((movement) => movement.id),
);
const movementIds = new Set(movements.map((movement) => movement.id));
// D-154 (Josh, 11 Nov 2026): the team's patients with an open episode who are in an ED now,
// as UMRN, ED name and time only. Read from the whole workspace, so the ED stay itself is
// never sent.
if (teamKey !== "" && scope.kind === "team") {
const umrns = new Map(workspace.patients.map((patient) => [patient.id as string, patient.umrn]));
communityEdArrivals = homeTeamHeadsUps(
workspace.movements,
{ id: communityTeamSlug(scope.teamName), name: scope.teamName },
communityEpisodeTeamsOf,
now,
(patientId) => umrns.get(patientId),
);
}
admissions = workspace.admissions.flatMap((admission) => {
if (
(admission.referralId !== null && askedOfTeam.has(admission.referralId)) ||
Expand Down Expand Up @@ -1328,6 +1350,7 @@ export function scopeWorkspace<T extends ScopableWorkspace>(
notices,
breakGlassPatientIds: visibleOnlyByGrant,
datesOnlyAdmissionIds,
...(communityEdArrivals ? { communityEdArrivals } : {}),
};
}

Expand Down
Loading
Loading