fix(stats): surface activity calendar refresh failures on Overview

- Show a retryable alert when the streak calendar refresh fails
- Use a calendar-specific message while overview totals still refresh
- Cover calendar failure in the Overview refresh/retry tests
This commit is contained in:
sudacode committed 2026-10-02 19:32:43 -07:00
1 parent 6e9ec3722e
commit 009519a006
2 files changed
+18 -10

No files matched your search

@@ -196,7 +196,7 @@ for (const mode of ['session', 'day', 'anime', 'failed'] as const) {
});
}
for (const failedRead of ['overview', 'sessions'] as const) {
for (const failedRead of ['overview', 'sessions', 'calendar'] as const) {
test(`Overview stays available during a failed ${failedRead} refresh and can retry`, async () => {
const uninstallDom = installDom();
const original = { ...apiClient };
@@ -216,7 +216,10 @@ for (const failedRead of ['overview', 'sessions'] as const) {
if (refreshing && failRefresh && failedRead === 'sessions') return pendingRefresh;
return sessions;
};
apiClient.getStreakCalendar = async () => [];
apiClient.getStreakCalendar = async () => {
if (refreshing && failRefresh && failedRead === 'calendar') return pendingRefresh;
return [];
};
apiClient.getKnownWordsSummary = async () => ({
totalUniqueWords: 0,
knownWordCount: 0,
@@ -225,6 +228,8 @@ for (const failedRead of ['overview', 'sessions'] as const) {
sessions = [];
refreshing = true;
};
// A calendar-only failure still lets the overview totals refresh.
const staleCards = failedRead === 'calendar' ? '0' : '2';
const container = document.createElement('div');
document.body.append(container);
const root = createRoot(container);
@@ -240,12 +245,12 @@ for (const failedRead of ['overview', 'sessions'] as const) {
assert.ok(deleteButton);
await act(async () => deleteButton.click());
assert.match(container.textContent ?? '', /Tracking Snapshot/);
assert.equal(metric(container, 'Cards Mined Today'), '2');
assert.equal(metric(container, 'Cards Mined Today'), staleCards);
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.equal(metric(container, 'Cards Mined Today'), staleCards);
assert.match(container.querySelector('[role="alert"]')?.textContent ?? '', /unavailable/);
const retryButton = [...container.querySelectorAll('button')].find(
@@ -29,7 +29,7 @@ export function OverviewTab({
}: OverviewTabProps) {
const [refreshKey, setRefreshKey] = useState(0);
const { data, sessions, setSessions, loading, error } = useOverview(refreshKey);
const { calendar, loading: calLoading } = useStreakCalendar(90, refreshKey);
const { calendar, loading: calLoading, error: calendarError } = useStreakCalendar(90, refreshKey);
const [deleteError, setDeleteError] = useState<string | null>(null);
const [deletingIds, setDeletingIds] = useState<Set<number>>(new Set());
const [knownWordsSummary, setKnownWordsSummary] = useState<{
@@ -123,16 +123,19 @@ export function OverviewTab({
}
};
const errorNotice = error ? (
const refreshError = error ?? calendarError;
const errorNotice = refreshError ? (
<div
role="alert"
className="flex items-center justify-between gap-3 rounded-lg border border-ctp-red/30 bg-ctp-red/10 p-3 text-sm text-ctp-red"
>
<span>
{data
? 'Could not refresh Overview. Displayed totals may be out of date.'
: 'Could not load Overview.'}{' '}
{error}
{!data
? 'Could not load Overview.'
: error
? 'Could not refresh Overview. Displayed totals may be out of date.'
: 'Could not refresh the activity calendar. It may be out of date.'}{' '}
{refreshError}
</span>
<button
type="button"