--- title: Overview --- ```js const API = "http://127.0.0.1:8000"; const POLL = 15_000; ``` ```js // Live polling — yields {data, ok, ts} every POLL ms const summaryState = (async function*() { while (true) { let data, ok = false; try { const r = await fetch(`${API}/state/summary`); ok = r.ok; data = ok ? await r.json() : {error: `HTTP ${r.status}`}; } catch (e) { data = {error: "API unreachable"}; } yield {data, ok, ts: new Date()}; await new Promise(res => setTimeout(res, POLL)); } })(); ``` ```js const summary = summaryState.data ?? {}; const _ok = summaryState.ok ?? false; const _ts = summaryState.ts; const totals = summary.totals ?? {}; const ws = totals.workstreams ?? {}; const tasks = totals.tasks ?? {}; const decisions = totals.decisions ?? {}; ``` ```js // Registered projects — milestone events tagged with registration const regsState = (async function*() { while (true) { let rows = []; try { const r = await fetch(`${API}/progress/?event_type=milestone&limit=500`); if (r.ok) { const all = await r.json(); rows = all.filter(e => e.summary?.startsWith("Project registered with State Hub:")); } } catch {} yield rows; await new Promise(res => setTimeout(res, POLL)); } })(); ``` # Custodian State Hub ```js display(html`
${_ok ? `Live · updated ${_ts?.toLocaleTimeString()}` : `Offline — run: cd ~/the-custodian/state-hub && make api`}
`); ``` ```js if (summary.error) display(html`
⚠️ ${summary.error}
`); ``` ## Status ```js display(html`

Active Workstreams

${ws.active ?? 0}

${ws.blocked ?? 0} blocked

Blocking Decisions

${(decisions.open ?? 0) + (decisions.escalated ?? 0)}

${decisions.escalated ?? 0} escalated

Blocked Tasks

${tasks.blocked ?? 0}

of ${tasks.total ?? 0} total

Events Today

${(summary.recent_progress ?? []).filter(e => e.created_at?.startsWith(new Date().toISOString().slice(0,10))).length}

last 20 shown below
`); ``` ## Registered Projects ```js const regs = regsState ?? []; if (regs.length === 0) { display(html`

No projects registered yet. Run custodian register-project inside a repo.

`); } else { display(Inputs.table(regs.map(e => ({ Project: e.detail?.project_path?.split("/").at(-1) ?? "—", Domain: e.detail?.domain ?? "—", Path: e.detail?.project_path ?? "—", Registered: new Date(e.created_at).toLocaleString(), })), {maxWidth: 900})); } ``` ## Open Workstreams by Domain ```js import * as Plot from "npm:@observablehq/plot"; const wsData = (summary.topics ?? []).map(t => ({ domain: t.domain, count: (t.workstreams ?? []).length, })); display(Plot.plot({ x: {label: "Domain"}, y: {label: "Open workstreams", grid: true}, marks: [ Plot.barY(wsData, {x: "domain", y: "count", fill: "domain", tip: true}), Plot.ruleY([0]), ], marginBottom: 80, width: 700, })); ``` ```js // Registered domains with no workstreams yet — show a getting-started hint const regs = regsState ?? []; const registeredDomains = new Set(regs.map(e => e.detail?.domain).filter(Boolean)); const emptyRegistered = (summary.topics ?? []).filter(t => registeredDomains.has(t.domain) && (t.workstreams ?? []).length === 0 ); if (emptyRegistered.length > 0) { display(html`
💡 Getting started

These registered projects have no workstreams yet:

`); } ``` ## Blocking Decisions ```js const blocking = summary.blocking_decisions ?? []; if (blocking.length === 0) { display(html`

✓ No blocking decisions.

`); } else { display(Inputs.table(blocking.map(d => ({ Title: d.title, Status: d.status, Deadline: d.deadline ? new Date(d.deadline).toLocaleDateString() : "—", Escalated: d.escalation_note ? "⚠️" : "", })))); } ``` ## Decisions Due Within 7 Days ```js const in7 = new Date(Date.now() + 7*24*60*60*1000); const due = (summary.blocking_decisions ?? []).filter(d => d.deadline && new Date(d.deadline) <= in7); if (due.length === 0) { display(html`

No decisions due in next 7 days.

`); } else { display(Inputs.table(due.map(d => ({ Title: d.title, Deadline: new Date(d.deadline).toLocaleString(), Status: d.status, })))); } ``` ## Recent Activity ```js display(Inputs.table((summary.recent_progress ?? []).map(e => ({ Time: new Date(e.created_at).toLocaleString(), Type: e.event_type, Author: e.author ?? "—", Summary: e.summary, })), {maxWidth: 900})); ```