mirror of
https://github.com/BetterCorp/BetterFrame.git
synced 2026-05-26 15:46:35 +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>
This commit is contained in:
parent
69e51197bf
commit
64f47a9a6b
18 changed files with 506 additions and 490 deletions
|
|
@ -1,8 +1,10 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct KioskBundle {
|
||||
pub kiosk_id: u32,
|
||||
#[serde(deserialize_with = "de_flexible_id")]
|
||||
pub kiosk_id: String,
|
||||
pub kiosk_name: String,
|
||||
/// Legacy single-display field (mirrors `displays[0]`). New code should
|
||||
/// iterate `displays` instead.
|
||||
|
|
@ -31,7 +33,7 @@ impl KioskBundle {
|
|||
}
|
||||
if let Some(d) = &self.display {
|
||||
return vec![BundleDisplayWithLayouts {
|
||||
id: d.id,
|
||||
id: d.id.clone(),
|
||||
name: d.name.clone(),
|
||||
width_px: d.width_px,
|
||||
height_px: d.height_px,
|
||||
|
|
@ -90,7 +92,8 @@ pub struct BundleCell {
|
|||
pub row_span: u32,
|
||||
pub col_span: u32,
|
||||
pub content_type: String,
|
||||
pub camera_id: Option<u32>,
|
||||
#[serde(default, deserialize_with = "de_flexible_id_opt")]
|
||||
pub camera_id: Option<String>,
|
||||
pub stream_selector: Option<String>,
|
||||
pub web_url: Option<String>,
|
||||
pub html_content: Option<String>,
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ declare module "h3" {
|
|||
|
||||
function syntheticApiKeyUser(keyPrefix: string): User {
|
||||
return {
|
||||
id: 0,
|
||||
id: "",
|
||||
username: `api:${keyPrefix}`,
|
||||
password_hash: "",
|
||||
role: "admin",
|
||||
|
|
|
|||
|
|
@ -163,9 +163,9 @@ function formValues(v: FormValue): string[] {
|
|||
return v ? [v] : [];
|
||||
}
|
||||
|
||||
function kioskOnvifSoapTransport(kioskId: number) {
|
||||
function kioskOnvifSoapTransport(kioskId: string) {
|
||||
return async (url: string, action: string, body: string, timeoutMs: number): Promise<string> => {
|
||||
if (!Number.isInteger(kioskId) || kioskId <= 0) {
|
||||
if (!kioskId) {
|
||||
throw new Error("invalid kiosk selected for discovery");
|
||||
}
|
||||
const response = await getCoordinator().requestKiosk<{
|
||||
|
|
@ -209,7 +209,7 @@ async function importDiscoveredCamera(
|
|||
username: string,
|
||||
password: string,
|
||||
streams: DiscoverAddStream[],
|
||||
): Promise<number | null> {
|
||||
): Promise<string | null> {
|
||||
if (streams.length === 0) return null;
|
||||
const main = streams.find((s) => s.role === "main") ?? streams[0]!;
|
||||
// Camera row's rtsp_url: full URL with credentials for display / backward compat.
|
||||
|
|
@ -283,7 +283,7 @@ function cellsOverlap(
|
|||
}
|
||||
|
||||
interface CellPos {
|
||||
id: number;
|
||||
id: string;
|
||||
row: number;
|
||||
col: number;
|
||||
row_span: number;
|
||||
|
|
@ -292,12 +292,12 @@ interface CellPos {
|
|||
|
||||
async function resolveOverlaps(
|
||||
deps: AdminDeps,
|
||||
layoutId: number,
|
||||
anchorId: number,
|
||||
layoutId: string,
|
||||
anchorId: string,
|
||||
pushAxis: "row" | "col",
|
||||
): Promise<void> {
|
||||
const all = await deps.repo.layoutCells(layoutId);
|
||||
const positions = new Map<number, CellPos>();
|
||||
const positions = new Map<string, CellPos>();
|
||||
for (const c of all) {
|
||||
positions.set(c.id, { id: c.id, row: c.row, col: c.col, row_span: c.row_span, col_span: c.col_span });
|
||||
}
|
||||
|
|
@ -353,8 +353,8 @@ async function resolveOverlaps(
|
|||
|
||||
async function shiftCellsForExpansion(
|
||||
deps: AdminDeps,
|
||||
layoutId: number,
|
||||
cellId: number,
|
||||
layoutId: string,
|
||||
cellId: string,
|
||||
direction: "left" | "right" | "above" | "bottom",
|
||||
): Promise<void> {
|
||||
const cell = await deps.repo.getLayoutCellById(cellId);
|
||||
|
|
@ -379,7 +379,7 @@ async function shiftCellsForExpansion(
|
|||
|
||||
async function shiftCellsForInsertion(
|
||||
deps: AdminDeps,
|
||||
layoutId: number,
|
||||
layoutId: string,
|
||||
axis: "row" | "col",
|
||||
fromIndex: number,
|
||||
crossStart: number,
|
||||
|
|
@ -541,8 +541,8 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
app.get("/admin/cameras", async (event) => {
|
||||
const user = event.context.user!;
|
||||
const cameras = await deps.repo.listCameras();
|
||||
const streamCounts = new Map<number, number>();
|
||||
const activeKiosks = new Map<number, number>(); // camera_id → count of kiosks rendering
|
||||
const streamCounts = new Map<string, number>();
|
||||
const activeKiosks = new Map<string, number>(); // camera_id → count of kiosks rendering
|
||||
for (const cam of cameras) {
|
||||
streamCounts.set(cam.id, (await deps.repo.listCameraStreams(cam.id)).length);
|
||||
activeKiosks.set(cam.id, (await deps.repo.listKiosksRenderingCamera(cam.id)).length);
|
||||
|
|
@ -643,7 +643,7 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
|
||||
try {
|
||||
const soapTransport = runner.startsWith("kiosk:")
|
||||
? kioskOnvifSoapTransport(Number(runner.slice("kiosk:".length)))
|
||||
? kioskOnvifSoapTransport(runner.slice("kiosk:".length))
|
||||
: undefined;
|
||||
const cameras = await onvifDiscover({ host, port, username, password, soapTransport });
|
||||
return htmlPage(CameraDiscoverResultsPage({
|
||||
|
|
@ -740,11 +740,11 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
errors.push("Select an entity type.");
|
||||
}
|
||||
|
||||
let cameraId: number | null = null;
|
||||
let cameraId: string | null = null;
|
||||
let htmlContent: string | null = null;
|
||||
let webUrl: string | null = null;
|
||||
if (type === "camera") {
|
||||
cameraId = body?.["camera_id"] ? Number(body["camera_id"]) : null;
|
||||
cameraId = body?.["camera_id"] ? String(body["camera_id"] ?? "") : null;
|
||||
if (!cameraId) errors.push("Pick a camera.");
|
||||
} else if (type === "html") {
|
||||
htmlContent = body?.["html_content"] ?? null;
|
||||
|
|
@ -776,7 +776,7 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
|
||||
app.get("/admin/entities/:id", async (event) => {
|
||||
const user = event.context.user!;
|
||||
const id = Number(getRouterParam(event, "id"));
|
||||
const id = (getRouterParam(event, "id") ?? "");
|
||||
const ent = await deps.repo.getEntityById(id);
|
||||
if (!ent) return new Response(null, { status: 302, headers: { location: "/admin/entities" } });
|
||||
return htmlPage(EntityEditPage({
|
||||
|
|
@ -787,14 +787,14 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
});
|
||||
|
||||
app.post("/admin/entities/:id", async (event) => {
|
||||
const id = Number(getRouterParam(event, "id"));
|
||||
const id = (getRouterParam(event, "id") ?? "");
|
||||
const ent = await deps.repo.getEntityById(id);
|
||||
if (!ent) return new Response(null, { status: 302, headers: { location: "/admin/entities" } });
|
||||
const body = await readBody<Record<string, string>>(event);
|
||||
const patch: {
|
||||
name?: string;
|
||||
description?: string | null;
|
||||
camera_id?: number | null;
|
||||
camera_id?: string | null;
|
||||
html_content?: string | null;
|
||||
web_url?: string | null;
|
||||
} = {
|
||||
|
|
@ -802,7 +802,7 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
description: (body?.["description"] ?? "").trim() || null,
|
||||
};
|
||||
if (ent.type === "camera") {
|
||||
patch.camera_id = body?.["camera_id"] ? Number(body["camera_id"]) : null;
|
||||
patch.camera_id = body?.["camera_id"] ? String(body["camera_id"] ?? "") : null;
|
||||
} else if (ent.type === "html") {
|
||||
patch.html_content = body?.["html_content"] ?? null;
|
||||
} else if (ent.type === "web") {
|
||||
|
|
@ -814,7 +814,7 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
});
|
||||
|
||||
app.post("/admin/entities/:id/delete", async (event) => {
|
||||
const id = Number(getRouterParam(event, "id"));
|
||||
const id = (getRouterParam(event, "id") ?? "");
|
||||
await deps.repo.deleteEntity(id);
|
||||
notifyKiosks();
|
||||
return new Response(null, { status: 302, headers: { location: "/admin/entities" } });
|
||||
|
|
@ -825,7 +825,7 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
// no kiosk currently has the camera in its active layout (or every kiosk
|
||||
// attempt times out). Used by the EntityEditPage "Test" preview.
|
||||
app.get("/admin/entities/:id/snapshot", async (event) => {
|
||||
const id = Number(getRouterParam(event, "id"));
|
||||
const id = (getRouterParam(event, "id") ?? "");
|
||||
const ent = await deps.repo.getEntityById(id);
|
||||
if (!ent || ent.type !== "camera" || ent.camera_id == null) {
|
||||
return new Response("Not a camera entity", { status: 404 });
|
||||
|
|
@ -902,7 +902,7 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
const labelsStr = (body?.["initial_labels"] ?? "").trim();
|
||||
const initialLabels = labelsStr ? labelsStr.split(",").map((s) => s.trim()).filter(Boolean) : undefined;
|
||||
const replaceIdRaw = (body?.["replace_kiosk_id"] ?? "").trim();
|
||||
const replaceKioskId = replaceIdRaw && replaceIdRaw !== "0" ? Number(replaceIdRaw) : undefined;
|
||||
const replaceKioskId = replaceIdRaw && replaceIdRaw !== "0" ? replaceIdRaw : undefined;
|
||||
const force = body?.["force"] === "1";
|
||||
|
||||
try {
|
||||
|
|
@ -939,7 +939,7 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
const user = event.context.user!;
|
||||
const layouts = await deps.repo.listLayouts();
|
||||
// For each layout, how many displays use it (for the list view).
|
||||
const displayCounts = new Map<number, number>();
|
||||
const displayCounts = new Map<string, number>();
|
||||
for (const l of layouts) {
|
||||
displayCounts.set(l.id, (await deps.repo.listDisplaysForLayout(l.id)).length);
|
||||
}
|
||||
|
|
@ -986,7 +986,7 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
|
||||
app.get("/admin/layouts/:id", async (event) => {
|
||||
const user = event.context.user!;
|
||||
const id = Number(getRouterParam(event, "id"));
|
||||
const id = (getRouterParam(event, "id") ?? "");
|
||||
const layout = await deps.repo.getLayoutById(id);
|
||||
if (!layout) return new Response(null, { status: 302, headers: { location: "/admin/layouts" } });
|
||||
const cells = await deps.repo.layoutCells(id);
|
||||
|
|
@ -1005,7 +1005,7 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
|
||||
app.post("/admin/layouts/:id", async (event) => {
|
||||
event.context.obs?.log.info("layout update {id} by {user}", { id: getRouterParam(event, "id") ?? "?", user: event.context.user?.username ?? "unknown" });
|
||||
const id = Number(getRouterParam(event, "id"));
|
||||
const id = (getRouterParam(event, "id") ?? "");
|
||||
const body = await readBody<Record<string, string>>(event);
|
||||
const coolingStr = body?.["cooling_timeout_seconds"] ?? "";
|
||||
const coolingTimeout = coolingStr.trim() === "" ? null : parseInt(coolingStr, 10);
|
||||
|
|
@ -1026,7 +1026,7 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
// For htmx requests (hx-request header), returns the grid fragment; otherwise
|
||||
// returns a 302 to the layout edit page.
|
||||
app.post("/admin/layouts/:id/cells", async (event) => {
|
||||
const layoutId = Number(getRouterParam(event, "id"));
|
||||
const layoutId = (getRouterParam(event, "id") ?? "");
|
||||
const body = await readBody<Record<string, string | number | { row: number; col: number }>>(event);
|
||||
|
||||
let row = 0;
|
||||
|
|
@ -1036,7 +1036,7 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
const direction = typeof body?.["direction"] === "string" ? (body["direction"] as string) : "";
|
||||
|
||||
if (afterCellIdRaw && direction) {
|
||||
const afterId = Number(afterCellIdRaw);
|
||||
const afterId = String(afterCellIdRaw);
|
||||
const cells = await deps.repo.layoutCells(layoutId);
|
||||
const ref = cells.find((c) => c.id === afterId);
|
||||
if (!ref) {
|
||||
|
|
@ -1097,8 +1097,8 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
|
||||
// GET a single cell in read mode (used by htmx Cancel button in inline edit).
|
||||
app.get("/admin/layouts/:id/cells/:cellId", async (event) => {
|
||||
const layoutId = Number(getRouterParam(event, "id"));
|
||||
const cellId = Number(getRouterParam(event, "cellId"));
|
||||
const layoutId = (getRouterParam(event, "id") ?? "");
|
||||
const cellId = (getRouterParam(event, "cellId") ?? "");
|
||||
const cell = await deps.repo.getLayoutCellById(cellId);
|
||||
if (!cell || cell.layout_id !== layoutId) {
|
||||
return new Response("Not Found", { status: 404 });
|
||||
|
|
@ -1110,8 +1110,8 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
|
||||
// GET a single cell in edit mode (htmx swap target for cell click).
|
||||
app.get("/admin/layouts/:id/cells/:cellId/edit", async (event) => {
|
||||
const layoutId = Number(getRouterParam(event, "id"));
|
||||
const cellId = Number(getRouterParam(event, "cellId"));
|
||||
const layoutId = (getRouterParam(event, "id") ?? "");
|
||||
const cellId = (getRouterParam(event, "cellId") ?? "");
|
||||
const cell = await deps.repo.getLayoutCellById(cellId);
|
||||
if (!cell || cell.layout_id !== layoutId) {
|
||||
return new Response("Not Found", { status: 404 });
|
||||
|
|
@ -1124,14 +1124,14 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
// Update a cell's entity binding + dimensions. Legacy content_type/web/html
|
||||
// columns are managed by assignCellEntity for bundle compatibility.
|
||||
app.post("/admin/layouts/:id/cells/:cellId", async (event) => {
|
||||
const layoutId = Number(getRouterParam(event, "id"));
|
||||
const cellId = Number(getRouterParam(event, "cellId"));
|
||||
const layoutId = (getRouterParam(event, "id") ?? "");
|
||||
const cellId = (getRouterParam(event, "cellId") ?? "");
|
||||
const body = await readBody<Record<string, string>>(event);
|
||||
|
||||
const entityIdRaw = body?.["entity_id"];
|
||||
const entityId =
|
||||
entityIdRaw && String(entityIdRaw).trim() !== "" ? Number(entityIdRaw) : null;
|
||||
await deps.repo.assignCellEntity(cellId, Number.isFinite(entityId) ? entityId : null);
|
||||
entityIdRaw && String(entityIdRaw).trim() !== "" ? String(entityIdRaw) : null;
|
||||
await deps.repo.assignCellEntity(cellId, entityId != null && entityId !== "" ? entityId : null);
|
||||
|
||||
// stream_selector + spans + fit are still settable on the cell.
|
||||
const dimsPatch: Record<string, unknown> = {};
|
||||
|
|
@ -1221,8 +1221,8 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
|
||||
// Resize a cell by ±1 on row_span or col_span. Returns the grid fragment.
|
||||
app.post("/admin/layouts/:id/cells/:cellId/resize", async (event) => {
|
||||
const layoutId = Number(getRouterParam(event, "id"));
|
||||
const cellId = Number(getRouterParam(event, "cellId"));
|
||||
const layoutId = (getRouterParam(event, "id") ?? "");
|
||||
const cellId = (getRouterParam(event, "cellId") ?? "");
|
||||
const body = await readBody<Record<string, string | number>>(event);
|
||||
const dim = String(body?.["dim"] ?? "");
|
||||
const delta = Number(body?.["delta"] ?? 0) || 0;
|
||||
|
|
@ -1257,8 +1257,8 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
|
||||
// Visual editor: drag-to-move a cell to a new grid position.
|
||||
app.post("/admin/layouts/:id/cells/:cellId/move", async (event) => {
|
||||
const layoutId = Number(getRouterParam(event, "id"));
|
||||
const cellId = Number(getRouterParam(event, "cellId"));
|
||||
const layoutId = (getRouterParam(event, "id") ?? "");
|
||||
const cellId = (getRouterParam(event, "cellId") ?? "");
|
||||
const body = await readBody<{ row: number; col: number }>(event);
|
||||
const row = Number(body?.row ?? 0);
|
||||
const col = Number(body?.col ?? 0);
|
||||
|
|
@ -1270,8 +1270,8 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
});
|
||||
|
||||
app.post("/admin/layouts/:id/cells/:cellId/delete", async (event) => {
|
||||
const layoutId = Number(getRouterParam(event, "id"));
|
||||
const cellId = Number(getRouterParam(event, "cellId"));
|
||||
const layoutId = (getRouterParam(event, "id") ?? "");
|
||||
const cellId = (getRouterParam(event, "cellId") ?? "");
|
||||
await deps.repo.deleteLayoutCell(cellId);
|
||||
notifyKiosks();
|
||||
if (isHtmxRequest(event)) {
|
||||
|
|
@ -1284,7 +1284,7 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
});
|
||||
|
||||
app.post("/admin/layouts/:id/clone", async (event) => {
|
||||
const id = Number(getRouterParam(event, "id"));
|
||||
const id = (getRouterParam(event, "id") ?? "");
|
||||
const clone = await deps.repo.cloneLayout(id);
|
||||
notifyKiosks();
|
||||
return new Response(null, { status: 302, headers: { location: `/admin/layouts/${clone.id}` } });
|
||||
|
|
@ -1292,7 +1292,7 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
|
||||
app.post("/admin/layouts/:id/delete", async (event) => {
|
||||
event.context.obs?.log.info("layout delete {id} by {user}", { id: getRouterParam(event, "id") ?? "?", user: event.context.user?.username ?? "unknown" });
|
||||
const id = Number(getRouterParam(event, "id"));
|
||||
const id = (getRouterParam(event, "id") ?? "");
|
||||
await deps.repo.deleteLayout(id);
|
||||
notifyKiosks();
|
||||
return new Response(null, { status: 302, headers: { location: "/admin/layouts" } });
|
||||
|
|
@ -1308,7 +1308,7 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
|
||||
app.get("/admin/displays/:id", async (event) => {
|
||||
const user = event.context.user!;
|
||||
const id = Number(getRouterParam(event, "id"));
|
||||
const id = (getRouterParam(event, "id") ?? "");
|
||||
const display = await deps.repo.getDisplayById(id);
|
||||
if (!display) return new Response(null, { status: 302, headers: { location: "/admin/displays" } });
|
||||
const attachedLayouts = await deps.repo.listLayoutsForDisplay(id);
|
||||
|
|
@ -1325,13 +1325,13 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
});
|
||||
|
||||
app.post("/admin/displays/:id", async (event) => {
|
||||
const id = Number(getRouterParam(event, "id"));
|
||||
const id = (getRouterParam(event, "id") ?? "");
|
||||
const body = await readBody<Record<string, string>>(event);
|
||||
const defaultLayoutIdRaw = body?.["default_layout_id"];
|
||||
const defaultLayoutId = defaultLayoutIdRaw ? Number(defaultLayoutIdRaw) : null;
|
||||
const defaultLayoutId = defaultLayoutIdRaw ? String(defaultLayoutIdRaw) : null;
|
||||
|
||||
// Validate default_layout_id is actually attached to this display.
|
||||
let validatedDefault: number | null = defaultLayoutId;
|
||||
let validatedDefault: string | null = defaultLayoutId;
|
||||
if (defaultLayoutId != null) {
|
||||
const attached = await deps.repo.listLayoutsForDisplay(id);
|
||||
if (!attached.some((l) => l.id === defaultLayoutId)) {
|
||||
|
|
@ -1353,7 +1353,7 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
});
|
||||
|
||||
// Render the attached + available layouts region for a display.
|
||||
const renderDisplayLayoutsFragment = async (displayId: number): Promise<Response> => {
|
||||
const renderDisplayLayoutsFragment = async (displayId: string): Promise<Response> => {
|
||||
const display = await deps.repo.getDisplayById(displayId);
|
||||
const attached = await deps.repo.listLayoutsForDisplay(displayId);
|
||||
const attachedIds = new Set(attached.map((l) => l.id));
|
||||
|
|
@ -1366,10 +1366,10 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
|
||||
// Attach a layout to a display.
|
||||
app.post("/admin/displays/:id/layouts", async (event) => {
|
||||
const displayId = Number(getRouterParam(event, "id"));
|
||||
const displayId = (getRouterParam(event, "id") ?? "");
|
||||
const body = await readBody<Record<string, string>>(event);
|
||||
const layoutId = body?.["layout_id"] ? Number(body["layout_id"]) : null;
|
||||
if (layoutId && Number.isFinite(layoutId)) {
|
||||
const layoutId = body?.["layout_id"] ? String(body["layout_id"]) : null;
|
||||
if (layoutId && layoutId) {
|
||||
await deps.repo.attachLayoutToDisplay(displayId, layoutId);
|
||||
notifyKiosks();
|
||||
}
|
||||
|
|
@ -1381,8 +1381,8 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
|
||||
// Detach a layout from a display.
|
||||
app.post("/admin/displays/:id/layouts/:layoutId/remove", async (event) => {
|
||||
const displayId = Number(getRouterParam(event, "id"));
|
||||
const layoutId = Number(getRouterParam(event, "layoutId"));
|
||||
const displayId = (getRouterParam(event, "id") ?? "");
|
||||
const layoutId = (getRouterParam(event, "layoutId") ?? "");
|
||||
await deps.repo.detachLayoutFromDisplay(displayId, layoutId);
|
||||
notifyKiosks();
|
||||
if (isHtmxRequest(event)) {
|
||||
|
|
@ -1412,7 +1412,7 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
});
|
||||
|
||||
app.post("/admin/labels/:id/delete", async (event) => {
|
||||
const id = Number(getRouterParam(event, "id"));
|
||||
const id = (getRouterParam(event, "id") ?? "");
|
||||
await deps.repo.deleteLabel(id);
|
||||
return new Response(null, { status: 302, headers: { location: "/admin/labels" } });
|
||||
});
|
||||
|
|
@ -1421,7 +1421,7 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
|
||||
app.get("/admin/cameras/:id", async (event) => {
|
||||
const user = event.context.user!;
|
||||
const id = Number(getRouterParam(event, "id"));
|
||||
const id = (getRouterParam(event, "id") ?? "");
|
||||
const camera = await deps.repo.getCameraById(id);
|
||||
if (!camera) return new Response(null, { status: 302, headers: { location: "/admin/cameras" } });
|
||||
|
||||
|
|
@ -1464,7 +1464,7 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
|
||||
app.post("/admin/cameras/:id", async (event) => {
|
||||
event.context.obs?.log.info("camera update {id} by {user}", { id: getRouterParam(event, "id") ?? "?", user: event.context.user?.username ?? "unknown" });
|
||||
const id = Number(getRouterParam(event, "id"));
|
||||
const id = (getRouterParam(event, "id") ?? "");
|
||||
const cam = await deps.repo.getCameraById(id);
|
||||
if (cam?.type === "cloud") {
|
||||
return new Response(null, { status: 302, headers: { location: `/admin/cameras/${id}` } });
|
||||
|
|
@ -1541,10 +1541,10 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
});
|
||||
|
||||
app.post("/admin/cameras/:id/labels", async (event) => {
|
||||
const camId = Number(getRouterParam(event, "id"));
|
||||
const camId = (getRouterParam(event, "id") ?? "");
|
||||
const body = await readBody<Record<string, string>>(event);
|
||||
const newLabel = (body?.["new_label"] ?? "").trim().toLowerCase();
|
||||
let labelId = body?.["label_id"] ? Number(body["label_id"]) : null;
|
||||
let labelId = body?.["label_id"] ? String(body["label_id"] ?? "") : null;
|
||||
|
||||
if (newLabel) {
|
||||
const label = await deps.repo.ensureLabel(newLabel);
|
||||
|
|
@ -1560,9 +1560,9 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
});
|
||||
|
||||
app.post("/admin/cameras/:id/labels/remove", async (event) => {
|
||||
const camId = Number(getRouterParam(event, "id"));
|
||||
const camId = (getRouterParam(event, "id") ?? "");
|
||||
const body = await readBody<Record<string, string>>(event);
|
||||
const labelId = Number(body?.["label_id"]);
|
||||
const labelId = String(body?.["label_id"] ?? "");
|
||||
await deps.repo.detachCameraLabel(camId, labelId);
|
||||
if (isHtmxRequest(event)) {
|
||||
return htmlFragment(renderCameraLabels(camId, await deps.repo.cameraLabelIds(camId), await deps.repo.listLabels()));
|
||||
|
|
@ -1573,7 +1573,7 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
// Refresh supported ONVIF event topics from the camera.
|
||||
// MERGE: new topics are added to the existing list, never removed.
|
||||
app.post("/admin/cameras/:id/refresh-events", async (event) => {
|
||||
const id = Number(getRouterParam(event, "id"));
|
||||
const id = (getRouterParam(event, "id") ?? "");
|
||||
const cam = await deps.repo.getCameraById(id);
|
||||
if (!cam || cam.type !== "onvif" || !cam.onvif_host) {
|
||||
return new Response(null, { status: 302, headers: { location: `/admin/cameras/${id}` } });
|
||||
|
|
@ -1591,7 +1591,7 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
runner = online ? `kiosk:${online.id}` : "server";
|
||||
}
|
||||
const soapTransport = runner.startsWith("kiosk:")
|
||||
? kioskOnvifSoapTransport(Number(runner.slice("kiosk:".length)))
|
||||
? kioskOnvifSoapTransport(runner.slice("kiosk:".length))
|
||||
: undefined;
|
||||
try {
|
||||
const discoveredTopics = await onvifGetEventProperties({
|
||||
|
|
@ -1618,7 +1618,7 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
|
||||
// Subscribe to all inactive event topics for this camera.
|
||||
app.post("/admin/cameras/:id/subscribe-events", async (event) => {
|
||||
const id = Number(getRouterParam(event, "id"));
|
||||
const id = (getRouterParam(event, "id") ?? "");
|
||||
const cam = await deps.repo.getCameraById(id);
|
||||
if (!cam) {
|
||||
return new Response(null, { status: 302, headers: { location: `/admin/cameras/${id}` } });
|
||||
|
|
@ -1629,7 +1629,7 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
|
||||
app.post("/admin/cameras/:id/delete", async (event) => {
|
||||
event.context.obs?.log.info("camera delete {id} by {user}", { id: getRouterParam(event, "id") ?? "?", user: event.context.user?.username ?? "unknown" });
|
||||
const id = Number(getRouterParam(event, "id"));
|
||||
const id = (getRouterParam(event, "id") ?? "");
|
||||
await deps.repo.deleteCamera(id);
|
||||
notifyKiosks();
|
||||
deps.nodered.forward("camera.changed", { camera_id: id, event: "deleted", source: "server" });
|
||||
|
|
@ -1643,7 +1643,7 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
};
|
||||
const escapeHtml = (s: string) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
||||
app.get("/admin/cameras/:id/events", async (event) => {
|
||||
const id = Number(getRouterParam(event, "id"));
|
||||
const id = (getRouterParam(event, "id") ?? "");
|
||||
const { events } = await deps.repo.queryEvents({
|
||||
camera_id: id,
|
||||
limit: 20,
|
||||
|
|
@ -1672,7 +1672,7 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
|
||||
app.get("/admin/kiosks/:id", async (event) => {
|
||||
const user = event.context.user!;
|
||||
const id = Number(getRouterParam(event, "id"));
|
||||
const id = (getRouterParam(event, "id") ?? "");
|
||||
const kiosk = await deps.repo.getKioskById(id);
|
||||
if (!kiosk) return new Response(null, { status: 302, headers: { location: "/admin/kiosks" } });
|
||||
const kioskLabels = (await deps.repo.listKioskLabels(id)).map((kl) => ({
|
||||
|
|
@ -1709,7 +1709,7 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
|
||||
// ---- GPIO bindings ----------------------------------------------------
|
||||
app.post("/admin/kiosks/:id/gpio", async (event) => {
|
||||
const kioskId = Number(getRouterParam(event, "id"));
|
||||
const kioskId = (getRouterParam(event, "id") ?? "");
|
||||
const body = await readBody<Record<string, string>>(event);
|
||||
const pin = Number(body?.["pin"]);
|
||||
const direction = (body?.["direction"] ?? "in") === "out" ? "out" : "in";
|
||||
|
|
@ -1735,8 +1735,8 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
});
|
||||
|
||||
app.post("/admin/kiosks/:id/gpio/:bindingId/delete", async (event) => {
|
||||
const kioskId = Number(getRouterParam(event, "id"));
|
||||
const bindingId = Number(getRouterParam(event, "bindingId"));
|
||||
const kioskId = (getRouterParam(event, "id") ?? "");
|
||||
const bindingId = (getRouterParam(event, "bindingId") ?? "");
|
||||
await deps.repo.deleteGpioBinding(bindingId);
|
||||
notifyKiosks();
|
||||
if (isHtmxRequest(event)) {
|
||||
|
|
@ -1748,7 +1748,7 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
});
|
||||
|
||||
app.post("/admin/kiosks/:id", async (event) => {
|
||||
const id = Number(getRouterParam(event, "id"));
|
||||
const id = (getRouterParam(event, "id") ?? "");
|
||||
const body = await readBody<Record<string, string>>(event);
|
||||
const kiosk = await deps.repo.getKioskById(id);
|
||||
await deps.repo.updateKiosk(id, {
|
||||
|
|
@ -1776,7 +1776,7 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
// cluster key it received at pairing), then bumps managed_config_version
|
||||
// so the next heartbeat ships it to the kiosk.
|
||||
app.post("/admin/kiosks/:id/managed-config", async (event) => {
|
||||
const id = Number(getRouterParam(event, "id"));
|
||||
const id = (getRouterParam(event, "id") ?? "");
|
||||
const kiosk = await deps.repo.getKioskById(id);
|
||||
if (!kiosk) throw new Error("kiosk not found");
|
||||
if (!kiosk.managed_image) throw new Error("kiosk is not running a managed image");
|
||||
|
|
@ -1845,11 +1845,11 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
});
|
||||
|
||||
app.post("/admin/kiosks/:id/labels", async (event) => {
|
||||
const kioskId = Number(getRouterParam(event, "id"));
|
||||
const kioskId = (getRouterParam(event, "id") ?? "");
|
||||
const body = await readBody<Record<string, string>>(event);
|
||||
const newLabel = (body?.["new_label"] ?? "").trim().toLowerCase();
|
||||
const role = (body?.["role"] ?? "consume") as "consume" | "operate";
|
||||
let labelId = body?.["label_id"] ? Number(body["label_id"]) : null;
|
||||
let labelId = body?.["label_id"] ? String(body["label_id"] ?? "") : null;
|
||||
|
||||
if (newLabel) {
|
||||
const label = await deps.repo.ensureLabel(newLabel);
|
||||
|
|
@ -1870,9 +1870,9 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
});
|
||||
|
||||
app.post("/admin/kiosks/:id/labels/remove", async (event) => {
|
||||
const kioskId = Number(getRouterParam(event, "id"));
|
||||
const kioskId = (getRouterParam(event, "id") ?? "");
|
||||
const body = await readBody<Record<string, string>>(event);
|
||||
const labelId = Number(body?.["label_id"]);
|
||||
const labelId = String(body?.["label_id"] ?? "");
|
||||
await deps.repo.detachKioskLabel(kioskId, labelId);
|
||||
if (isHtmxRequest(event)) {
|
||||
const kioskLabels = (await deps.repo.listKioskLabels(kioskId)).map((kl) => ({
|
||||
|
|
@ -1887,7 +1887,7 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
|
||||
app.post("/admin/kiosks/:id/delete", async (event) => {
|
||||
event.context.obs?.log.info("kiosk delete {id} by {user}", { id: getRouterParam(event, "id") ?? "?", user: event.context.user?.username ?? "unknown" });
|
||||
const id = Number(getRouterParam(event, "id"));
|
||||
const id = (getRouterParam(event, "id") ?? "");
|
||||
await deps.repo.deleteKiosk(id);
|
||||
return new Response(null, { status: 302, headers: { location: "/admin/kiosks" } });
|
||||
});
|
||||
|
|
@ -1897,7 +1897,7 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
// /ws/admin/debug/:kioskId and render output. The WS connection is
|
||||
// authenticated via the admin's API key.
|
||||
app.get("/admin/kiosks/:id/logs", async (event) => {
|
||||
const id = Number(getRouterParam(event, "id"));
|
||||
const id = (getRouterParam(event, "id") ?? "");
|
||||
const kiosk = await deps.repo.getKioskById(id);
|
||||
if (!kiosk) return new Response(null, { status: 302, headers: { location: "/admin/kiosks" } });
|
||||
const user = event.context.user!;
|
||||
|
|
@ -1945,7 +1945,7 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
});
|
||||
|
||||
app.get("/admin/kiosks/:id/terminal", async (event) => {
|
||||
const id = Number(getRouterParam(event, "id"));
|
||||
const id = (getRouterParam(event, "id") ?? "");
|
||||
const kiosk = await deps.repo.getKioskById(id);
|
||||
if (!kiosk) return new Response(null, { status: 302, headers: { location: "/admin/kiosks" } });
|
||||
// WS auth: browser sends session cookie automatically on WS upgrade.
|
||||
|
|
@ -2076,7 +2076,7 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
});
|
||||
|
||||
// ---- Layout switch ----------------------------------------------------
|
||||
const emitLayoutChanged = async (displayId: number | null, kioskId: number | null, layoutId: number) => {
|
||||
const emitLayoutChanged = async (displayId: string | null, kioskId: string | null, layoutId: string) => {
|
||||
const layout = await deps.repo.getLayoutById(layoutId);
|
||||
deps.nodered.forward("layout.changed", {
|
||||
display_id: displayId,
|
||||
|
|
@ -2088,13 +2088,13 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
};
|
||||
|
||||
const displayLayoutSwitch = async (event: any) => {
|
||||
const displayId = Number(getRouterParam(event, "displayId"));
|
||||
let layoutId = Number(getRouterParam(event, "layoutId"));
|
||||
if (!Number.isFinite(layoutId) || layoutId <= 0) {
|
||||
const displayId = (getRouterParam(event, "displayId") ?? "");
|
||||
let layoutId = getRouterParam(event, "layoutId") ?? "";
|
||||
if (!layoutId) {
|
||||
const body = await readBody<Record<string, string>>(event);
|
||||
layoutId = Number(body?.["layout_id"]);
|
||||
layoutId = String(body?.["layout_id"] ?? "");
|
||||
}
|
||||
if (Number.isFinite(displayId) && Number.isFinite(layoutId)) {
|
||||
if (displayId && layoutId) {
|
||||
const display = await deps.repo.getDisplayById(displayId);
|
||||
const attached = await deps.repo.listLayoutsForDisplay(displayId);
|
||||
const isAttached = attached.some((l) => l.id === layoutId);
|
||||
|
|
@ -2114,7 +2114,7 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
app.get("/admin/displays/:displayId/layout/:layoutId", displayLayoutSwitch);
|
||||
|
||||
const displayPower = async (event: any, state: "on" | "standby") => {
|
||||
const id = Number(getRouterParam(event, "id"));
|
||||
const id = (getRouterParam(event, "id") ?? "");
|
||||
const display = await deps.repo.getDisplayById(id);
|
||||
if (display?.kiosk_id) {
|
||||
getCoordinator().sendToKiosk(display.kiosk_id, {
|
||||
|
|
@ -2144,7 +2144,7 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
});
|
||||
|
||||
// ---- CEC power commands -----------------------------------------------
|
||||
const emitDisplayPower = async (kioskId: number, state: "on" | "standby") => {
|
||||
const emitDisplayPower = async (kioskId: string, state: "on" | "standby") => {
|
||||
const displays = await deps.repo.listDisplaysForKiosk(kioskId);
|
||||
const displayId = displays[0]?.id ?? null;
|
||||
const actual = state === "on" ? "awake" : "standby";
|
||||
|
|
@ -2164,7 +2164,7 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
};
|
||||
|
||||
app.post("/admin/kiosks/:id/power/standby", async (event) => {
|
||||
const id = Number(getRouterParam(event, "id"));
|
||||
const id = (getRouterParam(event, "id") ?? "");
|
||||
getCoordinator().sendToKiosk(id, { type: "standby" });
|
||||
await emitDisplayPower(id, "standby");
|
||||
await audit(deps.repo, event as any, "display.standby", { resource_type: "kiosk", resource_id: id });
|
||||
|
|
@ -2172,7 +2172,7 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
});
|
||||
|
||||
app.post("/admin/kiosks/:id/power/wake", async (event) => {
|
||||
const id = Number(getRouterParam(event, "id"));
|
||||
const id = (getRouterParam(event, "id") ?? "");
|
||||
getCoordinator().sendToKiosk(id, { type: "wake" });
|
||||
await emitDisplayPower(id, "on");
|
||||
await audit(deps.repo, event as any, "display.wake", { resource_type: "kiosk", resource_id: id });
|
||||
|
|
@ -2181,7 +2181,7 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
|
||||
// ---- Fan control ------------------------------------------------------
|
||||
app.post("/admin/kiosks/:id/fan", async (event) => {
|
||||
const id = Number(getRouterParam(event, "id"));
|
||||
const id = (getRouterParam(event, "id") ?? "");
|
||||
const body = await readBody<Record<string, string>>(event);
|
||||
const mode = body?.["mode"];
|
||||
if (mode === "auto") {
|
||||
|
|
@ -2216,7 +2216,7 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
});
|
||||
|
||||
app.get("/api/admin/cameras/:id", async (event) => {
|
||||
const id = Number(getRouterParam(event, "id"));
|
||||
const id = (getRouterParam(event, "id") ?? "");
|
||||
const cam = await deps.repo.getCameraById(id);
|
||||
if (!cam) return jsonResponse({ error: "not_found" }, 404);
|
||||
const streams = await deps.repo.listCameraStreams(id);
|
||||
|
|
@ -2231,7 +2231,7 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
});
|
||||
|
||||
app.get("/api/admin/displays/:id", async (event) => {
|
||||
const id = Number(getRouterParam(event, "id"));
|
||||
const id = (getRouterParam(event, "id") ?? "");
|
||||
const display = await deps.repo.getDisplayById(id);
|
||||
if (!display) return jsonResponse({ error: "not_found" }, 404);
|
||||
const attachedLayouts = await deps.repo.listLayoutsForDisplay(id);
|
||||
|
|
@ -2258,7 +2258,7 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
});
|
||||
|
||||
app.get("/api/admin/kiosks/:id", async (event) => {
|
||||
const id = Number(getRouterParam(event, "id"));
|
||||
const id = (getRouterParam(event, "id") ?? "");
|
||||
const kiosk = await deps.repo.getKioskById(id);
|
||||
if (!kiosk) return jsonResponse({ error: "not_found" }, 404);
|
||||
const displays = await deps.repo.listDisplaysForKiosk(id);
|
||||
|
|
@ -2276,7 +2276,7 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
});
|
||||
|
||||
app.get("/api/admin/layouts/:id", async (event) => {
|
||||
const id = Number(getRouterParam(event, "id"));
|
||||
const id = (getRouterParam(event, "id") ?? "");
|
||||
const layout = await deps.repo.getLayoutById(id);
|
||||
if (!layout) return jsonResponse({ error: "not_found" }, 404);
|
||||
const cells = await deps.repo.layoutCells(id);
|
||||
|
|
@ -2290,7 +2290,7 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
});
|
||||
|
||||
app.get("/api/admin/entities/:id", async (event) => {
|
||||
const id = Number(getRouterParam(event, "id"));
|
||||
const id = (getRouterParam(event, "id") ?? "");
|
||||
const entity = await deps.repo.getEntityById(id);
|
||||
if (!entity) return jsonResponse({ error: "not_found" }, 404);
|
||||
return jsonResponse({ entity });
|
||||
|
|
@ -2302,11 +2302,11 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
// across all set ops. Returns the post-mutation entity.
|
||||
|
||||
app.post("/api/admin/displays/:id/default-layout", async (event) => {
|
||||
const id = Number(getRouterParam(event, "id"));
|
||||
const id = (getRouterParam(event, "id") ?? "");
|
||||
const body = (await readBody<Record<string, unknown>>(event)) ?? {};
|
||||
const raw = body["value"] ?? body["default_layout_id"];
|
||||
const layoutId = raw == null || raw === "" ? null : Number(raw);
|
||||
if (raw != null && raw !== "" && !Number.isFinite(layoutId)) {
|
||||
const layoutId = raw == null || raw === "" ? null : String(raw);
|
||||
if (raw != null && raw !== "" && !layoutId) {
|
||||
return jsonResponse({ error: "invalid_value" }, 400);
|
||||
}
|
||||
if (layoutId != null) {
|
||||
|
|
@ -2322,7 +2322,7 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
});
|
||||
|
||||
app.post("/api/admin/kiosks/:id/enabled", async (event) => {
|
||||
const id = Number(getRouterParam(event, "id"));
|
||||
const id = (getRouterParam(event, "id") ?? "");
|
||||
const body = (await readBody<Record<string, unknown>>(event)) ?? {};
|
||||
const enabled = Boolean(body["value"] ?? body["enabled"]);
|
||||
await deps.repo.updateKiosk(id, { enabled } as any);
|
||||
|
|
@ -2332,7 +2332,7 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
});
|
||||
|
||||
app.post("/api/admin/cameras/:id/enabled", async (event) => {
|
||||
const id = Number(getRouterParam(event, "id"));
|
||||
const id = (getRouterParam(event, "id") ?? "");
|
||||
const body = (await readBody<Record<string, unknown>>(event)) ?? {};
|
||||
const enabled = Boolean(body["value"] ?? body["enabled"]);
|
||||
await deps.repo.updateCamera(id, { enabled } as any);
|
||||
|
|
@ -2344,7 +2344,7 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
});
|
||||
|
||||
app.post("/api/admin/layouts/:id/priority", async (event) => {
|
||||
const id = Number(getRouterParam(event, "id"));
|
||||
const id = (getRouterParam(event, "id") ?? "");
|
||||
const body = (await readBody<Record<string, unknown>>(event)) ?? {};
|
||||
const value = String(body["value"] ?? body["priority"] ?? "").toLowerCase();
|
||||
if (value !== "hot" && value !== "normal" && value !== "cold") {
|
||||
|
|
@ -2358,7 +2358,7 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|||
});
|
||||
|
||||
app.post("/api/admin/entities/:id/name", async (event) => {
|
||||
const id = Number(getRouterParam(event, "id"));
|
||||
const id = (getRouterParam(event, "id") ?? "");
|
||||
const body = (await readBody<Record<string, unknown>>(event)) ?? {};
|
||||
const name = String(body["value"] ?? body["name"] ?? "").trim();
|
||||
if (!name || name.length > 128) {
|
||||
|
|
|
|||
|
|
@ -153,7 +153,7 @@ export function registerFirmwareRoutes(app: H3, deps: AdminDeps): void {
|
|||
// ---- Per-kiosk firmware settings ----------------------------------------
|
||||
// POST channel + target_version (used by KioskFirmwarePanel form)
|
||||
app.post("/admin/kiosks/:id/firmware", async (event) => {
|
||||
const id = Number(getRouterParam(event, "id"));
|
||||
const id = (getRouterParam(event, "id") ?? "");
|
||||
const body = await readBody<Record<string, string>>(event);
|
||||
const channelRaw = (body?.["channel"] ?? "stable").trim() as FirmwareChannel;
|
||||
const targetRaw = (body?.["target_version"] ?? "").trim();
|
||||
|
|
@ -176,7 +176,7 @@ export function registerFirmwareRoutes(app: H3, deps: AdminDeps): void {
|
|||
// and pulls /api/kiosk/firmware/check immediately. The actual download
|
||||
// happens kiosk-side over the existing kiosk_key channel.
|
||||
app.post("/admin/kiosks/:id/firmware/push", (event) => {
|
||||
const id = Number(getRouterParam(event, "id"));
|
||||
const id = (getRouterParam(event, "id") ?? "");
|
||||
const dispatched = getCoordinator().sendToKiosk(id, { type: "firmware_check" });
|
||||
return { ok: true, dispatched };
|
||||
});
|
||||
|
|
@ -204,10 +204,10 @@ export function registerFirmwareRoutes(app: H3, deps: AdminDeps): void {
|
|||
if (!release) throw createError({ statusCode: 404, statusMessage: "release not found" });
|
||||
const percentage = clamp(Number(body?.["percentage"] ?? 100), 1, 100);
|
||||
const targetsRaw = body?.["target_kiosk_ids"];
|
||||
const targets: number[] = Array.isArray(targetsRaw)
|
||||
? targetsRaw.map((s) => Number(s)).filter((n) => Number.isFinite(n))
|
||||
const targets: string[] = Array.isArray(targetsRaw)
|
||||
? targetsRaw.map((s) => String(s)).filter((s) => s !== "")
|
||||
: typeof targetsRaw === "string" && targetsRaw
|
||||
? targetsRaw.split(",").map((s) => Number(s.trim())).filter((n) => Number.isFinite(n))
|
||||
? targetsRaw.split(",").map((s) => s.trim()).filter((s) => s !== "")
|
||||
: [];
|
||||
const user = event.context.user!;
|
||||
const rollout = await deps.repo.createFirmwareRollout({
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ export function registerOsUpdateRoutes(app: H3, deps: AdminDeps): void {
|
|||
|
||||
// ---- Per-kiosk OS-update settings ---------------------------------------
|
||||
app.post("/admin/kiosks/:id/os-update", async (event) => {
|
||||
const id = Number(getRouterParam(event, "id"));
|
||||
const id = (getRouterParam(event, "id") ?? "");
|
||||
const body = await readBody<Record<string, string>>(event);
|
||||
const channelRaw = (body?.["channel"] ?? "stable").trim() as FirmwareChannel;
|
||||
const targetRaw = (body?.["target_version"] ?? "").trim();
|
||||
|
|
@ -65,7 +65,7 @@ export function registerOsUpdateRoutes(app: H3, deps: AdminDeps): void {
|
|||
|
||||
// Push OS update now: server pings the kiosk via WS coordinator.
|
||||
app.post("/admin/kiosks/:id/os-update/push", async (event) => {
|
||||
const id = Number(getRouterParam(event, "id"));
|
||||
const id = (getRouterParam(event, "id") ?? "");
|
||||
const { getCoordinator } = await import("../../shared/coordinator-registry.js");
|
||||
const dispatched = getCoordinator().sendToKiosk(id, { type: "os_check" });
|
||||
return { ok: true, dispatched };
|
||||
|
|
@ -93,10 +93,10 @@ export function registerOsUpdateRoutes(app: H3, deps: AdminDeps): void {
|
|||
if (!release) throw createError({ statusCode: 404, statusMessage: "release not found" });
|
||||
const percentage = clamp(Number(body?.["percentage"] ?? 100), 1, 100);
|
||||
const targetsRaw = body?.["target_kiosk_ids"];
|
||||
const targets: number[] = Array.isArray(targetsRaw)
|
||||
? targetsRaw.map((s) => Number(s)).filter((n) => Number.isFinite(n))
|
||||
const targets: string[] = Array.isArray(targetsRaw)
|
||||
? targetsRaw.map((s) => String(s)).filter((s) => s !== "")
|
||||
: typeof targetsRaw === "string" && targetsRaw
|
||||
? targetsRaw.split(",").map((s) => Number(s.trim())).filter((n) => Number.isFinite(n))
|
||||
? targetsRaw.split(",").map((s) => s.trim()).filter((s) => s !== "")
|
||||
: [];
|
||||
const user = event.context.user!;
|
||||
const rollout = await deps.repo.createOsUpdateRollout({
|
||||
|
|
|
|||
|
|
@ -557,7 +557,7 @@ function registerKioskRoutes(
|
|||
// Sync displays reported by the kiosk
|
||||
if (Array.isArray(body?.displays)) {
|
||||
const existing = await repo.listDisplaysForKiosk(kiosk.id);
|
||||
const seenDisplayIds = new Set<number>();
|
||||
const seenDisplayIds = new Set<string>();
|
||||
for (const [position, reported] of body.displays.entries()) {
|
||||
const reportedIndex = Number.isInteger(reported.index) && reported.index! >= 0
|
||||
? reported.index!
|
||||
|
|
@ -659,7 +659,7 @@ function registerKioskRoutes(
|
|||
const body = await readBody<{
|
||||
topic: string;
|
||||
source_type?: string;
|
||||
camera_id?: number;
|
||||
camera_id?: string;
|
||||
property_op?: string;
|
||||
payload?: Record<string, unknown>;
|
||||
}>(event);
|
||||
|
|
@ -699,9 +699,9 @@ function registerKioskRoutes(
|
|||
// Side-effect: persist active layout per display so the admin UI can
|
||||
// surface "currently showing X" without having to query event_log.
|
||||
if (body.topic === "layout.changed") {
|
||||
const displayId = Number(body.payload?.["display_id"]);
|
||||
const layoutId = Number(body.payload?.["layout_id"]);
|
||||
if (Number.isInteger(displayId) && Number.isInteger(layoutId)) {
|
||||
const displayId = String(body.payload?.["display_id"] ?? "");
|
||||
const layoutId = String(body.payload?.["layout_id"] ?? "");
|
||||
if (displayId && layoutId) {
|
||||
try {
|
||||
await repo.updateDisplay(displayId, { active_layout_id: layoutId } as any);
|
||||
} catch {
|
||||
|
|
@ -1080,7 +1080,7 @@ function registerKioskRoutes(
|
|||
const kiosk = await auth.verifyKioskKey(token);
|
||||
if (!kiosk) throw createError({ statusCode: 401, statusMessage: "Invalid kiosk key" });
|
||||
|
||||
const cameraId = Number(getRouterParam(event, "id"));
|
||||
const cameraId = (getRouterParam(event, "id") ?? "");
|
||||
const camera = await repo.getCameraById(cameraId);
|
||||
if (!camera || camera.type !== "cloud" || !camera.cloud_account_id || !camera.cloud_vendor_camera_id) {
|
||||
throw createError({ statusCode: 404, statusMessage: "Cloud camera not found" });
|
||||
|
|
@ -1117,11 +1117,11 @@ function registerKioskRoutes(
|
|||
* targets the same half of the fleet across re-checks. Switch from 50%→100%
|
||||
* gracefully adds the previously-excluded half rather than reshuffling.
|
||||
*/
|
||||
function isKioskInRolloutBucket(kioskId: number, rolloutId: string, percentage: number): boolean {
|
||||
function isKioskInRolloutBucket(kioskId: string, rolloutId: string, percentage: number): boolean {
|
||||
if (percentage >= 100) return true;
|
||||
if (percentage <= 0) return false;
|
||||
const h = createHash("sha256")
|
||||
.update(`${rolloutId}:${String(kioskId)}`)
|
||||
.update(`${rolloutId}:${kioskId}`)
|
||||
.digest();
|
||||
const bucket = h.readUInt32BE(0) % 100;
|
||||
return bucket < percentage;
|
||||
|
|
|
|||
|
|
@ -86,14 +86,14 @@ export const EventSchemas = createEventSchemas({
|
|||
// ---- Connected kiosks -------------------------------------------------------
|
||||
|
||||
interface ConnectedKiosk {
|
||||
id: number;
|
||||
id: string;
|
||||
name: string;
|
||||
ws: WebSocket;
|
||||
}
|
||||
|
||||
const connectedKiosks = new Map<number, ConnectedKiosk>();
|
||||
const connectedKiosks = new Map<string, ConnectedKiosk>();
|
||||
const pendingRequests = new Map<string, {
|
||||
kioskId: number;
|
||||
kioskId: string;
|
||||
resolve: (value: unknown) => void;
|
||||
reject: (err: Error) => void;
|
||||
timer: ReturnType<typeof setTimeout>;
|
||||
|
|
@ -101,9 +101,9 @@ const pendingRequests = new Map<string, {
|
|||
|
||||
// Admin debug subscribers: admin WS connections subscribed to a kiosk's
|
||||
// journal/terminal output. Keyed by kiosk id → set of admin WebSockets.
|
||||
const debugSubscribers = new Map<number, Set<WebSocket>>();
|
||||
const debugSubscribers = new Map<string, Set<WebSocket>>();
|
||||
|
||||
function addDebugSubscriber(kioskId: number, adminWs: WebSocket): void {
|
||||
function addDebugSubscriber(kioskId: string, adminWs: WebSocket): void {
|
||||
let subs = debugSubscribers.get(kioskId);
|
||||
if (!subs) { subs = new Set(); debugSubscribers.set(kioskId, subs); }
|
||||
subs.add(adminWs);
|
||||
|
|
@ -117,7 +117,7 @@ function addDebugSubscriber(kioskId: number, adminWs: WebSocket): void {
|
|||
});
|
||||
}
|
||||
|
||||
function relayToDebugSubscribers(kioskId: number, message: string): void {
|
||||
function relayToDebugSubscribers(kioskId: string, message: string): void {
|
||||
const subs = debugSubscribers.get(kioskId);
|
||||
if (!subs) return;
|
||||
for (const ws of subs) {
|
||||
|
|
@ -136,9 +136,9 @@ function parseCookieValue(header: string, name: string): string | null {
|
|||
// Per-kiosk message queue: if kiosk is offline, buffer messages here.
|
||||
// Drain on reconnect. FIFO, cap at 100 messages per kiosk.
|
||||
const MESSAGE_QUEUE_CAP = 100;
|
||||
const offlineQueues = new Map<number, string[]>();
|
||||
const offlineQueues = new Map<string, string[]>();
|
||||
|
||||
function sendToKiosk(kioskId: number, message: object): boolean {
|
||||
function sendToKiosk(kioskId: string, message: object): boolean {
|
||||
const k = connectedKiosks.get(kioskId);
|
||||
const payload = JSON.stringify(message);
|
||||
if (!k || k.ws.readyState !== WebSocket.OPEN) {
|
||||
|
|
@ -153,7 +153,7 @@ function sendToKiosk(kioskId: number, message: object): boolean {
|
|||
return true;
|
||||
}
|
||||
|
||||
function drainOfflineQueue(kioskId: number): void {
|
||||
function drainOfflineQueue(kioskId: string): void {
|
||||
const q = offlineQueues.get(kioskId);
|
||||
if (!q || q.length === 0) return;
|
||||
const k = connectedKiosks.get(kioskId);
|
||||
|
|
@ -164,7 +164,7 @@ function drainOfflineQueue(kioskId: number): void {
|
|||
offlineQueues.delete(kioskId);
|
||||
}
|
||||
|
||||
function requestKiosk<T = unknown>(kioskId: number, message: object, timeoutMs = 10000): Promise<T> {
|
||||
function requestKiosk<T = unknown>(kioskId: string, message: object, timeoutMs = 10000): Promise<T> {
|
||||
const requestId = randomUUID();
|
||||
return new Promise<T>((resolve, reject) => {
|
||||
const timer = setTimeout(() => {
|
||||
|
|
@ -271,8 +271,8 @@ export class Plugin extends BSBService<InstanceType<typeof Config>, typeof Event
|
|||
// Subscribes to a kiosk's journal + terminal output stream.
|
||||
if (url.pathname.startsWith("/ws/admin/debug/")) {
|
||||
const kioskIdStr = url.pathname.split("/").pop() ?? "";
|
||||
const kioskId = Number(kioskIdStr);
|
||||
if (!Number.isInteger(kioskId) || kioskId <= 0) {
|
||||
const kioskId = String(kioskIdStr);
|
||||
if (!Number.isInteger(kioskId) || kioskId === "") {
|
||||
socket.write("HTTP/1.1 400 Bad Request\r\n\r\n");
|
||||
socket.destroy();
|
||||
return;
|
||||
|
|
@ -454,7 +454,7 @@ export class Plugin extends BSBService<InstanceType<typeof Config>, typeof Event
|
|||
requestKiosk,
|
||||
broadcastAll,
|
||||
notifyBundleChanged: () => broadcastAll({ type: "reload-bundle" }),
|
||||
notifyKioskBundleChanged: (kioskId: number) =>
|
||||
notifyKioskBundleChanged: (kioskId: string) =>
|
||||
sendToKiosk(kioskId, { type: "reload-bundle" }),
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import type { AuditActorType, AuditResult } from "./types.js";
|
|||
|
||||
interface AuditCtx {
|
||||
context?: {
|
||||
user?: { id?: number; username?: string };
|
||||
user?: { id?: string; username?: string };
|
||||
apiKeyPrefix?: string;
|
||||
session?: unknown;
|
||||
};
|
||||
|
|
@ -26,7 +26,7 @@ export interface AuditInput {
|
|||
result?: AuditResult;
|
||||
/** Override actor (e.g. when system performs action on behalf of nobody). */
|
||||
actor_type?: AuditActorType;
|
||||
actor_id?: number | null;
|
||||
actor_id?: string | null;
|
||||
actor_label?: string | null;
|
||||
}
|
||||
|
||||
|
|
@ -39,7 +39,7 @@ export async function audit(
|
|||
try {
|
||||
const ctx = event?.context;
|
||||
let actor_type: AuditActorType = input.actor_type ?? "system";
|
||||
let actor_id: number | null = input.actor_id ?? null;
|
||||
let actor_id: string | null = input.actor_id ?? null;
|
||||
let actor_label: string | null = input.actor_label ?? null;
|
||||
|
||||
if (!input.actor_type && ctx) {
|
||||
|
|
|
|||
|
|
@ -52,7 +52,7 @@ export interface AuthApi {
|
|||
expiresAt: string | null;
|
||||
}): Promise<{ apiKey: ApiKey; plaintext: string }>;
|
||||
verifyApiKey(plaintext: string, ip: string | null): Promise<ApiKey | null>;
|
||||
verifyKioskKey(plaintext: string): Promise<{ id: number } | null>;
|
||||
verifyKioskKey(plaintext: string): Promise<{ id: string } | null>;
|
||||
}
|
||||
|
||||
// ---- Constants --------------------------------------------------------------
|
||||
|
|
@ -274,7 +274,7 @@ export function createAuth(
|
|||
return null;
|
||||
}
|
||||
|
||||
async function verifyKioskKey(plaintext: string): Promise<{ id: number } | null> {
|
||||
async function verifyKioskKey(plaintext: string): Promise<{ id: string } | null> {
|
||||
if (plaintext.length < 8) return null;
|
||||
const prefix = plaintext.slice(0, 8);
|
||||
const candidates = await repo.listKiosksByKeyPrefix(prefix);
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ function buildStreamRtspUri(stream: CameraStream, cam: Camera): string {
|
|||
}
|
||||
|
||||
export interface BundleCamera {
|
||||
id: number;
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
rtsp_url: string | null;
|
||||
|
|
@ -52,7 +52,7 @@ export interface BundleCamera {
|
|||
event_sink: string;
|
||||
stream_policy: string;
|
||||
streams: Array<{
|
||||
id: number;
|
||||
id: string;
|
||||
role: string;
|
||||
name: string;
|
||||
/** Final playable RTSP URL with properly encoded credentials. */
|
||||
|
|
@ -70,7 +70,7 @@ export interface BundleCell {
|
|||
row_span: number;
|
||||
col_span: number;
|
||||
content_type: string;
|
||||
camera_id: number | null;
|
||||
camera_id: string | null;
|
||||
stream_selector: string | null;
|
||||
web_url: string | null;
|
||||
html_content: string | null;
|
||||
|
|
@ -94,7 +94,7 @@ export interface BundleCell {
|
|||
}
|
||||
|
||||
export interface BundleLayout {
|
||||
id: number;
|
||||
id: string;
|
||||
name: string;
|
||||
/** Computed from cells: max(col + col_span). 1 if no cells. */
|
||||
grid_cols: number;
|
||||
|
|
@ -102,7 +102,7 @@ export interface BundleLayout {
|
|||
grid_rows: number;
|
||||
priority: string;
|
||||
cooling_timeout_seconds: number | null;
|
||||
preload_camera_ids: number[];
|
||||
preload_camera_ids: string[];
|
||||
resets_idle_timer: boolean;
|
||||
/** True if the kiosk's display has this layout as its default_layout_id. */
|
||||
is_default: boolean;
|
||||
|
|
@ -110,13 +110,13 @@ export interface BundleLayout {
|
|||
}
|
||||
|
||||
export interface BundleDisplay {
|
||||
id: number;
|
||||
id: string;
|
||||
name: string;
|
||||
width_px: number;
|
||||
height_px: number;
|
||||
idle_timeout_seconds: number;
|
||||
sleep_timeout_seconds: number;
|
||||
default_layout_id: number | null;
|
||||
default_layout_id: string | null;
|
||||
}
|
||||
|
||||
export interface BundleDisplayWithLayouts extends BundleDisplay {
|
||||
|
|
@ -124,7 +124,7 @@ export interface BundleDisplayWithLayouts extends BundleDisplay {
|
|||
}
|
||||
|
||||
export interface BundleGpioBinding {
|
||||
id: number;
|
||||
id: string;
|
||||
chip: string;
|
||||
pin: number;
|
||||
direction: "in" | "out";
|
||||
|
|
@ -134,7 +134,7 @@ export interface BundleGpioBinding {
|
|||
}
|
||||
|
||||
export interface KioskBundle {
|
||||
kiosk_id: number;
|
||||
kiosk_id: string;
|
||||
kiosk_name: string;
|
||||
/**
|
||||
* @deprecated Use `displays` (array). Kept for backward compat with older
|
||||
|
|
@ -156,7 +156,7 @@ export interface KioskBundle {
|
|||
export async function generateBundle(
|
||||
repo: Repository,
|
||||
secrets: SecretsApi,
|
||||
kioskId: number,
|
||||
kioskId: string,
|
||||
clusterKey: string | undefined,
|
||||
obs?: Observable,
|
||||
): Promise<KioskBundle | null> {
|
||||
|
|
@ -194,13 +194,13 @@ export async function generateBundle(
|
|||
}
|
||||
|
||||
// Collect camera IDs across ALL displays' layouts (de-duped).
|
||||
const allLayoutIds = new Set<number>();
|
||||
const allLayoutIds = new Set<string>();
|
||||
for (const d of displays) {
|
||||
for (const l of await repo.layoutsForDisplayId(d.id)) allLayoutIds.add(l.id);
|
||||
}
|
||||
const cameras = await repo.camerasForLayoutIds([...allLayoutIds]);
|
||||
|
||||
async function buildLayouts(displayId: number, defaultLayoutId: number | null): Promise<BundleLayout[]> {
|
||||
async function buildLayouts(displayId: string, defaultLayoutId: string | null): Promise<BundleLayout[]> {
|
||||
const layouts = await repo.layoutsForDisplayId(displayId);
|
||||
const result: BundleLayout[] = [];
|
||||
for (const l of layouts) {
|
||||
|
|
@ -308,7 +308,7 @@ export async function generateBundle(
|
|||
const effectiveStreams = streams.length > 0 ? streams : (
|
||||
cam.type === "rtsp" && cam.rtsp_url
|
||||
? [{
|
||||
id: 0,
|
||||
id: "",
|
||||
role: "main" as const,
|
||||
name: "Main",
|
||||
rtsp_uri: cam.rtsp_url,
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@
|
|||
* service-coordinator-ws sets the implementation in its init().
|
||||
*/
|
||||
export interface CoordinatorApi {
|
||||
sendToKiosk(kioskId: number, message: object): boolean;
|
||||
requestKiosk<T = unknown>(kioskId: number, message: object, timeoutMs?: number): Promise<T>;
|
||||
sendToKiosk(kioskId: string, message: object): boolean;
|
||||
requestKiosk<T = unknown>(kioskId: string, message: object, timeoutMs?: number): Promise<T>;
|
||||
broadcastAll(message: object): void;
|
||||
notifyBundleChanged(): void;
|
||||
notifyKioskBundleChanged(kioskId: number): void;
|
||||
notifyKioskBundleChanged(kioskId: string): void;
|
||||
}
|
||||
|
||||
const noop: CoordinatorApi = {
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ const nn = (v: unknown): number | null =>
|
|||
|
||||
export function rowToUser(r: Row): User {
|
||||
return {
|
||||
id: n(r["id"]),
|
||||
id: s(r["id"]),
|
||||
username: s(r["username"]),
|
||||
password_hash: s(r["password_hash"]),
|
||||
role: s(r["role"]) as UserRole,
|
||||
|
|
@ -88,7 +88,7 @@ export function rowToUser(r: Row): User {
|
|||
export function rowToSession(r: Row): Session {
|
||||
return {
|
||||
id: s(r["id"]),
|
||||
user_id: n(r["user_id"]),
|
||||
user_id: s(r["user_id"]),
|
||||
csrf_token: s(r["csrf_token"]),
|
||||
totp_pending: b(r["totp_pending"]),
|
||||
user_agent: sn(r["user_agent"]),
|
||||
|
|
@ -102,7 +102,7 @@ export function rowToSession(r: Row): Session {
|
|||
|
||||
export function rowToApiKey(r: Row): ApiKey {
|
||||
return {
|
||||
id: n(r["id"]),
|
||||
id: s(r["id"]),
|
||||
name: s(r["name"]),
|
||||
key_hash: s(r["key_hash"]),
|
||||
key_prefix: s(r["key_prefix"]),
|
||||
|
|
@ -128,14 +128,14 @@ export function rowToSetupState(r: Row): SetupState {
|
|||
|
||||
export function rowToDisplay(r: Row): Display {
|
||||
return {
|
||||
id: n(r["id"]),
|
||||
id: s(r["id"]),
|
||||
name: s(r["name"]),
|
||||
index: n(r["index"]),
|
||||
is_primary: b(r["is_primary"]),
|
||||
kiosk_id: nn(r["kiosk_id"]),
|
||||
kiosk_id: sn(r["kiosk_id"]),
|
||||
width_px: n(r["width_px"]),
|
||||
height_px: n(r["height_px"]),
|
||||
default_layout_id: nn(r["default_layout_id"]),
|
||||
default_layout_id: sn(r["default_layout_id"]),
|
||||
idle_timeout_seconds: n(r["idle_timeout_seconds"]),
|
||||
sleep_timeout_seconds: n(r["sleep_timeout_seconds"]),
|
||||
cec_enabled: b(r["cec_enabled"]),
|
||||
|
|
@ -147,13 +147,13 @@ export function rowToDisplay(r: Row): Display {
|
|||
state_check_enabled: b(r["state_check_enabled"]),
|
||||
state_check_interval_seconds: n(r["state_check_interval_seconds"]),
|
||||
is_enabled: b(r["is_enabled"]),
|
||||
active_layout_id: nn(r["active_layout_id"]),
|
||||
active_layout_id: sn(r["active_layout_id"]),
|
||||
};
|
||||
}
|
||||
|
||||
export function rowToCamera(r: Row): Camera {
|
||||
return {
|
||||
id: n(r["id"]),
|
||||
id: s(r["id"]),
|
||||
name: s(r["name"]),
|
||||
type: s(r["type"]) as CameraType,
|
||||
rtsp_url: sn(r["rtsp_url"]),
|
||||
|
|
@ -178,8 +178,8 @@ export function rowToCamera(r: Row): Camera {
|
|||
|
||||
export function rowToCameraStream(r: Row): CameraStream {
|
||||
return {
|
||||
id: n(r["id"]),
|
||||
camera_id: n(r["camera_id"]),
|
||||
id: s(r["id"]),
|
||||
camera_id: s(r["camera_id"]),
|
||||
role: s(r["role"]) as StreamRole,
|
||||
name: s(r["name"]),
|
||||
profile_token: sn(r["profile_token"]),
|
||||
|
|
@ -198,7 +198,7 @@ export function rowToCameraStream(r: Row): CameraStream {
|
|||
|
||||
export function rowToLayoutTemplate(r: Row): LayoutTemplate {
|
||||
return {
|
||||
id: n(r["id"]),
|
||||
id: s(r["id"]),
|
||||
name: s(r["name"]),
|
||||
description: sn(r["description"]),
|
||||
regions: j<LayoutRegion[]>(r["regions"], []),
|
||||
|
|
@ -210,17 +210,17 @@ export function rowToLayoutTemplate(r: Row): LayoutTemplate {
|
|||
|
||||
export function rowToLayout(r: Row): Layout {
|
||||
return {
|
||||
id: n(r["id"]),
|
||||
id: s(r["id"]),
|
||||
name: s(r["name"]),
|
||||
description: sn(r["description"]),
|
||||
template_id: nn(r["template_id"]),
|
||||
template_id: sn(r["template_id"]),
|
||||
regions: j<LayoutRegion[]>(r["regions"], []),
|
||||
grid_cols: n(r["grid_cols"]) || 1,
|
||||
grid_rows: n(r["grid_rows"]) || 1,
|
||||
display_id: nn(r["display_id"]),
|
||||
display_id: sn(r["display_id"]),
|
||||
priority: s(r["priority"]) as LayoutPriority,
|
||||
cooling_timeout_seconds: nn(r["cooling_timeout_seconds"]),
|
||||
preload_camera_ids: j<number[]>(r["preload_camera_ids"], []),
|
||||
preload_camera_ids: j<string[]>(r["preload_camera_ids"], []),
|
||||
is_default: b(r["is_default"]),
|
||||
resets_idle_timer: b(r["resets_idle_timer"]),
|
||||
};
|
||||
|
|
@ -228,32 +228,32 @@ export function rowToLayout(r: Row): Layout {
|
|||
|
||||
export function rowToLayoutCell(r: Row): LayoutCell {
|
||||
return {
|
||||
id: n(r["id"]),
|
||||
layout_id: n(r["layout_id"]),
|
||||
id: s(r["id"]),
|
||||
layout_id: s(r["layout_id"]),
|
||||
region_name: s(r["region_name"]),
|
||||
row: n(r["row"]),
|
||||
col: n(r["col"]),
|
||||
row_span: n(r["row_span"]) || 1,
|
||||
col_span: n(r["col_span"]) || 1,
|
||||
content_type: s(r["content_type"]) as CellContentType,
|
||||
camera_id: nn(r["camera_id"]),
|
||||
camera_id: sn(r["camera_id"]),
|
||||
stream_selector: s(r["stream_selector"]) as StreamSelector,
|
||||
web_url: sn(r["web_url"]),
|
||||
html_content: sn(r["html_content"]),
|
||||
cooling_timeout_seconds: nn(r["cooling_timeout_seconds"]),
|
||||
options: j<Record<string, unknown>>(r["options"], {}),
|
||||
entity_id: nn(r["entity_id"]),
|
||||
entity_id: sn(r["entity_id"]),
|
||||
fit: (s(r["fit"]) || "cover") as "cover" | "contain" | "fill",
|
||||
};
|
||||
}
|
||||
|
||||
export function rowToEntity(r: Row): Entity {
|
||||
return {
|
||||
id: n(r["id"]),
|
||||
id: s(r["id"]),
|
||||
name: s(r["name"]),
|
||||
type: s(r["type"]) as EntityType,
|
||||
description: sn(r["description"]),
|
||||
camera_id: nn(r["camera_id"]),
|
||||
camera_id: sn(r["camera_id"]),
|
||||
html_content: sn(r["html_content"]),
|
||||
web_url: sn(r["web_url"]),
|
||||
dashboard_id: sn(r["dashboard_id"]),
|
||||
|
|
@ -263,7 +263,7 @@ export function rowToEntity(r: Row): Entity {
|
|||
|
||||
export function rowToKiosk(r: Row): Kiosk {
|
||||
return {
|
||||
id: n(r["id"]),
|
||||
id: s(r["id"]),
|
||||
name: s(r["name"]),
|
||||
description: sn(r["description"]),
|
||||
key_hash: s(r["key_hash"]),
|
||||
|
|
@ -276,7 +276,7 @@ export function rowToKiosk(r: Row): Kiosk {
|
|||
paired_at: sn(r["paired_at"]),
|
||||
last_seen_at: sn(r["last_seen_at"]),
|
||||
last_bundle_version: sn(r["last_bundle_version"]),
|
||||
display_id: nn(r["display_id"]),
|
||||
display_id: sn(r["display_id"]),
|
||||
cpu_temp_c: nn(r["cpu_temp_c"]),
|
||||
cpu_load_percent: nn(r["cpu_load_percent"]),
|
||||
fan_rpm: nn(r["fan_rpm"]),
|
||||
|
|
@ -314,10 +314,10 @@ export function rowToKiosk(r: Row): Kiosk {
|
|||
|
||||
export function rowToAuditEntry(r: Row): AuditEntry {
|
||||
return {
|
||||
id: n(r["id"]),
|
||||
id: s(r["id"]),
|
||||
ts: s(r["ts"]),
|
||||
actor_type: s(r["actor_type"]) as AuditActorType,
|
||||
actor_id: nn(r["actor_id"]),
|
||||
actor_id: sn(r["actor_id"]),
|
||||
actor_label: sn(r["actor_label"]),
|
||||
action: s(r["action"]),
|
||||
resource_type: sn(r["resource_type"]),
|
||||
|
|
@ -340,7 +340,7 @@ export function rowToFirmwareRelease(r: Row): FirmwareRelease {
|
|||
signature: s(r["signature"]),
|
||||
release_notes: sn(r["release_notes"]),
|
||||
uploaded_at: s(r["uploaded_at"]),
|
||||
uploaded_by: nn(r["uploaded_by"]),
|
||||
uploaded_by: sn(r["uploaded_by"]),
|
||||
yanked_at: sn(r["yanked_at"]),
|
||||
};
|
||||
}
|
||||
|
|
@ -349,13 +349,13 @@ export function rowToFirmwareRollout(r: Row): FirmwareRollout {
|
|||
return {
|
||||
id: s(r["id"]),
|
||||
release_id: s(r["release_id"]),
|
||||
target_kiosk_ids: j<number[]>(r["target_kiosk_ids"], []),
|
||||
target_kiosk_ids: j<string[]>(r["target_kiosk_ids"], []),
|
||||
state: s(r["state"]) as FirmwareRolloutState,
|
||||
percentage: n(r["percentage"]),
|
||||
started_at: sn(r["started_at"]),
|
||||
finished_at: sn(r["finished_at"]),
|
||||
created_at: s(r["created_at"]),
|
||||
created_by: nn(r["created_by"]),
|
||||
created_by: sn(r["created_by"]),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -371,7 +371,7 @@ export function rowToOsUpdateRelease(r: Row): OsUpdateRelease {
|
|||
bundle_format: "raucb",
|
||||
release_notes: sn(r["release_notes"]),
|
||||
uploaded_at: s(r["uploaded_at"]),
|
||||
uploaded_by: nn(r["uploaded_by"]),
|
||||
uploaded_by: sn(r["uploaded_by"]),
|
||||
yanked_at: sn(r["yanked_at"]),
|
||||
};
|
||||
}
|
||||
|
|
@ -380,19 +380,19 @@ export function rowToOsUpdateRollout(r: Row): OsUpdateRollout {
|
|||
return {
|
||||
id: s(r["id"]),
|
||||
release_id: s(r["release_id"]),
|
||||
target_kiosk_ids: j<number[]>(r["target_kiosk_ids"], []),
|
||||
target_kiosk_ids: j<string[]>(r["target_kiosk_ids"], []),
|
||||
state: s(r["state"]) as OsUpdateRolloutState,
|
||||
percentage: n(r["percentage"]),
|
||||
started_at: sn(r["started_at"]),
|
||||
finished_at: sn(r["finished_at"]),
|
||||
created_at: s(r["created_at"]),
|
||||
created_by: nn(r["created_by"]),
|
||||
created_by: sn(r["created_by"]),
|
||||
};
|
||||
}
|
||||
|
||||
export function rowToLabel(r: Row): Label {
|
||||
return {
|
||||
id: n(r["id"]),
|
||||
id: s(r["id"]),
|
||||
name: s(r["name"]),
|
||||
description: sn(r["description"]),
|
||||
color: sn(r["color"]),
|
||||
|
|
@ -402,8 +402,8 @@ export function rowToLabel(r: Row): Label {
|
|||
|
||||
export function rowToKioskLabel(r: Row): KioskLabel {
|
||||
return {
|
||||
kiosk_id: n(r["kiosk_id"]),
|
||||
label_id: n(r["label_id"]),
|
||||
kiosk_id: s(r["kiosk_id"]),
|
||||
label_id: s(r["label_id"]),
|
||||
role: s(r["role"]) as LabelRole,
|
||||
};
|
||||
}
|
||||
|
|
@ -417,7 +417,7 @@ export function rowToPairingCode(r: Row): PairingCode {
|
|||
issued_at: s(r["issued_at"]),
|
||||
expires_at: s(r["expires_at"]),
|
||||
consumed_at: sn(r["consumed_at"]),
|
||||
consumed_by_kiosk_id: nn(r["consumed_by_kiosk_id"]),
|
||||
consumed_by_kiosk_id: sn(r["consumed_by_kiosk_id"]),
|
||||
extras: j<Record<string, unknown>>(r["extras"], {}),
|
||||
};
|
||||
}
|
||||
|
|
@ -426,8 +426,8 @@ export function rowToKioskGpioBinding(r: Row): KioskGpioBinding {
|
|||
const pullRaw = sn(r["pull"]);
|
||||
const edgeRaw = sn(r["edge"]);
|
||||
return {
|
||||
id: n(r["id"]),
|
||||
kiosk_id: n(r["kiosk_id"]),
|
||||
id: s(r["id"]),
|
||||
kiosk_id: s(r["kiosk_id"]),
|
||||
chip: s(r["chip"]) || "gpiochip0",
|
||||
pin: n(r["pin"]),
|
||||
direction: s(r["direction"]) as GpioDirection,
|
||||
|
|
@ -440,9 +440,9 @@ export function rowToKioskGpioBinding(r: Row): KioskGpioBinding {
|
|||
|
||||
export function rowToEventLog(r: Row): EventLog {
|
||||
return {
|
||||
id: n(r["id"]),
|
||||
source_kiosk_id: nn(r["source_kiosk_id"]),
|
||||
source_camera_id: nn(r["source_camera_id"]),
|
||||
id: s(r["id"]),
|
||||
source_kiosk_id: sn(r["source_kiosk_id"]),
|
||||
source_camera_id: sn(r["source_camera_id"]),
|
||||
source_type: s(r["source_type"]) as EventSourceType,
|
||||
topic: s(r["topic"]),
|
||||
property_op: sn(r["property_op"]),
|
||||
|
|
@ -454,8 +454,8 @@ export function rowToEventLog(r: Row): EventLog {
|
|||
|
||||
export function rowToKioskLog(r: Row): KioskLog {
|
||||
return {
|
||||
id: n(r["id"]),
|
||||
kiosk_id: n(r["kiosk_id"]),
|
||||
id: s(r["id"]),
|
||||
kiosk_id: s(r["kiosk_id"]),
|
||||
level: s(r["level"]) as KioskLogLevel,
|
||||
message: s(r["message"]),
|
||||
context: j<Record<string, unknown>>(r["context"], {}),
|
||||
|
|
@ -480,11 +480,11 @@ export function rowToCloudAccount(r: Row): CloudAccount {
|
|||
|
||||
export function rowToCameraEventSubscription(r: Row): CameraEventSubscription {
|
||||
return {
|
||||
id: n(r["id"]),
|
||||
camera_id: n(r["camera_id"]),
|
||||
id: s(r["id"]),
|
||||
camera_id: s(r["camera_id"]),
|
||||
topic: s(r["topic"]),
|
||||
status: s(r["status"]) as EventSubscriptionStatus,
|
||||
subscribed_by_kiosk_id: nn(r["subscribed_by_kiosk_id"]),
|
||||
subscribed_by_kiosk_id: sn(r["subscribed_by_kiosk_id"]),
|
||||
event_source: sn(r["event_source"]),
|
||||
event_sink: sn(r["event_sink"]),
|
||||
last_event_at: sn(r["last_event_at"]),
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ export const PUBLIC_MIGRATIONS: readonly string[] = [
|
|||
)`,
|
||||
|
||||
`CREATE TABLE IF NOT EXISTS global_admins (
|
||||
id SERIAL PRIMARY KEY,
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
password_hash TEXT NOT NULL,
|
||||
is_active BOOLEAN NOT NULL DEFAULT true,
|
||||
|
|
@ -60,7 +60,7 @@ export const PUBLIC_MIGRATIONS: readonly string[] = [
|
|||
export const TENANT_MIGRATIONS: readonly string[] = [
|
||||
// ---- users ---------------------------------------------------------------
|
||||
`CREATE TABLE IF NOT EXISTS users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
password_hash TEXT NOT NULL,
|
||||
role TEXT NOT NULL DEFAULT 'operator' CHECK(role IN ('admin', 'operator')),
|
||||
|
|
@ -78,7 +78,7 @@ export const TENANT_MIGRATIONS: readonly string[] = [
|
|||
// ---- sessions ------------------------------------------------------------
|
||||
`CREATE TABLE IF NOT EXISTS sessions (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
csrf_token TEXT NOT NULL,
|
||||
totp_pending BOOLEAN NOT NULL DEFAULT false,
|
||||
user_agent TEXT,
|
||||
|
|
@ -93,7 +93,7 @@ export const TENANT_MIGRATIONS: readonly string[] = [
|
|||
|
||||
// ---- api_keys ------------------------------------------------------------
|
||||
`CREATE TABLE IF NOT EXISTS api_keys (
|
||||
id SERIAL PRIMARY KEY,
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
key_hash TEXT NOT NULL,
|
||||
key_prefix TEXT NOT NULL,
|
||||
|
|
@ -119,14 +119,14 @@ export const TENANT_MIGRATIONS: readonly string[] = [
|
|||
|
||||
// ---- displays (final schema — no UNIQUE on index, has kiosk_id) ----------
|
||||
`CREATE TABLE IF NOT EXISTS displays (
|
||||
id SERIAL PRIMARY KEY,
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
"index" INTEGER NOT NULL,
|
||||
is_primary BOOLEAN NOT NULL DEFAULT false,
|
||||
kiosk_id INTEGER,
|
||||
kiosk_id TEXT,
|
||||
width_px INTEGER NOT NULL DEFAULT 1920,
|
||||
height_px INTEGER NOT NULL DEFAULT 1080,
|
||||
default_layout_id INTEGER,
|
||||
default_layout_id TEXT,
|
||||
idle_timeout_seconds INTEGER NOT NULL DEFAULT 600,
|
||||
sleep_timeout_seconds INTEGER NOT NULL DEFAULT 1800,
|
||||
cec_enabled BOOLEAN NOT NULL DEFAULT true,
|
||||
|
|
@ -140,14 +140,14 @@ export const TENANT_MIGRATIONS: readonly string[] = [
|
|||
state_check_enabled BOOLEAN NOT NULL DEFAULT false,
|
||||
state_check_interval_seconds INTEGER NOT NULL DEFAULT 60,
|
||||
is_enabled BOOLEAN NOT NULL DEFAULT true,
|
||||
active_layout_id INTEGER
|
||||
active_layout_id TEXT
|
||||
)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_displays_kiosk ON displays(kiosk_id)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_displays_kiosk_index ON displays(kiosk_id, "index")`,
|
||||
|
||||
// ---- cameras -------------------------------------------------------------
|
||||
`CREATE TABLE IF NOT EXISTS cameras (
|
||||
id SERIAL PRIMARY KEY,
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
type TEXT NOT NULL CHECK(type IN ('rtsp', 'onvif', 'cloud')),
|
||||
rtsp_url TEXT,
|
||||
|
|
@ -172,8 +172,8 @@ export const TENANT_MIGRATIONS: readonly string[] = [
|
|||
`CREATE INDEX IF NOT EXISTS idx_cameras_cloud_account ON cameras(cloud_account_id)`,
|
||||
|
||||
`CREATE TABLE IF NOT EXISTS camera_streams (
|
||||
id SERIAL PRIMARY KEY,
|
||||
camera_id INTEGER NOT NULL REFERENCES cameras(id) ON DELETE CASCADE,
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
camera_id TEXT NOT NULL REFERENCES cameras(id) ON DELETE CASCADE,
|
||||
role TEXT NOT NULL CHECK(role IN ('main', 'sub', 'other')),
|
||||
name TEXT NOT NULL,
|
||||
profile_token TEXT,
|
||||
|
|
@ -192,7 +192,7 @@ export const TENANT_MIGRATIONS: readonly string[] = [
|
|||
|
||||
// ---- kiosks (final schema — all telemetry + update columns) --------------
|
||||
`CREATE TABLE IF NOT EXISTS kiosks (
|
||||
id SERIAL PRIMARY KEY,
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
description TEXT,
|
||||
key_hash TEXT NOT NULL,
|
||||
|
|
@ -205,7 +205,7 @@ export const TENANT_MIGRATIONS: readonly string[] = [
|
|||
paired_at TIMESTAMPTZ,
|
||||
last_seen_at TIMESTAMPTZ,
|
||||
last_bundle_version TEXT,
|
||||
display_id INTEGER REFERENCES displays(id) ON DELETE SET NULL,
|
||||
display_id TEXT REFERENCES displays(id) ON DELETE SET NULL,
|
||||
encrypt_key_encrypted TEXT,
|
||||
cpu_temp_c REAL,
|
||||
cpu_load_percent REAL,
|
||||
|
|
@ -243,7 +243,7 @@ export const TENANT_MIGRATIONS: readonly string[] = [
|
|||
|
||||
// ---- layouts (final schema — no template_id, no display_id) --------------
|
||||
`CREATE TABLE IF NOT EXISTS layouts (
|
||||
id SERIAL PRIMARY KEY,
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
description TEXT,
|
||||
priority TEXT NOT NULL DEFAULT 'normal' CHECK(priority IN ('hot', 'normal', 'cold')),
|
||||
|
|
@ -254,28 +254,28 @@ export const TENANT_MIGRATIONS: readonly string[] = [
|
|||
|
||||
// ---- display_layouts (join table) ----------------------------------------
|
||||
`CREATE TABLE IF NOT EXISTS display_layouts (
|
||||
display_id INTEGER NOT NULL REFERENCES displays(id) ON DELETE CASCADE,
|
||||
layout_id INTEGER NOT NULL REFERENCES layouts(id) ON DELETE CASCADE,
|
||||
display_id TEXT NOT NULL REFERENCES displays(id) ON DELETE CASCADE,
|
||||
layout_id TEXT NOT NULL REFERENCES layouts(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (display_id, layout_id)
|
||||
)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_display_layouts_layout ON display_layouts(layout_id)`,
|
||||
|
||||
// ---- layout_cells (final schema — no region_name) ------------------------
|
||||
`CREATE TABLE IF NOT EXISTS layout_cells (
|
||||
id SERIAL PRIMARY KEY,
|
||||
layout_id INTEGER NOT NULL REFERENCES layouts(id) ON DELETE CASCADE,
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
layout_id TEXT NOT NULL REFERENCES layouts(id) ON DELETE CASCADE,
|
||||
"row" INTEGER NOT NULL DEFAULT 0,
|
||||
col INTEGER NOT NULL DEFAULT 0,
|
||||
row_span INTEGER NOT NULL DEFAULT 1,
|
||||
col_span INTEGER NOT NULL DEFAULT 1,
|
||||
content_type TEXT NOT NULL CHECK(content_type IN ('none', 'camera', 'web', 'html')),
|
||||
camera_id INTEGER REFERENCES cameras(id) ON DELETE SET NULL,
|
||||
camera_id TEXT REFERENCES cameras(id) ON DELETE SET NULL,
|
||||
stream_selector TEXT,
|
||||
web_url TEXT,
|
||||
html_content TEXT,
|
||||
cooling_timeout_seconds INTEGER,
|
||||
options JSONB NOT NULL DEFAULT '{}',
|
||||
entity_id INTEGER,
|
||||
entity_id TEXT,
|
||||
fit TEXT NOT NULL DEFAULT 'cover'
|
||||
)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_layout_cells_layout ON layout_cells(layout_id)`,
|
||||
|
|
@ -283,26 +283,26 @@ export const TENANT_MIGRATIONS: readonly string[] = [
|
|||
|
||||
// ---- labels --------------------------------------------------------------
|
||||
`CREATE TABLE IF NOT EXISTS labels (
|
||||
id SERIAL PRIMARY KEY,
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
description TEXT,
|
||||
color TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS kiosk_labels (
|
||||
kiosk_id INTEGER NOT NULL REFERENCES kiosks(id) ON DELETE CASCADE,
|
||||
label_id INTEGER NOT NULL REFERENCES labels(id) ON DELETE CASCADE,
|
||||
kiosk_id TEXT NOT NULL REFERENCES kiosks(id) ON DELETE CASCADE,
|
||||
label_id TEXT NOT NULL REFERENCES labels(id) ON DELETE CASCADE,
|
||||
role TEXT NOT NULL CHECK(role IN ('consume', 'operate')),
|
||||
PRIMARY KEY (kiosk_id, label_id, role)
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS camera_labels (
|
||||
camera_id INTEGER NOT NULL REFERENCES cameras(id) ON DELETE CASCADE,
|
||||
label_id INTEGER NOT NULL REFERENCES labels(id) ON DELETE CASCADE,
|
||||
camera_id TEXT NOT NULL REFERENCES cameras(id) ON DELETE CASCADE,
|
||||
label_id TEXT NOT NULL REFERENCES labels(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (camera_id, label_id)
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS layout_labels (
|
||||
layout_id INTEGER NOT NULL REFERENCES layouts(id) ON DELETE CASCADE,
|
||||
label_id INTEGER NOT NULL REFERENCES labels(id) ON DELETE CASCADE,
|
||||
layout_id TEXT NOT NULL REFERENCES layouts(id) ON DELETE CASCADE,
|
||||
label_id TEXT NOT NULL REFERENCES labels(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (layout_id, label_id)
|
||||
)`,
|
||||
|
||||
|
|
@ -315,15 +315,15 @@ export const TENANT_MIGRATIONS: readonly string[] = [
|
|||
issued_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
expires_at TIMESTAMPTZ NOT NULL,
|
||||
consumed_at TIMESTAMPTZ,
|
||||
consumed_by_kiosk_id INTEGER REFERENCES kiosks(id) ON DELETE SET NULL,
|
||||
consumed_by_kiosk_id TEXT REFERENCES kiosks(id) ON DELETE SET NULL,
|
||||
extras JSONB NOT NULL DEFAULT '{}'
|
||||
)`,
|
||||
|
||||
// ---- event_log -----------------------------------------------------------
|
||||
`CREATE TABLE IF NOT EXISTS event_log (
|
||||
id SERIAL PRIMARY KEY,
|
||||
source_kiosk_id INTEGER REFERENCES kiosks(id) ON DELETE SET NULL,
|
||||
source_camera_id INTEGER REFERENCES cameras(id) ON DELETE SET NULL,
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
source_kiosk_id TEXT REFERENCES kiosks(id) ON DELETE SET NULL,
|
||||
source_camera_id TEXT REFERENCES cameras(id) ON DELETE SET NULL,
|
||||
source_type TEXT NOT NULL CHECK(source_type IN ('onvif', 'gpio', 'synthetic', 'system')),
|
||||
topic TEXT NOT NULL,
|
||||
property_op TEXT,
|
||||
|
|
@ -336,11 +336,11 @@ export const TENANT_MIGRATIONS: readonly string[] = [
|
|||
|
||||
// ---- entities ------------------------------------------------------------
|
||||
`CREATE TABLE IF NOT EXISTS entities (
|
||||
id SERIAL PRIMARY KEY,
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
name TEXT NOT NULL UNIQUE,
|
||||
type TEXT NOT NULL CHECK(type IN ('camera', 'html', 'web', 'dashboard')),
|
||||
description TEXT,
|
||||
camera_id INTEGER REFERENCES cameras(id) ON DELETE CASCADE,
|
||||
camera_id TEXT REFERENCES cameras(id) ON DELETE CASCADE,
|
||||
html_content TEXT,
|
||||
web_url TEXT,
|
||||
dashboard_id TEXT,
|
||||
|
|
@ -360,7 +360,7 @@ export const TENANT_MIGRATIONS: readonly string[] = [
|
|||
signature TEXT NOT NULL,
|
||||
release_notes TEXT,
|
||||
uploaded_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
uploaded_by INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
||||
uploaded_by TEXT REFERENCES users(id) ON DELETE SET NULL,
|
||||
yanked_at TIMESTAMPTZ,
|
||||
UNIQUE(version, arch)
|
||||
)`,
|
||||
|
|
@ -375,7 +375,7 @@ export const TENANT_MIGRATIONS: readonly string[] = [
|
|||
started_at TIMESTAMPTZ,
|
||||
finished_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
created_by INTEGER REFERENCES users(id) ON DELETE SET NULL
|
||||
created_by TEXT REFERENCES users(id) ON DELETE SET NULL
|
||||
)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_firmware_rollouts_state ON firmware_rollouts(state)`,
|
||||
|
||||
|
|
@ -391,7 +391,7 @@ export const TENANT_MIGRATIONS: readonly string[] = [
|
|||
bundle_format TEXT NOT NULL DEFAULT 'raucb' CHECK(bundle_format = 'raucb'),
|
||||
release_notes TEXT,
|
||||
uploaded_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
uploaded_by INTEGER REFERENCES users(id) ON DELETE SET NULL,
|
||||
uploaded_by TEXT REFERENCES users(id) ON DELETE SET NULL,
|
||||
yanked_at TIMESTAMPTZ,
|
||||
UNIQUE(version, compatibility)
|
||||
)`,
|
||||
|
|
@ -406,16 +406,16 @@ export const TENANT_MIGRATIONS: readonly string[] = [
|
|||
started_at TIMESTAMPTZ,
|
||||
finished_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
created_by INTEGER REFERENCES users(id) ON DELETE SET NULL
|
||||
created_by TEXT REFERENCES users(id) ON DELETE SET NULL
|
||||
)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_os_update_rollouts_state ON os_update_rollouts(state)`,
|
||||
|
||||
// ---- audit_log -----------------------------------------------------------
|
||||
`CREATE TABLE IF NOT EXISTS audit_log (
|
||||
id SERIAL PRIMARY KEY,
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
ts TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
actor_type TEXT NOT NULL CHECK(actor_type IN ('user', 'api_key', 'system', 'kiosk')),
|
||||
actor_id INTEGER,
|
||||
actor_id TEXT,
|
||||
actor_label TEXT,
|
||||
action TEXT NOT NULL,
|
||||
resource_type TEXT,
|
||||
|
|
@ -430,8 +430,8 @@ export const TENANT_MIGRATIONS: readonly string[] = [
|
|||
|
||||
// ---- kiosk GPIO bindings -------------------------------------------------
|
||||
`CREATE TABLE IF NOT EXISTS kiosk_gpio_bindings (
|
||||
id SERIAL PRIMARY KEY,
|
||||
kiosk_id INTEGER NOT NULL REFERENCES kiosks(id) ON DELETE CASCADE,
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
kiosk_id TEXT NOT NULL REFERENCES kiosks(id) ON DELETE CASCADE,
|
||||
chip TEXT NOT NULL DEFAULT 'gpiochip4',
|
||||
pin INTEGER NOT NULL,
|
||||
direction TEXT NOT NULL DEFAULT 'in' CHECK(direction IN ('in', 'out')),
|
||||
|
|
@ -445,8 +445,8 @@ export const TENANT_MIGRATIONS: readonly string[] = [
|
|||
|
||||
// ---- kiosk_logs ----------------------------------------------------------
|
||||
`CREATE TABLE IF NOT EXISTS kiosk_logs (
|
||||
id SERIAL PRIMARY KEY,
|
||||
kiosk_id INTEGER NOT NULL REFERENCES kiosks(id) ON DELETE CASCADE,
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
kiosk_id TEXT NOT NULL REFERENCES kiosks(id) ON DELETE CASCADE,
|
||||
level TEXT NOT NULL CHECK(level IN ('debug', 'info', 'warn', 'error')),
|
||||
message TEXT NOT NULL,
|
||||
context JSONB NOT NULL DEFAULT '{}',
|
||||
|
|
@ -471,11 +471,11 @@ export const TENANT_MIGRATIONS: readonly string[] = [
|
|||
|
||||
// ---- camera_event_subscriptions ---------------------------------------------
|
||||
`CREATE TABLE IF NOT EXISTS camera_event_subscriptions (
|
||||
id SERIAL PRIMARY KEY,
|
||||
camera_id INTEGER NOT NULL REFERENCES cameras(id) ON DELETE CASCADE,
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
camera_id TEXT NOT NULL REFERENCES cameras(id) ON DELETE CASCADE,
|
||||
topic TEXT NOT NULL,
|
||||
status TEXT NOT NULL DEFAULT 'inactive' CHECK(status IN ('inactive', 'pending', 'active', 'failed')),
|
||||
subscribed_by_kiosk_id INTEGER REFERENCES kiosks(id) ON DELETE SET NULL,
|
||||
subscribed_by_kiosk_id TEXT REFERENCES kiosks(id) ON DELETE SET NULL,
|
||||
last_event_at TIMESTAMPTZ,
|
||||
error_message TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -25,10 +25,10 @@ export interface MqttBridgeLog {
|
|||
}
|
||||
|
||||
export interface MqttBridge {
|
||||
publishEvent(kioskId: number | "server", topic: string, payload: Record<string, unknown>): void;
|
||||
publishTelemetry(kioskId: number, payload: Record<string, unknown>): void;
|
||||
publishEvent(kioskId: string | "server", topic: string, payload: Record<string, unknown>): void;
|
||||
publishTelemetry(kioskId: string, payload: Record<string, unknown>): void;
|
||||
/** Subscribe to inbound RPC. Callback gets parsed JSON or {} on parse error. */
|
||||
onRpc(handler: (kioskId: number, method: string, body: Record<string, unknown>) => void): void;
|
||||
onRpc(handler: (kioskId: string, method: string, body: Record<string, unknown>) => void): void;
|
||||
end(): void;
|
||||
}
|
||||
|
||||
|
|
@ -57,7 +57,7 @@ export function initMqttBridge(config: MqttConfig, log: MqttBridgeLog): MqttBrid
|
|||
const password = config.password;
|
||||
|
||||
let client: MqttClient | undefined;
|
||||
let rpcHandlers: Array<(k: number, m: string, b: Record<string, unknown>) => void> = [];
|
||||
let rpcHandlers: Array<(k: string, m: string, b: Record<string, unknown>) => void> = [];
|
||||
|
||||
try {
|
||||
client = mqtt.connect(url, {
|
||||
|
|
@ -97,9 +97,9 @@ export function initMqttBridge(config: MqttConfig, log: MqttBridgeLog): MqttBrid
|
|||
// Expected: <prefix>/<kiosk_id>/rpc/req/<method>
|
||||
const parts = topic.split("/");
|
||||
if (parts.length !== 5 || parts[0] !== prefix || parts[2] !== "rpc" || parts[3] !== "req") return;
|
||||
const kioskId = Number(parts[1]);
|
||||
const kioskId = parts[1] ?? "";
|
||||
const method = parts[4];
|
||||
if (!Number.isFinite(kioskId) || !method) return;
|
||||
if (!kioskId || !method) return;
|
||||
let body: Record<string, unknown> = {};
|
||||
try { body = JSON.parse(payload.toString("utf8")) as Record<string, unknown>; }
|
||||
catch { /* ignore body parse errors — handler can use empty */ }
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ export async function initiatePairing(
|
|||
|
||||
export interface PairingClaimResult {
|
||||
status: "pending" | "claimed";
|
||||
kioskId?: number;
|
||||
kioskId?: string;
|
||||
kioskName?: string;
|
||||
kioskKey?: string;
|
||||
clusterKey?: string;
|
||||
|
|
@ -123,7 +123,7 @@ export interface PairingConfirmInput {
|
|||
* pointed at the same kiosk id — only credentials + hardware metadata roll.
|
||||
* When set, nameOverride and initialLabels are ignored.
|
||||
*/
|
||||
replaceKioskId?: number;
|
||||
replaceKioskId?: string;
|
||||
/** Bypass replacement-target sanity checks (hardware_model / capabilities / managed_image). */
|
||||
force?: boolean;
|
||||
}
|
||||
|
|
@ -134,7 +134,7 @@ export async function confirmPairing(
|
|||
secrets: SecretsApi,
|
||||
input: PairingConfirmInput,
|
||||
obs?: Observable,
|
||||
): Promise<{ kioskId: number; kioskName: string }> {
|
||||
): Promise<{ kioskId: string; kioskName: string }> {
|
||||
obs?.log.info("confirm pairing for code {code}", { code: input.code });
|
||||
const pc = await repo.getPairingCode(input.code);
|
||||
if (!pc) throw new Error("pairing code not found");
|
||||
|
|
@ -145,7 +145,7 @@ export async function confirmPairing(
|
|||
const kioskKeyHash = await auth.hashPassword(kioskKeyPlaintext);
|
||||
const kioskKeyPrefix = kioskKeyPlaintext.slice(0, 8);
|
||||
|
||||
let kioskId: number;
|
||||
let kioskId: string;
|
||||
let kioskName: string;
|
||||
|
||||
if (input.replaceKioskId != null) {
|
||||
|
|
|
|||
|
|
@ -16,11 +16,11 @@ export type CellContentType = "none" | "camera" | "web" | "html";
|
|||
export type EntityType = "camera" | "html" | "web" | "dashboard";
|
||||
|
||||
export interface Entity {
|
||||
id: number;
|
||||
id: string;
|
||||
name: string;
|
||||
type: EntityType;
|
||||
description: string | null;
|
||||
camera_id: number | null;
|
||||
camera_id: string | null;
|
||||
html_content: string | null;
|
||||
web_url: string | null;
|
||||
/** Node-RED dashboard tab id; populated when type === "dashboard". */
|
||||
|
|
@ -33,7 +33,7 @@ export type LabelRole = "consume" | "operate";
|
|||
export type EventSourceType = "onvif" | "gpio" | "synthetic" | "system";
|
||||
|
||||
export interface User {
|
||||
id: number;
|
||||
id: string;
|
||||
username: string;
|
||||
password_hash: string;
|
||||
role: UserRole;
|
||||
|
|
@ -50,7 +50,7 @@ export interface User {
|
|||
|
||||
export interface Session {
|
||||
id: string; // hex32
|
||||
user_id: number;
|
||||
user_id: string;
|
||||
csrf_token: string;
|
||||
totp_pending: boolean;
|
||||
user_agent: string | null;
|
||||
|
|
@ -62,7 +62,7 @@ export interface Session {
|
|||
}
|
||||
|
||||
export interface ApiKey {
|
||||
id: number;
|
||||
id: string;
|
||||
name: string;
|
||||
key_hash: string;
|
||||
key_prefix: string; // indexed for O(1) lookup
|
||||
|
|
@ -84,14 +84,14 @@ export interface SetupState {
|
|||
}
|
||||
|
||||
export interface Display {
|
||||
id: number;
|
||||
id: string;
|
||||
name: string;
|
||||
index: number; // unique
|
||||
is_primary: boolean; // deprecated — kept for backward compat, not used
|
||||
kiosk_id: number | null; // FK → kiosks; displays belong to kiosks
|
||||
kiosk_id: string | null; // FK → kiosks; displays belong to kiosks
|
||||
width_px: number;
|
||||
height_px: number;
|
||||
default_layout_id: number | null;
|
||||
default_layout_id: string | null;
|
||||
idle_timeout_seconds: number;
|
||||
sleep_timeout_seconds: number;
|
||||
cec_enabled: boolean;
|
||||
|
|
@ -103,14 +103,14 @@ export interface Display {
|
|||
state_check_enabled: boolean;
|
||||
state_check_interval_seconds: number;
|
||||
is_enabled: boolean;
|
||||
active_layout_id: number | null;
|
||||
active_layout_id: string | null;
|
||||
}
|
||||
|
||||
export type EventSourceMode = "auto" | "server" | string; // string = "kiosk:<id>"
|
||||
export type EventSinkMode = "auto" | "server" | string;
|
||||
|
||||
export interface Camera {
|
||||
id: number;
|
||||
id: string;
|
||||
name: string;
|
||||
type: CameraType;
|
||||
rtsp_url: string | null;
|
||||
|
|
@ -133,8 +133,8 @@ export interface Camera {
|
|||
}
|
||||
|
||||
export interface CameraStream {
|
||||
id: number;
|
||||
camera_id: number;
|
||||
id: string;
|
||||
camera_id: string;
|
||||
role: StreamRole;
|
||||
name: string;
|
||||
profile_token: string | null;
|
||||
|
|
@ -154,7 +154,7 @@ export interface CameraStream {
|
|||
}
|
||||
|
||||
export interface LayoutTemplate {
|
||||
id: number;
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
regions: LayoutRegion[];
|
||||
|
|
@ -172,10 +172,10 @@ export interface LayoutRegion {
|
|||
}
|
||||
|
||||
export interface Layout {
|
||||
id: number;
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
template_id: number | null; // deprecated — kept nullable for backward compat
|
||||
template_id: string | null; // deprecated — kept nullable for backward compat
|
||||
/** @deprecated Cells now own their own position. Computed from cells at read time. */
|
||||
regions: LayoutRegion[];
|
||||
/** @deprecated Computed from cells: max(col + col_span). */
|
||||
|
|
@ -184,18 +184,18 @@ export interface Layout {
|
|||
grid_rows: number;
|
||||
/** @deprecated Layouts are now standalone; use display_layouts join table.
|
||||
* Column kept on the row for backward compat — will be removed in a future migration. */
|
||||
display_id: number | null;
|
||||
display_id: string | null;
|
||||
priority: LayoutPriority;
|
||||
cooling_timeout_seconds: number | null;
|
||||
preload_camera_ids: number[];
|
||||
preload_camera_ids: string[];
|
||||
/** @deprecated Per-display defaults live on `display.default_layout_id`. */
|
||||
is_default: boolean;
|
||||
resets_idle_timer: boolean;
|
||||
}
|
||||
|
||||
export interface LayoutCell {
|
||||
id: number;
|
||||
layout_id: number;
|
||||
id: string;
|
||||
layout_id: string;
|
||||
/** @deprecated Cells own their position via row/col/row_span/col_span now. */
|
||||
region_name: string;
|
||||
row: number;
|
||||
|
|
@ -203,18 +203,18 @@ export interface LayoutCell {
|
|||
row_span: number;
|
||||
col_span: number;
|
||||
content_type: CellContentType;
|
||||
camera_id: number | null;
|
||||
camera_id: string | null;
|
||||
stream_selector: StreamSelector;
|
||||
web_url: string | null;
|
||||
html_content: string | null;
|
||||
cooling_timeout_seconds: number | null;
|
||||
options: Record<string, unknown>;
|
||||
entity_id: number | null;
|
||||
entity_id: string | null;
|
||||
fit: "cover" | "contain" | "fill";
|
||||
}
|
||||
|
||||
export interface Kiosk {
|
||||
id: number;
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
key_hash: string;
|
||||
|
|
@ -227,7 +227,7 @@ export interface Kiosk {
|
|||
paired_at: string | null;
|
||||
last_seen_at: string | null;
|
||||
last_bundle_version: string | null;
|
||||
display_id: number | null; // deprecated — displays now point to kiosks via kiosk_id
|
||||
display_id: string | null; // deprecated — displays now point to kiosks via kiosk_id
|
||||
cpu_temp_c: number | null;
|
||||
cpu_load_percent: number | null;
|
||||
fan_rpm: number | null;
|
||||
|
|
@ -272,10 +272,10 @@ export type AuditActorType = "user" | "api_key" | "system" | "kiosk";
|
|||
export type AuditResult = "ok" | "failed";
|
||||
|
||||
export interface AuditEntry {
|
||||
id: number;
|
||||
id: string;
|
||||
ts: string;
|
||||
actor_type: AuditActorType;
|
||||
actor_id: number | null;
|
||||
actor_id: string | null;
|
||||
actor_label: string | null;
|
||||
action: string;
|
||||
resource_type: string | null;
|
||||
|
|
@ -300,20 +300,20 @@ export interface FirmwareRelease {
|
|||
signature: string;
|
||||
release_notes: string | null;
|
||||
uploaded_at: string;
|
||||
uploaded_by: number | null;
|
||||
uploaded_by: string | null;
|
||||
yanked_at: string | null;
|
||||
}
|
||||
|
||||
export interface FirmwareRollout {
|
||||
id: string;
|
||||
release_id: string;
|
||||
target_kiosk_ids: number[];
|
||||
target_kiosk_ids: string[];
|
||||
state: FirmwareRolloutState;
|
||||
percentage: number;
|
||||
started_at: string | null;
|
||||
finished_at: string | null;
|
||||
created_at: string;
|
||||
created_by: number | null;
|
||||
created_by: string | null;
|
||||
}
|
||||
|
||||
export interface OsUpdateRelease {
|
||||
|
|
@ -327,20 +327,20 @@ export interface OsUpdateRelease {
|
|||
bundle_format: "raucb";
|
||||
release_notes: string | null;
|
||||
uploaded_at: string;
|
||||
uploaded_by: number | null;
|
||||
uploaded_by: string | null;
|
||||
yanked_at: string | null;
|
||||
}
|
||||
|
||||
export interface OsUpdateRollout {
|
||||
id: string;
|
||||
release_id: string;
|
||||
target_kiosk_ids: number[];
|
||||
target_kiosk_ids: string[];
|
||||
state: OsUpdateRolloutState;
|
||||
percentage: number;
|
||||
started_at: string | null;
|
||||
finished_at: string | null;
|
||||
created_at: string;
|
||||
created_by: number | null;
|
||||
created_by: string | null;
|
||||
}
|
||||
|
||||
export type CloudVendor = "hikconnect" | "dahua" | "tuya" | "uniview" | "tplink";
|
||||
|
|
@ -358,7 +358,7 @@ export interface CloudAccount {
|
|||
}
|
||||
|
||||
export interface Label {
|
||||
id: number;
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
color: string | null;
|
||||
|
|
@ -366,8 +366,8 @@ export interface Label {
|
|||
}
|
||||
|
||||
export interface KioskLabel {
|
||||
kiosk_id: number;
|
||||
label_id: number;
|
||||
kiosk_id: string;
|
||||
label_id: string;
|
||||
role: LabelRole;
|
||||
}
|
||||
|
||||
|
|
@ -379,7 +379,7 @@ export interface PairingCode {
|
|||
issued_at: string;
|
||||
expires_at: string;
|
||||
consumed_at: string | null;
|
||||
consumed_by_kiosk_id: number | null;
|
||||
consumed_by_kiosk_id: string | null;
|
||||
extras: Record<string, unknown>;
|
||||
}
|
||||
|
||||
|
|
@ -388,8 +388,8 @@ export type GpioPull = "up" | "down" | "none";
|
|||
export type GpioEdge = "rising" | "falling" | "both";
|
||||
|
||||
export interface KioskGpioBinding {
|
||||
id: number;
|
||||
kiosk_id: number;
|
||||
id: string;
|
||||
kiosk_id: string;
|
||||
chip: string;
|
||||
pin: number;
|
||||
direction: GpioDirection;
|
||||
|
|
@ -400,9 +400,9 @@ export interface KioskGpioBinding {
|
|||
}
|
||||
|
||||
export interface EventLog {
|
||||
id: number;
|
||||
source_kiosk_id: number | null;
|
||||
source_camera_id: number | null;
|
||||
id: string;
|
||||
source_kiosk_id: string | null;
|
||||
source_camera_id: string | null;
|
||||
source_type: EventSourceType;
|
||||
topic: string;
|
||||
property_op: string | null;
|
||||
|
|
@ -414,11 +414,11 @@ export interface EventLog {
|
|||
export type EventSubscriptionStatus = "inactive" | "pending" | "active" | "failed";
|
||||
|
||||
export interface CameraEventSubscription {
|
||||
id: number;
|
||||
camera_id: number;
|
||||
id: string;
|
||||
camera_id: string;
|
||||
topic: string;
|
||||
status: EventSubscriptionStatus;
|
||||
subscribed_by_kiosk_id: number | null;
|
||||
subscribed_by_kiosk_id: string | null;
|
||||
event_source: string | null;
|
||||
event_sink: string | null;
|
||||
last_event_at: string | null;
|
||||
|
|
@ -428,8 +428,8 @@ export interface CameraEventSubscription {
|
|||
|
||||
export interface EventQueryFilters {
|
||||
topic?: string;
|
||||
kiosk_id?: number;
|
||||
camera_id?: number;
|
||||
kiosk_id?: string;
|
||||
camera_id?: string;
|
||||
source_type?: string;
|
||||
from?: string;
|
||||
to?: string;
|
||||
|
|
@ -440,8 +440,8 @@ export interface EventQueryFilters {
|
|||
export type KioskLogLevel = "debug" | "info" | "warn" | "error";
|
||||
|
||||
export interface KioskLog {
|
||||
id: number;
|
||||
kiosk_id: number;
|
||||
id: string;
|
||||
kiosk_id: string;
|
||||
level: KioskLogLevel;
|
||||
message: string;
|
||||
context: Record<string, unknown>;
|
||||
|
|
@ -450,7 +450,7 @@ export interface KioskLog {
|
|||
}
|
||||
|
||||
export interface KioskLogQueryFilters {
|
||||
kiosk_id: number;
|
||||
kiosk_id: string;
|
||||
level?: KioskLogLevel;
|
||||
from?: string;
|
||||
to?: string;
|
||||
|
|
|
|||
|
|
@ -114,8 +114,8 @@ export function OverviewPage(props: OverviewProps) {
|
|||
interface CamerasProps {
|
||||
user: string;
|
||||
cameras: Camera[];
|
||||
streamCounts: Map<number, number>;
|
||||
activeKiosks: Map<number, number>;
|
||||
streamCounts: Map<string, number>;
|
||||
activeKiosks: Map<string, number>;
|
||||
}
|
||||
|
||||
export function CamerasPage(props: CamerasProps) {
|
||||
|
|
@ -1165,9 +1165,9 @@ interface CameraSubscription {
|
|||
interface CameraEditProps {
|
||||
user: string;
|
||||
camera: Camera;
|
||||
labels: Array<{ label_id: number; name: string }>;
|
||||
labels: Array<{ label_id: string; name: string }>;
|
||||
allLabels: Label[];
|
||||
streams: Array<{ id: number; role: string; name: string; rtsp_uri: string; rtsp_host: string | null; rtsp_port: number | null; rtsp_path: string | null }>;
|
||||
streams: Array<{ id: string; role: string; name: string; rtsp_uri: string; rtsp_host: string | null; rtsp_port: number | null; rtsp_path: string | null }>;
|
||||
subscriptions: CameraSubscription[];
|
||||
eventSubscriptions?: CameraEventSubscription[];
|
||||
error?: string;
|
||||
|
|
@ -1180,8 +1180,8 @@ interface CameraEditProps {
|
|||
* hx-target="#camera-labels-<id>" hx-swap="innerHTML".
|
||||
*/
|
||||
export function renderCameraLabels(
|
||||
cameraId: number,
|
||||
labels: Array<{ label_id: number; name: string }>,
|
||||
cameraId: string,
|
||||
labels: Array<{ label_id: string; name: string }>,
|
||||
allLabels: Label[],
|
||||
): string {
|
||||
const labelsTargetSelector = `#camera-labels-${String(cameraId)}`;
|
||||
|
|
@ -1542,7 +1542,7 @@ export function CameraEditPage(props: CameraEditProps) {
|
|||
interface KioskEditProps {
|
||||
user: string;
|
||||
kiosk: Kiosk;
|
||||
labels: Array<{ label_id: number; name: string; role: string }>;
|
||||
labels: Array<{ label_id: string; name: string; role: string }>;
|
||||
allLabels: Label[];
|
||||
displays?: Display[];
|
||||
displayLayouts?: Array<{ display: Display; layouts: LayoutType[] }>;
|
||||
|
|
@ -1561,8 +1561,8 @@ interface KioskEditProps {
|
|||
* hx-target="#kiosk-labels-<id>" hx-swap="innerHTML".
|
||||
*/
|
||||
export function renderKioskLabels(
|
||||
kioskId: number,
|
||||
labels: Array<{ label_id: number; name: string; role: string }>,
|
||||
kioskId: string,
|
||||
labels: Array<{ label_id: string; name: string; role: string }>,
|
||||
allLabels: Label[],
|
||||
): string {
|
||||
const labelsTargetSelector = `#kiosk-labels-${String(kioskId)}`;
|
||||
|
|
@ -2138,7 +2138,7 @@ interface LayoutsPageProps {
|
|||
user: string;
|
||||
layouts: LayoutType[];
|
||||
/** layout_id → number of displays the layout is attached to */
|
||||
displayCounts: Map<number, number>;
|
||||
displayCounts: Map<string, number>;
|
||||
}
|
||||
|
||||
export function LayoutsPage(props: LayoutsPageProps) {
|
||||
|
|
@ -2305,8 +2305,8 @@ export const LAYOUT_BUILDER_CSS = `
|
|||
|
||||
function cellLabel(
|
||||
c: LayoutCell,
|
||||
entityById: Map<number, Entity>,
|
||||
cameraById: Map<number, Camera>,
|
||||
entityById: Map<string, Entity>,
|
||||
cameraById: Map<string, Camera>,
|
||||
): string {
|
||||
if (c.entity_id != null) {
|
||||
const ent = entityById.get(c.entity_id);
|
||||
|
|
@ -2331,15 +2331,15 @@ function cellGridStyle(c: LayoutCell): string {
|
|||
* suitable for hx-swap="outerHTML" against itself.
|
||||
*/
|
||||
export function renderCell(
|
||||
layoutId: number,
|
||||
layoutId: string,
|
||||
c: LayoutCell,
|
||||
entities: Entity[],
|
||||
cameras: Camera[],
|
||||
mode: "read" | "edit",
|
||||
): string {
|
||||
const cameraById = new Map<number, Camera>();
|
||||
const cameraById = new Map<string, Camera>();
|
||||
for (const cam of cameras) cameraById.set(cam.id, cam);
|
||||
const entityById = new Map<number, Entity>();
|
||||
const entityById = new Map<string, Entity>();
|
||||
for (const e of entities) entityById.set(e.id, e);
|
||||
const style = cellGridStyle(c);
|
||||
const cellGetUrl = `/admin/layouts/${String(layoutId)}/cells/${String(c.id)}`;
|
||||
|
|
@ -2535,7 +2535,7 @@ export function renderCell(
|
|||
* in via `hx-target="#layout-grid" hx-swap="innerHTML"` after add/delete/resize.
|
||||
*/
|
||||
export function renderGrid(
|
||||
layoutId: number,
|
||||
layoutId: string,
|
||||
cells: LayoutCell[],
|
||||
entities: Entity[],
|
||||
cameras: Camera[],
|
||||
|
|
@ -2721,8 +2721,8 @@ interface DisplayEditPageProps {
|
|||
* `renderDefaultLayoutSelect`.
|
||||
*/
|
||||
export function renderDisplayLayouts(
|
||||
displayId: number,
|
||||
defaultLayoutId: number | null,
|
||||
displayId: string,
|
||||
defaultLayoutId: string | null,
|
||||
attached: LayoutType[],
|
||||
available: LayoutType[],
|
||||
): string {
|
||||
|
|
@ -2829,7 +2829,7 @@ export function renderDisplayLayouts(
|
|||
* page. The id matches the in-page select so swap-by-id works.
|
||||
*/
|
||||
export function renderDefaultLayoutSelect(
|
||||
defaultLayoutId: number | null,
|
||||
defaultLayoutId: string | null,
|
||||
attached: LayoutType[],
|
||||
oob: boolean = false,
|
||||
): string {
|
||||
|
|
|
|||
Loading…
Reference in a new issue