From 6e9ec3722ef1102aa2cd3190020d727267aba842 Mon Sep 17 00:00:00 2001 From: sudacode Date: Fri, 2 Oct 2026 11:14:14 -0700 Subject: [PATCH] fix(stats): keep Overview usable when refresh fails --- changes/stats-mining-and-delete-accuracy.md | 2 +- changes/stats-overview-refresh-docs.md | 2 +- docs-site/immersion-tracking.md | 2 +- .../components/overview/OverviewTab.test.tsx | 69 +++++++++++++++++++ stats/src/components/overview/OverviewTab.tsx | 27 +++++++- 5 files changed, 96 insertions(+), 6 deletions(-) diff --git a/changes/stats-mining-and-delete-accuracy.md b/changes/stats-mining-and-delete-accuracy.md index d6e5c6a6..e9dcd081 100644 --- a/changes/stats-mining-and-delete-accuracy.md +++ b/changes/stats-mining-and-delete-accuracy.md @@ -4,4 +4,4 @@ area: stats - Fixed sentence mining (single and multi-line) counting each created card twice in session and lifetime stats. - Fixed deleting a session leaving word and kanji "last seen" dates pointing at the deleted session; affected rows are repaired once in the background on next launch. - Fixed re-seen words that the vocabulary filter hides still being counted in the new-words charts, so charts and the vocabulary summary now agree. Existing mismatches are repaired by the same background pass. -- Fixed Overview totals, charts, activity calendar, and vocabulary summary staying stale after deleting sessions. +- Fixed Overview totals, charts, activity calendar, and vocabulary summary staying stale after deleting sessions. Failed refreshes keep the dashboard visible and offer a retry. diff --git a/changes/stats-overview-refresh-docs.md b/changes/stats-overview-refresh-docs.md index c184d85f..dd1d4ac7 100644 --- a/changes/stats-overview-refresh-docs.md +++ b/changes/stats-overview-refresh-docs.md @@ -1,4 +1,4 @@ type: docs area: stats -- Documented the automatic Overview refresh after deleting sessions or session groups. +- Documented the automatic Overview refresh after deleting sessions or session groups and retrying failed refreshes. diff --git a/docs-site/immersion-tracking.md b/docs-site/immersion-tracking.md index 74cc62cd..0713d126 100644 --- a/docs-site/immersion-tracking.md +++ b/docs-site/immersion-tracking.md @@ -42,7 +42,7 @@ To share stats and watch history between machines, use [`subminer sync `]( Recent sessions, a streak calendar, watch-time history, and totals for completed episodes and shows. -Deleting a session or session group refreshes the Overview totals, charts, calendar, and vocabulary summary automatically. +Deleting a session or session group refreshes the Overview totals, charts, calendar, and vocabulary summary automatically. If a refresh fails, Overview keeps the previous totals visible and offers **Retry**. ![Stats Overview](/screenshots/stats-overview.png) diff --git a/stats/src/components/overview/OverviewTab.test.tsx b/stats/src/components/overview/OverviewTab.test.tsx index 041de23b..150a91a7 100644 --- a/stats/src/components/overview/OverviewTab.test.tsx +++ b/stats/src/components/overview/OverviewTab.test.tsx @@ -195,3 +195,72 @@ for (const mode of ['session', 'day', 'anime', 'failed'] as const) { } }); } + +for (const failedRead of ['overview', 'sessions'] as const) { + test(`Overview stays available during a failed ${failedRead} refresh and can retry`, async () => { + const uninstallDom = installDom(); + const original = { ...apiClient }; + const restoreConfirm = setDeleteConfirmPresenter(() => true); + let sessions = [session(1)]; + let refreshing = false; + let failRefresh = true; + let rejectRefresh = (_error: Error) => {}; + const pendingRefresh = new Promise((_resolve, reject) => { + rejectRefresh = reject; + }); + apiClient.getOverview = async () => { + if (refreshing && failRefresh && failedRead === 'overview') return pendingRefresh; + return overview(sessions); + }; + apiClient.getSessions = async () => { + if (refreshing && failRefresh && failedRead === 'sessions') return pendingRefresh; + return sessions; + }; + apiClient.getStreakCalendar = async () => []; + apiClient.getKnownWordsSummary = async () => ({ + totalUniqueWords: 0, + knownWordCount: 0, + }); + apiClient.deleteSession = async () => { + sessions = []; + refreshing = true; + }; + const container = document.createElement('div'); + document.body.append(container); + const root = createRoot(container); + try { + await act(async () => + root.render( + {}} onNavigateToSession={() => {}} />, + ), + ); + const deleteButton = [...container.querySelectorAll('button')].find((button) => + button.getAttribute('aria-label')?.startsWith('Delete session '), + ); + assert.ok(deleteButton); + await act(async () => deleteButton.click()); + assert.match(container.textContent ?? '', /Tracking Snapshot/); + assert.equal(metric(container, 'Cards Mined Today'), '2'); + assert.match(container.textContent ?? '', /No sessions yet/); + + await act(async () => rejectRefresh(new Error(`${failedRead} unavailable`))); + assert.match(container.textContent ?? '', /Tracking Snapshot/); + assert.equal(metric(container, 'Cards Mined Today'), '2'); + assert.match(container.querySelector('[role="alert"]')?.textContent ?? '', /unavailable/); + + const retryButton = [...container.querySelectorAll('button')].find( + (button) => button.textContent === 'Retry', + ); + assert.ok(retryButton, 'failed refresh should offer a retry'); + failRefresh = false; + await act(async () => retryButton.click()); + assert.equal(metric(container, 'Cards Mined Today'), '0'); + assert.equal(container.querySelector('[role="alert"]'), null); + } finally { + await act(async () => root.unmount()); + restoreConfirm(); + Object.assign(apiClient, original); + uninstallDom(); + } + }); +} diff --git a/stats/src/components/overview/OverviewTab.tsx b/stats/src/components/overview/OverviewTab.tsx index 291cd6ba..93d9dfd0 100644 --- a/stats/src/components/overview/OverviewTab.tsx +++ b/stats/src/components/overview/OverviewTab.tsx @@ -123,9 +123,29 @@ export function OverviewTab({ } }; - if (loading) return
Loading...
; - if (error) return
Error: {error}
; - if (!data) return null; + const errorNotice = error ? ( +
+ + {data + ? 'Could not refresh Overview. Displayed totals may be out of date.' + : 'Could not load Overview.'}{' '} + {error} + + +
+ ) : null; + + if (loading && !data) return
Loading...
; + if (!data) return errorNotice; const summary = buildOverviewSummary(data); const streakData = buildStreakCalendar(calendar); @@ -133,6 +153,7 @@ export function OverviewTab({ return (
+ {errorNotice}