Skip to content

Keep the last good panel data when a refresh fails - #53

Open
joshdev8 wants to merge 2 commits into
mainfrom
fix/stale-panel-data
Open

Keep the last good panel data when a refresh fails#53
joshdev8 wants to merge 2 commits into
mainfrom
fix/stale-panel-data

Conversation

@joshdev8

@joshdev8 joshdev8 commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Follows up the memoize finding from the #51 review. Investigating it turned up something larger than the finding described.

The bug

usePolled's docstring promises "a failed refresh keeps the last good data on screen." It can only honour that for transport failures. An upstream declining arrives as a successful 200 carrying available: false, so setData(json) replaced the last good payload with it and PanelBody fell through to PanelEmpty.

Probing the real memoize shows the server side compounding it:

1. upstream healthy                {"available":true,"rows":4}
2. upstream blips, TTL expired     {"available":false,"reason":"connection refused"}
3. next poll, still within new TTL {"available":false,"reason":"connection refused"}
4. upstream recovered (cached)     {"available":false,"reason":"connection refused"}   <-- still failing
5. after TTL expires again         {"available":true,"rows":4}

Step 4 is the part the review didn't mention: the cached failure is served for the whole TTL even after the upstream is healthy. Combined, a single failed poll blanked a panel for up to 60s (Upcoming) or 30s (Requests, Activity) — the exact outcome the invariant exists to prevent.

The fix

PanelBody remembers the most recent available payload and keeps rendering it when a later response is unavailable, above a marker naming the reason and carrying the same hint PanelEmpty would.

Staleness is stated, not hidden — silently showing stale numbers would be worse than showing none, and the hint keeps the fix discoverable without the panel going blank.

Why not fix memoize

The review proposed making memoize retain the last good value. I deliberately didn't:

  • It's generic over T and knows nothing about Result; teaching it the discriminant couples the cache to the payload type.
  • Caching failures is protective — it's what stops a dead upstream being hit every five seconds.
  • Retaining server-side risks serving stale data indefinitely with no signal, whereas the client can show staleness.

The display decision belongs where Result is already understood.

Verification

End to end against a live Seerr behind a toggleable proxy, without reloading between steps (a reload would reset the retained value and prove nothing):

  1. Healthy — four request rows render
  2. Upstream returns 503 — rows stay, marker appears: Showing last known data — HTTP 503
  3. Upstream healed — marker clears, rows refresh

Also confirmed the first-load case is untouched: with no prior good payload, an unavailable response still renders PanelEmpty.

Typecheck, lint, 42/42 tests and build all clean.

Not covered

Gauges and VpnCard don't use PanelBody and still drop upstream reasons — the other half of that review finding. Left for a separate change.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Panels now continue displaying the most recently available data when an upstream update is temporarily unavailable.
    • Added a clear “Showing last known data” warning to indicate that displayed information may be stale.
    • Panels without previously available data continue to show the appropriate empty state.

usePolled documents that "a failed refresh keeps the last good data on
screen", but it could only honour that for transport failures. An
upstream declining arrives as a *successful* 200 carrying
`available: false`, so setData() overwrote the last good payload with it
and PanelBody fell straight through to PanelEmpty.

The result was that one failed poll blanked a panel until the server's
TTL lapsed — up to 60s for the calendar, 30s for requests and activity —
and memoize would go on serving that cached failure for the rest of the
TTL even after the upstream recovered.

PanelBody now remembers the most recent available payload and keeps
rendering it when a later response is unavailable, above a marker that
names the reason and carries the same hint PanelEmpty would. Staleness is
stated rather than hidden: showing stale numbers silently would be worse
than showing none.

Left memoize alone deliberately. It is generic over T and knows nothing
about Result, and caching failures is what stops a dead upstream being
hit every five seconds — the display decision belongs on the client,
where Result is already understood.

Verified end to end against a live Seerr behind a toggleable proxy: good
data renders, breaking the upstream keeps the rows and adds "Showing last
known data — HTTP 503", and healing it clears the marker.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4f753ab2-5e9a-40cd-b223-a85d727b13c9

📥 Commits

Reviewing files that changed from the base of the PR and between 36eda46 and f182b49.

📒 Files selected for processing (1)
  • dashboard/web/src/components/Panel.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • dashboard/web/src/components/Panel.tsx

📝 Walkthrough

Walkthrough

Panel rendering retains the latest available result and displays it with a “Showing last known data” warning when a response is unavailable. Panels without prior data still show PanelEmpty.

Changes

Panel stale-data rendering

Layer / File(s) Summary
Stale state component and tracking
dashboard/web/src/components/Panel.tsx
Adds the exported PanelStale component and an internal hook that stores the latest available result.
Panel body rendering integration
dashboard/web/src/components/Panel.tsx
Centralizes child rendering and uses the stored result for unavailable responses, falling back to PanelEmpty when no prior result exists.

Estimated code review effort: 3 (Moderate) | ~20 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 and concisely describes retaining valid panel data after a refresh failure.
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/stale-panel-data

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

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 `@dashboard/web/src/components/Panel.tsx`:
- Around line 118-121: Update useLastAvailable so the lastAvailable ref is
assigned only inside a useLayoutEffect dependent on data, recording data when it
is available after the render commits. Remove the render-time assignment while
preserving the existing T | null return behavior.
🪄 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: 24e4ee1c-9279-467d-8433-a4f5b97bec6b

📥 Commits

Reviewing files that changed from the base of the PR and between 4659e92 and 36eda46.

📒 Files selected for processing (1)
  • dashboard/web/src/components/Panel.tsx

Comment thread dashboard/web/src/components/Panel.tsx
@joshdev8

joshdev8 commented Aug 2, 2026

Copy link
Copy Markdown
Owner Author

@claude fix issues

Copilot AI 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.

Pull request overview

This PR improves the dashboard panel UX by ensuring a temporary upstream “unavailable” response doesn’t blank out a panel that previously had good data, while clearly indicating when the displayed content is stale.

Changes:

  • Added a PanelStale warning banner to indicate the newest refresh is unavailable while continuing to render the last known available payload.
  • Introduced useLastAvailable to retain the most recent available: true payload and updated PanelBody to render stale + retained data when appropriate.
Suppressed comments (1)

dashboard/web/src/components/Panel.tsx:122

  • useLastAvailable currently mutates lastAvailable.current during render. In React 18, renders can be restarted/aborted; updating refs during render can leave lastAvailable reflecting a value from a render that never committed. Prefer committing this ref update in a useEffect tied to data.
function useLastAvailable<T extends object>(data: Result<T> | null): T | null {
  const lastAvailable = useRef<T | null>(null);
  if (data?.available) lastAvailable.current = data;
  return lastAvailable.current;
}

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread dashboard/web/src/components/Panel.tsx Outdated
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.

3 participants