Compare commits

...

5 Commits

Author SHA1 Message Date
Aveline 96b4850527
Merge dafebacdfd into 31ea366e51 2025-11-10 14:53:22 +01:00
Marc Brooks 31ea366e51
chore: upgrade UI packages (except vite) (#954) 2025-11-10 14:53:18 +01:00
Marc Brooks 7f2dcc84b4
fix: don't reload page if we didn't attempt an upgrade. (#955) 2025-11-10 12:58:57 +01:00
Siyuan dafebacdfd feat: reset USB gadget if needed 2025-11-05 15:47:31 +00:00
Siyuan f0595fff40 fix: await sleep needs to be called inside async function 2025-11-05 15:45:34 +00:00
7 changed files with 529 additions and 590 deletions

View File

@ -177,7 +177,7 @@ func (u *UsbGadget) Init() error {
u.udc = udcs[0] u.udc = udcs[0]
err := u.configureUsbGadget(false) err := u.configureUsbGadget(false, true)
if err != nil { if err != nil {
return u.logError("unable to initialize USB stack", err) return u.logError("unable to initialize USB stack", err)
} }
@ -185,13 +185,13 @@ func (u *UsbGadget) Init() error {
return nil return nil
} }
func (u *UsbGadget) UpdateGadgetConfig() error { func (u *UsbGadget) UpdateGadgetConfig(resetUsbIfNeeded bool) error {
u.configLock.Lock() u.configLock.Lock()
defer u.configLock.Unlock() defer u.configLock.Unlock()
u.loadGadgetConfig() u.loadGadgetConfig()
err := u.configureUsbGadget(true) err := u.configureUsbGadget(true, resetUsbIfNeeded)
if err != nil { if err != nil {
return u.logError("unable to update gadget config", err) return u.logError("unable to update gadget config", err)
} }
@ -199,14 +199,28 @@ func (u *UsbGadget) UpdateGadgetConfig() error {
return nil return nil
} }
func (u *UsbGadget) configureUsbGadget(resetUsb bool) error { func (u *UsbGadget) configureUsbGadget(resetUsb bool, resetUsbIfNeeded bool) error {
return u.WithTransaction(func() error { f := func(resetUsbBefore bool, resetUsbAfter bool) func() error {
u.tx.MountConfigFS() return func() error {
u.tx.CreateConfigPath() if resetUsbBefore {
u.tx.WriteGadgetConfig() u.tx.RebindUsb(true)
if resetUsb { }
u.tx.RebindUsb(true) u.tx.MountConfigFS()
u.tx.CreateConfigPath()
u.tx.WriteGadgetConfig()
if resetUsbAfter {
u.tx.RebindUsb(true)
}
return nil
} }
return nil }
})
// initial attempt to configure the gadget
err := u.WithTransaction(f(false, resetUsb))
if err != nil && !resetUsbIfNeeded {
return err
}
// if the initial attempt failed, try to configure the gadget again with the resetUsb flag
return u.WithTransaction(f(true, resetUsb))
} }

View File

@ -678,7 +678,7 @@ func rpcSetUsbConfig(usbConfig usbgadget.Config) error {
LoadConfig() LoadConfig()
config.UsbConfig = &usbConfig config.UsbConfig = &usbConfig
gadget.SetGadgetConfig(config.UsbConfig) gadget.SetGadgetConfig(config.UsbConfig)
return updateUsbRelatedConfig() return updateUsbRelatedConfig(false)
} }
func rpcGetWakeOnLanDevices() ([]WakeOnLanDevice, error) { func rpcGetWakeOnLanDevices() ([]WakeOnLanDevice, error) {
@ -890,8 +890,8 @@ func rpcGetUsbDevices() (usbgadget.Devices, error) {
return *config.UsbDevices, nil return *config.UsbDevices, nil
} }
func updateUsbRelatedConfig() error { func updateUsbRelatedConfig(resetUsbIfNeeded bool) error {
if err := gadget.UpdateGadgetConfig(); err != nil { if err := gadget.UpdateGadgetConfig(resetUsbIfNeeded); err != nil {
return fmt.Errorf("failed to write gadget config: %w", err) return fmt.Errorf("failed to write gadget config: %w", err)
} }
if err := SaveConfig(); err != nil { if err := SaveConfig(); err != nil {
@ -903,7 +903,7 @@ func updateUsbRelatedConfig() error {
func rpcSetUsbDevices(usbDevices usbgadget.Devices) error { func rpcSetUsbDevices(usbDevices usbgadget.Devices) error {
config.UsbDevices = &usbDevices config.UsbDevices = &usbDevices
gadget.SetGadgetDevices(config.UsbDevices) gadget.SetGadgetDevices(config.UsbDevices)
return updateUsbRelatedConfig() return updateUsbRelatedConfig(false)
} }
func rpcSetUsbDeviceState(device string, enabled bool) error { func rpcSetUsbDeviceState(device string, enabled bool) error {
@ -920,7 +920,7 @@ func rpcSetUsbDeviceState(device string, enabled bool) error {
return fmt.Errorf("invalid device: %s", device) return fmt.Errorf("invalid device: %s", device)
} }
gadget.SetGadgetDevices(config.UsbDevices) gadget.SetGadgetDevices(config.UsbDevices)
return updateUsbRelatedConfig() return updateUsbRelatedConfig(false)
} }
func rpcSetCloudUrl(apiUrl string, appUrl string) error { func rpcSetCloudUrl(apiUrl string, appUrl string) error {

1004
ui/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
{ {
"name": "kvm-ui", "name": "kvm-ui",
"private": true, "private": true,
"version": "2025.10.24.2140", "version": "2025.11.07.2130",
"type": "module", "type": "module",
"engines": { "engines": {
"node": "^22.20.0" "node": "^22.20.0"
@ -38,7 +38,7 @@
"@xterm/addon-webgl": "^0.18.0", "@xterm/addon-webgl": "^0.18.0",
"@xterm/xterm": "^5.5.0", "@xterm/xterm": "^5.5.0",
"cva": "^1.0.0-beta.4", "cva": "^1.0.0-beta.4",
"dayjs": "^1.11.18", "dayjs": "^1.11.19",
"eslint-import-resolver-alias": "^1.1.2", "eslint-import-resolver-alias": "^1.1.2",
"focus-trap-react": "^11.0.4", "focus-trap-react": "^11.0.4",
"framer-motion": "^12.23.24", "framer-motion": "^12.23.24",
@ -47,52 +47,52 @@
"react": "^19.2.0", "react": "^19.2.0",
"react-animate-height": "^3.2.3", "react-animate-height": "^3.2.3",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"react-hook-form": "^7.65.0", "react-hook-form": "^7.66.0",
"react-hot-toast": "^2.6.0", "react-hot-toast": "^2.6.0",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-router": "^7.9.5", "react-router": "^7.9.5",
"react-simple-keyboard": "^3.8.131", "react-simple-keyboard": "^3.8.132",
"react-use-websocket": "^4.13.0", "react-use-websocket": "^4.13.0",
"react-xtermjs": "^1.0.10", "react-xtermjs": "^1.0.10",
"recharts": "^3.3.0", "recharts": "^3.3.0",
"tailwind-merge": "^3.3.1", "tailwind-merge": "^3.3.1",
"usehooks-ts": "^3.1.1", "usehooks-ts": "^3.1.1",
"validator": "^13.15.15", "validator": "^13.15.20",
"zustand": "^4.5.2" "zustand": "^4.5.2"
}, },
"devDependencies": { "devDependencies": {
"@eslint/compat": "^1.4.1", "@eslint/compat": "^1.4.1",
"@eslint/eslintrc": "^3.3.1", "@eslint/eslintrc": "^3.3.1",
"@eslint/js": "^9.39.0", "@eslint/js": "^9.39.1",
"@inlang/cli": "^3.0.12", "@inlang/cli": "^3.0.12",
"@inlang/paraglide-js": "^2.4.0", "@inlang/paraglide-js": "^2.4.0",
"@inlang/plugin-m-function-matcher": "^2.1.0", "@inlang/plugin-m-function-matcher": "^2.1.0",
"@inlang/plugin-message-format": "^4.0.0", "@inlang/plugin-message-format": "^4.0.0",
"@inlang/sdk": "^2.4.9", "@inlang/sdk": "^2.4.9",
"@tailwindcss/forms": "^0.5.10", "@tailwindcss/forms": "^0.5.10",
"@tailwindcss/postcss": "^4.1.16", "@tailwindcss/postcss": "^4.1.17",
"@tailwindcss/typography": "^0.5.19", "@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.1.16", "@tailwindcss/vite": "^4.1.17",
"@types/react": "^19.2.2", "@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2", "@types/react-dom": "^19.2.2",
"@types/semver": "^7.7.1", "@types/semver": "^7.7.1",
"@types/validator": "^13.15.3", "@types/validator": "^13.15.4",
"@typescript-eslint/eslint-plugin": "^8.46.2", "@typescript-eslint/eslint-plugin": "^8.46.3",
"@typescript-eslint/parser": "^8.46.2", "@typescript-eslint/parser": "^8.46.3",
"@vitejs/plugin-react-swc": "^4.2.0", "@vitejs/plugin-react-swc": "^4.2.1",
"autoprefixer": "^10.4.21", "autoprefixer": "^10.4.21",
"eslint": "^9.38.0", "eslint": "^9.39.1",
"eslint-config-prettier": "^10.1.8", "eslint-config-prettier": "^10.1.8",
"eslint-plugin-import": "^2.32.0", "eslint-plugin-import": "^2.32.0",
"eslint-plugin-prettier": "^5.5.4", "eslint-plugin-prettier": "^5.5.4",
"eslint-plugin-react": "^7.37.5", "eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24", "eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.4.0", "globals": "^16.5.0",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"prettier": "^3.6.2", "prettier": "^3.6.2",
"prettier-plugin-tailwindcss": "^0.7.1", "prettier-plugin-tailwindcss": "^0.7.1",
"tailwindcss": "^4.1.16", "tailwindcss": "^4.1.17",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"vite": "^7.1.12", "vite": "^7.1.12",
"vite-tsconfig-paths": "^5.1.4" "vite-tsconfig-paths": "^5.1.4"

View File

@ -603,6 +603,9 @@ export interface UpdateState {
updateErrorMessage: string | null; updateErrorMessage: string | null;
setUpdateErrorMessage: (errorMessage: string) => void; setUpdateErrorMessage: (errorMessage: string) => void;
shouldReload: boolean;
setShouldReload: (reloadRequired: boolean) => void;
} }
export const useUpdateStore = create<UpdateState>(set => ({ export const useUpdateStore = create<UpdateState>(set => ({
@ -640,6 +643,9 @@ export const useUpdateStore = create<UpdateState>(set => ({
updateErrorMessage: null, updateErrorMessage: null,
setUpdateErrorMessage: (errorMessage: string) => setUpdateErrorMessage: (errorMessage: string) =>
set({ updateErrorMessage: errorMessage }), set({ updateErrorMessage: errorMessage }),
shouldReload: false,
setShouldReload: (reloadRequired: boolean) => set({ shouldReload: reloadRequired }),
})); }));
export type UsbConfigModalViews = "updateUsbConfig" | "updateUsbConfigSuccess"; export type UsbConfigModalViews = "updateUsbConfig" | "updateUsbConfigSuccess";
@ -850,12 +856,12 @@ export interface MacrosState {
loadMacros: () => Promise<void>; loadMacros: () => Promise<void>;
saveMacros: (macros: KeySequence[]) => Promise<void>; saveMacros: (macros: KeySequence[]) => Promise<void>;
sendFn: sendFn:
| (( | ((
method: string, method: string,
params: unknown, params: unknown,
callback?: ((resp: JsonRpcResponse) => void) | undefined, callback?: ((resp: JsonRpcResponse) => void) | undefined,
) => void) ) => void)
| null; | null;
setSendFn: ( setSendFn: (
sendFn: ( sendFn: (
method: string, method: string,

View File

@ -18,20 +18,24 @@ export default function SettingsGeneralUpdateRoute() {
const location = useLocation(); const location = useLocation();
const { updateSuccess } = location.state || {}; const { updateSuccess } = location.state || {};
const { setModalView, otaState } = useUpdateStore(); const { setModalView, otaState, shouldReload, setShouldReload } = useUpdateStore();
const { send } = useJsonRpc(); const { send } = useJsonRpc();
const onClose = useCallback(async () => { const onClose = useCallback(async () => {
navigate(".."); // back to the devices.$id.settings page 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); if (shouldReload) {
window.location.reload(); // force a full reload to ensure the current device/cloud UI version is loaded setShouldReload(false);
}, [navigate]); await sleep(1000); // Add 1s delay between navigation and calling reload() to prevent reload from interrupting the navigation.
window.location.reload(); // force a full reload to ensure the current device/cloud UI version is loaded
}
}, [navigate, setShouldReload, shouldReload]);
const onConfirmUpdate = useCallback(() => { const onConfirmUpdate = useCallback(() => {
setShouldReload(true);
send("tryUpdate", {}); send("tryUpdate", {});
setModalView("updating"); setModalView("updating");
}, [send, setModalView]); }, [send, setModalView, setShouldReload]);
useEffect(() => { useEffect(() => {
if (otaState.updating) { if (otaState.updating) {
@ -133,6 +137,7 @@ function LoadingState({
const { setModalView } = useUpdateStore(); const { setModalView } = useUpdateStore();
const progressBarRef = useRef<HTMLDivElement>(null); const progressBarRef = useRef<HTMLDivElement>(null);
useEffect(() => { useEffect(() => {
abortControllerRef.current = new AbortController(); abortControllerRef.current = new AbortController();
const signal = abortControllerRef.current.signal; const signal = abortControllerRef.current.signal;

View File

@ -66,7 +66,7 @@ func setMassStorageMode(cdrom bool) error {
return nil return nil
} }
return gadget.UpdateGadgetConfig() return gadget.UpdateGadgetConfig(true)
} }
func mountImage(imagePath string) error { func mountImage(imagePath string) error {