Compare commits

..

2 Commits

Author SHA1 Message Date
Marc Brooks ca1c36b84e
Revamp the OTA and reboot processing
OTA supplies port-reboot action to handle the rebooting device.
Make sure we force-reload the page after redirection.
Move reboot logic into hw.go and make it set the willReboot message with parameters if provided.
Improve logging consistency.
2025-10-20 22:24:46 -05:00
Marc Brooks d3b0f1bebc
Add inlang/paraglide-js localization
Remove the temporary directory after extracting buildkit
Localize the extension popovers.
Update package and fix tsconfig.json
Expand development directory guide
Move messages under localization
Popovers and sidebar
Update Chinese translations
Accidentally lost the changes that @ym provided, brought them back
File formatting pass
Localized all components, hooks, providers, hooks
Localize all pages except Settings
Bump packages
Settings Access page
Settings local auth page
Fix ref lint warning
Settings Advanced page
Fix UI lint warnings there were a bunch of ref and useEffect violations.
Settings appearance page
Settings general pages
Settings hardware page
Settings keyboard page
Settings macros pages
Settings mouse page
Settings page
Settings video page
Settings network page
Fix compilation issues
Ran machine translate
Use getLocale for date, relative time, and money formatting
Fix eslint
Delete unused messages
Added setting to choose locale
Merged in dev hotfix
Fix update status rendering
Add note to do back-translation
Improve developer guidance
Clean up some localization issues and extract UpdatingStatusCard
Fix copy-pasta errors
Packages update
Fix silly error in progress messages.
2025-10-20 18:23:13 -05:00
23 changed files with 307 additions and 280 deletions

32
hw.go
View File

@ -3,6 +3,7 @@ package kvm
import (
"fmt"
"os"
"os/exec"
"regexp"
"strings"
"sync"
@ -36,6 +37,37 @@ func readOtpEntropy() ([]byte, error) { //nolint:unused
return content[0x17:0x1C], nil
}
func hwReboot(force bool, postRebootAction *PostRebootAction, delay time.Duration) error {
logger.Info().Msgf("Reboot requested, rebooting in %d seconds...", delay)
writeJSONRPCEvent("willReboot", postRebootAction, currentSession)
time.Sleep(1 * time.Second) // Wait for the JSONRPCEvent to be sent
nativeInstance.SwitchToScreenIfDifferent("rebooting_screen")
time.Sleep(delay - (1 * time.Second)) // wait requested extra settle time
args := []string{}
if force {
args = append(args, "-f")
}
cmd := exec.Command("reboot", args...)
err := cmd.Start()
if err != nil {
logger.Error().Err(err).Msg("failed to reboot")
switchToMainScreen()
return fmt.Errorf("failed to reboot: %w", err)
}
// If the reboot command is successful, exit the program after 5 seconds
go func() {
time.Sleep(5 * time.Second)
os.Exit(0)
}()
return nil
}
var deviceID string
var deviceIDOnce sync.Once

View File

@ -173,34 +173,8 @@ func rpcGetDeviceID() (string, error) {
}
func rpcReboot(force bool) error {
logger.Info().Msg("Got reboot request from JSONRPC, rebooting...")
writeJSONRPCEvent("willReboot", nil, currentSession)
// Wait for the JSONRPCEvent to be sent
time.Sleep(1 * time.Second)
nativeInstance.SwitchToScreenIfDifferent("rebooting_screen")
args := []string{}
if force {
args = append(args, "-f")
}
cmd := exec.Command("reboot", args...)
err := cmd.Start()
if err != nil {
logger.Error().Err(err).Msg("failed to reboot")
switchToMainScreen()
return fmt.Errorf("failed to reboot: %w", err)
}
// If the reboot command is successful, exit the program after 5 seconds
go func() {
time.Sleep(5 * time.Second)
os.Exit(0)
}()
return nil
logger.Info().Msg("Got reboot request via RPC")
return hwReboot(force, nil, 0)
}
var streamFactor = 1.0

View File

@ -37,14 +37,17 @@ func initNative(systemVersion *semver.Version, appVersion *semver.Version) {
nativeLogger.Trace().Str("event", event).Msg("rpc event received")
switch event {
case "resetConfig":
nativeLogger.Info().Msg("Reset configuration request via native rpc event")
err := rpcResetConfig()
if err != nil {
nativeLogger.Warn().Err(err).Msg("error resetting config")
}
_ = rpcReboot(true)
case "reboot":
nativeLogger.Info().Msg("Reboot request via native rpc event")
_ = rpcReboot(true)
case "toggleDHCPClient":
nativeLogger.Info().Msg("Toggle DHCP request via native rpc event")
_ = rpcToggleDHCPClient()
default:
nativeLogger.Warn().Str("event", event).Msg("unknown rpc event received")

View File

@ -193,6 +193,7 @@ func shouldRebootForNetworkChange(oldConfig, newConfig *types.NetworkConfig) (re
oldIPv4Mode := oldConfig.IPv4Mode.String
newIPv4Mode := newConfig.IPv4Mode.String
// IPv4 mode change requires reboot
if newIPv4Mode != oldIPv4Mode {
rebootRequired = true
@ -284,7 +285,8 @@ func rpcSetNetworkSettings(settings RpcNetworkSettings) (*RpcNetworkSettings, er
}
if rebootRequired {
if err := rpcReboot(false); err != nil {
l.Info().Msg("Rebooting due to network changes")
if err := hwReboot(true, postRebootAction, 0); err != nil {
return nil, err
}
}

24
ota.go
View File

@ -487,25 +487,15 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
}
if rebootNeeded {
scopedLogger.Info().Msg("System Rebooting in 10s")
scopedLogger.Info().Msg("System Rebooting due to OTA update")
// TODO: Future enhancement - send postRebootAction to redirect to release notes
// Example:
// postRebootAction := &PostRebootAction{
// HealthCheck: "[..]/device/status",
// RedirectUrl: "[..]/settings/general/update?version=X.Y.Z",
// }
// writeJSONRPCEvent("willReboot", postRebootAction, currentSession)
postRebootAction := &PostRebootAction{
HealthCheck: "/device/status",
RedirectUrl: "/settings/general/update?version=" + remote.SystemVersion,
}
time.Sleep(10 * time.Second)
cmd := exec.Command("reboot")
err := cmd.Start()
if err != nil {
otaState.Error = fmt.Sprintf("Failed to start reboot: %v", err)
scopedLogger.Error().Err(err).Msg("Failed to start reboot")
return fmt.Errorf("failed to start reboot: %w", err)
} else {
os.Exit(0)
if err := hwReboot(true, postRebootAction, 10*time.Second); err != nil {
return fmt.Errorf("error requesting reboot: %w", err)
}
}

View File

@ -271,6 +271,7 @@
"general_update_status_downloading": "Downloader {update_type} opdatering…",
"general_update_status_fetching": "Henter opdateringsoplysninger…",
"general_update_status_installing": "Installation af {update_type} opdatering…",
"general_update_status_progress": "{part} fremskridt",
"general_update_status_verifying": "Bekræfter {update_type} opdatering…",
"general_update_system_type": "System",
"general_update_system_update_title": "Linux-systemopdatering",

View File

@ -31,7 +31,7 @@
"access_no_device_id": "Keine Geräte-ID verfügbar",
"access_private_key_description": "Aus Sicherheitsgründen wird es nach dem Speichern nicht angezeigt.",
"access_private_key_label": "Privater Schlüssel",
"access_provider_custom": "Brauch",
"access_provider_custom": "Benutzerdefiniert",
"access_provider_jetkvm": "JetKVM Cloud",
"access_remote_description": "Verwalten Sie den Modus des Fernzugriffs auf das Gerät",
"access_security_encryption": "Ende-zu-Ende-Verschlüsselung mit WebRTC (DTLS und SRTP)",
@ -42,7 +42,7 @@
"access_title": "Zugang",
"access_tls_certificate_description": "Fügen Sie unten Ihr TLS-Zertifikat ein. Geben Sie bei Zertifikatsketten die gesamte Kette an (Blatt-, Zwischen- und Stammzertifikate).",
"access_tls_certificate_title": "TLS-Zertifikat",
"access_tls_custom": "Brauch",
"access_tls_custom": "Benutzerdefiniert",
"access_tls_disabled": "Deaktiviert",
"access_tls_self_signed": "Selbstsigniert",
"access_tls_updated": "TLS-Einstellungen erfolgreich aktualisiert",
@ -108,7 +108,7 @@
"appearance_page_description": "Passen Sie das Erscheinungsbild Ihrer JetKVM-Schnittstelle an",
"appearance_theme": "Thema",
"appearance_theme_dark": "Dunkel",
"appearance_theme_light": "Licht",
"appearance_theme_light": "Hell",
"appearance_theme_system": "System",
"appearance_title": "Aussehen",
"attach": "Befestigen",
@ -271,6 +271,7 @@
"general_update_status_downloading": "Das Update {update_type} wird heruntergeladen…",
"general_update_status_fetching": "Update-Informationen werden abgerufen …",
"general_update_status_installing": "Das Update {update_type} wird installiert…",
"general_update_status_progress": "{part} Fortschritt",
"general_update_status_verifying": "Überprüfung des Updates {update_type} …",
"general_update_system_type": "System",
"general_update_system_update_title": "Linux-Systemupdate",
@ -412,7 +413,7 @@
"locale_de": "Deutsch",
"locale_en": "Englisch",
"locale_es": "Spanisch",
"locale_fr": "Deutsch",
"locale_fr": "Französisch",
"locale_it": "Italienisch",
"locale_nb": "Norwegisch (bokmål)",
"locale_sv": "Schwedisch",

View File

@ -271,6 +271,7 @@
"general_update_status_downloading": "Downloading {update_type} update…",
"general_update_status_fetching": "Fetching update information…",
"general_update_status_installing": "Installing {update_type} update…",
"general_update_status_progress": "{part} progress",
"general_update_status_verifying": "Verifying {update_type} update…",
"general_update_system_type": "System",
"general_update_system_update_title": "Linux System Update",

View File

@ -271,6 +271,7 @@
"general_update_status_downloading": "Descargando actualización {update_type} …",
"general_update_status_fetching": "Obteniendo información de actualización…",
"general_update_status_installing": "Instalando {update_type} actualización…",
"general_update_status_progress": "{part} progreso",
"general_update_status_verifying": "Verificando la actualización {update_type} …",
"general_update_system_type": "Sistema",
"general_update_system_update_title": "Actualización del sistema Linux",

View File

@ -271,6 +271,7 @@
"general_update_status_downloading": "Téléchargement de la mise à jour {update_type} …",
"general_update_status_fetching": "Récupération des informations de mise à jour…",
"general_update_status_installing": "Installation de la mise à jour {update_type} …",
"general_update_status_progress": "{part} progression",
"general_update_status_verifying": "Vérification de la mise à jour de {update_type} …",
"general_update_system_type": "Système",
"general_update_system_update_title": "Mise à jour du système Linux",

View File

@ -271,6 +271,7 @@
"general_update_status_downloading": "Scaricamento dell'aggiornamento {update_type} …",
"general_update_status_fetching": "Recupero delle informazioni di aggiornamento in corso…",
"general_update_status_installing": "Installazione dell'aggiornamento {update_type} …",
"general_update_status_progress": "{part} progresso",
"general_update_status_verifying": "Verifica dell'aggiornamento {update_type} …",
"general_update_system_type": "Sistema",
"general_update_system_update_title": "Aggiornamento del sistema Linux",

View File

@ -271,6 +271,7 @@
"general_update_status_downloading": "Laster ned {update_type} oppdatering…",
"general_update_status_fetching": "Henter oppdateringsinformasjon …",
"general_update_status_installing": "Installerer {update_type} oppdatering…",
"general_update_status_progress": "{part} fremgang",
"general_update_status_verifying": "Verifiserer {update_type} oppdatering…",
"general_update_system_type": "System",
"general_update_system_update_title": "Linux-systemoppdatering",

View File

@ -271,6 +271,7 @@
"general_update_status_downloading": "Laddar ner {update_type} uppdatering…",
"general_update_status_fetching": "Hämtar uppdateringsinformation…",
"general_update_status_installing": "Installerar {update_type} uppdatering…",
"general_update_status_progress": "{part} framsteg",
"general_update_status_verifying": "Verifierar {update_type} uppdatering…",
"general_update_system_type": "System",
"general_update_system_update_title": "Linux-systemuppdatering",

View File

@ -271,6 +271,7 @@
"general_update_status_downloading": "正在下载{update_type}更新…",
"general_update_status_fetching": "正在获取更新信息...",
"general_update_status_installing": "正在安装{update_type}更新...",
"general_update_status_progress": "{part}进度",
"general_update_status_verifying": "验证{update_type}更新…",
"general_update_system_type": "系统",
"general_update_system_update_title": "Linux 系统更新",

151
ui/package-lock.json generated
View File

@ -58,8 +58,8 @@
"@types/react-dom": "^19.2.2",
"@types/semver": "^7.7.1",
"@types/validator": "^13.15.3",
"@typescript-eslint/eslint-plugin": "^8.46.1",
"@typescript-eslint/parser": "^8.46.1",
"@typescript-eslint/eslint-plugin": "^8.46.2",
"@typescript-eslint/parser": "^8.46.2",
"@vitejs/plugin-react-swc": "^4.1.0",
"autoprefixer": "^10.4.21",
"eslint": "^9.38.0",
@ -2387,7 +2387,6 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.2.tgz",
"integrity": "sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==",
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.0.2"
}
@ -2397,7 +2396,6 @@
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.2.tgz",
"integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==",
"license": "MIT",
"peer": true,
"peerDependencies": {
"@types/react": "^19.2.0"
}
@ -2423,17 +2421,17 @@
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.46.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.1.tgz",
"integrity": "sha512-rUsLh8PXmBjdiPY+Emjz9NX2yHvhS11v0SR6xNJkm5GM1MO9ea/1GoDKlHHZGrOJclL/cZ2i/vRUYVtjRhrHVQ==",
"version": "8.46.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.2.tgz",
"integrity": "sha512-ZGBMToy857/NIPaaCucIUQgqueOiq7HeAKkhlvqVV4lm089zUFW6ikRySx2v+cAhKeUCPuWVHeimyk6Dw1iY3w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.46.1",
"@typescript-eslint/type-utils": "8.46.1",
"@typescript-eslint/utils": "8.46.1",
"@typescript-eslint/visitor-keys": "8.46.1",
"@typescript-eslint/scope-manager": "8.46.2",
"@typescript-eslint/type-utils": "8.46.2",
"@typescript-eslint/utils": "8.46.2",
"@typescript-eslint/visitor-keys": "8.46.2",
"graphemer": "^1.4.0",
"ignore": "^7.0.0",
"natural-compare": "^1.4.0",
@ -2447,7 +2445,7 @@
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.46.1",
"@typescript-eslint/parser": "^8.46.2",
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
@ -2463,17 +2461,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.46.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.1.tgz",
"integrity": "sha512-6JSSaBZmsKvEkbRUkf7Zj7dru/8ZCrJxAqArcLaVMee5907JdtEbKGsZ7zNiIm/UAkpGUkaSMZEXShnN2D1HZA==",
"version": "8.46.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.2.tgz",
"integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.46.1",
"@typescript-eslint/types": "8.46.1",
"@typescript-eslint/typescript-estree": "8.46.1",
"@typescript-eslint/visitor-keys": "8.46.1",
"@typescript-eslint/scope-manager": "8.46.2",
"@typescript-eslint/types": "8.46.2",
"@typescript-eslint/typescript-estree": "8.46.2",
"@typescript-eslint/visitor-keys": "8.46.2",
"debug": "^4.3.4"
},
"engines": {
@ -2489,14 +2486,14 @@
}
},
"node_modules/@typescript-eslint/project-service": {
"version": "8.46.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.1.tgz",
"integrity": "sha512-FOIaFVMHzRskXr5J4Jp8lFVV0gz5ngv3RHmn+E4HYxSJ3DgDzU7fVI1/M7Ijh1zf6S7HIoaIOtln1H5y8V+9Zg==",
"version": "8.46.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.2.tgz",
"integrity": "sha512-PULOLZ9iqwI7hXcmL4fVfIsBi6AN9YxRc0frbvmg8f+4hQAjQ5GYNKK0DIArNo+rOKmR/iBYwkpBmnIwin4wBg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.46.1",
"@typescript-eslint/types": "^8.46.1",
"@typescript-eslint/tsconfig-utils": "^8.46.2",
"@typescript-eslint/types": "^8.46.2",
"debug": "^4.3.4"
},
"engines": {
@ -2511,14 +2508,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.46.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.1.tgz",
"integrity": "sha512-weL9Gg3/5F0pVQKiF8eOXFZp8emqWzZsOJuWRUNtHT+UNV2xSJegmpCNQHy37aEQIbToTq7RHKhWvOsmbM680A==",
"version": "8.46.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.2.tgz",
"integrity": "sha512-LF4b/NmGvdWEHD2H4MsHD8ny6JpiVNDzrSZr3CsckEgCbAGZbYM4Cqxvi9L+WqDMT+51Ozy7lt2M+d0JLEuBqA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.46.1",
"@typescript-eslint/visitor-keys": "8.46.1"
"@typescript-eslint/types": "8.46.2",
"@typescript-eslint/visitor-keys": "8.46.2"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -2529,9 +2526,9 @@
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.46.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.1.tgz",
"integrity": "sha512-X88+J/CwFvlJB+mK09VFqx5FE4H5cXD+H/Bdza2aEWkSb8hnWIQorNcscRl4IEo1Cz9VI/+/r/jnGWkbWPx54g==",
"version": "8.46.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.2.tgz",
"integrity": "sha512-a7QH6fw4S57+F5y2FIxxSDyi5M4UfGF+Jl1bCGd7+L4KsaUY80GsiF/t0UoRFDHAguKlBaACWJRmdrc6Xfkkag==",
"dev": true,
"license": "MIT",
"engines": {
@ -2546,15 +2543,15 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.46.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.1.tgz",
"integrity": "sha512-+BlmiHIiqufBxkVnOtFwjah/vrkF4MtKKvpXrKSPLCkCtAp8H01/VV43sfqA98Od7nJpDcFnkwgyfQbOG0AMvw==",
"version": "8.46.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.2.tgz",
"integrity": "sha512-HbPM4LbaAAt/DjxXaG9yiS9brOOz6fabal4uvUmaUYe6l3K1phQDMQKBRUrr06BQkxkvIZVVHttqiybM9nJsLA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.46.1",
"@typescript-eslint/typescript-estree": "8.46.1",
"@typescript-eslint/utils": "8.46.1",
"@typescript-eslint/types": "8.46.2",
"@typescript-eslint/typescript-estree": "8.46.2",
"@typescript-eslint/utils": "8.46.2",
"debug": "^4.3.4",
"ts-api-utils": "^2.1.0"
},
@ -2571,9 +2568,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.46.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.1.tgz",
"integrity": "sha512-C+soprGBHwWBdkDpbaRC4paGBrkIXxVlNohadL5o0kfhsXqOC6GYH2S/Obmig+I0HTDl8wMaRySwrfrXVP8/pQ==",
"version": "8.46.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.2.tgz",
"integrity": "sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ==",
"dev": true,
"license": "MIT",
"engines": {
@ -2585,16 +2582,16 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.46.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.1.tgz",
"integrity": "sha512-uIifjT4s8cQKFQ8ZBXXyoUODtRoAd7F7+G8MKmtzj17+1UbdzFl52AzRyZRyKqPHhgzvXunnSckVu36flGy8cg==",
"version": "8.46.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.2.tgz",
"integrity": "sha512-f7rW7LJ2b7Uh2EiQ+7sza6RDZnajbNbemn54Ob6fRwQbgcIn+GWfyuHDHRYgRoZu1P4AayVScrRW+YfbTvPQoQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.46.1",
"@typescript-eslint/tsconfig-utils": "8.46.1",
"@typescript-eslint/types": "8.46.1",
"@typescript-eslint/visitor-keys": "8.46.1",
"@typescript-eslint/project-service": "8.46.2",
"@typescript-eslint/tsconfig-utils": "8.46.2",
"@typescript-eslint/types": "8.46.2",
"@typescript-eslint/visitor-keys": "8.46.2",
"debug": "^4.3.4",
"fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
@ -2640,16 +2637,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.46.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.1.tgz",
"integrity": "sha512-vkYUy6LdZS7q1v/Gxb2Zs7zziuXN0wxqsetJdeZdRe/f5dwJFglmuvZBfTUivCtjH725C1jWCDfpadadD95EDQ==",
"version": "8.46.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.2.tgz",
"integrity": "sha512-sExxzucx0Tud5tE0XqR0lT0psBQvEpnpiul9XbGUB1QwpWJJAps1O/Z7hJxLGiZLBKMCutjTzDgmd1muEhBnVg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.7.0",
"@typescript-eslint/scope-manager": "8.46.1",
"@typescript-eslint/types": "8.46.1",
"@typescript-eslint/typescript-estree": "8.46.1"
"@typescript-eslint/scope-manager": "8.46.2",
"@typescript-eslint/types": "8.46.2",
"@typescript-eslint/typescript-estree": "8.46.2"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -2664,13 +2661,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.46.1",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.1.tgz",
"integrity": "sha512-ptkmIf2iDkNUjdeu2bQqhFPV1m6qTnFFjg7PPDjxKWaMaP0Z6I9l30Jr3g5QqbZGdw8YdYvLp+XnqnWWZOg/NA==",
"version": "8.46.2",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.2.tgz",
"integrity": "sha512-tUFMXI4gxzzMXt4xpGJEsBsTox0XbNQ1y94EwlD/CuZwFcQP79xfQqMhau9HsRc/J0cAPA/HZt1dZPtGn9V/7w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.46.1",
"@typescript-eslint/types": "8.46.2",
"eslint-visitor-keys": "^4.2.1"
},
"engines": {
@ -2775,15 +2772,13 @@
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz",
"integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/acorn": {
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@ -3119,7 +3114,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.9",
"caniuse-lite": "^1.0.30001746",
@ -3349,8 +3343,7 @@
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/cva": {
"version": "1.0.0-beta.4",
@ -3946,7 +3939,6 @@
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.38.0.tgz",
"integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@ -4007,7 +3999,6 @@
"integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"eslint-config-prettier": "bin/cli.js"
},
@ -4081,7 +4072,6 @@
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz",
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.9",
@ -5504,7 +5494,6 @@
"integrity": "sha512-FIyV/64EkKhJmjgC0g2hygpBv5RNWVPyNCqSAD7eTCv6eFWNIi4PN1UvdSJGicN/o35bnevgis4Y0UDC0qi8jQ==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=14.0.0"
}
@ -6227,7 +6216,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
@ -6273,7 +6261,6 @@
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"prettier": "bin/prettier.cjs"
},
@ -6430,7 +6417,6 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
"integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@ -6453,7 +6439,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
"integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@ -6515,7 +6500,6 @@
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/use-sync-external-store": "^0.0.6",
"use-sync-external-store": "^1.4.0"
@ -6612,8 +6596,7 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/redux-thunk": {
"version": "3.1.0",
@ -6673,12 +6656,12 @@
"license": "MIT"
},
"node_modules/resolve": {
"version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
"integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==",
"version": "1.22.11",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
"integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
"license": "MIT",
"dependencies": {
"is-core-module": "^2.16.0",
"is-core-module": "^2.16.1",
"path-parse": "^1.0.7",
"supports-preserve-symlinks-flag": "^1.0.0"
},
@ -7203,8 +7186,7 @@
"version": "4.1.15",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.15.tgz",
"integrity": "sha512-k2WLnWkYFkdpRv+Oby3EBXIyQC8/s1HOFMBUViwtAh6Z5uAozeUSMQlIsn/c6Q2iJzqG6aJT3wdPaRNj70iYxQ==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/tapable": {
"version": "2.3.0",
@ -7264,7 +7246,6 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@ -7441,7 +7422,6 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@ -7625,7 +7605,6 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.11.tgz",
"integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==",
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
@ -7737,7 +7716,6 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@ -7886,7 +7864,6 @@
"integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==",
"dev": true,
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}

View File

@ -21,7 +21,7 @@
"i18n:validate": "inlang validate --project ./localization/jetKVM.UI.inlang",
"i18n:compile": "paraglide-js compile --project ./localization/jetKVM.UI.inlang --outdir ./localization/paraglide",
"i18n:machine-translate": "inlang machine translate --project ./localization/jetKVM.UI.inlang",
"i18n:audit": "npm run i18n:find-dupes && npm i18n:find-excess && npm run i18n:find-unused",
"i18n:audit": "npm run i18n:find-dupes && npm run i18n:find-excess && npm run i18n:find-unused",
"i18n:find-excess": "python3 ./tools/find_excess_messages.py",
"i18n:find-unused": "python3 ./tools/find_unused_messages.py",
"i18n:find-dupes": "python3 ./tools/find_duplicate_translations.py"
@ -77,8 +77,8 @@
"@types/react-dom": "^19.2.2",
"@types/semver": "^7.7.1",
"@types/validator": "^13.15.3",
"@typescript-eslint/eslint-plugin": "^8.46.1",
"@typescript-eslint/parser": "^8.46.1",
"@typescript-eslint/eslint-plugin": "^8.46.2",
"@typescript-eslint/parser": "^8.46.2",
"@vitejs/plugin-react-swc": "^4.1.0",
"autoprefixer": "^10.4.21",
"eslint": "^9.38.0",

View File

@ -0,0 +1,50 @@
import { CheckCircleIcon } from "@heroicons/react/24/solid"; // adjust import if you use a different icon set
import LoadingSpinner from "@components/LoadingSpinner"; // adjust import path if needed
import { m } from "@localizations/messages.js";
export interface UpdatePart {
pending: boolean;
status: string;
progress: number;
complete: boolean;
}
export default function UpdatingStatusCard({
label,
part,
}: {
label: string;
part: UpdatePart;
}) {
return (
<div className="space-y-2">
<div className="flex items-center justify-between">
<p className="text-sm font-semibold text-black dark:text-white">{label}</p>
{part.progress < 100 ? (
<LoadingSpinner className="h-4 w-4 text-blue-700 dark:text-blue-500" />
) : (
<CheckCircleIcon className="h-4 w-4 text-blue-700 dark:text-blue-500" />
)}
</div>
<div
className="h-2.5 w-full overflow-hidden rounded-full bg-slate-300 dark:bg-slate-600"
role="progressbar"
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={Math.round(part.progress)}
aria-label={m.general_update_status_progress({part: label})}
>
<div
className="h-2.5 rounded-full bg-blue-700 transition-all duration-500 ease-linear dark:bg-blue-500"
style={{ width: `${part.progress}%` }}
/>
</div>
<div className="flex justify-between text-sm text-slate-600 dark:text-slate-300">
<span>{part.status}</span>
{part.progress < 100 ? <span>{`${Math.round(part.progress)}%`}</span> : null}
</div>
</div>
);
}

View File

@ -476,6 +476,7 @@ export function RebootingOverlay({ show, postRebootAction }: RebootingOverlayPro
// Device is available, redirect to the specified URL
console.log('Device is available, redirecting to:', postRebootAction.redirectUrl);
window.location.href = postRebootAction.redirectUrl;
window.location.reload(true)
}
} catch (err) {
// Ignore errors - they're expected while device is rebooting

View File

@ -78,7 +78,7 @@ export function useHidRpc(onHidRpcMessage?: (payload: RpcMessage) => void) {
try {
data = message.marshal();
} catch (e) {
console.error("Failed to send HID RPC message", e);
console.error("Failed to marshal HID RPC message", e);
}
if (!data) return;
@ -223,13 +223,19 @@ export function useHidRpc(onHidRpcMessage?: (payload: RpcMessage) => void) {
setRpcHidProtocolVersion(null);
};
const errorHandler = (e: Event) => {
console.error(`Error on rpcHidChannel '${rpcHidChannel.label}': ${e}`)
};
rpcHidChannel.addEventListener("message", messageHandler);
rpcHidChannel.addEventListener("close", closeHandler);
rpcHidChannel.addEventListener("error", errorHandler);
rpcHidChannel.addEventListener("open", openHandler);
return () => {
rpcHidChannel.removeEventListener("message", messageHandler);
rpcHidChannel.removeEventListener("close", closeHandler);
rpcHidChannel.removeEventListener("error", errorHandler);
rpcHidChannel.removeEventListener("open", openHandler);
};
}, [

View File

@ -48,12 +48,12 @@ export default function SettingsGeneralRoute() {
const localeOptions = useMemo(() => {
return ["", ...locales]
.map((code) => {
const [localizedName, nativeName] = map_locale_code_to_name(code);
const [localizedName, nativeName] = map_locale_code_to_name(currentLocale, code);
// don't repeat the name if it's the same in both locales (or blank)
const label = nativeName && nativeName !== localizedName ? `${localizedName} - ${nativeName}` : localizedName;
return { value: code, label: label }
});
}, []);
}, [currentLocale]);
const handleLocaleChange = (newLocale: string) => {
if (newLocale === currentLocale) return;

View File

@ -1,6 +1,5 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useLocation, useNavigate } from "react-router";
import { CheckCircleIcon } from "@heroicons/react/20/solid";
import { useJsonRpc } from "@hooks/useJsonRpc";
import { UpdateState, useUpdateStore } from "@hooks/stores";
@ -9,6 +8,7 @@ import { SystemVersionInfo, useVersion } from "@hooks/useVersion";
import { Button } from "@components/Button";
import Card from "@components/Card";
import LoadingSpinner from "@components/LoadingSpinner";
import UpdatingStatusCard, { type UpdatePart} from "@components/UpdatingStatusCard";
import { m } from "@localizations/messages.js";
export default function SettingsGeneralUpdateRoute() {
@ -121,6 +121,7 @@ function LoadingState({
const abortControllerRef = useRef<AbortController | null>(null);
const { getVersionInfo } = useVersion();
const { setModalView } = useUpdateStore();
const progressBarRef = useRef<HTMLDivElement>(null);
useEffect(() => {
@ -128,6 +129,7 @@ function LoadingState({
const signal = abortControllerRef.current.signal;
const animationTimer = setTimeout(() => {
// we start the progress bar animation after a tiny delay to avoid react warnings
setProgressWidth("100%");
}, 0);
@ -144,6 +146,7 @@ function LoadingState({
.catch(error => {
if (!signal.aborted) {
console.error("LoadingState: Error fetching version info", error);
setModalView("error");
}
});
@ -151,7 +154,7 @@ function LoadingState({
clearTimeout(animationTimer);
abortControllerRef.current?.abort();
};
}, [getVersionInfo, onFinished]);
}, [getVersionInfo, onFinished, setModalView]);
return (
<div className="flex flex-col items-start justify-start space-y-4 text-left">
@ -186,77 +189,94 @@ function UpdatingDeviceState({
otaState: UpdateState["otaState"];
onMinimizeUpgradeDialog: () => void;
}) {
const calculateOverallProgress = (type: "system" | "app") => {
const downloadProgress = Math.round((otaState[`${type}DownloadProgress`] || 0) * 100);
const updateProgress = Math.round((otaState[`${type}UpdateProgress`] || 0) * 100);
const verificationProgress = Math.round(
(otaState[`${type}VerificationProgress`] || 0) * 100,
);
interface ProgressSummary {
system: UpdatePart;
app: UpdatePart;
areAllUpdatesComplete: boolean;
};
if (!downloadProgress && !updateProgress && !verificationProgress) {
return 0;
}
console.log(
`For ${type}:\n` +
` Download Progress: ${downloadProgress}% (${otaState[`${type}DownloadProgress`]})\n` +
` Update Progress: ${updateProgress}% (${otaState[`${type}UpdateProgress`]})\n` +
` Verification Progress: ${verificationProgress}% (${otaState[`${type}VerificationProgress`]})`,
);
if (type === "app") {
// App: 65% download, 34% verification, 1% update(There is no "real" update for the app)
return Math.min(
downloadProgress * 0.55 + verificationProgress * 0.54 + updateProgress * 0.01,
100,
const progress = useMemo<ProgressSummary>(() => {
const calculateOverallProgress = (type: "system" | "app") => {
const downloadProgress = Math.round((otaState[`${type}DownloadProgress`] || 0) * 100);
const updateProgress = Math.round((otaState[`${type}UpdateProgress`] || 0) * 100);
const verificationProgress = Math.round(
(otaState[`${type}VerificationProgress`] || 0) * 100,
);
if (!downloadProgress && !updateProgress && !verificationProgress) {
return 0;
}
if (type === "app") {
// App: 55% download, 54% verification, 1% update(There is no "real" update for the app)
return Math.round(Math.min(
downloadProgress * 0.55 + verificationProgress * 0.54 + updateProgress * 0.01,
100,
));
} else {
// System: 10% download, 10% verification, 80% update
return Math.round(Math.min(
downloadProgress * 0.1 + verificationProgress * 0.1 + updateProgress * 0.8,
100,
));
}
};
const getUpdateStatus = (type: "system" | "app") => {
const downloadFinishedAt = otaState[`${type}DownloadFinishedAt`];
const verifiedAt = otaState[`${type}VerifiedAt`];
const updatedAt = otaState[`${type}UpdatedAt`];
const update_type = () => (type === "system" ? m.general_update_system_type() : m.general_update_application_type());
if (!otaState.metadataFetchedAt) {
return m.general_update_status_fetching();
} else if (!downloadFinishedAt) {
return m.general_update_status_downloading({ update_type: update_type() });
} else if (!verifiedAt) {
return m.general_update_status_verifying({ update_type: update_type() });
} else if (!updatedAt) {
return m.general_update_status_installing({ update_type: update_type() });
} else {
return m.general_update_status_awaiting_reboot();
}
};
const isUpdateComplete = (type: "system" | "app") => {
return !!otaState[`${type}UpdatedAt`];
};
const systemUpdatePending = otaState.systemUpdatePending
const systemUpdateComplete = isUpdateComplete("system");
const appUpdatePending = otaState.appUpdatePending
const appUpdateComplete = isUpdateComplete("app");
let areAllUpdatesComplete: boolean;
if (!systemUpdatePending && !appUpdatePending) {
areAllUpdatesComplete = false;
} else if (systemUpdatePending && appUpdatePending) {
areAllUpdatesComplete = systemUpdateComplete && appUpdateComplete;
} else {
// System: 10% download, 90% update
return Math.min(
downloadProgress * 0.1 + verificationProgress * 0.1 + updateProgress * 0.8,
100,
);
areAllUpdatesComplete = systemUpdatePending ? systemUpdateComplete : appUpdateComplete;
}
};
const getUpdateStatus = (type: "system" | "app") => {
const downloadFinishedAt = otaState[`${type}DownloadFinishedAt`];
const verfiedAt = otaState[`${type}VerifiedAt`];
const updatedAt = otaState[`${type}UpdatedAt`];
const update_type = () => (type === "system" ? m.general_update_system_type() : m.general_update_application_type());
if (!otaState.metadataFetchedAt) {
return m.general_update_status_fetching();
} else if (!downloadFinishedAt) {
return m.general_update_status_downloading({ update_type });
} else if (!verfiedAt) {
return m.general_update_status_verifying({ update_type });
} else if (!updatedAt) {
return m.general_update_status_installing({ update_type });
} else {
return m.general_update_status_awaiting_reboot();
}
};
const isUpdateComplete = (type: "system" | "app") => {
return !!otaState[`${type}UpdatedAt`];
};
const areAllUpdatesComplete = () => {
if (otaState.systemUpdatePending && otaState.appUpdatePending) {
return isUpdateComplete("system") && isUpdateComplete("app");
}
return (
(otaState.systemUpdatePending && isUpdateComplete("system")) ||
(otaState.appUpdatePending && isUpdateComplete("app"))
);
};
const systemOverallProgress = calculateOverallProgress("system");
const systemUpdateStatus = getUpdateStatus("system");
const appOverallProgress = calculateOverallProgress("app");
const appUpdateStatus = getUpdateStatus("app");
return {
system: {
pending: systemUpdatePending,
status: getUpdateStatus("system"),
progress: calculateOverallProgress("system"),
complete: systemUpdateComplete,
},
app: {
pending: appUpdatePending,
status: getUpdateStatus("app"),
progress: calculateOverallProgress("app"),
complete: appUpdateComplete,
},
areAllUpdatesComplete,
};
}, [otaState]);
return (
<div className="flex flex-col items-start justify-start space-y-4 text-left">
@ -270,7 +290,7 @@ function UpdatingDeviceState({
</p>
</div>
<Card className="space-y-4 p-4">
{areAllUpdatesComplete() ? (
{progress.areAllUpdatesComplete ? (
<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" />
<div className="flex justify-between text-sm text-slate-600 dark:text-slate-300">
@ -281,68 +301,22 @@ function UpdatingDeviceState({
</div>
) : (
<>
{!(otaState.systemUpdatePending || otaState.appUpdatePending) && (
{!(progress.system.pending || progress.app.pending) && (
<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" />
</div>
)}
{otaState.systemUpdatePending && (
<div className="space-y-2">
<div className="flex items-center justify-between">
<p className="text-sm font-semibold text-black dark:text-white">
{m.general_update_system_update_title()}
</p>
{systemOverallProgress < 100 ? (
<LoadingSpinner className="h-4 w-4 text-blue-700 dark:text-blue-500" />
) : (
<CheckCircleIcon className="h-4 w-4 text-blue-700 dark:text-blue-500" />
)}
</div>
<div className="h-2.5 w-full overflow-hidden rounded-full bg-slate-300 dark:bg-slate-600">
<div
className="h-2.5 rounded-full bg-blue-700 transition-all duration-500 ease-linear dark:bg-blue-500"
style={{ width: `${systemOverallProgress}%` }}
></div>
</div>
<div className="flex justify-between text-sm text-slate-600 dark:text-slate-300">
<span>{systemUpdateStatus}</span>{" "}
{systemOverallProgress < 100
? (<span>{`${systemOverallProgress}%`}</span>)
: null}
</div>
</div>
{progress.system.pending && (
<UpdatingStatusCard label={m.general_update_system_update_title()} part={progress.system} />
)}
{otaState.appUpdatePending && (
<>
{otaState.systemUpdatePending && (
<hr className="dark:border-slate-600" />
)}
<div className="space-y-2">
<div className="flex items-center justify-between">
<p className="text-sm font-semibold text-black dark:text-white">
{m.general_update_app_update_title()}
</p>
{appOverallProgress < 100 ? (
<LoadingSpinner className="h-4 w-4 text-blue-700 dark:text-blue-500" />
) : (
<CheckCircleIcon className="h-4 w-4 text-blue-700 dark:text-blue-500" />
)}
</div>
<div className="h-2.5 w-full overflow-hidden rounded-full bg-slate-300 dark:bg-slate-600">
<div
className="h-2.5 rounded-full bg-blue-700 transition-all duration-500 ease-linear dark:bg-blue-500"
style={{ width: `${appOverallProgress}%` }}
></div>
</div>
<div className="flex justify-between text-sm text-slate-600 dark:text-slate-300">
<span>{appUpdateStatus}</span>{" "}
{appOverallProgress < 100
? (<span>{`${appOverallProgress}%`}</span>)
: null}
</div>
</div>
</>
{progress.system.pending && progress.app.pending && (
<hr className="dark:border-slate-600" />
)}
{progress.app.pending && (
<UpdatingStatusCard label={m.general_update_app_update_title()} part={progress.app} />
)}
</>
)}

View File

@ -485,13 +485,15 @@ export default function KvmIdRoute() {
const rpcDataChannel = pc.createDataChannel("rpc");
rpcDataChannel.onclose = () => console.log("rpcDataChannel has closed");
rpcDataChannel.onerror = (e: Event) => console.error(`Error on DataChannel '${rpcDataChannel.label}': ${e}`);
rpcDataChannel.onerror = (ev: Event) => console.error(`Error on DataChannel '${rpcDataChannel.label}': ${ev}`);
rpcDataChannel.onopen = () => {
setRpcDataChannel(rpcDataChannel);
};
const rpcHidChannel = pc.createDataChannel("hidrpc");
rpcHidChannel.binaryType = "arraybuffer";
rpcHidChannel.onclose = () => console.log("rpcHidChannel has closed");
rpcHidChannel.onerror = (ev: Event) => console.error(`Error on rpcHidChannel '${rpcHidChannel.label}': ${ev}`);
rpcHidChannel.onopen = () => {
setRpcHidChannel(rpcHidChannel);
};
@ -501,6 +503,8 @@ export default function KvmIdRoute() {
maxRetransmits: 0,
});
rpcHidUnreliableChannel.binaryType = "arraybuffer";
rpcHidUnreliableChannel.onclose = () => console.log("rpcHidUnreliableChannel has closed");
rpcHidUnreliableChannel.onerror = (ev: Event) => console.error(`Error on rpcHidUnreliableChannel '${rpcHidUnreliableChannel.label}': ${ev}`);
rpcHidUnreliableChannel.onopen = () => {
setRpcHidUnreliableChannel(rpcHidUnreliableChannel);
};
@ -510,6 +514,8 @@ export default function KvmIdRoute() {
maxRetransmits: 0,
});
rpcHidUnreliableNonOrderedChannel.binaryType = "arraybuffer";
rpcHidUnreliableNonOrderedChannel.onclose = () => console.log("rpcHidUnreliableNonOrderedChannel has closed");
rpcHidUnreliableNonOrderedChannel.onerror = (ev: Event) => console.error(`Error on rpcHidUnreliableNonOrderedChannel '${rpcHidUnreliableNonOrderedChannel.label}': ${ev}`);
rpcHidUnreliableNonOrderedChannel.onopen = () => {
setRpcHidUnreliableNonOrderedChannel(rpcHidUnreliableNonOrderedChannel);
};

View File

@ -1,6 +1,7 @@
import { KeySequence } from "@hooks/stores";
import { getLocale } from '@localizations/runtime.js';
import { m } from "@localizations/messages.js";
import { locales } from '@localizations/runtime.js';
export const formatters = {
date: (date: Date, options?: Intl.DateTimeFormatOptions) =>
@ -256,20 +257,22 @@ export function normalizeSortOrders(macros: KeySequence[]): KeySequence[] {
}));
};
export function map_locale_code_to_name(locale: string): [string, string] {
type LocaleCode = typeof locales[number];
export function map_locale_code_to_name(currentLocale: LocaleCode, locale: string): [string, string] {
// the first is the name in the current app locale (e.g. Inglese),
// the second is the name in the language of the locale itself (e.g. English)
switch (locale) {
case '': return [m.locale_auto(), ""];
case 'en': return [m.locale_en(), m.locale_en({}, { locale })];
case 'da': return [m.locale_da(), m.locale_da({}, { locale })];
case 'de': return [m.locale_de(), m.locale_de({}, { locale })];
case 'es': return [m.locale_es(), m.locale_es({}, { locale })];
case 'fr': return [m.locale_fr(), m.locale_fr({}, { locale })];
case 'it': return [m.locale_it(), m.locale_it({}, { locale })];
case 'nb': return [m.locale_nb(), m.locale_nb({}, { locale })];
case 'sv': return [m.locale_sv(), m.locale_sv({}, { locale })];
case 'zh': return [m.locale_zh(), m.locale_zh({}, { locale })];
case 'en': return [m.locale_en({}, { locale: currentLocale }), m.locale_en({}, { locale })];
case 'da': return [m.locale_da({}, { locale: currentLocale }), m.locale_da({}, { locale })];
case 'de': return [m.locale_de({}, { locale: currentLocale }), m.locale_de({}, { locale })];
case 'es': return [m.locale_es({}, { locale: currentLocale }), m.locale_es({}, { locale })];
case 'fr': return [m.locale_fr({}, { locale: currentLocale }), m.locale_fr({}, { locale })];
case 'it': return [m.locale_it({}, { locale: currentLocale }), m.locale_it({}, { locale })];
case 'nb': return [m.locale_nb({}, { locale: currentLocale }), m.locale_nb({}, { locale })];
case 'sv': return [m.locale_sv({}, { locale: currentLocale }), m.locale_sv({}, { locale })];
case 'zh': return [m.locale_zh({}, { locale: currentLocale }), m.locale_zh({}, { locale })];
default: return [locale, ""];
}
}