feat: show overview workstream mode counts

This commit is contained in:
2026-06-07 13:55:35 +02:00
parent b3f8ed63c2
commit 43742560df
2 changed files with 332 additions and 45 deletions

View File

@@ -151,43 +151,51 @@ display(html`<div class="warning" style="display:${summary.error ? '' : 'none'}"
## Workstreams by Repository
```js
// view() is the idiomatic Observable Framework reactive input:
// it displays the element AND returns a reactive value that re-runs dependent blocks.
const _chartMode = view(html`<select class="ws-mode-select">
<optgroup label="Lifecycle">
<option value="ready">ready</option>
<option value="active" selected>active</option>
<option value="blocked">blocked</option>
<option value="proposed">proposed</option>
<option value="backlog">backlog</option>
<option value="finished">finished</option>
<option value="archived">archived</option>
</optgroup>
<optgroup label="Health">
<option value="needs_review">needs review</option>
<option value="stalled">stalled</option>
</optgroup>
<optgroup label="Recently Changed">
<option value="1h">last 1 hour</option>
<option value="1d">last 24 hours</option>
<option value="7d">last 7 days</option>
<option value="30d">last 30 days</option>
<option value="today">today</option>
<option value="week">this week</option>
<option value="month">this month</option>
</optgroup>
</select>`);
```
```js
import * as Plot from "npm:@observablehq/plot";
// ── Filter workstreams by selected mode ───────────────────────────────────────
// Lifecycle modes match stored canonical status values.
// Health modes are derived labels; they are not stored lifecycle states.
// Time modes filter by updated_at / created_at.
const _STATUS_MODES = new Set(WORKSTREAM_STATUSES);
const _HEALTH_MODES = new Set(["needs_review", "stalled"]);
const _MODE_GROUPS = [
{
label: "Lifecycle",
options: [
["ready", "ready"],
["active", "active"],
["blocked", "blocked"],
["proposed", "proposed"],
["backlog", "backlog"],
["finished", "finished"],
["archived", "archived"],
],
},
{
label: "Health",
options: [
["needs_review", "needs review"],
["stalled", "stalled"],
],
},
{
label: "Recently Changed",
options: [
["1h", "last 1 hour"],
["1d", "last 24 hours"],
["7d", "last 7 days"],
["30d", "last 30 days"],
["today", "today"],
["week", "this week"],
["month", "this month"],
],
},
];
const _MODE_VALUES = new Set(_MODE_GROUPS.flatMap(group => group.options.map(([value]) => value)));
function _modeValue(mode) {
const value = typeof mode === "string" ? mode : mode?.value;
return _MODE_VALUES.has(value) ? value : "active";
}
function _timeCutoff(mode) {
const now = new Date();
@@ -205,20 +213,54 @@ function _timeCutoff(mode) {
return null;
}
const _chartWsFiltered = (
_STATUS_MODES.has(_chartMode)
? wsAll.filter(w => normalizeWorkstreamStatus(w.status) === _chartMode)
: _chartMode === "needs_review"
? wsAll.filter(needsReviewWorkstream)
: _chartMode === "stalled"
? wsAll.filter(isStalledWorkstream)
: (() => {
const since = _timeCutoff(_chartMode);
return wsAll.filter(w =>
new Date(w.updated_at) >= since || new Date(w.created_at) >= since
);
})()
);
function _validDate(value) {
const date = new Date(value);
return Number.isFinite(date.getTime()) ? date : null;
}
function _workstreamsForMode(mode, rows) {
const modeValue = _modeValue(mode);
const allRows = Array.isArray(rows) ? rows : [];
if (_STATUS_MODES.has(modeValue)) {
return allRows.filter(w => normalizeWorkstreamStatus(w.status) === modeValue);
}
if (modeValue === "needs_review") return allRows.filter(needsReviewWorkstream);
if (modeValue === "stalled") return allRows.filter(isStalledWorkstream);
const since = _timeCutoff(modeValue);
if (!since) return allRows.filter(w => normalizeWorkstreamStatus(w.status) === "active");
return allRows.filter(w => {
const updatedAt = _validDate(w.updated_at);
const createdAt = _validDate(w.created_at);
return (updatedAt && updatedAt >= since) || (createdAt && createdAt >= since);
});
}
const _savedChartMode = _MODE_VALUES.has(globalThis.__stateHubOverviewChartMode)
? globalThis.__stateHubOverviewChartMode
: "active";
const _modeSelect = html`<select
class="ws-mode-select"
aria-label="Workstream chart mode with matching workstream counts"
title="Choose which workstreams to show; counts are matching workstreams"
>
${_MODE_GROUPS.map(group => html`<optgroup label=${group.label}>
${group.options.map(([value, label]) => html`<option value=${value}>${label} (${_workstreamsForMode(value, wsAll).length})</option>`)}
</optgroup>`)}
</select>`;
_modeSelect.value = _savedChartMode;
_modeSelect.addEventListener("input", () => {
globalThis.__stateHubOverviewChartMode = _modeSelect.value;
});
// view() is the idiomatic Observable Framework reactive input:
// it displays the element AND returns a reactive value that re-runs dependent blocks.
const _chartMode = _modeValue(view(_modeSelect));
const _chartWsFiltered = _workstreamsForMode(_chartMode, wsAll);
```
```js
import * as Plot from "npm:@observablehq/plot";
// Sort by domain, then repository, then most recently updated workstream.
// The axis labels show each domain/repo group once.
@@ -625,6 +667,7 @@ display(Inputs.table((summary.recent_progress ?? []).map(e => ({
<style>
.live-indicator { font-size: 0.8rem; color: gray; position: relative; padding: 0.55rem 1.8rem 0.55rem 0.7rem; margin-bottom: 0.75rem; }
.ws-mode-bar { margin-bottom: 0.75rem; }
.ws-mode-select { min-width: 18rem; max-width: 100%; padding: 0.35rem 0.5rem; border-radius: 4px; border: 1px solid var(--theme-foreground-faint, #ddd); background: var(--theme-background); color: var(--theme-foreground); font: inherit; }
.card { background: var(--theme-background-alt); border-radius: 8px; padding: 1rem; }
.card.warn { border: 2px solid orange; }
.card-link { cursor: pointer; transition: box-shadow 0.15s, transform 0.1s; text-decoration: none; color: inherit; display: block; }

View File

@@ -0,0 +1,244 @@
---
id: STATE-WP-0057
type: workplan
title: "Overview Workstream Stage Counts"
domain: custodian
repo: state-hub
status: active
owner: codex
topic_slug: custodian
created: "2026-06-07"
updated: "2026-06-07"
state_hub_workstream_id: "43fb4c68-8ff8-4dc0-96f2-3c7976c16e9c"
---
# Overview Workstream Stage Counts
## Summary
Address the open dashboard-improvement suggestion asking for counts in the
Overview page's "Workstreams by Repository" selector. Operators should be able
to see how many workstreams match each lifecycle stage, derived health filter,
and recently-changed window before changing the chart mode.
This should be a small dashboard usability improvement, not a data-model
change. The Overview page already receives a bounded `/state/overview` read
model with `workplan_rows`, and it already uses shared workplan status helpers
to normalize lifecycle and derived health filters.
## Current Findings
Suggestion reviewed on 2026-06-07:
- `70e9bfd4-235d-4677-b053-39b78af8e5aa` — "UI: Workstreams by Repository"
asks: "In the selector for which workstreams to show I want to see the count
of workstreams in the respective stage."
- Location: `Overview | Custodian State Hub > Workstreams by Repository`
- Current status: `review`
Relevant repo findings:
- Older dashboard-improvement suggestions for TPSC repo labels and task /
workstream status editing are already finished under `STATE-WP-0043`.
- `dashboard/src/index.md` renders the Workstreams by Repository mode selector
with static `<option>` labels.
- The same page already loads `overview.workplan_rows` into `wsAll`, normalizes
workstream statuses, filters chart rows by the selected mode, and renders
empty-state copy for lifecycle, health, and time-window modes.
- `dashboard/src/components/workplan-status.js` provides the canonical
lifecycle statuses plus `normalizeWorkstreamStatus`, `needsReviewWorkstream`,
and `isStalledWorkstream`.
- `/state/overview` currently returns lifecycle totals and per-workplan rows,
so this appears frontend-only unless verification finds missing or stale row
data.
## Out of Scope
- Redesigning the Overview chart or dashboard information architecture.
- Changing workplan lifecycle semantics or adding new stored statuses.
- Reworking task or workstream status editing controls.
- Adding authentication, authorization, or multi-user mutation behavior.
- Replacing Observable Framework.
## T01 — Define Selector Count Semantics
```task
id: STATE-WP-0057-T01
status: done
priority: high
state_hub_task_id: "e2dd7231-f378-49c1-9869-0eb5970cc54d"
```
Define exactly what each selector count means so the option labels and chart
stay aligned.
Implementation notes:
- Count workstreams, not tasks.
- Lifecycle options count rows whose normalized `status` equals the option
value: `proposed`, `ready`, `active`, `blocked`, `backlog`, `finished`, and
`archived`.
- Health options count rows using the existing derived predicates:
`needsReviewWorkstream` and `isStalledWorkstream`.
- Recently-changed options count rows with `updated_at` or `created_at` on or
after the same cutoff used by the chart filter.
- Keep zero-count options visible so operators can distinguish "none" from a
missing mode.
Done when the count behavior is captured in the implementation structure or
dashboard docs and is reused consistently by the selector and chart.
## T02 — Render Count-Aware Selector Options
```task
id: STATE-WP-0057-T02
status: done
priority: high
state_hub_task_id: "b73557f4-c524-407f-b6b1-f48d89ba277f"
```
Replace the static selector labels on the Overview page with labels that include
the current matching count.
Implementation notes:
- Build a small option/group data structure in `dashboard/src/index.md` for the
Lifecycle, Health, and Recently Changed groups.
- Render labels such as `active (10)` while keeping option values unchanged.
- Preserve the existing default selection of `active`.
- Make labels update when `wsAll` refreshes, including stale-while-refresh data
from the existing overview loader.
- Avoid resetting the selected mode during normal polling refreshes.
- Keep the existing option order and group names unless there is a clear reason
to improve them.
Done when every visible selector mode shows a matching workstream count and
changing modes still filters the chart exactly as before.
## T03 — Share Filtering Logic Between Counts and Chart
```task
id: STATE-WP-0057-T03
status: done
priority: medium
state_hub_task_id: "eebfa200-3953-4de3-955b-f808c8c54a0a"
```
Reduce the chance of the selector counts drifting away from the chart rows by
using one mode-filtering path.
Implementation notes:
- Extract the current mode filter into a small local helper such as
`_workstreamsForMode(mode, rows)`.
- Reuse that helper for both the option counts and `_chartWsFiltered`.
- Keep `_STATUS_MODES`, `_HEALTH_MODES`, `_timeCutoff`, and existing status
helper imports.
- Preserve current empty messages, chart sorting, tooltip data, and href
behavior.
- Treat missing or invalid timestamps conservatively so a bad row does not make
time-window counts throw.
Done when selector counts and chart row counts are mechanically derived from the
same filter function.
## T04 — Polish Selector Fit and Accessibility
```task
id: STATE-WP-0057-T04
status: done
priority: medium
state_hub_task_id: "2aa95f2d-be3e-489c-b8ee-5b6018109061"
```
Make the count-aware selector fit cleanly in the Overview section across normal
desktop and mobile widths.
Implementation notes:
- Keep the selector compact and operational; do not add explanatory page text.
- Add a stable width or responsive `max-width` if longer option labels become
clipped.
- Add or preserve an accessible label/title that makes the count meaning clear.
- Ensure option text uses normal spacing and does not rely on color alone.
- Confirm the selector does not cause nearby chart content to overlap.
Done when the selector remains readable and professionally sized with both
small and large count values.
## T05 — Close the Suggestion Feedback Loop
```task
id: STATE-WP-0057-T05
status: progress
priority: medium
state_hub_task_id: "5e673e25-407d-43f0-95d6-5a596afc5b3b"
```
Update the dashboard-improvement record so the UI Feedback page tells the same
story as the workplan and shipped code.
Implementation notes:
- Add a note to suggestion `70e9bfd4-235d-4677-b053-39b78af8e5aa` that
references `STATE-WP-0057`.
- Move the suggestion through `plan`, `implement`, `test`, and `review` as the
implementation progresses.
- Move it to `finished` only after build and browser verification, or after
operator confirmation if review is intentionally deferred.
- Confirm the Todo page's open suggestion count and UI Feedback active/closed
sections reflect the final status.
Done when the technical-debt suggestion record has implementation evidence and
no longer appears as an open suggestion after verified completion.
## T06 — Verification
```task
id: STATE-WP-0057-T06
status: wait
priority: high
state_hub_task_id: "8513290e-02a4-428d-8f1a-5de4fe447aa8"
```
Verify the selector counts against code, build output, and running dashboard
behavior.
Implementation notes:
- Run `make dashboard-check`.
- If any API behavior changes, run the relevant backend tests for
`/state/overview`.
- Open the local dashboard and verify:
- Lifecycle counts match the chart rows for each lifecycle option.
- Health counts match `needs review` and `stalled` chart results.
- Recently-changed counts match the chart for at least one non-empty and one
empty time window.
- Polling refreshes update counts without losing the selected mode.
- Offline/stale overview behavior keeps last-known counts rather than
flashing misleading zeros.
- Check desktop and mobile widths for clipped option text or overlapping chart
content.
Done when the dashboard build passes and the Overview selector visibly reports
accurate workstream counts for all mode groups.
## Verification Notes
- 2026-06-07: Implemented the Overview selector change in
`dashboard/src/index.md`. The selector now renders lifecycle, health, and
recently-changed options with matching workstream counts.
- The selector and chart now share one `_workstreamsForMode(mode, rows)` helper
so option counts and chart rows are derived from the same filtering logic.
- Added selector fit/accessibility polish with a stable width, `aria-label`, and
title text.
- Live count check against `/state/overview` returned: `ready=2`, `active=12`,
`blocked=0`, `proposed=3`, `backlog=6`, `finished=350`, `archived=15`,
`needs_review=0`, `stalled=4`, `1h=5`, `1d=10`, `7d=54`, `30d=264`,
`today=7`, `week=54`, and `month=54`.
- `npm run test` in `dashboard/` passed 11 tests.
- `npm run build` in `dashboard/` passed: Observable built 61 pages and
validated 49 links.
- Browser click-through remains pending because the Codex in-app browser bridge
failed to start in this session with a Windows sandbox setup failure, and no
local Playwright/Puppeteer package is installed for a headless fallback.