/** * Reading the roster the backup-freshness check runs against. * * Split out from the check itself so the decision — is this backup too * old — stays pure, while the queries that feed it live here (Rule 2.3). * Shared by `celilo system audit` and by the scheduled `backups` monitor, * so both judge the same fleet from the same data. */ import { eq } from 'drizzle-orm'; import type { DbClient } from '../../db/client'; import { backups, moduleConfigs, modules } from '../../db/schema'; import type { ModuleManifest } from '../../manifest/schema'; import { BACKUP_SCHEDULE_CONFIG_KEY } from '../backup-schedule'; import { parseStoredConfigValue } from '../module-config'; import type { InstalledModuleBackupInfo } from './backups'; const DEPLOYED_STATES = ['INSTALLED', 'VERIFIED']; /** * Most recent COMPLETED backup per module, in epoch ms. * * The `backups` table is added via inline ALTER statements in * db/client.ts for upgraded databases, so a freshly-initialized DB built * from the drizzle journal alone may not have it yet. Absence means "no * backups recorded", never a crashed audit. */ function latestSuccessfulBackupByModule(db: DbClient): Map { const latest = new Map(); try { for (const backup of db.select().from(backups).where(eq(backups.status, 'completed')).all()) { if (!backup.moduleId || !backup.completedAt) continue; const at = backup.completedAt.getTime(); const previous = latest.get(backup.moduleId); if (previous === undefined || at > previous) latest.set(backup.moduleId, at); } } catch { // Table missing — leave the map empty. } return latest; } export function loadBackupAuditInfo(db: DbClient): InstalledModuleBackupInfo[] { const latest = latestSuccessfulBackupByModule(db); const overrides = backupScheduleOverrides(db); return db .select() .from(modules) .all() .filter((module) => DEPLOYED_STATES.includes(module.state)) .map((module) => ({ id: module.id, state: module.state, manifest: module.manifestData as ModuleManifest, scheduleOverride: overrides.get(module.id), lastSuccessfulBackupAt: latest.get(module.id) ?? null, })); } /** * Every operator backup-cadence override, by module. * * Read here rather than resolved here: the audit resolves override-against- * manifest through the same accessor the backup sweep uses, so the two cannot * disagree about what a module's cadence is. */ export function backupScheduleOverrides(db: DbClient): Map { const overrides = new Map(); for (const row of db .select() .from(moduleConfigs) .where(eq(moduleConfigs.key, BACKUP_SCHEDULE_CONFIG_KEY)) .all()) { overrides.set(row.moduleId, String(parseStoredConfigValue(row))); } return overrides; }