Files
platform/SYSTEM_MONITOR_API.md
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

128 lines
5.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.
```jsonc
{
"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`
```jsonc
{
"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`
```jsonc
{
"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>`
- `id`**numeric** 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).
```js
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
```