72ba4099ea
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.
49 lines
1.4 KiB
TypeScript
49 lines
1.4 KiB
TypeScript
// Thin API client for the operator/admin UI.
|
|
|
|
export interface ConfigField {
|
|
key: string;
|
|
label: string;
|
|
type: "string" | "number" | "boolean" | "host" | "port" | "secret" | "select";
|
|
required: boolean;
|
|
default?: string | number | boolean;
|
|
options?: { value: string; label: string }[];
|
|
help?: string;
|
|
}
|
|
|
|
export interface CatalogEntry {
|
|
id: string;
|
|
label: string;
|
|
description: string;
|
|
transports: string[];
|
|
configFields: ConfigField[];
|
|
}
|
|
|
|
export type DeviceCategory = "access" | "reader" | "camera" | "printer";
|
|
export type Catalog = Record<DeviceCategory, CatalogEntry[]>;
|
|
|
|
export async function fetchCatalog(): Promise<Catalog> {
|
|
const res = await fetch("/api/setup/catalog");
|
|
if (!res.ok) throw new Error(`catalog: ${res.status}`);
|
|
return res.json() as Promise<Catalog>;
|
|
}
|
|
|
|
export interface AssignBody {
|
|
lane: number;
|
|
category: DeviceCategory;
|
|
driverId: string;
|
|
config: Record<string, string | number | boolean>;
|
|
}
|
|
|
|
export async function assignDevice(token: string, body: AssignBody): Promise<unknown> {
|
|
const res = await fetch("/api/setup/assign", {
|
|
method: "POST",
|
|
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
|
body: JSON.stringify(body),
|
|
});
|
|
if (!res.ok) {
|
|
const msg = (await res.json().catch(() => ({}))) as { error?: string };
|
|
throw new Error(msg.error ?? `assign: ${res.status}`);
|
|
}
|
|
return res.json();
|
|
}
|