feat(claude-usage): conserver l’historique des jours clos au-delà de la fenêtre de 30 jours #55
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#55
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?
Contexte
Demande de Max du 2026-10-05, apres la mise en service du suivi de consommation Claude (la-compagnie-maximus#208). Analyse du 2026-10-08 sur
main@f694677. Premisses verifiees dans le code :CLAUDE_USAGE_PATH, que chaquePOST /claude-usageremplace (writeClaudeUsage,index.js) ;WINDOW_DAYS = 30,aggregate.js), le contrat en accepte 31 (MAX_DAYS,claude-usage.js), les plus recents en dernier ;RETENTION_DAYS,state.js), pour la vitesse du scan, pas pour l'historique.Un jour qui sort de la fenetre est perdu pour le dashboard. Les transcripts du ThinkPad remontent aujourd'hui au 2026-08-18 (mtime du plus ancien), et Claude Code les efface avec l'age : le rattrapage presse.
Decisions (Max, 2026-10-08)
days(input,output,cacheCreation,cacheRead,requests). Pas de projets par jour (demanderait unschemaVersion: 2, hors lot).GET /claude-usage/history, token de lecture.Travail a faire
1. Module pur
claude-usage-history.js(racine, dans l'image)history/YYYY-MM.json:{ "schemaVersion": 1, "month": "YYYY-MM", "days": [ { "date", "models" } ] }, jours tries du plus ancien au plus recent, meme forme qu'une entree dedaysdu snapshot.mergeDays(existing, days)pur : remplace ou ajoute chaque date, rend le fichier mensuel trie. Les dates sont rangees par mois de leur chaineYYYY-MM-DD, jamais par un calcul d'horloge.claude-usage.jsle nettoyeur d'un jour (aujourd'hui interne asanitizeClaudeUsage) plutot que le dupliquer. Un jour invalide est jete, un fichier hors format est traite comme illisible.COPYdu Dockerfile, avec un test comme celui qui existe pourclaude-usage.js;requireen colonne 0 (epingle deci.test.js).2. Ecriture dans
POST /claude-usage<dossier de CLAUDE_USAGE_PATH>/history/tous les jours du snapshot sanitise sauf le dernier. Un ou deux fichiers mensuels touches par POST.renameSync), patron dewriteClaudeUsage.[claude-usage] history ..., sans contenu). Un fichier mensuel illisible n'est pas ecrase (il garderait des jours sortis de la fenetre) : ce mois est saute et journalise, les autres mois s'ecrivent.__tests__/writes.test.js: l'ecriture soushistory/doit rester dans le dossier autorise pourPOST /claude-usage(verifier queviolations()accepte un sous-dossier ; sinon l'elargir pour ce seul cas).3. Route
GET /claude-usage/historyROUTES,tokenKind: "read"(aucunifdanscheckAuth()).?from=YYYY-MM&to=YYYY-MM, les deux optionnels. Par defaut, les 12 derniers mois jusqu'au mois du dernier jour archive. 400 si un format est invalide, sifrom > to, ou si l'ecart depasse 36 mois.{ from, to, days: [...] }, jours tries ; un mois absent est simplement vide ; un mois illisible est saute et journalise ; aucun fichier ->days: [].auth.test.js, dont les cas404 (not 401)), a la liste des routes de lecture dewrites.test.jset atest-curl.sh.4. Rattrapage ponctuel (
agent/claude-usage/backfill-history.js)aggregate()sur des fenetres passees (parametretodayrecule par pas de 30 jours, sans jours figes) jusqu'au plus ancien transcript ; sortie = un fichier JSON{ days: [...] }au format de l'archive. Ne lit jamais~/.claude/.credentials.json, n'envoie rien sur le reseau.docker exec <conteneur> node claude-usage-history.js --import <fichier>), avec la regle inverse de la regle en ligne : une date deja archivee gagne (l'archive vivante fait foi), seules les dates absentes sont ajoutees.agent/README.md.5. Docs
CLAUDE.md: endpoints (ecriture d'archive dansPOST /claude-usage, nouvelle route), montage (/data/claude-usage/history/sur le meme montage), gotcha (pas de balayage des.tmpdanshistory/, comme pour le snapshot), compteurs de tests.Fichiers concernes
claude-usage-history.js(nouveau),claude-usage.js(export du nettoyeur de jour),index.js(POST, route,ROUTES),Dockerfile(COPY)agent/claude-usage/backfill-history.js(nouveau),agent/README.md__tests__/claude-usage.test.js(ou un nouveauclaude-usage-history.test.js),__tests__/auth.test.js,__tests__/writes.test.js,__tests__/ci.test.jssi besoin,__tests__/agent-claude-usage.test.js,test-curl.sh,CLAUDE.mdSurface de test
history/non inscriptible : 204, snapshot ecrit..tmplaisse.from > to, formats invalides, mois absents ou illisibles, relecture sanitisee (champ inconnu jete,__proto__).Criteres d'acceptation
npm test,bash test-curl.sh,sh scripts/boot-from-copy.shverts, CI verteschemaVersion: 1; l'agent du cron n'est pas modifiewrites.test.jsvert, avec l'archive comme seule nouvelle surface d'ecritureChecklist post-merge (a confirmer, l'issue reste ouverte jusque-la)
GET /claude-usage/historyavec du contenu apres le premier POST.backfill-history.jssur le ThinkPad, scp, import dans le conteneur, verification par la route./data/claude-usage/(et plus largement/data/hosts,/data/defenseurs) est couvert par une sauvegarde du VPS. Non verifie a l'analyse : SSH en attente de reauthentification Tailscale, et aucun document du parc (la-compagnie-maximus/docs/) ne decrit de sauvegarde de/data/. S'il n'y en a pas, poser le geste et le documenter danscoolify-ops.md.Complexite
Complex : deux nouveaux modules, une surface d'ecriture qui s'etend, une route de lecture, une procedure d'import. Trois commits suggeres : module + ecriture, route, rattrapage.
Liens
la-compagnie-maximus#206, #207, #208 ; spec
spec-plan-suivi-claude.md(la-compagnie-maximus).PR #57 merged on 2026-10-09 (main @
c5ded9e). The issue stays open until the post-merge checklist is done:GET /claude-usage/historyshould have content after the next cron beat.node agent/claude-usage/backfill-history.js --out ~/claude-usage-backfill.json. Then scp it to the VPS and rundocker exec -i <ctr> node claude-usage-history.js --import - < file, and check the result through the route (procedure inagent/README.md). A trial run on 2026-10-08 produced 50 days, 2026-08-18 to 2026-10-06, with read_errors=0. The transcripts age out, so this is time-sensitive./data/claude-usage/(and/data/hosts,/data/defenseurs) is covered; if not, set it up and document it incoolify-ops.md.Steps 1 and 2 done on 2026-10-12.
GET /claude-usage/historyserved 29 closed days (2026-09-13 to 2026-10-11) before the import.backfill-history.jsproduced 29 days, 2026-09-12 to 2026-10-10, read_errors=0. Import:29 day(s) read, 1 added, 28 already archived, 2 month(s) written, exit 0. AfterwardsGET /claude-usage/history?from=2026-08&to=2026-10returns 30 days, 2026-09-12 to 2026-10-11. Both copies of the import file are deleted.Data loss: the 2026-10-08 trial reached back to 2026-08-18, but its output was not kept. By today the oldest local transcript dates from 2026-09-12 (Claude Code deletes transcripts after 30 days by default), so 2026-08-18 to 2026-09-11 (25 days) are gone for good. From now on, the live archive keeps every closed day.
Remaining: step 3 (VPS backup coverage of
/data/claude-usage,/data/hosts,/data/defenseurs), then close.Step 3 done on 2026-10-12. No file backup existed on the VPS (only
backup-postgres, which touched no/datadir)./data/claude-usagenow rides the nightly PostgreSQL backup chain (la-compagnie-maximus#295): tar, age-encrypted into the outbox, MinIO mirror 7 days, Lenovo pull 30 days. First run by hand:ok: true, archive 2466 bytes holdinghistory/2026-09.jsonandhistory/2026-10.json./data/hosts,/data/backups-pulland/data/defenseursare rewritten on every pass of their agent, so they stay out. Documented inla-compagnie-maximus/docs/postgres-ops.md(Backup section, with the restore), since the chain lives there. Follow-up for/admin: #61.All four steps done, closing.