Setup: manage multiple device instances per category (add/remove)

The data model was already multi-instance (lane_devices = one row per
instance; assign always inserts) -- the limitation was UI-only. Make the
whole flow support more than one of every category:

- Backend: add DELETE /api/setup/assign/:id (unassign by id). /state now
  redacts secrets (pushPassword/webPassword/relayPassword) via a shared
  redactSecrets() also used by /assign -- it was returning raw config rows.
- Web: SetupWizard reworked from one fixed slot per category into a list of
  assigned instances (driver/role/host + Remove) plus an "Add another" form.
  select-type config fields (e.g. printer role) now render as dropdowns.
- api.ts: add fetchState(), unassignDevice(), Assignment/SetupState types.

Verified via Fastify inject: two printers assigned to one lane both list,
no secret leak, delete -> 204, delete unknown -> 404, count drops to 1.
Full repo typechecks.

Wiki: first-run-setup documents multi-instance + delete + redaction.
This commit is contained in:
2026-06-14 20:39:39 +02:00
parent b2a0471b08
commit 39d4bac419
5 changed files with 299 additions and 61 deletions
+27 -1
View File
@@ -160,6 +160,32 @@ export interface AssignBody {
}
/** Save + configure the device (preconditions, push setup), then persist. */
export function assignDevice(body: AssignBody): Promise<{ id: string }> {
export function assignDevice(body: AssignBody): Promise<Assignment> {
return apiFetch("/api/setup/assign", { method: "POST", body: JSON.stringify(body) });
}
/** A persisted device assignment (one per instance; secrets stripped). */
export interface Assignment {
id: string;
lane: number;
category: DeviceCategory;
driverId: string;
config: DeviceConfig;
enabled: boolean;
createdAt?: string;
}
export interface SetupState {
completedAt: string | null;
assignments: Assignment[];
}
/** Current setup status + all assigned device instances. */
export function fetchState(): Promise<SetupState> {
return apiFetch<SetupState>("/api/setup/state");
}
/** Remove one assigned device instance by id. */
export function unassignDevice(id: string): Promise<void> {
return apiFetch(`/api/setup/assign/${id}`, { method: "DELETE" });
}