Device-agnostic driver registry + first-run setup

Make the device-adapter pattern selectable so the admin chooses hardware at
install — per lane, from a catalog of supported drivers. Adding a device =
registering one more driver; no business-logic change.

packages/devices:
- interfaces.ts: AccessControlDevice / ReaderDevice / CameraDevice / PrinterDevice
  (adds CameraDevice for entry/exit snapshot-on-event; access relay stays
  intent-only per "a barrier is not a door").
- registry.ts: driver catalog with per-driver config fields + factory, config
  validation, and a catalog payload for the setup UI.
- drivers/: stub adapters — access (zkteco, esp32-relay), reader (wiegand,
  tcp-ip), camera (hikvision, dahua). Real vendor protocols TBD.

packages/db:
- lane_devices + setup_state tables (migration 0001); re-export query helpers.

apps/server:
- routes/setup.ts: GET /api/setup/catalog (public schema), and admin-only
  /assign, /state, /complete with registry validation before persisting.
- extract auth.ts (requireJwtSecret, requireRole, JWT type aug).

apps/web:
- SetupWizard scaffold + api client: pick a driver per category for a lane,
  render its config fields.

wiki: device-registry + first-run-setup concept pages; cross-link from
device-adapter-pattern; index + log updated.

Verified: full turbo build (5/5); catalog lists all drivers; admin assign
persists; missing-config and no-token requests are rejected.
This commit is contained in:
2026-06-14 07:59:46 +02:00
parent 7de5c74500
commit 72ba4099ea
24 changed files with 1138 additions and 71 deletions
+60
View File
@@ -0,0 +1,60 @@
import type { DeviceHealth, ReaderDevice, ReaderEvent } from "../interfaces.js";
import type { DeviceConfig, ReaderDriver } from "../registry.js";
import { hostField, portField, stubLog } from "./common.js";
// Reader drivers (RF / optical). Two integration paths: Wiegand reads reach the
// access controller directly (autonomous); TCP-IP readers are seen host-side.
// See wiki/concepts/entry-exit-readers.md. STUBS only.
class StubReader implements ReaderDevice {
#cb: ((r: ReaderEvent) => void) | null = null;
constructor(
readonly driverId: string,
protected readonly config: DeviceConfig,
) {}
async connect(): Promise<void> {
stubLog(this.driverId, "connect");
}
async disconnect(): Promise<void> {
stubLog(this.driverId, "disconnect");
}
async healthCheck(): Promise<DeviceHealth> {
return { status: "ready", detail: "stub" };
}
onRead(cb: (r: ReaderEvent) => void): void {
this.#cb = cb;
stubLog(this.driverId, "onRead handler registered");
}
/** Test hook for stubs — real drivers emit from hardware events. */
protected emit(r: ReaderEvent): void {
this.#cb?.(r);
}
}
export const wiegandReaderDriver: ReaderDriver = {
id: "wiegand-reader",
category: "reader",
label: "Wiegand reader (into controller)",
description:
"RF/optical reader wired Wiegand 26/34 into the access controller's reader port. Autonomous offline decisions.",
transports: ["wiegand"],
configFields: [
{ key: "door", label: "Controller reader port / door", type: "number", required: true, default: 1 },
{ key: "format", label: "Wiegand format", type: "select", required: true, default: "26", options: [
{ value: "26", label: "Wiegand 26" },
{ value: "34", label: "Wiegand 34" },
] },
],
create: (c) => new StubReader("wiegand-reader", c),
};
export const tcpipReaderDriver: ReaderDriver = {
id: "tcpip-reader",
category: "reader",
label: "TCP/IP reader (host-side)",
description:
"Network RF/optical reader seen only by the host; host decides and commands the relay.",
transports: ["tcp-ip"],
configFields: [hostField, portField(9000)],
create: (c) => new StubReader("tcpip-reader", c),
};