Files
parking_solution/wiki/log.md
T
julian 4319fb86dc wiki: Dingtian relay decision, HTTP-over-MQTT, unmanned direction
- dingtian-relay: relay+input controller (4ch on hand). Inputs are decoupled
  from relays (configurable via input_link_relay) — solves the
  access-controller-button-flow blocker the UHPPOTE couldn't. Full protocol from
  the SDK (UDP string control :60001, `00` status parse, input_link_url push,
  multicast discovery). Driver + hardware test still to build.
- dingtian-vs-mqtt: use direct HTTP/UDP now; MQTT skipped (broker = extra infra
  + failure mode + overkill at one-host/few-devices scale) but kept for later
  multi-lane scale.
- autonomous-direction: record the roadmap to fully unmanned (no booth) and how
  it reshapes the threat model (operator-fraud -> unattended-machine threats),
  makes host-in-the-loop entry mandatory, and raises fail-state stakes.
- threat-model: note the unmanned shift. index + log.

gitignore the vendor SDK (dingtian/, 71MB of binaries/examples) — reference
only, protocol captured in the wiki.
2026-06-14 13:27:01 +02:00

7.0 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_state tables (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.

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.