chore(agent): phase roadmap from TODO.md, 5 phases
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
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
# 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
|
||||
|
||||
1. `src/dashboards/severity.py` (new) —
|
||||
- `THRESHOLDS: dict[str, tuple[float, float]]` keyed by metric name, with the ratified
|
||||
defaults: `cpu.usage_pct` 75/90, `mem.used_pct` 80/92, `disk.used_pct` 80/90,
|
||||
`cpu.temp_c` 75/90, `gpu.busy_pct` 85/95, `gpu.mem_used_pct` 85/95,
|
||||
`llm.kv_cache_usage_pct` 75/90, `ups.charge_pct` 50/20, `ups.battery_runtime` 600/300,
|
||||
`ups.load_pct` 70/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 | None` returning `ok`, `warn`,
|
||||
`fail`, or `None`. `None` in → `None` out, always.
|
||||
- `attach(metric: str, value: float | None) -> dict` returning
|
||||
`{"value": value, "severity": classify(...)}` for the payload shapes that are plain
|
||||
numbers, and `with_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
|
||||
renders `n/a` with no colour), and a threshold exists nowhere else in the app. Ties to
|
||||
the `AGENTS.md` "never invent a number" rule.
|
||||
2. `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), `None` and `float("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`**, not `ok|warn|danger` — it is what
|
||||
`dash.css` (`.bar.ok`, `.dot.warn`, `.chip.fail`, `ul.checks li.fail`) and
|
||||
`sources/nut.py`'s `health.checks[].status` already use. The TODO's word "danger" maps
|
||||
to `fail`; do not introduce a synonym for the same colour.
|
||||
- ASSUMPTION: `sources/nut.py`'s existing `health.checks[].status` values are left
|
||||
untouched (additive-only anchor). `severity` is a new field on *values*, computed by
|
||||
this module; `nut.assess()` keeps its own judgement about *conditions*.
|
||||
|
||||
## Testing & Quality
|
||||
|
||||
- Unit only, no I/O, no fixtures needed.
|
||||
- Coverage: **>90%** on `severity.py`.
|
||||
|
||||
## Completion Criteria
|
||||
|
||||
- [ ] `uv run pytest tests/test_severity.py -v` green
|
||||
- [ ] `uv 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 fail`
|
||||
- [ ] `uv run pytest` green overall; `ruff` + `pyright` clean
|
||||
- [ ] Nothing else changed yet — this task adds a leaf module and its tests
|
||||
Reference in New Issue
Block a user