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:
co-authored by
Claude Fable 5.1
parent
468f45251d
commit
b05a053920
+98
-4
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user