feat(backups): GET /backups and POST /backups/pull #60
No reviewers
Labels
No labels
autopilot:pending-human
source:analyste
source:defenseur
source:human
source:medic
status:approved
status:blocked
status:in-progress
status:needs-clarification
status:needs-fix
status:ready
status:review
status:triage
type:bug
type:feature
type:infra
type:refactor
type:schema
type:security
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: maximus/vps-health-api#60
Loading…
Reference in a new issue
No description provided.
Delete branch "issue-58-backups-routes"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Refs #58
Backup monitoring routes for the PostgreSQL cluster (la-compagnie-maximus#220), built on the
/claude-usagemodel: freshness is decided by the server only.What changes
GET /backups(read token):{ vps, lenovo, vpsError?, lenovoError? }.vpsis thestatus.jsonwritten bybackup-postgres.sh, rebuilt through a whitelist in the new pure modulebackups.js. It includes therotationblock from the 2026-10-10 issue comment.ok: falseis kept). Never coerced.lenovois the last pull report, re-validated on read.ageSeconds,staleandstaleAfterSecondsfrom the server (BACKUPS_STALE_SECONDS, 36 h). The VPS block is aged fromgeneratedAt, the workstation block from the server-stampedreceivedAt, never frompulledAt.nullblock plus an error code, never a 500, and the other block is still served.no_dataonly when both files are absent.POST /backups/pull: fourth token kindbackups-ingest, onBACKUPS_INGEST_TOKEN(503 naming the variable when it is unset).{ pulledAt, files, ok, error? }is validated strictly:erroris required iffokis false, and must bepull_failedorrotation_failed.receivedAt. The write is atomic, mode 600.writeClaudeUsage()becamewriteJsonAtomic(filePath, record), shared by both routes. Behaviour is unchanged: same temp name, and the existing claude-usage tests pass untouched.backups.jsis added to the DockerfileCOPY.Deviations from the issue text
readBody().run-pg-backup-pull.shtreats any 2xx as success.vpsError/lenovoErrortake one of four codes:unreadable,invalid_json,unsupported_schema,invalid_contract. A missing file gives anullblock with no error code.Tests
npm test: 865 passed (101 new in__tests__/backups.test.js;auth.test.js76 -> 84 with/backupsin the matrix;writes.test.js15 -> 18 with the third write surface).bash test-curl.sh: 57 passed (6 new socket cases). The count pinned inci.test.jsis updated.sh scripts/boot-from-copy.sh: PASS.pulledAt;errorraw;no_data;err.message;>=;ok: truewith an error;Post-merge checklist (hence
Refs, not a closing keyword)/var/backups/postgres-status->/data/backups-status:ro(directory, read-only), and/data/backups-pull->/data/backups-pull(read-write, owned by uid 1000, create it on the host first).BACKUPS_INGEST_TOKEN(is_runtime=true,is_buildtime=false).docker inspectthe mounts (RW=falseon backups-status,RW=trueon backups-pull), and check thatGET /backupsserves a realvpsblock, not just a 200.BACKUPS_API_URLandBACKUPS_INGEST_TOKENin~/.config/maximus-pg-backup-pull.env(mode 600). The next timer run must logreport ok status=204, andGET /backupsmust then show thelenovoblock./warmupare consumers in other repos.🤖 Generated with Claude Code
Track the PostgreSQL cluster backups (la-compagnie-maximus#220) the same way as /claude-usage: freshness is decided by the server. - GET /backups (read token) serves { vps, lenovo }: the status.json the VPS script writes, rebuilt through a whitelist (closed error codes, rotation block included), and the last workstation pull report. Each block gets ageSeconds, stale and staleAfterSeconds from the server (BACKUPS_STALE_SECONDS, 36 h): VPS on generatedAt, workstation on the server-stamped receivedAt. A broken file gives a null block plus vpsError/lenovoError, never a 500; no_data only when both are absent. - POST /backups/pull: fourth token kind, backups-ingest, on BACKUPS_INGEST_TOKEN (503 naming it when unset). Strict validation of { pulledAt, files, ok, error? }, server receivedAt, atomic write in mode 600 through writeJsonAtomic(), now shared with /claude-usage. Answers 204 and 413 like the two other ingest routes (the agent only checks for a 2xx), where the issue said 200 and 400. - backups.js joins the Dockerfile COPY line. Tests: backups.test.js (101 cases), auth/writes pins extended, 6 socket cases in test-curl.sh. Refs #58 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>Revue adversariale — PR #60
Verdict : APPROVE
Résumé. Les deux routes suivent fidèlement le modèle
/claude-usage: le routage passe avant l'authentification, le quatrième type de token passe par la tableTOKEN_KINDS, l'écriture est atomique en mode 600, la fraîcheur est décidée par le serveur, et une liste blanche jette les valeurs invalides sans jamais les forcer. Le contrat colle au vrai producteur, et chaque mutation essayée sauf une fait rougir la suite.Ce qui a été exécuté (copie hors dépôt de la tête
1b61924)npx vitest run: 865/865 verts.bash test-curl.shsur des ports isolés (3189/3188) : 57/57.sh scripts/boot-from-copy.sh(port 3187) : PASS.success(statut de commit).DATABASE_NAME_RE)nulljeté au lieu d'être gardé (putNullable)receivedAtdepull.jsonnon vérifié à la lectureisPlainObjectretiré surdatabasesok: falseaccepté sanserrorEISDIRtraité comme absentgeneratedAtfutur)CLAUDE_USAGE_PATHfilesaccepté comme n'importe quel nombreokretiré des composantsgeneratedAtretentionDays.offsitecopié brutLes fichiers ont été restaurés depuis les originaux (
diffvide) et la copie a été supprimée.Points vérifiés
la-compagnie-maximus/scripts/backup/backup-postgres.shécrit exactement les champs pris par la liste blanche (schemaVersion,generatedAtau formatdate -u +%Y-%m-%dT%H:%M:%SZ,ok,expectedCount,databases[]avecdumpedAtànullen cas d'échec,globals,mirror.uploadedAtànull,rotation,retentionDays.{local,offsite}). Ses huit codes d'erreur sont ceux deVPS_ERROR_CODES. L'entréeinvalid_nameporte un nom arbitraire : il est jeté ici, et l'entrée gardée.report()dansagent/pg-backup-pull/run-pg-backup-pull.sh(pulledAt,files,ok, pluserrorseulement quandokest faux, danspull_failed|rotation_failed) passe la validation stricte, et l'agent accepte le 204 (2??).BACKUPS_INGEST_TOKENn'ouvre que sa route, et le 503 nomme la variable.err.message, ni chemin, ni contenu.pull_refencloses(ref_action: none). LeRefs #58et la fermeture manuelle tiennent.feat(backups): ..., au format conventionnel.pushOversizedn'avale pas l'erreur de transport : un 413 remplacé par une socket coupée ferait rougir le test.Suggestions (non bloquantes)
retentionDays.offsite:__tests__/backups.test.js:281ne teste une valeur invalide que surlocal("7"). Copieroffsitebrut ne fait rougir aucun test. Le code est correct, l'ajout tient en une ligne (par exempleoffsite: -1dans le même cas). C'est une épingle sur du code correct : elle ne justifie pas un tour de plus, à glisser dans une PR voisine.CLAUDE.md, la puce Coolify (is_runtime=true, is_buildtime=false) ne nomme encore queHEALTH_TOKENetHOSTS_INGEST_TOKEN. La section Config couvre déjà les deux nouveaux tokens : un rappel en une ligne suffirait.La checklist post-merge du corps (montages, token, redeploy,
docker inspect, env du poste) est complète et cohérente avec la section Montages deCLAUDE.md.