From 140acd5852e29bfac3bca040cef3bfe0e11732ca Mon Sep 17 00:00:00 2001 From: sky5454 Date: Tue, 7 Apr 2026 22:34:28 +0800 Subject: [PATCH] fix(login): frontend web auth error handle --- web/frontend/src/components/app-header.tsx | 73 +++++++++++++--------- web/frontend/src/hooks/use-gateway.ts | 11 +++- web/frontend/src/routes/__root.tsx | 10 ++- web/frontend/src/store/gateway.ts | 11 +++- 4 files changed, 71 insertions(+), 34 deletions(-) diff --git a/web/frontend/src/components/app-header.tsx b/web/frontend/src/components/app-header.tsx index 1a673f598..798ac8ad5 100644 --- a/web/frontend/src/components/app-header.tsx +++ b/web/frontend/src/components/app-header.tsx @@ -49,10 +49,12 @@ export function AppHeader() { state: gwState, loading: gwLoading, canStart, + startReason, restartRequired, start, restart, stop, + error: gwError, } = useGateway() const isRunning = gwState === "running" @@ -196,37 +198,50 @@ export function AppHeader() { - {t("header.gateway.action.stop")} + {gwError ?? t("header.gateway.action.stop")} ) : ( - + + + {/* Wrap in span so the tooltip still fires when the button is disabled */} + + + + + {(gwError || (!canStart && startReason)) ? ( + {gwError ?? startReason} + ) : null} + )} (null) useEffect(() => { return subscribeGatewayPolling() @@ -23,6 +24,7 @@ export function useGateway() { const start = useCallback(async () => { if (!canStart) return + setError(null) setLoading(true) try { await startGateway() @@ -32,6 +34,7 @@ export function useGateway() { }) } catch (err) { console.error("Failed to start gateway:", err) + setError(err instanceof Error ? err.message : String(err)) } finally { await refreshGatewayState({ force: true }) setLoading(false) @@ -39,12 +42,14 @@ export function useGateway() { }, [canStart]) const stop = useCallback(async () => { + setError(null) setLoading(true) beginGatewayStoppingTransition() try { await stopGateway() } catch (err) { console.error("Failed to stop gateway:", err) + setError(err instanceof Error ? err.message : String(err)) cancelGatewayStoppingTransition() } finally { await refreshGatewayState({ force: true }) @@ -55,6 +60,7 @@ export function useGateway() { const restart = useCallback(async () => { if (state !== "running") return + setError(null) setLoading(true) try { await restartGateway() @@ -64,11 +70,12 @@ export function useGateway() { }) } catch (err) { console.error("Failed to restart gateway:", err) + setError(err instanceof Error ? err.message : String(err)) } finally { await refreshGatewayState({ force: true }) setLoading(false) } }, [state]) - return { state, loading, canStart, restartRequired, start, stop, restart } + return { state, loading, canStart, startReason, restartRequired, start, stop, restart, error } } diff --git a/web/frontend/src/routes/__root.tsx b/web/frontend/src/routes/__root.tsx index 33ad8c698..d287ce801 100644 --- a/web/frontend/src/routes/__root.tsx +++ b/web/frontend/src/routes/__root.tsx @@ -42,8 +42,14 @@ const RootLayout = () => { globalThis.location.assign("/launcher-login") } }) - .catch(() => { - // Network error or 401 — launcherFetch will handle redirect on real API calls. + .catch((err: unknown) => { + // For real HTTP errors (e.g. 503 when the auth store is unavailable), + // redirect to login so the user can re-authenticate rather than being + // silently stranded on the dashboard. Network failures (no response) + // are left alone — launcherFetch handles the 401 redirect on real calls. + if (err instanceof Error && /^status \d+$/.test(err.message)) { + globalThis.location.assign("/launcher-login") + } }) }, [isAuthPage]) diff --git a/web/frontend/src/store/gateway.ts b/web/frontend/src/store/gateway.ts index 1bdec6220..5bf6f3897 100644 --- a/web/frontend/src/store/gateway.ts +++ b/web/frontend/src/store/gateway.ts @@ -14,6 +14,7 @@ export type GatewayState = export interface GatewayStoreState { status: GatewayState canStart: boolean + startReason?: string restartRequired: boolean } @@ -57,6 +58,7 @@ function normalizeGatewayStoreState( if ( next.status === prev.status && next.canStart === prev.canStart && + next.startReason === prev.startReason && next.restartRequired === prev.restartRequired ) { return prev @@ -108,7 +110,10 @@ export function applyGatewayStatusToStore( data: Partial< Pick< GatewayStatusResponse, - "gateway_status" | "gateway_start_allowed" | "gateway_restart_required" + | "gateway_status" + | "gateway_start_allowed" + | "gateway_start_reason" + | "gateway_restart_required" > >, ) { @@ -121,6 +126,10 @@ export function applyGatewayStatusToStore( prev.status === "stopping" && data.gateway_status === "running" ? false : (data.gateway_start_allowed ?? prev.canStart), + startReason: + prev.status === "stopping" && data.gateway_status === "running" + ? prev.startReason + : (data.gateway_start_reason ?? prev.startReason), restartRequired: prev.status === "stopping" && data.gateway_status === "running" ? false