Skip to content

fix(portfolio): show an error state instead of $0.00 when the portfolio fetch fails - #718

Open
Seranged wants to merge 2 commits into
developmentfrom
fix/portfolio-load-error-state
Open

Seranged wants to merge 2 commits into
developmentfrom
fix/portfolio-load-error-state

Conversation

@Seranged

@Seranged Seranged commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

Problem

When fetchAndUpdatePortfolio in useEulerAccount throws, the catch block records a diagnostic but still calls markLoaded(), leaving visiblePortfolio/allPortfolio undefined. The portfolio page only substitutes "—" when positions exist with missing prices, so an outright load failure falls through to formatted zeros:

$0.00 Supplied · $0.00 Borrowed · $0.00 Net Worth

For an account with real deposits and borrows this is indistinguishable from an empty account, and the recorded portfolioDiagnostics are consumed nowhere in the UI — the failure is fully swallowed. In a lending context, showing a confident $0.00 where a user has funds can drive wrong decisions.

Fix

  • Add an explicit hasPositionsFetchError flag in useEulerAccount, set when a fetch throws and cleared at the start of every fresh attempt.
  • Expose hasPortfolioLoadError, computed as "a fetch failed and there is no portfolio to fall back on". Gating on the absence of data means a transient background-refresh failure keeps the last-good figures on screen, while a genuine cold-load failure lets the UI show an error.
  • On the portfolio page, render an error banner with a Retry action and show /- for the value and performance figures instead of zeros. The data-value capture attributes reflect the error too, so parity/e2e captures don't record misleading zeros.

This mirrors the existing hasQueryFailure → "Position data is incomplete" handling already used on the per-position page.

Testing

  • Added a unit test in tests/composables/useEulerAccount.test.ts covering the failed-fetch path (asserts hasPortfolioLoadError is set, totals stay zero, and an error-severity diagnostic is present).
  • npx vitest run tests/composables/useEulerAccount.test.ts — passes.
  • ESLint clean on the changed files.

Summary by CodeRabbit

  • Bug Fixes
    • Added clearer handling for portfolio load failures, so the app can distinguish between first-time load errors and temporary refresh issues.
    • Portfolio stats now show placeholders when data can’t be loaded, instead of displaying misleading values.
    • An error banner with a retry option now appears when the portfolio fails to load, making recovery easier.

…io fetch fails

When `fetchAndUpdatePortfolio` threw, the catch block recorded a diagnostic but
still called `markLoaded()`, leaving the portfolio refs `undefined`. The page's
display logic only substitutes "—" when positions exist with missing prices, so
an outright load failure fell through to formatted zeros: "$0.00 Supplied /
$0.00 Borrowed / $0.00 Net Worth". For an account with real deposits and borrows
this is indistinguishable from an empty account, and the recorded diagnostics
were consumed nowhere in the UI.

Track an explicit `hasPositionsFetchError` flag, set on a thrown fetch and
cleared on every fresh attempt, and expose `hasPortfolioLoadError` — true only
when a fetch failed AND there is no (stale) portfolio to fall back on, so a
transient background-refresh failure keeps the last-good figures on screen while
a genuine cold-load failure surfaces an error. The portfolio page renders a
dismissable-free error banner with a Retry action and shows "—"/"-" for the
totals and performance figures instead of zeros. Mirrors the existing
"Position data is incomplete" handling on the per-position page.

Adds a unit test covering the failed-fetch path.
@railway-app
railway-app Bot temporarily deployed to euler-lite / euler-lite-pr-718 July 9, 2026 14:50 Destroyed
@coderabbitai

coderabbitai Bot commented Jul 9, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 25 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository: euler-xyz/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2bc8b865-67cd-4d82-8645-4de53dd681ff

📥 Commits

Reviewing files that changed from the base of the PR and between 6a970c6 and 95a23fc.

📒 Files selected for processing (2)
  • composables/useEulerAccount.ts
  • tests/composables/useEulerAccount.test.ts
📝 Walkthrough

Walkthrough

Adds a hasPositionsFetchError flag and derived hasPortfolioLoadError computed to useEulerAccount, exposing whether a portfolio fetch failed with no fallback data. pages/portfolio.vue consumes this to show placeholder values, an error banner, and a retry action. Tests cover the failure path.

Changes

Portfolio load error handling

Layer / File(s) Summary
Composable fetch-error tracking
composables/useEulerAccount.ts
Adds hasPositionsFetchError ref cleared on reset/fetch-start and set on catch, plus a hasPortfolioLoadError computed exposed from the composable.
Portfolio page error UI and retry
pages/portfolio.vue
Guards Net APY, ROE, supplied/borrowed totals, and net asset value with placeholders when load fails; adds retryPortfolioLoad and an error banner with a retry button.
Fetch-failure test coverage
tests/composables/useEulerAccount.test.ts
Extends the test helper to simulate a failing fetch and adds a test verifying error state, empty portfolio, zeroed totals, and diagnostics.

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

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant PortfolioPage as portfolio.vue
  participant Composable as useEulerAccount
  participant API as fetchPortfolio

  PortfolioPage->>Composable: updatePositions()
  Composable->>Composable: clear hasPositionsFetchError
  Composable->>API: fetchPortfolio()
  API-->>Composable: throws error
  Composable->>Composable: set hasPositionsFetchError = true
  Composable->>Composable: compute hasPortfolioLoadError (no fallback portfolio)
  Composable-->>PortfolioPage: hasPortfolioLoadError = true
  PortfolioPage->>User: show error banner + placeholders
  User->>PortfolioPage: click Retry
  PortfolioPage->>Composable: retryPortfolioLoad -> updatePositions(fresh, preempt)
Loading

Suggested reviewers: kasperpawlowski

🚥 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 matches the main change: surfacing an error state when portfolio fetch fails instead of showing zero values.
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
  • Commit unit tests in branch fix/portfolio-load-error-state

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
pages/portfolio.vue (1)

137-167: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Placeholder character inconsistent between - and .

portfolioNetApyDisplay/portfolioRoeDisplay (and their data-value bindings at Lines 275, 298) fall back to a hyphen '-', while totalSuppliedDisplay/totalBorrowedDisplay/netAssetValueDisplay (and Lines 326, 349, 372) fall back to an em dash '—'. Minor visual inconsistency across the same error state.

Also applies to: 275-372

🤖 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 `@pages/portfolio.vue` around lines 137 - 167, The placeholder shown for
missing/error portfolio values is inconsistent between the display computeds in
portfolio.vue: portfolioNetApyDisplay and portfolioRoeDisplay use a hyphen while
totalSuppliedDisplay, totalBorrowedDisplay, and netAssetValueDisplay use an em
dash. Update the relevant computed fallback values and their related data-value
bindings so the same placeholder character is used consistently across all
portfolio metrics, matching the existing error state handling in those computed
properties.
🤖 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 `@composables/useEulerAccount.ts`:
- Line 129: The fetch flow in useEulerAccount’s fetchAndUpdatePortfolio is
clearing hasPositionsFetchError too early, which makes portfolio.vue drop its
error/placeholder state before new data is available. Remove the synchronous
reset at the start of fetchAndUpdatePortfolio and only set
hasPositionsFetchError.value = false on the success path, alongside the other
successful portfolio assignments, so the previous error state remains visible
until a fetch actually completes.

---

Nitpick comments:
In `@pages/portfolio.vue`:
- Around line 137-167: The placeholder shown for missing/error portfolio values
is inconsistent between the display computeds in portfolio.vue:
portfolioNetApyDisplay and portfolioRoeDisplay use a hyphen while
totalSuppliedDisplay, totalBorrowedDisplay, and netAssetValueDisplay use an em
dash. Update the relevant computed fallback values and their related data-value
bindings so the same placeholder character is used consistently across all
portfolio metrics, matching the existing error state handling in those computed
properties.
🪄 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: Repository: euler-xyz/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e52b2701-dd2c-4573-a7bd-427e4ae25f98

📥 Commits

Reviewing files that changed from the base of the PR and between 5b97c37 and 6a970c6.

📒 Files selected for processing (3)
  • composables/useEulerAccount.ts
  • pages/portfolio.vue
  • tests/composables/useEulerAccount.test.ts

Comment thread composables/useEulerAccount.ts Outdated

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

Leonard review — changes requested

Reviewed head 6a970c665e54f247a0cd1d0ac96dc019ebbe00b5.

This is the right direction: a cold portfolio fetch failure should not be rendered as a confident empty account. The new composable flag and portfolio-page placeholders cover the initial thrown-fetch path, and the focused test exercises that path.

One blocking edge remains, matching CodeRabbit's major comment after verification:

  • composables/useEulerAccount.ts: hasPositionsFetchError is cleared synchronously at the start of each new fetch attempt. After a cold-load failure, clicking Retry or waiting for a refresh immediately drops hasPortfolioLoadError while portfolio is still undefined, so the portfolio cards can show 0.00% / $0 again for the duration of the retry. That reintroduces the exact misleading-zero state this PR is trying to remove, just during the retry window. Keep the error/placeholder state until a new portfolio result is actually confirmed, then clear the error on the success path.

Validation performed:

  • npx vitest run tests/composables/useEulerAccount.test.ts — passed.
  • npm run typecheck — passed.
  • npm run build — passed, with existing Vite/Rollup chunk/dynamic-import warnings only.
  • Local throwaway regression test for “failed cold load → pending retry” — failed as expected on this head: hasPortfolioLoadError became false while portfolio was still undefined.

Scalability / maintainability hygiene:

  • Scope is appropriately centered in useEulerAccount, rather than a page-only formatting patch. That is the right reusable layer for portfolio consumers.
  • I checked sibling portfolio/error surfaces: position pages already have separate hasQueryFailure handling; this PR’s shared portfolio-load state is specifically for account portfolio data and does not need to be copied into position-card components.
  • The remaining issue is also best fixed in the shared composable, not with more page-level guards.

Bot feedback:

  • Verified CodeRabbit’s major retry-window finding as valid and blocking.
  • The placeholder-character nit is minor and not merge-blocking.

Browser/mobile smoke:

  • Incomplete for the isolated portfolio-fetch failure. A local headed/Xvfb attempt with forced RPC failures exercised a broader prerequisite failure mode rather than just fetchPortfolio, so I did not use or post those screenshots as evidence. The blocking retry invariant is covered more directly by the composable-level regression test above.

Clear the portfolio fetch error only after a successful refresh confirms replacement data, and cover the pending retry window with a composable regression test.

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

Leonard review — current head 95a23fc2f70ab476c08c776d44a0f371284b9c35

Verdict: no blocking findings. Submitting as a comment review, not approval.

The previous blocker on head 6a970c66 is resolved: hasPositionsFetchError is no longer cleared at fetch start, so a retry/background refresh keeps the cold-load error state visible until a successful portfolio response installs real data. The new regression test covers that exact pending-retry window.

What changed:

  • useEulerAccount now tracks thrown portfolio fetches separately from partial per-position pricing diagnostics.
  • hasPortfolioLoadError is only true when the latest fetch threw and no stale portfolio exists to fall back to.
  • pages/portfolio.vue shows an explicit portfolio-load error banner plus placeholders instead of confident $0 / 0.00% values.
  • Retry uses a fresh preemptive portfolio refresh.

Validation performed:

  • Diff and surrounding-code review for composables/useEulerAccount.ts, pages/portfolio.vue, and tests/composables/useEulerAccount.test.ts.
  • Focused regression: npx vitest run tests/composables/useEulerAccount.test.ts — passed, including cold-load failure and pending-retry persistence.
  • npm run typecheck — passed.
  • npm run lint -- composables/useEulerAccount.ts pages/portfolio.vue tests/composables/useEulerAccount.test.ts — exited 0; only pre-existing repo-wide any warnings surfaced outside this PR’s changed files.
  • npm run build — passed; build emitted existing chunk/sourcemap-style warnings only.
  • Headed Chromium/Xvfb smoke against a local production build: /portfolio?network=1&spy=0x68e7...8264 on desktop and mobile rendered the portfolio shell/tabs without material page errors. This was route/mobile coverage, not wallet/signing coverage.

Scalability / maintainability hygiene pass:

  • Sibling portfolio value/performance surfaces on the top-level portfolio page are covered by the shared hasPortfolioLoadError gate, not separate one-off guards per metric.
  • Position-detail hasQueryFailure handling and portfolio-item query-failure handling are related but not the same data boundary: they cover incomplete position/vault data after a portfolio exists, while this PR covers the cold portfolio-fetch failure before there is any portfolio to render. I do not see a missed sibling surface that would still show the same cold-load $0 bug.
  • The retry-persistence invariant is now in a focused composable test, which is the right level for this state machine.

Bot-feedback pass:

  • CodeRabbit’s earlier major concern about clearing the error at fetch start was valid on the old head and is fixed here.
  • Its remaining placeholder-character nit is low-value/non-blocking; I don’t see it changing correctness or user safety for this fix.

Coverage caveats:

  • I did not run wallet/signing smoke; this PR changes read-only portfolio load/error presentation only.
  • I did not post screenshots: the meaningful error state was validated through a controlled composable failure/retry test rather than a synthetic browser network-failure screenshot, and the browser pass was only route/mobile sanity coverage.

@railway-app

railway-app Bot commented Jul 9, 2026

Copy link
Copy Markdown

🚅 Deployed to the euler-lite-pr-718 environment in euler-lite(dev,PR previews)

Service Status Web Updated (UTC)
dev-build ✅ Success (View Logs) Web Jul 31, 2026 at 9:58 am

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