diff --git a/frontend/src/components/FleetView.tsx b/frontend/src/components/FleetView.tsx index 4c8b604a..d1175e12 100644 --- a/frontend/src/components/FleetView.tsx +++ b/frontend/src/components/FleetView.tsx @@ -9,6 +9,7 @@ import { } from 'lucide-react'; import { FleetMasthead } from './fleet/FleetMasthead'; import { FleetTopology } from './fleet/FleetTopology'; +import { ReconnectingOverlay } from './FleetView/ReconnectingOverlay'; import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Skeleton } from '@/components/ui/skeleton'; @@ -353,81 +354,6 @@ function UpdateStatusBadge({ status, error, onRetry, onDismiss }: { return null; } -interface ReconnectingOverlayProps { - /** Gateway boot timestamp captured pre-update. Null falls back to offline-then-online detection. */ - preUpdateStartedAt: number | null; -} - -function ReconnectingOverlay({ preUpdateStartedAt }: ReconnectingOverlayProps) { - const [elapsed, setElapsed] = useState(0); - const timedOut = elapsed >= 300; // 5 minutes - - useEffect(() => { - const timer = setInterval(() => setElapsed(s => s + 1), 1000); - return () => clearInterval(timer); - }, []); - - useEffect(() => { - if (timedOut) return; - let sawOffline = false; - const poll = setInterval(async () => { - try { - const res = await fetch('/api/health'); - if (!res.ok) { - sawOffline = true; - return; - } - const data = await res.json().catch(() => null) as { startedAt?: number } | null; - const currentStartedAt = typeof data?.startedAt === 'number' ? data.startedAt : null; - - if (preUpdateStartedAt !== null && currentStartedAt !== null) { - if (currentStartedAt !== preUpdateStartedAt) { - window.location.reload(); - } - return; - } - - // Fallback when we don't know the original startedAt: require an offline - // response first so we don't reload while the old process is still mid-pull. - if (sawOffline) { - window.location.reload(); - } - } catch { - sawOffline = true; - } - }, 3000); - return () => clearInterval(poll); - }, [timedOut, preUpdateStartedAt]); - - return ( -
-
- {timedOut ? ( - <> - -

Update timed out

-

- The server has not come back within 5 minutes. Check the Docker host directly. -

- - - ) : ( - <> - -

Updating Sencho...

-

- The server is pulling the latest image and restarting. This page will reload automatically. -

-

{elapsed}s elapsed

- - )} -
-
- ); -} - interface NodeCardProps { node: FleetNode; onNavigate: (nodeId: number, stackName: string) => void; diff --git a/frontend/src/components/FleetView/ReconnectingOverlay.tsx b/frontend/src/components/FleetView/ReconnectingOverlay.tsx new file mode 100644 index 00000000..eb35102e --- /dev/null +++ b/frontend/src/components/FleetView/ReconnectingOverlay.tsx @@ -0,0 +1,78 @@ +import { useState, useEffect } from 'react'; +import { AlertTriangle, Loader2 } from 'lucide-react'; +import { Button } from '@/components/ui/button'; + +interface ReconnectingOverlayProps { + /** Gateway boot timestamp captured pre-update. Null falls back to offline-then-online detection. */ + preUpdateStartedAt: number | null; +} + +export function ReconnectingOverlay({ preUpdateStartedAt }: ReconnectingOverlayProps) { + const [elapsed, setElapsed] = useState(0); + const timedOut = elapsed >= 300; // 5 minutes + + useEffect(() => { + const timer = setInterval(() => setElapsed(s => s + 1), 1000); + return () => clearInterval(timer); + }, []); + + useEffect(() => { + if (timedOut) return; + let sawOffline = false; + const poll = setInterval(async () => { + try { + const res = await fetch('/api/health'); + if (!res.ok) { + sawOffline = true; + return; + } + const data = await res.json().catch(() => null) as { startedAt?: number } | null; + const currentStartedAt = typeof data?.startedAt === 'number' ? data.startedAt : null; + + if (preUpdateStartedAt !== null && currentStartedAt !== null) { + if (currentStartedAt !== preUpdateStartedAt) { + window.location.reload(); + } + return; + } + + // Fallback when we don't know the original startedAt: require an offline + // response first so we don't reload while the old process is still mid-pull. + if (sawOffline) { + window.location.reload(); + } + } catch { + sawOffline = true; + } + }, 3000); + return () => clearInterval(poll); + }, [timedOut, preUpdateStartedAt]); + + return ( +
+
+ {timedOut ? ( + <> + +

Update timed out

+

+ The server has not come back within 5 minutes. Check the Docker host directly. +

+ + + ) : ( + <> + +

Updating Sencho...

+

+ The server is pulling the latest image and restarting. This page will reload automatically. +

+

{elapsed}s elapsed

+ + )} +
+
+ ); +}