feat: add network settings tab

This commit is contained in:
Siyuan Miao 2025-04-14 06:00:00 +02:00
parent eb8bac4013
commit 73b83557af
15 changed files with 662 additions and 104 deletions

View File

@ -165,9 +165,12 @@ func setCloudConnectionState(state CloudConnectionState) {
state = CloudConnectionStateNotConfigured
}
previousState := cloudConnectionState
cloudConnectionState = state
go waitCtrlAndRequestDisplayUpdate()
go waitCtrlAndRequestDisplayUpdate(
previousState != state,
)
}
func wsResetMetrics(established bool, sourceType string, source string) {

View File

@ -179,7 +179,7 @@ var (
waitDisplayUpdate = sync.Mutex{}
)
func requestDisplayUpdate() {
func requestDisplayUpdate(shouldWakeDisplay bool) {
displayUpdateLock.Lock()
defer displayUpdateLock.Unlock()
@ -188,19 +188,21 @@ func requestDisplayUpdate() {
return
}
go func() {
wakeDisplay(false)
if shouldWakeDisplay {
wakeDisplay(false)
}
displayLogger.Debug().Msg("display updating")
//TODO: only run once regardless how many pending updates
updateDisplay()
}()
}
func waitCtrlAndRequestDisplayUpdate() {
func waitCtrlAndRequestDisplayUpdate(shouldWakeDisplay bool) {
waitDisplayUpdate.Lock()
defer waitDisplayUpdate.Unlock()
waitCtrlClientConnected()
requestDisplayUpdate()
requestDisplayUpdate(shouldWakeDisplay)
}
func updateStaticContents() {
@ -376,7 +378,7 @@ func init() {
displayLogger.Info().Msg("display inited")
startBacklightTickers()
wakeDisplay(true)
requestDisplayUpdate()
requestDisplayUpdate(true)
}()
go watchTsEvents()

View File

@ -1,9 +1,11 @@
package udhcpc
import (
"bufio"
"encoding/json"
"fmt"
"net"
"os"
"reflect"
"strconv"
"strings"
@ -41,6 +43,8 @@ type Lease struct {
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
LeaseExpiry *time.Time `json:"lease_expiry,omitempty"` // The expiry time of the lease
isEmpty map[string]bool
}
@ -60,6 +64,40 @@ func (l *Lease) ToJSON() string {
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 {
// parse the lease file as a map
data := make(map[string]string)

View File

@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"os"
"time"
"github.com/fsnotify/fsnotify"
"github.com/rs/zerolog"
@ -140,6 +141,17 @@ func (c *DHCPClient) loadLeaseFile() error {
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.logger.Info().

View File

@ -961,6 +961,7 @@ var rpcHandlers = map[string]RPCHandler{
"deregisterDevice": {Func: rpcDeregisterDevice},
"getCloudState": {Func: rpcGetCloudState},
"getNetworkState": {Func: rpcGetNetworkState},
"getNetworkSettings": {Func: rpcGetNetworkSettings},
"renewDHCPLease": {Func: rpcRenewDHCPLease},
"keyboardReport": {Func: rpcKeyboardReport, Params: []string{"modifier", "keys"}},
"absMouseReport": {Func: rpcAbsMouseReport, Params: []string{"x", "y", "buttons"}},

View File

@ -29,11 +29,22 @@ const (
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 {
interfaceName string
interfaceUp bool
ipv4Addr *net.IP
ipv4Addresses []string
ipv6Addr *net.IP
ipv6Addresses []IPv6Address
ipv6LinkLocal *net.IP
macAddr *net.HardwareAddr
l *zerolog.Logger
@ -47,12 +58,65 @@ type NetworkInterfaceState struct {
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 {
InterfaceName string `json:"interface_name"`
MacAddress string `json:"mac_address"`
IPv4 string `json:"ipv4,omitempty"`
IPv6 string `json:"ipv6,omitempty"`
DHCPLease *udhcpc.Lease `json:"dhcp_lease,omitempty"`
InterfaceName string `json:"interface_name"`
MacAddress string `json:"mac_address"`
IPv4 string `json:"ipv4,omitempty"`
IPv6 string `json:"ipv6,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 {
@ -115,13 +179,13 @@ func NewNetworkInterfaceState(ifname string) *NetworkInterfaceState {
onStateChange: func(state *NetworkInterfaceState) {
go func() {
waitCtrlClientConnected()
requestDisplayUpdate()
requestDisplayUpdate(true)
}()
},
onInitialCheck: func(state *NetworkInterfaceState) {
go func() {
waitCtrlClientConnected()
requestDisplayUpdate()
requestDisplayUpdate(true)
}()
},
}
@ -140,7 +204,18 @@ func NewNetworkInterfaceState(ifname string) *NetworkInterfaceState {
PidFile: dhcpPidFile,
Logger: &logger,
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 (
ipv4Addresses = make([]net.IP, 0)
ipv6Addresses = make([]net.IP, 0)
ipv4Addresses = make([]net.IP, 0)
ipv4AddressesString = make([]string, 0)
ipv6Addresses = make([]IPv6Address, 0)
ipv6AddressesString = make([]string, 0)
ipv6LinkLocal *net.IP
)
for _, addr := range addrs {
@ -215,9 +293,15 @@ func (s *NetworkInterfaceState) update() (DhcpTargetState, error) {
continue
}
ipv4Addresses = append(ipv4Addresses, addr.IP)
ipv4AddressesString = append(ipv4AddressesString, addr.IPNet.String())
} else if addr.IP.To16() != nil {
scopedLogger := s.l.With().Str("ipv6", addr.IP.String()).Logger()
// check if it's a link local address
if addr.IP.IsLinkLocalUnicast() {
ipv6LinkLocal = &addr.IP
continue
}
if !addr.IP.IsGlobalUnicast() {
scopedLogger.Trace().Msg("not a global unicast address, skipping")
continue
@ -231,7 +315,14 @@ func (s *NetworkInterfaceState) update() (DhcpTargetState, error) {
}
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
}
}
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 {
// compare the addresses to see if there's a change
if s.ipv6Addr == nil || s.ipv6Addr.String() != ipv6Addresses[0].String() {
scopedLogger := s.l.With().Str("ipv6", ipv6Addresses[0].String()).Logger()
if s.ipv6Addr == nil || s.ipv6Addr.String() != ipv6Addresses[0].Address.String() {
scopedLogger := s.l.With().Str("ipv6", ipv6Addresses[0].Address.String()).Logger()
if s.ipv6Addr != nil {
scopedLogger.Info().
Str("old_ipv6", s.ipv6Addr.String()).
@ -262,7 +370,7 @@ func (s *NetworkInterfaceState) update() (DhcpTargetState, error) {
} else {
scopedLogger.Info().Msg("IPv6 address found")
}
s.ipv6Addr = &ipv6Addresses[0]
s.ipv6Addr = &ipv6Addresses[0].Address
changed = true
}
}
@ -313,15 +421,38 @@ func (s *NetworkInterfaceState) HandleLinkUpdate(update netlink.LinkUpdate) {
}
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{
InterfaceName: networkState.interfaceName,
MacAddress: networkState.macAddr.String(),
IPv4: networkState.ipv4Addr.String(),
IPv6: networkState.ipv6Addr.String(),
IPv6LinkLocal: networkState.ipv6LinkLocal.String(),
IPv4Addresses: networkState.ipv4Addresses,
IPv6Addresses: ipv6Addresses,
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 {
if networkState == nil {
return fmt.Errorf("network state not initialized")

View File

@ -15,5 +15,15 @@ echo "└───────────────────────
# Set the environment variable and run Vite
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
JETKVM_PROXY_URL="ws://$ip_address" npx vite dev --mode=device
popd > /dev/null

6
ui/package-lock.json generated
View File

@ -18,6 +18,7 @@
"@xterm/addon-webgl": "^0.18.0",
"@xterm/xterm": "^5.5.0",
"cva": "^1.0.0-beta.1",
"dayjs": "^1.11.13",
"eslint-import-resolver-alias": "^1.1.2",
"focus-trap-react": "^10.2.3",
"framer-motion": "^11.15.0",
@ -2460,6 +2461,11 @@
"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": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",

View File

@ -28,6 +28,7 @@
"@xterm/addon-webgl": "^0.18.0",
"@xterm/xterm": "^5.5.0",
"cva": "^1.0.0-beta.1",
"dayjs": "^1.11.13",
"eslint-import-resolver-alias": "^1.1.2",
"focus-trap-react": "^10.2.3",
"framer-motion": "^11.15.0",

View File

@ -663,6 +663,93 @@ export const useDeviceStore = create<DeviceState>(set => ({
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 {
keys: string[];
modifiers: string[];
@ -767,8 +854,8 @@ export const useMacrosStore = create<MacrosState>((set, get) => ({
for (let i = 0; i < macro.steps.length; i++) {
const step = macro.steps[i];
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`);
throw new 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`);
}
}
}

View File

@ -1,62 +1,79 @@
import { useCallback, useEffect, useState } from "react";
import { SettingsPageHeader } from "../components/SettingsPageheader";
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 { Button } from "@components/Button";
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 {
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;
dhcp_type?: string;
server_id?: string;
message?: string;
tftp?: string;
bootfile?: string;
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
dayjs.extend(relativeTime);
const defaultNetworkSettings: NetworkSettings = {
ipv4_mode: "unknown",
ipv6_mode: "unknown",
lldp_mode: "unknown",
lldp_tx_tlvs: [],
mdns_mode: "unknown",
time_sync_mode: "unknown",
}
export function LifeTimeLabel({ lifetime }: { lifetime: string }) {
if (lifetime == "") {
return <strong>N/A</strong>;
}
interface NetworkState {
interface_name?: string;
mac_address?: string;
ipv4?: string;
ipv6?: string;
dhcp_lease?: DhcpLease;
const [remaining, setRemaining] = useState<string | null>(null);
useEffect(() => {
setRemaining(dayjs(lifetime).fromNow());
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() {
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(() => {
send("getNetworkState", {}, resp => {
if ("error" in resp) return;
console.log(resp.result);
setNetworkState(resp.result as NetworkState);
});
}, [send]);
@ -74,7 +91,37 @@ export default function SettingsNetworkRoute() {
useEffect(() => {
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 (
<div className="space-y-4">
@ -82,54 +129,265 @@ export default function SettingsNetworkRoute() {
title="Network"
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">
<SettingsItem
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 className="space-y-4">
<SettingsItem
title="DHCP Lease"
description={<>
<ul>
{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>}
</ul>
</>}
title="Hostname"
description={
<>
Hostname for the device
<br />
<span className="text-xs text-slate-700 dark:text-slate-300">
Leave blank for default
</span>
</>
}
>
<Button
size="SM"
theme="light"
text="Renew lease"
onClick={() => {
handleRenewLease();
}}
<InputField
type="text"
placeholder="jetkvm"
value={""}
error={""}
onChange={e => {
console.log(e.target.value);
}}
/>
</SettingsItem>
</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>
);
}

View File

@ -20,11 +20,13 @@ import { cx } from "@/cva.config";
import {
DeviceSettingsState,
HidState,
NetworkState,
UpdateState,
useDeviceSettingsStore,
useDeviceStore,
useHidStore,
useMountMediaStore,
useNetworkStateStore,
User,
useRTCStore,
useUiStore,
@ -581,6 +583,8 @@ export default function KvmIdRoute() {
});
}, 10000);
const setNetworkState = useNetworkStateStore(state => state.setNetworkState);
const setUsbState = useHidStore(state => state.setUsbState);
const setHdmiState = useVideoStore(state => state.setHdmiState);
@ -600,6 +604,11 @@ export default function KvmIdRoute() {
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") {
const otaState = resp.params as UpdateState["otaState"];
setOtaState(otaState);

2
usb.go
View File

@ -66,6 +66,6 @@ func checkUSBState() {
usbState = newState
usbLogger.Info().Str("from", usbState).Str("to", newState).Msg("USB state changed")
requestDisplayUpdate()
requestDisplayUpdate(true)
triggerUSBStateUpdate()
}

View File

@ -43,7 +43,7 @@ func HandleVideoStateMessage(event CtrlResponse) {
}
lastVideoState = videoState
triggerVideoStateUpdate()
requestDisplayUpdate()
requestDisplayUpdate(true)
}
func rpcGetVideoState() (VideoInputState, error) {

View File

@ -205,7 +205,7 @@ func newSession(config SessionConfig) (*Session, error) {
var actionSessions = 0
func onActiveSessionsChanged() {
requestDisplayUpdate()
requestDisplayUpdate(true)
}
func onFirstSessionConnected() {