diff --git a/server/src/plugins/service-api-http/index.ts b/server/src/plugins/service-api-http/index.ts index 1f2301f..9ca5d41 100644 --- a/server/src/plugins/service-api-http/index.ts +++ b/server/src/plugins/service-api-http/index.ts @@ -511,6 +511,20 @@ function registerKioskRoutes( forwarded_to_nodered: false, }); + // 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)) { + try { + repo.updateDisplay(displayId, { active_layout_id: layoutId } as any); + } catch { + // Display might not exist; layout.changed is best-effort telemetry. + } + } + } + // Best-effort forward to Node-RED. Topics that have a dedicated trigger // node (bf-trigger-layout-changed etc.) expect a FLAT payload matching // what the admin-side emit produces — splat body.payload up to the top diff --git a/server/src/plugins/service-store/mappers.ts b/server/src/plugins/service-store/mappers.ts index cc87073..107eab1 100644 --- a/server/src/plugins/service-store/mappers.ts +++ b/server/src/plugins/service-store/mappers.ts @@ -141,6 +141,7 @@ 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"]), }; } diff --git a/server/src/plugins/service-store/migrations.ts b/server/src/plugins/service-store/migrations.ts index 37ad06c..8793cf9 100644 --- a/server/src/plugins/service-store/migrations.ts +++ b/server/src/plugins/service-store/migrations.ts @@ -867,6 +867,13 @@ export const MIGRATIONS: readonly MigrationEntry[] = [ addColumnIfNotExists(db, "displays", "actual_power_state_at", "TEXT"); }, + // Kiosk reports active layout per display via layout.changed events. + // Persist on the display row so the admin UI can highlight which layout + // is currently rendering instead of defaulting to first-in-list. + (db: DatabaseSync) => { + addColumnIfNotExists(db, "displays", "active_layout_id", "INTEGER REFERENCES layouts(id) ON DELETE SET NULL"); + }, + // Backfill hwmon/telemetry columns. They were originally added inline to // an earlier migration entry; existing deploys had already passed that // index via PRAGMA user_version, so the new columns silently never landed. diff --git a/server/src/shared/types.ts b/server/src/shared/types.ts index a3557b5..db810ca 100644 --- a/server/src/shared/types.ts +++ b/server/src/shared/types.ts @@ -103,6 +103,7 @@ export interface Display { state_check_enabled: boolean; state_check_interval_seconds: number; is_enabled: boolean; + active_layout_id: number | null; } export interface Camera { diff --git a/server/src/web-templates/admin-pages.tsx b/server/src/web-templates/admin-pages.tsx index 0c3a488..2a99515 100644 --- a/server/src/web-templates/admin-pages.tsx +++ b/server/src/web-templates/admin-pages.tsx @@ -1596,7 +1596,10 @@ export function KioskEditPage(props: KioskEditProps) { {layouts.length > 0 ? ( ) : ( @@ -2538,6 +2541,16 @@ export function DisplayEditPage(props: DisplayEditPageProps) { {d.kiosk_id && (
Kiosk: {props.kioskName ?? `#${String(d.kiosk_id)}`}
)} + {(() => { + const active = props.attachedLayouts.find((l) => l.id === d.active_layout_id); + return ( +
+ Active layout: {active + ? {active.name} + : (unknown — kiosk hasn't reported)} +
+ ); + })()} {props.attachedLayouts.length > 0 && d.kiosk_id ? (
@@ -2553,7 +2566,10 @@ export function DisplayEditPage(props: DisplayEditPageProps) { >