Give the panel a persistent status footer - #215
Merged
Conversation
Splitting the panel into Chat and Settings (#166) moved two signals behind a navigation click that should never need one: the placeholder-pricing caveat and the measurement-health findings. Both say the numbers you are looking at may not mean what you think, and both rendered only inside the Settings stats card. They now sit in chrome the centre pane scrolls within, visible from every view — same wording, same refresh points, a move rather than a copy. The footer is visible on the Settings view too, so leaving the originals in place would have shown every caveat twice to the reader most likely to be reading them. A failed /api/stats renders as "status unavailable", never as an empty bar. The bar is hidden when the store answered and had nothing to report, so silence would otherwise be indistinguishable from a clean bill of health — the defect the migrated-log detector exists to avoid, one surface out. Grey rather than amber: "could not ask" is a different claim from "asked, and it is bad". Sticky inside .main rather than fixed. Fixed would cover the sidebar and force the body to reserve space for a bar that is usually absent; a new scroll container would disturb the sidebar's height:100vh sticky, which the stylesheet's own comment records as deliberate. `.statusbar[hidden]` is not redundant: [hidden] is a UA rule and the author `display: flex` outranks it, so without the override the bar cannot hide — the exact trap the .view[hidden] comment predicted, arriving as predicted. Seven tests pin the contract and all seven were mutation-checked. Two started weak: an assertIn for the render call matched a commented-out one, so both are now anchored to the start of a line. Two PAIRS rows are added because the contrast module cannot see a new pairing of two existing tokens, only a new token. #188 also proposes configurable extra figures — tasks routed, spend avoided, the billing gate. Those are not built here: they need a home for display preferences, which #188's own scope note flags as deserving thought. The descope is recorded in the plan rather than left silent. Visual verification is queued as VRF-008. It could not be done from this session: the panel binds loopback only by a tested security norm, and the build host is not the operator's.
Two layout defects from the previous commit, both found by re-reading the diff against the case next door rather than the case that motivated it. The content column got 40px narrower. Moving the padding from `.main` to `.wrap` looked like a pure relocation, but `box-sizing: border-box` is global, so padding on `.wrap` comes out of its max-width instead of adding to it. 960 - 2x20 restores the 920 the column had. A footer must not narrow the page behind it. The bar's text did not line up with anything. `.statusbar` spanned the pane and padded 20px, while `.wrap` is a centred 960 column — so on a wide window the items sat at the pane's left edge and the cards they qualify sat hundreds of pixels to the right. The strip now carries only the background, the rule and the `hidden` state; an inner `.statusbar-inner` carries the same box as `.wrap`. Two independent max-widths that only look right while they agree, so a test asserts they agree, and another asserts the text is written into the inner column — writing to the strip's innerHTML replaces the column element and loses the alignment permanently, which the width check cannot see. The `.statusbar[hidden]` override stays and its comment is corrected: the layout rules moved to the inner column, so the strip declares no `display` today. The guard is what keeps adding one safe, which is not the same claim as the one the old comment made.
The media query re-insets `.wrap` to 16px and left the status bar's inner column at 20px, so the alignment the previous commit established held on a wide window and was 4px out on a phone. Only the horizontal value has to agree; a strip is shorter than a page by design.
Critic rev-20260908T074223Z-e8368b1f: 2 blocking, 8 warning, 12 note. Every
blocking and warning finding is fixed here; the notes that wanted no action
are dispositioned in the evidence store.
R-1/R-16 — a 500 rendered as a healthy store, which is the exact case this
footer was built to prevent. `getJSON` never checked `r.ok`, and this server
answers a failed read view with a well-formed {"error": ...} body; `fetch`
does not reject on 500, so the body parsed and every consumer read it as data
— the stats card as zeroes, the roster as an empty table, the footer as
nothing to report, hiding itself. Fixed at `getJSON` rather than in
`loadStats` alone: the other three consumers land in catch branches they
already have, so they gain an honest failure state instead of a silent empty
one, and the 404 door needs no server bug at all. I had written the opposite
claim into both CHANGELOG.md and ARCHITECTURE.md; both are corrected.
R-17 — the live region was populated before being unhidden, so the one
transition `role="status"` exists for was the one that would not announce.
Both branches now unhide first. Asserted as a statement SEQUENCE, because
with two branches a positional check always finds an earlier unhide and
passes against a reversed branch — the first version of that test did.
R-7/R-19 — `renderStatusBar` took ready-to-insert HTML for two items that are
pure text, putting the escaping obligation on every future caller for no
capability. It takes plain text; `statusItem` escapes, once.
R-18 — the catch asserted a cause it cannot know and dropped the only witness
to the real one. It now says what the code knows and `console.error`s the
rest.
R-4 — nothing pinned the show/hide toggle at all: drop either line and the
caveats never appear on any view with the suite still green.
R-2/R-8 — the plan headed chunk 01 `### Chunk 01:` and the rest `### Car 0N:`,
which the record parser cannot see; chunk 02's deliverable check never ran and
chunk 01's section swallowed the other three to end-of-file. One term per
level now, with the Coordinator's larger unit qualified where meant.
R-5/R-20 — what chunk 02 was accepted against is recorded in Status rather
than backfilled into its section as criteria, which would grade the code
against itself; its decisions are recorded in the artifact the tooling reads.
Chunks 03 and 04 get a Done-when that gates them on having acceptance
criteria BEFORE code, rather than invented ones now.
R-9/R-10/R-11/R-12 — doc drift, three pieces of it mine: a test comment whose
premise this work falsified, two comments narrating the diff rather than the
code, a plan overview still promising a header the train stopped building, and
deliverable lines naming an `app.js` the no-build-step constraint forbids.
Suite 803 green, lint clean. Every new assertion was mutation-checked; four
failed that check first and were rewritten.
Closing observations from verify-resolutions rev-20260908T080034Z-ff175903 (0 blocking, 0 findings), taken here because this commit was being made anyway rather than bought with another round. `statusItem` escaped its text and interpolated its class attribute raw. No data flow today — both call sites pass a literal or nothing — but the whole point of moving the escape to the sink was that the safe form is the only form the signature offers, and one of two interpolations does not achieve that. Pinned, so it is not an untested rider. The toggle-sequence helper sliced from `renderStatusBar` to a neighbour it named, so inserting any function between the two would have widened the slice and the sequence it asserts. It now ends at whatever top-level function comes next. Verified by inserting a decoy that sets `bar.hidden` between them: the slice holds. Not taken: pinning the narrow-viewport inset. It is correct, VRF-008 step 3 covers it by eye, and the axis that drifts under maintenance is max-width, which is already pinned — parsing a media-query block to assert 4px is disproportionate to a value nothing computes from. Suite 803 green, lint clean. Chunk 02 is ticked in the build plan.
Cumulative review rev-20260908T080745Z-b89c2e9b: 0 blocking, 2 warning,
14 note — nothing gated. These are the ones worth acting on; the rest are
recorded in the handoff.
R-10 — a 500's cause was discarded at the only place it is rendered. The
server composes {"error": str(exc)} and overrides log_message to silence its
own per-request stderr, so that body is the whole system's only account of
why a read failed, and `getJSON` threw on the status alone. VRF-008's
corrupt-pricing step walked straight into it: the operator would have read a
bare 500 while the parse error sat unread in the response. It is read and
logged now, and the two catches this branch newly routes 500s into log their
error instead of binding and dropping it.
R-9/R-11 — the same invariant's other remaining door. A 200 whose body is not
a stats payload left every key undefined, so the findings list was empty and
the bar hid: "asked, and all clear" over a response the panel could not read.
It now takes the could-not-read path. Chunk 03 reshapes that payload, which is
what makes it a guard rather than a comment.
R-4 — the content column's geometry was declared twice and pinned once, so
changing `.wrap`'s inset misaligned the footer with the suite green. Hoisted
to `--content-max` / `--content-inset`; both columns read them, and the test
now forbids a literal in either rather than policing two copies for equality.
`load_palette` skips non-colour tokens, so the contrast guards are unaffected.
R-14 — `test_the_failure_path_leaves_a_trace` used a bare assertIn and passed
against the line commented out, one line from assertions this same class
anchors. That is the shape this session added to learnings.md two hours ago.
R-13 — `docs/design/boundaries.md` and its private mirror stated the GUI
contract as "JSON shapes" and said nothing about status, which this branch
made load-bearing: a read view that wants to report a problem IN its payload
must answer 200 and use a field, as `health` does. Non-2xx now means "this
response is not data". The Design Overview Artifact is republished in the same
turn, per the parity rule — its degradation ladder claimed the panel "degrades
to a finding rather than to silence", which was false for this exact case
until now.
R-7/R-8 — README never mentioned the footer; the plan stated chunk 02's state
twice and the two disagreed.
Not taken: R-3/R-6, whether a screen reader announces a region unhidden and
filled in one synchronous task. Empirically reader-dependent, already VRF-008
step 6, and the fix if it reports silence is structural rather than a tweak.
Suite 805 green, lint clean. Six mutations run against the new guards, all
caught.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #188. Train 3, Chunk 02.
What
A persistent status footer beneath both panel views, carrying the two signals that qualify every figure the panel shows: the placeholder-pricing caveat and the measurement-health findings.
Both rendered inside the Settings stats card, which #166's Chat/Settings split put one navigation click from the view the panel opens on. Both say the numbers you are looking at may not mean what you think. Same wording, same refresh points (load, after a run, after a pricing save) — a move, not a copy: the footer is visible on Settings too, so duplicating would show every caveat twice to the reader most likely to be reading it.
Frontend only. No new endpoint, no new view function, no new control, no new dependency.
Why the failure states are the interesting part
The bar is hidden when the store answered and had nothing to report — so the strip only ever appears when something is wrong, rather than becoming chrome the reader learns to skip. That forced a third state, because an empty bar and an unreadable store looked identical.
Closing that properly meant fixing something older and wider than this chunk.
getJSONnever checkedr.ok. This server answers a failed read view with a well-formed{"error": …}body at HTTP 500, andfetchdoes not reject on 500 — so an error response parsed and every consumer read it as data: the stats card as zeroes, the roster as an empty table, and the new footer as "nothing to report", hiding itself over a store it could not read.Fixed at
getJSONrather than inloadStatsalone. The other three consumers land in catch branches they already had, so each gains an honest failure state instead of a silent empty one — and the 404 door needs no server bug at all, only a path regression, which matters because Chunk 03 reshapes this payload and Chunk 04 adds a route.The server's error body is now read and logged rather than reduced to a bare status code:
server.pyoverrideslog_messageto silence per-request stderr, so that body is the only rendering of the cause anywhere in the system.docs/design/boundaries.mdrecords that HTTP status is now part of the GUI endpoint contract — a read view that wants to report a problem in its payload must answer 200 and use a field, ashealthdoes.Descoped, deliberately
#188 also proposes configurable field selection plus tasks-routed, spend-avoided and the billing-gate state. Not built here. They need a home for display preferences, which #188's own scope note flags as deserving thought rather than reflex. Recorded as descoped in the build plan, the CHANGELOG and the chunk entry — not dropped.
Test plan
make test— 805 passed, 17 skipped; ruff and mypy clean (thetesttarget runslintfirst).assertIns matched their call commented out, and two compared positions across a two-branch function where there is always an earlier match to find. The ordering rule is now asserted as a statement sequence.totals.jsonreturned the health finding over/api/stats, and the served page carries the footer and all its call sites.PAIRSrows added totests/test_gui_contrast.py— the module cannot see a new pairing of two existing tokens, only a new token.What the tests do not cover, stated because the alternative is a false sense of coverage: the pixels. The panel binds loopback only (
tests/test_bind_address.py), and the build host is not the operator's, so showing it in a browser would have meant breaking a ratified security norm. VRF-008 carries six steps, including forcing the 500 door and a screen-reader check.Review
rev-20260908T074223Z-e8368b1f— 2 blocking, 8 warning, 12 note. Both blocking findings were real: the 500-renders-as-healthy case above, and### Car 0N:headings that the record parser cannot see, which meant chunk 02's deliverable check silently never ran.verify-resolutionsrev-20260908T080034Z-ff175903— 0/0, all ten confirmed resolved against the files rather than the diff.rev-20260908T080745Z-b89c2e9b— 0 blocking, 2 warning, 14 note. Both warnings actioned; the notes are dispositioned or recorded in the handoff.box-sizing: border-boxsilently narrowing the content column by 40px, and the bar's text aligning with nothing on a wide window.A claim I had to retract: I wrote into both
CHANGELOG.mdandARCHITECTURE.mdthat an unreadable store could not render as a healthy one, while that was false for the 500 case. Guarded one door, then documented the invariant. Both are corrected, andlearnings.mdgained two new members of the mutation-survivor family this produced.Note for the reviewer
prawduct-hook check-change-log-entryreportsno-entrystructurally on this repo —.prawduct/is gitignored by owner ruling, so git cannot see.prawduct/change-log.md. Both entries were verified by hand: the private one, and the trackedCHANGELOG.mdentry under[Unreleased]/### Added(minor-tier — user-visible new chrome, not### Internal). Tracked as #180.The Design Overview Artifact was republished in the same turn per the parity rule: its degradation ladder claimed the panel "degrades to a finding rather than to silence", which was false for this exact case until this branch.