diff --git a/deploy/scripts/setup-pi-kiosk.sh b/deploy/scripts/setup-pi-kiosk.sh index 9153eb3..86a6b0f 100755 --- a/deploy/scripts/setup-pi-kiosk.sh +++ b/deploy/scripts/setup-pi-kiosk.sh @@ -245,6 +245,32 @@ if [ "${INSTALL_KIOSK}" = "1" ]; then printf 'BetterFrame Kiosk\n\n' > /etc/issue rm -f /etc/update-motd.d/10-uname /etc/update-motd.d/* 2>/dev/null || true + echo "==> Installing invisible cursor theme" + CURSOR_DIR=/usr/share/icons/betterframe-empty/cursors + install -d -m 755 "$CURSOR_DIR" + install -m 644 "${REPO_ROOT}/deploy/cursor-theme/betterframe-empty/cursor.theme" \ + /usr/share/icons/betterframe-empty/index.theme + install -m 644 "${REPO_ROOT}/deploy/cursor-theme/betterframe-empty/cursor.theme" \ + /usr/share/icons/betterframe-empty/cursor.theme + python3 -c " +import struct, os +hdr = b'Xcur' + struct.pack('/dev/null || true + echo "==> Installing PAM + systemd unit + firmware rollback hook" install -m 644 "${REPO_ROOT}/deploy/pam.d/cage" /etc/pam.d/cage install -m 644 "${REPO_ROOT}/deploy/systemd/betterframe-kiosk.service" \ diff --git a/server/src/plugins/service-store/migrations-pg.ts b/server/src/plugins/service-store/migrations-pg.ts index bef052f..8f0be2a 100644 --- a/server/src/plugins/service-store/migrations-pg.ts +++ b/server/src/plugins/service-store/migrations-pg.ts @@ -434,4 +434,18 @@ export const TENANT_MIGRATIONS: readonly string[] = [ received_at TIMESTAMPTZ NOT NULL DEFAULT now() )`, `CREATE INDEX IF NOT EXISTS idx_kiosk_logs_kiosk ON kiosk_logs(kiosk_id, received_at DESC)`, + + // ---- cloud_accounts ------------------------------------------------------- + `CREATE TABLE IF NOT EXISTS cloud_accounts ( + id TEXT PRIMARY KEY, + vendor TEXT NOT NULL, + name TEXT NOT NULL, + credentials_encrypted TEXT NOT NULL, + is_active BOOLEAN NOT NULL DEFAULT true, + last_sync_at TIMESTAMPTZ, + last_sync_error TEXT, + camera_count INTEGER NOT NULL DEFAULT 0, + created_at TIMESTAMPTZ NOT NULL DEFAULT now() + )`, + `CREATE INDEX IF NOT EXISTS idx_cloud_accounts_vendor ON cloud_accounts(vendor)`, ]; diff --git a/server/src/shared/artifact-cleanup.ts b/server/src/shared/artifact-cleanup.ts index 47e185c..e56770e 100644 --- a/server/src/shared/artifact-cleanup.ts +++ b/server/src/shared/artifact-cleanup.ts @@ -3,8 +3,10 @@ * * Strategy: * 1. Delete artifact files for yanked releases (DB row kept for audit). + * Skip if an active/queued/paused rollout references the release. * 2. For each channel, keep only the N most recent non-yanked releases. - * Older ones are yanked + artifact deleted. + * Older ones get artifact deleted + DB row removed. Skip if referenced + * by an active rollout. * * Runs every 6 hours. Safe to call concurrently — worst case is two passes * trying to delete the same file (ENOENT is swallowed). @@ -33,8 +35,16 @@ async function removeFile(path: string): Promise { async function cleanupFirmware(repo: Repository, log: CleanupLog): Promise { let cleaned = 0; + const rollouts = await repo.listFirmwareRollouts(); + const activeReleaseIds = new Set( + rollouts + .filter((r) => r.state === "queued" || r.state === "active" || r.state === "paused") + .map((r) => r.release_id), + ); + const yanked = await repo.listYankedFirmwareReleases(); for (const r of yanked) { + if (activeReleaseIds.has(r.id)) continue; if (await removeFile(r.artifact_path)) { log.info(`firmware cleanup: deleted artifact for yanked ${r.version} (${r.arch})`); cleaned++; @@ -56,6 +66,7 @@ async function cleanupFirmware(repo: Repository, log: CleanupLog): Promise b.uploaded_at.localeCompare(a.uploaded_at)); const excess = releases.slice(KEEP_PER_CHANNEL); for (const r of excess) { + if (activeReleaseIds.has(r.id)) continue; if (await removeFile(r.artifact_path)) { log.info(`firmware cleanup: pruned old ${r.version} (${r.arch})`); cleaned++; @@ -70,8 +81,16 @@ async function cleanupFirmware(repo: Repository, log: CleanupLog): Promise { let cleaned = 0; + const rollouts = await repo.listOsUpdateRollouts(); + const activeReleaseIds = new Set( + rollouts + .filter((r) => r.state === "queued" || r.state === "active" || r.state === "paused") + .map((r) => r.release_id), + ); + const yanked = await repo.listYankedOsUpdateReleases(); for (const r of yanked) { + if (activeReleaseIds.has(r.id)) continue; if (await removeFile(r.artifact_path)) { log.info(`os-update cleanup: deleted artifact for yanked ${r.version}`); cleaned++; @@ -93,6 +112,7 @@ async function cleanupOsUpdates(repo: Repository, log: CleanupLog): Promise b.uploaded_at.localeCompare(a.uploaded_at)); const excess = releases.slice(KEEP_PER_CHANNEL); for (const r of excess) { + if (activeReleaseIds.has(r.id)) continue; if (await removeFile(r.artifact_path)) { log.info(`os-update cleanup: pruned old ${r.version}`); cleaned++;