Pulse — Architecture
Pulse is a Next.js (App Router) client-rendered command center over a deterministic in-browser simulation. The design constraint that shapes everything: the boundary between the data source and the UI is the same contract a real hospital feed would use, so the simulation is swappable.
┌──────────────────────────────────────────────────────────────────────────┐
│ src/lib/sim (the "hospital") │
│ │
│ seed.ts ──▶ initial WorldSnapshot ──┐ │
│ engine.tick(world, dt, rng): │ │
│ advance clocks ─▶ fire stochastic events ─▶ apply reducers │
│ ─▶ append to eventLog ─▶ recompute predictors ─▶ computeMetrics │
│ ─▶ runBrain() ─▶ NEW WorldSnapshot │
└───────────────┬───────────────────────────────────────────────────────────┘
│ commit (once per tick)
▼
src/lib/store.ts (Zustand: the single live snapshot + sim controls)
│ fine-grained selectors (src/lib/selectors.ts)
▼
src/components/* ('use client' islands) ◀── user actions (src/lib/sim/actions.ts)
Data model — src/lib/types.ts
One canonical model, all timestamps as sim-minutes since epoch. Core entities: Unit, Bed
(+BedAttributes), Patient (+WaitForecast, Barrier), StaffMember (+Credential), Gap
(+CascadeStep), EvsJob, TransportJob, Alert (+EscalationStep), Recommendation
(+ScoreFactor, alternatives), Metrics, SimEvent, Scenario, and the top-level
WorldSnapshot that holds entity maps (Record<id, T>), the alert/recommendation arrays, the
immutable eventLog, derived metrics, plus scenarioId and seed.
Simulation engine — src/lib/sim/
rng.ts— mulberry32 PRNG (Rng) soseed + scenariodeterministically replays the same timeline; plus name generators and auid.scenarios.ts— presets (Calm Tuesday,Friday ED Surge,Flu Season + 3 Sick Calls,Code Influx) that reweight event rates (arrivals/hour, admit bias, sick-call/deterioration/ discharge rates, high-acuity share).seed.ts— builds ~186 beds across 8 units, ~80 staff, an ED queue, boarders, coverage gaps, and EVS jobs, then runs one metrics + brain pass so the UI has content on first paint.engine.ts—tick(world, dt, rng)returns a new snapshot each tick. It shallow-clones viaclone.ts(new map/array containers, reused unchanged entity refs), advances EVS/transport clocks, fires Poisson-ish events (ED arrival, triage, room, admit, discharge order, bed vacate, sick call, deterioration, lab result), applies them as reducers, recomputes ED-facing predictors and census, thencomputeMetrics+runBrain.actions.ts— pure user-action mutators (assignBed,acceptRecommendation,overrideRecommendation,fillGap,advanceCascade,expediteDischarge,resolveBarrier,acknowledgeAlert,resolveAlert,escalateAlert,requestEvs,advanceEvsJob).assignBedis the canonical closed-loop example: assign target bed → vacate & dispatch EVS for the old bed → create transport → raise a receiving-unit alert → tally boarding-hours/moves saved → mark the recommendation executed.
Optimization brain — src/lib/brain/
predictors.ts— explainable weighted-sum predictors; each returns{score, breakdown}so the UI renders factor × weight = contribution. (admitProbability,waitForecast,predictedLOSHrs,deteriorationTier,acceptLikelihood,scoreBedForPatientwith hard-constraint gating.)solvers.ts—rankBedsForPatient,buildCascade/rankCandidatesForGap,unitRatio(acuity-weighted),computeMetrics(occupancy, boarders, ratio compliance, capacity/surge index, boarding forecast, closed-loop tallies).brain.ts—runBrain(world): regeneratesproposedrecommendations for current needs while preserving anything the user has acted on, sorts byurgency × score, and refreshes standing alerts (ratio breach, EVS stall, boarding pressure) deduped by stable id. Discrete event alerts (sick call, deterioration crossing critical) are raised inline in the engine.
Store & realtime — src/lib/store.ts, src/lib/selectors.ts
A single Zustand store holds world, running, speed, and the Rng. SimulationProvider
(src/components/providers/) runs a 1s interval calling tick() while running (1 real-sec = 2
sim-min × speed), and reads ?seed=/?scenario= on mount. Components subscribe through small
selector hooks (useEdPatients, usePlacementQueue, useOpenGaps, useProposedRecs,
useMetrics, …). The store contract — a snapshot plus an event stream — is identical to what an
SSE/WebSocket/FHIR-Subscription bridge would push; going live means replacing the engine tick with a
network source and keeping the store, selectors, and every component unchanged.
Design system — src/app/globals.css, src/components/ui/, src/lib/ui.ts
Tailwind v4, CSS-first. Tokens live in @theme: a near-black slate surface ramp, hairline borders,
and the clinical urgency scale as the only saturated color (stable/info/caution/warning/urgent/ critical), with a separate sequential acuity ramp (ESI 1–5) so the two never collide.
src/lib/ui.ts maps enums → color/label (urgencyVar, bedStatusMeta, esiMeta, severityTone,
recTypeMeta). Primitives are hand-written (shadcn-style, no UI dependency): Card, Badge,
Button, StatTile, UrgencyPill, StatusDot, CountdownPill, RingGauge, Sparkline,
Meter, KanbanColumn, DataTable, Sheet, Tabs, AreaTrend (Recharts). Density-first,
tabular-nums everywhere counts matter, motion minimal (only critical pulses).
App structure — src/app/
Root layout.tsx mounts <SimulationProvider> + <CommandShell> (nav rail + status ribbon). /
redirects to /command. Flagship routes: /command, /flow, /beds, /staffing, /feed.
Roadmap routes (/discharge, /perioperative, /ancillary, /safety, /transport, /revenue)
render a ModuleShelf wired to the same stores. Feature components are grouped by domain under
src/components/{command,flow,beds,staffing,feed,shared}.
Conventions
- Files using hooks/state/store start with
"use client"; static chrome stays server-rendered. - Colors only via tokens/helpers — never hardcoded hex in components.
- The simulation is deterministic and side-effect-free except the provider's interval; the same
seed + scenario always replays identically (no
Date.now/Math.randomin the model).