diff --git a/apps/web/src/pages/insights-page.tsx b/apps/web/src/pages/insights-page.tsx
index 62c08fe41..b646ea638 100644
--- a/apps/web/src/pages/insights-page.tsx
+++ b/apps/web/src/pages/insights-page.tsx
@@ -1,8 +1,10 @@
// Insights over packages/insights: cost KPIs, activity bars, token mosaic,
// cost-by-model, calls-by-tool, recent purpose runs, runs history, and
-// run-trace detail. Absent usage is zero metrics + zero day series
-// (EMPTY_OVERALL_USAGE / activitySeriesForWindow). Null cost/rate still
-// means "rate unknown" when turns exist — em-dash, not a fabricated cost.
+// run-trace detail. Data may still be EMPTY_OVERALL_USAGE /
+// activitySeriesForWindow at the client boundary; zero-turn landings hide
+// Cost / Activity / Tokens chrome rather than showing zero KPI tiles. Null
+// cost/rate still means "rate unknown" when turns exist — em-dash, not a
+// fabricated cost.
// Stage layout mirrors the shell mock: KPI row → chart/card grid → recent runs.
import {
@@ -687,6 +689,9 @@ function InsightsLanding({
const hitRate = cacheHitRate(usage);
const missingRates = modelsWithMissingRates(usage);
const activityDays = activitySeriesForWindow(activity ?? [], range);
+ const activityWindowEmpty = recentActivityDays(activityDays).every(
+ (d) => d.turns === 0,
+ );
const models = byModel !== null && byModel.length > 0 ? byModel : null;
const tools = byTool !== null && byTool.length > 0 ? byTool : null;
const recent = purposeRuns.slice(0, 12);
@@ -704,35 +709,41 @@ function InsightsLanding({
return (
-
-
-
+ {noUsageInWindow ? null : (
+
+ )}
+ {noUsageInWindow ? null : (
+
+ )}
+ {noUsageInWindow ? null : (
+
+ )}
) : null}
) : null}
- {noUsageInWindow ? (
+ {noUsageInWindow && !activityWindowEmpty ? (
No usage recorded yet in this window.
) : null}
@@ -824,7 +835,19 @@ function InsightsLanding({
-
+ {activityWindowEmpty ? (
+ }
+ title="No activity yet"
+ description={
+ noUsageInWindow
+ ? "No usage recorded yet in this window."
+ : "Activity shows up here once there are turns in this window."
+ }
+ />
+ ) : (
+
+ )}
{mosaicParts.length > 0 ? (
@@ -1325,9 +1348,10 @@ export function InsightsPage({
runs.kind === "loading" ||
routines.kind === "loading";
- // Usage/activity/tools errors must surface. Loading (and ready-empty /
- // no-tenant ready zeros from InsightsRoute) still render zero defaults so
- // the dashboard never invents spend. Runs/routines soft-empty on landing.
+ // Usage/activity/tools errors must surface. Loading and ready-empty /
+ // no-tenant still use EMPTY_OVERALL_USAGE at the data boundary so the
+ // dashboard never invents spend; zero-turn chrome hides usage tiles.
+ // Runs/routines soft-empty on landing.
const usageErrorRetry =
summary.kind === "error"
? summary.retry
@@ -1676,10 +1700,10 @@ export function InsightsRoute({ path }: { readonly path?: string }) {
effectiveTenantId === null
? { kind: "ready", data: EMPTY_OVERALL_USAGE }
: summary;
- const emptyList =
(q: APIQuery): APIQuery =>
- effectiveTenantId === null
- ? ({ kind: "ready", data: [] as unknown as T } as APIQuery)
- : q;
+ const activityForPage: APIQuery =
+ effectiveTenantId === null ? { kind: "ready", data: [] } : activity;
+ const byToolForPage: APIQuery =
+ effectiveTenantId === null ? { kind: "ready", data: [] } : byTool;
const runsForPage: APIQuery<{
data: readonly InsightsRun[];
nextCursor: string | null;
@@ -1710,8 +1734,8 @@ export function InsightsRoute({ path }: { readonly path?: string }) {
{
- test("ready-empty usage renders zero KPIs, not a load error", () => {
+ test("ready-empty usage hides usage KPI chrome, keeps Runs, not a load error", () => {
const markup = renderLanding({
summary: { kind: "ready", data: EMPTY_OVERALL_USAGE },
activity: { kind: "ready", data: [] },
byTool: { kind: "ready", data: [] },
});
expect(markup).not.toContain("load insights");
- expect(markup).toContain("$0.00");
+ expect(markup).not.toContain("$0.00");
+ expect(markup).not.toContain("Tokens in / out");
+ expect(markup).toContain("Runs");
expect(markup).toContain("Insights");
});
+ test("zero-turn landing does not render a padded empty activity chart", () => {
+ const markup = renderLanding({
+ summary: { kind: "ready", data: EMPTY_OVERALL_USAGE },
+ activity: { kind: "ready", data: [] },
+ byTool: { kind: "ready", data: [] },
+ });
+ expect(markup).not.toContain("Axis to");
+ });
+
+ test("all-zero activity days do not render a chart even when the raw series is non-empty", () => {
+ const markup = renderLanding({
+ summary: { kind: "ready", data: EMPTY_OVERALL_USAGE },
+ activity: {
+ kind: "ready",
+ data: [
+ { day: "2026-01-09", turns: 0, tokens: 0, byModel: [] },
+ { day: "2026-01-10", turns: 0, tokens: 0, byModel: [] },
+ { day: "2026-01-11", turns: 0, tokens: 0, byModel: [] },
+ { day: "2026-01-12", turns: 0, tokens: 0, byModel: [] },
+ { day: "2026-01-13", turns: 0, tokens: 0, byModel: [] },
+ { day: "2026-01-14", turns: 0, tokens: 0, byModel: [] },
+ { day: "2026-01-15", turns: 0, tokens: 0, byModel: [] },
+ ],
+ },
+ byTool: { kind: "ready", data: [] },
+ });
+ expect(markup).not.toContain("Axis to");
+ });
+
+ test("turns with a true zero cost still show $0.00", () => {
+ const markup = renderLanding({
+ summary: {
+ kind: "ready",
+ data: {
+ turns: 4,
+ tokens: {
+ input: 20,
+ cacheRead: 0,
+ cacheWrite: 0,
+ output: 10,
+ thinking: 0,
+ total: 30,
+ },
+ costUsd: 0,
+ byModel: [],
+ },
+ },
+ activity: {
+ kind: "ready",
+ data: [{ day: "2026-01-15", turns: 4, tokens: 30, byModel: [] }],
+ },
+ byTool: { kind: "ready", data: [] },
+ });
+ expect(markup).toContain("$0.00");
+ expect(markup).toContain("Tokens in / out");
+ });
+
test("summary API error surfaces load failure instead of zeros", () => {
const markup = renderLanding({
summary: {
@@ -791,6 +850,10 @@ describe("InsightsPage global landing — turn latency tiles (CL-6257)", () => {
expect(el.textContent).toContain("3.4s / 8.0s");
expect(el.textContent).toContain("To first token (p50 / p95)");
expect(el.textContent).toContain("1.2s / 3.4s");
+ expect(el.textContent).toContain("wait until first token");
+ expect(el.textContent).toContain("first token → reply posted");
+ expect(el.textContent).not.toContain("inference start");
+ expect(el.textContent).not.toContain("reactor");
// No cold starts this window — that stage tile does not render at all.
expect(el.textContent).not.toContain("Cold start");
});
@@ -808,5 +871,8 @@ describe("InsightsPage global landing — turn latency tiles (CL-6257)", () => {
});
expect(el.textContent).toContain("Cold start (p50 / p95)");
expect(el.textContent).toContain("30.0s / 45.0s");
+ expect(el.textContent).toContain("wait before the model starts");
+ expect(el.textContent).not.toContain("reactor");
+ expect(el.textContent).not.toContain("inference start");
});
});