fix(backup): persist last-success/error status; wall-clock-based schedule
BackupService tracked last-success/last-error as plain in-process fields and scheduled the daily backup via setInterval measured from process start — so any server restart (deploy/crash/OOM/reboot, routine under `restart: always`) silently reset the admin UI to "last successful backup: Never" and drifted the actual cadence, independent of whether backups were writing correctly to disk (they were — a real field incident at park-buzi showed 7 valid rotating backups on disk with the status stuck on "Never"). Persist last-success/error to new site_config columns (migration 0025) and add BackupService.isDue(), computed from the persisted timestamp instead of process uptime; server.ts now polls every 15 min and lets isDue() gate the actual run. No API/UI contract change. Claude-Session: https://claude.ai/code/session_01FWncR69HgGPuei1dLrW3cU
This commit is contained in:
@@ -336,12 +336,20 @@ export async function buildServer(opts: BuildOptions = {}): Promise<FastifyInsta
|
||||
void runSnapPrune(); // once at startup
|
||||
app.addHook("onClose", async () => clearInterval(snapPruneTimer));
|
||||
|
||||
// Scheduled encrypted backup — daily, unref'd. A no-op (silent) until BACKUP_TARGET_DIR +
|
||||
// BACKUP_KEY are configured; tolerates an unreachable/unmounted target by recording the
|
||||
// error and trying again next run. NOT run once at startup (a just-booted appliance after a
|
||||
// power cut shouldn't immediately write to a possibly-not-yet-mounted disk; the daily cadence
|
||||
// and the manual button cover it). See wiki/concepts/backup-recovery.md.
|
||||
const backupTimer = setInterval(() => void backupService.runScheduled(), 24 * 60 * 60 * 1000);
|
||||
// Scheduled encrypted backup — checked every 15 min, unref'd; `runScheduled()` itself is a
|
||||
// no-op unless a full 24h has actually elapsed since the last PERSISTED success (isDue(), in
|
||||
// backup-service.ts), so this frequent poll does not cause frequent backups. Deliberately
|
||||
// NOT a `setInterval(..., 24h)` measured from process start: that design silently reset its
|
||||
// own countdown on every restart (deploy/crash/OOM/reboot, all routine under `restart:
|
||||
// always`), which could push a day's backup out arbitrarily far AND — before last-success was
|
||||
// persisted — made the admin UI show "Never" despite valid backups already on disk
|
||||
// (2026-08-30 field incident, park-buzi). A short poll against a persisted, wall-clock
|
||||
// timestamp is immune to both restart timing and to any single restart cadence. A no-op
|
||||
// (silent) until BACKUP_TARGET_DIR + BACKUP_KEY are configured; tolerates an
|
||||
// unreachable/unmounted target by recording the error and trying again next check. NOT run
|
||||
// once at startup (a just-booted appliance after a power cut shouldn't immediately write to a
|
||||
// possibly-not-yet-mounted disk). See wiki/concepts/backup-recovery.md.
|
||||
const backupTimer = setInterval(() => void backupService.runScheduled(), 15 * 60 * 1000);
|
||||
backupTimer.unref();
|
||||
app.addHook("onClose", async () => clearInterval(backupTimer));
|
||||
if (backupService.configured) {
|
||||
|
||||
Reference in New Issue
Block a user