From 2fce23c5d6b18037982de27fb02e455b11b1f562 Mon Sep 17 00:00:00 2001 From: Marc Brooks Date: Fri, 26 Sep 2025 23:36:12 -0500 Subject: [PATCH 1/7] Better reporting of and process for OTA updating --- main.go | 27 ++++++--- ota.go | 45 ++++++++------ ui/src/hooks/stores.ts | 16 ++++- .../devices.$id.settings.general.update.tsx | 60 +++++++++++++++---- 4 files changed, 108 insertions(+), 40 deletions(-) diff --git a/main.go b/main.go index e9931d46..ef6af293 100644 --- a/main.go +++ b/main.go @@ -14,6 +14,7 @@ import ( var appCtx context.Context func Main() { + logger.Log().Msg("JetKVM Starting Up") LoadConfig() var cancel context.CancelFunc @@ -78,16 +79,16 @@ func Main() { initDisplay() go func() { + // wait for 15 minutes before starting auto-update checks + // this is to avoid interfering with initial setup processes + // and to ensure the system is stable before checking for updates time.Sleep(15 * time.Minute) - for { - logger.Debug().Bool("auto_update_enabled", config.AutoUpdateEnabled).Msg("UPDATING") - if !config.AutoUpdateEnabled { - return - } - if isTimeSyncNeeded() || !timeSync.IsSyncSuccess() { - logger.Debug().Msg("system time is not synced, will retry in 30 seconds") - time.Sleep(30 * time.Second) + for { + logger.Info().Bool("auto_update_enabled", config.AutoUpdateEnabled).Msg("auto-update check") + if !config.AutoUpdateEnabled { + logger.Debug().Msg("auto-update disabled") + time.Sleep(5 * time.Minute) // we'll check if auto-updates are enabled in five minutes continue } @@ -97,6 +98,12 @@ func Main() { continue } + if isTimeSyncNeeded() || !timeSync.IsSyncSuccess() { + logger.Debug().Msg("system time is not synced, will retry in 30 seconds") + time.Sleep(30 * time.Second) + continue + } + includePreRelease := config.IncludePreRelease err = TryUpdate(context.Background(), GetDeviceID(), includePreRelease) if err != nil { @@ -106,6 +113,7 @@ func Main() { time.Sleep(1 * time.Hour) } }() + //go RunFuseServer() go RunWebServer() @@ -122,7 +130,8 @@ func Main() { sigs := make(chan os.Signal, 1) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) <-sigs - logger.Info().Msg("JetKVM Shutting Down") + + logger.Log().Msg("JetKVM Shutting Down") //if fuseServer != nil { // err := setMassStorageImage(" ") // if err != nil { diff --git a/ota.go b/ota.go index bf0828dc..62273732 100644 --- a/ota.go +++ b/ota.go @@ -176,7 +176,7 @@ func downloadFile(ctx context.Context, path string, url string, downloadProgress if nr > 0 { nw, ew := file.Write(buf[0:nr]) if nw < nr { - return fmt.Errorf("short write: %d < %d", nw, nr) + return fmt.Errorf("short file write: %d < %d", nw, nr) } written += int64(nw) if ew != nil { @@ -240,7 +240,7 @@ func verifyFile(path string, expectedHash string, verifyProgress *float32, scope if nr > 0 { nw, ew := hash.Write(buf[0:nr]) if nw < nr { - return fmt.Errorf("short write: %d < %d", nw, nr) + return fmt.Errorf("short hash write: %d < %d", nw, nr) } verified += int64(nw) if ew != nil { @@ -260,11 +260,14 @@ func verifyFile(path string, expectedHash string, verifyProgress *float32, scope } } - hashSum := hash.Sum(nil) - scopedLogger.Info().Str("path", path).Str("hash", hex.EncodeToString(hashSum)).Msg("SHA256 hash of") + // close the file so we can rename below + fileToHash.Close() - if hex.EncodeToString(hashSum) != expectedHash { - return fmt.Errorf("hash mismatch: %x != %s", hashSum, expectedHash) + hashSum := hex.EncodeToString(hash.Sum(nil)) + scopedLogger.Info().Str("path", path).Str("hash", hashSum).Msg("SHA256 hash of") + + if hashSum != expectedHash { + return fmt.Errorf("hash mismatch: %s != %s", hashSum, expectedHash) } if err := os.Rename(unverifiedPath, path); err != nil { @@ -296,6 +299,8 @@ type OTAState struct { AppUpdatedAt *time.Time `json:"appUpdatedAt,omitempty"` SystemUpdateProgress float32 `json:"systemUpdateProgress,omitempty"` //TODO: port rk_ota, then implement SystemUpdatedAt *time.Time `json:"systemUpdatedAt,omitempty"` + RebootNeeded bool `json:"rebootNeeded,omitempty"` + Rebooting bool `json:"rebooting,omitempty"` } var otaState = OTAState{} @@ -313,7 +318,7 @@ func triggerOTAStateUpdate() { func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) error { scopedLogger := otaLogger.With(). Str("deviceId", deviceId). - Str("includePreRelease", fmt.Sprintf("%v", includePreRelease)). + Bool("includePreRelease", includePreRelease). Logger() scopedLogger.Info().Msg("Trying to update...") @@ -322,7 +327,8 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err } otaState = OTAState{ - Updating: true, + Updating: true, + RebootNeeded: false, } triggerOTAStateUpdate() @@ -335,7 +341,7 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err if err != nil { otaState.Error = fmt.Sprintf("Error checking for updates: %v", err) scopedLogger.Error().Err(err).Msg("Error checking for updates") - return fmt.Errorf("error checking for updates: %w", err) + return err } now := time.Now() @@ -349,8 +355,6 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err appUpdateAvailable := updateStatus.AppUpdateAvailable systemUpdateAvailable := updateStatus.SystemUpdateAvailable - rebootNeeded := false - if appUpdateAvailable { scopedLogger.Info(). Str("local", local.AppVersion). @@ -361,7 +365,6 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err if err != nil { otaState.Error = fmt.Sprintf("Error downloading app update: %v", err) scopedLogger.Error().Err(err).Msg("Error downloading app update") - triggerOTAStateUpdate() return err } downloadFinished := time.Now() @@ -378,18 +381,20 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err if err != nil { otaState.Error = fmt.Sprintf("Error verifying app update hash: %v", err) scopedLogger.Error().Err(err).Msg("Error verifying app update hash") - triggerOTAStateUpdate() return err } verifyFinished := time.Now() otaState.AppVerifiedAt = &verifyFinished otaState.AppVerificationProgress = 1 + triggerOTAStateUpdate() + otaState.AppUpdatedAt = &verifyFinished otaState.AppUpdateProgress = 1 triggerOTAStateUpdate() scopedLogger.Info().Msg("App update downloaded") - rebootNeeded = true + otaState.RebootNeeded = true + triggerOTAStateUpdate() } else { scopedLogger.Info().Msg("App is up to date") } @@ -404,7 +409,6 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err if err != nil { otaState.Error = fmt.Sprintf("Error downloading system update: %v", err) scopedLogger.Error().Err(err).Msg("Error downloading system update") - triggerOTAStateUpdate() return err } downloadFinished := time.Now() @@ -421,7 +425,6 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err if err != nil { otaState.Error = fmt.Sprintf("Error verifying system update hash: %v", err) scopedLogger.Error().Err(err).Msg("Error verifying system update hash") - triggerOTAStateUpdate() return err } scopedLogger.Info().Msg("System update downloaded") @@ -441,6 +444,7 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err scopedLogger.Error().Err(err).Msg("Error starting rk_ota command") return fmt.Errorf("error starting rk_ota command: %w", err) } + ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -481,14 +485,19 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err otaState.SystemUpdateProgress = 1 otaState.SystemUpdatedAt = &verifyFinished triggerOTAStateUpdate() - rebootNeeded = true + + otaState.RebootNeeded = true + triggerOTAStateUpdate() } else { scopedLogger.Info().Msg("System is up to date") } - if rebootNeeded { + if otaState.RebootNeeded { scopedLogger.Info().Msg("System Rebooting in 10s") time.Sleep(10 * time.Second) + otaState.Rebooting = true + triggerOTAStateUpdate() + cmd := exec.Command("reboot") err := cmd.Start() if err != nil { diff --git a/ui/src/hooks/stores.ts b/ui/src/hooks/stores.ts index bfbbb26e..1a7e9668 100644 --- a/ui/src/hooks/stores.ts +++ b/ui/src/hooks/stores.ts @@ -518,6 +518,7 @@ export type UpdateModalViews = | "upToDate" | "updateAvailable" | "updateCompleted" + | "rebooting" | "error"; export interface OtaState { @@ -549,19 +550,26 @@ export interface OtaState { systemUpdateProgress: number; systemUpdatedAt: string | null; + + rebootNeeded: boolean; + rebooting: boolean; }; export interface UpdateState { isUpdatePending: boolean; setIsUpdatePending: (isPending: boolean) => void; + updateDialogHasBeenMinimized: boolean; + setUpdateDialogHasBeenMinimized: (hasBeenMinimized: boolean) => void; + otaState: OtaState; setOtaState: (state: OtaState) => void; - setUpdateDialogHasBeenMinimized: (hasBeenMinimized: boolean) => void; + modalView: UpdateModalViews setModalView: (view: UpdateModalViews) => void; - setUpdateErrorMessage: (errorMessage: string) => void; + updateErrorMessage: string | null; + setUpdateErrorMessage: (errorMessage: string) => void; } export const useUpdateStore = create(set => ({ @@ -587,13 +595,17 @@ export const useUpdateStore = create(set => ({ appUpdatedAt: null, systemUpdateProgress: 0, systemUpdatedAt: null, + rebootNeeded: false, + rebooting: false, }, updateDialogHasBeenMinimized: false, setUpdateDialogHasBeenMinimized: (hasBeenMinimized: boolean) => set({ updateDialogHasBeenMinimized: hasBeenMinimized }), + modalView: "loading", setModalView: (view: UpdateModalViews) => set({ modalView: view }), + updateErrorMessage: null, setUpdateErrorMessage: (errorMessage: string) => set({ updateErrorMessage: errorMessage }), })); diff --git a/ui/src/routes/devices.$id.settings.general.update.tsx b/ui/src/routes/devices.$id.settings.general.update.tsx index 38c15412..5847ddfe 100644 --- a/ui/src/routes/devices.$id.settings.general.update.tsx +++ b/ui/src/routes/devices.$id.settings.general.update.tsx @@ -1,10 +1,11 @@ import { useLocation, useNavigate } from "react-router"; import { useCallback, useEffect, useRef, useState } from "react"; import { CheckCircleIcon } from "@heroicons/react/20/solid"; +import { MdConnectWithoutContact, MdRestartAlt } from "react-icons/md"; import Card from "@/components/Card"; import { useJsonRpc } from "@/hooks/useJsonRpc"; -import { Button } from "@components/Button"; +import { Button, LinkButton } from "@components/Button"; import { UpdateState, useUpdateStore } from "@/hooks/stores"; import LoadingSpinner from "@/components/LoadingSpinner"; import { useDeviceUiNavigation } from "@/hooks/useAppNavigation"; @@ -33,7 +34,7 @@ export default function SettingsGeneralUpdateRoute() { } else { setModalView("loading"); } - }, [otaState.updating, otaState.error, setModalView, updateSuccess]); + }, [otaState.error, otaState.updating, setModalView, updateSuccess]); { /* TODO: Migrate to using URLs instead of the global state. To simplify the refactoring, we'll keep the global state for now. */ @@ -41,8 +42,6 @@ export default function SettingsGeneralUpdateRoute() { return navigate("..")} onConfirmUpdate={onConfirmUpdate} />; } - - export function Dialog({ onClose, onConfirmUpdate, @@ -239,14 +238,18 @@ function UpdatingDeviceState({ if (!otaState.metadataFetchedAt) { return "Fetching update information..."; + } else if (otaState.rebooting) { + return "Rebooting..."; } else if (!downloadFinishedAt) { return `Downloading ${type} update...`; } else if (!verfiedAt) { return `Verifying ${type} update...`; } else if (!updatedAt) { return `Installing ${type} update...`; + } else if (otaState.rebootNeeded) { + return "Reboot needed"; } else { - return `Awaiting reboot`; + return "Awaiting reboot"; } }; @@ -278,12 +281,47 @@ function UpdatingDeviceState({ {areAllUpdatesComplete() ? (
- -
- - Rebooting to complete the update... - -
+ + {otaState.rebooting ? ( +
+ + Rebooting the device to complete the update... + +

+ This may take a few minutes. The device will automatically + reconnect once it is back online. If it doesn{"'"}t, you can + manually reconnect. + +

+
+ ) : ( + otaState.rebootNeeded && ( +
+ + Device reboot is pending... + +

+ The JetKVM is preparing to reboot. This may take a while. If it doesn{"'"}t automatically reboot + after a few minutes, you can manually request a reboot. + +

+
+ ) + )}
) : ( <> From 0984ca7e40d17a74cf07ef8f649d792115498c32 Mon Sep 17 00:00:00 2001 From: Marc Brooks Date: Tue, 30 Sep 2025 17:33:42 -0500 Subject: [PATCH 2/7] Add ability to request a reload to LinkButton and Link --- ui/src/components/Button.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/src/components/Button.tsx b/ui/src/components/Button.tsx index b1dc3ab9..dca64cf9 100644 --- a/ui/src/components/Button.tsx +++ b/ui/src/components/Button.tsx @@ -213,7 +213,7 @@ export const Button = React.forwardRef( Button.displayName = "Button"; type LinkPropsType = Pick & - React.ComponentProps & { disabled?: boolean }; + React.ComponentProps & { disabled?: boolean, reloadDocument?: boolean }; export const LinkButton = ({ to, ...props }: LinkPropsType) => { const classes = cx( "group outline-hidden", @@ -231,7 +231,7 @@ export const LinkButton = ({ to, ...props }: LinkPropsType) => { ); } else { return ( - + ); From 69f429d0a5ae77feb82ba39d8f66da0b0d037b11 Mon Sep 17 00:00:00 2001 From: Marc Brooks Date: Tue, 30 Sep 2025 17:40:37 -0500 Subject: [PATCH 3/7] Added exponential backoff to reconnection Also made the number of reconnect attempts settable Doesn't attempt a reconnection if we intentionally disconnect Make sure the fire-and-forget for TURN activity doesn't result in unhandled promise rejection. --- ui/src/routes/devices.$id.tsx | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/ui/src/routes/devices.$id.tsx b/ui/src/routes/devices.$id.tsx index a1ace077..1fcd46ab 100644 --- a/ui/src/routes/devices.$id.tsx +++ b/ui/src/routes/devices.$id.tsx @@ -146,6 +146,7 @@ export default function KvmIdRoute() { const { otaState, setOtaState, setModalView } = useUpdateStore(); const [loadingMessage, setLoadingMessage] = useState("Connecting to device..."); + const cleanupAndStopReconnecting = useCallback( function cleanupAndStopReconnecting() { console.log("Closing peer connection"); @@ -182,11 +183,11 @@ export default function KvmIdRoute() { pc: RTCPeerConnection, remoteDescription: RTCSessionDescriptionInit, ) { - setLoadingMessage("Setting remote description"); + setLoadingMessage("Setting remote description type:" + remoteDescription.type); try { await pc.setRemoteDescription(new RTCSessionDescription(remoteDescription)); - console.log("[setRemoteSessionDescription] Remote description set successfully"); + console.log("[setRemoteSessionDescription] Remote description set successfully to: " + remoteDescription.sdp); setLoadingMessage("Establishing secure connection..."); } catch (error) { console.error( @@ -231,9 +232,15 @@ export default function KvmIdRoute() { const ignoreOffer = useRef(false); const isSettingRemoteAnswerPending = useRef(false); const makingOffer = useRef(false); + const reconnectAttemptsRef = useRef(20); const wsProtocol = window.location.protocol === "https:" ? "wss:" : "ws:"; + const reconnectInterval = (attempt: number) => { + // Exponential backoff with a max of 10 seconds between attempts + return Math.min(500 * 2 ** attempt, 10000); + } + const { sendMessage, getWebSocket } = useWebSocket( isOnDevice ? `${wsProtocol}//${window.location.host}/webrtc/signaling/client` @@ -241,17 +248,16 @@ export default function KvmIdRoute() { { heartbeat: true, retryOnError: true, - reconnectAttempts: 15, - reconnectInterval: 1000, - onReconnectStop: () => { - console.debug("Reconnect stopped"); + reconnectAttempts: reconnectAttemptsRef.current, + reconnectInterval: reconnectInterval, + onReconnectStop: (attempt: number) => { + console.debug("Reconnect stopped after ", attempt, "attempts"); cleanupAndStopReconnecting(); }, shouldReconnect(event) { console.debug("[Websocket] shouldReconnect", event); - // TODO: Why true? - return true; + return !connectionFailed; // we always want to try to reconnect unless we're explicitly stopped }, onClose(event) { @@ -284,6 +290,7 @@ export default function KvmIdRoute() { */ const parsedMessage = JSON.parse(message.data); + if (parsedMessage.type === "device-metadata") { const { deviceVersion } = parsedMessage.data; console.debug("[Websocket] Received device-metadata message"); @@ -300,10 +307,12 @@ export default function KvmIdRoute() { console.log("[Websocket] Device is using new signaling"); isLegacySignalingEnabled.current = false; } + setupPeerConnection(); } if (!peerConnection) return; + if (parsedMessage.type === "answer") { console.debug("[Websocket] Received answer"); const readyForOffer = @@ -594,7 +603,9 @@ export default function KvmIdRoute() { api.POST(`${CLOUD_API}/webrtc/turn_activity`, { bytesReceived: bytesReceivedDelta, bytesSent: bytesSentDelta, - }); + }).catch(()=>{ + // we don't care about errors here, but we don't want unhandled promise rejections + }); }, 10000); const { setNetworkState} = useNetworkStateStore(); From 85c98ee998895eee5cbabc5371d73c1541d4c83d Mon Sep 17 00:00:00 2001 From: Marc Brooks Date: Tue, 30 Sep 2025 17:40:53 -0500 Subject: [PATCH 4/7] Fix comment --- ui/src/routes/devices.$id.settings.access._index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/routes/devices.$id.settings.access._index.tsx b/ui/src/routes/devices.$id.settings.access._index.tsx index f30bfef1..bc9c075a 100644 --- a/ui/src/routes/devices.$id.settings.access._index.tsx +++ b/ui/src/routes/devices.$id.settings.access._index.tsx @@ -98,7 +98,7 @@ export default function SettingsAccessIndexRoute() { } getCloudState(); - // In cloud mode, we need to navigate to the device overview page, as we don't a connection anymore + // In cloud mode, we need to navigate to the device overview page, as we don't have a connection anymore if (!isOnDevice) navigate("/"); return; }); From d7a56213eae0a108c16c449d5e118e7a13554f6d Mon Sep 17 00:00:00 2001 From: Marc Brooks Date: Tue, 30 Sep 2025 17:45:00 -0500 Subject: [PATCH 5/7] Added force page reload to the onClose events of update/reboot Updated the text about reboot/update and used a smaller button. Ensure we get the correct UI version. Also fixed comment about the system update progress --- .../devices.$id.settings.general.reboot.tsx | 8 ++++- .../devices.$id.settings.general.update.tsx | 33 ++++++++++++------- 2 files changed, 28 insertions(+), 13 deletions(-) diff --git a/ui/src/routes/devices.$id.settings.general.reboot.tsx b/ui/src/routes/devices.$id.settings.general.reboot.tsx index db0e0530..2c73b0a0 100644 --- a/ui/src/routes/devices.$id.settings.general.reboot.tsx +++ b/ui/src/routes/devices.$id.settings.general.reboot.tsx @@ -7,6 +7,12 @@ import { Button } from "@components/Button"; export default function SettingsGeneralRebootRoute() { const navigate = useNavigate(); const { send } = useJsonRpc(); + + const onClose = useCallback(() => { + navigate(".."); // back to the devices.$id.settings page + window.location.reload(); // force a full reload to ensure the current device/cloud UI version is loaded + }, [navigate]); + const onConfirmUpdate = useCallback(() => { // This is where we send the RPC to the golang binary @@ -16,7 +22,7 @@ export default function SettingsGeneralRebootRoute() { { /* TODO: Migrate to using URLs instead of the global state. To simplify the refactoring, we'll keep the global state for now. */ } - return navigate("..")} onConfirmUpdate={onConfirmUpdate} />; + return ; } export function Dialog({ diff --git a/ui/src/routes/devices.$id.settings.general.update.tsx b/ui/src/routes/devices.$id.settings.general.update.tsx index 5847ddfe..bb64e502 100644 --- a/ui/src/routes/devices.$id.settings.general.update.tsx +++ b/ui/src/routes/devices.$id.settings.general.update.tsx @@ -1,5 +1,5 @@ -import { useLocation, useNavigate } from "react-router"; import { useCallback, useEffect, useRef, useState } from "react"; +import { useLocation, useNavigate } from "react-router"; import { CheckCircleIcon } from "@heroicons/react/20/solid"; import { MdConnectWithoutContact, MdRestartAlt } from "react-icons/md"; @@ -19,6 +19,11 @@ export default function SettingsGeneralUpdateRoute() { const { setModalView, otaState } = useUpdateStore(); const { send } = useJsonRpc(); + const onClose = useCallback(() => { + navigate(".."); // back to the devices.$id.settings page + window.location.reload(); // force a full reload to ensure the current device/cloud UI version is loaded + }, [navigate]); + const onConfirmUpdate = useCallback(() => { send("tryUpdate", {}); setModalView("updating"); @@ -39,7 +44,7 @@ export default function SettingsGeneralUpdateRoute() { { /* TODO: Migrate to using URLs instead of the global state. To simplify the refactoring, we'll keep the global state for now. */ } - return navigate("..")} onConfirmUpdate={onConfirmUpdate} />; + return ; } export function Dialog({ @@ -223,7 +228,7 @@ function UpdatingDeviceState({ 100, ); } else { - // System: 10% download, 90% update + // System: 10% download, 10% verification, 80% update return Math.min( downloadProgress * 0.1 + verificationProgress * 0.1 + updateProgress * 0.8, 100, @@ -287,17 +292,19 @@ function UpdatingDeviceState({ Rebooting the device to complete the update... -

+

This may take a few minutes. The device will automatically - reconnect once it is back online. If it doesn{"'"}t, you can - manually reconnect. + reconnect once it is back online.
+ If it doesn{"'"}t reconnect automatically, you can manually + reconnect by clicking here:

@@ -307,15 +314,17 @@ function UpdatingDeviceState({ Device reboot is pending... -

- The JetKVM is preparing to reboot. This may take a while. If it doesn{"'"}t automatically reboot - after a few minutes, you can manually request a reboot. +

+ The JetKVM is preparing to reboot. This may take a while.
+ If it doesn{"'"}t automatically reboot after a few minutes, you + can manually request a reboot by clicking here:

From 72e2367011091aead21bd2ec7413ecbd13274181 Mon Sep 17 00:00:00 2001 From: Marc Brooks Date: Wed, 1 Oct 2025 09:28:34 -0500 Subject: [PATCH 6/7] Fix link for reconnect to point to settings --- ui/src/routes/devices.$id.settings.general.update.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ui/src/routes/devices.$id.settings.general.update.tsx b/ui/src/routes/devices.$id.settings.general.update.tsx index bb64e502..ee7896a0 100644 --- a/ui/src/routes/devices.$id.settings.general.update.tsx +++ b/ui/src/routes/devices.$id.settings.general.update.tsx @@ -292,7 +292,7 @@ function UpdatingDeviceState({ Rebooting the device to complete the update... -

+

This may take a few minutes. The device will automatically reconnect once it is back online.
If it doesn{"'"}t reconnect automatically, you can manually @@ -304,7 +304,7 @@ function UpdatingDeviceState({ LeadingIcon={MdConnectWithoutContact} textAlign="center" reloadDocument={true} - to={"/"} + to={".."} />

@@ -314,7 +314,7 @@ function UpdatingDeviceState({ Device reboot is pending... -

+

The JetKVM is preparing to reboot. This may take a while.
If it doesn{"'"}t automatically reboot after a few minutes, you can manually request a reboot by clicking here: From 70cd19ddbcf0b08d1972ca0d57aeca97a69dfddc Mon Sep 17 00:00:00 2001 From: Marc Brooks Date: Wed, 1 Oct 2025 11:49:31 -0500 Subject: [PATCH 7/7] Removed duplicate code between main and devices.$id The isOneDevice and checkAuth can be leveraged from devices.$id.tsx --- ui/src/main.tsx | 2 +- ui/src/routes/devices.$id.tsx | 24 +++++------------------- 2 files changed, 6 insertions(+), 20 deletions(-) diff --git a/ui/src/main.tsx b/ui/src/main.tsx index 79ca6717..ccd40934 100644 --- a/ui/src/main.tsx +++ b/ui/src/main.tsx @@ -85,7 +85,7 @@ export async function checkDeviceAuth() { } export async function checkAuth() { - return import.meta.env.MODE === "device" ? checkDeviceAuth() : checkCloudAuth(); + return isOnDevice ? checkDeviceAuth() : checkCloudAuth(); } let router; diff --git a/ui/src/routes/devices.$id.tsx b/ui/src/routes/devices.$id.tsx index 1fcd46ab..85e3c729 100644 --- a/ui/src/routes/devices.$id.tsx +++ b/ui/src/routes/devices.$id.tsx @@ -1,7 +1,6 @@ import { lazy, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Outlet, - redirect, useLoaderData, useLocation, useNavigate, @@ -15,7 +14,7 @@ import { FocusTrap } from "focus-trap-react"; import { motion, AnimatePresence } from "framer-motion"; import useWebSocket from "react-use-websocket"; -import { CLOUD_API, DEVICE_API } from "@/ui.config"; +import { CLOUD_API } from "@/ui.config"; import api from "@/api"; import { checkAuth, isInCloud, isOnDevice } from "@/main"; import { cx } from "@/cva.config"; @@ -48,7 +47,6 @@ import { } from "@/components/VideoOverlay"; import { useDeviceUiNavigation } from "@/hooks/useAppNavigation"; import { FeatureFlagProvider } from "@/providers/FeatureFlagProvider"; -import { DeviceStatus } from "@routes/welcome-local"; import { useVersion } from "@/hooks/useVersion"; interface LocalLoaderResp { @@ -70,20 +68,8 @@ export interface LocalDevice { } const deviceLoader = async () => { - const res = await api - .GET(`${DEVICE_API}/device/status`) - .then(res => res.json() as Promise); - - if (!res.isSetup) return redirect("/welcome"); - - const deviceRes = await api.GET(`${DEVICE_API}/device`); - if (deviceRes.status === 401) return redirect("/login-local"); - if (deviceRes.ok) { - const device = (await deviceRes.json()) as LocalDevice; - return { authMode: device.authMode }; - } - - throw new Error("Error fetching device"); + const device = await checkAuth(); + return { authMode: device.authMode } as LocalLoaderResp; }; const cloudLoader = async (params: Params): Promise => { @@ -106,11 +92,11 @@ const cloudLoader = async (params: Params): Promise => device: { id: string; name: string; user: { googleId: string } }; }; - return { user, iceConfig, deviceName: device.name || device.id }; + return { user, iceConfig, deviceName: device.name || device.id } as CloudLoaderResp; }; const loader: LoaderFunction = ({ params }: LoaderFunctionArgs) => { - return import.meta.env.MODE === "device" ? deviceLoader() : cloudLoader(params); + return isOnDevice ? deviceLoader() : cloudLoader(params); }; export default function KvmIdRoute() {