fix: PG cloud_accounts migration + rollout-safe cleanup + setup cursor

- Add cloud_accounts table to PostgreSQL tenant migrations (was only
  in SQLite).
- Artifact cleanup now skips releases referenced by active/queued/paused
  rollouts (CASCADE would delete the rollout).
- Add invisible cursor theme install to setup-pi-kiosk.sh (was only
  in pi-gen image build).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mitchell R 2026-05-23 02:59:27 +02:00
parent 1a87c97479
commit 851274d05d
No known key found for this signature in database
3 changed files with 61 additions and 1 deletions

View file

@ -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('<III', 16, 0x00010000, 1)
toc = struct.pack('<III', 0xfffd0002, 1, 28)
img = struct.pack('<IIIIIIIII', 36, 0xfffd0002, 1, 1, 1, 1, 0, 0, 0)
px = struct.pack('<I', 0)
data = hdr + toc + img + px
for name in ['default','left_ptr','arrow','watch','hand2','text','xterm',
'top_left_corner','top_right_corner','bottom_left_corner',
'bottom_right_corner','sb_h_double_arrow','sb_v_double_arrow',
'fleur','crosshair','question_arrow','x_cursor','pirate',
'sb_left_arrow','sb_right_arrow','sb_up_arrow','sb_down_arrow',
'top_side','bottom_side','left_side','right_side']:
with open(os.path.join('$CURSOR_DIR', name), 'wb') as f:
f.write(data)
"
update-alternatives --install /usr/share/icons/default/index.theme x-cursor-theme \
/usr/share/icons/betterframe-empty/cursor.theme 100 2>/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" \

View file

@ -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)`,
];

View file

@ -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<boolean> {
async function cleanupFirmware(repo: Repository, log: CleanupLog): Promise<number> {
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<numbe
releases.sort((a, b) => 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<numbe
async function cleanupOsUpdates(repo: Repository, log: CleanupLog): Promise<number> {
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<numb
releases.sort((a, b) => 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++;