phase: 07_llm_monitor_style_pages
**Phase 07 final verification pass — all tasks already in `complete/`; every criterion re-verified live.**
- **Old slot table gone**: no `#slots-card`/`#slots-table`/`renderSlots` in HTML/JS/CSS (only the new grid's `.slots-empty` *class*); pinned by `test_the_broken_slot_table_is_gone_from_html_js_and_css`.
- **`tests/fixtures/slots_gufo.json`**: present; live `GET ${GUFO_BASE_URL}/slots` this pass matches it key-for-key (row + `next_token[0]`), so the capture is reproducible; drift proven to fail (mutation tests + live `test_gufo_slots_answer_the_pinned_shape`, 9 live tests pass).
- **Aggregate block**: live page shows `2 of 2 engines`, running/waiting/prompt/decode/total-tokens straight from `fleet_totals()`, dot `ok`, no verdict colour; on a throwaway single-engine instance (:8091, private temp DB) `#fleet` is `hidden`, one full-width strip card + one panel, zero banners → "one clean half".
- **Strip + slot grid on live data**: 2 `.card.stat` (KV bars `bar ok` 13 %/0 %), gufo grid rendered real cards incl. a busy slot (`#2189`, 67849 prompt tok, 99 % cache share, `processing`), vLLM note-only with no grid; no `NaN`/`undefined` anywhere. Screenshots: `data/logs/llm-phase07-final-live-{two-engines,single-engine}.png`, `ups-charts-aria-labels.png`.
- **Defect found in earlier work and fixed**: `/ups`' four `LineChart`s shipped no `ariaLabel`, violating AGENTS.md's "every chart names itself for assistive tech" (`/system`+`/llm` were pinned, `/ups` wasn't) → added labels to `page-ups.js` and `test_every_ups_chart_names_itself_for_assistive_tech`; browser-verified `role="img"` + label on all four.
- **Gate**: `bash .agents/validate.sh` → **exit 0**; `uv run pytest --cov --cov-fail-under=94` → **604 passed, 9 skipped, coverage 97.91 %**; `uv run ruff check src tests agent` → All checks passed; `uv run pyright` → **0 errors**; `node --check` every page script ok; `uv run pytest -m live --live` → **9 passed**; `uv pip list --outdated` empty.
- **Docs**: README + AGENTS.md reviewed end-to-end; fixed test count (582→604), `/llm` source column + both architecture diagrams now name gufo `/slots`, AGENTS.md agent line gained `GET /schema` + status page, ariaLabel convention now covers all three chart pages, README Testing block now lists `bash .agents/validate.sh`. Env-var table, agent/install flags, Containerfile facts, `/api/*` shapes, range chips, remote URL all verified accurate — no other changes needed.
- **Live-infra**: no fleet/host/config change; the temp :8091 instance, its temp DB and the fake processes are removed; dev server restarted and `/api/health` `ok` (all 6 sources ok). One thing I deliberately did **not** touch: the gitignored local `.env.dev` still carries a comment claiming the NUT `[dashboards]` account has only battery-test `instcmds`, whereas AGENTS.md/README record `instcmds = all` (verified on hardware 2026-10-17) — operator-owned file, flag for the operator to reword.
- **Next pending phase**: `08_events_paging_and_copy`.
This commit is contained in:
@@ -0,0 +1,55 @@
|
||||
# Phase 07 — The LLM page looks like llm_monitor again
|
||||
|
||||
**Source:** `TODO.md` L5 — "make this look like the llm_monitor project": "The LLM dashboard is nothing like ~/Experimental/llm_monitor. I'm missing the 'slots' tables (I really liked that). I'm missing the aggregate throughput at the top. Basically, make this look like the llm_monitor project. The gufo slots table that exists isn't even working and I want it gone anyway."
|
||||
**Story:** n/a
|
||||
**Context:** `~/Projects/Experimental/llm_monitor/static/index.html` (the reference: fleet-totals section ~L710, shared stats strip ~L743, gufo slot cards `buildSlotCard`/`renderSlots` ~L1179, `/slots` passthrough with `/v1/slots` fallback ~L750), `src/dashboards/web/pages/llm.html` + `src/dashboards/web/static/page-llm.js` (phase-04 layout: per-engine heroes, `CELLS` sparkline list, alarm chips, four trend charts), `src/dashboards/sources/llm.py` (`fleet_totals()` ~L327 — already sums decode/prompt tok/s, running/waiting, token totals; `_sessions()` passes gufo's raw `/slots` rows through as `sessions.list`), `scripts/capture-fixtures.sh` (today captures engine `/metrics` only — **no `/slots` capture exists**, which is why the slot table never matched the live shape)
|
||||
|
||||
## Objective
|
||||
|
||||
Port `llm_monitor`'s visual structure onto the LLM page — the aggregate fleet block at the
|
||||
top, the per-engine stats strip, the slot-card grid — while keeping what phase 04 and the
|
||||
locked UI conventions own: the per-engine panel (two throughput heroes, compact sparkline
|
||||
cells, alarm chips, the four half-height trend charts) and every provenance/severity rule.
|
||||
The broken gufo slot table is gone and its replacement is fed by a `/slots` shape pinned
|
||||
from the live server, never from memory.
|
||||
|
||||
`llm_monitor` is the design reference, not a dependency: a separate app, nothing imported.
|
||||
|
||||
- ASSUMPTION (phase, confirmed with the operator): "full restyle" = port the three
|
||||
llm_monitor structures above (aggregate block, stats strip, slot grid). The per-engine
|
||||
panel and chart set stay; `llm_monitor`'s peak-ctx cell does **not** come over because
|
||||
the payload publishes no peak-context number — a shape is not a verdict, and a missing
|
||||
number prints `n/a`, never a borrowed one.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `06_system_page_gpu_verdicts` (todo) — same web assets (`dash.css`, `app.js`, page
|
||||
scripts); land in that order so the CSS token vocabulary is settled first.
|
||||
- `04_llm_chart_readability` (complete) — the layout this phase reworks; its NOTES
|
||||
(`NOTES-chart-set.md`) and provenance/severity conventions bind.
|
||||
|
||||
## Tasks
|
||||
|
||||
1. `01_slots_schema_and_table_removal.md` — delete the broken slot table; extend `capture-fixtures.sh` to record gufo's live `/slots` and pin the shape in a fixture + contract test.
|
||||
2. `02_fleet_totals_block.md` — render the aggregate block at the top from the existing `fleet_totals()` payload (hidden below two engines, as today).
|
||||
3. `03_engine_stats_strip.md` — port the shared stats strip: one card per engine (KV bar, tok/s, sessions; gufo extras; alarm chips), fed only by payload fields that already exist.
|
||||
4. `04_slot_grid.md` — the llm_monitor slot grid: one keyed card per gufo slot, updated in place, from `sessions.list`.
|
||||
|
||||
## Testing & Quality
|
||||
|
||||
- Hermetic: `tests/test_api.py` key-path pins (fleet block, strip, grid markup on served
|
||||
bytes), `node --check` on every touched script, fixture-driven `sessions.list` shape test.
|
||||
- Live: `uv run pytest -m live --live` — the `/slots` fixture test re-asserts against the
|
||||
running gufo (`deskwork:8731`), and the page renders with both engines live and with a
|
||||
single engine (one clean half, not a red panel).
|
||||
- Browser (DoD): side-by-side with `llm_monitor` on its own port against the same live
|
||||
engines; screenshots in `data/logs/` (two-engine, single-engine, gufo-busy states).
|
||||
- Coverage: **>90%** on new/modified code, ≥94% overall gate maintained.
|
||||
|
||||
## Completion Criteria
|
||||
|
||||
- [ ] the old `#slots-card` table is gone from HTML, CSS and JS (and from any test that pinned it)
|
||||
- [ ] `tests/fixtures/slots_gufo.json` exists, is produced by `./scripts/capture-fixtures.sh`, and the contract test fails if the live shape drifts
|
||||
- [ ] top aggregate block shows summed decode/prompt tok/s, running/waiting, token totals — hidden below two engines
|
||||
- [ ] stats strip + slot grid render on live data; single-engine page is one clean half
|
||||
- [ ] `bash .agents/validate.sh` green; screenshots in `data/logs/`
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
# Task 01 — Remove the broken slot table; pin the /slots schema from the live server
|
||||
|
||||
**Phase:** `07_llm_monitor_style_pages` · **Source:** `TODO.md:5` — "The gufo slots table that exists isn't even working and I want it gone anyway."
|
||||
**Story:** n/a
|
||||
|
||||
## Objective
|
||||
|
||||
Delete the slot table that has never matched gufo's live `/slots` shape, and replace
|
||||
"trust a memory of an upstream" (rule 7) with a recorded ground truth: a fixture captured
|
||||
from the running gufo, pinned by a contract test.
|
||||
|
||||
## Work
|
||||
|
||||
1. `scripts/capture-fixtures.sh` — add a gufo `/slots` capture: dial
|
||||
`${GUFO_BASE_URL%/}/slots` with the `/v1/slots` fallback (the same order
|
||||
`llm_monitor`'s passthrough at ~L750 uses — a server that only exposes the alias must
|
||||
still be capturable), save verbatim as `tests/fixtures/slots_gufo.json`. Skip cleanly
|
||||
(with a note) when `GUFO_BASE_URL` is unset, matching the script's existing engine
|
||||
handling.
|
||||
2. Delete the broken table end-to-end: `#slots-card` / `#slots-empty` / `#slots-table`
|
||||
markup in `src/dashboards/web/pages/llm.html`, `renderSlots()` and the dead helpers in
|
||||
`src/dashboards/web/static/page-llm.js`, and the CSS rules in `dash.css` that only that
|
||||
table used (the orphan rule for both `id`s and selectors — `tests/test_api.py` enforces
|
||||
both). Any test that pinned the table's markup or its guessed field names
|
||||
(`slot.state`, `slot.n_prompt`, `slot.decode_per_second`, …) gets the pin removed or
|
||||
turned into the new fixture-based assertion.
|
||||
3. `tests/` — add the shape contract: a table of the `/slots` field paths the slot grid
|
||||
(task 04) will read, asserted against `tests/fixtures/slots_gufo.json` hermetically and,
|
||||
in a `live`-marked test, against the running gufo — the same device as
|
||||
`tests/card_contract.py`. `tests/test_llm_source.py` keeps asserting that
|
||||
`sessions.list` is the passthrough (raw rows, no re-mapping in `sources/llm.py`'s
|
||||
`_sessions()` — the grid, not the source, decides what a slot field means).
|
||||
4. `./scripts/capture-fixtures.sh` — run it against the live fleet and commit the new
|
||||
fixture (serials/PII scrubbed, same as the existing captures).
|
||||
|
||||
- ASSUMPTION: the fixture is the *live* shape — whatever the running gufo answers today —
|
||||
not a README's; if the live server is idle (zero busy slots) at capture time, the
|
||||
capture still records the idle slot rows, which is the shape the grid must handle.
|
||||
|
||||
## Testing & Quality
|
||||
|
||||
- Unit: fixture-based shape test (hermetic), `node --check` on `page-llm.js`,
|
||||
`tests/test_api.py` orphan-`id`/orphan-selector rules still pass after the deletion.
|
||||
- Live: the `live`-marked shape test against `deskwork:8731`.
|
||||
- Coverage: **>90%** on modified code; overall gate (94%) maintained.
|
||||
|
||||
## Completion Criteria
|
||||
|
||||
- [ ] no `#slots-table` / `#slots-card` / `renderSlots` remains in HTML, JS or CSS
|
||||
- [ ] `tests/fixtures/slots_gufo.json` is in the repo and reproducible via `./scripts/capture-fixtures.sh`
|
||||
- [ ] the shape contract fails when the fixture (or the live server) drifts
|
||||
- [ ] `uv run pytest` green; `uv run ruff check src tests agent && uv run pyright` clean
|
||||
@@ -0,0 +1,54 @@
|
||||
# Task 02 — Aggregate throughput block at the top
|
||||
|
||||
**Phase:** `07_llm_monitor_style_pages` · **Source:** `TODO.md:5` — "I'm missing the aggregate throughput at the top."
|
||||
**Story:** n/a
|
||||
|
||||
## Objective
|
||||
|
||||
The page leads with the fleet: one block above the engine panels with the summed numbers
|
||||
llm_monitor's `#fleet` section shows — decode/prompt tok/s, running/waiting, token totals,
|
||||
engines answering — which `fleet_totals()` in `sources/llm.py` already computes over the
|
||||
engines that answered. Phase 04 retired the summed display to counts only; this task brings
|
||||
the sums back in llm_monitor's shape.
|
||||
|
||||
## Work
|
||||
|
||||
1. `src/dashboards/web/pages/llm.html` + `page-llm.js` — replace the current `#totals`
|
||||
card (counts only) with the llm_monitor-style fleet block: status dot, "all engines",
|
||||
an `N engines · …` tag, the note "sums only engines answering now", and the
|
||||
`running / waiting / prompt tok/s / decode tok/s / total tokens` row, rendered from
|
||||
`fleet_totals()` via the existing payload (`/api/llm/snapshot` → `fleet`).
|
||||
Provenance discipline stays: the sums carry no severity (throughput has no
|
||||
good/bad — the same rule phase 04 wrote), and a dead engine's last-good values
|
||||
contribute nothing to a total presented as current (`engines_live` vs `engines_total`
|
||||
makes that visible, exactly as `fleet_totals()`'s docstring promises).
|
||||
2. Keep the existing visibility rule — hidden below two engines (a cross-engine sum of one
|
||||
engine duplicates that engine's own panel; the page comment at `#totals` says so and
|
||||
`llm_monitor` hides the same way).
|
||||
3. `tests/test_api.py` — pin the `fleet` key paths the block reads
|
||||
(`engines_total`, `engines_live`, `decode_tokens_per_second`,
|
||||
`prompt_tokens_per_second`, `requests_active`, `requests_waiting`, `tokens_total`) with
|
||||
and without targets configured, and the served-markup presence/absence with one vs two
|
||||
engines.
|
||||
4. `dash.css` — block styling from the existing token vocabulary (`.metrics`/`.metric`
|
||||
patterns already in the file; no new palette).
|
||||
|
||||
- ASSUMPTION: no backend change — `fleet_totals()` already sums exactly the numbers the
|
||||
block prints; if a wanted number turns out to be missing from the payload, it stays
|
||||
`n/a` rather than inventing a new metric (rule 2).
|
||||
|
||||
## Testing & Quality
|
||||
|
||||
- Unit: `tests/test_api.py` (key paths + markup, one vs two engines), `node --check`.
|
||||
- Live: `uv run pytest -m live --live` with both engines; the block sums the two live
|
||||
panels' numbers (a hand-check against the per-engine values on the same screen).
|
||||
- Browser (DoD): screenshots in `data/logs/` — two-engine (block visible, sums correct),
|
||||
single-engine (block absent, one clean half).
|
||||
- Coverage: **>90%** on modified code; overall gate (94%) maintained.
|
||||
|
||||
## Completion Criteria
|
||||
|
||||
- [ ] the top block prints summed decode/prompt tok/s, running/waiting, token totals from `fleet_totals()`
|
||||
- [ ] hidden below two engines; a dead engine dims the tag and contributes nothing to the sums
|
||||
- [ ] no severity colour anywhere on the block
|
||||
- [ ] `uv run pytest` green; `uv run ruff check src tests agent && uv run pyright` clean
|
||||
@@ -0,0 +1,54 @@
|
||||
# Task 03 — Per-engine stats strip
|
||||
|
||||
**Phase:** `07_llm_monitor_style_pages` · **Source:** `TODO.md:5` — "Basically, make this look like the llm_monitor project." (the shared stats strip, `index.html` ~L743)
|
||||
**Story:** n/a
|
||||
|
||||
## Objective
|
||||
|
||||
Port llm_monitor's shared stats strip — one card per engine between the fleet block and
|
||||
the engine panels — so the read-across-the-room numbers (throughput, KV pressure,
|
||||
sessions, the gufo extras) sit in one row instead of being scattered inside the panels.
|
||||
The panels keep their detail; the strip is the summary.
|
||||
|
||||
## Work
|
||||
|
||||
1. `src/dashboards/web/pages/llm.html` + `page-llm.js` — one stat card per configured
|
||||
engine: status dot + name + timestamp, the KV-cache bar (the one rated cell — colour
|
||||
from the payload's `severity`, the existing `scale: 100` ratio→percent conversion),
|
||||
prompt tok/s and decode tok/s (with the provenance tags, uncoloured), total tokens,
|
||||
and the session counts (gufo: active slots via `sessions.list` length/`active`; vLLM:
|
||||
running/waiting from its count metrics).
|
||||
2. Gufo-only extras on the gufo card, vLLM-only rows on the vLLM card — the existing
|
||||
`only` scoping in `CELLS` is the model: prefix-cache hit %, spec-accept %
|
||||
(gufo), queue time (vLLM). **No peak-ctx cell** — the payload publishes no
|
||||
peak-context number (phase assumption; a missing number prints `n/a` or the row is
|
||||
absent, never a borrowed one).
|
||||
3. `preemptions` / `device_lost` remain **alarm chips that exist only while non-zero**
|
||||
(phase-04 rule, `.chip.fail` pattern) — on the strip card, not in a permanent cell.
|
||||
4. `dash.css` — the strip is a new layout of existing vocabulary (`.card`, `.metrics`,
|
||||
the KV bar already has its tokens); no new palette, no `.sev-*` family.
|
||||
5. `tests/test_api.py` — pin the strip's served markup (one card per engine, gufo/vLLM
|
||||
scoping) and the payload field paths it reads; `node --check`.
|
||||
6. The engine panels keep their hero row and the sparkline cells the strip *does not*
|
||||
show (latency means + p95s, queue mean); cells that end up rendered in **both** strip
|
||||
and panel get demoted from the panel to the strip — one value, one home (the
|
||||
"one value drawn twice" rule from the UPS hero row).
|
||||
|
||||
- ASSUMPTION: the strip renders whatever the payload already sends — no new field in
|
||||
`sources/llm.py` — so a single-engine deployment shows one card and the hidden
|
||||
fleet block, and nothing on the strip is ever a number the upstream did not publish.
|
||||
|
||||
## Testing & Quality
|
||||
|
||||
- Unit: `tests/test_api.py` (markup + field paths), `node --check`.
|
||||
- Live: both engines, then single-engine (one clean card, no red half).
|
||||
- Browser (DoD): screenshot in `data/logs/` — strip visible, KV bar coloured from the
|
||||
payload verdict, alarm chip present only in a non-zero state.
|
||||
- Coverage: **>90%** on modified code; overall gate (94%) maintained.
|
||||
|
||||
## Completion Criteria
|
||||
|
||||
- [ ] one card per configured engine, between the fleet block and the panels, with KV bar + tok/s + sessions
|
||||
- [ ] no value printed twice (strip vs panel); gufo/vLLM rows scoped correctly
|
||||
- [ ] alarm chips absent at zero, present and red while non-zero
|
||||
- [ ] `uv run pytest` green; `uv run ruff check src tests agent && uv run pyright` clean
|
||||
@@ -0,0 +1,53 @@
|
||||
# Task 04 — The gufo slot grid
|
||||
|
||||
**Phase:** `07_llm_monitor_style_pages` · **Source:** `TODO.md:5` — "I'm missing the 'slots' tables (I really liked that)."
|
||||
**Story:** n/a
|
||||
|
||||
## Objective
|
||||
|
||||
Bring back what the operator liked about llm_monitor: one card per gufo slot, keyed and
|
||||
created once, updated in place on refresh — fed by `sessions.list` (the raw `/slots`
|
||||
passthrough whose shape task 01 pinned in `tests/fixtures/slots_gufo.json`).
|
||||
|
||||
## Work
|
||||
|
||||
1. `src/dashboards/web/pages/llm.html` + `page-llm.js` — implement the grid with
|
||||
llm_monitor's `buildSlotCard`/`renderSlots` idiom (~L1179/L1209): a `.slot` card per
|
||||
slot id (keyed `Map`, created once, patched in place — a busy slot does not get
|
||||
rebuilt and re-announced on every poll), the card top shows the slot id + model, and
|
||||
the per-slot fields come **only from the pinned fixture's shape** (state/busy
|
||||
marker, prompt + generated token counts, cache share, per-slot speed — the exact keys
|
||||
the fixture records; a field the live shape does not carry prints `n/a`, never a
|
||||
guessed fallback chain like the old table's).
|
||||
2. The grid lives in the gufo section under the "sessions (GET /slots)" label; the
|
||||
empty state is the honest one (no slots answered → the panel says so, it does not
|
||||
render a card of nothing). A queued (deferred) request is **not** in `/slots` — the
|
||||
existing note under the grid stays (it is information, not flavour).
|
||||
3. vLLM section: keep the one-line note that vLLM exposes no per-slot JSON and its
|
||||
metrics are the equivalent of the session grid — no grid, no fake rows.
|
||||
4. `dash.css` — `.slot` / `.slot-top` / `.slot.busy` styling ported to the existing token
|
||||
vocabulary (busy state is a border/glyph, not a new palette entry); `node --check`.
|
||||
5. `tests/test_api.py` — pin the grid markup on served bytes (container, empty state,
|
||||
label); the fixture-driven render path is covered by the shape contract from task 01.
|
||||
|
||||
- ASSUMPTION: the slot card reads the fixture's fields one-to-one (no `a ?? b` fallback
|
||||
chains — the old table's `slot.n_prompt ?? slot.prompt_tokens` style is exactly the
|
||||
guessing that made it "not even working"); if a live capture shows two spellings of one
|
||||
fact, the contract names the canonical one and the other is not rendered.
|
||||
|
||||
## Testing & Quality
|
||||
|
||||
- Unit: fixture-driven render test (the pinned shape through `renderSlots`-equivalent
|
||||
logic, via served-bytes assertions), `node --check`.
|
||||
- Live: gufo with at least one active request (a card with real numbers) and an idle
|
||||
server (cards show idle, no invented speed).
|
||||
- Browser (DoD): screenshots in `data/logs/` — idle, one busy slot, multiple busy slots;
|
||||
refresh in place (cards do not flicker/rebuild).
|
||||
- Coverage: **>90%** on modified code; overall gate (94%) maintained.
|
||||
|
||||
## Completion Criteria
|
||||
|
||||
- [ ] one keyed card per gufo slot, created once and updated in place across refreshes
|
||||
- [ ] every printed field traces to a key in `tests/fixtures/slots_gufo.json` (no fallback chains)
|
||||
- [ ] idle server: honest empty/idle state; vLLM: note only, no grid
|
||||
- [ ] `uv run pytest` green; `uv run ruff check src tests agent && uv run pyright` clean
|
||||
Reference in New Issue
Block a user