Files
dashboards/.agents/phases/complete/26_ups_event_split/00_phase.md
T
ducoterra 9ff3c7889b phase: 26_ups_event_split
**Phase 26 `ups_event_split` — final verification pass (all 3 tasks already complete; nothing missing from `complete/`).**

Verified
- `page-ups.js`: `POWER_EVENT_KINDS = {status,test,action}` split, unconditional `refreshEvents()`, no `state.events`/toggle-fetch, shared `eventRow`, `#ups-errors-card.hidden = errors.length === 0`; `ups.html` error card above the disclosure, `hidden`, outside every `<details>`. Kind set cross-checked against the real writers (`collectors.py:393/409`, `api.py:1043/1058`, `poller.py:127/157/164/190`) — classification is correct.
- Re-ran the phase's browser pass live: `node data/logs/26-task03-verify.cjs` → **31 checks, 0 skipped, ALL PASS** (`data/logs/26-final-verify.out`) — poll-failure rows readable with the disclosure shut, card hidden on a power-only feed and appearing on the first `unreachable`, lists paging independently, 360 px clean.

Tests / lint (all green)
- `uv run pytest` → **1545 passed, 15 skipped**, exit 0 · `bash .agents/validate.sh` → exit 0, coverage **98.94%** (floor 94%) · `uv run ruff check src tests agent` clean · `uv run pyright` 0 errors · `node --check` clean · `uv run pytest -m live --live` (with `.env.dev`) → **11 passed**, dev server reads all 9 sources `ok`.

Completion criteria: poll-failure visible + power collapsed ✅ (pinned + browser); hidden-when-empty card, one `/api/events?source=ups` fetch, shared pager ✅; stale-last-good via the empty `catch` pin ✅; pytest/coverage/ruff/pyright ✅; `validate.sh` ✅; no behaviour change to prior work — `dash.css`/`app.js` untouched, disclosure CSS / pager / row-shape pins pass unchanged ✅.

Docs
- Fixed (pre-existing + this phase's drift): AGENTS.md "one helper with four call sites" → **five over four pages** and the `row(event)` sentence (phase 26 added a second `/ups` call site); invariant 5's `web/static/charts.js` → `src/dashboards/web/static/charts.js`; recorded the completion-pass transcript. README: "one helper renders every feed" now notes five lists. Full sweep otherwise consistent — all 38 env vars match `config.py` exactly, all served routes/scripts/contracts referenced exist, suite/coverage/live counts and marker splits verified.

Deviations: `fastapi 0.153.0 → 0.159.1` advisory left alone (validate.sh declares it advisory-not-a-gate; this phase touched no dependencies, matching prior phases' precedent). Next phase: **`27_mobile_tables_and_audit`**.
2026-10-21 19:42:19 -04:00

6.0 KiB

Phase 26 — UPS events: power events collapsible, connection errors always visible

Source: TODO.md L6 — "The phase I ran earlier collapsed all the events in the UPS tab. That wasn't the point. I wanted the power loss events in a collapsible list, not events like "NUT server NutError: cannot reach upsd at 10.1.0.126:3051 (timed out after 4s)" in a collapsible list." Story: n/a Context: phase 19 (complete) put the whole /ups event feed behind one collapsed details.events-box (no open attribute, lazy: refresh() fetches the feed only while state.events.open, the first expand fetches, a fresh re-expand paints from the <ul>'s kept children, painted/fetchedAt stamped on the success path alone, the Dash.eventsPager window of 50 inside the bounded 320px scroll). The operator's complaint is that the classification is wrong, not that a collapse exists: a poll failure like NUT server NutError: cannot reach upsd … (timed out after 4s) is hidden behind a summary nobody opens, while the power events it was meant to tuck away are the only ones the operator actually wants collapsed. The feed's kinds are fixed and known: the poller (collectors.py:490 — Job("nut", "ups", "NUT server", …), events under the dashboard name ups) writes unreachable / reachable / recording / store-error (poller.py:127/157/164/190 — the quoted NutError line is the unreachable message); the NutMonitor transition bookkeeping writes status (a state change lands at collectors.py:393) and test (the self-test verdict, collectors.py:409); api.py writes action for every command sent through POST /api/ups/command (the fixed-text action events, api.py:999/1014). The served-byte pins for the current shape are in tests/test_api.py: test_ups_power_events_feed_sits_behind_a_collapsed_disclosure (the details.events-box, no open, #events inside), test_ups_events_are_fetched_only_while_the_disclosure_is_open (the lazy gate — this phase inverts it on purpose), test_the_ups_event_rows_keep_the_date_time_and_severity_shape (the row shape), test_the_ups_events_disclosure_is_a_quiet_disclosure_not_a_new_component (the details.events-box CSS shape — unchanged by this phase). The page is src/dashboards/web/static/page-ups.js (refresh() around line 439, refreshEvents() around line 445, the disclosure's toggle handler in the DOMContentLoaded block) over the card in src/dashboards/web/pages/ups.html:75-78.

Objective

/ups renders two lists from the one unchanged feed fetch: the power events — state transitions (including the power-loss transitions), self-test verdicts and sent commands — stay behind the collapsed details.events-box disclosure, and the connection/error events (unreachable/reachable/recording/store-error) are a card that is always visible (hidden only while it has no rows to show). The split is page-side, keyed on the event's kind; /api/events gains no vocabulary.

Dependencies

  • — (none) — the phase builds on complete phase 19; it is scheduled before 27_mobile_tables_and_audit on purpose, because that phase's mobile audit captures /ups in the shape this phase leaves it in (two cards, one of them hidden-when-empty).

Tasks

  1. 01_page_side_event_split.md — the kind split in page-ups.js (power kinds status/test/action, everything else errors) and the removal of the phase-19 lazy gate: the feed is fetched on every poll, because a visible list that ages is worse than one more 200-event fetch per interval
  2. 02_render_both_lists_paged.md — the always-visible #events-errors card (above the disclosure, hidden while empty) and both lists paged through the shared Dash.eventsPager
  3. 03_repin_and_browser_verify.md — the pin re-pins (the disclosure stays, the lazy gate inverts, the row shape extends to the error list, the kind set is pinned so a silent upstream rename breaks a test) and the phase's real-browser verification

Testing & Quality

  • Unit/integration: the re-pinned and new pins in tests/test_api.py (task 03); the split itself is pinned on the served script (the kind set) and the served markup (the error card, hidden, not inside a <details>)
  • Coverage: >90% on new/modified code
  • Real-browser check (invariant 10): a data/logs/26-task03-*.cjs script in the 20-task01-rows.cjs convention, run against the restarted dev server: the disclosure shut by default, the error card's visibility matching the feed's error-kind rows, both lists' rows matching a direct /api/events?source=ups&limit=200 split, screenshots in data/logs/

Documentation & Consistency

  • The final pass updates the now-stale sentences in AGENTS.md: the event-feed paragraph's lazy-fetch block ("refresh() fetches the feed only while state.events.open is true, so a card nobody is looking at costs the poll loop nothing" and the first-expand/fresh-re-expand sentences) is re-written to the new fact (the one fetch serves two lists, always; the disclosure gates only the power list's visibility), and the pin description of test_ups_events_are_fetched_only_while_the_disclosure_is_open is re-pointed at its inverted successor. README.md is checked for the same sentence; none is expected.

Completion Criteria

  • a poll failure (e.g. NUT server NutError: cannot reach upsd …) is visible on /ups without opening anything, and the power events stay behind the collapsed disclosure (pinned + verified in a browser)
  • the error card is hidden while it has no rows and appears with its first error row; both lists share the one /api/events?source=ups fetch and the shared pager (pinned)
  • a failed fetch leaves both lists on their last good rows (the stale-last-good rule) — pinned where phase 19 pinned its
  • uv run pytest green, coverage not down, ruff + pyright clean
  • bash .agents/validate.sh green
  • no behavior change in completed work: the details.events-box CSS shape, the pager's page/scroll semantics, and the row's time/host/severity shape are untouched