Files
parking_solution/wiki/concepts/device-status-monitoring.md
T
julian f6e35bbebf
Build desktop / desktop (push) Successful in 4m16s
Build & push images / images (push) Successful in 2m46s
CI / check (push) Successful in 38s
fix(reader): correct the QR reader's identity — Dingtian DT-008, not "GEE"
An early wrong assumption named the QR/RFID access reader "GEE" /
"GEE/Fondvision" / "GEE-QR-ER80" (and summarized a raw GEE PDF as its
datasheet). There is no GEE device — it's the Dingtian DT-008
(dingtian-tech.com/en_us/qr_code_reader.html), the same vendor as the relay
board, which is why it integrates the identical HTTP-GET-push way.

Code:
- Driver symbol geeQrReaderDriver → dingtianQrReaderDriver; label →
  "Dingtian DT-008 QR/RFID reader (HTTP push)"; comments/description rewritten
  to the real DT-008 facts (Wiegand 26/34, TCP/IP, USB, RS485 — not RS-232;
  QR/barcode + ID/IC/NFC — not DataMatrix/1D).
- Persisted driverId "gee-qr-reader" → "dingtian-qr-reader" (the registry
  lookup key + the row created on assign in qr-reader.ts).
- Migration 0015 rewrites existing devices.driver_id rows so configured readers
  keep resolving (applied to the dev DB — 2 rows; the booth applies it on boot).
  Behaviour is unchanged: naming + the persisted id only.

Wiki + memory:
- Renamed entities/gee-qr-er80.md → dingtian-dt008-reader.md and
  sources/gee-qr-er80.md → dingtian-dt008.md; rewrote both to the real DT-008
  product-page specs while KEEPING all the verified-on-hardware protocol facts
  (cjihao serial, .jsp path, Connection: close). Fixed every cross-reference +
  "GEE" mention in 6 other pages. Memory gee-reader-serial-binding →
  dingtian-reader-serial-binding. The only surviving "GEE" mentions are
  deliberate naming-correction notes, the raw PDF filename, and the
  append-only log history.

Full workspace build/lint/test green; dev DB readers verified resolving to the
registered dingtian-qr-reader driver.

Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
2026-06-28 17:39:15 +02:00

110 lines
6.7 KiB
Markdown

---
type: concept
tags: [parking, device, monitoring, reliability, ui]
sources: []
updated: 2026-06-26
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.
> **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 [[dingtian-dt008-reader]].
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).