Files
parking_solution/wiki/concepts/device-status-monitoring.md
T
julian 898cf1953a docs(wiki): camera 503/stream, alarm URL helper, reader ICMP liveness
- lpr-camera.md: "503 Device Busy" can be PERSISTENT (main-stream saturation on
  the G3H) — the real fix is sub-stream selection, not just retry.
- device-status-monitoring.md: QR reader health was false-healthy (hardcoded
  "ready") until the ICMP-ping fix; document the push-device monitoring model.
- log entries for both 2026-06-26 sessions.

Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
2026-06-26 16:47:19 +02:00

6.7 KiB

type, tags, sources, updated, status
type tags sources updated status
concept
parking
device
monitoring
reliability
ui
2026-06-26 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. 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 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.

    Reader health was a LIE until 2026-06-26. The QR-reader adapter (a PUSH device: it GETs our backend on each scan and exposes no TCP port) had a hardcoded healthCheck → { ready, "stub" }, so two genuinely-offline readers still showed green. A push device that's silent is indistinguishable from a dead one — so claiming ready unconditionally is the worst failure (false-healthy). Fix: an optional reader IP (monitor-only; scans still resolve by serial) + an unprivileged ICMP ping (drivers/icmp.ts, shells /bin/ping in SOCK_DGRAM mode — no CAP_NET_RAW, no native dep; the booth compose sets net.ipv4.ping_group_range). Reply → ready, no reply → offline; no IP set → degraded ("set IP to monitor"), never a false green. Verified on hardware: pinged the real readers on the device VLAN. See gee-qr-er80.

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 (/api/ws): the hello frame carries the initial device-status set; a device-status frame is pushed per change. The web booth-console 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): 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.

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).
  • 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).