From 2398be6853f0092c34f0a0914a66ab807b0e21c0 Mon Sep 17 00:00:00 2001 From: Mitchell R Date: Sun, 10 May 2026 22:03:32 +0200 Subject: [PATCH] fix: drop legacy layouts.template_id/display_id columns via table rebuild - Migration v0.5: rebuild layouts table without template_id, display_id, regions, grid_cols, grid_rows, is_default - Migration v0.6: rebuild layout_cells table without region_name - Migration v0.7: drop layout_templates table entirely (concept removed) - createLayout simplified to clean column set (no sentinel values) - createLayoutCell simplified (no region_name placeholder) - Removed all layout_template repo methods (dead code) Answers user question "why template_id/display_id in template": SQLite can't drop columns without rebuilding the table. Now done properly. --- .../src/plugins/service-store/migrations.ts | 66 ++++++++++++++ .../src/plugins/service-store/repository.ts | 89 ++----------------- 2 files changed, 73 insertions(+), 82 deletions(-) diff --git a/server/src/plugins/service-store/migrations.ts b/server/src/plugins/service-store/migrations.ts index 5ffe174..0223cd5 100644 --- a/server/src/plugins/service-store/migrations.ts +++ b/server/src/plugins/service-store/migrations.ts @@ -366,4 +366,70 @@ export const MIGRATIONS: readonly MigrationEntry[] = [ } } }, + + // ---- v0.5: rebuild layouts table to drop legacy columns + // SQLite can't drop columns, so rebuild: create new schema → copy data → + // drop old → rename. Removes template_id, display_id, regions, grid_cols, + // grid_rows, is_default — cells own position now, displays attach via join. + (db: DatabaseSync) => { + const cols = db.prepare(`PRAGMA table_info("layouts")`).all() as Array<{ name: string }>; + const hasTemplateId = cols.some((c) => c.name === "template_id"); + if (!hasTemplateId) return; // already migrated + + db.exec("PRAGMA foreign_keys = OFF"); + db.exec(` + CREATE TABLE layouts_new ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL UNIQUE, + description TEXT, + priority TEXT NOT NULL DEFAULT 'normal' CHECK(priority IN ('hot', 'normal', 'cold')), + cooling_timeout_seconds INTEGER, + preload_camera_ids TEXT NOT NULL DEFAULT '[]', + resets_idle_timer INTEGER NOT NULL DEFAULT 1 + ) STRICT; + + INSERT INTO layouts_new (id, name, description, priority, cooling_timeout_seconds, preload_camera_ids, resets_idle_timer) + SELECT id, name, description, priority, cooling_timeout_seconds, preload_camera_ids, resets_idle_timer FROM layouts; + + DROP TABLE layouts; + ALTER TABLE layouts_new RENAME TO layouts; + `); + db.exec("PRAGMA foreign_keys = ON"); + }, + + // Same cleanup for layout_cells — drop region_name, layout_id FK stays + (db: DatabaseSync) => { + const cols = db.prepare(`PRAGMA table_info("layout_cells")`).all() as Array<{ name: string }>; + const hasRegionName = cols.some((c) => c.name === "region_name"); + if (!hasRegionName) return; + + db.exec("PRAGMA foreign_keys = OFF"); + db.exec(` + CREATE TABLE layout_cells_new ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + layout_id INTEGER 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 ('camera', 'web', 'html')), + camera_id INTEGER REFERENCES cameras(id) ON DELETE SET NULL, + stream_selector TEXT, + web_url TEXT, + html_content TEXT, + cooling_timeout_seconds INTEGER, + options TEXT NOT NULL DEFAULT '{}' + ) STRICT; + + INSERT INTO layout_cells_new (id, layout_id, row, col, row_span, col_span, content_type, camera_id, stream_selector, web_url, html_content, cooling_timeout_seconds, options) + SELECT id, layout_id, row, col, row_span, col_span, content_type, camera_id, stream_selector, web_url, html_content, cooling_timeout_seconds, options FROM layout_cells; + + DROP TABLE layout_cells; + ALTER TABLE layout_cells_new RENAME TO layout_cells; + `); + db.exec("PRAGMA foreign_keys = ON"); + }, + + // Drop layout_templates entirely — concept removed + `DROP TABLE IF EXISTS layout_templates`, ]; diff --git a/server/src/plugins/service-store/repository.ts b/server/src/plugins/service-store/repository.ts index c8183b9..b254510 100644 --- a/server/src/plugins/service-store/repository.ts +++ b/server/src/plugins/service-store/repository.ts @@ -415,61 +415,6 @@ export class Repository { // layout templates // =========================================================================== - listLayoutTemplates(): LayoutTemplate[] { - const rs = this.prep("SELECT * FROM layout_templates ORDER BY name").all(); - return rs.map((r) => rowToLayoutTemplate(r as Record)); - } - - getLayoutTemplateById(id: number): LayoutTemplate | null { - const r = this.prep("SELECT * FROM layout_templates WHERE id = ?").get(id); - return r ? rowToLayoutTemplate(r as Record) : null; - } - - createLayoutTemplate(input: { - name: string; - description?: string | null; - regions: unknown; - grid_cols?: number; - grid_rows?: number; - is_builtin?: boolean; - }): LayoutTemplate { - const result = this.prep( - `INSERT INTO layout_templates (name, description, regions, grid_cols, grid_rows, is_builtin) - VALUES (?, ?, ?, ?, ?, ?)`, - ).run( - input.name, - input.description ?? null, - J(input.regions), - input.grid_cols ?? 12, - input.grid_rows ?? 12, - B(input.is_builtin ?? false), - ); - const id = Number(result.lastInsertRowid); - void this.notify("layout_templates", "create", id); - const r = this.getLayoutTemplateById(id); - if (!r) throw new Error("layout_template vanished after insert"); - return r; - } - - updateLayoutTemplate(id: number, patch: { name?: string; description?: string | null; regions?: unknown; grid_cols?: number; grid_rows?: number }): void { - const sets: string[] = []; - const vals: unknown[] = []; - for (const [k, v] of Object.entries(patch)) { - if (k === "id") continue; - sets.push(`${k} = ?`); - vals.push(k === "regions" ? J(v) : (v === undefined ? null : v)); - } - if (sets.length === 0) return; - vals.push(id); - this.db.prepare(`UPDATE layout_templates SET ${sets.join(", ")} WHERE id = ?`).run(...vals as any[]); - void this.notify("layout_templates", "update", id); - } - - deleteLayoutTemplate(id: number): void { - this.db.prepare(`DELETE FROM layout_templates WHERE id = ?`).run(id); - void this.notify("layout_templates", "delete", id); - } - // =========================================================================== // layouts // =========================================================================== @@ -546,25 +491,15 @@ export class Repository { preload_camera_ids?: number[]; resets_idle_timer?: boolean; }): Layout { - // Legacy NOT NULL columns (template_id, display_id, regions, grid_*) are - // populated with sentinel values: cells own their position now and the - // grid is computed at read time. The columns will be dropped by a future - // migration — until then they're inert. const result = this.prep( - `INSERT INTO layouts (name, description, template_id, regions, grid_cols, grid_rows, display_id, priority, cooling_timeout_seconds, preload_camera_ids, is_default, resets_idle_timer) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + `INSERT INTO layouts (name, description, priority, cooling_timeout_seconds, preload_camera_ids, resets_idle_timer) + VALUES (?, ?, ?, ?, ?, ?)`, ).run( input.name, input.description ?? null, - 0, - J([]), - 1, - 1, - 0, input.priority ?? "normal", input.cooling_timeout_seconds ?? null, J(input.preload_camera_ids ?? []), - B(false), B(input.resets_idle_timer ?? true), ); const id = Number(result.lastInsertRowid); @@ -618,15 +553,11 @@ export class Repository { cooling_timeout_seconds?: number | null; options?: Record; }): LayoutCell { - // region_name column is legacy NOT NULL — synthesize a unique placeholder - // until the column is dropped by a future migration. Nothing reads it. - const placeholder = `cell_${input.layout_id}_${input.row}_${input.col}_${Date.now()}`; const result = this.prep( - `INSERT INTO layout_cells (layout_id, region_name, "row", col, row_span, col_span, content_type, camera_id, stream_selector, web_url, html_content, cooling_timeout_seconds, options) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + `INSERT INTO layout_cells (layout_id, "row", col, row_span, col_span, content_type, camera_id, stream_selector, web_url, html_content, cooling_timeout_seconds, options) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, ).run( input.layout_id, - placeholder, input.row, input.col, input.row_span ?? 1, @@ -1156,15 +1087,9 @@ export class Repository { return this.listLayoutCells(layoutId); } - layoutTemplates(ids: number[]): LayoutTemplate[] { - if (ids.length === 0) return []; - const placeholders = ids.map(() => "?").join(","); - const rs = this.db - .prepare( - `SELECT * FROM layout_templates WHERE id IN (${placeholders})`, - ) - .all(...(ids as never[])); - return rs.map((r) => rowToLayoutTemplate(r as Record)); + // Deprecated — layout_templates dropped in v0.5 + layoutTemplates(_ids: number[]): LayoutTemplate[] { + return []; } cameraLabelNames(cameraId: number): string[] {