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, state: gwState,
loading: gwLoading, loading: gwLoading,
canStart, canStart,
startReason,
restartRequired, restartRequired,
start, start,
restart, restart,
stop, stop,
error: gwError,
} = useGateway() } = useGateway()
const isRunning = gwState === "running" const isRunning = gwState === "running"
@ -196,9 +198,16 @@ export function AppHeader() {
<IconPower className="h-4 w-4 opacity-80" /> <IconPower className="h-4 w-4 opacity-80" />
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent>{t("header.gateway.action.stop")}</TooltipContent> <TooltipContent>{gwError ?? t("header.gateway.action.stop")}</TooltipContent>
</Tooltip> </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 <Button
variant={ variant={
isStarting || isRestarting || isStopping ? "secondary" : "default" isStarting || isRestarting || isStopping ? "secondary" : "default"
@ -206,7 +215,7 @@ export function AppHeader() {
size="sm" size="sm"
data-tour="gateway-button" data-tour="gateway-button"
className={`h-8 gap-2 px-3 ${isStopped ? "bg-green-500 text-white hover:bg-green-600" : "" className={`h-8 gap-2 px-3 ${isStopped ? "bg-green-500 text-white hover:bg-green-600" : ""
}`} } ${!canStart ? "pointer-events-none" : ""}`}
onClick={handleGatewayToggle} onClick={handleGatewayToggle}
disabled={ disabled={
gwLoading || isStarting || isRestarting || isStopping || !canStart gwLoading || isStarting || isRestarting || isStopping || !canStart
@ -227,6 +236,12 @@ export function AppHeader() {
: t("header.gateway.action.start")} : t("header.gateway.action.start")}
</span> </span>
</Button> </Button>
</span>
</TooltipTrigger>
{(gwError || (!canStart && startReason)) ? (
<TooltipContent>{gwError ?? startReason}</TooltipContent>
) : null}
</Tooltip>
)} )}
<Separator <Separator

View file

@ -13,8 +13,9 @@ import {
export function useGateway() { export function useGateway() {
const gateway = useAtomValue(gatewayAtom) const gateway = useAtomValue(gatewayAtom)
const { status: state, canStart, restartRequired } = gateway const { status: state, canStart, startReason, restartRequired } = gateway
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
useEffect(() => { useEffect(() => {
return subscribeGatewayPolling() return subscribeGatewayPolling()
@ -23,6 +24,7 @@ export function useGateway() {
const start = useCallback(async () => { const start = useCallback(async () => {
if (!canStart) return if (!canStart) return
setError(null)
setLoading(true) setLoading(true)
try { try {
await startGateway() await startGateway()
@ -32,6 +34,7 @@ export function useGateway() {
}) })
} catch (err) { } catch (err) {
console.error("Failed to start gateway:", err) console.error("Failed to start gateway:", err)
setError(err instanceof Error ? err.message : String(err))
} finally { } finally {
await refreshGatewayState({ force: true }) await refreshGatewayState({ force: true })
setLoading(false) setLoading(false)
@ -39,12 +42,14 @@ export function useGateway() {
}, [canStart]) }, [canStart])
const stop = useCallback(async () => { const stop = useCallback(async () => {
setError(null)
setLoading(true) setLoading(true)
beginGatewayStoppingTransition() beginGatewayStoppingTransition()
try { try {
await stopGateway() await stopGateway()
} catch (err) { } catch (err) {
console.error("Failed to stop gateway:", err) console.error("Failed to stop gateway:", err)
setError(err instanceof Error ? err.message : String(err))
cancelGatewayStoppingTransition() cancelGatewayStoppingTransition()
} finally { } finally {
await refreshGatewayState({ force: true }) await refreshGatewayState({ force: true })
@ -55,6 +60,7 @@ export function useGateway() {
const restart = useCallback(async () => { const restart = useCallback(async () => {
if (state !== "running") return if (state !== "running") return
setError(null)
setLoading(true) setLoading(true)
try { try {
await restartGateway() await restartGateway()
@ -64,11 +70,12 @@ export function useGateway() {
}) })
} catch (err) { } catch (err) {
console.error("Failed to restart gateway:", err) console.error("Failed to restart gateway:", err)
setError(err instanceof Error ? err.message : String(err))
} finally { } finally {
await refreshGatewayState({ force: true }) await refreshGatewayState({ force: true })
setLoading(false) setLoading(false)
} }
}, [state]) }, [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") globalThis.location.assign("/launcher-login")
} }
}) })
.catch(() => { .catch((err: unknown) => {
// Network error or 401 — launcherFetch will handle redirect on real API calls. // 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]) }, [isAuthPage])

View file

@ -14,6 +14,7 @@ export type GatewayState =
export interface GatewayStoreState { export interface GatewayStoreState {
status: GatewayState status: GatewayState
canStart: boolean canStart: boolean
startReason?: string
restartRequired: boolean restartRequired: boolean
} }
@ -57,6 +58,7 @@ function normalizeGatewayStoreState(
if ( if (
next.status === prev.status && next.status === prev.status &&
next.canStart === prev.canStart && next.canStart === prev.canStart &&
next.startReason === prev.startReason &&
next.restartRequired === prev.restartRequired next.restartRequired === prev.restartRequired
) { ) {
return prev return prev
@ -108,7 +110,10 @@ export function applyGatewayStatusToStore(
data: Partial< data: Partial<
Pick< Pick<
GatewayStatusResponse, 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" prev.status === "stopping" && data.gateway_status === "running"
? false ? false
: (data.gateway_start_allowed ?? prev.canStart), : (data.gateway_start_allowed ?? prev.canStart),
startReason:
prev.status === "stopping" && data.gateway_status === "running"
? prev.startReason
: (data.gateway_start_reason ?? prev.startReason),
restartRequired: restartRequired:
prev.status === "stopping" && data.gateway_status === "running" prev.status === "stopping" && data.gateway_status === "running"
? false ? false