Add the rongta PrinterDevice driver (ESC/POS over raw TCP 9100) and the device-agnostic pieces around it: - Roles + failover: each printer declares a role (entry-dispenser/booth- receipt) and failoverRank; printer-routing.ts picks the best healthy printer and falls back outside->booth for entry tickets (never the reverse). - Live status: MonitorableDevice.readStatus()/PrinterStatus capability. The Rongta driver scrapes the device's own /prn_stat.htm (Cover/Cutter/Paper End/Near End/Off-Line) rather than hand-decoding DLE EOT, whose reply bytes on this clone don't match the canonical ESC/POS bit layout (verified on hardware) -- avoids a false-healthy. Maps to ready/degraded/offline, fail safe on an unreachable or unexpected page. - Server PrinterMonitor polls enabled printers (PRINTER_POLL_MS, default 5s), caches latest, emits "printer-status" on change. Exposed via GET /api/printers/status and an SSE stream for the booth UI. Verified against 10.0.10.6: ready when healthy, offline when unreachable (no throw), bus emits on change and suppresses unchanged reads. Wiki: new rongta-printer entity, printer-roles-failover and printer-status-monitoring concepts; BOM/index/log updated.
16 KiB
Wiki Log
Append-only chronological record. Each entry: ## [YYYY-MM-DD] <op> | <subject>.
Query with grep "^## \[" log.md | tail -5.
[2026-06-14] ingest | Parking System — Architecture & Design Notes
First source ingested. Bootstrapped wiki scaffolding (CLAUDE.md schema, index.md, overview.md, log.md). Created source summary, 14 entity pages, 9 concept pages, and decision records (settled decisions + 6 open questions). Source is a dense design doc covering stack, threat model, device architecture, UHPPOTE access control, the custom ESP32 controller alternative, readers, and a reference BOM.
[2026-06-15] decision | JWT key choice + ESP32 deferred
From app work, not a new source. Added open-questions #7 (symmetric vs.
asymmetric JWT signing key — raised by the commit security review; prefer RS256/EdDSA
so verifying hosts hold only a public key, mirroring the ATECC608 / challenge-response
property). Marked esp32-custom-controller status: deferred per decision not to
implement device-level auth for now (access control stays on UHPPOTE + network
isolation); noted in open-questions #6. Updated local-jwt-auth (hardened secret
handling + 8h expiry, asymmetric-key pointer) and the index.
[2026-06-15] decision | Device-agnostic registry + first-run setup
From app work. Made the device-adapter-pattern selectable: added a
device-registry (catalog of drivers per category) and a first-run-setup
flow so the admin picks a device per lane at install. Categories: access
(ZKTeco / ESP32 relay), reader (Wiegand / TCP-IP), camera (Hikvision / Dahua,
snapshot-on-event), printer. Added a CameraDevice interface; new lane_devices
setup_statetables (migration 0001); admin-only setup endpoints. Stub drivers for now (no real vendor protocols yet). Verified catalog + assign + validation + auth end to end.
[2026-06-15] decision | UHPPOTE library chosen + real driver
Researched Node options for the UHPPOTE controller. Chose the official
uhppoted npm package (MIT, actively maintained, full API incl. openDoor,
get-event(s)/event-index, set-listener, restore-default — covers the whole
event-log-ingestion design). Rejected: raw-dgram DIY (reinvents the lib),
node-red-contrib-uhppoted (wrong model), Go REST sidecar (extra runtime). Added
it to @parking/devices and implemented a real uhppote uhppote-controller
access driver (pulseOpen→openDoor, healthCheck→getStatus), registered in the
catalog. CJS interop: default-import + destructure. Verified it builds, appears
in the catalog, and degrades to "offline" gracefully without hardware. Real
on-VLAN test still pending.
[2026-06-15] feature | Device discovery (UHPPOTE scan in setup)
The frontend had no way to find a UHPPOTE — but the controllers self-announce via
UDP broadcast. Added a generic device-discovery capability: optional
DiscoverableDriver.discover() on the registry, implemented by the uhppote
driver via getDevices. New admin-only GET /api/setup/discover/:driverId
(health-checks each found device); catalog now returns a discoverable list.
SetupWizard gains a "Scan for controllers" button that lists found devices with
health badges and auto-fills serial + host on selection. Verified: catalog flags
uhppote; discover runs and fails gracefully without hardware (broadcast EACCES);
non-discoverable driver → 400; no token → 401. Modeled generically so cameras
(ONVIF) can add discovery later.
[2026-06-15] test+blocker | UHPPOTE hardware bring-up + entry-flow blocker
Brought up the real UHPPOTE (serial 225088491, fw 09120) end to end. Fixed the networking path: WSL2 mirrored mode, then driver bugs — subnet-directed broadcast (the lib doesn't enable SO_BROADCAST for global 255.255.255.255), broadcast must match the target's subnet for unicast reply routing (health-check timeout fix), multi-subnet discovery, and serialized I/O (concurrent calls collided on :60001). Added .env loading (Node --env-file), env-gated+fail-closed SETUP_AUTH_BYPASS, and an authBypass flag so the wizard drops the token field. Test scripts in apps/server/scripts/ (uhppote-listen, uhppote-relay).
VERIFIED on hardware: discovery; host-commanded openDoor doors 1&2 (physical + reason="remote open door"); button presses live (reason="push button ok").
BLOCKER FOUND: the controller push-button input auto-opens the relay in firmware — no command to report-without-opening — so ticket-first entry (button→print→open) is impossible as wired. UHPPOTE can't do it on that input; ZKTeco might via a programmable aux input + PULL SDK but that's unverified and needs a new driver. Recorded in access-controller-button-flow + zkteco-controller. Entry-lane hardware decision paused to focus on the business side.
[2026-06-15] feature | Cookie-based auth/authz (login, CSRF)
Built real authentication: bcrypt login → JWT in an HttpOnly+SameSite=Strict
cookie, readable CSRF cookie + X-CSRF-Token header (double-submit) on mutations,
role-guarded routes. Routes: /api/auth/{login,logout,me}. First admin seeded via
pnpm --filter @parking/server seed-admin. Removed the SETUP_AUTH_BYPASS shim
and the wizard token field; the SPA gates on /api/auth/me and only shows setup to
admins. Same-origin via the Vite dev proxy and a new prod nginx config
(deploy/nginx.conf). Verified end to end (curl + browser): wrong pass→401,
login→cookies set, me→admin, assign without CSRF→403 / with→201, no cookie→401,
session persists across reload. Updated local-jwt-auth.
[2026-06-15] lint+docs | Dev-environment pages (WSL networking, workflow)
Captured hard-won dev knowledge that was only in commit messages: new
wsl-dev-networking (WSL2 NAT blocks UDP broadcast → mirrored mode + the
multi-interface / subnet-broadcast / IPv6-localhost gotchas that remained) and
local-dev-workflow (setup, seed:admin, the dev-server-hang from the broken
strip-types script → tsx, the 127.0.0.1 proxy fix, .env loading). Corrected the
earlier "broadcast permission (EACCES)" note in device-discovery — the real
cause was the lib not enabling SO_BROADCAST for the global 255.255.255.255;
documented the three verified broadcast gotchas + serialization. Added a reference
page type to the schema; new "Dev environment" index section.
[2026-06-15] decision | Dingtian relay chosen; HTTP over MQTT; unmanned direction
New relay+input controller on hand (Dingtian 4ch). Its inputs are decoupled from relays (configurable via input_link_relay) — solves the access-controller-button-flow blocker the UHPPOTE couldn't. Transport decision dingtian-vs-mqtt: direct HTTP/UDP now (UDP string for relay control on :60001; device input_link_url HTTP push for button events), MQTT skipped (broker = infra + failure mode + overkill at this scale) but kept for later multi-lane scale. Recorded the stated roadmap to fully unmanned, no-booth operation in autonomous-direction and its threat-model shift (operator-fraud → unattended-machine threats). New stub dingtian-relay with the full protocol from the SDK. Driver + on-hardware test still to build.
[2026-06-15] driver+test | Dingtian driver built; button blocker RESOLVED
Built the dingtian access driver (AccessControlDevice relay control + InputDevice
poll-based button events + new PreconditionDevice capability). Verified end to end on
real hardware (DT-R004 @ 10.0.10.172, HTTP config on :8080, UDP control :60001):
status read, relay pulse, input press/release. Disabled input_link_relay via the
driver's fixPreconditions (GET config → flag 0 + clear maps → POST config_set), then
confirmed: pressing inputs now fires NO relay (0000 status) — host-in-the-loop entry
works. The access-controller-button-flow blocker is RESOLVED. Gotcha recorded in
dingtian-relay: config_set requires injecting "command":"setconfig" after "status"
(GET omits it) or the write silently no-ops. Added httpPort config field (port 8080 ≠
default 80). Test script apps/server/scripts/dingtian-test.mjs. Next: input HTTP-push
endpoint + wiring input→ticket→pulseOpen.
[2026-06-15] cleanup | Remove UHPPOTE/ZKTeco code; wiki → rejected/historical
Neither UHPPOTE nor ZKTeco is used (Dingtian chosen). Removed their code:
deleted access-uhppote.ts, uhppoted.d.ts, access.ts (zkteco/esp32 stubs), the
three uhppote-*.mjs scripts; dropped the uhppoted npm dep from both packages;
unregistered uhppote/zkteco/esp32-relay from the driver registry; updated example
comments. Catalog access drivers now = dingtian only. Build green.
Wiki: kept the pages but marked uhppote-controller + zkteco-controller
rejected/historical, uhppote-vs-esp32 historical; re-pointed all "current
device" framing (standing-decisions, bom, overview, open-questions) to
dingtian-relay; noted no current driver uses device-discovery. Transferable
concepts (network-isolation, event-log-ingestion, barrier-not-a-door, threat-model)
kept as-is. Links lint clean; raw source untouched (immutable).
[2026-06-15] feature | Dingtian input HTTP-push → backend (no polling)
Wired the device's "Input Link URL" feature so it HTTP-pushes button events to
our backend — no polling. Driver configureInputPush() writes input_link_url
(per-input server/port/path, en=1, active-LOW, plain HTTP) via the config API
(reusing the #writeConfig + command:setconfig helper). New backend route
routes/devices.ts: public GET/POST /api/devices/dingtian/:deviceId/input/:n/{on,off}
→ emits onto an internal device-events bus (device-events.ts, EventEmitter) for
the entry flow to consume. VERIFIED on hardware: configured device, real presses
on all 4 inputs pushed to the backend (input N on+off, source = device IP). Trust
model recorded in device-input-flow: flat network / no VLAN → backend is source
of truth, every open is a signed event (out-of-band open = anomaly); push endpoint
not behind cookie auth (machine call), shared-secret available as defence-in-depth.
Next: wire signed event + ticket print + pulseOpen.
[2026-06-15] feature | Dingtian push auth via HTTP Digest (hardware-tested)
Secured the device→backend input push. Empirically tested auth options on the
device: HTTPS-to-self-signed FAILS, Basic works, Digest works → chose Digest
(MD5, qop=auth): password never on the wire, single-use nonces. Backend
digest-auth.ts (challenge/verify) + source-IP allowlist on the push route;
per-device pushUser/pushPassword generated on assign, written to the device and
stored in lane_devices (admin never types a URL/secret). Driver
configureInputPush now sets auth=2 + creds; the assign flow auto-configures the
device and persists the creds (net.ts derives the backend IP on the device's
subnet). Removed the earlier URL-token approach (token in URL is sniffable/logged).
TWO HARD-WON DEVICE BUGS fixed: (1) config_set requires an explicit Content-Length
— the device silently ignores chunked bodies (Node's default), which masqueraded
as "writes don't apply" all session; (2) the pass field caps at 31 chars →
use a 24-char password. Driver #writeConfig now polls-until-verified (device
reboots on apply). VERIFIED on hardware: assign auto-configures the device, then
all 4 inputs push with Digest auth, zero failures. Recorded in device-input-flow.
[2026-06-15] feature | Setup wizard: Test connection + Save & configure
Two-step device setup UX. New admin-only POST /api/setup/test (healthCheck + checkPreconditions, no save / no device change). The assign (Save) step now also fixes preconditions (disables input_link_relay) before configuring push — closing a gap where assigned devices could still auto-fire relays; fails the save with no DB row if device config fails (no orphan rows). SetupWizard wires the config fields → Test button (health badge + precondition warnings) → Save & configure button. Verified in-browser against the real device: Test shows ● ready + preconditions OK; Save persists the row AND writes the device's Input Link URL (push path matches the saved device id). Admin never logs into the device web UI. Updated first-run-setup.
[2026-06-15] feature | Device hardening: binary relay + relay_pw + disable channels
Hardened the Dingtian relay control for the flat (no-VLAN) network. Switched pulseOpen from the unauthenticated string protocol (:60001) to the binary protocol (:60000) with a relay password — the only authenticated relay option (frame verified on hardware: FF AA 03 ). New HardenableDevice capability: harden() sets a random relay_pw + disables unused channels (rs485/can/tcp×2/mqtt → p:255, keep UDP binary+string). Folded into the assign/Save flow (preconditions → harden → push); relayPassword stored in lane_devices. Verified end to end: assign configures + hardens the device, config API stays reachable, pulseOpen with the stored password fires the relay, without it is rejected.
⚠️ LESSON: enabling the device's HTTP CGI session check (session_en) on this firmware breaks the config-READ API (ECONNRESET) — locked us out, needed a FACTORY RESET to recover. harden() deliberately does NOT touch session_en. The open CGI API is accepted as flat-network reality; the signed log is the real guarantee. Recorded in device-input-flow + dingtian-relay.
[2026-06-14] query | Dingtian web-login rotation + CGI API is unauthenticated
While addressing "change the device's default admin/admin", traced the device web
UI JS (system.js) → the change-login endpoint is
GET /userset.cgi?<old_u>&<old_p>&<new_u>&<new_p>& (response &0&/& = success,
&2&/& = wrong old pw). Added a best-effort setWebLogin/#rotateWebLogin step
to harden() (new pw stored back as config webPassword, stripped from API
responses). KEY FINDING: the device CGI API needs NO authentication — config dump,
config write, relay fire, and userset.cgi itself all return 200 unauthenticated
(verified on 10.0.10.5). admin/admin gates only the browser UI; there is no
inbound-auth setting (only session_en, which bricks the read API). So rotating the
login is COSMETIC, not a boundary — the signed event log remains the real
guarantee. Recorded in dingtian-relay (new Hardening section).
[2026-06-14] ingest | Rongta 80mm printer driver + printer roles/failover
- Added
rongtaPrinterDevice driver (ESC/POS over raw TCP 9100); registered in registry. - Decision: ≥2 printers per lane by role (entry-dispenser outside, booth-receipt inside); entry ticket fails over outside→booth (asymmetric — receipts never print outside).
- Selection logic lives in packages/devices/printer-routing.ts (orderForRole, printWithFailover).
- One unit verified reachable at 10.0.10.6:9100 from host (TCP connect OK).
- New pages: rongta-printer, printer-roles-failover. Updated bom, index.
- Open: all-printers-down policy belongs to the (not-yet-built) entry flow, not the printer layer.
[2026-06-14] ingest | Live printer status monitoring
- Added MonitorableDevice.readStatus()/PrinterStatus capability in packages/devices.
- Rongta readStatus() scrapes the device's own /prn_stat.htm (Cover/Cutter/Paper End/Near End/ Off-Line) — chosen over hand-decoding DLE EOT because this clone's DLE EOT bytes don't match the canonical ESC/POS bit layout (verified on hardware; risk of false-healthy).
- Server PrinterMonitor: polls enabled monitorable printers (PRINTER_POLL_MS, default 5s), caches latest, emits "printer-status" on change. API: GET /api/printers/status + SSE stream.
- Verified live: 10.0.10.6 -> ready (all flags clear); unreachable host -> offline (no throw); bus emits on change, suppresses unchanged. Full repo typechecks (8/8).
- New page: printer-status-monitoring. Updated rongta-printer, index.
- Open: capture the page's actual text for an ACTIVE fault (pull paper / open cover) to confirm the Yes flip; wire degraded/offline into failover + entry-flow all-down policy.