feat(backup): admin-tunable retention + BACKUP_KEY as a Komodo secret
Retention (keep-last / keep-daily-days) is operational policy the on-site admin should tune, not a server env var requiring a redeploy -- same reasoning that moved the target directory to the UI. - Migration 0017: site_config.backup_keep_last + backup_keep_daily_days (nullable; null = code default 7 / 30 per field). - BackupService reads retention fresh each run; status() exposes keepLast + keepDailyDays. DEFAULT_BACKUP_RETENTION is now a pure code default (env reads gone). - PUT /api/backup/config accepts keepLast / keepDailyDays (non-negative int, or null to reset to default; 400 on negative). - UI: two retention fields on the Backup config card; one Save covers target + retention. i18n sq + en. BACKUP_KEY wired into Komodo: - komodo/resources.toml: BACKUP_KEY=[[park_buzi_backup_key]] (per-booth secret, alongside JWT / signing keys). - komodo/.env.komodo.example: documents it as the ONLY backup env var -- escrow it offsite alongside EVENT_SIGNING_KEY (recovery needs both); target + retention are admin-chosen in the UI / DB, not env. Server .env.example trimmed to just BACKUP_KEY. Also carries the small in-progress setup-intro i18n copy trim. Tests: 218 server tests green, incl. retention persist / reset-to-default / reject- negative and the updated status shape. Migration applies cleanly (needed a statement-breakpoint between the two ALTERs). Wiki backup-recovery updated. Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
This commit is contained in:
@@ -133,12 +133,15 @@ timer + the manual route**. What landed:
|
||||
**keep-last-N + one-per-day-within-N-days** (`pruneOldBackups`). Tested: round-trip decrypts to a
|
||||
**byte-identical, queryable DB**; a flipped byte or wrong key **fails GCM auth**; short key rejected;
|
||||
scratch plaintext always removed.
|
||||
- **`backup-service.ts`** — the **target directory is admin-chosen** (`site_config.backup_target_dir`,
|
||||
migration 0016) and read **fresh each run**, so changing it in the UI takes effect with no restart.
|
||||
Only the **encryption key stays an env/Komodo secret** (`BACKUP_KEY`) — a key must never live in the
|
||||
DB it backs up. Retention knobs (`BACKUP_KEEP_LAST`, `BACKUP_KEEP_DAILY_DAYS`) stay env. The service
|
||||
**serializes** concurrent runs (single in-flight guard) and records last-success / last-error;
|
||||
`status()` exposes `targetDir` + `keyPresent` so the UI distinguishes "no target" from "no key".
|
||||
- **`backup-service.ts`** — the **target directory AND retention are admin-chosen** in the UI
|
||||
(`site_config.backup_target_dir`, migration 0016; `backup_keep_last` + `backup_keep_daily_days`,
|
||||
migration 0017) and read **fresh each run**, so changing them takes effect with no restart. Retention
|
||||
columns are nullable → fall back to the code default (keep-last 7, keep-daily 30) per field. The
|
||||
**encryption key is the ONLY backup env/Komodo secret** (`BACKUP_KEY`) — a key must never live in the
|
||||
DB it backs up; target+retention are operational policy, not secrets. The service **serializes**
|
||||
concurrent runs (single in-flight guard) and records last-success / last-error; `status()` exposes
|
||||
`targetDir`, `keepLast`, `keepDailyDays` + `keyPresent` so the UI distinguishes "no target" from
|
||||
"no key".
|
||||
- **`routes/backup.ts`** — `GET /api/backup/status` (`backup:read`); `PUT /api/backup/config` to set/
|
||||
clear the target (`backup:update`); `POST /api/backup/test` to probe a candidate path server-side —
|
||||
exists / is-a-dir / writable (`backup:update`); `POST /api/backup/run` (`backup:create`), a clean
|
||||
@@ -146,9 +149,12 @@ timer + the manual route**. What landed:
|
||||
(`backup:read/update/create`) in `@parking/shared`. **No restore route** — out-of-band by design.
|
||||
- **`apps/web/src/BackupSettings.tsx`** — a Setup → **Backup** tab (gated `backup:read`): an editable
|
||||
**target-path field** with a **Test target** probe (localized ok/missing/not-a-dir/not-writable),
|
||||
**Save**, the status panel (config state, last-run size/pruned/error, a distinct amber **missing
|
||||
BACKUP_KEY** warning), a **Back up now** button, and the restore-is-out-of-band note. Full i18n
|
||||
(sq + en).
|
||||
**retention fields** (keep-last / keep-daily-days), one **Save**, the status panel (config state,
|
||||
last-run size/pruned/error, a distinct amber **missing BACKUP_KEY** warning), a **Back up now**
|
||||
button, and the restore-is-out-of-band note. Full i18n (sq + en).
|
||||
- **Komodo wiring.** `BACKUP_KEY` is a **per-booth Komodo secret** (`[[park_buzi_backup_key]]` in
|
||||
`komodo/resources.toml`; documented in `komodo/.env.komodo.example`), escrowed offsite alongside
|
||||
`EVENT_SIGNING_KEY`. It is the *only* backup env var — target + retention are in the DB.
|
||||
- **`server.ts`** — an **unref'd daily timer** (`backupService.runScheduled`), a **no-op until
|
||||
configured**, and **deliberately NOT run at startup** (a just-power-cut booth shouldn't write to a
|
||||
possibly-unmounted disk; the daily cadence + the manual button cover it).
|
||||
|
||||
+14
@@ -1948,3 +1948,17 @@ status panel (distinct amber "BACKUP_KEY missing" warning) + Back-up-now + resto
|
||||
i18n sq+en; nav.backup. Verified live with Playwright: typed path → Test "writable" → Save persisted →
|
||||
status reflects it + key-missing warning shown. build/lint/test green (whole monorepo). Updated
|
||||
[[backup-recovery]] as-built + [[open-questions]] #5.
|
||||
|
||||
## [2026-06-29] feat | Backup retention admin-tunable + BACKUP_KEY wired into Komodo
|
||||
Same reasoning as the target dir: backup retention is operational policy the on-site admin tunes, not a
|
||||
server env var requiring a redeploy. Moved BACKUP_KEEP_LAST/BACKUP_KEEP_DAILY_DAYS env → site_config
|
||||
(migration 0017: backup_keep_last, backup_keep_daily_days, both nullable → code default 7/30 per field).
|
||||
BackupService reads retention fresh each run; status() now exposes keepLast/keepDailyDays. PUT
|
||||
/api/backup/config extended to accept keepLast/keepDailyDays (non-negative int or null=reset-to-default,
|
||||
400 on negative). UI: two retention number fields on the Backup config card, one Save covers target +
|
||||
retention; i18n sq+en. DEFAULT_BACKUP_RETENTION is now a pure code default (env reads dropped). Komodo:
|
||||
BACKUP_KEY wired as a per-booth secret ([[park_buzi_backup_key]] in komodo/resources.toml; documented in
|
||||
komodo/.env.komodo.example as the ONLY backup env var — target+retention are UI/DB). Server .env.example
|
||||
trimmed to just BACKUP_KEY. build/lint/test green (218 server tests, incl. retention persist/reset/negative
|
||||
+ updated status shape). NOTE: dev API process was down after this round (live process, not code) — verified
|
||||
via the full test harness, not a live click-through this time. Updated [[backup-recovery]] as-built.
|
||||
|
||||
Reference in New Issue
Block a user