From 6ff5fb7583c4ad7a1ca17bf16d13666ad6252a14 Mon Sep 17 00:00:00 2001 From: Adam Shiervani Date: Tue, 14 Oct 2025 15:52:55 +0200 Subject: [PATCH] feat: implement RebootingOverlay component to handle device reboot --- ui/src/components/VideoOverlay.tsx | 201 ++++++++++++++++++++++++++++- 1 file changed, 200 insertions(+), 1 deletion(-) diff --git a/ui/src/components/VideoOverlay.tsx b/ui/src/components/VideoOverlay.tsx index 1c59e788..27711485 100644 --- a/ui/src/components/VideoOverlay.tsx +++ b/ui/src/components/VideoOverlay.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import React, { useEffect, useState, useRef } from "react"; import { ExclamationTriangleIcon } from "@heroicons/react/24/solid"; import { ArrowPathIcon, ArrowRightIcon } from "@heroicons/react/16/solid"; import { motion, AnimatePresence } from "framer-motion"; @@ -8,6 +8,11 @@ import { BsMouseFill } from "react-icons/bs"; import { Button, LinkButton } from "@components/Button"; import LoadingSpinner from "@components/LoadingSpinner"; import Card, { GridCard } from "@components/Card"; +import { useRTCStore } from "@/hooks/stores"; +import LogoBlue from "@/assets/logo-blue.svg"; +import LogoWhite from "@/assets/logo-white.svg"; +import { isOnDevice } from "@/main"; + interface OverlayContentProps { readonly children: React.ReactNode; @@ -392,3 +397,197 @@ export function PointerLockBar({ show }: PointerLockBarProps) { ); } + +interface RebootingOverlayProps { + readonly show: boolean; + readonly suggestedIp: string | null; +} + +export function RebootingOverlay({ show, suggestedIp }: RebootingOverlayProps) { + const { peerConnectionState } = useRTCStore(); + + // Check if we've already seen the connection drop (confirms reboot actually started) + const [hasSeenDisconnect, setHasSeenDisconnect] = useState( + ['disconnected', 'closed', 'failed'].includes(peerConnectionState ?? '') + ); + + // Track if we've timed out + const [hasTimedOut, setHasTimedOut] = useState(false); + + // Monitor for disconnect after reboot is initiated + useEffect(() => { + if (!show) return; + if (hasSeenDisconnect) return; + + if (['disconnected', 'closed', 'failed'].includes(peerConnectionState ?? '')) { + console.log('hasSeenDisconnect', hasSeenDisconnect); + setHasSeenDisconnect(true); + } + }, [show, peerConnectionState, hasSeenDisconnect]); + + // Set timeout after 30 seconds + useEffect(() => { + if (!show) { + setHasTimedOut(false); + return; + } + + const timeoutId = setTimeout(() => { + setHasTimedOut(true); + }, 30 * 1000); + + return () => { + clearTimeout(timeoutId); + }; + }, [show]); + + + // Poll suggested IP in device mode to detect when it's available + const abortControllerRef = useRef(null); + const isFetchingRef = useRef(false); + + useEffect(() => { + // Only run in device mode with a suggested IP + if (!isOnDevice || !suggestedIp || !show || !hasSeenDisconnect) { + return; + } + + const checkSuggestedIp = async () => { + // Don't start a new fetch if one is already in progress + if (isFetchingRef.current) { + return; + } + + // Cancel any pending fetch + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + } + + // Create new abort controller for this fetch + const abortController = new AbortController(); + abortControllerRef.current = abortController; + isFetchingRef.current = true; + + console.log('Checking suggested IP:', suggestedIp); + const timeoutId = window.setTimeout(() => abortController.abort(), 2000); + try { + const response = await fetch( + `${window.location.protocol}//${suggestedIp}/device/status`, + { + signal: abortController.signal, + } + ); + + if (response.ok) { + // Device is available at the new IP, redirect to it + console.log('Device is available at the new IP, redirecting to it'); + window.location.href = `${window.location.protocol}//${suggestedIp}`; + } + } catch (err) { + // Ignore errors - they're expected while device is rebooting + // Only log if it's not an abort error + if (err instanceof Error && err.name !== 'AbortError') { + console.debug('Error checking suggested IP:', err); + } + } finally { + clearTimeout(timeoutId); + isFetchingRef.current = false; + } + }; + + // Start interval (check every 2 seconds) + const intervalId = setInterval(checkSuggestedIp, 2000); + + // Also check immediately + checkSuggestedIp(); + + // Cleanup on unmount or when dependencies change + return () => { + clearInterval(intervalId); + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + } + isFetchingRef.current = false; + }; + }, [show, suggestedIp, hasTimedOut, hasSeenDisconnect]); + + return ( + + {show && ( + + + +
+
+ + +
+
+
+
+

{hasTimedOut ? "Unable to Reconnect" : "Device is Rebooting"}

+

+ {hasTimedOut ? ( + <> + The device may have restarted with a different IP address. Check the JetKVM's physical display to find the current IP address and reconnect. + + ) : ( + <> + Please wait while the device restarts. This usually takes 30-60 seconds. + {suggestedIp && ( + <> + {" "}If reconnection fails, the device may be at{" "} + + {suggestedIp} + + . + + )} + + )} +

+
+
+ +
+ {!hasTimedOut ? ( + <> + +

+ Waiting for device to restart... +

+ + ) : ( +
+
+ +

+ Automatic Reconnection Timed Out +

+
+
+ )} +
+
+
+
+
+
+
+
+ )} +
+ ); +}