feat(stats): /api/familiar/stats — CPU/mem/disk/net/GPU snapshot - #53
Conversation
Adds a polling endpoint dashboard widgets can consume for live host
metrics. Reads /proc/{stat,meminfo,loadavg,uptime,net/dev} directly and
shells out to df + nvidia-smi via Bun.spawnSync argv arrays. Per-core
CPU pct and per-iface Mbps are computed as deltas against a module-
scope prior sample; response is cached for 2s to cap load when many
widgets poll concurrently. GPU array is empty when nvidia-smi is
missing or fails so the frontend can hide that panel gracefully.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Warning You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again! |
jphein
left a comment
There was a problem hiding this comment.
APPROVE (posted as comment — cannot self-approve via CLI)
Clean implementation with no blocking issues.
Shell injection: All spawn calls use argv arrays — CLAUDE.md mandate met. No user input touches spawn args.
Linux paths: All /proc reads go through the injected readFile dep; handleStats wraps collectStats in try/catch returning JSON 500. Safe on non-Linux.
nvidia-smi missing: exitCode !== 0 returns []. Test covers exitCode 127 case correctly.
Cache correctness: expiresAt set at collection time; ts in body reflects when data was gathered, not when it's served.
CPU first-call: Returns [] explicitly with JSDoc noting it. Frontend must handle empty array (acceptable tradeoff vs zeros/NaN).
Test coverage: routes-stats.test.ts covers first-call shape, delta math, cache TTL, and nvidia-smi-missing. Complete.
Route wiring: Mounted with GET method guard, doesn't touch existing routes.
TypeScript types: StatsResponse interface exported for frontend mirroring.
Two minor points to consider (neither blocks merge):
src/routes/stats.ts:214: dead...(avail ? {} : {})spread is a no-op — looks like a leftover from a shape change. Remove it.tests/routes-stats.test.tsfixture header row says1B-blocksbut--outputorder issource,fstype,target,size,used,avail— misleading for future readers even though the header is sliced away.
Five non-blocking findings from the post-merge reviews:
- stats.ts: remove dead `...(avail ? {} : {})` spread (and the
now-unused `availStr`/`avail` locals it referenced).
- tests/routes-stats.test.ts: relabel the mock df header from the
display-form (`Filesystem ... 1B-blocks Used Avail`) to the
--output= field names so readers can map columns at a glance.
stats.ts skips the header line, so behavior is unchanged.
- web/style.css: `.main { max-width: none; }` targeted a class that
doesn't exist; drop the dot to match the actual <main> element.
- web/widgets/stats-init.js: forward `defaultSettings` to
dashboard.registerBlockType so widgets get their initial settings
on first mount (previous omission caused a one-cycle empty-settings
flash).
- web/widgets/stats-util.js: drop the hardcoded `#6ab68f` fallback
on the `--accent-cool` var; stats.css always loads alongside, so
the literal was silent dead code that would only win if the sheet
were missing — which violates the CSS-tokens-only mandate.
Summary
New endpoint
GET /api/familiar/statsexposes a host snapshot for dashboard widgets (CPU, memory, disk, network, GPU). Pairs with the upcoming Luna widgets + Reverie grid framework.Contract
{ "ts": "2026-05-28T...", "uptime_seconds": 106717, "cpu": { "cores": 8, "load_1m": 0.42, "load_5m": ..., "load_15m": ..., "per_core_pct": [12, 15, ...] }, "mem": { "total_mb": 32008, "used_mb": ..., "available_mb": ..., "swap_total_mb": ..., "swap_used_mb": ... }, "disk": [ { "mount": "/", "fs": "ext4", "total_gb": 1007, "used_gb": 689.4, "used_pct": 68.5 } ], "net": [ { "iface": "enp5s0", "rx_mbps": 0.07, "tx_mbps": 0.04, "rx_bytes_total": ..., "tx_bytes_total": ... } ], "gpu": [ { "index": 0, "name": "P102-100", "vram_total_mb": 10240, "vram_used_mb": 4096, "util_pct": 75, "temp_c": 65 } ] }Design notes
/proc/{stat,meminfo,loadavg,uptime,net/dev}read directly (sync);dfandnvidia-smishell out viaBun.spawnSync({ cmd: [...] })— argv arrays only, no shell strings (per CLAUDE.md).cpu.per_core_pctandnet[].{rx,tx}_mbpsare computed against a module-scope prior sample. The first request after process start returns[]for per-core and0Mbps; subsequent polls produce real deltas. Net Mbps = megabits/sec (not bytes).gpu: []. df failure →disk: []. Endpoint never 5xx's on missing optional collectors; only on /proc read errors (which are unrecoverable anyway).net. Pseudo-filesystems (tmpfs, devtmpfs, squashfs, efivarfs, overlay) excluded fromdiskviadf -x.Test plan
bun test tests/routes-stats.test.ts— 5 tests, all pass (shape, deltas, caching, nvidia-smi missing).bun run typecheck— clean./proc+ local nvidia-smi: response shape matches contract, values plausible.curl http://familiar:8080/api/familiar/statsfrom katana to verify 2× P102 GPU rows.Headline curl
🤖 Generated with Claude Code