phase: 19_ups_events_expandable

**Phase 19 final verification pass — all tasks already complete; everything green, no code changes needed.**

- **Verified (all 4 task files present in `complete/`)**: `details.events-box` disclosure on `/ups` with no `open` attribute and `#events` inside it; lazy fetch (`if (state.events.open) await refreshEvents()`), toggle stale rule on `state.pollMs`, success-path-only `painted`/`fetchedAt`; `charts-box`-shaped CSS with the `> .body` padding restatement; 4 new served-byte pins in `tests/test_api.py`.
- **Criterion 1 (collapsed on load, no first-poll fetch)**: PASS — re-ran `data/logs/19-task04-verify.cjs` against the live dev server (real upsd, 2 UPSes): **28/28 PASS**, `0` `/api/events` requests across 2 closed polls vs 4 snapshot polls; expand → exactly 1 request, 50 rows with `Oct 20, 07:11` timestamps + severity chips + `1–50 of 200` pager.
- **Criterion 2 (cache/stale/poll-while-open)**: PASS — fresh re-open 2→2 requests, stale re-open 2→3, loop refreshes while open, close asks for nothing, failed fetch keeps rows and doesn't stamp the cache.
- **Criterion 3**: `uv run pytest` → **1458 passed, 15 skipped**; `uv run pytest --cov` → **98.94%** (floor 94, unchanged); `uv run ruff check src tests agent` → clean; `uv run pyright` → **0 errors**.
- **Criterion 4**: `bash .agents/validate.sh` → **exit 0** (incl. `node --check` of all page scripts + repo-hygiene gates).
- **Criterion 5**: `data/logs/ups-events-closed.png` (collapsed `▸ POWER EVENTS`, 0 events requests noted in `ups-events-verify.out`) and `ups-events-open.png` (`▾`, bounded 320px list, pager strip) regenerated and inspected this pass.
- **Criterion 6 (no regressions)**: full suite green incl. UPS contract/pager pins; `uv run pytest -m live --live` (with `.env.dev`) → **11 passed**; phase 18's `nav-final-pass.cjs` sweep re-run → **45/45** (this phase's markup/CSS didn't touch the masthead).
- **Documentation**: full README.md (980 L) + AGENTS.md (1992 L) review against the code — verified env templates vs `config.py`, API route table vs `api.py`, installer flags vs `deploy/install-agent.sh`, agent routes vs `host_agent.py`, test/coverage/live tallies, `data/logs` artifacts, and live fleet facts (`HOST_TARGETS` all `http://`, all three agents `version 1.0.0`, nine sources `ok`). AGENTS.md UI conventions and README's events paragraph already carry the disclosure/lazy/stale sentences from tasks 01–04. **Verified consistent — no changes needed.**
- **Notable**: made zero repo edits (working tree holds only this phase's task work). `uv pip list --outdated` couldn't complete (PyPI 503 for the project name) — no dependency moved this pass, so rule 8 is not triggered; validate.sh's earlier advisory still lists `fastapi 0.153.0 → 0.156.3`, deliberately not upgraded in a UI/verification phase.
- **Next pending phase**: `.agents/phases/todo/20_masthead_nav_polish/` (tasks 01–04).
This commit is contained in:
2026-10-20 19:17:22 -04:00
parent f9b1565abb
commit 95df8b695b
31 changed files with 694 additions and 10 deletions
+28 -6
View File
@@ -997,7 +997,7 @@ set -a; . ./.env.dev; set +a # dev dials the fleet the prod way, no
./scripts/dev-server.sh status # per-source ok/DOWN — unauthenticated: /api/health
./scripts/dev-server.sh logs
uv run pytest # hermetic: 1454 passed + 15 skipped (11 live,
uv run pytest # hermetic: 1458 passed + 15 skipped (11 live,
# 4 store_perf), no network
uv run pytest --cov # coverage gate (98.94%; floor 94%)
uv run pytest -m live --live # opt-in: real fleet / engines / upsd
@@ -1390,7 +1390,28 @@ a `WeakMap` keyed by the `<ul>`: it survives a poll (so page 3 is still there tw
later — resetting every refresh would make page 2 unreadable) and resets to page 1 when the
event *set* changes (an operator reading page 3 of yesterday's history must not land on page 3
of a list that just changed). Rows are re-written only when their markup changed, which is
what keeps the reader's scroll position inside the container.
what keeps the reader's scroll position inside the container. The `/ups` feed is also
**expandable** (phase 19, `TODO.md:4`): its card body is a `details.events-box` disclosure with
no `open` attribute, so that card is one `Power events` summary line until the operator opens it
— a closed `<details>` renders no content, which is `/system`'s Charts rule applied to a feed,
and the `#events` id stays *inside* the disclosure so the pager and its bounded scroll keep their
target (pinned on the served bytes). The disclosure is also what makes it **lazy**: `refresh()`
fetches the feed only while `state.events.open` is true, so a card nobody is looking at costs the
poll loop nothing (a hidden-but-still-polled list is the half-measure this is). The first expand
fetches; a re-expand paints the rows already in the `<ul>` while they are younger than one poll
interval (`state.pollMs`, the page's own cadence) and re-fetches only past it — a `<details>`
keeps its children, so a fresh re-open costs no request and loses neither the pager's page nor
the reader's scroll. `painted`/`fetchedAt` are stamped on the success path alone, which is the
invariant-4 shape here: a failed fetch leaves the rows on screen, leaves the stamp where it was,
and the next open re-fetches. Its styling is `details.charts-box`'s shape with one structural
difference: that toggle sits *inside* a card body, while this `<summary>` **replaces** the card's
`header` — so it carries the header's type, padding, sunken ground and bottom rule (a closed card
reads as its heading, an open one puts the feed under the same rule every other card draws) and
no `margin-bottom` when open, because the `.body` below carries that spacing. That is also why
`details.events-box > .body` restates the `.8rem`: `.card > .body` is a **child** selector, so a
body moved inside a disclosure stops being padded until something says so — while `ul.events`
and `.events-pager` are class-based and needed no new rule to keep the 320px scroll and the
`1–50 of N` strip working inside it.
The `/system` host card is one row of six heroes — CPU, MEM, TEMP, DISK, GPU, NET (the
`.heroes` grid; `tests/test_api.py` pins the slots and their order on the served script).
@@ -1932,10 +1953,11 @@ memory plot; the `cpu.usage_pct@coreN` series is still stored and still queryabl
wraps, so nothing is lost and nothing gained: the masthead is complete at 360 px and the
table tail is still out of reach. The fix is a responsive-table pass (`.scroll` on the wide
tables, or a per-host card layout at ≤640px) and it touches pinned page shape on `/`, so it is
a phase rather than a cleanup. Two operator TODO items are unphased alongside it: never
accepting a non-mTLS agent (fleet gap 11's rollout, half of it), and keeping both
`⚙ Account` / `⚷ Certs` masthead links visible on the admin pages that today do not link to
themselves (phase 15's deliberate rule, now questioned).
a phase rather than a cleanup. The two operator TODO items that sat alongside it are phased
now, in `todo/`: `24_dev_mtls_rollout` (never accepting a non-mTLS agent — fleet gap 11's
rollout, half of it) and `20_masthead_nav_polish` (keeping both `⚙ Account` / `⚷ Certs`
masthead links visible on the admin pages that today do not link to themselves — phase 15's
deliberate rule, which that phase is the decision on).
## Locked decisions (and why)