feat(devices): live device-status footer across all categories
Generalise printer-only monitoring to every configured device. New DeviceMonitor polls all enabled devices each tick (default 8s): printers via rich readStatus(), relays/readers/cameras via the generic healthCheck() reachability probe, flattened to one traffic-light (ready/degraded/offline) + detail, deduped (emit on change only), fail-toward-offline. - device-status bus event + GET /api/devices/status snapshot. - Pushed over the existing /api/ws (hello carries the initial set; device-status frame per change). - Web: live-store devices map, WS handler, DeviceFooter chip-per-device (role label not vendor; click a degraded/offline chip for an issues panel). Verified roleKind resolution + change-only emit on a fresh DB. Note: the footer's UI surface (api type, router mount, i18n devices) rides in the subsequent subscription commit due to shared-file overlap. Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
@@ -83,6 +83,15 @@ the Active-Sessions "Open barrier" is disabled the same way. The server enforces
|
||||
(`requireShift` 409 `no_shift`) — the UI just front-runs the rejection. The live feed is **scoped to
|
||||
the open shift's window** (empty when no shift is open). See [[shift]] for the rule and the routes.
|
||||
|
||||
## The device-status footer
|
||||
|
||||
A **fixed footer** in the app shell shows the live status of every configured device — relays,
|
||||
readers, cameras, printers — one chip each (coloured dot + name + fault detail), with an "all ready
|
||||
/ N offline" roll-up. Fed by the unified [[device-status-monitoring|DeviceMonitor]] over the same
|
||||
`/api/ws` socket (`hello` carries the initial set; a `device-status` frame per change), held in the
|
||||
live store keyed by device id, with `GET /api/devices/status` as the seed/fallback. Visible on every
|
||||
screen, so the operator always sees the barrier relay's reachability and the printer's paper state.
|
||||
|
||||
## Dev notes
|
||||
- Vite proxies `/api/ws` (`ws: true`) to the backend; the backend's Origin allowlist must include the
|
||||
dev SPA origin (`WS_ALLOWED_ORIGINS=http://localhost:5173`). In production Fastify serves the SPA
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
---
|
||||
type: concept
|
||||
tags: [parking, device, monitoring, reliability, ui]
|
||||
sources: []
|
||||
updated: 2026-06-18
|
||||
status: open
|
||||
---
|
||||
|
||||
# Device status monitoring (the booth footer)
|
||||
|
||||
The booth shows a **fixed footer** with the live status of every configured device — relays,
|
||||
readers, cameras, printers — so an operator sees at a glance that the barrier relay is reachable,
|
||||
the exit scanner is up, and the ticket printer has paper. This generalises the printer-only
|
||||
[[printer-status-monitoring]] to **all four [[device-adapter-pattern|device categories]]**. A
|
||||
reliability control, not a threat-model one. (Built 2026-06-18.)
|
||||
|
||||
## What gets polled, and how
|
||||
|
||||
Every **enabled** row in `devices` is polled on an interval, regardless of category — the monitor
|
||||
talks only to the adapter interfaces ([[device-adapter-pattern]]), never a driver SDK:
|
||||
|
||||
- **Printers** → their rich `MonitorableDevice.readStatus()` (paper end / near-end, cover open,
|
||||
cutter error, off-line) — the same capability the existing [[printer-status-monitoring|PrinterMonitor]]
|
||||
uses. The footer surfaces the fault detail.
|
||||
- **Relays / readers / cameras** → the generic `Device.healthCheck()` **reachability** probe every
|
||||
adapter implements (`ready | degraded | offline`). This is presence/up-ness, not a deep fault
|
||||
model — a relay either answers or it doesn't.
|
||||
|
||||
Both collapse to one **traffic-light**: `ready | degraded | offline`, plus a `detail` string. Fail
|
||||
**toward "there's a problem"**, never false-healthy: a probe that throws or times out reads
|
||||
`offline` (consistent with [[printer-status-monitoring]]'s fail-safe mapping); a driver that's no
|
||||
longer registered reads `offline` ("driver not registered") rather than vanishing.
|
||||
|
||||
## The monitor (server)
|
||||
|
||||
`DeviceMonitor` (`apps/server/src/device-monitor.ts`), modelled on the PrinterMonitor:
|
||||
|
||||
- re-reads the device set each tick (a newly-assigned/removed device appears/disappears without a
|
||||
restart); drops cached status for devices that are gone or disabled;
|
||||
- polls every `DEVICE_POLL_MS` (default **8000ms**), never overlapping ticks;
|
||||
- caches the latest unified status per device id;
|
||||
- emits a `device-status` bus event **only when a device's state or detail changes** (deduped).
|
||||
|
||||
> **Relationship to the PrinterMonitor.** Both run. The PrinterMonitor stays the authority for the
|
||||
> printer-specific live detail + its SSE stream (`/api/printers/status*`) that the entry flow may
|
||||
> later depend on for [[printer-roles-failover]]. The DeviceMonitor is the **unified footer feed**
|
||||
> across all categories. They poll independently (printers get probed by both — cheap HTTP reads);
|
||||
> the small duplication is deliberate, to avoid coupling the footer to printer internals. Could be
|
||||
> consolidated later if the overlap ever matters.
|
||||
|
||||
## API / live UI
|
||||
|
||||
- `GET /api/devices/status` — cached snapshot of all devices (no device round-trip). Any
|
||||
authenticated role (operational, not a setup action).
|
||||
- Live updates ride the **one booth [[booth-console|WebSocket]]** (`/api/ws`): the `hello` frame
|
||||
carries the initial device-status set; a `device-status` frame is pushed per change. The web
|
||||
[[booth-console|live store]] holds the set keyed by device id; the REST snapshot seeds it / fills
|
||||
in if the socket is briefly down.
|
||||
- **`DeviceFooter`** (`apps/web/src/ui/DeviceFooter.tsx`) renders one **compact** chip per device —
|
||||
a coloured dot + a **role label, never the vendor** — ordered access → reader → camera → printer,
|
||||
with a right-aligned roll-up ("N with issues" / "all ready"). Mounted in the app shell so it's
|
||||
visible on every screen.
|
||||
|
||||
### Label = role, not vendor (refinement 2026-06-18)
|
||||
|
||||
The chip shows **what the device does, not who made it**: the localised category + a role/direction
|
||||
suffix → `Lexuesi hyrje`, `Printer kabina`, `Kamera dalje`. The server sends a structured
|
||||
**`roleKind`** token (not a composed string), the client localises it:
|
||||
- **reader / camera** → the direction inherited from its bound relay (`directionOf()` in
|
||||
[[entry-exit-points|device-resolve]]): `entry | exit | both`.
|
||||
- **access controller** → `entry | exit | both` from its `relays[]`, or **`mixed`** when it spans
|
||||
more than one direction; `null` if it declares none yet.
|
||||
- **printer** → `lane` (entry-dispenser) | `booth` (booth-receipt) — the [[printer-roles-failover]] role.
|
||||
- `null` → the chip shows the category alone.
|
||||
|
||||
### Detail does NOT pollute the footer (refinement 2026-06-18)
|
||||
|
||||
Chips stay short — **no inline fault text**. A device that is `degraded`/`offline` is clickable (so
|
||||
is the roll-up); clicking opens a small **issues panel** anchored above the footer that lists only
|
||||
the problem devices with their role label, state, the `detail` string, and the last-checked time.
|
||||
`ready` chips are non-interactive. The panel closes on outside-click / Escape (a lightweight
|
||||
popover — no extra dependency; only Radix Dialog is installed).
|
||||
|
||||
## Verified (2026-06-18)
|
||||
|
||||
On a fresh DB seeded with a stub relay, a TCP reader, and two printers (one reachable, one not):
|
||||
relay + reader → `ready` via `healthCheck`; the unreachable printer → `offline` (with a detail
|
||||
string, never threw); the bus emitted once per device on first observation, and a second unchanged
|
||||
tick was silent (change-only emit). Server + web build clean.
|
||||
|
||||
## Open / not yet done
|
||||
|
||||
- **Reachability ≠ correctness.** `healthCheck()` says a relay/reader answers, not that it's wired
|
||||
to the right barrier or reading cards — that's a setup/precondition concern ([[first-run-setup]],
|
||||
the Dingtian [[access-controller-button-flow|precondition checks]]).
|
||||
- **No per-device history / alerting.** The footer is point-in-time; a flapping device isn't
|
||||
tracked over time. Reconciliation-style alerting is out of scope here.
|
||||
- **Cameras** only expose `healthCheck` reachability today; a "last snapshot age" health signal
|
||||
could be richer ([[lpr-camera]], [[opencv-anpr-service]]).
|
||||
- Possible later **consolidation** of PrinterMonitor + DeviceMonitor (see the note above).
|
||||
@@ -7,6 +7,11 @@ updated: 2026-06-14
|
||||
|
||||
# Printer status monitoring
|
||||
|
||||
> **Generalised 2026-06-18:** the booth's all-device status **footer** is a separate, unified
|
||||
> monitor across every category (relays/readers/cameras/printers) — see
|
||||
> [[device-status-monitoring]]. This page remains the authority for the *printer-specific* rich
|
||||
> status (paper/cover/cutter) + its SSE stream; both monitors run.
|
||||
|
||||
The booth must know a printer is in trouble **before** a driver presses the entry button and no
|
||||
ticket comes out. So the system polls each printer's live status (paper out, cover open, cutter
|
||||
jam, off-line) and pushes changes to the operator UI. A reliability control, like
|
||||
|
||||
Reference in New Issue
Block a user