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:
2026-10-17 17:50:19 -04:00
parent de325caa49
commit 3d5ccd7a39
43 changed files with 2544 additions and 210 deletions
+32 -1
View File
@@ -3,6 +3,7 @@
#
# ./scripts/capture-fixtures.sh # needs the tunnels up: scripts/dev-tunnels.sh start
# ./scripts/capture-fixtures.sh --with-llm # also scrape the live engines' /metrics
# # (gufo /slots is captured without the flag)
#
# Fixtures are verbatim, not hand-written: a fixture you typed is a test of your memory.
# When a host's kernel or an engine's metric names change, re-run this and read the diff.
@@ -105,6 +106,30 @@ else
echo " (skipped UPS captures — NUT_HOST unset; see .env.dev.example)"
fi
# --- gufo /slots (per-slot rows) ---------------------------------------------
# `tests/slots_contract.py` pins the shape of the `/llm` slot grid (task 04) against
# `tests/fixtures/slots_gufo.json`, so this capture is NOT behind `--with-llm`: it is one
# small JSON document and the hermetic contract test needs it on disk. `/v1/slots` is the
# documented alias, tried second — the same order `collectors.fetch_engine` and
# `llm_monitor`'s passthrough use, so a server that only exposes the alias is still
# capturable. Skipped when GUFO_BASE_URL is unset, like the UPS captures. An idle engine
# still answers one row per parallel slot; that idle shape is what the grid must handle.
if [ -n "${GUFO_BASE_URL:-}" ]; then
gufo_base="${GUFO_BASE_URL%/}"
slots_tmp="$FIX/.slots_gufo.json.tmp"
if curl -fsS --max-time 8 "${gufo_base}/slots" -o "$slots_tmp" ||
curl -fsS --max-time 8 "${gufo_base}/v1/slots" -o "$slots_tmp"; then
mv "$slots_tmp" "$FIX/slots_gufo.json"
echo " ✓ slots_gufo.json ($(wc -c <"$FIX/slots_gufo.json") bytes)"
else
rm -f "$slots_tmp" # never overwrite a good fixture with a dead engine's empty file
echo " ✗ slots_gufo.json — gufo answered neither /slots nor /v1/slots at $gufo_base"
failed=1
fi
else
echo " (skipped gufo /slots capture — GUFO_BASE_URL unset; see .env.dev.example)"
fi
if [ "${1:-}" = "--with-llm" ]; then
if [ ${#ENGINES[@]} -eq 0 ]; then
echo " ! no GUFO_BASE_URL / VLLM_BASE_URL set (see .env.dev) — nothing to capture"
@@ -135,7 +160,10 @@ fi
# FQDN a host thinks it has, and a UPS's `ups.serial`/`device.serial` (which identifies the
# physical unit, and is echoed back in the card's variable dump). They are replaced
# deterministically so re-capturing does not churn the fixture, and the tests keep the same
# shapes to assert against.
# shapes to assert against. A fourth belongs only to the /slots capture: a busy slot's
# `prompt` field is *the user's in-flight request text* — personal data the slot grid never
# renders — so any non-empty prompt value is blanked, keeping the key (the shape) without
# the content.
python3 - "$FIX" <<'PY'
import pathlib
import re
@@ -167,6 +195,9 @@ for path in sorted(fix.iterdir()):
# "hostname": "gw.lab.example.com" -> "gw": the dashboard's host key is the target name
# from HOST_TARGETS, never whatever the box calls itself
text = re.sub(r'("hostname": "[^"]*?)\.[^"]*(")', r"\1\2", text)
if path.name.startswith("slots_"):
# only non-empty values are replaced, so an idle capture does not churn
text = re.sub(r'("prompt"\s*:\s*)"(?:[^"\\]|\\.)+"', r'\1""', text)
def scrub_serial(match: re.Match[str]) -> str:
value = match.group(2)