Skip to content

feat(stats): /api/familiar/stats — CPU/mem/disk/net/GPU snapshot - #53

Merged
jphein merged 1 commit into
mainfrom
feat/stats-endpoint
May 28, 2026
Merged

feat(stats): /api/familiar/stats — CPU/mem/disk/net/GPU snapshot#53
jphein merged 1 commit into
mainfrom
feat/stats-endpoint

Conversation

@jphein

@jphein jphein commented May 28, 2026

Copy link
Copy Markdown
Collaborator

Summary

New endpoint GET /api/familiar/stats exposes 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

  • Collection: /proc/{stat,meminfo,loadavg,uptime,net/dev} read directly (sync); df and nvidia-smi shell out via Bun.spawnSync({ cmd: [...] }) — argv arrays only, no shell strings (per CLAUDE.md).
  • Deltas: cpu.per_core_pct and net[].{rx,tx}_mbps are computed against a module-scope prior sample. The first request after process start returns [] for per-core and 0 Mbps; subsequent polls produce real deltas. Net Mbps = megabits/sec (not bytes).
  • Cache: 2-second in-process TTL. Many widgets polling at 1s share one snapshot. Cache window is documented inline; widgets that need fresher data should bump the poll interval, not assume sub-2s freshness.
  • Resilience: nvidia-smi missing or exit != 0 → gpu: []. df failure → disk: []. Endpoint never 5xx's on missing optional collectors; only on /proc read errors (which are unrecoverable anyway).
  • Filters: loopback iface excluded from net. Pseudo-filesystems (tmpfs, devtmpfs, squashfs, efivarfs, overlay) excluded from disk via df -x.

Test plan

  • bun test tests/routes-stats.test.ts — 5 tests, all pass (shape, deltas, caching, nvidia-smi missing).
  • bun run typecheck — clean.
  • Real-host smoke test against /proc + local nvidia-smi: response shape matches contract, values plausible.
  • Deploy to familiar, hit curl http://familiar:8080/api/familiar/stats from katana to verify 2× P102 GPU rows.

Headline curl

curl -s http://familiar:8080/api/familiar/stats | jq '{cpu_load: .cpu.load_1m, mem_pct: (.mem.used_mb / .mem.total_mb * 100 | floor), gpu_util: [.gpu[].util_pct]}'

🤖 Generated with Claude Code

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>
Copilot AI review requested due to automatic review settings May 28, 2026 18:18
@gemini-code-assist

Copy link
Copy Markdown

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@jphein jphein left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.ts fixture header row says 1B-blocks but --output order is source,fstype,target,size,used,avail — misleading for future readers even though the header is sliced away.

@jphein
jphein merged commit 3062502 into main May 28, 2026
1 check failed
jphein added a commit that referenced this pull request May 28, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants