From ad0b86c8a6a4caf3113a38f7497e2800d7244c81 Mon Sep 17 00:00:00 2001 From: Siyuan Date: Wed, 8 Oct 2025 15:36:02 +0000 Subject: [PATCH] fix state change detection --- internal/network/types/dhcp.go | 77 +++++++++ internal/network/types/rpc.go | 33 ++++ internal/network/types/type.go | 74 +-------- network.go | 8 +- pkg/nmlite/interface.go | 110 ------------- pkg/nmlite/interface_state.go | 152 ++++++++++++++++++ pkg/nmlite/utils.go | 70 ++++++++ .../routes/devices.$id.settings.network.tsx | 5 +- ui/src/routes/devices.$id.tsx | 12 +- 9 files changed, 349 insertions(+), 192 deletions(-) create mode 100644 internal/network/types/rpc.go create mode 100644 pkg/nmlite/interface_state.go create mode 100644 pkg/nmlite/utils.go diff --git a/internal/network/types/dhcp.go b/internal/network/types/dhcp.go index 5607849b..ff34e2f2 100644 --- a/internal/network/types/dhcp.go +++ b/internal/network/types/dhcp.go @@ -1,5 +1,10 @@ package types +import ( + "net" + "time" +) + // DHCPClient is the interface for a DHCP client. type DHCPClient interface { Domain() string @@ -13,3 +18,75 @@ type DHCPClient interface { Start() error Stop() error } + +// DHCPLease is a network configuration obtained by DHCP. +type DHCPLease struct { + // from https://udhcp.busybox.net/README.udhcpc + IPAddress net.IP `env:"ip" json:"ip"` // The obtained IP + Netmask net.IP `env:"subnet" json:"netmask"` // The assigned subnet mask + Broadcast net.IP `env:"broadcast" json:"broadcast"` // The broadcast address for this network + TTL int `env:"ipttl" json:"ttl,omitempty"` // The TTL to use for this network + MTU int `env:"mtu" json:"mtu,omitempty"` // The MTU to use for this network + HostName string `env:"hostname" json:"hostname,omitempty"` // The assigned hostname + Domain string `env:"domain" json:"domain,omitempty"` // The domain name of the network + SearchList []string `env:"search" json:"search_list,omitempty"` // The search list for the network + BootPNextServer net.IP `env:"siaddr" json:"bootp_next_server,omitempty"` // The bootp next server option + BootPServerName string `env:"sname" json:"bootp_server_name,omitempty"` // The bootp server name option + BootPFile string `env:"boot_file" json:"bootp_file,omitempty"` // The bootp boot file option + Timezone string `env:"timezone" json:"timezone,omitempty"` // Offset in seconds from UTC + Routers []net.IP `env:"router" json:"routers,omitempty"` // A list of routers + DNS []net.IP `env:"dns" json:"dns_servers,omitempty"` // A list of DNS servers + NTPServers []net.IP `env:"ntpsrv" json:"ntp_servers,omitempty"` // A list of NTP servers + LPRServers []net.IP `env:"lprsvr" json:"lpr_servers,omitempty"` // A list of LPR servers + TimeServers []net.IP `env:"timesvr" json:"_time_servers,omitempty"` // A list of time servers (obsolete) + IEN116NameServers []net.IP `env:"namesvr" json:"_name_servers,omitempty"` // A list of IEN 116 name servers (obsolete) + LogServers []net.IP `env:"logsvr" json:"_log_servers,omitempty"` // A list of MIT-LCS UDP log servers (obsolete) + CookieServers []net.IP `env:"cookiesvr" json:"_cookie_servers,omitempty"` // A list of RFC 865 cookie servers (obsolete) + WINSServers []net.IP `env:"wins" json:"_wins_servers,omitempty"` // A list of WINS servers + SwapServer net.IP `env:"swapsvr" json:"_swap_server,omitempty"` // The IP address of the client's swap server + BootSize int `env:"bootsize" json:"bootsize,omitempty"` // The length in 512 octect blocks of the bootfile + RootPath string `env:"rootpath" json:"root_path,omitempty"` // The path name of the client's root disk + LeaseTime time.Duration `env:"lease" json:"lease,omitempty"` // The lease time, in seconds + RenewalTime time.Duration `env:"renewal" json:"renewal,omitempty"` // The renewal time, in seconds + RebindingTime time.Duration `env:"rebinding" json:"rebinding,omitempty"` // The rebinding time, in seconds + DHCPType string `env:"dhcptype" json:"dhcp_type,omitempty"` // DHCP message type (safely ignored) + ServerID string `env:"serverid" json:"server_id,omitempty"` // The IP of the server + Message string `env:"message" json:"reason,omitempty"` // Reason for a DHCPNAK + TFTPServerName string `env:"tftp" json:"tftp,omitempty"` // The TFTP server name + BootFileName string `env:"bootfile" json:"bootfile,omitempty"` // The boot file name + Uptime time.Duration `env:"uptime" json:"uptime,omitempty"` // The uptime of the device when the lease was obtained, in seconds + ClassIdentifier string `env:"classid" json:"class_identifier,omitempty"` // The class identifier + LeaseExpiry *time.Time `json:"lease_expiry,omitempty"` // The expiry time of the lease + + InterfaceName string `json:"interface_name,omitempty"` // The name of the interface + DHCPClient string `json:"dhcp_client,omitempty"` // The DHCP client that obtained the lease +} + +// IsIPv6 returns true if the DHCP lease is for an IPv6 address +func (d *DHCPLease) IsIPv6() bool { + return d.IPAddress.To4() == nil +} + +// IPMask returns the IP mask for the DHCP lease +func (d *DHCPLease) IPMask() net.IPMask { + if d.IsIPv6() { + // TODO: not implemented + return nil + } + + mask := net.ParseIP(d.Netmask.String()) + return net.IPv4Mask(mask[12], mask[13], mask[14], mask[15]) +} + +// IPNet returns the IP net for the DHCP lease +func (d *DHCPLease) IPNet() *net.IPNet { + if d.IsIPv6() { + // TODO: not implemented + return nil + } + + return &net.IPNet{ + IP: d.IPAddress, + Mask: d.IPMask(), + } +} diff --git a/internal/network/types/rpc.go b/internal/network/types/rpc.go new file mode 100644 index 00000000..748d778e --- /dev/null +++ b/internal/network/types/rpc.go @@ -0,0 +1,33 @@ +package types + +import "time" + +type RpcIPv6Address struct { + Address string `json:"address"` + Prefix string `json:"prefix"` + ValidLifetime *time.Time `json:"valid_lifetime"` + PreferredLifetime *time.Time `json:"preferred_lifetime"` + Scope int `json:"scope"` +} + +type RpcInterfaceState struct { + InterfaceState + IPv6Addresses []RpcIPv6Address `json:"ipv6_addresses"` +} + +func (s *InterfaceState) ToRpcInterfaceState() *RpcInterfaceState { + addrs := make([]RpcIPv6Address, len(s.IPv6Addresses)) + for i, addr := range s.IPv6Addresses { + addrs[i] = RpcIPv6Address{ + Address: addr.Address.String(), + Prefix: addr.Prefix.String(), + ValidLifetime: addr.ValidLifetime, + PreferredLifetime: addr.PreferredLifetime, + Scope: addr.Scope, + } + } + return &RpcInterfaceState{ + InterfaceState: *s, + IPv6Addresses: addrs, + } +} diff --git a/internal/network/types/type.go b/internal/network/types/type.go index 83f64521..e664be6f 100644 --- a/internal/network/types/type.go +++ b/internal/network/types/type.go @@ -25,6 +25,8 @@ type IPv6Address struct { Prefix net.IPNet `json:"prefix"` ValidLifetime *time.Time `json:"valid_lifetime"` PreferredLifetime *time.Time `json:"preferred_lifetime"` + valid_lft int `json:"valid_lft"` + prefered_lft int `json:"prefered_lft"` Scope int `json:"scope"` } @@ -107,49 +109,6 @@ func (c *NetworkConfig) GetTransportProxyFunc() func(*http.Request) (*url.URL, e } } -// DHCPLease is a network configuration obtained by DHCP. -type DHCPLease struct { - // from https://udhcp.busybox.net/README.udhcpc - IPAddress net.IP `env:"ip" json:"ip"` // The obtained IP - Netmask net.IP `env:"subnet" json:"netmask"` // The assigned subnet mask - Broadcast net.IP `env:"broadcast" json:"broadcast"` // The broadcast address for this network - TTL int `env:"ipttl" json:"ttl,omitempty"` // The TTL to use for this network - MTU int `env:"mtu" json:"mtu,omitempty"` // The MTU to use for this network - HostName string `env:"hostname" json:"hostname,omitempty"` // The assigned hostname - Domain string `env:"domain" json:"domain,omitempty"` // The domain name of the network - SearchList []string `env:"search" json:"search_list,omitempty"` // The search list for the network - BootPNextServer net.IP `env:"siaddr" json:"bootp_next_server,omitempty"` // The bootp next server option - BootPServerName string `env:"sname" json:"bootp_server_name,omitempty"` // The bootp server name option - BootPFile string `env:"boot_file" json:"bootp_file,omitempty"` // The bootp boot file option - Timezone string `env:"timezone" json:"timezone,omitempty"` // Offset in seconds from UTC - Routers []net.IP `env:"router" json:"routers,omitempty"` // A list of routers - DNS []net.IP `env:"dns" json:"dns_servers,omitempty"` // A list of DNS servers - NTPServers []net.IP `env:"ntpsrv" json:"ntp_servers,omitempty"` // A list of NTP servers - LPRServers []net.IP `env:"lprsvr" json:"lpr_servers,omitempty"` // A list of LPR servers - TimeServers []net.IP `env:"timesvr" json:"_time_servers,omitempty"` // A list of time servers (obsolete) - IEN116NameServers []net.IP `env:"namesvr" json:"_name_servers,omitempty"` // A list of IEN 116 name servers (obsolete) - LogServers []net.IP `env:"logsvr" json:"_log_servers,omitempty"` // A list of MIT-LCS UDP log servers (obsolete) - CookieServers []net.IP `env:"cookiesvr" json:"_cookie_servers,omitempty"` // A list of RFC 865 cookie servers (obsolete) - WINSServers []net.IP `env:"wins" json:"_wins_servers,omitempty"` // A list of WINS servers - SwapServer net.IP `env:"swapsvr" json:"_swap_server,omitempty"` // The IP address of the client's swap server - BootSize int `env:"bootsize" json:"bootsize,omitempty"` // The length in 512 octect blocks of the bootfile - RootPath string `env:"rootpath" json:"root_path,omitempty"` // The path name of the client's root disk - LeaseTime time.Duration `env:"lease" json:"lease,omitempty"` // The lease time, in seconds - RenewalTime time.Duration `env:"renewal" json:"renewal,omitempty"` // The renewal time, in seconds - RebindingTime time.Duration `env:"rebinding" json:"rebinding,omitempty"` // The rebinding time, in seconds - DHCPType string `env:"dhcptype" json:"dhcp_type,omitempty"` // DHCP message type (safely ignored) - ServerID string `env:"serverid" json:"server_id,omitempty"` // The IP of the server - Message string `env:"message" json:"reason,omitempty"` // Reason for a DHCPNAK - TFTPServerName string `env:"tftp" json:"tftp,omitempty"` // The TFTP server name - BootFileName string `env:"bootfile" json:"bootfile,omitempty"` // The boot file name - Uptime time.Duration `env:"uptime" json:"uptime,omitempty"` // The uptime of the device when the lease was obtained, in seconds - ClassIdentifier string `env:"classid" json:"class_identifier,omitempty"` // The class identifier - LeaseExpiry *time.Time `json:"lease_expiry,omitempty"` // The expiry time of the lease - - InterfaceName string `json:"interface_name,omitempty"` // The name of the interface - DHCPClient string `json:"dhcp_client,omitempty"` // The DHCP client that obtained the lease -} - // InterfaceState represents the current state of a network interface type InterfaceState struct { InterfaceName string `json:"interface_name"` @@ -175,32 +134,3 @@ type NetworkConfigInterface interface { IPv4Addresses() []IPAddress IPv6Addresses() []IPAddress } - -// IsIPv6 returns true if the DHCP lease is for an IPv6 address -func (d *DHCPLease) IsIPv6() bool { - return d.IPAddress.To4() == nil -} - -// IPMask returns the IP mask for the DHCP lease -func (d *DHCPLease) IPMask() net.IPMask { - if d.IsIPv6() { - // TODO: not implemented - return nil - } - - mask := net.ParseIP(d.Netmask.String()) - return net.IPv4Mask(mask[12], mask[13], mask[14], mask[15]) -} - -// IPNet returns the IP net for the DHCP lease -func (d *DHCPLease) IPNet() *net.IPNet { - if d.IsIPv6() { - // TODO: not implemented - return nil - } - - return &net.IPNet{ - IP: d.IPAddress, - Mask: d.IPMask(), - } -} diff --git a/network.go b/network.go index 5e3496bb..ce8f60f8 100644 --- a/network.go +++ b/network.go @@ -67,6 +67,10 @@ func networkStateChanged(_ string, state types.InterfaceState) { // do not block the main thread go waitCtrlAndRequestDisplayUpdate(true, "network_state_changed") + if currentSession != nil { + writeJSONRPCEvent("networkState", state.ToRpcInterfaceState(), currentSession) + } + if state.Online { networkLogger.Info().Msg("network state changed to online, triggering time sync") triggerTimeSyncOnNetworkStateChange() @@ -108,9 +112,9 @@ func initNetwork() error { return nil } -func rpcGetNetworkState() *types.InterfaceState { +func rpcGetNetworkState() *types.RpcInterfaceState { state, _ := networkManager.GetInterfaceState(NetIfName) - return state + return state.ToRpcInterfaceState() } func rpcGetNetworkSettings() *RpcNetworkSettings { diff --git a/pkg/nmlite/interface.go b/pkg/nmlite/interface.go index b935fc9a..93d7f840 100644 --- a/pkg/nmlite/interface.go +++ b/pkg/nmlite/interface.go @@ -683,116 +683,6 @@ func (im *InterfaceManager) monitorInterfaceState() { } -// updateInterfaceState updates the current interface state -func (im *InterfaceManager) updateInterfaceState() error { - nl, err := im.link() - if err != nil { - return fmt.Errorf("failed to get interface: %w", err) - } - - // Check if state changed - stateChanged := false - - attrs := nl.Attrs() - isUp := attrs.OperState == netlink.OperUp - - // check if the interface has unicast addresses - isOnline := isUp && nl.HasGlobalUnicastAddress() - - // We should release the lock before calling the callbacks - // to avoid deadlocks - im.stateMu.Lock() - if im.state.Up != isUp { - im.state.Up = isUp - stateChanged = true - } - - if im.state.Online != isOnline { - im.state.Online = isOnline - stateChanged = true - } - - if im.state.MACAddress != attrs.HardwareAddr.String() { - im.state.MACAddress = attrs.HardwareAddr.String() - stateChanged = true - } - - // Update IP addresses - if err := im.updateIPAddresses(nl); err != nil { - im.logger.Error().Err(err).Msg("failed to update IP addresses") - } - - im.state.LastUpdated = time.Now() - im.stateMu.Unlock() - - // Notify callback if state changed - if stateChanged && im.onStateChange != nil { - im.logger.Debug().Interface("state", im.state).Msg("notifying state change") - im.onStateChange(*im.state) - } - - return nil -} - -// updateIPAddresses updates the IP addresses in the state -func (im *InterfaceManager) updateIPAddresses(nl *link.Link) error { - if err := nl.Refresh(); err != nil { - return fmt.Errorf("failed to refresh link: %w", err) - } - - addrs, err := nl.AddrList(link.AfUnspec) - if err != nil { - return fmt.Errorf("failed to get addresses: %w", err) - } - - var ( - ipv4Addresses []string - ipv6Addresses []types.IPv6Address - ipv4Addr, ipv6Addr string - ipv6LinkLocal string - ipv4Ready, ipv6Ready = false, false - ) - - for _, addr := range addrs { - im.logger.Debug(). - IPAddr("address", addr.IP). - Msg("checking address") - if addr.IP.To4() != nil { - // IPv4 address - ipv4Addresses = append(ipv4Addresses, addr.IPNet.String()) - if ipv4Addr == "" { - ipv4Addr = addr.IP.String() - ipv4Ready = true - } - } else if addr.IP.To16() != nil { - // IPv6 address - if addr.IP.IsLinkLocalUnicast() { - ipv6LinkLocal = addr.IP.String() - } else if addr.IP.IsGlobalUnicast() { - ipv6Addresses = append(ipv6Addresses, types.IPv6Address{ - Address: addr.IP, - Prefix: *addr.IPNet, - Scope: addr.Scope, - }) - if ipv6Addr == "" { - ipv6Addr = addr.IP.String() - ipv6Ready = true - } - } - } - } - - im.state.IPv4Addresses = ipv4Addresses - im.state.IPv6Addresses = ipv6Addresses - im.state.IPv6LinkLocal = ipv6LinkLocal - im.state.IPv4Address = ipv4Addr - im.state.IPv6Address = ipv6Addr - im.state.IPv4Ready = ipv4Ready - im.state.IPv6Ready = ipv6Ready - - return nil -} - // updateStateFromDHCPLease updates the state from a DHCP lease func (im *InterfaceManager) updateStateFromDHCPLease(lease *types.DHCPLease) { im.stateMu.Lock() diff --git a/pkg/nmlite/interface_state.go b/pkg/nmlite/interface_state.go new file mode 100644 index 00000000..96fbdb61 --- /dev/null +++ b/pkg/nmlite/interface_state.go @@ -0,0 +1,152 @@ +package nmlite + +import ( + "fmt" + "time" + + "github.com/jetkvm/kvm/internal/network/types" + "github.com/jetkvm/kvm/pkg/nmlite/link" + "github.com/vishvananda/netlink" +) + +// updateInterfaceState updates the current interface state +func (im *InterfaceManager) updateInterfaceState() error { + nl, err := im.link() + if err != nil { + return fmt.Errorf("failed to get interface: %w", err) + } + + var stateChanged bool + + attrs := nl.Attrs() + + // We should release the lock before calling the callbacks + // to avoid deadlocks + im.stateMu.Lock() + + // Check if the interface is up + isUp := attrs.OperState == netlink.OperUp + if im.state.Up != isUp { + im.state.Up = isUp + stateChanged = true + } + + // Check if the interface is online + isOnline := isUp && nl.HasGlobalUnicastAddress() + if im.state.Online != isOnline { + im.state.Online = isOnline + stateChanged = true + } + + // Check if the MAC address has changed + if im.state.MACAddress != attrs.HardwareAddr.String() { + im.state.MACAddress = attrs.HardwareAddr.String() + stateChanged = true + } + + // Update IP addresses + if ipChanged, err := im.updateInterfaceStateAddresses(nl); err != nil { + im.logger.Error().Err(err).Msg("failed to update IP addresses") + } else if ipChanged { + stateChanged = true + } + + im.state.LastUpdated = time.Now() + im.stateMu.Unlock() + + // Notify callback if state changed + if stateChanged && im.onStateChange != nil { + im.logger.Debug().Interface("state", im.state).Msg("notifying state change") + im.onStateChange(*im.state) + } + + return nil +} + +// updateIPAddresses updates the IP addresses in the state +func (im *InterfaceManager) updateInterfaceStateAddresses(nl *link.Link) (bool, error) { + addrs, err := nl.AddrList(link.AfUnspec) + if err != nil { + return false, fmt.Errorf("failed to get addresses: %w", err) + } + + var ( + ipv4Addresses []string + ipv6Addresses []types.IPv6Address + ipv4Addr, ipv6Addr string + ipv6LinkLocal string + ipv4Ready, ipv6Ready = false, false + stateChanged = false + ) + + for _, addr := range addrs { + im.logger.Debug(). + IPAddr("address", addr.IP). + Msg("checking address") + if addr.IP.To4() != nil { + // IPv4 address + ipv4Addresses = append(ipv4Addresses, addr.IPNet.String()) + if ipv4Addr == "" { + ipv4Addr = addr.IP.String() + ipv4Ready = true + } + continue + } + + // IPv6 address (if it's not an IPv4 address, it must be an IPv6 address) + if addr.IP.IsLinkLocalUnicast() { + ipv6LinkLocal = addr.IP.String() + continue + } else if !addr.IP.IsGlobalUnicast() { + continue + } + + ipv6Addresses = append(ipv6Addresses, types.IPv6Address{ + Address: addr.IP, + Prefix: *addr.IPNet, + Scope: addr.Scope, + ValidLifetime: lifetimeToTime(addr.ValidLft), + PreferredLifetime: lifetimeToTime(addr.PreferedLft), + }) + if ipv6Addr == "" { + ipv6Addr = addr.IP.String() + ipv6Ready = true + } + } + + if !compareStringSlices(im.state.IPv4Addresses, ipv4Addresses) { + im.state.IPv4Addresses = ipv4Addresses + stateChanged = true + } + + if !compareIPv6AddressSlices(im.state.IPv6Addresses, ipv6Addresses) { + im.state.IPv6Addresses = ipv6Addresses + stateChanged = true + } + + if im.state.IPv4Address != ipv4Addr { + im.state.IPv4Address = ipv4Addr + stateChanged = true + } + + if im.state.IPv6Address != ipv6Addr { + im.state.IPv6Address = ipv6Addr + stateChanged = true + } + if im.state.IPv6LinkLocal != ipv6LinkLocal { + im.state.IPv6LinkLocal = ipv6LinkLocal + stateChanged = true + } + + if im.state.IPv4Ready != ipv4Ready { + im.state.IPv4Ready = ipv4Ready + stateChanged = true + } + + if im.state.IPv6Ready != ipv6Ready { + im.state.IPv6Ready = ipv6Ready + stateChanged = true + } + + return stateChanged, nil +} diff --git a/pkg/nmlite/utils.go b/pkg/nmlite/utils.go new file mode 100644 index 00000000..15d5624c --- /dev/null +++ b/pkg/nmlite/utils.go @@ -0,0 +1,70 @@ +package nmlite + +import ( + "sort" + "time" + + "github.com/jetkvm/kvm/internal/network/types" +) + +func lifetimeToTime(lifetime int) *time.Time { + if lifetime == 0 { + return nil + } + + // Check for infinite lifetime (0xFFFFFFFF = 4294967295) + // This is used for static/permanent addresses + // Use uint32 to avoid int overflow on 32-bit systems + const infiniteLifetime uint32 = 0xFFFFFFFF + if uint32(lifetime) == infiniteLifetime || lifetime < 0 { + return nil // Infinite lifetime - no expiration + } + + // For finite lifetimes (SLAAC addresses) + t := time.Now().Add(time.Duration(lifetime) * time.Second) + return &t +} + +func compareStringSlices(a, b []string) bool { + if len(a) != len(b) { + return false + } + + sort.Strings(a) + sort.Strings(b) + + for i := range a { + if a[i] != b[i] { + return false + } + } + + return true +} + +func compareIPv6AddressSlices(a, b []types.IPv6Address) bool { + if len(a) != len(b) { + return false + } + + sort.SliceStable(a, func(i, j int) bool { + return a[i].Address.String() < b[j].Address.String() + }) + sort.SliceStable(b, func(i, j int) bool { + return b[i].Address.String() < a[j].Address.String() + }) + + for i := range a { + if a[i].Address.String() != b[i].Address.String() { + return false + } + if a[i].Prefix.String() != b[i].Prefix.String() { + return false + } + // we don't compare the lifetimes because they are not always same + if a[i].Scope != b[i].Scope { + return false + } + } + return true +} diff --git a/ui/src/routes/devices.$id.settings.network.tsx b/ui/src/routes/devices.$id.settings.network.tsx index 073234f4..621ff59c 100644 --- a/ui/src/routes/devices.$id.settings.network.tsx +++ b/ui/src/routes/devices.$id.settings.network.tsx @@ -8,7 +8,7 @@ import validator from "validator"; import { ConfirmDialog } from "@/components/ConfirmDialog"; import { SelectMenuBasic } from "@/components/SelectMenuBasic"; import { SettingsPageHeader } from "@/components/SettingsPageheader"; -import { NetworkSettings, NetworkState, useRTCStore } from "@/hooks/stores"; +import { NetworkSettings, NetworkState, useNetworkStateStore, useRTCStore } from "@/hooks/stores"; import notifications from "@/notifications"; import { getNetworkSettings, getNetworkState } from "@/utils/jsonrpc"; import { Button } from "@components/Button"; @@ -75,7 +75,8 @@ export function LifeTimeLabel({ lifetime }: { lifetime: string }) { export default function SettingsNetworkRoute() { const { send } = useJsonRpc(); - const [networkState, setNetworkState] = useState(null); + const networkState = useNetworkStateStore(state => state); + const setNetworkState = useNetworkStateStore(state => state.setNetworkState); // Some input needs direct state management. Mostly options that open more details const [customDomain, setCustomDomain] = useState(""); diff --git a/ui/src/routes/devices.$id.tsx b/ui/src/routes/devices.$id.tsx index a1ace077..05bf8e0a 100644 --- a/ui/src/routes/devices.$id.tsx +++ b/ui/src/routes/devices.$id.tsx @@ -123,9 +123,9 @@ export default function KvmIdRoute() { const params = useParams() as { id: string }; const { sidebarView, setSidebarView, disableVideoFocusTrap } = useUiStore(); - const [ queryParams, setQueryParams ] = useSearchParams(); + const [queryParams, setQueryParams] = useSearchParams(); - const { + const { peerConnection, setPeerConnection, peerConnectionState, setPeerConnectionState, setMediaStream, @@ -597,10 +597,10 @@ export default function KvmIdRoute() { }); }, 10000); - const { setNetworkState} = useNetworkStateStore(); + const { setNetworkState } = useNetworkStateStore(); const { setHdmiState } = useVideoStore(); - const { - keyboardLedState, setKeyboardLedState, + const { + keyboardLedState, setKeyboardLedState, keysDownState, setKeysDownState, setUsbState, } = useHidStore(); const setHidRpcDisabled = useRTCStore(state => state.setHidRpcDisabled); @@ -756,7 +756,7 @@ export default function KvmIdRoute() { if (location.pathname !== "/other-session") navigateTo("/"); }, [navigateTo, location.pathname]); - const { appVersion, getLocalVersion} = useVersion(); + const { appVersion, getLocalVersion } = useVersion(); useEffect(() => { if (appVersion) return;