--- title: Decisions --- ```js import {API, POLL_HEAVY, apiFetch, pollDelay, waitForVisible} from "./components/config.js"; ``` ```js // Fetch decisions + topics (for domain context) in parallel const decState = (async function*() { let failures = 0; while (true) { let data = [], ok = false; try { const [rd, rt] = await Promise.all([ apiFetch("/decisions/?limit=500"), apiFetch("/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_slug ?? 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; // Resolved / superseded: most recently decided first if (a.status === "resolved" || a.status === "superseded") { if (a.decided_at && b.decided_at) return b.decided_at.localeCompare(a.decided_at); return a.decided_at ? -1 : 1; } // Open / escalated: soonest deadline first, then most recently created if (a.deadline && b.deadline) return a.deadline.localeCompare(b.deadline); if (a.deadline) return -1; if (b.deadline) return 1; return b.created_at.localeCompare(a.created_at); }); } } catch {} failures = ok ? 0 : failures + 1; yield {data, ok, ts: new Date()}; await waitForVisible(pollDelay({ok, base: POLL_HEAVY, failures})); } })(); ``` ```js const data = decState.data ?? []; const _ok = decState.ok ?? false; const _ts = decState.ts; ``` ```js import {MultiSelect} from "./components/multiselect.js"; // Create filter form without displaying — shown below the chart via display(_filtersForm) const _filtersForm = Inputs.form( { type: MultiSelect(["pending", "made"], {label: "Type", placeholder: "All types"}), 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`
${type}${status}
${search}
`, } ); ``` ```js const filters = Generators.input(_filtersForm); ``` ```js 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 && !["resolved", "superseded"].includes(d.status)); 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(); } 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`; // 30.5 = avg days per month (365/12) } ``` # Decisions ```js import {withDocHelp} from "./components/doc-overlay.js"; import {injectTocTop} from "./components/toc-sidebar.js"; import {openActionConfirm} from "./components/action-confirm.js"; // ── KPI computation (uses full data, not filtered) ────────────────────────── 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; const _nowKpi = new Date(); 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; // Color: red = mean open > baseline; orange = any individual > baseline; black = all fine let _openAgeColor = "inherit"; if (_meanOpenMs !== null && _meanResolveMs !== null) { if (_meanOpenMs > _meanResolveMs) _openAgeColor = "#dc2626"; else if (_openAges.some(a => a > _meanResolveMs)) _openAgeColor = "#d97706"; } // ── Build the KPI infobox ─────────────────────────────────────────────────── const _kpiBox = html`
Decision Health
${_meanResolveMs !== null ? html`
avg resolve
${fmtDuration(_meanResolveMs)}
last ${_resolved5.length}
` : ""} ${_meanOpenMs !== null ? html`
avg open age
${fmtDuration(_meanOpenMs)}
${_openDecs.length} open
` : html`
no open decisions
`}
`; withDocHelp(_kpiBox, "/docs/decisions-kpi"); // ── Build live indicator ──────────────────────────────────────────────────── const _liveEl = html`
${_ok ? `Live · updated ${_ts?.toLocaleTimeString()}` : html`Offline — run: make api`}
`; withDocHelp(_liveEl, "/docs/live-data"); const _h1 = document.querySelector("#observablehq-main h1"); if (_h1) { _h1.style.position = "relative"; withDocHelp(_h1, "/docs/decisions"); } display(html`

Daily WSJF triage surfaces recurring decision and human-approval recommendations.

`); // ── Inject into TOC sidebar: KPI first (prepend → bottom), live last (→ top) ─ const _toc = document.querySelector("#observablehq-toc"); if (_toc) { injectTocTop("decisions-kpi-box", _kpiBox); injectTocTop("live-indicator", _liveEl); } else { display(html`
${_liveEl}${_kpiBox}
`); } ``` ## 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"; function _getTs(d) { return new Date(d.decided_at ?? d.created_at); } 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(); } } 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(); const _y = _now.getFullYear(), _mo = _now.getMonth(); let _start, _unit, _tickFmt; switch (period) { case "day": _start = new Date(_y, _mo, _now.getDate()); _unit = "hour"; _tickFmt = d => `${String(d.getHours()).padStart(2, "0")}:00`; break; case "week": { const _7ago = new Date(_now - 7 * 864e5); _start = new Date(_7ago.getFullYear(), _7ago.getMonth(), _7ago.getDate()); _unit = "day"; _tickFmt = d => d.toLocaleDateString(undefined, {weekday: "short", month: "short", day: "numeric"}); break; } case "month": _start = new Date(_y, _mo, 1); _unit = "week"; _tickFmt = d => `W/${d.toLocaleDateString(undefined, {month: "short", day: "numeric"})}`; break; case "quarter": _start = new Date(_y, Math.floor(_mo / 3) * 3, 1); _unit = "month"; _tickFmt = d => d.toLocaleDateString(undefined, {month: "short"}); break; case "YTD": _start = new Date(_y, 0, 1); _unit = "month"; _tickFmt = d => d.toLocaleDateString(undefined, {month: "short"}); break; case "year": { const _ago = new Date(_now - 365 * 864e5); _start = new Date(_ago.getFullYear(), _ago.getMonth(), 1); _unit = "month"; _tickFmt = d => d.toLocaleDateString(undefined, {month: "short", year: "2-digit"}); break; } default: { const _minTs = filtered.length ? Math.min(...filtered.map(d => _getTs(d))) : _now.getTime(); const _minD = new Date(_minTs); _start = new Date(_minD.getFullYear(), _minD.getMonth(), 1); _unit = "month"; _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}; }); if (_inWindow.length === 0) { display(html`

No decisions in this period.

`); } 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`
${_plotEl}
`; withDocHelp(_plotWrap, "/docs/decisions-kpi"); display(_plotWrap); } ``` ## Filter & List ```js const _filterWrap = html`
${_filtersForm}
`; withDocHelp(_filterWrap, "/docs/decisions-kpi"); display(_filterWrap); ``` ```js const _nowCards = new Date(); if (filtered.length === 0) { display(html`

No decisions match the current filter.

`); } else { display(html`
${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); 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; function onResolve() { openActionConfirm({ title: "Resolve Decision", entityTitle: d.title, label: "Rationale", placeholder: "Why is this resolved, and what was decided?", confirmLabel: "Resolve", onConfirm: async (rationale) => { const res = await fetch(`${API}/decisions/${d.id}/resolve`, { method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify({rationale, decided_by: "human"}), }); if (!res.ok) throw new Error(`API error ${res.status}`); }, }); } return html`
${d.decision_type} ${d.status === "escalated" ? "⚠ " : ""}${d.status} ${d.domain ? html`${d.domain}` : ""} ${due ? html` ${overdue ? "⚠ overdue" : "due"} ${due} ` : ""} ${_ageText} ${fmtDate(d.created_at)} ${_isOpen ? html`` : ""}
${d.title}
${snippet ? html`
${snippet}${snippet.length < (d.description || d.rationale || "").length ? "…" : ""}
` : ""} ${d.decided_by ? html`
✓ ${d.decided_by}${decided ? " · " + decided : ""}
` : ""} ${d.escalation_note && !["resolved", "superseded"].includes(d.status) ? html`
${d.escalation_note}
` : ""}
`; })}
`); } ``` ```js if (escalated.length > 0) { display(html`
⚠ ${escalated.length} escalated decision${escalated.length > 1 ? "s" : ""} require human approval before any action is taken (constitution §4).
`); } ```