Append phases 25-27 (masthead control placement, UPS event split, mobile tables + full mobile audit) derived from TODO.md L3-L7; the TODO file is cleared, its items now live in .agents/phases/todo/. Protocol B: PLAN.md and AGENTS.md untouched. LLM-Generated: true
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_auditon purpose, because that phase's mobile audit captures/upsin the shape this phase leaves it in (two cards, one of them hidden-when-empty).
Tasks
01_page_side_event_split.md— thekindsplit inpage-ups.js(power kindsstatus/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 interval02_render_both_lists_paged.md— the always-visible#events-errorscard (above the disclosure, hidden while empty) and both lists paged through the sharedDash.eventsPager03_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-*.cjsscript in the20-task01-rows.cjsconvention, 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=200split, screenshots indata/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 whilestate.events.openis 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 oftest_ups_events_are_fetched_only_while_the_disclosure_is_openis re-pointed at its inverted successor.README.mdis checked for the same sentence; none is expected.
Completion Criteria
- a poll failure (e.g.
NUT server NutError: cannot reach upsd …) is visible on/upswithout 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=upsfetch 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 pytestgreen, coverage not down,ruff+pyrightcleanbash .agents/validate.shgreen- no behavior change in completed work: the
details.events-boxCSS shape, the pager's page/scroll semantics, and the row's time/host/severity shape are untouched