diff --git a/frontend/e2e/settings-request-budget.spec.ts b/frontend/e2e/settings-request-budget.spec.ts index 3019a0d64..9fe38132f 100644 --- a/frontend/e2e/settings-request-budget.spec.ts +++ b/frontend/e2e/settings-request-budget.spec.ts @@ -3219,3 +3219,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 }).first()).toBeVisible(); +}); diff --git a/frontend/src/components/settings/CountersSettingsTab.tsx b/frontend/src/components/settings/CountersSettingsTab.tsx index c9288a8f6..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,18 +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') - .catch(() => ({ data: { setting: { value: 'false' } } })), - api - .get('/counters', { params: { include_inactive: 'true' } }) - .catch(() => ({ data: { counters: [] } })), + 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); @@ -82,22 +95,33 @@ 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: [] } })), + 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); }); @@ -227,7 +251,7 @@ export function CountersSettingsTab({ isOwner }: Props) { @@ -235,7 +259,7 @@ export function CountersSettingsTab({ isOwner }: Props) { {/* Counter management section */} - {enabled && ( + {(enabled || loadError) && (
@@ -244,7 +268,7 @@ export function CountersSettingsTab({ isOwner }: Props) {
+ ) : loadError ? ( +
+

{t('counterLoadFailed')}

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

{t('noCountersConfigured')}