mirror of https://github.com/jetkvm/kvm.git
Better reporting of and process for OTA updating
This commit is contained in:
parent
9438ab7778
commit
2fce23c5d6
27
main.go
27
main.go
|
@ -14,6 +14,7 @@ import (
|
||||||
var appCtx context.Context
|
var appCtx context.Context
|
||||||
|
|
||||||
func Main() {
|
func Main() {
|
||||||
|
logger.Log().Msg("JetKVM Starting Up")
|
||||||
LoadConfig()
|
LoadConfig()
|
||||||
|
|
||||||
var cancel context.CancelFunc
|
var cancel context.CancelFunc
|
||||||
|
@ -78,16 +79,16 @@ func Main() {
|
||||||
initDisplay()
|
initDisplay()
|
||||||
|
|
||||||
go func() {
|
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)
|
time.Sleep(15 * time.Minute)
|
||||||
for {
|
|
||||||
logger.Debug().Bool("auto_update_enabled", config.AutoUpdateEnabled).Msg("UPDATING")
|
|
||||||
if !config.AutoUpdateEnabled {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if isTimeSyncNeeded() || !timeSync.IsSyncSuccess() {
|
for {
|
||||||
logger.Debug().Msg("system time is not synced, will retry in 30 seconds")
|
logger.Info().Bool("auto_update_enabled", config.AutoUpdateEnabled).Msg("auto-update check")
|
||||||
time.Sleep(30 * time.Second)
|
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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -97,6 +98,12 @@ func Main() {
|
||||||
continue
|
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
|
includePreRelease := config.IncludePreRelease
|
||||||
err = TryUpdate(context.Background(), GetDeviceID(), includePreRelease)
|
err = TryUpdate(context.Background(), GetDeviceID(), includePreRelease)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -106,6 +113,7 @@ func Main() {
|
||||||
time.Sleep(1 * time.Hour)
|
time.Sleep(1 * time.Hour)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
//go RunFuseServer()
|
//go RunFuseServer()
|
||||||
go RunWebServer()
|
go RunWebServer()
|
||||||
|
|
||||||
|
@ -122,7 +130,8 @@ func Main() {
|
||||||
sigs := make(chan os.Signal, 1)
|
sigs := make(chan os.Signal, 1)
|
||||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||||
<-sigs
|
<-sigs
|
||||||
logger.Info().Msg("JetKVM Shutting Down")
|
|
||||||
|
logger.Log().Msg("JetKVM Shutting Down")
|
||||||
//if fuseServer != nil {
|
//if fuseServer != nil {
|
||||||
// err := setMassStorageImage(" ")
|
// err := setMassStorageImage(" ")
|
||||||
// if err != nil {
|
// if err != nil {
|
||||||
|
|
45
ota.go
45
ota.go
|
@ -176,7 +176,7 @@ func downloadFile(ctx context.Context, path string, url string, downloadProgress
|
||||||
if nr > 0 {
|
if nr > 0 {
|
||||||
nw, ew := file.Write(buf[0:nr])
|
nw, ew := file.Write(buf[0:nr])
|
||||||
if nw < 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)
|
written += int64(nw)
|
||||||
if ew != nil {
|
if ew != nil {
|
||||||
|
@ -240,7 +240,7 @@ func verifyFile(path string, expectedHash string, verifyProgress *float32, scope
|
||||||
if nr > 0 {
|
if nr > 0 {
|
||||||
nw, ew := hash.Write(buf[0:nr])
|
nw, ew := hash.Write(buf[0:nr])
|
||||||
if nw < 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)
|
verified += int64(nw)
|
||||||
if ew != nil {
|
if ew != nil {
|
||||||
|
@ -260,11 +260,14 @@ func verifyFile(path string, expectedHash string, verifyProgress *float32, scope
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hashSum := hash.Sum(nil)
|
// close the file so we can rename below
|
||||||
scopedLogger.Info().Str("path", path).Str("hash", hex.EncodeToString(hashSum)).Msg("SHA256 hash of")
|
fileToHash.Close()
|
||||||
|
|
||||||
if hex.EncodeToString(hashSum) != expectedHash {
|
hashSum := hex.EncodeToString(hash.Sum(nil))
|
||||||
return fmt.Errorf("hash mismatch: %x != %s", hashSum, expectedHash)
|
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 {
|
if err := os.Rename(unverifiedPath, path); err != nil {
|
||||||
|
@ -296,6 +299,8 @@ type OTAState struct {
|
||||||
AppUpdatedAt *time.Time `json:"appUpdatedAt,omitempty"`
|
AppUpdatedAt *time.Time `json:"appUpdatedAt,omitempty"`
|
||||||
SystemUpdateProgress float32 `json:"systemUpdateProgress,omitempty"` //TODO: port rk_ota, then implement
|
SystemUpdateProgress float32 `json:"systemUpdateProgress,omitempty"` //TODO: port rk_ota, then implement
|
||||||
SystemUpdatedAt *time.Time `json:"systemUpdatedAt,omitempty"`
|
SystemUpdatedAt *time.Time `json:"systemUpdatedAt,omitempty"`
|
||||||
|
RebootNeeded bool `json:"rebootNeeded,omitempty"`
|
||||||
|
Rebooting bool `json:"rebooting,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
var otaState = OTAState{}
|
var otaState = OTAState{}
|
||||||
|
@ -313,7 +318,7 @@ func triggerOTAStateUpdate() {
|
||||||
func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) error {
|
func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) error {
|
||||||
scopedLogger := otaLogger.With().
|
scopedLogger := otaLogger.With().
|
||||||
Str("deviceId", deviceId).
|
Str("deviceId", deviceId).
|
||||||
Str("includePreRelease", fmt.Sprintf("%v", includePreRelease)).
|
Bool("includePreRelease", includePreRelease).
|
||||||
Logger()
|
Logger()
|
||||||
|
|
||||||
scopedLogger.Info().Msg("Trying to update...")
|
scopedLogger.Info().Msg("Trying to update...")
|
||||||
|
@ -322,7 +327,8 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
|
||||||
}
|
}
|
||||||
|
|
||||||
otaState = OTAState{
|
otaState = OTAState{
|
||||||
Updating: true,
|
Updating: true,
|
||||||
|
RebootNeeded: false,
|
||||||
}
|
}
|
||||||
triggerOTAStateUpdate()
|
triggerOTAStateUpdate()
|
||||||
|
|
||||||
|
@ -335,7 +341,7 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
|
||||||
if err != nil {
|
if err != nil {
|
||||||
otaState.Error = fmt.Sprintf("Error checking for updates: %v", err)
|
otaState.Error = fmt.Sprintf("Error checking for updates: %v", err)
|
||||||
scopedLogger.Error().Err(err).Msg("Error checking for updates")
|
scopedLogger.Error().Err(err).Msg("Error checking for updates")
|
||||||
return fmt.Errorf("error checking for updates: %w", err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
@ -349,8 +355,6 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
|
||||||
appUpdateAvailable := updateStatus.AppUpdateAvailable
|
appUpdateAvailable := updateStatus.AppUpdateAvailable
|
||||||
systemUpdateAvailable := updateStatus.SystemUpdateAvailable
|
systemUpdateAvailable := updateStatus.SystemUpdateAvailable
|
||||||
|
|
||||||
rebootNeeded := false
|
|
||||||
|
|
||||||
if appUpdateAvailable {
|
if appUpdateAvailable {
|
||||||
scopedLogger.Info().
|
scopedLogger.Info().
|
||||||
Str("local", local.AppVersion).
|
Str("local", local.AppVersion).
|
||||||
|
@ -361,7 +365,6 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
|
||||||
if err != nil {
|
if err != nil {
|
||||||
otaState.Error = fmt.Sprintf("Error downloading app update: %v", err)
|
otaState.Error = fmt.Sprintf("Error downloading app update: %v", err)
|
||||||
scopedLogger.Error().Err(err).Msg("Error downloading app update")
|
scopedLogger.Error().Err(err).Msg("Error downloading app update")
|
||||||
triggerOTAStateUpdate()
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
downloadFinished := time.Now()
|
downloadFinished := time.Now()
|
||||||
|
@ -378,18 +381,20 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
|
||||||
if err != nil {
|
if err != nil {
|
||||||
otaState.Error = fmt.Sprintf("Error verifying app update hash: %v", err)
|
otaState.Error = fmt.Sprintf("Error verifying app update hash: %v", err)
|
||||||
scopedLogger.Error().Err(err).Msg("Error verifying app update hash")
|
scopedLogger.Error().Err(err).Msg("Error verifying app update hash")
|
||||||
triggerOTAStateUpdate()
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
verifyFinished := time.Now()
|
verifyFinished := time.Now()
|
||||||
otaState.AppVerifiedAt = &verifyFinished
|
otaState.AppVerifiedAt = &verifyFinished
|
||||||
otaState.AppVerificationProgress = 1
|
otaState.AppVerificationProgress = 1
|
||||||
|
triggerOTAStateUpdate()
|
||||||
|
|
||||||
otaState.AppUpdatedAt = &verifyFinished
|
otaState.AppUpdatedAt = &verifyFinished
|
||||||
otaState.AppUpdateProgress = 1
|
otaState.AppUpdateProgress = 1
|
||||||
triggerOTAStateUpdate()
|
triggerOTAStateUpdate()
|
||||||
|
|
||||||
scopedLogger.Info().Msg("App update downloaded")
|
scopedLogger.Info().Msg("App update downloaded")
|
||||||
rebootNeeded = true
|
otaState.RebootNeeded = true
|
||||||
|
triggerOTAStateUpdate()
|
||||||
} else {
|
} else {
|
||||||
scopedLogger.Info().Msg("App is up to date")
|
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 {
|
if err != nil {
|
||||||
otaState.Error = fmt.Sprintf("Error downloading system update: %v", err)
|
otaState.Error = fmt.Sprintf("Error downloading system update: %v", err)
|
||||||
scopedLogger.Error().Err(err).Msg("Error downloading system update")
|
scopedLogger.Error().Err(err).Msg("Error downloading system update")
|
||||||
triggerOTAStateUpdate()
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
downloadFinished := time.Now()
|
downloadFinished := time.Now()
|
||||||
|
@ -421,7 +425,6 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
|
||||||
if err != nil {
|
if err != nil {
|
||||||
otaState.Error = fmt.Sprintf("Error verifying system update hash: %v", err)
|
otaState.Error = fmt.Sprintf("Error verifying system update hash: %v", err)
|
||||||
scopedLogger.Error().Err(err).Msg("Error verifying system update hash")
|
scopedLogger.Error().Err(err).Msg("Error verifying system update hash")
|
||||||
triggerOTAStateUpdate()
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
scopedLogger.Info().Msg("System update downloaded")
|
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")
|
scopedLogger.Error().Err(err).Msg("Error starting rk_ota command")
|
||||||
return fmt.Errorf("error starting rk_ota command: %w", err)
|
return fmt.Errorf("error starting rk_ota command: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
|
@ -481,14 +485,19 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
|
||||||
otaState.SystemUpdateProgress = 1
|
otaState.SystemUpdateProgress = 1
|
||||||
otaState.SystemUpdatedAt = &verifyFinished
|
otaState.SystemUpdatedAt = &verifyFinished
|
||||||
triggerOTAStateUpdate()
|
triggerOTAStateUpdate()
|
||||||
rebootNeeded = true
|
|
||||||
|
otaState.RebootNeeded = true
|
||||||
|
triggerOTAStateUpdate()
|
||||||
} else {
|
} else {
|
||||||
scopedLogger.Info().Msg("System is up to date")
|
scopedLogger.Info().Msg("System is up to date")
|
||||||
}
|
}
|
||||||
|
|
||||||
if rebootNeeded {
|
if otaState.RebootNeeded {
|
||||||
scopedLogger.Info().Msg("System Rebooting in 10s")
|
scopedLogger.Info().Msg("System Rebooting in 10s")
|
||||||
time.Sleep(10 * time.Second)
|
time.Sleep(10 * time.Second)
|
||||||
|
otaState.Rebooting = true
|
||||||
|
triggerOTAStateUpdate()
|
||||||
|
|
||||||
cmd := exec.Command("reboot")
|
cmd := exec.Command("reboot")
|
||||||
err := cmd.Start()
|
err := cmd.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -518,6 +518,7 @@ export type UpdateModalViews =
|
||||||
| "upToDate"
|
| "upToDate"
|
||||||
| "updateAvailable"
|
| "updateAvailable"
|
||||||
| "updateCompleted"
|
| "updateCompleted"
|
||||||
|
| "rebooting"
|
||||||
| "error";
|
| "error";
|
||||||
|
|
||||||
export interface OtaState {
|
export interface OtaState {
|
||||||
|
@ -549,19 +550,26 @@ export interface OtaState {
|
||||||
|
|
||||||
systemUpdateProgress: number;
|
systemUpdateProgress: number;
|
||||||
systemUpdatedAt: string | null;
|
systemUpdatedAt: string | null;
|
||||||
|
|
||||||
|
rebootNeeded: boolean;
|
||||||
|
rebooting: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface UpdateState {
|
export interface UpdateState {
|
||||||
isUpdatePending: boolean;
|
isUpdatePending: boolean;
|
||||||
setIsUpdatePending: (isPending: boolean) => void;
|
setIsUpdatePending: (isPending: boolean) => void;
|
||||||
|
|
||||||
updateDialogHasBeenMinimized: boolean;
|
updateDialogHasBeenMinimized: boolean;
|
||||||
|
setUpdateDialogHasBeenMinimized: (hasBeenMinimized: boolean) => void;
|
||||||
|
|
||||||
otaState: OtaState;
|
otaState: OtaState;
|
||||||
setOtaState: (state: OtaState) => void;
|
setOtaState: (state: OtaState) => void;
|
||||||
setUpdateDialogHasBeenMinimized: (hasBeenMinimized: boolean) => void;
|
|
||||||
modalView: UpdateModalViews
|
modalView: UpdateModalViews
|
||||||
setModalView: (view: UpdateModalViews) => void;
|
setModalView: (view: UpdateModalViews) => void;
|
||||||
setUpdateErrorMessage: (errorMessage: string) => void;
|
|
||||||
updateErrorMessage: string | null;
|
updateErrorMessage: string | null;
|
||||||
|
setUpdateErrorMessage: (errorMessage: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useUpdateStore = create<UpdateState>(set => ({
|
export const useUpdateStore = create<UpdateState>(set => ({
|
||||||
|
@ -587,13 +595,17 @@ export const useUpdateStore = create<UpdateState>(set => ({
|
||||||
appUpdatedAt: null,
|
appUpdatedAt: null,
|
||||||
systemUpdateProgress: 0,
|
systemUpdateProgress: 0,
|
||||||
systemUpdatedAt: null,
|
systemUpdatedAt: null,
|
||||||
|
rebootNeeded: false,
|
||||||
|
rebooting: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
updateDialogHasBeenMinimized: false,
|
updateDialogHasBeenMinimized: false,
|
||||||
setUpdateDialogHasBeenMinimized: (hasBeenMinimized: boolean) =>
|
setUpdateDialogHasBeenMinimized: (hasBeenMinimized: boolean) =>
|
||||||
set({ updateDialogHasBeenMinimized: hasBeenMinimized }),
|
set({ updateDialogHasBeenMinimized: hasBeenMinimized }),
|
||||||
|
|
||||||
modalView: "loading",
|
modalView: "loading",
|
||||||
setModalView: (view: UpdateModalViews) => set({ modalView: view }),
|
setModalView: (view: UpdateModalViews) => set({ modalView: view }),
|
||||||
|
|
||||||
updateErrorMessage: null,
|
updateErrorMessage: null,
|
||||||
setUpdateErrorMessage: (errorMessage: string) => set({ updateErrorMessage: errorMessage }),
|
setUpdateErrorMessage: (errorMessage: string) => set({ updateErrorMessage: errorMessage }),
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -1,10 +1,11 @@
|
||||||
import { useLocation, useNavigate } from "react-router";
|
import { useLocation, useNavigate } from "react-router";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { CheckCircleIcon } from "@heroicons/react/20/solid";
|
import { CheckCircleIcon } from "@heroicons/react/20/solid";
|
||||||
|
import { MdConnectWithoutContact, MdRestartAlt } from "react-icons/md";
|
||||||
|
|
||||||
import Card from "@/components/Card";
|
import Card from "@/components/Card";
|
||||||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
import { Button } from "@components/Button";
|
import { Button, LinkButton } from "@components/Button";
|
||||||
import { UpdateState, useUpdateStore } from "@/hooks/stores";
|
import { UpdateState, useUpdateStore } from "@/hooks/stores";
|
||||||
import LoadingSpinner from "@/components/LoadingSpinner";
|
import LoadingSpinner from "@/components/LoadingSpinner";
|
||||||
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
|
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
|
||||||
|
@ -33,7 +34,7 @@ export default function SettingsGeneralUpdateRoute() {
|
||||||
} else {
|
} else {
|
||||||
setModalView("loading");
|
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. */
|
/* 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 <Dialog onClose={() => navigate("..")} onConfirmUpdate={onConfirmUpdate} />;
|
return <Dialog onClose={() => navigate("..")} onConfirmUpdate={onConfirmUpdate} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export function Dialog({
|
export function Dialog({
|
||||||
onClose,
|
onClose,
|
||||||
onConfirmUpdate,
|
onConfirmUpdate,
|
||||||
|
@ -239,14 +238,18 @@ function UpdatingDeviceState({
|
||||||
|
|
||||||
if (!otaState.metadataFetchedAt) {
|
if (!otaState.metadataFetchedAt) {
|
||||||
return "Fetching update information...";
|
return "Fetching update information...";
|
||||||
|
} else if (otaState.rebooting) {
|
||||||
|
return "Rebooting...";
|
||||||
} else if (!downloadFinishedAt) {
|
} else if (!downloadFinishedAt) {
|
||||||
return `Downloading ${type} update...`;
|
return `Downloading ${type} update...`;
|
||||||
} else if (!verfiedAt) {
|
} else if (!verfiedAt) {
|
||||||
return `Verifying ${type} update...`;
|
return `Verifying ${type} update...`;
|
||||||
} else if (!updatedAt) {
|
} else if (!updatedAt) {
|
||||||
return `Installing ${type} update...`;
|
return `Installing ${type} update...`;
|
||||||
|
} else if (otaState.rebootNeeded) {
|
||||||
|
return "Reboot needed";
|
||||||
} else {
|
} else {
|
||||||
return `Awaiting reboot`;
|
return "Awaiting reboot";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -278,12 +281,47 @@ function UpdatingDeviceState({
|
||||||
<Card className="space-y-4 p-4">
|
<Card className="space-y-4 p-4">
|
||||||
{areAllUpdatesComplete() ? (
|
{areAllUpdatesComplete() ? (
|
||||||
<div className="my-2 flex flex-col items-center space-y-2 text-center">
|
<div className="my-2 flex flex-col items-center space-y-2 text-center">
|
||||||
<LoadingSpinner className="h-6 w-6 text-blue-700 dark:text-blue-500" />
|
<CheckCircleIcon className="h-6 w-6 text-blue-700 dark:text-blue-500" />
|
||||||
<div className="flex justify-between text-sm text-slate-600 dark:text-slate-300">
|
{otaState.rebooting ? (
|
||||||
<span className="font-medium text-black dark:text-white">
|
<div className="flex justify-between text-sm text-slate-600 dark:text-slate-300">
|
||||||
Rebooting to complete the update...
|
<span className="font-medium text-black dark:text-white">
|
||||||
</span>
|
Rebooting the device to complete the update...
|
||||||
</div>
|
</span>
|
||||||
|
<p>
|
||||||
|
This may take a few minutes. The device will automatically
|
||||||
|
reconnect once it is back online. If it doesn{"'"}t, you can
|
||||||
|
manually reconnect.
|
||||||
|
<LinkButton
|
||||||
|
size="SM"
|
||||||
|
theme="light"
|
||||||
|
text="Reconnect to KVM"
|
||||||
|
LeadingIcon={MdConnectWithoutContact}
|
||||||
|
textAlign="center"
|
||||||
|
to={".."}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
otaState.rebootNeeded && (
|
||||||
|
<div className="flex justify-between text-sm text-slate-600 dark:text-slate-300">
|
||||||
|
<span className="font-medium text-black dark:text-white">
|
||||||
|
Device reboot is pending...
|
||||||
|
</span>
|
||||||
|
<p>
|
||||||
|
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.
|
||||||
|
<LinkButton
|
||||||
|
size="SM"
|
||||||
|
theme="light"
|
||||||
|
text="Reboot the KVM"
|
||||||
|
LeadingIcon={MdRestartAlt}
|
||||||
|
textAlign="center"
|
||||||
|
to={"../reboot"}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
|
|
Loading…
Reference in New Issue