diff --git a/.changeset/fn-7019-command-center-range.md b/.changeset/fn-7019-command-center-range.md
new file mode 100644
index 0000000000..2d7250a7b8
--- /dev/null
+++ b/.changeset/fn-7019-command-center-range.md
@@ -0,0 +1,7 @@
+---
+"@runfusion/fusion": patch
+---
+
+summary: Command Center date-range presets now correctly filter charts.
+category: fix
+dev: Honors open-ended Command Center analytics bounds and serializes All time explicitly.
diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md
index 1946c032c6..1cbeee9cc1 100644
--- a/docs/dashboard-guide.md
+++ b/docs/dashboard-guide.md
@@ -827,7 +827,8 @@ Navigation:
- Deep link: `?view=command-center`
Features:
-- Global date-range picker in the header scopes the analytics tabs; **Mission Control** remains live rather than historical.
+- Global date-range picker in the header scopes the analytics tabs; **Last 24h**, **Last 7 days**, **Last 30 days**, **All time**, and custom/open-ended ranges each request their selected analytics window. **Mission Control** remains live rather than historical.
+
- **Overview controls dashboard** sits at the top of the Overview landing surface on desktop and mobile. It includes AI engine stop/start backed by `globalPause`, live scheduler status from executor stats, range sliders for `maxConcurrent`, `maxTriageConcurrent`, and `maxWorktrees` that persist through `/api/settings`, and a compact theme dropdown with the same color-chip swatches and Shadcn variant list as Settings → Appearance. These controls reuse existing APIs and App-level theme setters; they do not add a new backend route or second theme owner.
- **Overview** summarizes token usage/cost, autonomy, active nodes, sessions, agent runs, tasks done, model breadth, and real open signals, and includes the SDLC throughput funnel for the selected range at the bottom of the Overview content in loading, error, empty, and populated states. Its token total and Live activity snapshot token metric refresh on a bounded live cadence and animate number changes while preserving reduced-motion preferences. The sessions card uses the selected-range `ActivityAnalytics.sessions` value already loaded for the overview. The Live activity snapshot also shows the current board-state count for tasks in progress, independent of the selected analytics date range. Overview includes a graph-rich software-factory snapshot with the existing tokens-by-model bar, tool-category bar, real recharts token-share pie, and the daily activity multi-series line chart placed before the daily activity sparkline/trend so the richer line graph sits higher in the chart grid. These reuse the already-loaded tokens, tools, activity, and signals analytics; the signals count comes from `/api/command-center/signals` and renders unavailable (`—`) while the incidents-backed response is loading or unavailable. 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.
diff --git a/packages/dashboard/app/components/command-center/DateRangePicker.tsx b/packages/dashboard/app/components/command-center/DateRangePicker.tsx
index 126e15528a..f0c4f001b6 100644
--- a/packages/dashboard/app/components/command-center/DateRangePicker.tsx
+++ b/packages/dashboard/app/components/command-center/DateRangePicker.tsx
@@ -4,9 +4,9 @@ import { Calendar } from "lucide-react";
import "./DateRangePicker.css";
export interface DateRange {
- /** ISO date string (YYYY-MM-DD) or null for an open lower bound. */
+ /** ISO date string/timestamp or null for an open lower bound. */
from: string | null;
- /** ISO date string (YYYY-MM-DD) or null for an open upper bound (now). */
+ /** ISO date string/timestamp or null for an open upper bound (now). */
to: string | null;
/** Identifier for the active preset, or "custom". */
preset: string;
@@ -35,8 +35,12 @@ export function defaultPresets(t: (key: string, fallback: string) => string): Da
}
export function rangeFromPreset(preset: DateRangePreset): DateRange {
+ /*
+ FNXC:CommandCenter 2026-06-25-00:00:
+ FN-7019 requires picker presets to serialize windows the server can distinguish. Bounded presets keep an open upper bound (`to: null`) so the server resolves `[from, now]`; All time must carry the selection timestamp as an explicit upper bound so it resolves `[epoch, selected-now]` instead of collapsing into the no-param default window.
+ */
if (preset.days === null) {
- return { from: null, to: null, preset: preset.id };
+ return { from: null, to: new Date(Date.now()).toISOString(), preset: preset.id };
}
const from = new Date(Date.now() - preset.days * 86_400_000);
return { from: from.toISOString().slice(0, 10), to: null, preset: preset.id };
@@ -151,7 +155,7 @@ export function DateRangePicker({ value, onChange, presets }: DateRangePickerPro
{t("commandCenter.range.to", "To")}
applyCustom(value.from, e.target.value || null)}
data-testid="cc-date-range-to"
/>
diff --git a/packages/dashboard/app/components/command-center/areas/__tests__/areas.test.tsx b/packages/dashboard/app/components/command-center/areas/__tests__/areas.test.tsx
index ab05135af8..a4328fcf03 100644
--- a/packages/dashboard/app/components/command-center/areas/__tests__/areas.test.tsx
+++ b/packages/dashboard/app/components/command-center/areas/__tests__/areas.test.tsx
@@ -57,7 +57,8 @@ import { ActivityArea } from "../ActivityArea";
import { EcosystemArea } from "../EcosystemArea";
import { useAnalyticsArea } from "../useAnalyticsArea";
import { ConfirmDialogProvider } from "../../../../hooks/useConfirm";
-import type { DateRange } from "../DateRangePicker";
+import { rangeQuery } from "../areaShared";
+import { defaultPresets, rangeFromPreset, type DateRange } from "../../DateRangePicker";
const range7d: DateRange = { from: "2026-06-08", to: null, preset: "7d" };
const customRange = (from: string, to: string): DateRange => ({ from, to, preset: "custom" });
@@ -363,6 +364,28 @@ function expectSvgLineFillsBoxAndKeepsRoundMarkers(testId: string, label: string
expect(pointPairs.at(-1)?.[0]).toBe(viewBoxWidth - 3);
}
+describe("rangeQuery / rangeFromPreset", () => {
+ it("serializes every default preset into a distinct server-resolvable query", () => {
+ vi.useFakeTimers({ now: new Date("2026-06-15T12:00:00.000Z") });
+ const presets = defaultPresets((_key, fallback) => fallback);
+ const queries = Object.fromEntries(presets.map((preset) => [preset.id, rangeQuery(rangeFromPreset(preset))]));
+
+ expect(queries).toEqual({
+ "24h": "?from=2026-06-14",
+ "7d": "?from=2026-06-08",
+ "30d": "?from=2026-05-16",
+ all: "?to=2026-06-15T12%3A00%3A00.000Z",
+ });
+ expect(new Set(Object.values(queries)).size).toBe(presets.length);
+ });
+
+ it("preserves custom and open-ended custom ranges without collapsing them", () => {
+ expect(rangeQuery(customRange("2026-06-01", "2026-06-10"))).toBe("?from=2026-06-01&to=2026-06-10");
+ expect(rangeQuery({ from: "2026-06-01", to: null, preset: "custom" })).toBe("?from=2026-06-01");
+ expect(rangeQuery({ from: null, to: "2026-06-10", preset: "custom" })).toBe("?to=2026-06-10");
+ });
+});
+
describe("useAnalyticsArea", () => {
it("polls only when pollMs is provided and clears the interval on unmount", async () => {
vi.useFakeTimers();
@@ -409,6 +432,35 @@ describe("useAnalyticsArea", () => {
expect(apiMock).toHaveBeenCalledTimes(1);
});
+ it("refetches with distinct request keys for each default preset", async () => {
+ vi.useFakeTimers({ now: new Date("2026-06-15T12:00:00.000Z") });
+ apiMock.mockResolvedValue({ ok: true });
+ const presets = defaultPresets((_key, fallback) => fallback);
+ const ranges = presets.map(rangeFromPreset);
+
+ const { rerender } = renderHook(
+ ({ range }) => useAnalyticsArea<{ ok: boolean }>("/command-center/tokens", range),
+ { initialProps: { range: ranges[0] as DateRange } },
+ );
+
+ await act(async () => {
+ await Promise.resolve();
+ });
+ for (const range of ranges.slice(1)) {
+ rerender({ range });
+ await act(async () => {
+ await Promise.resolve();
+ });
+ }
+
+ expect(apiMock.mock.calls.map(([path]) => path)).toEqual([
+ "/command-center/tokens?from=2026-06-14",
+ "/command-center/tokens?from=2026-06-08",
+ "/command-center/tokens?from=2026-05-16",
+ "/command-center/tokens?to=2026-06-15T12%3A00%3A00.000Z",
+ ]);
+ });
+
it("does not fetch or schedule polling for inverted custom ranges", async () => {
vi.useFakeTimers();
diff --git a/packages/dashboard/app/components/command-center/areas/areaShared.ts b/packages/dashboard/app/components/command-center/areas/areaShared.ts
index e2c2bd214b..e8f0cc309b 100644
--- a/packages/dashboard/app/components/command-center/areas/areaShared.ts
+++ b/packages/dashboard/app/components/command-center/areas/areaShared.ts
@@ -3,12 +3,16 @@ import type { DateRange } from "../DateRangePicker";
/*
FNXC:CommandCenter 2026-06-16-09:42:
Shared Command Center area helpers (PR #1683): date-range query building and count formatting reused across the analytics areas so range-to-query and unavailable-vs-zero rendering stay consistent.
+
+FNXC:CommandCenter 2026-06-25-00:00:
+FN-7019 defines null date bounds as open analytics windows, not as a request to default. `rangeQuery` preserves every non-null bound so bounded presets refetch with `from=...` and All time refetches with the picker's explicit `to=selected-now` upper bound.
*/
/**
* Build the `?from=&to=` query string for an analytics endpoint from a
- * {@link DateRange}. Open bounds (null) are omitted so the server applies its
- * documented default window. The picker already rejects `from > to`
+ * {@link DateRange}. Open bounds (null) are omitted because the server resolves
+ * one-sided requests as open windows; a range with no usable bounds remains the
+ * documented programmatic default. The picker already rejects `from > to`
* client-side, but we guard here too so a programmatic caller cannot send an
* inverted range.
*/
diff --git a/packages/dashboard/src/__tests__/register-command-center-routes.test.ts b/packages/dashboard/src/__tests__/register-command-center-routes.test.ts
index 0494217358..d61b9c7414 100644
--- a/packages/dashboard/src/__tests__/register-command-center-routes.test.ts
+++ b/packages/dashboard/src/__tests__/register-command-center-routes.test.ts
@@ -253,6 +253,7 @@ describe("register-command-center-routes", () => {
});
afterEach(() => {
+ vi.useRealTimers();
vi.restoreAllMocks();
mockInvalidateAllGlobalSettingsCaches.mockClear();
dbA.close();
@@ -524,6 +525,61 @@ describe("register-command-center-routes", () => {
expect(signals.body).toHaveProperty("bySeverity");
});
+ it("honors picker-shaped from-only ranges for tokens, activity, and productivity", async () => {
+ vi.useFakeTimers({ now: new Date("2026-04-01T00:00:00.000Z") });
+ seedAgentRun(dbA, { id: "run-open-bound", agentId: "agent-open", startedAt: "2026-03-02T00:00:00.000Z", status: "completed" });
+ seedCompletedTaskDuration(dbA, { id: "FN-open-duration", cumulativeActiveMs: 45_000, completedAt: "2026-03-03T00:00:00.000Z" });
+
+ const pickerRange = "from=2026-02-01T00%3A00%3A00.000Z";
+ const expectedTo = "2026-04-01T00:00:00.000Z";
+ const tokens = await request(app, "GET", `/api/command-center/tokens?${pickerRange}&projectId=proj-a`);
+ const activity = await request(app, "GET", `/api/command-center/activity?${pickerRange}&projectId=proj-a`);
+ const productivity = await request(app, "GET", `/api/command-center/productivity?${pickerRange}&projectId=proj-a`);
+
+ expect(tokens.status).toBe(200);
+ expect(tokens.body).toMatchObject({ from: "2026-02-01T00:00:00.000Z", to: expectedTo });
+ expect((tokens.body as { totals: { totalTokens: number } }).totals.totalTokens).toBe(200);
+ expect(activity.body).toMatchObject({ from: "2026-02-01T00:00:00.000Z", to: expectedTo });
+ expect((activity.body as { agentRuns: { total: number } }).agentRuns.total).toBe(1);
+ expect(productivity.body).toMatchObject({ from: "2026-02-01T00:00:00.000Z", to: expectedTo });
+ expect((productivity.body as { taskDuration: { completedTasks: number } }).taskDuration.completedTasks).toBe(1);
+
+ const defaultTokens = await request(app, "GET", "/api/command-center/tokens?projectId=proj-a");
+ const defaultActivity = await request(app, "GET", "/api/command-center/activity?projectId=proj-a");
+ const defaultProductivity = await request(app, "GET", "/api/command-center/productivity?projectId=proj-a");
+ expect(defaultTokens.body).toMatchObject({ from: "2026-03-25T00:00:00.000Z", to: expectedTo });
+ expect((defaultTokens.body as { totals: { totalTokens: number } }).totals.totalTokens).toBe(0);
+ expect((defaultActivity.body as { agentRuns: { total: number } }).agentRuns.total).toBe(0);
+ expect((defaultProductivity.body as { taskDuration: { completedTasks: number } }).taskDuration.completedTasks).toBe(0);
+ });
+
+ it("applies from-only resolved bounds on every range-consuming analytics endpoint", async () => {
+ vi.useFakeTimers({ now: new Date("2026-04-01T00:00:00.000Z") });
+ const endpoints = [
+ "tokens",
+ "tools",
+ "activity",
+ "productivity",
+ "team",
+ "github",
+ "signals",
+ "plugin-activations",
+ ];
+
+ for (const endpoint of endpoints) {
+ const res = await request(
+ app,
+ "GET",
+ `/api/command-center/${endpoint}?from=2026-02-01T00%3A00%3A00.000Z&projectId=proj-a`,
+ );
+ expect(res.status, endpoint).toBe(200);
+ expect(res.body, endpoint).toMatchObject({
+ from: "2026-02-01T00:00:00.000Z",
+ to: "2026-04-01T00:00:00.000Z",
+ });
+ }
+ });
+
it("runs the productivity LOC backfill route as a dry-run by default and respects writes", async () => {
const backfill = vi.fn(async (options?: { dryRun?: boolean }) => ({
scannedRows: 3,
@@ -889,9 +945,41 @@ describe("resolveRange / resolveGroupBy / resolveTokenGranularity (param parsing
expect(r.defaulted).toBe(true);
});
- it("defaults when a bound is unparseable", () => {
- const r = resolveRange({ from: "garbage", to: "2026-06-10T00:00:00.000Z" }, NOW);
+ it("honors a from-only bound as the symptom regression anchor", () => {
+ const r = resolveRange({ from: "2026-06-01T00:00:00.000Z" }, NOW);
+ expect(r.defaulted).toBe(false);
+ expect(r.from).toBe("2026-06-01T00:00:00.000Z");
+ expect(r.to).toBe(new Date(NOW).toISOString());
+ });
+
+ it("honors a to-only bound as an all-history window through that date", () => {
+ const r = resolveRange({ to: "2026-06-10T00:00:00.000Z" }, NOW);
+ expect(r.defaulted).toBe(false);
+ expect(r.from).toBe(new Date(0).toISOString());
+ expect(r.to).toBe("2026-06-10T00:00:00.000Z");
+ });
+
+ it("uses the remaining valid bound when the other bound is unparseable", () => {
+ const toOnly = resolveRange({ from: "garbage", to: "2026-06-10T00:00:00.000Z" }, NOW);
+ expect(toOnly).toEqual({
+ from: new Date(0).toISOString(),
+ to: "2026-06-10T00:00:00.000Z",
+ defaulted: false,
+ });
+
+ const fromOnly = resolveRange({ from: "2026-06-01T00:00:00.000Z", to: "garbage" }, NOW);
+ expect(fromOnly).toEqual({
+ from: "2026-06-01T00:00:00.000Z",
+ to: new Date(NOW).toISOString(),
+ defaulted: false,
+ });
+ });
+
+ it("defaults only when neither bound is usable", () => {
+ const r = resolveRange({ from: "garbage", to: "also-bad" }, NOW);
expect(r.defaulted).toBe(true);
+ expect(r.to).toBe(new Date(NOW).toISOString());
+ expect(r.from).toBe(new Date(NOW - DEFAULT_WINDOW_DAYS * 24 * 60 * 60 * 1000).toISOString());
});
it("accepts known groupBy values and ignores unknown ones", () => {
diff --git a/packages/dashboard/src/routes/register-command-center-routes.ts b/packages/dashboard/src/routes/register-command-center-routes.ts
index 52c2ee711e..cc5370de86 100644
--- a/packages/dashboard/src/routes/register-command-center-routes.ts
+++ b/packages/dashboard/src/routes/register-command-center-routes.ts
@@ -86,9 +86,13 @@ function isValidIso(value: string): boolean {
/**
* Resolve `from`/`to` query params into an always-valid ISO range.
*
- * Both bounds must be present, parseable, and ordered (`from <= to`); otherwise
- * the documented default window (last {@link DEFAULT_WINDOW_DAYS} days ending
- * now) is used and `defaulted` is true. `now` is injectable for tests.
+ * FNXC:CommandCenter 2026-06-25-00:00:
+ * FN-7019 fixes the picker/server contract: the date picker omits null bounds,
+ * so a from-only request means `[from, now]` and a to-only request means
+ * `[epoch, to]`. Only a truly empty/invalid range or an ordered-range violation
+ * may fall back to the documented default window; otherwise presets collapse to
+ * last-7-days and Command Center charts do not change when operators select a
+ * different range. `now` is injectable for tests.
*/
export function resolveRange(
query: Request["query"],
@@ -96,15 +100,17 @@ export function resolveRange(
): ResolvedRange {
const rawFrom = typeof query.from === "string" ? query.from : undefined;
const rawTo = typeof query.to === "string" ? query.to : undefined;
+ const fromMs = rawFrom !== undefined && isValidIso(rawFrom) ? Date.parse(rawFrom) : undefined;
+ const toMs = rawTo !== undefined && isValidIso(rawTo) ? Date.parse(rawTo) : undefined;
- if (
- rawFrom !== undefined &&
- rawTo !== undefined &&
- isValidIso(rawFrom) &&
- isValidIso(rawTo) &&
- Date.parse(rawFrom) <= Date.parse(rawTo)
- ) {
- return { from: rawFrom, to: rawTo, defaulted: false };
+ if (fromMs !== undefined && toMs !== undefined) {
+ if (fromMs <= toMs) {
+ return { from: rawFrom as string, to: rawTo as string, defaulted: false };
+ }
+ } else if (fromMs !== undefined) {
+ return { from: rawFrom as string, to: new Date(now).toISOString(), defaulted: false };
+ } else if (toMs !== undefined) {
+ return { from: new Date(0).toISOString(), to: rawTo as string, defaulted: false };
}
const to = new Date(now).toISOString();