generated from coulomb/repo-seed
feat: show overview workstream mode counts
This commit is contained in:
@@ -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; }
|
||||
|
||||
244
workplans/STATE-WP-0057-overview-workstream-stage-counts.md
Normal file
244
workplans/STATE-WP-0057-overview-workstream-stage-counts.md
Normal 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.
|
||||
Reference in New Issue
Block a user