fix(dashboard): project-scope Command Center and Reliability analytics#1971
Conversation
Apply FUX-037 projectId scoping to Command Center and Reliability view.
📝 WalkthroughWalkthroughThis PR threads an optional ChangesProject-scoped Command Center analytics
Estimated code review effort: 3 (Moderate) | ~30 minutes Sequence Diagram(s)sequenceDiagram
participant CommandCenter
participant AreaComponent
participant useAnalyticsArea
participant withProjectId
participant api
CommandCenter->>AreaComponent: render with projectId
AreaComponent->>useAnalyticsArea: fetch(path, range, {projectId})
useAnalyticsArea->>withProjectId: scope URL with projectId
withProjectId-->>useAnalyticsArea: scoped URL
useAnalyticsArea->>api: request(scoped URL)
api-->>useAnalyticsArea: analytics data
useAnalyticsArea-->>AreaComponent: data, loading, error
sequenceDiagram
participant ReliabilityView
participant api
participant HealthEndpoint
ReliabilityView->>api: load() /health/reliability?projectId
api->>HealthEndpoint: GET scoped path
HealthEndpoint-->>api: reliability data
api-->>ReliabilityView: update state
ReliabilityView->>api: resetStats() POST /health/reliability/reset?projectId
api->>HealthEndpoint: POST scoped reset
HealthEndpoint-->>api: reset ack
api-->>ReliabilityView: refresh via load()
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Warning Tools execution failed with the following error: Failed to run tools: 13 INTERNAL: Received RST_STREAM with code 2 (Internal server error) Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Greptile SummaryThis PR fixes FUX-037, where Command Center and Reliability analytics were fetching data without a
Confidence Score: 4/5The scoping logic is correct and fully covered by new tests; the missing changeset is the only blocker before this can be considered release-ready. The core fix is sound: A Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[CommandCenter\nprojectId prop] --> B[OverviewTab\nprojectId]
A --> C[Area Components\nTokens / Tools / Activity / Signals\nProductivity / Ecosystem / Github / Gitlab\nWorkflow / Team]
A --> D[SdlcFunnel\nprojectId]
A --> E[MissionControlPanel\nprojectId]
A --> F[ReliabilityView\nprojectId]
B --> G[useAnalyticsArea\noptions.projectId]
B --> H[api live-snapshot\nwithProjectId]
C --> G
D --> G
E --> I[useLiveSnapshot\nprojectId]
I --> J[api live-snapshot\nwithProjectId]
F --> K[api load\nwithProjectId]
F --> L[api reset\nwithProjectId]
G --> M[withProjectId\nwithRangeQuery\nendpoint + query + projectId]
M --> N[api\nGET /command-center/...]
style A fill:#4A90D9,color:#fff
style G fill:#5CB85C,color:#fff
style M fill:#F0AD4E,color:#fff
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
A[CommandCenter\nprojectId prop] --> B[OverviewTab\nprojectId]
A --> C[Area Components\nTokens / Tools / Activity / Signals\nProductivity / Ecosystem / Github / Gitlab\nWorkflow / Team]
A --> D[SdlcFunnel\nprojectId]
A --> E[MissionControlPanel\nprojectId]
A --> F[ReliabilityView\nprojectId]
B --> G[useAnalyticsArea\noptions.projectId]
B --> H[api live-snapshot\nwithProjectId]
C --> G
D --> G
E --> I[useLiveSnapshot\nprojectId]
I --> J[api live-snapshot\nwithProjectId]
F --> K[api load\nwithProjectId]
F --> L[api reset\nwithProjectId]
G --> M[withProjectId\nwithRangeQuery\nendpoint + query + projectId]
M --> N[api\nGET /command-center/...]
style A fill:#4A90D9,color:#fff
style G fill:#5CB85C,color:#fff
style M fill:#F0AD4E,color:#fff
|
| export interface AnalyticsAreaOptions { | ||
| /** Opt-in bounded polling interval in milliseconds; omitted means no polling. */ | ||
| pollMs?: number; | ||
| /** Currently-selected project id; when supplied, scopes the request via `projectId` query param. */ | ||
| projectId?: string; | ||
| } |
There was a problem hiding this comment.
Missing FNXC comment for the projectId scoping change
The AGENTS.md rule requires a date-stamped FNXC comment whenever a requirement-driven change is made to the codebase. The addition of projectId to AnalyticsAreaOptions is the central behavioral change for FUX-037, but it has no FNXC:CommandCenter entry describing the requirement. The existing comment in the file (line 39) covers a different feature. This file is the most important place to record why projectId was added, since every area component delegates to this hook. The same gap exists in CommandCenter.tsx, MissionControlPanel.tsx, ReliabilityView.tsx, and SdlcFunnel.tsx — none of them have a new FNXC entry describing the FUX-037 project-scoping requirement.
Context Used: AGENTS.md (source)
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| @@ -42,7 +43,7 @@ function formatDateTime(value: string | null | undefined): string { | |||
| return Number.isNaN(parsed.getTime()) ? value : parsed.toLocaleString(); | |||
There was a problem hiding this comment.
Missing changeset for a user-visible bug fix
AGENTS.md requires a changeset file whenever a change affects the published @runfusion/fusion package. @fusion/dashboard is bundled into @runfusion/fusion, and this PR is a behavioral bug fix (FUX-037: multi-project dashboard analytics were blending data across projects). No .changeset/<name>.md file exists in this PR — only the placeholder README.md is present. Without a changeset, this fix will not appear in operator release notes and the pnpm check:changesets gate may flag the PR. A "@runfusion/fusion": patch changeset with category: fix is needed.
Context Used: AGENTS.md (source)
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/dashboard/app/components/command-center/MissionControlPanel.tsx (1)
93-94: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy liftRace condition:
inFlightRefblocks the new fetch whenprojectIdchanges mid-flightWhen
projectIdchanges while a snapshot fetch is in-flight, theuseEffectcleanup/re-run cycle setsmountedRef.currentback totruebefore the old fetch resolves. The newload()call seesinFlightRef.current === trueand returns early. The old fetch then completes and sets snapshot data from the wrong project, with no immediate refetch scheduled. This can persist until the next SSE event or poll tick (5s for active sessions, potentially indefinite for idle state).The comment on lines 93-94 is also now stale —
loadcloses overprojectIdand IS recreated when it changes.🔒 Suggested fix: generation counter to ignore stale fetches
export function useLiveSnapshot(projectId?: string): LiveSnapshotState { const [snapshot, setSnapshot] = useState<LiveSnapshot | null>(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState<string | null>(null); const [polling, setPolling] = useState(false); const snapshotRef = useRef<LiveSnapshot | null>(null); const pollTimerRef = useRef<ReturnType<typeof setInterval> | null>(null); const inFlightRef = useRef(false); const mountedRef = useRef(true); + const loadGenRef = useRef(0); - // Stable callbacks below close over only refs + setState, so `load` (and the - // SSE subscription / poll interval that call it) never need to be recreated. + // `load` closes over `projectId` and is recreated when it changes. + // A generation counter guards against stale fetches from a previous project. const load = useCallback(async () => { if (inFlightRef.current) return; inFlightRef.current = true; + const gen = loadGenRef.current; try { const result = await api<LiveSnapshot>(withProjectId("/command-center/live", projectId)); + if (gen !== loadGenRef.current) return; if (!mountedRef.current) return; snapshotRef.current = result; setSnapshot(result); setError(null); } catch (loadError: unknown) { + if (gen !== loadGenRef.current) return; if (!mountedRef.current) return; setError(loadError instanceof Error ? loadError.message : "Failed to load live snapshot"); } finally { - inFlightRef.current = false; - if (mountedRef.current) { + if (gen === loadGenRef.current) { + inFlightRef.current = false; + } + if (gen === loadGenRef.current && mountedRef.current) { setIsLoading(false); const snap = snapshotRef.current; const inFlight = !!snap && (snap.activeSessions > 0 || snap.activeRuns > 0); if (inFlight) { if (pollTimerRef.current === null) { pollTimerRef.current = setInterval(() => { void load(); }, LIVE_POLL_INTERVAL_MS); setPolling(true); } } else { stopPolling(); } } } }, [stopPolling, projectId]); useEffect(() => { mountedRef.current = true; + loadGenRef.current++; + inFlightRef.current = false; void load(); const unsubscribe = subscribeSse("/api/events", {Also applies to: 104-138
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/dashboard/app/components/command-center/MissionControlPanel.tsx` around lines 93 - 94, `MissionControlPanel` has a stale-request race: when `projectId` changes during an in-flight `load`, the new effect reruns while `inFlightRef.current` is still true, so the next fetch is skipped and the old response can win. Update `load`/the related `useEffect` flow to use a generation/request token (or equivalent stale-response guard) so only the latest `projectId` fetch may commit state, and ensure the old response is ignored and a fresh load is triggered for the new project. Also refresh the stale callback comment near `load` to reflect that it now closes over `projectId` and is recreated on changes.
🧹 Nitpick comments (2)
packages/dashboard/app/components/command-center/areas/__tests__/areas.test.tsx (1)
1605-1620: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winCross-project leakage test should verify rendered content, not just the API call.
The test asserts
proj-arenders fixtureA's data (toContain("1")) but after rerendering withproj-b, it only checks the API was called withprojectId=proj-b— it never verifies the displayed content switched to fixtureB. A stale-state or caching bug that keepsproj-a's data visible would pass this test.As per coding guidelines, regression tests for bug fixes must include symptom verification, not only mechanism verification.
🧪 Suggested fix: add content assertion for proj-b
rerender(<TokensArea range={range7d} projectId="proj-b" />); - await waitFor(() => expect(apiMock.mock.calls.some(([path]) => typeof path === "string" && path.includes("projectId=proj-b"))).toBe(true)); + await waitFor(() => { + expect(apiMock.mock.calls.some(([path]) => typeof path === "string" && path.includes("projectId=proj-b"))).toBe(true); + expect(screen.getByTestId("cc-area-tokens").textContent).toContain("9"); + }); });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/dashboard/app/components/command-center/areas/__tests__/areas.test.tsx` around lines 1605 - 1620, The cross-project leakage test in TokensArea only verifies that the proj-b API request happens after rerender, but it does not confirm the UI updates to the new fixture. Update the test to assert the rendered cc-area-tokens content after rerendering with projectId="proj-b" reflects fixtureB, using the existing TokensArea render/rerender flow and the fixtureA/fixtureB setup so the regression test checks displayed state, not just apiMock activity.Source: Coding guidelines
packages/dashboard/app/components/command-center/__tests__/MissionControlPanel.test.tsx (1)
101-113: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd a
rerendertest forprojectIdchanges while mountedThe current test verifies the URL invariant via unmount/remount, but the real-world scenario — switching projects while
MissionControlPanelstays mounted — is not covered. Arerender-based test would also serve as a regression guard for theinFlightRefrace condition flagged inMissionControlPanel.tsx.As per coding guidelines: "When fixing a bug, regression tests must assert the general invariant across all known surfaces, and bug-fix work must include symptom verification and surface enumeration rather than only the single reproduction."
🧪 Suggested rerender test
it("refetches with new projectId when it changes via rerender", async () => { apiMock.mockResolvedValue(snapshot()); const { rerender } = render(<MissionControlPanel />); await flush(); expect(apiMock.mock.calls.at(-1)?.[0]).toBe("/command-center/live"); apiMock.mockClear(); apiMock.mockResolvedValue(snapshot()); rerender(<MissionControlPanel projectId="proj-abc" />); await flush(); expect(apiMock.mock.calls.at(-1)?.[0]).toBe("/command-center/live?projectId=proj-abc"); });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@packages/dashboard/app/components/command-center/__tests__/MissionControlPanel.test.tsx` around lines 101 - 113, The current MissionControlPanel test only covers projectId changes by unmounting and remounting, so add a rerender-based regression test in MissionControlPanel.test.tsx that changes projectId while the component stays mounted. Use render, rerender, and flush to verify the live snapshot request updates from /command-center/live to /command-center/live?projectId=proj-abc after the prop change, which also guards the inFlightRef race condition in MissionControlPanel.Source: Coding guidelines
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@packages/dashboard/app/components/command-center/areas/__tests__/TokensArea.test.tsx`:
- Around line 285-293: The TokensArea projectId scoping test only checks the
supplied-projectId path and does not verify omission when the prop is absent.
Update the TokensArea test case to cover both behaviors using TokensArea and
apiMock: keep the existing assertion for the projectId-present render, then add
a separate render without projectId and assert the tokens request URL does not
include projectId, matching the two-case pattern used by WorkflowArea and
GithubArea.
---
Outside diff comments:
In `@packages/dashboard/app/components/command-center/MissionControlPanel.tsx`:
- Around line 93-94: `MissionControlPanel` has a stale-request race: when
`projectId` changes during an in-flight `load`, the new effect reruns while
`inFlightRef.current` is still true, so the next fetch is skipped and the old
response can win. Update `load`/the related `useEffect` flow to use a
generation/request token (or equivalent stale-response guard) so only the latest
`projectId` fetch may commit state, and ensure the old response is ignored and a
fresh load is triggered for the new project. Also refresh the stale callback
comment near `load` to reflect that it now closes over `projectId` and is
recreated on changes.
---
Nitpick comments:
In
`@packages/dashboard/app/components/command-center/__tests__/MissionControlPanel.test.tsx`:
- Around line 101-113: The current MissionControlPanel test only covers
projectId changes by unmounting and remounting, so add a rerender-based
regression test in MissionControlPanel.test.tsx that changes projectId while the
component stays mounted. Use render, rerender, and flush to verify the live
snapshot request updates from /command-center/live to
/command-center/live?projectId=proj-abc after the prop change, which also guards
the inFlightRef race condition in MissionControlPanel.
In
`@packages/dashboard/app/components/command-center/areas/__tests__/areas.test.tsx`:
- Around line 1605-1620: The cross-project leakage test in TokensArea only
verifies that the proj-b API request happens after rerender, but it does not
confirm the UI updates to the new fixture. Update the test to assert the
rendered cc-area-tokens content after rerendering with projectId="proj-b"
reflects fixtureB, using the existing TokensArea render/rerender flow and the
fixtureA/fixtureB setup so the regression test checks displayed state, not just
apiMock activity.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 20ca8673-51f6-4aa9-8af8-ea29cfcb552a
📒 Files selected for processing (27)
packages/dashboard/app/api/legacy.tspackages/dashboard/app/components/ReliabilityView.tsxpackages/dashboard/app/components/__tests__/ReliabilityView.test.tsxpackages/dashboard/app/components/command-center/CommandCenter.tsxpackages/dashboard/app/components/command-center/MissionControlPanel.tsxpackages/dashboard/app/components/command-center/SdlcFunnel.tsxpackages/dashboard/app/components/command-center/__tests__/CommandCenter.mobile-scroll.test.tsxpackages/dashboard/app/components/command-center/__tests__/CommandCenter.tablet-layout.test.tsxpackages/dashboard/app/components/command-center/__tests__/CommandCenter.test.tsxpackages/dashboard/app/components/command-center/__tests__/MissionControlPanel.test.tsxpackages/dashboard/app/components/command-center/__tests__/SdlcFunnel.test.tsxpackages/dashboard/app/components/command-center/areas/ActivityArea.tsxpackages/dashboard/app/components/command-center/areas/EcosystemArea.tsxpackages/dashboard/app/components/command-center/areas/GithubArea.tsxpackages/dashboard/app/components/command-center/areas/GitlabArea.tsxpackages/dashboard/app/components/command-center/areas/ProductivityArea.tsxpackages/dashboard/app/components/command-center/areas/SignalsArea.tsxpackages/dashboard/app/components/command-center/areas/TeamArea.tsxpackages/dashboard/app/components/command-center/areas/TokensArea.tsxpackages/dashboard/app/components/command-center/areas/ToolsArea.tsxpackages/dashboard/app/components/command-center/areas/WorkflowArea.tsxpackages/dashboard/app/components/command-center/areas/__tests__/TokensArea.test.tsxpackages/dashboard/app/components/command-center/areas/__tests__/WorkflowArea.test.tsxpackages/dashboard/app/components/command-center/areas/__tests__/areas.github-signals.test.tsxpackages/dashboard/app/components/command-center/areas/__tests__/areas.gitlab-signals.test.tsxpackages/dashboard/app/components/command-center/areas/__tests__/areas.test.tsxpackages/dashboard/app/components/command-center/areas/useAnalyticsArea.ts
| describe("FUX-037: TokensArea projectId scoping", () => { | ||
| it("appends projectId to the tokens request when supplied, and omits it when not", async () => { | ||
| render(<TokensArea range={range7d} projectId="proj-tokens" />); | ||
| await screen.findByTestId("cc-tokens-table"); | ||
| expect(apiMock.mock.calls.at(-1)?.[0]).toBe( | ||
| "/command-center/tokens?groupBy=model&granularity=day&from=2026-06-08&projectId=proj-tokens", | ||
| ); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Test claims to verify omission but only covers the "supplied" case.
The test description says "appends projectId to the tokens request when supplied, and omits it when not", but the test body only renders with projectId="proj-tokens" and never verifies that projectId is absent when the prop is omitted. A regression where projectId is always appended would pass this test undetected.
As per coding guidelines, when fixing a bug, regression tests must assert the general invariant across all known surfaces. The WorkflowArea and GithubArea tests in this PR already demonstrate the correct two-case pattern.
🧪 Proposed fix: add the omission case
it("appends projectId to the tokens request when supplied, and omits it when not", async () => {
- render(<TokensArea range={range7d} projectId="proj-tokens" />);
- await screen.findByTestId("cc-tokens-table");
- expect(apiMock.mock.calls.at(-1)?.[0]).toBe(
- "/command-center/tokens?groupBy=model&granularity=day&from=2026-06-08&projectId=proj-tokens",
- );
+ const { unmount } = render(<TokensArea range={range7d} projectId="proj-tokens" />);
+ await screen.findByTestId("cc-tokens-table");
+ expect(apiMock.mock.calls.at(-1)?.[0]).toBe(
+ "/command-center/tokens?groupBy=model&granularity=day&from=2026-06-08&projectId=proj-tokens",
+ );
+ unmount();
+
+ apiMock.mockClear();
+ render(<TokensArea range={range7d} />);
+ await screen.findByTestId("cc-tokens-table");
+ expect(apiMock.mock.calls.at(-1)?.[0]).toBe(
+ "/command-center/tokens?groupBy=model&granularity=day&from=2026-06-08",
+ );
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| describe("FUX-037: TokensArea projectId scoping", () => { | |
| it("appends projectId to the tokens request when supplied, and omits it when not", async () => { | |
| render(<TokensArea range={range7d} projectId="proj-tokens" />); | |
| await screen.findByTestId("cc-tokens-table"); | |
| expect(apiMock.mock.calls.at(-1)?.[0]).toBe( | |
| "/command-center/tokens?groupBy=model&granularity=day&from=2026-06-08&projectId=proj-tokens", | |
| ); | |
| }); | |
| }); | |
| describe("FUX-037: TokensArea projectId scoping", () => { | |
| it("appends projectId to the tokens request when supplied, and omits it when not", async () => { | |
| const { unmount } = render(<TokensArea range={range7d} projectId="proj-tokens" />); | |
| await screen.findByTestId("cc-tokens-table"); | |
| expect(apiMock.mock.calls.at(-1)?.[0]).toBe( | |
| "/command-center/tokens?groupBy=model&granularity=day&from=2026-06-08&projectId=proj-tokens", | |
| ); | |
| unmount(); | |
| apiMock.mockClear(); | |
| render(<TokensArea range={range7d} />); | |
| await screen.findByTestId("cc-tokens-table"); | |
| expect(apiMock.mock.calls.at(-1)?.[0]).toBe( | |
| "/command-center/tokens?groupBy=model&granularity=day&from=2026-06-08", | |
| ); | |
| }); | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@packages/dashboard/app/components/command-center/areas/__tests__/TokensArea.test.tsx`
around lines 285 - 293, The TokensArea projectId scoping test only checks the
supplied-projectId path and does not verify omission when the prop is absent.
Update the TokensArea test case to cover both behaviors using TokensArea and
apiMock: keep the existing assertion for the projectId-present render, then add
a separate render without projectId and assert the tokens request URL does not
include projectId, matching the two-case pattern used by WorkflowArea and
GithubArea.
Source: Coding guidelines
Problem
Command Center and Reliability analytics were not scoped by projectId, causing multi-project dashboards to blend data across projects.
Change
Pipe projectId through Command Center, Mission Control, and the Reliability view, and use the scoped store resolver so analytics reflect the currently selected project.
Scope
Notes
This is the FUX-037 fix. v0.57.0 introduced scoped-store helpers elsewhere but did not project-scope the dashboard analytics surfaces.
Summary by CodeRabbit
New Features
Bug Fixes