**Gate defect found & fixed (the "does not lint" bug).** `.agents/validate.sh` — the phase gate — ran `python3 -m pytest` on `/usr/bin/python3` + `~/.local` site-packages (not the uv venv), ran `ruff check .` *only if* a ruff happened to be on PATH, and **never ran pyright or the coverage floor**. Rewrote it to the project's real gate: cd to repo root → `uv run pytest --cov --cov-fail-under=94` → `uv run ruff check src tests agent` → `uv run pyright` → `node --check web/static/*.js` → hygiene gates; a missing toolchain now fails loudly. Provoked it to confirm it bites (floor + a seeded F401 both fail).
**Verification (all green)**
- `bash .agents/validate.sh` → **exit 0**: 356 passed, 6 skipped · coverage **94.75%** (floor 94) · ruff `All checks passed!` · pyright `0 errors` · node syntax clean
- `uv run pytest --cov` → `severity.py` **100%**; `uv run pytest -m live --live` → **6 passed**; `uv pip list --outdated` → empty
- Criteria: `classify('cpu.usage_pct',…)` → `fail`/`ok`/`None` ✓ · `/api/ups/snapshot` carries `severity` beside `charge_pct`/`load_pct`/`battery_runtime` (apc now live-`warn` at 498 s — threshold working) ✓ · no `.sev-*`/synonym vocabulary (only a comment saying so) ✓ · pytest/coverage/lint ✓ · dev server restarted, `/api/health` ok 6/6 sources, all pages+assets 200 ✓
- Chromium against live fleet: `severityFrom` cell→`fail`, bare→`warn`, null→`''`, unrated→`''`; `hero` fail → `rgb(207,34,46)`, null → `n/a` with **no** verdict class, warn+bar 42%; all four pages render unchanged → `data/logs/phase01-final-{index,system,llm,ups}.png`
**Docs (full review, incl. pre-existing drift).** README: `/ups` row claimed "upsd on 3493" (fleet is 3051, stated elsewhere in the same file); config table omitted `DASH_HOST/PORT`, llm/ups `*_INTERVAL_SECONDS`, `*_TIMEOUT_SECONDS`, `GUFO/VLLM_API_KEY`, `POLL_MS_*`, `DEBUGPY` (+ dedup); severity bullet omitted `disks`/`gpus[]`/rated LLM cells and the ratio rescale; agent section listed only `/snapshot` though `/info`+`/health` exist. AGENTS.md: same 3493 claim; `install-agent.sh --token-file` (its flags are `--bind/--port/--token/--rotate-token/--status/--uninstall` — it hands *the agent* `--token-file`); gap 3 "Two commits" (now 6, still no `origin`); gap 1 given the sudoers install command + verified live gating reason; coverage line now 80% repo floor vs 94% gate floor; cyberpower `identity.model: GX1500U` vs `ups.description` quirk. `.env.dev.example`: `DEV_FORWARDS` remote port 3493→3051 and engine URLs that pointed at `127.0.0.1` with nothing forwarding them.
**Deviations.** No `ruff format` (30 files would reformat; the project gate is `ruff check` + pyright — reformatting completed phases is out of scope). **No live-infra changes**: enabling the battery-test buttons needs `deploy/nut-sudoers` installed on driveripper + a nut-server restart and a scoped `[dashboards]` `instcmds` user — prod write with a documented shutdown risk (AGENTS gap 1), so repo-side only. 36 pre-existing `ResourceWarning: unclosed database` from test fixtures are noise, not failures; left rather than churn ~15 fixtures. `severity.attach()` is unit-tested but has no production call site until phases 02–05.
**Next pending phase:** `02_overview_fleet_and_alerts` (`01_alerts_builder.md`).
3.7 KiB
3.7 KiB
Task 01 — Severity module
Phase: 01_severity_language · Source: TODO.md:5 — "proper green (good), yellow (warn), red (danger) color coding" and TODO.md:7 — "charge, load, and runtime left to be big, obvious, and color coded"
Story: n/a
Objective
Move the good/worrying/bad decision out of the browser and into one Python module, so the four pages cannot drift into four different opinions about what red means.
Today the thresholds live in call sites: page-system.js:129 passes 75, 90,
page-system.js:139 and :144 pass 80, 92, page-index.js:89 passes 80, 92,
page-llm.js:195 and :307 pass 75, 90, and Dash.severityFor (app.js:105) defaults
to 75/90. That is the drift this task ends.
Work
src/dashboards/severity.py(new) —THRESHOLDS: dict[str, tuple[float, float]]keyed by metric name, with the ratified defaults:cpu.usage_pct75/90,mem.used_pct80/92,disk.used_pct80/90,cpu.temp_c75/90,gpu.busy_pct85/95,gpu.mem_used_pct85/95,llm.kv_cache_usage_pct75/90,ups.charge_pct50/20,ups.battery_runtime600/300,ups.load_pct70/90.INVERTED: set[str]— metrics where lower is worse (ups.charge_pct,ups.battery_runtime), so callers cannot get the direction wrong.classify(metric: str, value: float | None) -> str | Nonereturningok,warn,fail, orNone.Nonein →Noneout, always.attach(metric: str, value: float | None) -> dictreturning{"value": value, "severity": classify(...)}for the payload shapes that are plain numbers, andwith_severity(metric, cell)for payloads that already are{"value":…, "source":…, "derived":…}(LLM metrics) — additive, never a rename.- Module docstring stating the two rules: unknown is not ok (it is
None, the page rendersn/awith no colour), and a threshold exists nowhere else in the app. Ties to theAGENTS.md"never invent a number" rule.
tests/test_severity.py(new) —- boundaries inclusive on the warn edge (
75 → warn,74.9 → ok), danger edge (90 → fail), every metric in the table, both directions for the inverted ones, unknown metric name →None(no exception),Noneandfloat("nan")→None. - a table sanity test asserting each entry's bounds are ordered correctly for its direction, so a future edit cannot silently invert a metric.
- ASSUMPTION: the numbers above were confirmed in chat on 2026-10-16; they match what the call sites already pass, except the two UPS metrics which are new.
- ASSUMPTION: the vocabulary is
ok|warn|fail, notok|warn|danger— it is whatdash.css(.bar.ok,.dot.warn,.chip.fail,ul.checks li.fail) andsources/nut.py'shealth.checks[].statusalready use. The TODO's word "danger" maps tofail; do not introduce a synonym for the same colour. - ASSUMPTION:
sources/nut.py's existinghealth.checks[].statusvalues are left untouched (additive-only anchor).severityis a new field on values, computed by this module;nut.assess()keeps its own judgement about conditions.
- boundaries inclusive on the warn edge (
Testing & Quality
- Unit only, no I/O, no fixtures needed.
- Coverage: >90% on
severity.py.
Completion Criteria
uv run pytest tests/test_severity.py -vgreenuv run python -c "from dashboards.severity import classify as c; print(c('cpu.usage_pct',95), c('cpu.usage_pct',60), c('cpu.usage_pct',None), c('ups.charge_pct',15))"→fail ok None failuv run pytestgreen overall;ruff+pyrightclean- Nothing else changed yet — this task adds a leaf module and its tests