feat(backups): pass the files[] block of status.json through GET /backups #61

Closed
opened 2026-10-12 23:52:12 +00:00 by maximus · 1 comment
Owner

Contexte

Since la-compagnie-maximus#295 (installed on the VPS 2026-10-12, vps-health-api#55), backup-postgres.sh archives the host directories listed in FILE_SOURCES (today claude-usage=/data/claude-usage) and writes one entry per directory in a new files[] block of status.json:

"files": [{ "name": "claude-usage", "sizeBytes": 2466, "archivedAt": "2026-10-12T23:52:38Z", "ok": true }]

sanitizeBackupsStatus() (backups.js) rebuilds the status from a whitelist, so it drops files silently today. The global ok already accounts for the files (the script folds them in), so a failed archive turns the status red, but GET /backups gives no cause.

Scope decided on 2026-10-12 (Max): this issue covers vps-health-api only. Showing the block on /admin belongs to la-compagnie-maximus#254 (the card does not exist yet), noted there in a comment.

Travail

  • backups.js: FILE_NAME_RE = /^[a-z0-9_-]{1,63}$/ (the script's own pattern, plus the 63 bound), a separate FILE_ERROR_CODES closed list (source_missing, archive_failed, encrypt_failed, invalid_name, exactly what the script can write; decided 2026-10-12), MAX_FILES bound (16), sanitizeFile() on the model of sanitizeDatabase() (name, sizeBytes, archivedAt nullable via putNullable, ok, error), and status.files filled like status.databases (only when the payload has an array). Export MAX_FILES and FILE_ERROR_CODES.
  • __tests__/backups.test.js: add files to validStatus() (the script's real output, so the identity and idempotence tests cover it), then cases on the model of databases: unknown field and __proto__ dropped inside an entry; archivedAt: null kept; invalid sizeBytes / archivedAt / ok dropped, never forced; each of the four codes kept; a database-only code (dump_failed) dropped on a file entry, and a file-only code (source_missing) dropped on a database entry; a name outside the regex dropped with the entry kept; bound at MAX_FILES; non-object entries filtered; files absent (older script) -> no files key, no vpsError; files not an array -> dropped, the rest served.
  • Through the route: GET /backups serves vps.files from a fixture file (one case in the GET /backups block).
  • CLAUDE.md: GET /backups paragraph (the files block, its regex, codes, bound), the test count line for backups.test.js and the total.

Fichiers concernés

  • backups.js (sanitizer, constants, exports)
  • __tests__/backups.test.js
  • CLAUDE.md

No change to the routes, auth, the Dockerfile COPY (backups.js already in it) nor test-curl.sh (pure sanitizer logic, nothing a real socket adds).

Surface de test

  • Existing: __tests__/backups.test.js, block sanitizeBackupsStatus — the whitelist (around line 202, the model for every new case) and GET /backups (around line 581).
  • writes.test.js and auth.test.js: unaffected (no new route, no new write).
  • Regression: the identity test with files in validStatus() pins the real script output.

Critères d'acceptation

  • GET /backups returns vps.files as written by the script, unchanged.
  • Every value outside the contract is dropped (never forced), at entry and field level, with the closed list specific to files.
  • A status.json without files keeps working (no vpsError, no files key).
  • npm test, bash test-curl.sh, sh scripts/boot-from-copy.sh green; mutation check on the regex, the code list and the bound.
  • Redeploy, then GET /backups in prod shows vps.files[0].name == "claude-usage".

Complexité

Simple: one pure function and one test block, on an existing pattern.

## Contexte Since la-compagnie-maximus#295 (installed on the VPS 2026-10-12, vps-health-api#55), `backup-postgres.sh` archives the host directories listed in `FILE_SOURCES` (today `claude-usage=/data/claude-usage`) and writes one entry per directory in a new `files[]` block of `status.json`: ```json "files": [{ "name": "claude-usage", "sizeBytes": 2466, "archivedAt": "2026-10-12T23:52:38Z", "ok": true }] ``` `sanitizeBackupsStatus()` (`backups.js`) rebuilds the status from a whitelist, so it drops `files` silently today. The global `ok` already accounts for the files (the script folds them in), so a failed archive turns the status red, but `GET /backups` gives no cause. Scope decided on 2026-10-12 (Max): this issue covers vps-health-api only. Showing the block on `/admin` belongs to la-compagnie-maximus#254 (the card does not exist yet), noted there in a comment. ## Travail - [ ] `backups.js`: `FILE_NAME_RE = /^[a-z0-9_-]{1,63}$/` (the script's own pattern, plus the 63 bound), a separate `FILE_ERROR_CODES` closed list (`source_missing`, `archive_failed`, `encrypt_failed`, `invalid_name`, exactly what the script can write; decided 2026-10-12), `MAX_FILES` bound (16), `sanitizeFile()` on the model of `sanitizeDatabase()` (`name`, `sizeBytes`, `archivedAt` nullable via `putNullable`, `ok`, `error`), and `status.files` filled like `status.databases` (only when the payload has an array). Export `MAX_FILES` and `FILE_ERROR_CODES`. - [ ] `__tests__/backups.test.js`: add `files` to `validStatus()` (the script's real output, so the identity and idempotence tests cover it), then cases on the model of `databases`: unknown field and `__proto__` dropped inside an entry; `archivedAt: null` kept; invalid `sizeBytes` / `archivedAt` / `ok` dropped, never forced; each of the four codes kept; a database-only code (`dump_failed`) dropped on a file entry, and a file-only code (`source_missing`) dropped on a database entry; a name outside the regex dropped with the entry kept; bound at `MAX_FILES`; non-object entries filtered; `files` absent (older script) -> no `files` key, no `vpsError`; `files` not an array -> dropped, the rest served. - [ ] Through the route: `GET /backups` serves `vps.files` from a fixture file (one case in the `GET /backups` block). - [ ] `CLAUDE.md`: `GET /backups` paragraph (the `files` block, its regex, codes, bound), the test count line for `backups.test.js` and the total. ## Fichiers concernés - `backups.js` (sanitizer, constants, exports) - `__tests__/backups.test.js` - `CLAUDE.md` No change to the routes, auth, the Dockerfile `COPY` (`backups.js` already in it) nor `test-curl.sh` (pure sanitizer logic, nothing a real socket adds). ## Surface de test - Existing: `__tests__/backups.test.js`, block `sanitizeBackupsStatus — the whitelist` (around line 202, the model for every new case) and `GET /backups` (around line 581). - `writes.test.js` and `auth.test.js`: unaffected (no new route, no new write). - Regression: the identity test with `files` in `validStatus()` pins the real script output. ## Critères d'acceptation - [ ] `GET /backups` returns `vps.files` as written by the script, unchanged. - [ ] Every value outside the contract is dropped (never forced), at entry and field level, with the closed list specific to files. - [ ] A `status.json` without `files` keeps working (no `vpsError`, no `files` key). - [ ] `npm test`, `bash test-curl.sh`, `sh scripts/boot-from-copy.sh` green; mutation check on the regex, the code list and the bound. - [ ] Redeploy, then `GET /backups` in prod shows `vps.files[0].name == "claude-usage"`. ## Complexité Simple: one pure function and one test block, on an existing pattern.
maximus added the
status:ready
type:feature
source:human
labels 2026-10-13 00:01:00 +00:00
maximus added
status:review
and removed
status:ready
labels 2026-10-13 00:04:19 +00:00
maximus added
status:approved
and removed
status:review
labels 2026-10-13 00:08:02 +00:00
Author
Owner

Done on 2026-10-12.

  • PR #62 merged (fast-forward, main @ 9fa1f39), review APPROVE; suggestion 1 (wording of the shared codes in CLAUDE.md) applied in 9fa1f39, suggestion 2 noted as harmless.
  • Coolify redeploy lw8skook4c80o0kgcg0gkkc0: finished.
  • Mounts after redeploy: RW=false on both Defenseurs mounts and /data/backups-status, RW=true on /data/hosts, /data/claude-usage, /data/backups-pull.
  • Prod GET /backups: vps.ok: true, no vpsError, vps.files = [{ name: "claude-usage", sizeBytes: 2466, archivedAt: "2026-10-12T23:52:38Z", ok: true }].

Display on /admin: la-compagnie-maximus#254. Closing.

Done on 2026-10-12. - PR #62 merged (fast-forward, `main` @ `9fa1f39`), review APPROVE; suggestion 1 (wording of the shared codes in `CLAUDE.md`) applied in `9fa1f39`, suggestion 2 noted as harmless. - Coolify redeploy `lw8skook4c80o0kgcg0gkkc0`: finished. - Mounts after redeploy: `RW=false` on both Defenseurs mounts and `/data/backups-status`, `RW=true` on `/data/hosts`, `/data/claude-usage`, `/data/backups-pull`. - Prod `GET /backups`: `vps.ok: true`, no `vpsError`, `vps.files = [{ name: "claude-usage", sizeBytes: 2466, archivedAt: "2026-10-12T23:52:38Z", ok: true }]`. Display on `/admin`: la-compagnie-maximus#254. Closing.
maximus removed the
status:approved
label 2026-10-13 00:20:52 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: maximus/vps-health-api#61
No description provided.