diff --git a/dashboard/src/index.md b/dashboard/src/index.md index 182d614..6d42b39 100644 --- a/dashboard/src/index.md +++ b/dashboard/src/index.md @@ -151,43 +151,51 @@ display(html`
- - - - - - - - - - - - - - - - - - - - - - -`); -``` - -```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``; +_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 => ({