fix(login): frontend web auth error handle

This commit is contained in:
sky5454 2026-04-07 22:34:28 +08:00
parent 0809b4d416
commit 140acd5852
4 changed files with 71 additions and 34 deletions

View file

@ -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,9 +198,16 @@ export function AppHeader() {
<IconPower className="h-4 w-4 opacity-80" />
</Button>
</TooltipTrigger>
<TooltipContent>{t("header.gateway.action.stop")}</TooltipContent>
<TooltipContent>{gwError ?? t("header.gateway.action.stop")}</TooltipContent>
</Tooltip>
) : (
<Tooltip delayDuration={(gwError || (!canStart && startReason)) ? 0 : 700}>
<TooltipTrigger asChild>
{/* Wrap in span so the tooltip still fires when the button is disabled */}
<span
className={!canStart && startReason ? "cursor-not-allowed" : undefined}
tabIndex={!canStart && startReason ? 0 : undefined}
>
<Button
variant={
isStarting || isRestarting || isStopping ? "secondary" : "default"
@ -206,7 +215,7 @@ export function AppHeader() {
size="sm"
data-tour="gateway-button"
className={`h-8 gap-2 px-3 ${isStopped ? "bg-green-500 text-white hover:bg-green-600" : ""
}`}
} ${!canStart ? "pointer-events-none" : ""}`}
onClick={handleGatewayToggle}
disabled={
gwLoading || isStarting || isRestarting || isStopping || !canStart
@ -227,6 +236,12 @@ export function AppHeader() {
: t("header.gateway.action.start")}
</span>
</Button>
</span>
</TooltipTrigger>
{(gwError || (!canStart && startReason)) ? (
<TooltipContent>{gwError ?? startReason}</TooltipContent>
) : null}
</Tooltip>
)}
<Separator

View file

@ -13,8 +13,9 @@ import {
export function useGateway() {
const gateway = useAtomValue(gatewayAtom)
const { status: state, canStart, restartRequired } = gateway
const { status: state, canStart, startReason, restartRequired } = gateway
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | 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 }
}

View file

@ -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])

View file

@ -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