feat(523): the backup job - an hourly tick that runs at the chosen hour, Back up now, and the last 30 runs
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 13s
CI & Build / TypeScript typecheck (push) Successful in 53s
CI & Build / integration (push) Successful in 1m9s
CI & Build / Python tests (push) Successful in 1m54s
CI & Build / Build & push image (push) Successful in 23s

services/backup_job.py reads its config when it fires, so startup reads
nothing (#4181) and a changed hour needs no reschedule. A run is held to
a 60-minute deadline (a hung NFS mount would otherwise hold the lock and
skip every later backup), never runs twice at once, and keeps its outcome
- file, size, duration, or the reason in words - in the bounded
backup_runs setting, which no backup carries. summarise() gives the page
its verdict: overdue when on and no success for 26h.

Admin API: GET/PUT /api/admin/backups, POST /backups/check (tests unsaved
form values, 45s deadline), POST /backups/run (202, background).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-15 17:34:01 -04:00
co-authored by Claude Opus 5.5
parent ff4f2ee5c2
commit 4248b450ea
6 changed files with 563 additions and 1 deletions
+5 -1
View File
@@ -776,7 +776,11 @@ def _rule_version_rows(rows) -> list[dict]:
# under SECRET_KEY, but a backup is exactly the thing copied off-site — it must
# not carry the key that writes to the place it is copied to. A restore leaves
# the destination configured and asks for the secret again.
_SETTINGS_NOT_EXPORTED = frozenset({"backup_s3_secret"})
#
# `backup_runs` is the scheduled backup's own run history: a record of what
# THIS instance sent where. Restored onto another instance it would claim
# backups that instance never made.
_SETTINGS_NOT_EXPORTED = frozenset({"backup_s3_secret", "backup_runs"})
def _setting_rows(rows) -> list[dict]: