OR Feature Suite
Room Status / Turnover / CSSD / Incident / Block Utilization shipped as one integrated OR suite with canonical record linkage.
Five OR features shipped as one integrated suite over a 30-iteration loop on 2026-05-14 → 2026-05-15. This doc is the single reference for the data model, the linkage to the canonical OR record, the cross-feature triggers, and the operator runbook.
Why these five together
These features had been requested as separate items:
Room Status Board · Turnover/Cleaning Workflow · CSSD Integration · Incident/Near-Miss Reporting · Surgeon Block Utilization Dashboard
They were built as a suite — not as five independent miniapps — because they share infrastructure (rooms, OR record, procedure_request_id, encounter_id) and they cross-fire (a CSSD recall produces an incident draft; a turnover gates a procedure_start). Shipping them in isolation would have produced 5 disconnected surfaces; shipping them together produced one coherent perioperative QM layer.
Canonical OR record
The canonical OR record in this system is the MongoDB OperatingRoomRequest entity, mirrored into Supabase as or_state_cache. One row per OR request, carrying:
or_state_cache (
id uuid PRIMARY KEY,
mongo_ref text UNIQUE, -- OperatingRoomRequest._id
encounter_id uuid NOT NULL, -- joins to encounter_journey_cache
patient_id uuid,
or_room_id uuid, -- legacy uuid (often per-env)
or_room_code text, -- 'OR-1' etc — durable join key (NEW)
surgeon_id text,
anesthesiologist_id text,
status text,
scheduled_at timestamptz,
procedure_started_at timestamptz,
procedure_finished_at timestamptz,
...
)
Every new feature table threads back to this row via or_state_id + encounter_id + patient_id so the 5 features share the same patient/case context. The linkage is bi-directional and lazy:
forward auto-link (20260515a)
┌───────────────────────────────────┐
│ ▼
or_incident_drafts ◀── procedure_request_id ── or_state_cache.mongo_ref
or_turnover_instances │
cssd_tray_case_usage │
▼
reverse fan-out (20260515f)
on or_state_cache write:
propagate or_state_id +
encounter_id + or_room_code
down to procedure_day_queue
+ 3 child tables
This closes the projector-vs-orchestrator temporal race: the procedure-day-queue projector emits queue rows from procedureRequestEvent events that arrive BEFORE the orchestrator finishes writing the OR record. Without the reverse trigger those rows stayed un-linked.
Migration order
20260514g_or_room_phase_board.sql Foundation: or_rooms + runtime state + v_or_room_status_board
20260514h_or_turnover_workflow.sql Templates + instances + open/complete/signoff RPCs
20260514i_cssd_tray_tracking.sql Trays + autoclave + lots + recall RPC + views
20260514j_or_incident_autodraft.sql Drafts + trigger on or_intraop_counts mismatch
20260514k_surgeon_block_utilization.sql Allocations + 3 utilization views
20260515a_or_record_linkage.sql Connects all 5 to or_state_cache (forward + auto-link triggers)
20260515b_or_turnover_procedure_start_gate.sql Wires turnover→blocked_actions[]
20260515c_or_feature_suite_demo_seed.sql One cohesive demo case threading through everything
20260515d_cssd_recall_to_incident.sql Recall fans out incident drafts per affected case
20260515e_or_room_warning_chips.sql v_or_room_warnings — per-room aggregated warning counts
20260515f_or_state_cache_reverse_link.sql Reverse fan-out on or_state_cache write
Apply in order. Each is idempotent.
Tables introduced
| Table | Rows seeded | Purpose |
|---|---|---|
or_rooms |
8 (OR-1…6, CYSTO-1, MINOR-1) | Physical room registry. code is durable; legacy_room_uuid bridges to existing or_state_cache.or_room_id. |
or_room_runtime_state |
8 | Per-room current phase + active/next procedure refs |
or_room_phase_audit_log |
0 | Append-only phase transition trail |
or_turnover_templates |
3 (default / contaminated / cardiac) | Versioned checklist library |
or_turnover_template_items |
23 | Line items per template, bilingual TH/EN, categorized |
or_turnover_instances |
1 demo (OR-3) | Per-case execution row |
or_turnover_instance_items |
~10 | Snapshot of template items, statuses |
cssd_trays |
6 (Major Lap / Minor / Cardiac / Ortho / OB-GYN / ENT) | Tray master inventory |
cssd_tray_instruments |
8 (Minor tray itemized) | For count audit |
cssd_autoclave_cycles |
4 demo (3 pass, 1 fail) | Sterilizer cycle log |
cssd_tray_lots |
~6 | Per-cycle tray instance, status: available/in_use/recalled/expired |
cssd_tray_case_usage |
1 demo | Tray ↔ case linkage |
cssd_recall_events |
0 | Recall audit log + counters |
or_incident_drafts |
1 demo (count_discrepancy) | Auto-spawned near-miss drafts |
or_incident_attachments |
0 | Photos / x-rays / count sheets |
surgeon_block_allocations |
~250 (8 surgeons × 5 weekdays × 38 days) | Block reservations |
surgeon_block_utilization_settings |
1 (default facility) | Target / underuse / overrun thresholds |
Views
| View | Drives |
|---|---|
v_or_room_status_board |
Kiosk status display (room phase + active procedure + next-up + encounter_id) |
v_or_room_warnings |
Status-board chip overlay (open incidents / cssd recalls / turnover blocking) |
v_or_room_ready_state |
Read-side gate signal (procedure-start can refuse based on this) |
v_cssd_affected_cases |
CSSD recall workflow — every affected case with patient HN/name/procedure/encounter_id |
v_cssd_pre_case_check |
Pre-case tray verification UI |
v_or_open_incidents |
OR Incident Inbox listing |
v_or_linkage_health |
Ops sanity check — linked vs unlinked row counts per child table |
v_surgeon_block_utilization |
Per-block live calc |
v_surgeon_block_utilization_daily |
Per-surgeon per-day rollup |
v_surgeon_block_utilization_monthly |
Per-surgeon per-month rollup |
RPCs
| RPC | Purpose | Idempotent |
|---|---|---|
or_room_phase_transition(room_id, to_phase, procedure_id?, actor?, reason?) |
Atomic phase change + audit + ready-flag toggle | yes |
or_turnover_open(room_id, prior_proc?, next_proc?, wound_class?, surgical_type?) |
Spawn instance via best-match template lookup | yes (returns existing open instance) |
or_turnover_complete_item(item_id, actor?, status='done', notes?) |
Tick / skip / block an item | yes |
or_turnover_signoff(instance_id, actor, role, secondary_actor?, secondary_role?) |
Attests ready-state; refuses if mandatory items remain | no (state-changing) |
or_turnover_set_next_case(instance_id, next_proc_id) |
Re-target the gate to a different next case | yes |
cssd_record_autoclave_cycle(cycle_code, …, tray_codes[]) |
Records cycle + auto-spawns lots + auto-fail on BI/CI/BD | yes |
cssd_link_tray_to_case(lot_number, procedure_id, room_id, actor) |
Pre-case tray verification + link | no |
cssd_recall_cycle(cycle_id, reason, detail, actor) |
Marks lot recalled + counts affected cases + fires fan-out trigger | no |
| `or_incident_resolve(incident_id, ‘dismiss’ | ‘escalate’, actor, reason?)` | Closes a draft as false alarm or escalates for review |
or_incident_sign(incident_id, actor, role, external_id?) |
Signs & submits the draft (in-app or external) | no |
link_or_record(case_id) |
Resolves (or_state_id, encounter_id, patient_id, or_room_code, or_room_id) for a procedure_request_id / UUID | yes (read-only) |
surgeon_block_recompute() |
Placeholder for future materialized view refresh | yes |
Triggers
| Trigger | When | What |
|---|---|---|
trg_or_count_to_incident |
After insert/update on or_intraop_counts where total_mismatch > 0 |
Auto-spawns / updates a count_discrepancy incident draft. Also auto-dismisses if mismatch returns to 0 before human action. |
trg_or_turnover_item_count |
After insert/update/delete on or_turnover_instance_items |
Recomputes items_total/items_completed/items_mandatory_remaining on the parent instance. |
trg_or_turnover_sync_gate |
After insert/update on or_turnover_instances.status or items_mandatory_remaining |
Appends/removes 'fire:procedure_start' from the next case’s procedure_day_queue.blocked_actions[]. |
trg_or_incident_drafts_autolink / trg_or_turnover_instances_autolink / trg_cssd_tray_case_usage_autolink |
Before insert/update on child tables | Fill or_state_id / encounter_id / patient_id from or_state_cache if missing. |
trg_or_state_cache_propagate_link (BEFORE) |
Before insert/update on or_state_cache.or_room_id or or_room_code |
Backfill or_room_code from or_rooms.legacy_room_uuid if caller didn’t supply it. |
trg_or_state_cache_fanout_link (AFTER) |
After insert/update on or_state_cache |
Propagate trio + or_room_code down into matching procedure_day_queue + 3 child tables. |
trg_cssd_recall_fanout_incidents |
After insert/update on cssd_recall_events where lots_affected > 0 |
Walks v_cssd_affected_cases and upserts one pending_review incident draft per affected (case, tray_lot). |
Cross-feature interactions
count mismatch on intraop counts
│
▼
or_incident_drafts (draft)
▲
│ also fans out per case
cssd_recall_events Status Board shows
(BI/CI/BD failure) warning chips per room:
│ - !N (open incidents)
▼ - CSSD N (recalled lots)
cssd_tray_lots → recalled - TURNOVER N (mand left)
│
▼
v_cssd_affected_cases ──▶ patient follow-up
turnover instance open
│
▼
blocked_actions[] += 'fire:procedure_start'
│
▼
record_procedure_event refuses the start
│
▼
signoff clears the gate
Frontend surfaces
| Route | Sandbox key | Drives from |
|---|---|---|
/operating-room/status-board |
OrRoomStatusBoard |
v_or_room_status_board + v_or_room_warnings |
/operating-room/turnover?roomCode=OR-3 |
OrTurnoverChecklist |
or_turnover_instances + items + template (realtime) |
/operating-room/cssd |
OrCssdConsole |
cssd_trays + cssd_autoclave_cycles + cssd_recall_events + v_cssd_affected_cases |
/operating-room/incidents |
OrIncidentInbox |
v_or_open_incidents + per-case or_incident_drafts |
/operating-room/block-utilization |
OrBlockUtilization |
v_surgeon_block_utilization + daily / monthly views |
/operating-room (landing) |
— | Live count chips on the 5 tiles |
Deep links from cards
| Card | Click target |
|---|---|
| Status board card (with active case) | /encounter/<active_encounter_id> |
| Status board card (turnover/cleaning, no active case) | /operating-room/turnover?roomCode=<code> |
| CSSD recall row (when linked) | ↗ /encounter/<encounter_id> |
| Incident card patient strip | ↗ /encounter/<encounter_id> |
| Block-util leaderboard row | (drill-down to be added) |
Demo seed entry point
After applying migrations, the seed in 20260515c_or_feature_suite_demo_seed.sql provisions one cohesive demo case (DEMO-OR-SUITE-001) threaded through:
or_state_cacherow → encounter11111111-2222-3333-4444-555555550001procedure_day_queuekeyed on the sameprocedure_request_idor_room_runtime_statefor OR-1 set to active w/ this caseor_turnover_instancesfor OR-3 withnext_procedure_request_id = DEMO-OR-SUITE-001(visualises the gate)cssd_tray_case_usagelinking a passed lot of Major Lap to the caseor_intraop_countswithtotal_mismatch = 1(auto-spawns the incident draft via trigger)surgeon_block_allocationsrow for today’s morning OR-1 block (Dr Tanaka)
UAT viewers can hit any of the 5 routes and see the SAME patient + case.
Operator runbook
Initial deploy (per region):
# 1. Apply 11 migrations in order from infrastructure/medbase/migrations/
# in the Supabase SQL editor (or psql -f). Each is idempotent.
# 2. The cohesive demo seed runs automatically with 20260515c.
# 3. Verify linkage:
SELECT * FROM v_or_linkage_health;
# All rows should have linked_rows = total_rows after the demo seed runs.
# 4. Smoke test:
# - Open /operating-room (landing) — 5 NEW tiles + live count chips
# - Open /operating-room/status-board — 8 room cards, OR-1 in INCISION
# with active case chip linking to encounter
# - Open /operating-room/turnover?roomCode=OR-3 — turnover in progress
# - Open /operating-room/cssd — 6 trays, 4 cycles (one in FAIL state)
# - Click "Recall" on the FAIL row → confirm → switch to Recalls tab
# → see the demo case listed under affected cases + verify it also
# appears in /operating-room/incidents
# - Open /operating-room/block-utilization — Dr Tanaka shows some
# utilization for today; OPEN blocks panel renders separately
Linkage health monitoring:
-- Run periodically. unlinked_rows should be near 0 except for legacy
-- cases without a corresponding or_state_cache row.
SELECT * FROM v_or_linkage_health;
Manual gate clearance (if a turnover instance is stuck):
-- 1. Identify the open instance
SELECT id, or_room_id, items_mandatory_remaining, status
FROM or_turnover_instances
WHERE status IN ('pending','in_progress');
-- 2a. Force sign-off (after manual ready-state verification)
SELECT or_turnover_signoff('<instance_id>', 'manual:supervisor', 'in_charge', NULL, NULL);
-- 2b. Or directly clear the gate (last resort, leaves audit gap)
SELECT _or_turnover_clear_gate('<next_procedure_request_id>');
Known limitations (post-demo follow-ups)
- Backend integration: the
record_procedure_eventRPC readsblocked_actions[]but the NestJS gateway hasn’t been wired to surface the gate reason to the OR worklist UI as a user-readable string yet. Today the frontend disables the button and shows a generic tooltip. - CSSD pre-case verification widget: planned for a separate iteration. Today the link is one-way (tray → case at the moment of use); a barcode-scan widget in the OR sign-in flow would be the natural place.
- Block-utilization drill-down: per-surgeon detail page (case list inside a block) is not yet wired from the leaderboard.
- Open-block reallocation: identifying surgeons who repeatedly under-use blocks doesn’t yet trigger an admin workflow to reallocate them. The insights row surfaces the candidates; reassignment is manual.
- Incident → external system submission:
or_incident_signaccepts anexternal_idfield but the bridge to the administration incidentReport NestJS service isn’t built. Today drafts stop at status=‘signed’.
File index
infrastructure/medbase/migrations/
20260514g_or_room_phase_board.sql
20260514h_or_turnover_workflow.sql
20260514i_cssd_tray_tracking.sql
20260514j_or_incident_autodraft.sql
20260514k_surgeon_block_utilization.sql
20260515a_or_record_linkage.sql
20260515b_or_turnover_procedure_start_gate.sql
20260515c_or_feature_suite_demo_seed.sql
20260515d_cssd_recall_to_incident.sql
20260515e_or_room_warning_chips.sql
20260515f_or_state_cache_reverse_link.sql
web/sandbox/targets/
OrRoomStatusBoardTarget.tsx
OrTurnoverChecklistTarget.tsx
OrCssdConsoleTarget.tsx
OrIncidentInboxTarget.tsx
OrBlockUtilizationTarget.tsx
web/src/containers/operating-room/
status-board/index.tsx
turnover/index.tsx
cssd/index.tsx
incidents/index.tsx
block-utilization/index.tsx
intraop-count/index.tsx (modified — embeds incident banner)
web/src/containers/operating-room-landing/
page.tsx (modified — 5 NEW tiles + live count chips)
web/src/routes/OperatingRoomRoutes.tsx (modified — 5 new lazy routes)
Authors
Built across 30 iterations on 2026-05-14 → 2026-05-15 by Claude Opus 4.7 (1M context) in collaboration with the operator.