2026-05-09 23:09:13 +00:00
|
|
|
/**
|
|
|
|
|
* Repository — typed accessor over the sqlite handle.
|
|
|
|
|
*
|
|
|
|
|
* Keeps prepared statements cached for the life of the connection. All
|
|
|
|
|
* mutating methods invoke the `notify` callback with (table, op, id) so the
|
|
|
|
|
* surrounding plugin can broadcast a `store.changed` event.
|
|
|
|
|
*
|
|
|
|
|
* NOT THREAD SAFE — node:sqlite is single-threaded, and so is Node. Don't
|
|
|
|
|
* cross workers with the same handle.
|
|
|
|
|
*/
|
|
|
|
|
import { randomBytes } from "node:crypto";
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
import { uuidv7 } from "uuidv7";
|
2026-05-25 23:47:24 +00:00
|
|
|
import type { Observable } from "@bsb/base";
|
2026-05-23 00:07:44 +00:00
|
|
|
import type { DbAdapter, RunResult, Row } from "./db-adapter.js";
|
2026-05-09 23:09:13 +00:00
|
|
|
|
|
|
|
|
import type {
|
|
|
|
|
ApiKey,
|
|
|
|
|
ApiKeyScope,
|
2026-05-14 05:38:18 +00:00
|
|
|
AuditActorType,
|
|
|
|
|
AuditEntry,
|
|
|
|
|
AuditResult,
|
2026-05-09 23:09:13 +00:00
|
|
|
Camera,
|
2026-05-26 00:38:43 +00:00
|
|
|
CameraEventSubscription,
|
2026-05-09 23:09:13 +00:00
|
|
|
CameraStream,
|
|
|
|
|
CameraType,
|
2026-05-23 00:34:03 +00:00
|
|
|
CloudAccount,
|
2026-05-09 23:09:13 +00:00
|
|
|
Display,
|
2026-05-10 21:18:44 +00:00
|
|
|
Entity,
|
|
|
|
|
EntityType,
|
2026-05-09 23:09:13 +00:00
|
|
|
EventLog,
|
2026-05-21 09:34:29 +00:00
|
|
|
EventQueryFilters,
|
2026-05-09 23:09:13 +00:00
|
|
|
EventSourceType,
|
2026-05-26 00:38:43 +00:00
|
|
|
EventSubscriptionStatus,
|
2026-05-13 18:56:42 +00:00
|
|
|
FirmwareChannel,
|
|
|
|
|
FirmwareRelease,
|
|
|
|
|
FirmwareRollout,
|
|
|
|
|
FirmwareRolloutState,
|
2026-05-12 23:18:22 +00:00
|
|
|
GpioDirection,
|
|
|
|
|
GpioEdge,
|
|
|
|
|
GpioPull,
|
2026-05-09 23:09:13 +00:00
|
|
|
Kiosk,
|
2026-05-12 23:18:22 +00:00
|
|
|
KioskGpioBinding,
|
2026-05-09 23:09:13 +00:00
|
|
|
KioskLabel,
|
2026-05-21 09:34:29 +00:00
|
|
|
KioskLog,
|
|
|
|
|
KioskLogLevel,
|
|
|
|
|
KioskLogQueryFilters,
|
2026-05-09 23:09:13 +00:00
|
|
|
Label,
|
|
|
|
|
LabelRole,
|
|
|
|
|
Layout,
|
|
|
|
|
LayoutCell,
|
|
|
|
|
LayoutTemplate,
|
2026-05-20 04:19:46 +00:00
|
|
|
OsUpdateRelease,
|
|
|
|
|
OsUpdateRollout,
|
|
|
|
|
OsUpdateRolloutState,
|
2026-05-09 23:09:13 +00:00
|
|
|
PairingCode,
|
|
|
|
|
Session,
|
|
|
|
|
SetupState,
|
|
|
|
|
StreamPolicy,
|
|
|
|
|
StreamRole,
|
|
|
|
|
User,
|
|
|
|
|
UserRole,
|
2026-05-24 00:48:32 +00:00
|
|
|
} from "../types.js";
|
2026-05-09 23:09:13 +00:00
|
|
|
import {
|
|
|
|
|
rowToApiKey,
|
2026-05-14 05:38:18 +00:00
|
|
|
rowToAuditEntry,
|
2026-05-09 23:09:13 +00:00
|
|
|
rowToCamera,
|
2026-05-26 00:38:43 +00:00
|
|
|
rowToCameraEventSubscription,
|
2026-05-23 00:34:03 +00:00
|
|
|
rowToCloudAccount,
|
2026-05-09 23:09:13 +00:00
|
|
|
rowToCameraStream,
|
|
|
|
|
rowToDisplay,
|
2026-05-10 21:18:44 +00:00
|
|
|
rowToEntity,
|
2026-05-09 23:09:13 +00:00
|
|
|
rowToEventLog,
|
2026-05-13 18:56:42 +00:00
|
|
|
rowToFirmwareRelease,
|
|
|
|
|
rowToFirmwareRollout,
|
2026-05-09 23:09:13 +00:00
|
|
|
rowToKiosk,
|
2026-05-12 23:18:22 +00:00
|
|
|
rowToKioskGpioBinding,
|
2026-05-21 09:34:29 +00:00
|
|
|
rowToKioskLog,
|
2026-05-09 23:09:13 +00:00
|
|
|
rowToLabel,
|
|
|
|
|
rowToLayout,
|
|
|
|
|
rowToLayoutCell,
|
|
|
|
|
rowToLayoutTemplate,
|
2026-05-20 04:19:46 +00:00
|
|
|
rowToOsUpdateRelease,
|
|
|
|
|
rowToOsUpdateRollout,
|
2026-05-09 23:09:13 +00:00
|
|
|
rowToPairingCode,
|
|
|
|
|
rowToSession,
|
|
|
|
|
rowToSetupState,
|
|
|
|
|
rowToUser,
|
|
|
|
|
} from "./mappers.js";
|
2026-05-23 10:55:04 +00:00
|
|
|
import { J, isoIn, isoNow, j } from "./util.js";
|
2026-05-09 23:09:13 +00:00
|
|
|
|
|
|
|
|
type NotifyFn = (
|
|
|
|
|
table: string,
|
|
|
|
|
op: "create" | "update" | "delete",
|
|
|
|
|
id?: string | number,
|
|
|
|
|
) => Promise<void>;
|
|
|
|
|
|
|
|
|
|
export class Repository {
|
2026-05-23 00:07:44 +00:00
|
|
|
readonly adapter: DbAdapter;
|
2026-05-09 23:09:13 +00:00
|
|
|
private readonly notify: NotifyFn;
|
2026-05-25 23:47:24 +00:00
|
|
|
private _obs?: Observable;
|
2026-05-09 23:09:13 +00:00
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
constructor(adapter: DbAdapter, notify: NotifyFn) {
|
|
|
|
|
this.adapter = adapter;
|
2026-05-09 23:09:13 +00:00
|
|
|
this.notify = notify;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 23:47:24 +00:00
|
|
|
/** Set a per-request observable for DB call tracing. */
|
|
|
|
|
withObs(obs: Observable): this {
|
|
|
|
|
this._obs = obs;
|
|
|
|
|
return this;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Clear the per-request observable. */
|
|
|
|
|
clearObs(): this {
|
|
|
|
|
this._obs = undefined;
|
|
|
|
|
return this;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
/** Run a write statement. Params are passed as an array. */
|
2026-05-25 23:47:24 +00:00
|
|
|
private async _run(sql: string, params: unknown[] = []): Promise<RunResult> {
|
|
|
|
|
const span = this._obs?.startSpan("db.run", { "db.statement": sql.slice(0, 100) });
|
|
|
|
|
try {
|
|
|
|
|
const result = await this.adapter.run(sql, params as any);
|
|
|
|
|
span?.end();
|
|
|
|
|
return result;
|
|
|
|
|
} catch (err) {
|
|
|
|
|
span?.log.error("db error: {err}", { err: (err as Error).message });
|
|
|
|
|
span?.end();
|
|
|
|
|
throw err;
|
|
|
|
|
}
|
2026-05-23 00:07:44 +00:00
|
|
|
}
|
|
|
|
|
/** Single-row query. */
|
2026-05-25 23:47:24 +00:00
|
|
|
private async _get<T = Row>(sql: string, params: unknown[] = []): Promise<T | undefined> {
|
|
|
|
|
const span = this._obs?.startSpan("db.get", { "db.statement": sql.slice(0, 100) });
|
|
|
|
|
try {
|
|
|
|
|
const result = await this.adapter.get<T>(sql, params as any);
|
|
|
|
|
span?.end();
|
|
|
|
|
return result;
|
|
|
|
|
} catch (err) {
|
|
|
|
|
span?.log.error("db error: {err}", { err: (err as Error).message });
|
|
|
|
|
span?.end();
|
|
|
|
|
throw err;
|
|
|
|
|
}
|
2026-05-23 00:07:44 +00:00
|
|
|
}
|
|
|
|
|
/** Multi-row query. */
|
2026-05-25 23:47:24 +00:00
|
|
|
private async _all<T = Row>(sql: string, params: unknown[] = []): Promise<T[]> {
|
|
|
|
|
const span = this._obs?.startSpan("db.all", { "db.statement": sql.slice(0, 100) });
|
|
|
|
|
try {
|
|
|
|
|
const result = await this.adapter.all<T>(sql, params as any);
|
|
|
|
|
span?.end();
|
|
|
|
|
return result;
|
|
|
|
|
} catch (err) {
|
|
|
|
|
span?.log.error("db error: {err}", { err: (err as Error).message });
|
|
|
|
|
span?.end();
|
|
|
|
|
throw err;
|
|
|
|
|
}
|
2026-05-23 00:07:44 +00:00
|
|
|
}
|
|
|
|
|
/** Execute DDL. */
|
|
|
|
|
private _exec(sql: string): Promise<void> {
|
|
|
|
|
return this.adapter.exec(sql);
|
2026-05-09 23:09:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Ad-hoc transaction. */
|
2026-05-23 00:07:44 +00:00
|
|
|
async transact<T>(fn: () => Promise<T>): Promise<T> {
|
|
|
|
|
return this.adapter.transaction(fn);
|
2026-05-09 23:09:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===========================================================================
|
|
|
|
|
// setup_state
|
|
|
|
|
// ===========================================================================
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async getSetupState(): Promise<SetupState> {
|
|
|
|
|
const r = await this._get("SELECT * FROM setup_state WHERE id = 1");
|
2026-05-09 23:09:13 +00:00
|
|
|
if (!r) throw new Error("setup_state row missing");
|
|
|
|
|
return rowToSetupState(r as Record<string, unknown>);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async isSetupComplete(): Promise<boolean> {
|
|
|
|
|
return (await this.getSetupState()).is_complete && (await this.countUsers()) > 0;
|
2026-05-09 23:09:13 +00:00
|
|
|
}
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async markSetupComplete(): Promise<void> {
|
|
|
|
|
await this._run(
|
2026-05-09 23:09:13 +00:00
|
|
|
`UPDATE setup_state
|
2026-05-24 03:18:43 +00:00
|
|
|
SET is_complete = ?,
|
2026-05-09 23:09:13 +00:00
|
|
|
completed_at = COALESCE(completed_at, ?)
|
|
|
|
|
WHERE id = 1`,
|
2026-05-24 03:18:43 +00:00
|
|
|
[true, isoNow()],
|
2026-05-23 00:07:44 +00:00
|
|
|
);
|
2026-05-09 23:09:13 +00:00
|
|
|
void this.notify("setup_state", "update", 1);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async setSetupExtra(key: string, value: unknown): Promise<void> {
|
|
|
|
|
const cur = (await this.getSetupState()).extras;
|
2026-05-09 23:09:13 +00:00
|
|
|
cur[key] = value;
|
2026-05-23 00:07:44 +00:00
|
|
|
await this._run("UPDATE setup_state SET extras = ? WHERE id = 1", [J(cur)]);
|
2026-05-09 23:09:13 +00:00
|
|
|
}
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async getSetupExtra(key: string): Promise<unknown> {
|
|
|
|
|
return (await this.getSetupState()).extras[key];
|
2026-05-09 23:09:13 +00:00
|
|
|
}
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async markClusterKeyProvisioned(): Promise<void> {
|
|
|
|
|
await this._run(
|
2026-05-24 03:18:43 +00:00
|
|
|
"UPDATE setup_state SET cluster_key_provisioned = ? WHERE id = 1",
|
|
|
|
|
[true],
|
2026-05-23 00:07:44 +00:00
|
|
|
);
|
2026-05-09 23:09:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===========================================================================
|
|
|
|
|
// users
|
|
|
|
|
// ===========================================================================
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async countUsers(): Promise<number> {
|
|
|
|
|
const r = await this._get<{ c: number }>("SELECT COUNT(*) AS c FROM users");
|
2026-05-09 23:09:13 +00:00
|
|
|
return r?.c ?? 0;
|
|
|
|
|
}
|
|
|
|
|
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
async getUserById(id: string): Promise<User | null> {
|
2026-05-23 00:07:44 +00:00
|
|
|
const r = await this._get("SELECT * FROM users WHERE id = ?", [id]);
|
2026-05-09 23:09:13 +00:00
|
|
|
return r ? rowToUser(r as Record<string, unknown>) : null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async getUserByUsername(username: string): Promise<User | null> {
|
|
|
|
|
const r = await this._get("SELECT * FROM users WHERE username = ?", [username]);
|
2026-05-09 23:09:13 +00:00
|
|
|
return r ? rowToUser(r as Record<string, unknown>) : null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async createUser(input: {
|
2026-05-09 23:09:13 +00:00
|
|
|
username: string;
|
|
|
|
|
password_hash: string;
|
|
|
|
|
role?: UserRole;
|
|
|
|
|
must_change_password?: boolean;
|
2026-05-23 00:07:44 +00:00
|
|
|
}): Promise<User> {
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
const id = uuidv7();
|
2026-05-09 23:09:13 +00:00
|
|
|
const role: UserRole = input.role ?? "operator";
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
await this._run(
|
|
|
|
|
`INSERT INTO users (id, username, password_hash, role, is_active, must_change_password)
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?)`,
|
2026-05-23 00:07:44 +00:00
|
|
|
[
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
id,
|
2026-05-23 00:07:44 +00:00
|
|
|
input.username,
|
|
|
|
|
input.password_hash,
|
|
|
|
|
role,
|
2026-05-23 10:55:04 +00:00
|
|
|
true,
|
|
|
|
|
Boolean(input.must_change_password),
|
2026-05-23 00:07:44 +00:00
|
|
|
],
|
2026-05-09 23:09:13 +00:00
|
|
|
);
|
|
|
|
|
void this.notify("users", "create", id);
|
2026-05-23 00:07:44 +00:00
|
|
|
const u = await this.getUserById(id);
|
2026-05-09 23:09:13 +00:00
|
|
|
if (!u) throw new Error("user vanished after insert");
|
|
|
|
|
return u;
|
|
|
|
|
}
|
|
|
|
|
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
async updateUser(id: string, patch: Partial<User>): Promise<void> {
|
2026-05-09 23:09:13 +00:00
|
|
|
const cols: string[] = [];
|
|
|
|
|
const vals: unknown[] = [];
|
|
|
|
|
if ("password_hash" in patch) {
|
|
|
|
|
cols.push("password_hash = ?");
|
|
|
|
|
vals.push(patch.password_hash);
|
|
|
|
|
}
|
|
|
|
|
if ("totp_enabled" in patch) {
|
|
|
|
|
cols.push("totp_enabled = ?");
|
2026-05-23 10:55:04 +00:00
|
|
|
vals.push(Boolean(patch.totp_enabled));
|
2026-05-09 23:09:13 +00:00
|
|
|
}
|
|
|
|
|
if ("totp_secret_encrypted" in patch) {
|
|
|
|
|
cols.push("totp_secret_encrypted = ?");
|
|
|
|
|
vals.push(patch.totp_secret_encrypted);
|
|
|
|
|
}
|
|
|
|
|
if ("recovery_codes_hashed" in patch) {
|
|
|
|
|
cols.push("recovery_codes_hashed = ?");
|
|
|
|
|
vals.push(J(patch.recovery_codes_hashed));
|
|
|
|
|
}
|
|
|
|
|
if ("must_change_password" in patch) {
|
|
|
|
|
cols.push("must_change_password = ?");
|
2026-05-23 10:55:04 +00:00
|
|
|
vals.push(Boolean(patch.must_change_password));
|
2026-05-09 23:09:13 +00:00
|
|
|
}
|
|
|
|
|
if ("failed_login_count" in patch) {
|
|
|
|
|
cols.push("failed_login_count = ?");
|
|
|
|
|
vals.push(patch.failed_login_count);
|
|
|
|
|
}
|
|
|
|
|
if ("locked_until" in patch) {
|
|
|
|
|
cols.push("locked_until = ?");
|
|
|
|
|
vals.push(patch.locked_until);
|
|
|
|
|
}
|
|
|
|
|
if ("last_login_at" in patch) {
|
|
|
|
|
cols.push("last_login_at = ?");
|
|
|
|
|
vals.push(patch.last_login_at);
|
|
|
|
|
}
|
|
|
|
|
if ("is_active" in patch) {
|
|
|
|
|
cols.push("is_active = ?");
|
2026-05-23 10:55:04 +00:00
|
|
|
vals.push(Boolean(patch.is_active));
|
2026-05-09 23:09:13 +00:00
|
|
|
}
|
|
|
|
|
if (cols.length === 0) return;
|
|
|
|
|
vals.push(id);
|
2026-05-23 00:07:44 +00:00
|
|
|
await this._run(`UPDATE users SET ${cols.join(", ")} WHERE id = ?`, vals);
|
2026-05-09 23:09:13 +00:00
|
|
|
void this.notify("users", "update", id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===========================================================================
|
|
|
|
|
// sessions
|
|
|
|
|
// ===========================================================================
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async createSession(input: {
|
2026-05-09 23:09:13 +00:00
|
|
|
id: string;
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
user_id: string;
|
2026-05-09 23:09:13 +00:00
|
|
|
csrf_token: string;
|
|
|
|
|
totp_pending: boolean;
|
|
|
|
|
user_agent: string | null;
|
|
|
|
|
ip_address: string | null;
|
|
|
|
|
expires_at: string; // absolute
|
2026-05-23 00:07:44 +00:00
|
|
|
}): Promise<Session> {
|
|
|
|
|
await this._run(
|
2026-05-09 23:09:13 +00:00
|
|
|
`INSERT INTO sessions
|
|
|
|
|
(id, user_id, csrf_token, totp_pending, user_agent, ip_address, expires_at)
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
2026-05-23 00:07:44 +00:00
|
|
|
[
|
|
|
|
|
input.id,
|
|
|
|
|
input.user_id,
|
|
|
|
|
input.csrf_token,
|
2026-05-23 10:55:04 +00:00
|
|
|
Boolean(input.totp_pending),
|
2026-05-23 00:07:44 +00:00
|
|
|
input.user_agent,
|
|
|
|
|
input.ip_address,
|
|
|
|
|
input.expires_at,
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
const s = await this.getSessionById(input.id);
|
2026-05-09 23:09:13 +00:00
|
|
|
if (!s) throw new Error("session vanished after insert");
|
|
|
|
|
return s;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async getSessionById(id: string): Promise<Session | null> {
|
|
|
|
|
const r = await this._get("SELECT * FROM sessions WHERE id = ?", [id]);
|
2026-05-09 23:09:13 +00:00
|
|
|
return r ? rowToSession(r as Record<string, unknown>) : null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async touchSession(id: string, lastSeenAt: string): Promise<void> {
|
|
|
|
|
await this._run("UPDATE sessions SET last_seen_at = ? WHERE id = ?", [
|
2026-05-09 23:09:13 +00:00
|
|
|
lastSeenAt,
|
|
|
|
|
id,
|
2026-05-23 00:07:44 +00:00
|
|
|
]);
|
2026-05-09 23:09:13 +00:00
|
|
|
}
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async setSessionTotpPending(id: string, pending: boolean): Promise<void> {
|
|
|
|
|
await this._run("UPDATE sessions SET totp_pending = ? WHERE id = ?", [
|
2026-05-23 10:55:04 +00:00
|
|
|
pending,
|
2026-05-09 23:09:13 +00:00
|
|
|
id,
|
2026-05-23 00:07:44 +00:00
|
|
|
]);
|
2026-05-09 23:09:13 +00:00
|
|
|
}
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async revokeSession(id: string): Promise<void> {
|
|
|
|
|
await this._run("UPDATE sessions SET revoked_at = ? WHERE id = ?", [isoNow(), id]);
|
2026-05-09 23:09:13 +00:00
|
|
|
}
|
|
|
|
|
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
async revokeAllSessionsForUser(userId: string): Promise<void> {
|
2026-05-23 00:07:44 +00:00
|
|
|
await this._run(
|
2026-05-09 23:09:13 +00:00
|
|
|
`UPDATE sessions SET revoked_at = ?
|
|
|
|
|
WHERE user_id = ? AND revoked_at IS NULL`,
|
2026-05-23 00:07:44 +00:00
|
|
|
[isoNow(), userId],
|
|
|
|
|
);
|
2026-05-09 23:09:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===========================================================================
|
|
|
|
|
// api_keys
|
|
|
|
|
// ===========================================================================
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async createApiKey(input: {
|
2026-05-09 23:09:13 +00:00
|
|
|
name: string;
|
|
|
|
|
key_hash: string;
|
|
|
|
|
key_prefix: string;
|
|
|
|
|
scopes: ApiKeyScope[];
|
|
|
|
|
expires_at: string | null;
|
2026-05-23 00:07:44 +00:00
|
|
|
}): Promise<ApiKey> {
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
const id = uuidv7();
|
|
|
|
|
await this._run(
|
|
|
|
|
`INSERT INTO api_keys (id, name, key_hash, key_prefix, scopes, expires_at)
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?)`,
|
2026-05-23 00:07:44 +00:00
|
|
|
[
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
id,
|
2026-05-23 00:07:44 +00:00
|
|
|
input.name,
|
|
|
|
|
input.key_hash,
|
|
|
|
|
input.key_prefix,
|
|
|
|
|
J(input.scopes),
|
|
|
|
|
input.expires_at,
|
|
|
|
|
],
|
2026-05-09 23:09:13 +00:00
|
|
|
);
|
|
|
|
|
void this.notify("api_keys", "create", id);
|
2026-05-23 00:07:44 +00:00
|
|
|
const k = await this.getApiKeyById(id);
|
2026-05-09 23:09:13 +00:00
|
|
|
if (!k) throw new Error("api_key vanished after insert");
|
|
|
|
|
return k;
|
|
|
|
|
}
|
|
|
|
|
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
async getApiKeyById(id: string): Promise<ApiKey | null> {
|
2026-05-23 00:07:44 +00:00
|
|
|
const r = await this._get("SELECT * FROM api_keys WHERE id = ?", [id]);
|
2026-05-09 23:09:13 +00:00
|
|
|
return r ? rowToApiKey(r as Record<string, unknown>) : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Lookup all candidates for a given prefix (typically returns 0 or 1). */
|
2026-05-23 00:07:44 +00:00
|
|
|
async listApiKeysByPrefix(prefix: string): Promise<ApiKey[]> {
|
|
|
|
|
const rs = await this._all(
|
2026-05-09 23:09:13 +00:00
|
|
|
"SELECT * FROM api_keys WHERE key_prefix = ? AND revoked_at IS NULL",
|
2026-05-23 00:07:44 +00:00
|
|
|
[prefix],
|
|
|
|
|
);
|
2026-05-09 23:09:13 +00:00
|
|
|
return rs.map((r) => rowToApiKey(r as Record<string, unknown>));
|
|
|
|
|
}
|
|
|
|
|
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
async touchApiKey(id: string, ip: string | null): Promise<void> {
|
2026-05-23 00:07:44 +00:00
|
|
|
await this._run(
|
2026-05-09 23:09:13 +00:00
|
|
|
"UPDATE api_keys SET last_used_at = ?, last_used_ip = ? WHERE id = ?",
|
2026-05-23 00:07:44 +00:00
|
|
|
[isoNow(), ip, id],
|
|
|
|
|
);
|
2026-05-09 23:09:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===========================================================================
|
|
|
|
|
// displays
|
|
|
|
|
// ===========================================================================
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async listDisplays(): Promise<Display[]> {
|
|
|
|
|
const rs = await this._all('SELECT * FROM displays ORDER BY "index"');
|
2026-05-09 23:09:13 +00:00
|
|
|
return rs.map((r) => rowToDisplay(r as Record<string, unknown>));
|
|
|
|
|
}
|
|
|
|
|
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
async getDisplayById(id: string): Promise<Display | null> {
|
2026-05-23 00:07:44 +00:00
|
|
|
const r = await this._get("SELECT * FROM displays WHERE id = ?", [id]);
|
2026-05-09 23:09:13 +00:00
|
|
|
return r ? rowToDisplay(r as Record<string, unknown>) : null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async createDefaultDisplay(): Promise<Display> {
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
const id = uuidv7();
|
|
|
|
|
await this._run(
|
|
|
|
|
`INSERT INTO displays (id, name, "index", is_primary)
|
|
|
|
|
VALUES (?, 'primary', 0, ?)`,
|
|
|
|
|
[id, false],
|
2026-05-23 00:07:44 +00:00
|
|
|
);
|
2026-05-09 23:09:13 +00:00
|
|
|
void this.notify("displays", "create", id);
|
2026-05-23 00:07:44 +00:00
|
|
|
const d = await this.getDisplayById(id);
|
2026-05-09 23:09:13 +00:00
|
|
|
if (!d) throw new Error("display vanished after insert");
|
|
|
|
|
return d;
|
|
|
|
|
}
|
|
|
|
|
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
async createDisplayForKiosk(kioskId: string, input: {
|
2026-05-10 19:39:09 +00:00
|
|
|
name: string;
|
|
|
|
|
index?: number;
|
|
|
|
|
width_px?: number;
|
|
|
|
|
height_px?: number;
|
2026-05-23 00:07:44 +00:00
|
|
|
}): Promise<Display> {
|
|
|
|
|
const idx = input.index ?? await this.nextDisplayIndexForKiosk(kioskId);
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
const id = uuidv7();
|
|
|
|
|
await this._run(
|
|
|
|
|
`INSERT INTO displays (id, name, "index", is_primary, kiosk_id, width_px, height_px)
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
2026-05-23 00:07:44 +00:00
|
|
|
[
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
id,
|
2026-05-23 00:07:44 +00:00
|
|
|
input.name,
|
|
|
|
|
idx,
|
2026-05-24 03:18:43 +00:00
|
|
|
false,
|
2026-05-23 00:07:44 +00:00
|
|
|
kioskId,
|
|
|
|
|
input.width_px ?? 1920,
|
|
|
|
|
input.height_px ?? 1080,
|
|
|
|
|
],
|
2026-05-10 19:39:09 +00:00
|
|
|
);
|
|
|
|
|
void this.notify("displays", "create", id);
|
2026-05-23 00:07:44 +00:00
|
|
|
const d = await this.getDisplayById(id);
|
2026-05-10 19:39:09 +00:00
|
|
|
if (!d) throw new Error("display vanished after insert");
|
|
|
|
|
return d;
|
|
|
|
|
}
|
|
|
|
|
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
async listDisplaysForKiosk(kioskId: string): Promise<Display[]> {
|
2026-05-23 00:07:44 +00:00
|
|
|
const rs = await this._all(
|
2026-05-10 19:39:09 +00:00
|
|
|
'SELECT * FROM displays WHERE kiosk_id = ? ORDER BY "index"',
|
2026-05-23 00:07:44 +00:00
|
|
|
[kioskId],
|
|
|
|
|
);
|
2026-05-10 19:39:09 +00:00
|
|
|
return rs.map((r) => rowToDisplay(r as Record<string, unknown>));
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-21 08:35:27 +00:00
|
|
|
/**
|
2026-05-21 09:54:25 +00:00
|
|
|
* Kiosks currently rendering this camera (active layout has a cell
|
|
|
|
|
* pointing at it). Subset of listKiosksWithCameraInBundle.
|
2026-05-21 08:35:27 +00:00
|
|
|
*/
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
async listKiosksRenderingCamera(cameraId: string): Promise<Kiosk[]> {
|
2026-05-23 00:07:44 +00:00
|
|
|
const rs = await this._all(
|
2026-05-21 08:35:27 +00:00
|
|
|
`SELECT DISTINCT k.*
|
|
|
|
|
FROM kiosks k
|
|
|
|
|
JOIN displays d ON d.kiosk_id = k.id
|
|
|
|
|
JOIN layout_cells lc ON lc.layout_id = d.active_layout_id
|
|
|
|
|
WHERE lc.camera_id = ?
|
|
|
|
|
AND d.active_layout_id IS NOT NULL
|
2026-05-26 00:08:09 +00:00
|
|
|
AND k.enabled = true`,
|
2026-05-23 00:07:44 +00:00
|
|
|
[cameraId],
|
|
|
|
|
);
|
2026-05-21 08:35:27 +00:00
|
|
|
return rs.map((r) => rowToKiosk(r as Record<string, unknown>));
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-21 09:54:25 +00:00
|
|
|
/**
|
|
|
|
|
* Kiosks that have this camera in ANY of their layouts (bundle-level).
|
|
|
|
|
* The kiosk's cached bundle includes the camera even when it's not the
|
|
|
|
|
* active layout, so snapshot requests via the kiosk LAN endpoint still
|
|
|
|
|
* resolve — the kiosk opens a short-lived RTSP connection from its own
|
|
|
|
|
* LAN position. Only when NO kiosk has the camera should the server
|
|
|
|
|
* fall back to pulling the stream itself.
|
|
|
|
|
*/
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
async listKiosksWithCameraInBundle(cameraId: string): Promise<Kiosk[]> {
|
2026-05-23 00:07:44 +00:00
|
|
|
const rs = await this._all(
|
2026-05-21 09:54:25 +00:00
|
|
|
`SELECT DISTINCT k.*
|
|
|
|
|
FROM kiosks k
|
|
|
|
|
JOIN displays d ON d.kiosk_id = k.id
|
2026-05-21 10:08:56 +00:00
|
|
|
JOIN display_layouts dl ON dl.display_id = d.id
|
|
|
|
|
JOIN layout_cells lc ON lc.layout_id = dl.layout_id
|
2026-05-21 09:54:25 +00:00
|
|
|
WHERE lc.camera_id = ?
|
2026-05-26 00:08:09 +00:00
|
|
|
AND k.enabled = true`,
|
2026-05-23 00:07:44 +00:00
|
|
|
[cameraId],
|
|
|
|
|
);
|
2026-05-21 09:54:25 +00:00
|
|
|
return rs.map((r) => rowToKiosk(r as Record<string, unknown>));
|
|
|
|
|
}
|
|
|
|
|
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
private async nextDisplayIndexForKiosk(kioskId: string): Promise<number> {
|
2026-05-23 00:07:44 +00:00
|
|
|
const r = await this._get<{ m: number | null }>('SELECT MAX("index") AS m FROM displays WHERE kiosk_id = ?', [kioskId]);
|
2026-05-10 19:39:09 +00:00
|
|
|
return (r?.m ?? -1) + 1;
|
|
|
|
|
}
|
|
|
|
|
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
async updateDisplay(id: string, patch: Partial<Display>): Promise<void> {
|
feat: layout/template/display CRUD + display-chain bundle routing
Major changes:
- Bundle now follows kiosk → display → layouts → cells → cameras
(no label filtering for v0.1)
- Setup creates default Fullscreen template + Default layout with
BetterFrame logo on the primary display
- Pairing auto-assigns kiosk to primary display
- Admin UI: full template CRUD with presets (fullscreen, 2x2, 1+3, 3x3)
- Admin UI: layout CRUD with cell management (assign cameras/web/html
to template regions)
- Admin UI: display editing (default layout, idle/sleep timeouts)
- Repository: added createLayoutTemplate, createLayout, createLayoutCell,
updateLayout, deleteLayout, layoutsForDisplayId, camerasForLayoutIds,
updateDisplay, and more
2026-05-10 01:45:53 +00:00
|
|
|
const sets: string[] = [];
|
|
|
|
|
const vals: unknown[] = [];
|
|
|
|
|
for (const [k, v] of Object.entries(patch)) {
|
|
|
|
|
if (k === "id") continue;
|
|
|
|
|
const col = k === "index" ? `"index"` : k;
|
|
|
|
|
sets.push(`${col} = ?`);
|
|
|
|
|
vals.push(v === undefined ? null : v);
|
|
|
|
|
}
|
|
|
|
|
if (sets.length === 0) return;
|
|
|
|
|
vals.push(id);
|
2026-05-23 00:07:44 +00:00
|
|
|
await this._run(`UPDATE displays SET ${sets.join(", ")} WHERE id = ?`, vals);
|
feat: layout/template/display CRUD + display-chain bundle routing
Major changes:
- Bundle now follows kiosk → display → layouts → cells → cameras
(no label filtering for v0.1)
- Setup creates default Fullscreen template + Default layout with
BetterFrame logo on the primary display
- Pairing auto-assigns kiosk to primary display
- Admin UI: full template CRUD with presets (fullscreen, 2x2, 1+3, 3x3)
- Admin UI: layout CRUD with cell management (assign cameras/web/html
to template regions)
- Admin UI: display editing (default layout, idle/sleep timeouts)
- Repository: added createLayoutTemplate, createLayout, createLayoutCell,
updateLayout, deleteLayout, layoutsForDisplayId, camerasForLayoutIds,
updateDisplay, and more
2026-05-10 01:45:53 +00:00
|
|
|
void this.notify("displays", "update", id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===========================================================================
|
|
|
|
|
// layout templates
|
|
|
|
|
// ===========================================================================
|
|
|
|
|
|
|
|
|
|
// ===========================================================================
|
|
|
|
|
// layouts
|
|
|
|
|
// ===========================================================================
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async listLayouts(): Promise<Layout[]> {
|
|
|
|
|
const rs = await this._all("SELECT * FROM layouts ORDER BY name");
|
feat: layout/template/display CRUD + display-chain bundle routing
Major changes:
- Bundle now follows kiosk → display → layouts → cells → cameras
(no label filtering for v0.1)
- Setup creates default Fullscreen template + Default layout with
BetterFrame logo on the primary display
- Pairing auto-assigns kiosk to primary display
- Admin UI: full template CRUD with presets (fullscreen, 2x2, 1+3, 3x3)
- Admin UI: layout CRUD with cell management (assign cameras/web/html
to template regions)
- Admin UI: display editing (default layout, idle/sleep timeouts)
- Repository: added createLayoutTemplate, createLayout, createLayoutCell,
updateLayout, deleteLayout, layoutsForDisplayId, camerasForLayoutIds,
updateDisplay, and more
2026-05-10 01:45:53 +00:00
|
|
|
return rs.map((r) => rowToLayout(r as Record<string, unknown>));
|
|
|
|
|
}
|
|
|
|
|
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
async getLayoutById(id: string): Promise<Layout | null> {
|
2026-05-23 00:07:44 +00:00
|
|
|
const r = await this._get("SELECT * FROM layouts WHERE id = ?", [id]);
|
feat: layout/template/display CRUD + display-chain bundle routing
Major changes:
- Bundle now follows kiosk → display → layouts → cells → cameras
(no label filtering for v0.1)
- Setup creates default Fullscreen template + Default layout with
BetterFrame logo on the primary display
- Pairing auto-assigns kiosk to primary display
- Admin UI: full template CRUD with presets (fullscreen, 2x2, 1+3, 3x3)
- Admin UI: layout CRUD with cell management (assign cameras/web/html
to template regions)
- Admin UI: display editing (default layout, idle/sleep timeouts)
- Repository: added createLayoutTemplate, createLayout, createLayoutCell,
updateLayout, deleteLayout, layoutsForDisplayId, camerasForLayoutIds,
updateDisplay, and more
2026-05-10 01:45:53 +00:00
|
|
|
return r ? rowToLayout(r as Record<string, unknown>) : null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-10 19:55:19 +00:00
|
|
|
/**
|
|
|
|
|
* @deprecated Use `listLayoutsForDisplay` which goes through the
|
|
|
|
|
* `display_layouts` join table. Kept as a thin alias for any
|
|
|
|
|
* callers still on the old API.
|
|
|
|
|
*/
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
async layoutsForDisplay(displayId: string): Promise<Layout[]> {
|
2026-05-10 19:55:19 +00:00
|
|
|
return this.listLayoutsForDisplay(displayId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** All layouts attached to the given display, via display_layouts. */
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
async listLayoutsForDisplay(displayId: string): Promise<Layout[]> {
|
2026-05-23 00:07:44 +00:00
|
|
|
const rs = await this._all(
|
2026-05-10 19:55:19 +00:00
|
|
|
`SELECT l.* FROM layouts l
|
|
|
|
|
JOIN display_layouts dl ON dl.layout_id = l.id
|
|
|
|
|
WHERE dl.display_id = ?
|
|
|
|
|
ORDER BY l.name`,
|
2026-05-23 00:07:44 +00:00
|
|
|
[displayId],
|
|
|
|
|
);
|
feat: layout/template/display CRUD + display-chain bundle routing
Major changes:
- Bundle now follows kiosk → display → layouts → cells → cameras
(no label filtering for v0.1)
- Setup creates default Fullscreen template + Default layout with
BetterFrame logo on the primary display
- Pairing auto-assigns kiosk to primary display
- Admin UI: full template CRUD with presets (fullscreen, 2x2, 1+3, 3x3)
- Admin UI: layout CRUD with cell management (assign cameras/web/html
to template regions)
- Admin UI: display editing (default layout, idle/sleep timeouts)
- Repository: added createLayoutTemplate, createLayout, createLayoutCell,
updateLayout, deleteLayout, layoutsForDisplayId, camerasForLayoutIds,
updateDisplay, and more
2026-05-10 01:45:53 +00:00
|
|
|
return rs.map((r) => rowToLayout(r as Record<string, unknown>));
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-10 19:55:19 +00:00
|
|
|
/** Inverse: all displays that have this layout attached. */
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
async listDisplaysForLayout(layoutId: string): Promise<Display[]> {
|
2026-05-23 00:07:44 +00:00
|
|
|
const rs = await this._all(
|
2026-05-10 19:55:19 +00:00
|
|
|
`SELECT d.* FROM displays d
|
|
|
|
|
JOIN display_layouts dl ON dl.display_id = d.id
|
|
|
|
|
WHERE dl.layout_id = ?
|
|
|
|
|
ORDER BY d."index"`,
|
2026-05-23 00:07:44 +00:00
|
|
|
[layoutId],
|
|
|
|
|
);
|
2026-05-10 19:55:19 +00:00
|
|
|
return rs.map((r) => rowToDisplay(r as Record<string, unknown>));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Idempotent attach. */
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
async attachLayoutToDisplay(displayId: string, layoutId: string): Promise<void> {
|
2026-05-23 00:07:44 +00:00
|
|
|
await this._run(
|
2026-05-10 19:55:19 +00:00
|
|
|
`INSERT OR IGNORE INTO display_layouts (display_id, layout_id)
|
|
|
|
|
VALUES (?, ?)`,
|
2026-05-23 00:07:44 +00:00
|
|
|
[displayId, layoutId],
|
|
|
|
|
);
|
2026-05-10 19:55:19 +00:00
|
|
|
void this.notify("display_layouts", "create", layoutId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Detach. If the display's default_layout_id pointed at this layout, clear it. */
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
async detachLayoutFromDisplay(displayId: string, layoutId: string): Promise<void> {
|
2026-05-23 00:07:44 +00:00
|
|
|
await this._run(
|
|
|
|
|
`DELETE FROM display_layouts WHERE display_id = ? AND layout_id = ?`,
|
|
|
|
|
[displayId, layoutId],
|
|
|
|
|
);
|
|
|
|
|
await this._run(
|
|
|
|
|
`UPDATE displays SET default_layout_id = NULL
|
|
|
|
|
WHERE id = ? AND default_layout_id = ?`,
|
|
|
|
|
[displayId, layoutId],
|
|
|
|
|
);
|
2026-05-10 19:55:19 +00:00
|
|
|
void this.notify("display_layouts", "delete", layoutId);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async createLayout(input: {
|
feat: layout/template/display CRUD + display-chain bundle routing
Major changes:
- Bundle now follows kiosk → display → layouts → cells → cameras
(no label filtering for v0.1)
- Setup creates default Fullscreen template + Default layout with
BetterFrame logo on the primary display
- Pairing auto-assigns kiosk to primary display
- Admin UI: full template CRUD with presets (fullscreen, 2x2, 1+3, 3x3)
- Admin UI: layout CRUD with cell management (assign cameras/web/html
to template regions)
- Admin UI: display editing (default layout, idle/sleep timeouts)
- Repository: added createLayoutTemplate, createLayout, createLayoutCell,
updateLayout, deleteLayout, layoutsForDisplayId, camerasForLayoutIds,
updateDisplay, and more
2026-05-10 01:45:53 +00:00
|
|
|
name: string;
|
|
|
|
|
description?: string | null;
|
|
|
|
|
priority?: string;
|
|
|
|
|
cooling_timeout_seconds?: number | null;
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
preload_camera_ids?: string[];
|
feat: layout/template/display CRUD + display-chain bundle routing
Major changes:
- Bundle now follows kiosk → display → layouts → cells → cameras
(no label filtering for v0.1)
- Setup creates default Fullscreen template + Default layout with
BetterFrame logo on the primary display
- Pairing auto-assigns kiosk to primary display
- Admin UI: full template CRUD with presets (fullscreen, 2x2, 1+3, 3x3)
- Admin UI: layout CRUD with cell management (assign cameras/web/html
to template regions)
- Admin UI: display editing (default layout, idle/sleep timeouts)
- Repository: added createLayoutTemplate, createLayout, createLayoutCell,
updateLayout, deleteLayout, layoutsForDisplayId, camerasForLayoutIds,
updateDisplay, and more
2026-05-10 01:45:53 +00:00
|
|
|
resets_idle_timer?: boolean;
|
2026-05-23 00:07:44 +00:00
|
|
|
}): Promise<Layout> {
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
const id = uuidv7();
|
|
|
|
|
await this._run(
|
|
|
|
|
`INSERT INTO layouts (id, name, description, priority, cooling_timeout_seconds, preload_camera_ids, resets_idle_timer)
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
2026-05-23 00:07:44 +00:00
|
|
|
[
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
id,
|
2026-05-23 00:07:44 +00:00
|
|
|
input.name,
|
|
|
|
|
input.description ?? null,
|
|
|
|
|
input.priority ?? "normal",
|
|
|
|
|
input.cooling_timeout_seconds ?? null,
|
|
|
|
|
J(input.preload_camera_ids ?? []),
|
2026-05-23 10:55:04 +00:00
|
|
|
Boolean(input.resets_idle_timer ?? true),
|
2026-05-23 00:07:44 +00:00
|
|
|
],
|
feat: layout/template/display CRUD + display-chain bundle routing
Major changes:
- Bundle now follows kiosk → display → layouts → cells → cameras
(no label filtering for v0.1)
- Setup creates default Fullscreen template + Default layout with
BetterFrame logo on the primary display
- Pairing auto-assigns kiosk to primary display
- Admin UI: full template CRUD with presets (fullscreen, 2x2, 1+3, 3x3)
- Admin UI: layout CRUD with cell management (assign cameras/web/html
to template regions)
- Admin UI: display editing (default layout, idle/sleep timeouts)
- Repository: added createLayoutTemplate, createLayout, createLayoutCell,
updateLayout, deleteLayout, layoutsForDisplayId, camerasForLayoutIds,
updateDisplay, and more
2026-05-10 01:45:53 +00:00
|
|
|
);
|
|
|
|
|
void this.notify("layouts", "create", id);
|
2026-05-23 00:07:44 +00:00
|
|
|
const r = await this.getLayoutById(id);
|
feat: layout/template/display CRUD + display-chain bundle routing
Major changes:
- Bundle now follows kiosk → display → layouts → cells → cameras
(no label filtering for v0.1)
- Setup creates default Fullscreen template + Default layout with
BetterFrame logo on the primary display
- Pairing auto-assigns kiosk to primary display
- Admin UI: full template CRUD with presets (fullscreen, 2x2, 1+3, 3x3)
- Admin UI: layout CRUD with cell management (assign cameras/web/html
to template regions)
- Admin UI: display editing (default layout, idle/sleep timeouts)
- Repository: added createLayoutTemplate, createLayout, createLayoutCell,
updateLayout, deleteLayout, layoutsForDisplayId, camerasForLayoutIds,
updateDisplay, and more
2026-05-10 01:45:53 +00:00
|
|
|
if (!r) throw new Error("layout vanished after insert");
|
|
|
|
|
return r;
|
|
|
|
|
}
|
|
|
|
|
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
async updateLayout(id: string, patch: Partial<Layout>): Promise<void> {
|
feat: layout/template/display CRUD + display-chain bundle routing
Major changes:
- Bundle now follows kiosk → display → layouts → cells → cameras
(no label filtering for v0.1)
- Setup creates default Fullscreen template + Default layout with
BetterFrame logo on the primary display
- Pairing auto-assigns kiosk to primary display
- Admin UI: full template CRUD with presets (fullscreen, 2x2, 1+3, 3x3)
- Admin UI: layout CRUD with cell management (assign cameras/web/html
to template regions)
- Admin UI: display editing (default layout, idle/sleep timeouts)
- Repository: added createLayoutTemplate, createLayout, createLayoutCell,
updateLayout, deleteLayout, layoutsForDisplayId, camerasForLayoutIds,
updateDisplay, and more
2026-05-10 01:45:53 +00:00
|
|
|
const sets: string[] = [];
|
|
|
|
|
const vals: unknown[] = [];
|
|
|
|
|
for (const [k, v] of Object.entries(patch)) {
|
2026-05-10 19:55:19 +00:00
|
|
|
if (k === "id" || k === "display_id") continue; // display_id deprecated
|
feat: layout/template/display CRUD + display-chain bundle routing
Major changes:
- Bundle now follows kiosk → display → layouts → cells → cameras
(no label filtering for v0.1)
- Setup creates default Fullscreen template + Default layout with
BetterFrame logo on the primary display
- Pairing auto-assigns kiosk to primary display
- Admin UI: full template CRUD with presets (fullscreen, 2x2, 1+3, 3x3)
- Admin UI: layout CRUD with cell management (assign cameras/web/html
to template regions)
- Admin UI: display editing (default layout, idle/sleep timeouts)
- Repository: added createLayoutTemplate, createLayout, createLayoutCell,
updateLayout, deleteLayout, layoutsForDisplayId, camerasForLayoutIds,
updateDisplay, and more
2026-05-10 01:45:53 +00:00
|
|
|
sets.push(`${k} = ?`);
|
2026-05-10 19:39:09 +00:00
|
|
|
if (k === "preload_camera_ids" || k === "regions") vals.push(J(v));
|
2026-05-23 10:55:04 +00:00
|
|
|
else if (typeof v === "boolean") vals.push(v);
|
feat: layout/template/display CRUD + display-chain bundle routing
Major changes:
- Bundle now follows kiosk → display → layouts → cells → cameras
(no label filtering for v0.1)
- Setup creates default Fullscreen template + Default layout with
BetterFrame logo on the primary display
- Pairing auto-assigns kiosk to primary display
- Admin UI: full template CRUD with presets (fullscreen, 2x2, 1+3, 3x3)
- Admin UI: layout CRUD with cell management (assign cameras/web/html
to template regions)
- Admin UI: display editing (default layout, idle/sleep timeouts)
- Repository: added createLayoutTemplate, createLayout, createLayoutCell,
updateLayout, deleteLayout, layoutsForDisplayId, camerasForLayoutIds,
updateDisplay, and more
2026-05-10 01:45:53 +00:00
|
|
|
else vals.push(v === undefined ? null : v);
|
|
|
|
|
}
|
|
|
|
|
if (sets.length === 0) return;
|
|
|
|
|
vals.push(id);
|
2026-05-23 00:07:44 +00:00
|
|
|
await this._run(`UPDATE layouts SET ${sets.join(", ")} WHERE id = ?`, vals);
|
feat: layout/template/display CRUD + display-chain bundle routing
Major changes:
- Bundle now follows kiosk → display → layouts → cells → cameras
(no label filtering for v0.1)
- Setup creates default Fullscreen template + Default layout with
BetterFrame logo on the primary display
- Pairing auto-assigns kiosk to primary display
- Admin UI: full template CRUD with presets (fullscreen, 2x2, 1+3, 3x3)
- Admin UI: layout CRUD with cell management (assign cameras/web/html
to template regions)
- Admin UI: display editing (default layout, idle/sleep timeouts)
- Repository: added createLayoutTemplate, createLayout, createLayoutCell,
updateLayout, deleteLayout, layoutsForDisplayId, camerasForLayoutIds,
updateDisplay, and more
2026-05-10 01:45:53 +00:00
|
|
|
void this.notify("layouts", "update", id);
|
|
|
|
|
}
|
|
|
|
|
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
async cloneLayout(id: string): Promise<Layout> {
|
2026-05-23 00:07:44 +00:00
|
|
|
const src = await this.getLayoutById(id);
|
feat(onvif-events): PullPoint subscription for all ONVIF cameras
New kiosk/src/onvif_events.rs: for each ONVIF camera in the bundle,
creates a PullPoint subscription, polls every 3s, parses
NotificationMessage XML into structured JSON (topic + source key/values
+ data key/values + timestamp), and POSTs to /api/kiosk/event with
source_type=onvif + camera_id.
Forwards ALL event topics: motion, ANPR (LicensePlateRecognition),
line crossing, intrusion, digital input, analytics, tamper — everything
the camera exposes. Node-RED sorts what matters.
Subscription lifecycle:
- CreatePullPointSubscription with 60s InitialTerminationTime
- Renew every 55s before timeout
- Unsubscribe on bundle change / shutdown
- Auto-resubscribe on pull/renew failure (30s backoff)
- Generation tracking via Weak<()> so old workers self-terminate
when start() is called with a new bundle
WSSE PasswordDigest auth for SOAP calls — same scheme the server's
onvif.ts uses. sha1 crate added.
BundleCamera extended with onvif_host/port/username/password_encrypted
fields (server already ships them; kiosk just wasn't deserializing).
Gated by BF_ENABLE_ONVIF_EVENTS=1. Enabled by default in the pi-gen
image env file.
TODO: cluster-key-based decryption of onvif_password_encrypted. For
now relies on the RTSP URI having plaintext credentials embedded (which
the ONVIF import path already ensures via rtspWithCredentials).
2026-05-21 10:03:30 +00:00
|
|
|
if (!src) throw new Error("layout not found");
|
|
|
|
|
|
|
|
|
|
let cloneName = `${src.name} (copy)`;
|
|
|
|
|
let suffix = 2;
|
2026-05-23 00:07:44 +00:00
|
|
|
while (await this._get("SELECT 1 FROM layouts WHERE name = ?", [cloneName])) {
|
feat(onvif-events): PullPoint subscription for all ONVIF cameras
New kiosk/src/onvif_events.rs: for each ONVIF camera in the bundle,
creates a PullPoint subscription, polls every 3s, parses
NotificationMessage XML into structured JSON (topic + source key/values
+ data key/values + timestamp), and POSTs to /api/kiosk/event with
source_type=onvif + camera_id.
Forwards ALL event topics: motion, ANPR (LicensePlateRecognition),
line crossing, intrusion, digital input, analytics, tamper — everything
the camera exposes. Node-RED sorts what matters.
Subscription lifecycle:
- CreatePullPointSubscription with 60s InitialTerminationTime
- Renew every 55s before timeout
- Unsubscribe on bundle change / shutdown
- Auto-resubscribe on pull/renew failure (30s backoff)
- Generation tracking via Weak<()> so old workers self-terminate
when start() is called with a new bundle
WSSE PasswordDigest auth for SOAP calls — same scheme the server's
onvif.ts uses. sha1 crate added.
BundleCamera extended with onvif_host/port/username/password_encrypted
fields (server already ships them; kiosk just wasn't deserializing).
Gated by BF_ENABLE_ONVIF_EVENTS=1. Enabled by default in the pi-gen
image env file.
TODO: cluster-key-based decryption of onvif_password_encrypted. For
now relies on the RTSP URI having plaintext credentials embedded (which
the ONVIF import path already ensures via rtspWithCredentials).
2026-05-21 10:03:30 +00:00
|
|
|
cloneName = `${src.name} (copy ${String(suffix)})`;
|
|
|
|
|
suffix++;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
const clone = await this.createLayout({
|
feat(onvif-events): PullPoint subscription for all ONVIF cameras
New kiosk/src/onvif_events.rs: for each ONVIF camera in the bundle,
creates a PullPoint subscription, polls every 3s, parses
NotificationMessage XML into structured JSON (topic + source key/values
+ data key/values + timestamp), and POSTs to /api/kiosk/event with
source_type=onvif + camera_id.
Forwards ALL event topics: motion, ANPR (LicensePlateRecognition),
line crossing, intrusion, digital input, analytics, tamper — everything
the camera exposes. Node-RED sorts what matters.
Subscription lifecycle:
- CreatePullPointSubscription with 60s InitialTerminationTime
- Renew every 55s before timeout
- Unsubscribe on bundle change / shutdown
- Auto-resubscribe on pull/renew failure (30s backoff)
- Generation tracking via Weak<()> so old workers self-terminate
when start() is called with a new bundle
WSSE PasswordDigest auth for SOAP calls — same scheme the server's
onvif.ts uses. sha1 crate added.
BundleCamera extended with onvif_host/port/username/password_encrypted
fields (server already ships them; kiosk just wasn't deserializing).
Gated by BF_ENABLE_ONVIF_EVENTS=1. Enabled by default in the pi-gen
image env file.
TODO: cluster-key-based decryption of onvif_password_encrypted. For
now relies on the RTSP URI having plaintext credentials embedded (which
the ONVIF import path already ensures via rtspWithCredentials).
2026-05-21 10:03:30 +00:00
|
|
|
name: cloneName,
|
|
|
|
|
description: src.description,
|
|
|
|
|
priority: src.priority,
|
|
|
|
|
cooling_timeout_seconds: src.cooling_timeout_seconds,
|
|
|
|
|
preload_camera_ids: src.preload_camera_ids,
|
|
|
|
|
resets_idle_timer: src.resets_idle_timer,
|
|
|
|
|
});
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
const cells = await this.listLayoutCells(id);
|
feat(onvif-events): PullPoint subscription for all ONVIF cameras
New kiosk/src/onvif_events.rs: for each ONVIF camera in the bundle,
creates a PullPoint subscription, polls every 3s, parses
NotificationMessage XML into structured JSON (topic + source key/values
+ data key/values + timestamp), and POSTs to /api/kiosk/event with
source_type=onvif + camera_id.
Forwards ALL event topics: motion, ANPR (LicensePlateRecognition),
line crossing, intrusion, digital input, analytics, tamper — everything
the camera exposes. Node-RED sorts what matters.
Subscription lifecycle:
- CreatePullPointSubscription with 60s InitialTerminationTime
- Renew every 55s before timeout
- Unsubscribe on bundle change / shutdown
- Auto-resubscribe on pull/renew failure (30s backoff)
- Generation tracking via Weak<()> so old workers self-terminate
when start() is called with a new bundle
WSSE PasswordDigest auth for SOAP calls — same scheme the server's
onvif.ts uses. sha1 crate added.
BundleCamera extended with onvif_host/port/username/password_encrypted
fields (server already ships them; kiosk just wasn't deserializing).
Gated by BF_ENABLE_ONVIF_EVENTS=1. Enabled by default in the pi-gen
image env file.
TODO: cluster-key-based decryption of onvif_password_encrypted. For
now relies on the RTSP URI having plaintext credentials embedded (which
the ONVIF import path already ensures via rtspWithCredentials).
2026-05-21 10:03:30 +00:00
|
|
|
for (const c of cells) {
|
2026-05-23 00:07:44 +00:00
|
|
|
await this.createLayoutCell({
|
feat(onvif-events): PullPoint subscription for all ONVIF cameras
New kiosk/src/onvif_events.rs: for each ONVIF camera in the bundle,
creates a PullPoint subscription, polls every 3s, parses
NotificationMessage XML into structured JSON (topic + source key/values
+ data key/values + timestamp), and POSTs to /api/kiosk/event with
source_type=onvif + camera_id.
Forwards ALL event topics: motion, ANPR (LicensePlateRecognition),
line crossing, intrusion, digital input, analytics, tamper — everything
the camera exposes. Node-RED sorts what matters.
Subscription lifecycle:
- CreatePullPointSubscription with 60s InitialTerminationTime
- Renew every 55s before timeout
- Unsubscribe on bundle change / shutdown
- Auto-resubscribe on pull/renew failure (30s backoff)
- Generation tracking via Weak<()> so old workers self-terminate
when start() is called with a new bundle
WSSE PasswordDigest auth for SOAP calls — same scheme the server's
onvif.ts uses. sha1 crate added.
BundleCamera extended with onvif_host/port/username/password_encrypted
fields (server already ships them; kiosk just wasn't deserializing).
Gated by BF_ENABLE_ONVIF_EVENTS=1. Enabled by default in the pi-gen
image env file.
TODO: cluster-key-based decryption of onvif_password_encrypted. For
now relies on the RTSP URI having plaintext credentials embedded (which
the ONVIF import path already ensures via rtspWithCredentials).
2026-05-21 10:03:30 +00:00
|
|
|
layout_id: clone.id,
|
|
|
|
|
row: c.row,
|
|
|
|
|
col: c.col,
|
|
|
|
|
row_span: c.row_span,
|
|
|
|
|
col_span: c.col_span,
|
|
|
|
|
content_type: c.content_type,
|
|
|
|
|
camera_id: c.camera_id,
|
|
|
|
|
stream_selector: c.stream_selector,
|
|
|
|
|
web_url: c.web_url,
|
|
|
|
|
html_content: c.html_content,
|
|
|
|
|
cooling_timeout_seconds: c.cooling_timeout_seconds,
|
|
|
|
|
options: c.options,
|
|
|
|
|
entity_id: c.entity_id,
|
|
|
|
|
fit: c.fit,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
const labels = await this._all<{ label_id: string }>(
|
feat(onvif-events): PullPoint subscription for all ONVIF cameras
New kiosk/src/onvif_events.rs: for each ONVIF camera in the bundle,
creates a PullPoint subscription, polls every 3s, parses
NotificationMessage XML into structured JSON (topic + source key/values
+ data key/values + timestamp), and POSTs to /api/kiosk/event with
source_type=onvif + camera_id.
Forwards ALL event topics: motion, ANPR (LicensePlateRecognition),
line crossing, intrusion, digital input, analytics, tamper — everything
the camera exposes. Node-RED sorts what matters.
Subscription lifecycle:
- CreatePullPointSubscription with 60s InitialTerminationTime
- Renew every 55s before timeout
- Unsubscribe on bundle change / shutdown
- Auto-resubscribe on pull/renew failure (30s backoff)
- Generation tracking via Weak<()> so old workers self-terminate
when start() is called with a new bundle
WSSE PasswordDigest auth for SOAP calls — same scheme the server's
onvif.ts uses. sha1 crate added.
BundleCamera extended with onvif_host/port/username/password_encrypted
fields (server already ships them; kiosk just wasn't deserializing).
Gated by BF_ENABLE_ONVIF_EVENTS=1. Enabled by default in the pi-gen
image env file.
TODO: cluster-key-based decryption of onvif_password_encrypted. For
now relies on the RTSP URI having plaintext credentials embedded (which
the ONVIF import path already ensures via rtspWithCredentials).
2026-05-21 10:03:30 +00:00
|
|
|
"SELECT label_id FROM layout_labels WHERE layout_id = ?",
|
2026-05-23 00:07:44 +00:00
|
|
|
[id],
|
|
|
|
|
);
|
feat(onvif-events): PullPoint subscription for all ONVIF cameras
New kiosk/src/onvif_events.rs: for each ONVIF camera in the bundle,
creates a PullPoint subscription, polls every 3s, parses
NotificationMessage XML into structured JSON (topic + source key/values
+ data key/values + timestamp), and POSTs to /api/kiosk/event with
source_type=onvif + camera_id.
Forwards ALL event topics: motion, ANPR (LicensePlateRecognition),
line crossing, intrusion, digital input, analytics, tamper — everything
the camera exposes. Node-RED sorts what matters.
Subscription lifecycle:
- CreatePullPointSubscription with 60s InitialTerminationTime
- Renew every 55s before timeout
- Unsubscribe on bundle change / shutdown
- Auto-resubscribe on pull/renew failure (30s backoff)
- Generation tracking via Weak<()> so old workers self-terminate
when start() is called with a new bundle
WSSE PasswordDigest auth for SOAP calls — same scheme the server's
onvif.ts uses. sha1 crate added.
BundleCamera extended with onvif_host/port/username/password_encrypted
fields (server already ships them; kiosk just wasn't deserializing).
Gated by BF_ENABLE_ONVIF_EVENTS=1. Enabled by default in the pi-gen
image env file.
TODO: cluster-key-based decryption of onvif_password_encrypted. For
now relies on the RTSP URI having plaintext credentials embedded (which
the ONVIF import path already ensures via rtspWithCredentials).
2026-05-21 10:03:30 +00:00
|
|
|
for (const ll of labels) {
|
2026-05-23 00:07:44 +00:00
|
|
|
await this.attachLayoutLabel(clone.id, ll.label_id);
|
feat(onvif-events): PullPoint subscription for all ONVIF cameras
New kiosk/src/onvif_events.rs: for each ONVIF camera in the bundle,
creates a PullPoint subscription, polls every 3s, parses
NotificationMessage XML into structured JSON (topic + source key/values
+ data key/values + timestamp), and POSTs to /api/kiosk/event with
source_type=onvif + camera_id.
Forwards ALL event topics: motion, ANPR (LicensePlateRecognition),
line crossing, intrusion, digital input, analytics, tamper — everything
the camera exposes. Node-RED sorts what matters.
Subscription lifecycle:
- CreatePullPointSubscription with 60s InitialTerminationTime
- Renew every 55s before timeout
- Unsubscribe on bundle change / shutdown
- Auto-resubscribe on pull/renew failure (30s backoff)
- Generation tracking via Weak<()> so old workers self-terminate
when start() is called with a new bundle
WSSE PasswordDigest auth for SOAP calls — same scheme the server's
onvif.ts uses. sha1 crate added.
BundleCamera extended with onvif_host/port/username/password_encrypted
fields (server already ships them; kiosk just wasn't deserializing).
Gated by BF_ENABLE_ONVIF_EVENTS=1. Enabled by default in the pi-gen
image env file.
TODO: cluster-key-based decryption of onvif_password_encrypted. For
now relies on the RTSP URI having plaintext credentials embedded (which
the ONVIF import path already ensures via rtspWithCredentials).
2026-05-21 10:03:30 +00:00
|
|
|
}
|
|
|
|
|
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
const displays = await this._all<{ display_id: string }>(
|
feat(onvif-events): PullPoint subscription for all ONVIF cameras
New kiosk/src/onvif_events.rs: for each ONVIF camera in the bundle,
creates a PullPoint subscription, polls every 3s, parses
NotificationMessage XML into structured JSON (topic + source key/values
+ data key/values + timestamp), and POSTs to /api/kiosk/event with
source_type=onvif + camera_id.
Forwards ALL event topics: motion, ANPR (LicensePlateRecognition),
line crossing, intrusion, digital input, analytics, tamper — everything
the camera exposes. Node-RED sorts what matters.
Subscription lifecycle:
- CreatePullPointSubscription with 60s InitialTerminationTime
- Renew every 55s before timeout
- Unsubscribe on bundle change / shutdown
- Auto-resubscribe on pull/renew failure (30s backoff)
- Generation tracking via Weak<()> so old workers self-terminate
when start() is called with a new bundle
WSSE PasswordDigest auth for SOAP calls — same scheme the server's
onvif.ts uses. sha1 crate added.
BundleCamera extended with onvif_host/port/username/password_encrypted
fields (server already ships them; kiosk just wasn't deserializing).
Gated by BF_ENABLE_ONVIF_EVENTS=1. Enabled by default in the pi-gen
image env file.
TODO: cluster-key-based decryption of onvif_password_encrypted. For
now relies on the RTSP URI having plaintext credentials embedded (which
the ONVIF import path already ensures via rtspWithCredentials).
2026-05-21 10:03:30 +00:00
|
|
|
"SELECT display_id FROM display_layouts WHERE layout_id = ?",
|
2026-05-23 00:07:44 +00:00
|
|
|
[id],
|
|
|
|
|
);
|
feat(onvif-events): PullPoint subscription for all ONVIF cameras
New kiosk/src/onvif_events.rs: for each ONVIF camera in the bundle,
creates a PullPoint subscription, polls every 3s, parses
NotificationMessage XML into structured JSON (topic + source key/values
+ data key/values + timestamp), and POSTs to /api/kiosk/event with
source_type=onvif + camera_id.
Forwards ALL event topics: motion, ANPR (LicensePlateRecognition),
line crossing, intrusion, digital input, analytics, tamper — everything
the camera exposes. Node-RED sorts what matters.
Subscription lifecycle:
- CreatePullPointSubscription with 60s InitialTerminationTime
- Renew every 55s before timeout
- Unsubscribe on bundle change / shutdown
- Auto-resubscribe on pull/renew failure (30s backoff)
- Generation tracking via Weak<()> so old workers self-terminate
when start() is called with a new bundle
WSSE PasswordDigest auth for SOAP calls — same scheme the server's
onvif.ts uses. sha1 crate added.
BundleCamera extended with onvif_host/port/username/password_encrypted
fields (server already ships them; kiosk just wasn't deserializing).
Gated by BF_ENABLE_ONVIF_EVENTS=1. Enabled by default in the pi-gen
image env file.
TODO: cluster-key-based decryption of onvif_password_encrypted. For
now relies on the RTSP URI having plaintext credentials embedded (which
the ONVIF import path already ensures via rtspWithCredentials).
2026-05-21 10:03:30 +00:00
|
|
|
for (const dl of displays) {
|
2026-05-23 00:07:44 +00:00
|
|
|
await this.attachLayoutToDisplay(dl.display_id, clone.id);
|
feat(onvif-events): PullPoint subscription for all ONVIF cameras
New kiosk/src/onvif_events.rs: for each ONVIF camera in the bundle,
creates a PullPoint subscription, polls every 3s, parses
NotificationMessage XML into structured JSON (topic + source key/values
+ data key/values + timestamp), and POSTs to /api/kiosk/event with
source_type=onvif + camera_id.
Forwards ALL event topics: motion, ANPR (LicensePlateRecognition),
line crossing, intrusion, digital input, analytics, tamper — everything
the camera exposes. Node-RED sorts what matters.
Subscription lifecycle:
- CreatePullPointSubscription with 60s InitialTerminationTime
- Renew every 55s before timeout
- Unsubscribe on bundle change / shutdown
- Auto-resubscribe on pull/renew failure (30s backoff)
- Generation tracking via Weak<()> so old workers self-terminate
when start() is called with a new bundle
WSSE PasswordDigest auth for SOAP calls — same scheme the server's
onvif.ts uses. sha1 crate added.
BundleCamera extended with onvif_host/port/username/password_encrypted
fields (server already ships them; kiosk just wasn't deserializing).
Gated by BF_ENABLE_ONVIF_EVENTS=1. Enabled by default in the pi-gen
image env file.
TODO: cluster-key-based decryption of onvif_password_encrypted. For
now relies on the RTSP URI having plaintext credentials embedded (which
the ONVIF import path already ensures via rtspWithCredentials).
2026-05-21 10:03:30 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return clone;
|
|
|
|
|
}
|
|
|
|
|
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
async deleteLayout(id: string): Promise<void> {
|
2026-05-23 00:07:44 +00:00
|
|
|
await this._run(`DELETE FROM layout_cells WHERE layout_id = ?`, [id]);
|
|
|
|
|
await this._run(`DELETE FROM layout_labels WHERE layout_id = ?`, [id]);
|
|
|
|
|
await this._run(`DELETE FROM display_layouts WHERE layout_id = ?`, [id]);
|
2026-05-10 19:55:19 +00:00
|
|
|
// Any display whose default pointed here gets cleared.
|
2026-05-23 00:07:44 +00:00
|
|
|
await this._run(`UPDATE displays SET default_layout_id = NULL WHERE default_layout_id = ?`, [id]);
|
|
|
|
|
await this._run(`DELETE FROM layouts WHERE id = ?`, [id]);
|
feat: layout/template/display CRUD + display-chain bundle routing
Major changes:
- Bundle now follows kiosk → display → layouts → cells → cameras
(no label filtering for v0.1)
- Setup creates default Fullscreen template + Default layout with
BetterFrame logo on the primary display
- Pairing auto-assigns kiosk to primary display
- Admin UI: full template CRUD with presets (fullscreen, 2x2, 1+3, 3x3)
- Admin UI: layout CRUD with cell management (assign cameras/web/html
to template regions)
- Admin UI: display editing (default layout, idle/sleep timeouts)
- Repository: added createLayoutTemplate, createLayout, createLayoutCell,
updateLayout, deleteLayout, layoutsForDisplayId, camerasForLayoutIds,
updateDisplay, and more
2026-05-10 01:45:53 +00:00
|
|
|
void this.notify("layouts", "delete", id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===========================================================================
|
|
|
|
|
// layout cells
|
|
|
|
|
// ===========================================================================
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async createLayoutCell(input: {
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
layout_id: string;
|
2026-05-10 19:55:19 +00:00
|
|
|
row: number;
|
|
|
|
|
col: number;
|
|
|
|
|
row_span?: number;
|
|
|
|
|
col_span?: number;
|
2026-05-10 21:18:44 +00:00
|
|
|
content_type?: string;
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
camera_id?: string | null;
|
feat: layout/template/display CRUD + display-chain bundle routing
Major changes:
- Bundle now follows kiosk → display → layouts → cells → cameras
(no label filtering for v0.1)
- Setup creates default Fullscreen template + Default layout with
BetterFrame logo on the primary display
- Pairing auto-assigns kiosk to primary display
- Admin UI: full template CRUD with presets (fullscreen, 2x2, 1+3, 3x3)
- Admin UI: layout CRUD with cell management (assign cameras/web/html
to template regions)
- Admin UI: display editing (default layout, idle/sleep timeouts)
- Repository: added createLayoutTemplate, createLayout, createLayoutCell,
updateLayout, deleteLayout, layoutsForDisplayId, camerasForLayoutIds,
updateDisplay, and more
2026-05-10 01:45:53 +00:00
|
|
|
stream_selector?: string | null;
|
|
|
|
|
web_url?: string | null;
|
|
|
|
|
html_content?: string | null;
|
|
|
|
|
cooling_timeout_seconds?: number | null;
|
|
|
|
|
options?: Record<string, unknown>;
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
entity_id?: string | null;
|
2026-05-11 11:52:22 +00:00
|
|
|
fit?: "cover" | "contain" | "fill";
|
2026-05-23 00:07:44 +00:00
|
|
|
}): Promise<LayoutCell> {
|
2026-05-10 21:18:44 +00:00
|
|
|
// Resolve content fields from the entity (if given). The legacy columns
|
2026-05-12 23:47:53 +00:00
|
|
|
// remain populated for backward-compatible bundle generation. Dashboard
|
|
|
|
|
// entities materialise as web cells pointing at /dash/<id> so the existing
|
|
|
|
|
// kiosk's WebKit cell path renders them with no app changes.
|
2026-05-11 07:38:50 +00:00
|
|
|
let contentType = input.content_type ?? "none";
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
let cameraId: string | null = input.camera_id ?? null;
|
2026-05-10 21:18:44 +00:00
|
|
|
let webUrl: string | null = input.web_url ?? null;
|
|
|
|
|
let htmlContent: string | null = input.html_content ?? null;
|
|
|
|
|
if (input.entity_id != null) {
|
2026-05-23 00:07:44 +00:00
|
|
|
const ent = await this.getEntityById(input.entity_id);
|
2026-05-10 21:18:44 +00:00
|
|
|
if (ent) {
|
2026-05-12 23:47:53 +00:00
|
|
|
contentType = ent.type === "dashboard" ? "web" : ent.type;
|
2026-05-10 21:18:44 +00:00
|
|
|
cameraId = ent.type === "camera" ? ent.camera_id : null;
|
2026-05-12 23:47:53 +00:00
|
|
|
webUrl =
|
|
|
|
|
ent.type === "web" ? ent.web_url :
|
|
|
|
|
ent.type === "dashboard" && ent.dashboard_id ? `/dash/${ent.dashboard_id}` :
|
|
|
|
|
null;
|
2026-05-10 21:18:44 +00:00
|
|
|
htmlContent = ent.type === "html" ? ent.html_content : null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
const id = uuidv7();
|
|
|
|
|
await this._run(
|
|
|
|
|
`INSERT INTO layout_cells (id, layout_id, "row", col, row_span, col_span, content_type, camera_id, stream_selector, web_url, html_content, cooling_timeout_seconds, options, entity_id, fit)
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
2026-05-23 00:07:44 +00:00
|
|
|
[
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
id,
|
2026-05-23 00:07:44 +00:00
|
|
|
input.layout_id,
|
|
|
|
|
input.row,
|
|
|
|
|
input.col,
|
|
|
|
|
input.row_span ?? 1,
|
|
|
|
|
input.col_span ?? 1,
|
|
|
|
|
contentType,
|
|
|
|
|
cameraId,
|
|
|
|
|
input.stream_selector ?? "auto",
|
|
|
|
|
webUrl,
|
|
|
|
|
htmlContent,
|
|
|
|
|
input.cooling_timeout_seconds ?? null,
|
|
|
|
|
J(input.options ?? {}),
|
|
|
|
|
input.entity_id ?? null,
|
|
|
|
|
input.fit ?? "cover",
|
|
|
|
|
],
|
feat: layout/template/display CRUD + display-chain bundle routing
Major changes:
- Bundle now follows kiosk → display → layouts → cells → cameras
(no label filtering for v0.1)
- Setup creates default Fullscreen template + Default layout with
BetterFrame logo on the primary display
- Pairing auto-assigns kiosk to primary display
- Admin UI: full template CRUD with presets (fullscreen, 2x2, 1+3, 3x3)
- Admin UI: layout CRUD with cell management (assign cameras/web/html
to template regions)
- Admin UI: display editing (default layout, idle/sleep timeouts)
- Repository: added createLayoutTemplate, createLayout, createLayoutCell,
updateLayout, deleteLayout, layoutsForDisplayId, camerasForLayoutIds,
updateDisplay, and more
2026-05-10 01:45:53 +00:00
|
|
|
);
|
|
|
|
|
void this.notify("layout_cells", "create", id);
|
2026-05-23 00:07:44 +00:00
|
|
|
const r = await this._get("SELECT * FROM layout_cells WHERE id = ?", [id]);
|
feat: layout/template/display CRUD + display-chain bundle routing
Major changes:
- Bundle now follows kiosk → display → layouts → cells → cameras
(no label filtering for v0.1)
- Setup creates default Fullscreen template + Default layout with
BetterFrame logo on the primary display
- Pairing auto-assigns kiosk to primary display
- Admin UI: full template CRUD with presets (fullscreen, 2x2, 1+3, 3x3)
- Admin UI: layout CRUD with cell management (assign cameras/web/html
to template regions)
- Admin UI: display editing (default layout, idle/sleep timeouts)
- Repository: added createLayoutTemplate, createLayout, createLayoutCell,
updateLayout, deleteLayout, layoutsForDisplayId, camerasForLayoutIds,
updateDisplay, and more
2026-05-10 01:45:53 +00:00
|
|
|
if (!r) throw new Error("layout_cell vanished after insert");
|
|
|
|
|
return rowToLayoutCell(r as Record<string, unknown>);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-10 21:18:44 +00:00
|
|
|
/**
|
|
|
|
|
* Assign (or clear) the entity for a cell. Also mirrors the resolved entity's
|
|
|
|
|
* type/camera/url/html into the legacy cell columns so bundle generation stays
|
|
|
|
|
* compatible with the existing kiosk.
|
|
|
|
|
*/
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
async assignCellEntity(cellId: string, entityId: string | null): Promise<void> {
|
2026-05-10 21:18:44 +00:00
|
|
|
if (entityId == null) {
|
2026-05-23 00:07:44 +00:00
|
|
|
await this._run(
|
|
|
|
|
`UPDATE layout_cells
|
|
|
|
|
SET entity_id = NULL,
|
|
|
|
|
content_type = 'none',
|
|
|
|
|
camera_id = NULL,
|
|
|
|
|
web_url = NULL,
|
|
|
|
|
html_content = NULL
|
|
|
|
|
WHERE id = ?`,
|
|
|
|
|
[cellId],
|
|
|
|
|
);
|
2026-05-10 21:18:44 +00:00
|
|
|
void this.notify("layout_cells", "update", cellId);
|
|
|
|
|
return;
|
|
|
|
|
}
|
2026-05-23 00:07:44 +00:00
|
|
|
const ent = await this.getEntityById(entityId);
|
2026-05-10 21:18:44 +00:00
|
|
|
if (!ent) return;
|
2026-05-12 23:47:53 +00:00
|
|
|
const cellContentType = ent.type === "dashboard" ? "web" : ent.type;
|
|
|
|
|
const cellWebUrl =
|
|
|
|
|
ent.type === "web" ? ent.web_url :
|
|
|
|
|
ent.type === "dashboard" && ent.dashboard_id ? `/dash/${ent.dashboard_id}` :
|
|
|
|
|
null;
|
2026-05-23 00:07:44 +00:00
|
|
|
await this._run(
|
|
|
|
|
`UPDATE layout_cells
|
|
|
|
|
SET entity_id = ?,
|
|
|
|
|
content_type = ?,
|
|
|
|
|
camera_id = ?,
|
|
|
|
|
web_url = ?,
|
|
|
|
|
html_content = ?
|
|
|
|
|
WHERE id = ?`,
|
|
|
|
|
[
|
2026-05-10 21:18:44 +00:00
|
|
|
ent.id,
|
2026-05-12 23:47:53 +00:00
|
|
|
cellContentType,
|
2026-05-10 21:18:44 +00:00
|
|
|
ent.type === "camera" ? ent.camera_id : null,
|
2026-05-12 23:47:53 +00:00
|
|
|
cellWebUrl,
|
2026-05-10 21:18:44 +00:00
|
|
|
ent.type === "html" ? ent.html_content : null,
|
|
|
|
|
cellId,
|
2026-05-23 00:07:44 +00:00
|
|
|
],
|
|
|
|
|
);
|
2026-05-10 21:18:44 +00:00
|
|
|
void this.notify("layout_cells", "update", cellId);
|
|
|
|
|
}
|
|
|
|
|
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
async updateLayoutCell(id: string, patch: Partial<LayoutCell>): Promise<void> {
|
feat: layout/template/display CRUD + display-chain bundle routing
Major changes:
- Bundle now follows kiosk → display → layouts → cells → cameras
(no label filtering for v0.1)
- Setup creates default Fullscreen template + Default layout with
BetterFrame logo on the primary display
- Pairing auto-assigns kiosk to primary display
- Admin UI: full template CRUD with presets (fullscreen, 2x2, 1+3, 3x3)
- Admin UI: layout CRUD with cell management (assign cameras/web/html
to template regions)
- Admin UI: display editing (default layout, idle/sleep timeouts)
- Repository: added createLayoutTemplate, createLayout, createLayoutCell,
updateLayout, deleteLayout, layoutsForDisplayId, camerasForLayoutIds,
updateDisplay, and more
2026-05-10 01:45:53 +00:00
|
|
|
const sets: string[] = [];
|
|
|
|
|
const vals: unknown[] = [];
|
|
|
|
|
for (const [k, v] of Object.entries(patch)) {
|
|
|
|
|
if (k === "id" || k === "layout_id") continue;
|
2026-05-10 19:55:19 +00:00
|
|
|
const col = k === "row" ? `"row"` : k;
|
|
|
|
|
sets.push(`${col} = ?`);
|
feat: layout/template/display CRUD + display-chain bundle routing
Major changes:
- Bundle now follows kiosk → display → layouts → cells → cameras
(no label filtering for v0.1)
- Setup creates default Fullscreen template + Default layout with
BetterFrame logo on the primary display
- Pairing auto-assigns kiosk to primary display
- Admin UI: full template CRUD with presets (fullscreen, 2x2, 1+3, 3x3)
- Admin UI: layout CRUD with cell management (assign cameras/web/html
to template regions)
- Admin UI: display editing (default layout, idle/sleep timeouts)
- Repository: added createLayoutTemplate, createLayout, createLayoutCell,
updateLayout, deleteLayout, layoutsForDisplayId, camerasForLayoutIds,
updateDisplay, and more
2026-05-10 01:45:53 +00:00
|
|
|
if (k === "options") vals.push(J(v));
|
|
|
|
|
else vals.push(v === undefined ? null : v);
|
|
|
|
|
}
|
|
|
|
|
if (sets.length === 0) return;
|
|
|
|
|
vals.push(id);
|
2026-05-23 00:07:44 +00:00
|
|
|
await this._run(`UPDATE layout_cells SET ${sets.join(", ")} WHERE id = ?`, vals);
|
feat: layout/template/display CRUD + display-chain bundle routing
Major changes:
- Bundle now follows kiosk → display → layouts → cells → cameras
(no label filtering for v0.1)
- Setup creates default Fullscreen template + Default layout with
BetterFrame logo on the primary display
- Pairing auto-assigns kiosk to primary display
- Admin UI: full template CRUD with presets (fullscreen, 2x2, 1+3, 3x3)
- Admin UI: layout CRUD with cell management (assign cameras/web/html
to template regions)
- Admin UI: display editing (default layout, idle/sleep timeouts)
- Repository: added createLayoutTemplate, createLayout, createLayoutCell,
updateLayout, deleteLayout, layoutsForDisplayId, camerasForLayoutIds,
updateDisplay, and more
2026-05-10 01:45:53 +00:00
|
|
|
void this.notify("layout_cells", "update", id);
|
|
|
|
|
}
|
|
|
|
|
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
async deleteLayoutCell(id: string): Promise<void> {
|
2026-05-23 00:07:44 +00:00
|
|
|
await this._run(`DELETE FROM layout_cells WHERE id = ?`, [id]);
|
feat: layout/template/display CRUD + display-chain bundle routing
Major changes:
- Bundle now follows kiosk → display → layouts → cells → cameras
(no label filtering for v0.1)
- Setup creates default Fullscreen template + Default layout with
BetterFrame logo on the primary display
- Pairing auto-assigns kiosk to primary display
- Admin UI: full template CRUD with presets (fullscreen, 2x2, 1+3, 3x3)
- Admin UI: layout CRUD with cell management (assign cameras/web/html
to template regions)
- Admin UI: display editing (default layout, idle/sleep timeouts)
- Repository: added createLayoutTemplate, createLayout, createLayoutCell,
updateLayout, deleteLayout, layoutsForDisplayId, camerasForLayoutIds,
updateDisplay, and more
2026-05-10 01:45:53 +00:00
|
|
|
void this.notify("layout_cells", "delete", id);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-10 19:55:19 +00:00
|
|
|
/**
|
|
|
|
|
* Shift cells along an axis to make room for an insertion (or close a gap
|
|
|
|
|
* after a deletion). For axis="row", any cell whose `row >= fromIndex` has
|
|
|
|
|
* its row bumped by `delta`. Same for axis="col". Used by the visual
|
|
|
|
|
* builder when adding a cell to the top/left of an existing one.
|
|
|
|
|
*/
|
2026-05-23 00:07:44 +00:00
|
|
|
async shiftCellsForLayout(
|
2026-05-10 19:55:19 +00:00
|
|
|
layoutId: number,
|
|
|
|
|
axis: "row" | "col",
|
|
|
|
|
fromIndex: number,
|
|
|
|
|
delta: number,
|
2026-05-23 00:07:44 +00:00
|
|
|
): Promise<void> {
|
2026-05-10 19:55:19 +00:00
|
|
|
if (delta === 0) return;
|
|
|
|
|
const colName = axis === "row" ? `"row"` : "col";
|
2026-05-23 00:07:44 +00:00
|
|
|
await this._run(
|
|
|
|
|
`UPDATE layout_cells
|
|
|
|
|
SET ${colName} = ${colName} + ?
|
|
|
|
|
WHERE layout_id = ?
|
|
|
|
|
AND ${colName} >= ?`,
|
|
|
|
|
[delta, layoutId, fromIndex],
|
|
|
|
|
);
|
2026-05-10 19:55:19 +00:00
|
|
|
void this.notify("layout_cells", "update", layoutId);
|
|
|
|
|
}
|
|
|
|
|
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
async listLayoutCells(layoutId: string): Promise<LayoutCell[]> {
|
2026-05-23 00:07:44 +00:00
|
|
|
const rs = await this._all(
|
2026-05-10 19:55:19 +00:00
|
|
|
`SELECT * FROM layout_cells WHERE layout_id = ? ORDER BY "row", col`,
|
2026-05-23 00:07:44 +00:00
|
|
|
[layoutId],
|
|
|
|
|
);
|
2026-05-10 19:55:19 +00:00
|
|
|
return rs.map((r) => rowToLayoutCell(r as Record<string, unknown>));
|
|
|
|
|
}
|
|
|
|
|
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
async getLayoutCellById(id: string): Promise<LayoutCell | null> {
|
2026-05-23 00:07:44 +00:00
|
|
|
const r = await this._get("SELECT * FROM layout_cells WHERE id = ?", [id]);
|
2026-05-10 20:31:37 +00:00
|
|
|
return r ? rowToLayoutCell(r as Record<string, unknown>) : null;
|
|
|
|
|
}
|
|
|
|
|
|
feat: layout/template/display CRUD + display-chain bundle routing
Major changes:
- Bundle now follows kiosk → display → layouts → cells → cameras
(no label filtering for v0.1)
- Setup creates default Fullscreen template + Default layout with
BetterFrame logo on the primary display
- Pairing auto-assigns kiosk to primary display
- Admin UI: full template CRUD with presets (fullscreen, 2x2, 1+3, 3x3)
- Admin UI: layout CRUD with cell management (assign cameras/web/html
to template regions)
- Admin UI: display editing (default layout, idle/sleep timeouts)
- Repository: added createLayoutTemplate, createLayout, createLayoutCell,
updateLayout, deleteLayout, layoutsForDisplayId, camerasForLayoutIds,
updateDisplay, and more
2026-05-10 01:45:53 +00:00
|
|
|
// ===========================================================================
|
|
|
|
|
// display-chain bundle queries (kiosk → display → layouts → cells → cameras)
|
|
|
|
|
// ===========================================================================
|
|
|
|
|
|
2026-05-10 19:55:19 +00:00
|
|
|
/** Bundle generation: layouts attached to a display via display_layouts. */
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
async layoutsForDisplayId(displayId: string): Promise<Layout[]> {
|
2026-05-10 19:55:19 +00:00
|
|
|
return this.listLayoutsForDisplay(displayId);
|
feat: layout/template/display CRUD + display-chain bundle routing
Major changes:
- Bundle now follows kiosk → display → layouts → cells → cameras
(no label filtering for v0.1)
- Setup creates default Fullscreen template + Default layout with
BetterFrame logo on the primary display
- Pairing auto-assigns kiosk to primary display
- Admin UI: full template CRUD with presets (fullscreen, 2x2, 1+3, 3x3)
- Admin UI: layout CRUD with cell management (assign cameras/web/html
to template regions)
- Admin UI: display editing (default layout, idle/sleep timeouts)
- Repository: added createLayoutTemplate, createLayout, createLayoutCell,
updateLayout, deleteLayout, layoutsForDisplayId, camerasForLayoutIds,
updateDisplay, and more
2026-05-10 01:45:53 +00:00
|
|
|
}
|
|
|
|
|
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
async camerasForLayoutIds(layoutIds: string[]): Promise<Camera[]> {
|
feat: layout/template/display CRUD + display-chain bundle routing
Major changes:
- Bundle now follows kiosk → display → layouts → cells → cameras
(no label filtering for v0.1)
- Setup creates default Fullscreen template + Default layout with
BetterFrame logo on the primary display
- Pairing auto-assigns kiosk to primary display
- Admin UI: full template CRUD with presets (fullscreen, 2x2, 1+3, 3x3)
- Admin UI: layout CRUD with cell management (assign cameras/web/html
to template regions)
- Admin UI: display editing (default layout, idle/sleep timeouts)
- Repository: added createLayoutTemplate, createLayout, createLayoutCell,
updateLayout, deleteLayout, layoutsForDisplayId, camerasForLayoutIds,
updateDisplay, and more
2026-05-10 01:45:53 +00:00
|
|
|
if (layoutIds.length === 0) return [];
|
|
|
|
|
const placeholders = layoutIds.map(() => "?").join(",");
|
2026-05-23 00:07:44 +00:00
|
|
|
const rs = await this._all(
|
|
|
|
|
`SELECT DISTINCT c.* FROM cameras c
|
|
|
|
|
JOIN layout_cells lc ON lc.camera_id = c.id
|
|
|
|
|
WHERE lc.layout_id IN (${placeholders})
|
2026-05-26 00:08:09 +00:00
|
|
|
AND c.enabled = true
|
2026-05-23 00:07:44 +00:00
|
|
|
ORDER BY c.name`,
|
|
|
|
|
layoutIds,
|
|
|
|
|
);
|
feat: layout/template/display CRUD + display-chain bundle routing
Major changes:
- Bundle now follows kiosk → display → layouts → cells → cameras
(no label filtering for v0.1)
- Setup creates default Fullscreen template + Default layout with
BetterFrame logo on the primary display
- Pairing auto-assigns kiosk to primary display
- Admin UI: full template CRUD with presets (fullscreen, 2x2, 1+3, 3x3)
- Admin UI: layout CRUD with cell management (assign cameras/web/html
to template regions)
- Admin UI: display editing (default layout, idle/sleep timeouts)
- Repository: added createLayoutTemplate, createLayout, createLayoutCell,
updateLayout, deleteLayout, layoutsForDisplayId, camerasForLayoutIds,
updateDisplay, and more
2026-05-10 01:45:53 +00:00
|
|
|
return rs.map((r) => rowToCamera(r as Record<string, unknown>));
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-09 23:09:13 +00:00
|
|
|
// ===========================================================================
|
|
|
|
|
// cameras
|
|
|
|
|
// ===========================================================================
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async listCameras(): Promise<Camera[]> {
|
|
|
|
|
const rs = await this._all("SELECT * FROM cameras ORDER BY name");
|
2026-05-09 23:09:13 +00:00
|
|
|
return rs.map((r) => rowToCamera(r as Record<string, unknown>));
|
|
|
|
|
}
|
|
|
|
|
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
async getCameraById(id: string): Promise<Camera | null> {
|
2026-05-23 00:07:44 +00:00
|
|
|
const r = await this._get("SELECT * FROM cameras WHERE id = ?", [id]);
|
2026-05-09 23:09:13 +00:00
|
|
|
return r ? rowToCamera(r as Record<string, unknown>) : null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async getCameraByName(name: string): Promise<Camera | null> {
|
|
|
|
|
const r = await this._get("SELECT * FROM cameras WHERE name = ?", [name]);
|
2026-05-09 23:09:13 +00:00
|
|
|
return r ? rowToCamera(r as Record<string, unknown>) : null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async createCamera(input: {
|
2026-05-09 23:09:13 +00:00
|
|
|
name: string;
|
|
|
|
|
type: CameraType;
|
|
|
|
|
rtsp_url?: string | null;
|
|
|
|
|
onvif_host?: string | null;
|
|
|
|
|
onvif_port?: number | null;
|
|
|
|
|
onvif_username?: string | null;
|
|
|
|
|
onvif_password?: string | null; // already-encrypted ciphertext
|
|
|
|
|
capabilities?: string[];
|
|
|
|
|
stream_policy?: StreamPolicy;
|
2026-05-23 00:07:44 +00:00
|
|
|
}): Promise<Camera> {
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
const id = uuidv7();
|
|
|
|
|
await this._run(
|
2026-05-09 23:09:13 +00:00
|
|
|
`INSERT INTO cameras
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
(id, name, type, rtsp_url, onvif_host, onvif_port, onvif_username,
|
2026-05-09 23:09:13 +00:00
|
|
|
onvif_password, capabilities, stream_policy)
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
2026-05-23 00:07:44 +00:00
|
|
|
[
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
id,
|
2026-05-23 00:07:44 +00:00
|
|
|
input.name,
|
|
|
|
|
input.type,
|
|
|
|
|
input.rtsp_url ?? null,
|
|
|
|
|
input.onvif_host ?? null,
|
|
|
|
|
input.onvif_port ?? null,
|
|
|
|
|
input.onvif_username ?? null,
|
|
|
|
|
input.onvif_password ?? null,
|
|
|
|
|
J(input.capabilities ?? []),
|
|
|
|
|
input.stream_policy ?? "auto",
|
|
|
|
|
],
|
2026-05-09 23:09:13 +00:00
|
|
|
);
|
|
|
|
|
void this.notify("cameras", "create", id);
|
2026-05-23 00:07:44 +00:00
|
|
|
const c = await this.getCameraById(id);
|
2026-05-09 23:09:13 +00:00
|
|
|
if (!c) throw new Error("camera vanished after insert");
|
2026-05-10 21:18:44 +00:00
|
|
|
// Mirror this camera as a reusable entity so it's pickable in cell editors.
|
2026-05-23 00:07:44 +00:00
|
|
|
await this.ensureCameraEntity(c);
|
2026-05-09 23:09:13 +00:00
|
|
|
return c;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 09:36:49 +00:00
|
|
|
async upsertCloudCamera(input: {
|
|
|
|
|
cloud_account_id: string;
|
|
|
|
|
cloud_vendor_camera_id: string;
|
|
|
|
|
name: string;
|
|
|
|
|
cloud_stream_url: string | null;
|
|
|
|
|
cloud_stream_type: string | null;
|
|
|
|
|
enabled: boolean;
|
|
|
|
|
}): Promise<Camera> {
|
|
|
|
|
const existing = await this._get(
|
|
|
|
|
"SELECT * FROM cameras WHERE cloud_account_id = ? AND cloud_vendor_camera_id = ?",
|
|
|
|
|
[input.cloud_account_id, input.cloud_vendor_camera_id],
|
|
|
|
|
);
|
|
|
|
|
if (existing) {
|
|
|
|
|
const cam = rowToCamera(existing as Record<string, unknown>);
|
|
|
|
|
await this._run(
|
|
|
|
|
`UPDATE cameras SET name = ?, cloud_stream_url = ?, cloud_stream_type = ?, enabled = ? WHERE id = ?`,
|
2026-05-23 10:55:04 +00:00
|
|
|
[input.name, input.cloud_stream_url, input.cloud_stream_type, Boolean(input.enabled), cam.id],
|
2026-05-23 09:36:49 +00:00
|
|
|
);
|
|
|
|
|
void this.notify("cameras", "update", cam.id);
|
|
|
|
|
return (await this.getCameraById(cam.id))!;
|
|
|
|
|
}
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
const id = uuidv7();
|
|
|
|
|
await this._run(
|
2026-05-23 09:36:49 +00:00
|
|
|
`INSERT INTO cameras
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
(id, name, type, cloud_account_id, cloud_vendor_camera_id, cloud_stream_url, cloud_stream_type, enabled)
|
|
|
|
|
VALUES (?, ?, 'cloud', ?, ?, ?, ?, ?)`,
|
|
|
|
|
[id, input.name, input.cloud_account_id, input.cloud_vendor_camera_id,
|
2026-05-23 10:55:04 +00:00
|
|
|
input.cloud_stream_url, input.cloud_stream_type, Boolean(input.enabled)],
|
2026-05-23 09:36:49 +00:00
|
|
|
);
|
|
|
|
|
void this.notify("cameras", "create", id);
|
|
|
|
|
const c = await this.getCameraById(id);
|
|
|
|
|
if (!c) throw new Error("cloud camera vanished after insert");
|
|
|
|
|
await this.ensureCameraEntity(c);
|
|
|
|
|
return c;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async listCloudCamerasByAccount(accountId: string): Promise<Camera[]> {
|
|
|
|
|
const rs = await this._all(
|
|
|
|
|
"SELECT * FROM cameras WHERE cloud_account_id = ? ORDER BY name",
|
|
|
|
|
[accountId],
|
|
|
|
|
);
|
|
|
|
|
return rs.map((r) => rowToCamera(r as Record<string, unknown>));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async deleteCloudCamerasNotIn(accountId: string, keepVendorIds: string[]): Promise<number> {
|
|
|
|
|
if (keepVendorIds.length === 0) {
|
|
|
|
|
const result = await this._run(
|
|
|
|
|
"DELETE FROM cameras WHERE cloud_account_id = ?",
|
|
|
|
|
[accountId],
|
|
|
|
|
);
|
|
|
|
|
return result.changes;
|
|
|
|
|
}
|
|
|
|
|
const placeholders = keepVendorIds.map(() => "?").join(",");
|
|
|
|
|
const result = await this._run(
|
|
|
|
|
`DELETE FROM cameras WHERE cloud_account_id = ? AND cloud_vendor_camera_id NOT IN (${placeholders})`,
|
|
|
|
|
[accountId, ...keepVendorIds],
|
|
|
|
|
);
|
|
|
|
|
return result.changes;
|
|
|
|
|
}
|
|
|
|
|
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
async listCameraStreams(cameraId: string): Promise<CameraStream[]> {
|
2026-05-23 00:07:44 +00:00
|
|
|
const rs = await this._all(
|
2026-05-09 23:09:13 +00:00
|
|
|
"SELECT * FROM camera_streams WHERE camera_id = ?",
|
2026-05-23 00:07:44 +00:00
|
|
|
[cameraId],
|
|
|
|
|
);
|
2026-05-09 23:09:13 +00:00
|
|
|
return rs.map((r) => rowToCameraStream(r as Record<string, unknown>));
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async createCameraStream(input: {
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
camera_id: string;
|
2026-05-09 23:09:13 +00:00
|
|
|
role: StreamRole;
|
|
|
|
|
name: string;
|
|
|
|
|
rtsp_uri: string;
|
|
|
|
|
profile_token?: string | null;
|
2026-05-26 04:51:33 +00:00
|
|
|
rtsp_host?: string | null;
|
|
|
|
|
rtsp_port?: number | null;
|
|
|
|
|
rtsp_path?: string | null;
|
2026-05-09 23:09:13 +00:00
|
|
|
width?: number | null;
|
|
|
|
|
height?: number | null;
|
|
|
|
|
encoding?: string | null;
|
|
|
|
|
framerate?: number | null;
|
|
|
|
|
bitrate_kbps?: number | null;
|
|
|
|
|
is_discovered?: boolean;
|
2026-05-23 00:07:44 +00:00
|
|
|
}): Promise<CameraStream> {
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
const id = uuidv7();
|
|
|
|
|
await this._run(
|
2026-05-09 23:09:13 +00:00
|
|
|
`INSERT INTO camera_streams
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
(id, camera_id, role, name, profile_token, rtsp_uri,
|
2026-05-26 04:51:33 +00:00
|
|
|
rtsp_host, rtsp_port, rtsp_path,
|
|
|
|
|
width, height, encoding, framerate, bitrate_kbps, is_discovered)
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
2026-05-23 00:07:44 +00:00
|
|
|
[
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
id,
|
2026-05-23 00:07:44 +00:00
|
|
|
input.camera_id,
|
|
|
|
|
input.role,
|
|
|
|
|
input.name,
|
|
|
|
|
input.profile_token ?? null,
|
|
|
|
|
input.rtsp_uri,
|
2026-05-26 04:51:33 +00:00
|
|
|
input.rtsp_host ?? null,
|
|
|
|
|
input.rtsp_port ?? null,
|
|
|
|
|
input.rtsp_path ?? null,
|
2026-05-23 00:07:44 +00:00
|
|
|
input.width ?? null,
|
|
|
|
|
input.height ?? null,
|
|
|
|
|
input.encoding ?? null,
|
|
|
|
|
input.framerate ?? null,
|
|
|
|
|
input.bitrate_kbps ?? null,
|
2026-05-23 10:55:04 +00:00
|
|
|
Boolean(input.is_discovered),
|
2026-05-23 00:07:44 +00:00
|
|
|
],
|
2026-05-09 23:09:13 +00:00
|
|
|
);
|
2026-05-23 00:07:44 +00:00
|
|
|
const r = await this._get("SELECT * FROM camera_streams WHERE id = ?", [id]);
|
2026-05-09 23:09:13 +00:00
|
|
|
if (!r) throw new Error("camera_stream vanished after insert");
|
|
|
|
|
void this.notify("camera_streams", "create", id);
|
|
|
|
|
return rowToCameraStream(r as Record<string, unknown>);
|
|
|
|
|
}
|
|
|
|
|
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
async updateCameraStream(id: string, patch: Partial<CameraStream>): Promise<void> {
|
2026-05-10 13:35:47 +00:00
|
|
|
const sets: string[] = [];
|
|
|
|
|
const vals: unknown[] = [];
|
|
|
|
|
for (const [k, v] of Object.entries(patch)) {
|
|
|
|
|
if (k === "id" || k === "camera_id") continue;
|
|
|
|
|
sets.push(`${k} = ?`);
|
|
|
|
|
vals.push(v === undefined ? null : v);
|
|
|
|
|
}
|
|
|
|
|
if (sets.length === 0) return;
|
|
|
|
|
vals.push(id);
|
2026-05-23 00:07:44 +00:00
|
|
|
await this._run(`UPDATE camera_streams SET ${sets.join(", ")} WHERE id = ?`, vals);
|
2026-05-10 13:35:47 +00:00
|
|
|
void this.notify("camera_streams", "update", id);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-09 23:09:13 +00:00
|
|
|
// ===========================================================================
|
|
|
|
|
// labels (incl. join tables)
|
|
|
|
|
// ===========================================================================
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async listLabels(): Promise<Label[]> {
|
|
|
|
|
const rs = await this._all("SELECT * FROM labels ORDER BY name");
|
2026-05-09 23:09:13 +00:00
|
|
|
return rs.map((r) => rowToLabel(r as Record<string, unknown>));
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async getLabelByName(name: string): Promise<Label | null> {
|
|
|
|
|
const r = await this._get("SELECT * FROM labels WHERE name = ?", [name]);
|
2026-05-09 23:09:13 +00:00
|
|
|
return r ? rowToLabel(r as Record<string, unknown>) : null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async createLabel(input: {
|
2026-05-09 23:09:13 +00:00
|
|
|
name: string;
|
|
|
|
|
description?: string | null;
|
|
|
|
|
color?: string | null;
|
2026-05-23 00:07:44 +00:00
|
|
|
}): Promise<Label> {
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
const id = uuidv7();
|
|
|
|
|
await this._run(
|
|
|
|
|
`INSERT INTO labels (id, name, description, color)
|
|
|
|
|
VALUES (?, ?, ?, ?)`,
|
|
|
|
|
[id, input.name, input.description ?? null, input.color ?? null],
|
2026-05-23 00:07:44 +00:00
|
|
|
);
|
2026-05-09 23:09:13 +00:00
|
|
|
void this.notify("labels", "create", id);
|
2026-05-23 00:07:44 +00:00
|
|
|
const r = await this._get("SELECT * FROM labels WHERE id = ?", [id]);
|
2026-05-09 23:09:13 +00:00
|
|
|
if (!r) throw new Error("label vanished after insert");
|
|
|
|
|
return rowToLabel(r as Record<string, unknown>);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Get-or-create label by name (used during pairing's free-text label input). */
|
2026-05-23 00:07:44 +00:00
|
|
|
async ensureLabel(name: string): Promise<Label> {
|
|
|
|
|
return (await this.getLabelByName(name)) ?? (await this.createLabel({ name }));
|
2026-05-09 23:09:13 +00:00
|
|
|
}
|
|
|
|
|
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
async attachKioskLabel(kioskId: string, labelId: string, role: LabelRole): Promise<void> {
|
2026-05-23 00:07:44 +00:00
|
|
|
await this._run(
|
2026-05-09 23:09:13 +00:00
|
|
|
`INSERT OR IGNORE INTO kiosk_labels (kiosk_id, label_id, role)
|
|
|
|
|
VALUES (?, ?, ?)`,
|
2026-05-23 00:07:44 +00:00
|
|
|
[kioskId, labelId, role],
|
|
|
|
|
);
|
2026-05-09 23:09:13 +00:00
|
|
|
}
|
|
|
|
|
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
async listKioskLabels(kioskId: string): Promise<Array<KioskLabel & { name: string }>> {
|
2026-05-23 00:07:44 +00:00
|
|
|
const rs = await this._all(
|
2026-05-09 23:09:13 +00:00
|
|
|
`SELECT kl.kiosk_id, kl.label_id, kl.role, l.name
|
|
|
|
|
FROM kiosk_labels kl
|
|
|
|
|
JOIN labels l ON l.id = kl.label_id
|
|
|
|
|
WHERE kl.kiosk_id = ?`,
|
2026-05-23 00:07:44 +00:00
|
|
|
[kioskId],
|
|
|
|
|
);
|
2026-05-09 23:09:13 +00:00
|
|
|
return rs.map((r) => {
|
|
|
|
|
const row = r as Record<string, unknown>;
|
|
|
|
|
return {
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
kiosk_id: String(row["kiosk_id"]),
|
|
|
|
|
label_id: String(row["label_id"]),
|
2026-05-09 23:09:13 +00:00
|
|
|
role: String(row["role"]) as LabelRole,
|
|
|
|
|
name: String(row["name"]),
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
async attachCameraLabel(cameraId: string, labelId: string): Promise<void> {
|
2026-05-23 00:07:44 +00:00
|
|
|
await this._run(
|
2026-05-09 23:09:13 +00:00
|
|
|
`INSERT OR IGNORE INTO camera_labels (camera_id, label_id)
|
|
|
|
|
VALUES (?, ?)`,
|
2026-05-23 00:07:44 +00:00
|
|
|
[cameraId, labelId],
|
|
|
|
|
);
|
2026-05-09 23:09:13 +00:00
|
|
|
}
|
|
|
|
|
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
async attachLayoutLabel(layoutId: string, labelId: string): Promise<void> {
|
2026-05-23 00:07:44 +00:00
|
|
|
await this._run(
|
2026-05-09 23:09:13 +00:00
|
|
|
`INSERT OR IGNORE INTO layout_labels (layout_id, label_id)
|
|
|
|
|
VALUES (?, ?)`,
|
2026-05-23 00:07:44 +00:00
|
|
|
[layoutId, labelId],
|
|
|
|
|
);
|
2026-05-09 23:09:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===========================================================================
|
|
|
|
|
// kiosks
|
|
|
|
|
// ===========================================================================
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async listKiosks(): Promise<Kiosk[]> {
|
|
|
|
|
const rs = await this._all("SELECT * FROM kiosks ORDER BY name");
|
2026-05-09 23:09:13 +00:00
|
|
|
return rs.map((r) => rowToKiosk(r as Record<string, unknown>));
|
|
|
|
|
}
|
|
|
|
|
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
async getKioskById(id: string): Promise<Kiosk | null> {
|
2026-05-23 00:07:44 +00:00
|
|
|
const r = await this._get("SELECT * FROM kiosks WHERE id = ?", [id]);
|
2026-05-09 23:09:13 +00:00
|
|
|
return r ? rowToKiosk(r as Record<string, unknown>) : null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async getKioskByName(name: string): Promise<Kiosk | null> {
|
|
|
|
|
const r = await this._get("SELECT * FROM kiosks WHERE name = ?", [name]);
|
2026-05-09 23:09:13 +00:00
|
|
|
return r ? rowToKiosk(r as Record<string, unknown>) : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Lookup candidates by Bearer-key prefix; verify hash at the call site. */
|
2026-05-23 00:07:44 +00:00
|
|
|
async listKiosksByKeyPrefix(prefix: string): Promise<Kiosk[]> {
|
|
|
|
|
const rs = await this._all(
|
2026-05-26 00:08:09 +00:00
|
|
|
"SELECT * FROM kiosks WHERE key_prefix = ? AND enabled = true",
|
2026-05-23 00:07:44 +00:00
|
|
|
[prefix],
|
|
|
|
|
);
|
2026-05-09 23:09:13 +00:00
|
|
|
return rs.map((r) => rowToKiosk(r as Record<string, unknown>));
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async createKiosk(input: {
|
2026-05-09 23:09:13 +00:00
|
|
|
name: string;
|
|
|
|
|
key_hash: string;
|
|
|
|
|
key_prefix: string;
|
|
|
|
|
capabilities?: string[];
|
|
|
|
|
hardware_model?: string | null;
|
2026-05-20 01:18:11 +00:00
|
|
|
managed_image?: boolean;
|
2026-05-23 00:07:44 +00:00
|
|
|
}): Promise<Kiosk> {
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
const id = uuidv7();
|
|
|
|
|
await this._run(
|
2026-05-09 23:09:13 +00:00
|
|
|
`INSERT INTO kiosks
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
(id, name, key_hash, key_prefix, capabilities, hardware_model, paired_at, managed_image)
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
2026-05-23 00:07:44 +00:00
|
|
|
[
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
id,
|
2026-05-23 00:07:44 +00:00
|
|
|
input.name,
|
|
|
|
|
input.key_hash,
|
|
|
|
|
input.key_prefix,
|
|
|
|
|
J(input.capabilities ?? []),
|
|
|
|
|
input.hardware_model ?? null,
|
|
|
|
|
isoNow(),
|
|
|
|
|
input.managed_image ? 1 : 0,
|
|
|
|
|
],
|
2026-05-09 23:09:13 +00:00
|
|
|
);
|
|
|
|
|
void this.notify("kiosks", "create", id);
|
2026-05-23 00:07:44 +00:00
|
|
|
const k = await this.getKioskById(id);
|
2026-05-09 23:09:13 +00:00
|
|
|
if (!k) throw new Error("kiosk vanished after insert");
|
|
|
|
|
return k;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-13 18:56:42 +00:00
|
|
|
/**
|
|
|
|
|
* Rekey an existing kiosk for a replacement device. Preserves identity
|
|
|
|
|
* (id, name) and downstream references (display_id, labels, gpio bindings,
|
|
|
|
|
* layouts that mention it), but issues fresh credentials + capabilities and
|
|
|
|
|
* resets transient runtime state so the old hardware can't reconnect.
|
|
|
|
|
*/
|
2026-05-23 00:07:44 +00:00
|
|
|
async replaceKioskKey(
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
id: string,
|
2026-05-13 18:56:42 +00:00
|
|
|
input: {
|
|
|
|
|
key_hash: string;
|
|
|
|
|
key_prefix: string;
|
|
|
|
|
capabilities?: string[];
|
|
|
|
|
hardware_model?: string | null;
|
|
|
|
|
},
|
2026-05-23 00:07:44 +00:00
|
|
|
): Promise<void> {
|
|
|
|
|
await this._run(
|
2026-05-13 18:56:42 +00:00
|
|
|
`UPDATE kiosks SET
|
|
|
|
|
key_hash = ?,
|
|
|
|
|
key_prefix = ?,
|
|
|
|
|
capabilities = ?,
|
|
|
|
|
hardware_model = ?,
|
|
|
|
|
paired_at = ?,
|
|
|
|
|
last_seen_at = NULL,
|
|
|
|
|
last_bundle_version = NULL,
|
|
|
|
|
kiosk_app_version = NULL,
|
|
|
|
|
os_version = NULL,
|
|
|
|
|
cpu_temp_c = NULL,
|
2026-05-21 00:03:05 +00:00
|
|
|
cpu_load_percent = NULL,
|
2026-05-13 18:56:42 +00:00
|
|
|
fan_rpm = NULL,
|
2026-05-21 00:03:05 +00:00
|
|
|
fan_pwm = NULL,
|
|
|
|
|
memory_total_mb = NULL,
|
|
|
|
|
memory_used_mb = NULL,
|
|
|
|
|
disk_total_mb = NULL,
|
|
|
|
|
disk_free_mb = NULL,
|
|
|
|
|
disk_used_percent = NULL
|
2026-05-13 18:56:42 +00:00
|
|
|
WHERE id = ?`,
|
2026-05-23 00:07:44 +00:00
|
|
|
[
|
|
|
|
|
input.key_hash,
|
|
|
|
|
input.key_prefix,
|
|
|
|
|
J(input.capabilities ?? []),
|
|
|
|
|
input.hardware_model ?? null,
|
|
|
|
|
isoNow(),
|
|
|
|
|
id,
|
|
|
|
|
],
|
2026-05-13 18:56:42 +00:00
|
|
|
);
|
|
|
|
|
void this.notify("kiosks", "update", id);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async touchKiosk(
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
id: string,
|
2026-05-09 23:09:13 +00:00
|
|
|
patch: {
|
|
|
|
|
bundle_version?: string | null;
|
|
|
|
|
kiosk_app_version?: string | null;
|
|
|
|
|
os_version?: string | null;
|
2026-05-11 09:47:07 +00:00
|
|
|
cpu_temp_c?: number | null;
|
2026-05-21 00:03:05 +00:00
|
|
|
cpu_load_percent?: number | null;
|
2026-05-11 09:47:07 +00:00
|
|
|
fan_rpm?: number | null;
|
|
|
|
|
fan_pwm?: number | null;
|
2026-05-21 00:03:05 +00:00
|
|
|
memory_total_mb?: number | null;
|
|
|
|
|
memory_used_mb?: number | null;
|
|
|
|
|
disk_total_mb?: number | null;
|
|
|
|
|
disk_free_mb?: number | null;
|
|
|
|
|
disk_used_percent?: number | null;
|
2026-05-14 05:24:21 +00:00
|
|
|
local_key?: string | null;
|
|
|
|
|
local_port?: number | null;
|
|
|
|
|
local_last_ip?: string | null;
|
2026-05-21 07:23:50 +00:00
|
|
|
reported_hostname?: string | null;
|
|
|
|
|
network_interfaces_json?: string | null;
|
2026-05-09 23:09:13 +00:00
|
|
|
},
|
2026-05-23 00:07:44 +00:00
|
|
|
): Promise<void> {
|
|
|
|
|
await this._run(
|
2026-05-09 23:09:13 +00:00
|
|
|
`UPDATE kiosks SET
|
|
|
|
|
last_seen_at = ?,
|
|
|
|
|
last_bundle_version = COALESCE(?, last_bundle_version),
|
|
|
|
|
kiosk_app_version = COALESCE(?, kiosk_app_version),
|
2026-05-11 09:47:07 +00:00
|
|
|
os_version = COALESCE(?, os_version),
|
|
|
|
|
cpu_temp_c = ?,
|
2026-05-21 00:03:05 +00:00
|
|
|
cpu_load_percent = ?,
|
2026-05-11 09:47:07 +00:00
|
|
|
fan_rpm = ?,
|
2026-05-14 05:24:21 +00:00
|
|
|
fan_pwm = ?,
|
2026-05-21 00:03:05 +00:00
|
|
|
memory_total_mb = ?,
|
|
|
|
|
memory_used_mb = ?,
|
|
|
|
|
disk_total_mb = ?,
|
|
|
|
|
disk_free_mb = ?,
|
|
|
|
|
disk_used_percent = ?,
|
2026-05-14 05:24:21 +00:00
|
|
|
local_key = COALESCE(?, local_key),
|
|
|
|
|
local_port = COALESCE(?, local_port),
|
2026-05-21 07:23:50 +00:00
|
|
|
local_last_ip = COALESCE(?, local_last_ip),
|
|
|
|
|
reported_hostname = COALESCE(?, reported_hostname),
|
|
|
|
|
network_interfaces_json = COALESCE(?, network_interfaces_json)
|
2026-05-09 23:09:13 +00:00
|
|
|
WHERE id = ?`,
|
2026-05-23 00:07:44 +00:00
|
|
|
[
|
|
|
|
|
isoNow(),
|
|
|
|
|
patch.bundle_version ?? null,
|
|
|
|
|
patch.kiosk_app_version ?? null,
|
|
|
|
|
patch.os_version ?? null,
|
|
|
|
|
patch.cpu_temp_c ?? null,
|
|
|
|
|
patch.cpu_load_percent ?? null,
|
|
|
|
|
patch.fan_rpm ?? null,
|
|
|
|
|
patch.fan_pwm ?? null,
|
|
|
|
|
patch.memory_total_mb ?? null,
|
|
|
|
|
patch.memory_used_mb ?? null,
|
|
|
|
|
patch.disk_total_mb ?? null,
|
|
|
|
|
patch.disk_free_mb ?? null,
|
|
|
|
|
patch.disk_used_percent ?? null,
|
|
|
|
|
patch.local_key ?? null,
|
|
|
|
|
patch.local_port ?? null,
|
|
|
|
|
patch.local_last_ip ?? null,
|
|
|
|
|
patch.reported_hostname ?? null,
|
|
|
|
|
patch.network_interfaces_json ?? null,
|
|
|
|
|
id,
|
|
|
|
|
],
|
2026-05-09 23:09:13 +00:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-14 05:38:18 +00:00
|
|
|
// ===========================================================================
|
|
|
|
|
// audit_log
|
|
|
|
|
// ===========================================================================
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async insertAudit(input: {
|
2026-05-14 05:38:18 +00:00
|
|
|
actor_type: AuditActorType;
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
actor_id: string | null;
|
2026-05-14 05:38:18 +00:00
|
|
|
actor_label: string | null;
|
|
|
|
|
action: string;
|
|
|
|
|
resource_type: string | null;
|
|
|
|
|
resource_id: string | null;
|
|
|
|
|
ip: string | null;
|
|
|
|
|
metadata: Record<string, unknown>;
|
|
|
|
|
result: AuditResult;
|
2026-05-23 00:07:44 +00:00
|
|
|
}): Promise<void> {
|
|
|
|
|
await this._run(
|
2026-05-14 05:38:18 +00:00
|
|
|
`INSERT INTO audit_log
|
|
|
|
|
(actor_type, actor_id, actor_label, action, resource_type,
|
|
|
|
|
resource_id, ip, metadata, result)
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
2026-05-23 00:07:44 +00:00
|
|
|
[
|
|
|
|
|
input.actor_type,
|
|
|
|
|
input.actor_id,
|
|
|
|
|
input.actor_label,
|
|
|
|
|
input.action,
|
|
|
|
|
input.resource_type,
|
|
|
|
|
input.resource_id,
|
|
|
|
|
input.ip,
|
|
|
|
|
J(input.metadata),
|
|
|
|
|
input.result,
|
|
|
|
|
],
|
2026-05-14 05:38:18 +00:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async listAudit(opts: {
|
2026-05-14 05:38:18 +00:00
|
|
|
limit?: number;
|
|
|
|
|
actor_type?: AuditActorType;
|
|
|
|
|
action_prefix?: string;
|
2026-05-23 00:07:44 +00:00
|
|
|
} = {}): Promise<AuditEntry[]> {
|
2026-05-14 05:38:18 +00:00
|
|
|
const limit = Math.min(Math.max(opts.limit ?? 200, 1), 1000);
|
|
|
|
|
const where: string[] = [];
|
|
|
|
|
const args: unknown[] = [];
|
|
|
|
|
if (opts.actor_type) {
|
|
|
|
|
where.push("actor_type = ?");
|
|
|
|
|
args.push(opts.actor_type);
|
|
|
|
|
}
|
|
|
|
|
if (opts.action_prefix) {
|
|
|
|
|
where.push("action LIKE ?");
|
|
|
|
|
args.push(`${opts.action_prefix}%`);
|
|
|
|
|
}
|
|
|
|
|
const sql = `SELECT * FROM audit_log ${where.length ? `WHERE ${where.join(" AND ")}` : ""} ORDER BY ts DESC LIMIT ?`;
|
|
|
|
|
args.push(limit);
|
2026-05-23 00:07:44 +00:00
|
|
|
const rs = await this._all(sql, args);
|
2026-05-14 05:38:18 +00:00
|
|
|
return rs.map((r) => rowToAuditEntry(r as Record<string, unknown>));
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-13 18:56:42 +00:00
|
|
|
// ===========================================================================
|
|
|
|
|
// firmware_releases + firmware_rollouts
|
|
|
|
|
// ===========================================================================
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async createFirmwareRelease(input: {
|
2026-05-13 18:56:42 +00:00
|
|
|
id: string;
|
|
|
|
|
version: string;
|
|
|
|
|
channel: FirmwareChannel;
|
|
|
|
|
arch: string;
|
|
|
|
|
artifact_path: string;
|
|
|
|
|
size_bytes: number;
|
|
|
|
|
sha256: string;
|
|
|
|
|
signature: string;
|
|
|
|
|
release_notes: string | null;
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
uploaded_by: string | null;
|
2026-05-23 00:07:44 +00:00
|
|
|
}): Promise<FirmwareRelease> {
|
|
|
|
|
await this._run(
|
2026-05-13 18:56:42 +00:00
|
|
|
`INSERT INTO firmware_releases
|
|
|
|
|
(id, version, channel, arch, artifact_path, size_bytes, sha256,
|
|
|
|
|
signature, release_notes, uploaded_by)
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
2026-05-23 00:07:44 +00:00
|
|
|
[
|
|
|
|
|
input.id,
|
|
|
|
|
input.version,
|
|
|
|
|
input.channel,
|
|
|
|
|
input.arch,
|
|
|
|
|
input.artifact_path,
|
|
|
|
|
input.size_bytes,
|
|
|
|
|
input.sha256,
|
|
|
|
|
input.signature,
|
|
|
|
|
input.release_notes,
|
|
|
|
|
input.uploaded_by,
|
|
|
|
|
],
|
2026-05-13 18:56:42 +00:00
|
|
|
);
|
|
|
|
|
void this.notify("firmware_releases", "create", input.id);
|
2026-05-23 00:07:44 +00:00
|
|
|
const r = await this.getFirmwareRelease(input.id);
|
2026-05-13 18:56:42 +00:00
|
|
|
if (!r) throw new Error("firmware release vanished after insert");
|
|
|
|
|
return r;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async getFirmwareRelease(id: string): Promise<FirmwareRelease | null> {
|
|
|
|
|
const r = await this._get("SELECT * FROM firmware_releases WHERE id = ?", [id]);
|
2026-05-13 18:56:42 +00:00
|
|
|
return r ? rowToFirmwareRelease(r as Record<string, unknown>) : null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async getFirmwareReleaseByVersionArch(version: string, arch: string): Promise<FirmwareRelease | null> {
|
|
|
|
|
const r = await this._get(
|
2026-05-13 18:56:42 +00:00
|
|
|
"SELECT * FROM firmware_releases WHERE version = ? AND arch = ?",
|
2026-05-23 00:07:44 +00:00
|
|
|
[version, arch],
|
|
|
|
|
);
|
2026-05-13 18:56:42 +00:00
|
|
|
return r ? rowToFirmwareRelease(r as Record<string, unknown>) : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Latest non-yanked release for a (channel, arch) pair. */
|
2026-05-23 00:07:44 +00:00
|
|
|
async getLatestFirmwareRelease(channel: FirmwareChannel, arch: string): Promise<FirmwareRelease | null> {
|
|
|
|
|
const r = await this._get(
|
2026-05-13 18:56:42 +00:00
|
|
|
`SELECT * FROM firmware_releases
|
|
|
|
|
WHERE channel = ? AND arch = ? AND yanked_at IS NULL
|
|
|
|
|
ORDER BY uploaded_at DESC
|
|
|
|
|
LIMIT 1`,
|
2026-05-23 00:07:44 +00:00
|
|
|
[channel, arch],
|
|
|
|
|
);
|
2026-05-13 18:56:42 +00:00
|
|
|
return r ? rowToFirmwareRelease(r as Record<string, unknown>) : null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async listFirmwareReleases(): Promise<FirmwareRelease[]> {
|
|
|
|
|
const rs = await this._all(
|
2026-05-13 18:56:42 +00:00
|
|
|
"SELECT * FROM firmware_releases ORDER BY uploaded_at DESC",
|
2026-05-23 00:07:44 +00:00
|
|
|
);
|
2026-05-13 18:56:42 +00:00
|
|
|
return rs.map((r) => rowToFirmwareRelease(r as Record<string, unknown>));
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async yankFirmwareRelease(id: string): Promise<void> {
|
|
|
|
|
await this._run("UPDATE firmware_releases SET yanked_at = ? WHERE id = ?", [isoNow(), id]);
|
2026-05-13 18:56:42 +00:00
|
|
|
void this.notify("firmware_releases", "update", id);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 00:56:56 +00:00
|
|
|
async deleteFirmwareRelease(id: string): Promise<void> {
|
|
|
|
|
await this._run("DELETE FROM firmware_releases WHERE id = ?", [id]);
|
|
|
|
|
void this.notify("firmware_releases", "delete", id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async listYankedFirmwareReleases(): Promise<FirmwareRelease[]> {
|
|
|
|
|
const rs = await this._all(
|
|
|
|
|
"SELECT * FROM firmware_releases WHERE yanked_at IS NOT NULL ORDER BY yanked_at ASC",
|
|
|
|
|
);
|
|
|
|
|
return rs.map((r) => rowToFirmwareRelease(r as Record<string, unknown>));
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-13 18:56:42 +00:00
|
|
|
/** Mark the per-kiosk firmware attempt state (called from /api/kiosk/firmware/applied). */
|
2026-05-23 00:07:44 +00:00
|
|
|
async recordKioskFirmwareAttempt(
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
kioskId: string,
|
2026-05-13 18:56:42 +00:00
|
|
|
version: string,
|
|
|
|
|
error: string | null,
|
2026-05-23 00:07:44 +00:00
|
|
|
): Promise<void> {
|
|
|
|
|
await this._run(
|
2026-05-13 18:56:42 +00:00
|
|
|
`UPDATE kiosks SET
|
|
|
|
|
firmware_last_attempt_at = ?,
|
|
|
|
|
firmware_last_attempt_version = ?,
|
|
|
|
|
firmware_last_error = ?
|
|
|
|
|
WHERE id = ?`,
|
2026-05-23 00:07:44 +00:00
|
|
|
[isoNow(), version, error, kioskId],
|
|
|
|
|
);
|
2026-05-13 18:56:42 +00:00
|
|
|
void this.notify("kiosks", "update", kioskId);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Set the per-kiosk update channel + optional explicit version pin. */
|
2026-05-23 00:07:44 +00:00
|
|
|
async setKioskFirmwarePref(
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
kioskId: string,
|
2026-05-13 18:56:42 +00:00
|
|
|
patch: { channel?: FirmwareChannel; target_version?: string | null },
|
2026-05-23 00:07:44 +00:00
|
|
|
): Promise<void> {
|
2026-05-13 18:56:42 +00:00
|
|
|
const sets: string[] = [];
|
|
|
|
|
const vals: unknown[] = [];
|
|
|
|
|
if (patch.channel !== undefined) {
|
|
|
|
|
sets.push("firmware_channel = ?");
|
|
|
|
|
vals.push(patch.channel);
|
|
|
|
|
}
|
|
|
|
|
if (patch.target_version !== undefined) {
|
|
|
|
|
sets.push("firmware_target_version = ?");
|
|
|
|
|
vals.push(patch.target_version);
|
|
|
|
|
}
|
|
|
|
|
if (sets.length === 0) return;
|
|
|
|
|
vals.push(kioskId);
|
2026-05-23 00:07:44 +00:00
|
|
|
await this._run(`UPDATE kiosks SET ${sets.join(", ")} WHERE id = ?`, vals);
|
2026-05-13 18:56:42 +00:00
|
|
|
void this.notify("kiosks", "update", kioskId);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async createFirmwareRollout(input: {
|
2026-05-13 18:56:42 +00:00
|
|
|
id: string;
|
|
|
|
|
release_id: string;
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
target_kiosk_ids: string[];
|
2026-05-13 18:56:42 +00:00
|
|
|
percentage: number;
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
created_by: string | null;
|
2026-05-23 00:07:44 +00:00
|
|
|
}): Promise<FirmwareRollout> {
|
|
|
|
|
await this._run(
|
2026-05-13 18:56:42 +00:00
|
|
|
`INSERT INTO firmware_rollouts
|
|
|
|
|
(id, release_id, target_kiosk_ids, percentage, created_by, state)
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, 'queued')`,
|
2026-05-23 00:07:44 +00:00
|
|
|
[
|
|
|
|
|
input.id,
|
|
|
|
|
input.release_id,
|
|
|
|
|
J(input.target_kiosk_ids),
|
|
|
|
|
input.percentage,
|
|
|
|
|
input.created_by,
|
|
|
|
|
],
|
2026-05-13 18:56:42 +00:00
|
|
|
);
|
|
|
|
|
void this.notify("firmware_rollouts", "create", input.id);
|
2026-05-23 00:07:44 +00:00
|
|
|
const r = await this.getFirmwareRollout(input.id);
|
2026-05-13 18:56:42 +00:00
|
|
|
if (!r) throw new Error("rollout vanished after insert");
|
|
|
|
|
return r;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async getFirmwareRollout(id: string): Promise<FirmwareRollout | null> {
|
|
|
|
|
const r = await this._get("SELECT * FROM firmware_rollouts WHERE id = ?", [id]);
|
2026-05-13 18:56:42 +00:00
|
|
|
return r ? rowToFirmwareRollout(r as Record<string, unknown>) : null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-14 05:28:20 +00:00
|
|
|
/**
|
|
|
|
|
* Active rollouts whose target list either includes this kiosk OR is
|
|
|
|
|
* empty (= "all kiosks on the release channel"). Ordered most-recent first
|
|
|
|
|
* so a newer rollout supersedes older ones.
|
|
|
|
|
*/
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
async listActiveRolloutsForKiosk(kioskId: string): Promise<FirmwareRollout[]> {
|
2026-05-23 00:07:44 +00:00
|
|
|
const rs = await this._all(
|
2026-05-14 05:28:20 +00:00
|
|
|
`SELECT * FROM firmware_rollouts WHERE state = 'active' ORDER BY created_at DESC`,
|
2026-05-23 00:07:44 +00:00
|
|
|
);
|
2026-05-14 05:28:20 +00:00
|
|
|
return rs
|
|
|
|
|
.map((r) => rowToFirmwareRollout(r as Record<string, unknown>))
|
|
|
|
|
.filter((r) => r.target_kiosk_ids.length === 0 || r.target_kiosk_ids.includes(kioskId));
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async listFirmwareRollouts(): Promise<FirmwareRollout[]> {
|
|
|
|
|
const rs = await this._all(
|
2026-05-13 18:56:42 +00:00
|
|
|
"SELECT * FROM firmware_rollouts ORDER BY created_at DESC",
|
2026-05-23 00:07:44 +00:00
|
|
|
);
|
2026-05-13 18:56:42 +00:00
|
|
|
return rs.map((r) => rowToFirmwareRollout(r as Record<string, unknown>));
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async updateFirmwareRolloutState(
|
2026-05-13 18:56:42 +00:00
|
|
|
id: string,
|
|
|
|
|
state: FirmwareRolloutState,
|
2026-05-23 00:07:44 +00:00
|
|
|
): Promise<void> {
|
2026-05-13 18:56:42 +00:00
|
|
|
const now = isoNow();
|
|
|
|
|
if (state === "active") {
|
2026-05-23 00:07:44 +00:00
|
|
|
await this._run(
|
2026-05-13 18:56:42 +00:00
|
|
|
`UPDATE firmware_rollouts SET state = ?, started_at = COALESCE(started_at, ?) WHERE id = ?`,
|
2026-05-23 00:07:44 +00:00
|
|
|
[state, now, id],
|
|
|
|
|
);
|
2026-05-13 18:56:42 +00:00
|
|
|
} else if (state === "complete") {
|
2026-05-23 00:07:44 +00:00
|
|
|
await this._run(
|
2026-05-13 18:56:42 +00:00
|
|
|
`UPDATE firmware_rollouts SET state = ?, finished_at = ? WHERE id = ?`,
|
2026-05-23 00:07:44 +00:00
|
|
|
[state, now, id],
|
|
|
|
|
);
|
2026-05-13 18:56:42 +00:00
|
|
|
} else {
|
2026-05-23 00:07:44 +00:00
|
|
|
await this._run(`UPDATE firmware_rollouts SET state = ? WHERE id = ?`, [state, id]);
|
2026-05-13 18:56:42 +00:00
|
|
|
}
|
|
|
|
|
void this.notify("firmware_rollouts", "update", id);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-20 04:19:46 +00:00
|
|
|
// ===========================================================================
|
|
|
|
|
// os_update_releases + os_update_rollouts
|
|
|
|
|
// ===========================================================================
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async createOsUpdateRelease(input: {
|
2026-05-20 04:19:46 +00:00
|
|
|
id: string;
|
|
|
|
|
version: string;
|
|
|
|
|
channel: FirmwareChannel;
|
|
|
|
|
compatibility: string;
|
|
|
|
|
artifact_path: string;
|
|
|
|
|
size_bytes: number;
|
|
|
|
|
sha256: string;
|
|
|
|
|
release_notes: string | null;
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
uploaded_by: string | null;
|
2026-05-23 00:07:44 +00:00
|
|
|
}): Promise<OsUpdateRelease> {
|
|
|
|
|
await this._run(
|
2026-05-20 04:19:46 +00:00
|
|
|
`INSERT INTO os_update_releases
|
|
|
|
|
(id, version, channel, compatibility, artifact_path, size_bytes, sha256,
|
|
|
|
|
bundle_format, release_notes, uploaded_by)
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, 'raucb', ?, ?)`,
|
2026-05-23 00:07:44 +00:00
|
|
|
[
|
|
|
|
|
input.id,
|
|
|
|
|
input.version,
|
|
|
|
|
input.channel,
|
|
|
|
|
input.compatibility,
|
|
|
|
|
input.artifact_path,
|
|
|
|
|
input.size_bytes,
|
|
|
|
|
input.sha256,
|
|
|
|
|
input.release_notes,
|
|
|
|
|
input.uploaded_by,
|
|
|
|
|
],
|
2026-05-20 04:19:46 +00:00
|
|
|
);
|
|
|
|
|
void this.notify("os_update_releases", "create", input.id);
|
2026-05-23 00:07:44 +00:00
|
|
|
const r = await this.getOsUpdateRelease(input.id);
|
2026-05-20 04:19:46 +00:00
|
|
|
if (!r) throw new Error("OS update release vanished after insert");
|
|
|
|
|
return r;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async getOsUpdateRelease(id: string): Promise<OsUpdateRelease | null> {
|
|
|
|
|
const r = await this._get("SELECT * FROM os_update_releases WHERE id = ?", [id]);
|
2026-05-20 04:19:46 +00:00
|
|
|
return r ? rowToOsUpdateRelease(r as Record<string, unknown>) : null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async getOsUpdateReleaseByVersionCompatibility(version: string, compatibility: string): Promise<OsUpdateRelease | null> {
|
|
|
|
|
const r = await this._get(
|
2026-05-20 04:19:46 +00:00
|
|
|
"SELECT * FROM os_update_releases WHERE version = ? AND compatibility = ?",
|
2026-05-23 00:07:44 +00:00
|
|
|
[version, compatibility],
|
|
|
|
|
);
|
2026-05-20 04:19:46 +00:00
|
|
|
return r ? rowToOsUpdateRelease(r as Record<string, unknown>) : null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async getLatestOsUpdateRelease(channel: FirmwareChannel, compatibility: string): Promise<OsUpdateRelease | null> {
|
|
|
|
|
const r = await this._get(
|
2026-05-20 04:19:46 +00:00
|
|
|
`SELECT * FROM os_update_releases
|
|
|
|
|
WHERE channel = ? AND compatibility = ? AND yanked_at IS NULL
|
|
|
|
|
ORDER BY uploaded_at DESC
|
|
|
|
|
LIMIT 1`,
|
2026-05-23 00:07:44 +00:00
|
|
|
[channel, compatibility],
|
|
|
|
|
);
|
2026-05-20 04:19:46 +00:00
|
|
|
return r ? rowToOsUpdateRelease(r as Record<string, unknown>) : null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async listOsUpdateReleases(): Promise<OsUpdateRelease[]> {
|
|
|
|
|
const rs = await this._all(
|
2026-05-20 04:19:46 +00:00
|
|
|
"SELECT * FROM os_update_releases ORDER BY uploaded_at DESC",
|
2026-05-23 00:07:44 +00:00
|
|
|
);
|
2026-05-20 04:19:46 +00:00
|
|
|
return rs.map((r) => rowToOsUpdateRelease(r as Record<string, unknown>));
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async yankOsUpdateRelease(id: string): Promise<void> {
|
|
|
|
|
await this._run("UPDATE os_update_releases SET yanked_at = ? WHERE id = ?", [isoNow(), id]);
|
2026-05-20 04:19:46 +00:00
|
|
|
void this.notify("os_update_releases", "update", id);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 00:56:56 +00:00
|
|
|
async deleteOsUpdateRelease(id: string): Promise<void> {
|
|
|
|
|
await this._run("DELETE FROM os_update_releases WHERE id = ?", [id]);
|
|
|
|
|
void this.notify("os_update_releases", "delete", id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async listYankedOsUpdateReleases(): Promise<OsUpdateRelease[]> {
|
|
|
|
|
const rs = await this._all(
|
|
|
|
|
"SELECT * FROM os_update_releases WHERE yanked_at IS NOT NULL ORDER BY yanked_at ASC",
|
|
|
|
|
);
|
|
|
|
|
return rs.map((r) => rowToOsUpdateRelease(r as Record<string, unknown>));
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async recordKioskOsUpdateAttempt(
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
kioskId: string,
|
2026-05-20 04:19:46 +00:00
|
|
|
version: string,
|
|
|
|
|
error: string | null,
|
2026-05-23 00:07:44 +00:00
|
|
|
): Promise<void> {
|
|
|
|
|
await this._run(
|
2026-05-20 04:19:46 +00:00
|
|
|
`UPDATE kiosks SET
|
|
|
|
|
os_update_last_attempt_at = ?,
|
|
|
|
|
os_update_last_attempt_version = ?,
|
|
|
|
|
os_update_last_error = ?
|
|
|
|
|
WHERE id = ?`,
|
2026-05-23 00:07:44 +00:00
|
|
|
[isoNow(), version, error, kioskId],
|
|
|
|
|
);
|
2026-05-20 04:19:46 +00:00
|
|
|
void this.notify("kiosks", "update", kioskId);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async setKioskOsUpdatePref(
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
kioskId: string,
|
2026-05-20 04:19:46 +00:00
|
|
|
patch: { channel?: FirmwareChannel; target_version?: string | null },
|
2026-05-23 00:07:44 +00:00
|
|
|
): Promise<void> {
|
2026-05-20 04:19:46 +00:00
|
|
|
const sets: string[] = [];
|
|
|
|
|
const vals: unknown[] = [];
|
|
|
|
|
if (patch.channel !== undefined) {
|
|
|
|
|
sets.push("os_update_channel = ?");
|
|
|
|
|
vals.push(patch.channel);
|
|
|
|
|
}
|
|
|
|
|
if (patch.target_version !== undefined) {
|
|
|
|
|
sets.push("os_update_target_version = ?");
|
|
|
|
|
vals.push(patch.target_version);
|
|
|
|
|
}
|
|
|
|
|
if (sets.length === 0) return;
|
|
|
|
|
vals.push(kioskId);
|
2026-05-23 00:07:44 +00:00
|
|
|
await this._run(`UPDATE kiosks SET ${sets.join(", ")} WHERE id = ?`, vals);
|
2026-05-20 04:19:46 +00:00
|
|
|
void this.notify("kiosks", "update", kioskId);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async createOsUpdateRollout(input: {
|
2026-05-20 04:19:46 +00:00
|
|
|
id: string;
|
|
|
|
|
release_id: string;
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
target_kiosk_ids: string[];
|
2026-05-20 04:19:46 +00:00
|
|
|
percentage: number;
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
created_by: string | null;
|
2026-05-23 00:07:44 +00:00
|
|
|
}): Promise<OsUpdateRollout> {
|
|
|
|
|
await this._run(
|
2026-05-20 04:19:46 +00:00
|
|
|
`INSERT INTO os_update_rollouts
|
|
|
|
|
(id, release_id, target_kiosk_ids, percentage, created_by, state)
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, 'queued')`,
|
2026-05-23 00:07:44 +00:00
|
|
|
[
|
|
|
|
|
input.id,
|
|
|
|
|
input.release_id,
|
|
|
|
|
J(input.target_kiosk_ids),
|
|
|
|
|
input.percentage,
|
|
|
|
|
input.created_by,
|
|
|
|
|
],
|
2026-05-20 04:19:46 +00:00
|
|
|
);
|
|
|
|
|
void this.notify("os_update_rollouts", "create", input.id);
|
2026-05-23 00:07:44 +00:00
|
|
|
const r = await this.getOsUpdateRollout(input.id);
|
2026-05-20 04:19:46 +00:00
|
|
|
if (!r) throw new Error("OS update rollout vanished after insert");
|
|
|
|
|
return r;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async getOsUpdateRollout(id: string): Promise<OsUpdateRollout | null> {
|
|
|
|
|
const r = await this._get("SELECT * FROM os_update_rollouts WHERE id = ?", [id]);
|
2026-05-20 04:19:46 +00:00
|
|
|
return r ? rowToOsUpdateRollout(r as Record<string, unknown>) : null;
|
|
|
|
|
}
|
|
|
|
|
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
async listActiveOsUpdateRolloutsForKiosk(kioskId: string): Promise<OsUpdateRollout[]> {
|
2026-05-23 00:07:44 +00:00
|
|
|
const rs = await this._all(
|
2026-05-20 04:19:46 +00:00
|
|
|
`SELECT * FROM os_update_rollouts WHERE state = 'active' ORDER BY created_at DESC`,
|
2026-05-23 00:07:44 +00:00
|
|
|
);
|
2026-05-20 04:19:46 +00:00
|
|
|
return rs
|
|
|
|
|
.map((r) => rowToOsUpdateRollout(r as Record<string, unknown>))
|
|
|
|
|
.filter((r) => r.target_kiosk_ids.length === 0 || r.target_kiosk_ids.includes(kioskId));
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async listOsUpdateRollouts(): Promise<OsUpdateRollout[]> {
|
|
|
|
|
const rs = await this._all(
|
2026-05-20 04:19:46 +00:00
|
|
|
"SELECT * FROM os_update_rollouts ORDER BY created_at DESC",
|
2026-05-23 00:07:44 +00:00
|
|
|
);
|
2026-05-20 04:19:46 +00:00
|
|
|
return rs.map((r) => rowToOsUpdateRollout(r as Record<string, unknown>));
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async updateOsUpdateRolloutState(
|
2026-05-20 04:19:46 +00:00
|
|
|
id: string,
|
|
|
|
|
state: OsUpdateRolloutState,
|
2026-05-23 00:07:44 +00:00
|
|
|
): Promise<void> {
|
2026-05-20 04:19:46 +00:00
|
|
|
const now = isoNow();
|
|
|
|
|
if (state === "active") {
|
2026-05-23 00:07:44 +00:00
|
|
|
await this._run(
|
2026-05-20 04:19:46 +00:00
|
|
|
`UPDATE os_update_rollouts SET state = ?, started_at = COALESCE(started_at, ?) WHERE id = ?`,
|
2026-05-23 00:07:44 +00:00
|
|
|
[state, now, id],
|
|
|
|
|
);
|
2026-05-20 04:19:46 +00:00
|
|
|
} else if (state === "complete") {
|
2026-05-23 00:07:44 +00:00
|
|
|
await this._run(
|
2026-05-20 04:19:46 +00:00
|
|
|
`UPDATE os_update_rollouts SET state = ?, finished_at = ? WHERE id = ?`,
|
2026-05-23 00:07:44 +00:00
|
|
|
[state, now, id],
|
|
|
|
|
);
|
2026-05-20 04:19:46 +00:00
|
|
|
} else {
|
2026-05-23 00:07:44 +00:00
|
|
|
await this._run(`UPDATE os_update_rollouts SET state = ? WHERE id = ?`, [state, id]);
|
2026-05-20 04:19:46 +00:00
|
|
|
}
|
|
|
|
|
void this.notify("os_update_rollouts", "update", id);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-09 23:09:13 +00:00
|
|
|
// ===========================================================================
|
|
|
|
|
// pairing_codes
|
|
|
|
|
// ===========================================================================
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async createPairingCode(input: {
|
2026-05-09 23:09:13 +00:00
|
|
|
code: string;
|
|
|
|
|
kiosk_proposed_name: string | null;
|
|
|
|
|
kiosk_hardware_model: string | null;
|
|
|
|
|
kiosk_capabilities: string[];
|
|
|
|
|
expires_at: string;
|
|
|
|
|
extras: Record<string, unknown>;
|
2026-05-23 00:07:44 +00:00
|
|
|
}): Promise<PairingCode> {
|
|
|
|
|
await this._run(
|
2026-05-09 23:09:13 +00:00
|
|
|
`INSERT INTO pairing_codes
|
|
|
|
|
(code, kiosk_proposed_name, kiosk_hardware_model, kiosk_capabilities,
|
|
|
|
|
expires_at, extras)
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?)`,
|
2026-05-23 00:07:44 +00:00
|
|
|
[
|
|
|
|
|
input.code,
|
|
|
|
|
input.kiosk_proposed_name,
|
|
|
|
|
input.kiosk_hardware_model,
|
|
|
|
|
J(input.kiosk_capabilities),
|
|
|
|
|
input.expires_at,
|
|
|
|
|
J(input.extras),
|
|
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
const r = await this._get("SELECT * FROM pairing_codes WHERE code = ?", [input.code]);
|
2026-05-09 23:09:13 +00:00
|
|
|
if (!r) throw new Error("pairing_code vanished after insert");
|
|
|
|
|
return rowToPairingCode(r as Record<string, unknown>);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async getPairingCode(code: string): Promise<PairingCode | null> {
|
|
|
|
|
const r = await this._get("SELECT * FROM pairing_codes WHERE code = ?", [code]);
|
2026-05-09 23:09:13 +00:00
|
|
|
return r ? rowToPairingCode(r as Record<string, unknown>) : null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async listPendingPairingCodes(): Promise<PairingCode[]> {
|
|
|
|
|
const rs = await this._all(
|
2026-05-09 23:09:13 +00:00
|
|
|
`SELECT * FROM pairing_codes
|
|
|
|
|
WHERE consumed_at IS NULL AND expires_at > ?
|
|
|
|
|
ORDER BY issued_at DESC`,
|
2026-05-23 00:07:44 +00:00
|
|
|
[isoNow()],
|
|
|
|
|
);
|
2026-05-09 23:09:13 +00:00
|
|
|
return rs.map((r) => rowToPairingCode(r as Record<string, unknown>));
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async markPairingCodeClaimed(
|
2026-05-09 23:09:13 +00:00
|
|
|
code: string,
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
kioskId: string,
|
2026-05-09 23:09:13 +00:00
|
|
|
extras: Record<string, unknown>,
|
2026-05-23 00:07:44 +00:00
|
|
|
): Promise<void> {
|
|
|
|
|
await this._run(
|
2026-05-09 23:09:13 +00:00
|
|
|
`UPDATE pairing_codes
|
|
|
|
|
SET consumed_at = ?,
|
|
|
|
|
consumed_by_kiosk_id = ?,
|
|
|
|
|
extras = ?
|
|
|
|
|
WHERE code = ?`,
|
2026-05-23 00:07:44 +00:00
|
|
|
[isoNow(), kioskId, J(extras), code],
|
|
|
|
|
);
|
2026-05-09 23:09:13 +00:00
|
|
|
}
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async updatePairingCodeExtras(code: string, extras: Record<string, unknown>): Promise<void> {
|
|
|
|
|
await this._run("UPDATE pairing_codes SET extras = ? WHERE code = ?", [
|
2026-05-09 23:09:13 +00:00
|
|
|
J(extras),
|
|
|
|
|
code,
|
2026-05-23 00:07:44 +00:00
|
|
|
]);
|
2026-05-09 23:09:13 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===========================================================================
|
|
|
|
|
// event_log
|
|
|
|
|
// ===========================================================================
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async insertEvent(input: {
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
source_kiosk_id: string | null;
|
|
|
|
|
source_camera_id: string | null;
|
2026-05-09 23:09:13 +00:00
|
|
|
source_type: EventSourceType;
|
|
|
|
|
topic: string;
|
|
|
|
|
property_op: string | null;
|
|
|
|
|
payload: Record<string, unknown>;
|
|
|
|
|
forwarded_to_nodered: boolean;
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
}): Promise<string> {
|
|
|
|
|
const id = uuidv7();
|
|
|
|
|
await this._run(
|
2026-05-09 23:09:13 +00:00
|
|
|
`INSERT INTO event_log
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
(id, source_kiosk_id, source_camera_id, source_type, topic,
|
2026-05-09 23:09:13 +00:00
|
|
|
property_op, payload, forwarded_to_nodered)
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
2026-05-23 00:07:44 +00:00
|
|
|
[
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
id,
|
2026-05-23 00:07:44 +00:00
|
|
|
input.source_kiosk_id,
|
|
|
|
|
input.source_camera_id,
|
|
|
|
|
input.source_type,
|
|
|
|
|
input.topic,
|
|
|
|
|
input.property_op,
|
|
|
|
|
J(input.payload),
|
2026-05-23 10:55:04 +00:00
|
|
|
Boolean(input.forwarded_to_nodered),
|
2026-05-23 00:07:44 +00:00
|
|
|
],
|
2026-05-09 23:09:13 +00:00
|
|
|
);
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
return id;
|
2026-05-09 23:09:13 +00:00
|
|
|
}
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async recentEvents(limit = 10): Promise<EventLog[]> {
|
|
|
|
|
const rs = await this._all(
|
2026-05-09 23:09:13 +00:00
|
|
|
"SELECT * FROM event_log ORDER BY received_at DESC LIMIT ?",
|
2026-05-23 00:07:44 +00:00
|
|
|
[limit],
|
|
|
|
|
);
|
2026-05-09 23:09:13 +00:00
|
|
|
return rs.map((r) => rowToEventLog(r as Record<string, unknown>));
|
|
|
|
|
}
|
|
|
|
|
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
async markEventForwarded(eventId: string): Promise<void> {
|
2026-05-24 03:18:43 +00:00
|
|
|
await this._run("UPDATE event_log SET forwarded_to_nodered = ? WHERE id = ?", [true, eventId]);
|
2026-05-21 09:34:29 +00:00
|
|
|
}
|
|
|
|
|
|
2026-05-22 23:30:26 +00:00
|
|
|
/**
|
|
|
|
|
* Delete event_log rows older than `days` AND trim to `maxRows` total.
|
|
|
|
|
* Returns the number of rows deleted.
|
|
|
|
|
*/
|
2026-05-23 00:07:44 +00:00
|
|
|
async purgeEventLog(days: number = 30, maxRows: number = 100_000): Promise<number> {
|
2026-05-22 23:30:26 +00:00
|
|
|
const cutoff = new Date(Date.now() - days * 86_400_000).toISOString();
|
2026-05-23 00:07:44 +00:00
|
|
|
const r1 = await this._run("DELETE FROM event_log WHERE received_at < ?", [cutoff]);
|
2026-05-22 23:30:26 +00:00
|
|
|
// Trim to maxRows by deleting oldest beyond the cap.
|
2026-05-23 00:07:44 +00:00
|
|
|
const r2 = await this._run(
|
2026-05-22 23:30:26 +00:00
|
|
|
`DELETE FROM event_log WHERE id NOT IN (
|
|
|
|
|
SELECT id FROM event_log ORDER BY received_at DESC LIMIT ?
|
|
|
|
|
)`,
|
2026-05-23 00:07:44 +00:00
|
|
|
[maxRows],
|
|
|
|
|
);
|
2026-05-22 23:30:26 +00:00
|
|
|
return Number(r1.changes) + Number(r2.changes);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async purgeAuditLog(days: number = 90): Promise<number> {
|
2026-05-22 23:30:26 +00:00
|
|
|
const cutoff = new Date(Date.now() - days * 86_400_000).toISOString();
|
2026-05-23 00:07:44 +00:00
|
|
|
const r = await this._run("DELETE FROM audit_log WHERE ts < ?", [cutoff]);
|
2026-05-22 23:30:26 +00:00
|
|
|
return Number(r.changes);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async purgeKioskLogs(days: number = 14): Promise<number> {
|
2026-05-22 23:30:26 +00:00
|
|
|
const cutoff = new Date(Date.now() - days * 86_400_000).toISOString();
|
2026-05-23 00:07:44 +00:00
|
|
|
const r = await this._run("DELETE FROM kiosk_logs WHERE received_at < ?", [cutoff]);
|
2026-05-22 23:30:26 +00:00
|
|
|
return Number(r.changes);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async queryEvents(filters: EventQueryFilters): Promise<{ events: EventLog[]; total: number }> {
|
2026-05-21 09:34:29 +00:00
|
|
|
const where: string[] = [];
|
|
|
|
|
const params: (string | number)[] = [];
|
|
|
|
|
|
|
|
|
|
if (filters.topic) {
|
|
|
|
|
where.push("topic = ?");
|
|
|
|
|
params.push(filters.topic);
|
|
|
|
|
}
|
|
|
|
|
if (filters.kiosk_id != null) {
|
|
|
|
|
where.push("source_kiosk_id = ?");
|
|
|
|
|
params.push(filters.kiosk_id);
|
|
|
|
|
}
|
2026-05-21 10:07:32 +00:00
|
|
|
if (filters.camera_id != null) {
|
|
|
|
|
where.push("source_camera_id = ?");
|
|
|
|
|
params.push(filters.camera_id);
|
|
|
|
|
}
|
|
|
|
|
if (filters.source_type) {
|
|
|
|
|
where.push("source_type = ?");
|
|
|
|
|
params.push(filters.source_type);
|
|
|
|
|
}
|
2026-05-21 09:34:29 +00:00
|
|
|
if (filters.from) {
|
|
|
|
|
where.push("received_at >= ?");
|
|
|
|
|
params.push(filters.from);
|
|
|
|
|
}
|
|
|
|
|
if (filters.to) {
|
|
|
|
|
where.push("received_at <= ?");
|
|
|
|
|
params.push(filters.to);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const clause = where.length > 0 ? `WHERE ${where.join(" AND ")}` : "";
|
|
|
|
|
const limit = filters.limit ?? 50;
|
|
|
|
|
const offset = filters.offset ?? 0;
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
const countRow = await this._get<Record<string, unknown>>(`SELECT COUNT(*) as cnt FROM event_log ${clause}`, params);
|
2026-05-21 09:34:29 +00:00
|
|
|
const total = Number(countRow?.["cnt"] ?? 0);
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
const rs = await this._all(
|
2026-05-21 09:34:29 +00:00
|
|
|
`SELECT * FROM event_log ${clause} ORDER BY received_at DESC LIMIT ? OFFSET ?`,
|
2026-05-23 00:07:44 +00:00
|
|
|
[...params, limit, offset],
|
|
|
|
|
);
|
2026-05-21 09:34:29 +00:00
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
events: rs.map((r) => rowToEventLog(r as Record<string, unknown>)),
|
|
|
|
|
total,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===========================================================================
|
|
|
|
|
// kiosk_logs
|
|
|
|
|
// ===========================================================================
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async insertKioskLogs(
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
kioskId: string,
|
2026-05-21 09:34:29 +00:00
|
|
|
entries: Array<{ level: KioskLogLevel; message: string; context?: Record<string, unknown>; logged_at?: string }>,
|
2026-05-23 00:07:44 +00:00
|
|
|
): Promise<number> {
|
2026-05-21 09:34:29 +00:00
|
|
|
const now = isoNow();
|
|
|
|
|
let count = 0;
|
|
|
|
|
for (const e of entries) {
|
2026-05-23 00:07:44 +00:00
|
|
|
await this._run(
|
|
|
|
|
`INSERT INTO kiosk_logs (kiosk_id, level, message, context, logged_at)
|
|
|
|
|
VALUES (?, ?, ?, ?, ?)`,
|
|
|
|
|
[kioskId, e.level, e.message, J(e.context ?? {}), e.logged_at ?? now],
|
|
|
|
|
);
|
2026-05-21 09:34:29 +00:00
|
|
|
count++;
|
|
|
|
|
}
|
2026-05-23 00:07:44 +00:00
|
|
|
await this.trimKioskLogs(kioskId, 500);
|
2026-05-21 09:34:29 +00:00
|
|
|
return count;
|
|
|
|
|
}
|
|
|
|
|
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
private async trimKioskLogs(kioskId: string, maxRows: number): Promise<void> {
|
2026-05-23 00:07:44 +00:00
|
|
|
await this._run(
|
2026-05-21 09:34:29 +00:00
|
|
|
`DELETE FROM kiosk_logs WHERE kiosk_id = ? AND id NOT IN (
|
|
|
|
|
SELECT id FROM kiosk_logs WHERE kiosk_id = ? ORDER BY received_at DESC LIMIT ?
|
|
|
|
|
)`,
|
2026-05-23 00:07:44 +00:00
|
|
|
[kioskId, kioskId, maxRows],
|
|
|
|
|
);
|
2026-05-21 09:34:29 +00:00
|
|
|
}
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async purgeOldKioskLogs(maxAgeHours: number): Promise<number> {
|
2026-05-21 09:34:29 +00:00
|
|
|
const cutoff = new Date(Date.now() - maxAgeHours * 3600_000).toISOString();
|
2026-05-23 00:07:44 +00:00
|
|
|
const result = await this._run(
|
2026-05-21 09:34:29 +00:00
|
|
|
"DELETE FROM kiosk_logs WHERE received_at < ?",
|
2026-05-23 00:07:44 +00:00
|
|
|
[cutoff],
|
|
|
|
|
);
|
2026-05-21 09:34:29 +00:00
|
|
|
return Number(result.changes);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async queryKioskLogs(filters: KioskLogQueryFilters): Promise<{ logs: KioskLog[]; total: number }> {
|
2026-05-21 09:34:29 +00:00
|
|
|
const where: string[] = ["kiosk_id = ?"];
|
|
|
|
|
const params: (string | number)[] = [filters.kiosk_id];
|
|
|
|
|
|
|
|
|
|
if (filters.level) {
|
|
|
|
|
where.push("level = ?");
|
|
|
|
|
params.push(filters.level);
|
|
|
|
|
}
|
|
|
|
|
if (filters.from) {
|
|
|
|
|
where.push("received_at >= ?");
|
|
|
|
|
params.push(filters.from);
|
|
|
|
|
}
|
|
|
|
|
if (filters.to) {
|
|
|
|
|
where.push("received_at <= ?");
|
|
|
|
|
params.push(filters.to);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const clause = `WHERE ${where.join(" AND ")}`;
|
|
|
|
|
const limit = filters.limit ?? 50;
|
|
|
|
|
const offset = filters.offset ?? 0;
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
const countRow = await this._get<Record<string, unknown>>(`SELECT COUNT(*) as cnt FROM kiosk_logs ${clause}`, params);
|
2026-05-21 09:34:29 +00:00
|
|
|
const total = Number(countRow?.["cnt"] ?? 0);
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
const rs = await this._all(
|
2026-05-21 09:34:29 +00:00
|
|
|
`SELECT * FROM kiosk_logs ${clause} ORDER BY received_at DESC LIMIT ? OFFSET ?`,
|
2026-05-23 00:07:44 +00:00
|
|
|
[...params, limit, offset],
|
|
|
|
|
);
|
2026-05-21 09:34:29 +00:00
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
logs: rs.map((r) => rowToKioskLog(r as Record<string, unknown>)),
|
|
|
|
|
total,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-09 23:09:13 +00:00
|
|
|
// ===========================================================================
|
|
|
|
|
// bundle queries (label-aware composite reads)
|
|
|
|
|
// ===========================================================================
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Returns label IDs + names attached to a kiosk by role.
|
|
|
|
|
* Used by `service-bundle` to scope a kiosk's view of the world.
|
|
|
|
|
*/
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
async bundleScope(kioskId: string): Promise<{
|
|
|
|
|
labelIds: string[];
|
2026-05-09 23:09:13 +00:00
|
|
|
labelNames: string[];
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
operateLabelIds: string[];
|
2026-05-09 23:09:13 +00:00
|
|
|
operateLabelNames: string[];
|
2026-05-23 00:07:44 +00:00
|
|
|
}> {
|
|
|
|
|
const all = await this.listKioskLabels(kioskId);
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
const labelIds: string[] = [];
|
2026-05-09 23:09:13 +00:00
|
|
|
const labelNames: string[] = [];
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
const operateLabelIds: string[] = [];
|
2026-05-09 23:09:13 +00:00
|
|
|
const operateLabelNames: string[] = [];
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
const seen = new Set<string>();
|
2026-05-09 23:09:13 +00:00
|
|
|
for (const kl of all) {
|
|
|
|
|
if (!seen.has(kl.label_id)) {
|
|
|
|
|
seen.add(kl.label_id);
|
|
|
|
|
labelIds.push(kl.label_id);
|
|
|
|
|
labelNames.push(kl.name);
|
|
|
|
|
}
|
|
|
|
|
if (kl.role === "operate") {
|
|
|
|
|
operateLabelIds.push(kl.label_id);
|
|
|
|
|
operateLabelNames.push(kl.name);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return { labelIds, labelNames, operateLabelIds, operateLabelNames };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Cameras whose label set intersects the given label IDs. */
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
async camerasForLabelIds(labelIds: string[]): Promise<Camera[]> {
|
2026-05-09 23:09:13 +00:00
|
|
|
if (labelIds.length === 0) return [];
|
|
|
|
|
const placeholders = labelIds.map(() => "?").join(",");
|
2026-05-23 00:07:44 +00:00
|
|
|
const rs = await this._all(
|
|
|
|
|
`SELECT DISTINCT c.* FROM cameras c
|
|
|
|
|
JOIN camera_labels cl ON cl.camera_id = c.id
|
|
|
|
|
WHERE cl.label_id IN (${placeholders})
|
2026-05-26 00:08:09 +00:00
|
|
|
AND c.enabled = true
|
2026-05-23 00:07:44 +00:00
|
|
|
ORDER BY c.name`,
|
|
|
|
|
labelIds,
|
|
|
|
|
);
|
2026-05-09 23:09:13 +00:00
|
|
|
return rs.map((r) => rowToCamera(r as Record<string, unknown>));
|
|
|
|
|
}
|
|
|
|
|
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
async layoutsForLabelIds(labelIds: string[]): Promise<Layout[]> {
|
2026-05-09 23:09:13 +00:00
|
|
|
if (labelIds.length === 0) return [];
|
|
|
|
|
const placeholders = labelIds.map(() => "?").join(",");
|
2026-05-23 00:07:44 +00:00
|
|
|
const rs = await this._all(
|
|
|
|
|
`SELECT DISTINCT l.* FROM layouts l
|
|
|
|
|
JOIN layout_labels ll ON ll.layout_id = l.id
|
|
|
|
|
WHERE ll.label_id IN (${placeholders})
|
|
|
|
|
ORDER BY l.name`,
|
|
|
|
|
labelIds,
|
|
|
|
|
);
|
2026-05-09 23:09:13 +00:00
|
|
|
return rs.map((r) => rowToLayout(r as Record<string, unknown>));
|
|
|
|
|
}
|
|
|
|
|
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
async layoutCells(layoutId: string): Promise<LayoutCell[]> {
|
2026-05-10 19:55:19 +00:00
|
|
|
return this.listLayoutCells(layoutId);
|
2026-05-09 23:09:13 +00:00
|
|
|
}
|
|
|
|
|
|
2026-05-10 20:03:32 +00:00
|
|
|
// Deprecated — layout_templates dropped in v0.5
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
layoutTemplates(_ids: string[]): LayoutTemplate[] {
|
2026-05-10 20:03:32 +00:00
|
|
|
return [];
|
2026-05-09 23:09:13 +00:00
|
|
|
}
|
|
|
|
|
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
async cameraLabelNames(cameraId: string): Promise<string[]> {
|
2026-05-23 00:07:44 +00:00
|
|
|
const rs = await this._all(
|
2026-05-09 23:09:13 +00:00
|
|
|
`SELECT l.name FROM camera_labels cl
|
|
|
|
|
JOIN labels l ON l.id = cl.label_id
|
|
|
|
|
WHERE cl.camera_id = ?`,
|
2026-05-23 00:07:44 +00:00
|
|
|
[cameraId],
|
|
|
|
|
);
|
2026-05-09 23:09:13 +00:00
|
|
|
return rs.map((r) => String((r as Record<string, unknown>)["name"]));
|
|
|
|
|
}
|
feat: full CRUD for cameras, kiosks, and labels in admin UI
- Camera edit page: rename, update RTSP/ONVIF, enable/disable,
attach/detach labels, view streams, delete
- Kiosk edit page: rename, enable/disable, attach/detach labels
with role (consume/operate), delete
- Labels page: create with color picker, delete
- Camera/kiosk names now link to edit pages
- Repository: added updateCamera, deleteCamera, updateKiosk,
deleteKiosk, detachCameraLabel, detachKioskLabel, deleteLabel,
updateLabel, cameraLabelIds
2026-05-10 01:24:04 +00:00
|
|
|
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
async cameraLabelIds(cameraId: string): Promise<Array<{ label_id: string; name: string }>> {
|
2026-05-23 00:07:44 +00:00
|
|
|
const rs = await this._all(
|
feat: full CRUD for cameras, kiosks, and labels in admin UI
- Camera edit page: rename, update RTSP/ONVIF, enable/disable,
attach/detach labels, view streams, delete
- Kiosk edit page: rename, enable/disable, attach/detach labels
with role (consume/operate), delete
- Labels page: create with color picker, delete
- Camera/kiosk names now link to edit pages
- Repository: added updateCamera, deleteCamera, updateKiosk,
deleteKiosk, detachCameraLabel, detachKioskLabel, deleteLabel,
updateLabel, cameraLabelIds
2026-05-10 01:24:04 +00:00
|
|
|
`SELECT cl.label_id, l.name FROM camera_labels cl
|
|
|
|
|
JOIN labels l ON l.id = cl.label_id
|
|
|
|
|
WHERE cl.camera_id = ?`,
|
2026-05-23 00:07:44 +00:00
|
|
|
[cameraId],
|
|
|
|
|
);
|
feat: full CRUD for cameras, kiosks, and labels in admin UI
- Camera edit page: rename, update RTSP/ONVIF, enable/disable,
attach/detach labels, view streams, delete
- Kiosk edit page: rename, enable/disable, attach/detach labels
with role (consume/operate), delete
- Labels page: create with color picker, delete
- Camera/kiosk names now link to edit pages
- Repository: added updateCamera, deleteCamera, updateKiosk,
deleteKiosk, detachCameraLabel, detachKioskLabel, deleteLabel,
updateLabel, cameraLabelIds
2026-05-10 01:24:04 +00:00
|
|
|
return rs.map((r) => {
|
|
|
|
|
const row = r as Record<string, unknown>;
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
return { label_id: String(row["label_id"]), name: String(row["name"]) };
|
feat: full CRUD for cameras, kiosks, and labels in admin UI
- Camera edit page: rename, update RTSP/ONVIF, enable/disable,
attach/detach labels, view streams, delete
- Kiosk edit page: rename, enable/disable, attach/detach labels
with role (consume/operate), delete
- Labels page: create with color picker, delete
- Camera/kiosk names now link to edit pages
- Repository: added updateCamera, deleteCamera, updateKiosk,
deleteKiosk, detachCameraLabel, detachKioskLabel, deleteLabel,
updateLabel, cameraLabelIds
2026-05-10 01:24:04 +00:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
async updateCamera(id: string, patch: Partial<Camera>): Promise<void> {
|
feat: full CRUD for cameras, kiosks, and labels in admin UI
- Camera edit page: rename, update RTSP/ONVIF, enable/disable,
attach/detach labels, view streams, delete
- Kiosk edit page: rename, enable/disable, attach/detach labels
with role (consume/operate), delete
- Labels page: create with color picker, delete
- Camera/kiosk names now link to edit pages
- Repository: added updateCamera, deleteCamera, updateKiosk,
deleteKiosk, detachCameraLabel, detachKioskLabel, deleteLabel,
updateLabel, cameraLabelIds
2026-05-10 01:24:04 +00:00
|
|
|
const sets: string[] = [];
|
|
|
|
|
const vals: unknown[] = [];
|
|
|
|
|
for (const [k, v] of Object.entries(patch)) {
|
|
|
|
|
if (k === "id" || k === "created_at") continue;
|
|
|
|
|
sets.push(`${k} = ?`);
|
2026-05-10 22:20:48 +00:00
|
|
|
if (k === "capabilities") vals.push(J(v));
|
2026-05-23 10:55:04 +00:00
|
|
|
else if (typeof v === "boolean") vals.push(v);
|
2026-05-10 22:20:48 +00:00
|
|
|
else vals.push(v === undefined ? null : v);
|
feat: full CRUD for cameras, kiosks, and labels in admin UI
- Camera edit page: rename, update RTSP/ONVIF, enable/disable,
attach/detach labels, view streams, delete
- Kiosk edit page: rename, enable/disable, attach/detach labels
with role (consume/operate), delete
- Labels page: create with color picker, delete
- Camera/kiosk names now link to edit pages
- Repository: added updateCamera, deleteCamera, updateKiosk,
deleteKiosk, detachCameraLabel, detachKioskLabel, deleteLabel,
updateLabel, cameraLabelIds
2026-05-10 01:24:04 +00:00
|
|
|
}
|
|
|
|
|
if (sets.length === 0) return;
|
|
|
|
|
vals.push(id);
|
2026-05-23 00:07:44 +00:00
|
|
|
await this._run(`UPDATE cameras SET ${sets.join(", ")} WHERE id = ?`, vals);
|
feat: full CRUD for cameras, kiosks, and labels in admin UI
- Camera edit page: rename, update RTSP/ONVIF, enable/disable,
attach/detach labels, view streams, delete
- Kiosk edit page: rename, enable/disable, attach/detach labels
with role (consume/operate), delete
- Labels page: create with color picker, delete
- Camera/kiosk names now link to edit pages
- Repository: added updateCamera, deleteCamera, updateKiosk,
deleteKiosk, detachCameraLabel, detachKioskLabel, deleteLabel,
updateLabel, cameraLabelIds
2026-05-10 01:24:04 +00:00
|
|
|
void this.notify("cameras", "update", id);
|
|
|
|
|
}
|
|
|
|
|
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
async deleteCamera(id: string): Promise<void> {
|
2026-05-23 00:07:44 +00:00
|
|
|
await this._run(`DELETE FROM camera_labels WHERE camera_id = ?`, [id]);
|
|
|
|
|
await this._run(`DELETE FROM camera_streams WHERE camera_id = ?`, [id]);
|
2026-05-10 21:18:44 +00:00
|
|
|
// Clear cells that referenced this camera (legacy column).
|
2026-05-23 00:07:44 +00:00
|
|
|
await this._run(`DELETE FROM layout_cells WHERE camera_id = ?`, [id]);
|
2026-05-10 21:18:44 +00:00
|
|
|
// entities row has ON DELETE CASCADE → camera-mirror entity goes away with
|
|
|
|
|
// the camera, which in turn sets layout_cells.entity_id NULL via the FK.
|
2026-05-23 00:07:44 +00:00
|
|
|
await this._run(`DELETE FROM cameras WHERE id = ?`, [id]);
|
feat: full CRUD for cameras, kiosks, and labels in admin UI
- Camera edit page: rename, update RTSP/ONVIF, enable/disable,
attach/detach labels, view streams, delete
- Kiosk edit page: rename, enable/disable, attach/detach labels
with role (consume/operate), delete
- Labels page: create with color picker, delete
- Camera/kiosk names now link to edit pages
- Repository: added updateCamera, deleteCamera, updateKiosk,
deleteKiosk, detachCameraLabel, detachKioskLabel, deleteLabel,
updateLabel, cameraLabelIds
2026-05-10 01:24:04 +00:00
|
|
|
void this.notify("cameras", "delete", id);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-10 21:18:44 +00:00
|
|
|
// ===========================================================================
|
|
|
|
|
// entities — reusable content pool (camera/html/web) bound to layout cells
|
|
|
|
|
// ===========================================================================
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async listEntities(): Promise<Entity[]> {
|
|
|
|
|
const rs = await this._all("SELECT * FROM entities ORDER BY name");
|
2026-05-10 21:18:44 +00:00
|
|
|
return rs.map((r) => rowToEntity(r as Record<string, unknown>));
|
|
|
|
|
}
|
|
|
|
|
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
async getEntityById(id: string): Promise<Entity | null> {
|
2026-05-23 00:07:44 +00:00
|
|
|
const r = await this._get("SELECT * FROM entities WHERE id = ?", [id]);
|
2026-05-10 21:18:44 +00:00
|
|
|
return r ? rowToEntity(r as Record<string, unknown>) : null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async getEntityByName(name: string): Promise<Entity | null> {
|
|
|
|
|
const r = await this._get("SELECT * FROM entities WHERE name = ?", [name]);
|
2026-05-10 21:18:44 +00:00
|
|
|
return r ? rowToEntity(r as Record<string, unknown>) : null;
|
|
|
|
|
}
|
|
|
|
|
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
async getEntityForCamera(cameraId: string): Promise<Entity | null> {
|
2026-05-23 00:07:44 +00:00
|
|
|
const r = await this._get(
|
2026-05-10 21:18:44 +00:00
|
|
|
`SELECT * FROM entities WHERE type = 'camera' AND camera_id = ? LIMIT 1`,
|
2026-05-23 00:07:44 +00:00
|
|
|
[cameraId],
|
|
|
|
|
);
|
2026-05-10 21:18:44 +00:00
|
|
|
return r ? rowToEntity(r as Record<string, unknown>) : null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async createEntity(input: {
|
2026-05-10 21:18:44 +00:00
|
|
|
name: string;
|
|
|
|
|
type: EntityType;
|
|
|
|
|
description?: string | null;
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
camera_id?: string | null;
|
2026-05-10 21:18:44 +00:00
|
|
|
html_content?: string | null;
|
|
|
|
|
web_url?: string | null;
|
2026-05-12 23:47:53 +00:00
|
|
|
dashboard_id?: string | null;
|
2026-05-23 00:07:44 +00:00
|
|
|
}): Promise<Entity> {
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
const id = uuidv7();
|
|
|
|
|
await this._run(
|
|
|
|
|
`INSERT INTO entities (id, name, type, description, camera_id, html_content, web_url, dashboard_id)
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
2026-05-23 00:07:44 +00:00
|
|
|
[
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
id,
|
2026-05-23 00:07:44 +00:00
|
|
|
input.name,
|
|
|
|
|
input.type,
|
|
|
|
|
input.description ?? null,
|
|
|
|
|
input.type === "camera" ? (input.camera_id ?? null) : null,
|
|
|
|
|
input.type === "html" ? (input.html_content ?? null) : null,
|
|
|
|
|
input.type === "web" ? (input.web_url ?? null) : null,
|
|
|
|
|
input.type === "dashboard" ? (input.dashboard_id ?? null) : null,
|
|
|
|
|
],
|
2026-05-10 21:18:44 +00:00
|
|
|
);
|
|
|
|
|
void this.notify("entities", "create", id);
|
2026-05-23 00:07:44 +00:00
|
|
|
const e = await this.getEntityById(id);
|
2026-05-10 21:18:44 +00:00
|
|
|
if (!e) throw new Error("entity vanished after insert");
|
|
|
|
|
return e;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-12 23:47:53 +00:00
|
|
|
/** Find a dashboard entity by Node-RED tab id (used by the sync flow). */
|
2026-05-23 00:07:44 +00:00
|
|
|
async getEntityForDashboard(dashboardId: string): Promise<Entity | null> {
|
|
|
|
|
const r = await this._get(
|
2026-05-12 23:47:53 +00:00
|
|
|
`SELECT * FROM entities WHERE type = 'dashboard' AND dashboard_id = ? LIMIT 1`,
|
2026-05-23 00:07:44 +00:00
|
|
|
[dashboardId],
|
|
|
|
|
);
|
2026-05-12 23:47:53 +00:00
|
|
|
return r ? rowToEntity(r as Record<string, unknown>) : null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async updateEntity(
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
id: string,
|
2026-05-10 21:18:44 +00:00
|
|
|
patch: {
|
|
|
|
|
name?: string;
|
|
|
|
|
description?: string | null;
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
camera_id?: string | null;
|
2026-05-10 21:18:44 +00:00
|
|
|
html_content?: string | null;
|
|
|
|
|
web_url?: string | null;
|
2026-05-12 23:47:53 +00:00
|
|
|
dashboard_id?: string | null;
|
2026-05-10 21:18:44 +00:00
|
|
|
},
|
2026-05-23 00:07:44 +00:00
|
|
|
): Promise<void> {
|
2026-05-10 21:18:44 +00:00
|
|
|
const sets: string[] = [];
|
|
|
|
|
const vals: unknown[] = [];
|
|
|
|
|
for (const [k, v] of Object.entries(patch)) {
|
|
|
|
|
sets.push(`${k} = ?`);
|
|
|
|
|
vals.push(v === undefined ? null : v);
|
|
|
|
|
}
|
|
|
|
|
if (sets.length === 0) return;
|
|
|
|
|
vals.push(id);
|
2026-05-23 00:07:44 +00:00
|
|
|
await this._run(`UPDATE entities SET ${sets.join(", ")} WHERE id = ?`, vals);
|
2026-05-10 21:18:44 +00:00
|
|
|
void this.notify("entities", "update", id);
|
|
|
|
|
|
|
|
|
|
// Propagate content fields into any cell that uses this entity, so the
|
2026-05-12 23:47:53 +00:00
|
|
|
// legacy cell columns stay aligned for bundle generation. Dashboard
|
|
|
|
|
// entities materialise as `web` cells pointing at /dash/<dashboard_id>.
|
2026-05-23 00:07:44 +00:00
|
|
|
const ent = await this.getEntityById(id);
|
2026-05-10 21:18:44 +00:00
|
|
|
if (!ent) return;
|
2026-05-12 23:47:53 +00:00
|
|
|
const cellContentType = ent.type === "dashboard" ? "web" : ent.type;
|
|
|
|
|
const cellWebUrl =
|
|
|
|
|
ent.type === "web" ? ent.web_url :
|
|
|
|
|
ent.type === "dashboard" && ent.dashboard_id ? `/dash/${ent.dashboard_id}` :
|
|
|
|
|
null;
|
2026-05-23 00:07:44 +00:00
|
|
|
await this._run(
|
|
|
|
|
`UPDATE layout_cells
|
|
|
|
|
SET content_type = ?,
|
|
|
|
|
camera_id = ?,
|
|
|
|
|
web_url = ?,
|
|
|
|
|
html_content = ?
|
|
|
|
|
WHERE entity_id = ?`,
|
|
|
|
|
[
|
2026-05-12 23:47:53 +00:00
|
|
|
cellContentType,
|
2026-05-10 21:18:44 +00:00
|
|
|
ent.type === "camera" ? ent.camera_id : null,
|
2026-05-12 23:47:53 +00:00
|
|
|
cellWebUrl,
|
2026-05-10 21:18:44 +00:00
|
|
|
ent.type === "html" ? ent.html_content : null,
|
|
|
|
|
id,
|
2026-05-23 00:07:44 +00:00
|
|
|
],
|
|
|
|
|
);
|
2026-05-10 21:18:44 +00:00
|
|
|
}
|
|
|
|
|
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
async deleteEntity(id: string): Promise<void> {
|
2026-05-10 21:18:44 +00:00
|
|
|
// FK ON DELETE SET NULL clears layout_cells.entity_id.
|
2026-05-23 00:07:44 +00:00
|
|
|
await this._run(`DELETE FROM entities WHERE id = ?`, [id]);
|
2026-05-10 21:18:44 +00:00
|
|
|
void this.notify("entities", "delete", id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Idempotent: ensure a camera-type entity exists for the given camera. If
|
|
|
|
|
* the camera's name is already taken by another entity, append the camera
|
|
|
|
|
* id to keep the name unique.
|
|
|
|
|
*/
|
2026-05-23 00:07:44 +00:00
|
|
|
async ensureCameraEntity(camera: Camera): Promise<Entity> {
|
|
|
|
|
const existing = await this.getEntityForCamera(camera.id);
|
2026-05-10 21:18:44 +00:00
|
|
|
if (existing) return existing;
|
|
|
|
|
let name = camera.name;
|
2026-05-23 00:07:44 +00:00
|
|
|
if (await this.getEntityByName(name)) {
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
name = `${camera.name} (cam ${camera.id.slice(0, 8)})`;
|
2026-05-10 21:18:44 +00:00
|
|
|
}
|
|
|
|
|
return this.createEntity({ name, type: "camera", camera_id: camera.id });
|
|
|
|
|
}
|
|
|
|
|
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
async updateKiosk(id: string, patch: Partial<Kiosk>): Promise<void> {
|
feat: full CRUD for cameras, kiosks, and labels in admin UI
- Camera edit page: rename, update RTSP/ONVIF, enable/disable,
attach/detach labels, view streams, delete
- Kiosk edit page: rename, enable/disable, attach/detach labels
with role (consume/operate), delete
- Labels page: create with color picker, delete
- Camera/kiosk names now link to edit pages
- Repository: added updateCamera, deleteCamera, updateKiosk,
deleteKiosk, detachCameraLabel, detachKioskLabel, deleteLabel,
updateLabel, cameraLabelIds
2026-05-10 01:24:04 +00:00
|
|
|
const sets: string[] = [];
|
|
|
|
|
const vals: unknown[] = [];
|
|
|
|
|
for (const [k, v] of Object.entries(patch)) {
|
|
|
|
|
if (k === "id" || k === "created_at" || k === "paired_at") continue;
|
|
|
|
|
sets.push(`${k} = ?`);
|
|
|
|
|
vals.push(v === undefined ? null : v);
|
|
|
|
|
}
|
|
|
|
|
if (sets.length === 0) return;
|
|
|
|
|
vals.push(id);
|
2026-05-23 00:07:44 +00:00
|
|
|
await this._run(`UPDATE kiosks SET ${sets.join(", ")} WHERE id = ?`, vals);
|
feat: full CRUD for cameras, kiosks, and labels in admin UI
- Camera edit page: rename, update RTSP/ONVIF, enable/disable,
attach/detach labels, view streams, delete
- Kiosk edit page: rename, enable/disable, attach/detach labels
with role (consume/operate), delete
- Labels page: create with color picker, delete
- Camera/kiosk names now link to edit pages
- Repository: added updateCamera, deleteCamera, updateKiosk,
deleteKiosk, detachCameraLabel, detachKioskLabel, deleteLabel,
updateLabel, cameraLabelIds
2026-05-10 01:24:04 +00:00
|
|
|
void this.notify("kiosks", "update", id);
|
|
|
|
|
}
|
|
|
|
|
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
async deleteKiosk(id: string): Promise<void> {
|
2026-05-23 00:07:44 +00:00
|
|
|
const displays = await this.listDisplaysForKiosk(id);
|
|
|
|
|
await this.transact(async () => {
|
2026-05-13 01:43:29 +00:00
|
|
|
for (const display of displays) {
|
2026-05-23 00:07:44 +00:00
|
|
|
await this._run(`DELETE FROM display_layouts WHERE display_id = ?`, [display.id]);
|
2026-05-13 01:43:29 +00:00
|
|
|
}
|
2026-05-23 00:07:44 +00:00
|
|
|
await this._run(`DELETE FROM displays WHERE kiosk_id = ?`, [id]);
|
|
|
|
|
await this._run(`DELETE FROM kiosk_labels WHERE kiosk_id = ?`, [id]);
|
|
|
|
|
await this._run(`DELETE FROM kiosk_gpio_bindings WHERE kiosk_id = ?`, [id]);
|
|
|
|
|
await this._run(`DELETE FROM kiosks WHERE id = ?`, [id]);
|
2026-05-13 01:43:29 +00:00
|
|
|
});
|
|
|
|
|
for (const display of displays) {
|
|
|
|
|
void this.notify("display_layouts", "delete", display.id);
|
|
|
|
|
void this.notify("displays", "delete", display.id);
|
|
|
|
|
}
|
feat: full CRUD for cameras, kiosks, and labels in admin UI
- Camera edit page: rename, update RTSP/ONVIF, enable/disable,
attach/detach labels, view streams, delete
- Kiosk edit page: rename, enable/disable, attach/detach labels
with role (consume/operate), delete
- Labels page: create with color picker, delete
- Camera/kiosk names now link to edit pages
- Repository: added updateCamera, deleteCamera, updateKiosk,
deleteKiosk, detachCameraLabel, detachKioskLabel, deleteLabel,
updateLabel, cameraLabelIds
2026-05-10 01:24:04 +00:00
|
|
|
void this.notify("kiosks", "delete", id);
|
|
|
|
|
}
|
|
|
|
|
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
async detachCameraLabel(cameraId: string, labelId: string): Promise<void> {
|
2026-05-23 00:07:44 +00:00
|
|
|
await this._run(`DELETE FROM camera_labels WHERE camera_id = ? AND label_id = ?`, [cameraId, labelId]);
|
feat: full CRUD for cameras, kiosks, and labels in admin UI
- Camera edit page: rename, update RTSP/ONVIF, enable/disable,
attach/detach labels, view streams, delete
- Kiosk edit page: rename, enable/disable, attach/detach labels
with role (consume/operate), delete
- Labels page: create with color picker, delete
- Camera/kiosk names now link to edit pages
- Repository: added updateCamera, deleteCamera, updateKiosk,
deleteKiosk, detachCameraLabel, detachKioskLabel, deleteLabel,
updateLabel, cameraLabelIds
2026-05-10 01:24:04 +00:00
|
|
|
}
|
|
|
|
|
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
async detachKioskLabel(kioskId: string, labelId: string): Promise<void> {
|
2026-05-23 00:07:44 +00:00
|
|
|
await this._run(`DELETE FROM kiosk_labels WHERE kiosk_id = ? AND label_id = ?`, [kioskId, labelId]);
|
feat: full CRUD for cameras, kiosks, and labels in admin UI
- Camera edit page: rename, update RTSP/ONVIF, enable/disable,
attach/detach labels, view streams, delete
- Kiosk edit page: rename, enable/disable, attach/detach labels
with role (consume/operate), delete
- Labels page: create with color picker, delete
- Camera/kiosk names now link to edit pages
- Repository: added updateCamera, deleteCamera, updateKiosk,
deleteKiosk, detachCameraLabel, detachKioskLabel, deleteLabel,
updateLabel, cameraLabelIds
2026-05-10 01:24:04 +00:00
|
|
|
}
|
|
|
|
|
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
async deleteLabel(id: string): Promise<void> {
|
2026-05-23 00:07:44 +00:00
|
|
|
await this._run(`DELETE FROM camera_labels WHERE label_id = ?`, [id]);
|
|
|
|
|
await this._run(`DELETE FROM kiosk_labels WHERE label_id = ?`, [id]);
|
|
|
|
|
await this._run(`DELETE FROM layout_labels WHERE label_id = ?`, [id]);
|
|
|
|
|
await this._run(`DELETE FROM labels WHERE id = ?`, [id]);
|
feat: full CRUD for cameras, kiosks, and labels in admin UI
- Camera edit page: rename, update RTSP/ONVIF, enable/disable,
attach/detach labels, view streams, delete
- Kiosk edit page: rename, enable/disable, attach/detach labels
with role (consume/operate), delete
- Labels page: create with color picker, delete
- Camera/kiosk names now link to edit pages
- Repository: added updateCamera, deleteCamera, updateKiosk,
deleteKiosk, detachCameraLabel, detachKioskLabel, deleteLabel,
updateLabel, cameraLabelIds
2026-05-10 01:24:04 +00:00
|
|
|
void this.notify("labels", "delete", id);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-12 23:18:22 +00:00
|
|
|
// ===========================================================================
|
|
|
|
|
// kiosk GPIO bindings
|
|
|
|
|
// ===========================================================================
|
|
|
|
|
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
async listGpioBindings(kioskId: string): Promise<KioskGpioBinding[]> {
|
2026-05-23 00:07:44 +00:00
|
|
|
const rs = await this._all(
|
2026-05-12 23:18:22 +00:00
|
|
|
"SELECT * FROM kiosk_gpio_bindings WHERE kiosk_id = ? ORDER BY chip, pin",
|
2026-05-23 00:07:44 +00:00
|
|
|
[kioskId],
|
|
|
|
|
);
|
2026-05-12 23:18:22 +00:00
|
|
|
return rs.map((r) => rowToKioskGpioBinding(r as Record<string, unknown>));
|
|
|
|
|
}
|
|
|
|
|
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
async getGpioBindingById(id: string): Promise<KioskGpioBinding | null> {
|
2026-05-23 00:07:44 +00:00
|
|
|
const r = await this._get("SELECT * FROM kiosk_gpio_bindings WHERE id = ?", [id]);
|
2026-05-12 23:18:22 +00:00
|
|
|
return r ? rowToKioskGpioBinding(r as Record<string, unknown>) : null;
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 00:07:44 +00:00
|
|
|
async createGpioBinding(input: {
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
kiosk_id: string;
|
2026-05-12 23:18:22 +00:00
|
|
|
chip?: string;
|
|
|
|
|
pin: number;
|
|
|
|
|
direction: GpioDirection;
|
|
|
|
|
pull?: GpioPull | null;
|
|
|
|
|
edge?: GpioEdge | null;
|
|
|
|
|
topic: string;
|
2026-05-23 00:07:44 +00:00
|
|
|
}): Promise<KioskGpioBinding> {
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
const id = uuidv7();
|
|
|
|
|
await this._run(
|
|
|
|
|
`INSERT INTO kiosk_gpio_bindings (id, kiosk_id, chip, pin, direction, pull, edge, topic)
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
2026-05-23 00:07:44 +00:00
|
|
|
[
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
id,
|
2026-05-23 00:07:44 +00:00
|
|
|
input.kiosk_id,
|
|
|
|
|
input.chip ?? "gpiochip0",
|
|
|
|
|
input.pin,
|
|
|
|
|
input.direction,
|
|
|
|
|
input.pull ?? null,
|
|
|
|
|
input.edge ?? null,
|
|
|
|
|
input.topic,
|
|
|
|
|
],
|
2026-05-12 23:18:22 +00:00
|
|
|
);
|
|
|
|
|
void this.notify("kiosk_gpio_bindings", "create", id);
|
2026-05-23 00:07:44 +00:00
|
|
|
const b = await this.getGpioBindingById(id);
|
2026-05-12 23:18:22 +00:00
|
|
|
if (!b) throw new Error("gpio binding vanished after insert");
|
|
|
|
|
return b;
|
|
|
|
|
}
|
|
|
|
|
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
async deleteGpioBinding(id: string): Promise<void> {
|
2026-05-23 00:07:44 +00:00
|
|
|
await this._run(`DELETE FROM kiosk_gpio_bindings WHERE id = ?`, [id]);
|
2026-05-12 23:18:22 +00:00
|
|
|
void this.notify("kiosk_gpio_bindings", "delete", id);
|
|
|
|
|
}
|
|
|
|
|
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
async updateLabel(id: string, patch: { name?: string; description?: string | null; color?: string | null }): Promise<void> {
|
feat: full CRUD for cameras, kiosks, and labels in admin UI
- Camera edit page: rename, update RTSP/ONVIF, enable/disable,
attach/detach labels, view streams, delete
- Kiosk edit page: rename, enable/disable, attach/detach labels
with role (consume/operate), delete
- Labels page: create with color picker, delete
- Camera/kiosk names now link to edit pages
- Repository: added updateCamera, deleteCamera, updateKiosk,
deleteKiosk, detachCameraLabel, detachKioskLabel, deleteLabel,
updateLabel, cameraLabelIds
2026-05-10 01:24:04 +00:00
|
|
|
const sets: string[] = [];
|
|
|
|
|
const vals: unknown[] = [];
|
|
|
|
|
for (const [k, v] of Object.entries(patch)) {
|
|
|
|
|
sets.push(`${k} = ?`);
|
|
|
|
|
vals.push(v === undefined ? null : v);
|
|
|
|
|
}
|
|
|
|
|
if (sets.length === 0) return;
|
|
|
|
|
vals.push(id);
|
2026-05-23 00:07:44 +00:00
|
|
|
await this._run(`UPDATE labels SET ${sets.join(", ")} WHERE id = ?`, vals);
|
feat: full CRUD for cameras, kiosks, and labels in admin UI
- Camera edit page: rename, update RTSP/ONVIF, enable/disable,
attach/detach labels, view streams, delete
- Kiosk edit page: rename, enable/disable, attach/detach labels
with role (consume/operate), delete
- Labels page: create with color picker, delete
- Camera/kiosk names now link to edit pages
- Repository: added updateCamera, deleteCamera, updateKiosk,
deleteKiosk, detachCameraLabel, detachKioskLabel, deleteLabel,
updateLabel, cameraLabelIds
2026-05-10 01:24:04 +00:00
|
|
|
void this.notify("labels", "update", id);
|
|
|
|
|
}
|
2026-05-23 00:34:03 +00:00
|
|
|
|
2026-05-26 00:38:43 +00:00
|
|
|
// ===========================================================================
|
|
|
|
|
// camera_event_subscriptions
|
|
|
|
|
// ===========================================================================
|
|
|
|
|
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
async listEventSubscriptions(cameraId: string): Promise<CameraEventSubscription[]> {
|
2026-05-26 00:38:43 +00:00
|
|
|
const rs = await this._all(
|
|
|
|
|
"SELECT * FROM camera_event_subscriptions WHERE camera_id = ? ORDER BY topic",
|
|
|
|
|
[cameraId],
|
|
|
|
|
);
|
|
|
|
|
return rs.map((r) => rowToCameraEventSubscription(r as Record<string, unknown>));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async upsertEventSubscription(input: {
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
camera_id: string;
|
2026-05-26 00:38:43 +00:00
|
|
|
topic: string;
|
|
|
|
|
status?: EventSubscriptionStatus;
|
|
|
|
|
}): Promise<void> {
|
|
|
|
|
const status = input.status ?? "inactive";
|
|
|
|
|
await this._run(
|
|
|
|
|
`INSERT INTO camera_event_subscriptions (camera_id, topic, status)
|
|
|
|
|
VALUES (?, ?, ?)
|
|
|
|
|
ON CONFLICT (camera_id, topic) DO UPDATE SET status = COALESCE(NULLIF(?, ''), camera_event_subscriptions.status)`,
|
|
|
|
|
[input.camera_id, input.topic, status, status],
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async updateEventSubscriptionStatus(
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
cameraId: string,
|
2026-05-26 00:38:43 +00:00
|
|
|
topic: string,
|
|
|
|
|
status: EventSubscriptionStatus,
|
|
|
|
|
error?: string | null,
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
await this._run(
|
|
|
|
|
`UPDATE camera_event_subscriptions
|
|
|
|
|
SET status = ?, error_message = ?
|
|
|
|
|
WHERE camera_id = ? AND topic = ?`,
|
|
|
|
|
[status, error ?? null, cameraId, topic],
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
async markEventReceived(cameraId: string, topic: string): Promise<void> {
|
2026-05-26 00:38:43 +00:00
|
|
|
await this._run(
|
|
|
|
|
`UPDATE camera_event_subscriptions
|
|
|
|
|
SET last_event_at = ?, status = 'active'
|
|
|
|
|
WHERE camera_id = ? AND topic = ?`,
|
|
|
|
|
[isoNow(), cameraId, topic],
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async setAllEventSubscriptionsStatus(
|
refactor: migrate all auto-increment PKs to UUIDv7 text IDs
Replace SERIAL/AUTOINCREMENT integer primary keys with UUIDv7 text
IDs across all 15 entity tables (users, api_keys, displays, cameras,
camera_streams, layouts, layout_cells, entities, kiosks, labels,
kiosk_gpio_bindings, event_log, kiosk_logs, audit_log,
camera_event_subscriptions). SetupState keeps id=1 INTEGER singleton.
Changes:
- types.ts: all id fields number->string, all FK fields number->string
- mappers.ts: n(r["id"])->s(r["id"]) for PKs, nn()->sn() for nullable FKs
- repository.ts: import uuidv7, generate IDs before INSERT, remove
RETURNING id, change all method signatures from number to string
- migrations-pg.ts: SERIAL->TEXT NOT NULL PRIMARY KEY, INTEGER FK->TEXT FK
- bundle.ts: all bundle interface IDs number->string
- pairing.ts, auth.ts: kioskId/userId types number->string
- coordinator-registry.ts: kioskId number->string
- audit.ts: actor_id number->string
- mqtt-bridge.ts: kioskId number->string in publish/subscribe
- All route handlers: Number(getRouterParam)->getRouterParam ?? ""
- admin-pages.tsx: template function params and Map types number->string
- kiosk/src/bundle.rs: flexible serde deserializer that accepts both
u32 (old) and String (new) IDs for backward compatibility
Fresh PG database -- no data migration needed, just schema changes.
SQLite migrations unchanged (dev-only, recreate DB for UUIDv7).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-26 05:11:45 +00:00
|
|
|
cameraId: string,
|
2026-05-26 00:38:43 +00:00
|
|
|
fromStatus: EventSubscriptionStatus,
|
|
|
|
|
toStatus: EventSubscriptionStatus,
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
await this._run(
|
|
|
|
|
`UPDATE camera_event_subscriptions
|
|
|
|
|
SET status = ?
|
|
|
|
|
WHERE camera_id = ? AND status = ?`,
|
|
|
|
|
[toStatus, cameraId, fromStatus],
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-23 00:34:03 +00:00
|
|
|
// ===========================================================================
|
|
|
|
|
// cloud_accounts
|
|
|
|
|
// ===========================================================================
|
|
|
|
|
|
|
|
|
|
async listCloudAccounts(): Promise<CloudAccount[]> {
|
|
|
|
|
const rs = await this._all("SELECT * FROM cloud_accounts ORDER BY vendor, name");
|
|
|
|
|
return rs.map((r) => rowToCloudAccount(r as Record<string, unknown>));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async getCloudAccount(id: string): Promise<CloudAccount | null> {
|
|
|
|
|
const r = await this._get("SELECT * FROM cloud_accounts WHERE id = ?", [id]);
|
|
|
|
|
return r ? rowToCloudAccount(r as Record<string, unknown>) : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async createCloudAccount(input: {
|
|
|
|
|
id: string;
|
|
|
|
|
vendor: string;
|
|
|
|
|
name: string;
|
|
|
|
|
credentials_encrypted: string;
|
|
|
|
|
}): Promise<CloudAccount> {
|
|
|
|
|
await this._run(
|
|
|
|
|
`INSERT INTO cloud_accounts (id, vendor, name, credentials_encrypted) VALUES (?, ?, ?, ?)`,
|
|
|
|
|
[input.id, input.vendor, input.name, input.credentials_encrypted],
|
|
|
|
|
);
|
|
|
|
|
const a = await this.getCloudAccount(input.id);
|
|
|
|
|
if (!a) throw new Error("cloud account vanished after insert");
|
|
|
|
|
return a;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async updateCloudAccount(id: string, patch: Partial<CloudAccount>): Promise<void> {
|
|
|
|
|
const sets: string[] = [];
|
|
|
|
|
const vals: unknown[] = [];
|
|
|
|
|
for (const [k, v] of Object.entries(patch)) {
|
|
|
|
|
if (k === "id" || k === "created_at") continue;
|
|
|
|
|
sets.push(`${k} = ?`);
|
|
|
|
|
vals.push(v === undefined ? null : v);
|
|
|
|
|
}
|
|
|
|
|
if (sets.length === 0) return;
|
|
|
|
|
vals.push(id);
|
|
|
|
|
await this._run(`UPDATE cloud_accounts SET ${sets.join(", ")} WHERE id = ?`, vals);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async deleteCloudAccount(id: string): Promise<void> {
|
|
|
|
|
await this._run("DELETE FROM cloud_accounts WHERE id = ?", [id]);
|
|
|
|
|
}
|
2026-05-09 23:09:13 +00:00
|
|
|
}
|