From 24e41d9f47d27f80d2fb7b59d38d9352731f1414 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Fri, 21 Aug 2026 09:10:56 -0700 Subject: [PATCH 1/3] Add tests for Insights KPI sparklines and running-now strip Covers the new per-day run/cost bucketing helpers and elapsed-time formatting backing the redesigned Insights KPI row, plus the "Running now" strip's real-data-only behavior (absent when nothing is in flight, present with the actual in-flight run's name). --- .../src/pages/insights-page-render.test.tsx | 47 ++++++++- apps/web/src/pages/insights-page.test.ts | 97 ++++++++++++++++++- 2 files changed, 138 insertions(+), 6 deletions(-) diff --git a/apps/web/src/pages/insights-page-render.test.tsx b/apps/web/src/pages/insights-page-render.test.tsx index 5a2314388..df39031e1 100644 --- a/apps/web/src/pages/insights-page-render.test.tsx +++ b/apps/web/src/pages/insights-page-render.test.tsx @@ -42,7 +42,13 @@ const benchState: BenchState = { onBenchCreated: () => {}, }; -function InsightsPageAtPath({ path }: { readonly path: string }) { +function InsightsPageAtPath({ + path, + runs = { data: [], nextCursor: null }, +}: { + readonly path: string; + readonly runs?: { data: unknown[]; nextCursor: string | null }; +}) { const range = useInsightsWindow(); return ( {}}> @@ -52,7 +58,7 @@ function InsightsPageAtPath({ path }: { readonly path: string }) { summary={readyEmpty(EMPTY_OVERALL_USAGE)} activity={readyEmpty([])} byTool={readyEmpty([])} - runs={readyEmpty({ data: [], nextCursor: null })} + runs={readyEmpty(runs)} routines={readyEmpty([])} workbenches={readyEmpty({ items: [] })} latency={{ kind: "loading" }} @@ -66,12 +72,15 @@ function InsightsPageAtPath({ path }: { readonly path: string }) { ); } -function render(path: string) { +function render( + path: string, + runs?: { data: unknown[]; nextCursor: string | null }, +) { container = document.createElement("div"); document.body.appendChild(container); root = createRoot(container); act(() => { - root?.render(); + root?.render(); }); return container; } @@ -91,3 +100,33 @@ describe("InsightsPage with a malformed URL escape", () => { expect(el.textContent).toContain("All workbenches"); }); }); + +describe("InsightsPage 'Running now' strip", () => { + test("no in-flight runs: the strip renders nothing, not an empty-state fixture", () => { + const el = render("/insights", { data: [], nextCursor: null }); + expect(el.textContent).not.toContain("Running now"); + }); + + test("a genuinely running run surfaces in the strip by name", () => { + const el = render("/insights", { + data: [ + { + id: "run_1", + tenantId: "tnt_bench_a", + definitionId: "wfd_a", + definitionName: "Weekly digest", + address: "addr", + status: "running", + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + routineId: null, + routineName: null, + }, + ], + nextCursor: null, + }); + expect(el.textContent).toContain("Running now"); + expect(el.textContent).toContain("1 in progress"); + expect(el.textContent).toContain("Weekly digest"); + }); +}); diff --git a/apps/web/src/pages/insights-page.test.ts b/apps/web/src/pages/insights-page.test.ts index 23816892c..b0927ea12 100644 --- a/apps/web/src/pages/insights-page.test.ts +++ b/apps/web/src/pages/insights-page.test.ts @@ -8,9 +8,15 @@ // own `/insights/workbench/:workbenchId` route (see `InsightsWorkbenchPage`), // so this landing scope never takes a workbench override anymore. import { describe, expect, test } from "bun:test"; +import type { DayActivity } from "@corbits/insights/client"; -import { resolveInsightsScope } from "./insights-page"; -import type { InsightsScope } from "../insights-api"; +import { + costPerDay, + elapsedLabel, + resolveInsightsScope, + runsPerDay, +} from "./insights-page"; +import type { InsightsRun, InsightsScope } from "../insights-api"; const workspaceMemberScope: InsightsScope = { tenantId: "tnt_bench_a", @@ -85,3 +91,90 @@ describe("resolveInsightsScope", () => { expect(result.effectiveTenantId).toBeNull(); }); }); + +function day( + partial: Partial & Pick, +): DayActivity { + return { turns: 0, tokens: 0, byModel: [], ...partial }; +} + +function run( + partial: Partial & Pick, +): InsightsRun { + return { + tenantId: "t1", + definitionId: "wfd_a", + definitionName: "Research brief", + address: "addr", + status: "running", + updatedAt: partial.createdAt, + routineId: null, + routineName: null, + ...partial, + }; +} + +describe("runsPerDay", () => { + test("buckets each run's date onto the matching day, zero elsewhere", () => { + const days = [ + day({ day: "2026-01-01" }), + day({ day: "2026-01-02" }), + day({ day: "2026-01-03" }), + ]; + const runs = [ + run({ id: "a", createdAt: "2026-01-01T09:00:00.000Z" }), + run({ id: "b", createdAt: "2026-01-01T18:00:00.000Z" }), + run({ id: "c", createdAt: "2026-01-03T00:00:01.000Z" }), + ]; + expect(runsPerDay(runs, days)).toEqual([2, 0, 1]); + }); + + test("a run outside the window contributes to no bucket", () => { + const days = [day({ day: "2026-01-01" })]; + const runs = [run({ id: "a", createdAt: "2025-12-25T00:00:00.000Z" })]; + expect(runsPerDay(runs, days)).toEqual([0]); + }); + + test("no days: returns an empty series, not a fabricated one", () => { + expect( + runsPerDay([run({ id: "a", createdAt: "2026-01-01T00:00:00.000Z" })], []), + ).toEqual([]); + }); +}); + +describe("costPerDay", () => { + test("sums known per-model costs for each day", () => { + const days = [ + day({ + day: "2026-01-01", + byModel: [ + { model: "opus-5", tokens: 100, costUsd: 1.5 }, + { model: "sonnet-5", tokens: 50, costUsd: 0.25 }, + ], + }), + day({ day: "2026-01-02", byModel: [] }), + ]; + expect(costPerDay(days)).toEqual([1.75, 0]); + }); + + test("a null model rate contributes 0, not NaN — caller decides whether to show it", () => { + const days = [ + day({ + day: "2026-01-01", + byModel: [{ model: "new-model", tokens: 10, costUsd: null }], + }), + ]; + expect(costPerDay(days)).toEqual([0]); + }); +}); + +describe("elapsedLabel", () => { + test("formats wall-clock time since createdAt", () => { + const now = Date.parse("2026-01-01T00:02:12.000Z"); + expect(elapsedLabel("2026-01-01T00:00:00.000Z", now)).toBe("2.2m"); + }); + + test("an invalid timestamp reads as unknown, not a fabricated duration", () => { + expect(elapsedLabel("not-a-date", Date.now())).toBe("—"); + }); +}); From 5d97bb98d71a7eecb52826b511e04ca3ef8d1ccf Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Fri, 21 Aug 2026 09:11:10 -0700 Subject: [PATCH 2/3] Insights: real per-stat sparklines and a running-now strip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restyles the KPI row and adds two of the three redesign deltas from the owner's mock, both backed by data the page already queries: - Each KPI tile now carries a real trend line via react-ui's StatGridItem sparkline slot — turns/tokens/runs per day from the activity series already driving the activity chart, and cost per day only when every model's rate is known this window (never a shape that would silently undercount an unpriced model). - A "Running now" strip lists runs genuinely in flight (status running/updating), using react-ui's RUN_STATUS_TONE/LABEL and StatusDot rather than an invented status palette. It renders nothing when nothing is running, same convention as react-ui's WorkflowDock — not a permanent "nothing running" fixture. The mock's third delta, a "Recommendations" rail, is intentionally not built: there is no real heuristic or data model behind it today (no credential-expiry, skill-version-regression, or model-fit signal reaches this page), and fabricating advice strings would violate this page's own no-invented-data convention. Everything else — KPI values, activity/token/cost/tool tables, run history and trace detail — is unchanged. --- apps/web/src/app.css | 67 +++++++++++++++ apps/web/src/pages/insights-page.tsx | 123 +++++++++++++++++++++++++++ 2 files changed, 190 insertions(+) diff --git a/apps/web/src/app.css b/apps/web/src/app.css index 13eea5353..9b6e7b2a0 100644 --- a/apps/web/src/app.css +++ b/apps/web/src/app.css @@ -2557,6 +2557,73 @@ tr.insights-row-clickable:hover { padding: 0.55rem 0.7rem; } +/* "Running now" strip — real in-flight runs only (see RunningNowStrip), + scrollable so a busy tenant doesn't push the layout wide. */ +.insights-running-now { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.insights-running-now-head { + display: flex; + align-items: baseline; + gap: 0.5rem; +} + +.insights-running-now-head h3 { + margin: 0; + font-size: 0.78rem; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--muted-foreground); +} + +.insights-running-now-count { + font-size: 0.75rem; + color: var(--muted-foreground); +} + +.insights-running-now-strip { + display: flex; + gap: 0.5rem; + margin: 0; + padding: 0.1rem 0.05rem 0.35rem; + list-style: none; + overflow-x: auto; +} + +.insights-flight { + display: flex; + flex-shrink: 0; + align-items: center; + gap: 0.5rem; + padding: 0.4rem 0.65rem; + border: 1px solid color-mix(in srgb, var(--foreground) 10%, transparent); + background: var(--card, var(--background)); + font-size: 0.8rem; + cursor: pointer; +} + +.insights-flight:hover { + background-color: color-mix(in srgb, var(--primary) 8%, transparent); +} + +.insights-flight-name { + max-width: 14rem; + overflow: hidden; + font-weight: 600; + text-overflow: ellipsis; + white-space: nowrap; +} + +.insights-flight-elapsed { + font-size: 0.75rem; + font-variant-numeric: tabular-nums; + color: var(--muted-foreground); +} + .insights-section { padding-top: 0.25rem; } diff --git a/apps/web/src/pages/insights-page.tsx b/apps/web/src/pages/insights-page.tsx index 2d8819e63..3309fbf44 100644 --- a/apps/web/src/pages/insights-page.tsx +++ b/apps/web/src/pages/insights-page.tsx @@ -10,9 +10,13 @@ import { BarChart, PageShell, RichEmptyState, + RUN_STATUS_DOT_TONE, + RUN_STATUS_LABEL, + RUN_STATUS_TONE, Skeleton, StatGrid, StatGridItem, + StatusDot, Table, TableBody, TableCell, @@ -249,12 +253,18 @@ function InsightsStat({ detail, onClick, loading, + sparklineValues, + sparklineLabel, }: { readonly label: string; readonly value: string; readonly detail?: string; readonly onClick?: () => void; readonly loading?: boolean; + /** Real per-day series backing this tile's trend line — omitted (not + * padded/estimated) whenever the underlying window lacks one. */ + readonly sparklineValues?: readonly number[]; + readonly sparklineLabel?: string; }) { if (loading === true) { return ( @@ -272,6 +282,8 @@ function InsightsStat({ value={value} {...(detail === undefined ? {} : { sub: detail })} {...(onClick === undefined ? {} : { onClick })} + {...(sparklineValues === undefined ? {} : { sparklineValues })} + {...(sparklineLabel === undefined ? {} : { sparklineLabel })} /> ); } @@ -400,6 +412,99 @@ function tokensOverTimeSeries(days: readonly DayActivity[]) { })); } +/** Real per-day run counts, bucketed onto `activityDays`' own UTC day keys — + * the Runs KPI's sparkline shape, built from the same run records the + * recent-runs/history tables render rather than a synthesized series. */ +export function runsPerDay( + runs: readonly InsightsRun[], + days: readonly DayActivity[], +): number[] { + const counts = new Map(days.map((d) => [d.day, 0] as const)); + for (const run of runs) { + const day = run.createdAt.slice(0, 10); + const current = counts.get(day); + if (current !== undefined) counts.set(day, current + 1); + } + return days.map((d) => counts.get(d.day) ?? 0); +} + +/** Real per-day cost, summed across models — the Cost KPI's sparkline + * shape. Callers only use this when every model's rate is known for the + * window (`modelsWithMissingRates` is empty); otherwise a day with an + * unpriced model would silently read as cheaper than it was. */ +export function costPerDay(days: readonly DayActivity[]): number[] { + return days.map((d) => + d.byModel.reduce((sum, m) => sum + (m.costUsd ?? 0), 0), + ); +} + +/** Wall-clock time since a run started, in the same "2m 12s" form as the + * rest of this page (`durationLabel`) — never a fabricated live counter. */ +export function elapsedLabel(createdAt: string, now: number): string { + const startMs = Date.parse(createdAt); + if (Number.isNaN(startMs)) return "—"; + return durationLabel(Math.max(0, now - startMs)); +} + +/** + * "Running now" — a horizontally scrolling strip of the runs actually in + * flight this instant (`status: running | updating`), not a fabricated + * live-metrics ticker. Renders nothing when nothing is running, same + * convention as react-ui's `WorkflowDock`: an empty "nothing running" strip + * is a permanent fixture reporting the normal case, not an empty state worth + * showing. + */ +function RunningNowStrip({ + runs, + onOpenRun, +}: { + readonly runs: readonly InsightsRun[]; + readonly onOpenRun: (id: string) => void; +}) { + const running = runs.filter( + (r) => r.status === "running" || r.status === "updating", + ); + if (running.length === 0) return null; + const now = Date.now(); + + return ( +
+
+

Running now

+ + {formatCount(running.length)} in progress + +
+
    + {running.map((run) => ( +
  • + +
  • + ))} +
+
+ ); +} + function ModelCostTable({ models, }: { @@ -565,6 +670,14 @@ function InsightsLanding({ const tokensSeries = tokensOverTimeSeries(activityDays); const noUsageInWindow = !loading && usage.turns === 0; + // KPI sparklines: only ever a real per-day series already backing this + // window's other charts, never estimated to fill a gap. + const turnsSparkline = activityDays.map((d) => d.turns); + const tokensSparkline = activityDays.map((d) => d.tokens); + const runsSparkline = runsPerDay(purposeRuns, activityDays); + const costSparkline = + missingRates.length === 0 ? costPerDay(activityDays) : undefined; + return (
@@ -573,12 +686,16 @@ function InsightsLanding({ value={tileValue(formatUsd(usage.costUsd), loading)} detail={`${formatCount(usage.tokens.total)} tokens`} loading={loading} + sparklineValues={costSparkline} + sparklineLabel="Cost per day this week" /> {workbenches !== null ? ( + + {latency !== null && latency.total.samples > 0 ? ( Date: Fri, 21 Aug 2026 09:23:50 -0700 Subject: [PATCH 3/3] Fix typecheck errors from the Insights KPI sparkline change MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - runsPerDay's day-count Map inferred value type 0 (a literal, not number) from the `as const` tuple spread, so incrementing a count failed to typecheck — the counts genuinely needed a number-typed Map, not a readonly-tuple one. - Two exactOptionalPropertyTypes violations: the Cost tile's sparklineValues can be genuinely absent (unpriced model this window) and must be omitted via spread rather than passed as an explicit undefined; same fix for the render test's optional runs stub. - Typed the render test's runs stub to the real InsightsRun shape instead of unknown[], so a real mismatch there stays visible. --- .../web/src/pages/insights-page-render.test.tsx | 17 +++++++++++------ apps/web/src/pages/insights-page.tsx | 6 ++++-- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/apps/web/src/pages/insights-page-render.test.tsx b/apps/web/src/pages/insights-page-render.test.tsx index df39031e1..d7e1c994f 100644 --- a/apps/web/src/pages/insights-page-render.test.tsx +++ b/apps/web/src/pages/insights-page-render.test.tsx @@ -16,8 +16,11 @@ import { EMPTY_OVERALL_USAGE } from "@corbits/insights/client"; import { InsightsPage, useInsightsWindow } from "./insights-page"; import { BenchContext } from "../bench-context"; import type { BenchState } from "../bench-context"; +import type { InsightsRun } from "../insights-api"; import { NavigationProvider } from "../navigation"; +type RunsStub = { data: readonly InsightsRun[]; nextCursor: string | null }; + let container: HTMLDivElement | null = null; let root: Root | null = null; @@ -47,7 +50,7 @@ function InsightsPageAtPath({ runs = { data: [], nextCursor: null }, }: { readonly path: string; - readonly runs?: { data: unknown[]; nextCursor: string | null }; + readonly runs?: RunsStub; }) { const range = useInsightsWindow(); return ( @@ -72,15 +75,17 @@ function InsightsPageAtPath({ ); } -function render( - path: string, - runs?: { data: unknown[]; nextCursor: string | null }, -) { +function render(path: string, runs?: RunsStub) { container = document.createElement("div"); document.body.appendChild(container); root = createRoot(container); act(() => { - root?.render(); + root?.render( + , + ); }); return container; } diff --git a/apps/web/src/pages/insights-page.tsx b/apps/web/src/pages/insights-page.tsx index 3309fbf44..8c1d2652b 100644 --- a/apps/web/src/pages/insights-page.tsx +++ b/apps/web/src/pages/insights-page.tsx @@ -419,7 +419,7 @@ export function runsPerDay( runs: readonly InsightsRun[], days: readonly DayActivity[], ): number[] { - const counts = new Map(days.map((d) => [d.day, 0] as const)); + const counts = new Map(days.map((d) => [d.day, 0])); for (const run of runs) { const day = run.createdAt.slice(0, 10); const current = counts.get(day); @@ -686,8 +686,10 @@ function InsightsLanding({ value={tileValue(formatUsd(usage.costUsd), loading)} detail={`${formatCount(usage.tokens.total)} tokens`} loading={loading} - sparklineValues={costSparkline} sparklineLabel="Cost per day this week" + {...(costSparkline === undefined + ? {} + : { sparklineValues: costSparkline })} />