FN-6619: wire overview to analytics

Command Center Overview now reports live analytics instead of placeholder empty data.

- Fetch token, tool, activity, and best-effort signal analytics for the overview cards.
- Show loading and error states around the core overview metrics while preserving the throughput section.
- Cover populated, empty, error, date-range, and mobile overview behavior in tests.

Files changed:
 .../components/command-center/CommandCenter.tsx    | 154 ++++++++++----
 .../__tests__/CommandCenter.mobile-scroll.test.tsx |  64 +++++-
 .../__tests__/CommandCenter.test.tsx               | 233 ++++++++++++++++++++-
 3 files changed, 408 insertions(+), 43 deletions(-)

Fusion-Task-Id: FN-6619
Fusion-Task-Lineage: 603bcbd1-7913-4014-aea7-9a941e16e29a
This commit is contained in:
gsxdsm
2026-06-17 21:29:50 -07:00
parent 0453a65bf1
commit cee24b8b8d
3 changed files with 408 additions and 43 deletions

View File

@@ -1,6 +1,8 @@
import { useCallback, useRef, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { AlertCircle, Gauge } from "lucide-react";
import type { ActivityAnalytics, TokenAnalytics, ToolAnalytics } from "@fusion/core";
import { api } from "../../api/legacy";
import { DateRangePicker, defaultPresets, rangeFromPreset, type DateRange } from "./DateRangePicker";
import { TokensArea } from "./areas/TokensArea";
import { ToolsArea } from "./areas/ToolsArea";
@@ -10,6 +12,9 @@ import { EcosystemArea } from "./areas/EcosystemArea";
import { SignalsArea } from "./areas/SignalsArea";
import { MissionControlPanel } from "./MissionControlPanel";
import { SdlcFunnel } from "./SdlcFunnel";
import { useAnalyticsArea } from "./areas/useAnalyticsArea";
import { formatCost, formatCount, isInvalidRange, rangeQuery } from "./areas/areaShared";
import type { SignalsAnalytics } from "./areas/SignalsArea";
import "./CommandCenter.css";
type SubViewId =
@@ -44,22 +49,95 @@ function useSubViews(): SubView[] {
interface OverviewStatCard {
id: string;
label: string;
value: string;
subLabel?: string;
}
/**
* Headline stat cards (one per measurement area). Values land once Phase A's
* analytics endpoints exist; until then each card shows the shared empty state.
*/
function OverviewTab({ hasData, range }: { hasData: boolean; range: DateRange }) {
/*
FNXC:CommandCenter 2026-06-17-00:00:
Overview is the Command Center landing surface, so it must reflect real analytics instead of shell placeholders. Show loading while core analytics have not settled, show the empty state only after settled zero data, and treat Signals as best-effort because that endpoint can be absent without invalidating tokens/tools/activity metrics.
*/
function OverviewTab({ range }: { range: DateRange }) {
const { t } = useTranslation("app");
const tokens = useAnalyticsArea<TokenAnalytics>("/command-center/tokens?groupBy=model", range);
const tools = useAnalyticsArea<ToolAnalytics>("/command-center/tools", range);
const activity = useAnalyticsArea<ActivityAnalytics>("/command-center/activity", range);
const [signals, setSignals] = useState<SignalsAnalytics | null>(null);
const [signalsLoading, setSignalsLoading] = useState(true);
const signalsQuery = rangeQuery(range);
const invalidRange = isInvalidRange(range);
useEffect(() => {
if (invalidRange) {
setSignalsLoading(false);
setSignals(null);
return;
}
let cancelled = false;
setSignalsLoading(true);
void (async () => {
try {
const result = await api<SignalsAnalytics>(`/command-center/signals${signalsQuery}`);
if (!cancelled) {
setSignals(result);
}
} catch {
if (!cancelled) {
setSignals(null);
}
} finally {
if (!cancelled) {
setSignalsLoading(false);
}
}
})();
return () => {
cancelled = true;
};
}, [signalsQuery, invalidRange]);
const tokenTotal = tokens.data?.totals?.totalTokens ?? 0;
const toolCalls = tools.data?.toolCalls ?? 0;
const activeNodes = activity.data?.activeNodes ?? 0;
const tasksDone = activity.data?.funnel?.doneInRange ?? 0;
const uniqueModels = tokens.data?.groups?.length ?? 0;
const hasActivityData =
(activity.data?.sessions ?? 0) > 0 ||
(activity.data?.messages ?? 0) > 0 ||
activeNodes > 0 ||
(activity.data?.activeAgents ?? 0) > 0 ||
tasksDone > 0;
const hasData = tokenTotal > 0 || toolCalls > 0 || hasActivityData;
const hasAllCoreData = tokens.data !== null && tools.data !== null && activity.data !== null;
const isInitialLoading = !hasAllCoreData && (tokens.isLoading || tools.isLoading || activity.isLoading);
const coreError = tokens.error ?? tools.error ?? activity.error;
const costLabel = tokens.data ? formatCost(tokens.data.cost?.usd ?? null, tokens.data.cost?.unavailable ?? true) : "—";
const autonomyLabel = tools.data
? tools.data.fullyAutonomous
? t("commandCenter.tools.ratioAutonomous", "{{ratio}} calls/session (fully autonomous)", {
ratio: tools.data.autonomyRatio.toFixed(1),
})
: `${tools.data.autonomyRatio.toFixed(1)}:1`
: "—";
const cards: OverviewStatCard[] = [
{ id: "tokens", label: t("commandCenter.overview.tokensCost", "Tokens & cost") },
{ id: "autonomy", label: t("commandCenter.overview.autonomy", "Autonomy ratio") },
{ id: "nodes", label: t("commandCenter.overview.activeNodes", "Active nodes") },
{ id: "tasksDone", label: t("commandCenter.overview.tasksDone", "Tasks done") },
{ id: "models", label: t("commandCenter.overview.uniqueModels", "Unique models") },
{ id: "signals", label: t("commandCenter.overview.openSignals", "Open signals") },
{
id: "tokens",
label: t("commandCenter.overview.tokensCost", "Tokens & cost"),
value: formatCount(tokenTotal),
subLabel: costLabel,
},
{ id: "autonomy", label: t("commandCenter.overview.autonomy", "Autonomy ratio"), value: autonomyLabel },
{ id: "nodes", label: t("commandCenter.overview.activeNodes", "Active nodes"), value: formatCount(activeNodes) },
{ id: "tasksDone", label: t("commandCenter.overview.tasksDone", "Tasks done"), value: formatCount(tasksDone) },
{ id: "models", label: t("commandCenter.overview.uniqueModels", "Unique models"), value: formatCount(uniqueModels) },
{
id: "signals",
label: t("commandCenter.overview.openSignals", "Open signals"),
value: signalsLoading ? "—" : signals ? formatCount(signals.open ?? 0) : "—",
},
];
// The throughput funnel reads its own data (activityLog transitions) and shows
@@ -71,6 +149,30 @@ function OverviewTab({ hasData, range }: { hasData: boolean; range: DateRange })
</div>
);
if (isInitialLoading) {
return (
<div className="cc-overview">
<div className="cc-loading" data-testid="command-center-overview-loading">
<div className="cc-chart-skeleton" />
<p>{t("commandCenter.loading", "Loading command center...")}</p>
</div>
{throughputSection}
</div>
);
}
if (coreError !== null && !hasData) {
return (
<div className="cc-overview">
<div className="cc-error" data-testid="command-center-overview-error" role="alert">
<AlertCircle size={24} />
<p>{coreError}</p>
</div>
{throughputSection}
</div>
);
}
if (!hasData) {
return (
<div className="cc-overview">
@@ -89,7 +191,8 @@ function OverviewTab({ hasData, range }: { hasData: boolean; range: DateRange })
{cards.map((card) => (
<div key={card.id} className="card cc-stat-card" data-testid={`command-center-stat-${card.id}`}>
<div className="cc-stat-label">{card.label}</div>
<div className="cc-stat-value">—</div>
<div className="cc-stat-value">{card.value}</div>
{card.subLabel ? <span className="cc-stat-sub">{card.subLabel}</span> : null}
</div>
))}
</div>
@@ -118,11 +221,6 @@ export function CommandCenter() {
const { t } = useTranslation("app");
const subViews = useSubViews();
const [activeTab, setActiveTab] = useState<SubViewId>("overview");
// Shell-only state: real loading/error wiring lands with the Phase A endpoints.
const [isLoading] = useState(false);
const [error] = useState<string | null>(null);
// No analytics endpoints yet, so there is no data to show — drives the empty state.
const hasData = false;
const [range, setRange] = useState<DateRange>(() => rangeFromPreset(defaultPresets((_k, f) => f)[1]));
@@ -173,7 +271,7 @@ export function CommandCenter() {
function renderActiveTab() {
switch (activeTab) {
case "overview":
return <OverviewTab hasData={hasData} range={range} />;
return <OverviewTab range={range} />;
case "tokens":
return <TokensArea range={range} />;
case "tools":
@@ -193,24 +291,6 @@ export function CommandCenter() {
}
}
if (isLoading) {
return (
<div className="cc-loading" data-testid="command-center-loading">
<div className="cc-chart-skeleton" style={{ width: "60%" }} />
<p>{t("commandCenter.loading", "Loading command center...")}</p>
</div>
);
}
if (error !== null) {
return (
<div className="cc-error" data-testid="command-center-error" role="alert">
<AlertCircle size={24} />
<p>{error}</p>
</div>
);
}
return (
<section className="command-center" data-testid="command-center">
<header className="cc-header">

View File

@@ -6,6 +6,64 @@ import "@testing-library/jest-dom";
import { loadStylesCss } from "../../../test/cssFixture";
import { CommandCenter } from "../CommandCenter";
const apiMock = vi.fn();
vi.mock("../../../api/legacy", () => ({
api: (path: string, opts?: RequestInit) => apiMock(path, opts),
}));
function emptyTokenFixture() {
return {
totals: { inputTokens: 0, outputTokens: 0, cachedTokens: 0, cacheWriteTokens: 0, totalTokens: 0, nTasks: 0 },
cost: { usd: null, unavailable: true, stale: false },
groups: [],
};
}
function emptyToolsFixture() {
return {
toolCalls: 0,
byCategory: [],
sessions: 0,
interventions: { approvals: 0, userSteers: 0, total: 0 },
autonomyRatio: 0,
fullyAutonomous: true,
};
}
function emptyActivityFixture() {
return {
sessions: 0,
messages: 0,
activeNodes: 0,
activeAgents: 0,
daily: [],
stickiness: 0,
mttr: { value: null, unavailable: true },
monitor: { mttr: { value: null, unavailable: true }, incidents: 0, deployments: 0 },
funnel: {
stages: [
{ stage: "triage", entered: 0, current: 0 },
{ stage: "done", entered: 0, current: 0 },
],
enteredInRange: 0,
doneInRange: 0,
completionRate: 0,
throughputPerDay: 0,
rangeDays: 7,
},
};
}
function mockEmptyOverviewApi() {
apiMock.mockImplementation((path: string) => {
if (path.startsWith("/command-center/tokens")) return Promise.resolve(emptyTokenFixture());
if (path.startsWith("/command-center/tools")) return Promise.resolve(emptyToolsFixture());
if (path.startsWith("/command-center/activity")) return Promise.resolve(emptyActivityFixture());
if (path.startsWith("/command-center/signals")) return Promise.resolve({ totalSignals: 0, open: 0, resolved: 0, mttr: { value: null, unavailable: true }, bySource: [], bySeverity: [] });
return Promise.reject(new Error(`Unhandled api path: ${path}`));
});
}
function injectCommandCenterCss() {
document.head.querySelector("style[data-testid='fn-6595-css']")?.remove();
const style = document.createElement("style");
@@ -52,15 +110,17 @@ function assertScrollOwnerContract(panel: HTMLElement) {
describe("CommandCenter mobile scroll regression (FN-6595)", () => {
beforeEach(() => {
apiMock.mockReset();
mockEmptyOverviewApi();
injectCommandCenterCss();
mockMobileMatchMedia(true);
});
it("keeps the tabpanel as the mobile scroll owner with pinned header and tabs", () => {
it("keeps the tabpanel as the mobile scroll owner with pinned header and tabs", async () => {
render(<CommandCenter />);
const overviewPanel = screen.getByTestId("command-center-panel-overview");
expect(screen.getByTestId("command-center-empty")).toBeTruthy();
await screen.findByTestId("command-center-empty");
assertScrollOwnerContract(overviewPanel);
fireEvent.click(screen.getByTestId("command-center-tab-tokens"));

View File

@@ -1,7 +1,153 @@
import { describe, it, expect } from "vitest";
import { render, screen, fireEvent, within } from "@testing-library/react";
/*
FNXC:CommandCenter 2026-06-17-00:00:
Command Center Overview must consume the same analytics endpoints as the detail tabs. These tests reproduce the prior always-empty landing page, then pin loading-before-empty, range re-derivation, and best-effort Signals behavior so Overview cannot regress into shell placeholders again.
*/
import { beforeEach, describe, it, expect, vi } from "vitest";
import { render, screen, fireEvent, within, waitFor } from "@testing-library/react";
import { CommandCenter } from "../CommandCenter";
const apiMock = vi.fn();
vi.mock("../../../api/legacy", () => ({
api: (path: string, opts?: RequestInit) => apiMock(path, opts),
}));
function tokenFixture(totalTokens = 1_500) {
return {
from: "2026-06-08",
to: null,
groupBy: "model",
totals: {
inputTokens: Math.round(totalTokens * 0.6),
outputTokens: Math.round(totalTokens * 0.3),
cachedTokens: Math.round(totalTokens * 0.1),
cacheWriteTokens: 0,
totalTokens,
nTasks: totalTokens > 0 ? 5 : 0,
},
cost: totalTokens > 0 ? { usd: 12.5, unavailable: false, stale: false } : { usd: null, unavailable: true, stale: false },
groups:
totalTokens > 0
? [
{
key: "gpt-4o",
inputTokens: 600,
outputTokens: 300,
cachedTokens: 100,
cacheWriteTokens: 0,
totalTokens: 900,
nTasks: 3,
cost: { usd: 9.0, unavailable: false, stale: false },
},
{
key: "claude-sonnet",
inputTokens: 400,
outputTokens: 200,
cachedTokens: 100,
cacheWriteTokens: 0,
totalTokens: 600,
nTasks: 2,
cost: { usd: 3.5, unavailable: false, stale: false },
},
]
: [],
};
}
function toolsFixture(toolCalls = 30) {
return {
from: "2026-06-08",
to: null,
toolCalls,
byCategory: toolCalls > 0 ? [{ category: "read", count: toolCalls }] : [],
sessions: toolCalls > 0 ? 3 : 0,
interventions: { approvals: toolCalls > 0 ? 2 : 0, userSteers: toolCalls > 0 ? 1 : 0, total: toolCalls > 0 ? 3 : 0 },
autonomyRatio: toolCalls > 0 ? 10 : 0,
fullyAutonomous: toolCalls === 0,
};
}
function activityFixture(overrides: Partial<Record<"sessions" | "messages" | "activeNodes" | "activeAgents" | "doneInRange", number>> = {}) {
const sessions = overrides.sessions ?? 4;
const messages = overrides.messages ?? 18;
const activeNodes = overrides.activeNodes ?? 3;
const activeAgents = overrides.activeAgents ?? 2;
const doneInRange = overrides.doneInRange ?? 7;
return {
from: "2026-06-08",
to: null,
sessions,
messages,
activeNodes,
activeAgents,
daily: [],
stickiness: activeAgents > 0 ? 0.5 : 0,
mttr: { value: null, unavailable: true },
monitor: { mttr: { value: null, unavailable: true }, incidents: 0, deployments: 0 },
funnel: {
stages: [
{ stage: "triage", entered: doneInRange, current: 0 },
{ stage: "done", entered: doneInRange, current: doneInRange },
],
enteredInRange: doneInRange,
doneInRange,
completionRate: doneInRange > 0 ? 1 : 0,
throughputPerDay: doneInRange > 0 ? 1 : 0,
rangeDays: 7,
},
};
}
const emptyActivityFixture = () =>
activityFixture({ sessions: 0, messages: 0, activeNodes: 0, activeAgents: 0, doneInRange: 0 });
function signalsFixture(open = 2) {
return {
totalSignals: open,
open,
resolved: 0,
mttr: { value: null, unavailable: true },
bySource: [],
bySeverity: [],
};
}
function mockOverviewApi({
tokens = tokenFixture(),
tools = toolsFixture(),
activity = activityFixture(),
signals = signalsFixture(),
}: {
tokens?: unknown;
tools?: unknown;
activity?: unknown;
signals?: unknown;
} = {}) {
apiMock.mockImplementation((path: string) => {
if (path.startsWith("/command-center/tokens")) return Promise.resolve(tokens);
if (path.startsWith("/command-center/tools")) return Promise.resolve(tools);
if (path.startsWith("/command-center/activity")) return Promise.resolve(activity);
if (path.startsWith("/command-center/signals")) {
return signals instanceof Error ? Promise.reject(signals) : Promise.resolve(signals);
}
return Promise.reject(new Error(`Unhandled api path: ${path}`));
});
}
function mockEmptyOverviewApi() {
mockOverviewApi({ tokens: tokenFixture(0), tools: toolsFixture(0), activity: emptyActivityFixture(), signals: signalsFixture(0) });
}
function statValue(testId: string) {
return within(screen.getByTestId(testId)).getByText((content, element) =>
element?.classList.contains("cc-stat-value") === true && content.length > 0,
).textContent;
}
beforeEach(() => {
apiMock.mockReset();
mockEmptyOverviewApi();
});
describe("CommandCenter shell", () => {
it("renders with the Overview tab active by default", () => {
render(<CommandCenter />);
@@ -10,9 +156,88 @@ describe("CommandCenter shell", () => {
expect(screen.getByTestId("command-center-panel-overview")).toBeTruthy();
});
it("renders the documented empty state when there is no data (no crash)", () => {
it("renders the documented empty state when there is no data (no crash)", async () => {
mockEmptyOverviewApi();
render(<CommandCenter />);
expect(screen.getByTestId("command-center-empty")).toBeTruthy();
expect(screen.queryByTestId("command-center-empty")).toBeNull();
expect(screen.getByTestId("command-center-overview-loading")).toBeTruthy();
await screen.findByTestId("command-center-empty");
});
it("renders live Overview headline values when analytics data exists", async () => {
mockOverviewApi();
render(<CommandCenter />);
await waitFor(() => expect(screen.queryByTestId("command-center-empty")).toBeNull());
await screen.findByTestId("command-center-stat-tokens");
expect(statValue("command-center-stat-tokens")).toBe("1,500");
expect(screen.getByTestId("command-center-stat-tokens").textContent).toContain("$12.50");
expect(statValue("command-center-stat-autonomy")).toBe("10.0:1");
expect(statValue("command-center-stat-nodes")).toBe("3");
expect(statValue("command-center-stat-tasksDone")).toBe("7");
expect(statValue("command-center-stat-models")).toBe("2");
expect(statValue("command-center-stat-signals")).toBe("2");
expect(screen.getByTestId("command-center-live-strip")).toBeTruthy();
expect(screen.getByTestId("command-center-throughput")).toBeTruthy();
});
it("renders cards for partially populated analytics instead of the empty state", async () => {
mockOverviewApi({ tokens: tokenFixture(0), tools: toolsFixture(0), activity: activityFixture({ sessions: 0, messages: 0, activeNodes: 1, activeAgents: 0, doneInRange: 0 }), signals: signalsFixture(0) });
render(<CommandCenter />);
await screen.findByTestId("command-center-stat-nodes");
expect(screen.queryByTestId("command-center-empty")).toBeNull();
expect(statValue("command-center-stat-tokens")).toBe("0");
expect(statValue("command-center-stat-nodes")).toBe("1");
});
it("keeps Overview populated when the signals endpoint is missing", async () => {
mockOverviewApi({ signals: new Error("API returned HTML instead of JSON (404)") });
render(<CommandCenter />);
await screen.findByTestId("command-center-stat-signals");
expect(screen.queryByTestId("command-center-empty")).toBeNull();
expect(screen.queryByTestId("command-center-overview-error")).toBeNull();
expect(statValue("command-center-stat-signals")).toBe("—");
});
it("surfaces a settled core-source error without staying in loading", async () => {
apiMock.mockImplementation((path: string) => {
if (path.startsWith("/command-center/tokens")) return Promise.reject(new Error("tokens failed"));
if (path.startsWith("/command-center/tools")) return Promise.resolve(toolsFixture(0));
if (path.startsWith("/command-center/activity")) return Promise.resolve(emptyActivityFixture());
if (path.startsWith("/command-center/signals")) return Promise.resolve(signalsFixture(0));
return Promise.reject(new Error(`Unhandled api path: ${path}`));
});
render(<CommandCenter />);
await screen.findByTestId("command-center-overview-error");
expect(screen.getByTestId("command-center-overview-error").textContent).toContain("tokens failed");
expect(screen.queryByTestId("command-center-overview-loading")).toBeNull();
expect(screen.queryByTestId("command-center-empty")).toBeNull();
});
it("re-fetches and re-derives the Overview empty state when the range changes", async () => {
apiMock.mockImplementation((path: string) => {
const populated = path.includes("from=");
if (path.startsWith("/command-center/tokens")) return Promise.resolve(populated ? tokenFixture() : tokenFixture(0));
if (path.startsWith("/command-center/tools")) return Promise.resolve(populated ? toolsFixture() : toolsFixture(0));
if (path.startsWith("/command-center/activity")) return Promise.resolve(populated ? activityFixture() : emptyActivityFixture());
if (path.startsWith("/command-center/signals")) return Promise.resolve(populated ? signalsFixture() : signalsFixture(0));
return Promise.reject(new Error(`Unhandled api path: ${path}`));
});
render(<CommandCenter />);
await screen.findByTestId("command-center-stat-tokens");
expect(screen.queryByTestId("command-center-empty")).toBeNull();
fireEvent.click(screen.getByTestId("cc-date-range-trigger"));
fireEvent.click(screen.getByTestId("cc-date-range-preset-all"));
await screen.findByTestId("command-center-empty");
expect(screen.queryByTestId("command-center-stat-tokens")).toBeNull();
expect(apiMock.mock.calls.some(([path]) => typeof path === "string" && path === "/command-center/tools")).toBe(true);
});
it("exposes the ARIA tabs pattern (tablist + tabs + tabpanel)", () => {