Room Monitor System
Generic configurable smart-room telemetry dashboard with HomeKit-style devices.
A generic, configurable “smart room” dashboard with HomeKit-style device
pairing. The same implementation drives ICU rooms, regular wards, OR
suites, exam rooms, nursing-home suites, lobbies, retail spaces — any
monitored physical space. The default content is fully neutral; per-tenant
deployments override every label, device group, energy highlight, and
section via TeleViewConfig + props on ``.
Quick glance
| Surface | Path | Purpose |
|---|---|---|
| Live dashboard (component) | web/packages/adt-kit/src/admission/components/wardview/TeleView/ |
The dashboard primitives — composable from any host |
| Live dashboard (page) | /ipd/room-monitor?ward=<id> |
Standalone page with ward selector + link to admin |
| Admission ward view toggle | /admission/wardview?view=tele |
Embedded in the existing ward-view chrome |
| In-patient ward view toggle | /in-patient/wardview?view=tele |
Same toggle, in-patient ward chrome |
| IPD landing quick link | /ipd/command-center → “Room Monitor” card |
One-click launcher from the IPD home |
| In-patient landing widget | /in-patient → “Room Monitor” card |
Same launcher from the in-patient home |
| Admin / pairing page | /admin/room-monitor |
Register, edit, remove, pair, send-test-reading |
| Realtime tables | tele_devices + tele_device_readings (Supabase) |
Read-only mirror; MongoDB owns the canonical ledger |
Architecture
Physical device ──MQTT/BACnet/etc.──▶ Backend collector
│
├──▶ MongoDB (canonical write ledger)
│
└──▶ Supabase (read-only mirror)
↓
postgres_changes
↓
useTeleRealtime ◀──┐
│ │
│ every dashboard subscribes
↓ │
TeleView ────────┘
Per project guardrails (web/CLAUDE.md):
- MongoDB is the canonical write ledger.
- Supabase is a read-only mirror. The frontend never writes to read-model tables in normal operation.
- The admin pairing page is the one allowed exception while the backend collector pipeline is still under construction; the same code path becomes a “request → backend” call once the collector exists, with no UI changes.
Layers
1. Schema — web/supabase/migrations/20260513_room_telemetry_devices.sql
Two tables, both registered with the supabase_realtime publication:
tele_devices— registry. One row per physical/virtual device. Carriesward_id,category(hvac / ambient / lighting / network / energy / camera / medical),kind(free-form tag), bilingual labels, optionalzone(for lighting),connectedflag, optionalconnector_keyresolving to aDeviceConnectorimplementation indiagnostic-devices/connectors.tele_device_readings— latest reading per(device_id, reading_kind). Composite primary key keeps a single snapshot per kind; historical traces live in MongoDB.
RLS: read-all for anon; no INSERT / UPDATE / DELETE policies (only the
service role can write). Apply with psql -f or via the Supabase SQL
editor; the migration is idempotent (CREATE TABLE IF NOT EXISTS, etc.).
2. Service layer — web/src/services/tele-device.service.ts
teleDeviceService exposes:
| Method | Purpose |
|---|---|
list({ wardId?, category? }) |
Fetch all devices, filtered |
create(input) |
Insert; auto-generates an id if absent |
update(id, patch) |
Field-level update |
remove(id) |
Cascade-deletes readings via FK |
toggleConnected(id, on) |
Convenience update |
writeReading(input) |
Upsert one reading row — used by the admin “test reading” button |
schemaApplied() |
Returns false when the migration hasn’t been applied — surfaces a friendly banner |
Recognises both 42P01 (raw Postgres) and PGRST205 (Supabase REST
schema-cache miss) and surfaces a single instruction: “Apply migration
20260513_room_telemetry_devices.sql, then refresh.”
3. Dashboard primitives — TeleView/
TeleView/
├── types.ts ← TeleDevice, TeleSnapshot, TeleSections, TeleViewConfig
├── simulation.ts ← useTeleSimulation() + defaultTeleConfig()
├── useTeleRealtime.ts ← Supabase subscription + initial fetch
├── cards.tsx ← HvacCard, LightingCard, EnergyCard, NetworkCard, DeviceStatusCard, CameraCard, EnvironmentalCard, KpiCard, TeleCard
└── index.tsx ← <TeleView> — composes everything
`` props:
| Prop | Default | Notes |
|---|---|---|
title |
"Room Monitor" |
Header text |
wardName |
"Room" |
First KPI card label |
wardSubtitle |
— | Smaller subtitle under wardName |
config |
defaultTeleConfig() |
Device roster + groups + cameras + occupants |
wardId |
— | Filters the Supabase subscription |
sections |
all on | { hvac?, camera?, lighting?, environmental?, energy?, network?, deviceStatus? } |
occupantsLabel |
"Occupants" |
Second KPI card label |
occupantsLabelTh |
— | Thai tooltip |
devicesLabel |
"Devices" |
Suffix on KPI cards |
A small chip next to the title shows the current data source:
- 🟢 Live · Supabase when at least one device is registered for the ward and the channel is subscribed.
- ⚪ Simulation when no devices are registered or the tables aren’t applied yet — the local jitter sampler keeps the dashboard alive.
4. Pairing UX — web/src/containers/admin/room-monitor/
admin/room-monitor/
├── page.tsx ← Admin page (table + manual-entry drawer + pairing trigger)
├── PairingFlow.tsx ← Multi-step "Add accessory" dialog
└── factoryCatalog.ts ← 17 device templates that the discovery scan returns
HomeKit-style pairing flow
Four steps in a single dialog with a top stepper:
- Discover — pulsing concentric rings around a central icon while
the simulated scan runs. 6 random templates from
factoryCatalogfade in over ~2s as cards with brand + blurb + RSSI. Each card is clickable. - Pair — large product icon + synthesised serial. 8-digit code input
in a monospaced field; an
autofilllink drops the right code (since this is a demo). A 1.2s spinner simulates the secure handshake before advancing. - Configure — friendly name (bilingual), Ward / Room ID (dropdown of
known wards or free-text), optional zone (for lighting templates).
Add to roomtriggersteleDeviceService.create(). - Done — green check, summary card with the saved device, “Pair another” / “Done” buttons.
The same flow lives behind the Pair new accessory button on the admin page. The previous direct-form path is still available as Manual entry for power users.
Factory catalogue
17 templates spanning every category. Each carries: model + brand +
emoji + accent + category + kind + bilingual default label + zone
options (for lighting) + connector key (for auto-resolution against
diagnostic-devices/connectors). Drop a new entry in
FACTORY_TEMPLATES and it appears in the discovery scan immediately —
no schema changes required.
5. Realtime stream — useTeleRealtime
Per-ward subscription to both tele_devices and tele_device_readings.
Gracefully handles three failure modes:
- Supabase not configured (placeholder env) → silently bails, dashboard uses simulation.
- Tables not applied yet →
42P01/PGRST205swallowed, dashboard uses simulation. - No devices registered for this ward →
hasLiveData = false, dashboard uses simulation.
State returned: { hasLiveData, isConnected, devices, readings }.
teleRealtimeToDevices(state) maps the registry rows into the
TeleDevice[] shape consumed by useTeleSimulation. The simulation
runs over the real device list when present — connected devices show
live values, disconnected devices stay in Standby.
Configuration model
Everything that’s hard-coded in the screenshot is overridable:
- Title: prop or
config.title - Occupancy label: prop or
config.occupantsLabel - Visible sections:
sections={{ camera: false, energy: false }}etc. - Device groups (rows in the “Device Status” card):
config.deviceGroups - Energy highlights (rows under the bar chart):
config.energyHighlights - Cameras:
config.cameras - Occupant count:
config.occupants(live count comes from the realtime device list in production)
Three demonstration presets ship in the sandbox target
(?target=TeleView): hospital, nursing-home, office. Same
component, three deployments, no fork.
Deployment
To turn on live data
-
Apply the migration:
psql "$SUPABASE_DB_URL" -f web/supabase/migrations/20260513_room_telemetry_devices.sqlOr paste it into the Supabase SQL editor.
-
Pair at least one device through
/admin/room-monitor— either via the Pair new accessory flow (recommended) or Manual entry. -
(Until the backend collector ships) Click the ⚡ icon on a device row to push a test reading. The “Simulation” chip on the dashboard flips to “Live · Supabase” within ~250ms.
To wire a real backend collector
The collector runs server-side (Moleculer service, Deno edge function, or a separate process). On every device reading it should:
- Write the canonical record to MongoDB.
- Upsert into
tele_device_readingsvia service-role credentials (onConflict: 'device_id,reading_kind'). - Update
tele_devices.last_seen_at+connectedwhen the device appears or drops off.
The frontend changes nothing — useTeleRealtime is already listening.
Gaps that remain
These need attention before a production rollout:
-
Reading-value pipe —
useTeleRealtimecollects readings into a map but doesn’t yet feed them into theAmbientSnapshot/EnergySnapshotslots. Today only device PRESENCE flows into the dashboard; reading values still come from the jitter simulation. Wirestate.readingsinto a derived snapshot inTeleView/index.tsx, falling back per-field when a reading is missing. -
Backend collector — see above. The pairing flow writes directly to Supabase as a demo path; production should funnel through the backend so MongoDB stays canonical.
-
Real discovery —
factoryCatalog.pickRandomTemplates()is a simulation. Production discovery should be driven by an mDNS / BLE / BACnet scan exposed by the collector.
Files at a glance
| File | Purpose |
|---|---|
web/supabase/migrations/20260513_room_telemetry_devices.sql |
Schema |
web/src/services/tele-device.service.ts |
CRUD service |
web/packages/adt-kit/src/admission/components/wardview/TeleView/types.ts |
Type definitions |
web/packages/adt-kit/src/admission/components/wardview/TeleView/simulation.ts |
Default config + jitter engine |
web/packages/adt-kit/src/admission/components/wardview/TeleView/useTeleRealtime.ts |
Supabase subscription |
web/packages/adt-kit/src/admission/components/wardview/TeleView/cards.tsx |
Card primitives |
web/packages/adt-kit/src/admission/components/wardview/TeleView/index.tsx |
`` |
web/packages/adt-kit/src/admission/components/wardview/useWardBedsRealtime.ts |
Bed realtime hook (separate from telemetry) |
web/packages/adt-kit/src/admission/components/wardview/WardView.tsx |
Ward + Patient + Tele toggle (admission) |
web/packages/adt-kit/src/in-patient/components/wardview/WardView.tsx |
Ward + Patient + Tele toggle (in-patient) |
web/src/containers/admin/room-monitor/page.tsx |
Admin page |
web/src/containers/admin/room-monitor/PairingFlow.tsx |
HomeKit-style flow |
web/src/containers/admin/room-monitor/factoryCatalog.ts |
Discovery templates |
web/src/containers/ipd/room-monitor/page.tsx |
Standalone Room Monitor page |
web/src/containers/ipd-command-center/components/IpdLandingHub.tsx |
IPD landing quick-link |
web/packages/adt-kit/src/in-patient/components/dashboard/RoomMonitorWidget.tsx |
In-patient landing widget |
web/src/routes/IPDRoutes.tsx |
/ipd/room-monitor route |
web/src/routes/AdminRoutes.tsx |
/admin/room-monitor route |
web/sandbox/registry.ts |
Sandbox entries: TeleView, PatientCardView, RoomMonitorAdmin |