Sensors: read-only tokens, live stream, Prometheus, status sensors, widgets, embeds

- sensors:read tokens open the sensor endpoints (here and on managed machines
  through the hub) and nothing else; full tokens are never accepted from a URL
- /api/sensors/stream: a shaped query that keeps answering, over SSE
- /metrics: Prometheus text format
- services, updates, ZFS and SMART as background status sensors
- widget library over the sensor API; custom widgets on the dashboard
- /embed/widget: one live widget in its own bundle, the only frameable path
- Dashboard tab is now Monitors

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Mitchell-Johnson
2026-09-19 07:37:10 -07:00
co-authored by Claude Fable 5.1
parent 468f45251d
commit b05a053920
20 changed files with 950 additions and 56 deletions
+98 -4
View File
@@ -76,9 +76,103 @@ POST /api/sensors/query
- `?full=1` → every value is the whole sensor object instead of the bare value. (In the
GET form `full` is therefore not usable as a key.)
## Live stream
`GET /api/sensors/stream` is a shaped query that keeps answering: the same object, pushed
as a server-sent event after every sample. The query is `?q=<URL-encoded JSON shape>` or
the flat form; `?full=1` as above; `?every=<ms>` spaces events out for a consumer that
doesn't want them at the sampling rate. A keepalive comment goes out every 25 s, and
`X-Accel-Buffering: no` keeps nginx from holding events back.
```js
new EventSource('/api/sensors/stream?token=…&q=' + encodeURIComponent(JSON.stringify({ cpu: 'cpu.usage', fans: 'fan.*' })))
.onmessage = (e) => render(JSON.parse(e.data))
```
## Auth: read-only tokens
Settings → Access tokens → **+ Read-only sensor token** (or `POST /api/tokens/read
{"name"}` with a session). Scope `sensors:read`: it opens every endpoint in this file,
here and on each managed machine through the hub (`/api/nodes/<machine>/sensors…`; the
hub talks to the node as itself, the caller's token stops at the hub), and nothing else.
- Send it as `Authorization: Bearer <token>`, or as `?token=<token>` where headers are
impossible (an iframe, a browser `EventSource`).
- A **full-control token is never accepted from a URL**, and a read-only token is never
accepted anywhere outside this file's endpoints.
## Prometheus
`GET /metrics` (or `/api/sensors/prometheus`, which also works through the hub as
`/api/nodes/<machine>/sensors/prometheus`). One gauge family per kind of measurement,
base units, the sensor ID and its real-world names as labels:
```
hamlab_temperature_celsius{id="temp.coretemp.core_0",chip="coretemp",label="Core 0"} 50
hamlab_usage_percent{id="fs.root.used_pct",fstype="ext4",mount="/"} 10.1
hamlab_state{id="ups.state",model="CPS CP1500PFCRM2U",value="Normal"} 1
hamlab_updates_pending{id="updates.pending",manager="apt"} 3
```
```yaml
scrape_configs:
- job_name: hamlab
authorization: { credentials: <read-only token> }
static_configs: [{ targets: ['10.0.0.234:8100'] }]
```
## Status sensors
Not everything is a /proc number. These are read in the background on their own clocks
(each sensor's `t` is its real age) by `internal/api/status_sensors.go`:
```
services.failed|failed_names|running every 30 s
updates.pending|security|reboot_required every 5 min
zfs.<pool>.health|healthy|size|used|used_pct|frag every 1 min
smart.<dev>.passed|warnings|power_on|wear every 10 min, never waking a sleeping disk
```
## Widgets
A widget is a spec — `{type, sensor, title?}` — over this API and nothing else
(`web/src/widgets.tsx`, fed by `useSensorStream` in `web/src/sensors.ts`). Types: `stat`,
`gauge`, `bar`, `spark`, and `list` for a pattern. Scale and colour come from the
sensor's own `min`/`max`/`warn`/`crit`.
- **In Ham Lab:** Monitors → Customize → **+ Add widget**. Saved with the layout,
per machine; all of a page's widgets share one stream.
- **Anywhere else:** `/embed/widget` is one widget alone on a page, live, in its own small
bundle (the console never loads), and the only path Ham Lab allows to be framed.
```
/embed/widget?type=gauge&sensor=cpu.usage&token=<read-only token>
&machine=grid &title=… &theme=dark|light|cyberpunk|retro &every=<ms> &bare=1
```
### Glance
Glance's own `custom-api` widget refetches on page load / cache expiry — fine for slow
facts, never live. An `iframe` of the embed is live.
```yaml
- type: iframe
title: core CPU
source: https://hamlab.bigham.dev/embed/widget?type=gauge&sensor=cpu.usage&bare=1&token=<read-only token>
height: 130
- type: custom-api
title: grid
cache: 1m
url: https://hamlab.bigham.dev/api/nodes/grid/sensors/query?pool=zfs.tank.used_pct&failed=services.failed&updates=updates.pending
headers: { Authorization: Bearer <read-only token> }
template: |
<div>tank {{ .JSON.Float "pool" }}% · {{ .JSON.Int "failed" }} failed · {{ .JSON.Int "updates" }} updates</div>
```
## Not yet
Read-only scoped tokens, Prometheus `/metrics`, a push stream, widgets built on this,
an embeddable widget page, and non-metric sensors (services, SMART, ZFS, updates). Until
scoped tokens exist, **do not hand a Ham Lab token to a dashboard** — today's tokens are
full control of the machine.
- The stock Monitors modules (CPU, Temperatures, Network…) still read `/api/metrics`;
only custom widgets run on sensors. Porting them is the remaining dogfooding.
- Per-unit service sensors, containers, token restriction to ID patterns, a fleet-wide
query in one request.