refactor: simplify version check and downgrade

This commit is contained in:
Siyuan 2025-11-07 12:20:45 +00:00
parent 329ad025bf
commit 1bca0c5e26
7 changed files with 193 additions and 126 deletions

View File

@ -239,10 +239,29 @@ func (s *State) getUpdateStatus(
systemUpdate = &currentSystemUpdate 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 // Get local versions
systemVersionLocal, appVersionLocal, err := s.getLocalVersion() systemVersionLocal, appVersionLocal, err := s.getLocalVersion()
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("error getting local version: %w", err) return fmt.Errorf("error getting local version: %w", err)
} }
appUpdate.localVersion = appVersionLocal.String() appUpdate.localVersion = appVersionLocal.String()
systemUpdate.localVersion = systemVersionLocal.String() systemUpdate.localVersion = systemVersionLocal.String()
@ -255,7 +274,7 @@ func (s *State) getUpdateStatus(
} else { } else {
err = fmt.Errorf("error checking for updates: %w", err) err = fmt.Errorf("error checking for updates: %w", err)
} }
return return err
} }
appUpdate.url = remoteMetadata.AppURL appUpdate.url = remoteMetadata.AppURL
appUpdate.hash = remoteMetadata.AppHash appUpdate.hash = remoteMetadata.AppHash
@ -269,7 +288,7 @@ func (s *State) getUpdateStatus(
systemVersionRemote, err := semver.NewVersion(remoteMetadata.SystemVersion) systemVersionRemote, err := semver.NewVersion(remoteMetadata.SystemVersion)
if err != nil { if err != nil {
err = fmt.Errorf("error parsing remote system version: %w", err) err = fmt.Errorf("error parsing remote system version: %w", err)
return return err
} }
systemUpdate.available = systemVersionRemote.GreaterThan(systemVersionLocal) systemUpdate.available = systemVersionRemote.GreaterThan(systemVersionLocal)
systemUpdate.downgradeAvailable = systemVersionRemote.LessThan(systemVersionLocal) systemUpdate.downgradeAvailable = systemVersionRemote.LessThan(systemVersionLocal)
@ -277,7 +296,7 @@ func (s *State) getUpdateStatus(
appVersionRemote, err := semver.NewVersion(remoteMetadata.AppVersion) appVersionRemote, err := semver.NewVersion(remoteMetadata.AppVersion)
if err != nil { if err != nil {
err = fmt.Errorf("error parsing remote app version: %w, %s", err, remoteMetadata.AppVersion) err = fmt.Errorf("error parsing remote app version: %w, %s", err, remoteMetadata.AppVersion)
return return err
} }
appUpdate.available = appVersionRemote.GreaterThan(appVersionLocal) appUpdate.available = appVersionRemote.GreaterThan(appVersionLocal)
appUpdate.downgradeAvailable = appVersionRemote.LessThan(appVersionLocal) appUpdate.downgradeAvailable = appVersionRemote.LessThan(appVersionLocal)
@ -293,18 +312,17 @@ func (s *State) getUpdateStatus(
appUpdate.available = false appUpdate.available = false
} }
s.componentUpdateStatuses["app"] = *appUpdate return nil
s.componentUpdateStatuses["system"] = *systemUpdate
return
} }
// GetUpdateStatus returns the current update status (for backwards compatibility) // GetUpdateStatus returns the current update status (for backwards compatibility)
func (s *State) GetUpdateStatus(ctx context.Context, params UpdateParams) (*UpdateStatus, error) { func (s *State) GetUpdateStatus(ctx context.Context, params UpdateParams) (*UpdateStatus, error) {
_, _, err := s.getUpdateStatus(ctx, params) appUpdate := &componentUpdateStatus{}
systemUpdate := &componentUpdateStatus{}
err := s.doGetUpdateStatus(ctx, params, appUpdate, systemUpdate)
if err != nil { if err != nil {
return nil, fmt.Errorf("error getting update status: %w", err) return nil, fmt.Errorf("error getting update status: %w", err)
} }
return s.ToUpdateStatus(), nil return toUpdateStatus(appUpdate, systemUpdate, ""), nil
} }

View File

@ -156,18 +156,7 @@ func (s *State) GetTargetVersion(component string) string {
return componentUpdate.targetVersion return componentUpdate.targetVersion
} }
// ToUpdateStatus converts the State to the UpdateStatus func toUpdateStatus(appUpdate *componentUpdateStatus, systemUpdate *componentUpdateStatus, error string) *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{ return &UpdateStatus{
Local: &LocalMetadata{ Local: &LocalMetadata{
AppVersion: appUpdate.localVersion, AppVersion: appUpdate.localVersion,
@ -185,10 +174,25 @@ func (s *State) ToUpdateStatus() *UpdateStatus {
SystemDowngradeAvailable: systemUpdate.downgradeAvailable, SystemDowngradeAvailable: systemUpdate.downgradeAvailable,
AppUpdateAvailable: appUpdate.available, AppUpdateAvailable: appUpdate.available,
AppDowngradeAvailable: appUpdate.downgradeAvailable, AppDowngradeAvailable: appUpdate.downgradeAvailable,
Error: s.error, Error: 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 // IsUpdatePending returns true if an update is pending
func (s *State) IsUpdatePending() bool { func (s *State) IsUpdatePending() bool {
return s.updating return s.updating

View File

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

73
ota.go
View File

@ -6,7 +6,6 @@ import (
"net/http" "net/http"
"os" "os"
"strings" "strings"
"time"
"github.com/Masterminds/semver/v3" "github.com/Masterminds/semver/v3"
"github.com/jetkvm/kvm/internal/ota" "github.com/jetkvm/kvm/internal/ota"
@ -135,72 +134,56 @@ func rpcGetLocalVersion() (*ota.LocalMetadata, error) {
}, nil }, nil
} }
// ComponentName represents the name of a component type updateParams struct {
type tryUpdateComponents struct {
AppTargetVersion string `json:"app"` AppTargetVersion string `json:"app"`
SystemTargetVersion string `json:"system"` SystemTargetVersion string `json:"system"`
Components string `json:"components,omitempty"` // components is a comma-separated list of components to update Components string `json:"components,omitempty"` // components is a comma-separated list of components to update
} }
func rpcTryUpdate() error { func rpcTryUpdate() error {
return rpcTryUpdateComponents(tryUpdateComponents{ return rpcTryUpdateComponents(updateParams{
AppTargetVersion: "", AppTargetVersion: "",
SystemTargetVersion: "", SystemTargetVersion: "",
}, config.IncludePreRelease, false, false) }, config.IncludePreRelease, false)
} }
func rpcTryUpdateComponents(components tryUpdateComponents, includePreRelease bool, checkOnly bool, resetConfig bool) error { // 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 {
updateParams := ota.UpdateParams{ updateParams := ota.UpdateParams{
DeviceID: GetDeviceID(), DeviceID: GetDeviceID(),
IncludePreRelease: includePreRelease, IncludePreRelease: includePreRelease,
CheckOnly: checkOnly,
ResetConfig: resetConfig, ResetConfig: resetConfig,
} }
logger.Info().Interface("components", components).Msg("components") updateParams.AppTargetVersion = params.AppTargetVersion
if err := otaState.SetTargetVersion("app", params.AppTargetVersion); err != nil {
currentAppTargetVersion := otaState.GetTargetVersion("app")
appTargetVersionChanged := currentAppTargetVersion != components.AppTargetVersion
updateParams.AppTargetVersion = components.AppTargetVersion
if err := otaState.SetTargetVersion("app", components.AppTargetVersion); err != nil {
return fmt.Errorf("failed to set app target version: %w", err) return fmt.Errorf("failed to set app target version: %w", err)
} }
currentSystemTargetVersion := otaState.GetTargetVersion("system") updateParams.SystemTargetVersion = params.SystemTargetVersion
systemTargetVersionChanged := currentSystemTargetVersion != components.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) return fmt.Errorf("failed to set system target version: %w", err)
} }
if components.Components != "" { if params.Components != "" {
updateParams.Components = strings.Split(components.Components, ",") updateParams.Components = strings.Split(params.Components, ",")
}
// if it's a check only update, we don't need to try to update, we just need to check if the version is available
// and return the error immediately then revert the previous target versions
if checkOnly {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
_, err := otaState.GetUpdateStatus(ctx, updateParams)
if err == nil {
return nil
}
// revert the previous target versions
if appTargetVersionChanged {
if err := otaState.SetTargetVersion("app", currentAppTargetVersion); err != nil {
return fmt.Errorf("failed to revert app target version: %w", err)
}
}
if systemTargetVersionChanged {
if err := otaState.SetTargetVersion("system", currentSystemTargetVersion); err != nil {
return fmt.Errorf("failed to revert system target version: %w", err)
}
}
return err
} }
go func() { go func() {

View File

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

View File

@ -23,7 +23,8 @@ export default function SettingsGeneralUpdateRoute() {
const { send } = useJsonRpc(); const { send } = useJsonRpc();
const downgrade = useMemo(() => searchParams.get("downgrade") === "true", [searchParams]); const downgrade = useMemo(() => searchParams.get("downgrade") === "true", [searchParams]);
const updateComponents = useMemo(() => searchParams.get("components") || "", [searchParams]); const customAppVersion = useMemo(() => searchParams.get("app") || "", [searchParams]);
const customSystemVersion = useMemo(() => searchParams.get("system") || "", [searchParams]);
const resetConfig = useMemo(() => searchParams.get("resetConfig") === "true", [searchParams]); const resetConfig = useMemo(() => searchParams.get("resetConfig") === "true", [searchParams]);
const onClose = useCallback(async () => { const onClose = useCallback(async () => {
@ -38,18 +39,28 @@ export default function SettingsGeneralUpdateRoute() {
setModalView("updating"); setModalView("updating");
}, [send, setModalView]); }, [send, setModalView]);
const onConfirmDowngrade = useCallback((system?: string, app?: string) => { const onConfirmDowngrade = useCallback(() => {
const components = [];
if (customSystemVersion) {
components.push("system");
}
if (customAppVersion) {
components.push("app");
}
send("tryUpdateComponents", { send("tryUpdateComponents", {
components: { params: {
system, app, components: components.join(","),
components: updateComponents app: customAppVersion,
system: customSystemVersion,
}, },
includePreRelease: true, includePreRelease: false,
checkOnly: false, resetConfig,
resetConfig: resetConfig, }, (resp) => {
if ("error" in resp) return;
setModalView("updating");
}); });
setModalView("updating"); }, [send, setModalView, customAppVersion, customSystemVersion, resetConfig]);
}, [send, setModalView, updateComponents, resetConfig]);
useEffect(() => { useEffect(() => {
if (otaState.updating) { if (otaState.updating) {
@ -64,10 +75,12 @@ export default function SettingsGeneralUpdateRoute() {
}, [otaState.error, otaState.updating, setModalView, updateSuccess]); }, [otaState.error, otaState.updating, setModalView, updateSuccess]);
return <Dialog return <Dialog
onClose={onClose} onClose={onClose}
onConfirmUpdate={onConfirmUpdate} onConfirmUpdate={onConfirmUpdate}
onConfirmDowngrade={onConfirmDowngrade} onConfirmDowngrade={onConfirmDowngrade}
downgrade={downgrade} downgrade={downgrade}
customAppVersion={customAppVersion}
customSystemVersion={customSystemVersion}
/>; />;
} }
@ -76,11 +89,15 @@ export function Dialog({
onConfirmUpdate, onConfirmUpdate,
onConfirmDowngrade, onConfirmDowngrade,
downgrade, downgrade,
customAppVersion,
customSystemVersion,
}: Readonly<{ }: Readonly<{
downgrade: boolean; downgrade: boolean;
onClose: () => void; onClose: () => void;
onConfirmUpdate: () => void; onConfirmUpdate: () => void;
onConfirmDowngrade: () => void; onConfirmDowngrade: () => void;
customAppVersion?: string;
customSystemVersion?: string;
}>) { }>) {
const { navigateTo } = useDeviceUiNavigation(); const { navigateTo } = useDeviceUiNavigation();
@ -92,8 +109,7 @@ export function Dialog({
(versionInfo: SystemVersionInfo) => { (versionInfo: SystemVersionInfo) => {
const hasUpdate = const hasUpdate =
versionInfo?.systemUpdateAvailable || versionInfo?.appUpdateAvailable; versionInfo?.systemUpdateAvailable || versionInfo?.appUpdateAvailable;
const hasDowngrade = const hasDowngrade = customSystemVersion !== undefined || customAppVersion !== undefined;
versionInfo?.systemDowngradeAvailable || versionInfo?.appDowngradeAvailable;
setVersionInfo(versionInfo); setVersionInfo(versionInfo);
@ -105,7 +121,7 @@ export function Dialog({
setModalView("upToDate"); setModalView("upToDate");
} }
}, },
[setModalView, downgrade], [setModalView, downgrade, customAppVersion, customSystemVersion],
); );
const onCancelDowngrade = useCallback(() => { const onCancelDowngrade = useCallback(() => {
@ -137,9 +153,10 @@ export function Dialog({
)} )}
{modalView === "updateDowngradeAvailable" && ( {modalView === "updateDowngradeAvailable" && (
<UpdateDowngradeAvailableState <UpdateDowngradeAvailableState
appVersion={customAppVersion}
systemVersion={customSystemVersion}
onConfirmDowngrade={onConfirmDowngrade} onConfirmDowngrade={onConfirmDowngrade}
onCancelDowngrade={onCancelDowngrade} onCancelDowngrade={onCancelDowngrade}
versionInfo={versionInfo!}
/> />
)} )}
@ -455,20 +472,19 @@ function UpdateAvailableState({
} }
function UpdateDowngradeAvailableState({ function UpdateDowngradeAvailableState({
versionInfo, appVersion,
systemVersion,
onConfirmDowngrade, onConfirmDowngrade,
onCancelDowngrade, onCancelDowngrade,
}: { }: {
versionInfo: SystemVersionInfo; appVersion?: string;
onConfirmDowngrade: (system?: string, app?: string) => void; systemVersion?: string;
onConfirmDowngrade: () => void;
onCancelDowngrade: () => void; onCancelDowngrade: () => void;
}) { }) {
const confirmDowngrade = useCallback(() => { const confirmDowngrade = useCallback(() => {
onConfirmDowngrade( onConfirmDowngrade();
versionInfo?.remote?.systemVersion || undefined, }, [onConfirmDowngrade]);
versionInfo?.remote?.appVersion || undefined,
);
}, [versionInfo, onConfirmDowngrade]);
return ( return (
<div className="flex flex-col items-start justify-start space-y-4 text-left"> <div className="flex flex-col items-start justify-start space-y-4 text-left">
<div className="text-left"> <div className="text-left">
@ -479,15 +495,15 @@ function UpdateDowngradeAvailableState({
{m.general_update_downgrade_available_description()} {m.general_update_downgrade_available_description()}
</p> </p>
<p className="mb-4 text-sm text-slate-600 dark:text-slate-300"> <p className="mb-4 text-sm text-slate-600 dark:text-slate-300">
{versionInfo?.systemDowngradeAvailable ? ( {systemVersion ? (
<> <>
<span className="font-semibold">{m.general_update_system_type()}</span>: {versionInfo?.remote?.systemVersion} <span className="font-semibold">{m.general_update_system_type()}</span>: {systemVersion}
<br /> <br />
</> </>
) : null} ) : null}
{versionInfo?.appDowngradeAvailable ? ( {appVersion ? (
<> <>
<span className="font-semibold">{m.general_update_application_type()}</span>: {versionInfo?.remote?.appVersion} <span className="font-semibold">{m.general_update_application_type()}</span>: {appVersion}
</> </>
) : null} ) : null}
</p> </p>

View File

@ -244,3 +244,21 @@ export async function getLocalVersion() {
if (response.error) throw response.error; if (response.error) throw response.error;
return response.result; 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;
}