--- title: Workstreams --- ```js const API = "http://127.0.0.1:8000"; const POLL = 15_000; ``` ```js // Fetch workstreams + topics + summary (for dep graph) in parallel const wsState = (async function*() { while (true) { let data = [], openWs = [], ok = false; try { const [rw, rt, rs] = await Promise.all([ fetch(`${API}/workstreams/`), fetch(`${API}/topics/`), fetch(`${API}/state/summary`), ]); ok = rw.ok && rt.ok && rs.ok; if (ok) { const [wsList, topicList, summary] = await Promise.all([rw.json(), rt.json(), rs.json()]); const topicMap = Object.fromEntries(topicList.map(t => [t.id, t])); data = wsList.map(w => ({ ...w, domain: topicMap[w.topic_id]?.domain ?? "unknown", topic_title: topicMap[w.topic_id]?.title ?? "—", })); // open_workstreams from summary carry depends_on / blocks lists openWs = summary.open_workstreams ?? []; } } catch {} yield {data, openWs, ok, ts: new Date()}; await new Promise(res => setTimeout(res, POLL)); } })(); ``` ```js const data = wsState.data ?? []; const openWs = wsState.openWs ?? []; const _ok = wsState.ok ?? false; const _ts = wsState.ts; ``` # Workstreams ```js display(html`
`); ``` ```js const domainOpts = ["(all)", ...new Set(data.map(w => w.domain))].sort(); const statusOpts = ["(all)", "active", "blocked", "completed", "archived"]; const domainFilter = view(Inputs.select(domainOpts, {label: "Domain"})); const statusFilter = view(Inputs.select(statusOpts, {label: "Status"})); const ownerFilter = view(Inputs.text({label: "Owner contains"})); ``` ```js const filtered = data.filter(w => (domainFilter === "(all)" || w.domain === domainFilter) && (statusFilter === "(all)" || w.status === statusFilter) && (!ownerFilter || (w.owner ?? "").toLowerCase().includes(ownerFilter.toLowerCase())) ); display(Inputs.table(filtered.map(w => ({ Title: w.title, Domain: w.domain, Status: w.status, Owner: w.owner ?? "—", Due: w.due_date ?? "—", Updated: new Date(w.updated_at).toLocaleDateString(), })), {rows: 20})); ``` ## Status Distribution ```js import * as Plot from "npm:@observablehq/plot"; const byStatus = Object.entries( filtered.reduce((acc, w) => { acc[w.status] = (acc[w.status] ?? 0) + 1; return acc; }, {}) ).map(([status, count]) => ({status, count})); display(Plot.plot({ marks: [ Plot.barX(byStatus, {y: "status", x: "count", fill: "status", tip: true}), Plot.ruleX([0]), ], marginLeft: 80, width: 500, })); ``` ## Dependencies ```js // Build dep cards from the enriched open_workstreams in the summary const wsWithDeps = openWs.filter(w => (domainFilter === "(all)" || (data.find(d => d.id === w.id)?.domain ?? "unknown") === domainFilter) && (statusFilter === "(all)" || w.status === statusFilter) && (w.depends_on.length > 0 || w.blocks.length > 0) ); if (wsWithDeps.length === 0) { display(html`No dependency edges recorded for the current filter. Use create_dependency() via the MCP server to link workstreams.