From b9cbc0c1e81fbafb2e931efc54764aeb2c6b4d4c Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Mon, 1 Jun 2026 07:43:32 +0200 Subject: [PATCH] fix(ui): exit infinite spinner with a retry card on failed initial load List pages wrapped content in where 'fetched' only flipped true once data arrived. With staleTime: Infinity + retry: 1, a transient network error on first load left the query in a permanent error state and the spinner stuck forever. Now 'fetched' also settles on query.isError, and a failed load shows a Result error card with a Refresh button that self-heals when the backend returns, mirroring the existing XrayPage pattern. Applied to clients, inbounds, groups, nodes, and the dashboard. Fixes #4723 --- frontend/src/api/queries/useNodesQuery.ts | 4 +++- frontend/src/api/queries/useStatusQuery.ts | 3 ++- frontend/src/hooks/useClients.ts | 4 +++- frontend/src/pages/clients/ClientsPage.tsx | 11 ++++++++++- frontend/src/pages/groups/GroupsPage.tsx | 11 ++++++++++- frontend/src/pages/inbounds/InboundsPage.tsx | 10 ++++++++++ frontend/src/pages/inbounds/useInbounds.ts | 5 ++++- frontend/src/pages/index/IndexPage.tsx | 10 +++++++++- frontend/src/pages/nodes/NodesPage.tsx | 11 +++++++++-- 9 files changed, 60 insertions(+), 9 deletions(-) diff --git a/frontend/src/api/queries/useNodesQuery.ts b/frontend/src/api/queries/useNodesQuery.ts index a916fd61..fc5ca3a9 100644 --- a/frontend/src/api/queries/useNodesQuery.ts +++ b/frontend/src/api/queries/useNodesQuery.ts @@ -76,6 +76,8 @@ export function useNodesQuery() { nodes, totals, loading: query.isFetching, - fetched: query.data !== undefined, + fetched: query.data !== undefined || query.isError, + fetchError: query.error ? (query.error as Error).message : '', + refetch: query.refetch, }; } diff --git a/frontend/src/api/queries/useStatusQuery.ts b/frontend/src/api/queries/useStatusQuery.ts index 755b73aa..e02e62e2 100644 --- a/frontend/src/api/queries/useStatusQuery.ts +++ b/frontend/src/api/queries/useStatusQuery.ts @@ -30,7 +30,8 @@ export function useStatusQuery() { return { status, - fetched: query.data !== undefined, + fetched: query.data !== undefined || query.isError, + fetchError: query.error ? (query.error as Error).message : '', refresh, }; } diff --git a/frontend/src/hooks/useClients.ts b/frontend/src/hooks/useClients.ts index 51b418c6..99b22cfa 100644 --- a/frontend/src/hooks/useClients.ts +++ b/frontend/src/hooks/useClients.ts @@ -213,7 +213,8 @@ export function useClients() { const total = listQuery.data?.total ?? 0; const filtered = listQuery.data?.filtered ?? 0; const allGroups = listQuery.data?.groups ?? []; - const fetched = listQuery.data !== undefined; + const fetched = listQuery.data !== undefined || listQuery.isError; + const fetchError = listQuery.error ? (listQuery.error as Error).message : ''; const loading = listQuery.isFetching; const inbounds = inboundOptionsQuery.data ?? []; @@ -532,6 +533,7 @@ export function useClients() { onlines, loading, fetched, + fetchError, subSettings, ipLimitEnable, tgBotEnable, diff --git a/frontend/src/pages/clients/ClientsPage.tsx b/frontend/src/pages/clients/ClientsPage.tsx index adb8f323..68118548 100644 --- a/frontend/src/pages/clients/ClientsPage.tsx +++ b/frontend/src/pages/clients/ClientsPage.tsx @@ -13,6 +13,7 @@ import { Modal, Pagination, Popover, + Result, Row, Select, Space, @@ -191,11 +192,12 @@ export default function ClientsPage() { summary: serverSummary, allGroups, setQuery, - inbounds, onlines, loading, fetched, subSettings, + inbounds, onlines, loading, fetched, fetchError, subSettings, ipLimitEnable, tgBotEnable, expireDiff, trafficDiff, pageSize, create, update, remove, bulkDelete, bulkAdjust, bulkAddToGroup, bulkRemoveFromGroup, attach, bulkAttach, detach, bulkDetach, resetTraffic, resetAllTraffics, delDepleted, setEnable, applyTrafficEvent, applyClientStatsEvent, + refresh, hydrate, } = useClients(); @@ -795,6 +797,13 @@ export default function ClientsPage() { {!fetched ? (
+ ) : fetchError ? ( + {t('refresh')}} + /> ) : ( diff --git a/frontend/src/pages/groups/GroupsPage.tsx b/frontend/src/pages/groups/GroupsPage.tsx index 4b1882cd..94b8357f 100644 --- a/frontend/src/pages/groups/GroupsPage.tsx +++ b/frontend/src/pages/groups/GroupsPage.tsx @@ -10,6 +10,7 @@ import { Input, Layout, Modal, + Result, Row, Space, Spin, @@ -97,7 +98,8 @@ export default function GroupsPage() { }); const groups = useMemo(() => groupsQuery.data ?? [], [groupsQuery.data]); const loading = groupsQuery.isFetching; - const fetched = groupsQuery.data !== undefined; + const fetched = groupsQuery.data !== undefined || groupsQuery.isError; + const fetchError = groupsQuery.error ? (groupsQuery.error as Error).message : ''; const invalidate = useCallback(() => { queryClient.invalidateQueries({ queryKey: keys.clients.root() }); @@ -435,6 +437,13 @@ export default function GroupsPage() { {!fetched ? (
+ ) : fetchError ? ( + groupsQuery.refetch()}>{t('refresh')}} + /> ) : ( diff --git a/frontend/src/pages/inbounds/InboundsPage.tsx b/frontend/src/pages/inbounds/InboundsPage.tsx index 0f3580ac..1ab35a31 100644 --- a/frontend/src/pages/inbounds/InboundsPage.tsx +++ b/frontend/src/pages/inbounds/InboundsPage.tsx @@ -1,11 +1,13 @@ import { lazy, useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { + Button, Card, Col, ConfigProvider, Layout, Modal, + Result, Row, Spin, Statistic, @@ -74,6 +76,7 @@ export default function InboundsPage() { const { fetched, + fetchError, dbInbounds, clientCount, onlineClients, @@ -559,6 +562,13 @@ export default function InboundsPage() { {!fetched ? (
+ ) : fetchError ? ( + {t('refresh')}} + /> ) : ( diff --git a/frontend/src/pages/inbounds/useInbounds.ts b/frontend/src/pages/inbounds/useInbounds.ts index 80853ba1..c949cd90 100644 --- a/frontend/src/pages/inbounds/useInbounds.ts +++ b/frontend/src/pages/inbounds/useInbounds.ts @@ -248,7 +248,9 @@ export function useInbounds() { if (lastOnlineQuery.data) setLastOnlineMap(lastOnlineQuery.data); }, [lastOnlineQuery.data]); - const fetched = slimQuery.data !== undefined && defaultsQuery.data !== undefined; + const fetched = (slimQuery.data !== undefined || slimQuery.isError) && (defaultsQuery.data !== undefined || defaultsQuery.isError); + const fetchErrorSource = slimQuery.error || defaultsQuery.error; + const fetchError = fetchErrorSource ? (fetchErrorSource as Error).message : ''; const refresh = useCallback(async () => { // Invalidate at the inbounds root so both `slim` (this page's list) @@ -373,6 +375,7 @@ export function useInbounds() { return { fetched, + fetchError, dbInbounds, clientCount, onlineClients, diff --git a/frontend/src/pages/index/IndexPage.tsx b/frontend/src/pages/index/IndexPage.tsx index 759f3127..ada2a629 100644 --- a/frontend/src/pages/index/IndexPage.tsx +++ b/frontend/src/pages/index/IndexPage.tsx @@ -8,6 +8,7 @@ import { Layout, message, Modal, + Result, Row, Space, Spin, @@ -58,7 +59,7 @@ import './IndexPage.css'; export default function IndexPage() { const { t } = useTranslation(); const { isDark, isUltra, antdThemeConfig } = useTheme(); - const { status, fetched, refresh } = useStatusQuery(); + const { status, fetched, fetchError, refresh } = useStatusQuery(); const { isMobile } = useMediaQuery(); const [messageApi, messageContextHolder] = message.useMessage(); useEffect(() => { setMessageInstance(messageApi); }, [messageApi]); @@ -168,6 +169,13 @@ export default function IndexPage() { > {!fetched ? (
+ ) : fetchError ? ( + {t('refresh')}} + /> ) : ( diff --git a/frontend/src/pages/nodes/NodesPage.tsx b/frontend/src/pages/nodes/NodesPage.tsx index 219834cd..c9dc5528 100644 --- a/frontend/src/pages/nodes/NodesPage.tsx +++ b/frontend/src/pages/nodes/NodesPage.tsx @@ -1,7 +1,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useQuery } from '@tanstack/react-query'; -import { Card, Col, ConfigProvider, Layout, Modal, Row, Spin, Statistic, message } from 'antd'; +import { Button, Card, Col, ConfigProvider, Layout, Modal, Result, Row, Spin, Statistic, message } from 'antd'; import { CheckCircleOutlined, CloseCircleOutlined, @@ -29,7 +29,7 @@ export default function NodesPage() { const [messageApi, messageContextHolder] = message.useMessage(); useEffect(() => { setMessageInstance(messageApi); }, [messageApi]); - const { nodes, loading, fetched, totals } = useNodesQuery(); + const { nodes, loading, fetched, fetchError, refetch, totals } = useNodesQuery(); const { create, update, remove, setEnable, testConnection, probe, updatePanels } = useNodeMutations(); const { data: latestVersion = '' } = useQuery({ @@ -159,6 +159,13 @@ export default function NodesPage() { {!fetched ? (
+ ) : fetchError ? ( + refetch()}>{t('refresh')}} + /> ) : (