Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions apps/web/src/bench/insights-tab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { useAPIQuery } from "../api";
import { insightsTopLevelRunsPath, TopLevelRunsSchema } from "../insights-api";
import { computeBenchInsights, durationLabel, formatCount } from "../insights-stats";
import { Link } from "../navigation";
import { INSIGHTS_PATH_PREFIX } from "../path-ids";
import { workbenchInsightsPath } from "../insights-deeplinks";
import { benchLink } from "../shell/page-crumbs";
import { OutcomeChart } from "../pages/bench-insights";

/** The bench Insights page in miniature: the same runs query and
Expand All @@ -30,7 +31,6 @@ export function InsightsTab({ workbenchTenantId }: { readonly workbenchTenantId:
["Succeeded", finished === 0 ? "—" : `${Math.round((stats.ok / finished) * 100)}%`],
["Median run", stats.medianMs === null ? "—" : durationLabel(stats.medianMs)],
];
const id = encodeURIComponent(workbenchTenantId);

return (
<div className="drawer-stack">
Expand All @@ -49,7 +49,10 @@ export function InsightsTab({ workbenchTenantId }: { readonly workbenchTenantId:
<h3>Runs per day</h3>
<OutcomeChart days={stats.days} />
</section>
<Link className="drawer-link" to={`${INSIGHTS_PATH_PREFIX}/workbench/${id}?from=${id}`}>
<Link
className="drawer-link"
to={benchLink(workbenchInsightsPath(workbenchTenantId), workbenchTenantId)}
>
Open Insights
</Link>
</div>
Expand Down
106 changes: 1 addition & 105 deletions apps/web/src/insights-stats.test.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,7 @@
import { describe, expect, test } from "bun:test";

import { FIRE_RUNNING_WINDOW_MS } from "@corbits/workflows/client";

import {
computeInsightsStats,
groupRunsByDefinition,
INSIGHTS_RECENT_LIMIT,
purposeRunsForInsights,
runDisplayName,
} from "./insights-stats";
import { groupRunsByDefinition, purposeRunsForInsights, runDisplayName } from "./insights-stats";
import type { InsightsRun } from "./insights-api";
import type { ScheduledWorkflowDefinition } from "./routines-api";

function run(partial: Partial<InsightsRun> & Pick<InsightsRun, "id" | "status">): InsightsRun {
return {
Expand All @@ -26,101 +17,6 @@ function run(partial: Partial<InsightsRun> & Pick<InsightsRun, "id" | "status">)
};
}

function scheduled(
partial: Partial<ScheduledWorkflowDefinition> &
Pick<ScheduledWorkflowDefinition, "definitionId" | "status">,
): ScheduledWorkflowDefinition {
return {
assetId: "ast_def",
name: "Daily dig",
tenantId: "t1",
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
schedule: "0 9 * * *",
...partial,
};
}

describe("computeInsightsStats", () => {
test("counts purposeful runs by status", () => {
const stats = computeInsightsStats(
[
run({
id: "1",
status: "running",
createdAt: "2026-01-03T00:00:00.000Z",
}),
run({
id: "2",
status: "error",
createdAt: "2026-01-02T00:00:00.000Z",
}),
run({
id: "3",
status: "stopped",
createdAt: "2026-01-01T00:00:00.000Z",
}),
],
[
scheduled({ definitionId: "r1", status: "deployed" }),
scheduled({ definitionId: "r2", status: "stopped" }),
],
INSIGHTS_RECENT_LIMIT,
Date.parse("2026-01-03T00:01:00.000Z"),
);

expect(stats.totalRuns).toBe(3);
expect(stats.running).toBe(1);
expect(stats.errored).toBe(1);
expect(stats.stopped).toBe(1);
expect(stats.routineCount).toBe(2);
expect(stats.enabledRoutines).toBe(1);
expect(stats.recentRuns.map((r) => r.id)).toEqual(["1", "2", "3"]);
});

test("a live running run past the fire window is still counted as running", () => {
const stats = computeInsightsStats(
[
run({
id: "stale",
status: "running",
createdAt: new Date(Date.now() - FIRE_RUNNING_WINDOW_MS - 1).toISOString(),
}),
],
[],
);
expect(stats.running).toBe(1);
});

test("endedAt drops a just-finished running run from the running count immediately", () => {
const stats = computeInsightsStats(
[
run({
id: "just-finished",
status: "running",
createdAt: new Date().toISOString(),
endedAt: new Date().toISOString(),
}),
],
[],
);
expect(stats.running).toBe(0);
});

test("limits recent runs", () => {
const runs = Array.from({ length: 5 }, (_, i) =>
run({
id: String(i),
status: "deployed",
createdAt: `2026-01-0${i + 1}T00:00:00.000Z`,
}),
);
const stats = computeInsightsStats(runs, [], 2);
expect(stats.recentRuns).toHaveLength(2);
expect(stats.deployed).toBe(5);
});
});

describe("purposeRunsForInsights", () => {
const deployment = run({ id: "ins_deployed", status: "running" });

Expand Down
74 changes: 8 additions & 66 deletions apps/web/src/insights-stats.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import { runOutcomeStatus, withListingAbandoned } from "@corbits/workflows/client";

import type { InsightsRun } from "./insights-api";
import type { ScheduledWorkflowDefinition } from "./routines-api";

/** Compact integer; null/undefined → em-dash. Lifted out of the deleted
* `@corbits/insights/client` — this app's own copy since it has
Expand All @@ -22,20 +21,6 @@ export function durationLabel(ms: number): string {
return `${(ms / 60_000).toFixed(1)}m`;
}

export type InsightsStats = {
readonly totalRuns: number;
readonly running: number;
readonly errored: number;
readonly stopped: number;
readonly deployed: number;
readonly routineCount: number;
readonly enabledRoutines: number;
readonly recentRuns: readonly InsightsRun[];
};

/** Cap recent-run table rows so the page stays scannable. */
export const INSIGHTS_RECENT_LIMIT = 12;

// Identity pass: the native `GET /workflows/runs` feed already excludes
// non-top-level runs, kept only for callers that still name it explicitly.
export function purposeRunsForInsights(runs: readonly InsightsRun[]): readonly InsightsRun[] {
Expand Down Expand Up @@ -89,53 +74,6 @@ export function groupRunsByDefinition(runs: readonly InsightsRun[]): readonly De
);
}

export function computeInsightsStats(
runs: readonly InsightsRun[],
routines: readonly ScheduledWorkflowDefinition[],
recentLimit: number = INSIGHTS_RECENT_LIMIT,
now: number = Date.now(),
): InsightsStats {
const purposeful = purposeRunsForInsights(runs);
let running = 0;
let errored = 0;
let stopped = 0;
let deployed = 0;
for (const run of purposeful) {
const outcome = runOutcomeStatus(withListingAbandoned(run, now), now) ?? run.status;
switch (outcome) {
case "running":
case "updating":
running += 1;
break;
case "error":
errored += 1;
break;
case "stopped":
case "completed":
stopped += 1;
break;
case "deployed":
deployed += 1;
break;
}
}

const recentRuns = [...purposeful]
.sort((a, b) => b.createdAt.localeCompare(a.createdAt))
.slice(0, recentLimit);

return {
totalRuns: purposeful.length,
running,
errored,
stopped,
deployed,
routineCount: routines.length,
enabledRoutines: routines.filter((r) => r.status === "deployed").length,
recentRuns,
};
}

export const BENCH_RANGES = [7, 30, 90] as const;
export type BenchRange = (typeof BENCH_RANGES)[number];

Expand All @@ -145,6 +83,8 @@ export type BenchDay = {
readonly date: Date;
readonly ok: number;
readonly fail: number;
/** Runs still going, stopped, or otherwise neither succeeded nor failed. */
readonly other: number;
};
export type BenchWorkflowRow = {
readonly key: string;
Expand Down Expand Up @@ -188,20 +128,20 @@ function bucketOf(run: InsightsRun, now: number): Bucket {
}

/** Rollups over the runs created in the last `range` local days (today
* included). "Other" outcomes (running, stopped) count as runs but are
* neither succeeded nor failed. */
* included). "Other" outcomes (running, stopped) count as runs and get their
* own chart segment, but are neither succeeded nor failed. */
export function computeBenchInsights(
runs: readonly InsightsRun[],
range: BenchRange,
now: number = Date.now(),
): BenchInsights {
const today = new Date(now);
today.setHours(0, 0, 0, 0);
const days: { date: Date; ok: number; fail: number }[] = [];
const days: { date: Date; ok: number; fail: number; other: number }[] = [];
for (let i = range - 1; i >= 0; i--) {
const date = new Date(today);
date.setDate(today.getDate() - i);
days.push({ date, ok: 0, fail: 0 });
days.push({ date, ok: 0, fail: 0, other: 0 });
}
const start = days[0]?.date.getTime() ?? 0;

Expand All @@ -224,6 +164,8 @@ export function computeBenchInsights(
} else if (bucket === "fail") {
fail += 1;
if (day !== undefined) day.fail += 1;
} else if (day !== undefined) {
day.other += 1;
}
const key = run.routineId ?? run.definitionId;
groups.set(key, [...(groups.get(key) ?? []), run]);
Expand Down
20 changes: 20 additions & 0 deletions apps/web/src/pages/bench-insights.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
.bi-head {
margin-bottom: 1rem;
}
.bi-head h1 {
margin: 0;
font-size: 1.5rem;
font-weight: 700;
}
.bi-head p {
margin: 0.25rem 0 0;
color: var(--muted-foreground);
}
.bi-other {
background: var(--muted-foreground);
opacity: 0.55;
}
.bi-legend .bi-other {
width: 10px;
height: 10px;
}
Loading
Loading