state-hub/dashboard/src/workstreams.md

102 lines
2.7 KiB
Markdown
Raw Normal View History

---
title: Workstreams
---
```js
const API = "http://127.0.0.1:8000";
const POLL = 15_000;
```
```js
// Fetch workstreams + topics in parallel, join on topic_id → domain/title
const wsState = (async function*() {
while (true) {
let data = [], ok = false;
try {
const [rw, rt] = await Promise.all([
fetch(`${API}/workstreams/`),
fetch(`${API}/topics/`),
]);
ok = rw.ok && rt.ok;
if (ok) {
const [wsList, topicList] = await Promise.all([rw.json(), rt.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 ?? "—",
}));
}
} catch {}
yield {data, ok, ts: new Date()};
await new Promise(res => setTimeout(res, POLL));
}
})();
```
```js
const data = wsState.data ?? [];
const _ok = wsState.ok ?? false;
const _ts = wsState.ts;
```
# Workstreams
```js
display(html`<div class="live-bar">
<span style="color:${_ok ? 'var(--theme-foreground-focus)' : 'red'}"></span>
${_ok
? `Live · updated ${_ts?.toLocaleTimeString()}`
: `<span style="color:red">Offline — run: <code>make api</code></span>`}
</div>`);
```
```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,
}));
```
<style>
.live-bar { font-size: 0.8rem; color: gray; text-align: right; margin-bottom: 0.5rem; }
</style>