--- title: Decisions --- ```js const API = "http://127.0.0.1:8000"; const POLL = 15_000; ``` ```js // Fetch decisions + topics (for domain context) in parallel const decState = (async function*() { while (true) { let data = [], ok = false; try { 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) => { // escalated first, then open, then resolved/superseded; within group by deadline asc 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; }); } } catch {} yield {data, ok, ts: new Date()}; await new Promise(res => setTimeout(res, POLL)); } })(); ``` ```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 — displayed 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}
`, } ); ``` ```js // Reactive value from the form without displaying it 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); 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(); } ``` # Decisions ```js display(html`
${_ok ? `Live · updated ${_ts?.toLocaleTimeString()}` : `Offline — run: make api`}
`); ``` ## 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"; // Returns the most meaningful timestamp for a decision function _getTs(d) { return new Date(d.decided_at ?? d.created_at); } // Map a timestamp to the start-of-bucket timestamp (as ms) 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(); } } // Generate all bucket start timestamps from start to end (inclusive) 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; } // Derive window + bucket config from the selected period const _now = new Date(); const _y = _now.getFullYear(); const _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 _52ago = new Date(_now - 365 * 864e5); _start = new Date(_52ago.getFullYear(), _52ago.getMonth(), 1); _unit = "month"; _tickFmt = d => d.toLocaleDateString(undefined, {month: "short", year: "2-digit"}); break; } default: { // "all" — start from earliest decision in filtered set 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; } } // Restrict to window (all period uses full filtered set) const _inWindow = period === "all" ? [...filtered] : filtered.filter(d => { const t = _getTs(d); return t >= _start && t <= _now; }); // Count per bucket 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); } // Build cumulative series let _cum = 0; const _chartData = _bktKeys.map(k => { const delta = _cntMap.get(k) || 0; _cum += delta; return {date: new Date(k), count: _cum, delta}; }); display(_inWindow.length === 0 ? html`

No decisions in this period.

` : 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, }) ); ``` ## Filter & List ```js display(_filtersForm); ``` ```js 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); return html`
${d.decision_type} ${d.status === "escalated" ? "⚠ " : ""}${d.status} ${d.domain ? html`${d.domain}` : ""} ${due ? html` ${overdue ? "⚠ overdue" : "due"} ${due} ` : ""} ${fmtDate(d.created_at)}
${d.title}
${snippet ? html`
${snippet}${snippet.length < (d.description || d.rationale || "").length ? "…" : ""}
` : ""} ${d.decided_by ? html`
✓ ${d.decided_by}${decided ? " · " + decided : ""}
` : ""} ${d.escalation_note ? 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).
`); } ```