From 4c1edbd3b22307ce2aa69ead1c6ebd873ffbf563 Mon Sep 17 00:00:00 2001 From: Mitchell R Date: Thu, 21 May 2026 11:46:20 +0200 Subject: [PATCH] fix(migrations): catch-all backfill for all missing tables/columns Every column + table added inside an already-passed PRAGMA user_version entry is re-created here with IF NOT EXISTS / addColumnIfNotExists so existing deploys finally pick them up: - kiosks: reported_hostname, network_interfaces_json - kiosks: os_update_channel, os_update_target_version, os_update_last_* - kiosks: managed_image, managed_config_* - displays: active_layout_id - os_update_releases table + indices - os_update_rollouts table + indices Rule going forward: NEVER add columns/tables inside existing migration entries. Always append a NEW entry at the end of the MIGRATIONS array. --- .../src/plugins/service-store/migrations.ts | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/server/src/plugins/service-store/migrations.ts b/server/src/plugins/service-store/migrations.ts index d2198ee..49ad0a5 100644 --- a/server/src/plugins/service-store/migrations.ts +++ b/server/src/plugins/service-store/migrations.ts @@ -900,4 +900,63 @@ export const MIGRATIONS: readonly MigrationEntry[] = [ ) STRICT`, `CREATE INDEX IF NOT EXISTS idx_kiosk_logs_kiosk_received ON kiosk_logs(kiosk_id, received_at DESC)`, `CREATE INDEX IF NOT EXISTS idx_kiosk_logs_level ON kiosk_logs(level, received_at DESC)`, + + // Catch-all backfill for tables/columns that were added inside earlier + // migration entries after existing deploys had already passed those + // indices via PRAGMA user_version. All IF NOT EXISTS / addColumnIfNotExists + // so they're safe to run on fresh DBs too. + (db: DatabaseSync) => { + // --- reported hostname + network interfaces (heartbeat telemetry) --- + addColumnIfNotExists(db, "kiosks", "reported_hostname", "TEXT"); + addColumnIfNotExists(db, "kiosks", "network_interfaces_json", "TEXT"); + + // --- OS update per-kiosk prefs --- + addColumnIfNotExists(db, "kiosks", "os_update_channel", "TEXT NOT NULL DEFAULT 'stable'"); + addColumnIfNotExists(db, "kiosks", "os_update_target_version", "TEXT"); + addColumnIfNotExists(db, "kiosks", "os_update_last_attempt_at", "TEXT"); + addColumnIfNotExists(db, "kiosks", "os_update_last_attempt_version", "TEXT"); + addColumnIfNotExists(db, "kiosks", "os_update_last_error", "TEXT"); + + // --- OS update releases + rollouts tables --- + db.exec(`CREATE TABLE IF NOT EXISTS os_update_releases ( + id TEXT PRIMARY KEY, + version TEXT NOT NULL, + channel TEXT NOT NULL CHECK(channel IN ('stable', 'beta', 'dev')), + compatibility TEXT NOT NULL, + artifact_path TEXT NOT NULL, + size_bytes INTEGER NOT NULL, + sha256 TEXT NOT NULL, + bundle_format TEXT NOT NULL DEFAULT 'raucb', + release_notes TEXT, + uploaded_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), + uploaded_by INTEGER REFERENCES users(id) ON DELETE SET NULL, + yanked_at TEXT + ) STRICT`); + db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS idx_os_update_releases_version_compat ON os_update_releases(version, compatibility)`); + db.exec(`CREATE INDEX IF NOT EXISTS idx_os_update_releases_channel ON os_update_releases(channel, compatibility, uploaded_at DESC)`); + + db.exec(`CREATE TABLE IF NOT EXISTS os_update_rollouts ( + id TEXT PRIMARY KEY, + release_id TEXT NOT NULL REFERENCES os_update_releases(id) ON DELETE CASCADE, + target_kiosk_ids TEXT NOT NULL DEFAULT '[]', + state TEXT NOT NULL DEFAULT 'queued' CHECK(state IN ('queued', 'active', 'paused', 'complete')), + percentage INTEGER NOT NULL DEFAULT 100 CHECK(percentage BETWEEN 1 AND 100), + started_at TEXT, + finished_at TEXT, + created_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now')), + created_by INTEGER REFERENCES users(id) ON DELETE SET NULL + ) STRICT`); + db.exec(`CREATE INDEX IF NOT EXISTS idx_os_update_rollouts_state ON os_update_rollouts(state)`); + + // --- managed-image config columns --- + addColumnIfNotExists(db, "kiosks", "managed_image", "INTEGER NOT NULL DEFAULT 0"); + addColumnIfNotExists(db, "kiosks", "managed_config_json", "TEXT"); + addColumnIfNotExists(db, "kiosks", "managed_config_version", "INTEGER NOT NULL DEFAULT 0"); + addColumnIfNotExists(db, "kiosks", "managed_config_applied_version", "INTEGER NOT NULL DEFAULT 0"); + addColumnIfNotExists(db, "kiosks", "managed_config_applied_at", "TEXT"); + addColumnIfNotExists(db, "kiosks", "managed_config_error", "TEXT"); + + // --- display active layout --- + addColumnIfNotExists(db, "displays", "active_layout_id", "INTEGER REFERENCES layouts(id) ON DELETE SET NULL"); + }, ];