feat(backup): daily backups to an S3 bucket, Settings → Backups, and the guide (#5673, #5675)
CI / lint (push) Successful in 2s
CI / integration (push) Failing after 1m15s
CI / unit (push) Failing after 1m28s
CI / publish (push) Skipped

#5673 "S3 client and the backup job: settings, schedule, history, metrics":
- steward/backup/s3.py: SigV4 over httpx, path-style; put and list only
  (an append-only key is enough; retention is the bucket's). Signing is held
  to AWS's published examples.
- steward/backup/job.py: a 15-minute tick. The daily backup goes at the hour
  chosen on the reader's clock; a missed day is made up, and a failure is
  retried an hour later. History of the last 30 runs, audit
  (backup.made / backup.failed), and backup_age_secs / backup_bytes under
  "Steward itself", resource backup, for alert rules.
- Settings → Backups: verdict first, then the bucket (secret sealed; a
  blank box keeps it), the schedule, Test the bucket, Back up now, and the
  recent runs.

#5675 "Docs: backing up to Archives, and restoring": docs/core/backups.md.

Also: the audit page now words the Prometheus target actions from #5668,
which showed as codes, and a guard fails on any logged action without
wording.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-12 07:23:58 -04:00
co-authored by Claude Opus 5.5
parent 357bb6480d
commit 49799bd0b3
16 changed files with 1254 additions and 5 deletions
+2
View File
@@ -286,6 +286,8 @@ METRICS: dict[str, tuple[str, str]] = {
"db_roundtrip_ms": ("Database round trip", "a trivial query to Steward's database, connection included"),
"unwatched_secs": ("Time not watching", "how long Steward wasn't recording before this minute; 0 when unbroken"),
"cleanup_secs": ("Hourly cleanup took", "how long the last retention and rollup run took"),
"backup_age_secs": ("Newest backup's age", "time since Steward last sent a backup to its bucket"),
"backup_bytes": ("Backup size", "the size of the last backup sent"),
"open_fds": ("Open files", "files Steward itself has open"),
"open_fds_pct": ("Open files (% of limit)", "files Steward has open, against its limit"),
"scrape_ms": ("Scrape time", "how long the target took to hand over its metrics"),
+15
View File
@@ -176,6 +176,20 @@ DEFAULTS: dict[str, Any] = {
# so "Sunday 08:00" means 08:00 where they are (#5378).
"reports.schedule_tz": "",
"reports.last_sent_at": "",
# Steward's own backups (#5673), sent to an S3 bucket (Fabled Archives).
"backup.enabled": False,
"backup.s3_endpoint": "",
"backup.s3_bucket": "",
"backup.s3_region": "us-east-1",
"backup.s3_access_key": "",
"backup.s3_secret_key": "",
"backup.s3_verify_tls": True,
"backup.schedule_hour": 3, # an hour in backup.schedule_tz
"backup.schedule_tz": "",
"backup.last_success_at": "",
"backup.last_attempt_at": "",
# Newest first: {at, ok, by, name, size, secs, error}.
"backup.history": [],
# What the last hourly cleanup did (#5469), written by core/cleanup.py for
# Settings → System: {"at": ISO time, "secs": float, "removed": [[what, n]]}.
"cleanup.last_run": {},
@@ -194,6 +208,7 @@ SECRET_KEYS: set[str] = {
# A webhook URL carries its own credential (Discord, Slack and ntfy put the
# token in the path).
"webhook.url",
"backup.s3_secret_key",
}