mirror of https://github.com/jetkvm/kvm.git
Compare commits
8 Commits
853b72a289
...
5a2d948192
| Author | SHA1 | Date |
|---|---|---|
|
|
5a2d948192 | |
|
|
0dcf56ef18 | |
|
|
0eb577b6f7 | |
|
|
5613555b39 | |
|
|
a40c27269a | |
|
|
c6cb2e9cb6 | |
|
|
567a6d5cbc | |
|
|
cc9ff74276 |
|
|
@ -104,6 +104,7 @@ type Config struct {
|
|||
UsbDevices *usbgadget.Devices `json:"usb_devices"`
|
||||
NetworkConfig *network.NetworkConfig `json:"network_config"`
|
||||
DefaultLogLevel string `json:"default_log_level"`
|
||||
VideoSleepAfterSec int `json:"video_sleep_after_sec"`
|
||||
}
|
||||
|
||||
func (c *Config) GetDisplayRotation() uint16 {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ type Native struct {
|
|||
onVideoFrameReceived func(frame []byte, duration time.Duration)
|
||||
onIndevEvent func(event string)
|
||||
onRpcEvent func(event string)
|
||||
sleepModeSupported bool
|
||||
videoLock sync.Mutex
|
||||
screenLock sync.Mutex
|
||||
}
|
||||
|
|
@ -62,6 +63,8 @@ func NewNative(opts NativeOptions) *Native {
|
|||
}
|
||||
}
|
||||
|
||||
sleepModeSupported := isSleepModeSupported()
|
||||
|
||||
return &Native{
|
||||
ready: make(chan struct{}),
|
||||
l: nativeLogger,
|
||||
|
|
@ -73,6 +76,7 @@ func NewNative(opts NativeOptions) *Native {
|
|||
onVideoFrameReceived: onVideoFrameReceived,
|
||||
onIndevEvent: onIndevEvent,
|
||||
onRpcEvent: onRpcEvent,
|
||||
sleepModeSupported: sleepModeSupported,
|
||||
videoLock: sync.Mutex{},
|
||||
screenLock: sync.Mutex{},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,12 @@
|
|||
package native
|
||||
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
const sleepModeFile = "/sys/devices/platform/ff470000.i2c/i2c-4/4-000f/sleep_mode"
|
||||
|
||||
// VideoState is the state of the video stream.
|
||||
type VideoState struct {
|
||||
Ready bool `json:"ready"`
|
||||
Error string `json:"error,omitempty"` //no_signal, no_lock, out_of_range
|
||||
|
|
@ -8,6 +15,58 @@ type VideoState struct {
|
|||
FramePerSecond float64 `json:"fps"`
|
||||
}
|
||||
|
||||
func isSleepModeSupported() bool {
|
||||
_, err := os.Stat(sleepModeFile)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (n *Native) setSleepMode(enabled bool) error {
|
||||
if !n.sleepModeSupported {
|
||||
return nil
|
||||
}
|
||||
|
||||
bEnabled := "0"
|
||||
if enabled {
|
||||
bEnabled = "1"
|
||||
}
|
||||
return os.WriteFile(sleepModeFile, []byte(bEnabled), 0644)
|
||||
}
|
||||
|
||||
func (n *Native) getSleepMode() (bool, error) {
|
||||
if !n.sleepModeSupported {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(sleepModeFile)
|
||||
if err == nil {
|
||||
return string(data) == "1", nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// VideoSetSleepMode sets the sleep mode for the video stream.
|
||||
func (n *Native) VideoSetSleepMode(enabled bool) error {
|
||||
n.videoLock.Lock()
|
||||
defer n.videoLock.Unlock()
|
||||
|
||||
return n.setSleepMode(enabled)
|
||||
}
|
||||
|
||||
// VideoGetSleepMode gets the sleep mode for the video stream.
|
||||
func (n *Native) VideoGetSleepMode() (bool, error) {
|
||||
n.videoLock.Lock()
|
||||
defer n.videoLock.Unlock()
|
||||
|
||||
return n.getSleepMode()
|
||||
}
|
||||
|
||||
// VideoSleepModeSupported checks if the sleep mode is supported.
|
||||
func (n *Native) VideoSleepModeSupported() bool {
|
||||
return n.sleepModeSupported
|
||||
}
|
||||
|
||||
// VideoSetQualityFactor sets the quality factor for the video stream.
|
||||
func (n *Native) VideoSetQualityFactor(factor float64) error {
|
||||
n.videoLock.Lock()
|
||||
defer n.videoLock.Unlock()
|
||||
|
|
@ -15,6 +74,7 @@ func (n *Native) VideoSetQualityFactor(factor float64) error {
|
|||
return videoSetStreamQualityFactor(factor)
|
||||
}
|
||||
|
||||
// VideoGetQualityFactor gets the quality factor for the video stream.
|
||||
func (n *Native) VideoGetQualityFactor() (float64, error) {
|
||||
n.videoLock.Lock()
|
||||
defer n.videoLock.Unlock()
|
||||
|
|
@ -22,6 +82,7 @@ func (n *Native) VideoGetQualityFactor() (float64, error) {
|
|||
return videoGetStreamQualityFactor()
|
||||
}
|
||||
|
||||
// VideoSetEDID sets the EDID for the video stream.
|
||||
func (n *Native) VideoSetEDID(edid string) error {
|
||||
n.videoLock.Lock()
|
||||
defer n.videoLock.Unlock()
|
||||
|
|
@ -29,6 +90,7 @@ func (n *Native) VideoSetEDID(edid string) error {
|
|||
return videoSetEDID(edid)
|
||||
}
|
||||
|
||||
// VideoGetEDID gets the EDID for the video stream.
|
||||
func (n *Native) VideoGetEDID() (string, error) {
|
||||
n.videoLock.Lock()
|
||||
defer n.videoLock.Unlock()
|
||||
|
|
@ -36,6 +98,7 @@ func (n *Native) VideoGetEDID() (string, error) {
|
|||
return videoGetEDID()
|
||||
}
|
||||
|
||||
// VideoLogStatus gets the log status for the video stream.
|
||||
func (n *Native) VideoLogStatus() (string, error) {
|
||||
n.videoLock.Lock()
|
||||
defer n.videoLock.Unlock()
|
||||
|
|
@ -43,6 +106,7 @@ func (n *Native) VideoLogStatus() (string, error) {
|
|||
return videoLogStatus(), nil
|
||||
}
|
||||
|
||||
// VideoStop stops the video stream.
|
||||
func (n *Native) VideoStop() error {
|
||||
n.videoLock.Lock()
|
||||
defer n.videoLock.Unlock()
|
||||
|
|
@ -51,10 +115,14 @@ func (n *Native) VideoStop() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// VideoStart starts the video stream.
|
||||
func (n *Native) VideoStart() error {
|
||||
n.videoLock.Lock()
|
||||
defer n.videoLock.Unlock()
|
||||
|
||||
// disable sleep mode before starting video
|
||||
_ = n.setSleepMode(false)
|
||||
|
||||
videoStart()
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1215,6 +1215,8 @@ var rpcHandlers = map[string]RPCHandler{
|
|||
"getEDID": {Func: rpcGetEDID},
|
||||
"setEDID": {Func: rpcSetEDID, Params: []string{"edid"}},
|
||||
"getVideoLogStatus": {Func: rpcGetVideoLogStatus},
|
||||
"getVideoSleepMode": {Func: rpcGetVideoSleepMode},
|
||||
"setVideoSleepMode": {Func: rpcSetVideoSleepMode, Params: []string{"duration"}},
|
||||
"getDevChannelState": {Func: rpcGetDevChannelState},
|
||||
"setDevChannelState": {Func: rpcSetDevChannelState, Params: []string{"enabled"}},
|
||||
"getLocalVersion": {Func: rpcGetLocalVersion},
|
||||
|
|
|
|||
3
main.go
3
main.go
|
|
@ -77,6 +77,9 @@ func Main() {
|
|||
// initialize display
|
||||
initDisplay()
|
||||
|
||||
// start video sleep mode timer
|
||||
startVideoSleepModeTicker()
|
||||
|
||||
go func() {
|
||||
time.Sleep(15 * time.Minute)
|
||||
for {
|
||||
|
|
|
|||
|
|
@ -457,5 +457,139 @@
|
|||
"wake_on_lan_magic_sent_success": "Magic Packet sent successfully",
|
||||
"wake_on_lan": "Wake On LAN",
|
||||
"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"
|
||||
}
|
||||
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "kvm-ui",
|
||||
"version": "2025.10.10.2300",
|
||||
"version": "2025.10.13.2055",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "kvm-ui",
|
||||
"version": "2025.10.10.2300",
|
||||
"version": "2025.10.13.2055",
|
||||
"dependencies": {
|
||||
"@headlessui/react": "^2.2.9",
|
||||
"@headlessui/tailwindcss": "^0.2.2",
|
||||
|
|
@ -31,7 +31,7 @@
|
|||
"react-hot-toast": "^2.6.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-router": "^7.9.4",
|
||||
"react-simple-keyboard": "^3.8.129",
|
||||
"react-simple-keyboard": "^3.8.130",
|
||||
"react-use-websocket": "^4.13.0",
|
||||
"react-xtermjs": "^1.0.10",
|
||||
"recharts": "^3.2.1",
|
||||
|
|
@ -54,11 +54,11 @@
|
|||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@tailwindcss/vite": "^4.1.14",
|
||||
"@types/react": "^19.2.2",
|
||||
"@types/react-dom": "^19.2.1",
|
||||
"@types/react-dom": "^19.2.2",
|
||||
"@types/semver": "^7.7.1",
|
||||
"@types/validator": "^13.15.3",
|
||||
"@typescript-eslint/eslint-plugin": "^8.46.0",
|
||||
"@typescript-eslint/parser": "^8.46.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.46.1",
|
||||
"@typescript-eslint/parser": "^8.46.1",
|
||||
"@vitejs/plugin-react-swc": "^4.1.0",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"eslint": "^9.37.0",
|
||||
|
|
@ -2408,9 +2408,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@types/react-dom": {
|
||||
"version": "19.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.1.tgz",
|
||||
"integrity": "sha512-/EEvYBdT3BflCWvTMO7YkYBHVE9Ci6XdqZciZANQgKpaiDRGOLIlRo91jbTNRQjgPFWVaRxcYc0luVNFitz57A==",
|
||||
"version": "19.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.2.tgz",
|
||||
"integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "^19.2.0"
|
||||
|
|
@ -2437,17 +2437,17 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.46.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.0.tgz",
|
||||
"integrity": "sha512-hA8gxBq4ukonVXPy0OKhiaUh/68D0E88GSmtC1iAEnGaieuDi38LhS7jdCHRLi6ErJBNDGCzvh5EnzdPwUc0DA==",
|
||||
"version": "8.46.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.1.tgz",
|
||||
"integrity": "sha512-rUsLh8PXmBjdiPY+Emjz9NX2yHvhS11v0SR6xNJkm5GM1MO9ea/1GoDKlHHZGrOJclL/cZ2i/vRUYVtjRhrHVQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.10.0",
|
||||
"@typescript-eslint/scope-manager": "8.46.0",
|
||||
"@typescript-eslint/type-utils": "8.46.0",
|
||||
"@typescript-eslint/utils": "8.46.0",
|
||||
"@typescript-eslint/visitor-keys": "8.46.0",
|
||||
"@typescript-eslint/scope-manager": "8.46.1",
|
||||
"@typescript-eslint/type-utils": "8.46.1",
|
||||
"@typescript-eslint/utils": "8.46.1",
|
||||
"@typescript-eslint/visitor-keys": "8.46.1",
|
||||
"graphemer": "^1.4.0",
|
||||
"ignore": "^7.0.0",
|
||||
"natural-compare": "^1.4.0",
|
||||
|
|
@ -2461,7 +2461,7 @@
|
|||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^8.46.0",
|
||||
"@typescript-eslint/parser": "^8.46.1",
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
|
|
@ -2477,16 +2477,16 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.46.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.0.tgz",
|
||||
"integrity": "sha512-n1H6IcDhmmUEG7TNVSspGmiHHutt7iVKtZwRppD7e04wha5MrkV1h3pti9xQLcCMt6YWsncpoT0HMjkH1FNwWQ==",
|
||||
"version": "8.46.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.1.tgz",
|
||||
"integrity": "sha512-6JSSaBZmsKvEkbRUkf7Zj7dru/8ZCrJxAqArcLaVMee5907JdtEbKGsZ7zNiIm/UAkpGUkaSMZEXShnN2D1HZA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.46.0",
|
||||
"@typescript-eslint/types": "8.46.0",
|
||||
"@typescript-eslint/typescript-estree": "8.46.0",
|
||||
"@typescript-eslint/visitor-keys": "8.46.0",
|
||||
"@typescript-eslint/scope-manager": "8.46.1",
|
||||
"@typescript-eslint/types": "8.46.1",
|
||||
"@typescript-eslint/typescript-estree": "8.46.1",
|
||||
"@typescript-eslint/visitor-keys": "8.46.1",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
|
|
@ -2502,14 +2502,14 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.46.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.0.tgz",
|
||||
"integrity": "sha512-OEhec0mH+U5Je2NZOeK1AbVCdm0ChyapAyTeXVIYTPXDJ3F07+cu87PPXcGoYqZ7M9YJVvFnfpGg1UmCIqM+QQ==",
|
||||
"version": "8.46.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.1.tgz",
|
||||
"integrity": "sha512-FOIaFVMHzRskXr5J4Jp8lFVV0gz5ngv3RHmn+E4HYxSJ3DgDzU7fVI1/M7Ijh1zf6S7HIoaIOtln1H5y8V+9Zg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.46.0",
|
||||
"@typescript-eslint/types": "^8.46.0",
|
||||
"@typescript-eslint/tsconfig-utils": "^8.46.1",
|
||||
"@typescript-eslint/types": "^8.46.1",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
|
|
@ -2524,14 +2524,14 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.46.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.0.tgz",
|
||||
"integrity": "sha512-lWETPa9XGcBes4jqAMYD9fW0j4n6hrPtTJwWDmtqgFO/4HF4jmdH/Q6wggTw5qIT5TXjKzbt7GsZUBnWoO3dqw==",
|
||||
"version": "8.46.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.1.tgz",
|
||||
"integrity": "sha512-weL9Gg3/5F0pVQKiF8eOXFZp8emqWzZsOJuWRUNtHT+UNV2xSJegmpCNQHy37aEQIbToTq7RHKhWvOsmbM680A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.46.0",
|
||||
"@typescript-eslint/visitor-keys": "8.46.0"
|
||||
"@typescript-eslint/types": "8.46.1",
|
||||
"@typescript-eslint/visitor-keys": "8.46.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
|
|
@ -2542,9 +2542,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.46.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.0.tgz",
|
||||
"integrity": "sha512-WrYXKGAHY836/N7zoK/kzi6p8tXFhasHh8ocFL9VZSAkvH956gfeRfcnhs3xzRy8qQ/dq3q44v1jvQieMFg2cw==",
|
||||
"version": "8.46.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.1.tgz",
|
||||
"integrity": "sha512-X88+J/CwFvlJB+mK09VFqx5FE4H5cXD+H/Bdza2aEWkSb8hnWIQorNcscRl4IEo1Cz9VI/+/r/jnGWkbWPx54g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
|
@ -2559,15 +2559,15 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.46.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.0.tgz",
|
||||
"integrity": "sha512-hy+lvYV1lZpVs2jRaEYvgCblZxUoJiPyCemwbQZ+NGulWkQRy0HRPYAoef/CNSzaLt+MLvMptZsHXHlkEilaeg==",
|
||||
"version": "8.46.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.1.tgz",
|
||||
"integrity": "sha512-+BlmiHIiqufBxkVnOtFwjah/vrkF4MtKKvpXrKSPLCkCtAp8H01/VV43sfqA98Od7nJpDcFnkwgyfQbOG0AMvw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.46.0",
|
||||
"@typescript-eslint/typescript-estree": "8.46.0",
|
||||
"@typescript-eslint/utils": "8.46.0",
|
||||
"@typescript-eslint/types": "8.46.1",
|
||||
"@typescript-eslint/typescript-estree": "8.46.1",
|
||||
"@typescript-eslint/utils": "8.46.1",
|
||||
"debug": "^4.3.4",
|
||||
"ts-api-utils": "^2.1.0"
|
||||
},
|
||||
|
|
@ -2584,9 +2584,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.46.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.0.tgz",
|
||||
"integrity": "sha512-bHGGJyVjSE4dJJIO5yyEWt/cHyNwga/zXGJbJJ8TiO01aVREK6gCTu3L+5wrkb1FbDkQ+TKjMNe9R/QQQP9+rA==",
|
||||
"version": "8.46.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.1.tgz",
|
||||
"integrity": "sha512-C+soprGBHwWBdkDpbaRC4paGBrkIXxVlNohadL5o0kfhsXqOC6GYH2S/Obmig+I0HTDl8wMaRySwrfrXVP8/pQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
|
@ -2598,16 +2598,16 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.46.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.0.tgz",
|
||||
"integrity": "sha512-ekDCUfVpAKWJbRfm8T1YRrCot1KFxZn21oV76v5Fj4tr7ELyk84OS+ouvYdcDAwZL89WpEkEj2DKQ+qg//+ucg==",
|
||||
"version": "8.46.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.1.tgz",
|
||||
"integrity": "sha512-uIifjT4s8cQKFQ8ZBXXyoUODtRoAd7F7+G8MKmtzj17+1UbdzFl52AzRyZRyKqPHhgzvXunnSckVu36flGy8cg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.46.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.46.0",
|
||||
"@typescript-eslint/types": "8.46.0",
|
||||
"@typescript-eslint/visitor-keys": "8.46.0",
|
||||
"@typescript-eslint/project-service": "8.46.1",
|
||||
"@typescript-eslint/tsconfig-utils": "8.46.1",
|
||||
"@typescript-eslint/types": "8.46.1",
|
||||
"@typescript-eslint/visitor-keys": "8.46.1",
|
||||
"debug": "^4.3.4",
|
||||
"fast-glob": "^3.3.2",
|
||||
"is-glob": "^4.0.3",
|
||||
|
|
@ -2653,16 +2653,16 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.46.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.0.tgz",
|
||||
"integrity": "sha512-nD6yGWPj1xiOm4Gk0k6hLSZz2XkNXhuYmyIrOWcHoPuAhjT9i5bAG+xbWPgFeNR8HPHHtpNKdYUXJl/D3x7f5g==",
|
||||
"version": "8.46.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.1.tgz",
|
||||
"integrity": "sha512-vkYUy6LdZS7q1v/Gxb2Zs7zziuXN0wxqsetJdeZdRe/f5dwJFglmuvZBfTUivCtjH725C1jWCDfpadadD95EDQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.7.0",
|
||||
"@typescript-eslint/scope-manager": "8.46.0",
|
||||
"@typescript-eslint/types": "8.46.0",
|
||||
"@typescript-eslint/typescript-estree": "8.46.0"
|
||||
"@typescript-eslint/scope-manager": "8.46.1",
|
||||
"@typescript-eslint/types": "8.46.1",
|
||||
"@typescript-eslint/typescript-estree": "8.46.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
|
|
@ -2677,13 +2677,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.46.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.0.tgz",
|
||||
"integrity": "sha512-FrvMpAK+hTbFy7vH5j1+tMYHMSKLE6RzluFJlkFNKD0p9YsUT75JlBSmr5so3QRzvMwU5/bIEdeNrxm8du8l3Q==",
|
||||
"version": "8.46.1",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.1.tgz",
|
||||
"integrity": "sha512-ptkmIf2iDkNUjdeu2bQqhFPV1m6qTnFFjg7PPDjxKWaMaP0Z6I9l30Jr3g5QqbZGdw8YdYvLp+XnqnWWZOg/NA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.46.0",
|
||||
"@typescript-eslint/types": "8.46.1",
|
||||
"eslint-visitor-keys": "^4.2.1"
|
||||
},
|
||||
"engines": {
|
||||
|
|
@ -6564,9 +6564,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/react-simple-keyboard": {
|
||||
"version": "3.8.129",
|
||||
"resolved": "https://registry.npmjs.org/react-simple-keyboard/-/react-simple-keyboard-3.8.129.tgz",
|
||||
"integrity": "sha512-dvZ+LjOAVkFFay8wZsg//VIMKqfr7tCp28scyFgidAufGjJ60yqWUdckTI1xue827DNb/rbiRuQm5B+3GjcEFQ==",
|
||||
"version": "3.8.130",
|
||||
"resolved": "https://registry.npmjs.org/react-simple-keyboard/-/react-simple-keyboard-3.8.130.tgz",
|
||||
"integrity": "sha512-sq51zg3fe4NPCRyDLYyAtot8+pIn9DmC+YqAEqx5FOIpHUC86Qvv2/0F2KvhLNDvgZ+5s4w649YKf1gWK8LiIQ==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "kvm-ui",
|
||||
"private": true,
|
||||
"version": "2025.10.10.2300",
|
||||
"version": "2025.10.13.2055",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": "^22.15.0"
|
||||
|
|
@ -44,7 +44,7 @@
|
|||
"react-hot-toast": "^2.6.0",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-router": "^7.9.4",
|
||||
"react-simple-keyboard": "^3.8.129",
|
||||
"react-simple-keyboard": "^3.8.130",
|
||||
"react-use-websocket": "^4.13.0",
|
||||
"react-xtermjs": "^1.0.10",
|
||||
"recharts": "^3.2.1",
|
||||
|
|
@ -67,11 +67,11 @@
|
|||
"@tailwindcss/typography": "^0.5.19",
|
||||
"@tailwindcss/vite": "^4.1.14",
|
||||
"@types/react": "^19.2.2",
|
||||
"@types/react-dom": "^19.2.1",
|
||||
"@types/react-dom": "^19.2.2",
|
||||
"@types/semver": "^7.7.1",
|
||||
"@types/validator": "^13.15.3",
|
||||
"@typescript-eslint/eslint-plugin": "^8.46.0",
|
||||
"@typescript-eslint/parser": "^8.46.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.46.1",
|
||||
"@typescript-eslint/parser": "^8.46.1",
|
||||
"@vitejs/plugin-react-swc": "^4.1.0",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"eslint": "^9.37.0",
|
||||
|
|
|
|||
|
|
@ -352,6 +352,7 @@ function UrlView({
|
|||
}) {
|
||||
const [usbMode, setUsbMode] = useState<RemoteVirtualMediaState["mode"]>("CDROM");
|
||||
const [url, setUrl] = useState<string>("");
|
||||
const [isUrlValid, setIsUrlValid] = useState(false);
|
||||
|
||||
const popularImages = [
|
||||
{
|
||||
|
|
@ -399,6 +400,12 @@ function UrlView({
|
|||
|
||||
const urlRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (urlRef.current) {
|
||||
setIsUrlValid(urlRef.current.validity.valid);
|
||||
}
|
||||
}, [url]);
|
||||
|
||||
function handleUrlChange(url: string) {
|
||||
setUrl(url);
|
||||
if (url.endsWith(".iso")) {
|
||||
|
|
@ -437,7 +444,7 @@ function UrlView({
|
|||
animationDelay: "0.1s",
|
||||
}}
|
||||
>
|
||||
<Fieldset disabled={!urlRef.current?.validity.valid || url.length === 0}>
|
||||
<Fieldset disabled={!isUrlValid || url.length === 0}>
|
||||
<UsbModeSelector usbMode={usbMode} setUsbMode={setUsbMode} />
|
||||
</Fieldset>
|
||||
<div className="flex space-x-2">
|
||||
|
|
@ -449,7 +456,7 @@ function UrlView({
|
|||
text={m.mount_button_mount_url()}
|
||||
onClick={() => onMount(url, usbMode)}
|
||||
disabled={
|
||||
mountInProgress || !urlRef.current?.validity.valid || url.length === 0
|
||||
mountInProgress || !isUrlValid || url.length === 0
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { redirect } 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) => {
|
||||
return redirect(getDeviceUiPath("/settings/general", params.id));
|
||||
|
|
|
|||
|
|
@ -1,22 +1,22 @@
|
|||
import { useLoaderData, useNavigate } from "react-router";
|
||||
import type { LoaderFunction } from "react-router";
|
||||
import { ShieldCheckIcon } from "@heroicons/react/24/outline";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { useLoaderData, useNavigate, type LoaderFunction } from "react-router";
|
||||
import { ShieldCheckIcon } from "@heroicons/react/24/outline";
|
||||
|
||||
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 { 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 { SettingsSectionHeader } from "@/components/SettingsSectionHeader";
|
||||
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
|
||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||
import { SettingsSectionHeader } from "@components/SettingsSectionHeader";
|
||||
import { TextAreaWithLabel } from "@components/TextArea";
|
||||
import api from "@/api";
|
||||
import notifications from "@/notifications";
|
||||
import { DEVICE_API } from "@/ui.config";
|
||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
import { isOnDevice } from "@/main";
|
||||
import { TextAreaWithLabel } from "@components/TextArea";
|
||||
import { m } from "@localizations/messages.js";
|
||||
|
||||
import { LocalDevice } from "./devices.$id";
|
||||
import { CloudState } from "./adopt";
|
||||
|
|
@ -92,7 +92,7 @@ export default function SettingsAccessIndexRoute() {
|
|||
send("deregisterDevice", {}, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to de-register device: ${resp.error.data || "Unknown error"}`,
|
||||
m.access_failed_deregister({ error: resp.error.data || "Unknown error" }),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -107,14 +107,14 @@ export default function SettingsAccessIndexRoute() {
|
|||
const onCloudAdoptClick = useCallback(
|
||||
(cloudApiUrl: string, cloudAppUrl: string) => {
|
||||
if (!deviceId) {
|
||||
notifications.error("No device ID available");
|
||||
notifications.error(m.access_no_device_id());
|
||||
return;
|
||||
}
|
||||
|
||||
send("setCloudUrl", { apiUrl: cloudApiUrl, appUrl: cloudAppUrl }, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to update cloud URL: ${resp.error.data || "Unknown error"}`,
|
||||
m.access_failed_update_cloud_url({ error: resp.error.data || "Unknown error" }),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -160,12 +160,12 @@ export default function SettingsAccessIndexRoute() {
|
|||
send("setTLSState", { state }, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to update TLS settings: ${resp.error.data || "Unknown error"}`,
|
||||
m.access_failed_update_tls({ error: resp.error.data || "Unknown error" }),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
notifications.success("TLS settings updated successfully");
|
||||
notifications.success(m.access_tls_updated());
|
||||
});
|
||||
}, [send]);
|
||||
|
||||
|
|
@ -206,22 +206,22 @@ export default function SettingsAccessIndexRoute() {
|
|||
return (
|
||||
<div className="space-y-4">
|
||||
<SettingsPageHeader
|
||||
title="Access"
|
||||
description="Manage the Access Control of the device"
|
||||
title={m.access_title()}
|
||||
description={m.access_description()}
|
||||
/>
|
||||
|
||||
{loaderData?.authMode && (
|
||||
<>
|
||||
<div className="space-y-4">
|
||||
<SettingsSectionHeader
|
||||
title="Local"
|
||||
description="Manage the mode of local access to the device"
|
||||
title={m.access_local_title()}
|
||||
description={m.access_local_description()}
|
||||
/>
|
||||
<>
|
||||
<SettingsItem
|
||||
title="HTTPS Mode"
|
||||
title={m.access_https_mode_title()}
|
||||
badge="Experimental"
|
||||
description="Configure secure HTTPS access to your device"
|
||||
description={m.access_https_description()}
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
|
|
@ -229,9 +229,9 @@ export default function SettingsAccessIndexRoute() {
|
|||
onChange={e => handleTlsModeChange(e.target.value)}
|
||||
disabled={tlsMode === "unknown"}
|
||||
options={[
|
||||
{ value: "disabled", label: "Disabled" },
|
||||
{ value: "self-signed", label: "Self-signed" },
|
||||
{ value: "custom", label: "Custom" },
|
||||
{ value: "disabled", label: m.access_tls_disabled() },
|
||||
{ value: "self-signed", label: m.access_tls_self_signed() },
|
||||
{ value: "custom", label: m.access_tls_custom() },
|
||||
]}
|
||||
/>
|
||||
</SettingsItem>
|
||||
|
|
@ -240,12 +240,12 @@ export default function SettingsAccessIndexRoute() {
|
|||
<div className="mt-4 space-y-4">
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title="TLS Certificate"
|
||||
description="Paste your TLS certificate below. For certificate chains, include the entire chain (leaf, intermediate, and root certificates)."
|
||||
title={m.access_tls_certificate_title()}
|
||||
description={m.access_tls_certificate_description()}
|
||||
/>
|
||||
<div className="space-y-4">
|
||||
<TextAreaWithLabel
|
||||
label="Certificate"
|
||||
label={m.access_certificate_label()}
|
||||
rows={3}
|
||||
placeholder={
|
||||
"-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----"
|
||||
|
|
@ -258,8 +258,8 @@ export default function SettingsAccessIndexRoute() {
|
|||
<div className="space-y-4">
|
||||
<div className="space-y-4">
|
||||
<TextAreaWithLabel
|
||||
label="Private Key"
|
||||
description="For security reasons, it will not be displayed after saving."
|
||||
label={m.access_private_key_label()}
|
||||
description={m.access_private_key_description()}
|
||||
rows={3}
|
||||
placeholder={
|
||||
"-----BEGIN PRIVATE KEY-----\n...\n-----END PRIVATE KEY-----"
|
||||
|
|
@ -274,7 +274,7 @@ export default function SettingsAccessIndexRoute() {
|
|||
<Button
|
||||
size="SM"
|
||||
theme="primary"
|
||||
text="Update TLS Settings"
|
||||
text={m.access_update_tls_settings()}
|
||||
onClick={handleCustomTlsUpdate}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -282,14 +282,14 @@ export default function SettingsAccessIndexRoute() {
|
|||
)}
|
||||
|
||||
<SettingsItem
|
||||
title="Authentication Mode"
|
||||
description={`Current mode: ${loaderData.authMode === "password" ? "Password protected" : "No password"}`}
|
||||
title={m.access_authentication_mode_title()}
|
||||
description={loaderData.authMode === "password" ? m.access_auth_mode_password() : m.access_auth_mode_no_password()}
|
||||
>
|
||||
{loaderData.authMode === "password" ? (
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text="Disable Protection"
|
||||
text={m.access_disable_protection()}
|
||||
onClick={() => {
|
||||
navigateTo("./local-auth", { state: { init: "deletePassword" } });
|
||||
}}
|
||||
|
|
@ -298,7 +298,7 @@ export default function SettingsAccessIndexRoute() {
|
|||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text="Enable Password"
|
||||
text={m.access_enable_password()}
|
||||
onClick={() => {
|
||||
navigateTo("./local-auth", { state: { init: "createPassword" } });
|
||||
}}
|
||||
|
|
@ -309,13 +309,13 @@ export default function SettingsAccessIndexRoute() {
|
|||
|
||||
{loaderData.authMode === "password" && (
|
||||
<SettingsItem
|
||||
title="Change Password"
|
||||
description="Update your device access password"
|
||||
title={m.access_change_password_title()}
|
||||
description={m.access_change_password_description()}
|
||||
>
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text="Change Password"
|
||||
text={m.access_change_password_button()}
|
||||
onClick={() => {
|
||||
navigateTo("./local-auth", { state: { init: "updatePassword" } });
|
||||
}}
|
||||
|
|
@ -330,23 +330,23 @@ export default function SettingsAccessIndexRoute() {
|
|||
<div className="space-y-4">
|
||||
<SettingsSectionHeader
|
||||
title="Remote"
|
||||
description="Manage the mode of Remote access to the device"
|
||||
description={m.access_remote_description()}
|
||||
/>
|
||||
|
||||
<div className="space-y-4">
|
||||
{!isAdopted && (
|
||||
<>
|
||||
<SettingsItem
|
||||
title="Cloud Provider"
|
||||
description="Select the cloud provider for your device"
|
||||
title={m.access_cloud_provider_title()}
|
||||
description={m.access_cloud_provider_description()}
|
||||
>
|
||||
<SelectMenuBasic
|
||||
size="SM"
|
||||
value={selectedProvider}
|
||||
onChange={e => handleProviderChange(e.target.value)}
|
||||
options={[
|
||||
{ value: "jetkvm", label: "JetKVM Cloud" },
|
||||
{ value: "custom", label: "Custom" },
|
||||
{ value: "jetkvm", label: m.access_provider_jetkvm() },
|
||||
{ value: "custom", label: m.access_provider_custom() },
|
||||
]}
|
||||
/>
|
||||
</SettingsItem>
|
||||
|
|
@ -356,7 +356,7 @@ export default function SettingsAccessIndexRoute() {
|
|||
<div className="flex items-end gap-x-2">
|
||||
<InputFieldWithLabel
|
||||
size="SM"
|
||||
label="Cloud API URL"
|
||||
label={m.access_cloud_api_url_label()}
|
||||
value={cloudApiUrl}
|
||||
onChange={e => setCloudApiUrl(e.target.value)}
|
||||
placeholder="https://api.example.com"
|
||||
|
|
@ -365,7 +365,7 @@ export default function SettingsAccessIndexRoute() {
|
|||
<div className="flex items-end gap-x-2">
|
||||
<InputFieldWithLabel
|
||||
size="SM"
|
||||
label="Cloud App URL"
|
||||
label={m.access_cloud_app_url_label()}
|
||||
value={cloudAppUrl}
|
||||
onChange={e => setCloudAppUrl(e.target.value)}
|
||||
placeholder="https://app.example.com"
|
||||
|
|
@ -384,26 +384,26 @@ export default function SettingsAccessIndexRoute() {
|
|||
<div className="space-y-3">
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-base font-bold text-slate-900 dark:text-white">
|
||||
Cloud Security
|
||||
{m.access_cloud_security_title()}
|
||||
</h3>
|
||||
<div>
|
||||
<ul className="list-disc space-y-1 pl-5 text-xs text-slate-700 dark:text-slate-300">
|
||||
<li>End-to-end encryption using WebRTC (DTLS and SRTP)</li>
|
||||
<li>Zero Trust security model</li>
|
||||
<li>OIDC (OpenID Connect) authentication</li>
|
||||
<li>All streams encrypted in transit</li>
|
||||
<li>{m.access_security_encryption()}</li>
|
||||
<li>{m.access_security_zero_trust()}</li>
|
||||
<li>{m.access_security_oidc()}</li>
|
||||
<li>{m.access_security_streams()}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-slate-700 dark:text-slate-300">
|
||||
All cloud components are open-source and available on{" "}
|
||||
{m.access_security_open_source()}{" "}
|
||||
<a
|
||||
href="https://github.com/jetkvm"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-medium text-blue-600 hover:text-blue-800 dark:text-blue-500 dark:hover:text-blue-400"
|
||||
>
|
||||
GitHub
|
||||
{m.access_github_link()}
|
||||
</a>
|
||||
.
|
||||
</div>
|
||||
|
|
@ -415,7 +415,7 @@ export default function SettingsAccessIndexRoute() {
|
|||
to="https://jetkvm.com/docs/networking/remote-access"
|
||||
size="SM"
|
||||
theme="light"
|
||||
text="Learn about our cloud security"
|
||||
text={m.access_learn_security()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -429,32 +429,32 @@ export default function SettingsAccessIndexRoute() {
|
|||
onClick={() => onCloudAdoptClick(cloudApiUrl, cloudAppUrl)}
|
||||
size="SM"
|
||||
theme="primary"
|
||||
text="Adopt KVM to Cloud"
|
||||
text={m.access_adopt_kvm()}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-slate-600 dark:text-slate-300">
|
||||
Your device is adopted to the Cloud
|
||||
{m.access_adopted_message()}
|
||||
</p>
|
||||
<div>
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text="De-register from Cloud"
|
||||
text={m.access_deregister()}
|
||||
className="text-red-600"
|
||||
onClick={() => {
|
||||
if (deviceId) {
|
||||
if (
|
||||
window.confirm(
|
||||
"Are you sure you want to de-register this device?",
|
||||
m.access_confirm_deregister(),
|
||||
)
|
||||
) {
|
||||
deregisterDevice();
|
||||
}
|
||||
} else {
|
||||
notifications.error("No device ID available");
|
||||
notifications.error(m.access_no_device_id());
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import { useState, useEffect } from "react";
|
||||
import { useLocation, useRevalidator } from "react-router";
|
||||
|
||||
import { useLocalAuthModalStore } from "@hooks/stores";
|
||||
import { useDeviceUiNavigation } from "@hooks/useAppNavigation";
|
||||
import { Button } from "@components/Button";
|
||||
import { InputFieldWithLabel } from "@/components/InputField";
|
||||
import api from "@/api";
|
||||
import { useLocalAuthModalStore } from "@/hooks/stores";
|
||||
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
|
||||
import { m } from "@localizations/messages.js";
|
||||
|
||||
export default function SecurityAccessLocalAuthRoute() {
|
||||
const { setModalView } = useLocalAuthModalStore();
|
||||
|
|
@ -34,12 +35,12 @@ export function Dialog({ onClose }: { onClose: () => void }) {
|
|||
|
||||
const handleCreatePassword = async (password: string, confirmPassword: string) => {
|
||||
if (password === "") {
|
||||
setError("Please enter a password");
|
||||
setError(m.local_auth_error_enter_password());
|
||||
return;
|
||||
}
|
||||
|
||||
if (password !== confirmPassword) {
|
||||
setError("Passwords do not match");
|
||||
setError(m.local_auth_error_passwords_not_match());
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -51,11 +52,11 @@ export function Dialog({ onClose }: { onClose: () => void }) {
|
|||
revalidator.revalidate();
|
||||
} else {
|
||||
const data = await res.json();
|
||||
setError(data.error || "An error occurred while setting the password");
|
||||
setError(data.error || m.local_auth_error_setting_password());
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setError("An error occurred while setting the password");
|
||||
setError(m.local_auth_error_setting_password());
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -65,17 +66,17 @@ export function Dialog({ onClose }: { onClose: () => void }) {
|
|||
confirmNewPassword: string,
|
||||
) => {
|
||||
if (newPassword !== confirmNewPassword) {
|
||||
setError("Passwords do not match");
|
||||
setError(m.local_auth_error_passwords_not_match());
|
||||
return;
|
||||
}
|
||||
|
||||
if (oldPassword === "") {
|
||||
setError("Please enter your old password");
|
||||
setError(m.local_auth_error_enter_old_password());
|
||||
return;
|
||||
}
|
||||
|
||||
if (newPassword === "") {
|
||||
setError("Please enter a new password");
|
||||
setError(m.local_auth_error_enter_new_password());
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -91,17 +92,17 @@ export function Dialog({ onClose }: { onClose: () => void }) {
|
|||
revalidator.revalidate();
|
||||
} else {
|
||||
const data = await res.json();
|
||||
setError(data.error || "An error occurred while changing the password");
|
||||
setError(data.error || m.local_auth_error_changing_password());
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setError("An error occurred while changing the password");
|
||||
setError(m.local_auth_error_changing_password());
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeletePassword = async (password: string) => {
|
||||
if (password === "") {
|
||||
setError("Please enter your current password");
|
||||
setError(m.local_auth_error_enter_current_password());
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -113,11 +114,11 @@ export function Dialog({ onClose }: { onClose: () => void }) {
|
|||
revalidator.revalidate();
|
||||
} else {
|
||||
const data = await res.json();
|
||||
setError(data.error || "An error occurred while disabling the password");
|
||||
setError(data.error || m.local_auth_error_disabling_password());
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setError("An error occurred while disabling the password");
|
||||
setError(m.local_auth_error_disabling_password());
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -150,24 +151,24 @@ export function Dialog({ onClose }: { onClose: () => void }) {
|
|||
|
||||
{modalView === "creationSuccess" && (
|
||||
<SuccessModal
|
||||
headline="Password Set Successfully"
|
||||
description="You've successfully set up local device protection. Your device is now secure against unauthorized local access."
|
||||
headline={m.local_auth_success_password_set_title()}
|
||||
description={m.local_auth_success_password_set_description()}
|
||||
onClose={onClose}
|
||||
/>
|
||||
)}
|
||||
|
||||
{modalView === "deleteSuccess" && (
|
||||
<SuccessModal
|
||||
headline="Password Protection Disabled"
|
||||
description="You've successfully disabled the password protection for local access. Remember, your device is now less secure."
|
||||
headline={m.local_auth_success_password_disabled_title()}
|
||||
description={m.local_auth_success_password_disabled_description()}
|
||||
onClose={onClose}
|
||||
/>
|
||||
)}
|
||||
|
||||
{modalView === "updateSuccess" && (
|
||||
<SuccessModal
|
||||
headline="Password Updated Successfully"
|
||||
description="You've successfully changed your local device protection password. Make sure to remember your new password for future access."
|
||||
headline={m.local_auth_success_password_updated_title()}
|
||||
description={m.local_auth_success_password_updated_description()}
|
||||
onClose={onClose}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -198,24 +199,24 @@ function CreatePasswordModal({
|
|||
>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold dark:text-white">
|
||||
Local Device Protection
|
||||
{m.local_auth_create_title()}
|
||||
</h2>
|
||||
<p className="text-sm text-slate-600 dark:text-slate-400">
|
||||
Create a password to protect your device from unauthorized local access.
|
||||
{m.local_auth_create_description()}
|
||||
</p>
|
||||
</div>
|
||||
<InputFieldWithLabel
|
||||
label="New Password"
|
||||
label={m.local_auth_create_new_password_label()}
|
||||
type="password"
|
||||
placeholder="Enter a strong password"
|
||||
placeholder={m.local_auth_create_new_password_placeholder()}
|
||||
value={password}
|
||||
autoFocus
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
/>
|
||||
<InputFieldWithLabel
|
||||
label="Confirm New Password"
|
||||
label={m.local_auth_confirm_new_password_label()}
|
||||
type="password"
|
||||
placeholder="Re-enter your password"
|
||||
placeholder={m.local_auth_create_confirm_password_placeholder()}
|
||||
value={confirmPassword}
|
||||
onChange={e => setConfirmPassword(e.target.value)}
|
||||
/>
|
||||
|
|
@ -224,10 +225,10 @@ function CreatePasswordModal({
|
|||
<Button
|
||||
size="SM"
|
||||
theme="primary"
|
||||
text="Secure Device"
|
||||
text={m.local_auth_create_secure_button()}
|
||||
onClick={() => onSetPassword(password, confirmPassword)}
|
||||
/>
|
||||
<Button size="SM" theme="light" text="Not Now" onClick={onCancel} />
|
||||
<Button size="SM" theme="light" text={m.local_auth_create_not_now_button()} onClick={onCancel} />
|
||||
</div>
|
||||
{error && <p className="text-sm text-red-500">{error}</p>}
|
||||
</form>
|
||||
|
|
@ -251,16 +252,16 @@ function DeletePasswordModal({
|
|||
<div className="space-y-4">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold dark:text-white">
|
||||
Disable Local Device Protection
|
||||
{m.local_auth_disable_local_device_protection_title()}
|
||||
</h2>
|
||||
<p className="text-sm text-slate-600 dark:text-slate-400">
|
||||
Enter your current password to disable local device protection.
|
||||
{m.local_auth_disable_local_device_protection_description()}
|
||||
</p>
|
||||
</div>
|
||||
<InputFieldWithLabel
|
||||
label="Current Password"
|
||||
label={m.local_auth_current_password_label()}
|
||||
type="password"
|
||||
placeholder="Enter your current password"
|
||||
placeholder={m.local_auth_enter_current_password_placeholder()}
|
||||
value={password}
|
||||
onChange={e => setPassword(e.target.value)}
|
||||
/>
|
||||
|
|
@ -268,10 +269,10 @@ function DeletePasswordModal({
|
|||
<Button
|
||||
size="SM"
|
||||
theme="danger"
|
||||
text="Disable Protection"
|
||||
text={m.local_auth_disable_protection_button()}
|
||||
onClick={() => onDeletePassword(password)}
|
||||
/>
|
||||
<Button size="SM" theme="light" text="Cancel" onClick={onCancel} />
|
||||
<Button size="SM" theme="light" text={m.cancel()} onClick={onCancel} />
|
||||
</div>
|
||||
{error && <p className="text-sm text-red-500">{error}</p>}
|
||||
</div>
|
||||
|
|
@ -306,31 +307,30 @@ function UpdatePasswordModal({
|
|||
>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold dark:text-white">
|
||||
Change Local Device Password
|
||||
{m.local_auth_change_local_device_password_title()}
|
||||
</h2>
|
||||
<p className="text-sm text-slate-600 dark:text-slate-400">
|
||||
Enter your current password and a new password to update your local device
|
||||
protection.
|
||||
{m.local_auth_change_local_device_password_description()}
|
||||
</p>
|
||||
</div>
|
||||
<InputFieldWithLabel
|
||||
label="Current Password"
|
||||
label={m.local_auth_current_password_label()}
|
||||
type="password"
|
||||
placeholder="Enter your current password"
|
||||
placeholder={m.local_auth_enter_current_password_placeholder()}
|
||||
value={oldPassword}
|
||||
onChange={e => setOldPassword(e.target.value)}
|
||||
/>
|
||||
<InputFieldWithLabel
|
||||
label="New Password"
|
||||
label={m.local_auth_new_password_label()}
|
||||
type="password"
|
||||
placeholder="Enter a new strong password"
|
||||
placeholder={m.local_auth_enter_new_password_placeholder()}
|
||||
value={newPassword}
|
||||
onChange={e => setNewPassword(e.target.value)}
|
||||
/>
|
||||
<InputFieldWithLabel
|
||||
label="Confirm New Password"
|
||||
label={m.local_auth_confirm_new_password_label()}
|
||||
type="password"
|
||||
placeholder="Re-enter your new password"
|
||||
placeholder={m.local_auth_reenter_new_password_placeholder()}
|
||||
value={confirmNewPassword}
|
||||
onChange={e => setConfirmNewPassword(e.target.value)}
|
||||
/>
|
||||
|
|
@ -338,10 +338,10 @@ function UpdatePasswordModal({
|
|||
<Button
|
||||
size="SM"
|
||||
theme="primary"
|
||||
text="Update Password"
|
||||
text={m.local_auth_update_password_button()}
|
||||
onClick={() => onUpdatePassword(oldPassword, newPassword, confirmNewPassword)}
|
||||
/>
|
||||
<Button size="SM" theme="light" text="Cancel" onClick={onCancel} />
|
||||
<Button size="SM" theme="light" text={m.cancel()} onClick={onCancel} />
|
||||
</div>
|
||||
{error && <p className="text-sm text-red-500">{error}</p>}
|
||||
</form>
|
||||
|
|
@ -365,7 +365,7 @@ function SuccessModal({
|
|||
<h2 className="text-lg font-semibold dark:text-white">{headline}</h2>
|
||||
<p className="text-sm text-slate-600 dark:text-slate-400">{description}</p>
|
||||
</div>
|
||||
<Button size="SM" theme="primary" text="Close" onClick={onClose} />
|
||||
<Button size="SM" theme="primary" text={m.close()} onClick={onClose} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
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 { SettingsItem } from "@components/SettingsItem";
|
||||
|
||||
import { Button } from "../components/Button";
|
||||
import Checkbox from "../components/Checkbox";
|
||||
import { ConfirmDialog } from "../components/ConfirmDialog";
|
||||
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";
|
||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||
import { TextAreaWithLabel } from "@components/TextArea";
|
||||
import { isOnDevice } from "@/main";
|
||||
import notifications from "@/notifications";
|
||||
import { m } from "@localizations/messages.js";
|
||||
|
||||
export default function SettingsAdvancedRoute() {
|
||||
const { send } = useJsonRpc();
|
||||
|
|
@ -65,7 +65,9 @@ export default function SettingsAdvancedRoute() {
|
|||
send("setUsbEmulationState", { enabled: enabled }, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to ${enabled ? "enable" : "disable"} USB emulation: ${resp.error.data || "Unknown error"}`,
|
||||
enabled
|
||||
? 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;
|
||||
}
|
||||
|
|
@ -80,11 +82,11 @@ export default function SettingsAdvancedRoute() {
|
|||
send("resetConfig", {}, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to reset configuration: ${resp.error.data || "Unknown error"}`,
|
||||
m.advanced_error_reset_config({error: resp.error.data || m.unknown_error()})
|
||||
);
|
||||
return;
|
||||
}
|
||||
notifications.success("Configuration reset to default successfully");
|
||||
notifications.success(m.advanced_success_reset_config());
|
||||
});
|
||||
}, [send]);
|
||||
|
||||
|
|
@ -92,11 +94,11 @@ export default function SettingsAdvancedRoute() {
|
|||
send("setSSHKeyState", { sshKey }, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to update SSH key: ${resp.error.data || "Unknown error"}`,
|
||||
m.advanced_error_update_ssh_key({error: resp.error.data || m.unknown_error()})
|
||||
);
|
||||
return;
|
||||
}
|
||||
notifications.success("SSH key updated successfully");
|
||||
notifications.success(m.advanced_success_update_ssh_key());
|
||||
});
|
||||
}, [send, sshKey]);
|
||||
|
||||
|
|
@ -105,7 +107,7 @@ export default function SettingsAdvancedRoute() {
|
|||
send("setDevModeState", { enabled: developerMode }, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to set dev mode: ${resp.error.data || "Unknown error"}`,
|
||||
m.advanced_error_set_dev_mode({error: resp.error.data || m.unknown_error()})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -120,7 +122,7 @@ export default function SettingsAdvancedRoute() {
|
|||
send("setDevChannelState", { enabled }, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to set dev channel state: ${resp.error.data || "Unknown error"}`,
|
||||
m.advanced_error_set_dev_channel({error: resp.error.data || m.unknown_error()})
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
@ -135,19 +137,17 @@ export default function SettingsAdvancedRoute() {
|
|||
send("setLocalLoopbackOnly", { enabled }, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to ${enabled ? "enable" : "disable"} loopback-only mode: ${resp.error.data || "Unknown error"}`,
|
||||
enabled
|
||||
? 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;
|
||||
}
|
||||
setLocalLoopbackOnly(enabled);
|
||||
if (enabled) {
|
||||
notifications.success(
|
||||
"Loopback-only mode enabled. Restart your device to apply.",
|
||||
);
|
||||
notifications.success(m.advanced_success_loopback_enabled());
|
||||
} else {
|
||||
notifications.success(
|
||||
"Loopback-only mode disabled. Restart your device to apply.",
|
||||
);
|
||||
notifications.success(m.advanced_success_loopback_disabled());
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
@ -175,14 +175,14 @@ export default function SettingsAdvancedRoute() {
|
|||
return (
|
||||
<div className="space-y-4">
|
||||
<SettingsPageHeader
|
||||
title="Advanced"
|
||||
description="Access additional settings for troubleshooting and customization"
|
||||
title={m.advanced_title()}
|
||||
description={m.advanced_description()}
|
||||
/>
|
||||
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title="Dev Channel Updates"
|
||||
description="Receive early updates from the development channel"
|
||||
title={m.advanced_dev_channel_title()}
|
||||
description={m.advanced_dev_channel_description()}
|
||||
>
|
||||
<Checkbox
|
||||
checked={devChannel}
|
||||
|
|
@ -192,8 +192,8 @@ export default function SettingsAdvancedRoute() {
|
|||
/>
|
||||
</SettingsItem>
|
||||
<SettingsItem
|
||||
title="Developer Mode"
|
||||
description="Enable advanced features for developers"
|
||||
title={m.advanced_developer_mode_title()}
|
||||
description={m.advanced_developer_mode_description()}
|
||||
>
|
||||
<Checkbox
|
||||
checked={settings.developerMode}
|
||||
|
|
@ -219,18 +219,17 @@ export default function SettingsAdvancedRoute() {
|
|||
<div className="space-y-3">
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-base font-bold text-slate-900 dark:text-white">
|
||||
Developer Mode Enabled
|
||||
{m.advanced_developer_mode_enabled_title()}
|
||||
</h3>
|
||||
<div>
|
||||
<ul className="list-disc space-y-1 pl-5 text-xs text-slate-700 dark:text-slate-300">
|
||||
<li>Security is weakened while active</li>
|
||||
<li>Only use if you understand the risks</li>
|
||||
<li>{m.advanced_developer_mode_warning_security()}</li>
|
||||
<li>{m.advanced_developer_mode_warning_risks()}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-slate-700 dark:text-slate-300">
|
||||
For advanced users only. Not for production use.
|
||||
{m.advanced_developer_mode_warning_advanced()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -238,8 +237,8 @@ export default function SettingsAdvancedRoute() {
|
|||
)}
|
||||
|
||||
<SettingsItem
|
||||
title="Loopback-Only Mode"
|
||||
description="Restrict web interface access to localhost only (127.0.0.1)"
|
||||
title={m.advanced_loopback_only_title()}
|
||||
description={m.advanced_loopback_only_description()}
|
||||
>
|
||||
<Checkbox
|
||||
checked={localLoopbackOnly}
|
||||
|
|
@ -250,25 +249,25 @@ export default function SettingsAdvancedRoute() {
|
|||
{isOnDevice && settings.developerMode && (
|
||||
<div className="space-y-4">
|
||||
<SettingsItem
|
||||
title="SSH Access"
|
||||
description="Add your SSH public key to enable secure remote access to the device"
|
||||
title={m.advanced_ssh_access_title()}
|
||||
description={m.advanced_ssh_access_description()}
|
||||
/>
|
||||
<div className="space-y-4">
|
||||
<TextAreaWithLabel
|
||||
label="SSH Public Key"
|
||||
label={m.advanced_ssh_public_key_label()}
|
||||
value={sshKey || ""}
|
||||
rows={3}
|
||||
onChange={e => setSSHKey(e.target.value)}
|
||||
placeholder="Enter your SSH public key"
|
||||
placeholder={m.advanced_ssh_public_key_placeholder()}
|
||||
/>
|
||||
<p className="text-xs text-slate-600 dark:text-slate-400">
|
||||
The default SSH user is <strong>root</strong>.
|
||||
{m.advanced_ssh_default_user()}<strong>root</strong>.
|
||||
</p>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<Button
|
||||
size="SM"
|
||||
theme="primary"
|
||||
text="Update SSH Key"
|
||||
text={m.advanced_update_ssh_key_button()}
|
||||
onClick={handleUpdateSSHKey}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -277,8 +276,8 @@ export default function SettingsAdvancedRoute() {
|
|||
)}
|
||||
|
||||
<SettingsItem
|
||||
title="Troubleshooting Mode"
|
||||
description="Diagnostic tools and additional controls for troubleshooting and development purposes"
|
||||
title={m.advanced_troubleshooting_mode_title()}
|
||||
description={m.advanced_troubleshooting_mode_description()}
|
||||
>
|
||||
<Checkbox
|
||||
defaultChecked={settings.debugMode}
|
||||
|
|
@ -291,27 +290,27 @@ export default function SettingsAdvancedRoute() {
|
|||
{settings.debugMode && (
|
||||
<>
|
||||
<SettingsItem
|
||||
title="USB Emulation"
|
||||
description="Control the USB emulation state"
|
||||
title={m.advanced_usb_emulation_title()}
|
||||
description={m.advanced_usb_emulation_description()}
|
||||
>
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text={
|
||||
usbEmulationEnabled ? "Disable USB Emulation" : "Enable USB Emulation"
|
||||
usbEmulationEnabled ? m.advanced_disable_usb_emulation() : m.advanced_enable_usb_emulation()
|
||||
}
|
||||
onClick={() => handleUsbEmulationToggle(!usbEmulationEnabled)}
|
||||
/>
|
||||
</SettingsItem>
|
||||
|
||||
<SettingsItem
|
||||
title="Reset Configuration"
|
||||
description="Reset configuration to default. This will log you out."
|
||||
title={m.advanced_reset_config_title()}
|
||||
description={m.advanced_reset_config_description()}
|
||||
>
|
||||
<Button
|
||||
size="SM"
|
||||
theme="light"
|
||||
text="Reset Config"
|
||||
text={m.advanced_reset_config_button()}
|
||||
onClick={() => {
|
||||
handleResetConfig();
|
||||
window.location.reload();
|
||||
|
|
@ -327,22 +326,23 @@ export default function SettingsAdvancedRoute() {
|
|||
onClose={() => {
|
||||
setShowLoopbackWarning(false);
|
||||
}}
|
||||
title="Enable Loopback-Only Mode?"
|
||||
title={m.advanced_loopback_warning_title()}
|
||||
description={
|
||||
<>
|
||||
<p>
|
||||
WARNING: This will restrict web interface access to localhost (127.0.0.1)
|
||||
only.
|
||||
{m.advanced_loopback_warning_description()}
|
||||
</p>
|
||||
<p>
|
||||
{m.advanced_loopback_warning_before()}
|
||||
</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">
|
||||
<li>SSH access configured and tested</li>
|
||||
<li>Cloud access enabled and working</li>
|
||||
<li>{m.advanced_loopback_warning_ssh()}</li>
|
||||
<li>{m.advanced_loopback_warning_cloud()}</li>
|
||||
</ul>
|
||||
</>
|
||||
}
|
||||
variant="warning"
|
||||
confirmText="I Understand, Enable Anyway"
|
||||
confirmText={m.advanced_loopback_warning_confirm()}
|
||||
onConfirm={confirmLoopbackModeEnable}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -41,8 +41,6 @@ export default function SettingsGeneralUpdateRoute() {
|
|||
return <Dialog onClose={() => navigate("..")} onConfirmUpdate={onConfirmUpdate} />;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function Dialog({
|
||||
onClose,
|
||||
onConfirmUpdate,
|
||||
|
|
@ -71,11 +69,6 @@ export function Dialog({
|
|||
[setModalView],
|
||||
);
|
||||
|
||||
// Reset modal view when dialog is opened
|
||||
useEffect(() => {
|
||||
setVersionInfo(null);
|
||||
}, [setModalView]);
|
||||
|
||||
return (
|
||||
<div className="pointer-events-auto relative mx-auto text-left">
|
||||
<div>
|
||||
|
|
@ -133,8 +126,6 @@ function LoadingState({
|
|||
|
||||
const progressBarRef = useRef<HTMLDivElement>(null);
|
||||
useEffect(() => {
|
||||
setProgressWidth("0%");
|
||||
|
||||
abortControllerRef.current = new AbortController();
|
||||
const signal = abortControllerRef.current.signal;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,20 +1,19 @@
|
|||
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 { 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 notifications from "../notifications";
|
||||
import { UsbInfoSetting } from "../components/UsbInfoSetting";
|
||||
import { FeatureFlag } from "../components/FeatureFlag";
|
||||
import { UsbInfoSetting } from "@components/UsbInfoSetting";
|
||||
import notifications from "@/notifications";
|
||||
|
||||
export default function SettingsHardwareRoute() {
|
||||
const { send } = useJsonRpc();
|
||||
const settings = useSettingsStore();
|
||||
const { setDisplayRotation } = useSettingsStore();
|
||||
const { displayRotation, setDisplayRotation } = useSettingsStore();
|
||||
|
||||
const handleDisplayRotationChange = (rotation: string) => {
|
||||
setDisplayRotation(rotation);
|
||||
|
|
@ -22,7 +21,7 @@ export default function SettingsHardwareRoute() {
|
|||
};
|
||||
|
||||
const handleDisplayRotationSave = () => {
|
||||
send("setDisplayRotation", { params: { rotation: settings.displayRotation } }, (resp: JsonRpcResponse) => {
|
||||
send("setDisplayRotation", { params: { rotation: displayRotation } }, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to set display orientation: ${resp.error.data || "Unknown error"}`,
|
||||
|
|
@ -33,7 +32,7 @@ export default function SettingsHardwareRoute() {
|
|||
});
|
||||
};
|
||||
|
||||
const { setBacklightSettings } = useSettingsStore();
|
||||
const { backlightSettings, setBacklightSettings } = useSettingsStore();
|
||||
|
||||
const handleBacklightSettingsChange = (settings: BacklightSettings) => {
|
||||
// If the user has set the display to dim after it turns off, set the dim_after
|
||||
|
|
@ -47,7 +46,7 @@ export default function SettingsHardwareRoute() {
|
|||
};
|
||||
|
||||
const handleBacklightSettingsSave = () => {
|
||||
send("setBacklightSettings", { params: settings.backlightSettings }, (resp: JsonRpcResponse) => {
|
||||
send("setBacklightSettings", { params: backlightSettings }, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
notifications.error(
|
||||
`Failed to set backlight settings: ${resp.error.data || "Unknown error"}`,
|
||||
|
|
@ -58,6 +57,21 @@ 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(() => {
|
||||
send("getBacklightSettings", {}, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) {
|
||||
|
|
@ -90,8 +104,7 @@ export default function SettingsHardwareRoute() {
|
|||
{ value: "90", label: "Inverted" },
|
||||
]}
|
||||
onChange={e => {
|
||||
settings.displayRotation = e.target.value;
|
||||
handleDisplayRotationChange(settings.displayRotation);
|
||||
handleDisplayRotationChange(e.target.value);
|
||||
}}
|
||||
/>
|
||||
</SettingsItem>
|
||||
|
|
@ -102,7 +115,7 @@ export default function SettingsHardwareRoute() {
|
|||
<SelectMenuBasic
|
||||
size="SM"
|
||||
label=""
|
||||
value={settings.backlightSettings.max_brightness.toString()}
|
||||
value={backlightSettings.max_brightness.toString()}
|
||||
options={[
|
||||
{ value: "0", label: "Off" },
|
||||
{ value: "10", label: "Low" },
|
||||
|
|
@ -110,12 +123,11 @@ export default function SettingsHardwareRoute() {
|
|||
{ value: "64", label: "High" },
|
||||
]}
|
||||
onChange={e => {
|
||||
settings.backlightSettings.max_brightness = parseInt(e.target.value);
|
||||
handleBacklightSettingsChange(settings.backlightSettings);
|
||||
handleBacklightMaxBrightnessChange(parseInt(e.target.value));
|
||||
}}
|
||||
/>
|
||||
</SettingsItem>
|
||||
{settings.backlightSettings.max_brightness != 0 && (
|
||||
{backlightSettings.max_brightness != 0 && (
|
||||
<>
|
||||
<SettingsItem
|
||||
title="Dim Display After"
|
||||
|
|
@ -124,7 +136,7 @@ export default function SettingsHardwareRoute() {
|
|||
<SelectMenuBasic
|
||||
size="SM"
|
||||
label=""
|
||||
value={settings.backlightSettings.dim_after.toString()}
|
||||
value={backlightSettings.dim_after.toString()}
|
||||
options={[
|
||||
{ value: "0", label: "Never" },
|
||||
{ value: "60", label: "1 Minute" },
|
||||
|
|
@ -134,8 +146,7 @@ export default function SettingsHardwareRoute() {
|
|||
{ value: "3600", label: "1 Hour" },
|
||||
]}
|
||||
onChange={e => {
|
||||
settings.backlightSettings.dim_after = parseInt(e.target.value);
|
||||
handleBacklightSettingsChange(settings.backlightSettings);
|
||||
handleBacklightDimAfterChange(parseInt(e.target.value));
|
||||
}}
|
||||
/>
|
||||
</SettingsItem>
|
||||
|
|
@ -146,7 +157,7 @@ export default function SettingsHardwareRoute() {
|
|||
<SelectMenuBasic
|
||||
size="SM"
|
||||
label=""
|
||||
value={settings.backlightSettings.off_after.toString()}
|
||||
value={backlightSettings.off_after.toString()}
|
||||
options={[
|
||||
{ value: "0", label: "Never" },
|
||||
{ value: "300", label: "5 Minutes" },
|
||||
|
|
@ -155,8 +166,7 @@ export default function SettingsHardwareRoute() {
|
|||
{ value: "3600", label: "1 Hour" },
|
||||
]}
|
||||
onChange={e => {
|
||||
settings.backlightSettings.off_after = parseInt(e.target.value);
|
||||
handleBacklightSettingsChange(settings.backlightSettings);
|
||||
handleBacklightOffAfterChange(parseInt(e.target.value));
|
||||
}}
|
||||
/>
|
||||
</SettingsItem>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import dayjs from "dayjs";
|
||||
import relativeTime from "dayjs/plugin/relativeTime";
|
||||
import { LuEthernetPort } from "react-icons/lu";
|
||||
|
|
@ -12,23 +12,22 @@ import {
|
|||
NetworkState,
|
||||
TimeSyncMode,
|
||||
useNetworkStateStore,
|
||||
} from "@/hooks/stores";
|
||||
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
|
||||
} from "@hooks/stores";
|
||||
import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
|
||||
import AutoHeight from "@components/AutoHeight";
|
||||
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 InputField, { InputFieldWithLabel } from "@components/InputField";
|
||||
import { SelectMenuBasic } from "@/components/SelectMenuBasic";
|
||||
import { SettingsPageHeader } from "@/components/SettingsPageheader";
|
||||
import Fieldset from "@/components/Fieldset";
|
||||
import { ConfirmDialog } from "@/components/ConfirmDialog";
|
||||
import Ipv6NetworkCard from "@components/Ipv6NetworkCard";
|
||||
import DhcpLeaseCard from "@components/DhcpLeaseCard";
|
||||
import { SelectMenuBasic } from "@components/SelectMenuBasic";
|
||||
import { SettingsItem } from "@components/SettingsItem";
|
||||
import { SettingsPageHeader } from "@components/SettingsPageheader";
|
||||
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);
|
||||
|
||||
const defaultNetworkSettings: NetworkSettings = {
|
||||
|
|
@ -46,14 +45,18 @@ const defaultNetworkSettings: NetworkSettings = {
|
|||
export function LifeTimeLabel({ lifetime }: { lifetime: string }) {
|
||||
const [remaining, setRemaining] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const updateRemaining = useCallback(() => {
|
||||
setRemaining(dayjs(lifetime).fromNow());
|
||||
}, [lifetime]);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => updateRemaining(), 0);
|
||||
|
||||
const interval = setInterval(() => {
|
||||
setRemaining(dayjs(lifetime).fromNow());
|
||||
updateRemaining();
|
||||
}, 1000 * 30);
|
||||
return () => clearInterval(interval);
|
||||
}, [lifetime]);
|
||||
}, [updateRemaining]);
|
||||
|
||||
if (lifetime == "") {
|
||||
return <strong>N/A</strong>;
|
||||
|
|
@ -81,24 +84,19 @@ export default function SettingsNetworkRoute() {
|
|||
useState<NetworkSettings>(defaultNetworkSettings);
|
||||
|
||||
// We use this to determine whether the settings have changed
|
||||
const firstNetworkSettings = useRef<NetworkSettings | undefined>(undefined);
|
||||
|
||||
const [firstNetworkSettings, setFirstNetworkSettings] = useState<NetworkSettings | undefined>(undefined);
|
||||
const [networkSettingsLoaded, setNetworkSettingsLoaded] = useState(false);
|
||||
|
||||
const [customDomain, setCustomDomain] = useState<string>("");
|
||||
const [selectedDomainOption, setSelectedDomainOption] = useState<string>("dhcp");
|
||||
|
||||
useEffect(() => {
|
||||
if (networkSettings.domain && networkSettingsLoaded) {
|
||||
// Check if the domain is one of the predefined options
|
||||
const selectedDomainOption = useMemo(() => {
|
||||
if (!networkSettingsLoaded) return "dhcp";
|
||||
const predefinedOptions = ["dhcp", "local"];
|
||||
if (predefinedOptions.includes(networkSettings.domain)) {
|
||||
setSelectedDomainOption(networkSettings.domain);
|
||||
} else {
|
||||
setSelectedDomainOption("custom");
|
||||
setCustomDomain(networkSettings.domain);
|
||||
}
|
||||
}
|
||||
return predefinedOptions.includes(networkSettings.domain) ? networkSettings.domain : "custom";
|
||||
}, [networkSettings.domain, networkSettingsLoaded]);
|
||||
|
||||
const customDomain = useMemo(() => {
|
||||
if (!networkSettingsLoaded) return "";
|
||||
const predefinedOptions = ["dhcp", "local"];
|
||||
return predefinedOptions.includes(networkSettings.domain) ? "" : networkSettings.domain;
|
||||
}, [networkSettings.domain, networkSettingsLoaded]);
|
||||
|
||||
const getNetworkSettings = useCallback(() => {
|
||||
|
|
@ -109,12 +107,12 @@ export default function SettingsNetworkRoute() {
|
|||
console.debug("Network settings: ", networkSettings);
|
||||
setNetworkSettings(networkSettings);
|
||||
|
||||
if (!firstNetworkSettings.current) {
|
||||
firstNetworkSettings.current = networkSettings;
|
||||
if (!firstNetworkSettings) {
|
||||
setFirstNetworkSettings(networkSettings);
|
||||
}
|
||||
setNetworkSettingsLoaded(true);
|
||||
});
|
||||
}, [send]);
|
||||
}, [send, firstNetworkSettings]);
|
||||
|
||||
const getNetworkState = useCallback(() => {
|
||||
send("getNetworkState", {}, (resp: JsonRpcResponse) => {
|
||||
|
|
@ -138,8 +136,7 @@ export default function SettingsNetworkRoute() {
|
|||
return;
|
||||
}
|
||||
const networkSettings = resp.result as 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;
|
||||
setFirstNetworkSettings(networkSettings);
|
||||
setNetworkSettings(networkSettings);
|
||||
getNetworkState();
|
||||
setNetworkSettingsLoaded(true);
|
||||
|
|
@ -160,8 +157,10 @@ export default function SettingsNetworkRoute() {
|
|||
}, [send]);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
getNetworkState();
|
||||
getNetworkSettings();
|
||||
}, 0);
|
||||
}, [getNetworkState, getNetworkSettings]);
|
||||
|
||||
const handleIpv4ModeChange = (value: IPv4Mode | string) => {
|
||||
|
|
@ -197,14 +196,12 @@ export default function SettingsNetworkRoute() {
|
|||
};
|
||||
|
||||
const handleDomainOptionChange = (value: string) => {
|
||||
setSelectedDomainOption(value);
|
||||
if (value !== "custom") {
|
||||
handleDomainChange(value);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCustomDomainChange = (value: string) => {
|
||||
setCustomDomain(value);
|
||||
handleDomainChange(value);
|
||||
};
|
||||
|
||||
|
|
@ -309,7 +306,6 @@ export default function SettingsNetworkRoute() {
|
|||
placeholder="home"
|
||||
value={customDomain}
|
||||
onChange={e => {
|
||||
setCustomDomain(e.target.value);
|
||||
handleCustomDomainChange(e.target.value);
|
||||
}}
|
||||
/>
|
||||
|
|
@ -361,7 +357,7 @@ export default function SettingsNetworkRoute() {
|
|||
<Button
|
||||
size="SM"
|
||||
theme="primary"
|
||||
disabled={firstNetworkSettings.current === networkSettings}
|
||||
disabled={firstNetworkSettings === networkSettings}
|
||||
text="Save Settings"
|
||||
onClick={() => setNetworkSettingsRemote(networkSettings)}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ export default function SettingsVideoRoute() {
|
|||
const [streamQuality, setStreamQuality] = useState("1");
|
||||
const [customEdidValue, setCustomEdidValue] = useState<string | null>(null);
|
||||
const [edid, setEdid] = useState<string | null>(null);
|
||||
const [edidLoading, setEdidLoading] = useState(false);
|
||||
const [edidLoading, setEdidLoading] = useState(true);
|
||||
const { debugMode } = useSettingsStore();
|
||||
// Video enhancement settings from store
|
||||
const {
|
||||
|
|
@ -63,7 +63,6 @@ export default function SettingsVideoRoute() {
|
|||
} = useSettingsStore();
|
||||
|
||||
useEffect(() => {
|
||||
setEdidLoading(true);
|
||||
send("getStreamQualityFactor", {}, (resp: JsonRpcResponse) => {
|
||||
if ("error" in resp) return;
|
||||
setStreamQuality(String(resp.result));
|
||||
|
|
|
|||
103
video.go
103
video.go
|
|
@ -1,10 +1,22 @@
|
|||
package kvm
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/jetkvm/kvm/internal/native"
|
||||
)
|
||||
|
||||
var lastVideoState native.VideoState
|
||||
var (
|
||||
lastVideoState native.VideoState
|
||||
videoSleepModeCtx context.Context
|
||||
videoSleepModeCancel context.CancelFunc
|
||||
)
|
||||
|
||||
const (
|
||||
defaultVideoSleepModeDuration = 1 * time.Minute
|
||||
)
|
||||
|
||||
func triggerVideoStateUpdate() {
|
||||
go func() {
|
||||
|
|
@ -17,3 +29,92 @@ func triggerVideoStateUpdate() {
|
|||
func rpcGetVideoState() (native.VideoState, error) {
|
||||
return lastVideoState, nil
|
||||
}
|
||||
|
||||
type rpcVideoSleepModeResponse struct {
|
||||
Supported bool `json:"supported"`
|
||||
Enabled bool `json:"enabled"`
|
||||
Duration int `json:"duration"`
|
||||
}
|
||||
|
||||
func rpcGetVideoSleepMode() rpcVideoSleepModeResponse {
|
||||
sleepMode, _ := nativeInstance.VideoGetSleepMode()
|
||||
return rpcVideoSleepModeResponse{
|
||||
Supported: nativeInstance.VideoSleepModeSupported(),
|
||||
Enabled: sleepMode,
|
||||
Duration: config.VideoSleepAfterSec,
|
||||
}
|
||||
}
|
||||
|
||||
func rpcSetVideoSleepMode(duration int) error {
|
||||
if duration < 0 {
|
||||
duration = -1 // disable
|
||||
}
|
||||
|
||||
config.VideoSleepAfterSec = duration
|
||||
if err := SaveConfig(); err != nil {
|
||||
return fmt.Errorf("failed to save config: %w", err)
|
||||
}
|
||||
|
||||
// we won't restart the ticker here,
|
||||
// as the session can't be inactive when this function is called
|
||||
return nil
|
||||
}
|
||||
|
||||
func stopVideoSleepModeTicker() {
|
||||
nativeLogger.Trace().Msg("stopping HDMI sleep mode ticker")
|
||||
|
||||
if videoSleepModeCancel != nil {
|
||||
nativeLogger.Trace().Msg("canceling HDMI sleep mode ticker context")
|
||||
videoSleepModeCancel()
|
||||
videoSleepModeCancel = nil
|
||||
videoSleepModeCtx = nil
|
||||
}
|
||||
}
|
||||
|
||||
func startVideoSleepModeTicker() {
|
||||
if !nativeInstance.VideoSleepModeSupported() {
|
||||
return
|
||||
}
|
||||
|
||||
var duration time.Duration
|
||||
|
||||
if config.VideoSleepAfterSec == 0 {
|
||||
duration = defaultVideoSleepModeDuration
|
||||
} else if config.VideoSleepAfterSec > 0 {
|
||||
duration = time.Duration(config.VideoSleepAfterSec) * time.Second
|
||||
} else {
|
||||
stopVideoSleepModeTicker()
|
||||
return
|
||||
}
|
||||
|
||||
// Stop any existing timer and goroutine
|
||||
stopVideoSleepModeTicker()
|
||||
|
||||
// Create new context for this ticker
|
||||
videoSleepModeCtx, videoSleepModeCancel = context.WithCancel(context.Background())
|
||||
|
||||
go doVideoSleepModeTicker(videoSleepModeCtx, duration)
|
||||
}
|
||||
|
||||
func doVideoSleepModeTicker(ctx context.Context, duration time.Duration) {
|
||||
timer := time.NewTimer(duration)
|
||||
defer timer.Stop()
|
||||
|
||||
nativeLogger.Trace().Msg("HDMI sleep mode ticker started")
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-timer.C:
|
||||
if getActiveSessions() > 0 {
|
||||
nativeLogger.Warn().Msg("not going to enter HDMI sleep mode because there are active sessions")
|
||||
continue
|
||||
}
|
||||
|
||||
nativeLogger.Trace().Msg("entering HDMI sleep mode")
|
||||
_ = nativeInstance.VideoSetSleepMode(true)
|
||||
case <-ctx.Done():
|
||||
nativeLogger.Trace().Msg("HDMI sleep mode ticker stopped")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
38
webrtc.go
38
webrtc.go
|
|
@ -39,6 +39,34 @@ type Session struct {
|
|||
keysDownStateQueue chan usbgadget.KeysDownState
|
||||
}
|
||||
|
||||
var (
|
||||
actionSessions int = 0
|
||||
activeSessionsMutex = &sync.Mutex{}
|
||||
)
|
||||
|
||||
func incrActiveSessions() int {
|
||||
activeSessionsMutex.Lock()
|
||||
defer activeSessionsMutex.Unlock()
|
||||
|
||||
actionSessions++
|
||||
return actionSessions
|
||||
}
|
||||
|
||||
func decrActiveSessions() int {
|
||||
activeSessionsMutex.Lock()
|
||||
defer activeSessionsMutex.Unlock()
|
||||
|
||||
actionSessions--
|
||||
return actionSessions
|
||||
}
|
||||
|
||||
func getActiveSessions() int {
|
||||
activeSessionsMutex.Lock()
|
||||
defer activeSessionsMutex.Unlock()
|
||||
|
||||
return actionSessions
|
||||
}
|
||||
|
||||
func (s *Session) resetKeepAliveTime() {
|
||||
s.keepAliveJitterLock.Lock()
|
||||
defer s.keepAliveJitterLock.Unlock()
|
||||
|
|
@ -312,9 +340,8 @@ func newSession(config SessionConfig) (*Session, error) {
|
|||
if connectionState == webrtc.ICEConnectionStateConnected {
|
||||
if !isConnected {
|
||||
isConnected = true
|
||||
actionSessions++
|
||||
onActiveSessionsChanged()
|
||||
if actionSessions == 1 {
|
||||
if incrActiveSessions() == 1 {
|
||||
onFirstSessionConnected()
|
||||
}
|
||||
}
|
||||
|
|
@ -353,9 +380,8 @@ func newSession(config SessionConfig) (*Session, error) {
|
|||
}
|
||||
if isConnected {
|
||||
isConnected = false
|
||||
actionSessions--
|
||||
onActiveSessionsChanged()
|
||||
if actionSessions == 0 {
|
||||
if decrActiveSessions() == 0 {
|
||||
onLastSessionDisconnected()
|
||||
}
|
||||
}
|
||||
|
|
@ -364,16 +390,16 @@ func newSession(config SessionConfig) (*Session, error) {
|
|||
return session, nil
|
||||
}
|
||||
|
||||
var actionSessions = 0
|
||||
|
||||
func onActiveSessionsChanged() {
|
||||
requestDisplayUpdate(true, "active_sessions_changed")
|
||||
}
|
||||
|
||||
func onFirstSessionConnected() {
|
||||
_ = nativeInstance.VideoStart()
|
||||
stopVideoSleepModeTicker()
|
||||
}
|
||||
|
||||
func onLastSessionDisconnected() {
|
||||
_ = nativeInstance.VideoStop()
|
||||
startVideoSleepModeTicker()
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue