No description
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
|
||
|---|---|---|
| __tests__ | ||
| .env.example | ||
| .gitignore | ||
| CLAUDE.md | ||
| Dockerfile | ||
| index.js | ||
| metrics.js | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| test-curl.sh | ||
| vitest.config.js | ||
vps-health-api
Lightweight health monitoring API for the VPS. Node 22, HTTP-native, zero runtime deps.
Endpoints
All endpoints require Authorization: Bearer $HEALTH_TOKEN.
| Method | Path | Description |
|---|---|---|
| GET | /health |
CPU, memory, disk, uptime, Logto reachability |
| GET | /defenseurs |
Defenseurs executive status (status.json) |
| GET | /defenseurs/findings?project=X |
Detailed findings for a project's Defenseur |
| GET | /reports/scans?date=YYYY-MM-DD |
Aggregated scan reports for a UTC date |
GET /defenseurs/findings
Query params:
project(required) — project name, looked up inagents-map.json(e.g.la-suite-booking)category(optional) — exact match, one ofdeps|secrets|code|acces|infraseverity(optional) — threshold, one ofCRITICAL|HIGH|MEDIUM|LOW|INFO- default (no param):
MEDIUM,HIGH,CRITICAL LOWreturnsLOW+MEDIUM+HIGH+CRITICALbut still hidesINFOINFOreturnsINFOonly (explicit opt-in)
- default (no param):
Responses:
200 { agent, project, timestamp, findings: Finding[] }— report present (emptyfindingsif clean scan; nostatusfield)200 { findings: [], status: "no_data" }— no report on file for the agent400— missingprojector invalidcategory/severity401— missing or invalid token404— unknown project500—agents-map.jsonunreadable or corrupted
Example:
curl -H "Authorization: Bearer $HEALTH_TOKEN" \
"https://health.lacompagniemaximus.com/defenseurs/findings?project=la-suite-booking&severity=HIGH"
Config
| Env var | Default | Purpose |
|---|---|---|
PORT |
3001 |
HTTP port |
HEALTH_TOKEN |
— | Bearer token (fail-closed if missing) |
REPORTS_DIR |
/data/defenseurs/reports |
Scan reports dir |
DEFENSEURS_AGENTS_MAP_PATH |
/data/defenseurs/agents-map.json |
Project -> agent snapshot |
LOGTO_HEALTH_URL |
auth.lacompagniemaximus.com | Logto OIDC discovery URL |
Bind-mounts (Coolify, read-only)
/home/defenseur/defenseurs/status.json->/data/defenseurs/status.json/home/defenseur/defenseurs/reports/->/data/defenseurs/reports//home/defenseur/defenseurs/agents-map.json->/data/defenseurs/agents-map.json
Tests
npm install
npm test