FN-6650: add Overview analytics charts

Add graph-rich Overview visuals to the Command Center using existing analytics data.

- Add tokens-by-model, tool-category, and daily activity chart cards to the Overview tab.
- Style the chart section with responsive dashboard-token layouts and reduced-motion safeguards.
- Cover populated, partial, edge-case, and mobile-scroll chart rendering in tests.
- Document the Overview charts and add a patch changeset for the published CLI package.

Files changed:
 .../fn-6650-command-center-overview-charts.md      |   5 +
 docs/dashboard-guide.md                            |   2 +-
 .../components/command-center/CommandCenter.css    | 111 +++++++++++++++++++++
 .../components/command-center/CommandCenter.tsx    |  73 +++++++++++++-
 .../__tests__/CommandCenter.mobile-scroll.test.tsx |  73 +++++++++++++-
 .../__tests__/CommandCenter.test.tsx               |  58 +++++++++++
 6 files changed, 313 insertions(+), 9 deletions(-)

Fusion-Task-Id: FN-6650
Fusion-Task-Lineage: 227f5809-ed71-42fb-8847-b137dbef6ac7
This commit is contained in:
gsxdsm
2026-06-18 14:38:07 -07:00
parent 538bf42e32
commit 2367918fdf
6 changed files with 313 additions and 9 deletions

View File

@@ -0,0 +1,5 @@
---
"@runfusion/fusion": patch
---
Add attractive Command Center Overview charts for tokens by model, tool categories, and daily activity using existing analytics data.

View File

@@ -663,7 +663,7 @@ Navigation:
Features:
- Global date-range picker in the header scopes the analytics tabs; **Mission Control** remains live rather than historical.
- **Overview** summarizes token usage/cost, autonomy, active nodes, tasks done, model breadth, and open signals, and includes the SDLC throughput funnel for the selected range. The SDLC completion rate is shown as a radial gauge and is calculated as cohort conversion from in-range triage entrants, so the rate is capped at 100% even when older tasks finish during the range.
- **Overview** summarizes token usage/cost, autonomy, active nodes, tasks done, model breadth, and open signals, and includes the SDLC throughput funnel for the selected range. It also shows a graph-rich software-factory snapshot with tokens-by-model, tool-category, and daily activity trend charts that reuse the already-loaded tokens, tools, and activity analytics; no extra endpoint is called. The chart reveal/glow accents are decorative and disabled when reduced-motion preferences are active. The SDLC completion rate is shown as a radial gauge and is calculated as cohort conversion from in-range triage entrants, so the rate is capped at 100% even when older tasks finish during the range.
- **Tokens** breaks down token totals, estimated cost, tasks, and per-model usage.
- **Tools** shows autonomy ratio, tool-call volume, intervention counts, sessions, and tool categories.
- **Activity** tracks sessions, messages, active nodes, active agents, stickiness, and daily activity sparklines.

View File

@@ -238,6 +238,109 @@ The Command Center must remain scrollable on mobile inside the overflow-hidden .
}
}
/*
FNXC:CommandCenterStyling 2026-06-18-00:00:
Overview charts must use dashboard tokens only and keep motion decorative; animations use --duration-* values and are disabled for reduced-motion users so the graph-rich snapshot does not violate accessibility or the mobile scroll contract.
*/
.cc-overview-charts {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: var(--space-3);
align-items: stretch;
}
.cc-overview-chart-card {
position: relative;
display: flex;
flex-direction: column;
gap: var(--space-3);
min-width: 0;
padding: var(--space-3);
border-color: color-mix(in srgb, var(--color-accent) 22%, var(--border-subtle));
background:
linear-gradient(145deg, color-mix(in srgb, var(--color-accent) 10%, transparent), transparent),
var(--surface-1);
box-shadow: 0 0 var(--space-4) color-mix(in srgb, var(--color-accent) 12%, transparent);
overflow: hidden;
animation: cc-overview-chart-rise var(--duration-normal) ease-out both;
}
.cc-overview-chart-card--trend {
grid-column: 1 / -1;
}
.cc-overview-chart-card::before {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(90deg, transparent, color-mix(in srgb, var(--color-accent) 16%, transparent), transparent);
opacity: 0;
pointer-events: none;
animation: cc-overview-chart-sheen calc(var(--duration-slow) * 7) ease-in-out infinite;
}
.cc-overview-chart-header {
position: relative;
z-index: 1;
display: flex;
flex-direction: column;
gap: var(--space-1);
}
.cc-overview-chart-header p {
margin: 0;
color: var(--text-muted);
font-size: var(--font-size-sm);
}
.cc-overview-chart-card .cc-bar-chart,
.cc-overview-chart-card .cc-sparkline {
position: relative;
z-index: 1;
}
.cc-overview-chart-card .cc-sparkline {
height: var(--space-16);
}
.cc-overview-chart-card .cc-bar-fill,
.cc-overview-chart-card .cc-sparkline-bar {
box-shadow: 0 0 var(--space-2) color-mix(in srgb, var(--color-accent) 30%, transparent);
}
@keyframes cc-overview-chart-rise {
from {
opacity: 0;
transform: translateY(var(--space-2));
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes cc-overview-chart-sheen {
0% {
opacity: 0;
transform: translateX(-100%);
}
45%,
55% {
opacity: 0.35;
}
100% {
opacity: 0;
transform: translateX(100%);
}
}
@media (prefers-reduced-motion: reduce) {
.cc-overview-chart-card,
.cc-overview-chart-card::before {
animation: none;
}
}
@media (max-width: 768px) {
.cc-live-strip {
grid-template-columns: 1fr;
@@ -246,6 +349,14 @@ The Command Center must remain scrollable on mobile inside the overflow-hidden .
.cc-live-strip-metrics {
grid-template-columns: 1fr;
}
.cc-overview-charts {
grid-template-columns: 1fr;
}
.cc-overview-chart-card--trend {
grid-column: auto;
}
}
/* ---- States ---- */

View File

@@ -1,4 +1,4 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { AlertCircle, Gauge } from "lucide-react";
import type { ActivityAnalytics, TokenAnalytics, ToolAnalytics } from "@fusion/core";
@@ -12,6 +12,7 @@ import { EcosystemArea } from "./areas/EcosystemArea";
import { SignalsArea } from "./areas/SignalsArea";
import { MissionControlPanel } from "./MissionControlPanel";
import { SdlcFunnel } from "./SdlcFunnel";
import { Bar, type BarDatum } from "./charts/Bar";
import { Sparkline } from "./charts/Sparkline";
import { useAnalyticsArea } from "./areas/useAnalyticsArea";
import { formatCost, formatCount, isInvalidRange, rangeQuery } from "./areas/areaShared";
@@ -105,10 +106,38 @@ function OverviewTab({ range }: { range: DateRange }) {
const tasksDone = activity.data?.funnel?.doneInRange ?? 0;
const inProgressTasks = activity.data?.funnel?.stages.find((stage) => stage.stage === "in-progress")?.entered ?? 0;
const uniqueModels = tokens.data?.groups?.length ?? 0;
const tokensByModelData = useMemo<BarDatum[]>(
() =>
[...(tokens.data?.groups ?? [])]
.sort((a, b) => b.totalTokens - a.totalTokens || (a.key ?? "").localeCompare(b.key ?? ""))
.slice(0, 8)
.map((g) => ({
label: g.key ?? t("commandCenter.tokens.unknownModel", "(unknown)"),
value: g.totalTokens,
valueLabel: formatCount(g.totalTokens),
})),
[tokens.data?.groups, t],
);
const toolCategoryData = useMemo<BarDatum[]>(
() =>
[...(tools.data?.byCategory ?? [])]
.sort((a, b) => b.count - a.count || a.category.localeCompare(b.category))
.map((c) => ({
label: c.category,
value: c.count,
valueLabel: formatCount(c.count),
})),
[tools.data?.byCategory],
);
const dailyActivityValues = useMemo(
() => (activity.data?.daily ?? []).map((day) => day.messages + day.activeAgents),
[activity.data?.daily],
);
const activityTrendValues =
activity.data && activity.data.daily.length > 0
? activity.data.daily.map((day) => day.messages + day.activeAgents)
dailyActivityValues.length > 0
? dailyActivityValues
: [activity.data?.sessions ?? 0, activity.data?.messages ?? 0, activeAgents, activeNodes, tasksDone];
const hasOverviewChartData = tokensByModelData.length > 0 || toolCategoryData.length > 0 || dailyActivityValues.length > 0;
const hasActivityData =
(activity.data?.sessions ?? 0) > 0 ||
(activity.data?.messages ?? 0) > 0 ||
@@ -237,6 +266,44 @@ function OverviewTab({ range }: { range: DateRange }) {
/>
</div>
</div>
{hasOverviewChartData ? (
/*
FNXC:CommandCenter 2026-06-18-00:00:
Overview must present an attractive, graph-rich software-factory snapshot reusing existing tokens/tools/activity analytics with no new endpoint, additive to the live strip and funnel.
*/
<section className="cc-overview-charts" data-testid="command-center-overview-charts">
{tokensByModelData.length > 0 ? (
<div className="card cc-overview-chart-card" data-testid="command-center-overview-chart-tokens">
<div className="cc-overview-chart-header">
<h3 className="cc-area-section-title">{t("commandCenter.overview.tokensByModel", "Tokens by model")}</h3>
<p>{t("commandCenter.overview.tokensByModelHint", "Top model token consumers in this range")}</p>
</div>
<Bar data={tokensByModelData} ariaLabel={t("commandCenter.overview.tokensByModel", "Tokens by model")} />
</div>
) : null}
{toolCategoryData.length > 0 ? (
<div className="card cc-overview-chart-card" data-testid="command-center-overview-chart-tools">
<div className="cc-overview-chart-header">
<h3 className="cc-area-section-title">{t("commandCenter.overview.toolCategories", "Tool categories")}</h3>
<p>{t("commandCenter.overview.toolCategoriesHint", "Autonomous work grouped by tool family")}</p>
</div>
<Bar data={toolCategoryData} ariaLabel={t("commandCenter.overview.toolCategories", "Tool categories")} />
</div>
) : null}
{dailyActivityValues.length > 0 ? (
<div className="card cc-overview-chart-card cc-overview-chart-card--trend" data-testid="command-center-overview-chart-activity">
<div className="cc-overview-chart-header">
<h3 className="cc-area-section-title">{t("commandCenter.overview.dailyActivity", "Daily activity trend")}</h3>
<p>{t("commandCenter.overview.dailyActivityHint", "Messages plus active agents per day")}</p>
</div>
<Sparkline
values={dailyActivityValues}
ariaLabel={t("commandCenter.overview.dailyActivityAria", "Daily activity trend")}
/>
</div>
) : null}
</section>
) : null}
{throughputSection}
</div>
);

View File

@@ -54,11 +54,65 @@ function emptyActivityFixture() {
};
}
function mockEmptyOverviewApi() {
function populatedTokenFixture() {
return {
...emptyTokenFixture(),
totals: { inputTokens: 600, outputTokens: 300, cachedTokens: 100, cacheWriteTokens: 0, totalTokens: 1000, nTasks: 3 },
cost: { usd: 9, unavailable: false, stale: false },
groups: [
{
key: "gpt-4o",
inputTokens: 600,
outputTokens: 300,
cachedTokens: 100,
cacheWriteTokens: 0,
totalTokens: 1000,
nTasks: 3,
cost: { usd: 9, unavailable: false, stale: false },
},
],
};
}
function populatedToolsFixture() {
return {
...emptyToolsFixture(),
toolCalls: 12,
byCategory: [{ category: "read", count: 12 }],
sessions: 2,
autonomyRatio: 6,
fullyAutonomous: false,
};
}
function populatedActivityFixture() {
return {
...emptyActivityFixture(),
sessions: 2,
messages: 8,
activeNodes: 2,
activeAgents: 1,
daily: [{ day: "2026-06-08", activeNodes: 2, activeAgents: 1, messages: 8 }],
funnel: {
...emptyActivityFixture().funnel,
stages: [
{ stage: "triage", entered: 2, current: 0 },
{ stage: "in-progress", entered: 1, current: 1 },
{ stage: "done", entered: 2, current: 2 },
],
enteredInRange: 2,
doneInRange: 2,
completionRate: 1,
throughputPerDay: 1,
},
};
}
function mockOverviewApi({ populated = false }: { populated?: boolean } = {}) {
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/tokens")) return Promise.resolve(populated ? populatedTokenFixture() : emptyTokenFixture());
if (path.startsWith("/command-center/tools")) return Promise.resolve(populated ? populatedToolsFixture() : emptyToolsFixture());
if (path.startsWith("/command-center/activity")) return Promise.resolve(populated ? populatedActivityFixture() : 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}`));
});
@@ -111,7 +165,7 @@ function assertScrollOwnerContract(panel: HTMLElement) {
describe("CommandCenter mobile scroll regression (FN-6595)", () => {
beforeEach(() => {
apiMock.mockReset();
mockEmptyOverviewApi();
mockOverviewApi();
injectCommandCenterCss();
mockMobileMatchMedia(true);
});
@@ -129,6 +183,15 @@ describe("CommandCenter mobile scroll regression (FN-6595)", () => {
assertScrollOwnerContract(tokensPanel);
});
it("preserves the mobile scroll owner when the populated Overview charts render", async () => {
mockOverviewApi({ populated: true });
render(<CommandCenter />);
await screen.findByTestId("command-center-overview-charts");
expect(screen.getByTestId("command-center-overview-chart-tokens")).toBeTruthy();
assertScrollOwnerContract(screen.getByTestId("command-center-panel-overview"));
});
it("keeps the same flex-fill scroll-owner contract outside the mobile breakpoint", () => {
mockMobileMatchMedia(false);
render(<CommandCenter />);

View File

@@ -163,7 +163,9 @@ describe("CommandCenter shell", () => {
render(<CommandCenter />);
expect(screen.queryByTestId("command-center-empty")).toBeNull();
expect(screen.getByTestId("command-center-overview-loading")).toBeTruthy();
expect(screen.queryByTestId("command-center-overview-charts")).toBeNull();
await screen.findByTestId("command-center-empty");
expect(screen.queryByTestId("command-center-overview-charts")).toBeNull();
});
it("renders live Overview headline values when analytics data exists", async () => {
@@ -188,6 +190,12 @@ describe("CommandCenter shell", () => {
expect(screen.getByTestId("command-center-throughput-trend")).toBeTruthy();
expect(screen.getByRole("img", { name: "Recent activity throughput trend" })).toBeTruthy();
expect(screen.getByTestId("command-center-throughput")).toBeTruthy();
const charts = screen.getByTestId("command-center-overview-charts");
expect(within(charts).getByText("Tokens by model")).toBeTruthy();
expect(within(screen.getByTestId("command-center-overview-chart-tokens")).getByText("gpt-4o")).toBeTruthy();
expect(within(screen.getByTestId("command-center-overview-chart-tools")).getByText("read")).toBeTruthy();
expect(screen.getByRole("img", { name: "Daily activity trend" })).toBeTruthy();
});
it("renders cards for partially populated analytics instead of the empty state", async () => {
@@ -198,6 +206,55 @@ describe("CommandCenter shell", () => {
expect(screen.queryByTestId("command-center-empty")).toBeNull();
expect(statValue("command-center-stat-tokens")).toBe("0");
expect(statValue("command-center-stat-nodes")).toBe("1");
expect(screen.queryByTestId("command-center-overview-charts")).toBeNull();
expect(screen.queryByTestId("command-center-overview-loading")).toBeNull();
expect(screen.queryByTestId("command-center-overview-error")).toBeNull();
});
it("renders no empty chart shell when some populated sources have no chart rows", async () => {
mockOverviewApi({ tokens: tokenFixture(), tools: toolsFixture(0), activity: activityFixture(), signals: signalsFixture(0) });
render(<CommandCenter />);
await screen.findByTestId("command-center-overview-charts");
expect(screen.getByTestId("command-center-overview-chart-tokens")).toBeTruthy();
expect(screen.queryByTestId("command-center-overview-chart-tools")).toBeNull();
expect(screen.getByTestId("command-center-overview-chart-activity")).toBeTruthy();
});
it("handles empty, undefined, single-item, and zero chart data without NaN output", async () => {
const tokensWithSingleZeroGroup = {
...tokenFixture(0),
groups: [
{
key: "idle-model",
inputTokens: 0,
outputTokens: 0,
cachedTokens: 0,
cacheWriteTokens: 0,
totalTokens: 0,
nTasks: 0,
cost: { usd: null, unavailable: true, stale: false },
},
],
};
const toolsWithoutCategories = { ...toolsFixture(1), byCategory: undefined };
const activityWithSingleZeroDay = {
...activityFixture({ sessions: 0, messages: 0, activeNodes: 1, activeAgents: 0, doneInRange: 0 }),
daily: [{ day: "2026-06-08", activeNodes: 0, activeAgents: 0, messages: 0 }],
};
mockOverviewApi({
tokens: tokensWithSingleZeroGroup,
tools: toolsWithoutCategories,
activity: activityWithSingleZeroDay,
signals: signalsFixture(0),
});
render(<CommandCenter />);
await screen.findByTestId("command-center-overview-charts");
expect(screen.getByTestId("command-center-overview-chart-tokens").textContent).toContain("idle-model");
expect(screen.queryByTestId("command-center-overview-chart-tools")).toBeNull();
expect(screen.getByTestId("command-center-overview-chart-activity")).toBeTruthy();
expect(screen.getByTestId("command-center-panel-overview").textContent).not.toContain("NaN");
});
it("keeps Overview populated when the signals endpoint is missing", async () => {
@@ -224,6 +281,7 @@ describe("CommandCenter shell", () => {
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();
expect(screen.queryByTestId("command-center-overview-charts")).toBeNull();
});
it("re-fetches and re-derives the Overview empty state when the range changes", async () => {