Skip to content

fix(shell-ui): surface dashboard fetch failures instead of loading forever - #1673

Merged
kwit75 merged 4 commits into
developfrom
fix/dashboard-surface-fetch-error
Jul 29, 2026
Merged

fix(shell-ui): surface dashboard fetch failures instead of loading forever#1673
kwit75 merged 4 commits into
developfrom
fix/dashboard-surface-fetch-error

Conversation

@kwit75

@kwit75 kwit75 commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Why

_fetchDashboard() catches every error and only console.logs it, leaving _data null. Consumers render their loading state whenever data is null — so a failed fetch is indistinguishable from a slow one, and the view sits on "Loading dashboard data..." forever.

That is exactly the incident in rocketride-saas #373: the call was returning Permission 'task.monitor' denied and the UI never surfaced it, so it read as a hang and reached us from a user rather than from an alert.

What

  • Track the failure as first-class state (_error), clear it on success.
  • Phrase permission denials in plain language rather than echoing a raw exception.
  • Expose it as error on the hook so views can render it.

Backward compatible — this only adds a field; the existing consumer (monitor-ui) destructures a subset and is unaffected. Verified with tsc --noEmit on apps/shell-ui: no errors in the changed file.

Note on where this lives

I originally fixed this in rocketride-saas (apps/admin-ui/src/useDashboardData.ts), but #314 (UI consistency program) has since deleted that copy and moved the hook here, carrying the swallow with it unchanged. This is the right home — fixing it here covers every consumer instead of one app. The saas-side PR is being closed in favour of this.

Follow-up

The saas DashboardView needs a small companion change to render error (with a Retry) instead of its EmptyState loading title. That lands after this, once the hook exposes the field.

Related: rocketride-saas #373 (incident + postmortem), #374 (backend root cause), server #1670 (engine stops swallowing the denial reason).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Dashboard loading issues are now surfaced through a dedicated error field in the dashboard data.
    • Permission/denied-style failures produce clearer, more specific user-facing messages using available response details, with safe fallback detection.
    • For access-denied-style failures, previously loaded dashboard content is no longer displayed alongside the error.
    • After a successful dashboard refresh, the error state is automatically cleared.

…rever

_fetchDashboard() caught every error and only console.log'd it, leaving _data
null. Consumers render their loading state whenever data is null, so a failed
fetch was indistinguishable from a slow one and the view sat on "Loading
dashboard data..." indefinitely.

That is what the "Dashboard hangs on Loading" report was (rocketride-saas #373):
the request was returning Permission 'task.monitor' denied and the UI simply
never said so.

Track the failure as first-class state, clear it on success, phrase permission
denials in plain language, and expose it as `error` on the hook so views can
render it. Adding a field is backward compatible — the existing consumer
destructures a subset and is unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015nTVr6jfSFYm1GppxbjghP
@github-actions

Copy link
Copy Markdown
Contributor
🤖 Internal: Discord sync marker

Auto-managed by the Discord notification workflow. Stores the linked Discord message ID and forum thread ID. Do not edit or delete.

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 33bd13f7-a7b7-4c72-9095-23ae070a6589

📥 Commits

Reviewing files that changed from the base of the PR and between d4c39a2 and e771353.

📒 Files selected for processing (1)
  • apps/shell-ui/src/hooks/useDashboardData.ts

📝 Walkthrough

Walkthrough

useDashboardData now tracks dashboard fetch errors, maps permission-related failures to user-facing messages, emits error changes, clears errors after successful fetches, and exposes the error through its public interface.

Changes

Dashboard error state

Layer / File(s) Summary
Snapshot and fetch error handling
apps/shell-ui/src/hooks/useDashboardData.ts
The dashboard snapshot and public DashboardData shape include error; fetch success clears it, while failures store a derived message, clear data for denied requests, and notify subscribers.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested labels: module:ui

Suggested reviewers: jmaionchi, stepmikhaylov, rod-christensen

Sequence Diagram(s)

sequenceDiagram
  participant useDashboardData
  participant getDashboard
  participant Subscribers
  useDashboardData->>getDashboard: Fetch dashboard data
  getDashboard-->>useDashboardData: Return data or fetch failure
  useDashboardData->>useDashboardData: Update data and error state
  useDashboardData->>Subscribers: Emit updated snapshot
Loading
🚥 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 reflects the main change: dashboard fetch failures are surfaced instead of leaving the UI stuck loading.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
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 docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/dashboard-surface-fetch-error

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

apps/shell-ui/src/hooks/useDashboardData.ts

Oops! Something went wrong! :(

ESLint: 9.39.5

TypeError: expand is not a function
at Minimatch.braceExpand (/node_modules/.pnpm/minimatch@3.1.5/node_modules/minimatch/minimatch.js:271:10)
at Minimatch.make (/node_modules/.pnpm/minimatch@3.1.5/node_modules/minimatch/minimatch.js:180:33)
at new Minimatch (/node_modules/.pnpm/minimatch@3.1.5/node_modules/minimatch/minimatch.js:156:8)
at doMatch (/node_modules/.pnpm/@eslint+config-array@0.21.2/node_modules/@eslint/config-array/dist/cjs/index.cjs:422:13)
at match (/node_modules/.pnpm/@eslint+config-array@0.21.2/node_modules/@eslint/config-array/dist/cjs/index.cjs:756:11)
at /node_modules/.pnpm/@eslint+config-array@0.21.2/node_modules/@eslint/config-array/dist/cjs/index.cjs:772:10
at Array.some ()
at pathMatches (/node_modules/.pnpm/@eslint+config-array@0.21.2/node_modules/@eslint/config-array/dist/cjs/index.cjs:767:44)
at /node_modules/.pnpm/@eslint+config-array@0.21.2/node_modules/@eslint/config-array/dist/cjs/index.cjs:1368:8
at FlatConfigArray.forEach ()


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.

@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.

Actionable comments posted: 1

🤖 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 `@apps/shell-ui/src/hooks/useDashboardData.ts`:
- Around line 88-93: Update the comment above _snapshot in the snapshot state
section to state that the object is replaced when data, events, or error
changes, matching the behavior in _emit.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: b5476f68-5b1c-4ca9-9462-848dea40ef66

📥 Commits

Reviewing files that changed from the base of the PR and between 3526218 and ef722b0.

📒 Files selected for processing (1)
  • apps/shell-ui/src/hooks/useDashboardData.ts

Comment thread apps/shell-ui/src/hooks/useDashboardData.ts Outdated
@kwit75
kwit75 requested a review from anandray July 25, 2026 00:08
anandray
anandray previously approved these changes Jul 25, 2026

@anandray anandray 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.

Verdict: approve. Right shape, backward-compatible, useful comment. Two minor notes.

What's good

  • Emits on failure now, not just success. That's the whole fix — consumers stop being trapped in data === null limbo when a fetch fails. Correct.
  • Backward-compatible additive change to DashboardSnapshot / DashboardData. monitor-ui destructures a subset and won't type-error; verified by kwit's tsc --noEmit.
  • Comment is exactly right — points at saas #373 as the incident source, explains why _data === null masked the failure. Reader lands here on triage and immediately knows why the field exists.
  • Permission-denial humanisation so callers can render user-friendly text rather than a raw exception string.
  • Consolidates the fix in the shell-ui hook rather than duplicating it in each consumer app. Right call given #314 moved this hook here.

Minor notes

1. _data is not cleared on error — intentional?

Current behaviour: a successful fetch populates _data; a subsequent failure sets _error but keeps _data. Consumers can render stale data alongside the error banner — usually the right UX ("here's what we had, warn about the failure"), but confirm it's intentional. If not, add _data = null in the catch block; if so, worth a line in the comment saying so.

2. console.log for the raw error, not console.error.

console.log is fine for dev but doesn't get the red styling / level filtering in browser devtools that console.error gets. Preexisting style, so defer if it matches convention here. Minor.

3. Permission-detection regex is broad but not incorrect.

/denied|permission|forbidden|403/i will catch legitimate permission denials and also match any 5xx that happens to contain "permission" in an unrelated context. Over-matching means "You do not have permission…" is shown for something that isn't strictly a permission issue — mildly confusing but never harmful. Fine as heuristic.

4. Wording nit: You do not have permission to view the dashboard. ${message} produces "You do not have permission to view the dashboard. Permission 'task.monitor' denied" — the doubled-up prefix reads a bit awkwardly. Consider dropping the raw message in the permission case (or preserving it only in a details tooltip).

Approving.

…al only

Both of @anandray's points.

**Doubled wording.** The raw message already says "permission denied" in
exactly the case the friendly sentence exists to explain, so the two
concatenated into "You do not have permission to view the dashboard. Permission
denied". Replaced rather than appended; the verbatim text still goes to the
console line directly above for anyone debugging.

**_data retention — answering rather than confirming.** The retention was
intentional for transient failures and should stay: a poll that blips must
leave the last good numbers on screen with an error beside them, not blank the
dashboard every time the network hiccups. Going empty on each failed poll
would be its own bug.

But it was wrong to apply that uniformly. A denial is not a blip — it means
this user is not entitled to these numbers, and continuing to show a previous
session's data under an "access denied" banner is both confusing and the wrong
default for something access-scoped. So `_data` is now cleared on denial only,
and the split is spelled out in the comment so the next reader does not have to
guess which half was deliberate.

Note the existing 104 tsc errors in apps/shell-ui are unrelated (unbuilt
workspace deps); count is identical with and without this change, and none are
in this file. CI does not typecheck shell-ui, so this was checked locally.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015nTVr6jfSFYm1GppxbjghP

@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.

Actionable comments posted: 1

🤖 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 `@apps/shell-ui/src/hooks/useDashboardData.ts`:
- Around line 127-147: Update the dashboard failure handling around
getDashboard/call so authorization failures are classified from structured
dapResult status or code, not only the message text. Include Unauthorized and
equivalent 401/403 forms in the denied detection, then preserve the existing
behavior of clearing _data only for denied failures while retaining cached data
for transient errors.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: c4914e19-b3cd-4a02-af0f-b4252b34d086

📥 Commits

Reviewing files that changed from the base of the PR and between ef722b0 and c33ba6b.

📒 Files selected for processing (1)
  • apps/shell-ui/src/hooks/useDashboardData.ts

Comment thread apps/shell-ui/src/hooks/useDashboardData.ts Outdated
@kwit75

kwit75 commented Jul 25, 2026

Copy link
Copy Markdown
Collaborator Author

Both done in c33ba6b.

Doubled wording. The raw message already says "permission denied" in exactly the case the friendly sentence exists to explain, so the two concatenated into "You do not have permission to view the dashboard. Permission denied". Replaced rather than appended — the verbatim text still goes to the console.log line directly above it for anyone debugging.

_data retention — answering rather than just confirming. It was intentional, but only half-right, and I think the half that was wrong matters.

Keeping the last good data on a transient failure is correct and should stay: a poll that blips must leave the numbers on screen with an error beside them, not blank the dashboard every time the network hiccups. Clearing on every failed poll would be its own bug.

A denial is not a blip. It means this user is not entitled to these numbers, and continuing to render a previous session's data under an "access denied" banner is both confusing and the wrong default for something access-scoped. So _data is now cleared on denial only, and the split is written into the comment so the next reader does not have to reverse-engineer which half was deliberate.

On the tsc situation, for the record: apps/shell-ui reports 104 errors, identical with and without this change, none in this file — unbuilt workspace deps. CI does not typecheck shell-ui at all, so I ran it locally.

@kwit75
kwit75 requested a review from anandray July 25, 2026 04:55
anandray
anandray previously approved these changes Jul 27, 2026

@anandray anandray 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.

Re-review approve. The transient-blip vs denial split on _data retention is a better answer than the binary I flagged — keeping stale data behind a permission banner would have been its own bug, keeping it through a network hiccup is right. The double-message wording is fixed by replacing rather than prefixing, and the raw message is preserved in the console line for debugging. Clean.

…rose

Two unresolved CodeRabbit threads. The second is the substantive one and it
breaks this PR on its own terms.

Denial detection was /denied|permission|forbidden|403/ over err.message. But
getDashboard() goes through call(), so a structured DAP failure arrives as
DAPException with the detail in `dapResult` — the human-readable message is not
guaranteed to carry it. And even on the text path the pattern had 403 but not
401 and not "unauthorized", so a plain `Unauthorized` fell through as a
transient blip.

Either way the result is the branch failing to fire for exactly the case it
exists for: a real denial would keep rendering the previous session's numbers
under an "access denied" banner, which is the behaviour this PR is meant to
remove.

Now reads dapResult.status / statusCode / code first and falls back to the
text, with 401 and unauthori[sz]ed added. Checked the classification against
both: Unauthorized, unauthorised, Not authorized, 403 Forbidden, bare 401 and
"failed with status 401" all classify as denied; timeout and network error stay
transient, so cached data still survives a blip.

Also corrected the snapshot comment, which claimed the object is replaced "only
when data or events change" — it is replaced on error too.

Related: #1673.

@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.

Actionable comments posted: 1

🤖 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 `@apps/shell-ui/src/hooks/useDashboardData.ts`:
- Around line 134-142: Update the denial detection near dapResult and denied to
recognize symbolic DAP authorization codes such as “UNAUTHORIZED” and
“FORBIDDEN” in addition to numeric status values. Ensure these codes set denied
even when the error message is generic, so prior access-scoped _data is not
retained after authorization failure.
🪄 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: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f85bba92-b85b-4484-8d9c-113542fb7412

📥 Commits

Reviewing files that changed from the base of the PR and between c33ba6b and d4c39a2.

📒 Files selected for processing (1)
  • apps/shell-ui/src/hooks/useDashboardData.ts

Comment thread apps/shell-ui/src/hooks/useDashboardData.ts Outdated
CodeRabbit caught a real hole in this PR. `Number()` over dapResult.code turns
a symbolic 'UNAUTHORIZED' or 'FORBIDDEN' into NaN, so neither status check
fired, and the prose regex only ever saw `message`. With a generic human
message — which is exactly the case this branch was written for, since the
structured detail is not guaranteed to reach the text — `denied` stayed false.

The consequence is the specific bug this PR is meant to kill: `_data` is
deliberately not cleared on failure so a network blip leaves the last good
numbers on screen, so a missed denial keeps rendering the previous session's
figures under a vague error instead of "you do not have permission".

Now matches the code as a string as well. The pattern is lifted to a named
const rather than duplicated, and carries no `g` flag, so reusing it across two
.test() calls has no lastIndex statefulness.

Verified with tsc against apps/shell-ui/tsconfig.json: no errors in this file.
(The remaining TS2307 "Cannot find module 'rocketride'" errors are the known
unbuilt client-typescript dist/types artifact, not this change — and worth
saying out loud that CI would not have caught a type error here either, since
nothing typechecks apps/** on a PR.)

Refs #1673.
@kwit75
kwit75 requested a review from anandray July 27, 2026 20:27

@anandray anandray 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.

Re-review approve. Reading the structured DAP error first is the right shape and it's what I should have suggested rather than just noting the regex was broad.

What's good

  • Structured status first, prose fallback second. DAP call() surfaces the detail on dapResult; matching only the human message is fragile because the message is not guaranteed to carry the code. Your framing ("a real denial would keep rendering the previous session's numbers under an 'access denied' banner") names the exact false-negative this branch exists to prevent.
  • Symbolic + numeric coverage. DAP might send code: 403 (numeric) or code: 'FORBIDDEN' (string). Number(...) on the symbolic form gives NaN → text-match path kicks in. Both handled.
  • Regex broadened correctly: unauthori[sz]ed, \b40[13]\b, both spellings. Wide enough to catch symbolic variants without over-matching random 5xx bodies.
  • Type-safe access pattern: (err as { dapResult?: Record<string, unknown> } | undefined)?.dapResult — optional chaining + narrowed cast. Safe.
  • No breaking type change: still exports error: string | null — unlike #1707, no consumer type is silently widened.

Nit (non-blocking)

The three-source disjunction (status, code as text, message) is a lot to hold in the head.

Would read cleaner extracted into a isDenial(err) helper next time you're near this file. Not for this PR — the inline version is fine and the comments do the work. Just a shape-nit for future refactoring.

Approving.

@kwit75
kwit75 merged commit c136cfe into develop Jul 29, 2026
25 checks passed
@kwit75
kwit75 deleted the fix/dashboard-surface-fetch-error branch July 29, 2026 00:42
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.

2 participants