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
57 changes: 57 additions & 0 deletions apps/web/src/pages/insights-page-render.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -134,4 +134,61 @@ describe("InsightsPage 'Running now' strip", () => {
expect(el.textContent).toContain("1 in progress");
expect(el.textContent).toContain("Weekly digest");
});

// Liveness is not a windowed property: a run that started long before the
// 7-day window and is still running must not disappear from the strip or
// read 0 in the "Running now" KPI just because its start time falls
// outside `range`.
test("a run started 8 days ago that is still running stays in the strip and the KPI", () => {
const eightDaysAgo = new Date(
Date.now() - 8 * 24 * 60 * 60 * 1000,
).toISOString();
const el = render("/insights", {
data: [
{
id: "run_long_haul",
tenantId: "tnt_bench_a",
definitionId: "wfd_a",
definitionName: "Long haul",
address: "addr",
status: "running",
createdAt: eightDaysAgo,
updatedAt: eightDaysAgo,
routineId: null,
routineName: null,
},
],
nextCursor: null,
});
expect(el.textContent).toContain("Running now");
expect(el.textContent).toContain("1 in progress");
expect(el.textContent).toContain("Long haul");
expect(el.textContent).toContain("in flight");
});

test("the elapsed label ticks forward while a run is live, not frozen at first render", async () => {
const startedAt = new Date(Date.now() - 2_000).toISOString();
const el = render("/insights", {
data: [
{
id: "run_ticking",
tenantId: "tnt_bench_a",
definitionId: "wfd_a",
definitionName: "Weekly digest",
address: "addr",
status: "running",
createdAt: startedAt,
updatedAt: startedAt,
routineId: null,
routineName: null,
},
],
nextCursor: null,
});
const before = el.textContent;
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 1_200));
});
expect(el.textContent).not.toBe(before);
});
});
64 changes: 57 additions & 7 deletions apps/web/src/pages/insights-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,29 @@ export function elapsedLabel(createdAt: string, now: number): string {
return durationLabel(Math.max(0, now - startMs));
}

const ELAPSED_TICK_MS = 1_000;

/** Ticks once a second while `enabled` — the clock the elapsed label next to
* the pulsing `StatusDot` reads from, so it counts up like the live indicator
* beside it instead of freezing at whatever instant this component mounted
* or last re-rendered for an unrelated reason. */
function useTickingNow(enabled: boolean): number {
const [now, setNow] = useState(() => Date.now());
useEffect(() => {
if (!enabled) return undefined;
const timer = setInterval(() => setNow(Date.now()), ELAPSED_TICK_MS);
return () => clearInterval(timer);
}, [enabled]);
return now;
}

/** A run actually in flight right now (`status: running | updating`) —
* liveness is not a windowed property, so this filters the full run set,
* never the range-filtered one. */
function isRunningNow(run: InsightsRun): boolean {
return run.status === "running" || run.status === "updating";
}

/**
* "Running now" — a horizontally scrolling strip of the runs actually in
* flight this instant (`status: running | updating`), not a fabricated
Expand All @@ -457,11 +480,9 @@ function RunningNowStrip({
readonly runs: readonly InsightsRun[];
readonly onOpenRun: (id: string) => void;
}) {
const running = runs.filter(
(r) => r.status === "running" || r.status === "updating",
);
const running = runs.filter(isRunningNow);
const now = useTickingNow(running.length > 0);
if (running.length === 0) return null;
const now = Date.now();

return (
<section className="insights-running-now" aria-label="Running now">
Expand Down Expand Up @@ -619,6 +640,7 @@ function InsightsLanding({
byModel,
byTool,
runs,
runsNextCursor,
routines,
workbenches,
latency,
Expand All @@ -633,6 +655,12 @@ function InsightsLanding({
readonly byModel: readonly ModelUsage[] | null;
readonly byTool: readonly ToolCall[] | null;
readonly runs: readonly InsightsRun[];
/** The feed's own `nextCursor` (`limit=100` fetch, see
* `insightsTopLevelRunsPath`) — non-null means more runs exist in this
* window than the 100 fetched, so the KPIs/sparkline/outcome chart below
* disclose the cap instead of silently presenting a truncated series as
* complete. */
readonly runsNextCursor: string | null;
readonly routines: readonly Routine[];
/** Null while `/workbenches` hasn't resolved (or this landing is already
* scoped to one workbench, where a breakdown of one has nothing to
Expand All @@ -653,6 +681,12 @@ function InsightsLanding({
const stats = computeInsightsStats(windowedRuns, routines);
const purposeRuns = purposeRunsForInsights(windowedRuns);

// Liveness is not a windowed property: a run that started before
// `range.from` and is still going is running right now regardless of when
// it started, so "Running now" reads off every fetched run, never the
// range-filtered subset above.
const runningNow = purposeRunsForInsights(runs).filter(isRunningNow);

// Absent usage → zeros at the client boundary (never demo peaks / em-dash
// for "no spend"). Real fetched summary is preserved when present.
const usage = summary ?? EMPTY_OVERALL_USAGE;
Expand Down Expand Up @@ -726,17 +760,17 @@ function InsightsLanding({
loading={loading}
/>
) : null}
{stats.running > 0 || loading ? (
{runningNow.length > 0 || loading ? (
<InsightsStat
label="Running now"
value={tileValue(formatCount(stats.running), loading)}
value={tileValue(formatCount(runningNow.length), loading)}
detail="in flight"
loading={loading}
/>
) : null}
</StatGrid>

<RunningNowStrip runs={purposeRuns} onOpenRun={onOpenRun} />
<RunningNowStrip runs={runningNow} onOpenRun={onOpenRun} />

{latency !== null && latency.total.samples > 0 ? (
<StatGrid columns={4}>
Expand Down Expand Up @@ -780,6 +814,21 @@ function InsightsLanding({
</p>
) : null}

{runsNextCursor !== null ? (
<p className="insights-note">
Runs, sparkline, and outcomes below reflect the 100 most recent runs —
more exist in this window.{" "}
<button
type="button"
className="font-semibold text-primary-emphasis"
onClick={onOpenRuns}
>
See all runs & traces
</button>
.
</p>
) : null}

<div className="insights-grid">
<section className="insights-panel">
<ActivityBars days={activityDays} />
Expand Down Expand Up @@ -1476,6 +1525,7 @@ export function InsightsPage({
byModel={byModelData}
byTool={byToolData}
runs={runsData}
runsNextCursor={runsNextCursor}
routines={routinesData}
workbenches={workbenchesData}
latency={latencyData}
Expand Down
45 changes: 45 additions & 0 deletions apps/web/test/insights-page.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,51 @@ describe("InsightsPage scope switcher", () => {
});
});

function renderAtPathWithRuns(path: string, nextCursor: string | null): string {
return renderToStaticMarkup(
<TestQueryProvider>
<NavigationProvider navigate={() => undefined}>
<BenchProvider>
<InsightsPage
path={path}
summary={{ kind: "ready", data: EMPTY_OVERALL_USAGE }}
activity={{ kind: "ready", data: [] }}
byTool={{ kind: "ready", data: [] }}
runs={{
kind: "ready",
data: { data: [purposeRun], nextCursor },
}}
routines={emptyRoutines}
workbenches={emptyWorkbenches}
latency={emptyLatency}
range={range}
scope={null}
resolveWorkbenchIdForTenant={() => null}
scopeLabel="All workbenches"
/>
</BenchProvider>
</NavigationProvider>
</TestQueryProvider>,
);
}

describe("InsightsPage landing run-cap disclosure", () => {
// The feed is fetched at a fixed limit=100 (see insightsTopLevelRunsPath).
// A non-null nextCursor means the window truly holds more runs than were
// fetched, so the KPIs/sparkline/outcome chart built from that truncated
// set must say so rather than presenting it as the complete series —
// InsightsRunsHistory already discloses its own cap; the landing must too.
test("a non-null nextCursor discloses the 100-run cap on the landing view", () => {
const markup = renderAtPathWithRuns("/insights", "cursor_2");
expect(markup).toContain("100 most recent runs");
});

test("a null nextCursor (fewer than 100 runs) shows no cap disclosure", () => {
const markup = renderAtPathWithRuns("/insights", null);
expect(markup).not.toContain("100 most recent runs");
});
});

describe("InsightsPage breadcrumbs", () => {
test("runs history puts an Insights / Run history trail in the top bar", () => {
const markup = renderAtPath("/insights/runs");
Expand Down
Loading