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; }