Files
6krrt/plans/verdict-bar-update-labels-review.md
adlee-was-taken 3523dcf93e docs(plans): give every plan a Status line so the queue is greppable
plans/ held 58 documents and exactly one said whether it was open. The rest
mixed finished work, reviews of shipped work, parked specs and genuinely
pending ones, with nothing distinguishing them, so "how many plans are in
the queue" had no answer short of reading all 58.

Now `grep -H '^Status:' plans/*.md` is the answer:

    50 done   3 in progress   2 planned   2 reference   1 parked

Statuses were derived rather than guessed: CLAUDE.md's own built list and
"What's NOT built yet" section, plus checking the subject exists in the
code. A review of work that shipped counts as done -- it records what was
found, it is not a request for anything. `reference` separates the two docs
that are conventions rather than work items (admin-design-standards,
admin-work-framework), which otherwise read as permanently-open plans.

The vocabulary is deliberately five words. A larger one invites "mostly
done" and "blocked-ish", which is how the directory became unreadable.

test_plans_declare_status.py keeps it from rotting: a new plan without a
marker fails, as does an unknown status, one buried below the eighth line,
or an open status with no reason -- "planned" alone is the state that rots,
since nobody can tell later whether it waits on a decision, a dependency,
or just nobody's turn.

Also updates the sweep plan with what landed and what did not, including
that #9 was not a defect.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VRQXz5SYZYVWscxS1QqF6U
2026-09-08 18:55:16 -04:00

99 lines
4.4 KiB
Markdown

# Review: verdict-bar chart breaks on its first live update
Status: done -- review of shipped work
**Scope:** just this one defect, introduced in `906c9e6`
("fix(admin): replace stretched verdict doughnut with partition bar and
split history into per-metric mini charts"), in
`admin/frontend/index.html`. Not a review of the rest of that commit (the
history mini-charts split was checked separately and is sound).
## Verdict: real regression, invisible on first load, breaks on every subsequent poll
`906c9e6` replaced the Verdict Mix doughnut with a horizontal "partition
bar" — one Chart.js `bar` dataset holding all N verdict counts, rendered as
a single row split into colored segments. That design depends on the chart
always having exactly **one** category on its index axis, with all N values
living inside that one dataset's `data` array.
Chart creation gets this right. `renderChart`'s `verdict-bar` branch
(`index.html:844-857`) ignores whatever `labels` it was called with and
hardcodes a single-element array:
```js
return new Chart(canvas, { type: 'bar', data: { labels: [''], datasets }, options: opts });
```
But `updateChart` — the path taken on every render *after* the first,
since `renderVerdict` calls it whenever `verdictChart` already exists
(`index.html:543`) — does not:
```js
function updateChart(name, labels, values, type, colors) { // index.html:898
if (name !== 'verdict' || !verdictChart) return;
verdictChart.data.labels = labels; // index.html:900
verdictChart.data.datasets[0].data = values;
verdictChart.data.datasets[0].backgroundColor = colors;
verdictChart.update();
}
```
`labels` here is `Object.keys(mix)` (`index.html:527`) — e.g.
`['pass', 'fail']`, one entry per verdict category, not the single-element
array the chart was created with. Line 900 overwrites `data.labels` with
that real array.
For a `stacked` bar chart on `indexAxis: 'y'` with a single dataset,
Chart.js uses `data.labels.length` to decide how many category rows to
draw. `stacked: true` (`index.html:854-855`) only merges multiple
*datasets* that share an index position — it does nothing to merge
multiple *values within one dataset* onto a single row. So the moment
`data.labels` goes from `['']` to `['pass', 'fail']`, the same one dataset
that used to render as one bar with two colored segments instead renders
as **two separate bars**, one per label. The "partition bar" concept only
holds together as long as `labels` stays a single blank entry, which is
exactly the invariant `updateChart` breaks.
## Why this passed the round of QA that caught the *last* verdict-chart bug
The previous regression (`verdictChart` never being assigned, causing
"Canvas is already in use") was caught by explicitly waiting 36+ seconds to
span a `REFRESH_MS` poll cycle
(`plans/.omo/evidence/task-8-admin-visual-fixes-v3-verdict.md`). That
discipline wasn't applied to `906c9e6` — it's a pure frontend diff with no
Playwright run, no screenshot, and no unit test attached
(`git show 906c9e6 --stat` touches only `admin/frontend/index.html`). A
single fresh-load screenshot would show a correct-looking single bar, since
first render always goes through `renderChart`, not `updateChart` — the
bug only shows up starting from the second render, i.e. the first 30-second
poll or SSE-triggered refresh after page load. Same blind spot as the last
bug, in a new function.
## Fix
`updateChart` is only ever called for `'verdict'` (the `name !== 'verdict'`
guard at the top makes it single-purpose), so there's no other caller
relying on the `data.labels` assignment. Drop it:
```js
function updateChart(name, labels, values, type, colors) {
if (name !== 'verdict' || !verdictChart) return;
verdictChart.data.datasets[0].data = values;
verdictChart.data.datasets[0].backgroundColor = colors;
verdictChart.update();
}
```
`labels` becomes an unused parameter at that point; either drop it from the
signature and its one call site (`index.html:543`), or leave it for
signature symmetry with `renderChart` — cosmetic, doesn't affect
correctness either way.
## Verification recommendation
A screenshot alone won't catch this class of bug twice. Confirm the fix the
same way the last one was confirmed: load `/admin/`, capture the verdict
bar on first render, wait past one `REFRESH_MS` cycle (30s+), capture again,
and diff — the bar should still be one row with the same segment count
before and after, not N separate rows.