feat(backups): GET /backups et POST /backups/pull (sauvegardes PostgreSQL) #58
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#58
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
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?
Routes de suivi des sauvegardes de la grappe PostgreSQL du VPS (maximus/la-compagnie-maximus#220) : lecture du
status.jsonécrit par le script du VPS et du dernier tirage du Lenovo, dépôt de ce tirage. Même modèle que/claude-usage: fraîcheur décidée par le serveur.Contrat
status.json(schemaVersion 1)Écrit par le script du VPS, en dernier et de façon atomique (fichier temporaire dans le même
dossier puis
rename, mode0644), dans/var/backups/postgres-status/status.json:okglobal : vrai seulement si chaque base, les globals et le miroir sontoket sidatabases.length === expectedCount(nombre de bases non modèles lu danspg_database)."ok": falseet"error"pris dans une liste fermée de codes :dump_failed,dump_empty,restore_list_failed,invalid_name,encrypt_failed,mirror_upload_failed,mirror_purge_failed. Jamais de stderr brut.POST /backups/pull, jetonBACKUPS_INGEST_TOKEN, corps{ "pulledAt": "<ISO>", "files": <entier>, "ok": true|false, "error"?: <code> }, codespull_failed,rotation_failed. Le serveur posereceivedAtlui-même.GET /backups(jeton de lecture) rend{ "vps": {...}|null, "lenovo": {...}|null }, chaque blocfiltré par liste blanche (comme
sanitizeClaudeUsage) et enrichi par le serveur deageSeconds,staleetstaleAfterSeconds(BACKUPS_STALE_SECONDS, 36 h par défaut ; âge du VPSsur
generatedAt, âge du Lenovo surreceivedAt).{ "status": "no_data" }si les deux manquent.La carte
/adminet/warmuplisentstaletel quel, sans recalcul.Travail
Issue dans
maximus/vps-health-api.GET /backups(jeton de lectureHEALTH_TOKEN) : litBACKUPS_STATUS_PATH(défaut
/data/backups-status/status.json) et le dernier dépôt du Lenovo (BACKUPS_PULL_PATH,défaut
/data/backups-pull/pull.json) ; filtre par liste blanche ; calculeageSeconds,stale,staleAfterSeconds(BACKUPS_STALE_SECONDS, défaut 129600) ; rend le contrat ci-dessus.POST /backups/pull: descripteur dansROUTES, tokenKindbackups-ingestdansTOKEN_KINDS,jeton
BACKUPS_INGEST_TOKEN; absent = 503 « BACKUPS_INGEST_TOKEN not configured » ; corps validé(champs, types, codes d'erreur de la liste, taille bornée comme
CLAUDE_USAGE_MAX_BODY_BYTES) ;receivedAtposé par le serveur ; écriture atomique.status.jsonillisible ouschemaVersioninconnue donnevps: nulletvpsError, jamais un 500.__tests__; README (variables, montages, tableau d'authentification).Fichiers concernés
index.js(ROUTES, TOKEN_KINDS)backups.js(nouveau, sur le modèle declaude-usage.js)__tests__/(nouveau fichier de tests)README.mdCritères d'acceptation
GET /backups: contrat ci-dessus,ageSeconds/stale/staleAfterSecondscalculés par le serveur ;{status:"no_data"}si rien ;vps: null+vpsErrorsur fichier illisible ouschemaVersioninconnue, jamais de 500.POST /backups/pull: 503 nommé sans jeton, 401 avec un mauvais jeton, 400 sur un corps invalide (champ manquant, code d'erreur hors liste, taille dépassée), 200 et écriture atomique sinon ;receivedAtposé par le serveur, celui du client ignoré.status.jsonn'est pas renvoyé.__tests__/pour chaque cas ci-dessus ; README à jour (variables, montages, authentification).Review caveats
/claude-usage(« Staleness is decided here and only here »,receivedAtposé par le serveur). Résolu : intégré :ageSeconds/stalecalculés parGET /backups,receivedAtserveur.erroraurait recopié le stderr depg_dump/rclone(hôtes, clés) jusqu'au dashboard. Résolu : intégré : codes fermés, liste blanche côté API.expectedCount.<ROUTE>_INGEST_TOKEN, code de « route fermée » non précisé. Résolu : intégré :BACKUPS_INGEST_TOKEN,TOKEN_KINDS, 503 nommé.Décisions prises en planification
BACKUPS_INGEST_TOKEN, convention<ROUTE>_INGEST_TOKEN.BACKUPS_STALE_SECONDS, 36 h par défaut).status:blockedjusqu'au premier passage des sauvegardes (maximus/la-compagnie-maximus#220), puis milestoneplanned-*(décision Max).Spec source
spec-plan-infra-vps.mddansmaximus/la-compagnie-maximus(révisé par/review-specle 2026-10-10). Ce corps est auto-suffisant.Ajout au contrat
status.json(schemaVersion 1) de #220, merge par la PR maximus/la-compagnie-maximus#261 : un bloc racinerotation,{"ok": true}ou{"ok": false, "error": "rotation_failed"}quand un dossier date n a pas pu etre purge sur le VPS. Il entre dans leokglobal. A ajouter a la liste blanche du filtre et a la carte (une purge en echec annonce un disque qui se remplit).rotation_failedexistait deja dans les codes du depot du Lenovo.Deployed and verified in prod on 2026-10-12 (PR #60,
mainat3f0abc1).docker inspect):/var/backups/postgres-status -> /data/backups-status RW=false,/data/backups-pull -> /data/backups-pull RW=true(host dir created uid 1000, mode 755)./proc/mountsin the container agrees (ro/rw).BACKUPS_INGEST_TOKENset in Coolify; fingerprint identical in the container and in~/.config/maximus-pg-backup-pull.env(mode 600) on the workstation.maximus-pg-backup-pull.service:pull ok latest=2026-10-12 files=9,report ok status=204.pull.jsonwritten mode 600, uid 1000.vpsok,expectedCount8, 8 databases all ok, globals/mirror/rotation ok, retention 7/30,stale: false;lenovofiles: 9,ok: true, serverreceivedAt,stale: false.GET /claude-usage/history-> 200.