Skip to content

fix(dashboard): scope reliability endpoints by projectId#1970

Open
Automata-intelligentsia wants to merge 1 commit into
Runfusion:mainfrom
Automata-intelligentsia:fux-042-reliability-project-scoping
Open

fix(dashboard): scope reliability endpoints by projectId#1970
Automata-intelligentsia wants to merge 1 commit into
Runfusion:mainfrom
Automata-intelligentsia:fux-042-reliability-project-scoping

Conversation

@Automata-intelligentsia

@Automata-intelligentsia Automata-intelligentsia commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Problem

Reliability GET and reset endpoints always used the server's root store, ignoring projectId. In multi-project setups this meant every project saw the same aggregate reliability stats.

Change

Mirror the Command Center pattern by using getProjectIdFromRequest and getScopedStore in both endpoints:

  • GET /api/health/reliability?projectId=...
  • POST /api/health/reliability/reset?projectId=...

When no projectId is supplied, behavior falls back to the root store (unchanged from before).

Notes

This is the FUX-042 fix. v0.57.0 introduced the project-scoping helpers used elsewhere but did not apply them to the reliability endpoints.

Testing

Existing unscoped tests continue to pass because the default path still uses the root store. Project-scoped regression tests are planned for a follow-up commit if desired.

Summary by CodeRabbit

  • Bug Fixes
    • Reliability health data now respects the selected project context, showing the correct project-specific settings and metrics.
    • Resetting reliability stats now applies to the current project when one is selected, instead of always affecting the default view.
    • Reliability calculations now use the appropriate scoped data for project-based requests.

Reliability GET and reset endpoints were always using the server's
root store, ignoring projectId. Mirror the Command Center pattern by
using getProjectIdFromRequest and getScopedStore so multi-project
servers report per-project reliability stats.

Refs FUX-042
@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The /api/health/reliability GET and /api/health/reliability/reset POST endpoints in the dashboard server now resolve a projectId from the request and use a project-scoped store (scopedStore) for reading settings/audit metrics and writing reliabilityStatsResetAt, falling back to the default store when no project is specified.

Changes

Project-scoped reliability endpoints

Layer / File(s) Summary
Reliability query scoping
packages/dashboard/src/server.ts
The GET reliability endpoint derives projectId, selects scopedStore accordingly, and reads settings, audit events, in-review counts, duration events, and merged task IDs from scopedStore instead of the default store.
Reliability reset scoping
packages/dashboard/src/server.ts
The POST reset endpoint resolves projectId and updates reliabilityStatsResetAt on the resolved scopedStore rather than always updating the default store.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly states the main change: dashboard reliability endpoints are now scoped by projectId.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR scopes the reliability GET and reset endpoints to per-project stores by mirroring the Command Center pattern (getProjectIdFromRequest + getScopedStore), fixing cross-project stat bleed in multi-project setups (FUX-042).

  • Both GET /api/health/reliability and POST /api/health/reliability/reset now forward scopedStore calls to the server-local getScopedStore helper, falling back to the root store when no projectId is supplied — preserving existing single-project behaviour.
  • Neither new call-site wraps await getScopedStore(projectId) in try/catch; unlike /api/engine/start and the terminal WebSocket handler, an unknown or failing projectId will produce an unhandled async rejection in Express 4 rather than a structured error response.
  • No FNXC change comment is present and no changeset documents the operator-visible fix.

Confidence Score: 3/5

The core scoping logic is directionally correct, but both new call-sites are missing error handling that every comparable endpoint in the same file has, making them prone to silent request hangs on any store-resolution failure.

Both new getScopedStore call-sites lack try/catch guards present in every equivalent endpoint in the same file. An unknown or misconfigured projectId will produce an unhandled async rejection rather than a structured HTTP error response, which is a present defect on the changed path.

packages/dashboard/src/server.ts — specifically the two new getScopedStore call-sites around the reliability GET and POST reset handlers

Important Files Changed

Filename Overview
packages/dashboard/src/server.ts Adds projectId scoping to GET /api/health/reliability and POST /api/health/reliability/reset via the server-local getScopedStore helper; both new call-sites lack try/catch, leaving unhandled async rejections when store resolution fails for an unknown projectId. Also missing FNXC comment and changeset.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Client
    participant Server as Express Server
    participant GPI as getProjectIdFromRequest
    participant GSS as getScopedStore (local)
    participant RS as resolveScopedStore
    participant SS as ScopedStore

    Client->>Server: "GET /api/health/reliability?projectId=X"
    Server->>GPI: extract projectId from req
    GPI-->>Server: X or undefined

    alt projectId present
        Server->>GSS: getScopedStore(X)
        GSS->>RS: resolveScopedStore(X, store, engineManager)
        RS-->>GSS: ScopedStore instance
        GSS-->>Server: scopedStore
        Note over Server: No try/catch - RS can throw
    else no projectId
        Server-->>Server: use root store
    end

    Server->>SS: scopedStore.getSettings()
    Server->>SS: scopedStore.getRunAuditEvents(...)
    SS-->>Server: reliability data
    Server-->>Client: JSON reliability response

    Client->>Server: "POST /api/health/reliability/reset?projectId=X"
    Server->>GPI: extract projectId
    GPI-->>Server: X or undefined
    alt projectId present
        Server->>GSS: getScopedStore(X)
        Note over Server: No try/catch here either
        GSS-->>Server: scopedStore
    end
    Server->>SS: scopedStore.updateSettings reliabilityStatsResetAt
    SS-->>Server: ok
    Server-->>Client: resetAt
Loading
%%{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"}}}%%
sequenceDiagram
    participant Client
    participant Server as Express Server
    participant GPI as getProjectIdFromRequest
    participant GSS as getScopedStore (local)
    participant RS as resolveScopedStore
    participant SS as ScopedStore

    Client->>Server: "GET /api/health/reliability?projectId=X"
    Server->>GPI: extract projectId from req
    GPI-->>Server: X or undefined

    alt projectId present
        Server->>GSS: getScopedStore(X)
        GSS->>RS: resolveScopedStore(X, store, engineManager)
        RS-->>GSS: ScopedStore instance
        GSS-->>Server: scopedStore
        Note over Server: No try/catch - RS can throw
    else no projectId
        Server-->>Server: use root store
    end

    Server->>SS: scopedStore.getSettings()
    Server->>SS: scopedStore.getRunAuditEvents(...)
    SS-->>Server: reliability data
    Server-->>Client: JSON reliability response

    Client->>Server: "POST /api/health/reliability/reset?projectId=X"
    Server->>GPI: extract projectId
    GPI-->>Server: X or undefined
    alt projectId present
        Server->>GSS: getScopedStore(X)
        Note over Server: No try/catch here either
        GSS-->>Server: scopedStore
    end
    Server->>SS: scopedStore.updateSettings reliabilityStatsResetAt
    SS-->>Server: ok
    Server-->>Client: resetAt
Loading

Reviews (1): Last reviewed commit: "fix(dashboard): scope reliability endpoi..." | Re-trigger Greptile

Comment on lines 1597 to +1599
app.get("/api/health/reliability", async (req, res) => {
const projectId = getProjectIdFromRequest(req);
const scopedStore = projectId ? await getScopedStore(projectId) : store;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Unhandled getScopedStore rejection in GET handler

Both the GET and POST endpoints call await getScopedStore(projectId) without wrapping it in try/catch. If store resolution fails for an unknown or misconfigured projectId (e.g. resolveScopedStore throws), Express 4 won't automatically propagate the async rejection to the error handler — the request will hang without a response. Compare with /api/engine/start (lines 1578–1594) and the terminal WebSocket handler (lines 2059–2077), both of which wrap getScopedStore/resolveScopedStore in try/catch and return a structured error response. The same guard is needed here so callers receive a 500 JSON response rather than a stalled connection.

Comment on lines +1703 to +1705
app.post("/api/health/reliability/reset", async (req, res) => {
const projectId = getProjectIdFromRequest(req);
const scopedStore = projectId ? await getScopedStore(projectId) : store;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Same missing error guard in POST reset handler

The reset endpoint has the identical problem: await getScopedStore(projectId) can throw if the projectId is unknown or the underlying store resolution fails, and there is no try/catch to translate that into a structured HTTP error. A caller posting a reset for a non-existent project will get a hanging request instead of a 500 or 404 response.

Comment on lines 1597 to +1599
app.get("/api/health/reliability", async (req, res) => {
const projectId = getProjectIdFromRequest(req);
const scopedStore = projectId ? await getScopedStore(projectId) : store;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Missing FNXC change comment

The project's coding convention (AGENTS.md) requires a FNXC:Area-of-product yyyy-MM-dd-hh:mm comment whenever new behavior is introduced or requirements change. The two modified blocks add multi-project scoping to the reliability endpoints but carry no FNXC annotation describing the FUX-042 requirement and the date of the change. This pattern appears consistently across the file for similar routing additions.

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!

Comment on lines +1703 to 1709
app.post("/api/health/reliability/reset", async (req, res) => {
const projectId = getProjectIdFromRequest(req);
const scopedStore = projectId ? await getScopedStore(projectId) : store;
const resetAt = new Date().toISOString();
await store.updateSettings({ reliabilityStatsResetAt: resetAt });
await scopedStore.updateSettings({ reliabilityStatsResetAt: resetAt });
res.json({ resetAt });
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Missing changeset for published package

AGENTS.md requires a changeset under .changeset/<name>.md with "@runfusion/fusion": patch for any bug fix that changes observable behavior in the published CLI. This PR fixes incorrect aggregate reliability stats in multi-project setups (FUX-042), which is a user-visible behaviour change for Fusion operators — a patch changeset with a fix category is expected.

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!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/dashboard/src/server.ts (1)

1597-1709: 📐 Maintainability & Code Quality | 🟠 Major

Bug fix (FUX-042) must include regression tests per coding guidelines.

This PR fixes reliability endpoints to be project-scoped, but tests are deferred to a follow-up. 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." Both the GET and POST reset endpoints should have tests verifying project-scoped isolation (different projectId values return independent stats) and the fallback behavior when projectId is absent.

🤖 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/src/server.ts` around lines 1597 - 1709, Add regression
tests for the reliability endpoints to cover project-scoped isolation and
fallback behavior. Update the tests around app.get("/api/health/reliability")
and app.post("/api/health/reliability/reset") so they verify different projectId
values read/write independent settings and stats, and that requests without a
projectId continue to use the shared store. Use the existing route handlers and
getProjectIdFromRequest/getScopedStore behavior as the reference points.

Source: Coding guidelines

🧹 Nitpick comments (1)
packages/dashboard/src/server.ts (1)

1598-1599: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Add error handling for scoped store resolution failures.

Neither endpoint wraps the getScopedStore call in a try-catch. If project store creation fails (e.g., getOrCreateProjectStore throws on database errors), the error falls through to the generic Express 500 handler with a vague message. The SSE endpoint at line 1108 handles this case explicitly with a targeted error response. Consider adding similar error handling here.

♻️ Proposed error handling for GET endpoint
  app.get("/api/health/reliability", async (req, res) => {
    const projectId = getProjectIdFromRequest(req);
-   const scopedStore = projectId ? await getScopedStore(projectId) : store;
+   let scopedStore: TaskStore;
+   try {
+     scopedStore = projectId ? await getScopedStore(projectId) : store;
+   } catch (err) {
+     sendErrorResponse(res, 500, err instanceof Error ? err.message : "Failed to resolve project store");
+     return;
+   }
    const rawWindowDays = req.query.windowDays;

Also applies to: 1704-1705

🤖 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/src/server.ts` around lines 1598 - 1599, The GET endpoint
and the other affected handler currently call getScopedStore without guarding
failures, so a project store creation error will fall through to the generic 500
handler. Add explicit try-catch around getScopedStore in the same places as the
SSE flow, and return a targeted error response when project-scoped store
resolution fails. Use the existing getProjectIdFromRequest and getScopedStore
flow to locate the change, and mirror the endpoint-specific handling already
used in the SSE handler.
🤖 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.

Outside diff comments:
In `@packages/dashboard/src/server.ts`:
- Around line 1597-1709: Add regression tests for the reliability endpoints to
cover project-scoped isolation and fallback behavior. Update the tests around
app.get("/api/health/reliability") and app.post("/api/health/reliability/reset")
so they verify different projectId values read/write independent settings and
stats, and that requests without a projectId continue to use the shared store.
Use the existing route handlers and getProjectIdFromRequest/getScopedStore
behavior as the reference points.

---

Nitpick comments:
In `@packages/dashboard/src/server.ts`:
- Around line 1598-1599: The GET endpoint and the other affected handler
currently call getScopedStore without guarding failures, so a project store
creation error will fall through to the generic 500 handler. Add explicit
try-catch around getScopedStore in the same places as the SSE flow, and return a
targeted error response when project-scoped store resolution fails. Use the
existing getProjectIdFromRequest and getScopedStore flow to locate the change,
and mirror the endpoint-specific handling already used in the SSE handler.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ffa07f4e-c3d4-452b-86ea-a0263fa1da9f

📥 Commits

Reviewing files that changed from the base of the PR and between f10c39f and 103acc7.

📒 Files selected for processing (1)
  • packages/dashboard/src/server.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant