Add state-hub v0.1 — local-first state service for the Custodian
Implements the first live layer of the Custodian cognitive infrastructure:
PostgreSQL schema, FastAPI REST API, FastMCP stdio server, and Observable
Framework telemetry dashboard.
- state-hub/: full stack (docker-compose, FastAPI, Alembic, MCP server, dashboard)
- 5 DB tables: topics, workstreams, tasks, decisions, progress_events
- 11 MCP tools + 5 resources registered in .mcp.json
- Observable dashboard: Overview, Workstreams, Decisions, Progress pages
- CLAUDE.md: session protocol (get_state_summary / add_progress_event ritual)
- ~/.claude/CLAUDE.md: global cross-project reference to the hub
- scripts/pull_image.py: WSL2 TLS-resilient Docker image downloader
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 17:47:49 +01:00
---
title: Decisions
---
```js
2026-02-24 23:19:26 +01:00
const API = "http://127.0.0.1:8000";
const POLL = 15_000;
Add state-hub v0.1 — local-first state service for the Custodian
Implements the first live layer of the Custodian cognitive infrastructure:
PostgreSQL schema, FastAPI REST API, FastMCP stdio server, and Observable
Framework telemetry dashboard.
- state-hub/: full stack (docker-compose, FastAPI, Alembic, MCP server, dashboard)
- 5 DB tables: topics, workstreams, tasks, decisions, progress_events
- 11 MCP tools + 5 resources registered in .mcp.json
- Observable dashboard: Overview, Workstreams, Decisions, Progress pages
- CLAUDE.md: session protocol (get_state_summary / add_progress_event ritual)
- ~/.claude/CLAUDE.md: global cross-project reference to the hub
- scripts/pull_image.py: WSL2 TLS-resilient Docker image downloader
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 17:47:49 +01:00
```
```js
Dashboard decisions: list view with MultiSelect filters
- Replace Pending/Made tab + Inputs.table with MultiSelect filters and
a proper list view
- Filters: Type (pending/made), Status (open/escalated/resolved/superseded),
title text search — all stable across polls (no data dependency)
- Each decision renders as a compact card: left border coloured by status
(blue=open, amber=escalated, green=resolved, gray=superseded), type and
status badges, domain context, deadline (red+overdue warning if past),
full title, description/rationale snippet, resolved-by attribution
- Decisions sorted: escalated → open → resolved → superseded, then by
deadline ascending within each group
- Fetch now includes topics alongside decisions for domain name join
- Escalation warning box and velocity chart retained
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 00:39:44 +01:00
// Fetch decisions + topics (for domain context) in parallel
2026-02-24 23:19:26 +01:00
const decState = (async function*() {
while (true) {
let data = [], ok = false;
try {
Dashboard decisions: list view with MultiSelect filters
- Replace Pending/Made tab + Inputs.table with MultiSelect filters and
a proper list view
- Filters: Type (pending/made), Status (open/escalated/resolved/superseded),
title text search — all stable across polls (no data dependency)
- Each decision renders as a compact card: left border coloured by status
(blue=open, amber=escalated, green=resolved, gray=superseded), type and
status badges, domain context, deadline (red+overdue warning if past),
full title, description/rationale snippet, resolved-by attribution
- Decisions sorted: escalated → open → resolved → superseded, then by
deadline ascending within each group
- Fetch now includes topics alongside decisions for domain name join
- Escalation warning box and velocity chart retained
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 00:39:44 +01:00
const [rd, rt] = await Promise.all([
fetch(`${API}/decisions/?limit=500` ),
fetch(`${API}/topics/` ),
]);
ok = rd.ok & & rt.ok;
if (ok) {
const [decisions, topics] = await Promise.all([rd.json(), rt.json()]);
const topicMap = Object.fromEntries(topics.map(t => [t.id, t]));
data = decisions
.map(d => ({
...d,
domain: topicMap[d.topic_id]?.domain ?? null,
topic_title: topicMap[d.topic_id]?.title ?? null,
}))
.sort((a, b) => {
const rank = {escalated: 0, open: 1, resolved: 2, superseded: 3};
const dr = (rank[a.status] ?? 9) - (rank[b.status] ?? 9);
if (dr !== 0) return dr;
if (a.deadline & & b.deadline) return a.deadline.localeCompare(b.deadline);
return a.deadline ? -1 : b.deadline ? 1 : 0;
});
}
2026-02-24 23:19:26 +01:00
} catch {}
yield {data, ok, ts: new Date()};
await new Promise(res => setTimeout(res, POLL));
}
})();
Add state-hub v0.1 — local-first state service for the Custodian
Implements the first live layer of the Custodian cognitive infrastructure:
PostgreSQL schema, FastAPI REST API, FastMCP stdio server, and Observable
Framework telemetry dashboard.
- state-hub/: full stack (docker-compose, FastAPI, Alembic, MCP server, dashboard)
- 5 DB tables: topics, workstreams, tasks, decisions, progress_events
- 11 MCP tools + 5 resources registered in .mcp.json
- Observable dashboard: Overview, Workstreams, Decisions, Progress pages
- CLAUDE.md: session protocol (get_state_summary / add_progress_event ritual)
- ~/.claude/CLAUDE.md: global cross-project reference to the hub
- scripts/pull_image.py: WSL2 TLS-resilient Docker image downloader
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 17:47:49 +01:00
```
```js
Dashboard decisions: list view with MultiSelect filters
- Replace Pending/Made tab + Inputs.table with MultiSelect filters and
a proper list view
- Filters: Type (pending/made), Status (open/escalated/resolved/superseded),
title text search — all stable across polls (no data dependency)
- Each decision renders as a compact card: left border coloured by status
(blue=open, amber=escalated, green=resolved, gray=superseded), type and
status badges, domain context, deadline (red+overdue warning if past),
full title, description/rationale snippet, resolved-by attribution
- Decisions sorted: escalated → open → resolved → superseded, then by
deadline ascending within each group
- Fetch now includes topics alongside decisions for domain name join
- Escalation warning box and velocity chart retained
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 00:39:44 +01:00
const data = decState.data ?? [];
const _ok = decState.ok ?? false;
const _ts = decState.ts;
Add state-hub v0.1 — local-first state service for the Custodian
Implements the first live layer of the Custodian cognitive infrastructure:
PostgreSQL schema, FastAPI REST API, FastMCP stdio server, and Observable
Framework telemetry dashboard.
- state-hub/: full stack (docker-compose, FastAPI, Alembic, MCP server, dashboard)
- 5 DB tables: topics, workstreams, tasks, decisions, progress_events
- 11 MCP tools + 5 resources registered in .mcp.json
- Observable dashboard: Overview, Workstreams, Decisions, Progress pages
- CLAUDE.md: session protocol (get_state_summary / add_progress_event ritual)
- ~/.claude/CLAUDE.md: global cross-project reference to the hub
- scripts/pull_image.py: WSL2 TLS-resilient Docker image downloader
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 17:47:49 +01:00
```
2026-02-24 23:19:26 +01:00
```js
Dashboard decisions: list view with MultiSelect filters
- Replace Pending/Made tab + Inputs.table with MultiSelect filters and
a proper list view
- Filters: Type (pending/made), Status (open/escalated/resolved/superseded),
title text search — all stable across polls (no data dependency)
- Each decision renders as a compact card: left border coloured by status
(blue=open, amber=escalated, green=resolved, gray=superseded), type and
status badges, domain context, deadline (red+overdue warning if past),
full title, description/rationale snippet, resolved-by attribution
- Decisions sorted: escalated → open → resolved → superseded, then by
deadline ascending within each group
- Fetch now includes topics alongside decisions for domain name join
- Escalation warning box and velocity chart retained
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 00:39:44 +01:00
import {MultiSelect} from "./components/multiselect.js";
dashboard: prominent KPI infobox, doc-overlay component, decisions reference page
KPI infobox
- Replace slim kpi-bar with a boxed card (border, shadow, 195–240px) floating
right in a flex header alongside the live indicator
- Rows: avg resolve time (last ≤5 resolved) + avg open age (all open)
- avg open age colored via CSS var --oc: red/orange/black per threshold
- "no open decisions" shown as muted italic when queue is empty
doc-overlay component (src/components/doc-overlay.js)
- withDocHelp(element, docPath) — adds absolute-positioned ? button
that is invisible until the parent is hovered; click opens overlay
- Overlay: fixed backdrop + animated box with iframe; closes on Esc,
backdrop click, or the close button
- CSS injected once via style tag (STYLE_ID guard, same pattern as MultiSelect)
? buttons wired up in decisions.md
- KPI infobox → /docs/decisions-kpi
- Cumulative chart (wrapped in position:relative div) → /docs/decisions-kpi
- Filter & List section header → /docs/decisions-kpi
Reference page (src/docs/decisions-kpi.md)
- Standalone Observable Framework page at /docs/decisions-kpi
- Documents: KPI card (avg resolve, avg open age, color thresholds),
Resolution History chart (cumulative, period→resolution mapping, filter
interaction, timestamp logic), Filter & List (type/status/search, card
age badge, escalation)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 11:48:47 +01:00
// Create filter form without displaying — shown below the chart via display(_filtersForm)
2026-02-26 07:22:25 +01:00
const _filtersForm = Inputs.form(
Dashboard decisions: list view with MultiSelect filters
- Replace Pending/Made tab + Inputs.table with MultiSelect filters and
a proper list view
- Filters: Type (pending/made), Status (open/escalated/resolved/superseded),
title text search — all stable across polls (no data dependency)
- Each decision renders as a compact card: left border coloured by status
(blue=open, amber=escalated, green=resolved, gray=superseded), type and
status badges, domain context, deadline (red+overdue warning if past),
full title, description/rationale snippet, resolved-by attribution
- Decisions sorted: escalated → open → resolved → superseded, then by
deadline ascending within each group
- Fetch now includes topics alongside decisions for domain name join
- Escalation warning box and velocity chart retained
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 00:39:44 +01:00
{
2026-02-26 07:22:25 +01:00
type: MultiSelect(["pending", "made"], {label: "Type", placeholder: "All types"}),
Dashboard decisions: list view with MultiSelect filters
- Replace Pending/Made tab + Inputs.table with MultiSelect filters and
a proper list view
- Filters: Type (pending/made), Status (open/escalated/resolved/superseded),
title text search — all stable across polls (no data dependency)
- Each decision renders as a compact card: left border coloured by status
(blue=open, amber=escalated, green=resolved, gray=superseded), type and
status badges, domain context, deadline (red+overdue warning if past),
full title, description/rationale snippet, resolved-by attribution
- Decisions sorted: escalated → open → resolved → superseded, then by
deadline ascending within each group
- Fetch now includes topics alongside decisions for domain name join
- Escalation warning box and velocity chart retained
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 00:39:44 +01:00
status: MultiSelect(["open", "escalated", "resolved", "superseded"], {label: "Status", placeholder: "All statuses"}),
search: Inputs.text({placeholder: "Search title…", style: "width:160px"}),
},
{
template: ({type, status, search}) => html`< div class = "filter-bar" >
${type}${status}
< div class = "filter-search" > ${search}< / div >
< / div > `,
}
2026-02-26 07:22:25 +01:00
);
```
```js
const filters = Generators.input(_filtersForm);
2026-02-24 23:19:26 +01:00
```
Add state-hub v0.1 — local-first state service for the Custodian
Implements the first live layer of the Custodian cognitive infrastructure:
PostgreSQL schema, FastAPI REST API, FastMCP stdio server, and Observable
Framework telemetry dashboard.
- state-hub/: full stack (docker-compose, FastAPI, Alembic, MCP server, dashboard)
- 5 DB tables: topics, workstreams, tasks, decisions, progress_events
- 11 MCP tools + 5 resources registered in .mcp.json
- Observable dashboard: Overview, Workstreams, Decisions, Progress pages
- CLAUDE.md: session protocol (get_state_summary / add_progress_event ritual)
- ~/.claude/CLAUDE.md: global cross-project reference to the hub
- scripts/pull_image.py: WSL2 TLS-resilient Docker image downloader
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 17:47:49 +01:00
2026-02-24 23:19:26 +01:00
```js
Dashboard decisions: list view with MultiSelect filters
- Replace Pending/Made tab + Inputs.table with MultiSelect filters and
a proper list view
- Filters: Type (pending/made), Status (open/escalated/resolved/superseded),
title text search — all stable across polls (no data dependency)
- Each decision renders as a compact card: left border coloured by status
(blue=open, amber=escalated, green=resolved, gray=superseded), type and
status badges, domain context, deadline (red+overdue warning if past),
full title, description/rationale snippet, resolved-by attribution
- Decisions sorted: escalated → open → resolved → superseded, then by
deadline ascending within each group
- Fetch now includes topics alongside decisions for domain name join
- Escalation warning box and velocity chart retained
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 00:39:44 +01:00
const filtered = data.filter(d =>
(filters.type.length === 0 || filters.type.includes(d.decision_type)) & &
(filters.status.length === 0 || filters.status.includes(d.status)) & &
(!filters.search || d.title.toLowerCase().includes(filters.search.toLowerCase()))
);
const escalated = filtered.filter(d => d.escalation_note);
const STATUS_BORDER = {open: "steelblue", escalated: "#f59e0b ", resolved: "#22c55e ", superseded: "#aaa "};
const TYPE_CLASS = {pending: "badge-pending", made: "badge-made"};
function fmtDate(iso) {
return iso ? new Date(iso).toLocaleDateString(undefined, {day: "numeric", month: "short", year: "numeric"}) : null;
}
function isOverdue(iso) {
return iso & & new Date(iso) < new Date ( ) ;
}
2026-02-26 07:50:54 +01:00
function fmtDuration(ms) {
if (ms < 0 ) ms = 0;
const h = 3_600_000, d = 86_400_000, w = 7 * d;
if (ms < 2 * h ) return `${Math.floor(ms / 60_000)}m` ;
if (ms < 2 * d ) return `${Math.floor(ms / h)}h` ;
if (ms < 2 * w ) return `${Math.floor(ms / d)}d` ;
if (ms < 8 * w ) return `${Math.floor(ms / w)}w` ;
return `${Math.round(ms / (30.5 * d))}mo` ;
}
2026-02-26 07:22:25 +01:00
```
# Decisions
```js
dashboard: prominent KPI infobox, doc-overlay component, decisions reference page
KPI infobox
- Replace slim kpi-bar with a boxed card (border, shadow, 195–240px) floating
right in a flex header alongside the live indicator
- Rows: avg resolve time (last ≤5 resolved) + avg open age (all open)
- avg open age colored via CSS var --oc: red/orange/black per threshold
- "no open decisions" shown as muted italic when queue is empty
doc-overlay component (src/components/doc-overlay.js)
- withDocHelp(element, docPath) — adds absolute-positioned ? button
that is invisible until the parent is hovered; click opens overlay
- Overlay: fixed backdrop + animated box with iframe; closes on Esc,
backdrop click, or the close button
- CSS injected once via style tag (STYLE_ID guard, same pattern as MultiSelect)
? buttons wired up in decisions.md
- KPI infobox → /docs/decisions-kpi
- Cumulative chart (wrapped in position:relative div) → /docs/decisions-kpi
- Filter & List section header → /docs/decisions-kpi
Reference page (src/docs/decisions-kpi.md)
- Standalone Observable Framework page at /docs/decisions-kpi
- Documents: KPI card (avg resolve, avg open age, color thresholds),
Resolution History chart (cumulative, period→resolution mapping, filter
interaction, timestamp logic), Filter & List (type/status/search, card
age badge, escalation)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 11:48:47 +01:00
import {withDocHelp} from "./components/doc-overlay.js";
2026-02-26 07:50:54 +01:00
dashboard: prominent KPI infobox, doc-overlay component, decisions reference page
KPI infobox
- Replace slim kpi-bar with a boxed card (border, shadow, 195–240px) floating
right in a flex header alongside the live indicator
- Rows: avg resolve time (last ≤5 resolved) + avg open age (all open)
- avg open age colored via CSS var --oc: red/orange/black per threshold
- "no open decisions" shown as muted italic when queue is empty
doc-overlay component (src/components/doc-overlay.js)
- withDocHelp(element, docPath) — adds absolute-positioned ? button
that is invisible until the parent is hovered; click opens overlay
- Overlay: fixed backdrop + animated box with iframe; closes on Esc,
backdrop click, or the close button
- CSS injected once via style tag (STYLE_ID guard, same pattern as MultiSelect)
? buttons wired up in decisions.md
- KPI infobox → /docs/decisions-kpi
- Cumulative chart (wrapped in position:relative div) → /docs/decisions-kpi
- Filter & List section header → /docs/decisions-kpi
Reference page (src/docs/decisions-kpi.md)
- Standalone Observable Framework page at /docs/decisions-kpi
- Documents: KPI card (avg resolve, avg open age, color thresholds),
Resolution History chart (cumulative, period→resolution mapping, filter
interaction, timestamp logic), Filter & List (type/status/search, card
age badge, escalation)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 11:48:47 +01:00
// ── KPI computation (uses full data, not filtered) ──────────────────────────
2026-02-26 07:50:54 +01:00
const _resolved5 = data
.filter(d => d.decided_at & & d.created_at)
.sort((a, b) => b.decided_at.localeCompare(a.decided_at))
.slice(0, 5);
const _meanResolveMs = _resolved5.length
? _resolved5.reduce((s, d) => s + (new Date(d.decided_at) - new Date(d.created_at)), 0) / _resolved5.length
: null;
dashboard: prominent KPI infobox, doc-overlay component, decisions reference page
KPI infobox
- Replace slim kpi-bar with a boxed card (border, shadow, 195–240px) floating
right in a flex header alongside the live indicator
- Rows: avg resolve time (last ≤5 resolved) + avg open age (all open)
- avg open age colored via CSS var --oc: red/orange/black per threshold
- "no open decisions" shown as muted italic when queue is empty
doc-overlay component (src/components/doc-overlay.js)
- withDocHelp(element, docPath) — adds absolute-positioned ? button
that is invisible until the parent is hovered; click opens overlay
- Overlay: fixed backdrop + animated box with iframe; closes on Esc,
backdrop click, or the close button
- CSS injected once via style tag (STYLE_ID guard, same pattern as MultiSelect)
? buttons wired up in decisions.md
- KPI infobox → /docs/decisions-kpi
- Cumulative chart (wrapped in position:relative div) → /docs/decisions-kpi
- Filter & List section header → /docs/decisions-kpi
Reference page (src/docs/decisions-kpi.md)
- Standalone Observable Framework page at /docs/decisions-kpi
- Documents: KPI card (avg resolve, avg open age, color thresholds),
Resolution History chart (cumulative, period→resolution mapping, filter
interaction, timestamp logic), Filter & List (type/status/search, card
age badge, escalation)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 11:48:47 +01:00
const _nowKpi = new Date();
2026-02-26 07:50:54 +01:00
const _openDecs = data.filter(d => d.status === "open" || d.status === "escalated");
const _openAges = _openDecs.map(d => _nowKpi - new Date(d.created_at));
const _meanOpenMs = _openAges.length
? _openAges.reduce((s, a) => s + a, 0) / _openAges.length
: null;
dashboard: prominent KPI infobox, doc-overlay component, decisions reference page
KPI infobox
- Replace slim kpi-bar with a boxed card (border, shadow, 195–240px) floating
right in a flex header alongside the live indicator
- Rows: avg resolve time (last ≤5 resolved) + avg open age (all open)
- avg open age colored via CSS var --oc: red/orange/black per threshold
- "no open decisions" shown as muted italic when queue is empty
doc-overlay component (src/components/doc-overlay.js)
- withDocHelp(element, docPath) — adds absolute-positioned ? button
that is invisible until the parent is hovered; click opens overlay
- Overlay: fixed backdrop + animated box with iframe; closes on Esc,
backdrop click, or the close button
- CSS injected once via style tag (STYLE_ID guard, same pattern as MultiSelect)
? buttons wired up in decisions.md
- KPI infobox → /docs/decisions-kpi
- Cumulative chart (wrapped in position:relative div) → /docs/decisions-kpi
- Filter & List section header → /docs/decisions-kpi
Reference page (src/docs/decisions-kpi.md)
- Standalone Observable Framework page at /docs/decisions-kpi
- Documents: KPI card (avg resolve, avg open age, color thresholds),
Resolution History chart (cumulative, period→resolution mapping, filter
interaction, timestamp logic), Filter & List (type/status/search, card
age badge, escalation)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 11:48:47 +01:00
// Color: red = mean open > baseline; orange = any individual > baseline; black = all fine
2026-02-26 07:50:54 +01:00
let _openAgeColor = "inherit";
if (_meanOpenMs !== null & & _meanResolveMs !== null) {
dashboard: prominent KPI infobox, doc-overlay component, decisions reference page
KPI infobox
- Replace slim kpi-bar with a boxed card (border, shadow, 195–240px) floating
right in a flex header alongside the live indicator
- Rows: avg resolve time (last ≤5 resolved) + avg open age (all open)
- avg open age colored via CSS var --oc: red/orange/black per threshold
- "no open decisions" shown as muted italic when queue is empty
doc-overlay component (src/components/doc-overlay.js)
- withDocHelp(element, docPath) — adds absolute-positioned ? button
that is invisible until the parent is hovered; click opens overlay
- Overlay: fixed backdrop + animated box with iframe; closes on Esc,
backdrop click, or the close button
- CSS injected once via style tag (STYLE_ID guard, same pattern as MultiSelect)
? buttons wired up in decisions.md
- KPI infobox → /docs/decisions-kpi
- Cumulative chart (wrapped in position:relative div) → /docs/decisions-kpi
- Filter & List section header → /docs/decisions-kpi
Reference page (src/docs/decisions-kpi.md)
- Standalone Observable Framework page at /docs/decisions-kpi
- Documents: KPI card (avg resolve, avg open age, color thresholds),
Resolution History chart (cumulative, period→resolution mapping, filter
interaction, timestamp logic), Filter & List (type/status/search, card
age badge, escalation)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 11:48:47 +01:00
if (_meanOpenMs > _meanResolveMs) _openAgeColor = "#dc2626 ";
else if (_openAges.some(a => a > _meanResolveMs)) _openAgeColor = "#d97706 ";
2026-02-26 07:50:54 +01:00
}
dashboard: prominent KPI infobox, doc-overlay component, decisions reference page
KPI infobox
- Replace slim kpi-bar with a boxed card (border, shadow, 195–240px) floating
right in a flex header alongside the live indicator
- Rows: avg resolve time (last ≤5 resolved) + avg open age (all open)
- avg open age colored via CSS var --oc: red/orange/black per threshold
- "no open decisions" shown as muted italic when queue is empty
doc-overlay component (src/components/doc-overlay.js)
- withDocHelp(element, docPath) — adds absolute-positioned ? button
that is invisible until the parent is hovered; click opens overlay
- Overlay: fixed backdrop + animated box with iframe; closes on Esc,
backdrop click, or the close button
- CSS injected once via style tag (STYLE_ID guard, same pattern as MultiSelect)
? buttons wired up in decisions.md
- KPI infobox → /docs/decisions-kpi
- Cumulative chart (wrapped in position:relative div) → /docs/decisions-kpi
- Filter & List section header → /docs/decisions-kpi
Reference page (src/docs/decisions-kpi.md)
- Standalone Observable Framework page at /docs/decisions-kpi
- Documents: KPI card (avg resolve, avg open age, color thresholds),
Resolution History chart (cumulative, period→resolution mapping, filter
interaction, timestamp logic), Filter & List (type/status/search, card
age badge, escalation)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 11:48:47 +01:00
// ── Build the KPI infobox ───────────────────────────────────────────────────
const _kpiBox = html`< div class = "kpi-infobox" >
< div class = "kpi-infobox-title" > Decision Health< / div >
${_meanResolveMs !== null ? html`< div class = "kpi-row" >
< span class = "kpi-row-label" > avg resolve< / span >
< div class = "kpi-row-right" >
< div class = "kpi-row-value" > ${fmtDuration(_meanResolveMs)}< / div >
< div class = "kpi-row-sub" > last ${_resolved5.length}< / div >
< / div >
< / div > ` : ""}
${_meanOpenMs !== null ? html`< div class = "kpi-row" style = "--oc:${_openAgeColor}" >
< span class = "kpi-row-label" > avg open age< / span >
< div class = "kpi-row-right" >
< div class = "kpi-row-value kpi-colorable" > ${fmtDuration(_meanOpenMs)}< / div >
< div class = "kpi-row-sub kpi-colorable" > ${_openDecs.length} open< / div >
< / div >
</ div > ` : html` < div class = "kpi-row" >< span class = "kpi-row-label kpi-muted" > no open decisions</ span ></ div > `}
< / div > `;
withDocHelp(_kpiBox, "/docs/decisions-kpi");
2026-02-26 13:29:34 +01:00
// ── Inject into the TOC sidebar (right column, non-scrolling) ───────────────
2026-02-26 13:33:23 +01:00
_kpiBox.id = "decisions-kpi-box";
2026-02-26 13:29:34 +01:00
const _toc = document.querySelector("#observablehq -toc");
2026-02-26 13:33:23 +01:00
if (_toc) {
document.getElementById("decisions-kpi-box")?.remove();
_toc.prepend(_kpiBox);
} else display(html`<div style="float:right;width:215px;margin-left:1rem">${_kpiBox}</div>` );
2026-02-26 12:07:49 +01:00
```
```js
display(html`< div class = "live-indicator" >
< span style = "color:${_ok ? 'var(--theme-foreground-focus)' : 'red'}" > ●< / span >
${_ok
? `Live · updated ${_ts?.toLocaleTimeString()}`
: html`<span style="color:red">Offline — run: <code>make api</code></span>` }
2026-02-26 07:22:25 +01:00
< / div > `);
```
## Resolution History
```js
const period = view(Inputs.radio(
["day", "week", "month", "quarter", "YTD", "year", "all"],
{value: "month", label: "Period"}
));
```
```js
import * as Plot from "npm:@observablehq/plot ";
dashboard: prominent KPI infobox, doc-overlay component, decisions reference page
KPI infobox
- Replace slim kpi-bar with a boxed card (border, shadow, 195–240px) floating
right in a flex header alongside the live indicator
- Rows: avg resolve time (last ≤5 resolved) + avg open age (all open)
- avg open age colored via CSS var --oc: red/orange/black per threshold
- "no open decisions" shown as muted italic when queue is empty
doc-overlay component (src/components/doc-overlay.js)
- withDocHelp(element, docPath) — adds absolute-positioned ? button
that is invisible until the parent is hovered; click opens overlay
- Overlay: fixed backdrop + animated box with iframe; closes on Esc,
backdrop click, or the close button
- CSS injected once via style tag (STYLE_ID guard, same pattern as MultiSelect)
? buttons wired up in decisions.md
- KPI infobox → /docs/decisions-kpi
- Cumulative chart (wrapped in position:relative div) → /docs/decisions-kpi
- Filter & List section header → /docs/decisions-kpi
Reference page (src/docs/decisions-kpi.md)
- Standalone Observable Framework page at /docs/decisions-kpi
- Documents: KPI card (avg resolve, avg open age, color thresholds),
Resolution History chart (cumulative, period→resolution mapping, filter
interaction, timestamp logic), Filter & List (type/status/search, card
age badge, escalation)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 11:48:47 +01:00
function _getTs(d) { return new Date(d.decided_at ?? d.created_at); }
2026-02-26 07:22:25 +01:00
function _bucketKey(t, unit, start) {
switch (unit) {
case "hour": return new Date(t.getFullYear(), t.getMonth(), t.getDate(), t.getHours()).getTime();
case "day": return new Date(t.getFullYear(), t.getMonth(), t.getDate()).getTime();
case "week": {
const w = Math.floor((t - start) / (7 * 864e5));
return start.getTime() + w * 7 * 864e5;
}
case "month": return new Date(t.getFullYear(), t.getMonth(), 1).getTime();
}
}
Dashboard decisions: list view with MultiSelect filters
- Replace Pending/Made tab + Inputs.table with MultiSelect filters and
a proper list view
- Filters: Type (pending/made), Status (open/escalated/resolved/superseded),
title text search — all stable across polls (no data dependency)
- Each decision renders as a compact card: left border coloured by status
(blue=open, amber=escalated, green=resolved, gray=superseded), type and
status badges, domain context, deadline (red+overdue warning if past),
full title, description/rationale snippet, resolved-by attribution
- Decisions sorted: escalated → open → resolved → superseded, then by
deadline ascending within each group
- Fetch now includes topics alongside decisions for domain name join
- Escalation warning box and velocity chart retained
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 00:39:44 +01:00
2026-02-26 07:22:25 +01:00
function _genBuckets(start, end, unit) {
const bkts = [];
let cur = new Date(start);
while (cur < = end) {
bkts.push(cur.getTime());
if (unit === "hour") cur = new Date(cur.getTime() + 36e5);
else if (unit === "day") cur = new Date(cur.getTime() + 864e5);
else if (unit === "week") cur = new Date(cur.getTime() + 7 * 864e5);
else if (unit === "month") cur = new Date(cur.getFullYear(), cur.getMonth() + 1, 1);
}
return bkts;
}
const _now = new Date();
dashboard: prominent KPI infobox, doc-overlay component, decisions reference page
KPI infobox
- Replace slim kpi-bar with a boxed card (border, shadow, 195–240px) floating
right in a flex header alongside the live indicator
- Rows: avg resolve time (last ≤5 resolved) + avg open age (all open)
- avg open age colored via CSS var --oc: red/orange/black per threshold
- "no open decisions" shown as muted italic when queue is empty
doc-overlay component (src/components/doc-overlay.js)
- withDocHelp(element, docPath) — adds absolute-positioned ? button
that is invisible until the parent is hovered; click opens overlay
- Overlay: fixed backdrop + animated box with iframe; closes on Esc,
backdrop click, or the close button
- CSS injected once via style tag (STYLE_ID guard, same pattern as MultiSelect)
? buttons wired up in decisions.md
- KPI infobox → /docs/decisions-kpi
- Cumulative chart (wrapped in position:relative div) → /docs/decisions-kpi
- Filter & List section header → /docs/decisions-kpi
Reference page (src/docs/decisions-kpi.md)
- Standalone Observable Framework page at /docs/decisions-kpi
- Documents: KPI card (avg resolve, avg open age, color thresholds),
Resolution History chart (cumulative, period→resolution mapping, filter
interaction, timestamp logic), Filter & List (type/status/search, card
age badge, escalation)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 11:48:47 +01:00
const _y = _now.getFullYear(), _mo = _now.getMonth();
2026-02-26 07:22:25 +01:00
let _start, _unit, _tickFmt;
switch (period) {
case "day":
dashboard: prominent KPI infobox, doc-overlay component, decisions reference page
KPI infobox
- Replace slim kpi-bar with a boxed card (border, shadow, 195–240px) floating
right in a flex header alongside the live indicator
- Rows: avg resolve time (last ≤5 resolved) + avg open age (all open)
- avg open age colored via CSS var --oc: red/orange/black per threshold
- "no open decisions" shown as muted italic when queue is empty
doc-overlay component (src/components/doc-overlay.js)
- withDocHelp(element, docPath) — adds absolute-positioned ? button
that is invisible until the parent is hovered; click opens overlay
- Overlay: fixed backdrop + animated box with iframe; closes on Esc,
backdrop click, or the close button
- CSS injected once via style tag (STYLE_ID guard, same pattern as MultiSelect)
? buttons wired up in decisions.md
- KPI infobox → /docs/decisions-kpi
- Cumulative chart (wrapped in position:relative div) → /docs/decisions-kpi
- Filter & List section header → /docs/decisions-kpi
Reference page (src/docs/decisions-kpi.md)
- Standalone Observable Framework page at /docs/decisions-kpi
- Documents: KPI card (avg resolve, avg open age, color thresholds),
Resolution History chart (cumulative, period→resolution mapping, filter
interaction, timestamp logic), Filter & List (type/status/search, card
age badge, escalation)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 11:48:47 +01:00
_start = new Date(_y, _mo, _now.getDate());
_unit = "hour";
2026-02-26 07:22:25 +01:00
_tickFmt = d => `${String(d.getHours()).padStart(2, "0")}:00` ;
break;
case "week": {
const _7ago = new Date(_now - 7 * 864e5);
dashboard: prominent KPI infobox, doc-overlay component, decisions reference page
KPI infobox
- Replace slim kpi-bar with a boxed card (border, shadow, 195–240px) floating
right in a flex header alongside the live indicator
- Rows: avg resolve time (last ≤5 resolved) + avg open age (all open)
- avg open age colored via CSS var --oc: red/orange/black per threshold
- "no open decisions" shown as muted italic when queue is empty
doc-overlay component (src/components/doc-overlay.js)
- withDocHelp(element, docPath) — adds absolute-positioned ? button
that is invisible until the parent is hovered; click opens overlay
- Overlay: fixed backdrop + animated box with iframe; closes on Esc,
backdrop click, or the close button
- CSS injected once via style tag (STYLE_ID guard, same pattern as MultiSelect)
? buttons wired up in decisions.md
- KPI infobox → /docs/decisions-kpi
- Cumulative chart (wrapped in position:relative div) → /docs/decisions-kpi
- Filter & List section header → /docs/decisions-kpi
Reference page (src/docs/decisions-kpi.md)
- Standalone Observable Framework page at /docs/decisions-kpi
- Documents: KPI card (avg resolve, avg open age, color thresholds),
Resolution History chart (cumulative, period→resolution mapping, filter
interaction, timestamp logic), Filter & List (type/status/search, card
age badge, escalation)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 11:48:47 +01:00
_start = new Date(_7ago.getFullYear(), _7ago.getMonth(), _7ago.getDate());
_unit = "day";
2026-02-26 07:22:25 +01:00
_tickFmt = d => d.toLocaleDateString(undefined, {weekday: "short", month: "short", day: "numeric"});
break;
}
case "month":
dashboard: prominent KPI infobox, doc-overlay component, decisions reference page
KPI infobox
- Replace slim kpi-bar with a boxed card (border, shadow, 195–240px) floating
right in a flex header alongside the live indicator
- Rows: avg resolve time (last ≤5 resolved) + avg open age (all open)
- avg open age colored via CSS var --oc: red/orange/black per threshold
- "no open decisions" shown as muted italic when queue is empty
doc-overlay component (src/components/doc-overlay.js)
- withDocHelp(element, docPath) — adds absolute-positioned ? button
that is invisible until the parent is hovered; click opens overlay
- Overlay: fixed backdrop + animated box with iframe; closes on Esc,
backdrop click, or the close button
- CSS injected once via style tag (STYLE_ID guard, same pattern as MultiSelect)
? buttons wired up in decisions.md
- KPI infobox → /docs/decisions-kpi
- Cumulative chart (wrapped in position:relative div) → /docs/decisions-kpi
- Filter & List section header → /docs/decisions-kpi
Reference page (src/docs/decisions-kpi.md)
- Standalone Observable Framework page at /docs/decisions-kpi
- Documents: KPI card (avg resolve, avg open age, color thresholds),
Resolution History chart (cumulative, period→resolution mapping, filter
interaction, timestamp logic), Filter & List (type/status/search, card
age badge, escalation)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 11:48:47 +01:00
_start = new Date(_y, _mo, 1);
_unit = "week";
2026-02-26 07:22:25 +01:00
_tickFmt = d => `W/${d.toLocaleDateString(undefined, {month: "short", day: "numeric"})}` ;
break;
case "quarter":
dashboard: prominent KPI infobox, doc-overlay component, decisions reference page
KPI infobox
- Replace slim kpi-bar with a boxed card (border, shadow, 195–240px) floating
right in a flex header alongside the live indicator
- Rows: avg resolve time (last ≤5 resolved) + avg open age (all open)
- avg open age colored via CSS var --oc: red/orange/black per threshold
- "no open decisions" shown as muted italic when queue is empty
doc-overlay component (src/components/doc-overlay.js)
- withDocHelp(element, docPath) — adds absolute-positioned ? button
that is invisible until the parent is hovered; click opens overlay
- Overlay: fixed backdrop + animated box with iframe; closes on Esc,
backdrop click, or the close button
- CSS injected once via style tag (STYLE_ID guard, same pattern as MultiSelect)
? buttons wired up in decisions.md
- KPI infobox → /docs/decisions-kpi
- Cumulative chart (wrapped in position:relative div) → /docs/decisions-kpi
- Filter & List section header → /docs/decisions-kpi
Reference page (src/docs/decisions-kpi.md)
- Standalone Observable Framework page at /docs/decisions-kpi
- Documents: KPI card (avg resolve, avg open age, color thresholds),
Resolution History chart (cumulative, period→resolution mapping, filter
interaction, timestamp logic), Filter & List (type/status/search, card
age badge, escalation)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 11:48:47 +01:00
_start = new Date(_y, Math.floor(_mo / 3) * 3, 1);
_unit = "month";
2026-02-26 07:22:25 +01:00
_tickFmt = d => d.toLocaleDateString(undefined, {month: "short"});
break;
case "YTD":
dashboard: prominent KPI infobox, doc-overlay component, decisions reference page
KPI infobox
- Replace slim kpi-bar with a boxed card (border, shadow, 195–240px) floating
right in a flex header alongside the live indicator
- Rows: avg resolve time (last ≤5 resolved) + avg open age (all open)
- avg open age colored via CSS var --oc: red/orange/black per threshold
- "no open decisions" shown as muted italic when queue is empty
doc-overlay component (src/components/doc-overlay.js)
- withDocHelp(element, docPath) — adds absolute-positioned ? button
that is invisible until the parent is hovered; click opens overlay
- Overlay: fixed backdrop + animated box with iframe; closes on Esc,
backdrop click, or the close button
- CSS injected once via style tag (STYLE_ID guard, same pattern as MultiSelect)
? buttons wired up in decisions.md
- KPI infobox → /docs/decisions-kpi
- Cumulative chart (wrapped in position:relative div) → /docs/decisions-kpi
- Filter & List section header → /docs/decisions-kpi
Reference page (src/docs/decisions-kpi.md)
- Standalone Observable Framework page at /docs/decisions-kpi
- Documents: KPI card (avg resolve, avg open age, color thresholds),
Resolution History chart (cumulative, period→resolution mapping, filter
interaction, timestamp logic), Filter & List (type/status/search, card
age badge, escalation)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 11:48:47 +01:00
_start = new Date(_y, 0, 1);
_unit = "month";
2026-02-26 07:22:25 +01:00
_tickFmt = d => d.toLocaleDateString(undefined, {month: "short"});
break;
case "year": {
dashboard: prominent KPI infobox, doc-overlay component, decisions reference page
KPI infobox
- Replace slim kpi-bar with a boxed card (border, shadow, 195–240px) floating
right in a flex header alongside the live indicator
- Rows: avg resolve time (last ≤5 resolved) + avg open age (all open)
- avg open age colored via CSS var --oc: red/orange/black per threshold
- "no open decisions" shown as muted italic when queue is empty
doc-overlay component (src/components/doc-overlay.js)
- withDocHelp(element, docPath) — adds absolute-positioned ? button
that is invisible until the parent is hovered; click opens overlay
- Overlay: fixed backdrop + animated box with iframe; closes on Esc,
backdrop click, or the close button
- CSS injected once via style tag (STYLE_ID guard, same pattern as MultiSelect)
? buttons wired up in decisions.md
- KPI infobox → /docs/decisions-kpi
- Cumulative chart (wrapped in position:relative div) → /docs/decisions-kpi
- Filter & List section header → /docs/decisions-kpi
Reference page (src/docs/decisions-kpi.md)
- Standalone Observable Framework page at /docs/decisions-kpi
- Documents: KPI card (avg resolve, avg open age, color thresholds),
Resolution History chart (cumulative, period→resolution mapping, filter
interaction, timestamp logic), Filter & List (type/status/search, card
age badge, escalation)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 11:48:47 +01:00
const _ago = new Date(_now - 365 * 864e5);
_start = new Date(_ago.getFullYear(), _ago.getMonth(), 1);
_unit = "month";
2026-02-26 07:22:25 +01:00
_tickFmt = d => d.toLocaleDateString(undefined, {month: "short", year: "2-digit"});
break;
}
default: {
dashboard: prominent KPI infobox, doc-overlay component, decisions reference page
KPI infobox
- Replace slim kpi-bar with a boxed card (border, shadow, 195–240px) floating
right in a flex header alongside the live indicator
- Rows: avg resolve time (last ≤5 resolved) + avg open age (all open)
- avg open age colored via CSS var --oc: red/orange/black per threshold
- "no open decisions" shown as muted italic when queue is empty
doc-overlay component (src/components/doc-overlay.js)
- withDocHelp(element, docPath) — adds absolute-positioned ? button
that is invisible until the parent is hovered; click opens overlay
- Overlay: fixed backdrop + animated box with iframe; closes on Esc,
backdrop click, or the close button
- CSS injected once via style tag (STYLE_ID guard, same pattern as MultiSelect)
? buttons wired up in decisions.md
- KPI infobox → /docs/decisions-kpi
- Cumulative chart (wrapped in position:relative div) → /docs/decisions-kpi
- Filter & List section header → /docs/decisions-kpi
Reference page (src/docs/decisions-kpi.md)
- Standalone Observable Framework page at /docs/decisions-kpi
- Documents: KPI card (avg resolve, avg open age, color thresholds),
Resolution History chart (cumulative, period→resolution mapping, filter
interaction, timestamp logic), Filter & List (type/status/search, card
age badge, escalation)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 11:48:47 +01:00
const _minTs = filtered.length ? Math.min(...filtered.map(d => _getTs(d))) : _now.getTime();
2026-02-26 07:22:25 +01:00
const _minD = new Date(_minTs);
dashboard: prominent KPI infobox, doc-overlay component, decisions reference page
KPI infobox
- Replace slim kpi-bar with a boxed card (border, shadow, 195–240px) floating
right in a flex header alongside the live indicator
- Rows: avg resolve time (last ≤5 resolved) + avg open age (all open)
- avg open age colored via CSS var --oc: red/orange/black per threshold
- "no open decisions" shown as muted italic when queue is empty
doc-overlay component (src/components/doc-overlay.js)
- withDocHelp(element, docPath) — adds absolute-positioned ? button
that is invisible until the parent is hovered; click opens overlay
- Overlay: fixed backdrop + animated box with iframe; closes on Esc,
backdrop click, or the close button
- CSS injected once via style tag (STYLE_ID guard, same pattern as MultiSelect)
? buttons wired up in decisions.md
- KPI infobox → /docs/decisions-kpi
- Cumulative chart (wrapped in position:relative div) → /docs/decisions-kpi
- Filter & List section header → /docs/decisions-kpi
Reference page (src/docs/decisions-kpi.md)
- Standalone Observable Framework page at /docs/decisions-kpi
- Documents: KPI card (avg resolve, avg open age, color thresholds),
Resolution History chart (cumulative, period→resolution mapping, filter
interaction, timestamp logic), Filter & List (type/status/search, card
age badge, escalation)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 11:48:47 +01:00
_start = new Date(_minD.getFullYear(), _minD.getMonth(), 1);
_unit = "month";
2026-02-26 07:22:25 +01:00
_tickFmt = d => d.toLocaleDateString(undefined, {month: "short", year: "numeric"});
break;
}
}
const _inWindow = period === "all"
? [...filtered]
: filtered.filter(d => { const t = _getTs(d); return t >= _start & & t < = _now; });
const _bktKeys = _genBuckets(_start, _now, _unit);
const _cntMap = new Map(_bktKeys.map(k => [k, 0]));
for (const d of _inWindow) {
const key = _bucketKey(_getTs(d), _unit, _start);
if (_cntMap.has(key)) _cntMap.set(key, (_cntMap.get(key) || 0) + 1);
}
let _cum = 0;
const _chartData = _bktKeys.map(k => {
const delta = _cntMap.get(k) || 0;
_cum += delta;
return {date: new Date(k), count: _cum, delta};
});
dashboard: prominent KPI infobox, doc-overlay component, decisions reference page
KPI infobox
- Replace slim kpi-bar with a boxed card (border, shadow, 195–240px) floating
right in a flex header alongside the live indicator
- Rows: avg resolve time (last ≤5 resolved) + avg open age (all open)
- avg open age colored via CSS var --oc: red/orange/black per threshold
- "no open decisions" shown as muted italic when queue is empty
doc-overlay component (src/components/doc-overlay.js)
- withDocHelp(element, docPath) — adds absolute-positioned ? button
that is invisible until the parent is hovered; click opens overlay
- Overlay: fixed backdrop + animated box with iframe; closes on Esc,
backdrop click, or the close button
- CSS injected once via style tag (STYLE_ID guard, same pattern as MultiSelect)
? buttons wired up in decisions.md
- KPI infobox → /docs/decisions-kpi
- Cumulative chart (wrapped in position:relative div) → /docs/decisions-kpi
- Filter & List section header → /docs/decisions-kpi
Reference page (src/docs/decisions-kpi.md)
- Standalone Observable Framework page at /docs/decisions-kpi
- Documents: KPI card (avg resolve, avg open age, color thresholds),
Resolution History chart (cumulative, period→resolution mapping, filter
interaction, timestamp logic), Filter & List (type/status/search, card
age badge, escalation)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 11:48:47 +01:00
if (_inWindow.length === 0) {
display(html`<p class="dim">No decisions in this period.</p>` );
} else {
const _plotEl = Plot.plot({
x: {type: "time", tickFormat: _tickFmt, tickRotate: -30, label: null},
y: {grid: true, label: "Cumulative decisions"},
marks: [
Plot.areaY(_chartData, {x: "date", y: "count", fill: "steelblue", fillOpacity: 0.15, curve: "step-after"}),
Plot.lineY(_chartData, {x: "date", y: "count", stroke: "steelblue", strokeWidth: 2, curve: "step-after"}),
Plot.dot(_chartData.filter(d => d.delta > 0), {
x: "date", y: "count", fill: "steelblue", r: 4, tip: true,
title: d => `${_tickFmt(d.date)}\n+${d.delta} → ${d.count} total` ,
}),
Plot.ruleY([0]),
],
marginBottom: 70,
width: 700,
});
const _plotWrap = html`<div style="position:relative">${_plotEl}</div>` ;
withDocHelp(_plotWrap, "/docs/decisions-kpi");
display(_plotWrap);
}
2026-02-26 07:22:25 +01:00
```
## Filter & List
```js
dashboard: prominent KPI infobox, doc-overlay component, decisions reference page
KPI infobox
- Replace slim kpi-bar with a boxed card (border, shadow, 195–240px) floating
right in a flex header alongside the live indicator
- Rows: avg resolve time (last ≤5 resolved) + avg open age (all open)
- avg open age colored via CSS var --oc: red/orange/black per threshold
- "no open decisions" shown as muted italic when queue is empty
doc-overlay component (src/components/doc-overlay.js)
- withDocHelp(element, docPath) — adds absolute-positioned ? button
that is invisible until the parent is hovered; click opens overlay
- Overlay: fixed backdrop + animated box with iframe; closes on Esc,
backdrop click, or the close button
- CSS injected once via style tag (STYLE_ID guard, same pattern as MultiSelect)
? buttons wired up in decisions.md
- KPI infobox → /docs/decisions-kpi
- Cumulative chart (wrapped in position:relative div) → /docs/decisions-kpi
- Filter & List section header → /docs/decisions-kpi
Reference page (src/docs/decisions-kpi.md)
- Standalone Observable Framework page at /docs/decisions-kpi
- Documents: KPI card (avg resolve, avg open age, color thresholds),
Resolution History chart (cumulative, period→resolution mapping, filter
interaction, timestamp logic), Filter & List (type/status/search, card
age badge, escalation)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 11:48:47 +01:00
const _filterWrap = html`<div style="position:relative;padding-right:2rem">${_filtersForm}</div>` ;
withDocHelp(_filterWrap, "/docs/decisions-kpi");
display(_filterWrap);
2026-02-26 07:22:25 +01:00
```
```js
2026-02-26 07:50:54 +01:00
const _nowCards = new Date();
Dashboard decisions: list view with MultiSelect filters
- Replace Pending/Made tab + Inputs.table with MultiSelect filters and
a proper list view
- Filters: Type (pending/made), Status (open/escalated/resolved/superseded),
title text search — all stable across polls (no data dependency)
- Each decision renders as a compact card: left border coloured by status
(blue=open, amber=escalated, green=resolved, gray=superseded), type and
status badges, domain context, deadline (red+overdue warning if past),
full title, description/rationale snippet, resolved-by attribution
- Decisions sorted: escalated → open → resolved → superseded, then by
deadline ascending within each group
- Fetch now includes topics alongside decisions for domain name join
- Escalation warning box and velocity chart retained
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 00:39:44 +01:00
if (filtered.length === 0) {
display(html`<p class="dim">No decisions match the current filter.</p>` );
} else {
display(html`< div class = "dec-list" > ${filtered.map(d => {
const border = STATUS_BORDER[d.status] ?? "#ccc ";
const snippet = (d.description || d.rationale || "").slice(0, 200);
const due = fmtDate(d.deadline);
const decided = fmtDate(d.decided_at);
const overdue = isOverdue(d.deadline);
2026-02-26 07:50:54 +01:00
const _isOpen = d.status === "open" || d.status === "escalated";
const _ageMs = d.decided_at
? new Date(d.decided_at) - new Date(d.created_at)
: _nowCards - new Date(d.created_at);
const _ageText = d.decided_at ? `took ${fmtDuration(_ageMs)}` : `open ${fmtDuration(_ageMs)}` ;
const _ageWarn = _isOpen & & _meanResolveMs !== null & & _ageMs > _meanResolveMs;
Dashboard decisions: list view with MultiSelect filters
- Replace Pending/Made tab + Inputs.table with MultiSelect filters and
a proper list view
- Filters: Type (pending/made), Status (open/escalated/resolved/superseded),
title text search — all stable across polls (no data dependency)
- Each decision renders as a compact card: left border coloured by status
(blue=open, amber=escalated, green=resolved, gray=superseded), type and
status badges, domain context, deadline (red+overdue warning if past),
full title, description/rationale snippet, resolved-by attribution
- Decisions sorted: escalated → open → resolved → superseded, then by
deadline ascending within each group
- Fetch now includes topics alongside decisions for domain name join
- Escalation warning box and velocity chart retained
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 00:39:44 +01:00
return html`< div class = "dec-item" style = "border-left-color:${border}" >
< div class = "dec-item-header" >
< span class = "dec-badge ${TYPE_CLASS[d.decision_type] ?? ''}" > ${d.decision_type}< / span >
< span class = "dec-badge dec-badge-status dec-badge-${d.status}" >
${d.status === "escalated" ? "⚠ " : ""}${d.status}
< / span >
${d.domain ? html`<span class="dec-context">${d.domain}</span>` : ""}
${due ? html`< span class = "dec-due ${overdue ? 'dec-overdue' : ''}" >
${overdue ? "⚠ overdue" : "due"} ${due}
< / span > ` : ""}
2026-02-26 07:50:54 +01:00
< span class = "dec-age ${_ageWarn ? 'dec-age-warn' : ''}" > ${_ageText}< / span >
Dashboard decisions: list view with MultiSelect filters
- Replace Pending/Made tab + Inputs.table with MultiSelect filters and
a proper list view
- Filters: Type (pending/made), Status (open/escalated/resolved/superseded),
title text search — all stable across polls (no data dependency)
- Each decision renders as a compact card: left border coloured by status
(blue=open, amber=escalated, green=resolved, gray=superseded), type and
status badges, domain context, deadline (red+overdue warning if past),
full title, description/rationale snippet, resolved-by attribution
- Decisions sorted: escalated → open → resolved → superseded, then by
deadline ascending within each group
- Fetch now includes topics alongside decisions for domain name join
- Escalation warning box and velocity chart retained
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 00:39:44 +01:00
< span class = "dec-date" > ${fmtDate(d.created_at)}< / span >
< / div >
< div class = "dec-title" > ${d.title}< / div >
${snippet ? html`<div class="dec-snippet">${snippet}${snippet.length < (d.description || d.rationale || "").length ? "…" : ""}</div>` : ""}
${d.decided_by ? html`<div class="dec-resolved-by">✓ ${d.decided_by}${decided ? " · " + decided : ""}</div>` : ""}
${d.escalation_note ? html`<div class="dec-escalation-note">${d.escalation_note}</div>` : ""}
< / div > `;
})}< / div > `);
}
Add state-hub v0.1 — local-first state service for the Custodian
Implements the first live layer of the Custodian cognitive infrastructure:
PostgreSQL schema, FastAPI REST API, FastMCP stdio server, and Observable
Framework telemetry dashboard.
- state-hub/: full stack (docker-compose, FastAPI, Alembic, MCP server, dashboard)
- 5 DB tables: topics, workstreams, tasks, decisions, progress_events
- 11 MCP tools + 5 resources registered in .mcp.json
- Observable dashboard: Overview, Workstreams, Decisions, Progress pages
- CLAUDE.md: session protocol (get_state_summary / add_progress_event ritual)
- ~/.claude/CLAUDE.md: global cross-project reference to the hub
- scripts/pull_image.py: WSL2 TLS-resilient Docker image downloader
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 17:47:49 +01:00
```
```js
Dashboard decisions: list view with MultiSelect filters
- Replace Pending/Made tab + Inputs.table with MultiSelect filters and
a proper list view
- Filters: Type (pending/made), Status (open/escalated/resolved/superseded),
title text search — all stable across polls (no data dependency)
- Each decision renders as a compact card: left border coloured by status
(blue=open, amber=escalated, green=resolved, gray=superseded), type and
status badges, domain context, deadline (red+overdue warning if past),
full title, description/rationale snippet, resolved-by attribution
- Decisions sorted: escalated → open → resolved → superseded, then by
deadline ascending within each group
- Fetch now includes topics alongside decisions for domain name join
- Escalation warning box and velocity chart retained
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 00:39:44 +01:00
if (escalated.length > 0) {
Add state-hub v0.1 — local-first state service for the Custodian
Implements the first live layer of the Custodian cognitive infrastructure:
PostgreSQL schema, FastAPI REST API, FastMCP stdio server, and Observable
Framework telemetry dashboard.
- state-hub/: full stack (docker-compose, FastAPI, Alembic, MCP server, dashboard)
- 5 DB tables: topics, workstreams, tasks, decisions, progress_events
- 11 MCP tools + 5 resources registered in .mcp.json
- Observable dashboard: Overview, Workstreams, Decisions, Progress pages
- CLAUDE.md: session protocol (get_state_summary / add_progress_event ritual)
- ~/.claude/CLAUDE.md: global cross-project reference to the hub
- scripts/pull_image.py: WSL2 TLS-resilient Docker image downloader
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 17:47:49 +01:00
display(html`< div class = "escalation-box" >
Dashboard decisions: list view with MultiSelect filters
- Replace Pending/Made tab + Inputs.table with MultiSelect filters and
a proper list view
- Filters: Type (pending/made), Status (open/escalated/resolved/superseded),
title text search — all stable across polls (no data dependency)
- Each decision renders as a compact card: left border coloured by status
(blue=open, amber=escalated, green=resolved, gray=superseded), type and
status badges, domain context, deadline (red+overdue warning if past),
full title, description/rationale snippet, resolved-by attribution
- Decisions sorted: escalated → open → resolved → superseded, then by
deadline ascending within each group
- Fetch now includes topics alongside decisions for domain name join
- Escalation warning box and velocity chart retained
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 00:39:44 +01:00
< strong > ⚠ ${escalated.length} escalated decision${escalated.length > 1 ? "s" : ""} require human approval before any action is taken (constitution §4).< / strong >
< ul > ${escalated.map(d => html`<li><b>${d.title}</b>: ${d.escalation_note}</li>` )}</ ul >
Add state-hub v0.1 — local-first state service for the Custodian
Implements the first live layer of the Custodian cognitive infrastructure:
PostgreSQL schema, FastAPI REST API, FastMCP stdio server, and Observable
Framework telemetry dashboard.
- state-hub/: full stack (docker-compose, FastAPI, Alembic, MCP server, dashboard)
- 5 DB tables: topics, workstreams, tasks, decisions, progress_events
- 11 MCP tools + 5 resources registered in .mcp.json
- Observable dashboard: Overview, Workstreams, Decisions, Progress pages
- CLAUDE.md: session protocol (get_state_summary / add_progress_event ritual)
- ~/.claude/CLAUDE.md: global cross-project reference to the hub
- scripts/pull_image.py: WSL2 TLS-resilient Docker image downloader
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 17:47:49 +01:00
< / div > `);
}
```
< style >
2026-02-26 12:07:49 +01:00
/* ── Live indicator ───────────────────────────────────────────────────────── */
dashboard: prominent KPI infobox, doc-overlay component, decisions reference page
KPI infobox
- Replace slim kpi-bar with a boxed card (border, shadow, 195–240px) floating
right in a flex header alongside the live indicator
- Rows: avg resolve time (last ≤5 resolved) + avg open age (all open)
- avg open age colored via CSS var --oc: red/orange/black per threshold
- "no open decisions" shown as muted italic when queue is empty
doc-overlay component (src/components/doc-overlay.js)
- withDocHelp(element, docPath) — adds absolute-positioned ? button
that is invisible until the parent is hovered; click opens overlay
- Overlay: fixed backdrop + animated box with iframe; closes on Esc,
backdrop click, or the close button
- CSS injected once via style tag (STYLE_ID guard, same pattern as MultiSelect)
? buttons wired up in decisions.md
- KPI infobox → /docs/decisions-kpi
- Cumulative chart (wrapped in position:relative div) → /docs/decisions-kpi
- Filter & List section header → /docs/decisions-kpi
Reference page (src/docs/decisions-kpi.md)
- Standalone Observable Framework page at /docs/decisions-kpi
- Documents: KPI card (avg resolve, avg open age, color thresholds),
Resolution History chart (cumulative, period→resolution mapping, filter
interaction, timestamp logic), Filter & List (type/status/search, card
age badge, escalation)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 11:48:47 +01:00
.live-indicator {
font-size: 0.8rem;
color: gray;
2026-02-26 12:07:49 +01:00
text-align: right;
margin-bottom: 0.75rem;
dashboard: prominent KPI infobox, doc-overlay component, decisions reference page
KPI infobox
- Replace slim kpi-bar with a boxed card (border, shadow, 195–240px) floating
right in a flex header alongside the live indicator
- Rows: avg resolve time (last ≤5 resolved) + avg open age (all open)
- avg open age colored via CSS var --oc: red/orange/black per threshold
- "no open decisions" shown as muted italic when queue is empty
doc-overlay component (src/components/doc-overlay.js)
- withDocHelp(element, docPath) — adds absolute-positioned ? button
that is invisible until the parent is hovered; click opens overlay
- Overlay: fixed backdrop + animated box with iframe; closes on Esc,
backdrop click, or the close button
- CSS injected once via style tag (STYLE_ID guard, same pattern as MultiSelect)
? buttons wired up in decisions.md
- KPI infobox → /docs/decisions-kpi
- Cumulative chart (wrapped in position:relative div) → /docs/decisions-kpi
- Filter & List section header → /docs/decisions-kpi
Reference page (src/docs/decisions-kpi.md)
- Standalone Observable Framework page at /docs/decisions-kpi
- Documents: KPI card (avg resolve, avg open age, color thresholds),
Resolution History chart (cumulative, period→resolution mapping, filter
interaction, timestamp logic), Filter & List (type/status/search, card
age badge, escalation)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 11:48:47 +01:00
}
/* ── KPI infobox ──────────────────────────────────────────────────────────── */
.kpi-infobox {
background: var(--theme-background-alt, #f9f9f9 );
border: 1px solid var(--theme-foreground-faint, #e0e0e0 );
border-radius: 10px;
padding: 0.75rem 1rem;
position: relative;
box-shadow: 0 1px 6px rgba(0,0,0,0.07);
2026-02-26 13:29:34 +01:00
margin-bottom: 1.25rem;
dashboard: prominent KPI infobox, doc-overlay component, decisions reference page
KPI infobox
- Replace slim kpi-bar with a boxed card (border, shadow, 195–240px) floating
right in a flex header alongside the live indicator
- Rows: avg resolve time (last ≤5 resolved) + avg open age (all open)
- avg open age colored via CSS var --oc: red/orange/black per threshold
- "no open decisions" shown as muted italic when queue is empty
doc-overlay component (src/components/doc-overlay.js)
- withDocHelp(element, docPath) — adds absolute-positioned ? button
that is invisible until the parent is hovered; click opens overlay
- Overlay: fixed backdrop + animated box with iframe; closes on Esc,
backdrop click, or the close button
- CSS injected once via style tag (STYLE_ID guard, same pattern as MultiSelect)
? buttons wired up in decisions.md
- KPI infobox → /docs/decisions-kpi
- Cumulative chart (wrapped in position:relative div) → /docs/decisions-kpi
- Filter & List section header → /docs/decisions-kpi
Reference page (src/docs/decisions-kpi.md)
- Standalone Observable Framework page at /docs/decisions-kpi
- Documents: KPI card (avg resolve, avg open age, color thresholds),
Resolution History chart (cumulative, period→resolution mapping, filter
interaction, timestamp logic), Filter & List (type/status/search, card
age badge, escalation)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 11:48:47 +01:00
}
.kpi-infobox-title {
font-size: 0.68rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--theme-foreground-muted, #888 );
margin-bottom: 0.55rem;
padding-right: 1.6rem; /* room for ? button */
}
.kpi-row {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
padding: 0.3rem 0;
}
.kpi-row + .kpi-row {
border-top: 1px solid var(--theme-foreground-faint, #eee );
}
.kpi-row-label {
font-size: 0.8rem;
color: var(--theme-foreground-muted, #666 );
white-space: nowrap;
}
.kpi-muted { color: var(--theme-foreground-faint, #aaa ); font-style: italic; }
.kpi-row-right { text-align: right; }
.kpi-row-value {
font-size: 1.25rem;
font-weight: 700;
font-variant-numeric: tabular-nums;
line-height: 1.1;
color: var(--theme-foreground, #111 );
}
.kpi-colorable { color: var(--oc, inherit); }
.kpi-row-sub {
font-size: 0.68rem;
color: var(--theme-foreground-faint, #aaa );
line-height: 1.2;
}
/* ── Utility ──────────────────────────────────────────────────────────────── */
Dashboard decisions: list view with MultiSelect filters
- Replace Pending/Made tab + Inputs.table with MultiSelect filters and
a proper list view
- Filters: Type (pending/made), Status (open/escalated/resolved/superseded),
title text search — all stable across polls (no data dependency)
- Each decision renders as a compact card: left border coloured by status
(blue=open, amber=escalated, green=resolved, gray=superseded), type and
status badges, domain context, deadline (red+overdue warning if past),
full title, description/rationale snippet, resolved-by attribution
- Decisions sorted: escalated → open → resolved → superseded, then by
deadline ascending within each group
- Fetch now includes topics alongside decisions for domain name join
- Escalation warning box and velocity chart retained
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 00:39:44 +01:00
.dim { color: gray; font-style: italic; }
dashboard: prominent KPI infobox, doc-overlay component, decisions reference page
KPI infobox
- Replace slim kpi-bar with a boxed card (border, shadow, 195–240px) floating
right in a flex header alongside the live indicator
- Rows: avg resolve time (last ≤5 resolved) + avg open age (all open)
- avg open age colored via CSS var --oc: red/orange/black per threshold
- "no open decisions" shown as muted italic when queue is empty
doc-overlay component (src/components/doc-overlay.js)
- withDocHelp(element, docPath) — adds absolute-positioned ? button
that is invisible until the parent is hovered; click opens overlay
- Overlay: fixed backdrop + animated box with iframe; closes on Esc,
backdrop click, or the close button
- CSS injected once via style tag (STYLE_ID guard, same pattern as MultiSelect)
? buttons wired up in decisions.md
- KPI infobox → /docs/decisions-kpi
- Cumulative chart (wrapped in position:relative div) → /docs/decisions-kpi
- Filter & List section header → /docs/decisions-kpi
Reference page (src/docs/decisions-kpi.md)
- Standalone Observable Framework page at /docs/decisions-kpi
- Documents: KPI card (avg resolve, avg open age, color thresholds),
Resolution History chart (cumulative, period→resolution mapping, filter
interaction, timestamp logic), Filter & List (type/status/search, card
age badge, escalation)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 11:48:47 +01:00
/* ── Filter bar ───────────────────────────────────────────────────────────── */
Dashboard decisions: list view with MultiSelect filters
- Replace Pending/Made tab + Inputs.table with MultiSelect filters and
a proper list view
- Filters: Type (pending/made), Status (open/escalated/resolved/superseded),
title text search — all stable across polls (no data dependency)
- Each decision renders as a compact card: left border coloured by status
(blue=open, amber=escalated, green=resolved, gray=superseded), type and
status badges, domain context, deadline (red+overdue warning if past),
full title, description/rationale snippet, resolved-by attribution
- Decisions sorted: escalated → open → resolved → superseded, then by
deadline ascending within each group
- Fetch now includes topics alongside decisions for domain name join
- Escalation warning box and velocity chart retained
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 00:39:44 +01:00
.filter-bar { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; margin-bottom: 1rem; }
.filter-search { display: flex; align-items: center; }
.filter-search input { height: 30px; font-size: 0.85rem; padding: 0.25rem 0.5rem; border-radius: 6px; border: 1px solid var(--theme-foreground-faint, #ccc ); background: var(--theme-background, #fff ); font-family: inherit; color: inherit; }
dashboard: prominent KPI infobox, doc-overlay component, decisions reference page
KPI infobox
- Replace slim kpi-bar with a boxed card (border, shadow, 195–240px) floating
right in a flex header alongside the live indicator
- Rows: avg resolve time (last ≤5 resolved) + avg open age (all open)
- avg open age colored via CSS var --oc: red/orange/black per threshold
- "no open decisions" shown as muted italic when queue is empty
doc-overlay component (src/components/doc-overlay.js)
- withDocHelp(element, docPath) — adds absolute-positioned ? button
that is invisible until the parent is hovered; click opens overlay
- Overlay: fixed backdrop + animated box with iframe; closes on Esc,
backdrop click, or the close button
- CSS injected once via style tag (STYLE_ID guard, same pattern as MultiSelect)
? buttons wired up in decisions.md
- KPI infobox → /docs/decisions-kpi
- Cumulative chart (wrapped in position:relative div) → /docs/decisions-kpi
- Filter & List section header → /docs/decisions-kpi
Reference page (src/docs/decisions-kpi.md)
- Standalone Observable Framework page at /docs/decisions-kpi
- Documents: KPI card (avg resolve, avg open age, color thresholds),
Resolution History chart (cumulative, period→resolution mapping, filter
interaction, timestamp logic), Filter & List (type/status/search, card
age badge, escalation)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 11:48:47 +01:00
/* ── Decision list ────────────────────────────────────────────────────────── */
Dashboard decisions: list view with MultiSelect filters
- Replace Pending/Made tab + Inputs.table with MultiSelect filters and
a proper list view
- Filters: Type (pending/made), Status (open/escalated/resolved/superseded),
title text search — all stable across polls (no data dependency)
- Each decision renders as a compact card: left border coloured by status
(blue=open, amber=escalated, green=resolved, gray=superseded), type and
status badges, domain context, deadline (red+overdue warning if past),
full title, description/rationale snippet, resolved-by attribution
- Decisions sorted: escalated → open → resolved → superseded, then by
deadline ascending within each group
- Fetch now includes topics alongside decisions for domain name join
- Escalation warning box and velocity chart retained
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 00:39:44 +01:00
.dec-list { display: flex; flex-direction: column; gap: 0.5rem; }
.dec-item { border-left: 3px solid #ccc ; border-radius: 0 6px 6px 0; background: var(--theme-background-alt); padding: 0.65rem 0.9rem; }
.dec-item-header { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin-bottom: 0.3rem; font-size: 0.75rem; }
.dec-badge { display: inline-block; padding: 0.1rem 0.45rem; border-radius: 10px; font-size: 0.7rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; }
.badge-pending { background: #fef3c7 ; color: #92400e ; }
.badge-made { background: #e0e7ff ; color: #3730a3 ; }
.dec-badge-status { font-weight: 500; text-transform: capitalize; }
.dec-badge-open { background: #dbeafe ; color: #1e40af ; }
.dec-badge-escalated { background: #fef3c7 ; color: #92400e ; }
.dec-badge-resolved { background: #dcfce7 ; color: #166534 ; }
.dec-badge-superseded { background: #f3f4f6 ; color: #6b7280 ; }
.dec-context { color: var(--theme-foreground-muted); }
dashboard: prominent KPI infobox, doc-overlay component, decisions reference page
KPI infobox
- Replace slim kpi-bar with a boxed card (border, shadow, 195–240px) floating
right in a flex header alongside the live indicator
- Rows: avg resolve time (last ≤5 resolved) + avg open age (all open)
- avg open age colored via CSS var --oc: red/orange/black per threshold
- "no open decisions" shown as muted italic when queue is empty
doc-overlay component (src/components/doc-overlay.js)
- withDocHelp(element, docPath) — adds absolute-positioned ? button
that is invisible until the parent is hovered; click opens overlay
- Overlay: fixed backdrop + animated box with iframe; closes on Esc,
backdrop click, or the close button
- CSS injected once via style tag (STYLE_ID guard, same pattern as MultiSelect)
? buttons wired up in decisions.md
- KPI infobox → /docs/decisions-kpi
- Cumulative chart (wrapped in position:relative div) → /docs/decisions-kpi
- Filter & List section header → /docs/decisions-kpi
Reference page (src/docs/decisions-kpi.md)
- Standalone Observable Framework page at /docs/decisions-kpi
- Documents: KPI card (avg resolve, avg open age, color thresholds),
Resolution History chart (cumulative, period→resolution mapping, filter
interaction, timestamp logic), Filter & List (type/status/search, card
age badge, escalation)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 11:48:47 +01:00
.dec-due { color: steelblue; }
Dashboard decisions: list view with MultiSelect filters
- Replace Pending/Made tab + Inputs.table with MultiSelect filters and
a proper list view
- Filters: Type (pending/made), Status (open/escalated/resolved/superseded),
title text search — all stable across polls (no data dependency)
- Each decision renders as a compact card: left border coloured by status
(blue=open, amber=escalated, green=resolved, gray=superseded), type and
status badges, domain context, deadline (red+overdue warning if past),
full title, description/rationale snippet, resolved-by attribution
- Decisions sorted: escalated → open → resolved → superseded, then by
deadline ascending within each group
- Fetch now includes topics alongside decisions for domain name join
- Escalation warning box and velocity chart retained
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 00:39:44 +01:00
.dec-overdue { color: #dc2626 ; font-weight: 600; }
.dec-date { color: var(--theme-foreground-faint); margin-left: auto; }
2026-02-26 07:50:54 +01:00
.dec-age { font-size: 0.72rem; color: var(--theme-foreground-faint, #aaa ); font-variant-numeric: tabular-nums; }
.dec-age-warn { color: #d97706 ; font-weight: 600; }
Dashboard decisions: list view with MultiSelect filters
- Replace Pending/Made tab + Inputs.table with MultiSelect filters and
a proper list view
- Filters: Type (pending/made), Status (open/escalated/resolved/superseded),
title text search — all stable across polls (no data dependency)
- Each decision renders as a compact card: left border coloured by status
(blue=open, amber=escalated, green=resolved, gray=superseded), type and
status badges, domain context, deadline (red+overdue warning if past),
full title, description/rationale snippet, resolved-by attribution
- Decisions sorted: escalated → open → resolved → superseded, then by
deadline ascending within each group
- Fetch now includes topics alongside decisions for domain name join
- Escalation warning box and velocity chart retained
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 00:39:44 +01:00
.dec-title { font-weight: 600; font-size: 0.95rem; margin-bottom: 0.2rem; }
.dec-snippet { font-size: 0.82rem; color: var(--theme-foreground-muted); line-height: 1.45; white-space: pre-wrap; }
.dec-resolved-by { font-size: 0.78rem; color: #22c55e ; margin-top: 0.3rem; }
.dec-escalation-note { font-size: 0.78rem; color: #b45309 ; margin-top: 0.3rem; background: #fef3c7 ; border-radius: 4px; padding: 0.25rem 0.5rem; }
dashboard: prominent KPI infobox, doc-overlay component, decisions reference page
KPI infobox
- Replace slim kpi-bar with a boxed card (border, shadow, 195–240px) floating
right in a flex header alongside the live indicator
- Rows: avg resolve time (last ≤5 resolved) + avg open age (all open)
- avg open age colored via CSS var --oc: red/orange/black per threshold
- "no open decisions" shown as muted italic when queue is empty
doc-overlay component (src/components/doc-overlay.js)
- withDocHelp(element, docPath) — adds absolute-positioned ? button
that is invisible until the parent is hovered; click opens overlay
- Overlay: fixed backdrop + animated box with iframe; closes on Esc,
backdrop click, or the close button
- CSS injected once via style tag (STYLE_ID guard, same pattern as MultiSelect)
? buttons wired up in decisions.md
- KPI infobox → /docs/decisions-kpi
- Cumulative chart (wrapped in position:relative div) → /docs/decisions-kpi
- Filter & List section header → /docs/decisions-kpi
Reference page (src/docs/decisions-kpi.md)
- Standalone Observable Framework page at /docs/decisions-kpi
- Documents: KPI card (avg resolve, avg open age, color thresholds),
Resolution History chart (cumulative, period→resolution mapping, filter
interaction, timestamp logic), Filter & List (type/status/search, card
age badge, escalation)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-26 11:48:47 +01:00
/* ── Escalation warning ───────────────────────────────────────────────────── */
Add state-hub v0.1 — local-first state service for the Custodian
Implements the first live layer of the Custodian cognitive infrastructure:
PostgreSQL schema, FastAPI REST API, FastMCP stdio server, and Observable
Framework telemetry dashboard.
- state-hub/: full stack (docker-compose, FastAPI, Alembic, MCP server, dashboard)
- 5 DB tables: topics, workstreams, tasks, decisions, progress_events
- 11 MCP tools + 5 resources registered in .mcp.json
- Observable dashboard: Overview, Workstreams, Decisions, Progress pages
- CLAUDE.md: session protocol (get_state_summary / add_progress_event ritual)
- ~/.claude/CLAUDE.md: global cross-project reference to the hub
- scripts/pull_image.py: WSL2 TLS-resilient Docker image downloader
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-24 17:47:49 +01:00
.escalation-box { background: #fff3cd ; border: 2px solid orange; border-radius: 8px; padding: 1rem; margin-top: 1rem; }
< / style >