fix(login): frontend web auth error handle
This commit is contained in:
parent
0809b4d416
commit
140acd5852
4 changed files with 71 additions and 34 deletions
|
|
@ -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,37 +198,50 @@ 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>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Tooltip delayDuration={(gwError || (!canStart && startReason)) ? 0 : 700}>
|
||||||
variant={
|
<TooltipTrigger asChild>
|
||||||
isStarting || isRestarting || isStopping ? "secondary" : "default"
|
{/* Wrap in span so the tooltip still fires when the button is disabled */}
|
||||||
}
|
<span
|
||||||
size="sm"
|
className={!canStart && startReason ? "cursor-not-allowed" : undefined}
|
||||||
data-tour="gateway-button"
|
tabIndex={!canStart && startReason ? 0 : undefined}
|
||||||
className={`h-8 gap-2 px-3 ${isStopped ? "bg-green-500 text-white hover:bg-green-600" : ""
|
>
|
||||||
}`}
|
<Button
|
||||||
onClick={handleGatewayToggle}
|
variant={
|
||||||
disabled={
|
isStarting || isRestarting || isStopping ? "secondary" : "default"
|
||||||
gwLoading || isStarting || isRestarting || isStopping || !canStart
|
}
|
||||||
}
|
size="sm"
|
||||||
>
|
data-tour="gateway-button"
|
||||||
{gwLoading || isStarting || isRestarting || isStopping ? (
|
className={`h-8 gap-2 px-3 ${isStopped ? "bg-green-500 text-white hover:bg-green-600" : ""
|
||||||
<IconLoader2 className="h-4 w-4 animate-spin opacity-70" />
|
} ${!canStart ? "pointer-events-none" : ""}`}
|
||||||
) : (
|
onClick={handleGatewayToggle}
|
||||||
<IconPlayerPlay className="h-4 w-4 opacity-80" />
|
disabled={
|
||||||
)}
|
gwLoading || isStarting || isRestarting || isStopping || !canStart
|
||||||
<span className="text-xs font-semibold">
|
}
|
||||||
{isStopping
|
>
|
||||||
? t("header.gateway.status.stopping")
|
{gwLoading || isStarting || isRestarting || isStopping ? (
|
||||||
: isRestarting
|
<IconLoader2 className="h-4 w-4 animate-spin opacity-70" />
|
||||||
? t("header.gateway.status.restarting")
|
) : (
|
||||||
: isStarting
|
<IconPlayerPlay className="h-4 w-4 opacity-80" />
|
||||||
? t("header.gateway.status.starting")
|
)}
|
||||||
: t("header.gateway.action.start")}
|
<span className="text-xs font-semibold">
|
||||||
</span>
|
{isStopping
|
||||||
</Button>
|
? t("header.gateway.status.stopping")
|
||||||
|
: isRestarting
|
||||||
|
? t("header.gateway.status.restarting")
|
||||||
|
: isStarting
|
||||||
|
? t("header.gateway.status.starting")
|
||||||
|
: t("header.gateway.action.start")}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
{(gwError || (!canStart && startReason)) ? (
|
||||||
|
<TooltipContent>{gwError ?? startReason}</TooltipContent>
|
||||||
|
) : null}
|
||||||
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Separator
|
<Separator
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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])
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue