feat(vision): configurability — SetupWizard ANPR toggle, footer health chip, env docs

Make the vision service genuinely configurable (was env-only).

- SetupWizard: an "ANPR" checkbox on the camera form (writes config.anpr; persisted
  only when on; sq+en) — opt-in is no longer raw JSON.
- DeviceMonitor optionally takes the VisionClient and probes /health each tick, emitting
  a "vision" pseudo-device → a Vision chip (ready/degraded/offline + recognizer) in the
  booth footer when VISION_ENABLED, no chip when off. Widened the DeviceStatus category
  union (server + web) + footer maps + devices.catVision. Verified: ready/fast_alpr when
  up, 0 chips when disabled.
- apps/vision/.env.example (Python service) + a VISION_* block in apps/server/.env.example
  (Node side) + a Configuration section in opencv-anpr-service.md covering all four
  layers and the caveats: the two processes share the VISION_ prefix but need SEPARATE
  .env files; bind /analyze to 127.0.0.1; cache model weights at deploy; an unbound anpr
  camera recognizes but every read is refused.

Build + lint green.

Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
2026-06-19 16:41:29 +02:00
parent 540b333b06
commit 4af8b56dda
13 changed files with 168 additions and 23 deletions
+33 -1
View File
@@ -197,7 +197,39 @@ the threshold on real on-site captures (angle/night/dirt). (4) the **weight-prov
**Bottom line: consume it as a gated advisory identity source feeding the existing `kind:"plate"` path
— not as sole authority — and Job 2 is still required for the anti-spoofing value.** The
adapter + the opt-in poll→read trigger are now **both built and verified end-to-end**; remaining is
field tuning (3), the provenance check (4), the SetupWizard `anpr` toggle, and Job 2.
field tuning (3), the provenance check (4), and Job 2.
## Configuration (2026-06-19)
Turning it on touches **four layers** — two env sets (one per process), per-camera data, and deploy.
The Python service and the Node server **both** read the `VISION_` prefix but are **separate
processes**, so give each its **own `.env`** (`apps/vision/.env` and `apps/server/.env`) — don't merge
them. `.env.example` files document both.
**1. The Python service (`apps/vision/.env`):** `VISION_RECOGNIZER=fast_alpr` (the default `stub`
recognizes nothing), `VISION_HOST`/`VISION_PORT` (prefer **`127.0.0.1`** — only the Node backend calls
`/analyze`, so don't expose it off-host), `VISION_OCR_MODEL`/`VISION_DETECTOR_MODEL` (leave defaults —
the AL-benchmark winners), `VISION_MIN_CONFIDENCE`. Install the models with `uv sync --extra alpr`;
weights download on first run, so **cache them at build/deploy** for the air-gapped appliance.
**2. The Node server (`apps/server/.env`):** `VISION_ENABLED=1` is the **master switch** (off by
default — nothing polls or shows without it); `VISION_URL` must match the service's host:port;
`VISION_TIMEOUT_MS` (slow-call cap so a lane never hangs), `VISION_POLL_MS`, `VISION_DEDUPE_MS`,
`VISION_MIN_CONFIDENCE` (re-applied client-side).
**3. Per-camera opt-in (device config, not env):** a camera does ANPR only when its config has **both**
`anpr: true` **and** a relay binding (`controllerId` + `relay`). The `anpr` flag is a **checkbox on the
camera form in the [[first-run-setup|SetupWizard]]** (built 2026-06-19). Without the binding the
[[entry-exit-points|dispatcher]] refuses every read ("reader not bound to a barrier") — so an
unbound ANPR camera recognizes but every read is rejected (and logged with its snapshot).
**4. Footer health:** when `VISION_ENABLED`, the [[device-status-monitoring|DeviceMonitor]] probes the
service's `/health` each tick and shows a **"Vision" chip** in the booth footer (ready/degraded/offline
+ the recognizer name); no chip when disabled. So the operator sees at a glance whether vision is up.
> **Network isolation** ([[network-isolation]]): cameras live on the isolated device VLAN, so the
> vision service must reach that VLAN to pull snapshots — but its own `/analyze` should bind
> **localhost** (Node is the only caller). Keep the AGPL/heavy stack contained to this process.
## Open
+4
View File
@@ -936,3 +936,7 @@ Wired the vision service into the entry/exit flows via the READ BUS. VisionReade
## [2026-06-19] feat | Persist every recognized plate + snapshot (ANPR audit trail, non-blocking)
VisionReader now PERSISTS every confident plate read so a recognition is investigable — and switched from emitRead to calling ReadDispatcher.dispatch directly (like qr-reader) to capture the OUTCOME. On a confident plate it: (1) stores the SNAPSHOT bytes in `snapshots` keyed by identity=PLATE — the SAME identity the flow signs its anomaly/event with — so GET /api/snapshots/by-identity/:plate (the booth event-detail modal's SnapshotStrip, which already uses e.identity) shows the car's photo against that anomaly with ZERO UI changes; (2) records an unsigned device_events{kind:"read"} breadcrumb with plate/confidence/region/modelVersion/snapshotId + the dispatch outcome (accepted + reason) = a queryable ANPR log independent of the signed ledger; (3) dispatches the read — NON-BLOCKING: a refused read just returns rejected (no barrier hold), logged with its snapshot for investigation. Plate stays advisory (exit demands payment; subscription matches only a bound plate). VERIFIED e2e: a recognized AL plate (AA558EE) with no open session → signed exit.refused.noSession anomaly (identity=plate), stored a 555KB snapshot under that plate, read-breadcrumb accepted:false reason:"no open session", and by-identity returned the image (status 200, 1 snapshot) → the refused read is investigable with its picture. Build+lint green. VisionReader constructor now takes the ReadDispatcher (wired in server.ts). Answers "is a recognized plate saved?" — now YES for both transient + subscriber, as telemetry + evidence image, regardless of match. Updated [[opencv-anpr-service]].
## [2026-06-19] feat | Vision service configuration — SetupWizard ANPR toggle, footer health chip, .env.example
Made the vision service genuinely configurable (was env-only). THREE additions: (1) SetupWizard CAMERA form now has an "ANPR / Njohja e targave" checkbox (writes config.anpr; only persisted when on; sq+en) — opt-in is no longer raw JSON. (2) DeviceMonitor now optionally takes the VisionClient and probes its /health each tick, emitting a "vision" pseudo-device status (id vision-service, category "vision" — widened the DeviceStatusEvent + frontend DeviceStatus category unions + the footer CATEGORY_KEY/ORDER maps + devices.catVision sq/en) → a "Vision · ready/degraded/offline" chip in the booth footer; NO chip when VISION_ENABLED off. Verified: emits ready/fast_alpr when up, 0 chips when disabled. (3) apps/vision/.env.example (the Python service env) + VISION_* block appended to apps/server/.env.example (the Node side) + a "Configuration" section in [[opencv-anpr-service]] documenting all FOUR layers (python env / node env / per-camera anpr+binding / footer health) and the caveats: the two processes SHARE the VISION_ prefix but need SEPARATE .env files; bind /analyze to 127.0.0.1 (Node is the only caller); models download on first run so cache at deploy; an unbound anpr camera recognizes but every read is refused. Build+lint green. Updated [[opencv-anpr-service]] (Configuration section; SetupWizard-toggle gap closed), vision README.