mirror of https://github.com/jetkvm/kvm.git
feat: add network settings tab
This commit is contained in:
parent
eb8bac4013
commit
73b83557af
5
cloud.go
5
cloud.go
|
@ -165,9 +165,12 @@ func setCloudConnectionState(state CloudConnectionState) {
|
||||||
state = CloudConnectionStateNotConfigured
|
state = CloudConnectionStateNotConfigured
|
||||||
}
|
}
|
||||||
|
|
||||||
|
previousState := cloudConnectionState
|
||||||
cloudConnectionState = state
|
cloudConnectionState = state
|
||||||
|
|
||||||
go waitCtrlAndRequestDisplayUpdate()
|
go waitCtrlAndRequestDisplayUpdate(
|
||||||
|
previousState != state,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func wsResetMetrics(established bool, sourceType string, source string) {
|
func wsResetMetrics(established bool, sourceType string, source string) {
|
||||||
|
|
12
display.go
12
display.go
|
@ -179,7 +179,7 @@ var (
|
||||||
waitDisplayUpdate = sync.Mutex{}
|
waitDisplayUpdate = sync.Mutex{}
|
||||||
)
|
)
|
||||||
|
|
||||||
func requestDisplayUpdate() {
|
func requestDisplayUpdate(shouldWakeDisplay bool) {
|
||||||
displayUpdateLock.Lock()
|
displayUpdateLock.Lock()
|
||||||
defer displayUpdateLock.Unlock()
|
defer displayUpdateLock.Unlock()
|
||||||
|
|
||||||
|
@ -188,19 +188,21 @@ func requestDisplayUpdate() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
go func() {
|
go func() {
|
||||||
wakeDisplay(false)
|
if shouldWakeDisplay {
|
||||||
|
wakeDisplay(false)
|
||||||
|
}
|
||||||
displayLogger.Debug().Msg("display updating")
|
displayLogger.Debug().Msg("display updating")
|
||||||
//TODO: only run once regardless how many pending updates
|
//TODO: only run once regardless how many pending updates
|
||||||
updateDisplay()
|
updateDisplay()
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
func waitCtrlAndRequestDisplayUpdate() {
|
func waitCtrlAndRequestDisplayUpdate(shouldWakeDisplay bool) {
|
||||||
waitDisplayUpdate.Lock()
|
waitDisplayUpdate.Lock()
|
||||||
defer waitDisplayUpdate.Unlock()
|
defer waitDisplayUpdate.Unlock()
|
||||||
|
|
||||||
waitCtrlClientConnected()
|
waitCtrlClientConnected()
|
||||||
requestDisplayUpdate()
|
requestDisplayUpdate(shouldWakeDisplay)
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateStaticContents() {
|
func updateStaticContents() {
|
||||||
|
@ -376,7 +378,7 @@ func init() {
|
||||||
displayLogger.Info().Msg("display inited")
|
displayLogger.Info().Msg("display inited")
|
||||||
startBacklightTickers()
|
startBacklightTickers()
|
||||||
wakeDisplay(true)
|
wakeDisplay(true)
|
||||||
requestDisplayUpdate()
|
requestDisplayUpdate(true)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
go watchTsEvents()
|
go watchTsEvents()
|
||||||
|
|
|
@ -1,9 +1,11 @@
|
||||||
package udhcpc
|
package udhcpc
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -41,6 +43,8 @@ type Lease struct {
|
||||||
Message string `env:"message" json:"reason,omitempty"` // Reason for a DHCPNAK
|
Message string `env:"message" json:"reason,omitempty"` // Reason for a DHCPNAK
|
||||||
TFTPServerName string `env:"tftp" json:"tftp,omitempty"` // The TFTP server name
|
TFTPServerName string `env:"tftp" json:"tftp,omitempty"` // The TFTP server name
|
||||||
BootFileName string `env:"bootfile" json:"bootfile,omitempty"` // The boot file 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
|
||||||
|
LeaseExpiry *time.Time `json:"lease_expiry,omitempty"` // The expiry time of the lease
|
||||||
isEmpty map[string]bool
|
isEmpty map[string]bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -60,6 +64,40 @@ func (l *Lease) ToJSON() string {
|
||||||
return string(json)
|
return string(json)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (l *Lease) SetLeaseExpiry() (time.Time, error) {
|
||||||
|
if l.Uptime == 0 || l.LeaseTime == 0 {
|
||||||
|
return time.Time{}, fmt.Errorf("uptime or lease time isn't set")
|
||||||
|
}
|
||||||
|
|
||||||
|
// get the uptime of the device
|
||||||
|
|
||||||
|
file, err := os.Open("/proc/uptime")
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}, fmt.Errorf("failed to open uptime file: %w", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
var uptime time.Duration
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(file)
|
||||||
|
for scanner.Scan() {
|
||||||
|
text := scanner.Text()
|
||||||
|
parts := strings.Split(text, " ")
|
||||||
|
uptime, err = time.ParseDuration(parts[0] + "s")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return time.Time{}, fmt.Errorf("failed to parse uptime: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
relativeLeaseRemaining := (l.Uptime + l.LeaseTime) - uptime
|
||||||
|
leaseExpiry := time.Now().Add(relativeLeaseRemaining)
|
||||||
|
|
||||||
|
l.LeaseExpiry = &leaseExpiry
|
||||||
|
|
||||||
|
return leaseExpiry, nil
|
||||||
|
}
|
||||||
|
|
||||||
func UnmarshalDHCPCLease(lease *Lease, str string) error {
|
func UnmarshalDHCPCLease(lease *Lease, str string) error {
|
||||||
// parse the lease file as a map
|
// parse the lease file as a map
|
||||||
data := make(map[string]string)
|
data := make(map[string]string)
|
||||||
|
|
|
@ -4,6 +4,7 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/fsnotify/fsnotify"
|
"github.com/fsnotify/fsnotify"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
|
@ -140,6 +141,17 @@ func (c *DHCPClient) loadLeaseFile() error {
|
||||||
msg = "udhcpc lease loaded"
|
msg = "udhcpc lease loaded"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
leaseExpiry, err := lease.SetLeaseExpiry()
|
||||||
|
if err != nil {
|
||||||
|
c.logger.Error().Err(err).Msg("failed to get dhcp lease expiry")
|
||||||
|
} else {
|
||||||
|
expiresIn := leaseExpiry.Sub(time.Now())
|
||||||
|
c.logger.Info().
|
||||||
|
Interface("expiry", leaseExpiry).
|
||||||
|
Str("expiresIn", expiresIn.String()).
|
||||||
|
Msg("current dhcp lease expiry time calculated")
|
||||||
|
}
|
||||||
|
|
||||||
c.onLeaseChange(lease)
|
c.onLeaseChange(lease)
|
||||||
|
|
||||||
c.logger.Info().
|
c.logger.Info().
|
||||||
|
|
|
@ -961,6 +961,7 @@ var rpcHandlers = map[string]RPCHandler{
|
||||||
"deregisterDevice": {Func: rpcDeregisterDevice},
|
"deregisterDevice": {Func: rpcDeregisterDevice},
|
||||||
"getCloudState": {Func: rpcGetCloudState},
|
"getCloudState": {Func: rpcGetCloudState},
|
||||||
"getNetworkState": {Func: rpcGetNetworkState},
|
"getNetworkState": {Func: rpcGetNetworkState},
|
||||||
|
"getNetworkSettings": {Func: rpcGetNetworkSettings},
|
||||||
"renewDHCPLease": {Func: rpcRenewDHCPLease},
|
"renewDHCPLease": {Func: rpcRenewDHCPLease},
|
||||||
"keyboardReport": {Func: rpcKeyboardReport, Params: []string{"modifier", "keys"}},
|
"keyboardReport": {Func: rpcKeyboardReport, Params: []string{"modifier", "keys"}},
|
||||||
"absMouseReport": {Func: rpcAbsMouseReport, Params: []string{"x", "y", "buttons"}},
|
"absMouseReport": {Func: rpcAbsMouseReport, Params: []string{"x", "y", "buttons"}},
|
||||||
|
|
159
network.go
159
network.go
|
@ -29,11 +29,22 @@ const (
|
||||||
DhcpTargetStateRelease
|
DhcpTargetStateRelease
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type IPv6Address struct {
|
||||||
|
Address net.IP `json:"address"`
|
||||||
|
Prefix net.IPNet `json:"prefix"`
|
||||||
|
ValidLifetime *time.Time `json:"valid_lifetime"`
|
||||||
|
PreferredLifetime *time.Time `json:"preferred_lifetime"`
|
||||||
|
Scope int `json:"scope"`
|
||||||
|
}
|
||||||
|
|
||||||
type NetworkInterfaceState struct {
|
type NetworkInterfaceState struct {
|
||||||
interfaceName string
|
interfaceName string
|
||||||
interfaceUp bool
|
interfaceUp bool
|
||||||
ipv4Addr *net.IP
|
ipv4Addr *net.IP
|
||||||
|
ipv4Addresses []string
|
||||||
ipv6Addr *net.IP
|
ipv6Addr *net.IP
|
||||||
|
ipv6Addresses []IPv6Address
|
||||||
|
ipv6LinkLocal *net.IP
|
||||||
macAddr *net.HardwareAddr
|
macAddr *net.HardwareAddr
|
||||||
|
|
||||||
l *zerolog.Logger
|
l *zerolog.Logger
|
||||||
|
@ -47,12 +58,65 @@ type NetworkInterfaceState struct {
|
||||||
checked bool
|
checked bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type NetworkConfig struct {
|
||||||
|
Hostname string `json:"hostname,omitempty"`
|
||||||
|
Domain string `json:"domain,omitempty"`
|
||||||
|
|
||||||
|
IPv4Mode string `json:"ipv4_mode" one_of:"dhcp,static,disabled" default:"dhcp"`
|
||||||
|
IPv4Static struct {
|
||||||
|
Address string `json:"address" validate_type:"ipv4"`
|
||||||
|
Netmask string `json:"netmask" validate_type:"ipv4"`
|
||||||
|
Gateway string `json:"gateway" validate_type:"ipv4"`
|
||||||
|
DNS []string `json:"dns" validate_type:"ipv4"`
|
||||||
|
} `json:"ipv4_static,omitempty" required_if:"ipv4_mode,static"`
|
||||||
|
|
||||||
|
IPv6Mode string `json:"ipv6_mode" one_of:"slaac,dhcpv6,slaac_and_dhcpv6,static,link_local,disabled" default:"slaac"`
|
||||||
|
IPv6Static struct {
|
||||||
|
Address string `json:"address" validate_type:"ipv6"`
|
||||||
|
Netmask string `json:"netmask" validate_type:"ipv6"`
|
||||||
|
Gateway string `json:"gateway" validate_type:"ipv6"`
|
||||||
|
DNS []string `json:"dns" validate_type:"ipv6"`
|
||||||
|
} `json:"ipv6_static,omitempty" required_if:"ipv6_mode,static"`
|
||||||
|
|
||||||
|
LLDPMode string `json:"lldp_mode,omitempty" one_of:"disabled,basic,all" default:"basic"`
|
||||||
|
LLDPTxTLVs []string `json:"lldp_tx_tlvs,omitempty" one_of:"chassis,port,system,vlan" default:"chassis,port,system,vlan"`
|
||||||
|
MDNSMode string `json:"mdns_mode,omitempty" one_of:"disabled,auto,ipv4_only,ipv6_only" default:"auto"`
|
||||||
|
TimeSyncMode string `json:"time_sync_mode,omitempty" one_of:"ntp_only,ntp_and_http,http_only,custom" default:"ntp_and_http"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RpcIPv6Address struct {
|
||||||
|
Address string `json:"address"`
|
||||||
|
ValidLifetime *time.Time `json:"valid_lifetime,omitempty"`
|
||||||
|
PreferredLifetime *time.Time `json:"preferred_lifetime,omitempty"`
|
||||||
|
Scope int `json:"scope"`
|
||||||
|
}
|
||||||
|
|
||||||
type RpcNetworkState struct {
|
type RpcNetworkState struct {
|
||||||
InterfaceName string `json:"interface_name"`
|
InterfaceName string `json:"interface_name"`
|
||||||
MacAddress string `json:"mac_address"`
|
MacAddress string `json:"mac_address"`
|
||||||
IPv4 string `json:"ipv4,omitempty"`
|
IPv4 string `json:"ipv4,omitempty"`
|
||||||
IPv6 string `json:"ipv6,omitempty"`
|
IPv6 string `json:"ipv6,omitempty"`
|
||||||
DHCPLease *udhcpc.Lease `json:"dhcp_lease,omitempty"`
|
IPv6LinkLocal string `json:"ipv6_link_local,omitempty"`
|
||||||
|
IPv4Addresses []string `json:"ipv4_addresses,omitempty"`
|
||||||
|
IPv6Addresses []RpcIPv6Address `json:"ipv6_addresses,omitempty"`
|
||||||
|
DHCPLease *udhcpc.Lease `json:"dhcp_lease,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RpcNetworkSettings struct {
|
||||||
|
IPv4Mode string `json:"ipv4_mode,omitempty"`
|
||||||
|
IPv6Mode string `json:"ipv6_mode,omitempty"`
|
||||||
|
LLDPMode string `json:"lldp_mode,omitempty"`
|
||||||
|
LLDPTxTLVs []string `json:"lldp_tx_tlvs,omitempty"`
|
||||||
|
MDNSMode string `json:"mdns_mode,omitempty"`
|
||||||
|
TimeSyncMode string `json:"time_sync_mode,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func lifetimeToTime(lifetime int) *time.Time {
|
||||||
|
if lifetime == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
t := time.Now().Add(time.Duration(lifetime) * time.Second)
|
||||||
|
return &t
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *NetworkInterfaceState) IsUp() bool {
|
func (s *NetworkInterfaceState) IsUp() bool {
|
||||||
|
@ -115,13 +179,13 @@ func NewNetworkInterfaceState(ifname string) *NetworkInterfaceState {
|
||||||
onStateChange: func(state *NetworkInterfaceState) {
|
onStateChange: func(state *NetworkInterfaceState) {
|
||||||
go func() {
|
go func() {
|
||||||
waitCtrlClientConnected()
|
waitCtrlClientConnected()
|
||||||
requestDisplayUpdate()
|
requestDisplayUpdate(true)
|
||||||
}()
|
}()
|
||||||
},
|
},
|
||||||
onInitialCheck: func(state *NetworkInterfaceState) {
|
onInitialCheck: func(state *NetworkInterfaceState) {
|
||||||
go func() {
|
go func() {
|
||||||
waitCtrlClientConnected()
|
waitCtrlClientConnected()
|
||||||
requestDisplayUpdate()
|
requestDisplayUpdate(true)
|
||||||
}()
|
}()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -140,7 +204,18 @@ func NewNetworkInterfaceState(ifname string) *NetworkInterfaceState {
|
||||||
PidFile: dhcpPidFile,
|
PidFile: dhcpPidFile,
|
||||||
Logger: &logger,
|
Logger: &logger,
|
||||||
OnLeaseChange: func(lease *udhcpc.Lease) {
|
OnLeaseChange: func(lease *udhcpc.Lease) {
|
||||||
_, _ = s.update()
|
_, err := s.update()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error().Err(err).Msg("failed to update network state")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if currentSession == nil {
|
||||||
|
logger.Info().Msg("No active RPC session, skipping network state update")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJSONRPCEvent("networkState", rpcGetNetworkState(), currentSession)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -196,8 +271,11 @@ func (s *NetworkInterfaceState) update() (DhcpTargetState, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ipv4Addresses = make([]net.IP, 0)
|
ipv4Addresses = make([]net.IP, 0)
|
||||||
ipv6Addresses = make([]net.IP, 0)
|
ipv4AddressesString = make([]string, 0)
|
||||||
|
ipv6Addresses = make([]IPv6Address, 0)
|
||||||
|
ipv6AddressesString = make([]string, 0)
|
||||||
|
ipv6LinkLocal *net.IP
|
||||||
)
|
)
|
||||||
|
|
||||||
for _, addr := range addrs {
|
for _, addr := range addrs {
|
||||||
|
@ -215,9 +293,15 @@ func (s *NetworkInterfaceState) update() (DhcpTargetState, error) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
ipv4Addresses = append(ipv4Addresses, addr.IP)
|
ipv4Addresses = append(ipv4Addresses, addr.IP)
|
||||||
|
ipv4AddressesString = append(ipv4AddressesString, addr.IPNet.String())
|
||||||
} else if addr.IP.To16() != nil {
|
} else if addr.IP.To16() != nil {
|
||||||
scopedLogger := s.l.With().Str("ipv6", addr.IP.String()).Logger()
|
scopedLogger := s.l.With().Str("ipv6", addr.IP.String()).Logger()
|
||||||
// check if it's a link local address
|
// check if it's a link local address
|
||||||
|
if addr.IP.IsLinkLocalUnicast() {
|
||||||
|
ipv6LinkLocal = &addr.IP
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
if !addr.IP.IsGlobalUnicast() {
|
if !addr.IP.IsGlobalUnicast() {
|
||||||
scopedLogger.Trace().Msg("not a global unicast address, skipping")
|
scopedLogger.Trace().Msg("not a global unicast address, skipping")
|
||||||
continue
|
continue
|
||||||
|
@ -231,7 +315,14 @@ func (s *NetworkInterfaceState) update() (DhcpTargetState, error) {
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
ipv6Addresses = append(ipv6Addresses, addr.IP)
|
ipv6Addresses = append(ipv6Addresses, IPv6Address{
|
||||||
|
Address: addr.IP,
|
||||||
|
Prefix: *addr.IPNet,
|
||||||
|
ValidLifetime: lifetimeToTime(addr.ValidLft),
|
||||||
|
PreferredLifetime: lifetimeToTime(addr.PreferedLft),
|
||||||
|
Scope: addr.Scope,
|
||||||
|
})
|
||||||
|
ipv6AddressesString = append(ipv6AddressesString, addr.IPNet.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -250,11 +341,28 @@ func (s *NetworkInterfaceState) update() (DhcpTargetState, error) {
|
||||||
changed = true
|
changed = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
s.ipv4Addresses = ipv4AddressesString
|
||||||
|
|
||||||
|
if ipv6LinkLocal != nil {
|
||||||
|
if s.ipv6LinkLocal == nil || s.ipv6LinkLocal.String() != ipv6LinkLocal.String() {
|
||||||
|
scopedLogger := s.l.With().Str("ipv6", ipv6LinkLocal.String()).Logger()
|
||||||
|
if s.ipv6LinkLocal != nil {
|
||||||
|
scopedLogger.Info().
|
||||||
|
Str("old_ipv6", s.ipv6LinkLocal.String()).
|
||||||
|
Msg("IPv6 link local address changed")
|
||||||
|
} else {
|
||||||
|
scopedLogger.Info().Msg("IPv6 link local address found")
|
||||||
|
}
|
||||||
|
s.ipv6LinkLocal = ipv6LinkLocal
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.ipv6Addresses = ipv6Addresses
|
||||||
|
|
||||||
if len(ipv6Addresses) > 0 {
|
if len(ipv6Addresses) > 0 {
|
||||||
// compare the addresses to see if there's a change
|
// compare the addresses to see if there's a change
|
||||||
if s.ipv6Addr == nil || s.ipv6Addr.String() != ipv6Addresses[0].String() {
|
if s.ipv6Addr == nil || s.ipv6Addr.String() != ipv6Addresses[0].Address.String() {
|
||||||
scopedLogger := s.l.With().Str("ipv6", ipv6Addresses[0].String()).Logger()
|
scopedLogger := s.l.With().Str("ipv6", ipv6Addresses[0].Address.String()).Logger()
|
||||||
if s.ipv6Addr != nil {
|
if s.ipv6Addr != nil {
|
||||||
scopedLogger.Info().
|
scopedLogger.Info().
|
||||||
Str("old_ipv6", s.ipv6Addr.String()).
|
Str("old_ipv6", s.ipv6Addr.String()).
|
||||||
|
@ -262,7 +370,7 @@ func (s *NetworkInterfaceState) update() (DhcpTargetState, error) {
|
||||||
} else {
|
} else {
|
||||||
scopedLogger.Info().Msg("IPv6 address found")
|
scopedLogger.Info().Msg("IPv6 address found")
|
||||||
}
|
}
|
||||||
s.ipv6Addr = &ipv6Addresses[0]
|
s.ipv6Addr = &ipv6Addresses[0].Address
|
||||||
changed = true
|
changed = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -313,15 +421,38 @@ func (s *NetworkInterfaceState) HandleLinkUpdate(update netlink.LinkUpdate) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func rpcGetNetworkState() RpcNetworkState {
|
func rpcGetNetworkState() RpcNetworkState {
|
||||||
|
ipv6Addresses := make([]RpcIPv6Address, 0)
|
||||||
|
for _, addr := range networkState.ipv6Addresses {
|
||||||
|
ipv6Addresses = append(ipv6Addresses, RpcIPv6Address{
|
||||||
|
Address: addr.Prefix.String(),
|
||||||
|
ValidLifetime: addr.ValidLifetime,
|
||||||
|
PreferredLifetime: addr.PreferredLifetime,
|
||||||
|
Scope: addr.Scope,
|
||||||
|
})
|
||||||
|
}
|
||||||
return RpcNetworkState{
|
return RpcNetworkState{
|
||||||
InterfaceName: networkState.interfaceName,
|
InterfaceName: networkState.interfaceName,
|
||||||
MacAddress: networkState.macAddr.String(),
|
MacAddress: networkState.macAddr.String(),
|
||||||
IPv4: networkState.ipv4Addr.String(),
|
IPv4: networkState.ipv4Addr.String(),
|
||||||
IPv6: networkState.ipv6Addr.String(),
|
IPv6: networkState.ipv6Addr.String(),
|
||||||
|
IPv6LinkLocal: networkState.ipv6LinkLocal.String(),
|
||||||
|
IPv4Addresses: networkState.ipv4Addresses,
|
||||||
|
IPv6Addresses: ipv6Addresses,
|
||||||
DHCPLease: networkState.dhcpClient.GetLease(),
|
DHCPLease: networkState.dhcpClient.GetLease(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func rpcGetNetworkSettings() RpcNetworkSettings {
|
||||||
|
return RpcNetworkSettings{
|
||||||
|
IPv4Mode: "dhcp",
|
||||||
|
IPv6Mode: "slaac",
|
||||||
|
LLDPMode: "basic",
|
||||||
|
LLDPTxTLVs: []string{"chassis", "port", "system", "vlan"},
|
||||||
|
MDNSMode: "auto",
|
||||||
|
TimeSyncMode: "ntp_and_http",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func rpcRenewDHCPLease() error {
|
func rpcRenewDHCPLease() error {
|
||||||
if networkState == nil {
|
if networkState == nil {
|
||||||
return fmt.Errorf("network state not initialized")
|
return fmt.Errorf("network state not initialized")
|
||||||
|
|
|
@ -15,5 +15,15 @@ echo "└───────────────────────
|
||||||
|
|
||||||
# Set the environment variable and run Vite
|
# Set the environment variable and run Vite
|
||||||
echo "Starting development server with JetKVM device at: $ip_address"
|
echo "Starting development server with JetKVM device at: $ip_address"
|
||||||
|
|
||||||
|
# Check if pwd is the current directory of the script
|
||||||
|
if [ "$(pwd)" != "$(dirname "$0")" ]; then
|
||||||
|
pushd "$(dirname "$0")" > /dev/null
|
||||||
|
echo "Changed directory to: $(pwd)"
|
||||||
|
fi
|
||||||
|
|
||||||
sleep 1
|
sleep 1
|
||||||
|
|
||||||
JETKVM_PROXY_URL="ws://$ip_address" npx vite dev --mode=device
|
JETKVM_PROXY_URL="ws://$ip_address" npx vite dev --mode=device
|
||||||
|
|
||||||
|
popd > /dev/null
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
"@xterm/addon-webgl": "^0.18.0",
|
"@xterm/addon-webgl": "^0.18.0",
|
||||||
"@xterm/xterm": "^5.5.0",
|
"@xterm/xterm": "^5.5.0",
|
||||||
"cva": "^1.0.0-beta.1",
|
"cva": "^1.0.0-beta.1",
|
||||||
|
"dayjs": "^1.11.13",
|
||||||
"eslint-import-resolver-alias": "^1.1.2",
|
"eslint-import-resolver-alias": "^1.1.2",
|
||||||
"focus-trap-react": "^10.2.3",
|
"focus-trap-react": "^10.2.3",
|
||||||
"framer-motion": "^11.15.0",
|
"framer-motion": "^11.15.0",
|
||||||
|
@ -2460,6 +2461,11 @@
|
||||||
"url": "https://github.com/sponsors/ljharb"
|
"url": "https://github.com/sponsors/ljharb"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/dayjs": {
|
||||||
|
"version": "1.11.13",
|
||||||
|
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
|
||||||
|
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg=="
|
||||||
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.3.4",
|
"version": "4.3.4",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
|
||||||
|
|
|
@ -28,6 +28,7 @@
|
||||||
"@xterm/addon-webgl": "^0.18.0",
|
"@xterm/addon-webgl": "^0.18.0",
|
||||||
"@xterm/xterm": "^5.5.0",
|
"@xterm/xterm": "^5.5.0",
|
||||||
"cva": "^1.0.0-beta.1",
|
"cva": "^1.0.0-beta.1",
|
||||||
|
"dayjs": "^1.11.13",
|
||||||
"eslint-import-resolver-alias": "^1.1.2",
|
"eslint-import-resolver-alias": "^1.1.2",
|
||||||
"focus-trap-react": "^10.2.3",
|
"focus-trap-react": "^10.2.3",
|
||||||
"framer-motion": "^11.15.0",
|
"framer-motion": "^11.15.0",
|
||||||
|
|
|
@ -663,6 +663,93 @@ export const useDeviceStore = create<DeviceState>(set => ({
|
||||||
setSystemVersion: version => set({ systemVersion: version }),
|
setSystemVersion: version => set({ systemVersion: version }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
export interface DhcpLease {
|
||||||
|
ip?: string;
|
||||||
|
netmask?: string;
|
||||||
|
broadcast?: string;
|
||||||
|
ttl?: string;
|
||||||
|
mtu?: string;
|
||||||
|
hostname?: string;
|
||||||
|
domain?: string;
|
||||||
|
bootp_next_server?: string;
|
||||||
|
bootp_server_name?: string;
|
||||||
|
bootp_file?: string;
|
||||||
|
timezone?: string;
|
||||||
|
routers?: string[];
|
||||||
|
dns?: string[];
|
||||||
|
ntp_servers?: string[];
|
||||||
|
lpr_servers?: string[];
|
||||||
|
_time_servers?: string[];
|
||||||
|
_name_servers?: string[];
|
||||||
|
_log_servers?: string[];
|
||||||
|
_cookie_servers?: string[];
|
||||||
|
_wins_servers?: string[];
|
||||||
|
_swap_server?: string;
|
||||||
|
boot_size?: string;
|
||||||
|
root_path?: string;
|
||||||
|
lease?: string;
|
||||||
|
lease_expiry?: Date;
|
||||||
|
dhcp_type?: string;
|
||||||
|
server_id?: string;
|
||||||
|
message?: string;
|
||||||
|
tftp?: string;
|
||||||
|
bootfile?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IPv6Address {
|
||||||
|
address: string;
|
||||||
|
prefix: string;
|
||||||
|
valid_lifetime: string;
|
||||||
|
preferred_lifetime: string;
|
||||||
|
scope: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NetworkState {
|
||||||
|
interface_name?: string;
|
||||||
|
mac_address?: string;
|
||||||
|
ipv4?: string;
|
||||||
|
ipv4_addresses?: string[];
|
||||||
|
ipv6?: string;
|
||||||
|
ipv6_addresses?: IPv6Address[];
|
||||||
|
ipv6_link_local?: string;
|
||||||
|
dhcp_lease?: DhcpLease;
|
||||||
|
|
||||||
|
setNetworkState: (state: NetworkState) => void;
|
||||||
|
setDhcpLease: (lease: NetworkState["dhcp_lease"]) => void;
|
||||||
|
setDhcpLeaseExpiry: (expiry: Date) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export type IPv6Mode = "disabled" | "slaac" | "dhcpv6" | "slaac_and_dhcpv6" | "static" | "link_local" | "unknown";
|
||||||
|
export type IPv4Mode = "disabled" | "static" | "dhcp" | "unknown";
|
||||||
|
export type LLDPMode = "disabled" | "basic" | "all" | "unknown";
|
||||||
|
export type mDNSMode = "disabled" | "auto" | "ipv4_only" | "ipv6_only" | "unknown";
|
||||||
|
export type TimeSyncMode = "ntp_only" | "ntp_and_http" | "http_only" | "custom" | "unknown";
|
||||||
|
|
||||||
|
export interface NetworkSettings {
|
||||||
|
ipv4_mode: IPv4Mode;
|
||||||
|
ipv6_mode: IPv6Mode;
|
||||||
|
lldp_mode: LLDPMode;
|
||||||
|
lldp_tx_tlvs: string[];
|
||||||
|
mdns_mode: mDNSMode;
|
||||||
|
time_sync_mode: TimeSyncMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useNetworkStateStore = create<NetworkState>((set, get) => ({
|
||||||
|
setNetworkState: (state: NetworkState) => set(state),
|
||||||
|
setDhcpLease: (lease: NetworkState["dhcp_lease"]) => set({ dhcp_lease: lease }),
|
||||||
|
setDhcpLeaseExpiry: (expiry: Date) => {
|
||||||
|
const lease = get().dhcp_lease;
|
||||||
|
if (!lease) {
|
||||||
|
console.warn("No lease found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lease.lease_expiry = expiry.toISOString();
|
||||||
|
set({ dhcp_lease: lease });
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
export interface KeySequenceStep {
|
export interface KeySequenceStep {
|
||||||
keys: string[];
|
keys: string[];
|
||||||
modifiers: string[];
|
modifiers: string[];
|
||||||
|
@ -767,8 +854,8 @@ export const useMacrosStore = create<MacrosState>((set, get) => ({
|
||||||
for (let i = 0; i < macro.steps.length; i++) {
|
for (let i = 0; i < macro.steps.length; i++) {
|
||||||
const step = macro.steps[i];
|
const step = macro.steps[i];
|
||||||
if (step.keys && step.keys.length > MAX_KEYS_PER_STEP) {
|
if (step.keys && step.keys.length > MAX_KEYS_PER_STEP) {
|
||||||
console.error(`Cannot save: macro "${macro.name}" step ${i+1} exceeds maximum of ${MAX_KEYS_PER_STEP} keys`);
|
console.error(`Cannot save: macro "${macro.name}" step ${i + 1} exceeds maximum of ${MAX_KEYS_PER_STEP} keys`);
|
||||||
throw new Error(`Cannot save: macro "${macro.name}" step ${i+1} exceeds maximum of ${MAX_KEYS_PER_STEP} keys`);
|
throw new Error(`Cannot save: macro "${macro.name}" step ${i + 1} exceeds maximum of ${MAX_KEYS_PER_STEP} keys`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,62 +1,79 @@
|
||||||
import { useCallback, useEffect, useState } from "react";
|
import { useCallback, useEffect, useState } from "react";
|
||||||
|
|
||||||
import { SettingsPageHeader } from "../components/SettingsPageheader";
|
|
||||||
import { SelectMenuBasic } from "../components/SelectMenuBasic";
|
import { SelectMenuBasic } from "../components/SelectMenuBasic";
|
||||||
|
import { SettingsPageHeader } from "../components/SettingsPageheader";
|
||||||
|
|
||||||
import { SettingsItem } from "./devices.$id.settings";
|
import { IPv4Mode, IPv6Mode, LLDPMode, mDNSMode, NetworkSettings, NetworkState, TimeSyncMode, useNetworkStateStore } from "@/hooks/stores";
|
||||||
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
import { useJsonRpc } from "@/hooks/useJsonRpc";
|
||||||
import { Button } from "@components/Button";
|
|
||||||
import notifications from "@/notifications";
|
import notifications from "@/notifications";
|
||||||
|
import { Button } from "@components/Button";
|
||||||
|
import { GridCard } from "@components/Card";
|
||||||
|
import InputField from "@components/InputField";
|
||||||
|
import { SettingsItem } from "./devices.$id.settings";
|
||||||
|
|
||||||
interface DhcpLease {
|
import dayjs from 'dayjs';
|
||||||
ip?: string;
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||||
netmask?: string;
|
|
||||||
broadcast?: string;
|
dayjs.extend(relativeTime);
|
||||||
ttl?: string;
|
|
||||||
mtu?: string;
|
const defaultNetworkSettings: NetworkSettings = {
|
||||||
hostname?: string;
|
ipv4_mode: "unknown",
|
||||||
domain?: string;
|
ipv6_mode: "unknown",
|
||||||
bootp_next_server?: string;
|
lldp_mode: "unknown",
|
||||||
bootp_server_name?: string;
|
lldp_tx_tlvs: [],
|
||||||
bootp_file?: string;
|
mdns_mode: "unknown",
|
||||||
timezone?: string;
|
time_sync_mode: "unknown",
|
||||||
routers?: string[];
|
|
||||||
dns?: string[];
|
|
||||||
ntp_servers?: string[];
|
|
||||||
lpr_servers?: string[];
|
|
||||||
_time_servers?: string[];
|
|
||||||
_name_servers?: string[];
|
|
||||||
_log_servers?: string[];
|
|
||||||
_cookie_servers?: string[];
|
|
||||||
_wins_servers?: string[];
|
|
||||||
_swap_server?: string;
|
|
||||||
boot_size?: string;
|
|
||||||
root_path?: string;
|
|
||||||
lease?: string;
|
|
||||||
dhcp_type?: string;
|
|
||||||
server_id?: string;
|
|
||||||
message?: string;
|
|
||||||
tftp?: string;
|
|
||||||
bootfile?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function LifeTimeLabel({ lifetime }: { lifetime: string }) {
|
||||||
|
if (lifetime == "") {
|
||||||
|
return <strong>N/A</strong>;
|
||||||
|
}
|
||||||
|
|
||||||
interface NetworkState {
|
const [remaining, setRemaining] = useState<string | null>(null);
|
||||||
interface_name?: string;
|
|
||||||
mac_address?: string;
|
useEffect(() => {
|
||||||
ipv4?: string;
|
setRemaining(dayjs(lifetime).fromNow());
|
||||||
ipv6?: string;
|
|
||||||
dhcp_lease?: DhcpLease;
|
const interval = setInterval(() => {
|
||||||
|
setRemaining(dayjs(lifetime).fromNow());
|
||||||
|
}, 1000 * 30);
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [lifetime]);
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<strong>{dayjs(lifetime).format()}</strong>
|
||||||
|
{remaining && <>
|
||||||
|
{" "}<span className="text-xs text-slate-700 dark:text-slate-300">
|
||||||
|
({remaining})
|
||||||
|
</span>
|
||||||
|
</>}
|
||||||
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function SettingsNetworkRoute() {
|
export default function SettingsNetworkRoute() {
|
||||||
const [send] = useJsonRpc();
|
const [send] = useJsonRpc();
|
||||||
const [networkState, setNetworkState] = useState<NetworkState | null>(null);
|
const [networkState, setNetworkState] = useNetworkStateStore(state => [state, state.setNetworkState]);
|
||||||
|
|
||||||
|
const [networkSettings, setNetworkSettings] = useState<NetworkSettings>(defaultNetworkSettings);
|
||||||
|
const [networkSettingsLoaded, setNetworkSettingsLoaded] = useState(false);
|
||||||
|
|
||||||
|
const [dhcpLeaseExpiry, setDhcpLeaseExpiry] = useState<Date | null>(null);
|
||||||
|
const [dhcpLeaseExpiryRemaining, setDhcpLeaseExpiryRemaining] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const getNetworkSettings = useCallback(() => {
|
||||||
|
setNetworkSettingsLoaded(false);
|
||||||
|
send("getNetworkSettings", {}, resp => {
|
||||||
|
if ("error" in resp) return;
|
||||||
|
setNetworkSettings(resp.result as NetworkSettings);
|
||||||
|
setNetworkSettingsLoaded(true);
|
||||||
|
});
|
||||||
|
}, [send]);
|
||||||
|
|
||||||
const getNetworkState = useCallback(() => {
|
const getNetworkState = useCallback(() => {
|
||||||
send("getNetworkState", {}, resp => {
|
send("getNetworkState", {}, resp => {
|
||||||
if ("error" in resp) return;
|
if ("error" in resp) return;
|
||||||
|
console.log(resp.result);
|
||||||
setNetworkState(resp.result as NetworkState);
|
setNetworkState(resp.result as NetworkState);
|
||||||
});
|
});
|
||||||
}, [send]);
|
}, [send]);
|
||||||
|
@ -74,7 +91,37 @@ export default function SettingsNetworkRoute() {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getNetworkState();
|
getNetworkState();
|
||||||
}, [getNetworkState]);
|
getNetworkSettings();
|
||||||
|
}, [getNetworkState, getNetworkSettings]);
|
||||||
|
|
||||||
|
const handleIpv4ModeChange = (value: IPv4Mode | string) => {
|
||||||
|
setNetworkSettings({ ...networkSettings, ipv4_mode: value as IPv4Mode });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleIpv6ModeChange = (value: IPv6Mode | string) => {
|
||||||
|
setNetworkSettings({ ...networkSettings, ipv6_mode: value as IPv6Mode });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLldpModeChange = (value: LLDPMode | string) => {
|
||||||
|
setNetworkSettings({ ...networkSettings, lldp_mode: value as LLDPMode });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLldpTxTlvsChange = (value: string[]) => {
|
||||||
|
setNetworkSettings({ ...networkSettings, lldp_tx_tlvs: value });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMdnsModeChange = (value: mDNSMode | string) => {
|
||||||
|
setNetworkSettings({ ...networkSettings, mdns_mode: value as mDNSMode });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTimeSyncModeChange = (value: TimeSyncMode | string) => {
|
||||||
|
setNetworkSettings({ ...networkSettings, time_sync_mode: value as TimeSyncMode });
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterUnknown = useCallback((options: { value: string; label: string; }[]) => {
|
||||||
|
if (!networkSettingsLoaded) return options;
|
||||||
|
return options.filter(option => option.value !== "unknown");
|
||||||
|
}, [networkSettingsLoaded]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
@ -82,54 +129,265 @@ export default function SettingsNetworkRoute() {
|
||||||
title="Network"
|
title="Network"
|
||||||
description="Configure your network settings"
|
description="Configure your network settings"
|
||||||
/>
|
/>
|
||||||
<div className="space-y-4">
|
|
||||||
<SettingsItem
|
|
||||||
title="IPv4 Address"
|
|
||||||
description={
|
|
||||||
<span className="select-text font-mono">{networkState?.ipv4}</span>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-4">
|
|
||||||
<SettingsItem
|
|
||||||
title="IPv6 Address"
|
|
||||||
description={<span className="select-text font-mono">{networkState?.ipv6}</span>}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<SettingsItem
|
<SettingsItem
|
||||||
title="MAC Address"
|
title="MAC Address"
|
||||||
description={<span className="select-auto font-mono">{networkState?.mac_address}</span>}
|
description={<></>}
|
||||||
/>
|
>
|
||||||
|
<span className="select-auto font-mono text-xs text-slate-700 dark:text-slate-300">
|
||||||
|
{networkState?.mac_address}
|
||||||
|
</span>
|
||||||
|
</SettingsItem>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<SettingsItem
|
<SettingsItem
|
||||||
title="DHCP Lease"
|
title="Hostname"
|
||||||
description={<>
|
description={
|
||||||
<ul>
|
<>
|
||||||
{networkState?.dhcp_lease?.ip && <li>IP: <strong>{networkState?.dhcp_lease?.ip}</strong></li>}
|
Hostname for the device
|
||||||
{networkState?.dhcp_lease?.netmask && <li>Subnet: <strong>{networkState?.dhcp_lease?.netmask}</strong></li>}
|
<br />
|
||||||
{networkState?.dhcp_lease?.broadcast && <li>Broadcast: <strong>{networkState?.dhcp_lease?.broadcast}</strong></li>}
|
<span className="text-xs text-slate-700 dark:text-slate-300">
|
||||||
{networkState?.dhcp_lease?.ttl && <li>TTL: <strong>{networkState?.dhcp_lease?.ttl}</strong></li>}
|
Leave blank for default
|
||||||
{networkState?.dhcp_lease?.mtu && <li>MTU: <strong>{networkState?.dhcp_lease?.mtu}</strong></li>}
|
</span>
|
||||||
{networkState?.dhcp_lease?.hostname && <li>Hostname: <strong>{networkState?.dhcp_lease?.hostname}</strong></li>}
|
</>
|
||||||
{networkState?.dhcp_lease?.domain && <li>Domain: <strong>{networkState?.dhcp_lease?.domain}</strong></li>}
|
}
|
||||||
{networkState?.dhcp_lease?.routers && <li>Gateway: <strong>{networkState?.dhcp_lease?.routers.join(", ")}</strong></li>}
|
|
||||||
{networkState?.dhcp_lease?.dns && <li>DNS: <strong>{networkState?.dhcp_lease?.dns.join(", ")}</strong></li>}
|
|
||||||
{networkState?.dhcp_lease?.ntp_servers && <li>NTP Servers: <strong>{networkState?.dhcp_lease?.ntp_servers.join(", ")}</strong></li>}
|
|
||||||
</ul>
|
|
||||||
</>}
|
|
||||||
>
|
>
|
||||||
<Button
|
<InputField
|
||||||
size="SM"
|
type="text"
|
||||||
theme="light"
|
placeholder="jetkvm"
|
||||||
text="Renew lease"
|
value={""}
|
||||||
onClick={() => {
|
error={""}
|
||||||
handleRenewLease();
|
onChange={e => {
|
||||||
}}
|
console.log(e.target.value);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<SettingsItem
|
||||||
|
title="Domain"
|
||||||
|
description={
|
||||||
|
<>
|
||||||
|
Domain for the device
|
||||||
|
<br />
|
||||||
|
<span className="text-xs text-slate-700 dark:text-slate-300">
|
||||||
|
Leave blank to use DHCP provided domain, if there is no domain, use <span className="font-mono">local</span>
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<InputField
|
||||||
|
type="text"
|
||||||
|
placeholder="local"
|
||||||
|
value={""}
|
||||||
|
error={""}
|
||||||
|
onChange={e => {
|
||||||
|
console.log(e.target.value);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</SettingsItem>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<SettingsItem
|
||||||
|
title="IPv4 Mode"
|
||||||
|
description="Configure the IPv4 mode"
|
||||||
|
>
|
||||||
|
<SelectMenuBasic
|
||||||
|
size="SM"
|
||||||
|
value={networkSettings.ipv4_mode}
|
||||||
|
onChange={e => handleIpv4ModeChange(e.target.value)}
|
||||||
|
disabled={!networkSettingsLoaded}
|
||||||
|
options={filterUnknown([
|
||||||
|
{ value: "dhcp", label: "DHCP" },
|
||||||
|
// { value: "static", label: "Static" },
|
||||||
|
])}
|
||||||
|
/>
|
||||||
|
</SettingsItem>
|
||||||
|
{networkState?.dhcp_lease && (
|
||||||
|
<GridCard>
|
||||||
|
<div className="flex items-start gap-x-4 p-4">
|
||||||
|
<div className="space-y-3 w-full">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h3 className="text-base font-bold text-slate-900 dark:text-white">
|
||||||
|
Current DHCP Lease
|
||||||
|
</h3>
|
||||||
|
<div>
|
||||||
|
<ul className="list-none space-y-1 text-xs text-slate-700 dark:text-slate-300">
|
||||||
|
{networkState?.dhcp_lease?.ip && <li>IP: <strong>{networkState?.dhcp_lease?.ip}</strong></li>}
|
||||||
|
{networkState?.dhcp_lease?.netmask && <li>Subnet: <strong>{networkState?.dhcp_lease?.netmask}</strong></li>}
|
||||||
|
{networkState?.dhcp_lease?.broadcast && <li>Broadcast: <strong>{networkState?.dhcp_lease?.broadcast}</strong></li>}
|
||||||
|
{networkState?.dhcp_lease?.ttl && <li>TTL: <strong>{networkState?.dhcp_lease?.ttl}</strong></li>}
|
||||||
|
{networkState?.dhcp_lease?.mtu && <li>MTU: <strong>{networkState?.dhcp_lease?.mtu}</strong></li>}
|
||||||
|
{networkState?.dhcp_lease?.hostname && <li>Hostname: <strong>{networkState?.dhcp_lease?.hostname}</strong></li>}
|
||||||
|
{networkState?.dhcp_lease?.domain && <li>Domain: <strong>{networkState?.dhcp_lease?.domain}</strong></li>}
|
||||||
|
{networkState?.dhcp_lease?.routers && <li>Gateway: <strong>{networkState?.dhcp_lease?.routers.join(", ")}</strong></li>}
|
||||||
|
{networkState?.dhcp_lease?.dns && <li>DNS: <strong>{networkState?.dhcp_lease?.dns.join(", ")}</strong></li>}
|
||||||
|
{networkState?.dhcp_lease?.ntp_servers && <li>NTP Servers: <strong>{networkState?.dhcp_lease?.ntp_servers.join(", ")}</strong></li>}
|
||||||
|
{networkState?.dhcp_lease?.server_id && <li>Server ID: <strong>{networkState?.dhcp_lease?.server_id}</strong></li>}
|
||||||
|
{networkState?.dhcp_lease?.bootp_next_server && <li>BootP Next Server: <strong>{networkState?.dhcp_lease?.bootp_next_server}</strong></li>}
|
||||||
|
{networkState?.dhcp_lease?.bootp_server_name && <li>BootP Server Name: <strong>{networkState?.dhcp_lease?.bootp_server_name}</strong></li>}
|
||||||
|
{networkState?.dhcp_lease?.bootp_file && <li>Boot File: <strong>{networkState?.dhcp_lease?.bootp_file}</strong></li>}
|
||||||
|
{networkState?.dhcp_lease?.lease_expiry && <li>
|
||||||
|
Lease Expiry: <LifeTimeLabel lifetime={`${networkState?.dhcp_lease?.lease_expiry}`} />
|
||||||
|
</li>}
|
||||||
|
{/* {JSON.stringify(networkState?.dhcp_lease)} */}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<hr className="block w-full dark:border-slate-600" />
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
size="SM"
|
||||||
|
theme="danger"
|
||||||
|
text="Renew lease"
|
||||||
|
onClick={() => {
|
||||||
|
handleRenewLease();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</GridCard>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<SettingsItem
|
||||||
|
title="IPv6 Mode"
|
||||||
|
description="Configure the IPv6 mode"
|
||||||
|
>
|
||||||
|
<SelectMenuBasic
|
||||||
|
size="SM"
|
||||||
|
value={networkSettings.ipv6_mode}
|
||||||
|
onChange={e => handleIpv6ModeChange(e.target.value)}
|
||||||
|
disabled={!networkSettingsLoaded}
|
||||||
|
options={filterUnknown([
|
||||||
|
// { value: "disabled", label: "Disabled" },
|
||||||
|
{ value: "slaac", label: "SLAAC" },
|
||||||
|
// { value: "dhcpv6", label: "DHCPv6" },
|
||||||
|
// { value: "slaac_and_dhcpv6", label: "SLAAC and DHCPv6" },
|
||||||
|
// { value: "static", label: "Static" },
|
||||||
|
// { value: "link_local", label: "Link-local only" },
|
||||||
|
])}
|
||||||
|
/>
|
||||||
|
</SettingsItem>
|
||||||
|
{networkState?.ipv6_addresses && (
|
||||||
|
<GridCard>
|
||||||
|
<div className="flex items-start gap-x-4 p-4">
|
||||||
|
<div className="space-y-3 w-full">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h3 className="text-base font-bold text-slate-900 dark:text-white">
|
||||||
|
IPv6 Information
|
||||||
|
</h3>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-bold text-slate-900 dark:text-white">
|
||||||
|
IPv6 Link-local
|
||||||
|
</h4>
|
||||||
|
<p className="text-xs text-slate-700 dark:text-slate-300">
|
||||||
|
{networkState?.ipv6_link_local}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-bold text-slate-900 dark:text-white">
|
||||||
|
IPv6 Addresses
|
||||||
|
</h4>
|
||||||
|
<ul className="list-none space-y-1 text-xs text-slate-700 dark:text-slate-300">
|
||||||
|
{networkState?.ipv6_addresses && networkState?.ipv6_addresses.map(addr => (
|
||||||
|
<li key={addr.address}>
|
||||||
|
{addr.address}
|
||||||
|
{addr.valid_lifetime && <>
|
||||||
|
<br />
|
||||||
|
- valid_lft: {" "}
|
||||||
|
<span className="text-xs text-slate-700 dark:text-slate-300">
|
||||||
|
<LifeTimeLabel lifetime={`${addr.valid_lifetime}`} />
|
||||||
|
</span>
|
||||||
|
</>}
|
||||||
|
{addr.preferred_lifetime && <>
|
||||||
|
<br />
|
||||||
|
- pref_lft: {" "}
|
||||||
|
<span className="text-xs text-slate-700 dark:text-slate-300">
|
||||||
|
<LifeTimeLabel lifetime={`${addr.preferred_lifetime}`} />
|
||||||
|
</span>
|
||||||
|
</>}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</GridCard>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<SettingsItem
|
||||||
|
title="LLDP"
|
||||||
|
description="Control which TLVs will be sent over Link Layer Discovery Protocol"
|
||||||
|
>
|
||||||
|
<SelectMenuBasic
|
||||||
|
size="SM"
|
||||||
|
value={networkSettings.lldp_mode}
|
||||||
|
onChange={e => handleLldpModeChange(e.target.value)}
|
||||||
|
disabled={!networkSettingsLoaded}
|
||||||
|
options={filterUnknown([
|
||||||
|
{ value: "disabled", label: "Disabled" },
|
||||||
|
{ value: "basic", label: "Basic" },
|
||||||
|
{ value: "all", label: "All" },
|
||||||
|
])}
|
||||||
|
/>
|
||||||
|
</SettingsItem>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<SettingsItem
|
||||||
|
title="mDNS"
|
||||||
|
description="Control mDNS (multicast DNS) operational mode"
|
||||||
|
>
|
||||||
|
<SelectMenuBasic
|
||||||
|
size="SM"
|
||||||
|
value={networkSettings.mdns_mode}
|
||||||
|
onChange={e => handleMdnsModeChange(e.target.value)}
|
||||||
|
disabled={!networkSettingsLoaded}
|
||||||
|
options={filterUnknown([
|
||||||
|
{ value: "disabled", label: "Disabled" },
|
||||||
|
{ value: "auto", label: "Auto" },
|
||||||
|
{ value: "ipv4_only", label: "IPv4 only" },
|
||||||
|
{ value: "ipv6_only", label: "IPv6 only" },
|
||||||
|
])}
|
||||||
|
/>
|
||||||
|
</SettingsItem>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<SettingsItem
|
||||||
|
title="Time synchronization"
|
||||||
|
description="Configure time synchronization settings"
|
||||||
|
>
|
||||||
|
<SelectMenuBasic
|
||||||
|
size="SM"
|
||||||
|
value={networkSettings.time_sync_mode}
|
||||||
|
onChange={e => handleTimeSyncModeChange(e.target.value)}
|
||||||
|
disabled={!networkSettingsLoaded}
|
||||||
|
options={filterUnknown([
|
||||||
|
{ value: "unknown", label: "..." },
|
||||||
|
{ value: "auto", label: "Auto" },
|
||||||
|
{ value: "ntp_only", label: "NTP only" },
|
||||||
|
{ value: "ntp_and_http", label: "NTP and HTTP" },
|
||||||
|
{ value: "http_only", label: "HTTP only" },
|
||||||
|
{ value: "custom", label: "Custom" },
|
||||||
|
])}
|
||||||
|
/>
|
||||||
|
</SettingsItem>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-end gap-x-2">
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
console.log("save settings");
|
||||||
|
}}
|
||||||
|
size="SM"
|
||||||
|
theme="light"
|
||||||
|
text="Save Settings"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,11 +20,13 @@ import { cx } from "@/cva.config";
|
||||||
import {
|
import {
|
||||||
DeviceSettingsState,
|
DeviceSettingsState,
|
||||||
HidState,
|
HidState,
|
||||||
|
NetworkState,
|
||||||
UpdateState,
|
UpdateState,
|
||||||
useDeviceSettingsStore,
|
useDeviceSettingsStore,
|
||||||
useDeviceStore,
|
useDeviceStore,
|
||||||
useHidStore,
|
useHidStore,
|
||||||
useMountMediaStore,
|
useMountMediaStore,
|
||||||
|
useNetworkStateStore,
|
||||||
User,
|
User,
|
||||||
useRTCStore,
|
useRTCStore,
|
||||||
useUiStore,
|
useUiStore,
|
||||||
|
@ -581,6 +583,8 @@ export default function KvmIdRoute() {
|
||||||
});
|
});
|
||||||
}, 10000);
|
}, 10000);
|
||||||
|
|
||||||
|
const setNetworkState = useNetworkStateStore(state => state.setNetworkState);
|
||||||
|
|
||||||
const setUsbState = useHidStore(state => state.setUsbState);
|
const setUsbState = useHidStore(state => state.setUsbState);
|
||||||
const setHdmiState = useVideoStore(state => state.setHdmiState);
|
const setHdmiState = useVideoStore(state => state.setHdmiState);
|
||||||
|
|
||||||
|
@ -600,6 +604,11 @@ export default function KvmIdRoute() {
|
||||||
setHdmiState(resp.params as Parameters<VideoState["setHdmiState"]>[0]);
|
setHdmiState(resp.params as Parameters<VideoState["setHdmiState"]>[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (resp.method === "networkState") {
|
||||||
|
console.log("Setting network state", resp.params);
|
||||||
|
setNetworkState(resp.params as NetworkState);
|
||||||
|
}
|
||||||
|
|
||||||
if (resp.method === "otaState") {
|
if (resp.method === "otaState") {
|
||||||
const otaState = resp.params as UpdateState["otaState"];
|
const otaState = resp.params as UpdateState["otaState"];
|
||||||
setOtaState(otaState);
|
setOtaState(otaState);
|
||||||
|
|
2
usb.go
2
usb.go
|
@ -66,6 +66,6 @@ func checkUSBState() {
|
||||||
usbState = newState
|
usbState = newState
|
||||||
|
|
||||||
usbLogger.Info().Str("from", usbState).Str("to", newState).Msg("USB state changed")
|
usbLogger.Info().Str("from", usbState).Str("to", newState).Msg("USB state changed")
|
||||||
requestDisplayUpdate()
|
requestDisplayUpdate(true)
|
||||||
triggerUSBStateUpdate()
|
triggerUSBStateUpdate()
|
||||||
}
|
}
|
||||||
|
|
2
video.go
2
video.go
|
@ -43,7 +43,7 @@ func HandleVideoStateMessage(event CtrlResponse) {
|
||||||
}
|
}
|
||||||
lastVideoState = videoState
|
lastVideoState = videoState
|
||||||
triggerVideoStateUpdate()
|
triggerVideoStateUpdate()
|
||||||
requestDisplayUpdate()
|
requestDisplayUpdate(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func rpcGetVideoState() (VideoInputState, error) {
|
func rpcGetVideoState() (VideoInputState, error) {
|
||||||
|
|
|
@ -205,7 +205,7 @@ func newSession(config SessionConfig) (*Session, error) {
|
||||||
var actionSessions = 0
|
var actionSessions = 0
|
||||||
|
|
||||||
func onActiveSessionsChanged() {
|
func onActiveSessionsChanged() {
|
||||||
requestDisplayUpdate()
|
requestDisplayUpdate(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
func onFirstSessionConnected() {
|
func onFirstSessionConnected() {
|
||||||
|
|
Loading…
Reference in New Issue