import { useEffect, useState } from "react"; import { discoverDevices, fetchCatalog, type Catalog, type CatalogEntry, type DeviceCategory, type DiscoveredDevice, } from "./api.js"; // First-run setup wizard (scaffold). The admin picks a device per category for a // lane from the driver catalog and fills in its connection config. Drivers that // support LAN discovery (e.g. UHPPOTE) get a "Scan" button that lists found // devices; selecting one auto-fills the config. See wiki/concepts/first-run-setup.md // and device-discovery.md. // // NOTE: discovery + assign require an admin token. Wiring the real login flow is // a follow-up; for now a token is read from a field so the scan can be exercised. const CATEGORIES: { key: DeviceCategory; title: string }[] = [ { key: "access", title: "Access controller" }, { key: "reader", title: "Reader" }, { key: "camera", title: "Camera (entry/exit snapshot)" }, { key: "printer", title: "Printer" }, ]; export function SetupWizard() { const [catalog, setCatalog] = useState(null); const [lane, setLane] = useState(1); const [picked, setPicked] = useState>>({}); const [token, setToken] = useState(""); const [error, setError] = useState(null); useEffect(() => { fetchCatalog().then(setCatalog).catch((e: Error) => setError(e.message)); }, []); if (error) return

Failed to load catalog: {error}

; if (!catalog) return

Loading device catalog…

; return (

First-run setup

{catalog.authBypass ? ( ⚠️ auth bypass on (testing) — no token needed ) : ( )}
{CATEGORIES.map(({ key, title }) => ( setPicked((p) => ({ ...p, [key]: id }))} /> ))}
); } function CategoryPicker({ title, entries, discoverableIds, token, authBypass, selectedId, onSelect, }: { title: string; entries: CatalogEntry[]; discoverableIds: string[]; token: string; authBypass: boolean; selectedId: string | undefined; onSelect: (id: string) => void; }) { const selected = entries.find((e) => e.id === selectedId); const canDiscover = selected != null && discoverableIds.includes(selected.id); // Config values (auto-filled by discovery, editable by hand). const [config, setConfig] = useState>({}); const [found, setFound] = useState(null); const [scanning, setScanning] = useState(false); const [scanError, setScanError] = useState(null); async function scan() { if (!selected) return; setScanning(true); setScanError(null); try { setFound(await discoverDevices(token, selected.id)); } catch (e) { setScanError((e as Error).message); } finally { setScanning(false); } } function applyDiscovered(d: DiscoveredDevice) { setConfig((c) => ({ ...c, ...(d.config as Record) })); } return (
{title} {entries.length === 0 ? ( No drivers registered. ) : ( )} {selected && (

{selected.description}

{canDiscover && (
{!authBypass && !token && ( enter an admin token to scan )} {scanError && {scanError}} {found && found.length === 0 &&

No controllers found on the LAN.

} {found && found.length > 0 && (
    {found.map((d) => (
  • {" "} {d.label}{" "} {d.info?.firmware && · fw {d.info.firmware}}
  • ))}
)}
)} {selected.configFields.map((f) => (
))}
)}
); } function HealthBadge({ status }: { status: string }) { const color = status === "ready" ? "#16a34a" : status === "degraded" ? "#d97706" : "#dc2626"; return ● {status}; }