-
Notifications
You must be signed in to change notification settings - Fork 1
Web App
This page maps the staff EMR application: every route, how the shell and command palette work, and exactly what mock mode does. It is for anyone working in apps/web.
The 26-route application described here is on the
feat/emr-appbranch and has not merged todev. Ondev,apps/webis still the landing skeleton with pillar cards.
Next.js 16 App Router, React 19. Every route follows the same two-file pattern:
-
page.tsxis a server component that ownsmetadataand nothing else. -
<Name>Screen.tsxis a'use client'component that owns the screen.
The split exists because @openrunic/ui components use React state, which the server-component condition does not provide. Route files contain no logic beyond awaiting params.
There are no route handlers, no middleware.ts, and no route groups. layout.tsx exists only at the root.
| Route | What it does |
|---|---|
/ |
Public landing page. Three pillar cards and a compliance footer. The only route that does not use the app shell. |
/schedule |
The day view. Provider columns with a live "now" rule, a day pager, a right rail, an available-slot finder, a booking modal, and check-in. |
/schedule/flow-board |
The patient flow board. Five status columns with counts, two clocks per patient, one-click status advance, room assignment, and filters for provider, room, and delayed-only. |
/patients |
Patient search. One dominant input searching given, family, preferred name, and MRN, plus four saved views phrased as questions. The zero-result state offers registration. |
/patients/new |
Registration. Four required fields. Duplicate detection runs while the name is typed, and a strong match blocks the save until the user states an override rather than clicking through a second time. |
/patients/[id]/insurance |
Coverage and eligibility. One card per coverage in priority order, reordered with buttons rather than drag. Eligibility verification has four designed outcomes including a payer outage, each with its own tone. |
| Route | What it does |
|---|---|
/patients/[id] |
Chart home. A persistent, undismissable patient context rail, six tabs each carrying a count, and actions to print the summary or open the visit note. |
/encounters/[id] |
The visit workspace. A block-editor note with slash commands, a signature block, content hashing, and draft handling, with the chart rail always present. |
/inbox |
The typed inbox. Five streams, an assignment filter, and rows that finish their disposition inline. Every row carries an SLA state; the list sorts overdue first, then due soonest. |
| Route | What it does |
|---|---|
/orders |
The order ledger. Eight columns, a status filter, order age, and a flag for a transmitted-but-unacknowledged order with a retry. |
/orders/new |
The order composer, the largest screen in the app. The catalogue is ranked against the patient's problems before typing starts, the picker never loses focus, and warnings are tiered so a critical alert holds the signature until a reason is given. Pend and sign are separate buttons. |
/results |
The results sign-off queue, abnormal first. Values are rendered against reference ranges in words. Bulk signing is available, and critical values are excluded from the batch. |
| Route | What it does |
|---|---|
/billing |
The front door. Four headline numbers, links to the five workbenches, and an aged-balance card. It holds no state and offers no verbs of its own. |
/billing/charges |
The fee sheet. Procedure panels stay on screen, diagnosis justification uses pointer letters, and the scrub panel prevents marking a sheet ready with an unjustified line. Removed lines are struck through and restorable. |
/billing/claims |
The claim workbench, with a detail drawer. Status chips are the primary navigation. Rows blocked by the scrubber are unselectable and say why. There are no files anywhere; acknowledgements are events in the claim's own history. |
/billing/remittance |
Remittance posting. Auto-post is the default, and non-matching lines are lifted into an exception queue above the ledger with their disposition in the row. No file upload anywhere. |
/billing/statements |
Statements and ageing. Ageing buckets, dunning stage, text-to-pay and card-on-file. The preview drawer is mandatory before a run, because the escalation wording is only visible there. |
/billing/payments |
Checkout and allocation on one desk. The remainder is the most prominent number, and the payment button stays disabled until the allocation balances. |
| Route | What it does |
|---|---|
/reports |
A practice dashboard plus a report shell. Stat tiles tint only when a threshold is breached, and every number links into the workbench that owns it. Below is a filterable visit report with pinned totals and CSV export. |
/admin |
The admin hub. A list of six cards, each heading a real link. Not a dashboard. |
/admin/users |
Users and roles. A permission matrix over roles and capabilities, plain-language role summaries, and per-user grants shown as labelled exceptions. Nobody is deleted; the confirm dialog deactivates. |
/admin/facilities |
Facilities. Identity, billing attributes such as place-of-service code and NPI, the hours grid the slot engine reads, and the rooms the flow board reads, all on one screen. |
/admin/forms |
The form builder. Three panes at wide widths, preview always one toggle away, published versions immutable and saying so. See Form engine. |
/admin/audit |
The audit viewer. Deliberately has no edit control of any kind. Filters by action, actor, purpose of use, and date; the detail drawer renders the hash chain so tamper evidence is visible. CSV export. |
/admin/integrations |
Partner seams: prescribing, clearinghouse, labs, payments, fax, text, and video. A banner lists broken seams, test-connection is a first-class action, and credentials appear as a secret reference and never as a value. |
/admin/developer |
The developer platform in three tabs: API keys shown once and revocable rather than deletable, SMART on FHIR apps with launch history, and webhook subscriptions with per-delivery outcomes written as sentences. |
apps/web/src/components/shell/AppShell.tsx composes every screen except the landing page:
CommandProvider
└── SideNav (rail from lg, off-canvas drawer below, focus-trapped)
└── TopBar (area name, facility tag, Demo data badge, command trigger, user)
└── main#main-content
├── breadcrumb, h1, description, actions, content
└── aside (page context rail)
└── CommandPalette
The root layout.tsx renders a skip link and imports @openrunic/ui/styles.css before ./globals.css. That order is load-bearing: the app layer must win ties against the library.
Navigation is one flat group, not nested. navigation.ts declares eight primary areas in workflow order rather than alphabetically: Schedule, Flow Board, Patients, Inbox, Orders, Billing, Reports, Admin. Thirteen further routes are reachable through the palette but have no rail row. The active area is chosen by longest matching href prefix, so /patients/<id> still lights Patients.
Opened with Cmd-K or Ctrl-K, from a single window keydown listener that toggles rather than only opening. There is also a real labelled button in the top bar, so the feature is discoverable without knowing the shortcut.
It is a real <dialog> containing an ARIA combobox. Focus never leaves the input; the highlighted row is published through aria-activedescendant. Arrow keys wrap, Home and End jump, Enter runs, Escape closes, Tab is trapped, and focus returns to the trigger on close.
Three groups, in this order:
Patients. Live search with a 150 ms debounce, six results, each showing MRN and date of birth as the hint.
Go to. The 21 navigation targets, each with synonym keywords so a partial or misspelled query still lands.
Actions. Registered per screen and live only while that screen is mounted. Examples include finding an available slot, walking in a patient, jumping to a chart tab, printing a summary, and opening the chart from a note.
Matching is deterministic and tiered: prefix, then word start, then contains, then subsequence, stable by registration order within a tier. There is no fuzzy scoring that reorders results between keystrokes.
Screens register their commands through a one-line ScreenCommands component rendered inside AppShell, because the provider lives inside the shell and calling the hook in the screen body would throw.
Two environment variables, both NEXT_PUBLIC_, read in apps/web/src/lib/api/config.ts:
| Variable | Default | Effect |
|---|---|---|
NEXT_PUBLIC_API_MODE |
mock |
Only the literal string live switches. Anything else, including unset, is mock. |
NEXT_PUBLIC_API_BASE_URL |
http://localhost:4000 |
Base URL when live. |
Mode is resolved once at module load. There is no runtime toggle.
The mock client is not a stub. It satisfies the same client interface as the HTTP client and applies the same filters, sorts, and pagination the API applies: the same search fields, the same sort keys, the same default and maximum page sizes, and the same totalPages floor of one. It fails the same way too, rejecting a miss with an error carrying a real RFC 9457 problem document.
Two details worth knowing. It adds 140 ms of artificial latency outside tests, so loading states are actually visible in a browser and instant in a suite. And it caches a lowercased search haystack per patient in a WeakMap, because the palette searches on every keystroke.
Writes are deliberately not implemented in the mock layer. The stated reason is that a fixture which accepts writes teaches screens to trust state the server never saw. Every screen that appears to write holds the change in local component state and says so, usually with a toast and an undo.
Mock mode is surfaced rather than hidden: a persistent "Demo data" badge in the top bar, a caption on the schedule screen, a fixed clock so screenshots are reproducible, and fixture-backed browser tab titles.
Only two of the five data clients switch. api (patients and appointments) and chartApi (chart summary and notes) become HTTP clients. adminApi, worklist (orders, results, inbox), and billing are hardcoded to fixtures with no live branch at all, because the API answers 501 for those aggregates.
Authentication is not wired: getToken returns null, so no Authorization header is sent and the API answers 401. The screens render that honestly through their error state rather than falling back to fixtures.
Tokens come from @openrunic/ui. globals.css is the app layer, sectioned by comment banner per feature area, and its stated policy is no hex values, no off-scale spacing, and no invented shadows.
It adds only a handful of root values: widened font fallback stacks and shell geometry (gutter, rail width, top bar height). One documented stopgap exists, a caution status colour built with color-mix, flagged in a comment as a proposed library token because the library ships three status tiers and this screen needed four.
There is no dark mode. No prefers-color-scheme handling, no data-theme, no theme provider. The theme colour is hardcoded to bone.
Fonts and brand marks are self-hosted by policy and are not vendored into the repository. Without the binaries present, the fallback stack carries the interface and only the optical-size axis is lost.
vitest.config.mts uses the istanbul coverage provider and deliberately omits @vitejs/plugin-react. The plugin's second transform pass double-instruments files under istanbul and roughly halves reported coverage; the config uses esbuild's automatic JSX runtime instead. There are no thresholds in the config, because CI enforces floors on the merged coverage map rather than per shard. See Testing strategy.
next.config.ts sets poweredByHeader: false, disables the agent-rules file generation, and disables typed routes with a written justification: type-check runs before build in CI, and the palette pushes hrefs built at runtime.
- All writes are session-local. No screen persists anything.
- Three of five data clients have no live path. Setting live mode does not change admin, reports, orders, results, inbox, or billing.
- Authentication is not wired. The user name in the top bar is a hardcoded default.
- Rail badge counts are declared but not passed through. The library item type supports a badge; the shell maps only label and icon.
-
/patients/[id]/insuranceis not registered in the palette, unlike every other route. -
The command palette forks focus handling into the app rather than using a library primitive, with an in-file note proposing a dialog and combobox pair for
@openrunic/ui. -
apps/web/README.mdis stale. It describes the app as route skeletons, which the code contradicts.
openrunic is an open-source operating system for human health. Pre-alpha: do not run it in production, and never put real patient data into it.
Repository · Licence (AGPL-3.0-only) · Security policy · Contributing · Code of conduct
Where this wiki and the repository disagree, the repository is right.