Compare commits

..

3 Commits

Author SHA1 Message Date
Marc Brooks 5f617079ab
Merge ca1c36b84e into 2444817455 2025-10-21 03:24:58 +00:00
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 ( import (
"fmt" "fmt"
"os" "os"
"os/exec"
"regexp" "regexp"
"strings" "strings"
"sync" "sync"
@ -36,6 +37,37 @@ func readOtpEntropy() ([]byte, error) { //nolint:unused
return content[0x17:0x1C], nil 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 deviceID string
var deviceIDOnce sync.Once var deviceIDOnce sync.Once

View File

@ -173,34 +173,8 @@ func rpcGetDeviceID() (string, error) {
} }
func rpcReboot(force bool) error { func rpcReboot(force bool) error {
logger.Info().Msg("Got reboot request from JSONRPC, rebooting...") logger.Info().Msg("Got reboot request via RPC")
return hwReboot(force, nil, 0)
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
} }
var streamFactor = 1.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") nativeLogger.Trace().Str("event", event).Msg("rpc event received")
switch event { switch event {
case "resetConfig": case "resetConfig":
nativeLogger.Info().Msg("Reset configuration request via native rpc event")
err := rpcResetConfig() err := rpcResetConfig()
if err != nil { if err != nil {
nativeLogger.Warn().Err(err).Msg("error resetting config") nativeLogger.Warn().Err(err).Msg("error resetting config")
} }
_ = rpcReboot(true) _ = rpcReboot(true)
case "reboot": case "reboot":
nativeLogger.Info().Msg("Reboot request via native rpc event")
_ = rpcReboot(true) _ = rpcReboot(true)
case "toggleDHCPClient": case "toggleDHCPClient":
nativeLogger.Info().Msg("Toggle DHCP request via native rpc event")
_ = rpcToggleDHCPClient() _ = rpcToggleDHCPClient()
default: default:
nativeLogger.Warn().Str("event", event).Msg("unknown rpc event received") 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 oldIPv4Mode := oldConfig.IPv4Mode.String
newIPv4Mode := newConfig.IPv4Mode.String newIPv4Mode := newConfig.IPv4Mode.String
// IPv4 mode change requires reboot // IPv4 mode change requires reboot
if newIPv4Mode != oldIPv4Mode { if newIPv4Mode != oldIPv4Mode {
rebootRequired = true rebootRequired = true
@ -284,7 +285,8 @@ func rpcSetNetworkSettings(settings RpcNetworkSettings) (*RpcNetworkSettings, er
} }
if rebootRequired { 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 return nil, err
} }
} }

24
ota.go
View File

@ -487,25 +487,15 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
} }
if rebootNeeded { 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 postRebootAction := &PostRebootAction{
// Example: HealthCheck: "/device/status",
// postRebootAction := &PostRebootAction{ RedirectUrl: "/settings/general/update?version=" + remote.SystemVersion,
// HealthCheck: "[..]/device/status", }
// RedirectUrl: "[..]/settings/general/update?version=X.Y.Z",
// }
// writeJSONRPCEvent("willReboot", postRebootAction, currentSession)
time.Sleep(10 * time.Second) if err := hwReboot(true, postRebootAction, 10*time.Second); err != nil {
cmd := exec.Command("reboot") return fmt.Errorf("error requesting reboot: %w", err)
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)
} }
} }

View File

@ -271,6 +271,7 @@
"general_update_status_downloading": "Downloader {update_type} opdatering…", "general_update_status_downloading": "Downloader {update_type} opdatering…",
"general_update_status_fetching": "Henter opdateringsoplysninger…", "general_update_status_fetching": "Henter opdateringsoplysninger…",
"general_update_status_installing": "Installation af {update_type} opdatering…", "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_status_verifying": "Bekræfter {update_type} opdatering…",
"general_update_system_type": "System", "general_update_system_type": "System",
"general_update_system_update_title": "Linux-systemopdatering", "general_update_system_update_title": "Linux-systemopdatering",

View File

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

View File

@ -271,6 +271,7 @@
"general_update_status_downloading": "Downloading {update_type} update…", "general_update_status_downloading": "Downloading {update_type} update…",
"general_update_status_fetching": "Fetching update information…", "general_update_status_fetching": "Fetching update information…",
"general_update_status_installing": "Installing {update_type} update…", "general_update_status_installing": "Installing {update_type} update…",
"general_update_status_progress": "{part} progress",
"general_update_status_verifying": "Verifying {update_type} update…", "general_update_status_verifying": "Verifying {update_type} update…",
"general_update_system_type": "System", "general_update_system_type": "System",
"general_update_system_update_title": "Linux System Update", "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_downloading": "Descargando actualización {update_type} …",
"general_update_status_fetching": "Obteniendo información de actualización…", "general_update_status_fetching": "Obteniendo información de actualización…",
"general_update_status_installing": "Instalando {update_type} 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_status_verifying": "Verificando la actualización {update_type} …",
"general_update_system_type": "Sistema", "general_update_system_type": "Sistema",
"general_update_system_update_title": "Actualización del sistema Linux", "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_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_fetching": "Récupération des informations de mise à jour…",
"general_update_status_installing": "Installation de la mise à jour {update_type} …", "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_status_verifying": "Vérification de la mise à jour de {update_type} …",
"general_update_system_type": "Système", "general_update_system_type": "Système",
"general_update_system_update_title": "Mise à jour du système Linux", "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_downloading": "Scaricamento dell'aggiornamento {update_type} …",
"general_update_status_fetching": "Recupero delle informazioni di aggiornamento in corso…", "general_update_status_fetching": "Recupero delle informazioni di aggiornamento in corso…",
"general_update_status_installing": "Installazione dell'aggiornamento {update_type} …", "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_status_verifying": "Verifica dell'aggiornamento {update_type} …",
"general_update_system_type": "Sistema", "general_update_system_type": "Sistema",
"general_update_system_update_title": "Aggiornamento del sistema Linux", "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_downloading": "Laster ned {update_type} oppdatering…",
"general_update_status_fetching": "Henter oppdateringsinformasjon …", "general_update_status_fetching": "Henter oppdateringsinformasjon …",
"general_update_status_installing": "Installerer {update_type} oppdatering…", "general_update_status_installing": "Installerer {update_type} oppdatering…",
"general_update_status_progress": "{part} fremgang",
"general_update_status_verifying": "Verifiserer {update_type} oppdatering…", "general_update_status_verifying": "Verifiserer {update_type} oppdatering…",
"general_update_system_type": "System", "general_update_system_type": "System",
"general_update_system_update_title": "Linux-systemoppdatering", "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_downloading": "Laddar ner {update_type} uppdatering…",
"general_update_status_fetching": "Hämtar uppdateringsinformation…", "general_update_status_fetching": "Hämtar uppdateringsinformation…",
"general_update_status_installing": "Installerar {update_type} uppdatering…", "general_update_status_installing": "Installerar {update_type} uppdatering…",
"general_update_status_progress": "{part} framsteg",
"general_update_status_verifying": "Verifierar {update_type} uppdatering…", "general_update_status_verifying": "Verifierar {update_type} uppdatering…",
"general_update_system_type": "System", "general_update_system_type": "System",
"general_update_system_update_title": "Linux-systemuppdatering", "general_update_system_update_title": "Linux-systemuppdatering",

View File

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

View File

@ -21,7 +21,7 @@
"i18n:validate": "inlang validate --project ./localization/jetKVM.UI.inlang", "i18n:validate": "inlang validate --project ./localization/jetKVM.UI.inlang",
"i18n:compile": "paraglide-js compile --project ./localization/jetKVM.UI.inlang --outdir ./localization/paraglide", "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: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-excess": "python3 ./tools/find_excess_messages.py",
"i18n:find-unused": "python3 ./tools/find_unused_messages.py", "i18n:find-unused": "python3 ./tools/find_unused_messages.py",
"i18n:find-dupes": "python3 ./tools/find_duplicate_translations.py" "i18n:find-dupes": "python3 ./tools/find_duplicate_translations.py"
@ -77,8 +77,8 @@
"@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.3",
"@typescript-eslint/eslint-plugin": "^8.46.1", "@typescript-eslint/eslint-plugin": "^8.46.2",
"@typescript-eslint/parser": "^8.46.1", "@typescript-eslint/parser": "^8.46.2",
"@vitejs/plugin-react-swc": "^4.1.0", "@vitejs/plugin-react-swc": "^4.1.0",
"autoprefixer": "^10.4.21", "autoprefixer": "^10.4.21",
"eslint": "^9.38.0", "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 // Device is available, redirect to the specified URL
console.log('Device is available, redirecting to:', postRebootAction.redirectUrl); console.log('Device is available, redirecting to:', postRebootAction.redirectUrl);
window.location.href = postRebootAction.redirectUrl; window.location.href = postRebootAction.redirectUrl;
window.location.reload(true)
} }
} catch (err) { } catch (err) {
// Ignore errors - they're expected while device is rebooting // Ignore errors - they're expected while device is rebooting

View File

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

View File

@ -48,12 +48,12 @@ export default function SettingsGeneralRoute() {
const localeOptions = useMemo(() => { const localeOptions = useMemo(() => {
return ["", ...locales] return ["", ...locales]
.map((code) => { .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) // don't repeat the name if it's the same in both locales (or blank)
const label = nativeName && nativeName !== localizedName ? `${localizedName} - ${nativeName}` : localizedName; const label = nativeName && nativeName !== localizedName ? `${localizedName} - ${nativeName}` : localizedName;
return { value: code, label: label } return { value: code, label: label }
}); });
}, []); }, [currentLocale]);
const handleLocaleChange = (newLocale: string) => { const handleLocaleChange = (newLocale: string) => {
if (newLocale === currentLocale) return; 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 { useLocation, useNavigate } from "react-router";
import { CheckCircleIcon } from "@heroicons/react/20/solid";
import { useJsonRpc } from "@hooks/useJsonRpc"; import { useJsonRpc } from "@hooks/useJsonRpc";
import { UpdateState, useUpdateStore } from "@hooks/stores"; import { UpdateState, useUpdateStore } from "@hooks/stores";
@ -9,6 +8,7 @@ import { SystemVersionInfo, useVersion } from "@hooks/useVersion";
import { Button } from "@components/Button"; import { Button } from "@components/Button";
import Card from "@components/Card"; import Card from "@components/Card";
import LoadingSpinner from "@components/LoadingSpinner"; import LoadingSpinner from "@components/LoadingSpinner";
import UpdatingStatusCard, { type UpdatePart} from "@components/UpdatingStatusCard";
import { m } from "@localizations/messages.js"; import { m } from "@localizations/messages.js";
export default function SettingsGeneralUpdateRoute() { export default function SettingsGeneralUpdateRoute() {
@ -121,6 +121,7 @@ function LoadingState({
const abortControllerRef = useRef<AbortController | null>(null); const abortControllerRef = useRef<AbortController | null>(null);
const { getVersionInfo } = useVersion(); const { getVersionInfo } = useVersion();
const { setModalView } = useUpdateStore();
const progressBarRef = useRef<HTMLDivElement>(null); const progressBarRef = useRef<HTMLDivElement>(null);
useEffect(() => { useEffect(() => {
@ -128,6 +129,7 @@ function LoadingState({
const signal = abortControllerRef.current.signal; const signal = abortControllerRef.current.signal;
const animationTimer = setTimeout(() => { const animationTimer = setTimeout(() => {
// we start the progress bar animation after a tiny delay to avoid react warnings
setProgressWidth("100%"); setProgressWidth("100%");
}, 0); }, 0);
@ -144,6 +146,7 @@ function LoadingState({
.catch(error => { .catch(error => {
if (!signal.aborted) { if (!signal.aborted) {
console.error("LoadingState: Error fetching version info", error); console.error("LoadingState: Error fetching version info", error);
setModalView("error");
} }
}); });
@ -151,7 +154,7 @@ function LoadingState({
clearTimeout(animationTimer); clearTimeout(animationTimer);
abortControllerRef.current?.abort(); abortControllerRef.current?.abort();
}; };
}, [getVersionInfo, onFinished]); }, [getVersionInfo, onFinished, setModalView]);
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">
@ -186,77 +189,94 @@ function UpdatingDeviceState({
otaState: UpdateState["otaState"]; otaState: UpdateState["otaState"];
onMinimizeUpgradeDialog: () => void; onMinimizeUpgradeDialog: () => void;
}) { }) {
const calculateOverallProgress = (type: "system" | "app") => { interface ProgressSummary {
const downloadProgress = Math.round((otaState[`${type}DownloadProgress`] || 0) * 100); system: UpdatePart;
const updateProgress = Math.round((otaState[`${type}UpdateProgress`] || 0) * 100); app: UpdatePart;
const verificationProgress = Math.round( areAllUpdatesComplete: boolean;
(otaState[`${type}VerificationProgress`] || 0) * 100, };
);
if (!downloadProgress && !updateProgress && !verificationProgress) { const progress = useMemo<ProgressSummary>(() => {
return 0; const calculateOverallProgress = (type: "system" | "app") => {
} const downloadProgress = Math.round((otaState[`${type}DownloadProgress`] || 0) * 100);
const updateProgress = Math.round((otaState[`${type}UpdateProgress`] || 0) * 100);
console.log( const verificationProgress = Math.round(
`For ${type}:\n` + (otaState[`${type}VerificationProgress`] || 0) * 100,
` 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,
); );
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 { } else {
// System: 10% download, 90% update areAllUpdatesComplete = systemUpdatePending ? systemUpdateComplete : appUpdateComplete;
return Math.min(
downloadProgress * 0.1 + verificationProgress * 0.1 + updateProgress * 0.8,
100,
);
} }
};
const getUpdateStatus = (type: "system" | "app") => { return {
const downloadFinishedAt = otaState[`${type}DownloadFinishedAt`]; system: {
const verfiedAt = otaState[`${type}VerifiedAt`]; pending: systemUpdatePending,
const updatedAt = otaState[`${type}UpdatedAt`]; status: getUpdateStatus("system"),
progress: calculateOverallProgress("system"),
const update_type = () => (type === "system" ? m.general_update_system_type() : m.general_update_application_type()); complete: systemUpdateComplete,
},
if (!otaState.metadataFetchedAt) { app: {
return m.general_update_status_fetching(); pending: appUpdatePending,
} else if (!downloadFinishedAt) { status: getUpdateStatus("app"),
return m.general_update_status_downloading({ update_type }); progress: calculateOverallProgress("app"),
} else if (!verfiedAt) { complete: appUpdateComplete,
return m.general_update_status_verifying({ update_type }); },
} else if (!updatedAt) { areAllUpdatesComplete,
return m.general_update_status_installing({ update_type }); };
} else { }, [otaState]);
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 ( 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">
@ -270,7 +290,7 @@ function UpdatingDeviceState({
</p> </p>
</div> </div>
<Card className="space-y-4 p-4"> <Card className="space-y-4 p-4">
{areAllUpdatesComplete() ? ( {progress.areAllUpdatesComplete ? (
<div className="my-2 flex flex-col items-center space-y-2 text-center"> <div className="my-2 flex flex-col items-center space-y-2 text-center">
<LoadingSpinner className="h-6 w-6 text-blue-700 dark:text-blue-500" /> <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"> <div className="flex justify-between text-sm text-slate-600 dark:text-slate-300">
@ -281,68 +301,22 @@ function UpdatingDeviceState({
</div> </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"> <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" /> <LoadingSpinner className="h-6 w-6 text-blue-700 dark:text-blue-500" />
</div> </div>
)} )}
{otaState.systemUpdatePending && ( {progress.system.pending && (
<div className="space-y-2"> <UpdatingStatusCard label={m.general_update_system_update_title()} part={progress.system} />
<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>
)} )}
{otaState.appUpdatePending && (
<> {progress.system.pending && progress.app.pending && (
{otaState.systemUpdatePending && ( <hr className="dark:border-slate-600" />
<hr className="dark:border-slate-600" /> )}
)}
<div className="space-y-2"> {progress.app.pending && (
<div className="flex items-center justify-between"> <UpdatingStatusCard label={m.general_update_app_update_title()} part={progress.app} />
<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>
</>
)} )}
</> </>
)} )}

View File

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

View File

@ -1,6 +1,7 @@
import { KeySequence } from "@hooks/stores"; import { KeySequence } from "@hooks/stores";
import { getLocale } from '@localizations/runtime.js'; import { getLocale } from '@localizations/runtime.js';
import { m } from "@localizations/messages.js"; import { m } from "@localizations/messages.js";
import { locales } from '@localizations/runtime.js';
export const formatters = { export const formatters = {
date: (date: Date, options?: Intl.DateTimeFormatOptions) => 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 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) // the second is the name in the language of the locale itself (e.g. English)
switch (locale) { switch (locale) {
case '': return [m.locale_auto(), ""]; case '': return [m.locale_auto(), ""];
case 'en': return [m.locale_en(), m.locale_en({}, { locale })]; case 'en': return [m.locale_en({}, { locale: currentLocale }), m.locale_en({}, { locale })];
case 'da': return [m.locale_da(), m.locale_da({}, { locale })]; case 'da': return [m.locale_da({}, { locale: currentLocale }), m.locale_da({}, { locale })];
case 'de': return [m.locale_de(), m.locale_de({}, { locale })]; case 'de': return [m.locale_de({}, { locale: currentLocale }), m.locale_de({}, { locale })];
case 'es': return [m.locale_es(), m.locale_es({}, { locale })]; case 'es': return [m.locale_es({}, { locale: currentLocale }), m.locale_es({}, { locale })];
case 'fr': return [m.locale_fr(), m.locale_fr({}, { locale })]; case 'fr': return [m.locale_fr({}, { locale: currentLocale }), m.locale_fr({}, { locale })];
case 'it': return [m.locale_it(), m.locale_it({}, { locale })]; case 'it': return [m.locale_it({}, { locale: currentLocale }), m.locale_it({}, { locale })];
case 'nb': return [m.locale_nb(), m.locale_nb({}, { locale })]; case 'nb': return [m.locale_nb({}, { locale: currentLocale }), m.locale_nb({}, { locale })];
case 'sv': return [m.locale_sv(), m.locale_sv({}, { locale })]; case 'sv': return [m.locale_sv({}, { locale: currentLocale }), m.locale_sv({}, { locale })];
case 'zh': return [m.locale_zh(), m.locale_zh({}, { locale })]; case 'zh': return [m.locale_zh({}, { locale: currentLocale }), m.locale_zh({}, { locale })];
default: return [locale, ""]; default: return [locale, ""];
} }
} }