Move readProcStat, getCpuPercent, getDisk and the cpu/memory/disk payload
assembly out of index.js into a standalone metrics.js, so the upcoming
local workstation agent can reuse the same collection code. No behaviour
change: /health returns the exact same fields, in the same order.
Notes:
- collectMetrics() returns the { cpu, memory, disk } slice; getHealth()
spreads it, keeping the JSON key order the admin dashboard relies on.
- getHealth() keeps Promise.all([collectMetrics(), getLogtoHealth()]).
The 500ms CPU sample and the 3s Logto check are deliberately concurrent;
serializing them would push the p99 of /health to ~3.5s.
- collectMetrics() awaits the CPU sample as its only await, so callers
running it inside a Promise.all keep their concurrency.
- Dockerfile COPY lists files one by one, so metrics.js had to be added
there or the container would crash on MODULE_NOT_FOUND at startup.
- New __tests__/health.test.js: metrics.js exports, field-for-field
payload shape, and a latency guard (<1.5s with a 1200ms stubbed Logto).
Verified by mutation: serializing the two calls fails the latency test
at ~1743ms while the field comparison still passes.
- Runtime stays 0-dependency; the 14 existing tests are untouched.
Resolves #11
4.9 KiB
4.9 KiB
VPS Health API
API sante minimaliste pour le VPS. ~127 lignes, Node 22 + HTTP natif.
Endpoints
GET /health— CPU, memoire, disque, uptime, logto ({status, responseTimeMs, error?})GET /defenseurs— contenu de status.json (rapports defenseurs)GET /reports/scans?date=YYYY-MM-DD— agrege les rapportsdefenseur-<agent>_<date>*.jsondu jour, format{ date, count, reports: Report[] }. FiltreisScanReport(exclutdefenseur-auto_*.json). Date validee par regex (path traversal bloque). Consommateur :defenseur-autoworkstation cron (remplace le pre-rsync SSH). Exemple :curl -H "Authorization: Bearer $TOKEN" "https://health.lacompagniemaximus.com/reports/scans?date=2026-05-07".GET /defenseurs/findings?project=X— findings detailles du Defenseur correspondant. Query params :project=<name>(obligatoire, lookup viaagents-map.json),category=<deps|secrets|code|acces|infra>(optionnel, exact match),severity=<CRITICAL|HIGH|MEDIUM|LOW|INFO>(optionnel, threshold inclusif vers le haut). Sansseverity-> MEDIUM+HIGH+CRITICAL (cache LOW+INFO).severity=LOW-> LOW+MEDIUM+HIGH+CRITICAL (cache toujours INFO, asymetrie volontaire).severity=INFO-> INFO uniquement (opt-in explicite). Reponses : 200{ agent, project, timestamp, findings[] }si report present (sans champstatus) ; 200{ findings: [], status: "no_data" }si pas de report ; 400 sans project ou param invalide ; 404 projet inconnu ; 500 siagents-map.jsoncorrompu. Consommateurs : admin dashboard Vercel (drill-down), futur skill/analyse-vulnerabilite. Exemple :curl -H "Authorization: Bearer $TOKEN" "https://health.lacompagniemaximus.com/defenseurs/findings?project=la-suite-booking&severity=HIGH".
Auth
- Bearer token via env
HEALTH_TOKEN - Fail-closed : si
HEALTH_TOKENnon configure, toutes les requetes sont refusees - Coolify :
HEALTH_TOKENdoit etreis_runtime=true, is_buildtime=false. Buildtime fait fuiter le secret en clair dansapplication_deployment_queues.logs. Voirla-compagnie-maximus/docs/coolify-ops.mdsection "Secrets en buildtime".
Config
- Port :
3001(envPORT) LOGTO_HEALTH_URL: URL du.well-known/openid-configuration(default auth.lacompagniemaximus.com)REPORTS_DIR: dossier lu par/reports/scanset/defenseurs/findings(default/data/defenseurs/reports)DEFENSEURS_AGENTS_MAP_PATH: snapshot project->agent ecrit par le Sergent (default/data/defenseurs/agents-map.json)- Montages Coolify (Persistent Storages, UI seulement — aucun endpoint API) :
/data/defenseurs(host) ->/data/defenseurs:status.json+agents-map.json, ecrits directement la par le Sergent./home/defenseur/defenseurs/status.jsonest un leurre obsolete lu par personne./home/defenseur/defenseurs/reports(host) ->/data/defenseurs/reports: rapports de scan (+ sous-dirarchive/). Pose le 2026-07-15 (issue #10).
Deploy
Pas d'auto-deploy : l'app Coolify n'a pas de Source Forgejo (source_id=null, migration trackee dans la-compagnie-maximus#133). Apres un merge sur main, trigger manuel :
curl -H "Authorization: Bearer $(cat ~/.coolify-token)" \
"https://coolify.lacompagniemaximus.com/api/v1/deploy?uuid=u8000gsg044wsk0oo0w884ok&force=true"
(ou bouton Redeploy dans l'UI Coolify.)
Tests
npm test(vitest) — 20 cas__tests__/findings.test.js—/defenseurs/findings(14 cas : auth, validation, filtres severity/category, asymetrie INFO, scan clean vs no_data, JSON corrompu)__tests__/health.test.js— modulemetrics.js+/health(6 cas : exports, payload champ pour champ, garde de latence)
- Runtime reste 0-dep ; vitest en devDep uniquement
Gotchas
- Pas d'Express — HTTP natif Node.js uniquement
- Le
COPYdu Dockerfile liste les fichiers un par un (package.json index.js metrics.js) : tout nouveau module runtime doit y etre ajoute, sinon le conteneur plante au demarrage sur unMODULE_NOT_FOUNDque les tests locaux ne voient pas. getHealth()gardePromise.all([collectMetrics(), getLogtoHealth()]): l'echantillon CPU de 500 ms et le check Logto (timeout 3 s) sont deliberement concurrents. Les serialiser ferait monter le p99 de/healtha ~3,5 s. Garde de non-regression : le test de latence dans__tests__/health.test.js.- Le
status.jsonetagents-map.jsonsont ecrits par le Sergent defenseurs, pas par cette API (read-only) agents-map.jsondoit etre present sur le VPS avant le deploy : verifier viassh ubuntu@vps 'ls /data/defenseurs/agents-map.json'(PAS/home/defenseur/..., leurre obsolete). Sinon/defenseurs/findingsretourne 500.- Coolify ignore silencieusement
-vdanscustom_docker_run_options— les volumes passent par les Persistent Storages (UI seulement). Details dansla-compagnie-maximus/docs/coolify-ops.md. - Severity threshold est asymetrique :
?severity=LOWretourne LOW+MEDIUM+HIGH+CRITICAL mais cache INFO. INFO est seulement accessible via?severity=INFOexplicite (cache le bruit par defaut).