feat(claude-usage): conserver l’historique des jours clos au-delà de la fenêtre de 30 jours #55

Closed
opened 2026-10-05 23:12:56 +00:00 by maximus · 3 comments
Owner

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 :

  • le serveur ne garde qu'un fichier, CLAUDE_USAGE_PATH, que chaque POST /claude-usage remplace (writeClaudeUsage, index.js) ;
  • le snapshot couvre 30 jours Toronto (WINDOW_DAYS = 30, aggregate.js), le contrat en accepte 31 (MAX_DAYS, claude-usage.js), les plus recents en dernier ;
  • le cache de l'agent garde 35 jours (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)

Sujet Decision
Regle d'archivage Dernier snapshot gagne : chaque POST valide reecrit dans l'archive tous les jours qu'il porte, sauf le dernier (jour en cours). Un jour qui sort de la fenetre garde sa derniere valeur, celle que l'agent a figee a J+2 (complete depuis #54). Ecarte : l'ecriture unique proposee a l'origine, qui figerait pour toujours un jour archive depuis un passage incomplet (erreur de lecture, jour encore ouvert cote agent).
Contenu par jour Totaux par modele seulement, meme forme que days (input, output, cacheCreation, cacheRead, requests). Pas de projets par jour (demanderait un schemaVersion: 2, hors lot).
Conservation Illimitee (environ 1-2 Ko par jour), aucune purge.
Lecture Nouvelle route GET /claude-usage/history, token de lecture.
Rattrapage Script ponctuel dans ce lot, sans nouvelle route d'ecriture.
Sauvegarde VPS Etape de ce lot (checklist post-merge). L'issue reste ouverte tant qu'elle n'est pas verifiee.

Travail a faire

1. Module pur claude-usage-history.js (racine, dans l'image)

  • Format d'un fichier mensuel 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 de days du snapshot.
  • mergeDays(existing, days) pur : remplace ou ajoute chaque date, rend le fichier mensuel trie. Les dates sont rangees par mois de leur chaine YYYY-MM-DD, jamais par un calcul d'horloge.
  • Relecture d'un fichier mensuel par la meme liste blanche que le snapshot : exporter de claude-usage.js le nettoyeur d'un jour (aujourd'hui interne a sanitizeClaudeUsage) plutot que le dupliquer. Un jour invalide est jete, un fichier hors format est traite comme illisible.
  • Ajouter le module a la ligne COPY du Dockerfile, avec un test comme celui qui existe pour claude-usage.js ; require en colonne 0 (epingle de ci.test.js).

2. Ecriture dans POST /claude-usage

  • Apres l'ecriture du snapshot (donnee principale d'abord), fusionner dans <dossier de CLAUDE_USAGE_PATH>/history/ tous les jours du snapshot sanitise sauf le dernier. Un ou deux fichiers mensuels touches par POST.
  • Ecriture atomique de chaque fichier mensuel (temporaire dans le meme dossier, mode 600, renameSync), patron de writeClaudeUsage.
  • Un echec d'archivage ne change pas la reponse : 204, erreur journalisee ([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 sous history/ doit rester dans le dossier autorise pour POST /claude-usage (verifier que violations() accepte un sous-dossier ; sinon l'elargir pour ce seul cas).

3. Route GET /claude-usage/history

  • Entree dans la table ROUTES, tokenKind: "read" (aucun if dans checkAuth()).
  • ?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, si from > to, ou si l'ecart depasse 36 mois.
  • Reponse 200 { from, to, days: [...] }, jours tries ; un mois absent est simplement vide ; un mois illisible est saute et journalise ; aucun fichier -> days: [].
  • Ajouter la route aux matrices d'auth (auth.test.js, dont les cas 404 (not 401)), a la liste des routes de lecture de writes.test.js et a test-curl.sh.

4. Rattrapage ponctuel (agent/claude-usage/backfill-history.js)

  • Lance depuis le depot (pas copie dans l'installation du cron) : aggregate() sur des fenetres passees (parametre today recule 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.
  • Import cote VPS sans nouvelle route d'ecriture : depot du fichier par scp, puis fusion dans le conteneur par une commande du module (ex. 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.
  • Procedure ecrite dans agent/README.md.

5. Docs

  • CLAUDE.md : endpoints (ecriture d'archive dans POST /claude-usage, nouvelle route), montage (/data/claude-usage/history/ sur le meme montage), gotcha (pas de balayage des .tmp dans history/, 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 nouveau claude-usage-history.test.js), __tests__/auth.test.js, __tests__/writes.test.js, __tests__/ci.test.js si besoin, __tests__/agent-claude-usage.test.js, test-curl.sh, CLAUDE.md

Surface de test

  • POST archive tous les jours sauf le dernier ; un second POST identique laisse l'archive identique octet pour octet ; un POST qui porte un jour avec d'autres valeurs le remplace ; un jour sorti de la fenetre garde sa valeur.
  • Snapshot a cheval sur deux mois : deux fichiers, chaque jour dans le bon.
  • Fichier mensuel illisible : 204, snapshot ecrit, ce mois intact, l'autre mois ecrit, erreur journalisee. Dossier history/ non inscriptible : 204, snapshot ecrit.
  • Ecriture atomique en mode 600, aucun .tmp laisse.
  • Route : defauts, bornes 36 mois, from > to, formats invalides, mois absents ou illisibles, relecture sanitisee (champ inconnu jete, __proto__).
  • Import : une date deja archivee n'est jamais remplacee ; une date absente est ajoutee ; un fichier d'import invalide n'ecrit rien.
  • Backfill : sur des fixtures, fenetres passees correctes et contigues, aucun fichier ecrit hors de la sortie demandee, aucun appel reseau.
  • Chaque nouveau comportement verifie par mutation.

Criteres d'acceptation

  • Toute la surface de test ci-dessus, verte ; npm test, bash test-curl.sh, sh scripts/boot-from-copy.sh verts, CI verte
  • Aucun changement de contrat schemaVersion: 1 ; l'agent du cron n'est pas modifie
  • writes.test.js vert, avec l'archive comme seule nouvelle surface d'ecriture

Checklist post-merge (a confirmer, l'issue reste ouverte jusque-la)

  1. Redeploy Coolify, puis GET /claude-usage/history avec du contenu apres le premier POST.
  2. Rattrapage : backfill-history.js sur le ThinkPad, scp, import dans le conteneur, verification par la route.
  3. Sauvegarde : verifier si /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 dans coolify-ops.md.
  4. Fermer l'issue a la main avec les preuves.

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).

## 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 : - le serveur ne garde qu'un fichier, `CLAUDE_USAGE_PATH`, que chaque `POST /claude-usage` remplace (`writeClaudeUsage`, `index.js`) ; - le snapshot couvre 30 jours Toronto (`WINDOW_DAYS = 30`, `aggregate.js`), le contrat en accepte 31 (`MAX_DAYS`, `claude-usage.js`), les plus recents en dernier ; - le cache de l'agent garde 35 jours (`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) | Sujet | Decision | |---|---| | Regle d'archivage | **Dernier snapshot gagne** : chaque POST valide reecrit dans l'archive tous les jours qu'il porte, sauf le dernier (jour en cours). Un jour qui sort de la fenetre garde sa derniere valeur, celle que l'agent a figee a J+2 (complete depuis #54). Ecarte : l'ecriture unique proposee a l'origine, qui figerait pour toujours un jour archive depuis un passage incomplet (erreur de lecture, jour encore ouvert cote agent). | | Contenu par jour | Totaux par modele seulement, meme forme que `days` (`input`, `output`, `cacheCreation`, `cacheRead`, `requests`). Pas de projets par jour (demanderait un `schemaVersion: 2`, hors lot). | | Conservation | Illimitee (environ 1-2 Ko par jour), aucune purge. | | Lecture | Nouvelle route `GET /claude-usage/history`, token de lecture. | | Rattrapage | Script ponctuel dans ce lot, sans nouvelle route d'ecriture. | | Sauvegarde VPS | Etape de ce lot (checklist post-merge). L'issue reste ouverte tant qu'elle n'est pas verifiee. | ## Travail a faire ### 1. Module pur `claude-usage-history.js` (racine, dans l'image) - [ ] Format d'un fichier mensuel `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 de `days` du snapshot. - [ ] `mergeDays(existing, days)` pur : remplace ou ajoute chaque date, rend le fichier mensuel trie. Les dates sont rangees par mois de leur chaine `YYYY-MM-DD`, jamais par un calcul d'horloge. - [ ] Relecture d'un fichier mensuel par la meme liste blanche que le snapshot : exporter de `claude-usage.js` le nettoyeur d'un jour (aujourd'hui interne a `sanitizeClaudeUsage`) plutot que le dupliquer. Un jour invalide est jete, un fichier hors format est traite comme illisible. - [ ] Ajouter le module a la ligne `COPY` du Dockerfile, avec un test comme celui qui existe pour `claude-usage.js` ; `require` en colonne 0 (epingle de `ci.test.js`). ### 2. Ecriture dans `POST /claude-usage` - [ ] Apres l'ecriture du snapshot (donnee principale d'abord), fusionner dans `<dossier de CLAUDE_USAGE_PATH>/history/` tous les jours du snapshot sanitise **sauf le dernier**. Un ou deux fichiers mensuels touches par POST. - [ ] Ecriture atomique de chaque fichier mensuel (temporaire dans le meme dossier, mode 600, `renameSync`), patron de `writeClaudeUsage`. - [ ] Un echec d'archivage ne change pas la reponse : 204, erreur journalisee (`[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 sous `history/` doit rester dans le dossier autorise pour `POST /claude-usage` (verifier que `violations()` accepte un sous-dossier ; sinon l'elargir pour ce seul cas). ### 3. Route `GET /claude-usage/history` - [ ] Entree dans la table `ROUTES`, `tokenKind: "read"` (aucun `if` dans `checkAuth()`). - [ ] `?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, si `from > to`, ou si l'ecart depasse 36 mois. - [ ] Reponse 200 `{ from, to, days: [...] }`, jours tries ; un mois absent est simplement vide ; un mois illisible est saute et journalise ; aucun fichier -> `days: []`. - [ ] Ajouter la route aux matrices d'auth (`auth.test.js`, dont les cas `404 (not 401)`), a la liste des routes de lecture de `writes.test.js` et a `test-curl.sh`. ### 4. Rattrapage ponctuel (`agent/claude-usage/backfill-history.js`) - [ ] Lance depuis le depot (pas copie dans l'installation du cron) : `aggregate()` sur des fenetres passees (parametre `today` recule 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. - [ ] Import cote VPS sans nouvelle route d'ecriture : depot du fichier par scp, puis fusion dans le conteneur par une commande du module (ex. `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. - [ ] Procedure ecrite dans `agent/README.md`. ### 5. Docs - [ ] `CLAUDE.md` : endpoints (ecriture d'archive dans `POST /claude-usage`, nouvelle route), montage (`/data/claude-usage/history/` sur le meme montage), gotcha (pas de balayage des `.tmp` dans `history/`, 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 nouveau `claude-usage-history.test.js`), `__tests__/auth.test.js`, `__tests__/writes.test.js`, `__tests__/ci.test.js` si besoin, `__tests__/agent-claude-usage.test.js`, `test-curl.sh`, `CLAUDE.md` ## Surface de test - POST archive tous les jours sauf le dernier ; un second POST identique laisse l'archive identique octet pour octet ; un POST qui porte un jour avec d'autres valeurs le remplace ; un jour sorti de la fenetre garde sa valeur. - Snapshot a cheval sur deux mois : deux fichiers, chaque jour dans le bon. - Fichier mensuel illisible : 204, snapshot ecrit, ce mois intact, l'autre mois ecrit, erreur journalisee. Dossier `history/` non inscriptible : 204, snapshot ecrit. - Ecriture atomique en mode 600, aucun `.tmp` laisse. - Route : defauts, bornes 36 mois, `from > to`, formats invalides, mois absents ou illisibles, relecture sanitisee (champ inconnu jete, `__proto__`). - Import : une date deja archivee n'est jamais remplacee ; une date absente est ajoutee ; un fichier d'import invalide n'ecrit rien. - Backfill : sur des fixtures, fenetres passees correctes et contigues, aucun fichier ecrit hors de la sortie demandee, aucun appel reseau. - Chaque nouveau comportement verifie par mutation. ## Criteres d'acceptation - [ ] Toute la surface de test ci-dessus, verte ; `npm test`, `bash test-curl.sh`, `sh scripts/boot-from-copy.sh` verts, CI verte - [ ] Aucun changement de contrat `schemaVersion: 1` ; l'agent du cron n'est pas modifie - [ ] `writes.test.js` vert, avec l'archive comme seule nouvelle surface d'ecriture ## Checklist post-merge (a confirmer, l'issue reste ouverte jusque-la) 1. Redeploy Coolify, puis `GET /claude-usage/history` avec du contenu apres le premier POST. 2. Rattrapage : `backfill-history.js` sur le ThinkPad, scp, import dans le conteneur, verification par la route. 3. **Sauvegarde** : verifier si `/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 dans `coolify-ops.md`. 4. Fermer l'issue a la main avec les preuves. ## 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).
maximus added the
status:triage
type:feature
source:human
labels 2026-10-05 23:12:56 +00:00
maximus added
status:ready
and removed
status:triage
labels 2026-10-08 23:05:32 +00:00
maximus added
status:review
and removed
status:ready
labels 2026-10-09 00:47:22 +00:00
maximus added
status:approved
and removed
status:review
labels 2026-10-09 01:23:07 +00:00
maximus added
status:blocked
and removed
status:approved
labels 2026-10-09 01:48:06 +00:00
Author
Owner

PR #57 merged on 2026-10-09 (main @ c5ded9e). The issue stays open until the post-merge checklist is done:

  1. Coolify redeploy (manual, no auto-deploy), then GET /claude-usage/history should have content after the next cron beat.
  2. Backfill: on the ThinkPad, node agent/claude-usage/backfill-history.js --out ~/claude-usage-backfill.json. Then scp it to the VPS and run docker exec -i <ctr> node claude-usage-history.js --import - < file, and check the result through the route (procedure in agent/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.
  3. VPS backup: check whether /data/claude-usage/ (and /data/hosts, /data/defenseurs) is covered; if not, set it up and document it in coolify-ops.md.
  4. Close by hand with the evidence.
PR #57 merged on 2026-10-09 (main @ c5ded9e). The issue stays open until the post-merge checklist is done: 1. Coolify redeploy (manual, no auto-deploy), then `GET /claude-usage/history` should have content after the next cron beat. 2. Backfill: on the ThinkPad, `node agent/claude-usage/backfill-history.js --out ~/claude-usage-backfill.json`. Then scp it to the VPS and run `docker exec -i <ctr> node claude-usage-history.js --import - < file`, and check the result through the route (procedure in `agent/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. 3. VPS backup: check whether `/data/claude-usage/` (and `/data/hosts`, `/data/defenseurs`) is covered; if not, set it up and document it in `coolify-ops.md`. 4. Close by hand with the evidence.
Author
Owner

Steps 1 and 2 done on 2026-10-12.

  1. Redeployed: GET /claude-usage/history served 29 closed days (2026-09-13 to 2026-10-11) before the import.
  2. Backfill: backfill-history.js produced 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. Afterwards GET /claude-usage/history?from=2026-08&to=2026-10 returns 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.

Steps 1 and 2 done on 2026-10-12. 1. Redeployed: `GET /claude-usage/history` served 29 closed days (2026-09-13 to 2026-10-11) before the import. 2. Backfill: `backfill-history.js` produced 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. Afterwards `GET /claude-usage/history?from=2026-08&to=2026-10` returns 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.
Author
Owner

Step 3 done on 2026-10-12. No file backup existed on the VPS (only backup-postgres, which touched no /data dir). /data/claude-usage now 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 holding history/2026-09.json and history/2026-10.json. /data/hosts, /data/backups-pull and /data/defenseurs are rewritten on every pass of their agent, so they stay out. Documented in la-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.

Step 3 done on 2026-10-12. No file backup existed on the VPS (only `backup-postgres`, which touched no `/data` dir). `/data/claude-usage` now 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 holding `history/2026-09.json` and `history/2026-10.json`. `/data/hosts`, `/data/backups-pull` and `/data/defenseurs` are rewritten on every pass of their agent, so they stay out. Documented in `la-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.
maximus removed the
status:blocked
label 2026-10-12 23:53:00 +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#55
No description provided.