Compare commits

..

14 Commits

Author SHA1 Message Date
Aveline e186d3b674
Merge branch 'dev' into r/ota 2025-11-06 16:56:50 +01:00
Adam Shiervani 8c88f72ba4 feat: add acknowledgment checkbox for version changes in advanced settings 2025-11-06 11:54:50 +01:00
Adam Shiervani 931413acd9
Apply suggestion from @Copilot
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-06 11:18:54 +01:00
Adam Shiervani 550ff2e108
Apply suggestion from @Copilot
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-06 11:18:31 +01:00
Adam Shiervani cc36d83154
Apply suggestion from @Copilot
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-11-06 11:15:54 +01:00
Siyuan bce968000c feat: redirect to setup page after config reset 2025-10-31 17:56:20 +00:00
Adam Shiervani 0a299bbe18 feat: enhance version update settings with reset configuration option 2025-10-31 18:51:51 +01:00
Siyuan ae197c530b feat: allow configuration to be reset during update 2025-10-31 17:39:23 +00:00
Siyuan e63c7fce0d fix: update components 2025-10-31 17:28:00 +00:00
Siyuan 9e8ada32b3 cleanup: ota state 2025-10-31 16:15:42 +00:00
Siyuan 2447e8fff2 feat: downgrade 2025-10-31 15:50:41 +00:00
Adam Shiervani a69f7c9c50 feat: add version update functionality to advanced settings 2025-10-29 12:05:40 +01:00
Adam Shiervani 019c73cd61 refactor: mprove UI settings structure with NestedSettingsGroup 2025-10-29 11:50:27 +01:00
Siyuan 325de4a33a WIP: OTA refactor 2025-10-29 09:07:30 +00:00
12 changed files with 163 additions and 305 deletions

2
hw.go
View File

@ -39,7 +39,7 @@ func readOtpEntropy() ([]byte, error) { //nolint:unused
return content[0x17:0x1C], nil
}
func hwReboot(force bool, postRebootAction *ota.PostRebootAction, delay time.Duration) error {
func hwReboot(force bool, postRebootAction *ota.PostRebootAction, delay time.Duration) error { //nolint:unused
logger.Info().Msgf("Reboot requested, rebooting in %d seconds...", delay)
writeJSONRPCEvent("willReboot", postRebootAction, currentSession)

View File

@ -1,8 +0,0 @@
package ota
import "errors"
var (
// ErrVersionNotFound is returned when the specified version is not found
ErrVersionNotFound = errors.New("specified version not found")
)

View File

@ -3,7 +3,6 @@ package ota
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
@ -24,14 +23,12 @@ func (s *State) GetReleaseAPIEndpoint() string {
}
// getUpdateURL returns the update URL for the given parameters
func (s *State) getUpdateURL(params UpdateParams) (string, error, bool) {
func (s *State) getUpdateURL(params UpdateParams) (string, error) {
updateURL, err := url.Parse(s.releaseAPIEndpoint)
if err != nil {
return "", fmt.Errorf("error parsing update metadata URL: %w", err), false
return "", fmt.Errorf("error parsing update metadata URL: %w", err)
}
isCustomVersion := false
appTargetVersion := s.GetTargetVersion("app")
if appTargetVersion != "" && params.AppTargetVersion == "" {
params.AppTargetVersion = appTargetVersion
@ -46,21 +43,19 @@ func (s *State) getUpdateURL(params UpdateParams) (string, error, bool) {
query.Set("prerelease", fmt.Sprintf("%v", params.IncludePreRelease))
if params.AppTargetVersion != "" {
query.Set("appVersion", params.AppTargetVersion)
isCustomVersion = true
}
if params.SystemTargetVersion != "" {
query.Set("systemVersion", params.SystemTargetVersion)
isCustomVersion = true
}
updateURL.RawQuery = query.Encode()
return updateURL.String(), nil, isCustomVersion
return updateURL.String(), nil
}
func (s *State) fetchUpdateMetadata(ctx context.Context, params UpdateParams) (*UpdateMetadata, error) {
metadata := &UpdateMetadata{}
url, err, isCustomVersion := s.getUpdateURL(params)
url, err := s.getUpdateURL(params)
if err != nil {
return nil, fmt.Errorf("error getting update URL: %w", err)
}
@ -82,10 +77,6 @@ func (s *State) fetchUpdateMetadata(ctx context.Context, params UpdateParams) (*
}
defer resp.Body.Close()
if isCustomVersion && resp.StatusCode == http.StatusNotFound {
return nil, ErrVersionNotFound
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
}
@ -198,7 +189,7 @@ func (s *State) doUpdate(ctx context.Context, params UpdateParams) error {
postRebootAction := &PostRebootAction{
HealthCheck: "/device/status",
RedirectTo: redirectUrl,
RedirectUrl: redirectUrl,
}
if err := s.reboot(true, postRebootAction, 10*time.Second); err != nil {
@ -239,29 +230,10 @@ func (s *State) getUpdateStatus(
systemUpdate = &currentSystemUpdate
}
err = s.doGetUpdateStatus(ctx, params, appUpdate, systemUpdate)
if err != nil {
return nil, nil, err
}
s.componentUpdateStatuses["app"] = *appUpdate
s.componentUpdateStatuses["system"] = *systemUpdate
return appUpdate, systemUpdate, nil
}
// doGetUpdateStatus is the internal function that gets the update status
// it WON'T change the state of the OTA state
func (s *State) doGetUpdateStatus(
ctx context.Context,
params UpdateParams,
appUpdate *componentUpdateStatus,
systemUpdate *componentUpdateStatus,
) error {
// Get local versions
systemVersionLocal, appVersionLocal, err := s.getLocalVersion()
if err != nil {
return fmt.Errorf("error getting local version: %w", err)
return nil, nil, fmt.Errorf("error getting local version: %w", err)
}
appUpdate.localVersion = appVersionLocal.String()
systemUpdate.localVersion = systemVersionLocal.String()
@ -269,12 +241,8 @@ func (s *State) doGetUpdateStatus(
// Get remote metadata
remoteMetadata, err := s.fetchUpdateMetadata(ctx, params)
if err != nil {
if err == ErrVersionNotFound || errors.Unwrap(err) == ErrVersionNotFound {
err = ErrVersionNotFound
} else {
err = fmt.Errorf("error checking for updates: %w", err)
}
return err
return
}
appUpdate.url = remoteMetadata.AppURL
appUpdate.hash = remoteMetadata.AppHash
@ -288,7 +256,7 @@ func (s *State) doGetUpdateStatus(
systemVersionRemote, err := semver.NewVersion(remoteMetadata.SystemVersion)
if err != nil {
err = fmt.Errorf("error parsing remote system version: %w", err)
return err
return
}
systemUpdate.available = systemVersionRemote.GreaterThan(systemVersionLocal)
systemUpdate.downgradeAvailable = systemVersionRemote.LessThan(systemVersionLocal)
@ -296,7 +264,7 @@ func (s *State) doGetUpdateStatus(
appVersionRemote, err := semver.NewVersion(remoteMetadata.AppVersion)
if err != nil {
err = fmt.Errorf("error parsing remote app version: %w, %s", err, remoteMetadata.AppVersion)
return err
return
}
appUpdate.available = appVersionRemote.GreaterThan(appVersionLocal)
appUpdate.downgradeAvailable = appVersionRemote.LessThan(appVersionLocal)
@ -312,17 +280,18 @@ func (s *State) doGetUpdateStatus(
appUpdate.available = false
}
return nil
s.componentUpdateStatuses["app"] = *appUpdate
s.componentUpdateStatuses["system"] = *systemUpdate
return
}
// GetUpdateStatus returns the current update status (for backwards compatibility)
func (s *State) GetUpdateStatus(ctx context.Context, params UpdateParams) (*UpdateStatus, error) {
appUpdate := &componentUpdateStatus{}
systemUpdate := &componentUpdateStatus{}
err := s.doGetUpdateStatus(ctx, params, appUpdate, systemUpdate)
_, _, err := s.getUpdateStatus(ctx, params)
if err != nil {
return nil, fmt.Errorf("error getting update status: %w", err)
}
return toUpdateStatus(appUpdate, systemUpdate, ""), nil
return s.ToUpdateStatus(), nil
}

View File

@ -43,7 +43,7 @@ type UpdateStatus struct {
// It is used to redirect the user to a specific page after a reboot
type PostRebootAction struct {
HealthCheck string `json:"healthCheck"` // The health check URL to call after the reboot
RedirectTo string `json:"redirectTo"` // The URL to redirect to after the reboot
RedirectUrl string `json:"redirectUrl"` // The URL to redirect to after the reboot
}
// componentUpdateStatus represents the status of a component update
@ -156,7 +156,18 @@ func (s *State) GetTargetVersion(component string) string {
return componentUpdate.targetVersion
}
func toUpdateStatus(appUpdate *componentUpdateStatus, systemUpdate *componentUpdateStatus, error string) *UpdateStatus {
// ToUpdateStatus converts the State to the UpdateStatus
func (s *State) ToUpdateStatus() *UpdateStatus {
appUpdate, ok := s.componentUpdateStatuses["app"]
if !ok {
return nil
}
systemUpdate, ok := s.componentUpdateStatuses["system"]
if !ok {
return nil
}
return &UpdateStatus{
Local: &LocalMetadata{
AppVersion: appUpdate.localVersion,
@ -174,25 +185,10 @@ func toUpdateStatus(appUpdate *componentUpdateStatus, systemUpdate *componentUpd
SystemDowngradeAvailable: systemUpdate.downgradeAvailable,
AppUpdateAvailable: appUpdate.available,
AppDowngradeAvailable: appUpdate.downgradeAvailable,
Error: error,
Error: s.error,
}
}
// ToUpdateStatus converts the State to the UpdateStatus
func (s *State) ToUpdateStatus() *UpdateStatus {
appUpdate, ok := s.componentUpdateStatuses["app"]
if !ok {
return nil
}
systemUpdate, ok := s.componentUpdateStatuses["system"]
if !ok {
return nil
}
return toUpdateStatus(&appUpdate, &systemUpdate, s.error)
}
// IsUpdatePending returns true if an update is pending
func (s *State) IsUpdatePending() bool {
return s.updating

View File

@ -1151,10 +1151,9 @@ var rpcHandlers = map[string]RPCHandler{
"setDevChannelState": {Func: rpcSetDevChannelState, Params: []string{"enabled"}},
"getLocalVersion": {Func: rpcGetLocalVersion},
"getUpdateStatus": {Func: rpcGetUpdateStatus},
"checkUpdateComponents": {Func: rpcCheckUpdateComponents, Params: []string{"params", "includePreRelease"}},
"getUpdateStatusChannel": {Func: rpcGetUpdateStatusChannel},
"tryUpdate": {Func: rpcTryUpdate},
"tryUpdateComponents": {Func: rpcTryUpdateComponents, Params: []string{"params", "includePreRelease", "resetConfig"}},
"tryUpdateComponents": {Func: rpcTryUpdateComponents, Params: []string{"components", "includePreRelease", "checkOnly", "resetConfig"}},
"cancelDowngrade": {Func: rpcCancelDowngrade},
"getDevModeState": {Func: rpcGetDevModeState},
"setDevModeState": {Func: rpcSetDevModeState, Params: []string{"enabled"}},

42
ota.go
View File

@ -134,56 +134,42 @@ func rpcGetLocalVersion() (*ota.LocalMetadata, error) {
}, nil
}
type updateParams struct {
// ComponentName represents the name of a component
type tryUpdateComponents struct {
AppTargetVersion string `json:"app"`
SystemTargetVersion string `json:"system"`
Components string `json:"components,omitempty"` // components is a comma-separated list of components to update
}
func rpcTryUpdate() error {
return rpcTryUpdateComponents(updateParams{
return rpcTryUpdateComponents(tryUpdateComponents{
AppTargetVersion: "",
SystemTargetVersion: "",
}, config.IncludePreRelease, false)
}, config.IncludePreRelease, false, false)
}
// rpcCheckUpdateComponents checks the update status for the given components
func rpcCheckUpdateComponents(params updateParams, includePreRelease bool) (*ota.UpdateStatus, error) {
updateParams := ota.UpdateParams{
DeviceID: GetDeviceID(),
IncludePreRelease: includePreRelease,
AppTargetVersion: params.AppTargetVersion,
SystemTargetVersion: params.SystemTargetVersion,
}
if params.Components != "" {
updateParams.Components = strings.Split(params.Components, ",")
}
info, err := otaState.GetUpdateStatus(context.Background(), updateParams)
if err != nil {
return nil, fmt.Errorf("failed to check update: %w", err)
}
return info, nil
}
func rpcTryUpdateComponents(params updateParams, includePreRelease bool, resetConfig bool) error {
func rpcTryUpdateComponents(components tryUpdateComponents, includePreRelease bool, checkOnly bool, resetConfig bool) error {
updateParams := ota.UpdateParams{
DeviceID: GetDeviceID(),
IncludePreRelease: includePreRelease,
CheckOnly: checkOnly,
ResetConfig: resetConfig,
}
updateParams.AppTargetVersion = params.AppTargetVersion
if err := otaState.SetTargetVersion("app", params.AppTargetVersion); err != nil {
logger.Info().Interface("components", components).Msg("components")
updateParams.AppTargetVersion = components.AppTargetVersion
if err := otaState.SetTargetVersion("app", components.AppTargetVersion); err != nil {
return fmt.Errorf("failed to set app target version: %w", err)
}
updateParams.SystemTargetVersion = params.SystemTargetVersion
if err := otaState.SetTargetVersion("system", params.SystemTargetVersion); err != nil {
updateParams.SystemTargetVersion = components.SystemTargetVersion
if err := otaState.SetTargetVersion("system", components.SystemTargetVersion); err != nil {
return fmt.Errorf("failed to set system target version: %w", err)
}
if params.Components != "" {
updateParams.Components = strings.Split(params.Components, ",")
if components.Components != "" {
updateParams.Components = strings.Split(components.Components, ",")
}
go func() {

View File

@ -74,7 +74,6 @@
"advanced_error_update_ssh_key": "Failed to update SSH key: {error}",
"advanced_error_usb_emulation_disable": "Failed to disable USB emulation: {error}",
"advanced_error_usb_emulation_enable": "Failed to enable USB emulation: {error}",
"advanced_error_version_update": "Failed to initiate version update: {error}",
"advanced_loopback_only_description": "Restrict web interface access to localhost only (127.0.0.1)",
"advanced_loopback_only_title": "Loopback-Only Mode",
"advanced_loopback_warning_before": "Before enabling this feature, make sure you have either:",
@ -101,19 +100,6 @@
"advanced_update_ssh_key_button": "Update SSH Key",
"advanced_usb_emulation_description": "Control the USB emulation state",
"advanced_usb_emulation_title": "USB Emulation",
"advanced_version_update_app_label": "App Version",
"advanced_version_update_button": "Update to Version",
"advanced_version_update_description": "Install a specific version from GitHub releases",
"advanced_version_update_github_link": "JetKVM releases page",
"advanced_version_update_helper": "Find available versions on the",
"advanced_version_update_reset_config_description": "Reset configuration after the update",
"advanced_version_update_reset_config_label": "Reset configuration",
"advanced_version_update_system_label": "System Version",
"advanced_version_update_target_app": "App only",
"advanced_version_update_target_both": "Both App and System",
"advanced_version_update_target_label": "What to update",
"advanced_version_update_target_system": "System only",
"advanced_version_update_title": "Update to Specific Version",
"already_adopted_new_owner": "If you're the new owner, please ask the previous owner to de-register the device from their account in the cloud dashboard. If you believe this is an error, contact our support team for assistance.",
"already_adopted_other_user": "This device is currently registered to another user in our cloud dashboard.",
"already_adopted_return_to_dashboard": "Return to Dashboard",
@ -255,8 +241,8 @@
"general_auto_update_description": "Automatically update the device to the latest version",
"general_auto_update_error": "Failed to set auto-update: {error}",
"general_auto_update_title": "Auto Update",
"general_check_for_stable_updates": "Downgrade",
"general_check_for_updates": "Check for Updates",
"general_check_for_stable_updates": "Downgrade",
"general_page_description": "Configure device settings and update preferences",
"general_reboot_description": "Do you want to proceed with rebooting the system?",
"general_reboot_device": "Reboot Device",
@ -276,13 +262,9 @@
"general_update_checking_title": "Checking for updates…",
"general_update_completed_description": "Your device has been successfully updated to the latest version. Enjoy the new features and improvements!",
"general_update_completed_title": "Update Completed Successfully",
"general_update_downgrade_available_description": "A downgrade is available to revert to a previous version.",
"general_update_downgrade_available_title": "Downgrade Available",
"general_update_downgrade_button": "Downgrade Now",
"general_update_error_description": "An error occurred while updating your device. Please try again later.",
"general_update_error_details": "Error details: {errorMessage}",
"general_update_error_title": "Update Error",
"general_update_keep_current_button": "Keep Current Version",
"general_update_later_button": "Do it later",
"general_update_now_button": "Update Now",
"general_update_rebooting": "Rebooting to complete the update…",
@ -916,5 +898,23 @@
"wake_on_lan_invalid_mac": "Invalid MAC address",
"wake_on_lan_magic_sent_success": "Magic Packet sent successfully",
"welcome_to_jetkvm": "Welcome to JetKVM",
"welcome_to_jetkvm_description": "Control any computer remotely"
"welcome_to_jetkvm_description": "Control any computer remotely",
"advanced_version_update_app_label": "App Version",
"advanced_version_update_button": "Update to Version",
"advanced_version_update_description": "Install a specific version from GitHub releases",
"advanced_version_update_github_link": "JetKVM releases page",
"advanced_version_update_helper": "Find available versions on the",
"advanced_version_update_system_label": "System Version",
"advanced_version_update_target_app": "App only",
"advanced_version_update_target_both": "Both App and System",
"advanced_version_update_target_label": "What to update",
"advanced_version_update_target_system": "System only",
"advanced_version_update_title": "Update to Specific Version",
"advanced_error_version_update": "Failed to initiate version update: {error}",
"general_update_downgrade_available_description": "A downgrade is available to revert to a previous version.",
"general_update_downgrade_available_title": "Downgrade Available",
"general_update_downgrade_button": "Downgrade Now",
"general_update_keep_current_button": "Keep Current Version",
"advanced_version_update_reset_config_description": "Reset configuration after the update",
"advanced_version_update_reset_config_label": "Reset configuration"
}

View File

@ -1,4 +1,5 @@
import { useCallback } from "react";
import { useCallback, useMemo } from "react";
import semver from "semver";
import { useDeviceStore } from "@/hooks/stores";
import { JsonRpcError, RpcMethodNotFound } from "@/hooks/useJsonRpc";
@ -29,44 +30,51 @@ export function useVersion() {
setSystemVersion,
} = useDeviceStore();
const getVersionInfo = useCallback(async () => {
try {
const result = await getUpdateStatus();
setAppVersion(result.local.appVersion);
setSystemVersion(result.local.systemVersion);
return result;
} catch (error) {
const jsonRpcError = error as JsonRpcError;
notifications.error(m.updates_failed_check({ error: jsonRpcError.message }));
throw jsonRpcError;
if (result.error) {
notifications.error(m.updates_failed_check({ error: String(result.error) }));
reject(new Error("Failed to check for updates"));
} else {
resolve(result);
}
}, [setAppVersion, setSystemVersion]);
}
});
});
}, [send, setAppVersion, setSystemVersion]);
const isOnDevVersion = useMemo(() => {
if (appVersion && semver.prerelease(appVersion)) return true;
if (systemVersion && semver.prerelease(systemVersion)) return true;
return false;
}, [appVersion, systemVersion]);
const getLocalVersion = useCallback(() => {
return new Promise<VersionInfo>((resolve, reject) => {
send("getLocalVersion", {}, (resp: JsonRpcResponse) => {
if ("error" in resp) {
console.log(resp.error)
if (resp.error.code === RpcMethodNotFound) {
console.warn("Failed to get device version, using legacy version");
return getVersionInfo().then(result => resolve(result.local)).catch(reject);
}
console.error("Failed to get device version N", resp.error);
notifications.error(m.updates_failed_get_device_version({ error: String(resp.error) }));
reject(new Error("Failed to get device version"));
} else {
const result = resp.result as VersionInfo;
const getLocalVersion = useCallback(async () => {
try {
const result = await getLocalVersionRpc();
setAppVersion(result.appVersion);
setSystemVersion(result.systemVersion);
return result;
} catch (error: unknown) {
const jsonRpcError = error as JsonRpcError;
if (jsonRpcError.code === RpcMethodNotFound) {
console.error("Failed to get local version, using legacy remote version");
const result = await getVersionInfo();
return result.local;
resolve(result);
}
console.error("Failed to get device version", jsonRpcError);
notifications.error(m.updates_failed_get_device_version({ error: jsonRpcError.message }));
throw jsonRpcError;
}
}, [setAppVersion, setSystemVersion, getVersionInfo]);
});
});
}, [send, setAppVersion, setSystemVersion, getVersionInfo]);
return {
getVersionInfo,
getLocalVersion,
appVersion,
systemVersion,
isOnDevVersion,
};
}

View File

@ -1,7 +1,7 @@
import { useCallback, useEffect, useState } from "react";
import { useSettingsStore } from "@hooks/stores";
import { JsonRpcError, JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
import { useDeviceUiNavigation } from "@hooks/useAppNavigation";
import { Button } from "@components/Button";
import Checkbox, { CheckboxWithLabel } from "@components/Checkbox";
@ -17,8 +17,6 @@ import { isOnDevice } from "@/main";
import notifications from "@/notifications";
import { m } from "@localizations/messages.js";
import { sleep } from "@/utils";
import { checkUpdateComponents } from "@/utils/jsonrpc";
import { SystemVersionInfo } from "@hooks/useVersion";
export default function SettingsAdvancedRoute() {
const { send } = useJsonRpc();
@ -35,7 +33,7 @@ export default function SettingsAdvancedRoute() {
const [systemVersion, setSystemVersion] = useState<string>("");
const [resetConfig, setResetConfig] = useState(false);
const [versionChangeAcknowledged, setVersionChangeAcknowledged] = useState(false);
const [versionUpdateLoading, setVersionUpdateLoading] = useState(false);
const settings = useSettingsStore();
useEffect(() => {
@ -185,57 +183,34 @@ export default function SettingsAdvancedRoute() {
setShowLoopbackWarning(false);
}, [applyLoopbackOnlyMode, setShowLoopbackWarning]);
const handleVersionUpdateError = useCallback((error?: JsonRpcError) => {
notifications.error(
m.advanced_error_version_update({
error: error?.data ?? error?.message ?? m.unknown_error()
}),
{ duration: 1000 * 15 } // 15 seconds
);
setVersionUpdateLoading(false);
}, []);
const handleVersionUpdate = useCallback(async () => {
const components = updateTarget === "both" ? ["app", "system"] : [updateTarget];
let versionInfo: SystemVersionInfo | undefined;
try {
// we do not need to set it to false if check succeeds,
// because it will be redirected to the update page later
setVersionUpdateLoading(true);
versionInfo = await checkUpdateComponents({
components: components.join(","),
const handleVersionUpdate = useCallback(() => {
const params = {
components: {
app: appVersion,
system: systemVersion,
}, devChannel);
console.log("versionInfo", versionInfo);
} catch (error: unknown) {
const jsonRpcError = error as JsonRpcError;
handleVersionUpdateError(jsonRpcError);
},
includePreRelease: devChannel,
checkOnly: true,
// no need to reset config for a check only update
resetConfig: false,
};
send("tryUpdateComponents", params, (resp: JsonRpcResponse) => {
if ("error" in resp) {
notifications.error(
m.advanced_error_version_update({ error: resp.error.data || m.unknown_error() })
);
return;
}
if (!versionInfo) {
handleVersionUpdateError();
return;
}
const pageParams = new URLSearchParams();
pageParams.set("downgrade", "true");
if (components.includes("app") && versionInfo.remote?.appVersion && versionInfo.appDowngradeAvailable) {
pageParams.set("app", versionInfo.remote?.appVersion);
}
if (components.includes("system") && versionInfo.remote?.systemVersion && versionInfo.systemDowngradeAvailable) {
pageParams.set("system", versionInfo.remote?.systemVersion);
}
pageParams.set("resetConfig", resetConfig.toString());
pageParams.set("components", updateTarget === "both" ? "app,system" : updateTarget);
// Navigate to update page
navigateTo(`/settings/general/update?${pageParams.toString()}`);
}, [
updateTarget, appVersion, systemVersion, devChannel,
navigateTo, resetConfig, handleVersionUpdateError,
setVersionUpdateLoading
]);
});
}, [updateTarget, appVersion, systemVersion, devChannel, send, navigateTo, resetConfig]);
return (
<div className="space-y-4">
@ -399,10 +374,8 @@ export default function SettingsAdvancedRoute() {
(updateTarget === "app" && !appVersion) ||
(updateTarget === "system" && !systemVersion) ||
(updateTarget === "both" && (!appVersion || !systemVersion)) ||
!versionChangeAcknowledged ||
versionUpdateLoading
!versionChangeAcknowledged
}
loading={versionUpdateLoading}
onClick={handleVersionUpdate}
/>
</div>

View File

@ -23,44 +23,26 @@ export default function SettingsGeneralUpdateRoute() {
const { send } = useJsonRpc();
const downgrade = useMemo(() => searchParams.get("downgrade") === "true", [searchParams]);
const customAppVersion = useMemo(() => searchParams.get("app") || "", [searchParams]);
const customSystemVersion = useMemo(() => searchParams.get("system") || "", [searchParams]);
const updateComponents = useMemo(() => searchParams.get("components") || "", [searchParams]);
const resetConfig = useMemo(() => searchParams.get("resetConfig") === "true", [searchParams]);
const onClose = useCallback(async () => {
navigate(".."); // back to the devices.$id.settings page
// Add 1s delay between navigation and calling reload() to prevent reload from interrupting the navigation.
await sleep(1000);
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");
}, [send, setModalView]);
const onConfirmDowngrade = useCallback(() => {
const components = [];
if (customSystemVersion) {
components.push("system");
}
if (customAppVersion) {
components.push("app");
}
const onConfirmDowngrade = useCallback((system?: string, app?: string) => {
send("tryUpdateComponents", {
params: {
components: components.join(","),
app: customAppVersion,
system: customSystemVersion,
components: {
system, app,
components: updateComponents
},
includePreRelease: false,
resetConfig,
}, (resp) => {
if ("error" in resp) return;
setModalView("updating");
includePreRelease: true,
checkOnly: false,
resetConfig: resetConfig,
});
}, [send, setModalView, customAppVersion, customSystemVersion, resetConfig]);
setModalView("updating");
}, [send, setModalView, updateComponents, resetConfig]);
useEffect(() => {
if (otaState.updating) {
@ -75,12 +57,10 @@ export default function SettingsGeneralUpdateRoute() {
}, [otaState.error, otaState.updating, setModalView, updateSuccess]);
return <Dialog
onClose={onClose}
onClose={() => navigate("..")}
onConfirmUpdate={onConfirmUpdate}
onConfirmDowngrade={onConfirmDowngrade}
downgrade={downgrade}
customAppVersion={customAppVersion}
customSystemVersion={customSystemVersion}
/>;
}
@ -89,15 +69,11 @@ export function Dialog({
onConfirmUpdate,
onConfirmDowngrade,
downgrade,
customAppVersion,
customSystemVersion,
}: Readonly<{
downgrade: boolean;
onClose: () => void;
onConfirmUpdate: () => void;
onConfirmDowngrade: () => void;
customAppVersion?: string;
customSystemVersion?: string;
}>) {
const { navigateTo } = useDeviceUiNavigation();
@ -109,7 +85,8 @@ export function Dialog({
(versionInfo: SystemVersionInfo) => {
const hasUpdate =
versionInfo?.systemUpdateAvailable || versionInfo?.appUpdateAvailable;
const hasDowngrade = customSystemVersion !== undefined || customAppVersion !== undefined;
const hasDowngrade =
versionInfo?.systemDowngradeAvailable || versionInfo?.appDowngradeAvailable;
setVersionInfo(versionInfo);
@ -121,7 +98,7 @@ export function Dialog({
setModalView("upToDate");
}
},
[setModalView, downgrade, customAppVersion, customSystemVersion],
[setModalView, downgrade],
);
const onCancelDowngrade = useCallback(() => {
@ -153,10 +130,9 @@ export function Dialog({
)}
{modalView === "updateDowngradeAvailable" && (
<UpdateDowngradeAvailableState
appVersion={customAppVersion}
systemVersion={customSystemVersion}
onConfirmDowngrade={onConfirmDowngrade}
onCancelDowngrade={onCancelDowngrade}
versionInfo={versionInfo!}
/>
)}
@ -472,19 +448,20 @@ function UpdateAvailableState({
}
function UpdateDowngradeAvailableState({
appVersion,
systemVersion,
versionInfo,
onConfirmDowngrade,
onCancelDowngrade,
}: {
appVersion?: string;
systemVersion?: string;
onConfirmDowngrade: () => void;
versionInfo: SystemVersionInfo;
onConfirmDowngrade: (system?: string, app?: string) => void;
onCancelDowngrade: () => void;
}) {
const confirmDowngrade = useCallback(() => {
onConfirmDowngrade();
}, [onConfirmDowngrade]);
onConfirmDowngrade(
versionInfo?.remote?.systemVersion || undefined,
versionInfo?.remote?.appVersion || undefined,
);
}, [versionInfo, onConfirmDowngrade]);
return (
<div className="flex flex-col items-start justify-start space-y-4 text-left">
<div className="text-left">
@ -495,15 +472,15 @@ function UpdateDowngradeAvailableState({
{m.general_update_downgrade_available_description()}
</p>
<p className="mb-4 text-sm text-slate-600 dark:text-slate-300">
{systemVersion ? (
{versionInfo?.systemDowngradeAvailable ? (
<>
<span className="font-semibold">{m.general_update_system_type()}</span>: {systemVersion}
<span className="font-semibold">{m.general_update_system_type()}</span>: {versionInfo?.remote?.systemVersion}
<br />
</>
) : null}
{appVersion ? (
{versionInfo?.appDowngradeAvailable ? (
<>
<span className="font-semibold">{m.general_update_application_type()}</span>: {appVersion}
<span className="font-semibold">{m.general_update_application_type()}</span>: {versionInfo?.remote?.appVersion}
</>
) : null}
</p>

View File

@ -220,9 +220,7 @@ export interface SystemVersionInfo {
local: VersionInfo;
remote?: VersionInfo;
systemUpdateAvailable: boolean;
systemDowngradeAvailable: boolean;
appUpdateAvailable: boolean;
appDowngradeAvailable: boolean;
error?: string;
}
@ -244,21 +242,3 @@ export async function getLocalVersion() {
if (response.error) throw response.error;
return response.result;
}
export interface updateParams {
app?: string;
system?: string;
components?: string;
}
export async function checkUpdateComponents(params: updateParams, includePreRelease: boolean) {
const response = await callJsonRpc<SystemVersionInfo>({
method: "checkUpdateComponents",
params: {
params,
includePreRelease,
},
});
if (response.error) throw response.error;
return response.result;
}

24
web.go
View File

@ -8,7 +8,6 @@ import (
"errors"
"fmt"
"io/fs"
"net"
"net/http"
"net/http/pprof"
"path/filepath"
@ -185,8 +184,6 @@ func setupRouter() *gin.Engine {
protected.PUT("/auth/password-local", handleUpdatePassword)
protected.DELETE("/auth/local-password", handleDeletePassword)
protected.POST("/storage/upload", handleUploadHttp)
protected.POST("/device/send-wol/:mac-addr", handleSendWOLMagicPacket)
}
// Catch-all route for SPA
@ -344,6 +341,7 @@ func handleWebRTCSignalWsMessages(
l.Trace().Msg("sending ping frame")
err := wsCon.Ping(runCtx)
if err != nil {
l.Warn().Str("error", err.Error()).Msg("websocket ping error")
cancelRun()
@ -809,23 +807,3 @@ func handleSetup(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"message": "Device setup completed successfully"})
}
func handleSendWOLMagicPacket(c *gin.Context) {
inputMacAddr := c.Param("mac-addr")
macAddr, err := net.ParseMAC(inputMacAddr)
if err != nil {
logger.Warn().Err(err).Str("sendWol", inputMacAddr).Msg("Invalid mac address provided")
c.String(http.StatusBadRequest, "Invalid mac address provided")
return
}
macAddrString := macAddr.String()
err = rpcSendWOLMagicPacket(macAddrString)
if err != nil {
logger.Warn().Err(err).Str("sendWOL", macAddrString).Msg("Failed to send WOL magic packet")
c.String(http.StatusInternalServerError, "Failed to send WOL to %s: %v", macAddrString, err)
return
}
c.String(http.StatusOK, "WOL sent to %s ", macAddr)
}