From c5763c3ed8b7a3d4dfb7d3b23ff6f6cdc8457ded Mon Sep 17 00:00:00 2001 From: Markus Hartung Date: Sun, 30 Aug 2026 18:34:51 -0300 Subject: [PATCH] =?UTF-8?q?feat(dashboard):=20overviewProjection=20?= =?UTF-8?q?=E2=80=94=20counters=20+=20kanban=20columns?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../orchestration/model/overviewProjection.ts | 40 ++++++++++++++++ tests/unit/ui/overviewProjection.test.ts | 47 +++++++++++++++++++ 2 files changed, 87 insertions(+) create mode 100644 src/app/(dashboard)/dashboard/orchestration/model/overviewProjection.ts create mode 100644 tests/unit/ui/overviewProjection.test.ts diff --git a/src/app/(dashboard)/dashboard/orchestration/model/overviewProjection.ts b/src/app/(dashboard)/dashboard/orchestration/model/overviewProjection.ts new file mode 100644 index 0000000000..abfb606b15 --- /dev/null +++ b/src/app/(dashboard)/dashboard/orchestration/model/overviewProjection.ts @@ -0,0 +1,40 @@ +/** OrchSnapshot → overview counters + kanban columns. Pure. */ +import { + ORCH_STATES, + type OrchNode, + type OrchSnapshot, + type OrchState, +} from "./orchestrationTypes"; + +export interface OverviewData { + counts: Record; + columns: { + queued: OrchNode[]; + running: OrchNode[]; + waiting_approval: OrchNode[]; + done: OrchNode[]; + }; +} + +export function overviewProjection(snap: OrchSnapshot, comboActive: number): OverviewData { + const counts = Object.fromEntries(ORCH_STATES.map((s) => [s, 0])) as Record; + const columns: OverviewData["columns"] = { + queued: [], + running: [], + waiting_approval: [], + done: [], + }; + + for (const n of snap.nodes) { + if (n.kind !== "work" || !n.state) continue; + counts[n.state] += 1; + if (n.state === "queued" || n.state === "running" || n.state === "waiting_approval") { + columns[n.state].push(n); + } else { + columns.done.push(n); + } + } + columns.done.sort((a, b) => Date.parse(b.updatedAt ?? "0") - Date.parse(a.updatedAt ?? "0")); + counts.running += comboActive; + return { counts, columns }; +} diff --git a/tests/unit/ui/overviewProjection.test.ts b/tests/unit/ui/overviewProjection.test.ts new file mode 100644 index 0000000000..bfbb3e5e57 --- /dev/null +++ b/tests/unit/ui/overviewProjection.test.ts @@ -0,0 +1,47 @@ +/** Run: node --import tsx/esm --test tests/unit/ui/overviewProjection.test.ts */ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import { overviewProjection } from "../../../src/app/(dashboard)/dashboard/orchestration/model/overviewProjection.ts"; +import type { OrchSnapshot } from "../../../src/app/(dashboard)/dashboard/orchestration/model/orchestrationTypes.ts"; + +const snap: OrchSnapshot = { + nodes: [ + { id: "orchestrator", kind: "orchestrator", label: "OmniRoute" }, + { id: "cloud-agent:1", kind: "work", source: "cloud-agent", state: "running", label: "a" }, + { + id: "cloud-agent:2", + kind: "work", + source: "cloud-agent", + state: "waiting_approval", + label: "b", + }, + { + id: "a2a:3", + kind: "work", + source: "a2a", + state: "failed", + label: "c", + updatedAt: "2026-08-30T11:00:00Z", + }, + { id: "a2a:3:activity", kind: "activity", source: "a2a", state: "running", label: "noise" }, + ], + edges: [], + sources: [], + generatedAt: "2026-08-30T12:00:00Z", +}; + +describe("overviewProjection", () => { + it("counts only work nodes and adds comboActive to running", () => { + const { counts } = overviewProjection(snap, 4); + assert.equal(counts.running, 1 + 4); + assert.equal(counts.waiting_approval, 1); + assert.equal(counts.failed, 1); + assert.equal(counts.queued, 0); + }); + it("folds terminals into the done column", () => { + const { columns } = overviewProjection(snap, 0); + assert.equal(columns.done.length, 1); + assert.equal(columns.done[0].id, "a2a:3"); + assert.equal(columns.running.length, 1); + }); +});