server: GEE/Dingtian QR reader endpoint + synchronous ReadOutcome

The reader HTTP-GETs on each scan and beeps/acts on our JSON reply (host-in-the-
loop, synchronous). New route GET/POST /qa/mcardsea.php parses the SDK query,
runs the scan through the read dispatcher (permit match -> permit flow; else
transient exit), and replies the SDK verdict: status 1=valid (beep 2x) /
0=invalid (beep 1x), output, time-sync.

Refactored the read flows to return a ReadOutcome {accepted, direction, reason}
so the reply reflects the real accept/reject decision (ReadDispatcher.dispatch,
ExitFlow.handleAt, PermitFlow.run). Fire-and-forget readers ignore it.

Reader's lane is keyed off its serial (cjihao) as lane_devices.id for now;
endpoint is public (reader has no auth, on the device subnet).

Verified via inject: valid permit QR -> status:1 + open; re-scan -> permit exit;
unknown QR -> status:0; barrier-less lane -> status:0.
This commit is contained in:
2026-06-16 12:12:09 +02:00
parent f67c1ead87
commit 392d44d842
8 changed files with 173 additions and 30 deletions
+21 -5
View File
@@ -56,9 +56,25 @@ barrier. ([[device-input-flow]] is the analogous push pattern; this one also ret
open questions are **moot** — it's HTTP. Wiegand is the reader's *output line* on a valid read
(the reply `output` field), not the host transport.
## Next
## As-built (2026-06-16)
A backend route (like `routes/devices.ts` for the Dingtian push) that parses the GET, **decides**
(reuse the permit/exit lookup), replies the JSON verdict, and emits on the `read` bus. The
[[device-registry]] `reader` entry can model it for [[first-run-setup]] (server IP/port are set in
the vendor tool; the app side is the endpoint). Build-ready — protocol fully known.
- **Endpoint** `GET/POST /qa/mcardsea.php` (`apps/server/src/routes/qr-reader.ts`, public — the
reader has no auth, sits on the device subnet). Parses `cardid/mjihao/cjihao/status/time`, runs
the scan through the **read dispatcher** (permit match → permit flow; else transient exit), and
replies the **SDK verdict**: `status` 1=valid(beep 2×)/0=invalid(beep 1×), `output` 0, `time`.
- The read flows were refactored to **return a `ReadOutcome` { accepted, direction, reason }** so the
endpoint's reply reflects the real accept/reject (the dispatcher decides AND opens the barrier via
the flows). A fire-and-forget reader ignores the outcome.
- **Lane mapping:** the endpoint keys the reader's `lane_devices` id off the device **serial
(`cjihao`)** for now — so assign the reader with `lane_devices.id = <serial>`. Refine when the
setup wizard models the reader's server-side identity properly.
- Verified via inject: valid permit QR → `status:1` + open; re-scan → permit exit (still valid);
unknown QR → `status:0`; reader on a barrier-less lane → `status:0`.
## Open / to confirm on hardware
- A **live scan** still hadn't reached the server during bring-up (no beep). With the real endpoint
now replying the verdict, re-test: scan → expect a beep + a GET in the server log. If still
nothing, it's the reader's scan/trigger/mode (not the server).
- **Reader→lane identity:** confirm what the device actually sends as `cjihao`/`mjihao` and align the
`lane_devices` assignment (the wizard doesn't yet capture the reader's serial as its id).
+16
View File
@@ -640,3 +640,19 @@ guarantee. Recorded in [[dingtian-relay]] (new Hardening section).
[[index]]. SDK kept in place (bulky+binaries), not copied to raw/.
- NEXT: backend route — parse GET, DECIDE (reuse permit/exit lookup), reply JSON verdict, emit on
read bus. Refactor read flows to RETURN an outcome so the reply can reflect accept/reject.
## [2026-06-16] build+fix | QR reader endpoint + ReadOutcome refactor; dev-DB migrate fix
- DB FIX: dev server crashed `no such table: lane_devices`. Cause: server `.env` DATABASE_URL points
at `apps/server/parking.sqlite` (the old dev DB I'd moved aside during the ledger split; new
migrations added since). Applied `drizzle-kit migrate` to that path → all 14 tables present. Fresh
DB → needs `seed-admin` + device re-assignment (empty, expected).
- REFACTOR: read flows now RETURN a `ReadOutcome {accepted,direction,reason}` (device-events.ts).
`ReadDispatcher.dispatch`, `ExitFlow.handleAt`, `PermitFlow.run` updated. A synchronous reader can
answer the device; fire-and-forget readers ignore it.
- ENDPOINT: `routes/qr-reader.ts` — `GET/POST /qa/mcardsea.php` (public; reader has no auth, on the
device subnet). Parses the SDK GET, dispatches the scan, replies the SDK verdict (status 1/0 →
beep 2×/1×, output 0, time-sync). Reader's lane keyed off device serial (cjihao) as lane_devices.id
for now.
- VERIFIED via inject: valid permit QR→status:1+open; re-scan→permit exit; unknown→status:0; reader
on barrier-less lane→status:0. Full build 5/5.
- Updated [[gee-qr-er80]] (endpoint as-built + hardware open items).