From 1d359d74395f86fccea44711002153f2584aef6d Mon Sep 17 00:00:00 2001 From: khaira777 <777gurkirat@gmail.com> Date: Sat, 7 Nov 2026 13:03:51 -0500 Subject: [PATCH 1/2] fix(settings): report counters load failures Per-request catch fallbacks turned a failed /counters or multi_counter_enabled load into disabled mode with an empty list, so the counterLoadFailed toast never ran. Let failures reach it; the settings route already returns the default for a missing key. --- frontend/e2e/settings-request-budget.spec.ts | 16 ++++++++++++++++ .../components/settings/CountersSettingsTab.tsx | 16 ++++------------ 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/frontend/e2e/settings-request-budget.spec.ts b/frontend/e2e/settings-request-budget.spec.ts index 1b58cf67b..48dd04fe5 100644 --- a/frontend/e2e/settings-request-budget.spec.ts +++ b/frontend/e2e/settings-request-budget.spec.ts @@ -3195,3 +3195,19 @@ test('Refreshing local or Drive backup history while Load more is in flight igno await expect(remoteRows.nth(5)).toHaveAttribute('data-backup-id', allRemoteBackups[5].id); await expect(remoteRows.nth(14)).toHaveAttribute('data-backup-id', allRemoteBackups[14].id); }); + +test('Counters tab reports a failed counters load instead of showing an empty list', async ({ + page, +}) => { + await startMockedSettingsSession(page); + await page.route('**/api/counters**', async (route) => { + await route.fulfill({ + status: 500, + contentType: 'application/json', + body: JSON.stringify({ error: 'unavailable' }), + }); + }); + + await page.goto(`${BASE}/settings?tab=counters`); + await expect(page.getByText('Failed to load counters.', { exact: true })).toBeVisible(); +}); diff --git a/frontend/src/components/settings/CountersSettingsTab.tsx b/frontend/src/components/settings/CountersSettingsTab.tsx index c9288a8f6..a7767bbcc 100644 --- a/frontend/src/components/settings/CountersSettingsTab.tsx +++ b/frontend/src/components/settings/CountersSettingsTab.tsx @@ -64,12 +64,8 @@ export function CountersSettingsTab({ isOwner }: Props) { const loadData = useCallback(async () => { try { const [settingRes, countersRes] = await Promise.all([ - api - .get('/settings/multi_counter_enabled') - .catch(() => ({ data: { setting: { value: 'false' } } })), - api - .get('/counters', { params: { include_inactive: 'true' } }) - .catch(() => ({ data: { counters: [] } })), + api.get('/settings/multi_counter_enabled'), + api.get('/counters', { params: { include_inactive: 'true' } }), ]); setEnabled(settingRes.data?.setting?.value === 'true'); setCounters(countersRes.data?.counters || []); @@ -83,12 +79,8 @@ export function CountersSettingsTab({ isOwner }: Props) { useEffect(() => { let cancelled = false; Promise.all([ - api - .get('/settings/multi_counter_enabled') - .catch(() => ({ data: { setting: { value: 'false' } } })), - api - .get('/counters', { params: { include_inactive: 'true' } }) - .catch(() => ({ data: { counters: [] } })), + api.get('/settings/multi_counter_enabled'), + api.get('/counters', { params: { include_inactive: 'true' } }), ]) .then(([settingRes, countersRes]) => { if (cancelled) return; From 60c794b5aa14a19fb5a86cad2307cce938a66bd4 Mon Sep 17 00:00:00 2001 From: khaira777 <777gurkirat@gmail.com> Date: Sat, 7 Nov 2026 13:36:47 -0500 Subject: [PATCH 2/2] fix(settings): preserve partial counter loads and disable controls on load error --- frontend/e2e/settings-request-budget.spec.ts | 2 +- .../settings/CountersSettingsTab.tsx | 63 +++++++++++++++---- 2 files changed, 52 insertions(+), 13 deletions(-) diff --git a/frontend/e2e/settings-request-budget.spec.ts b/frontend/e2e/settings-request-budget.spec.ts index 48dd04fe5..a41e2077d 100644 --- a/frontend/e2e/settings-request-budget.spec.ts +++ b/frontend/e2e/settings-request-budget.spec.ts @@ -3209,5 +3209,5 @@ test('Counters tab reports a failed counters load instead of showing an empty li }); await page.goto(`${BASE}/settings?tab=counters`); - await expect(page.getByText('Failed to load counters.', { exact: true })).toBeVisible(); + await expect(page.getByText('Failed to load counters.', { exact: true }).first()).toBeVisible(); }); diff --git a/frontend/src/components/settings/CountersSettingsTab.tsx b/frontend/src/components/settings/CountersSettingsTab.tsx index a7767bbcc..faba71146 100644 --- a/frontend/src/components/settings/CountersSettingsTab.tsx +++ b/frontend/src/components/settings/CountersSettingsTab.tsx @@ -48,6 +48,7 @@ export function CountersSettingsTab({ isOwner }: Props) { const [savingEnabled, setSavingEnabled] = useState(false); const [counters, setCounters] = useState([]); const [loading, setLoading] = useState(true); + const [loadError, setLoadError] = useState(false); // Add / Edit Modal state const [modalOpen, setModalOpen] = useState(false); @@ -62,14 +63,30 @@ export function CountersSettingsTab({ isOwner }: Props) { const [deletingId, setDeletingId] = useState(null); const loadData = useCallback(async () => { + setLoading(true); try { - const [settingRes, countersRes] = await Promise.all([ - api.get('/settings/multi_counter_enabled'), + const [settingResult, countersResult] = await Promise.allSettled([ + api.get('/settings/multi_counter_enabled').catch((err: unknown) => { + if ((err as { response?: { status?: number } })?.response?.status === 404) { + return { data: { setting: { value: 'false' } } }; + } + throw err; + }), api.get('/counters', { params: { include_inactive: 'true' } }), ]); - setEnabled(settingRes.data?.setting?.value === 'true'); - setCounters(countersRes.data?.counters || []); + if (settingResult.status === 'fulfilled') { + setEnabled(settingResult.value.data?.setting?.value === 'true'); + } + if (countersResult.status === 'fulfilled') { + setCounters(countersResult.value.data?.counters || []); + } + const failed = settingResult.status === 'rejected' || countersResult.status === 'rejected'; + setLoadError(failed); + if (failed) { + toast.error(t('counterLoadFailed')); + } } catch { + setLoadError(true); toast.error(t('counterLoadFailed')); } finally { setLoading(false); @@ -78,18 +95,33 @@ export function CountersSettingsTab({ isOwner }: Props) { useEffect(() => { let cancelled = false; - Promise.all([ - api.get('/settings/multi_counter_enabled'), + Promise.allSettled([ + api.get('/settings/multi_counter_enabled').catch((err: unknown) => { + if ((err as { response?: { status?: number } })?.response?.status === 404) { + return { data: { setting: { value: 'false' } } }; + } + throw err; + }), api.get('/counters', { params: { include_inactive: 'true' } }), ]) - .then(([settingRes, countersRes]) => { + .then(([settingResult, countersResult]) => { if (cancelled) return; - setEnabled(settingRes.data?.setting?.value === 'true'); - setCounters(countersRes.data?.counters || []); + if (settingResult.status === 'fulfilled') { + setEnabled(settingResult.value.data?.setting?.value === 'true'); + } + if (countersResult.status === 'fulfilled') { + setCounters(countersResult.value.data?.counters || []); + } + const failed = settingResult.status === 'rejected' || countersResult.status === 'rejected'; + setLoadError(failed); + if (failed) { + toast.error(t('counterLoadFailed')); + } setLoading(false); }) .catch(() => { if (cancelled) return; + setLoadError(true); toast.error(t('counterLoadFailed')); setLoading(false); }); @@ -219,7 +251,7 @@ export function CountersSettingsTab({ isOwner }: Props) { @@ -227,7 +259,7 @@ export function CountersSettingsTab({ isOwner }: Props) { {/* Counter management section */} - {enabled && ( + {(enabled || loadError) && (
@@ -236,7 +268,7 @@ export function CountersSettingsTab({ isOwner }: Props) {
+ ) : loadError ? ( +
+

{t('counterLoadFailed')}

+ +
) : counters.length === 0 ? (

{t('noCountersConfigured')}