Ajouter les routes POST et GET /claude-usage #48

Closed
opened 2026-10-04 12:59:46 +00:00 by maximus · 0 comments
Owner

Refs : spec-decisions-suivi-claude.md + spec-plan-suivi-claude.md (dépôt la-compagnie-maximus). Ce corps est autosuffisant : tout ce qu'il faut pour implémenter est ci-dessous.

Route d'ingestion du snapshot de consommation Claude poussé par l'agent du ThinkPad, et route de lecture pour le dashboard /admin. C'est la deuxième route d'écriture de l'API : elle a son propre token et ne doit jamais accepter celui des postes.

Contrat du snapshot (schemaVersion: 1, figé)

Corps de POST /claude-usage, partagé entre vps-health-api (agent + serveur) et la-compagnie-maximus (dashboard) :

{
  "schemaVersion": 1,
  "generatedAt": "2026-09-27T19:00:00.000Z",     // horloge du poste, informatif
  "claudeCodeVersion": "2.1.283",                 // champ `version` du transcript le plus récent
  "timeZone": "America/Toronto",
  "limits": {
    "status": "ok",          // ok | token_expired | http_error | unavailable | disabled
    "detail": "http_429",    // optionnel : no_credentials | parse_error | no_version | timeout | network | bad_response | http_<code>
    "fetchedAt": "2026-09-27T19:00:00.000Z",     // date des items (dernières valeurs valides), ou null
    "items": [
      { "kind": "session", "group": "session", "percent": 1,
        "severity": "normal", "resetsAt": "2026-09-27T21:30:00Z", "model": null },
      { "kind": "weekly_scoped", "group": "weekly", "percent": 63,
        "severity": "normal", "resetsAt": "2026-10-01T07:00:00Z", "model": "Fable" }
    ]
  },
  "days": [                  // exactement 30 jours Toronto, du plus ancien à aujourd'hui, jours vides inclus
    { "date": "2026-09-27",
      "models": { "claude-opus-5-5": { "input": 2, "output": 1950, "cacheCreation": 43176,
                                        "cacheRead": 33814, "requests": 1 } } }
  ],
  "projects": [              // top 8 sur 30 j (tokens de travail = input + output + cacheCreation), puis "(autres)"
    { "name": "simpl-resultat", "work": 123456, "cacheRead": 7890123, "requests": 812 }
  ]
}

Bornes serveur (sanitizeClaudeUsage, liste blanche, tout champ inconnu jeté) : corps ≤ 64 Ko ; days ≤ 31, date ^\d{4}-\d{2}-\d{2}$ ; models ≤ 12 clés ^[a-z0-9][a-z0-9.\-]{0,63}$ ; limits.items ≤ 12 ; projects ≤ 9 ; nombres Number.isSafeInteger ≥ 0, percent 0-100 ; status dans l'enum ; detail ^[a-z0-9_]{1,32}$ sinon jeté ; autres chaînes ≤ 128 car. ; dates ISO validées. Le serveur ajoute receivedAt.

Réponse de GET /claude-usage : { staleAfterSeconds, receivedAt, ageSeconds, stale, snapshot }, ou { "status": "no_data" } si le fichier est absent ou illisible.

Tâches

  • Refactor du gate : checkAuth (aujourd'hui binaire : tokenKind === "ingest" → HOSTS_INGEST_TOKEN, sinon HEALTH_TOKEN) lit une table tokenKind → token : read → HEALTH_TOKEN, ingest → HOSTS_INGEST_TOKEN, claude-ingest → CLAUDE_USAGE_INGEST_TOKEN. Réponse quand le token manque définie par entrée : read → 401 comme aujourd'hui, chaque ingestion → 503 nommant sa propre variable. Comparaison via tokenMatches pour toutes les entrées. Le gate reste le point unique d'authentification.
  • readBody(req, res, maxBytes) : /hosts/<id> garde 4096 (constante actuelle MAX_BODY_BYTES), /claude-usage passe 65536. Plafond compté sur les octets reçus.
  • Module pur claude-usage.js à la racine, à côté de metrics.js : sanitizeClaudeUsage + bornes du contrat, exporté. index.js ne fait que brancher les routes. Ajouter claude-usage.js à la ligne COPY du Dockerfile et mettre à jour le test de __tests__/agent.test.js qui fige cette liste (il interdit COPY . . et l'agent, pas un nouveau fichier serveur).
  • Env vars : CLAUDE_USAGE_INGEST_TOKEN, CLAUDE_USAGE_PATH (défaut /data/claude-usage/claude-usage.json), CLAUDE_USAGE_STALE_SECONDS (défaut 2700).
  • POST /claude-usage (tokenKind: "claude-ingest") : JSON invalide → 400 ; sanitizeClaudeUsage ; receivedAt serveur ; écriture atomique (tmp suffixé pid + rename, mode 600, patron writeHostSnapshot) ; 204.
  • GET /claude-usage (tokenKind: "read") : fichier re-sanitisé + ageSeconds, stale (ageSeconds > CLAUDE_USAGE_STALE_SECONDS), staleAfterSeconds ; {status:"no_data"} si absent ou illisible.
  • Journaliser les 401/403 comme les autres routes.
  • README.md : les deux routes, les trois env vars, le montage /data/claude-usage (lecture-écriture, propriétaire uid 1000).

Fichiers concernés

  • index.js
  • claude-usage.js (nouveau)
  • Dockerfile
  • __tests__/agent.test.js
  • __tests__/claude-usage.test.js (nouveau)
  • README.md

Critères d'acceptation

  • POST sans token ou mauvais token → 401 ; token de HOSTS_INGEST_TOKEN sur /claude-usage → 401 ; token Claude sur POST /hosts/<id> → 401
  • CLAUDE_USAGE_INGEST_TOKEN non posé → 503 dont le message nomme CLAUDE_USAGE_INGEST_TOKEN (et /hosts garde son 503 nommant HOSTS_INGEST_TOKEN)
  • Corps de 65 537 octets → 413 ; /hosts/<id> toujours 413 à 4 097 octets
  • JSON invalide → 400 ; champ inconnu, nombre négatif, nombre > 2^53, detail hors regex, status hors enum : jetés (testé)
  • POST valide puis GET → snapshot identique (après sanitize) + receivedAt, ageSeconds, stale
  • GET sans token → 401 ; fichier absent → {status:"no_data"}
  • Toute la suite existante (hosts, auth, findings, health, state, agent) passe sans modification autre que la liste COPY du test agent

Review caveats

  • 🔴 (résolu) Gate binaire : ajouter la route sans refactor ferait accepter le token des postes ou renverrait 413 à chaque envoi.
  • 🟡 (résolu) Plafond : le pire cas dans les bornes fait ~46 Ko, d'où 64 Ko et non 32.
  • 🟡 (résolu) detail jamais en texte libre (risque de transporter un corps de réponse amont).

Décisions prises en planification

  • Token dédié CLAUDE_USAGE_INGEST_TOKEN (rotation indépendante, isolation des routes).
  • Péremption décidée par le serveur seul (stale) ; le dashboard ne la recalcule pas.
  • Sanitizer dans un module à part plutôt que dans index.js, pour que l'agent (issue suivante) teste son snapshot contre le vrai sanitizer.

Spec source

la-compagnie-maximus : spec-plan-suivi-claude.md, Issue 1

Refs : [spec-decisions-suivi-claude.md](https://git.lacompagniemaximus.com/maximus/la-compagnie-maximus/src/branch/main/spec-decisions-suivi-claude.md) + [spec-plan-suivi-claude.md](https://git.lacompagniemaximus.com/maximus/la-compagnie-maximus/src/branch/main/spec-plan-suivi-claude.md) (dépôt la-compagnie-maximus). Ce corps est autosuffisant : tout ce qu'il faut pour implémenter est ci-dessous. Route d'ingestion du snapshot de consommation Claude poussé par l'agent du ThinkPad, et route de lecture pour le dashboard `/admin`. C'est la deuxième route d'écriture de l'API : elle a son propre token et ne doit jamais accepter celui des postes. ## Contrat du snapshot (`schemaVersion: 1`, figé) Corps de `POST /claude-usage`, partagé entre `vps-health-api` (agent + serveur) et `la-compagnie-maximus` (dashboard) : ```jsonc { "schemaVersion": 1, "generatedAt": "2026-09-27T19:00:00.000Z", // horloge du poste, informatif "claudeCodeVersion": "2.1.283", // champ `version` du transcript le plus récent "timeZone": "America/Toronto", "limits": { "status": "ok", // ok | token_expired | http_error | unavailable | disabled "detail": "http_429", // optionnel : no_credentials | parse_error | no_version | timeout | network | bad_response | http_<code> "fetchedAt": "2026-09-27T19:00:00.000Z", // date des items (dernières valeurs valides), ou null "items": [ { "kind": "session", "group": "session", "percent": 1, "severity": "normal", "resetsAt": "2026-09-27T21:30:00Z", "model": null }, { "kind": "weekly_scoped", "group": "weekly", "percent": 63, "severity": "normal", "resetsAt": "2026-10-01T07:00:00Z", "model": "Fable" } ] }, "days": [ // exactement 30 jours Toronto, du plus ancien à aujourd'hui, jours vides inclus { "date": "2026-09-27", "models": { "claude-opus-5-5": { "input": 2, "output": 1950, "cacheCreation": 43176, "cacheRead": 33814, "requests": 1 } } } ], "projects": [ // top 8 sur 30 j (tokens de travail = input + output + cacheCreation), puis "(autres)" { "name": "simpl-resultat", "work": 123456, "cacheRead": 7890123, "requests": 812 } ] } ``` Bornes serveur (`sanitizeClaudeUsage`, liste blanche, tout champ inconnu jeté) : corps ≤ 64 Ko ; `days` ≤ 31, `date` `^\d{4}-\d{2}-\d{2}$` ; `models` ≤ 12 clés `^[a-z0-9][a-z0-9.\-]{0,63}$` ; `limits.items` ≤ 12 ; `projects` ≤ 9 ; nombres `Number.isSafeInteger` ≥ 0, `percent` 0-100 ; `status` dans l'enum ; `detail` `^[a-z0-9_]{1,32}$` sinon jeté ; autres chaînes ≤ 128 car. ; dates ISO validées. Le serveur ajoute `receivedAt`. Réponse de `GET /claude-usage` : `{ staleAfterSeconds, receivedAt, ageSeconds, stale, snapshot }`, ou `{ "status": "no_data" }` si le fichier est absent ou illisible. ## Tâches - [ ] Refactor du gate : `checkAuth` (aujourd'hui binaire : `tokenKind === "ingest"` → `HOSTS_INGEST_TOKEN`, sinon `HEALTH_TOKEN`) lit une table `tokenKind` → token : `read` → `HEALTH_TOKEN`, `ingest` → `HOSTS_INGEST_TOKEN`, `claude-ingest` → `CLAUDE_USAGE_INGEST_TOKEN`. Réponse quand le token manque définie par entrée : `read` → 401 comme aujourd'hui, chaque ingestion → 503 nommant sa propre variable. Comparaison via `tokenMatches` pour toutes les entrées. Le gate reste le point unique d'authentification. - [ ] `readBody(req, res, maxBytes)` : `/hosts/<id>` garde 4096 (constante actuelle `MAX_BODY_BYTES`), `/claude-usage` passe 65536. Plafond compté sur les octets reçus. - [ ] Module pur `claude-usage.js` à la racine, à côté de `metrics.js` : `sanitizeClaudeUsage` + bornes du contrat, exporté. `index.js` ne fait que brancher les routes. Ajouter `claude-usage.js` à la ligne `COPY` du `Dockerfile` et mettre à jour le test de `__tests__/agent.test.js` qui fige cette liste (il interdit `COPY . .` et l'agent, pas un nouveau fichier serveur). - [ ] Env vars : `CLAUDE_USAGE_INGEST_TOKEN`, `CLAUDE_USAGE_PATH` (défaut `/data/claude-usage/claude-usage.json`), `CLAUDE_USAGE_STALE_SECONDS` (défaut 2700). - [ ] `POST /claude-usage` (`tokenKind: "claude-ingest"`) : JSON invalide → 400 ; `sanitizeClaudeUsage` ; `receivedAt` serveur ; écriture atomique (tmp suffixé pid + rename, mode 600, patron `writeHostSnapshot`) ; 204. - [ ] `GET /claude-usage` (`tokenKind: "read"`) : fichier re-sanitisé + `ageSeconds`, `stale` (`ageSeconds > CLAUDE_USAGE_STALE_SECONDS`), `staleAfterSeconds` ; `{status:"no_data"}` si absent ou illisible. - [ ] Journaliser les 401/403 comme les autres routes. - [ ] `README.md` : les deux routes, les trois env vars, le montage `/data/claude-usage` (lecture-écriture, propriétaire uid 1000). ## Fichiers concernés - `index.js` - `claude-usage.js` (nouveau) - `Dockerfile` - `__tests__/agent.test.js` - `__tests__/claude-usage.test.js` (nouveau) - `README.md` ## Critères d'acceptation - [ ] POST sans token ou mauvais token → 401 ; token de `HOSTS_INGEST_TOKEN` sur `/claude-usage` → 401 ; token Claude sur `POST /hosts/<id>` → 401 - [ ] `CLAUDE_USAGE_INGEST_TOKEN` non posé → 503 dont le message nomme `CLAUDE_USAGE_INGEST_TOKEN` (et `/hosts` garde son 503 nommant `HOSTS_INGEST_TOKEN`) - [ ] Corps de 65 537 octets → 413 ; `/hosts/<id>` toujours 413 à 4 097 octets - [ ] JSON invalide → 400 ; champ inconnu, nombre négatif, nombre > 2^53, `detail` hors regex, `status` hors enum : jetés (testé) - [ ] POST valide puis GET → snapshot identique (après sanitize) + `receivedAt`, `ageSeconds`, `stale` - [ ] GET sans token → 401 ; fichier absent → `{status:"no_data"}` - [ ] Toute la suite existante (`hosts`, `auth`, `findings`, `health`, `state`, `agent`) passe sans modification autre que la liste `COPY` du test agent ## Review caveats - 🔴 (résolu) Gate binaire : ajouter la route sans refactor ferait accepter le token des postes ou renverrait 413 à chaque envoi. - 🟡 (résolu) Plafond : le pire cas dans les bornes fait ~46 Ko, d'où 64 Ko et non 32. - 🟡 (résolu) `detail` jamais en texte libre (risque de transporter un corps de réponse amont). ## Décisions prises en planification - Token dédié `CLAUDE_USAGE_INGEST_TOKEN` (rotation indépendante, isolation des routes). - Péremption décidée par le serveur seul (`stale`) ; le dashboard ne la recalcule pas. - Sanitizer dans un module à part plutôt que dans `index.js`, pour que l'agent (issue suivante) teste son snapshot contre le vrai sanitizer. ## Spec source la-compagnie-maximus : `spec-plan-suivi-claude.md`, Issue 1
maximus added this to the planned-2026-10-04-suivi-claude milestone 2026-10-04 12:59:46 +00:00
maximus added the
status:ready
type:feature
source:human
labels 2026-10-04 12:59:46 +00:00
maximus added
status:in-progress
and removed
status:ready
labels 2026-10-04 13:06:45 +00:00
maximus added
status:review
and removed
status:in-progress
labels 2026-10-04 13:17:32 +00:00
maximus added
status:approved
and removed
status:review
labels 2026-10-04 14:49:30 +00:00
maximus removed the
status:approved
label 2026-10-04 19:30:04 +00:00
Sign in to join this conversation.
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#48
No description provided.