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:
@@ -0,0 +1,11 @@
|
||||
-- Last-success/last-error for the encrypted DB backup were previously tracked only as
|
||||
-- in-process fields on BackupService (never written to the DB) — so every server restart
|
||||
-- (deploy/crash/OOM/host reboot, all routine under `restart: always`) silently reset the admin
|
||||
-- UI's "last successful backup" to "Never", even with valid, correctly-rotating backups already
|
||||
-- on disk (2026-08-30 field incident, park-buzi). Four additive, nullable columns; null = no
|
||||
-- run recorded yet (or, for the error pair, no failure since the last success). See
|
||||
-- wiki/concepts/backup-recovery.md.
|
||||
ALTER TABLE `site_config` ADD `backup_last_success_at` text;--> statement-breakpoint
|
||||
ALTER TABLE `site_config` ADD `backup_last_result_json` text;--> statement-breakpoint
|
||||
ALTER TABLE `site_config` ADD `backup_last_error_at` text;--> statement-breakpoint
|
||||
ALTER TABLE `site_config` ADD `backup_last_error` text;
|
||||
@@ -176,6 +176,13 @@
|
||||
"when": 1783948800000,
|
||||
"tag": "0024_validation_programs",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 25,
|
||||
"version": "6",
|
||||
"when": 1788078414270,
|
||||
"tag": "0025_backup_last_status",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -288,6 +288,20 @@ export const siteConfig = sqliteTable("site_config", {
|
||||
backupKeepLast: integer("backup_keep_last"),
|
||||
/** Beyond keepLast, keep one backup per day for this many days. null ⇒ code default (30). */
|
||||
backupKeepDailyDays: integer("backup_keep_daily_days"),
|
||||
/** ISO timestamp of the last backup that actually completed successfully. Persisted here
|
||||
* (not just in-process memory) so the admin UI's "last successful backup" survives a
|
||||
* server restart — before this column existed, a restart silently reset that status to
|
||||
* "Never" even with valid backups already on disk. null = no successful run recorded yet.
|
||||
* See wiki/concepts/backup-recovery.md. */
|
||||
backupLastSuccessAt: text("backup_last_success_at"),
|
||||
/** JSON-encoded { path, bytes, prunedFiles } of the last successful run, for the same
|
||||
* restart-durability reason as backupLastSuccessAt. null = none recorded yet. */
|
||||
backupLastResultJson: text("backup_last_result_json"),
|
||||
/** ISO timestamp of the last FAILED scheduled/manual backup attempt, persisted for the same
|
||||
* reason. null = no failure recorded (or none since the last success). */
|
||||
backupLastErrorAt: text("backup_last_error_at"),
|
||||
/** Error message of the last failed attempt. Cleared (set null) on the next success. */
|
||||
backupLastError: text("backup_last_error"),
|
||||
updatedAt: text("updated_at")
|
||||
.notNull()
|
||||
.default(sql`(current_timestamp)`),
|
||||
|
||||
Reference in New Issue
Block a user