Central Order Inspector - Workspace
Integrating the central order inspector into the clinic workspace.
Date: 2026-05-25 Commits:
a1bef0e5d— feat(order-system): add OrderSystemPanel to clinic workspace via DynamicContentRenderer- (this session, uncommitted) — swap OrderSystemPanel → CentralOrderInspector in workspace tab
Status: ✅ TypeScript-clean (0 errors), ✅ visually verified via headless Playwright
TL;DR
The Central Order Panel clinic workspace tab now renders the full CentralOrderInspector (the same 18-entity inspector that lives at /?target=CentralOrderSystemInspector in the standalone sandbox) inside any patient profile, with the patient’s _id and encounter ID auto-flowed through props.
Reachable in production at:
- Clinic workspace tab:
modules.CentralOrderPanel→ mounted viaDynamicContentRenderer - Standalone sandbox:
http://localhost:5179/?target=OrderSystemPanelor?target=CentralOrderSystemInspector
Why we ended up here (the journey)
Phase 1 — MedbaseExtensionPanel wiring (pre-session, complete)
Wired the MedbaseExtensionPanel into the central-order-inspector miniapp so it surfaces voice ordering, contract overrides, vocabulary lookup, and stockout alerts.
Phase 2 — Supabase migration deployment (pre-session, complete)
Deployed read-model migrations (order_bus, department_queues, encounter_journey_cache updates) to the PH demo Supabase project.
Phase 3 — Workspace integration (this session)
Initial attempt: Built a slim OrderSystemPanel component (~695 LOC) — a compact order table + “New Order” dialog — and wired it as the workspace tab.
Course correction: User feedback after visual test: “this looks like a worse off version from the previous inspector — what is this for?”
Final decision: Swap the workspace tab to render the full CentralOrderInspector directly. The slim panel is kept in the package barrel as an unused export for future use cases (e.g. mobile, sidebar widget), but no consumers reference it.
File changes
Modified
-
web/packages/miniapps/central-order-inspector/CentralOrderInspector.tsxAddedCentralOrderSystemInspectorPropsinterface with optionalpatientId?andencounterId?. When passed, they initializeliveStateinstead of falling back toDEFAULT_PATIENT_IDand the localStorage-cached encounter.export interface CentralOrderSystemInspectorProps { patientId?: string; encounterId?: string; } export default function CentralOrderSystemInspector( props: CentralOrderSystemInspectorProps = {}, ) { // ... const [liveState, setLiveState] = React.useState<LiveState>(() => ({ patientId: props.patientId || DEFAULT_PATIENT_ID, encounterId: props.encounterId || getCachedEncounter(), token: getCachedToken(), results: {}, })); -
web/src/common/components/shared-engine/dynamic-core/engine/DynamicContentRenderer.tsx- Import swap:
OrderSystemPanel→CentralOrderInspector - Switch case
CENTRAL_ORDER_PANEL/'CentralOrderPanel'now renders the inspector with patient/encounter pulled from the workspace context:case DynamicCoreApp.CENTRAL_ORDER_PANEL: case 'CentralOrderPanel': { const copPatientData = profile || patientRefForInternalLogic; const copEncounter = lastEncounter || patientRefForInternalLogic?.lastEncounter; content = ( <CentralOrderInspector patientId={copPatientData?._id} encounterId={copEncounter?._id || copEncounter?.id || encounterRef || undefined} /> ); break; }
- Import swap:
-
web/src/setup/dynamic/DynamicCoreApp.tsAdded enum entry:// Central Order Panel — full inspector + live tests + realtime read-models CENTRAL_ORDER_PANEL = 'modules.CentralOrderPanel', -
web/src/locales/en/coreApps.json+web/src/locales/th/coreApps.jsonAdded bilingual labels:- en:
"CentralOrderPanel": "Central Order Panel" - th:
"CentralOrderPanel": "ระบบคำสั่งการรักษาส่วนกลาง"
- en:
-
web/packages/miniapps/central-order-inspector/index.tsBarrel exports for both surfaces:export { default, default as CentralOrderInspector } from './CentralOrderInspector'; export type { CentralOrderSystemInspectorProps } from './CentralOrderInspector'; export { OrderSystemPanel } from './OrderSystemPanel'; // kept for future use export type { OrderSystemPanelProps } from './OrderSystemPanel'; -
web/sandbox/registry.tsTwo entries:OrderSystemPanel(sandbox alias for the workspace render path) +CentralOrderSystemInspector(direct inspector access).
New
web/sandbox/targets/OrderSystemPanelTarget.tsx— WrapsCentralOrderInspectorwith sandbox demo IDs so the URL the workspace generates renders correctly.
Architecture notes
Component contract
CentralOrderInspector accepts:
├─ patientId?: string (defaults to DEFAULT_PATIENT_ID — '[DEMO] ใจดี รักไทย')
└─ encounterId?: string (defaults to getCachedEncounter() from localStorage)
Both props are read-once at mount (initial state). The “Find any active encounter” and “Re-resolve for patient” buttons inside the inspector can still mutate liveState afterwards — the props are only the seed.
Data flow (workspace path)
Clinic workspace tab opens
│
▼
DynamicContentRenderer receives module: 'modules.CentralOrderPanel'
│
▼
Switch case extracts from workspace context:
- patientData (from `profile` or `patientRefForInternalLogic`)
- encounter (from `lastEncounter` or patientRefForInternalLogic?.lastEncounter)
│
▼
Renders <CentralOrderInspector patientId={...} encounterId={...} />
│
▼
Inspector mounts → liveState seeded with workspace context → live tests
hit `/api/...` proxy → PH demo backend → response captured → Supabase
realtime subscriptions fire for the encounter → patient journey + read
models + recent runs update live.
Dual data source handling
Per web/CLAUDE.md “DUAL DATA SOURCE RULE”:
copPatientData?._id— supports MongoDB (REST API) shapecopEncounter?._id || copEncounter?.id— supports both MongoDB and Supabase- Falls back to
encounterRef(workspace’s encounter URL parameter) orundefined
The inspector’s getCachedEncounter() only kicks in when the workspace context provides no encounter.
What the inspector exposes (in the workspace tab)
Left sidebar — 18 entities
Order Request (central), Medication Request, Lab Request, Imaging Request, Procedure Request, Consult Request, Blood Request, Blood Product Request, Nutrition Request, EMS Request, Operating Room Request, Lab Request (alt), Pathology Request, Nuclear Medicine Request, Radiation Oncology Request, plus parent/child relations (8 with parent, 5 spawn children).
Live Test panel
- Patient ID / Encounter ID inputs (seeded from workspace)
- “Find any active encounter” + “Re-resolve for patient” buttons
- “Login (sa)” → captures dev JWT token
- "Run live test → " — POSTs the catalog payload through
/api/(Vercel rewrite → ALB → gateway → service) - Captures HTTP status, response body, created entity ID
Real-time read-model section (4 panels)
encounter_journey_cache(manifest)order_bus(cross-department stream)department_queues(operational)hospital_events(raw event stream)
All four subscribe via Supabase realtime channels filtered by encounter ID — rows flash green on insert as the orchestrator processes new orders.
Patient Journey
Chronological merge of hospital_events + order_bus + department_queues for the current encounter — visualizes the full lifecycle from REST POST to read-model materialization.
Recent Runs (cross-session)
Persistent log of every live test ever run, stored in a Supabase table — survives page reloads and shows test history across sessions.
Six bottom tabs
- Pipeline Docs (⓪–⑨) — interactive walkthrough of the 10 layers
- Output — last live test’s full HTTP response + created Mongo doc + Supabase rows
- Order Contract (3-layer) — system/hospital/department contract resolution viewer
- Order Views (6 modes) — table / kanban / timeline / split / pivot / form
- Medbase (2 tools) — voice-order pipeline + contract overrides
- AI Voice — speech-to-order with intent classification
Testing & verification
TypeScript
node ./node_modules/typescript/bin/tsc --noEmit --skipLibCheck
# → 0 errors
Playwright (headless, clean cache)
NODE_PATH=.../playwright-core@1.55.0/node_modules node -e "
const { chromium } = require('playwright-core');
...
await p.goto('http://localhost:5179/?target=OrderSystemPanel');
...
"
Result: Inspector renders with 18 entities visible, live test panel showing sandbox-patient-001 / sandbox-encounter-001 (props flowed through), realtime panels mounted.
Screenshots captured at:
/tmp/inspector-workspace.png(final inspector render)/tmp/osp-pw.png(earlier slim-panel render, pre-swap)/tmp/osp-dialog.png(earlier slim-panel “New Order” dialog)
Why we needed Playwright (not Chrome)
Chrome’s HTTP cache held a 197-entry version of sandbox/registry.ts from before recent entries were added. ?t= cache busting didn’t bust Chrome’s per-URL cache, and hard refresh (Cmd+Shift+R) didn’t either. curl consistently saw the fresh file; only Chrome was stuck. Playwright launches a fresh Chromium with empty storage, so no stale cache.
Workaround for any developer hitting this:
- Incognito / private window
- DevTools (F12) → Network → “Disable cache” → reload
- DevTools → Application → Storage → “Clear site data” → reload
Deployment status
| Surface | Status |
|---|---|
| TypeScript compilation | ✅ 0 errors |
| Vite dev server transform | ✅ Clean |
Commit on main |
⏳ Pending (this session’s swap is uncommitted; previous OrderSystemPanel commit a1bef0e5d is on main and 4 commits behind HEAD) |
| Vercel auto-deploy | ⏳ Will deploy on next push to main |
| PH demo backend | ⚠ Gateway port 3001 not listening on EC2 (pre-existing infra issue, blocks live-test auth) |
Next steps (if/when prioritized)
- Commit + push the inspector swap so Vercel picks it up
- Fix PH demo gateway so live tests work end-to-end on
his-thailand.vercel.app - Workspace layout polish — the inspector is wide; verify it fits in the workspace tab panel on smaller viewports (consider responsive collapsing of the entity sidebar)
- Persist patient context — when the user navigates between patients, the inspector currently resets only its
liveStateinitial value. If the user opens patient A’s tab, runs a test, then switches to patient B’s profile, the inspector for A is unmounted and B’s inspector is freshly seeded. Verify this is the desired behavior or add a key-on-patientId remount. - Permissions gating — the inspector includes a “Login (sa)” button that captures a JWT. In production, this should be hidden when the user is already authenticated as their own role (no need for sa login).
Cross-references
- Inspector source:
web/packages/miniapps/central-order-inspector/CentralOrderInspector.tsx - Slim panel (unused):
web/packages/miniapps/central-order-inspector/OrderSystemPanel.tsx - Catalog of 18 entities:
web/packages/miniapps/central-order-inspector/catalog.ts - Live-test engine:
web/packages/miniapps/central-order-inspector/liveTest.ts - Sandbox target:
web/sandbox/targets/OrderSystemPanelTarget.tsx,web/sandbox/targets/CentralOrderSystemInspector/index.tsx - Workspace integration:
web/src/common/components/shared-engine/dynamic-core/engine/DynamicContentRenderer.tsx(search forCENTRAL_ORDER_PANEL) - Enum:
web/src/setup/dynamic/DynamicCoreApp.ts - Locales:
web/src/locales/{en,th}/coreApps.json→tabs.CentralOrderPanel,modules.CentralOrderPanel