feat(admin): say per account how fresh the off-host copy is

The nightly off-host job reports counts — uploaded, failed, total — and a
count cannot name the account it is failing for. A cohort where one object
has been refused every night for a month reads as "99 uploaded, 1 failed"
each time, which looks like weather rather than like an account with no
disaster-recovery copy at all.

The worker now writes a ledger row when an object lands (account, instant,
size), and Admin → Backups gains a second card that rates every account
against the nightly schedule: fresh inside one run, due when one run
produced nothing, stale when two did not, never when no object has ever
landed. On a host without BACKUP_S3_* the card says off-host backup is not
configured and stops there.

The verdicts are computed from the ledger, never from a bucket listing: the
worker's grant is deliberately PutObject + GetObject, and an admin page must
not put the operator's credentials on its render path to state a number the
database already holds.
This commit is contained in:
Marc-André Bombeck committed 2026-09-10 09:27:58 +02:00
1 parent 348d9e0739
commit 1c10bbbde8
23 files changed
+797 -10

No files matched your search

+20
View File
@@ -215,6 +215,26 @@ A run where SOME account got a copy still succeeds, with the rest counted in
`offhost_backup_failed`. Failing the whole queue over one account's object
would re-upload everybody's on every retry.
### Which account has no copy
The counts above say how many accounts were uploaded, never which. An account
whose object has failed every night for a month reads as `99 uploaded, 1
failed` each time, which looks like weather.
**Admin console → Backups → Off-host copies** answers it per account: when this
host last put that account's encrypted copy in the bucket, how big it was, and
how that compares with the nightly schedule. **Fresh** is inside one run,
**due** means one run produced nothing for that account, **stale** means two
did not, and **never** means no object has ever landed. On a host without
`BACKUP_S3_*` the card says off-host backup is not configured and stops there.
The verdicts come from a ledger the worker writes when the object lands
(`offhost_backup_state`), not from a listing of the bucket. Keep the worker's
grant as documented — PutObject, GetObject, AbortMultipartUpload — and the card
still tells the truth. A row that goes stale while the run reports success is
that account's object being refused, and the reason is in the run's
`offhost_backup_failures` meta.
## The weekly in-database backup (`data-backup`)
Separate from the off-host job above, and easy to confuse with it. A second