diff --git a/kiosk/src/server.rs b/kiosk/src/server.rs index 7cc6511..314cb83 100644 --- a/kiosk/src/server.rs +++ b/kiosk/src/server.rs @@ -200,8 +200,8 @@ pub fn heartbeat( hw: &crate::hwmon::HwInfo, ) { let client = reqwest::blocking::Client::new(); - let display_info: Vec<_> = displays.iter().map(|(name, w, h)| { - serde_json::json!({ "name": name, "width_px": w, "height_px": h }) + let display_info: Vec<_> = displays.iter().enumerate().map(|(index, (name, w, h))| { + serde_json::json!({ "index": index, "name": name, "width_px": w, "height_px": h }) }).collect(); let _ = client .post(format!("{server}/api/kiosk/heartbeat")) diff --git a/server/src/plugins/service-api-http/index.ts b/server/src/plugins/service-api-http/index.ts index 320c4ba..4ac337d 100644 --- a/server/src/plugins/service-api-http/index.ts +++ b/server/src/plugins/service-api-http/index.ts @@ -258,7 +258,7 @@ function registerKioskRoutes( bundle_version?: string; kiosk_app_version?: string; os_version?: string; - displays?: Array<{ name: string; width_px: number; height_px: number }>; + displays?: Array<{ index?: number; name: string; width_px: number; height_px: number }>; cpu_temp_c?: number | null; fan_rpm?: number | null; fan_pwm?: number | null; @@ -276,24 +276,45 @@ function registerKioskRoutes( // Sync displays reported by the kiosk if (Array.isArray(body?.displays)) { const existing = repo.listDisplaysForKiosk(kiosk.id); - for (const reported of body.displays) { - const match = existing.find((d) => d.name.endsWith(reported.name)); + const seenDisplayIds = new Set(); + for (const [position, reported] of body.displays.entries()) { + const reportedIndex = Number.isInteger(reported.index) && reported.index! >= 0 + ? reported.index! + : position; + const match = existing.find((d) => d.name.endsWith(reported.name)) + ?? existing.find((d) => d.index === reportedIndex); if (match) { - if (match.width_px !== reported.width_px || match.height_px !== reported.height_px) { + seenDisplayIds.add(match.id); + if ( + match.name !== reported.name + || match.index !== reportedIndex + || match.width_px !== reported.width_px + || match.height_px !== reported.height_px + ) { repo.updateDisplay(match.id, { + name: reported.name, + index: reportedIndex, width_px: reported.width_px, height_px: reported.height_px, } as any); } } else { // New display — create it - repo.createDisplayForKiosk(kiosk.id, { + const created = repo.createDisplayForKiosk(kiosk.id, { name: reported.name, + index: reportedIndex, width_px: reported.width_px, height_px: reported.height_px, }); + seenDisplayIds.add(created.id); } } + for (const display of existing) { + if (seenDisplayIds.has(display.id) || !display.is_enabled) continue; + if (!display.name.endsWith(" HDMI-0")) continue; + if (repo.listLayoutsForDisplay(display.id).length > 0) continue; + repo.updateDisplay(display.id, { is_enabled: false } as any); + } } return { ok: true, now: new Date().toISOString() }; diff --git a/server/src/plugins/service-store/migrations.ts b/server/src/plugins/service-store/migrations.ts index f87b92d..cf4c464 100644 --- a/server/src/plugins/service-store/migrations.ts +++ b/server/src/plugins/service-store/migrations.ts @@ -657,4 +657,56 @@ export const MIGRATIONS: readonly MigrationEntry[] = [ (db: DatabaseSync) => { addColumnIfNotExists(db, "displays", "is_enabled", "INTEGER NOT NULL DEFAULT 1"); }, + + // ---- displays.index is local to the kiosk, not globally unique ------------- + (db: DatabaseSync) => { + const row = db + .prepare("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'displays'") + .get() as { sql?: string } | undefined; + if (!row?.sql || !row.sql.includes('"index" INTEGER NOT NULL UNIQUE')) return; + + db.exec("PRAGMA foreign_keys = OFF"); + db.exec(` + CREATE TABLE displays_new ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + "index" INTEGER NOT NULL, + is_primary INTEGER NOT NULL DEFAULT 0, + width_px INTEGER NOT NULL DEFAULT 1920, + height_px INTEGER NOT NULL DEFAULT 1080, + default_layout_id INTEGER, + idle_timeout_seconds INTEGER NOT NULL DEFAULT 600, + sleep_timeout_seconds INTEGER NOT NULL DEFAULT 1800, + cec_enabled INTEGER NOT NULL DEFAULT 1, + cec_device_path TEXT, + cec_logical_address INTEGER, + desired_power_state TEXT NOT NULL DEFAULT 'follow_layout' + CHECK(desired_power_state IN ('follow_layout', 'on', 'standby')), + state_check_enabled INTEGER NOT NULL DEFAULT 0, + state_check_interval_seconds INTEGER NOT NULL DEFAULT 60, + kiosk_id INTEGER REFERENCES kiosks(id) ON DELETE SET NULL, + is_enabled INTEGER NOT NULL DEFAULT 1 + ) STRICT; + + INSERT INTO displays_new ( + id, name, "index", is_primary, width_px, height_px, default_layout_id, + idle_timeout_seconds, sleep_timeout_seconds, cec_enabled, cec_device_path, + cec_logical_address, desired_power_state, state_check_enabled, + state_check_interval_seconds, kiosk_id, is_enabled + ) + SELECT + id, name, "index", is_primary, width_px, height_px, default_layout_id, + idle_timeout_seconds, sleep_timeout_seconds, cec_enabled, cec_device_path, + cec_logical_address, desired_power_state, state_check_enabled, + state_check_interval_seconds, kiosk_id, is_enabled + FROM displays; + + DROP TABLE displays; + ALTER TABLE displays_new RENAME TO displays; + 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"); + `); + db.exec("PRAGMA foreign_keys = ON"); + }, ]; diff --git a/server/src/plugins/service-store/repository.ts b/server/src/plugins/service-store/repository.ts index 530bb82..8f7a3a1 100644 --- a/server/src/plugins/service-store/repository.ts +++ b/server/src/plugins/service-store/repository.ts @@ -373,8 +373,7 @@ export class Repository { width_px?: number; height_px?: number; }): Display { - // Find next available index - const idx = input.index ?? this.nextDisplayIndex(); + const idx = input.index ?? this.nextDisplayIndexForKiosk(kioskId); const result = this.prep( `INSERT INTO displays (name, "index", is_primary, kiosk_id, width_px, height_px) VALUES (?, ?, 0, ?, ?, ?)`, @@ -399,8 +398,8 @@ export class Repository { return rs.map((r) => rowToDisplay(r as Record)); } - private nextDisplayIndex(): number { - const r = this.prep('SELECT MAX("index") AS m FROM displays').get() as { m: number | null } | undefined; + private nextDisplayIndexForKiosk(kioskId: number): number { + const r = this.prep('SELECT MAX("index") AS m FROM displays WHERE kiosk_id = ?').get(kioskId) as { m: number | null } | undefined; return (r?.m ?? -1) + 1; }