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:
@@ -0,0 +1,36 @@
|
||||
# Phase 19 — Expandable UPS power-events list
|
||||
|
||||
**Source:** `TODO.md` L4 — "The UPS tab should have an expandable, clear list of last incidents with dates/times"
|
||||
**Story:** n/a
|
||||
**Context:** `src/dashboards/web/pages/ups.html` lines 72–76 (the `Power events` card: `<header><h3>` + a `ul.events#events` holding a `loading…` placeholder), `src/dashboards/web/static/page-ups.js` (`refreshEvents()` at line 430 — fetches `/api/events?limit=200&source=ups` and hands the rows to `Dash.eventsPager` with the date/time + severity-chip + host row shape; called unconditionally from `refresh()` at line 427, which the page's `poll(refresh, state.pollMs)` loop drives, `state.pollMs` from `config.ups.poll_ms || 5000`), `src/dashboards/web/static/app.js` (`Dash.eventsPager` — 50 rows a page, paging state in a `WeakMap` keyed by the `<ul>`, rows re-written only when their markup changed), `src/dashboards/web/static/dash.css` (`details.charts-box` at lines 714–727 — the summary marker `▸`/`▾`, hidden webkit marker, hover; the `.events` bounded 320px scroll and `.events-pager` strip), and the pattern this phase mirrors — `/system`'s per-host Charts disclosure (`system.html` line 81, `page-system.js` line 315: "a `<details>` renders nothing, so … nothing is built or drawn while the stack is shut") and its pin `test_system_history_charts_are_collapsed_behind_a_toggle`
|
||||
|
||||
## Objective
|
||||
|
||||
The `/ups` power-events list becomes what the TODO asks for — *expandable*: the card is collapsed by default behind a `<details>` disclosure in the repo's own `charts-box` style, nothing is fetched or drawn while it is shut (the `/system` rule), the first expand fetches and builds, a re-expand paints from the data in hand unless it is stale, and the page's poll loop refreshes the list only while the disclosure is open. The "clear" half already ships and stays: every row carries its date/time, the severity chip, and the host, and the 50-per-page pager bounds the list. No new API surface — the existing `/api/events?source=ups` is the whole backend.
|
||||
|
||||
## Dependencies
|
||||
- `18_nav_hamburger` (todo) — both phases touch `ups.html` (the masthead, the events card); landing 18 first keeps this phase's diff confined to the events section. No shared code between them.
|
||||
|
||||
## Tasks
|
||||
1. `01_events_disclosure.md` — the `ups.html` markup: the card body becomes a collapsed-by-default `<details class="events-box">`
|
||||
2. `02_events_lazy_loading.md` — `page-ups.js`: no fetch while closed, first-expand fetch, re-expand stale rule, poll-while-open
|
||||
3. `03_disclosure_css.md` — `dash.css`: the `details.events-box` summary styling in the `charts-box` shape, pager and bounded list inside
|
||||
4. `04_tests_and_verification.md` — the served-page pins (disclosure present and unopened, the lazy rule, the unchanged row shape) and the closed/open browser verification
|
||||
|
||||
## Testing & Quality
|
||||
- Unit/integration: `tests/test_api.py` — the served-page pins (see task 04), alongside the untouched `Dash.eventsPager` behaviour (a re-paint with the same data keeps the reader's page — the `WeakMap` rule, pinned by the existing event-feed tests)
|
||||
- Coverage: **>90%** on new/modified code (the phase is markup + JS + CSS; the gate applies to the changed Python test files)
|
||||
- The phase is "Complete" only when the **entire** project test suite passes 100% (every test in the project, not just this phase's)
|
||||
|
||||
## Documentation & Consistency
|
||||
|
||||
- The final pass reviews `README.md` and `AGENTS.md` in full against the project as it stands after this phase — fixing every inconsistency found, **including pre-existing ones not caused by this phase**.
|
||||
- AGENTS.md's `UI conventions` section already documents the event pager ("An event feed is a window, not a printout") and the `/system` Charts disclosure; it gains the `/ups` disclosure's sentence (collapsed by default, lazy-built, poll-while-open, the stale rule) in the section's voice. The README's `/ups` table row ("power events") stays true — the list is on the page, now behind a summary.
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] `/ups` loads with the Power events card collapsed (a summary line, no list visible, no `/api/events` request in the page's first poll) and the expand shows the bounded, paged list with date/time rows
|
||||
- [ ] closing and re-opening within one poll interval paints the cached rows without a refetch; a stale cache refetches on open; while open, the poll loop keeps the list current
|
||||
- [ ] `uv run pytest` green, coverage not down, `ruff` + `pyright` clean
|
||||
- [ ] `bash .agents/validate.sh` green (including `node --check` of the modified `page-ups.js`)
|
||||
- [ ] closed- and open-state screenshots in `data/logs/` against the running dev server (invariant 10)
|
||||
- [ ] no behavior change in completed work: every other element on `/ups` (cards, charts, dialog, pager behaviour) is unchanged
|
||||
@@ -0,0 +1,40 @@
|
||||
# Task 01 — The disclosure markup
|
||||
|
||||
**Phase:** `19_ups_events_expandable` · **Source:** `TODO.md:4` — "The UPS tab should have an expandable, clear list of last incidents with dates/times"
|
||||
**Story:** n/a
|
||||
|
||||
## Objective
|
||||
|
||||
The `Power events` card on `/ups` becomes a collapsed-by-default `<details>` disclosure, in the markup shape the repo already owns for `/system`'s Charts stack. The card stays a card; only its body moves inside the disclosure. The `loading…` placeholder stays in the `ul` — it is the honest state between the first expand and the first successful fetch (the pager's output replaces it on arrival; an empty feed then prints the pager's own `empty` sentence, which is a different fact).
|
||||
|
||||
## Work
|
||||
|
||||
1. `src/dashboards/web/pages/ups.html` — restructure the section at lines 72–76 from
|
||||
```html
|
||||
<section class="card" style="margin-top:.85rem">
|
||||
<header><h3>Power events</h3></header>
|
||||
<div class="body"><ul class="events" id="events"><li class="muted">loading…</li></ul></div>
|
||||
</section>
|
||||
```
|
||||
to
|
||||
```html
|
||||
<section class="card" style="margin-top:.85rem">
|
||||
<details class="events-box">
|
||||
<summary>Power events</summary>
|
||||
<div class="body"><ul class="events" id="events"><li class="muted">loading…</li></ul></div>
|
||||
</details>
|
||||
</section>
|
||||
```
|
||||
— no `open` attribute (collapsed by default; `ASSUMPTION:` confirmed by the operator — "expandable" means the list is shut until the operator opens it, the `/system` Charts pattern). The `#events` id, the `ul.events` class (the bounded 320px scroll + `.events-pager` strip target it), the `loading…` placeholder, and the pager's contract are all the page-script's, so nothing else in the file moves.
|
||||
2. `ASSUMPTION:` the summary is the plain word `Power events` (the old `<h3>` text) — no count chip on the summary. A count that has not been fetched would be an invented number (rule 2), and the pager under the list already says `1–50 of N` once data is in hand.
|
||||
3. `ASSUMPTION:` the disclosure is a plain `details` (no `data-k` attribute) — `/system`'s `data-k="charts"` exists for the per-host JS that rebuilds the stack; here the single page-level `page-ups.js` queries it by class, and the pin in task 04 names the class, not an attribute.
|
||||
|
||||
## Testing & Quality
|
||||
- Integration: the served-page pin lands in task 04 (`details.events-box` present on the served `/ups`, no `open` attribute, the `summary` label, the `#events` ul inside, no `loading…` placeholder); the untouched page renders (the browser verification is task 04)
|
||||
- Coverage: **>90%** on modified code (HTML only; the gate's suite must stay green)
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] the served `/ups` contains the restructured section exactly as named (a `GET` pin in task 04: `details.events-box`, no `open` attribute, the `summary` label, the `#events` ul with its `loading…` placeholder inside)
|
||||
- [ ] the page loads in a browser with the card collapsed: a `Power events` summary line, no list visible (task 04's screenshot)
|
||||
- [ ] `uv run pytest -q` green; `bash .agents/validate.sh` green
|
||||
- [ ] no behavior change in completed work: the rest of the page (cards, charts, dialog) is byte-identical in markup
|
||||
@@ -0,0 +1,49 @@
|
||||
# Task 02 — The lazy loading in `page-ups.js`
|
||||
|
||||
**Phase:** `19_ups_events_expandable` · **Source:** `TODO.md:4` — "The UPS tab should have an expandable, clear list of last incidents with dates/times"
|
||||
**Story:** n/a
|
||||
|
||||
## Objective
|
||||
|
||||
Nothing is fetched or drawn while the disclosure is shut (the `/system` rule: "nothing is built or drawn while the stack is shut"), the first expand fetches and builds, a re-expand paints from the data in hand unless it is stale, and the page's poll loop refreshes the list only while the disclosure is open. The row shape, the pager, and the `loading…`-then-rows sequence are unchanged.
|
||||
|
||||
## Work
|
||||
|
||||
1. `src/dashboards/web/static/page-ups.js` — `state` (line 17) gains one key, in the object's existing style: `events: { open: false, painted: false, fetchedAt: 0 }`.
|
||||
2. Same file, the `refresh()` function (the `await refreshEvents();` at line 427 is its last statement) — the unconditional call becomes the open-gate:
|
||||
```js
|
||||
if (state.events.open) await refreshEvents();
|
||||
```
|
||||
— the poll loop (the `state.poller = poll(refresh, state.pollMs)` at line 464) keeps driving the list **only while the disclosure is open**; closed, the page pays nothing for a feed nobody can see.
|
||||
3. Same file, `refreshEvents()` (line 430) — on success, before/after the existing `Dash.eventsPager(…)` call, stamp the state: `state.events.fetchedAt = Date.now(); state.events.painted = true;`. The existing `catch (err) { /* next poll */ }` stays untouched — a failed fetch leaves `painted`/`fetchedAt` where they were, so the next open re-fetches, and a failed poll while open keeps the last good rows (the invariant-4 shape, already the page's behaviour for everything else).
|
||||
4. Same file, inside the `DOMContentLoaded` handler, before `state.poller = poll(…)`:
|
||||
```js
|
||||
const eventsBox = $('.events-box');
|
||||
if (eventsBox) {
|
||||
state.events.open = eventsBox.open; // false — task 01 ships no `open` attribute
|
||||
eventsBox.addEventListener('toggle', () => {
|
||||
state.events.open = eventsBox.open;
|
||||
if (!eventsBox.open) return;
|
||||
// First expand: `painted` is false — fetch. Re-expand: paint from the data
|
||||
// in hand only while it is fresh; a cache older than one poll interval is
|
||||
// stale and re-fetches. A fresh, already-painted list needs nothing: the
|
||||
// `<details>` keeps its children, so the rows (and the pager's page, in its
|
||||
// WeakMap) are already where the operator left them.
|
||||
if (!state.events.painted || Date.now() - state.events.fetchedAt > state.pollMs) {
|
||||
refreshEvents();
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
`ASSUMPTION:` the stale bound is **one poll interval** (`state.pollMs`, from `config.ups.poll_ms || 5000`) — the `/system` Charts rule re-fetches a cached response "from another window"; here the window is the page's own, so one interval is the honest bound. A re-open within the interval paints the cache; a re-open after it re-fetches.
|
||||
5. `ASSUMPTION:` no `loading…` fiddling — the placeholder from task 01 stays in the `ul` until the first successful fetch's pager output replaces it (a `<details>` does not destroy its children, so the placeholder, the rows, and the pager strip all survive collapse/re-expand untouched).
|
||||
|
||||
## Testing & Quality
|
||||
- Integration: the served-script pins land in task 04 (the `state.events` shape, the `refresh()` open-gate, the `toggle` listener with its stale rule — asserted on the served bytes, the repo's way for JS behaviour since it has no JS test runner); `node --check` of the modified file passes (the gate); the untouched `Dash.eventsPager` tests stay green
|
||||
- Coverage: **>90%** on modified code (JS; the gate's Python checks apply to task 04's test files)
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] in a browser: the page's first poll makes **no** `/api/events` request (devtools network panel) while the card is collapsed; the first expand makes exactly one; the list then updates on the poll cadence while open and stops when closed
|
||||
- [ ] close → re-open within one poll interval: no request, the rows are exactly as left (page, scroll); close → wait longer than the interval → open: one request, fresh rows
|
||||
- [ ] a failed fetch (devtools offline) while open keeps the last good rows and the next poll retries; the next open after a failure re-fetches
|
||||
- [ ] `uv run pytest -q` green; `bash .agents/validate.sh` green (includes `node --check`)
|
||||
@@ -0,0 +1,42 @@
|
||||
# Task 03 — The disclosure CSS
|
||||
|
||||
**Phase:** `19_ups_events_expandable` · **Source:** `TODO.md:4` — "The UPS tab should have an expandable, clear list of last incidents with dates/times"
|
||||
**Story:** n/a
|
||||
|
||||
## Objective
|
||||
|
||||
The `details.events-box` summary renders in the shape the repo already owns for `/system`'s Charts disclosure (`details.charts-box`, `dash.css` lines 714–727): the `▸`/`▾` marker, the hidden webkit marker, the hover, and the open-state de-emphasis — so a second opinion about the drive's word is never the bug, and a page that ships the summary without the style is the drift this task's pin catches.
|
||||
|
||||
## Work
|
||||
|
||||
1. `src/dashboards/web/static/dash.css` — beside the existing `details.charts-box` rules (lines 714–727), add the `events-box` rules in the same block's voice (one selector per property, the file's style):
|
||||
```css
|
||||
details.events-box { min-width: 0; }
|
||||
details.events-box > summary {
|
||||
cursor: pointer; list-style: none;
|
||||
font-weight: 600; color: var(--text-muted);
|
||||
padding: .4rem .8rem .4rem 1.4rem;
|
||||
position: relative;
|
||||
}
|
||||
details.events-box > summary::-webkit-details-marker { display: none; }
|
||||
details.events-box > summary::before {
|
||||
content: "\25B8"; position: absolute; left: .6rem;
|
||||
font-size: 9px; color: var(--text-faint);
|
||||
}
|
||||
details.events-box[open] > summary::before { content: "\25BE"; }
|
||||
details.events-box > summary:hover { color: var(--text-muted); }
|
||||
details.events-box[open] > summary { color: var(--text-muted); margin-bottom: .55rem; }
|
||||
```
|
||||
(match the `charts-box` block's actual property set line-for-line — the snippet is the shape, the neighbouring block is the ground truth; do not invent tokens `:root` does not declare — the file's rule, pinned by `tests/test_api.py`).
|
||||
2. The card's existing `.body` wrapper (task 01's markup) carries the `ul.events` bounded scroll (the existing 320px rule) and the `.events-pager` strip — **no new rules** for those; verify they resolve inside the disclosure (the `.body` is a child of `details`, and the existing selectors are class-based, so they do — confirm in the browser, task 04).
|
||||
3. `ASSUMPTION:` no open/close animation on the disclosure — `charts-box` has none, and the file's `prefers-reduced-motion` block exists for the things that do move; a summary that snaps is the house style.
|
||||
|
||||
## Testing & Quality
|
||||
- Integration: the served-stylesheet pin lands in task 04 (the `details.events-box` rule family present, the `::before` markers, no token a `:root` does not declare — the existing `test` that fails if a CSS rule reaches for an undeclared token must pass untouched); the `node --check` gate is unaffected (CSS), and the page loads in the browser (task 04)
|
||||
- Coverage: **>90%** on modified code (CSS only; the gate's suite must stay green)
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] collapsed: the `Power events` summary line shows the `▸` marker in the card header area, muted, with the card's existing padding rhythm
|
||||
- [ ] open: the marker flips to `▾`, the summary de-emphasises, and the bounded scroll list + pager strip render exactly as the other feeds' (the 320px scroll, the `1–50 of N` strip with its arrows)
|
||||
- [ ] the page's other cards (the battery charts' `details`-free section, the `#ups-cards` grid) are pixel-identical
|
||||
- [ ] `bash .agents/validate.sh` green
|
||||
@@ -0,0 +1,31 @@
|
||||
# Task 04 — Pins and browser verification
|
||||
|
||||
**Phase:** `19_ups_events_expandable` · **Source:** `TODO.md:4` — "The UPS tab should have an expandable, clear list of last incidents with dates/times"
|
||||
**Story:** n/a
|
||||
|
||||
## Objective
|
||||
|
||||
The phase is pinned on the served bytes the way this repo pins every page surface: the disclosure is on the served `/ups` and **unopened**, the lazy-load rule is in the served script, and the row shape — the "clear" half of the TODO (date/time, severity chip, host) — is unchanged. Then the designed degradation is proven in a real browser: the closed state, the open state, and the no-request-while-closed fact.
|
||||
|
||||
## Work
|
||||
|
||||
1. `tests/test_api.py` — new tests, in the file's served-page style (the `self_test_app` harness and the `test_system_history_charts_are_collapsed_behind_a_toggle` pin at the `/system` disclosure are the models):
|
||||
- `test_the_ups_power_events_list_is_a_collapsed_disclosure`: a signed `GET /ups` finds `details.events-box` with **no** `open` attribute, a `summary` reading `Power events`, and the `ul.events#events` (with its `loading…` placeholder) inside it — and the old always-open shape is gone (no bare `header > h3` directly above the `ul` at the section level).
|
||||
- `test_the_ups_events_fetch_is_gated_on_the_disclosure`: the served `page-ups.js` carries the lazy rule — the `state.events` shape (`open`, `painted`, `fetchedAt`), the `refresh()` open-gate (`state.events.open` guard on the `refreshEvents()` call), and the `toggle` listener with the stale bound (`state.pollMs`) — asserted on the served script text, the repo's established device for JS behaviour (no JS test runner; the `node --check` in the gate covers syntax).
|
||||
- `test_the_ups_event_rows_keep_the_date_time_and_severity_shape`: the served `page-ups.js` row template still renders the `<time>` with the date/time `toLocaleString` options, the `sev ${event.severity}` chip, and the host — the "clear list with dates/times" the TODO asks for, pinned so a "cleanup" that drops the timestamp fails here instead of shipping.
|
||||
2. The existing event-feed pins (`tests/test_api.py`'s `Dash.eventsPager`-behaviour tests, the `EVENT_FEED_LIMIT`/`EVENT_PAGE_SIZE` pairing) pass **untouched** — the phase adds no API surface and no pagination vocabulary.
|
||||
3. Browser verification (invariant 10): `./scripts/dev-server.sh restart`, sign in with `testpass`, real upsd in the loop (the dev config dials the fleet's `upsd` on 3051 — two real UPSes, no `NUT_SIMULATE`):
|
||||
- `data/logs/ups-events-closed.png` — the page with the card collapsed: the `Power events` summary line, no list, and the devtools network panel showing **no** `/api/events?source=ups` request on the first poll (screenshot the panel or note the request count in the filename's companion note).
|
||||
- `data/logs/ups-events-open.png` — the expanded card: the bounded scroll list with date/time rows, the severity chips, and the `1–50 of N` pager strip (or the empty sentence, if the event table is genuinely empty — either is the correct state; the pin is the *shape*, not the data).
|
||||
4. `AGENTS.md` — in the `UI conventions` section, beside the event-pager paragraph ("An event feed is a window, not a printout") and the `/system` Charts-disclosure paragraph, add the `/ups` sentence in the section's voice: the list is a collapsed-by-default `details` disclosure, nothing is fetched while it is shut, the first expand fetches and builds, a re-expand paints the cache while it is fresher than one poll interval (stale re-fetches), and the poll loop refreshes only while open. `README.md` is checked in the phase's final pass (its `/ups` row — "power events" — stays true; the list is on the page, behind a summary).
|
||||
|
||||
## Testing & Quality
|
||||
- Integration: the three new served-page tests above + the untouched pager/limit pairing tests + `test_system_history_charts_are_collapsed_behind_a_toggle` (the pattern pin, untouched)
|
||||
- Coverage: **>90%** on the new test code; the full gate green
|
||||
|
||||
## Completion Criteria
|
||||
- [ ] `uv run pytest tests/test_api.py -q` green, including the three new tests and the untouched event-feed pins
|
||||
- [ ] `data/logs/ups-events-closed.png` and `data/logs/ups-events-open.png` exist and show the states as named (closed: summary only; open: bounded list + pager; the closed shot's note records the zero `/api/events` requests on the first poll)
|
||||
- [ ] `bash .agents/validate.sh` green (pytest --cov --cov-fail-under=94, ruff, pyright, node --check)
|
||||
- [ ] AGENTS.md's UI conventions carry the disclosure sentence; README.md reviewed and consistent
|
||||
- [ ] no behavior change in completed work: the rest of `/ups` (cards, charts, the confirm dialog, the banners) renders exactly as before
|
||||
Reference in New Issue
Block a user