Add per-workstream task counts to state summary and dashboard

API:
- WorkstreamWithTaskCounts schema extends WorkstreamRead with
  tasks_total/todo/in_progress/blocked/done fields
- /state/summary now includes these counts in open_workstreams via
  a single extra GROUP BY query (workstream_id, status)

Dashboard:
- Replace domain workstream-count bar with a horizontal stacked
  progress bar per workstream (done/in-progress/blocked/todo)
- Workstreams with no tasks show "no tasks yet" annotation
- Workstreams with tasks show "X/N done" label after the bar
- Sorted by domain then title so domains group naturally

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-25 00:50:43 +01:00
parent 379a3b1a01
commit cabeefe070
4 changed files with 77 additions and 18 deletions

View File

@@ -117,21 +117,55 @@ if (regs.length === 0) {
```js
import * as Plot from "npm:@observablehq/plot";
const wsData = (summary.topics ?? []).map(t => ({
domain: t.domain,
count: (t.workstreams ?? []).length,
}));
const topicById = Object.fromEntries((summary.topics ?? []).map(t => [t.id, t.domain]));
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,
}));
const openWs = (summary.open_workstreams ?? []).map(w => ({
title: w.title,
domain: topicById[w.topic_id] ?? "unknown",
done: w.tasks_done ?? 0,
in_progress: w.tasks_in_progress ?? 0,
blocked: w.tasks_blocked ?? 0,
todo: w.tasks_todo ?? 0,
total: w.tasks_total ?? 0,
})).sort((a, b) => a.domain.localeCompare(b.domain) || a.title.localeCompare(b.title));
const statusOrder = ["done", "in progress", "blocked", "todo"];
const statusColors = ["#4caf50", "steelblue", "#ff7043", "#e0e0e0"];
const taskRows = openWs.flatMap(w => [
{label: w.title, domain: w.domain, status: "done", count: w.done},
{label: w.title, domain: w.domain, status: "in progress", count: w.in_progress},
{label: w.title, domain: w.domain, status: "blocked", count: w.blocked},
{label: w.title, domain: w.domain, status: "todo", count: w.todo},
]).filter(d => d.count > 0);
if (openWs.length === 0) {
display(html`<p style="color:gray">No open workstreams.</p>`);
} else {
display(Plot.plot({
y: {label: null, tickSize: 0, domain: openWs.map(w => w.title)},
x: {label: "Tasks", grid: true},
color: {domain: statusOrder, range: statusColors, legend: true},
marks: [
Plot.barX(taskRows, {y: "label", x: "count", fill: "status", tip: true}),
Plot.text(openWs.filter(w => w.total > 0), {
y: "title", x: "total",
text: d => ` ${d.done}/${d.total}`,
dx: 4, textAnchor: "start", fontSize: 11, fill: "gray",
}),
Plot.text(openWs.filter(w => w.total === 0), {
y: "title", x: 0,
text: () => " no tasks yet",
textAnchor: "start", fontSize: 11, fill: "#aaa",
}),
Plot.ruleX([0]),
],
marginLeft: 200,
marginRight: 70,
height: Math.max(80, openWs.length * 44 + 50),
width: 700,
}));
}
```
```js