Files
platform/SYSTEM_MONITOR_API.md
T
pastilhasandClaude Opus 4.8 de8f70562b docs: SYSTEM_MONITOR_API.md contract for the app + drop dead Pm2Logs.tsx
Full /api/system-monitor/* contract (stats snapshot incl. cpu/mem/disks/temp/
gpu/net/power, pm2, docker, and the two SSE log streams) with response shapes,
auth (Bearer or ?token=), owner-only note, and the net/power rate caveats — so
the app can implement the same views. Also removes the orphaned Pm2Logs.tsx
(superseded by LogStream) that a prior commit left tracked.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 11:03:10 +00:00

5.1 KiB
Raw Blame History

System Monitor API (/api/system-monitor/*)

Everything the /system-monitor web screen renders, for building the same in the app.

Auth & access

  • Send the JWT as Authorization: Bearer <token>, or as ?token=<token> in the query string (required for the SSE endpoints — EventSource can't set headers).
  • Owner-only. These routes are gated to the platform owner. Non-owner accounts (e.g. music-app users) are confined to /api/auth + /api/music and will get 403 here. The full officer-mobile client (which authenticates as the owner from an owner-allowed origin) has access; the music app does not.
  • All responses are application/json except the two /logs endpoints, which are text/event-stream.

GET /api/system-monitor/stats

One full snapshot. Poll it on a steady interval (the web client uses 2 s) — a few fields are rates computed from the delta since your previous call (see notes), so a steady cadence matters.

{
  "hostname": "alpha",
  "platform": "Linux 6.8.0-136-generic",
  "uptimeSec": 614031,
  "loadavg": [0.59, 0.60, 0.81],          // 1 / 5 / 15 min
  "cpu": {
    "model": "AMD Ryzen 9 7940HS w/ Radeon 780M Graphics",
    "cores": 16,
    "usagePct": 2.6,                       // overall, 0100
    "perCore": [3.1, 0.0, 12.4, ...]       // length === cores
  },
  "mem": {                                 // null if unreadable
    "totalBytes": 65100000000, "usedBytes": 26800000000, "freeBytes": 38300000000,
    "usedPct": 41.2, "swapTotalBytes": 0, "swapUsedBytes": 0
  },
  "disks": [                               // real mounts (tmpfs/overlay excluded)
    { "mount": "/", "fsType": "ext4", "totalBytes": 0, "usedBytes": 0, "usedPct": 32 }
  ],
  "processes": [                           // top 20 by CPU
    { "pid": 1234, "user": "pastilhas", "cpuPct": 21.8, "memPct": 1.2, "command": "radicle-node" }
  ],
  "temp": {                                // null if no hwmon
    "cpuC": 56.0,                          // chosen CPU sensor, °C (null if none matched)
    "cpuLabel": "k10temp · Tctl",
    "sensors": [ { "name": "amdgpu", "label": "edge", "celsius": 55.0 }, ... ]  // every hwmon temp
  },
  "gpu": {                                 // null if no /sys/class/drm gpu_busy_percent
    "busyPct": 0, "vramUsedBytes": 2092957696, "vramTotalBytes": 2147483648
  },
  "net": {                                 // null if /proc/net/dev unreadable
    "rxBytesPerSec": 0, "txBytesPerSec": 0,           // aggregate (excludes lo)
    "interfaces": [ { "name": "eth0", "rxBytesPerSec": 0, "txBytesPerSec": 0 } ]  // active only, busiest first
  },
  "power": {                               // null if unreadable
    "cpuWatts": null,                      // RAPL is root-only by default → usually null
    "gpuWatts": 38.1                       // amdgpu hwmon
  },
  "timestamp": 1785150000000
}

Notes

  • net.*BytesPerSec and power.cpuWatts are deltas since the previous /stats call. The first call returns 0/null for these; steady-interval polling gives stable numbers.
  • cpuWatts is usually null — RAPL energy_uj is root-only unless a udev rule opens it. gpuWatts works.
  • Any section can be null on hardware that doesn't expose it — render defensively.

GET /api/system-monitor/pm2

{
  "processes": [
    { "id": 0,                 // pm2 id (pm_id) — use this for the logs endpoint
      "name": "officer",
      "status": "online",      // online | stopped | errored | …
      "pid": 3339851,          // OS pid, or null
      "cpuPct": 0,
      "memBytes": 10354688,
      "restarts": 44,
      "uptimeMs": 420000 }     // 0 unless status === "online"
  ],
  "error": "…"                 // present only if pm2 couldn't be read
}

GET /api/system-monitor/docker

{
  "containers": [
    { "id": "abc123def456",    // short id (12 chars) — use for the logs endpoint
      "name": "jellyfin",
      "image": "jellyfin/jellyfin",
      "state": "running",      // running | exited | …
      "status": "Up 3 hours",
      "ports": "0.0.0.0:9301->8096/tcp" }
  ],
  "error": "…"
}

Live logs (SSE)

Both stream one data: <log line> frame per line, plus : hb heartbeat comments every 15 s. The server kills the underlying tail when the connection closes. Open with EventSource using ?token=.

GET /api/system-monitor/pm2/logs?id=<pm_id>&lines=<n>

  • idnumeric pm2 id from /pm2 (required).
  • lines — initial backlog, default 100, max 1000.
  • Source: pm2 logs <id> --raw (combined stdout+stderr, follows live). The first frames include a short pm2 [TAILING] … header.

GET /api/system-monitor/docker/logs?id=<container>&lines=<n>

  • id — container id or name from /docker (charset-validated).
  • lines — initial backlog (--tail), default 100, max 1000.
  • Source: docker logs -f --tail <n> <id> (combined stdout+stderr).
const es = new EventSource(`/api/system-monitor/pm2/logs?id=0&lines=150&token=${token}`);
es.onmessage = (e) => appendLine(e.data);
// close es to stop the tail