Compare commits

..

1 Commits

Author SHA1 Message Date
Marc Brooks 853b72a289
Merge 774615557c into cc9ff74276 2025-10-10 19:53:38 -05:00
12 changed files with 316 additions and 453 deletions

View File

@ -457,139 +457,5 @@
"wake_on_lan_magic_sent_success": "Magic Packet sent successfully", "wake_on_lan_magic_sent_success": "Magic Packet sent successfully",
"wake_on_lan": "Wake On LAN", "wake_on_lan": "Wake On LAN",
"welcome_to_jetkvm_description": "Control any computer remotely", "welcome_to_jetkvm_description": "Control any computer remotely",
"welcome_to_jetkvm": "Welcome to JetKVM", "welcome_to_jetkvm": "Welcome to JetKVM"
"access_adopt_kvm": "Adopt KVM to Cloud",
"access_adopted_message": "Your device is adopted to the Cloud",
"access_auth_mode_no_password": "Current mode: No password",
"access_auth_mode_password": "Current mode: Password protected",
"access_authentication_mode_title": "Authentication Mode",
"access_certificate_label": "Certificate",
"access_change_password_button": "Change Password",
"access_change_password_description": "Update your device access password",
"access_change_password_title": "Change Password",
"access_cloud_api_url_label": "Cloud API URL",
"access_cloud_app_url_label": "Cloud Application URL",
"access_cloud_provider_description": "Select the cloud provider for your device",
"access_cloud_provider_title": "Cloud Provider",
"access_cloud_security_title": "Cloud Security",
"access_confirm_deregister": "Are you sure you want to de-register this device?",
"access_deregister": "De-register from Cloud",
"access_description": "Manage the Access Control of the device",
"access_disable_protection": "Disable Protection",
"access_enable_password": "Enable Password",
"access_failed_deregister": "Failed to de-register device: {error}",
"access_failed_update_cloud_url": "Failed to update cloud URL: {error}",
"access_failed_update_tls": "Failed to update TLS settings: {error}",
"access_github_link": "GitHub",
"access_https_description": "Configure secure HTTPS access to your device",
"access_https_mode_title": "HTTPS Mode",
"access_learn_security": "Learn about our cloud security",
"access_local_description": "Manage the mode of local access to the device",
"access_local_title": "Local",
"access_no_device_id": "No device ID available",
"access_private_key_description": "For security reasons, it will not be displayed after saving.",
"access_private_key_label": "Private Key",
"access_provider_custom": "Custom",
"access_provider_jetkvm": "JetKVM Cloud",
"access_remote_description": "Manage the mode of Remote access to the device",
"access_security_encryption": "End-to-end encryption using WebRTC (DTLS and SRTP)",
"access_security_oidc": "OIDC (OpenID Connect) authentication",
"access_security_open_source": "All cloud components are open-source and available on GitHub.",
"access_security_streams": "All streams encrypted in transit",
"access_security_zero_trust": "Zero Trust security model",
"access_title": "Access",
"access_tls_certificate_description": "Paste your TLS certificate below. For certificate chains, include the entire chain (leaf, intermediate, and root certificates).",
"access_tls_certificate_title": "TLS Certificate",
"access_tls_custom": "Custom",
"access_tls_disabled": "Disabled",
"access_tls_self_signed": "Self-signed",
"access_tls_updated": "TLS settings updated successfully",
"access_update_tls_settings": "Update TLS Settings",
"local_auth_change_local_device_password_description": "Enter your current password and a new password to update your local device protection.",
"local_auth_change_local_device_password_title": "Change Local Device Password",
"local_auth_confirm_new_password_label": "Confirm New Password",
"local_auth_create_confirm_password_label": "Confirm New Password",
"local_auth_create_confirm_password_placeholder": "Re-enter your password",
"local_auth_create_description": "Create a password to protect your device from unauthorized local access.",
"local_auth_create_new_password_label": "New Password",
"local_auth_create_new_password_placeholder": "Enter a strong password",
"local_auth_create_not_now_button": "Not Now",
"local_auth_create_secure_button": "Secure Device",
"local_auth_create_title": "Local Device Protection",
"local_auth_current_password_label": "Current Password",
"local_auth_disable_local_device_protection_description": "Enter your current password to disable local device protection.",
"local_auth_disable_local_device_protection_title": "Disable Local Device Protection",
"local_auth_disable_protection_button": "Disable Protection",
"local_auth_enter_current_password_placeholder": "Enter your current password",
"local_auth_enter_new_password_placeholder": "Enter a new strong password",
"local_auth_error_changing_password": "An error occurred while changing the password",
"local_auth_error_disabling_password": "An error occurred while disabling the password",
"local_auth_error_enter_current_password": "Please enter your current password",
"local_auth_error_enter_new_password": "Please enter a new password",
"local_auth_error_enter_old_password": "Please enter your old password",
"local_auth_error_enter_password": "Please enter a password",
"local_auth_error_passwords_not_match": "Passwords do not match",
"local_auth_error_setting_password": "An error occurred while setting the password",
"local_auth_new_password_label": "New Password",
"local_auth_reenter_new_password_placeholder": "Re-enter your new password",
"local_auth_success_password_disabled_description": "You've successfully disabled the password protection for local access. Remember, your device is now less secure.",
"local_auth_success_password_disabled_title": "Password Protection Disabled",
"local_auth_success_password_set_description": "You've successfully set up local device protection. Your device is now secure against unauthorized local access.",
"local_auth_success_password_set_title": "Password Set Successfully",
"local_auth_success_password_updated_description": "You've successfully changed your local device protection password. Make sure to remember your new password for future access.",
"local_auth_success_password_updated_title": "Password Updated Successfully",
"local_auth_update_confirm_password_label": "Confirm New Password",
"local_auth_update_current_password_label": "Current Password",
"local_auth_update_description": "Enter your current password and a new password to update your local device protection.",
"local_auth_update_new_password_label": "New Password",
"local_auth_update_password_button": "Update Password",
"local_auth_update_title": "Change Local Device Password",
"advanced_description": "Access additional settings for troubleshooting and customization",
"advanced_dev_channel_description": "Receive early updates from the development channel",
"advanced_dev_channel_title": "Dev Channel Updates",
"advanced_developer_mode_description": "Enable advanced features for developers",
"advanced_developer_mode_enabled_title": "Developer Mode Enabled",
"advanced_developer_mode_title": "Developer Mode",
"advanced_developer_mode_warning_advanced": "For advanced users only. Not for production use.",
"advanced_developer_mode_warning_risks": "Only use if you understand the risks",
"advanced_developer_mode_warning_security": "Security is weakened while active",
"advanced_disable_usb_emulation": "Disable USB Emulation",
"advanced_enable_usb_emulation": "Enable USB Emulation",
"advanced_error_loopback_disable": "Failed to disable loopback-only mode: {error}",
"advanced_error_loopback_enable": "Failed to enable loopback-only mode: {error}",
"advanced_error_reset_config": "Failed to reset configuration: {error}",
"advanced_error_set_dev_channel": "Failed to set dev channel state: {error}",
"advanced_error_set_dev_mode": "Failed to set dev mode: {error}",
"advanced_error_update_ssh_key": "Failed to update SSH key: {error}",
"advanced_error_usb_emulation_disable": "Failed to disable USB emulation: {error}",
"advanced_error_usb_emulation_enable": "Failed to enable USB emulation: {error}",
"advanced_loopback_only_description": "Restrict web interface access to localhost only (127.0.0.1)",
"advanced_loopback_only_title": "Loopback-Only Mode",
"advanced_loopback_warning_before": "Before enabling this feature, make sure you have either:",
"advanced_loopback_warning_cloud": "Cloud access enabled and working",
"advanced_loopback_warning_confirm": "I Understand, Enable Anyway",
"advanced_loopback_warning_description": "WARNING: This will restrict web interface access to localhost (127.0.0.1) only.",
"advanced_loopback_warning_ssh": "SSH access configured and tested",
"advanced_loopback_warning_title": "Enable Loopback-Only Mode?",
"advanced_reset_config_button": "Reset Config",
"advanced_reset_config_description": "Reset configuration to default. This will log you out.",
"advanced_reset_config_title": "Reset Configuration",
"advanced_ssh_access_description": "Add your SSH public key to enable secure remote access to the device",
"advanced_ssh_access_title": "SSH Access",
"advanced_ssh_default_user": "The default SSH user is",
"advanced_ssh_public_key_label": "SSH Public Key",
"advanced_ssh_public_key_placeholder": "Enter your SSH public key",
"advanced_success_loopback_disabled": "Loopback-only mode disabled. Restart your device to apply.",
"advanced_success_loopback_enabled": "Loopback-only mode enabled. Restart your device to apply.",
"advanced_success_reset_config": "Configuration reset to default successfully",
"advanced_success_update_ssh_key": "SSH key updated successfully",
"advanced_title": "Advanced",
"advanced_troubleshooting_mode_description": "Diagnostic tools and additional controls for troubleshooting and development purposes",
"advanced_troubleshooting_mode_title": "Troubleshooting Mode",
"advanced_update_ssh_key_button": "Update SSH Key",
"advanced_usb_emulation_description": "Control the USB emulation state",
"advanced_usb_emulation_title": "USB Emulation"
} }

132
ui/package-lock.json generated
View File

@ -1,12 +1,12 @@
{ {
"name": "kvm-ui", "name": "kvm-ui",
"version": "2025.10.13.2055", "version": "2025.10.10.2300",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "kvm-ui", "name": "kvm-ui",
"version": "2025.10.13.2055", "version": "2025.10.10.2300",
"dependencies": { "dependencies": {
"@headlessui/react": "^2.2.9", "@headlessui/react": "^2.2.9",
"@headlessui/tailwindcss": "^0.2.2", "@headlessui/tailwindcss": "^0.2.2",
@ -31,7 +31,7 @@
"react-hot-toast": "^2.6.0", "react-hot-toast": "^2.6.0",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-router": "^7.9.4", "react-router": "^7.9.4",
"react-simple-keyboard": "^3.8.130", "react-simple-keyboard": "^3.8.129",
"react-use-websocket": "^4.13.0", "react-use-websocket": "^4.13.0",
"react-xtermjs": "^1.0.10", "react-xtermjs": "^1.0.10",
"recharts": "^3.2.1", "recharts": "^3.2.1",
@ -54,11 +54,11 @@
"@tailwindcss/typography": "^0.5.19", "@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.1.14", "@tailwindcss/vite": "^4.1.14",
"@types/react": "^19.2.2", "@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2", "@types/react-dom": "^19.2.1",
"@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.0",
"@typescript-eslint/parser": "^8.46.1", "@typescript-eslint/parser": "^8.46.0",
"@vitejs/plugin-react-swc": "^4.1.0", "@vitejs/plugin-react-swc": "^4.1.0",
"autoprefixer": "^10.4.21", "autoprefixer": "^10.4.21",
"eslint": "^9.37.0", "eslint": "^9.37.0",
@ -2408,9 +2408,9 @@
} }
}, },
"node_modules/@types/react-dom": { "node_modules/@types/react-dom": {
"version": "19.2.2", "version": "19.2.1",
"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.1.tgz",
"integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==", "integrity": "sha512-/EEvYBdT3BflCWvTMO7YkYBHVE9Ci6XdqZciZANQgKpaiDRGOLIlRo91jbTNRQjgPFWVaRxcYc0luVNFitz57A==",
"license": "MIT", "license": "MIT",
"peerDependencies": { "peerDependencies": {
"@types/react": "^19.2.0" "@types/react": "^19.2.0"
@ -2437,17 +2437,17 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.46.1", "version": "8.46.0",
"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.0.tgz",
"integrity": "sha512-rUsLh8PXmBjdiPY+Emjz9NX2yHvhS11v0SR6xNJkm5GM1MO9ea/1GoDKlHHZGrOJclL/cZ2i/vRUYVtjRhrHVQ==", "integrity": "sha512-hA8gxBq4ukonVXPy0OKhiaUh/68D0E88GSmtC1iAEnGaieuDi38LhS7jdCHRLi6ErJBNDGCzvh5EnzdPwUc0DA==",
"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.0",
"@typescript-eslint/type-utils": "8.46.1", "@typescript-eslint/type-utils": "8.46.0",
"@typescript-eslint/utils": "8.46.1", "@typescript-eslint/utils": "8.46.0",
"@typescript-eslint/visitor-keys": "8.46.1", "@typescript-eslint/visitor-keys": "8.46.0",
"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",
@ -2461,7 +2461,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.0",
"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"
} }
@ -2477,16 +2477,16 @@
} }
}, },
"node_modules/@typescript-eslint/parser": { "node_modules/@typescript-eslint/parser": {
"version": "8.46.1", "version": "8.46.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.0.tgz",
"integrity": "sha512-6JSSaBZmsKvEkbRUkf7Zj7dru/8ZCrJxAqArcLaVMee5907JdtEbKGsZ7zNiIm/UAkpGUkaSMZEXShnN2D1HZA==", "integrity": "sha512-n1H6IcDhmmUEG7TNVSspGmiHHutt7iVKtZwRppD7e04wha5MrkV1h3pti9xQLcCMt6YWsncpoT0HMjkH1FNwWQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "8.46.1", "@typescript-eslint/scope-manager": "8.46.0",
"@typescript-eslint/types": "8.46.1", "@typescript-eslint/types": "8.46.0",
"@typescript-eslint/typescript-estree": "8.46.1", "@typescript-eslint/typescript-estree": "8.46.0",
"@typescript-eslint/visitor-keys": "8.46.1", "@typescript-eslint/visitor-keys": "8.46.0",
"debug": "^4.3.4" "debug": "^4.3.4"
}, },
"engines": { "engines": {
@ -2502,14 +2502,14 @@
} }
}, },
"node_modules/@typescript-eslint/project-service": { "node_modules/@typescript-eslint/project-service": {
"version": "8.46.1", "version": "8.46.0",
"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.0.tgz",
"integrity": "sha512-FOIaFVMHzRskXr5J4Jp8lFVV0gz5ngv3RHmn+E4HYxSJ3DgDzU7fVI1/M7Ijh1zf6S7HIoaIOtln1H5y8V+9Zg==", "integrity": "sha512-OEhec0mH+U5Je2NZOeK1AbVCdm0ChyapAyTeXVIYTPXDJ3F07+cu87PPXcGoYqZ7M9YJVvFnfpGg1UmCIqM+QQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.46.1", "@typescript-eslint/tsconfig-utils": "^8.46.0",
"@typescript-eslint/types": "^8.46.1", "@typescript-eslint/types": "^8.46.0",
"debug": "^4.3.4" "debug": "^4.3.4"
}, },
"engines": { "engines": {
@ -2524,14 +2524,14 @@
} }
}, },
"node_modules/@typescript-eslint/scope-manager": { "node_modules/@typescript-eslint/scope-manager": {
"version": "8.46.1", "version": "8.46.0",
"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.0.tgz",
"integrity": "sha512-weL9Gg3/5F0pVQKiF8eOXFZp8emqWzZsOJuWRUNtHT+UNV2xSJegmpCNQHy37aEQIbToTq7RHKhWvOsmbM680A==", "integrity": "sha512-lWETPa9XGcBes4jqAMYD9fW0j4n6hrPtTJwWDmtqgFO/4HF4jmdH/Q6wggTw5qIT5TXjKzbt7GsZUBnWoO3dqw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "8.46.1", "@typescript-eslint/types": "8.46.0",
"@typescript-eslint/visitor-keys": "8.46.1" "@typescript-eslint/visitor-keys": "8.46.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -2542,9 +2542,9 @@
} }
}, },
"node_modules/@typescript-eslint/tsconfig-utils": { "node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.46.1", "version": "8.46.0",
"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.0.tgz",
"integrity": "sha512-X88+J/CwFvlJB+mK09VFqx5FE4H5cXD+H/Bdza2aEWkSb8hnWIQorNcscRl4IEo1Cz9VI/+/r/jnGWkbWPx54g==", "integrity": "sha512-WrYXKGAHY836/N7zoK/kzi6p8tXFhasHh8ocFL9VZSAkvH956gfeRfcnhs3xzRy8qQ/dq3q44v1jvQieMFg2cw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@ -2559,15 +2559,15 @@
} }
}, },
"node_modules/@typescript-eslint/type-utils": { "node_modules/@typescript-eslint/type-utils": {
"version": "8.46.1", "version": "8.46.0",
"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.0.tgz",
"integrity": "sha512-+BlmiHIiqufBxkVnOtFwjah/vrkF4MtKKvpXrKSPLCkCtAp8H01/VV43sfqA98Od7nJpDcFnkwgyfQbOG0AMvw==", "integrity": "sha512-hy+lvYV1lZpVs2jRaEYvgCblZxUoJiPyCemwbQZ+NGulWkQRy0HRPYAoef/CNSzaLt+MLvMptZsHXHlkEilaeg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "8.46.1", "@typescript-eslint/types": "8.46.0",
"@typescript-eslint/typescript-estree": "8.46.1", "@typescript-eslint/typescript-estree": "8.46.0",
"@typescript-eslint/utils": "8.46.1", "@typescript-eslint/utils": "8.46.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"ts-api-utils": "^2.1.0" "ts-api-utils": "^2.1.0"
}, },
@ -2584,9 +2584,9 @@
} }
}, },
"node_modules/@typescript-eslint/types": { "node_modules/@typescript-eslint/types": {
"version": "8.46.1", "version": "8.46.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.0.tgz",
"integrity": "sha512-C+soprGBHwWBdkDpbaRC4paGBrkIXxVlNohadL5o0kfhsXqOC6GYH2S/Obmig+I0HTDl8wMaRySwrfrXVP8/pQ==", "integrity": "sha512-bHGGJyVjSE4dJJIO5yyEWt/cHyNwga/zXGJbJJ8TiO01aVREK6gCTu3L+5wrkb1FbDkQ+TKjMNe9R/QQQP9+rA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@ -2598,16 +2598,16 @@
} }
}, },
"node_modules/@typescript-eslint/typescript-estree": { "node_modules/@typescript-eslint/typescript-estree": {
"version": "8.46.1", "version": "8.46.0",
"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.0.tgz",
"integrity": "sha512-uIifjT4s8cQKFQ8ZBXXyoUODtRoAd7F7+G8MKmtzj17+1UbdzFl52AzRyZRyKqPHhgzvXunnSckVu36flGy8cg==", "integrity": "sha512-ekDCUfVpAKWJbRfm8T1YRrCot1KFxZn21oV76v5Fj4tr7ELyk84OS+ouvYdcDAwZL89WpEkEj2DKQ+qg//+ucg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/project-service": "8.46.1", "@typescript-eslint/project-service": "8.46.0",
"@typescript-eslint/tsconfig-utils": "8.46.1", "@typescript-eslint/tsconfig-utils": "8.46.0",
"@typescript-eslint/types": "8.46.1", "@typescript-eslint/types": "8.46.0",
"@typescript-eslint/visitor-keys": "8.46.1", "@typescript-eslint/visitor-keys": "8.46.0",
"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",
@ -2653,16 +2653,16 @@
} }
}, },
"node_modules/@typescript-eslint/utils": { "node_modules/@typescript-eslint/utils": {
"version": "8.46.1", "version": "8.46.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.0.tgz",
"integrity": "sha512-vkYUy6LdZS7q1v/Gxb2Zs7zziuXN0wxqsetJdeZdRe/f5dwJFglmuvZBfTUivCtjH725C1jWCDfpadadD95EDQ==", "integrity": "sha512-nD6yGWPj1xiOm4Gk0k6hLSZz2XkNXhuYmyIrOWcHoPuAhjT9i5bAG+xbWPgFeNR8HPHHtpNKdYUXJl/D3x7f5g==",
"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.0",
"@typescript-eslint/types": "8.46.1", "@typescript-eslint/types": "8.46.0",
"@typescript-eslint/typescript-estree": "8.46.1" "@typescript-eslint/typescript-estree": "8.46.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@ -2677,13 +2677,13 @@
} }
}, },
"node_modules/@typescript-eslint/visitor-keys": { "node_modules/@typescript-eslint/visitor-keys": {
"version": "8.46.1", "version": "8.46.0",
"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.0.tgz",
"integrity": "sha512-ptkmIf2iDkNUjdeu2bQqhFPV1m6qTnFFjg7PPDjxKWaMaP0Z6I9l30Jr3g5QqbZGdw8YdYvLp+XnqnWWZOg/NA==", "integrity": "sha512-FrvMpAK+hTbFy7vH5j1+tMYHMSKLE6RzluFJlkFNKD0p9YsUT75JlBSmr5so3QRzvMwU5/bIEdeNrxm8du8l3Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "8.46.1", "@typescript-eslint/types": "8.46.0",
"eslint-visitor-keys": "^4.2.1" "eslint-visitor-keys": "^4.2.1"
}, },
"engines": { "engines": {
@ -6564,9 +6564,9 @@
} }
}, },
"node_modules/react-simple-keyboard": { "node_modules/react-simple-keyboard": {
"version": "3.8.130", "version": "3.8.129",
"resolved": "https://registry.npmjs.org/react-simple-keyboard/-/react-simple-keyboard-3.8.130.tgz", "resolved": "https://registry.npmjs.org/react-simple-keyboard/-/react-simple-keyboard-3.8.129.tgz",
"integrity": "sha512-sq51zg3fe4NPCRyDLYyAtot8+pIn9DmC+YqAEqx5FOIpHUC86Qvv2/0F2KvhLNDvgZ+5s4w649YKf1gWK8LiIQ==", "integrity": "sha512-dvZ+LjOAVkFFay8wZsg//VIMKqfr7tCp28scyFgidAufGjJ60yqWUdckTI1xue827DNb/rbiRuQm5B+3GjcEFQ==",
"license": "MIT", "license": "MIT",
"peerDependencies": { "peerDependencies": {
"react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",

View File

@ -1,7 +1,7 @@
{ {
"name": "kvm-ui", "name": "kvm-ui",
"private": true, "private": true,
"version": "2025.10.13.2055", "version": "2025.10.10.2300",
"type": "module", "type": "module",
"engines": { "engines": {
"node": "^22.15.0" "node": "^22.15.0"
@ -44,7 +44,7 @@
"react-hot-toast": "^2.6.0", "react-hot-toast": "^2.6.0",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-router": "^7.9.4", "react-router": "^7.9.4",
"react-simple-keyboard": "^3.8.130", "react-simple-keyboard": "^3.8.129",
"react-use-websocket": "^4.13.0", "react-use-websocket": "^4.13.0",
"react-xtermjs": "^1.0.10", "react-xtermjs": "^1.0.10",
"recharts": "^3.2.1", "recharts": "^3.2.1",
@ -67,11 +67,11 @@
"@tailwindcss/typography": "^0.5.19", "@tailwindcss/typography": "^0.5.19",
"@tailwindcss/vite": "^4.1.14", "@tailwindcss/vite": "^4.1.14",
"@types/react": "^19.2.2", "@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2", "@types/react-dom": "^19.2.1",
"@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.0",
"@typescript-eslint/parser": "^8.46.1", "@typescript-eslint/parser": "^8.46.0",
"@vitejs/plugin-react-swc": "^4.1.0", "@vitejs/plugin-react-swc": "^4.1.0",
"autoprefixer": "^10.4.21", "autoprefixer": "^10.4.21",
"eslint": "^9.37.0", "eslint": "^9.37.0",

View File

@ -352,7 +352,6 @@ function UrlView({
}) { }) {
const [usbMode, setUsbMode] = useState<RemoteVirtualMediaState["mode"]>("CDROM"); const [usbMode, setUsbMode] = useState<RemoteVirtualMediaState["mode"]>("CDROM");
const [url, setUrl] = useState<string>(""); const [url, setUrl] = useState<string>("");
const [isUrlValid, setIsUrlValid] = useState(false);
const popularImages = [ const popularImages = [
{ {
@ -400,12 +399,6 @@ function UrlView({
const urlRef = useRef<HTMLInputElement>(null); const urlRef = useRef<HTMLInputElement>(null);
useEffect(() => {
if (urlRef.current) {
setIsUrlValid(urlRef.current.validity.valid);
}
}, [url]);
function handleUrlChange(url: string) { function handleUrlChange(url: string) {
setUrl(url); setUrl(url);
if (url.endsWith(".iso")) { if (url.endsWith(".iso")) {
@ -444,7 +437,7 @@ function UrlView({
animationDelay: "0.1s", animationDelay: "0.1s",
}} }}
> >
<Fieldset disabled={!isUrlValid || url.length === 0}> <Fieldset disabled={!urlRef.current?.validity.valid || url.length === 0}>
<UsbModeSelector usbMode={usbMode} setUsbMode={setUsbMode} /> <UsbModeSelector usbMode={usbMode} setUsbMode={setUsbMode} />
</Fieldset> </Fieldset>
<div className="flex space-x-2"> <div className="flex space-x-2">
@ -456,7 +449,7 @@ function UrlView({
text={m.mount_button_mount_url()} text={m.mount_button_mount_url()}
onClick={() => onMount(url, usbMode)} onClick={() => onMount(url, usbMode)}
disabled={ disabled={
mountInProgress || !isUrlValid || url.length === 0 mountInProgress || !urlRef.current?.validity.valid || url.length === 0
} }
/> />
</div> </div>

View File

@ -1,7 +1,7 @@
import { redirect } from "react-router"; import { redirect } from "react-router";
import type { LoaderFunction, LoaderFunctionArgs } from "react-router"; import type { LoaderFunction, LoaderFunctionArgs } from "react-router";
import { getDeviceUiPath } from "@hooks/useAppNavigation"; import { getDeviceUiPath } from "../hooks/useAppNavigation";
const loader: LoaderFunction = ({ params }: LoaderFunctionArgs) => { const loader: LoaderFunction = ({ params }: LoaderFunctionArgs) => {
return redirect(getDeviceUiPath("/settings/general", params.id)); return redirect(getDeviceUiPath("/settings/general", params.id));

View File

@ -1,22 +1,22 @@
import { useCallback, useEffect, useState } from "react"; import { useLoaderData, useNavigate } from "react-router";
import { useLoaderData, useNavigate, type LoaderFunction } from "react-router"; import type { LoaderFunction } from "react-router";
import { ShieldCheckIcon } from "@heroicons/react/24/outline"; import { ShieldCheckIcon } from "@heroicons/react/24/outline";
import { useCallback, useEffect, useState } from "react";
import { useDeviceUiNavigation } from "@hooks/useAppNavigation";
import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
import { GridCard } from "@components/Card";
import { Button, LinkButton } from "@components/Button";
import { InputFieldWithLabel } from "@components/InputField";
import { SelectMenuBasic } from "@components/SelectMenuBasic";
import { SettingsItem } from "@components/SettingsItem";
import { SettingsPageHeader } from "@components/SettingsPageheader";
import { SettingsSectionHeader } from "@components/SettingsSectionHeader";
import { TextAreaWithLabel } from "@components/TextArea";
import api from "@/api"; import api from "@/api";
import { SettingsPageHeader } from "@components/SettingsPageheader";
import { GridCard } from "@/components/Card";
import { Button, LinkButton } from "@/components/Button";
import { InputFieldWithLabel } from "@/components/InputField";
import { SelectMenuBasic } from "@/components/SelectMenuBasic";
import { SettingsItem } from "@components/SettingsItem";
import { SettingsSectionHeader } from "@/components/SettingsSectionHeader";
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
import notifications from "@/notifications"; import notifications from "@/notifications";
import { DEVICE_API } from "@/ui.config"; import { DEVICE_API } from "@/ui.config";
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
import { isOnDevice } from "@/main"; import { isOnDevice } from "@/main";
import { m } from "@localizations/messages.js"; import { TextAreaWithLabel } from "@components/TextArea";
import { LocalDevice } from "./devices.$id"; import { LocalDevice } from "./devices.$id";
import { CloudState } from "./adopt"; import { CloudState } from "./adopt";
@ -92,7 +92,7 @@ export default function SettingsAccessIndexRoute() {
send("deregisterDevice", {}, (resp: JsonRpcResponse) => { send("deregisterDevice", {}, (resp: JsonRpcResponse) => {
if ("error" in resp) { if ("error" in resp) {
notifications.error( notifications.error(
m.access_failed_deregister({ error: resp.error.data || "Unknown error" }), `Failed to de-register device: ${resp.error.data || "Unknown error"}`,
); );
return; return;
} }
@ -107,14 +107,14 @@ export default function SettingsAccessIndexRoute() {
const onCloudAdoptClick = useCallback( const onCloudAdoptClick = useCallback(
(cloudApiUrl: string, cloudAppUrl: string) => { (cloudApiUrl: string, cloudAppUrl: string) => {
if (!deviceId) { if (!deviceId) {
notifications.error(m.access_no_device_id()); notifications.error("No device ID available");
return; return;
} }
send("setCloudUrl", { apiUrl: cloudApiUrl, appUrl: cloudAppUrl }, (resp: JsonRpcResponse) => { send("setCloudUrl", { apiUrl: cloudApiUrl, appUrl: cloudAppUrl }, (resp: JsonRpcResponse) => {
if ("error" in resp) { if ("error" in resp) {
notifications.error( notifications.error(
m.access_failed_update_cloud_url({ error: resp.error.data || "Unknown error" }), `Failed to update cloud URL: ${resp.error.data || "Unknown error"}`,
); );
return; return;
} }
@ -160,12 +160,12 @@ export default function SettingsAccessIndexRoute() {
send("setTLSState", { state }, (resp: JsonRpcResponse) => { send("setTLSState", { state }, (resp: JsonRpcResponse) => {
if ("error" in resp) { if ("error" in resp) {
notifications.error( notifications.error(
m.access_failed_update_tls({ error: resp.error.data || "Unknown error" }), `Failed to update TLS settings: ${resp.error.data || "Unknown error"}`,
); );
return; return;
} }
notifications.success(m.access_tls_updated()); notifications.success("TLS settings updated successfully");
}); });
}, [send]); }, [send]);
@ -206,22 +206,22 @@ export default function SettingsAccessIndexRoute() {
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<SettingsPageHeader <SettingsPageHeader
title={m.access_title()} title="Access"
description={m.access_description()} description="Manage the Access Control of the device"
/> />
{loaderData?.authMode && ( {loaderData?.authMode && (
<> <>
<div className="space-y-4"> <div className="space-y-4">
<SettingsSectionHeader <SettingsSectionHeader
title={m.access_local_title()} title="Local"
description={m.access_local_description()} description="Manage the mode of local access to the device"
/> />
<> <>
<SettingsItem <SettingsItem
title={m.access_https_mode_title()} title="HTTPS Mode"
badge="Experimental" badge="Experimental"
description={m.access_https_description()} description="Configure secure HTTPS access to your device"
> >
<SelectMenuBasic <SelectMenuBasic
size="SM" size="SM"
@ -229,9 +229,9 @@ export default function SettingsAccessIndexRoute() {
onChange={e => handleTlsModeChange(e.target.value)} onChange={e => handleTlsModeChange(e.target.value)}
disabled={tlsMode === "unknown"} disabled={tlsMode === "unknown"}
options={[ options={[
{ value: "disabled", label: m.access_tls_disabled() }, { value: "disabled", label: "Disabled" },
{ value: "self-signed", label: m.access_tls_self_signed() }, { value: "self-signed", label: "Self-signed" },
{ value: "custom", label: m.access_tls_custom() }, { value: "custom", label: "Custom" },
]} ]}
/> />
</SettingsItem> </SettingsItem>
@ -240,12 +240,12 @@ export default function SettingsAccessIndexRoute() {
<div className="mt-4 space-y-4"> <div className="mt-4 space-y-4">
<div className="space-y-4"> <div className="space-y-4">
<SettingsItem <SettingsItem
title={m.access_tls_certificate_title()} title="TLS Certificate"
description={m.access_tls_certificate_description()} description="Paste your TLS certificate below. For certificate chains, include the entire chain (leaf, intermediate, and root certificates)."
/> />
<div className="space-y-4"> <div className="space-y-4">
<TextAreaWithLabel <TextAreaWithLabel
label={m.access_certificate_label()} label="Certificate"
rows={3} rows={3}
placeholder={ placeholder={
"-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----" "-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"
@ -258,8 +258,8 @@ export default function SettingsAccessIndexRoute() {
<div className="space-y-4"> <div className="space-y-4">
<div className="space-y-4"> <div className="space-y-4">
<TextAreaWithLabel <TextAreaWithLabel
label={m.access_private_key_label()} label="Private Key"
description={m.access_private_key_description()} description="For security reasons, it will not be displayed after saving."
rows={3} rows={3}
placeholder={ placeholder={
"-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----" "-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----"
@ -274,7 +274,7 @@ export default function SettingsAccessIndexRoute() {
<Button <Button
size="SM" size="SM"
theme="primary" theme="primary"
text={m.access_update_tls_settings()} text="Update TLS Settings"
onClick={handleCustomTlsUpdate} onClick={handleCustomTlsUpdate}
/> />
</div> </div>
@ -282,14 +282,14 @@ export default function SettingsAccessIndexRoute() {
)} )}
<SettingsItem <SettingsItem
title={m.access_authentication_mode_title()} title="Authentication Mode"
description={loaderData.authMode === "password" ? m.access_auth_mode_password() : m.access_auth_mode_no_password()} description={`Current mode: ${loaderData.authMode === "password" ? "Password protected" : "No password"}`}
> >
{loaderData.authMode === "password" ? ( {loaderData.authMode === "password" ? (
<Button <Button
size="SM" size="SM"
theme="light" theme="light"
text={m.access_disable_protection()} text="Disable Protection"
onClick={() => { onClick={() => {
navigateTo("./local-auth", { state: { init: "deletePassword" } }); navigateTo("./local-auth", { state: { init: "deletePassword" } });
}} }}
@ -298,7 +298,7 @@ export default function SettingsAccessIndexRoute() {
<Button <Button
size="SM" size="SM"
theme="light" theme="light"
text={m.access_enable_password()} text="Enable Password"
onClick={() => { onClick={() => {
navigateTo("./local-auth", { state: { init: "createPassword" } }); navigateTo("./local-auth", { state: { init: "createPassword" } });
}} }}
@ -309,13 +309,13 @@ export default function SettingsAccessIndexRoute() {
{loaderData.authMode === "password" && ( {loaderData.authMode === "password" && (
<SettingsItem <SettingsItem
title={m.access_change_password_title()} title="Change Password"
description={m.access_change_password_description()} description="Update your device access password"
> >
<Button <Button
size="SM" size="SM"
theme="light" theme="light"
text={m.access_change_password_button()} text="Change Password"
onClick={() => { onClick={() => {
navigateTo("./local-auth", { state: { init: "updatePassword" } }); navigateTo("./local-auth", { state: { init: "updatePassword" } });
}} }}
@ -330,23 +330,23 @@ export default function SettingsAccessIndexRoute() {
<div className="space-y-4"> <div className="space-y-4">
<SettingsSectionHeader <SettingsSectionHeader
title="Remote" title="Remote"
description={m.access_remote_description()} description="Manage the mode of Remote access to the device"
/> />
<div className="space-y-4"> <div className="space-y-4">
{!isAdopted && ( {!isAdopted && (
<> <>
<SettingsItem <SettingsItem
title={m.access_cloud_provider_title()} title="Cloud Provider"
description={m.access_cloud_provider_description()} description="Select the cloud provider for your device"
> >
<SelectMenuBasic <SelectMenuBasic
size="SM" size="SM"
value={selectedProvider} value={selectedProvider}
onChange={e => handleProviderChange(e.target.value)} onChange={e => handleProviderChange(e.target.value)}
options={[ options={[
{ value: "jetkvm", label: m.access_provider_jetkvm() }, { value: "jetkvm", label: "JetKVM Cloud" },
{ value: "custom", label: m.access_provider_custom() }, { value: "custom", label: "Custom" },
]} ]}
/> />
</SettingsItem> </SettingsItem>
@ -356,7 +356,7 @@ export default function SettingsAccessIndexRoute() {
<div className="flex items-end gap-x-2"> <div className="flex items-end gap-x-2">
<InputFieldWithLabel <InputFieldWithLabel
size="SM" size="SM"
label={m.access_cloud_api_url_label()} label="Cloud API URL"
value={cloudApiUrl} value={cloudApiUrl}
onChange={e => setCloudApiUrl(e.target.value)} onChange={e => setCloudApiUrl(e.target.value)}
placeholder="https://api.example.com" placeholder="https://api.example.com"
@ -365,7 +365,7 @@ export default function SettingsAccessIndexRoute() {
<div className="flex items-end gap-x-2"> <div className="flex items-end gap-x-2">
<InputFieldWithLabel <InputFieldWithLabel
size="SM" size="SM"
label={m.access_cloud_app_url_label()} label="Cloud App URL"
value={cloudAppUrl} value={cloudAppUrl}
onChange={e => setCloudAppUrl(e.target.value)} onChange={e => setCloudAppUrl(e.target.value)}
placeholder="https://app.example.com" placeholder="https://app.example.com"
@ -384,26 +384,26 @@ export default function SettingsAccessIndexRoute() {
<div className="space-y-3"> <div className="space-y-3">
<div className="space-y-2"> <div className="space-y-2">
<h3 className="text-base font-bold text-slate-900 dark:text-white"> <h3 className="text-base font-bold text-slate-900 dark:text-white">
{m.access_cloud_security_title()} Cloud Security
</h3> </h3>
<div> <div>
<ul className="list-disc space-y-1 pl-5 text-xs text-slate-700 dark:text-slate-300"> <ul className="list-disc space-y-1 pl-5 text-xs text-slate-700 dark:text-slate-300">
<li>{m.access_security_encryption()}</li> <li>End-to-end encryption using WebRTC (DTLS and SRTP)</li>
<li>{m.access_security_zero_trust()}</li> <li>Zero Trust security model</li>
<li>{m.access_security_oidc()}</li> <li>OIDC (OpenID Connect) authentication</li>
<li>{m.access_security_streams()}</li> <li>All streams encrypted in transit</li>
</ul> </ul>
</div> </div>
<div className="text-xs text-slate-700 dark:text-slate-300"> <div className="text-xs text-slate-700 dark:text-slate-300">
{m.access_security_open_source()}{" "} All cloud components are open-source and available on{" "}
<a <a
href="https://github.com/jetkvm" href="https://github.com/jetkvm"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="font-medium text-blue-600 hover:text-blue-800 dark:text-blue-500 dark:hover:text-blue-400" className="font-medium text-blue-600 hover:text-blue-800 dark:text-blue-500 dark:hover:text-blue-400"
> >
{m.access_github_link()} GitHub
</a> </a>
. .
</div> </div>
@ -415,7 +415,7 @@ export default function SettingsAccessIndexRoute() {
to="https://jetkvm.com/docs/networking/remote-access" to="https://jetkvm.com/docs/networking/remote-access"
size="SM" size="SM"
theme="light" theme="light"
text={m.access_learn_security()} text="Learn about our cloud security"
/> />
</div> </div>
</div> </div>
@ -429,32 +429,32 @@ export default function SettingsAccessIndexRoute() {
onClick={() => onCloudAdoptClick(cloudApiUrl, cloudAppUrl)} onClick={() => onCloudAdoptClick(cloudApiUrl, cloudAppUrl)}
size="SM" size="SM"
theme="primary" theme="primary"
text={m.access_adopt_kvm()} text="Adopt KVM to Cloud"
/> />
</div> </div>
) : ( ) : (
<div> <div>
<div className="space-y-2"> <div className="space-y-2">
<p className="text-sm text-slate-600 dark:text-slate-300"> <p className="text-sm text-slate-600 dark:text-slate-300">
{m.access_adopted_message()} Your device is adopted to the Cloud
</p> </p>
<div> <div>
<Button <Button
size="SM" size="SM"
theme="light" theme="light"
text={m.access_deregister()} text="De-register from Cloud"
className="text-red-600" className="text-red-600"
onClick={() => { onClick={() => {
if (deviceId) { if (deviceId) {
if ( if (
window.confirm( window.confirm(
m.access_confirm_deregister(), "Are you sure you want to de-register this device?",
) )
) { ) {
deregisterDevice(); deregisterDevice();
} }
} else { } else {
notifications.error(m.access_no_device_id()); notifications.error("No device ID available");
} }
}} }}
/> />

View File

@ -1,12 +1,11 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { useLocation, useRevalidator } from "react-router"; import { useLocation, useRevalidator } from "react-router";
import { useLocalAuthModalStore } from "@hooks/stores";
import { useDeviceUiNavigation } from "@hooks/useAppNavigation";
import { Button } from "@components/Button"; import { Button } from "@components/Button";
import { InputFieldWithLabel } from "@/components/InputField"; import { InputFieldWithLabel } from "@/components/InputField";
import api from "@/api"; import api from "@/api";
import { m } from "@localizations/messages.js"; import { useLocalAuthModalStore } from "@/hooks/stores";
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
export default function SecurityAccessLocalAuthRoute() { export default function SecurityAccessLocalAuthRoute() {
const { setModalView } = useLocalAuthModalStore(); const { setModalView } = useLocalAuthModalStore();
@ -35,12 +34,12 @@ export function Dialog({ onClose }: { onClose: () => void }) {
const handleCreatePassword = async (password: string, confirmPassword: string) => { const handleCreatePassword = async (password: string, confirmPassword: string) => {
if (password === "") { if (password === "") {
setError(m.local_auth_error_enter_password()); setError("Please enter a password");
return; return;
} }
if (password !== confirmPassword) { if (password !== confirmPassword) {
setError(m.local_auth_error_passwords_not_match()); setError("Passwords do not match");
return; return;
} }
@ -52,11 +51,11 @@ export function Dialog({ onClose }: { onClose: () => void }) {
revalidator.revalidate(); revalidator.revalidate();
} else { } else {
const data = await res.json(); const data = await res.json();
setError(data.error || m.local_auth_error_setting_password()); setError(data.error || "An error occurred while setting the password");
} }
} catch (error) { } catch (error) {
console.error(error); console.error(error);
setError(m.local_auth_error_setting_password()); setError("An error occurred while setting the password");
} }
}; };
@ -66,17 +65,17 @@ export function Dialog({ onClose }: { onClose: () => void }) {
confirmNewPassword: string, confirmNewPassword: string,
) => { ) => {
if (newPassword !== confirmNewPassword) { if (newPassword !== confirmNewPassword) {
setError(m.local_auth_error_passwords_not_match()); setError("Passwords do not match");
return; return;
} }
if (oldPassword === "") { if (oldPassword === "") {
setError(m.local_auth_error_enter_old_password()); setError("Please enter your old password");
return; return;
} }
if (newPassword === "") { if (newPassword === "") {
setError(m.local_auth_error_enter_new_password()); setError("Please enter a new password");
return; return;
} }
@ -92,17 +91,17 @@ export function Dialog({ onClose }: { onClose: () => void }) {
revalidator.revalidate(); revalidator.revalidate();
} else { } else {
const data = await res.json(); const data = await res.json();
setError(data.error || m.local_auth_error_changing_password()); setError(data.error || "An error occurred while changing the password");
} }
} catch (error) { } catch (error) {
console.error(error); console.error(error);
setError(m.local_auth_error_changing_password()); setError("An error occurred while changing the password");
} }
}; };
const handleDeletePassword = async (password: string) => { const handleDeletePassword = async (password: string) => {
if (password === "") { if (password === "") {
setError(m.local_auth_error_enter_current_password()); setError("Please enter your current password");
return; return;
} }
@ -114,11 +113,11 @@ export function Dialog({ onClose }: { onClose: () => void }) {
revalidator.revalidate(); revalidator.revalidate();
} else { } else {
const data = await res.json(); const data = await res.json();
setError(data.error || m.local_auth_error_disabling_password()); setError(data.error || "An error occurred while disabling the password");
} }
} catch (error) { } catch (error) {
console.error(error); console.error(error);
setError(m.local_auth_error_disabling_password()); setError("An error occurred while disabling the password");
} }
}; };
@ -151,24 +150,24 @@ export function Dialog({ onClose }: { onClose: () => void }) {
{modalView === "creationSuccess" && ( {modalView === "creationSuccess" && (
<SuccessModal <SuccessModal
headline={m.local_auth_success_password_set_title()} headline="Password Set Successfully"
description={m.local_auth_success_password_set_description()} description="You've successfully set up local device protection. Your device is now secure against unauthorized local access."
onClose={onClose} onClose={onClose}
/> />
)} )}
{modalView === "deleteSuccess" && ( {modalView === "deleteSuccess" && (
<SuccessModal <SuccessModal
headline={m.local_auth_success_password_disabled_title()} headline="Password Protection Disabled"
description={m.local_auth_success_password_disabled_description()} description="You've successfully disabled the password protection for local access. Remember, your device is now less secure."
onClose={onClose} onClose={onClose}
/> />
)} )}
{modalView === "updateSuccess" && ( {modalView === "updateSuccess" && (
<SuccessModal <SuccessModal
headline={m.local_auth_success_password_updated_title()} headline="Password Updated Successfully"
description={m.local_auth_success_password_updated_description()} description="You've successfully changed your local device protection password. Make sure to remember your new password for future access."
onClose={onClose} onClose={onClose}
/> />
)} )}
@ -199,24 +198,24 @@ function CreatePasswordModal({
> >
<div> <div>
<h2 className="text-lg font-semibold dark:text-white"> <h2 className="text-lg font-semibold dark:text-white">
{m.local_auth_create_title()} Local Device Protection
</h2> </h2>
<p className="text-sm text-slate-600 dark:text-slate-400"> <p className="text-sm text-slate-600 dark:text-slate-400">
{m.local_auth_create_description()} Create a password to protect your device from unauthorized local access.
</p> </p>
</div> </div>
<InputFieldWithLabel <InputFieldWithLabel
label={m.local_auth_create_new_password_label()} label="New Password"
type="password" type="password"
placeholder={m.local_auth_create_new_password_placeholder()} placeholder="Enter a strong password"
value={password} value={password}
autoFocus autoFocus
onChange={e => setPassword(e.target.value)} onChange={e => setPassword(e.target.value)}
/> />
<InputFieldWithLabel <InputFieldWithLabel
label={m.local_auth_confirm_new_password_label()} label="Confirm New Password"
type="password" type="password"
placeholder={m.local_auth_create_confirm_password_placeholder()} placeholder="Re-enter your password"
value={confirmPassword} value={confirmPassword}
onChange={e => setConfirmPassword(e.target.value)} onChange={e => setConfirmPassword(e.target.value)}
/> />
@ -225,10 +224,10 @@ function CreatePasswordModal({
<Button <Button
size="SM" size="SM"
theme="primary" theme="primary"
text={m.local_auth_create_secure_button()} text="Secure Device"
onClick={() => onSetPassword(password, confirmPassword)} onClick={() => onSetPassword(password, confirmPassword)}
/> />
<Button size="SM" theme="light" text={m.local_auth_create_not_now_button()} onClick={onCancel} /> <Button size="SM" theme="light" text="Not Now" onClick={onCancel} />
</div> </div>
{error && <p className="text-sm text-red-500">{error}</p>} {error && <p className="text-sm text-red-500">{error}</p>}
</form> </form>
@ -252,16 +251,16 @@ function DeletePasswordModal({
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
<h2 className="text-lg font-semibold dark:text-white"> <h2 className="text-lg font-semibold dark:text-white">
{m.local_auth_disable_local_device_protection_title()} Disable Local Device Protection
</h2> </h2>
<p className="text-sm text-slate-600 dark:text-slate-400"> <p className="text-sm text-slate-600 dark:text-slate-400">
{m.local_auth_disable_local_device_protection_description()} Enter your current password to disable local device protection.
</p> </p>
</div> </div>
<InputFieldWithLabel <InputFieldWithLabel
label={m.local_auth_current_password_label()} label="Current Password"
type="password" type="password"
placeholder={m.local_auth_enter_current_password_placeholder()} placeholder="Enter your current password"
value={password} value={password}
onChange={e => setPassword(e.target.value)} onChange={e => setPassword(e.target.value)}
/> />
@ -269,10 +268,10 @@ function DeletePasswordModal({
<Button <Button
size="SM" size="SM"
theme="danger" theme="danger"
text={m.local_auth_disable_protection_button()} text="Disable Protection"
onClick={() => onDeletePassword(password)} onClick={() => onDeletePassword(password)}
/> />
<Button size="SM" theme="light" text={m.cancel()} onClick={onCancel} /> <Button size="SM" theme="light" text="Cancel" onClick={onCancel} />
</div> </div>
{error && <p className="text-sm text-red-500">{error}</p>} {error && <p className="text-sm text-red-500">{error}</p>}
</div> </div>
@ -307,30 +306,31 @@ function UpdatePasswordModal({
> >
<div> <div>
<h2 className="text-lg font-semibold dark:text-white"> <h2 className="text-lg font-semibold dark:text-white">
{m.local_auth_change_local_device_password_title()} Change Local Device Password
</h2> </h2>
<p className="text-sm text-slate-600 dark:text-slate-400"> <p className="text-sm text-slate-600 dark:text-slate-400">
{m.local_auth_change_local_device_password_description()} Enter your current password and a new password to update your local device
protection.
</p> </p>
</div> </div>
<InputFieldWithLabel <InputFieldWithLabel
label={m.local_auth_current_password_label()} label="Current Password"
type="password" type="password"
placeholder={m.local_auth_enter_current_password_placeholder()} placeholder="Enter your current password"
value={oldPassword} value={oldPassword}
onChange={e => setOldPassword(e.target.value)} onChange={e => setOldPassword(e.target.value)}
/> />
<InputFieldWithLabel <InputFieldWithLabel
label={m.local_auth_new_password_label()} label="New Password"
type="password" type="password"
placeholder={m.local_auth_enter_new_password_placeholder()} placeholder="Enter a new strong password"
value={newPassword} value={newPassword}
onChange={e => setNewPassword(e.target.value)} onChange={e => setNewPassword(e.target.value)}
/> />
<InputFieldWithLabel <InputFieldWithLabel
label={m.local_auth_confirm_new_password_label()} label="Confirm New Password"
type="password" type="password"
placeholder={m.local_auth_reenter_new_password_placeholder()} placeholder="Re-enter your new password"
value={confirmNewPassword} value={confirmNewPassword}
onChange={e => setConfirmNewPassword(e.target.value)} onChange={e => setConfirmNewPassword(e.target.value)}
/> />
@ -338,10 +338,10 @@ function UpdatePasswordModal({
<Button <Button
size="SM" size="SM"
theme="primary" theme="primary"
text={m.local_auth_update_password_button()} text="Update Password"
onClick={() => onUpdatePassword(oldPassword, newPassword, confirmNewPassword)} onClick={() => onUpdatePassword(oldPassword, newPassword, confirmNewPassword)}
/> />
<Button size="SM" theme="light" text={m.cancel()} onClick={onCancel} /> <Button size="SM" theme="light" text="Cancel" onClick={onCancel} />
</div> </div>
{error && <p className="text-sm text-red-500">{error}</p>} {error && <p className="text-sm text-red-500">{error}</p>}
</form> </form>
@ -365,7 +365,7 @@ function SuccessModal({
<h2 className="text-lg font-semibold dark:text-white">{headline}</h2> <h2 className="text-lg font-semibold dark:text-white">{headline}</h2>
<p className="text-sm text-slate-600 dark:text-slate-400">{description}</p> <p className="text-sm text-slate-600 dark:text-slate-400">{description}</p>
</div> </div>
<Button size="SM" theme="primary" text={m.close()} onClick={onClose} /> <Button size="SM" theme="primary" text="Close" onClick={onClose} />
</div> </div>
</div> </div>
); );

View File

@ -1,17 +1,17 @@
import { useCallback, useEffect, useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { useSettingsStore } from "@hooks/stores";
import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
import { Button } from "@components/Button";
import Checkbox from "@components/Checkbox";
import { ConfirmDialog } from "@components/ConfirmDialog";
import { GridCard } from "@components/Card"; import { GridCard } from "@components/Card";
import { SettingsItem } from "@components/SettingsItem"; import { SettingsItem } from "@components/SettingsItem";
import { SettingsPageHeader } from "@components/SettingsPageheader";
import { TextAreaWithLabel } from "@components/TextArea"; import { Button } from "../components/Button";
import { isOnDevice } from "@/main"; import Checkbox from "../components/Checkbox";
import notifications from "@/notifications"; import { ConfirmDialog } from "../components/ConfirmDialog";
import { m } from "@localizations/messages.js"; import { SettingsPageHeader } from "../components/SettingsPageheader";
import { TextAreaWithLabel } from "../components/TextArea";
import { useSettingsStore } from "../hooks/stores";
import { JsonRpcResponse, useJsonRpc } from "../hooks/useJsonRpc";
import { isOnDevice } from "../main";
import notifications from "../notifications";
export default function SettingsAdvancedRoute() { export default function SettingsAdvancedRoute() {
const { send } = useJsonRpc(); const { send } = useJsonRpc();
@ -65,9 +65,7 @@ export default function SettingsAdvancedRoute() {
send("setUsbEmulationState", { enabled: enabled }, (resp: JsonRpcResponse) => { send("setUsbEmulationState", { enabled: enabled }, (resp: JsonRpcResponse) => {
if ("error" in resp) { if ("error" in resp) {
notifications.error( notifications.error(
enabled `Failed to ${enabled ? "enable" : "disable"} USB emulation: ${resp.error.data || "Unknown error"}`,
? m.advanced_error_usb_emulation_enable({error: resp.error.data || m.unknown_error()})
: m.advanced_error_usb_emulation_disable({error: resp.error.data || m.unknown_error()})
); );
return; return;
} }
@ -82,11 +80,11 @@ export default function SettingsAdvancedRoute() {
send("resetConfig", {}, (resp: JsonRpcResponse) => { send("resetConfig", {}, (resp: JsonRpcResponse) => {
if ("error" in resp) { if ("error" in resp) {
notifications.error( notifications.error(
m.advanced_error_reset_config({error: resp.error.data || m.unknown_error()}) `Failed to reset configuration: ${resp.error.data || "Unknown error"}`,
); );
return; return;
} }
notifications.success(m.advanced_success_reset_config()); notifications.success("Configuration reset to default successfully");
}); });
}, [send]); }, [send]);
@ -94,11 +92,11 @@ export default function SettingsAdvancedRoute() {
send("setSSHKeyState", { sshKey }, (resp: JsonRpcResponse) => { send("setSSHKeyState", { sshKey }, (resp: JsonRpcResponse) => {
if ("error" in resp) { if ("error" in resp) {
notifications.error( notifications.error(
m.advanced_error_update_ssh_key({error: resp.error.data || m.unknown_error()}) `Failed to update SSH key: ${resp.error.data || "Unknown error"}`,
); );
return; return;
} }
notifications.success(m.advanced_success_update_ssh_key()); notifications.success("SSH key updated successfully");
}); });
}, [send, sshKey]); }, [send, sshKey]);
@ -107,7 +105,7 @@ export default function SettingsAdvancedRoute() {
send("setDevModeState", { enabled: developerMode }, (resp: JsonRpcResponse) => { send("setDevModeState", { enabled: developerMode }, (resp: JsonRpcResponse) => {
if ("error" in resp) { if ("error" in resp) {
notifications.error( notifications.error(
m.advanced_error_set_dev_mode({error: resp.error.data || m.unknown_error()}) `Failed to set dev mode: ${resp.error.data || "Unknown error"}`,
); );
return; return;
} }
@ -122,7 +120,7 @@ export default function SettingsAdvancedRoute() {
send("setDevChannelState", { enabled }, (resp: JsonRpcResponse) => { send("setDevChannelState", { enabled }, (resp: JsonRpcResponse) => {
if ("error" in resp) { if ("error" in resp) {
notifications.error( notifications.error(
m.advanced_error_set_dev_channel({error: resp.error.data || m.unknown_error()}) `Failed to set dev channel state: ${resp.error.data || "Unknown error"}`,
); );
return; return;
} }
@ -137,17 +135,19 @@ export default function SettingsAdvancedRoute() {
send("setLocalLoopbackOnly", { enabled }, (resp: JsonRpcResponse) => { send("setLocalLoopbackOnly", { enabled }, (resp: JsonRpcResponse) => {
if ("error" in resp) { if ("error" in resp) {
notifications.error( notifications.error(
enabled `Failed to ${enabled ? "enable" : "disable"} loopback-only mode: ${resp.error.data || "Unknown error"}`,
? m.advanced_error_loopback_enable({error: resp.error.data || m.unknown_error()})
: m.advanced_error_loopback_disable({error: resp.error.data || m.unknown_error()})
); );
return; return;
} }
setLocalLoopbackOnly(enabled); setLocalLoopbackOnly(enabled);
if (enabled) { if (enabled) {
notifications.success(m.advanced_success_loopback_enabled()); notifications.success(
"Loopback-only mode enabled. Restart your device to apply.",
);
} else { } else {
notifications.success(m.advanced_success_loopback_disabled()); notifications.success(
"Loopback-only mode disabled. Restart your device to apply.",
);
} }
}); });
}, },
@ -175,14 +175,14 @@ export default function SettingsAdvancedRoute() {
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<SettingsPageHeader <SettingsPageHeader
title={m.advanced_title()} title="Advanced"
description={m.advanced_description()} description="Access additional settings for troubleshooting and customization"
/> />
<div className="space-y-4"> <div className="space-y-4">
<SettingsItem <SettingsItem
title={m.advanced_dev_channel_title()} title="Dev Channel Updates"
description={m.advanced_dev_channel_description()} description="Receive early updates from the development channel"
> >
<Checkbox <Checkbox
checked={devChannel} checked={devChannel}
@ -192,8 +192,8 @@ export default function SettingsAdvancedRoute() {
/> />
</SettingsItem> </SettingsItem>
<SettingsItem <SettingsItem
title={m.advanced_developer_mode_title()} title="Developer Mode"
description={m.advanced_developer_mode_description()} description="Enable advanced features for developers"
> >
<Checkbox <Checkbox
checked={settings.developerMode} checked={settings.developerMode}
@ -219,17 +219,18 @@ export default function SettingsAdvancedRoute() {
<div className="space-y-3"> <div className="space-y-3">
<div className="space-y-2"> <div className="space-y-2">
<h3 className="text-base font-bold text-slate-900 dark:text-white"> <h3 className="text-base font-bold text-slate-900 dark:text-white">
{m.advanced_developer_mode_enabled_title()} Developer Mode Enabled
</h3> </h3>
<div> <div>
<ul className="list-disc space-y-1 pl-5 text-xs text-slate-700 dark:text-slate-300"> <ul className="list-disc space-y-1 pl-5 text-xs text-slate-700 dark:text-slate-300">
<li>{m.advanced_developer_mode_warning_security()}</li> <li>Security is weakened while active</li>
<li>{m.advanced_developer_mode_warning_risks()}</li> <li>Only use if you understand the risks</li>
</ul> </ul>
</div> </div>
</div> </div>
<div className="text-xs text-slate-700 dark:text-slate-300"> <div className="text-xs text-slate-700 dark:text-slate-300">
{m.advanced_developer_mode_warning_advanced()} For advanced users only. Not for production use.
</div> </div>
</div> </div>
</div> </div>
@ -237,8 +238,8 @@ export default function SettingsAdvancedRoute() {
)} )}
<SettingsItem <SettingsItem
title={m.advanced_loopback_only_title()} title="Loopback-Only Mode"
description={m.advanced_loopback_only_description()} description="Restrict web interface access to localhost only (127.0.0.1)"
> >
<Checkbox <Checkbox
checked={localLoopbackOnly} checked={localLoopbackOnly}
@ -249,25 +250,25 @@ export default function SettingsAdvancedRoute() {
{isOnDevice && settings.developerMode && ( {isOnDevice && settings.developerMode && (
<div className="space-y-4"> <div className="space-y-4">
<SettingsItem <SettingsItem
title={m.advanced_ssh_access_title()} title="SSH Access"
description={m.advanced_ssh_access_description()} description="Add your SSH public key to enable secure remote access to the device"
/> />
<div className="space-y-4"> <div className="space-y-4">
<TextAreaWithLabel <TextAreaWithLabel
label={m.advanced_ssh_public_key_label()} label="SSH Public Key"
value={sshKey || ""} value={sshKey || ""}
rows={3} rows={3}
onChange={e => setSSHKey(e.target.value)} onChange={e => setSSHKey(e.target.value)}
placeholder={m.advanced_ssh_public_key_placeholder()} placeholder="Enter your SSH public key"
/> />
<p className="text-xs text-slate-600 dark:text-slate-400"> <p className="text-xs text-slate-600 dark:text-slate-400">
{m.advanced_ssh_default_user()}<strong>root</strong>. The default SSH user is <strong>root</strong>.
</p> </p>
<div className="flex items-center gap-x-2"> <div className="flex items-center gap-x-2">
<Button <Button
size="SM" size="SM"
theme="primary" theme="primary"
text={m.advanced_update_ssh_key_button()} text="Update SSH Key"
onClick={handleUpdateSSHKey} onClick={handleUpdateSSHKey}
/> />
</div> </div>
@ -276,8 +277,8 @@ export default function SettingsAdvancedRoute() {
)} )}
<SettingsItem <SettingsItem
title={m.advanced_troubleshooting_mode_title()} title="Troubleshooting Mode"
description={m.advanced_troubleshooting_mode_description()} description="Diagnostic tools and additional controls for troubleshooting and development purposes"
> >
<Checkbox <Checkbox
defaultChecked={settings.debugMode} defaultChecked={settings.debugMode}
@ -290,27 +291,27 @@ export default function SettingsAdvancedRoute() {
{settings.debugMode && ( {settings.debugMode && (
<> <>
<SettingsItem <SettingsItem
title={m.advanced_usb_emulation_title()} title="USB Emulation"
description={m.advanced_usb_emulation_description()} description="Control the USB emulation state"
> >
<Button <Button
size="SM" size="SM"
theme="light" theme="light"
text={ text={
usbEmulationEnabled ? m.advanced_disable_usb_emulation() : m.advanced_enable_usb_emulation() usbEmulationEnabled ? "Disable USB Emulation" : "Enable USB Emulation"
} }
onClick={() => handleUsbEmulationToggle(!usbEmulationEnabled)} onClick={() => handleUsbEmulationToggle(!usbEmulationEnabled)}
/> />
</SettingsItem> </SettingsItem>
<SettingsItem <SettingsItem
title={m.advanced_reset_config_title()} title="Reset Configuration"
description={m.advanced_reset_config_description()} description="Reset configuration to default. This will log you out."
> >
<Button <Button
size="SM" size="SM"
theme="light" theme="light"
text={m.advanced_reset_config_button()} text="Reset Config"
onClick={() => { onClick={() => {
handleResetConfig(); handleResetConfig();
window.location.reload(); window.location.reload();
@ -326,23 +327,22 @@ export default function SettingsAdvancedRoute() {
onClose={() => { onClose={() => {
setShowLoopbackWarning(false); setShowLoopbackWarning(false);
}} }}
title={m.advanced_loopback_warning_title()} title="Enable Loopback-Only Mode?"
description={ description={
<> <>
<p> <p>
{m.advanced_loopback_warning_description()} WARNING: This will restrict web interface access to localhost (127.0.0.1)
</p> only.
<p>
{m.advanced_loopback_warning_before()}
</p> </p>
<p>Before enabling this feature, make sure you have either:</p>
<ul className="list-disc space-y-1 pl-5 text-xs text-slate-700 dark:text-slate-300"> <ul className="list-disc space-y-1 pl-5 text-xs text-slate-700 dark:text-slate-300">
<li>{m.advanced_loopback_warning_ssh()}</li> <li>SSH access configured and tested</li>
<li>{m.advanced_loopback_warning_cloud()}</li> <li>Cloud access enabled and working</li>
</ul> </ul>
</> </>
} }
variant="warning" variant="warning"
confirmText={m.advanced_loopback_warning_confirm()} confirmText="I Understand, Enable Anyway"
onConfirm={confirmLoopbackModeEnable} onConfirm={confirmLoopbackModeEnable}
/> />
</div> </div>

View File

@ -41,6 +41,8 @@ export default function SettingsGeneralUpdateRoute() {
return <Dialog onClose={() => navigate("..")} onConfirmUpdate={onConfirmUpdate} />; return <Dialog onClose={() => navigate("..")} onConfirmUpdate={onConfirmUpdate} />;
} }
export function Dialog({ export function Dialog({
onClose, onClose,
onConfirmUpdate, onConfirmUpdate,
@ -69,6 +71,11 @@ export function Dialog({
[setModalView], [setModalView],
); );
// Reset modal view when dialog is opened
useEffect(() => {
setVersionInfo(null);
}, [setModalView]);
return ( return (
<div className="pointer-events-auto relative mx-auto text-left"> <div className="pointer-events-auto relative mx-auto text-left">
<div> <div>
@ -126,6 +133,8 @@ function LoadingState({
const progressBarRef = useRef<HTMLDivElement>(null); const progressBarRef = useRef<HTMLDivElement>(null);
useEffect(() => { useEffect(() => {
setProgressWidth("0%");
abortControllerRef.current = new AbortController(); abortControllerRef.current = new AbortController();
const signal = abortControllerRef.current.signal; const signal = abortControllerRef.current.signal;

View File

@ -1,19 +1,20 @@
import { useEffect } from "react"; import { useEffect } from "react";
import { BacklightSettings, useSettingsStore } from "@hooks/stores";
import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
import { FeatureFlag } from "@components/FeatureFlag";
import { SelectMenuBasic } from "@components/SelectMenuBasic";
import { SettingsItem } from "@components/SettingsItem"; import { SettingsItem } from "@components/SettingsItem";
import { SettingsPageHeader } from "@components/SettingsPageheader"; import { SettingsPageHeader } from "@components/SettingsPageheader";
import { BacklightSettings, useSettingsStore } from "@/hooks/stores";
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
import { SelectMenuBasic } from "@components/SelectMenuBasic";
import { UsbDeviceSetting } from "@components/UsbDeviceSetting"; import { UsbDeviceSetting } from "@components/UsbDeviceSetting";
import { UsbInfoSetting } from "@components/UsbInfoSetting";
import notifications from "@/notifications"; import notifications from "../notifications";
import { UsbInfoSetting } from "../components/UsbInfoSetting";
import { FeatureFlag } from "../components/FeatureFlag";
export default function SettingsHardwareRoute() { export default function SettingsHardwareRoute() {
const { send } = useJsonRpc(); const { send } = useJsonRpc();
const settings = useSettingsStore(); const settings = useSettingsStore();
const { displayRotation, setDisplayRotation } = useSettingsStore(); const { setDisplayRotation } = useSettingsStore();
const handleDisplayRotationChange = (rotation: string) => { const handleDisplayRotationChange = (rotation: string) => {
setDisplayRotation(rotation); setDisplayRotation(rotation);
@ -21,7 +22,7 @@ export default function SettingsHardwareRoute() {
}; };
const handleDisplayRotationSave = () => { const handleDisplayRotationSave = () => {
send("setDisplayRotation", { params: { rotation: displayRotation } }, (resp: JsonRpcResponse) => { send("setDisplayRotation", { params: { rotation: settings.displayRotation } }, (resp: JsonRpcResponse) => {
if ("error" in resp) { if ("error" in resp) {
notifications.error( notifications.error(
`Failed to set display orientation: ${resp.error.data || "Unknown error"}`, `Failed to set display orientation: ${resp.error.data || "Unknown error"}`,
@ -32,7 +33,7 @@ export default function SettingsHardwareRoute() {
}); });
}; };
const { backlightSettings, setBacklightSettings } = useSettingsStore(); const { setBacklightSettings } = useSettingsStore();
const handleBacklightSettingsChange = (settings: BacklightSettings) => { const handleBacklightSettingsChange = (settings: BacklightSettings) => {
// If the user has set the display to dim after it turns off, set the dim_after // If the user has set the display to dim after it turns off, set the dim_after
@ -46,7 +47,7 @@ export default function SettingsHardwareRoute() {
}; };
const handleBacklightSettingsSave = () => { const handleBacklightSettingsSave = () => {
send("setBacklightSettings", { params: backlightSettings }, (resp: JsonRpcResponse) => { send("setBacklightSettings", { params: settings.backlightSettings }, (resp: JsonRpcResponse) => {
if ("error" in resp) { if ("error" in resp) {
notifications.error( notifications.error(
`Failed to set backlight settings: ${resp.error.data || "Unknown error"}`, `Failed to set backlight settings: ${resp.error.data || "Unknown error"}`,
@ -57,21 +58,6 @@ export default function SettingsHardwareRoute() {
}); });
}; };
const handleBacklightMaxBrightnessChange = (max_brightness: number) => {
const settings = { ...backlightSettings, max_brightness };
handleBacklightSettingsChange(settings);
};
const handleBacklightDimAfterChange = (dim_after: number) => {
const settings = { ...backlightSettings, dim_after };
handleBacklightSettingsChange(settings);
};
const handleBacklightOffAfterChange = (off_after: number) => {
const settings = { ...backlightSettings, off_after };
handleBacklightSettingsChange(settings);
};
useEffect(() => { useEffect(() => {
send("getBacklightSettings", {}, (resp: JsonRpcResponse) => { send("getBacklightSettings", {}, (resp: JsonRpcResponse) => {
if ("error" in resp) { if ("error" in resp) {
@ -104,7 +90,8 @@ export default function SettingsHardwareRoute() {
{ value: "90", label: "Inverted" }, { value: "90", label: "Inverted" },
]} ]}
onChange={e => { onChange={e => {
handleDisplayRotationChange(e.target.value); settings.displayRotation = e.target.value;
handleDisplayRotationChange(settings.displayRotation);
}} }}
/> />
</SettingsItem> </SettingsItem>
@ -115,7 +102,7 @@ export default function SettingsHardwareRoute() {
<SelectMenuBasic <SelectMenuBasic
size="SM" size="SM"
label="" label=""
value={backlightSettings.max_brightness.toString()} value={settings.backlightSettings.max_brightness.toString()}
options={[ options={[
{ value: "0", label: "Off" }, { value: "0", label: "Off" },
{ value: "10", label: "Low" }, { value: "10", label: "Low" },
@ -123,11 +110,12 @@ export default function SettingsHardwareRoute() {
{ value: "64", label: "High" }, { value: "64", label: "High" },
]} ]}
onChange={e => { onChange={e => {
handleBacklightMaxBrightnessChange(parseInt(e.target.value)); settings.backlightSettings.max_brightness = parseInt(e.target.value);
handleBacklightSettingsChange(settings.backlightSettings);
}} }}
/> />
</SettingsItem> </SettingsItem>
{backlightSettings.max_brightness != 0 && ( {settings.backlightSettings.max_brightness != 0 && (
<> <>
<SettingsItem <SettingsItem
title="Dim Display After" title="Dim Display After"
@ -136,7 +124,7 @@ export default function SettingsHardwareRoute() {
<SelectMenuBasic <SelectMenuBasic
size="SM" size="SM"
label="" label=""
value={backlightSettings.dim_after.toString()} value={settings.backlightSettings.dim_after.toString()}
options={[ options={[
{ value: "0", label: "Never" }, { value: "0", label: "Never" },
{ value: "60", label: "1 Minute" }, { value: "60", label: "1 Minute" },
@ -146,7 +134,8 @@ export default function SettingsHardwareRoute() {
{ value: "3600", label: "1 Hour" }, { value: "3600", label: "1 Hour" },
]} ]}
onChange={e => { onChange={e => {
handleBacklightDimAfterChange(parseInt(e.target.value)); settings.backlightSettings.dim_after = parseInt(e.target.value);
handleBacklightSettingsChange(settings.backlightSettings);
}} }}
/> />
</SettingsItem> </SettingsItem>
@ -157,7 +146,7 @@ export default function SettingsHardwareRoute() {
<SelectMenuBasic <SelectMenuBasic
size="SM" size="SM"
label="" label=""
value={backlightSettings.off_after.toString()} value={settings.backlightSettings.off_after.toString()}
options={[ options={[
{ value: "0", label: "Never" }, { value: "0", label: "Never" },
{ value: "300", label: "5 Minutes" }, { value: "300", label: "5 Minutes" },
@ -166,7 +155,8 @@ export default function SettingsHardwareRoute() {
{ value: "3600", label: "1 Hour" }, { value: "3600", label: "1 Hour" },
]} ]}
onChange={e => { onChange={e => {
handleBacklightOffAfterChange(parseInt(e.target.value)); settings.backlightSettings.off_after = parseInt(e.target.value);
handleBacklightSettingsChange(settings.backlightSettings);
}} }}
/> />
</SettingsItem> </SettingsItem>

View File

@ -1,4 +1,4 @@
import { useCallback, useEffect, useMemo, useState } from "react"; import { useCallback, useEffect, useRef, useState } from "react";
import dayjs from "dayjs"; import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime"; import relativeTime from "dayjs/plugin/relativeTime";
import { LuEthernetPort } from "react-icons/lu"; import { LuEthernetPort } from "react-icons/lu";
@ -12,22 +12,23 @@ import {
NetworkState, NetworkState,
TimeSyncMode, TimeSyncMode,
useNetworkStateStore, useNetworkStateStore,
} from "@hooks/stores"; } from "@/hooks/stores";
import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc"; import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
import AutoHeight from "@components/AutoHeight";
import { Button } from "@components/Button"; import { Button } from "@components/Button";
import { ConfirmDialog } from "@components/ConfirmDialog";
import EmptyCard from "@components/EmptyCard";
import Fieldset from "@components/Fieldset";
import { GridCard } from "@components/Card"; import { GridCard } from "@components/Card";
import InputField, { InputFieldWithLabel } from "@components/InputField"; import InputField, { InputFieldWithLabel } from "@components/InputField";
import Ipv6NetworkCard from "@components/Ipv6NetworkCard"; import { SelectMenuBasic } from "@/components/SelectMenuBasic";
import DhcpLeaseCard from "@components/DhcpLeaseCard"; import { SettingsPageHeader } from "@/components/SettingsPageheader";
import { SelectMenuBasic } from "@components/SelectMenuBasic"; import Fieldset from "@/components/Fieldset";
import { ConfirmDialog } from "@/components/ConfirmDialog";
import { SettingsItem } from "@components/SettingsItem"; import { SettingsItem } from "@components/SettingsItem";
import { SettingsPageHeader } from "@components/SettingsPageheader";
import notifications from "@/notifications"; import notifications from "@/notifications";
import Ipv6NetworkCard from "../components/Ipv6NetworkCard";
import EmptyCard from "../components/EmptyCard";
import AutoHeight from "../components/AutoHeight";
import DhcpLeaseCard from "../components/DhcpLeaseCard";
dayjs.extend(relativeTime); dayjs.extend(relativeTime);
const defaultNetworkSettings: NetworkSettings = { const defaultNetworkSettings: NetworkSettings = {
@ -45,18 +46,14 @@ const defaultNetworkSettings: NetworkSettings = {
export function LifeTimeLabel({ lifetime }: { lifetime: string }) { export function LifeTimeLabel({ lifetime }: { lifetime: string }) {
const [remaining, setRemaining] = useState<string | null>(null); const [remaining, setRemaining] = useState<string | null>(null);
const updateRemaining = useCallback(() => {
setRemaining(dayjs(lifetime).fromNow());
}, [lifetime]);
useEffect(() => { useEffect(() => {
setTimeout(() => updateRemaining(), 0); setRemaining(dayjs(lifetime).fromNow());
const interval = setInterval(() => { const interval = setInterval(() => {
updateRemaining(); setRemaining(dayjs(lifetime).fromNow());
}, 1000 * 30); }, 1000 * 30);
return () => clearInterval(interval); return () => clearInterval(interval);
}, [updateRemaining]); }, [lifetime]);
if (lifetime == "") { if (lifetime == "") {
return <strong>N/A</strong>; return <strong>N/A</strong>;
@ -84,19 +81,24 @@ export default function SettingsNetworkRoute() {
useState<NetworkSettings>(defaultNetworkSettings); useState<NetworkSettings>(defaultNetworkSettings);
// We use this to determine whether the settings have changed // We use this to determine whether the settings have changed
const [firstNetworkSettings, setFirstNetworkSettings] = useState<NetworkSettings | undefined>(undefined); const firstNetworkSettings = useRef<NetworkSettings | undefined>(undefined);
const [networkSettingsLoaded, setNetworkSettingsLoaded] = useState(false); const [networkSettingsLoaded, setNetworkSettingsLoaded] = useState(false);
const selectedDomainOption = useMemo(() => { const [customDomain, setCustomDomain] = useState<string>("");
if (!networkSettingsLoaded) return "dhcp"; const [selectedDomainOption, setSelectedDomainOption] = useState<string>("dhcp");
const predefinedOptions = ["dhcp", "local"];
return predefinedOptions.includes(networkSettings.domain) ? networkSettings.domain : "custom";
}, [networkSettings.domain, networkSettingsLoaded]);
const customDomain = useMemo(() => { useEffect(() => {
if (!networkSettingsLoaded) return ""; if (networkSettings.domain && networkSettingsLoaded) {
const predefinedOptions = ["dhcp", "local"]; // Check if the domain is one of the predefined options
return predefinedOptions.includes(networkSettings.domain) ? "" : networkSettings.domain; const predefinedOptions = ["dhcp", "local"];
if (predefinedOptions.includes(networkSettings.domain)) {
setSelectedDomainOption(networkSettings.domain);
} else {
setSelectedDomainOption("custom");
setCustomDomain(networkSettings.domain);
}
}
}, [networkSettings.domain, networkSettingsLoaded]); }, [networkSettings.domain, networkSettingsLoaded]);
const getNetworkSettings = useCallback(() => { const getNetworkSettings = useCallback(() => {
@ -107,12 +109,12 @@ export default function SettingsNetworkRoute() {
console.debug("Network settings: ", networkSettings); console.debug("Network settings: ", networkSettings);
setNetworkSettings(networkSettings); setNetworkSettings(networkSettings);
if (!firstNetworkSettings) { if (!firstNetworkSettings.current) {
setFirstNetworkSettings(networkSettings); firstNetworkSettings.current = networkSettings;
} }
setNetworkSettingsLoaded(true); setNetworkSettingsLoaded(true);
}); });
}, [send, firstNetworkSettings]); }, [send]);
const getNetworkState = useCallback(() => { const getNetworkState = useCallback(() => {
send("getNetworkState", {}, (resp: JsonRpcResponse) => { send("getNetworkState", {}, (resp: JsonRpcResponse) => {
@ -130,13 +132,14 @@ export default function SettingsNetworkRoute() {
if ("error" in resp) { if ("error" in resp) {
notifications.error( notifications.error(
"Failed to save network settings: " + "Failed to save network settings: " +
(resp.error.data ? resp.error.data : resp.error.message), (resp.error.data ? resp.error.data : resp.error.message),
); );
setNetworkSettingsLoaded(true); setNetworkSettingsLoaded(true);
return; return;
} }
const networkSettings = resp.result as NetworkSettings; const networkSettings = resp.result as NetworkSettings;
setFirstNetworkSettings(networkSettings); // We need to update the firstNetworkSettings ref to the new settings so we can use it to determine if the settings have changed
firstNetworkSettings.current = networkSettings;
setNetworkSettings(networkSettings); setNetworkSettings(networkSettings);
getNetworkState(); getNetworkState();
setNetworkSettingsLoaded(true); setNetworkSettingsLoaded(true);
@ -157,10 +160,8 @@ export default function SettingsNetworkRoute() {
}, [send]); }, [send]);
useEffect(() => { useEffect(() => {
setTimeout(() => { getNetworkState();
getNetworkState(); getNetworkSettings();
getNetworkSettings();
}, 0);
}, [getNetworkState, getNetworkSettings]); }, [getNetworkState, getNetworkSettings]);
const handleIpv4ModeChange = (value: IPv4Mode | string) => { const handleIpv4ModeChange = (value: IPv4Mode | string) => {
@ -196,12 +197,14 @@ export default function SettingsNetworkRoute() {
}; };
const handleDomainOptionChange = (value: string) => { const handleDomainOptionChange = (value: string) => {
setSelectedDomainOption(value);
if (value !== "custom") { if (value !== "custom") {
handleDomainChange(value); handleDomainChange(value);
} }
}; };
const handleCustomDomainChange = (value: string) => { const handleCustomDomainChange = (value: string) => {
setCustomDomain(value);
handleDomainChange(value); handleDomainChange(value);
}; };
@ -306,6 +309,7 @@ export default function SettingsNetworkRoute() {
placeholder="home" placeholder="home"
value={customDomain} value={customDomain}
onChange={e => { onChange={e => {
setCustomDomain(e.target.value);
handleCustomDomainChange(e.target.value); handleCustomDomainChange(e.target.value);
}} }}
/> />
@ -357,7 +361,7 @@ export default function SettingsNetworkRoute() {
<Button <Button
size="SM" size="SM"
theme="primary" theme="primary"
disabled={firstNetworkSettings === networkSettings} disabled={firstNetworkSettings.current === networkSettings}
text="Save Settings" text="Save Settings"
onClick={() => setNetworkSettingsRemote(networkSettings)} onClick={() => setNetworkSettingsRemote(networkSettings)}
/> />
@ -425,7 +429,7 @@ export default function SettingsNetworkRoute() {
</SettingsItem> </SettingsItem>
<AutoHeight> <AutoHeight>
{!networkSettingsLoaded && {!networkSettingsLoaded &&
!(networkState?.ipv6_addresses && networkState.ipv6_addresses.length > 0) ? ( !(networkState?.ipv6_addresses && networkState.ipv6_addresses.length > 0) ? (
<GridCard> <GridCard>
<div className="p-4"> <div className="p-4">
<div className="space-y-4"> <div className="space-y-4">

View File

@ -50,7 +50,7 @@ export default function SettingsVideoRoute() {
const [streamQuality, setStreamQuality] = useState("1"); const [streamQuality, setStreamQuality] = useState("1");
const [customEdidValue, setCustomEdidValue] = useState<string | null>(null); const [customEdidValue, setCustomEdidValue] = useState<string | null>(null);
const [edid, setEdid] = useState<string | null>(null); const [edid, setEdid] = useState<string | null>(null);
const [edidLoading, setEdidLoading] = useState(true); const [edidLoading, setEdidLoading] = useState(false);
const { debugMode } = useSettingsStore(); const { debugMode } = useSettingsStore();
// Video enhancement settings from store // Video enhancement settings from store
const { const {
@ -63,6 +63,7 @@ export default function SettingsVideoRoute() {
} = useSettingsStore(); } = useSettingsStore();
useEffect(() => { useEffect(() => {
setEdidLoading(true);
send("getStreamQualityFactor", {}, (resp: JsonRpcResponse) => { send("getStreamQualityFactor", {}, (resp: JsonRpcResponse) => {
if ("error" in resp) return; if ("error" in resp) return;
setStreamQuality(String(resp.result)); setStreamQuality(String(resp.result));