FN-6651: fix Command Center in-progress count

Command Center now shows current board-state in-progress tasks instead of historical funnel entries.

- Fetch the live Command Center snapshot for Overview live activity metrics.
- Source tasks-in-progress from current live column counts with loading and failure fallbacks.
- Cover populated, zero, missing-column, pending, failure, range-independent, and mobile overview cases.
- Document that the live snapshot count is independent of analytics date ranges.

Files changed:
 docs/dashboard-guide.md                            |  2 +-
 .../components/command-center/CommandCenter.tsx    | 36 +++++++-
 .../__tests__/CommandCenter.mobile-scroll.test.tsx | 12 +++
 .../__tests__/CommandCenter.test.tsx               | 98 +++++++++++++++++++++-
 4 files changed, 142 insertions(+), 6 deletions(-)

Fusion-Task-Id: FN-6651

Fusion-Task-Lineage: a4c01d6f-437f-49e0-baf9-fb6b76d5e8e8
This commit is contained in:
gsxdsm
2026-06-18 14:59:26 -07:00
parent 662a09b631
commit e14f33568c
4 changed files with 142 additions and 6 deletions

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. 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.
- **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. Its Live activity snapshot shows the current board-state count for tasks in progress, independent of the selected analytics date 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. 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, and stickiness, then renders live animated line charts for messages/day, active agents/day, active nodes/day, and combined throughput/day (`messages + active agents + active nodes`). These charts reuse the existing activity analytics endpoint, refresh on a bounded 15-second cadence while mounted, keep the previous data visible during refreshes, and disable decorative draw-on motion for reduced-motion users.

View File

@@ -1,7 +1,7 @@
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";
import type { ActivityAnalytics, LiveSnapshot, TokenAnalytics, ToolAnalytics } from "@fusion/core";
import { api } from "../../api/legacy";
import { DateRangePicker, defaultPresets, rangeFromPreset, type DateRange } from "./DateRangePicker";
import { TokensArea } from "./areas/TokensArea";
@@ -66,6 +66,8 @@ function OverviewTab({ range }: { range: DateRange }) {
const activity = useAnalyticsArea<ActivityAnalytics>("/command-center/activity", range);
const [signals, setSignals] = useState<SignalsAnalytics | null>(null);
const [signalsLoading, setSignalsLoading] = useState(true);
const [liveSnapshot, setLiveSnapshot] = useState<LiveSnapshot | null>(null);
const [liveSnapshotLoading, setLiveSnapshotLoading] = useState(true);
const signalsQuery = rangeQuery(range);
const invalidRange = isInvalidRange(range);
@@ -99,12 +101,40 @@ function OverviewTab({ range }: { range: DateRange }) {
};
}, [signalsQuery, invalidRange]);
useEffect(() => {
let cancelled = false;
setLiveSnapshotLoading(true);
void (async () => {
try {
const result = await api<LiveSnapshot>("/command-center/live");
if (!cancelled) {
setLiveSnapshot(result);
}
} catch {
if (!cancelled) {
setLiveSnapshot(null);
}
} finally {
if (!cancelled) {
setLiveSnapshotLoading(false);
}
}
})();
return () => {
cancelled = true;
};
}, []);
const tokenTotal = tokens.data?.totals?.totalTokens ?? 0;
const toolCalls = tools.data?.toolCalls ?? 0;
const activeNodes = activity.data?.activeNodes ?? 0;
const activeAgents = activity.data?.activeAgents ?? 0;
const tasksDone = activity.data?.funnel?.doneInRange ?? 0;
const inProgressTasks = activity.data?.funnel?.stages.find((stage) => stage.stage === "in-progress")?.entered ?? 0;
/*
FNXC:CommandCenter 2026-06-18-00:00:
The Live activity snapshot "tasks in progress" metric must reflect current board state from /command-center/live columns, not the date-range SDLC funnel entered count, because cumulative transitions inflate with history and do not decrease when tasks leave in-progress.
*/
const inProgressTasks = liveSnapshot?.columns.find((column) => column.column === "in-progress")?.count ?? 0;
const uniqueModels = tokens.data?.groups?.length ?? 0;
const tokensByModelData = useMemo<BarDatum[]>(
() =>
@@ -246,7 +276,7 @@ function OverviewTab({ range }: { range: DateRange }) {
</div>
<div className="cc-live-strip-metrics" data-testid="command-center-live-snapshot">
<span className="cc-live-metric" data-testid="command-center-live-tasks-in-progress">
<span className="cc-live-metric-value">{formatCount(inProgressTasks)}</span>
<span className="cc-live-metric-value">{liveSnapshotLoading ? "—" : formatCount(inProgressTasks)}</span>
<span className="cc-live-metric-label">{t("commandCenter.overview.tasksInProgress", "tasks in progress")}</span>
</span>
<span className="cc-live-metric" data-testid="command-center-live-agents-working">

View File

@@ -114,6 +114,17 @@ function mockOverviewApi({ populated = false }: { populated?: boolean } = {}) {
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: [] });
if (path === "/command-center/live") {
return Promise.resolve({
capturedAt: "2026-06-18T00:00:00.000Z",
activeSessions: 0,
activeRuns: 0,
activeNodes: 0,
sessions: [],
runs: [],
columns: [{ column: "in-progress", count: populated ? 1 : 0 }],
});
}
return Promise.reject(new Error(`Unhandled api path: ${path}`));
});
}
@@ -189,6 +200,7 @@ describe("CommandCenter mobile scroll regression (FN-6595)", () => {
await screen.findByTestId("command-center-overview-charts");
expect(screen.getByTestId("command-center-overview-chart-tokens")).toBeTruthy();
expect(screen.getByTestId("command-center-live-tasks-in-progress")).toBeTruthy();
assertScrollOwnerContract(screen.getByTestId("command-center-panel-overview"));
});

View File

@@ -113,16 +113,30 @@ function signalsFixture(open = 2) {
};
}
function liveFixture(columns: Array<{ column: string; count: number }> = [{ column: "in-progress", count: 3 }]) {
return {
capturedAt: "2026-06-18T00:00:00.000Z",
activeSessions: 0,
activeRuns: 0,
activeNodes: 0,
sessions: [],
runs: [],
columns,
};
}
function mockOverviewApi({
tokens = tokenFixture(),
tools = toolsFixture(),
activity = activityFixture(),
signals = signalsFixture(),
live = liveFixture(),
}: {
tokens?: unknown;
tools?: unknown;
activity?: unknown;
signals?: unknown;
live?: unknown;
} = {}) {
apiMock.mockImplementation((path: string) => {
if (path.startsWith("/command-center/tokens")) return Promise.resolve(tokens);
@@ -131,12 +145,15 @@ function mockOverviewApi({
if (path.startsWith("/command-center/signals")) {
return signals instanceof Error ? Promise.reject(signals) : Promise.resolve(signals);
}
if (path === "/command-center/live") {
return live instanceof Error ? Promise.reject(live) : Promise.resolve(live);
}
return Promise.reject(new Error(`Unhandled api path: ${path}`));
});
}
function mockEmptyOverviewApi() {
mockOverviewApi({ tokens: tokenFixture(0), tools: toolsFixture(0), activity: emptyActivityFixture(), signals: signalsFixture(0) });
mockOverviewApi({ tokens: tokenFixture(0), tools: toolsFixture(0), activity: emptyActivityFixture(), signals: signalsFixture(0), live: liveFixture([{ column: "in-progress", count: 0 }]) });
}
function statValue(testId: string) {
@@ -145,6 +162,10 @@ function statValue(testId: string) {
).textContent;
}
function liveMetricValue(testId = "command-center-live-tasks-in-progress") {
return screen.getByTestId(testId).querySelector(".cc-live-metric-value")?.textContent ?? null;
}
beforeEach(() => {
apiMock.mockReset();
mockEmptyOverviewApi();
@@ -184,7 +205,7 @@ describe("CommandCenter shell", () => {
expect(statValue("command-center-stat-signals")).toBe("2");
expect(screen.getByTestId("command-center-live-strip")).toBeTruthy();
expect(screen.getByTestId("command-center-live-snapshot")).toBeTruthy();
expect(screen.getByTestId("command-center-live-tasks-in-progress").textContent).toContain("3");
await waitFor(() => expect(liveMetricValue()).toBe("3"));
expect(screen.getByTestId("command-center-live-agents-working").textContent).toContain("2");
expect(screen.getByTestId("command-center-live-open-signals").textContent).toContain("2");
expect(screen.getByTestId("command-center-throughput-trend")).toBeTruthy();
@@ -198,6 +219,77 @@ describe("CommandCenter shell", () => {
expect(screen.getByRole("img", { name: "Daily activity trend" })).toBeTruthy();
});
it("sources live tasks in progress from current column counts instead of funnel entered", async () => {
mockOverviewApi({
activity: activityFixture({ inProgress: 12 }),
live: liveFixture([{ column: "in-progress", count: 2 }]),
});
render(<CommandCenter />);
await screen.findByTestId("command-center-live-tasks-in-progress");
await waitFor(() => expect(liveMetricValue()).toBe("2"));
expect(liveMetricValue()).not.toBe("12");
});
it("renders zero when the live in-progress column count is zero", async () => {
mockOverviewApi({ live: liveFixture([{ column: "in-progress", count: 0 }]) });
render(<CommandCenter />);
await screen.findByTestId("command-center-live-tasks-in-progress");
await waitFor(() => expect(liveMetricValue()).toBe("0"));
});
it("defaults to zero when the live snapshot omits the in-progress column", async () => {
mockOverviewApi({ live: liveFixture([{ column: "todo", count: 5 }]) });
render(<CommandCenter />);
await screen.findByTestId("command-center-live-tasks-in-progress");
await waitFor(() => expect(liveMetricValue()).toBe("0"));
});
it("renders a deterministic placeholder while the live snapshot is pending", async () => {
const live = new Promise(() => undefined);
mockOverviewApi({ live });
render(<CommandCenter />);
await screen.findByTestId("command-center-live-tasks-in-progress");
expect(liveMetricValue()).toBe("—");
});
it("falls back without crashing when the live snapshot fetch fails", async () => {
mockOverviewApi({ live: new Error("live failed") });
render(<CommandCenter />);
await screen.findByTestId("command-center-live-tasks-in-progress");
await waitFor(() => expect(liveMetricValue()).toBe("0"));
expect(screen.queryByTestId("command-center-overview-error")).toBeNull();
});
it("keeps the live in-progress count range-independent while tasks done follows the range", async () => {
apiMock.mockImplementation((path: string) => {
const allTime = typeof path === "string" && !path.includes("from=");
if (path.startsWith("/command-center/tokens")) return Promise.resolve(tokenFixture());
if (path.startsWith("/command-center/tools")) return Promise.resolve(toolsFixture());
if (path.startsWith("/command-center/activity")) {
return Promise.resolve(activityFixture({ doneInRange: allTime ? 21 : 7, inProgress: allTime ? 99 : 12 }));
}
if (path.startsWith("/command-center/signals")) return Promise.resolve(signalsFixture(2));
if (path === "/command-center/live") return Promise.resolve(liveFixture([{ column: "in-progress", count: 4 }]));
return Promise.reject(new Error(`Unhandled api path: ${path}`));
});
render(<CommandCenter />);
await screen.findByTestId("command-center-stat-tasksDone");
await waitFor(() => expect(liveMetricValue()).toBe("4"));
expect(statValue("command-center-stat-tasksDone")).toBe("7");
fireEvent.click(screen.getByTestId("cc-date-range-trigger"));
fireEvent.click(screen.getByTestId("cc-date-range-preset-all"));
await waitFor(() => expect(statValue("command-center-stat-tasksDone")).toBe("21"));
expect(liveMetricValue()).toBe("4");
});
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 />);
@@ -273,6 +365,7 @@ describe("CommandCenter shell", () => {
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));
if (path === "/command-center/live") return Promise.resolve(liveFixture([{ column: "in-progress", count: 0 }]));
return Promise.reject(new Error(`Unhandled api path: ${path}`));
});
render(<CommandCenter />);
@@ -291,6 +384,7 @@ describe("CommandCenter shell", () => {
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));
if (path === "/command-center/live") return Promise.resolve(liveFixture([{ column: "in-progress", count: populated ? 3 : 0 }]));
return Promise.reject(new Error(`Unhandled api path: ${path}`));
});
render(<CommandCenter />);