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:
@@ -50,6 +50,8 @@ describe("GET /api/backup/status", () => {
|
||||
expect(body).toMatchObject({
|
||||
configured: false,
|
||||
targetDir: null,
|
||||
keepLast: 7, // code defaults surfaced when unset
|
||||
keepDailyDays: 30,
|
||||
running: false,
|
||||
lastSuccessAt: null,
|
||||
lastError: null,
|
||||
@@ -99,6 +101,37 @@ describe("PUT /api/backup/config — admin-chosen target", () => {
|
||||
});
|
||||
expect(clear.json().targetDir).toBeNull();
|
||||
});
|
||||
|
||||
it("persists retention and resets to defaults on null", async () => {
|
||||
const { username, password } = await seedUser(db, { username: "boss", roleId: "admin" });
|
||||
const { cookie, csrf } = await login(app, username, password);
|
||||
|
||||
const set = await app.inject({
|
||||
method: "PUT", url: "/api/backup/config",
|
||||
headers: { cookie, "x-csrf-token": csrf },
|
||||
payload: { keepLast: 3, keepDailyDays: 14 },
|
||||
});
|
||||
expect(set.json()).toMatchObject({ keepLast: 3, keepDailyDays: 14 });
|
||||
|
||||
// null resets to the code default.
|
||||
const reset = await app.inject({
|
||||
method: "PUT", url: "/api/backup/config",
|
||||
headers: { cookie, "x-csrf-token": csrf },
|
||||
payload: { keepLast: null, keepDailyDays: null },
|
||||
});
|
||||
expect(reset.json()).toMatchObject({ keepLast: 7, keepDailyDays: 30 });
|
||||
});
|
||||
|
||||
it("rejects a negative retention value (400)", async () => {
|
||||
const { username, password } = await seedUser(db, { username: "boss", roleId: "admin" });
|
||||
const { cookie, csrf } = await login(app, username, password);
|
||||
const res = await app.inject({
|
||||
method: "PUT", url: "/api/backup/config",
|
||||
headers: { cookie, "x-csrf-token": csrf },
|
||||
payload: { keepLast: -1 },
|
||||
});
|
||||
expect(res.statusCode).toBe(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe("POST /api/backup/test — path probe", () => {
|
||||
|
||||
Reference in New Issue
Block a user