The five UI items in TODO.md become an executable pipeline. Planning files
only — no application code changed.
- 01_severity_language: one Python threshold table + `severity` on the
values the pages colour, plus the .hero style. Today the thresholds are
magic numbers at six JS call sites (75/90 here, 80/92 there), which is
exactly how "same colour, four meanings" happens.
- 02_overview_fleet_and_alerts: /api/summary gains a derived `alerts`
list; the page drops to fleet + warnings. Unconfigured sources must not
alert — that is invariant 3, tested.
- 03_system_per_host_panels: per-host hero cards, no per-core, shared
multi-host charts and the detail picker deleted. The six card metrics
already exist in the payload, so the first task pins them with tests
instead of inventing fields.
- 04_llm_chart_readability: charts.js already exports sparkline() and no
page calls it — that is why the page reads flat. Port llm_monitor's
drawSpark behaviour (fixed max, right growth, p95 foots) and keep the
provenance tags.
- 05_ups_hero_battery_glyph: charge/load/runtime hero + an SVG battery
whose fill is the charge, verified against both real UPSes and the
low-charge/on-battery states only the simulator can produce.
PLAN.md records the ratified anchors and the deviation worth knowing:
severity vocabulary is ok|warn|fail, matching dash.css and nut.py, not the
"danger" wording in the proposal — one synonym for one colour.
TODO.md cleared to a bare heading; the items are now phases. .gitignore
stops ignoring all of .agents/ (the roadmap is project history) while
keeping phase-sessions/ and pipeline.log out. deploy/nut-sudoers rewritten:
its header still blamed a port mystery that is solved; the remaining need
is reading upsd.users so adding a scoped dashboards user does not delete
the accounts upsmon authenticates as.
LLM-Generated: true
Nothing was listening on 3493, so the dashboard said "connection refused"
and fell back to the simulator while upsd was healthy the whole time. It
binds a nonstandard port: `ss -ltnH` + a NUT handshake found it, and
`upsc -L localhost:3051` lists both batteries. No host config had to
change — dev now forwards 3494 -> 3051 and `/ups` shows real numbers,
including an honest "battery is 5.3 years old" on the APC.
Docs now say what to check before blaming the service (`ss -ltnH`,
`printf 'LIST UPS\n' | nc 127.0.0.1 PORT`, `upsc -L host:port`), and the
remaining UPS gap is rewritten: reads work, instant commands still need a
scoped upsd.users entry — which needs a *read* of that file, because
`tee` writing it blind would drop the users upsmon authenticates as.
Real-hardware quirks recorded while verifying: battery.mfr.date is the
date that means something (battery.date is often an unset placeholder),
CyberPower reports `battery.mfr.date: CPS`, and neither driver reports a
battery temperature — all three already render as unknown rather than a
invented number.
LLM-Generated: true
README advertised `uv run pytest -m live --live`, but there was no --live
option and no `live` marker registered — the command errored out, so the
one workflow that catches an upstream renaming a series did not exist.
- conftest adds --live and skips `live`-marked tests without it, so the
default run stays hermetic (258 passed, 6 skipped, no network).
- tests/test_live_fleet.py drives the real code paths (collectors.*,
NutMonitor.collect) against the fleet, both engines, and upsd. Each
skips when its source is unset, and none of them may write — a battery
test on live hardware is not a unit test. The host test polls twice
because a rate has no second point on the first scrape.
- The agent's real-/proc test stays in the default run and is explicitly
not `live`: it touches only the box running pytest.
- dev-tunnels.sh grows DEV_FORWARDS for services already listening
remotely, which is how upsd gets reachable without opening 3493;
pidfiles are now host+port so one host can carry two tunnels.
- requires-python, ruff and pyright were still on 3.13 while the venv and
Containerfile are 3.14, so the lock resolved for a stack nobody deploys.
LLM-Generated: true
The brief asked for an AGENTS.md capturing the overall plan and it never
landed, while two files already pointed readers at it. It now holds the
invariants (read-only by construction, never invent a number,
unconfigured is not broken, no CDN, no guessed metric names), the locked
decisions with their reasons, the verified fleet facts, the commands, and
the four known gaps.
Also corrected the docs that had drifted:
- upsd config lives in /etc/ups on driveripper, not /etc/nut, and the
real blocker is that upsd binds nothing on 3493 — with the sudo rules
still needed written down in deploy/nut-sudoers.
- .env.example told operators to set `LISTEN 0.0.0.0` and `actions = -`;
both are worse than needed. Loopback + ssh forward, and per-command
instcmds instead of granting load.off.
- nut.py claimed the command path is gated three times and that the
confirmation asks for the UPS name; it is gated four times and asks
for DANGER.
- .gitignore described a tracked .agents/ roadmap that does not exist.
LLM-Generated: true
The README referenced `deploy/install-agent.sh` and a `Containerfile` that
did not exist, so a fresh clone had no way to actually run this anywhere.
* agent: `--token-file` (and AGENT_TOKEN_FILE). The token guards hostname,
mounts, disk and GPU models and process names, and argv is world-readable
through `ps`, so a file has to win over the flag. Missing or empty token
files are fatal — falling back to "no token" would expose the host.
* deploy/install-agent.sh: creates a system user, generates the token once
(re-runs reuse it, --rotate-token for a new one), stores it 0600
root-owned, and runs the agent under ProtectSystem=strict with no
capabilities; --status and --uninstall included.
* Containerfile: two-stage, uv.lock resolved in the builder and slim Python
in the runtime. Verified with podman — the image serves /api/health as an
unprivileged user and writes its history under /var/lib/dashboards.
UV_PROJECT_ENVIRONMENT and --no-editable are what make the venv survive
being copied into the runtime stage.
* .containerignore keeps .git, .venv, data/ and .env out of the build
context.
258 tests, 94% coverage; ruff and pyright clean.
LLM-Generated: true
Three dashboards served by one FastAPI app with a SQLite history store
(raw / 1m / 1h) and hand-written SVG charts: no Prometheus, no Grafana,
no CDN, no build step. Nothing is fetched from the internet at runtime.
* system: a stdlib-only agent (agent/host_agent.py) reports /proc and
/sys counters; the dashboard turns them into rates over a labelled
window, so every number says how it was measured.
* llm: gufo/llama.cpp and vLLM /metrics are parsed by an in-house
exposition parser. Measured delta/elapsed throughput beats a native
per-request gauge, and each value carries its provenance.
* ups: a NUT wire-protocol client plus health checks that name what
they looked at. Commands are gated by NUT_ALLOW_ACTIONS,
DASH_READ_ONLY and a DANGER confirmation, re-checked server-side.
Missing values render as n/a rather than 0, and an unconfigured source
answers 404 {"configured": false} without dialling anything, so "not
deployed here" never looks like an outage.
253 tests at 94% coverage against captured real fixtures; ruff and
pyright clean.
LLM-Generated: true