Files
platform/SYSTEM_MONITOR_API.md
T
pastilhasandClaude Opus 5 d56be0301d retire the single-user claim from the docs it outlived
CLAUDE.md asserted "single-user is a hard invariant, not a stage" while
users held six rows and role_capabilities held grants. Every doc that
repeated it is corrected here, in prose and in the code comments that
carried the same claim.

The accurate statement is narrower: one owner who bypasses every check,
other accounts holding only what their role is granted, and a set of
capabilities — terminal, chat, files, tasks, items, desktop, browser — that
are structurally ungrantable because they execute as the owner's OS user.

TODO.md gains a Multi-user section for what the read turned up: no way to
create a second account, dashboards.id colliding across users, authorize.ts
untested, pty/vault/opencode taking no identity, Radicale still owner_only.

claude-sidecar-isolation.md's open question is answered rather than left
open — the per-email spawn model is dead weight, because chat is an
execution capability and no second account can ever reach it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 21:58:44 +00:00

132 lines
5.4 KiB
Markdown
Raw 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 belong to the `server-admin` capability, which is `kind: 'admin'` and
therefore never grantable — a non-owner account gets `403` here whatever its role. The full
**officer-mobile** client (which authenticates as the owner) has access; the music app does not.
- Note for anyone who read this before 2026-08-07: the old rule was that non-owner accounts were
confined to a hardcoded `/api/auth` + `/api/music`. That list is gone, replaced by per-role
capability grants. The *outcome* for these routes is unchanged — still owner-only — but the reason is
now the capability's kind, not a two-element array.
- 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
```