Compare commits

..

2 Commits

Author SHA1 Message Date
Marc Brooks 56a13da763
Merge 83d544f0f7 into 403141c96a 2025-10-15 15:10:09 +00:00
Marc Brooks 83d544f0f7
Add inlang/paraglide-js localization
Remove the temporary directory after extracting buildkit
Localize the extension popovers.
Update package and fix tsconfig.json
Expand development directory guide
Move messages under localization
Popovers and sidebar
Update Chinese translations
Accidentally lost the changes that @ym provided, brought them back
File formatting pass
Localized all components, hooks, providers, hooks
Localize all pages except Settings
Bump packages
Settings Access page
Settings local auth page
Fix ref lint warning
Settings Advanced page
Fix UI lint warnings there were a bunch of ref and useEffect violations.
Settings appearance page
Settings general pages
Settings hardware page
Settings keyboard page
Settings macros pages
Settings mouse page
Settings page
Settings video page
Settings network page
Fix compilation issues
Ran machine translate
Use getLocale for date, relative time, and money formatting
Fix eslint
Delete unused messages
Added setting to choose locale
Merged in dev hotfix
Fix update status rendering
2025-10-15 10:09:56 -05:00
108 changed files with 8728 additions and 16036 deletions

View File

@ -4,11 +4,12 @@
"features": { "features": {
"ghcr.io/devcontainers/features/node:1": { "ghcr.io/devcontainers/features/node:1": {
// Should match what is defined in ui/package.json // Should match what is defined in ui/package.json
"version": "22.20.0" "version": "22.19.0"
} }
}, },
"mounts": [ "mounts": [
"source=${localEnv:HOME}/.ssh,target=/home/vscode/.ssh,type=bind,consistency=cached" "source=${localEnv:HOME}/.ssh,target=/home/vscode/.ssh,type=bind,consistency=cached",
"source=/var/run/docker.sock,target=/var/run/docker.sock,type=bind"
], ],
"onCreateCommand": ".devcontainer/install-deps.sh", "onCreateCommand": ".devcontainer/install-deps.sh",
"customizations": { "customizations": {

View File

@ -4,7 +4,7 @@
"features": { "features": {
"ghcr.io/devcontainers/features/node:1": { "ghcr.io/devcontainers/features/node:1": {
// Should match what is defined in ui/package.json // Should match what is defined in ui/package.json
"version": "22.20.0" "version": "22.19.0"
} }
}, },
"runArgs": [ "runArgs": [

View File

@ -3,12 +3,5 @@
"cva", "cva",
"cx" "cx"
], ],
"gopls": { "git.ignoreLimitWarning": true
"build.buildFlags": [
"-tags",
"synctrace"
]
},
"git.ignoreLimitWarning": true,
"cmake.sourceDirectory": "/workspaces/kvm-static-ip/internal/native/cgo"
} }

View File

@ -12,13 +12,7 @@ BUILDKIT_FLAVOR := arm-rockchip830-linux-uclibcgnueabihf
BUILDKIT_PATH ?= /opt/jetkvm-native-buildkit BUILDKIT_PATH ?= /opt/jetkvm-native-buildkit
SKIP_NATIVE_IF_EXISTS ?= 0 SKIP_NATIVE_IF_EXISTS ?= 0
SKIP_UI_BUILD ?= 0 SKIP_UI_BUILD ?= 0
ENABLE_SYNC_TRACE ?= 0
GO_BUILD_ARGS := -tags netgo,timetzdata,nomsgpack GO_BUILD_ARGS := -tags netgo,timetzdata,nomsgpack
ifeq ($(ENABLE_SYNC_TRACE), 1)
GO_BUILD_ARGS := $(GO_BUILD_ARGS),synctrace
endif
GO_RELEASE_BUILD_ARGS := -trimpath $(GO_BUILD_ARGS) GO_RELEASE_BUILD_ARGS := -trimpath $(GO_BUILD_ARGS)
GO_LDFLAGS := \ GO_LDFLAGS := \
-s -w \ -s -w \

View File

@ -494,7 +494,7 @@ func RunWebsocketClient() {
} }
// If the network is not up, well, we can't connect to the cloud. // If the network is not up, well, we can't connect to the cloud.
if !networkManager.IsOnline() { if !networkState.IsOnline() {
cloudLogger.Warn().Msg("waiting for network to be online, will retry in 3 seconds") cloudLogger.Warn().Msg("waiting for network to be online, will retry in 3 seconds")
time.Sleep(3 * time.Second) time.Sleep(3 * time.Second)
continue continue

View File

@ -17,8 +17,8 @@ import (
const ( const (
envChildID = "JETKVM_CHILD_ID" envChildID = "JETKVM_CHILD_ID"
errorDumpDir = "/userdata/jetkvm/crashdump" errorDumpDir = "/userdata/jetkvm/"
errorDumpLastFile = "last-crash.log" errorDumpStateFile = ".has_error_dump"
errorDumpTemplate = "jetkvm-%s.log" errorDumpTemplate = "jetkvm-%s.log"
) )
@ -117,47 +117,30 @@ func supervise() error {
return nil return nil
} }
func isSymlinkTo(oldName, newName string) bool { func createErrorDump(logFile *os.File) {
file, err := os.Stat(newName) logFile.Close()
// touch the error dump state file
if err := os.WriteFile(filepath.Join(errorDumpDir, errorDumpStateFile), []byte{}, 0644); err != nil {
return
}
fileName := fmt.Sprintf(errorDumpTemplate, time.Now().Format("20060102150405"))
filePath := filepath.Join(errorDumpDir, fileName)
if err := os.Rename(logFile.Name(), filePath); err == nil {
fmt.Printf("error dump created: %s\n", filePath)
return
}
fnSrc, err := os.Open(logFile.Name())
if err != nil { if err != nil {
return false return
}
if file.Mode()&os.ModeSymlink != os.ModeSymlink {
return false
}
target, err := os.Readlink(newName)
if err != nil {
return false
}
return target == oldName
}
func ensureSymlink(oldName, newName string) error {
if isSymlinkTo(oldName, newName) {
return nil
}
_ = os.Remove(newName)
return os.Symlink(oldName, newName)
}
func renameFile(f *os.File, newName string) error {
_ = f.Close()
// try to rename the file first
if err := os.Rename(f.Name(), newName); err == nil {
return nil
}
// copy the log file to the error dump directory
fnSrc, err := os.Open(f.Name())
if err != nil {
return fmt.Errorf("failed to open file: %w", err)
} }
defer fnSrc.Close() defer fnSrc.Close()
fnDst, err := os.Create(newName) fnDst, err := os.Create(filePath)
if err != nil { if err != nil {
return fmt.Errorf("failed to create file: %w", err) return
} }
defer fnDst.Close() defer fnDst.Close()
@ -165,60 +148,18 @@ func renameFile(f *os.File, newName string) error {
for { for {
n, err := fnSrc.Read(buf) n, err := fnSrc.Read(buf)
if err != nil && err != io.EOF { if err != nil && err != io.EOF {
return fmt.Errorf("failed to read file: %w", err) return
} }
if n == 0 { if n == 0 {
break break
} }
if _, err := fnDst.Write(buf[:n]); err != nil { if _, err := fnDst.Write(buf[:n]); err != nil {
return fmt.Errorf("failed to write file: %w", err)
}
}
return nil
}
func ensureErrorDumpDir() error {
// TODO: check if the directory is writable
f, err := os.Stat(errorDumpDir)
if err == nil && f.IsDir() {
return nil
}
if err := os.MkdirAll(errorDumpDir, 0755); err != nil {
return fmt.Errorf("failed to create error dump directory: %w", err)
}
return nil
}
func createErrorDump(logFile *os.File) {
fmt.Println()
fileName := fmt.Sprintf(
errorDumpTemplate,
time.Now().Format("20060102-150405"),
)
// check if the directory exists
if err := ensureErrorDumpDir(); err != nil {
fmt.Printf("failed to ensure error dump directory: %v\n", err)
return return
} }
filePath := filepath.Join(errorDumpDir, fileName)
if err := renameFile(logFile, filePath); err != nil {
fmt.Printf("failed to rename file: %v\n", err)
return
} }
fmt.Printf("error dump copied: %s\n", filePath) fmt.Printf("error dump created: %s\n", filePath)
lastFilePath := filepath.Join(errorDumpDir, errorDumpLastFile)
if err := ensureSymlink(filePath, lastFilePath); err != nil {
fmt.Printf("failed to create symlink: %v\n", err)
return
}
} }
func doSupervise() { func doSupervise() {

View File

@ -7,9 +7,8 @@ import (
"strconv" "strconv"
"sync" "sync"
"github.com/jetkvm/kvm/internal/confparser"
"github.com/jetkvm/kvm/internal/logging" "github.com/jetkvm/kvm/internal/logging"
"github.com/jetkvm/kvm/internal/network/types" "github.com/jetkvm/kvm/internal/network"
"github.com/jetkvm/kvm/internal/usbgadget" "github.com/jetkvm/kvm/internal/usbgadget"
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto" "github.com/prometheus/client_golang/prometheus/promauto"
@ -103,7 +102,7 @@ type Config struct {
TLSMode string `json:"tls_mode"` // options: "self-signed", "user-defined", "" TLSMode string `json:"tls_mode"` // options: "self-signed", "user-defined", ""
UsbConfig *usbgadget.Config `json:"usb_config"` UsbConfig *usbgadget.Config `json:"usb_config"`
UsbDevices *usbgadget.Devices `json:"usb_devices"` UsbDevices *usbgadget.Devices `json:"usb_devices"`
NetworkConfig *types.NetworkConfig `json:"network_config"` NetworkConfig *network.NetworkConfig `json:"network_config"`
DefaultLogLevel string `json:"default_log_level"` DefaultLogLevel string `json:"default_log_level"`
VideoSleepAfterSec int `json:"video_sleep_after_sec"` VideoSleepAfterSec int `json:"video_sleep_after_sec"`
} }
@ -129,32 +128,7 @@ func (c *Config) SetDisplayRotation(rotation string) error {
const configPath = "/userdata/kvm_config.json" const configPath = "/userdata/kvm_config.json"
// it's a temporary solution to avoid sharing the same pointer var defaultConfig = &Config{
// we should migrate to a proper config solution in the future
var (
defaultJigglerConfig = JigglerConfig{
InactivityLimitSeconds: 60,
JitterPercentage: 25,
ScheduleCronTab: "0 * * * * *",
Timezone: "UTC",
}
defaultUsbConfig = usbgadget.Config{
VendorId: "0x1d6b", //The Linux Foundation
ProductId: "0x0104", //Multifunction Composite Gadget
SerialNumber: "",
Manufacturer: "JetKVM",
Product: "USB Emulation Device",
}
defaultUsbDevices = usbgadget.Devices{
AbsoluteMouse: true,
RelativeMouse: true,
Keyboard: true,
MassStorage: true,
}
)
func getDefaultConfig() Config {
return Config{
CloudURL: "https://api.jetkvm.com", CloudURL: "https://api.jetkvm.com",
CloudAppURL: "https://app.jetkvm.com", CloudAppURL: "https://app.jetkvm.com",
AutoUpdateEnabled: true, // Set a default value AutoUpdateEnabled: true, // Set a default value
@ -167,17 +141,28 @@ func getDefaultConfig() Config {
DisplayOffAfterSec: 1800, // 30 minutes DisplayOffAfterSec: 1800, // 30 minutes
JigglerEnabled: false, JigglerEnabled: false,
// This is the "Standard" jiggler option in the UI // This is the "Standard" jiggler option in the UI
JigglerConfig: func() *JigglerConfig { c := defaultJigglerConfig; return &c }(), JigglerConfig: &JigglerConfig{
InactivityLimitSeconds: 60,
JitterPercentage: 25,
ScheduleCronTab: "0 * * * * *",
Timezone: "UTC",
},
TLSMode: "", TLSMode: "",
UsbConfig: func() *usbgadget.Config { c := defaultUsbConfig; return &c }(), UsbConfig: &usbgadget.Config{
UsbDevices: func() *usbgadget.Devices { c := defaultUsbDevices; return &c }(), VendorId: "0x1d6b", //The Linux Foundation
NetworkConfig: func() *types.NetworkConfig { ProductId: "0x0104", //Multifunction Composite Gadget
c := &types.NetworkConfig{} SerialNumber: "",
_ = confparser.SetDefaultsAndValidate(c) Manufacturer: "JetKVM",
return c Product: "USB Emulation Device",
}(), },
UsbDevices: &usbgadget.Devices{
AbsoluteMouse: true,
RelativeMouse: true,
Keyboard: true,
MassStorage: true,
},
NetworkConfig: &network.NetworkConfig{},
DefaultLogLevel: "INFO", DefaultLogLevel: "INFO",
}
} }
var ( var (
@ -210,8 +195,7 @@ func LoadConfig() {
} }
// load the default config // load the default config
defaultConfig := getDefaultConfig() config = defaultConfig
config = &defaultConfig
file, err := os.Open(configPath) file, err := os.Open(configPath)
if err != nil { if err != nil {
@ -223,7 +207,7 @@ func LoadConfig() {
defer file.Close() defer file.Close()
// load and merge the default config with the user config // load and merge the default config with the user config
loadedConfig := defaultConfig loadedConfig := *defaultConfig
if err := json.NewDecoder(file).Decode(&loadedConfig); err != nil { if err := json.NewDecoder(file).Decode(&loadedConfig); err != nil {
logger.Warn().Err(err).Msg("config file JSON parsing failed") logger.Warn().Err(err).Msg("config file JSON parsing failed")
configSuccess.Set(0.0) configSuccess.Set(0.0)
@ -232,19 +216,19 @@ func LoadConfig() {
// merge the user config with the default config // merge the user config with the default config
if loadedConfig.UsbConfig == nil { if loadedConfig.UsbConfig == nil {
loadedConfig.UsbConfig = getDefaultConfig().UsbConfig loadedConfig.UsbConfig = defaultConfig.UsbConfig
} }
if loadedConfig.UsbDevices == nil { if loadedConfig.UsbDevices == nil {
loadedConfig.UsbDevices = getDefaultConfig().UsbDevices loadedConfig.UsbDevices = defaultConfig.UsbDevices
} }
if loadedConfig.NetworkConfig == nil { if loadedConfig.NetworkConfig == nil {
loadedConfig.NetworkConfig = getDefaultConfig().NetworkConfig loadedConfig.NetworkConfig = defaultConfig.NetworkConfig
} }
if loadedConfig.JigglerConfig == nil { if loadedConfig.JigglerConfig == nil {
loadedConfig.JigglerConfig = getDefaultConfig().JigglerConfig loadedConfig.JigglerConfig = defaultConfig.JigglerConfig
} }
// fixup old keyboard layout value // fixup old keyboard layout value
@ -263,25 +247,17 @@ func LoadConfig() {
} }
func SaveConfig() error { func SaveConfig() error {
return saveConfig(configPath)
}
func SaveBackupConfig() error {
return saveConfig(configPath + ".bak")
}
func saveConfig(path string) error {
configLock.Lock() configLock.Lock()
defer configLock.Unlock() defer configLock.Unlock()
logger.Trace().Str("path", path).Msg("Saving config") logger.Trace().Str("path", configPath).Msg("Saving config")
// fixup old keyboard layout value // fixup old keyboard layout value
if config.KeyboardLayout == "en_US" { if config.KeyboardLayout == "en_US" {
config.KeyboardLayout = "en-US" config.KeyboardLayout = "en-US"
} }
file, err := os.Create(path) file, err := os.Create(configPath)
if err != nil { if err != nil {
return fmt.Errorf("failed to create config file: %w", err) return fmt.Errorf("failed to create config file: %w", err)
} }
@ -297,7 +273,7 @@ func saveConfig(path string) error {
return fmt.Errorf("failed to wite config: %w", err) return fmt.Errorf("failed to wite config: %w", err)
} }
logger.Info().Str("path", path).Msg("config saved") logger.Info().Str("path", configPath).Msg("config saved")
return nil return nil
} }

View File

@ -27,12 +27,7 @@ const (
) )
func switchToMainScreen() { func switchToMainScreen() {
if networkManager == nil { if networkState.IsUp() {
nativeInstance.SwitchToScreenIfDifferent("no_network_screen")
return
}
if networkManager.IsUp() {
nativeInstance.SwitchToScreenIfDifferent("home_screen") nativeInstance.SwitchToScreenIfDifferent("home_screen")
} else { } else {
nativeInstance.SwitchToScreenIfDifferent("no_network_screen") nativeInstance.SwitchToScreenIfDifferent("no_network_screen")
@ -40,21 +35,13 @@ func switchToMainScreen() {
} }
func updateDisplay() { func updateDisplay() {
if networkManager != nil { nativeInstance.UpdateLabelIfChanged("home_info_ipv4_addr", networkState.IPv4String())
nativeInstance.UpdateLabelIfChanged("home_info_ipv4_addr", networkManager.IPv4String()) nativeInstance.UpdateLabelAndChangeVisibility("home_info_ipv6_addr", networkState.IPv6String())
nativeInstance.UpdateLabelAndChangeVisibility("home_info_ipv6_addr", networkManager.IPv6String())
nativeInstance.UpdateLabelIfChanged("home_info_mac_addr", networkManager.MACString())
}
_, _ = nativeInstance.UIObjHide("menu_btn_network") _, _ = nativeInstance.UIObjHide("menu_btn_network")
_, _ = nativeInstance.UIObjHide("menu_btn_access") _, _ = nativeInstance.UIObjHide("menu_btn_access")
switch config.NetworkConfig.DHCPClient.String { nativeInstance.UpdateLabelIfChanged("home_info_mac_addr", networkState.MACString())
case "jetdhcpc":
nativeInstance.UpdateLabelIfChanged("dhcp_client_change_label", "Change to udhcpc")
case "udhcpc":
nativeInstance.UpdateLabelIfChanged("dhcp_client_change_label", "Change to JetKVM")
}
if usbState == "configured" { if usbState == "configured" {
nativeInstance.UpdateLabelIfChanged("usb_status_label", "Connected") nativeInstance.UpdateLabelIfChanged("usb_status_label", "Connected")
@ -72,7 +59,7 @@ func updateDisplay() {
} }
nativeInstance.UpdateLabelIfChanged("cloud_status_label", fmt.Sprintf("%d active", actionSessions)) nativeInstance.UpdateLabelIfChanged("cloud_status_label", fmt.Sprintf("%d active", actionSessions))
if networkManager != nil && networkManager.IsUp() { if networkState.IsUp() {
nativeInstance.UISetVar("main_screen", "home_screen") nativeInstance.UISetVar("main_screen", "home_screen")
nativeInstance.SwitchToScreenIf("home_screen", []string{"no_network_screen", "boot_screen"}) nativeInstance.SwitchToScreenIf("home_screen", []string{"no_network_screen", "boot_screen"})
} else { } else {
@ -188,7 +175,7 @@ func requestDisplayUpdate(shouldWakeDisplay bool, reason string) {
wakeDisplay(false, reason) wakeDisplay(false, reason)
} }
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()
}() }()
} }
@ -197,14 +184,13 @@ func waitCtrlAndRequestDisplayUpdate(shouldWakeDisplay bool, reason string) {
waitDisplayUpdate.Lock() waitDisplayUpdate.Lock()
defer waitDisplayUpdate.Unlock() defer waitDisplayUpdate.Unlock()
// nativeInstance.WaitCtrlClientConnected()
requestDisplayUpdate(shouldWakeDisplay, reason) requestDisplayUpdate(shouldWakeDisplay, reason)
} }
func updateStaticContents() { func updateStaticContents() {
//contents that never change //contents that never change
if networkManager != nil { nativeInstance.UpdateLabelIfChanged("home_info_mac_addr", networkState.MACString())
nativeInstance.UpdateLabelIfChanged("home_info_mac_addr", networkManager.MACString())
}
// get cpu info // get cpu info
if cpuInfo, err := os.ReadFile("/proc/cpuinfo"); err == nil { if cpuInfo, err := os.ReadFile("/proc/cpuinfo"); err == nil {
@ -340,9 +326,12 @@ func startBacklightTickers() {
dimTicker = time.NewTicker(time.Duration(config.DisplayDimAfterSec) * time.Second) dimTicker = time.NewTicker(time.Duration(config.DisplayDimAfterSec) * time.Second)
go func() { go func() {
for range dimTicker.C { for { //nolint:staticcheck
select {
case <-dimTicker.C:
tick_displayDim() tick_displayDim()
} }
}
}() }()
} }
@ -351,9 +340,12 @@ func startBacklightTickers() {
offTicker = time.NewTicker(time.Duration(config.DisplayOffAfterSec) * time.Second) offTicker = time.NewTicker(time.Duration(config.DisplayOffAfterSec) * time.Second)
go func() { go func() {
for range offTicker.C { for { //nolint:staticcheck
select {
case <-offTicker.C:
tick_displayOff() tick_displayOff()
} }
}
}() }()
} }
} }

8
go.mod
View File

@ -16,7 +16,6 @@ require (
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/guregu/null/v6 v6.0.0 github.com/guregu/null/v6 v6.0.0
github.com/gwatts/rootcerts v0.0.0-20250901182336-dc5ae18bd79f github.com/gwatts/rootcerts v0.0.0-20250901182336-dc5ae18bd79f
github.com/insomniacslk/dhcp v0.0.0-20250919081422-f80a1952f48e
github.com/pion/logging v0.2.4 github.com/pion/logging v0.2.4
github.com/pion/mdns/v2 v2.0.7 github.com/pion/mdns/v2 v2.0.7
github.com/pion/webrtc/v4 v4.1.4 github.com/pion/webrtc/v4 v4.1.4
@ -55,20 +54,15 @@ require (
github.com/go-playground/validator/v10 v10.27.0 // indirect github.com/go-playground/validator/v10 v10.27.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-json v0.10.5 // indirect
github.com/jonboulle/clockwork v0.5.0 // indirect github.com/jonboulle/clockwork v0.5.0 // indirect
github.com/josharian/native v1.1.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mdlayher/ndp v1.1.0 // indirect
github.com/mdlayher/packet v1.1.2 // indirect
github.com/mdlayher/socket v0.4.1 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pierrec/lz4/v4 v4.1.14 // indirect
github.com/pilebones/go-udev v0.9.1 // indirect github.com/pilebones/go-udev v0.9.1 // indirect
github.com/pion/datachannel v1.5.10 // indirect github.com/pion/datachannel v1.5.10 // indirect
github.com/pion/dtls/v3 v3.0.7 // indirect github.com/pion/dtls/v3 v3.0.7 // indirect
@ -88,14 +82,12 @@ require (
github.com/robfig/cron/v3 v3.0.1 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 // indirect
github.com/ugorji/go/codec v1.3.0 // indirect github.com/ugorji/go/codec v1.3.0 // indirect
github.com/vishvananda/netns v0.0.5 // indirect github.com/vishvananda/netns v0.0.5 // indirect
github.com/wlynxg/anet v0.0.5 // indirect github.com/wlynxg/anet v0.0.5 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect
golang.org/x/arch v0.20.0 // indirect golang.org/x/arch v0.20.0 // indirect
golang.org/x/oauth2 v0.30.0 // indirect golang.org/x/oauth2 v0.30.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/text v0.29.0 // indirect golang.org/x/text v0.29.0 // indirect
google.golang.org/protobuf v1.36.9 // indirect google.golang.org/protobuf v1.36.9 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect

22
go.sum
View File

@ -66,15 +66,8 @@ github.com/guregu/null/v6 v6.0.0 h1:N14VRS+4di81i1PXRiprbQJ9EM9gqBa0+KVMeS/QSjQ=
github.com/guregu/null/v6 v6.0.0/go.mod h1:hrMIhIfrOZeLPZhROSn149tpw2gHkidAqxoXNyeX3iQ= github.com/guregu/null/v6 v6.0.0/go.mod h1:hrMIhIfrOZeLPZhROSn149tpw2gHkidAqxoXNyeX3iQ=
github.com/gwatts/rootcerts v0.0.0-20250901182336-dc5ae18bd79f h1:08t2PbrkDgW2+mwCQ3jhKUBrCM9Bc9SeH5j2Dst3B+0= github.com/gwatts/rootcerts v0.0.0-20250901182336-dc5ae18bd79f h1:08t2PbrkDgW2+mwCQ3jhKUBrCM9Bc9SeH5j2Dst3B+0=
github.com/gwatts/rootcerts v0.0.0-20250901182336-dc5ae18bd79f/go.mod h1:5Kt9XkWvkGi2OHOq0QsGxebHmhCcqJ8KCbNg/a6+n+g= github.com/gwatts/rootcerts v0.0.0-20250901182336-dc5ae18bd79f/go.mod h1:5Kt9XkWvkGi2OHOq0QsGxebHmhCcqJ8KCbNg/a6+n+g=
github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714 h1:/jC7qQFrv8CrSJVmaolDVOxTfS9kc36uB6H40kdbQq8=
github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714/go.mod h1:2Goc3h8EklBH5mspfHFxBnEoURQCGzQQH1ga9Myjvis=
github.com/insomniacslk/dhcp v0.0.0-20250919081422-f80a1952f48e h1:nu5z6Kg+gMNW6tdqnVjg/QEJ8Nw71IJQqOtWj00XHEU=
github.com/insomniacslk/dhcp v0.0.0-20250919081422-f80a1952f48e/go.mod h1:qfvBmyDNp+/liLEYWRvqny/PEz9hGe2Dz833eXILSmo=
github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I= github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60= github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/josharian/native v1.1.0 h1:uuaP0hAbW7Y4l0ZRQ6C9zfb7Mg1mbFKry/xzDAfmtLA=
github.com/josharian/native v1.1.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
@ -99,12 +92,6 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mdlayher/ndp v1.1.0 h1:QylGKGVtH60sKZUE88+IW5ila1Z/M9/OXhWdsVKuscs=
github.com/mdlayher/ndp v1.1.0/go.mod h1:FmgESgemgjl38vuOIyAHWUUL6vQKA/pQNkvXdWsdQFM=
github.com/mdlayher/packet v1.1.2 h1:3Up1NG6LZrsgDVn6X4L9Ge/iyRyxFEFD9o6Pr3Q1nQY=
github.com/mdlayher/packet v1.1.2/go.mod h1:GEu1+n9sG5VtiRE4SydOmX5GTwyyYlteZiFU+x0kew4=
github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U=
github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@ -114,8 +101,6 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pierrec/lz4/v4 v4.1.14 h1:+fL8AQEZtz/ijeNnpduH0bROTu0O3NZAlPjQxGn8LwE=
github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pilebones/go-udev v0.9.1 h1:uN72M1C1fgzhsVmBGEM8w9RD1JY4iVsPZpr+Z6rb3O8= github.com/pilebones/go-udev v0.9.1 h1:uN72M1C1fgzhsVmBGEM8w9RD1JY4iVsPZpr+Z6rb3O8=
github.com/pilebones/go-udev v0.9.1/go.mod h1:Bgcl07crebF3JSeS4+nuaRvhWFdCeFoBhXXeAp93XNo= github.com/pilebones/go-udev v0.9.1/go.mod h1:Bgcl07crebF3JSeS4+nuaRvhWFdCeFoBhXXeAp93XNo=
github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o= github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o=
@ -176,8 +161,6 @@ github.com/sourcegraph/tf-dag v0.2.2-0.20250131204052-3e8ff1477b4f/go.mod h1:pzr
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
@ -186,8 +169,6 @@ github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923 h1:tHNk7XK9GkmKUR6Gh8gVBKXc2MVSZ4G/NnWLtzw4gNA=
github.com/u-root/uio v0.0.0-20230220225925-ffce2a382923/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264=
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
github.com/vearutop/statigz v1.5.0 h1:FuWwZiT82yBw4xbWdWIawiP2XFTyEPhIo8upRxiKLqk= github.com/vearutop/statigz v1.5.0 h1:FuWwZiT82yBw4xbWdWIawiP2XFTyEPhIo8upRxiKLqk=
@ -212,9 +193,6 @@ golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI=
golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

View File

@ -16,22 +16,22 @@ import (
type FieldConfig struct { type FieldConfig struct {
Name string Name string
Required bool Required bool
RequiredIf map[string]interface{} RequiredIf map[string]any
OneOf []string OneOf []string
ValidateTypes []string ValidateTypes []string
Defaults interface{} Defaults any
IsEmpty bool IsEmpty bool
CurrentValue interface{} CurrentValue any
TypeString string TypeString string
Delegated bool Delegated bool
shouldUpdateValue bool shouldUpdateValue bool
} }
func SetDefaultsAndValidate(config interface{}) error { func SetDefaultsAndValidate(config any) error {
return setDefaultsAndValidate(config, true) return setDefaultsAndValidate(config, true)
} }
func setDefaultsAndValidate(config interface{}, isRoot bool) error { func setDefaultsAndValidate(config any, isRoot bool) error {
// first we need to check if the config is a pointer // first we need to check if the config is a pointer
if reflect.TypeOf(config).Kind() != reflect.Ptr { if reflect.TypeOf(config).Kind() != reflect.Ptr {
return fmt.Errorf("config is not a pointer") return fmt.Errorf("config is not a pointer")
@ -55,7 +55,7 @@ func setDefaultsAndValidate(config interface{}, isRoot bool) error {
Name: field.Name, Name: field.Name,
OneOf: splitString(field.Tag.Get("one_of")), OneOf: splitString(field.Tag.Get("one_of")),
ValidateTypes: splitString(field.Tag.Get("validate_type")), ValidateTypes: splitString(field.Tag.Get("validate_type")),
RequiredIf: make(map[string]interface{}), RequiredIf: make(map[string]any),
CurrentValue: fieldValue.Interface(), CurrentValue: fieldValue.Interface(),
IsEmpty: false, IsEmpty: false,
TypeString: fieldType, TypeString: fieldType,
@ -142,8 +142,8 @@ func setDefaultsAndValidate(config interface{}, isRoot bool) error {
// now check if the field has required_if // now check if the field has required_if
requiredIf := field.Tag.Get("required_if") requiredIf := field.Tag.Get("required_if")
if requiredIf != "" { if requiredIf != "" {
requiredIfParts := strings.Split(requiredIf, ",") requiredIfParts := strings.SplitSeq(requiredIf, ",")
for _, part := range requiredIfParts { for part := range requiredIfParts {
partVal := strings.SplitN(part, "=", 2) partVal := strings.SplitN(part, "=", 2)
if len(partVal) != 2 { if len(partVal) != 2 {
return fmt.Errorf("invalid required_if for field `%s`: %s", field.Name, requiredIf) return fmt.Errorf("invalid required_if for field `%s`: %s", field.Name, requiredIf)
@ -168,7 +168,7 @@ func setDefaultsAndValidate(config interface{}, isRoot bool) error {
return nil return nil
} }
func validateFields(config interface{}, fields map[string]FieldConfig) error { func validateFields(config any, fields map[string]FieldConfig) error {
// now we can start to validate the fields // now we can start to validate the fields
for _, fieldConfig := range fields { for _, fieldConfig := range fields {
if err := fieldConfig.validate(fields); err != nil { if err := fieldConfig.validate(fields); err != nil {
@ -215,7 +215,7 @@ func (f *FieldConfig) validate(fields map[string]FieldConfig) error {
return nil return nil
} }
func (f *FieldConfig) populate(config interface{}) { func (f *FieldConfig) populate(config any) {
// update the field if it's not empty // update the field if it's not empty
if !f.shouldUpdateValue { if !f.shouldUpdateValue {
return return
@ -346,17 +346,6 @@ func (f *FieldConfig) validateField() error {
return nil return nil
} }
// Handle []string types, like dns servers, time sync ntp servers, etc.
if slice, ok := f.CurrentValue.([]string); ok {
for i, item := range slice {
if err := f.validateSingleValue(item, i); err != nil {
return err
}
}
return nil
}
// Handle single string types
val, err := toString(f.CurrentValue) val, err := toString(f.CurrentValue)
if err != nil { if err != nil {
return fmt.Errorf("field `%s` cannot use validate_type: %s", f.Name, err) return fmt.Errorf("field `%s` cannot use validate_type: %s", f.Name, err)
@ -366,71 +355,30 @@ func (f *FieldConfig) validateField() error {
return nil return nil
} }
return f.validateSingleValue(val, -1)
}
func (f *FieldConfig) validateSingleValue(val string, index int) error {
for _, validateType := range f.ValidateTypes { for _, validateType := range f.ValidateTypes {
var fieldRef string
if index >= 0 {
fieldRef = fmt.Sprintf("field `%s[%d]`", f.Name, index)
} else {
fieldRef = fmt.Sprintf("field `%s`", f.Name)
}
switch validateType { switch validateType {
case "int":
if _, err := strconv.Atoi(val); err != nil {
return fmt.Errorf("field `%s` is not a valid integer: %s", fieldRef, val)
}
case "ipv6_prefix_length":
valInt, err := strconv.Atoi(val)
if err != nil {
return fmt.Errorf("field `%s` is not a valid IPv6 prefix length: %s", fieldRef, val)
}
if valInt < 0 || valInt > 128 {
return fmt.Errorf("field `%s` is not a valid IPv6 prefix length: %s", fieldRef, val)
}
case "ipv4": case "ipv4":
if net.ParseIP(val).To4() == nil { if net.ParseIP(val).To4() == nil {
return fmt.Errorf("field `%s` is not a valid IPv4 address: %s", fieldRef, val) return fmt.Errorf("field `%s` is not a valid IPv4 address: %s", f.Name, val)
} }
case "ipv6": case "ipv6":
if net.ParseIP(val).To16() == nil { if net.ParseIP(val).To16() == nil {
return fmt.Errorf("field `%s` is not a valid IPv6 address: %s", fieldRef, val) return fmt.Errorf("field `%s` is not a valid IPv6 address: %s", f.Name, val)
}
case "ipv6_prefix":
if i, _, err := net.ParseCIDR(val); err != nil {
if i.To16() == nil {
return fmt.Errorf("field `%s` is not a valid IPv6 prefix: %s", fieldRef, val)
}
}
case "ipv4_or_ipv6":
if net.ParseIP(val) == nil {
return fmt.Errorf("%s is not a valid IPv4 or IPv6 address: %s", fieldRef, val)
} }
case "hwaddr": case "hwaddr":
if _, err := net.ParseMAC(val); err != nil { if _, err := net.ParseMAC(val); err != nil {
return fmt.Errorf("%s is not a valid MAC address: %s", fieldRef, val) return fmt.Errorf("field `%s` is not a valid MAC address: %s", f.Name, val)
} }
case "hostname": case "hostname":
if _, err := idna.Lookup.ToASCII(val); err != nil { if _, err := idna.Lookup.ToASCII(val); err != nil {
return fmt.Errorf("%s is not a valid hostname: %s", fieldRef, val) return fmt.Errorf("field `%s` is not a valid hostname: %s", f.Name, val)
} }
case "proxy": case "proxy":
if url, err := url.Parse(val); err != nil || (url.Scheme != "http" && url.Scheme != "https") || url.Host == "" { if url, err := url.Parse(val); err != nil || (url.Scheme != "http" && url.Scheme != "https") || url.Host == "" {
return fmt.Errorf("%s is not a valid HTTP proxy URL: %s", fieldRef, val) return fmt.Errorf("field `%s` is not a valid HTTP proxy URL: %s", f.Name, val)
}
case "url":
if _, err := url.Parse(val); err != nil {
return fmt.Errorf("%s is not a valid URL: %s", fieldRef, val)
}
case "cidr":
if _, _, err := net.ParseCIDR(val); err != nil {
return fmt.Errorf("%s is not a valid CIDR notation: %s", fieldRef, val)
} }
default: default:
return fmt.Errorf("field `%s` cannot use validate_type: unsupported validator: %s", fieldRef, validateType) return fmt.Errorf("field `%s` cannot use validate_type: unsupported validator: %s", f.Name, validateType)
} }
} }

View File

@ -25,7 +25,7 @@ type testIPv4StaticConfig struct {
type testIPv6StaticConfig struct { type testIPv6StaticConfig struct {
Address null.String `json:"address" validate_type:"ipv6" required:"true"` Address null.String `json:"address" validate_type:"ipv6" required:"true"`
PrefixLength null.Int `json:"prefix_length" validate_type:"ipv6_prefix_length" required:"true"` Prefix null.String `json:"prefix" validate_type:"ipv6" required:"true"`
Gateway null.String `json:"gateway" validate_type:"ipv6" required:"true"` Gateway null.String `json:"gateway" validate_type:"ipv6" required:"true"`
DNS []string `json:"dns" validate_type:"ipv6" required:"true"` DNS []string `json:"dns" validate_type:"ipv6" required:"true"`
} }
@ -39,7 +39,7 @@ type testNetworkConfig struct {
IPv6Mode null.String `json:"ipv6_mode" one_of:"slaac,dhcpv6,slaac_and_dhcpv6,static,link_local,disabled" default:"slaac"` IPv6Mode null.String `json:"ipv6_mode" one_of:"slaac,dhcpv6,slaac_and_dhcpv6,static,link_local,disabled" default:"slaac"`
IPv6Static *testIPv6StaticConfig `json:"ipv6_static,omitempty" required_if:"IPv6Mode=static"` IPv6Static *testIPv6StaticConfig `json:"ipv6_static,omitempty" required_if:"IPv6Mode=static"`
LLDPMode null.String `json:"lldp_mode,omitempty" one_of:"disabled,basic,all" default:"basic"` LLDPMode null.String `json:"lldp_mode,omitempty" one_of:"disabled,rx_only,tx_only,enabled" default:"enabled"`
LLDPTxTLVs []string `json:"lldp_tx_tlvs,omitempty" one_of:"chassis,port,system,vlan" default:"chassis,port,system,vlan"` LLDPTxTLVs []string `json:"lldp_tx_tlvs,omitempty" one_of:"chassis,port,system,vlan" default:"chassis,port,system,vlan"`
MDNSMode null.String `json:"mdns_mode,omitempty" one_of:"disabled,auto,ipv4_only,ipv6_only" default:"auto"` MDNSMode null.String `json:"mdns_mode,omitempty" one_of:"disabled,auto,ipv4_only,ipv6_only" default:"auto"`
TimeSyncMode null.String `json:"time_sync_mode,omitempty" one_of:"ntp_only,ntp_and_http,http_only,custom" default:"ntp_and_http"` TimeSyncMode null.String `json:"time_sync_mode,omitempty" one_of:"ntp_only,ntp_and_http,http_only,custom" default:"ntp_and_http"`

View File

@ -16,7 +16,7 @@ func splitString(s string) []string {
return strings.Split(s, ",") return strings.Split(s, ",")
} }
func toString(v interface{}) (string, error) { func toString(v any) (string, error) {
switch v := v.(type) { switch v := v.(type) {
case string: case string:
return v, nil return v, nil

View File

@ -146,17 +146,14 @@ func (m *MDNS) start(allowRestart bool) error {
return nil return nil
} }
// Start starts the mDNS server
func (m *MDNS) Start() error { func (m *MDNS) Start() error {
return m.start(false) return m.start(false)
} }
// Restart restarts the mDNS server
func (m *MDNS) Restart() error { func (m *MDNS) Restart() error {
return m.start(true) return m.start(true)
} }
// Stop stops the mDNS server
func (m *MDNS) Stop() error { func (m *MDNS) Stop() error {
m.lock.Lock() m.lock.Lock()
defer m.lock.Unlock() defer m.lock.Unlock()
@ -168,45 +165,26 @@ func (m *MDNS) Stop() error {
return m.conn.Close() return m.conn.Close()
} }
func (m *MDNS) setLocalNames(localNames []string) { func (m *MDNS) SetLocalNames(localNames []string, always bool) error {
m.lock.Lock() if reflect.DeepEqual(m.localNames, localNames) && !always {
defer m.lock.Unlock() return nil
if reflect.DeepEqual(m.localNames, localNames) {
return
} }
m.localNames = localNames m.localNames = localNames
_ = m.Restart()
return nil
} }
func (m *MDNS) setListenOptions(listenOptions *MDNSListenOptions) { func (m *MDNS) SetListenOptions(listenOptions *MDNSListenOptions) error {
m.lock.Lock()
defer m.lock.Unlock()
if m.listenOptions != nil && if m.listenOptions != nil &&
m.listenOptions.IPv4 == listenOptions.IPv4 && m.listenOptions.IPv4 == listenOptions.IPv4 &&
m.listenOptions.IPv6 == listenOptions.IPv6 { m.listenOptions.IPv6 == listenOptions.IPv6 {
return return nil
} }
m.listenOptions = listenOptions m.listenOptions = listenOptions
} _ = m.Restart()
// SetLocalNames sets the local names and restarts the mDNS server return nil
func (m *MDNS) SetLocalNames(localNames []string) error {
m.setLocalNames(localNames)
return m.Restart()
}
// SetListenOptions sets the listen options and restarts the mDNS server
func (m *MDNS) SetListenOptions(listenOptions *MDNSListenOptions) error {
m.setListenOptions(listenOptions)
return m.Restart()
}
// SetOptions sets the local names and listen options and restarts the mDNS server
func (m *MDNS) SetOptions(options *MDNSOptions) error {
m.setLocalNames(options.LocalNames)
m.setListenOptions(options.ListenOptions)
return m.Restart()
} }

File diff suppressed because one or more lines are too long

View File

@ -48,10 +48,6 @@ void action_switch_to_reset_config(lv_event_t *e) {
loadScreen(SCREEN_ID_RESET_CONFIG_SCREEN); loadScreen(SCREEN_ID_RESET_CONFIG_SCREEN);
} }
void action_switch_to_dhcpc(lv_event_t *e) {
loadScreen(SCREEN_ID_SWITCH_DHCP_CLIENT_SCREEN);
}
void action_switch_to_reboot(lv_event_t *e) { void action_switch_to_reboot(lv_event_t *e) {
loadScreen(SCREEN_ID_REBOOT_SCREEN); loadScreen(SCREEN_ID_REBOOT_SCREEN);
} }
@ -79,19 +75,15 @@ void action_about_screen_gesture(lv_event_t * e) {
// user_data doesn't seem to be working, so we use a global variable here // user_data doesn't seem to be working, so we use a global variable here
static uint32_t t_reset_config; static uint32_t t_reset_config;
static uint32_t t_reboot; static uint32_t t_reboot;
static uint32_t t_dhcpc;
static bool b_reboot = false; static bool b_reboot = false;
static bool b_reset_config = false; static bool b_reset_config = false;
static bool b_dhcpc = false;
static bool b_reboot_lock = false; static bool b_reboot_lock = false;
static bool b_reset_config_lock = false; static bool b_reset_config_lock = false;
static bool b_dhcpc_lock = false;
const int RESET_CONFIG_HOLD_TIME = 10; const int RESET_CONFIG_HOLD_TIME = 10;
const int REBOOT_HOLD_TIME = 5; const int REBOOT_HOLD_TIME = 5;
const int DHCPC_HOLD_TIME = 5;
typedef struct { typedef struct {
uint32_t *start_time; uint32_t *start_time;
@ -161,22 +153,6 @@ void action_reset_config(lv_event_t * e) {
handle_hold_action(e, &config); handle_hold_action(e, &config);
} }
void action_dhcpc(lv_event_t * e) {
hold_action_config_t config = {
.start_time = &t_dhcpc,
.completed = &b_dhcpc,
.lock = &b_dhcpc_lock,
.hold_time_seconds = DHCPC_HOLD_TIME,
.rpc_method = "toggleDHCPClient",
.button_obj = NULL, // No button/spinner for reboot
.spinner_obj = NULL,
.label_obj = objects.dhcpc_label,
.default_text = "Press and hold for\n5 seconds"
};
handle_hold_action(e, &config);
}
void action_reboot(lv_event_t * e) { void action_reboot(lv_event_t * e) {
hold_action_config_t config = { hold_action_config_t config = {
.start_time = &t_reboot, .start_time = &t_reboot,

View File

@ -24,8 +24,6 @@ extern void action_handle_common_press_event(lv_event_t * e);
extern void action_reset_config(lv_event_t * e); extern void action_reset_config(lv_event_t * e);
extern void action_reboot(lv_event_t * e); extern void action_reboot(lv_event_t * e);
extern void action_switch_to_reboot(lv_event_t * e); extern void action_switch_to_reboot(lv_event_t * e);
extern void action_dhcpc(lv_event_t * e);
extern void action_switch_to_dhcpc(lv_event_t * e);
#ifdef __cplusplus #ifdef __cplusplus

View File

@ -887,26 +887,6 @@ void create_screen_menu_advanced_screen() {
} }
} }
} }
{
// MenuBtnDHCPClient
lv_obj_t *obj = lv_button_create(parent_obj);
objects.menu_btn_dhcp_client = obj;
lv_obj_set_pos(obj, 0, 0);
lv_obj_set_size(obj, LV_PCT(100), 50);
lv_obj_add_event_cb(obj, action_switch_to_dhcpc, LV_EVENT_PRESSED, (void *)0);
lv_obj_clear_flag(obj, LV_OBJ_FLAG_SNAPPABLE);
add_style_menu_button(obj);
{
lv_obj_t *parent_obj = obj;
{
lv_obj_t *obj = lv_label_create(parent_obj);
lv_obj_set_pos(obj, 0, 0);
lv_obj_set_size(obj, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
add_style_menu_button_label(obj);
lv_label_set_text(obj, "DHCP Client");
}
}
}
{ {
// MenuBtnAdvancedResetConfig // MenuBtnAdvancedResetConfig
lv_obj_t *obj = lv_button_create(parent_obj); lv_obj_t *obj = lv_button_create(parent_obj);
@ -2217,221 +2197,6 @@ void create_screen_rebooting_screen() {
void tick_screen_rebooting_screen() { void tick_screen_rebooting_screen() {
} }
void create_screen_switch_dhcp_client_screen() {
lv_obj_t *obj = lv_obj_create(0);
objects.switch_dhcp_client_screen = obj;
lv_obj_set_pos(obj, 0, 0);
lv_obj_set_size(obj, 300, 240);
lv_obj_add_event_cb(obj, action_about_screen_gesture, LV_EVENT_GESTURE, (void *)0);
add_style_flex_screen_menu(obj);
{
lv_obj_t *parent_obj = obj;
{
lv_obj_t *obj = lv_obj_create(parent_obj);
lv_obj_set_pos(obj, 0, 0);
lv_obj_set_size(obj, LV_PCT(100), LV_PCT(100));
lv_obj_set_style_pad_left(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_pad_top(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_pad_right(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_pad_bottom(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_bg_opa(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_border_width(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_radius(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_clear_flag(obj, LV_OBJ_FLAG_SCROLLABLE);
add_style_flex_start(obj);
{
lv_obj_t *parent_obj = obj;
{
// DHCPClientHeader
lv_obj_t *obj = lv_obj_create(parent_obj);
objects.dhcp_client_header = obj;
lv_obj_set_pos(obj, 0, 0);
lv_obj_set_size(obj, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_pad_left(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_pad_top(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_pad_bottom(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_bg_opa(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_border_width(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_radius(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
add_style_flow_row_space_between(obj);
lv_obj_set_style_pad_right(obj, 4, LV_PART_MAIN | LV_STATE_DEFAULT);
{
lv_obj_t *parent_obj = obj;
{
lv_obj_t *obj = lv_button_create(parent_obj);
lv_obj_set_pos(obj, 0, 0);
lv_obj_set_size(obj, 32, 32);
lv_obj_add_event_cb(obj, action_switch_to_menu, LV_EVENT_CLICKED, (void *)0);
add_style_back_button(obj);
{
lv_obj_t *parent_obj = obj;
{
lv_obj_t *obj = lv_image_create(parent_obj);
lv_obj_set_pos(obj, -1, 2);
lv_obj_set_size(obj, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
lv_image_set_src(obj, &img_back_caret);
}
}
}
{
lv_obj_t *obj = lv_label_create(parent_obj);
lv_obj_set_pos(obj, LV_PCT(0), LV_PCT(0));
lv_obj_set_size(obj, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
add_style_header_link(obj);
lv_label_set_text(obj, "DHCP Client");
}
}
}
{
// DHCPClientContainer
lv_obj_t *obj = lv_obj_create(parent_obj);
objects.dhcp_client_container = obj;
lv_obj_set_pos(obj, 0, 0);
lv_obj_set_size(obj, LV_PCT(100), LV_PCT(80));
lv_obj_set_style_pad_left(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_pad_top(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_pad_bottom(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_bg_opa(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_border_width(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_radius(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_scrollbar_mode(obj, LV_SCROLLBAR_MODE_AUTO);
lv_obj_set_scroll_dir(obj, LV_DIR_VER);
lv_obj_set_scroll_snap_x(obj, LV_SCROLL_SNAP_START);
add_style_flex_column_start(obj);
lv_obj_set_style_pad_right(obj, 4, LV_PART_MAIN | LV_STATE_DEFAULT);
{
lv_obj_t *parent_obj = obj;
{
lv_obj_t *obj = lv_obj_create(parent_obj);
lv_obj_set_pos(obj, 0, 0);
lv_obj_set_size(obj, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_pad_left(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_pad_top(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_pad_bottom(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_bg_opa(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_border_width(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_radius(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_clear_flag(obj, LV_OBJ_FLAG_SCROLLABLE);
add_style_flex_column_start(obj);
lv_obj_set_style_pad_right(obj, 10, LV_PART_MAIN | LV_STATE_DEFAULT);
{
lv_obj_t *parent_obj = obj;
{
// DHCPClientLabelContainer
lv_obj_t *obj = lv_obj_create(parent_obj);
objects.dhcp_client_label_container = obj;
lv_obj_set_pos(obj, 0, 0);
lv_obj_set_size(obj, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_bg_opa(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_border_width(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_radius(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_clear_flag(obj, LV_OBJ_FLAG_SCROLLABLE);
add_style_flex_column_start(obj);
lv_obj_set_style_pad_right(obj, 10, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_pad_left(obj, 10, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_pad_top(obj, 10, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_pad_bottom(obj, 10, LV_PART_MAIN | LV_STATE_DEFAULT);
{
lv_obj_t *parent_obj = obj;
{
// DHCPC_Label
lv_obj_t *obj = lv_label_create(parent_obj);
objects.dhcpc_label = obj;
lv_obj_set_pos(obj, 0, 0);
lv_obj_set_size(obj, LV_PCT(100), LV_SIZE_CONTENT);
add_style_info_content_label(obj);
lv_obj_set_style_text_font(obj, &ui_font_font_book20, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_label_set_text(obj, "Press and hold for\n5 seconds");
}
}
}
{
// DHCPClientSpinner
lv_obj_t *obj = lv_obj_create(parent_obj);
objects.dhcp_client_spinner = obj;
lv_obj_set_pos(obj, 0, 0);
lv_obj_set_size(obj, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_pad_left(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_pad_top(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_pad_right(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_pad_bottom(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_bg_opa(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_border_width(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_radius(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_add_flag(obj, LV_OBJ_FLAG_HIDDEN);
lv_obj_clear_flag(obj, LV_OBJ_FLAG_CLICKABLE|LV_OBJ_FLAG_SCROLLABLE);
add_style_flex_column_start(obj);
lv_obj_set_style_flex_main_place(obj, LV_FLEX_ALIGN_CENTER, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_flex_cross_place(obj, LV_FLEX_ALIGN_CENTER, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_flex_track_place(obj, LV_FLEX_ALIGN_CENTER, LV_PART_MAIN | LV_STATE_DEFAULT);
{
lv_obj_t *parent_obj = obj;
{
lv_obj_t *obj = lv_spinner_create(parent_obj);
lv_obj_set_pos(obj, 0, 0);
lv_obj_set_size(obj, 80, 80);
lv_spinner_set_anim_params(obj, 1000, 60);
}
}
}
{
// DHCPClientButton
lv_obj_t *obj = lv_obj_create(parent_obj);
objects.dhcp_client_button = obj;
lv_obj_set_pos(obj, 0, 0);
lv_obj_set_size(obj, LV_PCT(100), LV_SIZE_CONTENT);
lv_obj_set_style_pad_left(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_pad_top(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_pad_right(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_pad_bottom(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_bg_opa(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_border_width(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_radius(obj, 0, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_clear_flag(obj, LV_OBJ_FLAG_SCROLLABLE);
add_style_flex_column_start(obj);
{
lv_obj_t *parent_obj = obj;
{
lv_obj_t *obj = lv_button_create(parent_obj);
objects.obj2 = obj;
lv_obj_set_pos(obj, 0, 0);
lv_obj_set_size(obj, LV_PCT(100), 50);
lv_obj_add_event_cb(obj, action_dhcpc, LV_EVENT_PRESSED, (void *)0);
lv_obj_add_event_cb(obj, action_dhcpc, LV_EVENT_PRESSING, (void *)0);
lv_obj_add_event_cb(obj, action_dhcpc, LV_EVENT_RELEASED, (void *)0);
lv_obj_set_style_bg_color(obj, lv_color_hex(0xffdc2626), LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_text_align(obj, LV_TEXT_ALIGN_LEFT, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_pad_right(obj, 13, LV_PART_MAIN | LV_STATE_DEFAULT);
{
lv_obj_t *parent_obj = obj;
{
// DHCPClientChangeLabel
lv_obj_t *obj = lv_label_create(parent_obj);
objects.dhcp_client_change_label = obj;
lv_obj_set_pos(obj, 0, 0);
lv_obj_set_size(obj, LV_SIZE_CONTENT, LV_SIZE_CONTENT);
lv_obj_set_style_align(obj, LV_ALIGN_CENTER, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_obj_set_style_text_align(obj, LV_TEXT_ALIGN_LEFT, LV_PART_MAIN | LV_STATE_DEFAULT);
lv_label_set_text(obj, "Switch to udhcpc");
}
}
}
}
}
}
}
}
}
}
}
}
tick_screen_switch_dhcp_client_screen();
}
void tick_screen_switch_dhcp_client_screen() {
}
typedef void (*tick_screen_func_t)(); typedef void (*tick_screen_func_t)();
@ -2447,7 +2212,6 @@ tick_screen_func_t tick_screen_funcs[] = {
tick_screen_reset_config_screen, tick_screen_reset_config_screen,
tick_screen_reboot_screen, tick_screen_reboot_screen,
tick_screen_rebooting_screen, tick_screen_rebooting_screen,
tick_screen_switch_dhcp_client_screen,
}; };
void tick_screen(int screen_index) { void tick_screen(int screen_index) {
tick_screen_funcs[screen_index](); tick_screen_funcs[screen_index]();
@ -2472,5 +2236,4 @@ void create_screens() {
create_screen_reset_config_screen(); create_screen_reset_config_screen();
create_screen_reboot_screen(); create_screen_reboot_screen();
create_screen_rebooting_screen(); create_screen_rebooting_screen();
create_screen_switch_dhcp_client_screen();
} }

View File

@ -19,7 +19,6 @@ typedef struct _objects_t {
lv_obj_t *reset_config_screen; lv_obj_t *reset_config_screen;
lv_obj_t *reboot_screen; lv_obj_t *reboot_screen;
lv_obj_t *rebooting_screen; lv_obj_t *rebooting_screen;
lv_obj_t *switch_dhcp_client_screen;
lv_obj_t *boot_logo; lv_obj_t *boot_logo;
lv_obj_t *boot_screen_version; lv_obj_t *boot_screen_version;
lv_obj_t *no_network_header_container; lv_obj_t *no_network_header_container;
@ -55,7 +54,6 @@ typedef struct _objects_t {
lv_obj_t *menu_btn_advanced_developer_mode; lv_obj_t *menu_btn_advanced_developer_mode;
lv_obj_t *menu_btn_advanced_usb_emulation; lv_obj_t *menu_btn_advanced_usb_emulation;
lv_obj_t *menu_btn_advanced_reboot; lv_obj_t *menu_btn_advanced_reboot;
lv_obj_t *menu_btn_dhcp_client;
lv_obj_t *menu_btn_advanced_reset_config; lv_obj_t *menu_btn_advanced_reset_config;
lv_obj_t *menu_header_container_2; lv_obj_t *menu_header_container_2;
lv_obj_t *menu_items_container_2; lv_obj_t *menu_items_container_2;
@ -103,14 +101,6 @@ typedef struct _objects_t {
lv_obj_t *obj1; lv_obj_t *obj1;
lv_obj_t *reboot_in_progress_logo; lv_obj_t *reboot_in_progress_logo;
lv_obj_t *reboot_in_progress_label; lv_obj_t *reboot_in_progress_label;
lv_obj_t *dhcp_client_header;
lv_obj_t *dhcp_client_container;
lv_obj_t *dhcp_client_label_container;
lv_obj_t *dhcpc_label;
lv_obj_t *dhcp_client_spinner;
lv_obj_t *dhcp_client_button;
lv_obj_t *obj2;
lv_obj_t *dhcp_client_change_label;
} objects_t; } objects_t;
extern objects_t objects; extern objects_t objects;
@ -127,7 +117,6 @@ enum ScreensEnum {
SCREEN_ID_RESET_CONFIG_SCREEN = 9, SCREEN_ID_RESET_CONFIG_SCREEN = 9,
SCREEN_ID_REBOOT_SCREEN = 10, SCREEN_ID_REBOOT_SCREEN = 10,
SCREEN_ID_REBOOTING_SCREEN = 11, SCREEN_ID_REBOOTING_SCREEN = 11,
SCREEN_ID_SWITCH_DHCP_CLIENT_SCREEN = 12,
}; };
void create_screen_boot_screen(); void create_screen_boot_screen();
@ -163,9 +152,6 @@ void tick_screen_reboot_screen();
void create_screen_rebooting_screen(); void create_screen_rebooting_screen();
void tick_screen_rebooting_screen(); void tick_screen_rebooting_screen();
void create_screen_switch_dhcp_client_screen();
void tick_screen_switch_dhcp_client_screen();
void tick_screen_by_id(enum ScreensEnum screenId); void tick_screen_by_id(enum ScreensEnum screenId);
void tick_screen(int screen_index); void tick_screen(int screen_index);

View File

@ -1,13 +1,25 @@
package types package network
import ( import (
"fmt"
"net"
"net/http" "net/http"
"net/url" "net/url"
"time"
"github.com/guregu/null/v6" "github.com/guregu/null/v6"
"github.com/jetkvm/kvm/internal/mdns"
"golang.org/x/net/idna"
) )
// IPv4StaticConfig represents static IPv4 configuration 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 IPv4StaticConfig struct { type IPv4StaticConfig struct {
Address null.String `json:"address,omitempty" validate_type:"ipv4" required:"true"` Address null.String `json:"address,omitempty" validate_type:"ipv4" required:"true"`
Netmask null.String `json:"netmask,omitempty" validate_type:"ipv4" required:"true"` Netmask null.String `json:"netmask,omitempty" validate_type:"ipv4" required:"true"`
@ -15,23 +27,13 @@ type IPv4StaticConfig struct {
DNS []string `json:"dns,omitempty" validate_type:"ipv4" required:"true"` DNS []string `json:"dns,omitempty" validate_type:"ipv4" required:"true"`
} }
// IPv6StaticConfig represents static IPv6 configuration
type IPv6StaticConfig struct { type IPv6StaticConfig struct {
Prefix null.String `json:"prefix,omitempty" validate_type:"ipv6_prefix" required:"true"` Address null.String `json:"address,omitempty" validate_type:"ipv6" required:"true"`
Prefix null.String `json:"prefix,omitempty" validate_type:"ipv6" required:"true"`
Gateway null.String `json:"gateway,omitempty" validate_type:"ipv6" required:"true"` Gateway null.String `json:"gateway,omitempty" validate_type:"ipv6" required:"true"`
DNS []string `json:"dns,omitempty" validate_type:"ipv6" required:"true"` DNS []string `json:"dns,omitempty" validate_type:"ipv6" required:"true"`
} }
// MDNSListenOptions represents MDNS listening options
type MDNSListenOptions struct {
IPv4 bool
IPv6 bool
}
// NetworkConfig represents the complete network configuration for an interface
type NetworkConfig struct { type NetworkConfig struct {
DHCPClient null.String `json:"dhcp_client,omitempty" one_of:"jetdhcpc,udhcpc" default:"jetdhcpc"`
Hostname null.String `json:"hostname,omitempty" validate_type:"hostname"` Hostname null.String `json:"hostname,omitempty" validate_type:"hostname"`
HTTPProxy null.String `json:"http_proxy,omitempty" validate_type:"proxy"` HTTPProxy null.String `json:"http_proxy,omitempty" validate_type:"proxy"`
Domain null.String `json:"domain,omitempty" validate_type:"hostname"` Domain null.String `json:"domain,omitempty" validate_type:"hostname"`
@ -42,7 +44,7 @@ type NetworkConfig struct {
IPv6Mode null.String `json:"ipv6_mode,omitempty" one_of:"slaac,dhcpv6,slaac_and_dhcpv6,static,link_local,disabled" default:"slaac"` IPv6Mode null.String `json:"ipv6_mode,omitempty" one_of:"slaac,dhcpv6,slaac_and_dhcpv6,static,link_local,disabled" default:"slaac"`
IPv6Static *IPv6StaticConfig `json:"ipv6_static,omitempty" required_if:"IPv6Mode=static"` IPv6Static *IPv6StaticConfig `json:"ipv6_static,omitempty" required_if:"IPv6Mode=static"`
LLDPMode null.String `json:"lldp_mode,omitempty" one_of:"disabled,basic,all" default:"basic"` LLDPMode null.String `json:"lldp_mode,omitempty" one_of:"disabled,rx_only,tx_only,basic,all,enabled" default:"enabled"`
LLDPTxTLVs []string `json:"lldp_tx_tlvs,omitempty" one_of:"chassis,port,system,vlan" default:"chassis,port,system,vlan"` LLDPTxTLVs []string `json:"lldp_tx_tlvs,omitempty" one_of:"chassis,port,system,vlan" default:"chassis,port,system,vlan"`
MDNSMode null.String `json:"mdns_mode,omitempty" one_of:"disabled,auto,ipv4_only,ipv6_only" default:"auto"` MDNSMode null.String `json:"mdns_mode,omitempty" one_of:"disabled,auto,ipv4_only,ipv6_only" default:"auto"`
TimeSyncMode null.String `json:"time_sync_mode,omitempty" one_of:"ntp_only,ntp_and_http,http_only,custom" default:"ntp_and_http"` TimeSyncMode null.String `json:"time_sync_mode,omitempty" one_of:"ntp_only,ntp_and_http,http_only,custom" default:"ntp_and_http"`
@ -53,15 +55,13 @@ type NetworkConfig struct {
TimeSyncHTTPUrls []string `json:"time_sync_http_urls,omitempty" validate_type:"url" required_if:"TimeSyncOrdering=http_user_provided"` TimeSyncHTTPUrls []string `json:"time_sync_http_urls,omitempty" validate_type:"url" required_if:"TimeSyncOrdering=http_user_provided"`
} }
// GetMDNSMode returns the MDNS mode configuration func (c *NetworkConfig) GetMDNSMode() *mdns.MDNSListenOptions {
func (c *NetworkConfig) GetMDNSMode() *MDNSListenOptions { listenOptions := &mdns.MDNSListenOptions{
mode := c.MDNSMode.String IPv4: c.IPv4Mode.String != "disabled",
listenOptions := &MDNSListenOptions{ IPv6: c.IPv6Mode.String != "disabled",
IPv4: true,
IPv6: true,
} }
switch mode { switch c.MDNSMode.String {
case "ipv4_only": case "ipv4_only":
listenOptions.IPv6 = false listenOptions.IPv6 = false
case "ipv6_only": case "ipv6_only":
@ -74,21 +74,53 @@ func (c *NetworkConfig) GetMDNSMode() *MDNSListenOptions {
return listenOptions return listenOptions
} }
// GetTransportProxyFunc returns a function for HTTP proxy configuration func (s *NetworkConfig) GetTransportProxyFunc() func(*http.Request) (*url.URL, error) {
func (c *NetworkConfig) GetTransportProxyFunc() func(*http.Request) (*url.URL, error) {
return func(*http.Request) (*url.URL, error) { return func(*http.Request) (*url.URL, error) {
if c.HTTPProxy.String == "" { if s.HTTPProxy.String == "" {
return nil, nil return nil, nil
} else { } else {
proxyURL, _ := url.Parse(c.HTTPProxy.String) proxyUrl, _ := url.Parse(s.HTTPProxy.String)
return proxyURL, nil return proxyUrl, nil
} }
} }
} }
// NetworkConfig interface for backward compatibility func (s *NetworkInterfaceState) GetHostname() string {
type NetworkConfigInterface interface { hostname := ToValidHostname(s.config.Hostname.String)
InterfaceName() string
IPv4Addresses() []IPAddress if hostname == "" {
IPv6Addresses() []IPAddress return s.defaultHostname
}
return hostname
}
func ToValidDomain(domain string) string {
ascii, err := idna.Lookup.ToASCII(domain)
if err != nil {
return ""
}
return ascii
}
func (s *NetworkInterfaceState) GetDomain() string {
domain := ToValidDomain(s.config.Domain.String)
if domain == "" {
lease := s.dhcpClient.GetLease()
if lease != nil && lease.Domain != "" {
domain = ToValidDomain(lease.Domain)
}
}
if domain == "" {
return "local"
}
return domain
}
func (s *NetworkInterfaceState) GetFQDN() string {
return fmt.Sprintf("%s.%s", s.GetHostname(), s.GetDomain())
} }

11
internal/network/dhcp.go Normal file
View File

@ -0,0 +1,11 @@
package network
type DhcpTargetState int
const (
DhcpTargetStateDoNothing DhcpTargetState = iota
DhcpTargetStateStart
DhcpTargetStateStop
DhcpTargetStateRenew
DhcpTargetStateRelease
)

View File

@ -0,0 +1,137 @@
package network
import (
"fmt"
"io"
"os"
"os/exec"
"strings"
"sync"
"golang.org/x/net/idna"
)
const (
hostnamePath = "/etc/hostname"
hostsPath = "/etc/hosts"
)
var (
hostnameLock sync.Mutex = sync.Mutex{}
)
func updateEtcHosts(hostname string, fqdn string) error {
// update /etc/hosts
hostsFile, err := os.OpenFile(hostsPath, os.O_RDWR|os.O_SYNC, os.ModeExclusive)
if err != nil {
return fmt.Errorf("failed to open %s: %w", hostsPath, err)
}
defer hostsFile.Close()
// read all lines
if _, err := hostsFile.Seek(0, io.SeekStart); err != nil {
return fmt.Errorf("failed to seek %s: %w", hostsPath, err)
}
lines, err := io.ReadAll(hostsFile)
if err != nil {
return fmt.Errorf("failed to read %s: %w", hostsPath, err)
}
newLines := []string{}
hostLine := fmt.Sprintf("127.0.1.1\t%s %s", hostname, fqdn)
hostLineExists := false
for line := range strings.SplitSeq(string(lines), "\n") {
if strings.HasPrefix(line, "127.0.1.1") {
hostLineExists = true
line = hostLine
}
newLines = append(newLines, line)
}
if !hostLineExists {
newLines = append(newLines, hostLine)
}
if err := hostsFile.Truncate(0); err != nil {
return fmt.Errorf("failed to truncate %s: %w", hostsPath, err)
}
if _, err := hostsFile.Seek(0, io.SeekStart); err != nil {
return fmt.Errorf("failed to seek %s: %w", hostsPath, err)
}
if _, err := hostsFile.Write([]byte(strings.Join(newLines, "\n"))); err != nil {
return fmt.Errorf("failed to write %s: %w", hostsPath, err)
}
return nil
}
func ToValidHostname(hostname string) string {
ascii, err := idna.Lookup.ToASCII(hostname)
if err != nil {
return ""
}
return ascii
}
func SetHostname(hostname string, fqdn string) error {
hostnameLock.Lock()
defer hostnameLock.Unlock()
hostname = ToValidHostname(strings.TrimSpace(hostname))
fqdn = ToValidHostname(strings.TrimSpace(fqdn))
if hostname == "" {
return fmt.Errorf("invalid hostname: %s", hostname)
}
if fqdn == "" {
fqdn = hostname
}
// update /etc/hostname
if err := os.WriteFile(hostnamePath, []byte(hostname), 0644); err != nil {
return fmt.Errorf("failed to write %s: %w", hostnamePath, err)
}
// update /etc/hosts
if err := updateEtcHosts(hostname, fqdn); err != nil {
return fmt.Errorf("failed to update /etc/hosts: %w", err)
}
// run hostname
if err := exec.Command("hostname", "-F", hostnamePath).Run(); err != nil {
return fmt.Errorf("failed to run hostname: %w", err)
}
return nil
}
func (s *NetworkInterfaceState) setHostnameIfNotSame() error {
hostname := s.GetHostname()
currentHostname, _ := os.Hostname()
fqdn := fmt.Sprintf("%s.%s", hostname, s.GetDomain())
if currentHostname == hostname && s.currentFqdn == fqdn && s.currentHostname == hostname {
return nil
}
scopedLogger := s.l.With().Str("hostname", hostname).Str("fqdn", fqdn).Logger()
err := SetHostname(hostname, fqdn)
if err != nil {
scopedLogger.Error().Err(err).Msg("failed to set hostname")
return err
}
s.currentHostname = hostname
s.currentFqdn = fqdn
scopedLogger.Info().Msg("hostname set")
return nil
}

403
internal/network/netif.go Normal file
View File

@ -0,0 +1,403 @@
package network
import (
"fmt"
"net"
"sync"
"github.com/jetkvm/kvm/internal/confparser"
"github.com/jetkvm/kvm/internal/logging"
"github.com/jetkvm/kvm/internal/udhcpc"
"github.com/rs/zerolog"
"github.com/vishvananda/netlink"
)
type NetworkInterfaceState struct {
interfaceName string
interfaceUp bool
ipv4Addr *net.IP
ipv4Addresses []string
ipv6Addr *net.IP
ipv6Addresses []IPv6Address
ipv6LinkLocal *net.IP
ntpAddresses []*net.IP
macAddr *net.HardwareAddr
l *zerolog.Logger
stateLock sync.Mutex
config *NetworkConfig
dhcpClient *udhcpc.DHCPClient
defaultHostname string
currentHostname string
currentFqdn string
onStateChange func(state *NetworkInterfaceState)
onInitialCheck func(state *NetworkInterfaceState)
cbConfigChange func(config *NetworkConfig)
checked bool
}
type NetworkInterfaceOptions struct {
InterfaceName string
DhcpPidFile string
Logger *zerolog.Logger
DefaultHostname string
OnStateChange func(state *NetworkInterfaceState)
OnInitialCheck func(state *NetworkInterfaceState)
OnDhcpLeaseChange func(lease *udhcpc.Lease, state *NetworkInterfaceState)
OnConfigChange func(config *NetworkConfig)
NetworkConfig *NetworkConfig
}
func NewNetworkInterfaceState(opts *NetworkInterfaceOptions) (*NetworkInterfaceState, error) {
if opts.NetworkConfig == nil {
return nil, fmt.Errorf("NetworkConfig can not be nil")
}
if opts.DefaultHostname == "" {
opts.DefaultHostname = "jetkvm"
}
err := confparser.SetDefaultsAndValidate(opts.NetworkConfig)
if err != nil {
return nil, err
}
l := opts.Logger
s := &NetworkInterfaceState{
interfaceName: opts.InterfaceName,
defaultHostname: opts.DefaultHostname,
stateLock: sync.Mutex{},
l: l,
onStateChange: opts.OnStateChange,
onInitialCheck: opts.OnInitialCheck,
cbConfigChange: opts.OnConfigChange,
config: opts.NetworkConfig,
ntpAddresses: make([]*net.IP, 0),
}
// create the dhcp client
dhcpClient := udhcpc.NewDHCPClient(&udhcpc.DHCPClientOptions{
InterfaceName: opts.InterfaceName,
PidFile: opts.DhcpPidFile,
Logger: l,
OnLeaseChange: func(lease *udhcpc.Lease) {
_, err := s.update()
if err != nil {
opts.Logger.Error().Err(err).Msg("failed to update network state")
return
}
_ = s.updateNtpServersFromLease(lease)
_ = s.setHostnameIfNotSame()
opts.OnDhcpLeaseChange(lease, s)
},
})
s.dhcpClient = dhcpClient
return s, nil
}
func (s *NetworkInterfaceState) IsUp() bool {
return s.interfaceUp
}
func (s *NetworkInterfaceState) HasIPAssigned() bool {
return s.ipv4Addr != nil || s.ipv6Addr != nil
}
func (s *NetworkInterfaceState) IsOnline() bool {
return s.IsUp() && s.HasIPAssigned()
}
func (s *NetworkInterfaceState) IPv4() *net.IP {
return s.ipv4Addr
}
func (s *NetworkInterfaceState) IPv4String() string {
if s.ipv4Addr == nil {
return "..."
}
return s.ipv4Addr.String()
}
func (s *NetworkInterfaceState) IPv6() *net.IP {
return s.ipv6Addr
}
func (s *NetworkInterfaceState) IPv6String() string {
if s.ipv6Addr == nil {
return "..."
}
return s.ipv6Addr.String()
}
func (s *NetworkInterfaceState) NtpAddresses() []*net.IP {
return s.ntpAddresses
}
func (s *NetworkInterfaceState) NtpAddressesString() []string {
ntpServers := []string{}
if s != nil {
s.l.Debug().Any("s", s).Msg("getting NTP address strings")
if len(s.ntpAddresses) > 0 {
for _, server := range s.ntpAddresses {
s.l.Debug().IPAddr("server", *server).Msg("converting NTP address")
ntpServers = append(ntpServers, server.String())
}
}
}
return ntpServers
}
func (s *NetworkInterfaceState) MAC() *net.HardwareAddr {
return s.macAddr
}
func (s *NetworkInterfaceState) MACString() string {
if s.macAddr == nil {
return ""
}
return s.macAddr.String()
}
func (s *NetworkInterfaceState) update() (DhcpTargetState, error) {
s.stateLock.Lock()
defer s.stateLock.Unlock()
dhcpTargetState := DhcpTargetStateDoNothing
iface, err := netlink.LinkByName(s.interfaceName)
if err != nil {
s.l.Error().Err(err).Msg("failed to get interface")
return dhcpTargetState, err
}
// detect if the interface status changed
var changed bool
attrs := iface.Attrs()
state := attrs.OperState
newInterfaceUp := state == netlink.OperUp
// check if the interface is coming up
interfaceGoingUp := !s.interfaceUp && newInterfaceUp
interfaceGoingDown := s.interfaceUp && !newInterfaceUp
if s.interfaceUp != newInterfaceUp {
s.interfaceUp = newInterfaceUp
changed = true
}
if changed {
if interfaceGoingUp {
s.l.Info().Msg("interface state transitioned to up")
dhcpTargetState = DhcpTargetStateRenew
} else if interfaceGoingDown {
s.l.Info().Msg("interface state transitioned to down")
}
}
// set the mac address
s.macAddr = &attrs.HardwareAddr
// get the ip addresses
addrs, err := netlinkAddrs(iface)
if err != nil {
return dhcpTargetState, logging.ErrorfL(s.l, "failed to get ip addresses", err)
}
var (
ipv4Addresses = make([]net.IP, 0)
ipv4AddressesString = make([]string, 0)
ipv6Addresses = make([]IPv6Address, 0)
// ipv6AddressesString = make([]string, 0)
ipv6LinkLocal *net.IP
)
for _, addr := range addrs {
if addr.IP.To4() != nil {
scopedLogger := s.l.With().Str("ipv4", addr.IP.String()).Logger()
if interfaceGoingDown {
// remove all IPv4 addresses from the interface.
scopedLogger.Info().Msg("state transitioned to down, removing IPv4 address")
err := netlink.AddrDel(iface, &addr)
if err != nil {
scopedLogger.Warn().Err(err).Msg("failed to delete address")
}
// notify the DHCP client to release the lease
dhcpTargetState = DhcpTargetStateRelease
continue
}
ipv4Addresses = append(ipv4Addresses, addr.IP)
ipv4AddressesString = append(ipv4AddressesString, addr.IPNet.String())
} else if addr.IP.To16() != nil {
if s.config.IPv6Mode.String == "disabled" {
continue
}
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
}
if interfaceGoingDown {
scopedLogger.Info().Msg("state transitioned to down, removing IPv6 address")
err := netlink.AddrDel(iface, &addr)
if err != nil {
scopedLogger.Warn().Err(err).Msg("failed to delete address")
}
continue
}
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())
}
}
if len(ipv4Addresses) > 0 {
// compare the addresses to see if there's a change
if s.ipv4Addr == nil || s.ipv4Addr.String() != ipv4Addresses[0].String() {
scopedLogger := s.l.With().Str("ipv4", ipv4Addresses[0].String()).Logger()
if s.ipv4Addr != nil {
scopedLogger.Info().
Str("old_ipv4", s.ipv4Addr.String()).
Msg("IPv4 address changed")
} else {
scopedLogger.Info().Msg("IPv4 address found")
}
s.ipv4Addr = &ipv4Addresses[0]
changed = true
}
}
s.ipv4Addresses = ipv4AddressesString
if s.config.IPv6Mode.String != "disabled" {
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].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()).
Msg("IPv6 address changed")
} else {
scopedLogger.Info().Msg("IPv6 address found")
}
s.ipv6Addr = &ipv6Addresses[0].Address
changed = true
}
}
}
// if it's the initial check, we'll set changed to false
initialCheck := !s.checked
if initialCheck {
s.checked = true
changed = false
if dhcpTargetState == DhcpTargetStateRenew {
// it's the initial check, we'll start the DHCP client
// dhcpTargetState = DhcpTargetStateStart
// TODO: manage DHCP client start/stop
dhcpTargetState = DhcpTargetStateDoNothing
}
}
if initialCheck {
s.handleInitialCheck()
} else if changed {
s.handleStateChange()
}
return dhcpTargetState, nil
}
func (s *NetworkInterfaceState) updateNtpServersFromLease(lease *udhcpc.Lease) error {
if lease != nil && len(lease.NTPServers) > 0 {
s.l.Info().Msg("lease found, updating DHCP NTP addresses")
s.ntpAddresses = make([]*net.IP, 0, len(lease.NTPServers))
for _, ntpServer := range lease.NTPServers {
if ntpServer != nil {
s.l.Info().IPAddr("ntp_server", ntpServer).Msg("NTP server found in lease")
s.ntpAddresses = append(s.ntpAddresses, &ntpServer)
}
}
} else {
s.l.Info().Msg("no NTP servers found in lease")
s.ntpAddresses = make([]*net.IP, 0, len(s.config.TimeSyncNTPServers))
}
return nil
}
func (s *NetworkInterfaceState) handleInitialCheck() {
// if s.IsUp() {}
s.onInitialCheck(s)
}
func (s *NetworkInterfaceState) handleStateChange() {
// if s.IsUp() {} else {}
s.onStateChange(s)
}
func (s *NetworkInterfaceState) CheckAndUpdateDhcp() error {
dhcpTargetState, err := s.update()
if err != nil {
return logging.ErrorfL(s.l, "failed to update network state", err)
}
switch dhcpTargetState {
case DhcpTargetStateRenew:
s.l.Info().Msg("renewing DHCP lease")
_ = s.dhcpClient.Renew()
case DhcpTargetStateRelease:
s.l.Info().Msg("releasing DHCP lease")
_ = s.dhcpClient.Release()
case DhcpTargetStateStart:
s.l.Warn().Msg("dhcpTargetStateStart not implemented")
case DhcpTargetStateStop:
s.l.Warn().Msg("dhcpTargetStateStop not implemented")
}
return nil
}
func (s *NetworkInterfaceState) onConfigChange(config *NetworkConfig) {
_ = s.setHostnameIfNotSame()
s.cbConfigChange(config)
}

View File

@ -0,0 +1,58 @@
//go:build linux
package network
import (
"time"
"github.com/vishvananda/netlink"
"github.com/vishvananda/netlink/nl"
)
func (s *NetworkInterfaceState) HandleLinkUpdate(update netlink.LinkUpdate) {
if update.Link.Attrs().Name == s.interfaceName {
s.l.Info().Interface("update", update).Msg("interface link update received")
_ = s.CheckAndUpdateDhcp()
}
}
func (s *NetworkInterfaceState) Run() error {
updates := make(chan netlink.LinkUpdate)
done := make(chan struct{})
if err := netlink.LinkSubscribe(updates, done); err != nil {
s.l.Warn().Err(err).Msg("failed to subscribe to link updates")
return err
}
_ = s.setHostnameIfNotSame()
// run the dhcp client
go s.dhcpClient.Run() // nolint:errcheck
if err := s.CheckAndUpdateDhcp(); err != nil {
return err
}
go func() {
ticker := time.NewTicker(1 * time.Second)
defer ticker.Stop()
for {
select {
case update := <-updates:
s.HandleLinkUpdate(update)
case <-ticker.C:
_ = s.CheckAndUpdateDhcp()
case <-done:
return
}
}
}()
return nil
}
func netlinkAddrs(iface netlink.Link) ([]netlink.Addr, error) {
return netlink.AddrList(iface, nl.FAMILY_ALL)
}

View File

@ -0,0 +1,21 @@
//go:build !linux
package network
import (
"fmt"
"github.com/vishvananda/netlink"
)
func (s *NetworkInterfaceState) HandleLinkUpdate() error {
return fmt.Errorf("not implemented")
}
func (s *NetworkInterfaceState) Run() error {
return fmt.Errorf("not implemented")
}
func netlinkAddrs(iface netlink.Link) ([]netlink.Addr, error) {
return nil, fmt.Errorf("not implemented")
}

126
internal/network/rpc.go Normal file
View File

@ -0,0 +1,126 @@
package network
import (
"fmt"
"time"
"github.com/jetkvm/kvm/internal/confparser"
"github.com/jetkvm/kvm/internal/udhcpc"
)
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"`
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 {
NetworkConfig
}
func (s *NetworkInterfaceState) MacAddress() string {
if s.macAddr == nil {
return ""
}
return s.macAddr.String()
}
func (s *NetworkInterfaceState) IPv4Address() string {
if s.ipv4Addr == nil {
return ""
}
return s.ipv4Addr.String()
}
func (s *NetworkInterfaceState) IPv6Address() string {
if s.ipv6Addr == nil {
return ""
}
return s.ipv6Addr.String()
}
func (s *NetworkInterfaceState) IPv6LinkLocalAddress() string {
if s.ipv6LinkLocal == nil {
return ""
}
return s.ipv6LinkLocal.String()
}
func (s *NetworkInterfaceState) RpcGetNetworkState() RpcNetworkState {
ipv6Addresses := make([]RpcIPv6Address, 0)
if s.ipv6Addresses != nil && s.config.IPv6Mode.String != "disabled" {
for _, addr := range s.ipv6Addresses {
ipv6Addresses = append(ipv6Addresses, RpcIPv6Address{
Address: addr.Prefix.String(),
ValidLifetime: addr.ValidLifetime,
PreferredLifetime: addr.PreferredLifetime,
Scope: addr.Scope,
})
}
}
return RpcNetworkState{
InterfaceName: s.interfaceName,
MacAddress: s.MacAddress(),
IPv4: s.IPv4Address(),
IPv6: s.IPv6Address(),
IPv6LinkLocal: s.IPv6LinkLocalAddress(),
IPv4Addresses: s.ipv4Addresses,
IPv6Addresses: ipv6Addresses,
DHCPLease: s.dhcpClient.GetLease(),
}
}
func (s *NetworkInterfaceState) RpcGetNetworkSettings() RpcNetworkSettings {
if s.config == nil {
return RpcNetworkSettings{}
}
return RpcNetworkSettings{
NetworkConfig: *s.config,
}
}
func (s *NetworkInterfaceState) RpcSetNetworkSettings(settings RpcNetworkSettings) error {
currentSettings := s.config
err := confparser.SetDefaultsAndValidate(&settings.NetworkConfig)
if err != nil {
return err
}
if IsSame(currentSettings, settings.NetworkConfig) {
// no changes, do nothing
return nil
}
s.config = &settings.NetworkConfig
s.onConfigChange(s.config)
return nil
}
func (s *NetworkInterfaceState) RpcRenewDHCPLease() error {
if s.dhcpClient == nil {
return fmt.Errorf("dhcp client not initialized")
}
return s.dhcpClient.Renew()
}

View File

@ -1,62 +0,0 @@
package types
import (
"net"
"time"
"golang.org/x/sys/unix"
)
// InterfaceState represents the current state of a network interface
type InterfaceState struct {
InterfaceName string `json:"interface_name"`
Hostname string `json:"hostname"`
MACAddress string `json:"mac_address"`
Up bool `json:"up"`
Online bool `json:"online"`
IPv4Ready bool `json:"ipv4_ready"`
IPv6Ready bool `json:"ipv6_ready"`
IPv4Address string `json:"ipv4_address,omitempty"`
IPv6Address string `json:"ipv6_address,omitempty"`
IPv6LinkLocal string `json:"ipv6_link_local,omitempty"`
IPv6Gateway string `json:"ipv6_gateway,omitempty"`
IPv4Addresses []string `json:"ipv4_addresses,omitempty"`
IPv6Addresses []IPv6Address `json:"ipv6_addresses,omitempty"`
NTPServers []net.IP `json:"ntp_servers,omitempty"`
DHCPLease4 *DHCPLease `json:"dhcp_lease,omitempty"`
DHCPLease6 *DHCPLease `json:"dhcp_lease6,omitempty"`
LastUpdated time.Time `json:"last_updated"`
}
// RpcInterfaceState is the RPC representation of an interface state
type RpcInterfaceState struct {
InterfaceState
IPv6Addresses []RpcIPv6Address `json:"ipv6_addresses"`
}
// ToRpcInterfaceState converts an InterfaceState to a RpcInterfaceState
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,
Flags: addr.Flags,
FlagSecondary: addr.Flags&unix.IFA_F_SECONDARY != 0,
FlagPermanent: addr.Flags&unix.IFA_F_PERMANENT != 0,
FlagTemporary: addr.Flags&unix.IFA_F_TEMPORARY != 0,
FlagStablePrivacy: addr.Flags&unix.IFA_F_STABLE_PRIVACY != 0,
FlagDeprecated: addr.Flags&unix.IFA_F_DEPRECATED != 0,
FlagOptimistic: addr.Flags&unix.IFA_F_OPTIMISTIC != 0,
FlagDADFailed: addr.Flags&unix.IFA_F_DADFAILED != 0,
FlagTentative: addr.Flags&unix.IFA_F_TENTATIVE != 0,
}
}
return &RpcInterfaceState{
InterfaceState: *s,
IPv6Addresses: addrs,
}
}

View File

@ -1,85 +0,0 @@
package types
import (
"net"
"slices"
"time"
"github.com/vishvananda/netlink"
)
// IPAddress represents a network interface address
type IPAddress struct {
Family int
Address net.IPNet
Gateway net.IP
MTU int
Secondary bool
Permanent bool
}
func (a *IPAddress) String() string {
return a.Address.String()
}
func (a *IPAddress) Compare(n netlink.Addr) bool {
if !a.Address.IP.Equal(n.IP) {
return false
}
if slices.Compare(a.Address.Mask, n.Mask) != 0 {
return false
}
return true
}
func (a *IPAddress) NetlinkAddr() netlink.Addr {
return netlink.Addr{
IPNet: &a.Address,
}
}
func (a *IPAddress) DefaultRoute(linkIndex int) netlink.Route {
return netlink.Route{
Dst: nil,
Gw: a.Gateway,
LinkIndex: linkIndex,
}
}
// ParsedIPConfig represents the parsed IP configuration
type ParsedIPConfig struct {
Addresses []IPAddress
Nameservers []net.IP
SearchList []string
Domain string
MTU int
Interface string
}
// IPv6Address represents an IPv6 address with lifetime information
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"`
Flags int `json:"flags"`
Scope int `json:"scope"`
}
// RpcIPv6Address is the RPC representation of an IPv6 address
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"`
Flags int `json:"flags"`
FlagSecondary bool `json:"flag_secondary"`
FlagPermanent bool `json:"flag_permanent"`
FlagTemporary bool `json:"flag_temporary"`
FlagStablePrivacy bool `json:"flag_stable_privacy"`
FlagDeprecated bool `json:"flag_deprecated"`
FlagOptimistic bool `json:"flag_optimistic"`
FlagDADFailed bool `json:"flag_dad_failed"`
FlagTentative bool `json:"flag_tentative"`
}

View File

@ -1,22 +0,0 @@
package types
import "net"
// InterfaceResolvConf represents the DNS configuration for a network interface
type InterfaceResolvConf struct {
NameServers []net.IP `json:"nameservers"`
SearchList []string `json:"search_list"`
Domain string `json:"domain,omitempty"` // TODO: remove this once we have a better way to handle the domain
Source string `json:"source,omitempty"`
}
// InterfaceResolvConfMap ..
type InterfaceResolvConfMap map[string]InterfaceResolvConf
// ResolvConf represents the DNS configuration for the system
type ResolvConf struct {
ConfigIPv4 InterfaceResolvConfMap `json:"config_ipv4"`
ConfigIPv6 InterfaceResolvConfMap `json:"config_ipv6"`
Domain string `json:"domain"`
HostName string `json:"host_name"`
}

26
internal/network/utils.go Normal file
View File

@ -0,0 +1,26 @@
package network
import (
"encoding/json"
"time"
)
func lifetimeToTime(lifetime int) *time.Time {
if lifetime == 0 {
return nil
}
t := time.Now().Add(time.Duration(lifetime) * time.Second)
return &t
}
func IsSame(a, b any) bool {
aJSON, err := json.Marshal(a)
if err != nil {
return false
}
bJSON, err := json.Marshal(b)
if err != nil {
return false
}
return string(aJSON) == string(bJSON)
}

View File

@ -1,149 +0,0 @@
//go:build synctrace
package sync
import (
"fmt"
"reflect"
"runtime"
"sync"
"time"
"github.com/jetkvm/kvm/internal/logging"
"github.com/rs/zerolog"
)
var defaultLogger = logging.GetSubsystemLogger("synctrace")
func logTrace(msg string) {
if defaultLogger.GetLevel() > zerolog.TraceLevel {
return
}
logTrack(3).Trace().Msg(msg)
}
func logTrack(callerSkip int) *zerolog.Logger {
l := *defaultLogger
if l.GetLevel() > zerolog.TraceLevel {
return &l
}
pc, file, no, ok := runtime.Caller(callerSkip)
if ok {
l = l.With().
Str("file", file).
Int("line", no).
Logger()
details := runtime.FuncForPC(pc)
if details != nil {
l = l.With().
Str("func", details.Name()).
Logger()
}
}
return &l
}
func logLockTrack(i string) *zerolog.Logger {
l := logTrack(4).
With().
Str("index", i).
Logger()
return &l
}
var (
indexMu sync.Mutex
lockCount map[string]int = make(map[string]int)
unlockCount map[string]int = make(map[string]int)
lastLock map[string]time.Time = make(map[string]time.Time)
)
type trackable interface {
sync.Locker
}
func getIndex(t trackable) string {
ptr := reflect.ValueOf(t).Pointer()
return fmt.Sprintf("%x", ptr)
}
func increaseLockCount(i string) {
indexMu.Lock()
defer indexMu.Unlock()
if _, ok := lockCount[i]; !ok {
lockCount[i] = 0
}
lockCount[i]++
if _, ok := lastLock[i]; !ok {
lastLock[i] = time.Now()
}
}
func increaseUnlockCount(i string) {
indexMu.Lock()
defer indexMu.Unlock()
if _, ok := unlockCount[i]; !ok {
unlockCount[i] = 0
}
unlockCount[i]++
}
func logLock(t trackable) {
i := getIndex(t)
increaseLockCount(i)
logLockTrack(i).Trace().Msg("locking mutex")
}
func logUnlock(t trackable) {
i := getIndex(t)
increaseUnlockCount(i)
logLockTrack(i).Trace().Msg("unlocking mutex")
}
func logTryLock(t trackable) {
i := getIndex(t)
logLockTrack(i).Trace().Msg("trying to lock mutex")
}
func logTryLockResult(t trackable, l bool) {
if !l {
return
}
i := getIndex(t)
increaseLockCount(i)
logLockTrack(i).Trace().Msg("locked mutex")
}
func logRLock(t trackable) {
i := getIndex(t)
increaseLockCount(i)
logLockTrack(i).Trace().Msg("locking mutex for reading")
}
func logRUnlock(t trackable) {
i := getIndex(t)
increaseUnlockCount(i)
logLockTrack(i).Trace().Msg("unlocking mutex for reading")
}
func logTryRLock(t trackable) {
i := getIndex(t)
logLockTrack(i).Trace().Msg("trying to lock mutex for reading")
}
func logTryRLockResult(t trackable, l bool) {
if !l {
return
}
i := getIndex(t)
increaseLockCount(i)
logLockTrack(i).Trace().Msg("locked mutex for reading")
}

View File

@ -1,69 +0,0 @@
//go:build synctrace
package sync
import (
gosync "sync"
)
// Mutex is a wrapper around the sync.Mutex
type Mutex struct {
mu gosync.Mutex
}
// Lock locks the mutex
func (m *Mutex) Lock() {
logLock(m)
m.mu.Lock()
}
// Unlock unlocks the mutex
func (m *Mutex) Unlock() {
logUnlock(m)
m.mu.Unlock()
}
// TryLock tries to lock the mutex
func (m *Mutex) TryLock() bool {
logTryLock(m)
l := m.mu.TryLock()
logTryLockResult(m, l)
return l
}
// RWMutex is a wrapper around the sync.RWMutex
type RWMutex struct {
mu gosync.RWMutex
}
// Lock locks the mutex
func (m *RWMutex) Lock() {
logLock(m)
m.mu.Lock()
}
// Unlock unlocks the mutex
func (m *RWMutex) Unlock() {
logUnlock(m)
m.mu.Unlock()
}
// RLock locks the mutex for reading
func (m *RWMutex) RLock() {
logRLock(m)
m.mu.RLock()
}
// RUnlock unlocks the mutex for reading
func (m *RWMutex) RUnlock() {
logRUnlock(m)
m.mu.RUnlock()
}
// TryRLock tries to lock the mutex for reading
func (m *RWMutex) TryRLock() bool {
logTryRLock(m)
l := m.mu.TryRLock()
logTryRLockResult(m, l)
return l
}

View File

@ -1,18 +0,0 @@
//go:build synctrace
package sync
import (
gosync "sync"
)
// Once is a wrapper around the sync.Once
type Once struct {
mu gosync.Once
}
// Do calls the function f if and only if Do has not been called before for this instance of Once.
func (o *Once) Do(f func()) {
logTrace("Doing once")
o.mu.Do(f)
}

View File

@ -1,92 +0,0 @@
//go:build !synctrace
package sync
import (
gosync "sync"
)
// Mutex is a wrapper around the sync.Mutex
type Mutex struct {
mu gosync.Mutex
}
// Lock locks the mutex
func (m *Mutex) Lock() {
m.mu.Lock()
}
// Unlock unlocks the mutex
func (m *Mutex) Unlock() {
m.mu.Unlock()
}
// TryLock tries to lock the mutex
func (m *Mutex) TryLock() bool {
return m.mu.TryLock()
}
// RWMutex is a wrapper around the sync.RWMutex
type RWMutex struct {
mu gosync.RWMutex
}
// Lock locks the mutex
func (m *RWMutex) Lock() {
m.mu.Lock()
}
// Unlock unlocks the mutex
func (m *RWMutex) Unlock() {
m.mu.Unlock()
}
// RLock locks the mutex for reading
func (m *RWMutex) RLock() {
m.mu.RLock()
}
// RUnlock unlocks the mutex for reading
func (m *RWMutex) RUnlock() {
m.mu.RUnlock()
}
// TryRLock tries to lock the mutex for reading
func (m *RWMutex) TryRLock() bool {
return m.mu.TryRLock()
}
// TryLock tries to lock the mutex
func (m *RWMutex) TryLock() bool {
return m.mu.TryLock()
}
// WaitGroup is a wrapper around the sync.WaitGroup
type WaitGroup struct {
wg gosync.WaitGroup
}
// Add adds a function to the wait group
func (w *WaitGroup) Add(delta int) {
w.wg.Add(delta)
}
// Done decrements the wait group counter
func (w *WaitGroup) Done() {
w.wg.Done()
}
// Wait waits for the wait group to finish
func (w *WaitGroup) Wait() {
w.wg.Wait()
}
// Once is a wrapper around the sync.Once
type Once struct {
mu gosync.Once
}
// Do calls the function f if and only if Do has not been called before for this instance of Once.
func (o *Once) Do(f func()) {
o.mu.Do(f)
}

View File

@ -1,30 +0,0 @@
//go:build synctrace
package sync
import (
gosync "sync"
)
// WaitGroup is a wrapper around the sync.WaitGroup
type WaitGroup struct {
wg gosync.WaitGroup
}
// Add adds a function to the wait group
func (w *WaitGroup) Add(delta int) {
logTrace("Adding to wait group")
w.wg.Add(delta)
}
// Done decrements the wait group counter
func (w *WaitGroup) Done() {
logTrace("Done with wait group")
w.wg.Done()
}
// Wait waits for the wait group to finish
func (w *WaitGroup) Wait() {
logTrace("Waiting for wait group")
w.wg.Wait()
}

View File

@ -3,14 +3,13 @@ package timesync
import ( import (
"context" "context"
"math/rand/v2" "math/rand/v2"
"net"
"strconv" "strconv"
"time" "time"
"github.com/beevik/ntp" "github.com/beevik/ntp"
) )
var DefaultNTPServerIPs = []string{ var defaultNTPServerIPs = []string{
// These servers are known by static IP and as such don't need DNS lookups // These servers are known by static IP and as such don't need DNS lookups
// These are from Google and Cloudflare since if they're down, the internet // These are from Google and Cloudflare since if they're down, the internet
// is broken anyway // is broken anyway
@ -28,7 +27,7 @@ var DefaultNTPServerIPs = []string{
"2001:4860:4806:c::", // time.google.com IPv6 "2001:4860:4806:c::", // time.google.com IPv6
} }
var DefaultNTPServerHostnames = []string{ var defaultNTPServerHostnames = []string{
// should use something from https://github.com/jauderho/public-ntp-servers // should use something from https://github.com/jauderho/public-ntp-servers
"time.apple.com", "time.apple.com",
"time.aws.com", "time.aws.com",
@ -38,48 +37,7 @@ var DefaultNTPServerHostnames = []string{
"pool.ntp.org", "pool.ntp.org",
} }
func (t *TimeSync) filterNTPServers(ntpServers []string) ([]string, error) {
if len(ntpServers) == 0 {
return nil, nil
}
hasIPv4, err := t.preCheckIPv4()
if err != nil {
t.l.Error().Err(err).Msg("failed to check IPv4")
return nil, err
}
hasIPv6, err := t.preCheckIPv6()
if err != nil {
t.l.Error().Err(err).Msg("failed to check IPv6")
return nil, err
}
filteredServers := []string{}
for _, server := range ntpServers {
ip := net.ParseIP(server)
t.l.Trace().Str("server", server).Interface("ip", ip).Msg("checking NTP server")
if ip == nil {
continue
}
if hasIPv4 && ip.To4() != nil {
filteredServers = append(filteredServers, server)
}
if hasIPv6 && ip.To16() != nil {
filteredServers = append(filteredServers, server)
}
}
return filteredServers, nil
}
func (t *TimeSync) queryNetworkTime(ntpServers []string) (now *time.Time, offset *time.Duration) { func (t *TimeSync) queryNetworkTime(ntpServers []string) (now *time.Time, offset *time.Duration) {
ntpServers, err := t.filterNTPServers(ntpServers)
if err != nil {
t.l.Error().Err(err).Msg("failed to filter NTP servers")
return nil, nil
}
chunkSize := int(t.networkConfig.TimeSyncParallel.ValueOr(4)) chunkSize := int(t.networkConfig.TimeSyncParallel.ValueOr(4))
t.l.Info().Strs("servers", ntpServers).Int("chunkSize", chunkSize).Msg("querying NTP servers") t.l.Info().Strs("servers", ntpServers).Int("chunkSize", chunkSize).Msg("querying NTP servers")

View File

@ -7,7 +7,7 @@ import (
"sync" "sync"
"time" "time"
"github.com/jetkvm/kvm/internal/network/types" "github.com/jetkvm/kvm/internal/network"
"github.com/rs/zerolog" "github.com/rs/zerolog"
) )
@ -24,13 +24,11 @@ var (
timeSyncRetryInterval = 0 * time.Second timeSyncRetryInterval = 0 * time.Second
) )
type PreCheckFunc func() (bool, error)
type TimeSync struct { type TimeSync struct {
syncLock *sync.Mutex syncLock *sync.Mutex
l *zerolog.Logger l *zerolog.Logger
networkConfig *types.NetworkConfig networkConfig *network.NetworkConfig
dhcpNtpAddresses []string dhcpNtpAddresses []string
rtcDevicePath string rtcDevicePath string
@ -38,19 +36,14 @@ type TimeSync struct {
rtcLock *sync.Mutex rtcLock *sync.Mutex
syncSuccess bool syncSuccess bool
timer *time.Timer
preCheckFunc PreCheckFunc preCheckFunc func() (bool, error)
preCheckIPv4 PreCheckFunc
preCheckIPv6 PreCheckFunc
} }
type TimeSyncOptions struct { type TimeSyncOptions struct {
PreCheckFunc PreCheckFunc PreCheckFunc func() (bool, error)
PreCheckIPv4 PreCheckFunc
PreCheckIPv6 PreCheckFunc
Logger *zerolog.Logger Logger *zerolog.Logger
NetworkConfig *types.NetworkConfig NetworkConfig *network.NetworkConfig
} }
type SyncMode struct { type SyncMode struct {
@ -76,10 +69,7 @@ func NewTimeSync(opts *TimeSyncOptions) *TimeSync {
rtcDevicePath: rtcDevice, rtcDevicePath: rtcDevice,
rtcLock: &sync.Mutex{}, rtcLock: &sync.Mutex{},
preCheckFunc: opts.PreCheckFunc, preCheckFunc: opts.PreCheckFunc,
preCheckIPv4: opts.PreCheckIPv4,
preCheckIPv6: opts.PreCheckIPv6,
networkConfig: opts.NetworkConfig, networkConfig: opts.NetworkConfig,
timer: time.NewTimer(timeSyncWaitNetUpInt),
} }
if t.rtcDevicePath != "" { if t.rtcDevicePath != "" {
@ -122,64 +112,49 @@ func (t *TimeSync) getSyncMode() SyncMode {
} }
} }
t.l.Debug(). t.l.Debug().Strs("Ordering", syncMode.Ordering).Bool("Ntp", syncMode.Ntp).Bool("Http", syncMode.Http).Bool("NtpUseFallback", syncMode.NtpUseFallback).Bool("HttpUseFallback", syncMode.HttpUseFallback).Msg("sync mode")
Strs("Ordering", syncMode.Ordering).
Bool("Ntp", syncMode.Ntp).
Bool("Http", syncMode.Http).
Bool("NtpUseFallback", syncMode.NtpUseFallback).
Bool("HttpUseFallback", syncMode.HttpUseFallback).
Msg("sync mode")
return syncMode return syncMode
} }
func (t *TimeSync) timeSyncLoop() { func (t *TimeSync) doTimeSync() {
metricTimeSyncStatus.Set(0) metricTimeSyncStatus.Set(0)
for {
// use a timer here instead of sleep
for range t.timer.C {
if ok, err := t.preCheckFunc(); !ok { if ok, err := t.preCheckFunc(); !ok {
if err != nil { if err != nil {
t.l.Error().Err(err).Msg("pre-check failed") t.l.Error().Err(err).Msg("pre-check failed")
} }
t.timer.Reset(timeSyncWaitNetChkInt) time.Sleep(timeSyncWaitNetChkInt)
continue continue
} }
t.l.Info().Msg("syncing system time") t.l.Info().Msg("syncing system time")
start := time.Now() start := time.Now()
err := t.sync() err := t.Sync()
if err != nil { if err != nil {
t.l.Error().Str("error", err.Error()).Msg("failed to sync system time") t.l.Error().Str("error", err.Error()).Msg("failed to sync system time")
// retry after a delay // retry after a delay
timeSyncRetryInterval += timeSyncRetryStep timeSyncRetryInterval += timeSyncRetryStep
t.timer.Reset(timeSyncRetryInterval) time.Sleep(timeSyncRetryInterval)
// reset the retry interval if it exceeds the max interval // reset the retry interval if it exceeds the max interval
if timeSyncRetryInterval > timeSyncRetryMaxInt { if timeSyncRetryInterval > timeSyncRetryMaxInt {
timeSyncRetryInterval = 0 timeSyncRetryInterval = 0
} }
continue continue
} }
isInitialSync := !t.syncSuccess
t.syncSuccess = true t.syncSuccess = true
t.l.Info().Str("now", time.Now().Format(time.RFC3339)). t.l.Info().Str("now", time.Now().Format(time.RFC3339)).
Str("time_taken", time.Since(start).String()). Str("time_taken", time.Since(start).String()).
Bool("is_initial_sync", isInitialSync).
Msg("time sync successful") Msg("time sync successful")
metricTimeSyncStatus.Set(1) metricTimeSyncStatus.Set(1)
t.timer.Reset(timeSyncInterval) // after the first sync is done time.Sleep(timeSyncInterval) // after the first sync is done
} }
} }
func (t *TimeSync) sync() error { func (t *TimeSync) Sync() error {
t.syncLock.Lock()
defer t.syncLock.Unlock()
var ( var (
now *time.Time now *time.Time
offset *time.Duration offset *time.Duration
@ -213,10 +188,10 @@ Orders:
case "ntp": case "ntp":
if syncMode.Ntp && syncMode.NtpUseFallback { if syncMode.Ntp && syncMode.NtpUseFallback {
log.Info().Msg("using NTP fallback IPs") log.Info().Msg("using NTP fallback IPs")
now, offset = t.queryNetworkTime(DefaultNTPServerIPs) now, offset = t.queryNetworkTime(defaultNTPServerIPs)
if now == nil { if now == nil {
log.Info().Msg("using NTP fallback hostnames") log.Info().Msg("using NTP fallback hostnames")
now, offset = t.queryNetworkTime(DefaultNTPServerHostnames) now, offset = t.queryNetworkTime(defaultNTPServerHostnames)
} }
if now != nil { if now != nil {
break Orders break Orders
@ -264,25 +239,12 @@ Orders:
return nil return nil
} }
// Sync triggers a manual time sync
func (t *TimeSync) Sync() error {
if !t.syncLock.TryLock() {
t.l.Warn().Msg("sync already in progress, skipping")
return nil
}
t.syncLock.Unlock()
return t.sync()
}
// IsSyncSuccess returns true if the system time is synchronized
func (t *TimeSync) IsSyncSuccess() bool { func (t *TimeSync) IsSyncSuccess() bool {
return t.syncSuccess return t.syncSuccess
} }
// Start starts the time sync
func (t *TimeSync) Start() { func (t *TimeSync) Start() {
go t.timeSyncLoop() go t.doTimeSync()
} }
func (t *TimeSync) setSystemTime(now time.Time) error { func (t *TimeSync) setSystemTime(now time.Time) error {

View File

@ -1,26 +1,18 @@
package types package udhcpc
import ( import (
"bufio"
"encoding/json"
"fmt"
"net" "net"
"os"
"reflect"
"strconv"
"strings"
"time" "time"
) )
// DHCPClient is the interface for a DHCP client. type Lease struct {
type DHCPClient interface {
Domain() string
Lease4() *DHCPLease
Lease6() *DHCPLease
Renew() error
Release() error
SetIPv4(enabled bool)
SetIPv6(enabled bool)
SetOnLeaseChange(callback func(lease *DHCPLease))
Start() error
Stop() error
}
// DHCPLease is a network configuration obtained by DHCP.
type DHCPLease struct {
// from https://udhcp.busybox.net/README.udhcpc // from https://udhcp.busybox.net/README.udhcpc
IPAddress net.IP `env:"ip" json:"ip"` // The obtained IP IPAddress net.IP `env:"ip" json:"ip"` // The obtained IP
Netmask net.IP `env:"subnet" json:"netmask"` // The assigned subnet mask Netmask net.IP `env:"subnet" json:"netmask"` // The assigned subnet mask
@ -29,7 +21,6 @@ type DHCPLease struct {
MTU int `env:"mtu" json:"mtu,omitempty"` // The MTU 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 HostName string `env:"hostname" json:"hostname,omitempty"` // The assigned hostname
Domain string `env:"domain" json:"domain,omitempty"` // The domain name of the network 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 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 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 BootPFile string `env:"boot_file" json:"bootp_file,omitempty"` // The bootp boot file option
@ -47,46 +38,149 @@ type DHCPLease struct {
BootSize int `env:"bootsize" json:"bootsize,omitempty"` // The length in 512 octect blocks of the bootfile 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 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 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) 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 ServerID string `env:"serverid" json:"server_id,omitempty"` // The IP of the server
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 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 LeaseExpiry *time.Time `json:"lease_expiry,omitempty"` // The expiry time of the lease
isEmpty map[string]bool
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 (l *Lease) setIsEmpty(m map[string]bool) {
func (d *DHCPLease) IsIPv6() bool { l.isEmpty = m
return d.IPAddress.To4() == nil
} }
// IPMask returns the IP mask for the DHCP lease func (l *Lease) IsEmpty(key string) bool {
func (d *DHCPLease) IPMask() net.IPMask { return l.isEmpty[key]
if d.IsIPv6() { }
// TODO: not implemented
func (l *Lease) ToJSON() string {
json, err := json.Marshal(l)
if err != nil {
return ""
}
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)
for line := range strings.SplitSeq(str, "\n") {
line = strings.TrimSpace(line)
// skip empty lines and comments
if line == "" || strings.HasPrefix(line, "#") {
continue
}
parts := strings.SplitN(line, "=", 2)
if len(parts) != 2 {
continue
}
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
data[key] = value
}
// now iterate over the lease struct and set the values
leaseType := reflect.TypeOf(lease).Elem()
leaseValue := reflect.ValueOf(lease).Elem()
valuesParsed := make(map[string]bool)
for i := 0; i < leaseType.NumField(); i++ {
field := leaseValue.Field(i)
// get the env tag
key := leaseType.Field(i).Tag.Get("env")
if key == "" {
continue
}
valuesParsed[key] = false
// get the value from the data map
value, ok := data[key]
if !ok || value == "" {
continue
}
switch field.Interface().(type) {
case string:
field.SetString(value)
case int:
val, err := strconv.Atoi(value)
if err != nil {
continue
}
field.SetInt(int64(val))
case time.Duration:
val, err := time.ParseDuration(value + "s")
if err != nil {
continue
}
field.Set(reflect.ValueOf(val))
case net.IP:
ip := net.ParseIP(value)
if ip == nil {
continue
}
field.Set(reflect.ValueOf(ip))
case []net.IP:
val := make([]net.IP, 0)
for ipStr := range strings.FieldsSeq(value) {
ip := net.ParseIP(ipStr)
if ip == nil {
continue
}
val = append(val, ip)
}
field.Set(reflect.ValueOf(val))
default:
return fmt.Errorf("unsupported field `%s` type: %s", key, field.Type().String())
}
valuesParsed[key] = true
}
lease.setIsEmpty(valuesParsed)
return nil 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(),
}
} }

View File

@ -6,13 +6,9 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"reflect" "reflect"
"time" "time"
"github.com/jetkvm/kvm/internal/sync"
"github.com/fsnotify/fsnotify" "github.com/fsnotify/fsnotify"
"github.com/jetkvm/kvm/internal/network/types"
"github.com/rs/zerolog" "github.com/rs/zerolog"
) )
@ -22,22 +18,20 @@ const (
) )
type DHCPClient struct { type DHCPClient struct {
types.DHCPClient
InterfaceName string InterfaceName string
leaseFile string leaseFile string
pidFile string pidFile string
lease *Lease lease *Lease
logger *zerolog.Logger logger *zerolog.Logger
process *os.Process process *os.Process
runOnce sync.Once onLeaseChange func(lease *Lease)
onLeaseChange func(lease *types.DHCPLease)
} }
type DHCPClientOptions struct { type DHCPClientOptions struct {
InterfaceName string InterfaceName string
PidFile string PidFile string
Logger *zerolog.Logger Logger *zerolog.Logger
OnLeaseChange func(lease *types.DHCPLease) OnLeaseChange func(lease *Lease)
} }
var defaultLogger = zerolog.New(os.Stdout).Level(zerolog.InfoLevel) var defaultLogger = zerolog.New(os.Stdout).Level(zerolog.InfoLevel)
@ -73,8 +67,8 @@ func (c *DHCPClient) getWatchPaths() []string {
} }
// Run starts the DHCP client and watches the lease file for changes. // Run starts the DHCP client and watches the lease file for changes.
// this is a blocking call. // this isn't a blocking call, and the lease file is reloaded when a change is detected.
func (c *DHCPClient) run() error { func (c *DHCPClient) Run() error {
err := c.loadLeaseFile() err := c.loadLeaseFile()
if err != nil && !errors.Is(err, os.ErrNotExist) { if err != nil && !errors.Is(err, os.ErrNotExist) {
return err return err
@ -131,7 +125,7 @@ func (c *DHCPClient) run() error {
// c.logger.Error().Msg("udhcpc process not found") // c.logger.Error().Msg("udhcpc process not found")
// } // }
// block the goroutine // block the goroutine until the lease file is updated
<-make(chan struct{}) <-make(chan struct{})
return nil return nil
@ -188,7 +182,7 @@ func (c *DHCPClient) loadLeaseFile() error {
Msg("current dhcp lease expiry time calculated") Msg("current dhcp lease expiry time calculated")
} }
c.onLeaseChange(lease.ToDHCPLease()) c.onLeaseChange(lease)
c.logger.Info(). c.logger.Info().
Str("ip", lease.IPAddress.String()). Str("ip", lease.IPAddress.String()).
@ -202,47 +196,3 @@ func (c *DHCPClient) loadLeaseFile() error {
func (c *DHCPClient) GetLease() *Lease { func (c *DHCPClient) GetLease() *Lease {
return c.lease return c.lease
} }
func (c *DHCPClient) Domain() string {
return c.lease.Domain
}
func (c *DHCPClient) Lease4() *types.DHCPLease {
if c.lease == nil {
return nil
}
return c.lease.ToDHCPLease()
}
func (c *DHCPClient) Lease6() *types.DHCPLease {
// TODO: implement
return nil
}
func (c *DHCPClient) SetIPv4(enabled bool) {
// TODO: implement
}
func (c *DHCPClient) SetIPv6(enabled bool) {
// TODO: implement
}
func (c *DHCPClient) SetOnLeaseChange(callback func(lease *types.DHCPLease)) {
c.onLeaseChange = callback
}
func (c *DHCPClient) Start() error {
c.runOnce.Do(func() {
go func() {
err := c.run()
if err != nil {
c.logger.Error().Err(err).Msg("failed to run udhcpc")
}
}()
})
return nil
}
func (c *DHCPClient) Stop() error {
return c.KillProcess() // udhcpc already has KillProcess()
}

View File

@ -175,10 +175,6 @@ func rpcGetDeviceID() (string, error) {
func rpcReboot(force bool) error { func rpcReboot(force bool) error {
logger.Info().Msg("Got reboot request from JSONRPC, rebooting...") logger.Info().Msg("Got reboot request from JSONRPC, rebooting...")
writeJSONRPCEvent("willReboot", nil, currentSession)
// Wait for the JSONRPCEvent to be sent
time.Sleep(1 * time.Second)
nativeInstance.SwitchToScreenIfDifferent("rebooting_screen") nativeInstance.SwitchToScreenIfDifferent("rebooting_screen")
args := []string{} args := []string{}
@ -724,8 +720,7 @@ func rpcSetWakeOnLanDevices(params SetWakeOnLanDevicesParams) error {
} }
func rpcResetConfig() error { func rpcResetConfig() error {
defaultConfig := getDefaultConfig() config = defaultConfig
config = &defaultConfig
if err := SaveConfig(); err != nil { if err := SaveConfig(); err != nil {
return fmt.Errorf("failed to reset config: %w", err) return fmt.Errorf("failed to reset config: %w", err)
} }

View File

@ -33,7 +33,6 @@ func Main() {
go runWatchdog() go runWatchdog()
go confirmCurrentSystem() go confirmCurrentSystem()
initDisplay()
initNative(systemVersionLocal, appVersionLocal) initNative(systemVersionLocal, appVersionLocal)
http.DefaultClient.Timeout = 1 * time.Minute http.DefaultClient.Timeout = 1 * time.Minute
@ -75,6 +74,9 @@ func Main() {
} }
initJiggler() initJiggler()
// initialize display
initDisplay()
// start video sleep mode timer // start video sleep mode timer
startVideoSleepModeTicker() startVideoSleepModeTicker()

14
mdns.go
View File

@ -1,23 +1,19 @@
package kvm package kvm
import ( import (
"fmt"
"github.com/jetkvm/kvm/internal/mdns" "github.com/jetkvm/kvm/internal/mdns"
) )
var mDNS *mdns.MDNS var mDNS *mdns.MDNS
func initMdns() error { func initMdns() error {
options := getMdnsOptions()
if options == nil {
return fmt.Errorf("failed to get mDNS options")
}
m, err := mdns.NewMDNS(&mdns.MDNSOptions{ m, err := mdns.NewMDNS(&mdns.MDNSOptions{
Logger: logger, Logger: logger,
LocalNames: options.LocalNames, LocalNames: []string{
ListenOptions: options.ListenOptions, networkState.GetHostname(),
networkState.GetFQDN(),
},
ListenOptions: config.NetworkConfig.GetMDNSMode(),
}) })
if err != nil { if err != nil {
return err return err

View File

@ -43,8 +43,6 @@ func initNative(systemVersion *semver.Version, appVersion *semver.Version) {
_ = rpcReboot(true) _ = rpcReboot(true)
case "reboot": case "reboot":
_ = rpcReboot(true) _ = rpcReboot(true)
case "toggleDHCPClient":
_ = rpcToggleDHCPClient()
default: default:
nativeLogger.Warn().Str("event", event).Msg("unknown rpc event received") nativeLogger.Warn().Str("event", event).Msg("unknown rpc event received")
} }

View File

@ -1,14 +1,10 @@
package kvm package kvm
import ( import (
"context"
"fmt" "fmt"
"reflect"
"github.com/jetkvm/kvm/internal/confparser" "github.com/jetkvm/kvm/internal/network"
"github.com/jetkvm/kvm/internal/mdns" "github.com/jetkvm/kvm/internal/udhcpc"
"github.com/jetkvm/kvm/internal/network/types"
"github.com/jetkvm/kvm/pkg/nmlite"
) )
const ( const (
@ -16,297 +12,114 @@ const (
) )
var ( var (
networkManager *nmlite.NetworkManager networkState *network.NetworkInterfaceState
) )
type RpcNetworkSettings struct { func networkStateChanged(isOnline bool) {
types.NetworkConfig
}
func (s *RpcNetworkSettings) ToNetworkConfig() *types.NetworkConfig {
return &s.NetworkConfig
}
type PostRebootAction struct {
HealthCheck string `json:"healthCheck"`
RedirectUrl string `json:"redirectUrl"`
}
func toRpcNetworkSettings(config *types.NetworkConfig) *RpcNetworkSettings {
return &RpcNetworkSettings{
NetworkConfig: *config,
}
}
func getMdnsOptions() *mdns.MDNSOptions {
if networkManager == nil {
return nil
}
var ipv4, ipv6 bool
switch config.NetworkConfig.MDNSMode.String {
case "auto":
ipv4 = true
ipv6 = true
case "ipv4_only":
ipv4 = true
case "ipv6_only":
ipv6 = true
}
return &mdns.MDNSOptions{
LocalNames: []string{
networkManager.Hostname(),
networkManager.FQDN(),
},
ListenOptions: &mdns.MDNSListenOptions{
IPv4: ipv4,
IPv6: ipv6,
},
}
}
func restartMdns() {
if mDNS == nil {
return
}
options := getMdnsOptions()
if options == nil {
return
}
if err := mDNS.SetOptions(options); err != nil {
networkLogger.Error().Err(err).Msg("failed to restart mDNS")
}
}
func triggerTimeSyncOnNetworkStateChange() {
if timeSync == nil {
return
}
// set the NTP servers from the network manager
if networkManager != nil {
ntpServers := make([]string, len(networkManager.NTPServers()))
for i, server := range networkManager.NTPServers() {
ntpServers[i] = server.String()
}
networkLogger.Info().Strs("ntpServers", ntpServers).Msg("setting NTP servers from network manager")
timeSync.SetDhcpNtpAddresses(ntpServers)
}
// sync time
go func() {
if err := timeSync.Sync(); err != nil {
networkLogger.Error().Err(err).Msg("failed to sync time after network state change")
}
}()
}
func networkStateChanged(_ string, state types.InterfaceState) {
// do not block the main thread // do not block the main thread
go waitCtrlAndRequestDisplayUpdate(true, "network_state_changed") go waitCtrlAndRequestDisplayUpdate(true, "network_state_changed")
if currentSession != nil { if timeSync != nil {
writeJSONRPCEvent("networkState", state.ToRpcInterfaceState(), currentSession) if networkState != nil {
timeSync.SetDhcpNtpAddresses(networkState.NtpAddressesString())
} }
if state.Online { if err := timeSync.Sync(); err != nil {
networkLogger.Info().Msg("network state changed to online, triggering time sync") networkLogger.Error().Err(err).Msg("failed to sync time after network state change")
triggerTimeSyncOnNetworkStateChange() }
} }
// always restart mDNS when the network state changes // always restart mDNS when the network state changes
if mDNS != nil { if mDNS != nil {
restartMdns() _ = mDNS.SetListenOptions(config.NetworkConfig.GetMDNSMode())
} _ = mDNS.SetLocalNames([]string{
} networkState.GetHostname(),
networkState.GetFQDN(),
func validateNetworkConfig() { }, true)
err := confparser.SetDefaultsAndValidate(config.NetworkConfig)
if err == nil {
return
} }
networkLogger.Error().Err(err).Msg("failed to validate config, reverting to default config") // if the network is now online, trigger an NTP sync if still needed
if err := SaveBackupConfig(); err != nil { if isOnline && timeSync != nil && (isTimeSyncNeeded() || !timeSync.IsSyncSuccess()) {
networkLogger.Error().Err(err).Msg("failed to save backup config") if err := timeSync.Sync(); err != nil {
logger.Warn().Str("error", err.Error()).Msg("unable to sync time on network state change")
} }
// do not use a pointer to the default config
// it has been already changed during LoadConfig
config.NetworkConfig = &(types.NetworkConfig{})
if err := SaveConfig(); err != nil {
networkLogger.Error().Err(err).Msg("failed to save config")
} }
} }
func initNetwork() error { func initNetwork() error {
ensureConfigLoaded() ensureConfigLoaded()
// validate the config, if it's invalid, revert to the default config and save the backup state, err := network.NewNetworkInterfaceState(&network.NetworkInterfaceOptions{
validateNetworkConfig() DefaultHostname: GetDefaultHostname(),
InterfaceName: NetIfName,
NetworkConfig: config.NetworkConfig,
Logger: networkLogger,
OnStateChange: func(state *network.NetworkInterfaceState) {
networkStateChanged(state.IsOnline())
},
OnInitialCheck: func(state *network.NetworkInterfaceState) {
networkStateChanged(state.IsOnline())
},
OnDhcpLeaseChange: func(lease *udhcpc.Lease, state *network.NetworkInterfaceState) {
networkStateChanged(state.IsOnline())
nc := config.NetworkConfig if currentSession == nil {
return
nm := nmlite.NewNetworkManager(context.Background(), networkLogger)
networkLogger.Info().Interface("networkConfig", nc).Str("hostname", nc.Hostname.String).Str("domain", nc.Domain.String).Msg("initializing network manager")
_ = setHostname(nm, nc.Hostname.String, nc.Domain.String)
nm.SetOnInterfaceStateChange(networkStateChanged)
if err := nm.AddInterface(NetIfName, nc); err != nil {
return fmt.Errorf("failed to add interface: %w", err)
}
_ = nm.CleanUpLegacyDHCPClients()
networkManager = nm
return nil
}
func setHostname(nm *nmlite.NetworkManager, hostname, domain string) error {
if nm == nil {
return nil
} }
if hostname == "" { writeJSONRPCEvent("networkState", networkState.RpcGetNetworkState(), currentSession)
hostname = GetDefaultHostname() },
OnConfigChange: func(networkConfig *network.NetworkConfig) {
config.NetworkConfig = networkConfig
networkStateChanged(false)
if mDNS != nil {
_ = mDNS.SetListenOptions(networkConfig.GetMDNSMode())
_ = mDNS.SetLocalNames([]string{
networkState.GetHostname(),
networkState.GetFQDN(),
}, true)
} }
},
})
return nm.SetHostname(hostname, domain) if state == nil {
} if err == nil {
return fmt.Errorf("failed to create NetworkInterfaceState")
func shouldRebootForNetworkChange(oldConfig, newConfig *types.NetworkConfig) (rebootRequired bool, postRebootAction *PostRebootAction) {
oldDhcpClient := oldConfig.DHCPClient.String
l := networkLogger.With().
Interface("old", oldConfig).
Interface("new", newConfig).
Logger()
// DHCP client change always requires reboot
if newConfig.DHCPClient.String != oldDhcpClient {
rebootRequired = true
l.Info().Msg("DHCP client changed, reboot required")
return rebootRequired, postRebootAction
} }
oldIPv4Mode := oldConfig.IPv4Mode.String
newIPv4Mode := newConfig.IPv4Mode.String
// IPv4 mode change requires reboot
if newIPv4Mode != oldIPv4Mode {
rebootRequired = true
l.Info().Msg("IPv4 mode changed with udhcpc, reboot required")
if newIPv4Mode == "static" && oldIPv4Mode != "static" {
postRebootAction = &PostRebootAction{
HealthCheck: fmt.Sprintf("//%s/device/status", newConfig.IPv4Static.Address.String),
RedirectUrl: fmt.Sprintf("//%s", newConfig.IPv4Static.Address.String),
}
l.Info().Interface("postRebootAction", postRebootAction).Msg("IPv4 mode changed to static, reboot required")
}
return rebootRequired, postRebootAction
}
// IPv4 static config changes require reboot
if !reflect.DeepEqual(oldConfig.IPv4Static, newConfig.IPv4Static) {
rebootRequired = true
// Handle IP change for redirect (only if both are not nil and IP changed)
if newConfig.IPv4Static != nil && oldConfig.IPv4Static != nil &&
newConfig.IPv4Static.Address.String != oldConfig.IPv4Static.Address.String {
postRebootAction = &PostRebootAction{
HealthCheck: fmt.Sprintf("//%s/device/status", newConfig.IPv4Static.Address.String),
RedirectUrl: fmt.Sprintf("//%s", newConfig.IPv4Static.Address.String),
}
l.Info().Interface("postRebootAction", postRebootAction).Msg("IPv4 static config changed, reboot required")
}
return rebootRequired, postRebootAction
}
// IPv6 mode change requires reboot when using udhcpc
if newConfig.IPv6Mode.String != oldConfig.IPv6Mode.String && oldDhcpClient == "udhcpc" {
rebootRequired = true
l.Info().Msg("IPv6 mode changed with udhcpc, reboot required")
}
return rebootRequired, postRebootAction
}
func rpcGetNetworkState() *types.RpcInterfaceState {
state, _ := networkManager.GetInterfaceState(NetIfName)
return state.ToRpcInterfaceState()
}
func rpcGetNetworkSettings() *RpcNetworkSettings {
return toRpcNetworkSettings(config.NetworkConfig)
}
func rpcSetNetworkSettings(settings RpcNetworkSettings) (*RpcNetworkSettings, error) {
netConfig := settings.ToNetworkConfig()
l := networkLogger.With().
Str("interface", NetIfName).
Interface("newConfig", netConfig).
Logger()
l.Debug().Msg("setting new config")
// Check if reboot is needed
rebootRequired, postRebootAction := shouldRebootForNetworkChange(config.NetworkConfig, netConfig)
// If reboot required, send willReboot event before applying network config
if rebootRequired {
l.Info().Msg("Sending willReboot event before applying network config")
writeJSONRPCEvent("willReboot", postRebootAction, currentSession)
}
_ = setHostname(networkManager, netConfig.Hostname.String, netConfig.Domain.String)
s := networkManager.SetInterfaceConfig(NetIfName, netConfig)
if s != nil {
return nil, s
}
l.Debug().Msg("new config applied")
newConfig, err := networkManager.GetInterfaceConfig(NetIfName)
if err != nil {
return nil, err
}
config.NetworkConfig = newConfig
l.Debug().Msg("saving new config")
if err := SaveConfig(); err != nil {
return nil, err
}
if rebootRequired {
if err := rpcReboot(false); err != nil {
return nil, err
}
}
return toRpcNetworkSettings(newConfig), nil
}
func rpcRenewDHCPLease() error {
return networkManager.RenewDHCPLease(NetIfName)
}
func rpcToggleDHCPClient() error {
switch config.NetworkConfig.DHCPClient.String {
case "jetdhcpc":
config.NetworkConfig.DHCPClient.String = "udhcpc"
case "udhcpc":
config.NetworkConfig.DHCPClient.String = "jetdhcpc"
}
if err := SaveConfig(); err != nil {
return err return err
} }
return rpcReboot(true) if err := state.Run(); err != nil {
return err
}
networkState = state
return nil
}
func rpcGetNetworkState() network.RpcNetworkState {
return networkState.RpcGetNetworkState()
}
func rpcGetNetworkSettings() network.RpcNetworkSettings {
return networkState.RpcGetNetworkSettings()
}
func rpcSetNetworkSettings(settings network.RpcNetworkSettings) (*network.RpcNetworkSettings, error) {
s := networkState.RpcSetNetworkSettings(settings)
if s != nil {
return nil, s
}
if err := SaveConfig(); err != nil {
return nil, err
}
return &network.RpcNetworkSettings{NetworkConfig: *config.NetworkConfig}, nil
}
func rpcRenewDHCPLease() error {
return networkState.RpcRenewDHCPLease()
} }

9
ota.go
View File

@ -488,15 +488,6 @@ func TryUpdate(ctx context.Context, deviceId string, includePreRelease bool) err
if rebootNeeded { if rebootNeeded {
scopedLogger.Info().Msg("System Rebooting in 10s") scopedLogger.Info().Msg("System Rebooting in 10s")
// TODO: Future enhancement - send postRebootAction to redirect to release notes
// Example:
// postRebootAction := &PostRebootAction{
// HealthCheck: "[..]/device/status",
// RedirectUrl: "[..]/settings/general/update?version=X.Y.Z",
// }
// writeJSONRPCEvent("willReboot", postRebootAction, currentSession)
time.Sleep(10 * time.Second) time.Sleep(10 * time.Second)
cmd := exec.Command("reboot") cmd := exec.Command("reboot")
err := cmd.Start() err := cmd.Start()

View File

@ -1,219 +0,0 @@
// Package nmlite provides DHCP client functionality for the network manager.
package nmlite
import (
"context"
"fmt"
"github.com/jetkvm/kvm/internal/network/types"
"github.com/jetkvm/kvm/pkg/nmlite/jetdhcpc"
"github.com/jetkvm/kvm/pkg/nmlite/udhcpc"
"github.com/rs/zerolog"
)
// DHCPClient wraps the dhclient package for use in the network manager
type DHCPClient struct {
ctx context.Context
ifaceName string
logger *zerolog.Logger
client types.DHCPClient
clientType string
// Configuration
ipv4Enabled bool
ipv6Enabled bool
// Callbacks
onLeaseChange func(lease *types.DHCPLease)
}
// NewDHCPClient creates a new DHCP client
func NewDHCPClient(ctx context.Context, ifaceName string, logger *zerolog.Logger, clientType string) (*DHCPClient, error) {
if ifaceName == "" {
return nil, fmt.Errorf("interface name cannot be empty")
}
if logger == nil {
return nil, fmt.Errorf("logger cannot be nil")
}
return &DHCPClient{
ctx: ctx,
ifaceName: ifaceName,
logger: logger,
clientType: clientType,
}, nil
}
// SetIPv4 enables or disables IPv4 DHCP
func (dc *DHCPClient) SetIPv4(enabled bool) {
dc.ipv4Enabled = enabled
if dc.client != nil {
dc.client.SetIPv4(enabled)
}
}
// SetIPv6 enables or disables IPv6 DHCP
func (dc *DHCPClient) SetIPv6(enabled bool) {
dc.ipv6Enabled = enabled
if dc.client != nil {
dc.client.SetIPv6(enabled)
}
}
// SetOnLeaseChange sets the callback for lease changes
func (dc *DHCPClient) SetOnLeaseChange(callback func(lease *types.DHCPLease)) {
dc.onLeaseChange = callback
}
func (dc *DHCPClient) initClient() (types.DHCPClient, error) {
switch dc.clientType {
case "jetdhcpc":
return dc.initJetDHCPC()
case "udhcpc":
return dc.initUDHCPC()
default:
return nil, fmt.Errorf("invalid client type: %s", dc.clientType)
}
}
func (dc *DHCPClient) initJetDHCPC() (types.DHCPClient, error) {
return jetdhcpc.NewClient(dc.ctx, []string{dc.ifaceName}, &jetdhcpc.Config{
IPv4: dc.ipv4Enabled,
IPv6: dc.ipv6Enabled,
V4ClientIdentifier: true,
OnLease4Change: func(lease *types.DHCPLease) {
dc.handleLeaseChange(lease, false)
},
OnLease6Change: func(lease *types.DHCPLease) {
dc.handleLeaseChange(lease, true)
},
UpdateResolvConf: func(nameservers []string) error {
// This will be handled by the resolv.conf manager
dc.logger.Debug().
Interface("nameservers", nameservers).
Msg("DHCP client requested resolv.conf update")
return nil
},
}, dc.logger)
}
func (dc *DHCPClient) initUDHCPC() (types.DHCPClient, error) {
c := udhcpc.NewDHCPClient(&udhcpc.DHCPClientOptions{
InterfaceName: dc.ifaceName,
PidFile: "",
Logger: dc.logger,
OnLeaseChange: func(lease *types.DHCPLease) {
dc.handleLeaseChange(lease, false)
},
})
return c, nil
}
// Start starts the DHCP client
func (dc *DHCPClient) Start() error {
if dc.client != nil {
dc.logger.Warn().Msg("DHCP client already started")
return nil
}
dc.logger.Info().Msg("starting DHCP client")
// Create the underlying DHCP client
client, err := dc.initClient()
if err != nil {
return fmt.Errorf("failed to create DHCP client: %w", err)
}
dc.client = client
// Start the client
if err := dc.client.Start(); err != nil {
dc.client = nil
return fmt.Errorf("failed to start DHCP client: %w", err)
}
dc.logger.Info().Msg("DHCP client started")
return nil
}
func (dc *DHCPClient) Domain() string {
if dc.client == nil {
return ""
}
return dc.client.Domain()
}
func (dc *DHCPClient) Lease4() *types.DHCPLease {
if dc.client == nil {
return nil
}
return dc.client.Lease4()
}
func (dc *DHCPClient) Lease6() *types.DHCPLease {
if dc.client == nil {
return nil
}
return dc.client.Lease6()
}
// Stop stops the DHCP client
func (dc *DHCPClient) Stop() error {
if dc.client == nil {
return nil
}
dc.logger.Info().Msg("stopping DHCP client")
dc.client = nil
dc.logger.Info().Msg("DHCP client stopped")
return nil
}
// Renew renews the DHCP lease
func (dc *DHCPClient) Renew() error {
if dc.client == nil {
return fmt.Errorf("DHCP client not started")
}
dc.logger.Info().Msg("renewing DHCP lease")
if err := dc.client.Renew(); err != nil {
return fmt.Errorf("failed to renew DHCP lease: %w", err)
}
return nil
}
// Release releases the DHCP lease
func (dc *DHCPClient) Release() error {
if dc.client == nil {
return fmt.Errorf("DHCP client not started")
}
dc.logger.Info().Msg("releasing DHCP lease")
if err := dc.client.Release(); err != nil {
return fmt.Errorf("failed to release DHCP lease: %w", err)
}
return nil
}
// handleLeaseChange handles lease changes from the underlying DHCP client
func (dc *DHCPClient) handleLeaseChange(lease *types.DHCPLease, isIPv6 bool) {
if lease == nil {
return
}
dc.logger.Info().
Bool("ipv6", isIPv6).
Str("ip", lease.IPAddress.String()).
Msg("DHCP lease changed")
// copy the lease to avoid race conditions
leaseCopy := *lease
// Notify callback
if dc.onLeaseChange != nil {
dc.onLeaseChange(&leaseCopy)
}
}

View File

@ -1,261 +0,0 @@
package nmlite
import (
"fmt"
"io"
"os"
"os/exec"
"strings"
"golang.org/x/net/idna"
)
const (
hostnamePath = "/etc/hostname"
hostsPath = "/etc/hosts"
)
// SetHostname sets the system hostname and updates /etc/hosts
func (hm *ResolvConfManager) SetHostname(hostname, domain string) error {
hostname = ToValidHostname(strings.TrimSpace(hostname))
domain = ToValidHostname(strings.TrimSpace(domain))
if hostname == "" {
return fmt.Errorf("invalid hostname: %s", hostname)
}
hm.hostname = hostname
hm.domain = domain
return hm.reconcileHostname()
}
func (hm *ResolvConfManager) Domain() string {
hm.mu.Lock()
defer hm.mu.Unlock()
return hm.getDomain()
}
func (hm *ResolvConfManager) Hostname() string {
hm.mu.Lock()
defer hm.mu.Unlock()
return hm.getHostname()
}
func (hm *ResolvConfManager) FQDN() string {
hm.mu.Lock()
defer hm.mu.Unlock()
return hm.getFQDN()
}
func (hm *ResolvConfManager) getFQDN() string {
hostname := hm.getHostname()
domain := hm.getDomain()
if domain == "" {
return hostname
}
return fmt.Sprintf("%s.%s", hostname, domain)
}
func (hm *ResolvConfManager) getHostname() string {
if hm.hostname != "" {
return hm.hostname
}
return "jetkvm"
}
func (hm *ResolvConfManager) getDomain() string {
if hm.domain != "" {
return hm.domain
}
for _, iface := range hm.conf.ConfigIPv4 {
if iface.Domain != "" {
return iface.Domain
}
}
for _, iface := range hm.conf.ConfigIPv6 {
if iface.Domain != "" {
return iface.Domain
}
}
return "local"
}
func (hm *ResolvConfManager) reconcileHostname() error {
hm.mu.Lock()
domain := hm.getDomain()
hostname := hm.hostname
if hostname == "" {
hostname = "jetkvm"
}
hm.mu.Unlock()
fqdn := hostname
if fqdn != "" {
fqdn = fmt.Sprintf("%s.%s", hostname, domain)
}
hm.logger.Info().
Str("hostname", hostname).
Str("fqdn", fqdn).
Msg("setting hostname")
// Update /etc/hostname
if err := hm.updateEtcHostname(hostname); err != nil {
return fmt.Errorf("failed to update /etc/hostname: %w", err)
}
// Update /etc/hosts
if err := hm.updateEtcHosts(hostname, fqdn); err != nil {
return fmt.Errorf("failed to update /etc/hosts: %w", err)
}
// Set the hostname using hostname command
if err := hm.setSystemHostname(hostname); err != nil {
return fmt.Errorf("failed to set system hostname: %w", err)
}
hm.logger.Info().
Str("hostname", hostname).
Str("fqdn", fqdn).
Msg("hostname set successfully")
return nil
}
// GetCurrentHostname returns the current system hostname
func (hm *ResolvConfManager) GetCurrentHostname() (string, error) {
return os.Hostname()
}
// GetCurrentFQDN returns the current FQDN
func (hm *ResolvConfManager) GetCurrentFQDN() (string, error) {
hostname, err := hm.GetCurrentHostname()
if err != nil {
return "", err
}
// Try to get the FQDN from /etc/hosts
return hm.getFQDNFromHosts(hostname)
}
// updateEtcHostname updates the /etc/hostname file
func (hm *ResolvConfManager) updateEtcHostname(hostname string) error {
if err := os.WriteFile(hostnamePath, []byte(hostname), 0644); err != nil {
return fmt.Errorf("failed to write %s: %w", hostnamePath, err)
}
hm.logger.Debug().Str("file", hostnamePath).Str("hostname", hostname).Msg("updated /etc/hostname")
return nil
}
// updateEtcHosts updates the /etc/hosts file
func (hm *ResolvConfManager) updateEtcHosts(hostname, fqdn string) error {
// Open /etc/hosts for reading and writing
hostsFile, err := os.OpenFile(hostsPath, os.O_RDWR|os.O_SYNC, os.ModeExclusive)
if err != nil {
return fmt.Errorf("failed to open %s: %w", hostsPath, err)
}
defer hostsFile.Close()
// Read all lines
if _, err := hostsFile.Seek(0, io.SeekStart); err != nil {
return fmt.Errorf("failed to seek %s: %w", hostsPath, err)
}
lines, err := io.ReadAll(hostsFile)
if err != nil {
return fmt.Errorf("failed to read %s: %w", hostsPath, err)
}
// Process lines
newLines := []string{}
hostLine := fmt.Sprintf("127.0.1.1\t%s %s", hostname, fqdn)
hostLineExists := false
for _, line := range strings.Split(string(lines), "\n") {
if strings.HasPrefix(line, "127.0.1.1") {
hostLineExists = true
line = hostLine
}
newLines = append(newLines, line)
}
// Add host line if it doesn't exist
if !hostLineExists {
newLines = append(newLines, hostLine)
}
// Write back to file
if err := hostsFile.Truncate(0); err != nil {
return fmt.Errorf("failed to truncate %s: %w", hostsPath, err)
}
if _, err := hostsFile.Seek(0, io.SeekStart); err != nil {
return fmt.Errorf("failed to seek %s: %w", hostsPath, err)
}
if _, err := hostsFile.Write([]byte(strings.Join(newLines, "\n"))); err != nil {
return fmt.Errorf("failed to write %s: %w", hostsPath, err)
}
hm.logger.Debug().
Str("file", hostsPath).
Str("hostname", hostname).
Str("fqdn", fqdn).
Msg("updated /etc/hosts")
return nil
}
// setSystemHostname sets the system hostname using the hostname command
func (hm *ResolvConfManager) setSystemHostname(hostname string) error {
cmd := exec.Command("hostname", "-F", hostnamePath)
if err := cmd.Run(); err != nil {
return fmt.Errorf("failed to run hostname command: %w", err)
}
hm.logger.Debug().Str("hostname", hostname).Msg("set system hostname")
return nil
}
// getFQDNFromHosts tries to get the FQDN from /etc/hosts
func (hm *ResolvConfManager) getFQDNFromHosts(hostname string) (string, error) {
content, err := os.ReadFile(hostsPath)
if err != nil {
return hostname, nil // Return hostname as fallback
}
lines := strings.Split(string(content), "\n")
for _, line := range lines {
if strings.HasPrefix(line, "127.0.1.1") {
parts := strings.Fields(line)
if len(parts) >= 2 {
// The second part should be the FQDN
return parts[1], nil
}
}
}
return hostname, nil // Return hostname as fallback
}
// ToValidHostname converts a hostname to a valid format
func ToValidHostname(hostname string) string {
ascii, err := idna.Lookup.ToASCII(hostname)
if err != nil {
return ""
}
return ascii
}
// ValidateHostname validates a hostname
func ValidateHostname(hostname string) error {
_, err := idna.Lookup.ToASCII(hostname)
return err
}

View File

@ -1,853 +0,0 @@
package nmlite
import (
"context"
"fmt"
"net"
"net/netip"
"time"
"github.com/jetkvm/kvm/internal/sync"
"github.com/jetkvm/kvm/internal/confparser"
"github.com/jetkvm/kvm/internal/logging"
"github.com/jetkvm/kvm/internal/network/types"
"github.com/jetkvm/kvm/pkg/nmlite/link"
"github.com/mdlayher/ndp"
"github.com/rs/zerolog"
"github.com/vishvananda/netlink"
)
type ResolvConfChangeCallback func(family int, resolvConf *types.InterfaceResolvConf) error
// InterfaceManager manages a single network interface
type InterfaceManager struct {
ctx context.Context
ifaceName string
config *types.NetworkConfig
logger *zerolog.Logger
state *types.InterfaceState
linkState *link.Link
stateMu sync.RWMutex
// Network components
staticConfig *StaticConfigManager
dhcpClient *DHCPClient
// Callbacks
onStateChange func(state types.InterfaceState)
onConfigChange func(config *types.NetworkConfig)
onDHCPLeaseChange func(lease *types.DHCPLease)
onResolvConfChange ResolvConfChangeCallback
// Control
stopCh chan struct{}
wg sync.WaitGroup
}
// NewInterfaceManager creates a new interface manager
func NewInterfaceManager(ctx context.Context, ifaceName string, config *types.NetworkConfig, logger *zerolog.Logger) (*InterfaceManager, error) {
if config == nil {
return nil, fmt.Errorf("config cannot be nil")
}
if logger == nil {
logger = logging.GetSubsystemLogger("interface")
}
scopedLogger := logger.With().Str("interface", ifaceName).Logger()
// Validate and set defaults
if err := confparser.SetDefaultsAndValidate(config); err != nil {
return nil, fmt.Errorf("invalid config: %w", err)
}
im := &InterfaceManager{
ctx: ctx,
ifaceName: ifaceName,
config: config,
logger: &scopedLogger,
state: &types.InterfaceState{
InterfaceName: ifaceName,
// LastUpdated: time.Now(),
},
stopCh: make(chan struct{}),
}
// Initialize components
var err error
im.staticConfig, err = NewStaticConfigManager(ifaceName, &scopedLogger)
if err != nil {
return nil, fmt.Errorf("failed to create static config manager: %w", err)
}
// create the dhcp client
im.dhcpClient, err = NewDHCPClient(ctx, ifaceName, &scopedLogger, config.DHCPClient.String)
if err != nil {
return nil, fmt.Errorf("failed to create DHCP client: %w", err)
}
// Set up DHCP client callbacks
im.dhcpClient.SetOnLeaseChange(func(lease *types.DHCPLease) {
if im.config.IPv4Mode.String != "dhcp" {
im.logger.Warn().Str("mode", im.config.IPv4Mode.String).Msg("ignoring DHCP lease, current mode is not DHCP")
return
}
if err := im.applyDHCPLease(lease); err != nil {
im.logger.Error().Err(err).Msg("failed to apply DHCP lease")
}
im.updateStateFromDHCPLease(lease)
if im.onDHCPLeaseChange != nil {
im.onDHCPLeaseChange(lease)
}
})
return im, nil
}
// Start starts managing the interface
func (im *InterfaceManager) Start() error {
im.stateMu.Lock()
defer im.stateMu.Unlock()
im.logger.Info().Msg("starting interface manager")
// Start monitoring interface state
im.wg.Add(1)
go im.monitorInterfaceState()
nl := getNetlinkManager()
// Set the link state
linkState, err := nl.GetLinkByName(im.ifaceName)
if err != nil {
return fmt.Errorf("failed to get interface: %w", err)
}
im.linkState = linkState
// Bring the interface up
_, linkUpErr := nl.EnsureInterfaceUpWithTimeout(
im.ctx,
im.linkState,
30*time.Second,
)
// Set callback after the interface is up
nl.AddStateChangeCallback(im.ifaceName, link.StateChangeCallback{
Async: true,
Func: func(link *link.Link) {
im.handleLinkStateChange(link)
},
})
if linkUpErr != nil {
im.logger.Error().Err(linkUpErr).Msg("failed to bring interface up, continuing anyway")
} else {
// Apply initial configuration
if err := im.applyConfiguration(); err != nil {
im.logger.Error().Err(err).Msg("failed to apply initial configuration")
return err
}
}
im.logger.Info().Msg("interface manager started")
return nil
}
// Stop stops managing the interface
func (im *InterfaceManager) Stop() error {
im.logger.Info().Msg("stopping interface manager")
close(im.stopCh)
im.wg.Wait()
// Stop DHCP client
if im.dhcpClient != nil {
if err := im.dhcpClient.Stop(); err != nil {
return fmt.Errorf("failed to stop DHCP client: %w", err)
}
}
im.logger.Info().Msg("interface manager stopped")
return nil
}
func (im *InterfaceManager) link() (*link.Link, error) {
nl := getNetlinkManager()
if nl == nil {
return nil, fmt.Errorf("netlink manager not initialized")
}
return nl.GetLinkByName(im.ifaceName)
}
// IsUp returns true if the interface is up
func (im *InterfaceManager) IsUp() bool {
im.stateMu.RLock()
defer im.stateMu.RUnlock()
if im.state == nil {
return false
}
return im.state.Up
}
// IsOnline returns true if the interface is online
func (im *InterfaceManager) IsOnline() bool {
im.stateMu.RLock()
defer im.stateMu.RUnlock()
if im.state == nil {
return false
}
return im.state.Online
}
// IPv4Ready returns true if the interface has an IPv4 address
func (im *InterfaceManager) IPv4Ready() bool {
im.stateMu.RLock()
defer im.stateMu.RUnlock()
if im.state == nil {
return false
}
return im.state.IPv4Ready
}
// IPv6Ready returns true if the interface has an IPv6 address
func (im *InterfaceManager) IPv6Ready() bool {
im.stateMu.RLock()
defer im.stateMu.RUnlock()
if im.state == nil {
return false
}
return im.state.IPv6Ready
}
// GetIPv4Addresses returns the IPv4 addresses of the interface
func (im *InterfaceManager) GetIPv4Addresses() []string {
im.stateMu.RLock()
defer im.stateMu.RUnlock()
if im.state == nil {
return []string{}
}
return im.state.IPv4Addresses
}
// GetIPv4Address returns the IPv4 address of the interface
func (im *InterfaceManager) GetIPv4Address() string {
im.stateMu.RLock()
defer im.stateMu.RUnlock()
if im.state == nil {
return ""
}
return im.state.IPv4Address
}
// GetIPv6Address returns the IPv6 address of the interface
func (im *InterfaceManager) GetIPv6Address() string {
im.stateMu.RLock()
defer im.stateMu.RUnlock()
if im.state == nil {
return ""
}
return im.state.IPv6Address
}
// GetIPv6Addresses returns the IPv6 addresses of the interface
func (im *InterfaceManager) GetIPv6Addresses() []string {
im.stateMu.RLock()
defer im.stateMu.RUnlock()
addresses := []string{}
if im.state == nil {
return addresses
}
for _, addr := range im.state.IPv6Addresses {
addresses = append(addresses, addr.Address.String())
}
return addresses
}
// GetMACAddress returns the MAC address of the interface
func (im *InterfaceManager) GetMACAddress() string {
im.stateMu.RLock()
defer im.stateMu.RUnlock()
if im.state == nil {
return ""
}
return im.state.MACAddress
}
// GetState returns the current interface state
func (im *InterfaceManager) GetState() *types.InterfaceState {
im.stateMu.RLock()
defer im.stateMu.RUnlock()
// Return a copy to avoid race conditions
im.logger.Debug().Interface("state", im.state).Msg("getting interface state")
state := *im.state
return &state
}
// NTPServers returns the NTP servers of the interface
func (im *InterfaceManager) NTPServers() []net.IP {
im.stateMu.RLock()
defer im.stateMu.RUnlock()
if im.state == nil {
return []net.IP{}
}
return im.state.NTPServers
}
func (im *InterfaceManager) Domain() string {
im.stateMu.RLock()
defer im.stateMu.RUnlock()
if im.state == nil {
return ""
}
if im.state.DHCPLease4 != nil {
return im.state.DHCPLease4.Domain
}
if im.state.DHCPLease6 != nil {
return im.state.DHCPLease6.Domain
}
return ""
}
// GetConfig returns the current interface configuration
func (im *InterfaceManager) GetConfig() *types.NetworkConfig {
// Return a copy to avoid race conditions
config := *im.config
return &config
}
// ApplyConfiguration applies the current configuration to the interface
func (im *InterfaceManager) ApplyConfiguration() error {
return im.applyConfiguration()
}
// SetConfig updates the interface configuration
func (im *InterfaceManager) SetConfig(config *types.NetworkConfig) error {
if config == nil {
return fmt.Errorf("config cannot be nil")
}
// Validate and set defaults
if err := confparser.SetDefaultsAndValidate(config); err != nil {
return fmt.Errorf("invalid config: %w", err)
}
im.config = config
// Apply the new configuration
if err := im.applyConfiguration(); err != nil {
im.logger.Error().Err(err).Msg("failed to apply new configuration")
return err
}
// Notify callback
if im.onConfigChange != nil {
im.onConfigChange(config)
}
im.logger.Info().Msg("configuration updated")
return nil
}
// RenewDHCPLease renews the DHCP lease
func (im *InterfaceManager) RenewDHCPLease() error {
if im.dhcpClient == nil {
return fmt.Errorf("DHCP client not available")
}
return im.dhcpClient.Renew()
}
// SetOnStateChange sets the callback for state changes
func (im *InterfaceManager) SetOnStateChange(callback func(state types.InterfaceState)) {
im.onStateChange = callback
}
// SetOnConfigChange sets the callback for configuration changes
func (im *InterfaceManager) SetOnConfigChange(callback func(config *types.NetworkConfig)) {
im.onConfigChange = callback
}
// SetOnDHCPLeaseChange sets the callback for DHCP lease changes
func (im *InterfaceManager) SetOnDHCPLeaseChange(callback func(lease *types.DHCPLease)) {
im.onDHCPLeaseChange = callback
}
// SetOnResolvConfChange sets the callback for resolv.conf changes
func (im *InterfaceManager) SetOnResolvConfChange(callback ResolvConfChangeCallback) {
im.onResolvConfChange = callback
}
// applyConfiguration applies the current configuration to the interface
func (im *InterfaceManager) applyConfiguration() error {
im.logger.Info().Msg("applying configuration")
// Apply IPv4 configuration
if err := im.applyIPv4Config(); err != nil {
return fmt.Errorf("failed to apply IPv4 config: %w", err)
}
// Apply IPv6 configuration
if err := im.applyIPv6Config(); err != nil {
return fmt.Errorf("failed to apply IPv6 config: %w", err)
}
return nil
}
// applyIPv4Config applies IPv4 configuration
func (im *InterfaceManager) applyIPv4Config() error {
mode := im.config.IPv4Mode.String
im.logger.Info().Str("mode", mode).Msg("applying IPv4 configuration")
switch mode {
case "static":
return im.applyIPv4Static()
case "dhcp":
return im.applyIPv4DHCP()
case "disabled":
return im.disableIPv4()
default:
return fmt.Errorf("invalid IPv4 mode: %s", mode)
}
}
// applyIPv6Config applies IPv6 configuration
func (im *InterfaceManager) applyIPv6Config() error {
mode := im.config.IPv6Mode.String
im.logger.Info().Str("mode", mode).Msg("applying IPv6 configuration")
switch mode {
case "static":
return im.applyIPv6Static()
case "dhcpv6":
return im.applyIPv6DHCP()
case "slaac":
return im.applyIPv6SLAAC()
case "slaac_and_dhcpv6":
return im.applyIPv6SLAACAndDHCP()
case "link_local":
return im.applyIPv6LinkLocal()
case "disabled":
return im.disableIPv6()
default:
return fmt.Errorf("invalid IPv6 mode: %s", mode)
}
}
// applyIPv4Static applies static IPv4 configuration
func (im *InterfaceManager) applyIPv4Static() error {
if im.config.IPv4Static == nil {
return fmt.Errorf("IPv4 static configuration is nil")
}
im.logger.Info().Msg("stopping DHCP")
// Disable DHCP
if im.dhcpClient != nil {
im.dhcpClient.SetIPv4(false)
}
im.logger.Info().Interface("config", im.config.IPv4Static).Msg("applying IPv4 static configuration")
config, err := im.staticConfig.ToIPv4Static(im.config.IPv4Static)
if err != nil {
return fmt.Errorf("failed to convert IPv4 static configuration: %w", err)
}
im.logger.Info().Interface("config", config).Msg("converted IPv4 static configuration")
if err := im.onResolvConfChange(link.AfInet, &types.InterfaceResolvConf{
NameServers: config.Nameservers,
Source: "static",
}); err != nil {
im.logger.Warn().Err(err).Msg("failed to update resolv.conf")
}
return im.ReconcileLinkAddrs(config.Addresses, link.AfInet)
}
// applyIPv4DHCP applies DHCP IPv4 configuration
func (im *InterfaceManager) applyIPv4DHCP() error {
if im.dhcpClient == nil {
return fmt.Errorf("DHCP client not available")
}
// Enable DHCP
im.dhcpClient.SetIPv4(true)
return im.dhcpClient.Start()
}
// disableIPv4 disables IPv4
func (im *InterfaceManager) disableIPv4() error {
// Disable DHCP
if im.dhcpClient != nil {
im.dhcpClient.SetIPv4(false)
}
// Remove all IPv4 addresses
return im.staticConfig.DisableIPv4()
}
// applyIPv6Static applies static IPv6 configuration
func (im *InterfaceManager) applyIPv6Static() error {
if im.config.IPv6Static == nil {
return fmt.Errorf("IPv6 static configuration is nil")
}
im.logger.Info().Msg("stopping DHCPv6")
// Disable DHCPv6
if im.dhcpClient != nil {
im.dhcpClient.SetIPv6(false)
}
// Apply static configuration
config, err := im.staticConfig.ToIPv6Static(im.config.IPv6Static)
if err != nil {
return fmt.Errorf("failed to convert IPv6 static configuration: %w", err)
}
im.logger.Info().Interface("config", config).Msg("converted IPv6 static configuration")
if err := im.onResolvConfChange(link.AfInet6, &types.InterfaceResolvConf{
NameServers: config.Nameservers,
Source: "static",
}); err != nil {
im.logger.Warn().Err(err).Msg("failed to update resolv.conf")
}
return im.ReconcileLinkAddrs(config.Addresses, link.AfInet6)
}
// applyIPv6DHCP applies DHCPv6 configuration
func (im *InterfaceManager) applyIPv6DHCP() error {
if im.dhcpClient == nil {
return fmt.Errorf("DHCP client not available")
}
// Enable DHCPv6
im.dhcpClient.SetIPv6(true)
return im.dhcpClient.Start()
}
// applyIPv6SLAAC applies SLAAC configuration
func (im *InterfaceManager) applyIPv6SLAAC() error {
// Disable DHCPv6
if im.dhcpClient != nil {
im.dhcpClient.SetIPv6(false)
}
// Remove static IPv6 configuration
l, err := im.link()
if err != nil {
return fmt.Errorf("failed to get interface: %w", err)
}
netlinkMgr := getNetlinkManager()
// Ensure interface is up
if err := netlinkMgr.EnsureInterfaceUp(l); err != nil {
return fmt.Errorf("failed to bring interface up: %w", err)
}
if err := netlinkMgr.RemoveNonLinkLocalIPv6Addresses(l); err != nil {
return fmt.Errorf("failed to remove non-link-local IPv6 addresses: %w", err)
}
if err := im.SendRouterSolicitation(); err != nil {
im.logger.Error().Err(err).Msg("failed to send router solicitation, continuing anyway")
}
// Enable SLAAC
return im.staticConfig.EnableIPv6SLAAC()
}
// applyIPv6SLAACAndDHCP applies SLAAC + DHCPv6 configuration
func (im *InterfaceManager) applyIPv6SLAACAndDHCP() error {
// Enable both SLAAC and DHCPv6
if im.dhcpClient != nil {
im.dhcpClient.SetIPv6(true)
if err := im.dhcpClient.Start(); err != nil {
return fmt.Errorf("failed to start DHCP client: %w", err)
}
}
return im.staticConfig.EnableIPv6SLAAC()
}
// applyIPv6LinkLocal applies link-local only IPv6 configuration
func (im *InterfaceManager) applyIPv6LinkLocal() error {
// Disable DHCPv6
if im.dhcpClient != nil {
im.dhcpClient.SetIPv6(false)
}
// Enable link-local only
return im.staticConfig.EnableIPv6LinkLocal()
}
// disableIPv6 disables IPv6
func (im *InterfaceManager) disableIPv6() error {
// Disable DHCPv6
if im.dhcpClient != nil {
im.dhcpClient.SetIPv6(false)
}
// Disable IPv6
return im.staticConfig.DisableIPv6()
}
func (im *InterfaceManager) handleLinkStateChange(link *link.Link) {
{
im.stateMu.Lock()
defer im.stateMu.Unlock()
if link.IsSame(im.linkState) {
return
}
im.linkState = link
}
im.logger.Info().Interface("link", link).Msg("link state changed")
operState := link.Attrs().OperState
if operState == netlink.OperUp {
im.handleLinkUp()
} else {
im.handleLinkDown()
}
}
// SendRouterSolicitation sends a router solicitation
func (im *InterfaceManager) SendRouterSolicitation() error {
im.logger.Info().Msg("sending router solicitation")
m := &ndp.RouterSolicitation{}
l, err := im.link()
if err != nil {
return fmt.Errorf("failed to get interface: %w", err)
}
if l.Attrs().OperState != netlink.OperUp {
return fmt.Errorf("interface %s is not up", im.ifaceName)
}
iface := l.Interface()
if iface == nil {
return fmt.Errorf("failed to get net.Interface for %s", im.ifaceName)
}
hwAddr := l.HardwareAddr()
if hwAddr == nil {
return fmt.Errorf("failed to get hardware address for %s", im.ifaceName)
}
c, _, err := ndp.Listen(iface, ndp.LinkLocal)
if err != nil {
return fmt.Errorf("failed to create NDP listener on %s: %w", im.ifaceName, err)
}
m.Options = append(m.Options, &ndp.LinkLayerAddress{
Addr: hwAddr,
Direction: ndp.Source,
})
targetAddr := netip.MustParseAddr("ff02::2")
if err := c.WriteTo(m, nil, targetAddr); err != nil {
c.Close()
return fmt.Errorf("failed to write to %s: %w", targetAddr.String(), err)
}
im.logger.Info().Msg("router solicitation sent")
c.Close()
return nil
}
func (im *InterfaceManager) handleLinkUp() {
im.logger.Info().Msg("link up")
if err := im.applyConfiguration(); err != nil {
im.logger.Error().Err(err).Msg("failed to apply configuration")
}
if im.config.IPv4Mode.String == "dhcp" {
if err := im.dhcpClient.Renew(); err != nil {
im.logger.Error().Err(err).Msg("failed to renew DHCP lease")
}
}
if im.config.IPv6Mode.String == "slaac" {
if err := im.staticConfig.EnableIPv6SLAAC(); err != nil {
im.logger.Error().Err(err).Msg("failed to enable IPv6 SLAAC")
}
if err := im.SendRouterSolicitation(); err != nil {
im.logger.Error().Err(err).Msg("failed to send router solicitation")
}
}
}
func (im *InterfaceManager) handleLinkDown() {
im.logger.Info().Msg("link down")
if im.config.IPv4Mode.String == "dhcp" {
if err := im.dhcpClient.Stop(); err != nil {
im.logger.Error().Err(err).Msg("failed to stop DHCP client")
}
}
netlinkMgr := getNetlinkManager()
if err := netlinkMgr.RemoveAllAddresses(im.linkState, link.AfInet); err != nil {
im.logger.Error().Err(err).Msg("failed to remove all IPv4 addresses")
}
if err := netlinkMgr.RemoveNonLinkLocalIPv6Addresses(im.linkState); err != nil {
im.logger.Error().Err(err).Msg("failed to remove non-link-local IPv6 addresses")
}
}
// monitorInterfaceState monitors the interface state and updates accordingly
func (im *InterfaceManager) monitorInterfaceState() {
defer im.wg.Done()
im.logger.Debug().Msg("monitoring interface state")
// TODO: use netlink subscription instead of polling
ticker := time.NewTicker(5 * time.Second)
defer ticker.Stop()
for {
select {
case <-im.ctx.Done():
return
case <-im.stopCh:
return
case <-ticker.C:
if err := im.updateInterfaceState(); err != nil {
im.logger.Error().Err(err).Msg("failed to update interface state")
}
}
}
}
// updateStateFromDHCPLease updates the state from a DHCP lease
func (im *InterfaceManager) updateStateFromDHCPLease(lease *types.DHCPLease) {
family := link.AfInet
im.stateMu.Lock()
if lease.IsIPv6() {
im.state.DHCPLease6 = lease
family = link.AfInet6
} else {
im.state.DHCPLease4 = lease
}
im.stateMu.Unlock()
// Update resolv.conf with DNS information
if im.onResolvConfChange == nil {
return
}
if im.ifaceName == "" {
im.logger.Warn().Msg("interface name is empty, skipping resolv.conf update")
return
}
if err := im.onResolvConfChange(family, &types.InterfaceResolvConf{
NameServers: lease.DNS,
SearchList: lease.SearchList,
Source: "dhcp",
}); err != nil {
im.logger.Warn().Err(err).Msg("failed to update resolv.conf")
}
}
// ReconcileLinkAddrs reconciles the link addresses
func (im *InterfaceManager) ReconcileLinkAddrs(addrs []types.IPAddress, family int) error {
nl := getNetlinkManager()
link, err := im.link()
if err != nil {
return fmt.Errorf("failed to get interface: %w", err)
}
if link == nil {
return fmt.Errorf("failed to get interface: %w", err)
}
return nl.ReconcileLink(link, addrs, family)
}
// applyDHCPLease applies DHCP lease configuration using ReconcileLinkAddrs
func (im *InterfaceManager) applyDHCPLease(lease *types.DHCPLease) error {
if lease == nil {
return fmt.Errorf("DHCP lease is nil")
}
if lease.DHCPClient != "jetdhcpc" {
im.logger.Warn().Str("dhcp_client", lease.DHCPClient).Msg("ignoring DHCP lease, not implemented yet")
return nil
}
if lease.IsIPv6() {
im.logger.Warn().Msg("ignoring IPv6 DHCP lease, not implemented yet")
return nil
}
// Convert DHCP lease to IPv4Config
ipv4Config := im.convertDHCPLeaseToIPv4Config(lease)
// Apply the configuration using ReconcileLinkAddrs
return im.ReconcileLinkAddrs([]types.IPAddress{*ipv4Config}, link.AfInet)
}
// convertDHCPLeaseToIPv4Config converts a DHCP lease to IPv4Config
func (im *InterfaceManager) convertDHCPLeaseToIPv4Config(lease *types.DHCPLease) *types.IPAddress {
ipNet := lease.IPNet()
if ipNet == nil {
return nil
}
// Create IPv4Address
ipv4Addr := types.IPAddress{
Address: *ipNet,
Gateway: lease.Routers[0],
Secondary: false,
Permanent: false,
}
im.logger.Trace().
Interface("ipv4Addr", ipv4Addr).
Interface("lease", lease).
Msg("converted DHCP lease to IPv4Config")
// Create IPv4Config
return &ipv4Addr
}

View File

@ -1,163 +0,0 @@
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) {
mgr := getNetlinkManager()
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
ipv6Gateway string
ipv4Ready, ipv6Ready = false, false
stateChanged = false
)
routes, _ := mgr.ListDefaultRoutes(link.AfInet6)
if len(routes) > 0 {
ipv6Gateway = routes[0].Gw.String()
}
for _, addr := range addrs {
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,
Flags: addr.Flags,
ValidLifetime: lifetimeToTime(addr.ValidLft),
PreferredLifetime: lifetimeToTime(addr.PreferedLft),
})
if ipv6Addr == "" {
ipv6Addr = addr.IP.String()
ipv6Ready = true
}
}
if !sortAndCompareStringSlices(im.state.IPv4Addresses, ipv4Addresses) {
im.state.IPv4Addresses = ipv4Addresses
stateChanged = true
}
if !sortAndCompareIPv6AddressSlices(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.IPv6Gateway != ipv6Gateway {
im.state.IPv6Gateway = ipv6Gateway
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
}

View File

@ -1,407 +0,0 @@
package jetdhcpc
import (
"context"
"errors"
"net"
"slices"
"time"
"github.com/jetkvm/kvm/internal/sync"
"github.com/jetkvm/kvm/pkg/nmlite/link"
"github.com/insomniacslk/dhcp/dhcpv4"
"github.com/insomniacslk/dhcp/dhcpv6"
"github.com/jetkvm/kvm/internal/network/types"
"github.com/rs/zerolog"
)
const (
VendorIdentifier = "jetkvm"
)
var (
ErrIPv6LinkTimeout = errors.New("timeout after waiting for a non-tentative IPv6 address")
ErrIPv6RouteTimeout = errors.New("timeout after waiting for an IPv6 route")
ErrInterfaceUpTimeout = errors.New("timeout after waiting for an interface to come up")
ErrInterfaceUpCanceled = errors.New("context canceled while waiting for an interface to come up")
)
type LeaseChangeHandler func(lease *types.DHCPLease)
// Config is a DHCP client configuration.
type Config struct {
LinkUpTimeout time.Duration
// Timeout is the timeout for one DHCP request attempt.
Timeout time.Duration
// Retries is how many times to retry DHCP attempts.
Retries int
// IPv4 is whether to request an IPv4 lease.
IPv4 bool
// IPv6 is whether to request an IPv6 lease.
IPv6 bool
// Modifiers4 allows modifications to the IPv4 DHCP request.
Modifiers4 []dhcpv4.Modifier
// Modifiers6 allows modifications to the IPv6 DHCP request.
Modifiers6 []dhcpv6.Modifier
// V6ServerAddr can be a unicast or broadcast destination for DHCPv6
// messages.
//
// If not set, it will default to nclient6's default (all servers &
// relay agents).
V6ServerAddr *net.UDPAddr
// V6ClientPort is the port that is used to send and receive DHCPv6
// messages.
//
// If not set, it will default to dhcpv6's default (546).
V6ClientPort *int
// V4ServerAddr can be a unicast or broadcast destination for IPv4 DHCP
// messages.
//
// If not set, it will default to nclient4's default (DHCP broadcast
// address).
V4ServerAddr *net.UDPAddr
// If true, add Client Identifier (61) option to the IPv4 request.
V4ClientIdentifier bool
Hostname string
OnLease4Change LeaseChangeHandler
OnLease6Change LeaseChangeHandler
UpdateResolvConf func([]string) error
}
// Client is a DHCP client.
type Client struct {
types.DHCPClient
ifaces []string
cfg Config
l *zerolog.Logger
ctx context.Context
// TODO: support multiple interfaces
currentLease4 *Lease
currentLease6 *Lease
mu sync.Mutex
cfgMu sync.Mutex
lease4Mu sync.Mutex
lease6Mu sync.Mutex
timer4 *time.Timer
timer6 *time.Timer
stateDir string
}
var (
defaultTimerDuration = 1 * time.Second
defaultLinkUpTimeout = 30 * time.Second
maxRenewalAttemptDuration = 2 * time.Hour
)
// NewClient creates a new DHCP client for the given interface.
func NewClient(ctx context.Context, ifaces []string, c *Config, l *zerolog.Logger) (*Client, error) {
timer4 := time.NewTimer(defaultTimerDuration)
timer6 := time.NewTimer(defaultTimerDuration)
cfg := *c
if cfg.LinkUpTimeout == 0 {
cfg.LinkUpTimeout = defaultLinkUpTimeout
}
if cfg.Timeout == 0 {
cfg.Timeout = defaultLinkUpTimeout
}
if cfg.Retries == 0 {
cfg.Retries = 3
}
return &Client{
ctx: ctx,
ifaces: ifaces,
cfg: cfg,
l: l,
stateDir: "/run/jetkvm-dhcp",
currentLease4: nil,
currentLease6: nil,
lease4Mu: sync.Mutex{},
lease6Mu: sync.Mutex{},
mu: sync.Mutex{},
cfgMu: sync.Mutex{},
timer4: timer4,
timer6: timer6,
}, nil
}
func resetTimer(t *time.Timer, l *zerolog.Logger) {
l.Debug().Dur("delay", defaultTimerDuration).Msg("will retry later")
t.Reset(defaultTimerDuration)
}
func getRenewalTime(lease *Lease) time.Duration {
if lease.RenewalTime <= 0 || lease.LeaseTime > maxRenewalAttemptDuration/2 {
return maxRenewalAttemptDuration
}
return lease.RenewalTime
}
func (c *Client) requestLoop(t *time.Timer, family int, ifname string) {
l := c.l.With().Str("interface", ifname).Int("family", family).Logger()
for range t.C {
l.Info().Msg("requesting lease")
if _, err := c.ensureInterfaceUp(ifname); err != nil {
l.Error().Err(err).Msg("failed to ensure interface up")
resetTimer(t, c.l)
continue
}
var (
lease *Lease
err error
)
switch family {
case link.AfInet:
lease, err = c.requestLease4(ifname)
case link.AfInet6:
lease, err = c.requestLease6(ifname)
}
if err != nil {
l.Error().Err(err).Msg("failed to request lease")
resetTimer(t, c.l)
continue
}
c.handleLeaseChange(lease)
nextRenewal := getRenewalTime(lease)
l.Info().
Dur("nextRenewal", nextRenewal).
Dur("leaseTime", lease.LeaseTime).
Dur("rebindingTime", lease.RebindingTime).
Msg("sleeping until next renewal")
t.Reset(nextRenewal)
}
}
func (c *Client) ensureInterfaceUp(ifname string) (*link.Link, error) {
nlm := link.GetNetlinkManager()
iface, err := nlm.GetLinkByName(ifname)
if err != nil {
return nil, err
}
return nlm.EnsureInterfaceUpWithTimeout(c.ctx, iface, c.cfg.LinkUpTimeout)
}
// Lease4 returns the current IPv4 lease
func (c *Client) Lease4() *types.DHCPLease {
c.lease4Mu.Lock()
defer c.lease4Mu.Unlock()
if c.currentLease4 == nil {
return nil
}
return c.currentLease4.ToDHCPLease()
}
// Lease6 returns the current IPv6 lease
func (c *Client) Lease6() *types.DHCPLease {
c.lease6Mu.Lock()
defer c.lease6Mu.Unlock()
if c.currentLease6 == nil {
return nil
}
return c.currentLease6.ToDHCPLease()
}
// Domain returns the current domain
func (c *Client) Domain() string {
c.lease4Mu.Lock()
defer c.lease4Mu.Unlock()
if c.currentLease4 != nil {
return c.currentLease4.Domain
}
c.lease6Mu.Lock()
defer c.lease6Mu.Unlock()
if c.currentLease6 != nil {
return c.currentLease6.Domain
}
return ""
}
// handleLeaseChange handles lease changes
func (c *Client) handleLeaseChange(lease *Lease) {
// do not use defer here, because we need to unlock the mutex before returning
ipv4 := lease.p4 != nil
if ipv4 {
c.lease4Mu.Lock()
c.currentLease4 = lease
c.lease4Mu.Unlock()
} else {
c.lease6Mu.Lock()
c.currentLease6 = lease
c.lease6Mu.Unlock()
}
c.apply()
// TODO: handle lease expiration
if c.cfg.OnLease4Change != nil && ipv4 {
c.cfg.OnLease4Change(lease.ToDHCPLease())
}
if c.cfg.OnLease6Change != nil && !ipv4 {
c.cfg.OnLease6Change(lease.ToDHCPLease())
}
}
func (c *Client) Renew() error {
c.timer4.Reset(defaultTimerDuration)
c.timer6.Reset(defaultTimerDuration)
return nil
}
func (c *Client) Release() error {
// TODO: implement
return nil
}
func (c *Client) SetIPv4(ipv4 bool) {
c.cfgMu.Lock()
defer c.cfgMu.Unlock()
currentIPv4 := c.cfg.IPv4
c.cfg.IPv4 = ipv4
if currentIPv4 == ipv4 {
return
}
if !ipv4 {
c.lease4Mu.Lock()
c.currentLease4 = nil
c.lease4Mu.Unlock()
c.timer4.Stop()
}
c.timer4.Reset(defaultTimerDuration)
}
func (c *Client) SetIPv6(ipv6 bool) {
c.cfgMu.Lock()
defer c.cfgMu.Unlock()
currentIPv6 := c.cfg.IPv6
c.cfg.IPv6 = ipv6
if currentIPv6 == ipv6 {
return
}
if !ipv6 {
c.lease6Mu.Lock()
c.currentLease6 = nil
c.lease6Mu.Unlock()
c.timer6.Stop()
}
c.timer6.Reset(defaultTimerDuration)
}
func (c *Client) Start() error {
if err := c.killUdhcpc(); err != nil {
c.l.Warn().Err(err).Msg("failed to kill udhcpc processes, continuing anyway")
}
for _, iface := range c.ifaces {
if c.cfg.IPv4 {
go c.requestLoop(c.timer4, link.AfInet, iface)
}
if c.cfg.IPv6 {
go c.requestLoop(c.timer6, link.AfInet6, iface)
}
}
return nil
}
func (c *Client) apply() {
var (
iface string
nameservers []net.IP
searchList []string
domain string
)
if c.currentLease4 != nil {
iface = c.currentLease4.InterfaceName
nameservers = c.currentLease4.DNS
searchList = c.currentLease4.SearchList
domain = c.currentLease4.Domain
}
if c.currentLease6 != nil {
iface = c.currentLease6.InterfaceName
nameservers = append(nameservers, c.currentLease6.DNS...)
searchList = append(searchList, c.currentLease6.SearchList...)
domain = c.currentLease6.Domain
}
// deduplicate searchList
searchList = slices.Compact(searchList)
if c.cfg.UpdateResolvConf == nil {
c.l.Warn().Msg("no UpdateResolvConf function set, skipping resolv.conf update")
return
}
c.l.Info().
Str("interface", iface).
Interface("nameservers", nameservers).
Interface("searchList", searchList).
Str("domain", domain).
Msg("updating resolv.conf")
// Convert net.IP to string slice
var nameserverStrings []string
for _, ns := range nameservers {
nameserverStrings = append(nameserverStrings, ns.String())
}
if err := c.cfg.UpdateResolvConf(nameserverStrings); err != nil {
c.l.Error().Err(err).Msg("failed to update resolv.conf")
}
}

View File

@ -1,85 +0,0 @@
package jetdhcpc
import (
"fmt"
"github.com/insomniacslk/dhcp/dhcpv4"
"github.com/insomniacslk/dhcp/dhcpv4/nclient4"
"github.com/vishvananda/netlink"
)
func (c *Client) requestLease4(ifname string) (*Lease, error) {
iface, err := netlink.LinkByName(ifname)
if err != nil {
return nil, err
}
l := c.l.With().Str("interface", ifname).Logger()
mods := []nclient4.ClientOpt{
nclient4.WithTimeout(c.cfg.Timeout),
nclient4.WithRetry(c.cfg.Retries),
}
mods = append(mods, c.getDHCP4Logger(ifname))
if c.cfg.V4ServerAddr != nil {
mods = append(mods, nclient4.WithServerAddr(c.cfg.V4ServerAddr))
}
client, err := nclient4.New(ifname, mods...)
if err != nil {
return nil, err
}
defer client.Close()
// Prepend modifiers with default options, so they can be overridden.
reqmods := append(
[]dhcpv4.Modifier{
dhcpv4.WithOption(dhcpv4.OptClassIdentifier(VendorIdentifier)),
dhcpv4.WithRequestedOptions(
dhcpv4.OptionSubnetMask,
dhcpv4.OptionInterfaceMTU,
dhcpv4.OptionNTPServers,
dhcpv4.OptionDomainName,
dhcpv4.OptionDomainNameServer,
dhcpv4.OptionDNSDomainSearchList,
),
},
c.cfg.Modifiers4...)
if c.cfg.V4ClientIdentifier {
// Client Id is hardware type + mac per RFC 2132 9.14.
ident := []byte{0x01} // Type ethernet
ident = append(ident, iface.Attrs().HardwareAddr...)
reqmods = append(reqmods, dhcpv4.WithOption(dhcpv4.OptClientIdentifier(ident)))
}
if c.cfg.Hostname != "" {
reqmods = append(reqmods, dhcpv4.WithOption(dhcpv4.OptHostName(c.cfg.Hostname)))
}
l.Info().Msg("attempting to get DHCPv4 lease")
var (
lease *nclient4.Lease
reqErr error
)
if c.currentLease4 != nil {
l.Info().Msg("current lease is not nil, renewing")
lease, reqErr = client.Renew(c.ctx, c.currentLease4.p4, reqmods...)
} else {
l.Info().Msg("current lease is nil, requesting new lease")
lease, reqErr = client.Request(c.ctx, reqmods...)
}
if reqErr != nil {
return nil, reqErr
}
if lease == nil || lease.ACK == nil {
return nil, fmt.Errorf("failed to acquire DHCPv4 lease")
}
summaryStructured(lease.ACK, &l).Info().Msgf("DHCPv4 lease acquired: %s", lease.ACK.String())
l.Trace().Interface("options", lease.ACK.Options.String()).Msg("DHCPv4 lease options")
return fromNclient4Lease(lease, ifname), nil
}

View File

@ -1,135 +0,0 @@
package jetdhcpc
import (
"net"
"time"
"github.com/insomniacslk/dhcp/dhcpv6"
"github.com/insomniacslk/dhcp/dhcpv6/nclient6"
"github.com/rs/zerolog"
"github.com/vishvananda/netlink"
)
// isIPv6LinkReady returns true if the interface has a link-local address
// which is not tentative.
func isIPv6LinkReady(l netlink.Link, logger *zerolog.Logger) (bool, error) {
addrs, err := netlink.AddrList(l, 10) // AF_INET6
if err != nil {
return false, err
}
for _, addr := range addrs {
if addr.IP.IsLinkLocalUnicast() && (addr.Flags&0x40 == 0) { // IFA_F_TENTATIVE
if addr.Flags&0x80 != 0 { // IFA_F_DADFAILED
logger.Warn().Str("address", addr.IP.String()).Msg("DADFAILED for address, continuing anyhow")
}
return true, nil
}
}
return false, nil
}
// isIPv6RouteReady returns true if serverAddr is reachable.
func isIPv6RouteReady(serverAddr net.IP) waitForCondition {
return func(l netlink.Link, logger *zerolog.Logger) (bool, error) {
if serverAddr.IsMulticast() {
return true, nil
}
routes, err := netlink.RouteList(l, 10) // AF_INET6
if err != nil {
return false, err
}
for _, route := range routes {
if route.LinkIndex != l.Attrs().Index {
continue
}
// Default route.
if route.Dst == nil {
return true, nil
}
if route.Dst.Contains(serverAddr) {
return true, nil
}
}
return false, nil
}
}
func (c *Client) requestLease6(ifname string) (*Lease, error) {
l := c.l.With().Str("interface", ifname).Logger()
iface, err := netlink.LinkByName(ifname)
if err != nil {
return nil, err
}
clientPort := dhcpv6.DefaultClientPort
if c.cfg.V6ClientPort != nil {
clientPort = *c.cfg.V6ClientPort
}
// For ipv6, we cannot bind to the port until Duplicate Address
// Detection (DAD) is complete which is indicated by the link being no
// longer marked as "tentative". This usually takes about a second.
// If the link is never going to be ready, don't wait forever.
// (The user may not have configured a ctx with a timeout.)
linkUpTimeout := time.After(c.cfg.LinkUpTimeout)
if err := c.waitFor(
iface,
linkUpTimeout,
isIPv6LinkReady,
ErrIPv6LinkTimeout,
); err != nil {
return nil, err
}
// If user specified a non-multicast address, make sure it's routable before we start.
if c.cfg.V6ServerAddr != nil {
if err := c.waitFor(
iface,
linkUpTimeout,
isIPv6RouteReady(c.cfg.V6ServerAddr.IP),
ErrIPv6RouteTimeout,
); err != nil {
return nil, err
}
}
mods := []nclient6.ClientOpt{
nclient6.WithTimeout(c.cfg.Timeout),
nclient6.WithRetry(c.cfg.Retries),
c.getDHCP6Logger(),
}
if c.cfg.V6ServerAddr != nil {
mods = append(mods, nclient6.WithBroadcastAddr(c.cfg.V6ServerAddr))
}
conn, err := nclient6.NewIPv6UDPConn(iface.Attrs().Name, clientPort)
if err != nil {
return nil, err
}
client, err := nclient6.NewWithConn(conn, iface.Attrs().HardwareAddr, mods...)
if err != nil {
return nil, err
}
defer client.Close()
// Prepend modifiers with default options, so they can be overridden.
reqmods := append(
[]dhcpv6.Modifier{
dhcpv6.WithNetboot,
},
c.cfg.Modifiers6...)
l.Info().Msg("attempting to get DHCPv6 lease")
p, err := client.RapidSolicit(c.ctx, reqmods...)
if err != nil {
return nil, err
}
l.Info().Msgf("DHCPv6 lease acquired: %s", p.Summary())
return fromNclient6Lease(p, ifname), nil
}

View File

@ -1,313 +0,0 @@
package jetdhcpc
import (
"bufio"
"encoding/binary"
"encoding/json"
"fmt"
"net"
"os"
"reflect"
"strconv"
"strings"
"time"
"github.com/insomniacslk/dhcp/dhcpv4"
"github.com/insomniacslk/dhcp/dhcpv4/nclient4"
"github.com/insomniacslk/dhcp/dhcpv6"
"github.com/jetkvm/kvm/internal/network/types"
)
var (
defaultLeaseTime = time.Duration(30 * time.Minute)
defaultRenewalTime = time.Duration(15 * time.Minute)
)
// Lease is a network configuration obtained by DHCP.
type Lease struct {
types.DHCPLease
p4 *nclient4.Lease
p6 *dhcpv6.Message
isEmpty map[string]bool
}
// ToDHCPLease converts a lease to a DHCP lease.
func (l *Lease) ToDHCPLease() *types.DHCPLease {
lease := &l.DHCPLease
lease.DHCPClient = "jetdhcpc"
return lease
}
// fromNclient4Lease creates a lease from a nclient4.Lease.
func fromNclient4Lease(l *nclient4.Lease, iface string) *Lease {
lease := &Lease{}
lease.p4 = l
// only the fields that we need are set
lease.Routers = l.ACK.Router()
lease.IPAddress = l.ACK.YourIPAddr
lease.Netmask = net.IP(l.ACK.SubnetMask())
lease.Broadcast = l.ACK.BroadcastAddress()
lease.NTPServers = l.ACK.NTPServers()
lease.HostName = l.ACK.HostName()
lease.Domain = l.ACK.DomainName()
searchList := l.ACK.DomainSearch()
if searchList != nil {
lease.SearchList = searchList.Labels
}
lease.DNS = l.ACK.DNS()
lease.ClassIdentifier = l.ACK.ClassIdentifier()
lease.ServerID = l.ACK.ServerIdentifier().String()
mtu := l.ACK.Options.Get(dhcpv4.OptionInterfaceMTU)
if mtu != nil {
lease.MTU = int(binary.BigEndian.Uint16(mtu))
}
lease.Message = l.ACK.Message()
lease.LeaseTime = l.ACK.IPAddressLeaseTime(defaultLeaseTime)
lease.RenewalTime = l.ACK.IPAddressRenewalTime(defaultRenewalTime)
lease.InterfaceName = iface
return lease
}
// fromNclient6Lease creates a lease from a nclient6.Message.
func fromNclient6Lease(l *dhcpv6.Message, iface string) *Lease {
lease := &Lease{}
lease.p6 = l
iana := l.Options.OneIANA()
if iana == nil {
return nil
}
address := iana.Options.OneAddress()
if address == nil {
return nil
}
lease.IPAddress = address.IPv6Addr
lease.Netmask = net.IP(net.CIDRMask(128, 128))
lease.DNS = l.Options.DNS()
// lease.LeaseTime = iana.Options.OnePreferredLifetime()
// lease.RenewalTime = iana.Options.OneValidLifetime()
// lease.RebindingTime = iana.Options.OneRebindingTime()
lease.InterfaceName = iface
return lease
}
func (l *Lease) setIsEmpty(m map[string]bool) {
l.isEmpty = m
}
// IsEmpty returns true if the lease is empty for the given key.
func (l *Lease) IsEmpty(key string) bool {
return l.isEmpty[key]
}
// ToJSON returns the lease as a JSON string.
func (l *Lease) ToJSON() string {
json, err := json.Marshal(l)
if err != nil {
return ""
}
return string(json)
}
// SetLeaseExpiry sets the lease expiry time.
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 (l *Lease) Apply() error {
if l.p4 != nil {
return l.applyIPv4()
}
if l.p6 != nil {
return l.applyIPv6()
}
return nil
}
func (l *Lease) applyIPv4() error {
return nil
}
func (l *Lease) applyIPv6() error {
return nil
}
// UnmarshalDHCPCLease unmarshals a lease from a string.
func UnmarshalDHCPCLease(lease *Lease, str string) error {
// parse the lease file as a map
data := make(map[string]string)
for _, line := range strings.Split(str, "\n") {
line = strings.TrimSpace(line)
// skip empty lines and comments
if line == "" || strings.HasPrefix(line, "#") {
continue
}
parts := strings.SplitN(line, "=", 2)
if len(parts) != 2 {
continue
}
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
data[key] = value
}
// now iterate over the lease struct and set the values
leaseType := reflect.TypeOf(lease).Elem()
leaseValue := reflect.ValueOf(lease).Elem()
valuesParsed := make(map[string]bool)
for i := 0; i < leaseType.NumField(); i++ {
field := leaseValue.Field(i)
// get the env tag
key := leaseType.Field(i).Tag.Get("env")
if key == "" {
continue
}
valuesParsed[key] = false
// get the value from the data map
value, ok := data[key]
if !ok || value == "" {
continue
}
switch field.Interface().(type) {
case string:
field.SetString(value)
case int:
val, err := strconv.Atoi(value)
if err != nil {
continue
}
field.SetInt(int64(val))
case time.Duration:
val, err := time.ParseDuration(value + "s")
if err != nil {
continue
}
field.Set(reflect.ValueOf(val))
case net.IP:
ip := net.ParseIP(value)
if ip == nil {
continue
}
field.Set(reflect.ValueOf(ip))
case []net.IP:
val := make([]net.IP, 0)
for _, ipStr := range strings.Fields(value) {
ip := net.ParseIP(ipStr)
if ip == nil {
continue
}
val = append(val, ip)
}
field.Set(reflect.ValueOf(val))
default:
return fmt.Errorf("unsupported field `%s` type: %s", key, field.Type().String())
}
valuesParsed[key] = true
}
lease.setIsEmpty(valuesParsed)
return nil
}
// MarshalDHCPCLease marshals a lease to a string.
func MarshalDHCPCLease(lease *Lease) (string, error) {
leaseType := reflect.TypeOf(lease).Elem()
leaseValue := reflect.ValueOf(lease).Elem()
leaseFile := ""
for i := 0; i < leaseType.NumField(); i++ {
field := leaseValue.Field(i)
key := leaseType.Field(i).Tag.Get("env")
if key == "" {
continue
}
outValue := ""
switch field.Interface().(type) {
case string:
outValue = field.String()
case int:
outValue = strconv.Itoa(int(field.Int()))
case time.Duration:
outValue = strconv.Itoa(int(field.Int()))
case net.IP:
outValue = field.String()
case []net.IP:
ips := field.Interface().([]net.IP)
ipStrings := make([]string, len(ips))
for i, ip := range ips {
ipStrings[i] = ip.String()
}
outValue = strings.Join(ipStrings, " ")
default:
return "", fmt.Errorf("unsupported field `%s` type: %s", key, field.Type().String())
}
leaseFile += fmt.Sprintf("%s=%s\n", key, outValue)
}
return leaseFile, nil
}

View File

@ -1,102 +0,0 @@
package jetdhcpc
import (
"bytes"
"io"
"os"
"path/filepath"
"strconv"
"strings"
"syscall"
"github.com/rs/zerolog"
)
func readFileNoStat(filename string) ([]byte, error) {
const maxBufferSize = 1024 * 1024
f, err := os.Open(filename)
if err != nil {
return nil, err
}
defer f.Close()
reader := io.LimitReader(f, maxBufferSize)
return io.ReadAll(reader)
}
func toCmdline(path string) ([]string, error) {
data, err := readFileNoStat(path)
if err != nil {
return nil, err
}
if len(data) < 1 {
return []string{}, nil
}
return strings.Split(string(bytes.TrimRight(data, "\x00")), "\x00"), nil
}
// KillUdhcpC kills all udhcpc processes
func KillUdhcpC(l *zerolog.Logger) error {
// read procfs for udhcpc processes
// we do not use procfs.AllProcs() because we want to avoid the overhead of reading the entire procfs
processes, err := os.ReadDir("/proc")
if err != nil {
return err
}
matchedPids := make([]int, 0)
// iterate over the processes
for _, d := range processes {
// check if file is numeric
pid, err := strconv.Atoi(d.Name())
if err != nil {
continue
}
// check if it's a directory
if !d.IsDir() {
continue
}
cmdline, err := toCmdline(filepath.Join("/proc", d.Name(), "cmdline"))
if err != nil {
continue
}
if len(cmdline) < 1 {
continue
}
if cmdline[0] != "udhcpc" {
continue
}
matchedPids = append(matchedPids, pid)
}
if len(matchedPids) == 0 {
l.Info().Msg("no udhcpc processes found")
return nil
}
l.Info().Ints("pids", matchedPids).Msg("found udhcpc processes, terminating")
for _, pid := range matchedPids {
err := syscall.Kill(pid, syscall.SIGTERM)
if err != nil {
return err
}
l.Info().Int("pid", pid).Msg("terminated udhcpc process")
}
return nil
}
func (c *Client) killUdhcpc() error {
return KillUdhcpC(c.l)
}

View File

@ -1,64 +0,0 @@
package jetdhcpc
import (
"github.com/insomniacslk/dhcp/dhcpv4"
"github.com/insomniacslk/dhcp/dhcpv4/nclient4"
"github.com/insomniacslk/dhcp/dhcpv6/nclient6"
"github.com/rs/zerolog"
)
type dhcpLogger struct {
// Printfer is used for actual output of the logger
nclient4.Printfer
l *zerolog.Logger
}
// Printf prints a log message as-is via predefined Printfer
func (s dhcpLogger) Printf(format string, v ...interface{}) {
s.l.Info().Msgf(format, v...)
}
// PrintMessage prints a DHCP message in the short format via predefined Printfer
func (s dhcpLogger) PrintMessage(prefix string, message *dhcpv4.DHCPv4) {
s.l.Info().Msgf("%s: %s", prefix, message.String())
}
func summaryStructured(d *dhcpv4.DHCPv4, l *zerolog.Logger) *zerolog.Logger {
logger := l.With().
Str("opCode", d.OpCode.String()).
Str("hwType", d.HWType.String()).
Int("hopCount", int(d.HopCount)).
Str("transactionID", d.TransactionID.String()).
Int("numSeconds", int(d.NumSeconds)).
Str("flagsString", d.FlagsToString()).
Int("flags", int(d.Flags)).
Str("clientIP", d.ClientIPAddr.String()).
Str("yourIP", d.YourIPAddr.String()).
Str("serverIP", d.ServerIPAddr.String()).
Str("gatewayIP", d.GatewayIPAddr.String()).
Str("clientMAC", d.ClientHWAddr.String()).
Str("serverHostname", d.ServerHostName).
Str("bootFileName", d.BootFileName).
Str("options", d.Options.Summary(nil)).
Logger()
return &logger
}
func (c *Client) getDHCP4Logger(ifname string) nclient4.ClientOpt {
logger := c.l.With().
Str("interface", ifname).
Str("source", "dhcp4").
Logger()
return nclient4.WithLogger(dhcpLogger{
l: &logger,
})
}
// TODO: nclient6 doesn't implement the WithLogger option,
// we might need to open a PR to add it
func (c *Client) getDHCP6Logger() nclient6.ClientOpt {
return nclient6.WithSummaryLogger()
}

View File

@ -1,247 +0,0 @@
package jetdhcpc
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"time"
"github.com/jetkvm/kvm/internal/network/types"
)
const (
// DefaultStateDir is the default state directory
DefaultStateDir = "/var/run/"
// DHCPStateFile is the name of the DHCP state file
DHCPStateFile = "jetkvm_dhcp_state.json"
)
// DHCPState represents the persistent state of DHCP clients
type DHCPState struct {
InterfaceStates map[string]*InterfaceDHCPState `json:"interface_states"`
LastUpdated time.Time `json:"last_updated"`
Version string `json:"version"`
}
// InterfaceDHCPState represents the DHCP state for a specific interface
type InterfaceDHCPState struct {
InterfaceName string `json:"interface_name"`
IPv4Enabled bool `json:"ipv4_enabled"`
IPv6Enabled bool `json:"ipv6_enabled"`
IPv4Lease *Lease `json:"ipv4_lease,omitempty"`
IPv6Lease *Lease `json:"ipv6_lease,omitempty"`
LastRenewal time.Time `json:"last_renewal"`
Config *types.NetworkConfig `json:"config,omitempty"`
}
// SaveState saves the current DHCP state to disk
func (c *Client) SaveState(state *DHCPState) error {
if state == nil {
return fmt.Errorf("state cannot be nil")
}
// Return error if state directory doesn't exist
if _, err := os.Stat(c.stateDir); os.IsNotExist(err) {
return fmt.Errorf("state directory does not exist: %w", err)
}
// Update timestamp
state.LastUpdated = time.Now()
state.Version = "1.0"
// Serialize state
data, err := json.MarshalIndent(state, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal state: %w", err)
}
// Write to temporary file first, then rename to ensure atomic operation
tmpFile, err := os.CreateTemp(c.stateDir, DHCPStateFile)
if err != nil {
return fmt.Errorf("failed to create temporary file: %w", err)
}
defer tmpFile.Close()
if err := os.WriteFile(tmpFile.Name(), data, 0644); err != nil {
return fmt.Errorf("failed to write state file: %w", err)
}
stateFile := filepath.Join(c.stateDir, DHCPStateFile)
if err := os.Rename(tmpFile.Name(), stateFile); err != nil {
os.Remove(tmpFile.Name())
return fmt.Errorf("failed to rename state file: %w", err)
}
c.l.Debug().Str("file", stateFile).Msg("DHCP state saved")
return nil
}
// LoadState loads the DHCP state from disk
func (c *Client) LoadState() (*DHCPState, error) {
stateFile := filepath.Join(c.stateDir, DHCPStateFile)
// Check if state file exists
if _, err := os.Stat(stateFile); os.IsNotExist(err) {
c.l.Debug().Msg("No existing DHCP state file found")
return &DHCPState{
InterfaceStates: make(map[string]*InterfaceDHCPState),
LastUpdated: time.Now(),
Version: "1.0",
}, nil
}
// Read state file
data, err := os.ReadFile(stateFile)
if err != nil {
return nil, fmt.Errorf("failed to read state file: %w", err)
}
// Deserialize state
var state DHCPState
if err := json.Unmarshal(data, &state); err != nil {
return nil, fmt.Errorf("failed to unmarshal state: %w", err)
}
// Initialize interface states map if nil
if state.InterfaceStates == nil {
state.InterfaceStates = make(map[string]*InterfaceDHCPState)
}
c.l.Debug().Str("file", stateFile).Msg("DHCP state loaded")
return &state, nil
}
// UpdateInterfaceState updates the state for a specific interface
func (c *Client) UpdateInterfaceState(ifaceName string, state *InterfaceDHCPState) error {
// Load current state
currentState, err := c.LoadState()
if err != nil {
return fmt.Errorf("failed to load current state: %w", err)
}
// Update interface state
currentState.InterfaceStates[ifaceName] = state
// Save updated state
return c.SaveState(currentState)
}
// GetInterfaceState gets the state for a specific interface
func (c *Client) GetInterfaceState(ifaceName string) (*InterfaceDHCPState, error) {
state, err := c.LoadState()
if err != nil {
return nil, fmt.Errorf("failed to load state: %w", err)
}
return state.InterfaceStates[ifaceName], nil
}
// RemoveInterfaceState removes the state for a specific interface
func (c *Client) RemoveInterfaceState(ifaceName string) error {
// Load current state
currentState, err := c.LoadState()
if err != nil {
return fmt.Errorf("failed to load current state: %w", err)
}
// Remove interface state
delete(currentState.InterfaceStates, ifaceName)
// Save updated state
return c.SaveState(currentState)
}
// IsLeaseValid checks if a DHCP lease is still valid
func (c *Client) IsLeaseValid(lease *Lease) bool {
if lease == nil {
return false
}
// Check if lease has expired
if lease.LeaseExpiry == nil {
return false
}
return time.Now().Before(*lease.LeaseExpiry)
}
// ShouldRenewLease checks if a lease should be renewed
func (c *Client) ShouldRenewLease(lease *Lease) bool {
if !c.IsLeaseValid(lease) {
return false
}
expiry := *lease.LeaseExpiry
leaseTime := time.Now().Add(time.Duration(lease.LeaseTime) * time.Second)
// Renew if lease expires within 50% of its lifetime
leaseDuration := expiry.Sub(leaseTime)
renewalTime := leaseTime.Add(leaseDuration / 2)
return time.Now().After(renewalTime)
}
// CleanupExpiredStates removes expired states from the state file
func (c *Client) CleanupExpiredStates() error {
state, err := c.LoadState()
if err != nil {
return fmt.Errorf("failed to load state: %w", err)
}
cleaned := false
for ifaceName, ifaceState := range state.InterfaceStates {
// Remove interface state if both leases are expired
ipv4Valid := c.IsLeaseValid(ifaceState.IPv4Lease)
ipv6Valid := c.IsLeaseValid(ifaceState.IPv6Lease)
if !ipv4Valid && !ipv6Valid {
delete(state.InterfaceStates, ifaceName)
cleaned = true
c.l.Debug().Str("interface", ifaceName).Msg("Removed expired DHCP state")
}
}
if cleaned {
return c.SaveState(state)
}
return nil
}
// GetStateSummary returns a summary of the current state
func (c *Client) GetStateSummary() (map[string]interface{}, error) {
state, err := c.LoadState()
if err != nil {
return nil, fmt.Errorf("failed to load state: %w", err)
}
summary := map[string]interface{}{
"last_updated": state.LastUpdated,
"version": state.Version,
"interface_count": len(state.InterfaceStates),
"interfaces": make(map[string]interface{}),
}
interfaces := summary["interfaces"].(map[string]interface{})
for ifaceName, ifaceState := range state.InterfaceStates {
interfaceInfo := map[string]interface{}{
"ipv4_enabled": ifaceState.IPv4Enabled,
"ipv6_enabled": ifaceState.IPv6Enabled,
"last_renewal": ifaceState.LastRenewal,
// "ipv4_lease_valid": c.IsLeaseValid(ifaceState.IPv4Lease.(*Lease)),
// "ipv6_lease_valid": c.IsLeaseValid(ifaceState.IPv6Lease),
}
if ifaceState.IPv4Lease != nil {
interfaceInfo["ipv4_lease_expiry"] = ifaceState.IPv4Lease.LeaseExpiry
}
if ifaceState.IPv6Lease != nil {
interfaceInfo["ipv6_lease_expiry"] = ifaceState.IPv6Lease.LeaseExpiry
}
interfaces[ifaceName] = interfaceInfo
}
return summary, nil
}

View File

@ -1,48 +0,0 @@
package jetdhcpc
import (
"context"
"time"
"github.com/rs/zerolog"
"github.com/vishvananda/netlink"
)
type waitForCondition func(l netlink.Link, logger *zerolog.Logger) (ready bool, err error)
func (c *Client) waitFor(
link netlink.Link,
timeout <-chan time.Time,
condition waitForCondition,
timeoutError error,
) error {
return waitFor(c.ctx, link, c.l, timeout, condition, timeoutError)
}
func waitFor(
ctx context.Context,
link netlink.Link,
logger *zerolog.Logger,
timeout <-chan time.Time,
condition waitForCondition,
timeoutError error,
) error {
for {
if ready, err := condition(link, logger); err != nil {
return err
} else if ready {
break
}
select {
case <-time.After(100 * time.Millisecond):
continue
case <-timeout:
return timeoutError
case <-ctx.Done():
return timeoutError
}
}
return nil
}

View File

@ -1,13 +0,0 @@
package link
const (
// AfUnspec is the unspecified address family constant
AfUnspec = 0
// AfInet is the IPv4 address family constant
AfInet = 2
// AfInet6 is the IPv6 address family constant
AfInet6 = 10
sysctlBase = "/proc/sys"
sysctlFileMode = 0640
)

View File

@ -1,544 +0,0 @@
package link
import (
"context"
"fmt"
"net"
"time"
"github.com/jetkvm/kvm/internal/sync"
"github.com/jetkvm/kvm/internal/network/types"
"github.com/rs/zerolog"
"github.com/vishvananda/netlink"
)
// StateChangeHandler is the function type for link state callbacks
type StateChangeHandler func(link *Link)
// StateChangeCallback is the struct for link state callbacks
type StateChangeCallback struct {
Async bool
Func StateChangeHandler
}
// NetlinkManager provides centralized netlink operations
type NetlinkManager struct {
logger *zerolog.Logger
mu sync.RWMutex
stateChangeCallbacks map[string][]StateChangeCallback
}
func newNetlinkManager(logger *zerolog.Logger) *NetlinkManager {
if logger == nil {
logger = &zerolog.Logger{} // Default no-op logger
}
n := &NetlinkManager{
logger: logger,
stateChangeCallbacks: make(map[string][]StateChangeCallback),
}
n.monitorStateChange()
return n
}
// GetNetlinkManager returns the singleton NetlinkManager instance
func GetNetlinkManager() *NetlinkManager {
netlinkManagerOnce.Do(func() {
netlinkManagerInstance = newNetlinkManager(nil)
})
return netlinkManagerInstance
}
// InitializeNetlinkManager initializes the singleton NetlinkManager with a logger
func InitializeNetlinkManager(logger *zerolog.Logger) *NetlinkManager {
netlinkManagerOnce.Do(func() {
netlinkManagerInstance = newNetlinkManager(logger)
})
return netlinkManagerInstance
}
// AddStateChangeCallback adds a callback for link state changes
func (nm *NetlinkManager) AddStateChangeCallback(ifname string, callback StateChangeCallback) {
nm.mu.Lock()
defer nm.mu.Unlock()
if _, ok := nm.stateChangeCallbacks[ifname]; !ok {
nm.stateChangeCallbacks[ifname] = make([]StateChangeCallback, 0)
}
nm.stateChangeCallbacks[ifname] = append(nm.stateChangeCallbacks[ifname], callback)
}
// Interface operations
func (nm *NetlinkManager) monitorStateChange() {
updateCh := make(chan netlink.LinkUpdate)
// we don't need to stop the subscription, as it will be closed when the program exits
stopCh := make(chan struct{}) //nolint:unused
if err := netlink.LinkSubscribe(updateCh, stopCh); err != nil {
nm.logger.Error().Err(err).Msg("failed to subscribe to link state changes")
}
nm.logger.Info().Msg("state change monitoring started")
go func() {
for update := range updateCh {
nm.runCallbacks(update)
}
}()
}
func (nm *NetlinkManager) runCallbacks(update netlink.LinkUpdate) {
nm.mu.RLock()
defer nm.mu.RUnlock()
ifname := update.Link.Attrs().Name
callbacks, ok := nm.stateChangeCallbacks[ifname]
l := nm.logger.With().Str("interface", ifname).Logger()
if !ok {
l.Trace().Msg("no state change callbacks for interface")
return
}
for _, callback := range callbacks {
l.Trace().
Interface("callback", callback).
Bool("async", callback.Async).
Msg("calling callback")
if callback.Async {
go callback.Func(&Link{Link: update.Link})
} else {
callback.Func(&Link{Link: update.Link})
}
}
}
// GetLinkByName gets a network link by name
func (nm *NetlinkManager) GetLinkByName(name string) (*Link, error) {
nm.mu.RLock()
defer nm.mu.RUnlock()
link, err := netlink.LinkByName(name)
if err != nil {
return nil, err
}
return &Link{Link: link}, nil
}
// LinkSetUp brings a network interface up
func (nm *NetlinkManager) LinkSetUp(link *Link) error {
nm.mu.RLock()
defer nm.mu.RUnlock()
return netlink.LinkSetUp(link)
}
// LinkSetDown brings a network interface down
func (nm *NetlinkManager) LinkSetDown(link *Link) error {
nm.mu.RLock()
defer nm.mu.RUnlock()
return netlink.LinkSetDown(link)
}
// EnsureInterfaceUp ensures the interface is up
func (nm *NetlinkManager) EnsureInterfaceUp(link *Link) error {
if link.Attrs().OperState == netlink.OperUp {
return nil
}
return nm.LinkSetUp(link)
}
// EnsureInterfaceUpWithTimeout ensures the interface is up with timeout and retry logic
func (nm *NetlinkManager) EnsureInterfaceUpWithTimeout(ctx context.Context, iface *Link, timeout time.Duration) (*Link, error) {
ifname := iface.Attrs().Name
l := nm.logger.With().Str("interface", ifname).Logger()
linkUpTimeout := time.After(timeout)
var attempt int
start := time.Now()
for {
link, err := nm.GetLinkByName(ifname)
if err != nil {
return nil, err
}
state := link.Attrs().OperState
l = l.With().
Int("attempt", attempt).
Dur("duration", time.Since(start)).
Str("state", state.String()).
Logger()
if state == netlink.OperUp || state == netlink.OperUnknown {
if attempt > 0 {
l.Info().Int("attempt", attempt-1).Msg("interface is up")
}
return link, nil
}
l.Info().Msg("bringing up interface")
// bring up the interface
if err = nm.LinkSetUp(link); err != nil {
l.Error().Err(err).Msg("interface can't make it up")
}
// refresh the link attributes
if err = link.Refresh(); err != nil {
l.Error().Err(err).Msg("failed to refresh link attributes")
}
// check the state again
state = link.Attrs().OperState
l = l.With().Str("new_state", state.String()).Logger()
if state == netlink.OperUp {
l.Info().Msg("interface is up")
return link, nil
}
l.Warn().Msg("interface is still down, retrying")
select {
case <-time.After(500 * time.Millisecond):
attempt++
continue
case <-ctx.Done():
if err != nil {
return nil, err
}
return nil, ErrInterfaceUpCanceled
case <-linkUpTimeout:
attempt++
l.Error().
Int("attempt", attempt).Msg("interface is still down after timeout")
if err != nil {
return nil, err
}
return nil, ErrInterfaceUpTimeout
}
}
}
// Address operations
// AddrList gets all addresses for a link
func (nm *NetlinkManager) AddrList(link *Link, family int) ([]netlink.Addr, error) {
nm.mu.RLock()
defer nm.mu.RUnlock()
return netlink.AddrList(link, family)
}
// AddrAdd adds an address to a link
func (nm *NetlinkManager) AddrAdd(link *Link, addr *netlink.Addr) error {
nm.mu.RLock()
defer nm.mu.RUnlock()
return netlink.AddrAdd(link, addr)
}
// AddrDel removes an address from a link
func (nm *NetlinkManager) AddrDel(link *Link, addr *netlink.Addr) error {
nm.mu.RLock()
defer nm.mu.RUnlock()
return netlink.AddrDel(link, addr)
}
// RemoveAllAddresses removes all addresses of a specific family from a link
func (nm *NetlinkManager) RemoveAllAddresses(link *Link, family int) error {
addrs, err := nm.AddrList(link, family)
if err != nil {
return fmt.Errorf("failed to get addresses: %w", err)
}
for _, addr := range addrs {
if err := nm.AddrDel(link, &addr); err != nil {
nm.logger.Warn().Err(err).Str("address", addr.IP.String()).Msg("failed to remove address")
}
}
return nil
}
// RemoveNonLinkLocalIPv6Addresses removes all non-link-local IPv6 addresses
func (nm *NetlinkManager) RemoveNonLinkLocalIPv6Addresses(link *Link) error {
addrs, err := nm.AddrList(link, AfInet6)
if err != nil {
return fmt.Errorf("failed to get IPv6 addresses: %w", err)
}
for _, addr := range addrs {
if !addr.IP.IsLinkLocalUnicast() {
if err := nm.AddrDel(link, &addr); err != nil {
nm.logger.Warn().Err(err).Str("address", addr.IP.String()).Msg("failed to remove IPv6 address")
}
}
}
return nil
}
// RouteList gets all routes
func (nm *NetlinkManager) RouteList(link *Link, family int) ([]netlink.Route, error) {
nm.mu.RLock()
defer nm.mu.RUnlock()
return netlink.RouteList(link, family)
}
// RouteAdd adds a route
func (nm *NetlinkManager) RouteAdd(route *netlink.Route) error {
nm.mu.RLock()
defer nm.mu.RUnlock()
return netlink.RouteAdd(route)
}
// RouteDel removes a route
func (nm *NetlinkManager) RouteDel(route *netlink.Route) error {
nm.mu.RLock()
defer nm.mu.RUnlock()
return netlink.RouteDel(route)
}
// RouteReplace replaces a route
func (nm *NetlinkManager) RouteReplace(route *netlink.Route) error {
nm.mu.RLock()
defer nm.mu.RUnlock()
return netlink.RouteReplace(route)
}
// ListDefaultRoutes lists the default routes for the given family
func (nm *NetlinkManager) ListDefaultRoutes(family int) ([]netlink.Route, error) {
routes, err := netlink.RouteListFiltered(
family,
&netlink.Route{Dst: nil, Table: 254},
netlink.RT_FILTER_DST|netlink.RT_FILTER_TABLE,
)
if err != nil {
nm.logger.Error().Err(err).Int("family", family).Msg("failed to list default routes")
return nil, err
}
return routes, nil
}
// HasDefaultRoute checks if a default route exists for the given family
func (nm *NetlinkManager) HasDefaultRoute(family int) bool {
routes, err := nm.ListDefaultRoutes(family)
if err != nil {
return false
}
return len(routes) > 0
}
// AddDefaultRoute adds a default route
func (nm *NetlinkManager) AddDefaultRoute(link *Link, gateway net.IP, family int) error {
var dst *net.IPNet
switch family {
case AfInet:
dst = &ipv4DefaultRoute
case AfInet6:
dst = &ipv6DefaultRoute
default:
return fmt.Errorf("unsupported address family: %d", family)
}
route := &netlink.Route{
Dst: dst,
Gw: gateway,
LinkIndex: link.Attrs().Index,
}
return nm.RouteReplace(route)
}
// RemoveDefaultRoute removes the default route for the given family
func (nm *NetlinkManager) RemoveDefaultRoute(family int) error {
routes, err := nm.RouteList(nil, family)
if err != nil {
return fmt.Errorf("failed to get routes: %w", err)
}
for _, route := range routes {
if route.Dst != nil {
if family == AfInet && route.Dst.IP.Equal(net.IPv4zero) && route.Dst.Mask.String() == "0.0.0.0/0" {
if err := nm.RouteDel(&route); err != nil {
nm.logger.Warn().Err(err).Msg("failed to remove IPv4 default route")
}
}
if family == AfInet6 && route.Dst.IP.Equal(net.IPv6zero) && route.Dst.Mask.String() == "::/0" {
if err := nm.RouteDel(&route); err != nil {
nm.logger.Warn().Err(err).Msg("failed to remove IPv6 default route")
}
}
}
}
return nil
}
func (nm *NetlinkManager) reconcileDefaultRoute(link *Link, expected map[string]net.IP, family int) error {
linkIndex := link.Attrs().Index
added := 0
toRemove := make([]*netlink.Route, 0)
defaultRoutes, err := nm.ListDefaultRoutes(family)
if err != nil {
return fmt.Errorf("failed to get default routes: %w", err)
}
// check existing default routes
for _, defaultRoute := range defaultRoutes {
// only check the default routes for the current link
// TODO: we should also check others later
if defaultRoute.LinkIndex != linkIndex {
continue
}
key := defaultRoute.Gw.String()
if _, ok := expected[key]; !ok {
toRemove = append(toRemove, &defaultRoute)
continue
}
nm.logger.Warn().Str("gateway", key).Msg("keeping default route")
delete(expected, key)
}
// remove remaining default routes
for _, defaultRoute := range toRemove {
nm.logger.Warn().Str("gateway", defaultRoute.Gw.String()).Msg("removing default route")
if err := nm.RouteDel(defaultRoute); err != nil {
nm.logger.Warn().Err(err).Msg("failed to remove default route")
}
}
// add remaining expected default routes
for _, gateway := range expected {
nm.logger.Warn().Str("gateway", gateway.String()).Msg("adding default route")
route := &netlink.Route{
Dst: &ipv4DefaultRoute,
Gw: gateway,
LinkIndex: linkIndex,
}
if family == AfInet6 {
route.Dst = &ipv6DefaultRoute
}
if err := nm.RouteAdd(route); err != nil {
nm.logger.Warn().Err(err).Interface("route", route).Msg("failed to add default route")
}
added++
}
nm.logger.Info().
Int("added", added).
Int("removed", len(toRemove)).
Msg("default routes reconciled")
return nil
}
// ReconcileLink reconciles the addresses and routes of a link
func (nm *NetlinkManager) ReconcileLink(link *Link, expected []types.IPAddress, family int) error {
toAdd := make([]*types.IPAddress, 0)
toRemove := make([]*netlink.Addr, 0)
toUpdate := make([]*types.IPAddress, 0)
expectedAddrs := make(map[string]*types.IPAddress)
expectedGateways := make(map[string]net.IP)
mtu := link.Attrs().MTU
expectedMTU := mtu
// add all expected addresses to the map
for _, addr := range expected {
expectedAddrs[addr.String()] = &addr
if addr.Gateway != nil {
expectedGateways[addr.String()] = addr.Gateway
}
if addr.MTU != 0 {
mtu = addr.MTU
}
}
if expectedMTU != mtu {
if err := link.SetMTU(expectedMTU); err != nil {
nm.logger.Warn().Err(err).Int("expected_mtu", expectedMTU).Int("mtu", mtu).Msg("failed to set MTU")
}
}
addrs, err := nm.AddrList(link, family)
if err != nil {
return fmt.Errorf("failed to get addresses: %w", err)
}
// check existing addresses
for _, addr := range addrs {
// skip the link-local address
if addr.IP.IsLinkLocalUnicast() {
continue
}
expectedAddr, ok := expectedAddrs[addr.IPNet.String()]
if !ok {
toRemove = append(toRemove, &addr)
continue
}
// if it's not fully equal, we need to update it
if !expectedAddr.Compare(addr) {
toUpdate = append(toUpdate, expectedAddr)
continue
}
// remove it from expected addresses
delete(expectedAddrs, addr.IPNet.String())
}
// add remaining expected addresses
for _, addr := range expectedAddrs {
toAdd = append(toAdd, addr)
}
for _, addr := range toUpdate {
netlinkAddr := addr.NetlinkAddr()
if err := nm.AddrDel(link, &netlinkAddr); err != nil {
nm.logger.Warn().Err(err).Str("address", addr.Address.String()).Msg("failed to update address")
}
// we'll add it again later
toAdd = append(toAdd, addr)
}
for _, addr := range toAdd {
netlinkAddr := addr.NetlinkAddr()
if err := nm.AddrAdd(link, &netlinkAddr); err != nil {
nm.logger.Warn().Err(err).Str("address", addr.Address.String()).Msg("failed to add address")
}
}
for _, netlinkAddr := range toRemove {
if err := nm.AddrDel(link, netlinkAddr); err != nil {
nm.logger.Warn().Err(err).Str("address", netlinkAddr.IP.String()).Msg("failed to remove address")
}
}
for _, addr := range toAdd {
netlinkAddr := addr.NetlinkAddr()
if err := nm.AddrAdd(link, &netlinkAddr); err != nil {
nm.logger.Warn().Err(err).Str("address", addr.Address.String()).Msg("failed to add address")
}
}
actualToAdd := len(toAdd) - len(toUpdate)
if len(toAdd) > 0 || len(toUpdate) > 0 || len(toRemove) > 0 {
nm.logger.Info().
Int("added", actualToAdd).
Int("updated", len(toUpdate)).
Int("removed", len(toRemove)).
Msg("addresses reconciled")
}
if err := nm.reconcileDefaultRoute(link, expectedGateways, family); err != nil {
nm.logger.Warn().Err(err).Msg("failed to reconcile default route")
}
return nil
}

View File

@ -1,164 +0,0 @@
// Package link provides a wrapper around netlink.Link and provides a singleton netlink manager.
package link
import (
"errors"
"fmt"
"net"
"github.com/jetkvm/kvm/internal/sync"
"github.com/vishvananda/netlink"
)
var (
ipv4DefaultRoute = net.IPNet{
IP: net.IPv4zero,
Mask: net.CIDRMask(0, 0),
}
ipv6DefaultRoute = net.IPNet{
IP: net.IPv6zero,
Mask: net.CIDRMask(0, 0),
}
// Singleton instance
netlinkManagerInstance *NetlinkManager
netlinkManagerOnce sync.Once
// ErrInterfaceUpTimeout is the error returned when the interface does not come up within the timeout
ErrInterfaceUpTimeout = errors.New("timeout after waiting for an interface to come up")
// ErrInterfaceUpCanceled is the error returned when the interface does not come up due to context cancellation
ErrInterfaceUpCanceled = errors.New("context canceled while waiting for an interface to come up")
)
// Link is a wrapper around netlink.Link
type Link struct {
netlink.Link
mu sync.Mutex
}
// All lock actions should be done in external functions
// and the internal functions should not be called directly
func (l *Link) refresh() error {
linkName := l.ifName()
link, err := netlink.LinkByName(linkName)
if err != nil {
return err
}
if link == nil {
return fmt.Errorf("link not found: %s", linkName)
}
l.Link = link
return nil
}
func (l *Link) attrs() *netlink.LinkAttrs {
return l.Link.Attrs()
}
func (l *Link) ifName() string {
attrs := l.attrs()
if attrs.Name == "" {
return ""
}
return attrs.Name
}
// Refresh refreshes the link
func (l *Link) Refresh() error {
l.mu.Lock()
defer l.mu.Unlock()
return l.refresh()
}
// Attrs returns the attributes of the link
func (l *Link) Attrs() *netlink.LinkAttrs {
l.mu.Lock()
defer l.mu.Unlock()
return l.attrs()
}
// Interface returns the interface of the link
func (l *Link) Interface() *net.Interface {
l.mu.Lock()
defer l.mu.Unlock()
ifname := l.ifName()
if ifname == "" {
return nil
}
iface, err := net.InterfaceByName(ifname)
if err != nil {
return nil
}
return iface
}
// HardwareAddr returns the hardware address of the link
func (l *Link) HardwareAddr() net.HardwareAddr {
l.mu.Lock()
defer l.mu.Unlock()
attrs := l.attrs()
if attrs.HardwareAddr == nil {
return nil
}
return attrs.HardwareAddr
}
// AddrList returns the addresses of the link
func (l *Link) AddrList(family int) ([]netlink.Addr, error) {
l.mu.Lock()
defer l.mu.Unlock()
return netlink.AddrList(l.Link, family)
}
func (l *Link) SetMTU(mtu int) error {
l.mu.Lock()
defer l.mu.Unlock()
return netlink.LinkSetMTU(l.Link, mtu)
}
// HasGlobalUnicastAddress returns true if the link has a global unicast address
func (l *Link) HasGlobalUnicastAddress() bool {
addrs, err := l.AddrList(AfUnspec)
if err != nil {
return false
}
for _, addr := range addrs {
if addr.IP.IsGlobalUnicast() {
return true
}
}
return false
}
// IsSame checks if the link is the same as another link
func (l *Link) IsSame(other *Link) bool {
if l == nil || other == nil {
return false
}
a := l.Attrs()
b := other.Attrs()
if a.OperState != b.OperState {
return false
}
if a.Index != b.Index {
return false
}
if a.MTU != b.MTU {
return false
}
if a.HardwareAddr.String() != b.HardwareAddr.String() {
return false
}
return true
}

View File

@ -1,52 +0,0 @@
package link
import (
"fmt"
"os"
"path"
"strconv"
"strings"
)
func (nm *NetlinkManager) setSysctlValues(ifaceName string, values map[string]int) error {
for name, value := range values {
name = fmt.Sprintf(name, ifaceName)
name = strings.ReplaceAll(name, ".", "/")
if err := os.WriteFile(path.Join(sysctlBase, name), []byte(strconv.Itoa(value)), sysctlFileMode); err != nil {
return fmt.Errorf("failed to set sysctl %s=%d: %w", name, value, err)
}
}
return nil
}
// EnableIPv6 enables IPv6 on the interface
func (nm *NetlinkManager) EnableIPv6(ifaceName string) error {
return nm.setSysctlValues(ifaceName, map[string]int{
"net.ipv6.conf.%s.disable_ipv6": 0,
"net.ipv6.conf.%s.accept_ra": 2,
})
}
// DisableIPv6 disables IPv6 on the interface
func (nm *NetlinkManager) DisableIPv6(ifaceName string) error {
return nm.setSysctlValues(ifaceName, map[string]int{
"net.ipv6.conf.%s.disable_ipv6": 1,
})
}
// EnableIPv6SLAAC enables IPv6 SLAAC on the interface
func (nm *NetlinkManager) EnableIPv6SLAAC(ifaceName string) error {
return nm.setSysctlValues(ifaceName, map[string]int{
"net.ipv6.conf.%s.disable_ipv6": 0,
"net.ipv6.conf.%s.accept_ra": 2,
})
}
// EnableIPv6LinkLocal enables IPv6 link-local only on the interface
func (nm *NetlinkManager) EnableIPv6LinkLocal(ifaceName string) error {
return nm.setSysctlValues(ifaceName, map[string]int{
"net.ipv6.conf.%s.disable_ipv6": 0,
"net.ipv6.conf.%s.accept_ra": 0,
})
}

View File

@ -1,13 +0,0 @@
package link
import (
"net"
)
// IPv4Address represents an IPv4 address and its gateway
type IPv4Address struct {
Address net.IPNet
Gateway net.IP
Secondary bool
Permanent bool
}

View File

@ -1,87 +0,0 @@
package link
import (
"fmt"
"net"
"strings"
)
// ParseIPv4Netmask parses an IPv4 netmask string and returns the IPNet
func ParseIPv4Netmask(address, netmask string) (*net.IPNet, error) {
if strings.Contains(address, "/") {
_, ipNet, err := net.ParseCIDR(address)
if err != nil {
return nil, fmt.Errorf("invalid IPv4 address: %s", address)
}
return ipNet, nil
}
ip := net.ParseIP(address)
if ip == nil {
return nil, fmt.Errorf("invalid IPv4 address: %s", address)
}
if ip.To4() == nil {
return nil, fmt.Errorf("not an IPv4 address: %s", address)
}
mask := net.ParseIP(netmask)
if mask == nil {
return nil, fmt.Errorf("invalid IPv4 netmask: %s", netmask)
}
if mask.To4() == nil {
return nil, fmt.Errorf("not an IPv4 netmask: %s", netmask)
}
return &net.IPNet{
IP: ip,
Mask: net.IPv4Mask(mask[12], mask[13], mask[14], mask[15]),
}, nil
}
// ParseIPv6Prefix parses an IPv6 address and prefix length
func ParseIPv6Prefix(address string, prefixLength int) (*net.IPNet, error) {
if strings.Contains(address, "/") {
_, ipNet, err := net.ParseCIDR(address)
if err != nil {
return nil, fmt.Errorf("invalid IPv6 address: %s", address)
}
return ipNet, nil
}
ip := net.ParseIP(address)
if ip == nil {
return nil, fmt.Errorf("invalid IPv6 address: %s", address)
}
if ip.To16() == nil || ip.To4() != nil {
return nil, fmt.Errorf("not an IPv6 address: %s", address)
}
if prefixLength < 0 || prefixLength > 128 {
return nil, fmt.Errorf("invalid IPv6 prefix length: %d (must be 0-128)", prefixLength)
}
return &net.IPNet{
IP: ip,
Mask: net.CIDRMask(prefixLength, 128),
}, nil
}
// ValidateIPAddress validates an IP address
func ValidateIPAddress(address string, isIPv6 bool) error {
ip := net.ParseIP(address)
if ip == nil {
return fmt.Errorf("invalid IP address: %s", address)
}
if isIPv6 {
if ip.To16() == nil || ip.To4() != nil {
return fmt.Errorf("not an IPv6 address: %s", address)
}
} else {
if ip.To4() == nil {
return fmt.Errorf("not an IPv4 address: %s", address)
}
}
return nil
}

View File

@ -1,260 +0,0 @@
// Package nmlite provides a lightweight network management system.
// It supports multiple network interfaces with static and DHCP configuration,
// IPv4/IPv6 support, and proper separation of concerns.
package nmlite
import (
"context"
"fmt"
"github.com/jetkvm/kvm/internal/sync"
"github.com/jetkvm/kvm/internal/logging"
"github.com/jetkvm/kvm/internal/network/types"
"github.com/jetkvm/kvm/pkg/nmlite/jetdhcpc"
"github.com/jetkvm/kvm/pkg/nmlite/link"
"github.com/rs/zerolog"
)
// NetworkManager manages multiple network interfaces
type NetworkManager struct {
interfaces map[string]*InterfaceManager
mu sync.RWMutex
logger *zerolog.Logger
ctx context.Context
cancel context.CancelFunc
resolvConf *ResolvConfManager
// Callback functions for state changes
onInterfaceStateChange func(iface string, state types.InterfaceState)
onConfigChange func(iface string, config *types.NetworkConfig)
onDHCPLeaseChange func(iface string, lease *types.DHCPLease)
}
// NewNetworkManager creates a new network manager
func NewNetworkManager(ctx context.Context, logger *zerolog.Logger) *NetworkManager {
if logger == nil {
logger = logging.GetSubsystemLogger("nm")
}
// Initialize the NetlinkManager singleton
link.InitializeNetlinkManager(logger)
ctx, cancel := context.WithCancel(ctx)
return &NetworkManager{
interfaces: make(map[string]*InterfaceManager),
logger: logger,
ctx: ctx,
cancel: cancel,
resolvConf: NewResolvConfManager(logger),
}
}
// SetHostname sets the hostname and domain for the network manager
func (nm *NetworkManager) SetHostname(hostname string, domain string) error {
return nm.resolvConf.SetHostname(hostname, domain)
}
// Domain returns the effective domain for the network manager
func (nm *NetworkManager) Domain() string {
return nm.resolvConf.Domain()
}
// AddInterface adds a new network interface to be managed
func (nm *NetworkManager) AddInterface(iface string, config *types.NetworkConfig) error {
nm.mu.Lock()
defer nm.mu.Unlock()
if _, exists := nm.interfaces[iface]; exists {
return fmt.Errorf("interface %s already managed", iface)
}
im, err := NewInterfaceManager(nm.ctx, iface, config, nm.logger)
if err != nil {
return fmt.Errorf("failed to create interface manager for %s: %w", iface, err)
}
// Set up callbacks
im.SetOnStateChange(func(state types.InterfaceState) {
if nm.onInterfaceStateChange != nil {
state.Hostname = nm.Hostname()
nm.onInterfaceStateChange(iface, state)
}
})
im.SetOnConfigChange(func(config *types.NetworkConfig) {
if nm.onConfigChange != nil {
nm.onConfigChange(iface, config)
}
})
im.SetOnDHCPLeaseChange(func(lease *types.DHCPLease) {
if nm.onDHCPLeaseChange != nil {
nm.onDHCPLeaseChange(iface, lease)
}
})
im.SetOnResolvConfChange(func(family int, resolvConf *types.InterfaceResolvConf) error {
return nm.resolvConf.SetInterfaceConfig(iface, family, *resolvConf)
})
nm.interfaces[iface] = im
// Start monitoring the interface
if err := im.Start(); err != nil {
delete(nm.interfaces, iface)
return fmt.Errorf("failed to start interface manager for %s: %w", iface, err)
}
nm.logger.Info().Str("interface", iface).Msg("added interface to network manager")
return nil
}
// RemoveInterface removes a network interface from management
func (nm *NetworkManager) RemoveInterface(iface string) error {
nm.mu.Lock()
defer nm.mu.Unlock()
im, exists := nm.interfaces[iface]
if !exists {
return fmt.Errorf("interface %s not managed", iface)
}
if err := im.Stop(); err != nil {
nm.logger.Error().Err(err).Str("interface", iface).Msg("failed to stop interface manager")
}
delete(nm.interfaces, iface)
nm.logger.Info().Str("interface", iface).Msg("removed interface from network manager")
return nil
}
// GetInterface returns the interface manager for a specific interface
func (nm *NetworkManager) GetInterface(iface string) (*InterfaceManager, error) {
nm.mu.RLock()
defer nm.mu.RUnlock()
im, exists := nm.interfaces[iface]
if !exists {
return nil, fmt.Errorf("interface %s not managed", iface)
}
return im, nil
}
// ListInterfaces returns a list of all managed interfaces
func (nm *NetworkManager) ListInterfaces() []string {
nm.mu.RLock()
defer nm.mu.RUnlock()
interfaces := make([]string, 0, len(nm.interfaces))
for iface := range nm.interfaces {
interfaces = append(interfaces, iface)
}
return interfaces
}
// GetInterfaceState returns the current state of a specific interface
func (nm *NetworkManager) GetInterfaceState(iface string) (*types.InterfaceState, error) {
im, err := nm.GetInterface(iface)
if err != nil {
return nil, err
}
state := im.GetState()
state.Hostname = nm.Hostname()
return state, nil
}
// GetInterfaceConfig returns the current configuration of a specific interface
func (nm *NetworkManager) GetInterfaceConfig(iface string) (*types.NetworkConfig, error) {
im, err := nm.GetInterface(iface)
if err != nil {
return nil, err
}
return im.GetConfig(), nil
}
// SetInterfaceConfig updates the configuration of a specific interface
func (nm *NetworkManager) SetInterfaceConfig(iface string, config *types.NetworkConfig) error {
im, err := nm.GetInterface(iface)
if err != nil {
return err
}
return im.SetConfig(config)
}
// RenewDHCPLease renews the DHCP lease for a specific interface
func (nm *NetworkManager) RenewDHCPLease(iface string) error {
im, err := nm.GetInterface(iface)
if err != nil {
return err
}
return im.RenewDHCPLease()
}
// SetOnInterfaceStateChange sets the callback for interface state changes
func (nm *NetworkManager) SetOnInterfaceStateChange(callback func(iface string, state types.InterfaceState)) {
nm.onInterfaceStateChange = callback
}
// SetOnConfigChange sets the callback for configuration changes
func (nm *NetworkManager) SetOnConfigChange(callback func(iface string, config *types.NetworkConfig)) {
nm.onConfigChange = callback
}
// SetOnDHCPLeaseChange sets the callback for DHCP lease changes
func (nm *NetworkManager) SetOnDHCPLeaseChange(callback func(iface string, lease *types.DHCPLease)) {
nm.onDHCPLeaseChange = callback
}
func (nm *NetworkManager) shouldKillLegacyDHCPClients() bool {
nm.mu.RLock()
defer nm.mu.RUnlock()
// TODO: remove it when we need to support multiple interfaces
for _, im := range nm.interfaces {
if im.dhcpClient.clientType != "udhcpc" {
return true
}
if im.config.IPv4Mode.String != "dhcp" {
return true
}
}
return false
}
// CleanUpLegacyDHCPClients cleans up legacy DHCP clients
func (nm *NetworkManager) CleanUpLegacyDHCPClients() error {
shouldKill := nm.shouldKillLegacyDHCPClients()
if shouldKill {
return jetdhcpc.KillUdhcpC(nm.logger)
}
return nil
}
// Stop stops the network manager and all managed interfaces
func (nm *NetworkManager) Stop() error {
nm.mu.Lock()
defer nm.mu.Unlock()
var lastErr error
for iface, im := range nm.interfaces {
if err := im.Stop(); err != nil {
nm.logger.Error().Err(err).Str("interface", iface).Msg("failed to stop interface manager")
lastErr = err
}
}
nm.cancel()
nm.logger.Info().Msg("network manager stopped")
return lastErr
}

View File

@ -1,7 +0,0 @@
package nmlite
import "github.com/jetkvm/kvm/pkg/nmlite/link"
func getNetlinkManager() *link.NetlinkManager {
return link.GetNetlinkManager()
}

View File

@ -1,209 +0,0 @@
package nmlite
import (
"bytes"
"fmt"
"html/template"
"os"
"strings"
"github.com/jetkvm/kvm/internal/network/types"
"github.com/jetkvm/kvm/internal/sync"
"github.com/jetkvm/kvm/pkg/nmlite/link"
"github.com/rs/zerolog"
)
const (
resolvConfPath = "/etc/resolv.conf"
resolvConfFileMode = 0644
resolvConfTemplate = `# the resolv.conf file is managed by JetKVM
# DO NOT EDIT THIS FILE BY HAND -- YOUR CHANGES WILL BE OVERWRITTEN
{{ if .searchList }}
search {{ join .searchList " " }}
{{- end -}}
{{ if .domain }}
domain {{ .domain }}
{{- end -}}
{{ range $ns, $comment := .nameservers }}
nameserver {{ printf "%s" $ns }} # {{ join $comment ", " }}
{{- end }}
`
)
var (
tplFuncMap = template.FuncMap{
"join": strings.Join,
}
)
// ResolvConfManager manages the resolv.conf file
type ResolvConfManager struct {
logger *zerolog.Logger
mu sync.Mutex
conf *types.ResolvConf
hostname string
domain string
}
// NewResolvConfManager creates a new resolv.conf manager
func NewResolvConfManager(logger *zerolog.Logger) *ResolvConfManager {
if logger == nil {
// Create a no-op logger if none provided
logger = &zerolog.Logger{}
}
return &ResolvConfManager{
logger: logger,
mu: sync.Mutex{},
conf: &types.ResolvConf{
ConfigIPv4: make(map[string]types.InterfaceResolvConf),
ConfigIPv6: make(map[string]types.InterfaceResolvConf),
},
}
}
// SetInterfaceConfig sets the resolv.conf configuration for a specific interface
func (rcm *ResolvConfManager) SetInterfaceConfig(iface string, family int, config types.InterfaceResolvConf) error {
// DO NOT USE defer HERE, rcm.update() also locks the mutex
rcm.mu.Lock()
switch family {
case link.AfInet:
rcm.conf.ConfigIPv4[iface] = config
case link.AfInet6:
rcm.conf.ConfigIPv6[iface] = config
default:
rcm.mu.Unlock()
return fmt.Errorf("invalid family: %d", family)
}
rcm.mu.Unlock()
if err := rcm.reconcileHostname(); err != nil {
return fmt.Errorf("failed to reconcile hostname: %w", err)
}
return rcm.update()
}
// SetConfig sets the resolv.conf configuration
func (rcm *ResolvConfManager) SetConfig(resolvConf *types.ResolvConf) error {
if resolvConf == nil {
return fmt.Errorf("resolvConf cannot be nil")
}
rcm.mu.Lock()
rcm.conf = resolvConf
defer rcm.mu.Unlock()
return rcm.update()
}
// Reconcile reconciles the resolv.conf configuration
func (rcm *ResolvConfManager) Reconcile() error {
if err := rcm.reconcileHostname(); err != nil {
return fmt.Errorf("failed to reconcile hostname: %w", err)
}
return rcm.update()
}
// Update updates the resolv.conf file
func (rcm *ResolvConfManager) update() error {
rcm.mu.Lock()
defer rcm.mu.Unlock()
rcm.logger.Debug().Msg("updating resolv.conf")
// Generate resolv.conf content
content, err := rcm.generateResolvConf(rcm.conf)
if err != nil {
return fmt.Errorf("failed to generate resolv.conf: %w", err)
}
// Check if the file is the same
if _, err := os.Stat(resolvConfPath); err == nil {
existingContent, err := os.ReadFile(resolvConfPath)
if err != nil {
rcm.logger.Warn().Err(err).Msg("failed to read existing resolv.conf")
}
if bytes.Equal(existingContent, content) {
rcm.logger.Debug().Msg("resolv.conf is the same, skipping write")
return nil
}
}
// Write to file
if err := os.WriteFile(resolvConfPath, content, resolvConfFileMode); err != nil {
return fmt.Errorf("failed to write resolv.conf: %w", err)
}
rcm.logger.Info().
Interface("config", rcm.conf).
Msg("resolv.conf updated successfully")
return nil
}
type configMap map[string][]string
func mergeConfig(nameservers *configMap, searchList *configMap, config *types.InterfaceResolvConfMap) {
localNameservers := *nameservers
localSearchList := *searchList
for ifname, iface := range *config {
comment := ifname
if iface.Source != "" {
comment += fmt.Sprintf(" (%s)", iface.Source)
}
for _, ip := range iface.NameServers {
ns := ip.String()
if _, ok := localNameservers[ns]; !ok {
localNameservers[ns] = []string{}
}
localNameservers[ns] = append(localNameservers[ns], comment)
}
for _, search := range iface.SearchList {
search = strings.Trim(search, ".")
if _, ok := localSearchList[search]; !ok {
localSearchList[search] = []string{}
}
localSearchList[search] = append(localSearchList[search], comment)
}
}
*nameservers = localNameservers
*searchList = localSearchList
}
// generateResolvConf generates resolv.conf content
func (rcm *ResolvConfManager) generateResolvConf(conf *types.ResolvConf) ([]byte, error) {
tmpl, err := template.New("resolv.conf").Funcs(tplFuncMap).Parse(resolvConfTemplate)
if err != nil {
return nil, fmt.Errorf("failed to parse template: %w", err)
}
// merge the nameservers and searchList
nameservers := configMap{}
searchList := configMap{}
mergeConfig(&nameservers, &searchList, &conf.ConfigIPv4)
mergeConfig(&nameservers, &searchList, &conf.ConfigIPv6)
flattenedSearchList := []string{}
for search := range searchList {
flattenedSearchList = append(flattenedSearchList, search)
}
var buf bytes.Buffer
if err := tmpl.Execute(&buf, map[string]any{
"nameservers": nameservers,
"searchList": flattenedSearchList,
}); err != nil {
return nil, fmt.Errorf("failed to execute template: %w", err)
}
return buf.Bytes(), nil
}

View File

@ -1,106 +0,0 @@
package nmlite
import "net"
func (nm *NetworkManager) IsOnline() bool {
for _, iface := range nm.interfaces {
if iface.IsOnline() {
return true
}
}
return false
}
func (nm *NetworkManager) IsUp() bool {
for _, iface := range nm.interfaces {
if iface.IsUp() {
return true
}
}
return false
}
func (nm *NetworkManager) Hostname() string {
return nm.resolvConf.Hostname()
}
func (nm *NetworkManager) FQDN() string {
return nm.resolvConf.FQDN()
}
func (nm *NetworkManager) NTPServers() []net.IP {
servers := []net.IP{}
for _, iface := range nm.interfaces {
servers = append(servers, iface.NTPServers()...)
}
return servers
}
func (nm *NetworkManager) NTPServerStrings() []string {
servers := []string{}
for _, server := range nm.NTPServers() {
servers = append(servers, server.String())
}
return servers
}
func (nm *NetworkManager) GetIPv4Addresses() []string {
for _, iface := range nm.interfaces {
return iface.GetIPv4Addresses()
}
return []string{}
}
func (nm *NetworkManager) GetIPv4Address() string {
for _, iface := range nm.interfaces {
return iface.GetIPv4Address()
}
return ""
}
func (nm *NetworkManager) GetIPv6Address() string {
for _, iface := range nm.interfaces {
return iface.GetIPv6Address()
}
return ""
}
func (nm *NetworkManager) GetIPv6Addresses() []string {
for _, iface := range nm.interfaces {
return iface.GetIPv6Addresses()
}
return []string{}
}
func (nm *NetworkManager) GetMACAddress() string {
for _, iface := range nm.interfaces {
return iface.GetMACAddress()
}
return ""
}
func (nm *NetworkManager) IPv4Ready() bool {
for _, iface := range nm.interfaces {
return iface.IPv4Ready()
}
return false
}
func (nm *NetworkManager) IPv6Ready() bool {
for _, iface := range nm.interfaces {
return iface.IPv6Ready()
}
return false
}
func (nm *NetworkManager) IPv4String() string {
return nm.GetIPv4Address()
}
func (nm *NetworkManager) IPv6String() string {
return nm.GetIPv6Address()
}
func (nm *NetworkManager) MACString() string {
return nm.GetMACAddress()
}

View File

@ -1,184 +0,0 @@
package nmlite
import (
"fmt"
"net"
"github.com/jetkvm/kvm/internal/network/types"
"github.com/jetkvm/kvm/pkg/nmlite/link"
"github.com/rs/zerolog"
)
// StaticConfigManager manages static network configuration
type StaticConfigManager struct {
ifaceName string
logger *zerolog.Logger
}
// NewStaticConfigManager creates a new static configuration manager
func NewStaticConfigManager(ifaceName string, logger *zerolog.Logger) (*StaticConfigManager, error) {
if ifaceName == "" {
return nil, fmt.Errorf("interface name cannot be empty")
}
if logger == nil {
return nil, fmt.Errorf("logger cannot be nil")
}
return &StaticConfigManager{
ifaceName: ifaceName,
logger: logger,
}, nil
}
// ToIPv4Static applies static IPv4 configuration
func (scm *StaticConfigManager) ToIPv4Static(config *types.IPv4StaticConfig) (*types.ParsedIPConfig, error) {
if config == nil {
return nil, fmt.Errorf("config is nil")
}
// Parse IP address and netmask
ipNet, err := link.ParseIPv4Netmask(config.Address.String, config.Netmask.String)
if err != nil {
return nil, err
}
scm.logger.Info().Str("ipNet", ipNet.String()).Interface("ipc", config).Msg("parsed IPv4 address and netmask")
// Parse gateway
gateway := net.ParseIP(config.Gateway.String)
if gateway == nil {
return nil, fmt.Errorf("invalid gateway: %s", config.Gateway.String)
}
// Parse DNS servers
var dns []net.IP
for _, dnsStr := range config.DNS {
if err := link.ValidateIPAddress(dnsStr, false); err != nil {
return nil, fmt.Errorf("invalid DNS server: %w", err)
}
dns = append(dns, net.ParseIP(dnsStr))
}
address := types.IPAddress{
Family: link.AfInet,
Address: *ipNet,
Gateway: gateway,
Secondary: false,
Permanent: true,
}
return &types.ParsedIPConfig{
Addresses: []types.IPAddress{address},
Nameservers: dns,
Interface: scm.ifaceName,
}, nil
}
// ToIPv6Static applies static IPv6 configuration
func (scm *StaticConfigManager) ToIPv6Static(config *types.IPv6StaticConfig) (*types.ParsedIPConfig, error) {
if config == nil {
return nil, fmt.Errorf("config is nil")
}
// Parse IP address and prefix
ipNet, err := link.ParseIPv6Prefix(config.Prefix.String, 64) // Default to /64 if not specified
if err != nil {
return nil, err
}
// Parse gateway
gateway := net.ParseIP(config.Gateway.String)
if gateway == nil {
return nil, fmt.Errorf("invalid gateway: %s", config.Gateway.String)
}
// Parse DNS servers
var dns []net.IP
for _, dnsStr := range config.DNS {
dnsIP := net.ParseIP(dnsStr)
if dnsIP == nil {
return nil, fmt.Errorf("invalid DNS server: %s", dnsStr)
}
dns = append(dns, dnsIP)
}
address := types.IPAddress{
Family: link.AfInet6,
Address: *ipNet,
Gateway: gateway,
Secondary: false,
Permanent: true,
}
return &types.ParsedIPConfig{
Addresses: []types.IPAddress{address},
Nameservers: dns,
Interface: scm.ifaceName,
}, nil
}
// DisableIPv4 disables IPv4 on the interface
func (scm *StaticConfigManager) DisableIPv4() error {
scm.logger.Info().Msg("disabling IPv4")
netlinkMgr := getNetlinkManager()
iface, err := netlinkMgr.GetLinkByName(scm.ifaceName)
if err != nil {
return fmt.Errorf("failed to get interface: %w", err)
}
// Remove all IPv4 addresses
if err := netlinkMgr.RemoveAllAddresses(iface, link.AfInet); err != nil {
return fmt.Errorf("failed to remove IPv4 addresses: %w", err)
}
// Remove default route
if err := scm.removeIPv4DefaultRoute(); err != nil {
scm.logger.Warn().Err(err).Msg("failed to remove IPv4 default route")
}
scm.logger.Info().Msg("IPv4 disabled")
return nil
}
// DisableIPv6 disables IPv6 on the interface
func (scm *StaticConfigManager) DisableIPv6() error {
scm.logger.Info().Msg("disabling IPv6")
netlinkMgr := getNetlinkManager()
return netlinkMgr.DisableIPv6(scm.ifaceName)
}
// EnableIPv6SLAAC enables IPv6 SLAAC
func (scm *StaticConfigManager) EnableIPv6SLAAC() error {
scm.logger.Info().Msg("enabling IPv6 SLAAC")
netlinkMgr := getNetlinkManager()
return netlinkMgr.EnableIPv6SLAAC(scm.ifaceName)
}
// EnableIPv6LinkLocal enables IPv6 link-local only
func (scm *StaticConfigManager) EnableIPv6LinkLocal() error {
scm.logger.Info().Msg("enabling IPv6 link-local only")
netlinkMgr := getNetlinkManager()
if err := netlinkMgr.EnableIPv6LinkLocal(scm.ifaceName); err != nil {
return err
}
// Remove all non-link-local IPv6 addresses
link, err := netlinkMgr.GetLinkByName(scm.ifaceName)
if err != nil {
return fmt.Errorf("failed to get interface: %w", err)
}
if err := netlinkMgr.RemoveNonLinkLocalIPv6Addresses(link); err != nil {
return fmt.Errorf("failed to remove non-link-local IPv6 addresses: %w", err)
}
return netlinkMgr.EnsureInterfaceUp(link)
}
// removeIPv4DefaultRoute removes IPv4 default route
func (scm *StaticConfigManager) removeIPv4DefaultRoute() error {
netlinkMgr := getNetlinkManager()
return netlinkMgr.RemoveDefaultRoute(link.AfInet)
}

View File

@ -1,171 +0,0 @@
package udhcpc
import (
"bufio"
"encoding/json"
"fmt"
"net"
"os"
"reflect"
"strconv"
"strings"
"time"
"github.com/jetkvm/kvm/internal/network/types"
)
type Lease struct {
types.DHCPLease
// from https://udhcp.busybox.net/README.udhcpc
isEmpty map[string]bool
}
func (l *Lease) setIsEmpty(m map[string]bool) {
l.isEmpty = m
}
// IsEmpty returns true if the lease is empty for the given key.
func (l *Lease) IsEmpty(key string) bool {
return l.isEmpty[key]
}
// ToJSON returns the lease as a JSON string.
func (l *Lease) ToJSON() string {
json, err := json.Marshal(l)
if err != nil {
return ""
}
return string(json)
}
// ToDHCPLease converts a lease to a DHCP lease.
func (l *Lease) ToDHCPLease() *types.DHCPLease {
lease := &l.DHCPLease
lease.DHCPClient = "udhcpc"
return lease
}
// SetLeaseExpiry sets the lease expiry time.
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
}
// UnmarshalDHCPCLease unmarshals a lease from a string.
func UnmarshalDHCPCLease(obj *Lease, str string) error {
lease := &obj.DHCPLease
// parse the lease file as a map
data := make(map[string]string)
for line := range strings.SplitSeq(str, "\n") {
line = strings.TrimSpace(line)
// skip empty lines and comments
if line == "" || strings.HasPrefix(line, "#") {
continue
}
parts := strings.SplitN(line, "=", 2)
if len(parts) != 2 {
continue
}
key := strings.TrimSpace(parts[0])
value := strings.TrimSpace(parts[1])
data[key] = value
}
// now iterate over the lease struct and set the values
leaseType := reflect.TypeOf(lease).Elem()
leaseValue := reflect.ValueOf(lease).Elem()
valuesParsed := make(map[string]bool)
for i := 0; i < leaseType.NumField(); i++ {
field := leaseValue.Field(i)
// get the env tag
key := leaseType.Field(i).Tag.Get("env")
if key == "" {
continue
}
valuesParsed[key] = false
// get the value from the data map
value, ok := data[key]
if !ok || value == "" {
continue
}
switch field.Interface().(type) {
case string:
field.SetString(value)
case int:
val, err := strconv.Atoi(value)
if err != nil {
continue
}
field.SetInt(int64(val))
case time.Duration:
val, err := time.ParseDuration(value + "s")
if err != nil {
continue
}
field.Set(reflect.ValueOf(val))
case net.IP:
ip := net.ParseIP(value)
if ip == nil {
continue
}
field.Set(reflect.ValueOf(ip))
case []net.IP:
val := make([]net.IP, 0)
for ipStr := range strings.FieldsSeq(value) {
ip := net.ParseIP(ipStr)
if ip == nil {
continue
}
val = append(val, ip)
}
field.Set(reflect.ValueOf(val))
default:
return fmt.Errorf("unsupported field `%s` type: %s", key, field.Type().String())
}
valuesParsed[key] = true
}
obj.setIsEmpty(valuesParsed)
return nil
}

View File

@ -1,76 +0,0 @@
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 sortAndCompareStringSlices(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 sortAndCompareIPv6AddressSlices(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
}
if a[i].Flags != b[i].Flags {
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
}

View File

@ -17,7 +17,6 @@ show_help() {
echo " --skip-ui-build Skip frontend/UI build" echo " --skip-ui-build Skip frontend/UI build"
echo " --skip-native-build Skip native build" echo " --skip-native-build Skip native build"
echo " --disable-docker Disable docker build" echo " --disable-docker Disable docker build"
echo " --enable-sync-trace Enable sync trace (do not use in release builds)"
echo " -i, --install Build for release and install the app" echo " -i, --install Build for release and install the app"
echo " --help Display this help message" echo " --help Display this help message"
echo echo
@ -33,7 +32,6 @@ REMOTE_PATH="/userdata/jetkvm/bin"
SKIP_UI_BUILD=false SKIP_UI_BUILD=false
SKIP_UI_BUILD_RELEASE=0 SKIP_UI_BUILD_RELEASE=0
SKIP_NATIVE_BUILD=0 SKIP_NATIVE_BUILD=0
ENABLE_SYNC_TRACE=0
RESET_USB_HID_DEVICE=false RESET_USB_HID_DEVICE=false
LOG_TRACE_SCOPES="${LOG_TRACE_SCOPES:-jetkvm,cloud,websocket,native,jsonrpc}" LOG_TRACE_SCOPES="${LOG_TRACE_SCOPES:-jetkvm,cloud,websocket,native,jsonrpc}"
RUN_GO_TESTS=false RUN_GO_TESTS=false
@ -66,11 +64,6 @@ while [[ $# -gt 0 ]]; do
RESET_USB_HID_DEVICE=true RESET_USB_HID_DEVICE=true
shift shift
;; ;;
--enable-sync-trace)
ENABLE_SYNC_TRACE=1
LOG_TRACE_SCOPES="${LOG_TRACE_SCOPES},synctrace"
shift
;;
--disable-docker) --disable-docker)
BUILD_IN_DOCKER=false BUILD_IN_DOCKER=false
shift shift
@ -187,10 +180,7 @@ fi
if [ "$INSTALL_APP" = true ] if [ "$INSTALL_APP" = true ]
then then
msg_info "▶ Building release binary" msg_info "▶ Building release binary"
do_make build_release \ do_make build_release SKIP_NATIVE_IF_EXISTS=${SKIP_NATIVE_BUILD} SKIP_UI_BUILD=${SKIP_UI_BUILD_RELEASE}
SKIP_NATIVE_IF_EXISTS=${SKIP_NATIVE_BUILD} \
SKIP_UI_BUILD=${SKIP_UI_BUILD_RELEASE} \
ENABLE_SYNC_TRACE=${ENABLE_SYNC_TRACE}
# Copy the binary to the remote host as if we were the OTA updater. # Copy the binary to the remote host as if we were the OTA updater.
ssh "${REMOTE_USER}@${REMOTE_HOST}" "cat > /userdata/jetkvm/jetkvm_app.update" < bin/jetkvm_app ssh "${REMOTE_USER}@${REMOTE_HOST}" "cat > /userdata/jetkvm/jetkvm_app.update" < bin/jetkvm_app
@ -199,10 +189,7 @@ then
ssh "${REMOTE_USER}@${REMOTE_HOST}" "reboot" ssh "${REMOTE_USER}@${REMOTE_HOST}" "reboot"
else else
msg_info "▶ Building development binary" msg_info "▶ Building development binary"
do_make build_dev \ do_make build_dev SKIP_NATIVE_IF_EXISTS=${SKIP_NATIVE_BUILD} SKIP_UI_BUILD=${SKIP_UI_BUILD_RELEASE}
SKIP_NATIVE_IF_EXISTS=${SKIP_NATIVE_BUILD} \
SKIP_UI_BUILD=${SKIP_UI_BUILD_RELEASE} \
ENABLE_SYNC_TRACE=${ENABLE_SYNC_TRACE}
# Kill any existing instances of the application # Kill any existing instances of the application
ssh "${REMOTE_USER}@${REMOTE_HOST}" "killall jetkvm_app_debug || true" ssh "${REMOTE_USER}@${REMOTE_HOST}" "killall jetkvm_app_debug || true"

View File

@ -43,20 +43,8 @@ func initTimeSync() {
timeSync = timesync.NewTimeSync(&timesync.TimeSyncOptions{ timeSync = timesync.NewTimeSync(&timesync.TimeSyncOptions{
Logger: timesyncLogger, Logger: timesyncLogger,
NetworkConfig: config.NetworkConfig, NetworkConfig: config.NetworkConfig,
PreCheckIPv4: func() (bool, error) {
if !networkManager.IPv4Ready() {
return false, nil
}
return true, nil
},
PreCheckIPv6: func() (bool, error) {
if !networkManager.IPv6Ready() {
return false, nil
}
return true, nil
},
PreCheckFunc: func() (bool, error) { PreCheckFunc: func() (bool, error) {
if !networkManager.IsOnline() { if !networkState.IsOnline() {
return false, nil return false, nil
} }
return true, nil return true, nil

View File

@ -1,21 +0,0 @@
#!/usr/bin/env python3
import json
from pathlib import Path
messages_dir = Path(__file__).resolve().parent.parent / 'ui' / 'localization' / 'messages'
files = list(messages_dir.glob('*.json'))
for f in files:
data = json.loads(f.read_text(encoding='utf-8'))
# Keep $schema first if present
schema = None
if '$schema' in data:
schema = data.pop('$schema')
sorted_items = dict(sorted(data.items()))
if schema is not None:
out = {'$schema': schema}
out.update(sorted_items)
else:
out = sorted_items
f.write_text(json.dumps(out, ensure_ascii=False, indent=4) + '\n', encoding='utf-8')
print(f'Processed {len(files)} files in {messages_dir}')

View File

@ -107,10 +107,10 @@
"already_adopted_title": "Enheden er allerede registreret", "already_adopted_title": "Enheden er allerede registreret",
"appearance_description": "Vælg dit foretrukne farvetema", "appearance_description": "Vælg dit foretrukne farvetema",
"appearance_page_description": "Tilpas udseendet og følelsen af din JetKVM-grænseflade", "appearance_page_description": "Tilpas udseendet og følelsen af din JetKVM-grænseflade",
"appearance_theme": "Tema",
"appearance_theme_dark": "Mørk", "appearance_theme_dark": "Mørk",
"appearance_theme_light": "Lys", "appearance_theme_light": "Lys",
"appearance_theme_system": "System", "appearance_theme_system": "System",
"appearance_theme": "Tema",
"appearance_title": "Udseende", "appearance_title": "Udseende",
"attach": "Vedhæft", "attach": "Vedhæft",
"atx_power_control_get_state_error": "Kunne ikke hente ATX-strømtilstand: {error}", "atx_power_control_get_state_error": "Kunne ikke hente ATX-strømtilstand: {error}",
@ -121,85 +121,85 @@
"atx_power_control_reset_button": "Nulstil", "atx_power_control_reset_button": "Nulstil",
"atx_power_control_send_action_error": "Kunne ikke sende ATX-strømfunktion {action} : {error}", "atx_power_control_send_action_error": "Kunne ikke sende ATX-strømfunktion {action} : {error}",
"atx_power_control_short_power_button": "Kort tryk", "atx_power_control_short_power_button": "Kort tryk",
"auth_authentication_mode": "Vælg venligst en godkendelsestilstand",
"auth_authentication_mode_error": "Der opstod en fejl under indstilling af godkendelsestilstanden", "auth_authentication_mode_error": "Der opstod en fejl under indstilling af godkendelsestilstanden",
"auth_authentication_mode_invalid": "Ugyldig godkendelsestilstand", "auth_authentication_mode_invalid": "Ugyldig godkendelsestilstand",
"auth_connect_to_cloud": "Tilslut din JetKVM til skyen", "auth_authentication_mode": "Vælg venligst en godkendelsestilstand",
"auth_connect_to_cloud_action": "Log ind og tilslut enhed", "auth_connect_to_cloud_action": "Log ind og tilslut enhed",
"auth_connect_to_cloud_description": "Lås op for fjernadgang og avancerede funktioner på din enhed", "auth_connect_to_cloud_description": "Lås op for fjernadgang og avancerede funktioner på din enhed",
"auth_connect_to_cloud": "Tilslut din JetKVM til skyen",
"auth_header_cta_already_have_account": "Har du allerede en konto?", "auth_header_cta_already_have_account": "Har du allerede en konto?",
"auth_header_cta_dont_have_account": "Har du ikke en konto?", "auth_header_cta_dont_have_account": "Har du ikke en konto?",
"auth_header_cta_new_to_jetkvm": "Ny bruger af JetKVM?", "auth_header_cta_new_to_jetkvm": "Ny bruger af JetKVM?",
"auth_login": "Log ind på din JetKVM-konto",
"auth_login_action": "Log ind", "auth_login_action": "Log ind",
"auth_login_description": "Log ind for at få adgang til og administrere dine enheder sikkert", "auth_login_description": "Log ind for at få adgang til og administrere dine enheder sikkert",
"auth_mode_local": "Lokal godkendelsesmetode", "auth_login": "Log ind på din JetKVM-konto",
"auth_mode_local_change_later": "Du kan altid ændre din godkendelsesmetode senere i indstillingerne.", "auth_mode_local_change_later": "Du kan altid ændre din godkendelsesmetode senere i indstillingerne.",
"auth_mode_local_description": "Vælg, hvordan du vil sikre din JetKVM-enhed lokalt.", "auth_mode_local_description": "Vælg, hvordan du vil sikre din JetKVM-enhed lokalt.",
"auth_mode_local_no_password": "Ingen adgangskode",
"auth_mode_local_no_password_description": "Hurtig adgang uden adgangskodegodkendelse.", "auth_mode_local_no_password_description": "Hurtig adgang uden adgangskodegodkendelse.",
"auth_mode_local_password": "Adgangskode", "auth_mode_local_no_password": "Ingen adgangskode",
"auth_mode_local_password_confirm_description": "Bekræft din adgangskode", "auth_mode_local_password_confirm_description": "Bekræft din adgangskode",
"auth_mode_local_password_confirm_label": "Bekræft adgangskode", "auth_mode_local_password_confirm_label": "Bekræft adgangskode",
"auth_mode_local_password_description": "Sikr din enhed med en adgangskode for ekstra beskyttelse.", "auth_mode_local_password_description": "Sikr din enhed med en adgangskode for ekstra beskyttelse.",
"auth_mode_local_password_do_not_match": "Adgangskoderne stemmer ikke overens", "auth_mode_local_password_do_not_match": "Adgangskoderne stemmer ikke overens",
"auth_mode_local_password_failed_set": "Kunne ikke angive adgangskode: {error}", "auth_mode_local_password_failed_set": "Kunne ikke angive adgangskode: {error}",
"auth_mode_local_password_note": "Denne adgangskode vil blive brugt til at sikre dine enhedsdata og beskytte mod uautoriseret adgang.",
"auth_mode_local_password_note_local": "Alle data forbliver på din lokale enhed.", "auth_mode_local_password_note_local": "Alle data forbliver på din lokale enhed.",
"auth_mode_local_password_set": "Indstil en adgangskode", "auth_mode_local_password_note": "Denne adgangskode vil blive brugt til at sikre dine enhedsdata og beskytte mod uautoriseret adgang.",
"auth_mode_local_password_set_button": "Indstil adgangskode", "auth_mode_local_password_set_button": "Indstil adgangskode",
"auth_mode_local_password_set_description": "Opret en stærk adgangskode for at sikre din JetKVM-enhed lokalt.", "auth_mode_local_password_set_description": "Opret en stærk adgangskode for at sikre din JetKVM-enhed lokalt.",
"auth_mode_local_password_set_label": "Indtast en adgangskode", "auth_mode_local_password_set_label": "Indtast en adgangskode",
"auth_mode_local_password_set": "Indstil en adgangskode",
"auth_mode_local_password": "Adgangskode",
"auth_mode_local": "Lokal godkendelsesmetode",
"auth_signup_connect_to_cloud_action": "Tilmeld og tilslut enhed", "auth_signup_connect_to_cloud_action": "Tilmeld og tilslut enhed",
"auth_signup_create_account": "Opret din JetKVM-konto",
"auth_signup_create_account_action": "Opret konto", "auth_signup_create_account_action": "Opret konto",
"auth_signup_create_account_description": "Opret din konto, og begynd nemt at administrere dine enheder.", "auth_signup_create_account_description": "Opret din konto, og begynd nemt at administrere dine enheder.",
"back": "Tilbage", "auth_signup_create_account": "Opret din JetKVM-konto",
"back_to_devices": "Tilbage til Enheder", "back_to_devices": "Tilbage til Enheder",
"back": "Tilbage",
"cancel": "Ophæve", "cancel": "Ophæve",
"close": "Tæt", "close": "Tæt",
"cloud_kvms": "Cloud KVM'er",
"cloud_kvms_description": "Administrer dine cloud-KVM'er, og opret forbindelse til dem sikkert.", "cloud_kvms_description": "Administrer dine cloud-KVM'er, og opret forbindelse til dem sikkert.",
"cloud_kvms_no_devices": "Ingen enheder fundet",
"cloud_kvms_no_devices_description": "Du har endnu ingen enheder med aktiveret JetKVM Cloud.", "cloud_kvms_no_devices_description": "Du har endnu ingen enheder med aktiveret JetKVM Cloud.",
"cloud_kvms_no_devices": "Ingen enheder fundet",
"cloud_kvms": "Cloud KVM'er",
"confirm": "Bekræfte", "confirm": "Bekræfte",
"connect_to_kvm": "Opret forbindelse til KVM", "connect_to_kvm": "Opret forbindelse til KVM",
"connecting_to_device": "Forbinder til enhed…", "connecting_to_device": "Forbinder til enhed…",
"connection_established": "Forbindelse etableret", "connection_established": "Forbindelse etableret",
"connection_stats_badge_jitter": "Jitter",
"connection_stats_badge_jitter_buffer_avg_delay": "Jitterbuffer gennemsnitlig forsinkelse", "connection_stats_badge_jitter_buffer_avg_delay": "Jitterbuffer gennemsnitlig forsinkelse",
"connection_stats_connection": "Forbindelse", "connection_stats_badge_jitter": "Jitter",
"connection_stats_connection_description": "Forbindelsen mellem klienten og JetKVM'en.", "connection_stats_connection_description": "Forbindelsen mellem klienten og JetKVM'en.",
"connection_stats_frames_per_second": "Billeder per sekund", "connection_stats_connection": "Forbindelse",
"connection_stats_frames_per_second_description": "Antal indgående videobilleder vist pr. sekund.", "connection_stats_frames_per_second_description": "Antal indgående videobilleder vist pr. sekund.",
"connection_stats_network_stability": "Netværksstabilitet", "connection_stats_frames_per_second": "Billeder per sekund",
"connection_stats_network_stability_description": "Hvor stabil strømmen af indgående videopakker er på tværs af netværket.", "connection_stats_network_stability_description": "Hvor stabil strømmen af indgående videopakker er på tværs af netværket.",
"connection_stats_packets_lost": "Pakker mistet", "connection_stats_network_stability": "Netværksstabilitet",
"connection_stats_packets_lost_description": "Antal mistede indgående video-RTP-pakker.", "connection_stats_packets_lost_description": "Antal mistede indgående video-RTP-pakker.",
"connection_stats_playback_delay": "Afspilningsforsinkelse", "connection_stats_packets_lost": "Pakker mistet",
"connection_stats_playback_delay_description": "Forsinkelse tilføjet af jitterbufferen for at jævne afspilningen, når billeder ankommer ujævnt.", "connection_stats_playback_delay_description": "Forsinkelse tilføjet af jitterbufferen for at jævne afspilningen, når billeder ankommer ujævnt.",
"connection_stats_round_trip_time": "Rundturstid", "connection_stats_playback_delay": "Afspilningsforsinkelse",
"connection_stats_round_trip_time_description": "Rundrejsetid for det aktive ICE-kandidatpar mellem peers.", "connection_stats_round_trip_time_description": "Rundrejsetid for det aktive ICE-kandidatpar mellem peers.",
"connection_stats_round_trip_time": "Rundturstid",
"connection_stats_sidebar": "Forbindelsesstatistik", "connection_stats_sidebar": "Forbindelsesstatistik",
"connection_stats_video": "Video",
"connection_stats_video_description": "Videostreamen fra JetKVM'en til klienten.", "connection_stats_video_description": "Videostreamen fra JetKVM'en til klienten.",
"connection_stats_video": "Video",
"continue": "Fortsætte", "continue": "Fortsætte",
"creating_peer_connection": "Opretter peer-forbindelse…", "creating_peer_connection": "Opretter peer-forbindelse…",
"dc_power_control_current": "Strøm",
"dc_power_control_current_unit": "EN", "dc_power_control_current_unit": "EN",
"dc_power_control_current": "Strøm",
"dc_power_control_get_state_error": "Kunne ikke hente DC-strømtilstand: {error}", "dc_power_control_get_state_error": "Kunne ikke hente DC-strømtilstand: {error}",
"dc_power_control_power": "Magt",
"dc_power_control_power_off_button": "Sluk", "dc_power_control_power_off_button": "Sluk",
"dc_power_control_power_off_state": "Sluk", "dc_power_control_power_off_state": "Sluk",
"dc_power_control_power_on_button": "Tænd", "dc_power_control_power_on_button": "Tænd",
"dc_power_control_power_on_state": "Tænd", "dc_power_control_power_on_state": "Tænd",
"dc_power_control_power_unit": "V", "dc_power_control_power_unit": "V",
"dc_power_control_power": "Magt",
"dc_power_control_restore_last_state": "Sidste stat", "dc_power_control_restore_last_state": "Sidste stat",
"dc_power_control_restore_power_state": "Gendan strømtab", "dc_power_control_restore_power_state": "Gendan strømtab",
"dc_power_control_set_power_state_error": "Kunne ikke sende DC-strømstatus til {enabled} : {error}", "dc_power_control_set_power_state_error": "Kunne ikke sende DC-strømstatus til {enabled} : {error}",
"dc_power_control_set_restore_state_error": "Kunne ikke sende DC-strømgendannelsesstatus til {state} : {error}", "dc_power_control_set_restore_state_error": "Kunne ikke sende DC-strømgendannelsesstatus til {state} : {error}",
"dc_power_control_voltage": "Spænding",
"dc_power_control_voltage_unit": "V", "dc_power_control_voltage_unit": "V",
"dc_power_control_voltage": "Spænding",
"delete": "Slet", "delete": "Slet",
"deregister_button": "Afregistrering fra Cloud", "deregister_button": "Afregistrering fra Cloud",
"deregister_cloud_devices": "Cloud-enheder", "deregister_cloud_devices": "Cloud-enheder",
@ -226,12 +226,12 @@
"extension_popover_load_and_manage_extensions": "Indlæs og administrer dine udvidelser", "extension_popover_load_and_manage_extensions": "Indlæs og administrer dine udvidelser",
"extension_popover_set_error_notification": "Kunne ikke angive aktiv udvidelse: {error}", "extension_popover_set_error_notification": "Kunne ikke angive aktiv udvidelse: {error}",
"extension_popover_unload_extension": "Fjern udvidelse", "extension_popover_unload_extension": "Fjern udvidelse",
"extension_serial_console": "Seriel konsol",
"extension_serial_console_description": "Få adgang til din serielle konsoludvidelse", "extension_serial_console_description": "Få adgang til din serielle konsoludvidelse",
"extensions_atx_power_control": "ATX-strømstyring", "extension_serial_console": "Seriel konsol",
"extensions_atx_power_control_description": "Styr din maskines strømtilstand via ATX-strømstyring.", "extensions_atx_power_control_description": "Styr din maskines strømtilstand via ATX-strømstyring.",
"extensions_dc_power_control": "DC-strømstyring", "extensions_atx_power_control": "ATX-strømstyring",
"extensions_dc_power_control_description": "Styr din DC-strømforlænger", "extensions_dc_power_control_description": "Styr din DC-strømforlænger",
"extensions_dc_power_control": "DC-strømstyring",
"extensions_popover_extensions": "Udvidelser", "extensions_popover_extensions": "Udvidelser",
"gathering_ice_candidates": "Samler ICE-kandidater…", "gathering_ice_candidates": "Samler ICE-kandidater…",
"general_app_version": "App: {version}", "general_app_version": "App: {version}",
@ -241,8 +241,8 @@
"general_check_for_updates": "Tjek for opdateringer", "general_check_for_updates": "Tjek for opdateringer",
"general_page_description": "Konfigurer enhedsindstillinger og opdater præferencer", "general_page_description": "Konfigurer enhedsindstillinger og opdater præferencer",
"general_reboot_description": "Vil du fortsætte med at genstarte systemet?", "general_reboot_description": "Vil du fortsætte med at genstarte systemet?",
"general_reboot_device": "Genstart enhed",
"general_reboot_device_description": "Sluk og tænd for JetKVM'en", "general_reboot_device_description": "Sluk og tænd for JetKVM'en",
"general_reboot_device": "Genstart enhed",
"general_reboot_no_button": "Ingen", "general_reboot_no_button": "Ingen",
"general_reboot_title": "Genstart JetKVM", "general_reboot_title": "Genstart JetKVM",
"general_reboot_yes_button": "Ja", "general_reboot_yes_button": "Ja",
@ -295,9 +295,9 @@
"hardware_display_orientation_title": "Skærmretning", "hardware_display_orientation_title": "Skærmretning",
"hardware_display_wake_up_note": "Skærmen vågner op, når forbindelsestilstanden ændres, eller når den berøres.", "hardware_display_wake_up_note": "Skærmen vågner op, når forbindelsestilstanden ændres, eller når den berøres.",
"hardware_page_description": "Konfigurer skærmindstillinger og hardwareindstillinger for din JetKVM-enhed", "hardware_page_description": "Konfigurer skærmindstillinger og hardwareindstillinger for din JetKVM-enhed",
"hardware_time_10_minutes": "10 minutter",
"hardware_time_1_hour": "1 time", "hardware_time_1_hour": "1 time",
"hardware_time_1_minute": "1 minut", "hardware_time_1_minute": "1 minut",
"hardware_time_10_minutes": "10 minutter",
"hardware_time_30_minutes": "30 minutter", "hardware_time_30_minutes": "30 minutter",
"hardware_time_5_minutes": "5 minutter", "hardware_time_5_minutes": "5 minutter",
"hardware_time_never": "Aldrig", "hardware_time_never": "Aldrig",
@ -327,7 +327,6 @@
"invalid_password": "Ugyldig adgangskode", "invalid_password": "Ugyldig adgangskode",
"ip_address": "IP-adresse", "ip_address": "IP-adresse",
"ipv6_address_label": "Adresse", "ipv6_address_label": "Adresse",
"ipv6_gateway": "Gateway",
"ipv6_information": "IPv6-oplysninger", "ipv6_information": "IPv6-oplysninger",
"ipv6_link_local": "Link-lokal", "ipv6_link_local": "Link-lokal",
"ipv6_preferred_lifetime": "Foretrukken levetid", "ipv6_preferred_lifetime": "Foretrukken levetid",
@ -410,8 +409,8 @@
"log_in": "Log ind", "log_in": "Log ind",
"log_out": "Log ud", "log_out": "Log ud",
"logged_in_as": "Logget ind som", "logged_in_as": "Logget ind som",
"login_enter_password": "Indtast din adgangskode",
"login_enter_password_description": "Indtast din adgangskode for at få adgang til din JetKVM.", "login_enter_password_description": "Indtast din adgangskode for at få adgang til din JetKVM.",
"login_enter_password": "Indtast din adgangskode",
"login_error": "Der opstod en fejl under login", "login_error": "Der opstod en fejl under login",
"login_forgot_password": "Glemt adgangskode?", "login_forgot_password": "Glemt adgangskode?",
"login_password_label": "Adgangskode", "login_password_label": "Adgangskode",
@ -425,8 +424,8 @@
"macro_name_required": "Navn er påkrævet", "macro_name_required": "Navn er påkrævet",
"macro_name_too_long": "Navnet skal være mindre end 50 tegn", "macro_name_too_long": "Navnet skal være mindre end 50 tegn",
"macro_please_fix_validation_errors": "Ret venligst valideringsfejlene", "macro_please_fix_validation_errors": "Ret venligst valideringsfejlene",
"macro_save": "Gem makro",
"macro_save_error": "Der opstod en fejl under lagring.", "macro_save_error": "Der opstod en fejl under lagring.",
"macro_save": "Gem makro",
"macro_step_count": "{steps} / {max} trin", "macro_step_count": "{steps} / {max} trin",
"macro_step_duration_description": "Tid til at vente, før man udfører det næste trin.", "macro_step_duration_description": "Tid til at vente, før man udfører det næste trin.",
"macro_step_duration_label": "Trinvarighed", "macro_step_duration_label": "Trinvarighed",
@ -440,8 +439,8 @@
"macro_steps_description": "Taster/modifikatorer udføres i rækkefølge med en forsinkelse mellem hvert trin.", "macro_steps_description": "Taster/modifikatorer udføres i rækkefølge med en forsinkelse mellem hvert trin.",
"macro_steps_label": "Trin", "macro_steps_label": "Trin",
"macros_add_description": "Opret en ny tastaturmakro", "macros_add_description": "Opret en ny tastaturmakro",
"macros_add_new": "Tilføj ny makro",
"macros_add_new_macro": "Tilføj ny makro", "macros_add_new_macro": "Tilføj ny makro",
"macros_add_new": "Tilføj ny makro",
"macros_aria_add_new": "Tilføj ny makro", "macros_aria_add_new": "Tilføj ny makro",
"macros_aria_delete": "Slet makro {name}", "macros_aria_delete": "Slet makro {name}",
"macros_aria_duplicate": "Dupliker makro {name}", "macros_aria_duplicate": "Dupliker makro {name}",
@ -463,14 +462,14 @@
"macros_edit_button": "Redigere", "macros_edit_button": "Redigere",
"macros_edit_description": "Rediger din tastaturmakro", "macros_edit_description": "Rediger din tastaturmakro",
"macros_edit_title": "Rediger makro", "macros_edit_title": "Rediger makro",
"macros_failed_create": "Kunne ikke oprette makro",
"macros_failed_create_error": "Kunne ikke oprette makro: {error}", "macros_failed_create_error": "Kunne ikke oprette makro: {error}",
"macros_failed_delete": "Makroen kunne ikke slettes", "macros_failed_create": "Kunne ikke oprette makro",
"macros_failed_delete_error": "Kunne ikke slette makroen: {error}", "macros_failed_delete_error": "Kunne ikke slette makroen: {error}",
"macros_failed_duplicate": "Makroen kunne ikke duplikeres", "macros_failed_delete": "Makroen kunne ikke slettes",
"macros_failed_duplicate_error": "Kunne ikke duplikere makro: {error}", "macros_failed_duplicate_error": "Kunne ikke duplikere makro: {error}",
"macros_failed_reorder": "Kunne ikke omarrangere makroer", "macros_failed_duplicate": "Makroen kunne ikke duplikeres",
"macros_failed_reorder_error": "Kunne ikke omarrangere makroer: {error}", "macros_failed_reorder_error": "Kunne ikke omarrangere makroer: {error}",
"macros_failed_reorder": "Kunne ikke omarrangere makroer",
"macros_failed_update": "Makroen kunne ikke opdateres", "macros_failed_update": "Makroen kunne ikke opdateres",
"macros_failed_update_error": "Kunne ikke opdatere makroen: {error}", "macros_failed_update_error": "Kunne ikke opdatere makroen: {error}",
"macros_invalid_data": "Ugyldige makrodata", "macros_invalid_data": "Ugyldige makrodata",
@ -507,8 +506,8 @@
"mount_error_list_storage": "Fejl ved liste over lagerfiler: {error}", "mount_error_list_storage": "Fejl ved liste over lagerfiler: {error}",
"mount_error_title": "Monteringsfejl", "mount_error_title": "Monteringsfejl",
"mount_get_state_error": "Kunne ikke hente virtuel medietilstand: {error}", "mount_get_state_error": "Kunne ikke hente virtuel medietilstand: {error}",
"mount_jetkvm_storage": "JetKVM-lagerbeslag",
"mount_jetkvm_storage_description": "Monter tidligere uploadede filer fra JetKVM-lageret", "mount_jetkvm_storage_description": "Monter tidligere uploadede filer fra JetKVM-lageret",
"mount_jetkvm_storage": "JetKVM-lagerbeslag",
"mount_mode_cdrom": "CD/DVD", "mount_mode_cdrom": "CD/DVD",
"mount_mode_disk": "Disk", "mount_mode_disk": "Disk",
"mount_mounted_as": "Monteret som", "mount_mounted_as": "Monteret som",
@ -521,8 +520,8 @@
"mount_popular_images": "Populære billeder", "mount_popular_images": "Populære billeder",
"mount_streaming_from_url": "Streaming fra URL", "mount_streaming_from_url": "Streaming fra URL",
"mount_supported_formats": "Understøttede formater: ISO, IMG", "mount_supported_formats": "Understøttede formater: ISO, IMG",
"mount_unmount": "Afmonter",
"mount_unmount_error": "Kunne ikke afmontere billede: {error}", "mount_unmount_error": "Kunne ikke afmontere billede: {error}",
"mount_unmount": "Afmonter",
"mount_upload_description": "Vælg en billedfil, der skal uploades til JetKVM-lageret", "mount_upload_description": "Vælg en billedfil, der skal uploades til JetKVM-lageret",
"mount_upload_error": "Uploadfejl: {error}", "mount_upload_error": "Uploadfejl: {error}",
"mount_upload_failed_datachannel": "Kunne ikke oprette datakanal til filupload", "mount_upload_failed_datachannel": "Kunne ikke oprette datakanal til filupload",
@ -530,8 +529,8 @@
"mount_upload_successful": "Uploaden er gennemført", "mount_upload_successful": "Uploaden er gennemført",
"mount_upload_title": "Upload nyt billede", "mount_upload_title": "Upload nyt billede",
"mount_uploaded_has_been_uploaded": "{name} er blevet uploadet", "mount_uploaded_has_been_uploaded": "{name} er blevet uploadet",
"mount_uploading": "Uploader…",
"mount_uploading_with_name": "Uploader {name}", "mount_uploading_with_name": "Uploader {name}",
"mount_uploading": "Uploader…",
"mount_url_description": "Monter filer fra enhver offentlig webadresse", "mount_url_description": "Monter filer fra enhver offentlig webadresse",
"mount_url_input_label": "Billed-URL", "mount_url_input_label": "Billed-URL",
"mount_url_mount": "URL-montering", "mount_url_mount": "URL-montering",
@ -539,10 +538,10 @@
"mount_view_device_title": "Monter fra JetKVM-lager", "mount_view_device_title": "Monter fra JetKVM-lager",
"mount_view_url_description": "Indtast en URL til den billedfil, der skal monteres", "mount_view_url_description": "Indtast en URL til den billedfil, der skal monteres",
"mount_view_url_title": "Monter fra URL", "mount_view_url_title": "Monter fra URL",
"mount_virtual_media": "Virtuelle medier",
"mount_virtual_media_description": "Monter et billede for at starte fra eller installere et operativsystem.", "mount_virtual_media_description": "Monter et billede for at starte fra eller installere et operativsystem.",
"mount_virtual_media_source": "Virtuel mediekilde",
"mount_virtual_media_source_description": "Vælg hvordan du vil montere dine virtuelle medier", "mount_virtual_media_source_description": "Vælg hvordan du vil montere dine virtuelle medier",
"mount_virtual_media_source": "Virtuel mediekilde",
"mount_virtual_media": "Virtuelle medier",
"mouse_alt_finger": "Fingerberøring af en skærm", "mouse_alt_finger": "Fingerberøring af en skærm",
"mouse_alt_mouse": "Musikon", "mouse_alt_mouse": "Musikon",
"mouse_description": "Konfigurer markørens adfærd og interaktionsindstillinger for din enhed", "mouse_description": "Konfigurer markørens adfærd og interaktionsindstillinger for din enhed",
@ -559,10 +558,10 @@
"mouse_jiggler_light": "Lys - 5m", "mouse_jiggler_light": "Lys - 5m",
"mouse_jiggler_standard": "Standard - 1 m", "mouse_jiggler_standard": "Standard - 1 m",
"mouse_jiggler_title": "Jiggler", "mouse_jiggler_title": "Jiggler",
"mouse_mode_absolute": "Absolut",
"mouse_mode_absolute_description": "Mest bekvemme", "mouse_mode_absolute_description": "Mest bekvemme",
"mouse_mode_relative": "Relativ", "mouse_mode_absolute": "Absolut",
"mouse_mode_relative_description": "Mest kompatible", "mouse_mode_relative_description": "Mest kompatible",
"mouse_mode_relative": "Relativ",
"mouse_modes_description": "Vælg musens inputtilstand", "mouse_modes_description": "Vælg musens inputtilstand",
"mouse_modes_title": "Tilstande", "mouse_modes_title": "Tilstande",
"mouse_scroll_high": "Høj", "mouse_scroll_high": "Høj",
@ -575,17 +574,12 @@
"mouse_title": "Mus", "mouse_title": "Mus",
"network_custom_domain": "Brugerdefineret domæne", "network_custom_domain": "Brugerdefineret domæne",
"network_description": "Konfigurer dine netværksindstillinger", "network_description": "Konfigurer dine netværksindstillinger",
"network_dhcp_client_description": "Konfigurer hvilken DHCP-klient der skal bruges",
"network_dhcp_client_jetkvm": "JetKVM Intern",
"network_dhcp_client_title": "DHCP-klient",
"network_dhcp_information": "DHCP-oplysninger", "network_dhcp_information": "DHCP-oplysninger",
"network_dhcp_lease_renew": "Forny DHCP-lease",
"network_dhcp_lease_renew_confirm": "Forny lejekontrakt",
"network_dhcp_lease_renew_confirm_description": "Dette vil anmode om en ny IP-adresse fra din DHCP-server. Din enhed kan midlertidigt miste netværksforbindelsen under denne proces.", "network_dhcp_lease_renew_confirm_description": "Dette vil anmode om en ny IP-adresse fra din DHCP-server. Din enhed kan midlertidigt miste netværksforbindelsen under denne proces.",
"network_dhcp_lease_renew_confirm_new_a": "Hvis du modtager en ny IP-adresse", "network_dhcp_lease_renew_confirm": "Forny lejekontrakt",
"network_dhcp_lease_renew_confirm_new_b": "du skal muligvis genoprette forbindelsen ved hjælp af den nye adresse",
"network_dhcp_lease_renew_failed": "Kunne ikke forny leasing: {error}", "network_dhcp_lease_renew_failed": "Kunne ikke forny leasing: {error}",
"network_dhcp_lease_renew_success": "DHCP-lease fornyet", "network_dhcp_lease_renew_success": "DHCP-lease fornyet",
"network_dhcp_lease_renew": "Forny DHCP-lease",
"network_domain_custom": "Skik", "network_domain_custom": "Skik",
"network_domain_description": "Netværksdomænesuffiks for enheden", "network_domain_description": "Netværksdomænesuffiks for enheden",
"network_domain_dhcp_provided": "DHCP leveret", "network_domain_dhcp_provided": "DHCP leveret",
@ -594,35 +588,21 @@
"network_hostname_description": "Enhedsidentifikator på netværket. Tom for systemstandard", "network_hostname_description": "Enhedsidentifikator på netværket. Tom for systemstandard",
"network_hostname_title": "Værtsnavn", "network_hostname_title": "Værtsnavn",
"network_http_proxy_description": "Proxyserver til udgående HTTP(S)-anmodninger fra enheden. Tom, hvis ingen er til stede.", "network_http_proxy_description": "Proxyserver til udgående HTTP(S)-anmodninger fra enheden. Tom, hvis ingen er til stede.",
"network_http_proxy_invalid": "Ugyldig HTTP-proxy-URL",
"network_http_proxy_title": "HTTP-proxy", "network_http_proxy_title": "HTTP-proxy",
"network_ipv4_address": "IPv4-adresse",
"network_ipv4_dns": "IPv4 DNS",
"network_ipv4_gateway": "IPv4-gateway",
"network_ipv4_mode_description": "Konfigurer IPv4-tilstanden", "network_ipv4_mode_description": "Konfigurer IPv4-tilstanden",
"network_ipv4_mode_dhcp": "DHCP", "network_ipv4_mode_dhcp": "DHCP",
"network_ipv4_mode_static": "Statisk",
"network_ipv4_mode_title": "IPv4-tilstand", "network_ipv4_mode_title": "IPv4-tilstand",
"network_ipv4_netmask": "IPv4-netmaske",
"network_ipv6_address": "IPv6-adresse",
"network_ipv6_information": "IPv6-oplysninger", "network_ipv6_information": "IPv6-oplysninger",
"network_ipv6_mode_description": "Konfigurer IPv6-tilstanden", "network_ipv6_mode_description": "Konfigurer IPv6-tilstanden",
"network_ipv6_mode_dhcpv6": "DHCPv6",
"network_ipv6_mode_disabled": "Handicappet", "network_ipv6_mode_disabled": "Handicappet",
"network_ipv6_mode_link_local": "Kun link-lokal",
"network_ipv6_mode_slaac": "SLAAC", "network_ipv6_mode_slaac": "SLAAC",
"network_ipv6_mode_slaac_dhcpv6": "SLAAC + DHCPv6",
"network_ipv6_mode_static": "Statisk",
"network_ipv6_mode_title": "IPv6-tilstand", "network_ipv6_mode_title": "IPv6-tilstand",
"network_ipv6_netmask": "IPv6-netmaske",
"network_ipv6_no_addresses": "Ingen IPv6-adresser konfigureret", "network_ipv6_no_addresses": "Ingen IPv6-adresser konfigureret",
"network_ll_dp_all": "Alle", "network_ll_dp_all": "Alle",
"network_ll_dp_basic": "Grundlæggende", "network_ll_dp_basic": "Grundlæggende",
"network_ll_dp_description": "Styr hvilke TLV'er der sendes via Link Layer Discovery Protocol", "network_ll_dp_description": "Styr hvilke TLV'er der sendes via Link Layer Discovery Protocol",
"network_ll_dp_disabled": "Handicappet", "network_ll_dp_disabled": "Handicappet",
"network_ll_dp_title": "LLDP", "network_ll_dp_title": "LLDP",
"network_mac_address_copy_error": "Kunne ikke kopiere MAC-adressen",
"network_mac_address_copy_success": "MAC-adresse { mac } kopieret til udklipsholder",
"network_mac_address_description": "Hardware-identifikator for netværksgrænsefladen", "network_mac_address_description": "Hardware-identifikator for netværksgrænsefladen",
"network_mac_address_title": "MAC-adresse", "network_mac_address_title": "MAC-adresse",
"network_mdns_auto": "Bil", "network_mdns_auto": "Bil",
@ -632,19 +612,9 @@
"network_mdns_ipv6_only": "Kun IPv6", "network_mdns_ipv6_only": "Kun IPv6",
"network_mdns_title": "mDNS", "network_mdns_title": "mDNS",
"network_no_dhcp_lease": "Ingen DHCP-leaseoplysninger tilgængelige", "network_no_dhcp_lease": "Ingen DHCP-leaseoplysninger tilgængelige",
"network_no_information_description": "Ingen netværkskonfiguration tilgængelig",
"network_no_information_headline": "Netværksoplysninger",
"network_pending_dhcp_mode_change_description": "Gem indstillinger for at aktivere DHCP-tilstand og se leasingoplysninger",
"network_pending_dhcp_mode_change_headline": "Afventer ændring af DHCP IPv4-tilstand",
"network_save_settings": "Gem indstillinger",
"network_save_settings_apply_title": "Anvend netværksindstillinger",
"network_save_settings_confirm": "Anvend ændringer",
"network_save_settings_confirm_description": "Følgende netværksindstillinger vil blive anvendt. Disse ændringer kan kræve en genstart og forårsage en kortvarig afbrydelse.",
"network_save_settings_confirm_heading": "Konfigurationsændringer",
"network_save_settings_failed": "Kunne ikke gemme netværksindstillinger: {error}", "network_save_settings_failed": "Kunne ikke gemme netværksindstillinger: {error}",
"network_save_settings_success": "Netværksindstillinger gemt", "network_save_settings_success": "Netværksindstillinger gemt",
"network_settings_invalid_ipv4_cidr": "Ugyldig CIDR-notation for IPv4-adresse", "network_save_settings": "Gem indstillinger",
"network_settings_load_error": "Kunne ikke indlæse netværksindstillinger: {error}",
"network_time_sync_description": "Konfigurer indstillinger for tidssynkronisering", "network_time_sync_description": "Konfigurer indstillinger for tidssynkronisering",
"network_time_sync_http_only": "Kun HTTP", "network_time_sync_http_only": "Kun HTTP",
"network_time_sync_ntp_and_http": "NTP og HTTP", "network_time_sync_ntp_and_http": "NTP og HTTP",
@ -670,8 +640,8 @@
"paste_modal_failed_paste": "Kunne ikke indsætte tekst: {error}", "paste_modal_failed_paste": "Kunne ikke indsætte tekst: {error}",
"paste_modal_invalid_chars_intro": "Følgende tegn vil ikke blive indsat:", "paste_modal_invalid_chars_intro": "Følgende tegn vil ikke blive indsat:",
"paste_modal_paste_from_host": "Indsæt fra vært", "paste_modal_paste_from_host": "Indsæt fra vært",
"paste_modal_paste_text": "Indsæt tekst",
"paste_modal_paste_text_description": "Indsæt tekst fra din klient til den eksterne vært", "paste_modal_paste_text_description": "Indsæt tekst fra din klient til den eksterne vært",
"paste_modal_paste_text": "Indsæt tekst",
"paste_modal_sending_using_layout": "Sender tekst ved hjælp af tastaturlayout: {iso} - {name}", "paste_modal_sending_using_layout": "Sender tekst ved hjælp af tastaturlayout: {iso} - {name}",
"peer_connection_closed": "Lukket", "peer_connection_closed": "Lukket",
"peer_connection_closing": "Lukker", "peer_connection_closing": "Lukker",
@ -698,20 +668,20 @@
"retry": "Prøv igen", "retry": "Prøv igen",
"saving": "Gemmer…", "saving": "Gemmer…",
"search_placeholder": "Søge…", "search_placeholder": "Søge…",
"serial_console": "Seriel konsol",
"serial_console_baud_rate": "Baudhastighed", "serial_console_baud_rate": "Baudhastighed",
"serial_console_configure_description": "Konfigurer dine serielle konsolindstillinger", "serial_console_configure_description": "Konfigurer dine serielle konsolindstillinger",
"serial_console_data_bits": "Databits", "serial_console_data_bits": "Databits",
"serial_console_get_settings_error": "Kunne ikke hente indstillinger for seriel konsol: {error}", "serial_console_get_settings_error": "Kunne ikke hente indstillinger for seriel konsol: {error}",
"serial_console_open_console": "Åbn konsol", "serial_console_open_console": "Åbn konsol",
"serial_console_parity": "Paritet",
"serial_console_parity_even": "Lige paritet", "serial_console_parity_even": "Lige paritet",
"serial_console_parity_mark": "Mark Paritet", "serial_console_parity_mark": "Mark Paritet",
"serial_console_parity_none": "Ingen paritet", "serial_console_parity_none": "Ingen paritet",
"serial_console_parity_odd": "Ulige paritet", "serial_console_parity_odd": "Ulige paritet",
"serial_console_parity_space": "Rumparitet", "serial_console_parity_space": "Rumparitet",
"serial_console_parity": "Paritet",
"serial_console_set_settings_error": "Kunne ikke indstille seriel konsolindstillinger til {settings} : {error}", "serial_console_set_settings_error": "Kunne ikke indstille seriel konsolindstillinger til {settings} : {error}",
"serial_console_stop_bits": "Stopbits", "serial_console_stop_bits": "Stopbits",
"serial_console": "Seriel konsol",
"setting_remote_description": "Indstilling af fjernbetjeningsbeskrivelse", "setting_remote_description": "Indstilling af fjernbetjeningsbeskrivelse",
"setting_remote_session_description": "Indstilling af beskrivelse af fjernsession...", "setting_remote_session_description": "Indstilling af beskrivelse af fjernsession...",
"setting_up_connection_to_device": "Opretter forbindelse til enhed...", "setting_up_connection_to_device": "Opretter forbindelse til enhed...",
@ -721,8 +691,8 @@
"settings_back_to_kvm": "Tilbage til KVM", "settings_back_to_kvm": "Tilbage til KVM",
"settings_general": "Generel", "settings_general": "Generel",
"settings_hardware": "Hardware", "settings_hardware": "Hardware",
"settings_keyboard": "Tastatur",
"settings_keyboard_macros": "Tastaturmakroer", "settings_keyboard_macros": "Tastaturmakroer",
"settings_keyboard": "Tastatur",
"settings_mouse": "Mus", "settings_mouse": "Mus",
"settings_network": "Netværk", "settings_network": "Netværk",
"settings_video": "Video", "settings_video": "Video",
@ -742,7 +712,6 @@
"updates_failed_check": "Kunne ikke søge efter opdateringer: {error}", "updates_failed_check": "Kunne ikke søge efter opdateringer: {error}",
"updates_failed_get_device_version": "Kunne ikke hente enhedsversion: {error}", "updates_failed_get_device_version": "Kunne ikke hente enhedsversion: {error}",
"updating_leave_device_on": "Sluk venligst ikke din enhed…", "updating_leave_device_on": "Sluk venligst ikke din enhed…",
"usb": "USB",
"usb_config_custom": "Skik", "usb_config_custom": "Skik",
"usb_config_default": "JetKVM-standard", "usb_config_default": "JetKVM-standard",
"usb_config_dell": "Dell Multimedia Pro-tastatur", "usb_config_dell": "Dell Multimedia Pro-tastatur",
@ -789,6 +758,7 @@
"usb_state_connecting": "Forbinder", "usb_state_connecting": "Forbinder",
"usb_state_disconnected": "Afbrudt", "usb_state_disconnected": "Afbrudt",
"usb_state_low_power_mode": "Lavstrømstilstand", "usb_state_low_power_mode": "Lavstrømstilstand",
"usb": "USB",
"user_interface_language_description": "Vælg det sprog, der skal bruges i JetKVM-brugergrænsefladen", "user_interface_language_description": "Vælg det sprog, der skal bruges i JetKVM-brugergrænsefladen",
"user_interface_language_title": "Grænsefladesprog", "user_interface_language_title": "Grænsefladesprog",
"video_brightness_description": "Lysstyrkeniveau ( {value} x)", "video_brightness_description": "Lysstyrkeniveau ( {value} x)",
@ -855,7 +825,6 @@
"video_title": "Video", "video_title": "Video",
"view_details": "Se detaljer", "view_details": "Se detaljer",
"virtual_keyboard_header": "Virtuelt tastatur", "virtual_keyboard_header": "Virtuelt tastatur",
"wake_on_lan": "Vågn på LAN",
"wake_on_lan_add_device_device_name": "Enhedsnavn", "wake_on_lan_add_device_device_name": "Enhedsnavn",
"wake_on_lan_add_device_example_device_name": "Plex-medieserver", "wake_on_lan_add_device_example_device_name": "Plex-medieserver",
"wake_on_lan_add_device_mac_address": "MAC-adresse", "wake_on_lan_add_device_mac_address": "MAC-adresse",
@ -871,6 +840,7 @@
"wake_on_lan_failed_send_magic": "Kunne ikke sende Magic Packet", "wake_on_lan_failed_send_magic": "Kunne ikke sende Magic Packet",
"wake_on_lan_invalid_mac": "Ugyldig MAC-adresse", "wake_on_lan_invalid_mac": "Ugyldig MAC-adresse",
"wake_on_lan_magic_sent_success": "Magisk pakke sendt", "wake_on_lan_magic_sent_success": "Magisk pakke sendt",
"welcome_to_jetkvm": "Velkommen til JetKVM", "wake_on_lan": "Vågn på LAN",
"welcome_to_jetkvm_description": "Styr enhver computer eksternt" "welcome_to_jetkvm_description": "Styr enhver computer eksternt",
"welcome_to_jetkvm": "Velkommen til JetKVM"
} }

View File

@ -107,10 +107,10 @@
"already_adopted_title": "Gerät bereits registriert", "already_adopted_title": "Gerät bereits registriert",
"appearance_description": "Wählen Sie Ihr bevorzugtes Farbthema", "appearance_description": "Wählen Sie Ihr bevorzugtes Farbthema",
"appearance_page_description": "Passen Sie das Erscheinungsbild Ihrer JetKVM-Schnittstelle an", "appearance_page_description": "Passen Sie das Erscheinungsbild Ihrer JetKVM-Schnittstelle an",
"appearance_theme": "Thema",
"appearance_theme_dark": "Dunkel", "appearance_theme_dark": "Dunkel",
"appearance_theme_light": "Licht", "appearance_theme_light": "Licht",
"appearance_theme_system": "System", "appearance_theme_system": "System",
"appearance_theme": "Thema",
"appearance_title": "Aussehen", "appearance_title": "Aussehen",
"attach": "Befestigen", "attach": "Befestigen",
"atx_power_control_get_state_error": "ATX-Stromversorgungsstatus konnte nicht abgerufen werden: {error}", "atx_power_control_get_state_error": "ATX-Stromversorgungsstatus konnte nicht abgerufen werden: {error}",
@ -121,85 +121,85 @@
"atx_power_control_reset_button": "Zurücksetzen", "atx_power_control_reset_button": "Zurücksetzen",
"atx_power_control_send_action_error": "ATX-Stromversorgungsaktion {action} konnte nicht gesendet werden: {error}", "atx_power_control_send_action_error": "ATX-Stromversorgungsaktion {action} konnte nicht gesendet werden: {error}",
"atx_power_control_short_power_button": "Kurzes Drücken", "atx_power_control_short_power_button": "Kurzes Drücken",
"auth_authentication_mode": "Bitte wählen Sie einen Authentifizierungsmodus",
"auth_authentication_mode_error": "Beim Einstellen des Authentifizierungsmodus ist ein Fehler aufgetreten", "auth_authentication_mode_error": "Beim Einstellen des Authentifizierungsmodus ist ein Fehler aufgetreten",
"auth_authentication_mode_invalid": "Ungültiger Authentifizierungsmodus", "auth_authentication_mode_invalid": "Ungültiger Authentifizierungsmodus",
"auth_connect_to_cloud": "Verbinden Sie Ihr JetKVM mit der Cloud", "auth_authentication_mode": "Bitte wählen Sie einen Authentifizierungsmodus",
"auth_connect_to_cloud_action": "Anmelden und Gerät verbinden", "auth_connect_to_cloud_action": "Anmelden und Gerät verbinden",
"auth_connect_to_cloud_description": "Schalten Sie den Fernzugriff und erweiterte Funktionen für Ihr Gerät frei", "auth_connect_to_cloud_description": "Schalten Sie den Fernzugriff und erweiterte Funktionen für Ihr Gerät frei",
"auth_connect_to_cloud": "Verbinden Sie Ihr JetKVM mit der Cloud",
"auth_header_cta_already_have_account": "Hast du schon ein Konto?", "auth_header_cta_already_have_account": "Hast du schon ein Konto?",
"auth_header_cta_dont_have_account": "Sie haben noch kein Konto?", "auth_header_cta_dont_have_account": "Sie haben noch kein Konto?",
"auth_header_cta_new_to_jetkvm": "Neu bei JetKVM?", "auth_header_cta_new_to_jetkvm": "Neu bei JetKVM?",
"auth_login": "Melden Sie sich bei Ihrem JetKVM-Konto an",
"auth_login_action": "Einloggen", "auth_login_action": "Einloggen",
"auth_login_description": "Melden Sie sich an, um sicher auf Ihre Geräte zuzugreifen und sie zu verwalten", "auth_login_description": "Melden Sie sich an, um sicher auf Ihre Geräte zuzugreifen und sie zu verwalten",
"auth_mode_local": "Lokale Authentifizierungsmethode", "auth_login": "Melden Sie sich bei Ihrem JetKVM-Konto an",
"auth_mode_local_change_later": "Sie können Ihre Authentifizierungsmethode später jederzeit in den Einstellungen ändern.", "auth_mode_local_change_later": "Sie können Ihre Authentifizierungsmethode später jederzeit in den Einstellungen ändern.",
"auth_mode_local_description": "Wählen Sie aus, wie Sie Ihr JetKVM-Gerät lokal sichern möchten.", "auth_mode_local_description": "Wählen Sie aus, wie Sie Ihr JetKVM-Gerät lokal sichern möchten.",
"auth_mode_local_no_password": "Kein Passwort",
"auth_mode_local_no_password_description": "Schneller Zugriff ohne Passwortauthentifizierung.", "auth_mode_local_no_password_description": "Schneller Zugriff ohne Passwortauthentifizierung.",
"auth_mode_local_password": "Passwort", "auth_mode_local_no_password": "Kein Passwort",
"auth_mode_local_password_confirm_description": "Bestätigen Sie Ihr Passwort", "auth_mode_local_password_confirm_description": "Bestätigen Sie Ihr Passwort",
"auth_mode_local_password_confirm_label": "Passwort bestätigen", "auth_mode_local_password_confirm_label": "Passwort bestätigen",
"auth_mode_local_password_description": "Sichern Sie Ihr Gerät für zusätzlichen Schutz mit einem Passwort.", "auth_mode_local_password_description": "Sichern Sie Ihr Gerät für zusätzlichen Schutz mit einem Passwort.",
"auth_mode_local_password_do_not_match": "Passwörter stimmen nicht überein", "auth_mode_local_password_do_not_match": "Passwörter stimmen nicht überein",
"auth_mode_local_password_failed_set": "Kennwort konnte nicht festgelegt werden: {error}", "auth_mode_local_password_failed_set": "Kennwort konnte nicht festgelegt werden: {error}",
"auth_mode_local_password_note": "Dieses Passwort wird verwendet, um Ihre Gerätedaten zu sichern und vor unbefugtem Zugriff zu schützen.",
"auth_mode_local_password_note_local": "Alle Daten verbleiben auf Ihrem lokalen Gerät.", "auth_mode_local_password_note_local": "Alle Daten verbleiben auf Ihrem lokalen Gerät.",
"auth_mode_local_password_set": "Legen Sie ein Passwort fest", "auth_mode_local_password_note": "Dieses Passwort wird verwendet, um Ihre Gerätedaten zu sichern und vor unbefugtem Zugriff zu schützen.",
"auth_mode_local_password_set_button": "Passwort festlegen", "auth_mode_local_password_set_button": "Passwort festlegen",
"auth_mode_local_password_set_description": "Erstellen Sie ein sicheres Passwort, um Ihr JetKVM-Gerät lokal zu sichern.", "auth_mode_local_password_set_description": "Erstellen Sie ein sicheres Passwort, um Ihr JetKVM-Gerät lokal zu sichern.",
"auth_mode_local_password_set_label": "Geben Sie ein Passwort ein", "auth_mode_local_password_set_label": "Geben Sie ein Passwort ein",
"auth_mode_local_password_set": "Legen Sie ein Passwort fest",
"auth_mode_local_password": "Passwort",
"auth_mode_local": "Lokale Authentifizierungsmethode",
"auth_signup_connect_to_cloud_action": "Anmelden und Gerät verbinden", "auth_signup_connect_to_cloud_action": "Anmelden und Gerät verbinden",
"auth_signup_create_account": "Erstellen Sie Ihr JetKVM-Konto",
"auth_signup_create_account_action": "Benutzerkonto erstellen", "auth_signup_create_account_action": "Benutzerkonto erstellen",
"auth_signup_create_account_description": "Erstellen Sie Ihr Konto und beginnen Sie mit der einfachen Verwaltung Ihrer Geräte.", "auth_signup_create_account_description": "Erstellen Sie Ihr Konto und beginnen Sie mit der einfachen Verwaltung Ihrer Geräte.",
"back": "Zurück", "auth_signup_create_account": "Erstellen Sie Ihr JetKVM-Konto",
"back_to_devices": "Zurück zu Geräte", "back_to_devices": "Zurück zu Geräte",
"back": "Zurück",
"cancel": "Stornieren", "cancel": "Stornieren",
"close": "Schließen", "close": "Schließen",
"cloud_kvms": "Cloud-KVMs",
"cloud_kvms_description": "Verwalten Sie Ihre Cloud-KVMs und stellen Sie eine sichere Verbindung zu ihnen her.", "cloud_kvms_description": "Verwalten Sie Ihre Cloud-KVMs und stellen Sie eine sichere Verbindung zu ihnen her.",
"cloud_kvms_no_devices": "Keine Geräte gefunden",
"cloud_kvms_no_devices_description": "Sie haben noch keine Geräte mit aktivierter JetKVM Cloud.", "cloud_kvms_no_devices_description": "Sie haben noch keine Geräte mit aktivierter JetKVM Cloud.",
"cloud_kvms_no_devices": "Keine Geräte gefunden",
"cloud_kvms": "Cloud-KVMs",
"confirm": "Bestätigen", "confirm": "Bestätigen",
"connect_to_kvm": "Mit KVM verbinden", "connect_to_kvm": "Mit KVM verbinden",
"connecting_to_device": "Verbindung zum Gerät wird hergestellt…", "connecting_to_device": "Verbindung zum Gerät wird hergestellt…",
"connection_established": "Verbindung hergestellt", "connection_established": "Verbindung hergestellt",
"connection_stats_badge_jitter": "Jitter",
"connection_stats_badge_jitter_buffer_avg_delay": "Jitter-Puffer Durchschnittliche Verzögerung", "connection_stats_badge_jitter_buffer_avg_delay": "Jitter-Puffer Durchschnittliche Verzögerung",
"connection_stats_connection": "Verbindung", "connection_stats_badge_jitter": "Jitter",
"connection_stats_connection_description": "Die Verbindung zwischen dem Client und dem JetKVM.", "connection_stats_connection_description": "Die Verbindung zwischen dem Client und dem JetKVM.",
"connection_stats_frames_per_second": "Bilder pro Sekunde", "connection_stats_connection": "Verbindung",
"connection_stats_frames_per_second_description": "Anzahl der pro Sekunde angezeigten eingehenden Videobilder.", "connection_stats_frames_per_second_description": "Anzahl der pro Sekunde angezeigten eingehenden Videobilder.",
"connection_stats_network_stability": "Netzwerkstabilität", "connection_stats_frames_per_second": "Bilder pro Sekunde",
"connection_stats_network_stability_description": "Wie gleichmäßig der Fluss eingehender Videopakete im Netzwerk ist.", "connection_stats_network_stability_description": "Wie gleichmäßig der Fluss eingehender Videopakete im Netzwerk ist.",
"connection_stats_packets_lost": "Verlorene Pakete", "connection_stats_network_stability": "Netzwerkstabilität",
"connection_stats_packets_lost_description": "Anzahl der verlorenen eingehenden Video-RTP-Pakete.", "connection_stats_packets_lost_description": "Anzahl der verlorenen eingehenden Video-RTP-Pakete.",
"connection_stats_playback_delay": "Wiedergabeverzögerung", "connection_stats_packets_lost": "Verlorene Pakete",
"connection_stats_playback_delay_description": "Durch den Jitter-Puffer hinzugefügte Verzögerung, um die Wiedergabe zu glätten, wenn die Frames ungleichmäßig ankommen.", "connection_stats_playback_delay_description": "Durch den Jitter-Puffer hinzugefügte Verzögerung, um die Wiedergabe zu glätten, wenn die Frames ungleichmäßig ankommen.",
"connection_stats_round_trip_time": "Round-Trip-Zeit", "connection_stats_playback_delay": "Wiedergabeverzögerung",
"connection_stats_round_trip_time_description": "Roundtrip-Zeit für das aktive ICE-Kandidatenpaar zwischen Peers.", "connection_stats_round_trip_time_description": "Roundtrip-Zeit für das aktive ICE-Kandidatenpaar zwischen Peers.",
"connection_stats_round_trip_time": "Round-Trip-Zeit",
"connection_stats_sidebar": "Verbindungsstatistiken", "connection_stats_sidebar": "Verbindungsstatistiken",
"connection_stats_video": "Video",
"connection_stats_video_description": "Der Videostream vom JetKVM zum Client.", "connection_stats_video_description": "Der Videostream vom JetKVM zum Client.",
"connection_stats_video": "Video",
"continue": "Weitermachen", "continue": "Weitermachen",
"creating_peer_connection": "Peer-Verbindung wird hergestellt …", "creating_peer_connection": "Peer-Verbindung wird hergestellt …",
"dc_power_control_current": "Aktuell",
"dc_power_control_current_unit": "A", "dc_power_control_current_unit": "A",
"dc_power_control_current": "Aktuell",
"dc_power_control_get_state_error": "Der Gleichstromstatus konnte nicht abgerufen werden: {error}", "dc_power_control_get_state_error": "Der Gleichstromstatus konnte nicht abgerufen werden: {error}",
"dc_power_control_power": "Leistung",
"dc_power_control_power_off_button": "Ausschalten", "dc_power_control_power_off_button": "Ausschalten",
"dc_power_control_power_off_state": "Ausschalten", "dc_power_control_power_off_state": "Ausschalten",
"dc_power_control_power_on_button": "Einschalten", "dc_power_control_power_on_button": "Einschalten",
"dc_power_control_power_on_state": "Einschalten", "dc_power_control_power_on_state": "Einschalten",
"dc_power_control_power_unit": "W", "dc_power_control_power_unit": "W",
"dc_power_control_power": "Leistung",
"dc_power_control_restore_last_state": "Letzter Zustand", "dc_power_control_restore_last_state": "Letzter Zustand",
"dc_power_control_restore_power_state": "Wiederherstellung nach Stromausfall", "dc_power_control_restore_power_state": "Wiederherstellung nach Stromausfall",
"dc_power_control_set_power_state_error": "Der DC-Stromversorgungsstatus konnte nicht an {enabled} werden: {error}", "dc_power_control_set_power_state_error": "Der DC-Stromversorgungsstatus konnte nicht an {enabled} werden: {error}",
"dc_power_control_set_restore_state_error": "Der Status zur Wiederherstellung der Gleichstromversorgung konnte nicht an {state} gesendet werden: {error}", "dc_power_control_set_restore_state_error": "Der Status zur Wiederherstellung der Gleichstromversorgung konnte nicht an {state} gesendet werden: {error}",
"dc_power_control_voltage": "Stromspannung",
"dc_power_control_voltage_unit": "V", "dc_power_control_voltage_unit": "V",
"dc_power_control_voltage": "Stromspannung",
"delete": "Löschen", "delete": "Löschen",
"deregister_button": "Abmelden von der Cloud", "deregister_button": "Abmelden von der Cloud",
"deregister_cloud_devices": "Cloud-Geräte", "deregister_cloud_devices": "Cloud-Geräte",
@ -226,12 +226,12 @@
"extension_popover_load_and_manage_extensions": "Laden und verwalten Sie Ihre Erweiterungen", "extension_popover_load_and_manage_extensions": "Laden und verwalten Sie Ihre Erweiterungen",
"extension_popover_set_error_notification": "Fehler beim Festlegen der aktiven Erweiterung: {error}", "extension_popover_set_error_notification": "Fehler beim Festlegen der aktiven Erweiterung: {error}",
"extension_popover_unload_extension": "Erweiterung entladen", "extension_popover_unload_extension": "Erweiterung entladen",
"extension_serial_console": "Serielle Konsole",
"extension_serial_console_description": "Greifen Sie auf Ihre serielle Konsolenerweiterung zu", "extension_serial_console_description": "Greifen Sie auf Ihre serielle Konsolenerweiterung zu",
"extensions_atx_power_control": "ATX-Stromsteuerung", "extension_serial_console": "Serielle Konsole",
"extensions_atx_power_control_description": "Steuern Sie den Energiezustand Ihrer Maschine über die ATX-Energiesteuerung.", "extensions_atx_power_control_description": "Steuern Sie den Energiezustand Ihrer Maschine über die ATX-Energiesteuerung.",
"extensions_dc_power_control": "Gleichstromsteuerung", "extensions_atx_power_control": "ATX-Stromsteuerung",
"extensions_dc_power_control_description": "Steuern Sie Ihre DC-Stromerweiterung", "extensions_dc_power_control_description": "Steuern Sie Ihre DC-Stromerweiterung",
"extensions_dc_power_control": "Gleichstromsteuerung",
"extensions_popover_extensions": "Erweiterungen", "extensions_popover_extensions": "Erweiterungen",
"gathering_ice_candidates": "ICE-Kandidaten zusammenbringen …", "gathering_ice_candidates": "ICE-Kandidaten zusammenbringen …",
"general_app_version": "App: {version}", "general_app_version": "App: {version}",
@ -241,8 +241,8 @@
"general_check_for_updates": "Nach Updates suchen", "general_check_for_updates": "Nach Updates suchen",
"general_page_description": "Geräteeinstellungen konfigurieren und Voreinstellungen aktualisieren", "general_page_description": "Geräteeinstellungen konfigurieren und Voreinstellungen aktualisieren",
"general_reboot_description": "Möchten Sie mit dem Neustart des Systems fortfahren?", "general_reboot_description": "Möchten Sie mit dem Neustart des Systems fortfahren?",
"general_reboot_device": "Gerät neu starten",
"general_reboot_device_description": "Schalten Sie den JetKVM aus und wieder ein", "general_reboot_device_description": "Schalten Sie den JetKVM aus und wieder ein",
"general_reboot_device": "Gerät neu starten",
"general_reboot_no_button": "NEIN", "general_reboot_no_button": "NEIN",
"general_reboot_title": "Starten Sie JetKVM neu", "general_reboot_title": "Starten Sie JetKVM neu",
"general_reboot_yes_button": "Ja", "general_reboot_yes_button": "Ja",
@ -295,9 +295,9 @@
"hardware_display_orientation_title": "Anzeigeausrichtung", "hardware_display_orientation_title": "Anzeigeausrichtung",
"hardware_display_wake_up_note": "Das Display wird aktiviert, wenn sich der Verbindungsstatus ändert oder wenn es berührt wird.", "hardware_display_wake_up_note": "Das Display wird aktiviert, wenn sich der Verbindungsstatus ändert oder wenn es berührt wird.",
"hardware_page_description": "Konfigurieren Sie Anzeigeeinstellungen und Hardwareoptionen für Ihr JetKVM-Gerät", "hardware_page_description": "Konfigurieren Sie Anzeigeeinstellungen und Hardwareoptionen für Ihr JetKVM-Gerät",
"hardware_time_10_minutes": "10 Minuten",
"hardware_time_1_hour": "1 Stunde", "hardware_time_1_hour": "1 Stunde",
"hardware_time_1_minute": "1 Minute", "hardware_time_1_minute": "1 Minute",
"hardware_time_10_minutes": "10 Minuten",
"hardware_time_30_minutes": "30 Minuten", "hardware_time_30_minutes": "30 Minuten",
"hardware_time_5_minutes": "5 Minuten", "hardware_time_5_minutes": "5 Minuten",
"hardware_time_never": "Niemals", "hardware_time_never": "Niemals",
@ -327,7 +327,6 @@
"invalid_password": "Ungültiges Passwort", "invalid_password": "Ungültiges Passwort",
"ip_address": "IP-Adresse", "ip_address": "IP-Adresse",
"ipv6_address_label": "Adresse", "ipv6_address_label": "Adresse",
"ipv6_gateway": "Tor",
"ipv6_information": "IPv6-Informationen", "ipv6_information": "IPv6-Informationen",
"ipv6_link_local": "Link-lokal", "ipv6_link_local": "Link-lokal",
"ipv6_preferred_lifetime": "Bevorzugte Lebensdauer", "ipv6_preferred_lifetime": "Bevorzugte Lebensdauer",
@ -410,8 +409,8 @@
"log_in": "Einloggen", "log_in": "Einloggen",
"log_out": "Ausloggen", "log_out": "Ausloggen",
"logged_in_as": "Angemeldet als", "logged_in_as": "Angemeldet als",
"login_enter_password": "Geben Sie Ihr Passwort ein",
"login_enter_password_description": "Geben Sie Ihr Passwort ein, um auf Ihr JetKVM zuzugreifen.", "login_enter_password_description": "Geben Sie Ihr Passwort ein, um auf Ihr JetKVM zuzugreifen.",
"login_enter_password": "Geben Sie Ihr Passwort ein",
"login_error": "Beim Anmelden ist ein Fehler aufgetreten", "login_error": "Beim Anmelden ist ein Fehler aufgetreten",
"login_forgot_password": "Passwort vergessen?", "login_forgot_password": "Passwort vergessen?",
"login_password_label": "Passwort", "login_password_label": "Passwort",
@ -425,8 +424,8 @@
"macro_name_required": "Name ist erforderlich", "macro_name_required": "Name ist erforderlich",
"macro_name_too_long": "Der Name muss weniger als 50 Zeichen lang sein", "macro_name_too_long": "Der Name muss weniger als 50 Zeichen lang sein",
"macro_please_fix_validation_errors": "Bitte beheben Sie die Validierungsfehler", "macro_please_fix_validation_errors": "Bitte beheben Sie die Validierungsfehler",
"macro_save": "Makro speichern",
"macro_save_error": "Beim Speichern ist ein Fehler aufgetreten.", "macro_save_error": "Beim Speichern ist ein Fehler aufgetreten.",
"macro_save": "Makro speichern",
"macro_step_count": "{steps} / {max} Schritte", "macro_step_count": "{steps} / {max} Schritte",
"macro_step_duration_description": "Wartezeit vor der Ausführung des nächsten Schritts.", "macro_step_duration_description": "Wartezeit vor der Ausführung des nächsten Schritts.",
"macro_step_duration_label": "Schrittdauer", "macro_step_duration_label": "Schrittdauer",
@ -440,8 +439,8 @@
"macro_steps_description": "Tasten/Modifikatoren werden nacheinander mit einer Verzögerung zwischen den einzelnen Schritten ausgeführt.", "macro_steps_description": "Tasten/Modifikatoren werden nacheinander mit einer Verzögerung zwischen den einzelnen Schritten ausgeführt.",
"macro_steps_label": "Schritte", "macro_steps_label": "Schritte",
"macros_add_description": "Erstellen Sie ein neues Tastaturmakro", "macros_add_description": "Erstellen Sie ein neues Tastaturmakro",
"macros_add_new": "Neues Makro hinzufügen",
"macros_add_new_macro": "Neues Makro hinzufügen", "macros_add_new_macro": "Neues Makro hinzufügen",
"macros_add_new": "Neues Makro hinzufügen",
"macros_aria_add_new": "Neues Makro hinzufügen", "macros_aria_add_new": "Neues Makro hinzufügen",
"macros_aria_delete": "Makro löschen {name}", "macros_aria_delete": "Makro löschen {name}",
"macros_aria_duplicate": "Doppeltes Makro {name}", "macros_aria_duplicate": "Doppeltes Makro {name}",
@ -463,16 +462,16 @@
"macros_edit_button": "Bearbeiten", "macros_edit_button": "Bearbeiten",
"macros_edit_description": "Ändern Sie Ihr Tastaturmakro", "macros_edit_description": "Ändern Sie Ihr Tastaturmakro",
"macros_edit_title": "Makro bearbeiten", "macros_edit_title": "Makro bearbeiten",
"macros_failed_create": "Makro konnte nicht erstellt werden",
"macros_failed_create_error": "Makro konnte nicht erstellt werden: {error}", "macros_failed_create_error": "Makro konnte nicht erstellt werden: {error}",
"macros_failed_delete": "Makro konnte nicht gelöscht werden", "macros_failed_create": "Makro konnte nicht erstellt werden",
"macros_failed_delete_error": "Makro konnte nicht gelöscht werden: {error}", "macros_failed_delete_error": "Makro konnte nicht gelöscht werden: {error}",
"macros_failed_duplicate": "Makro konnte nicht dupliziert werden", "macros_failed_delete": "Makro konnte nicht gelöscht werden",
"macros_failed_duplicate_error": "Makro konnte nicht dupliziert werden: {error}", "macros_failed_duplicate_error": "Makro konnte nicht dupliziert werden: {error}",
"macros_failed_reorder": "Makros konnten nicht neu angeordnet werden", "macros_failed_duplicate": "Makro konnte nicht dupliziert werden",
"macros_failed_reorder_error": "Fehler beim Neuordnen der Makros: {error}", "macros_failed_reorder_error": "Fehler beim Neuordnen der Makros: {error}",
"macros_failed_update": "Makro konnte nicht aktualisiert werden", "macros_failed_reorder": "Makros konnten nicht neu angeordnet werden",
"macros_failed_update_error": "Makro konnte nicht aktualisiert werden: {error}", "macros_failed_update_error": "Makro konnte nicht aktualisiert werden: {error}",
"macros_failed_update": "Makro konnte nicht aktualisiert werden",
"macros_invalid_data": "Ungültige Makrodaten", "macros_invalid_data": "Ungültige Makrodaten",
"macros_loading": "Makros werden geladen …", "macros_loading": "Makros werden geladen …",
"macros_max_reached": "Max. erreicht", "macros_max_reached": "Max. erreicht",
@ -507,8 +506,8 @@
"mount_error_list_storage": "Fehler beim Auflisten der Speicherdateien: {error}", "mount_error_list_storage": "Fehler beim Auflisten der Speicherdateien: {error}",
"mount_error_title": "Mount-Fehler", "mount_error_title": "Mount-Fehler",
"mount_get_state_error": "Der Status des virtuellen Mediums konnte nicht abgerufen werden: {error}", "mount_get_state_error": "Der Status des virtuellen Mediums konnte nicht abgerufen werden: {error}",
"mount_jetkvm_storage": "JetKVM-Speicherhalterung",
"mount_jetkvm_storage_description": "Mounten Sie zuvor hochgeladene Dateien aus dem JetKVM-Speicher", "mount_jetkvm_storage_description": "Mounten Sie zuvor hochgeladene Dateien aus dem JetKVM-Speicher",
"mount_jetkvm_storage": "JetKVM-Speicherhalterung",
"mount_mode_cdrom": "CD/DVD", "mount_mode_cdrom": "CD/DVD",
"mount_mode_disk": "Scheibe", "mount_mode_disk": "Scheibe",
"mount_mounted_as": "Montiert als", "mount_mounted_as": "Montiert als",
@ -521,8 +520,8 @@
"mount_popular_images": "Beliebte Bilder", "mount_popular_images": "Beliebte Bilder",
"mount_streaming_from_url": "Streaming von URL", "mount_streaming_from_url": "Streaming von URL",
"mount_supported_formats": "Unterstützte Formate: ISO, IMG", "mount_supported_formats": "Unterstützte Formate: ISO, IMG",
"mount_unmount": "Aushängen",
"mount_unmount_error": "Abbild konnte nicht ausgehängt werden: {error}", "mount_unmount_error": "Abbild konnte nicht ausgehängt werden: {error}",
"mount_unmount": "Aushängen",
"mount_upload_description": "Wählen Sie eine Bilddatei zum Hochladen in den JetKVM-Speicher aus", "mount_upload_description": "Wählen Sie eine Bilddatei zum Hochladen in den JetKVM-Speicher aus",
"mount_upload_error": "Upload-Fehler: {error}", "mount_upload_error": "Upload-Fehler: {error}",
"mount_upload_failed_datachannel": "Fehler beim Erstellen des Datenkanals für den Datei-Upload", "mount_upload_failed_datachannel": "Fehler beim Erstellen des Datenkanals für den Datei-Upload",
@ -530,8 +529,8 @@
"mount_upload_successful": "Upload erfolgreich", "mount_upload_successful": "Upload erfolgreich",
"mount_upload_title": "Neues Bild hochladen", "mount_upload_title": "Neues Bild hochladen",
"mount_uploaded_has_been_uploaded": "{name} wurde hochgeladen", "mount_uploaded_has_been_uploaded": "{name} wurde hochgeladen",
"mount_uploading": "Hochladen…",
"mount_uploading_with_name": "Hochladen von {name}", "mount_uploading_with_name": "Hochladen von {name}",
"mount_uploading": "Hochladen…",
"mount_url_description": "Mounten Sie Dateien von jeder öffentlichen Webadresse", "mount_url_description": "Mounten Sie Dateien von jeder öffentlichen Webadresse",
"mount_url_input_label": "Bild-URL", "mount_url_input_label": "Bild-URL",
"mount_url_mount": "URL-Mount", "mount_url_mount": "URL-Mount",
@ -539,10 +538,10 @@
"mount_view_device_title": "Mounten vom JetKVM-Speicher", "mount_view_device_title": "Mounten vom JetKVM-Speicher",
"mount_view_url_description": "Geben Sie eine URL zur zu mountenden Bilddatei ein", "mount_view_url_description": "Geben Sie eine URL zur zu mountenden Bilddatei ein",
"mount_view_url_title": "Von URL einbinden", "mount_view_url_title": "Von URL einbinden",
"mount_virtual_media": "Virtuelle Medien",
"mount_virtual_media_description": "Mounten Sie ein Image, um von einem Betriebssystem zu booten oder es zu installieren.", "mount_virtual_media_description": "Mounten Sie ein Image, um von einem Betriebssystem zu booten oder es zu installieren.",
"mount_virtual_media_source": "Virtuelle Medienquelle",
"mount_virtual_media_source_description": "Wählen Sie, wie Sie Ihr virtuelles Medium mounten möchten", "mount_virtual_media_source_description": "Wählen Sie, wie Sie Ihr virtuelles Medium mounten möchten",
"mount_virtual_media_source": "Virtuelle Medienquelle",
"mount_virtual_media": "Virtuelle Medien",
"mouse_alt_finger": "Finger berührt einen Bildschirm", "mouse_alt_finger": "Finger berührt einen Bildschirm",
"mouse_alt_mouse": "Maussymbol", "mouse_alt_mouse": "Maussymbol",
"mouse_description": "Konfigurieren Sie das Cursorverhalten und die Interaktionseinstellungen für Ihr Gerät", "mouse_description": "Konfigurieren Sie das Cursorverhalten und die Interaktionseinstellungen für Ihr Gerät",
@ -559,10 +558,10 @@
"mouse_jiggler_light": "Licht - 5m", "mouse_jiggler_light": "Licht - 5m",
"mouse_jiggler_standard": "Standard - 1 m", "mouse_jiggler_standard": "Standard - 1 m",
"mouse_jiggler_title": "Wackel", "mouse_jiggler_title": "Wackel",
"mouse_mode_absolute": "Absolute",
"mouse_mode_absolute_description": "Am bequemsten", "mouse_mode_absolute_description": "Am bequemsten",
"mouse_mode_relative": "Relativ", "mouse_mode_absolute": "Absolute",
"mouse_mode_relative_description": "Am kompatibelsten", "mouse_mode_relative_description": "Am kompatibelsten",
"mouse_mode_relative": "Relativ",
"mouse_modes_description": "Wählen Sie den Mauseingabemodus", "mouse_modes_description": "Wählen Sie den Mauseingabemodus",
"mouse_modes_title": "Modi", "mouse_modes_title": "Modi",
"mouse_scroll_high": "Hoch", "mouse_scroll_high": "Hoch",
@ -575,17 +574,12 @@
"mouse_title": "Maus", "mouse_title": "Maus",
"network_custom_domain": "Benutzerdefinierte Domäne", "network_custom_domain": "Benutzerdefinierte Domäne",
"network_description": "Konfigurieren Sie Ihre Netzwerkeinstellungen", "network_description": "Konfigurieren Sie Ihre Netzwerkeinstellungen",
"network_dhcp_client_description": "Konfigurieren Sie, welcher DHCP-Client verwendet werden soll",
"network_dhcp_client_jetkvm": "JetKVM intern",
"network_dhcp_client_title": "DHCP-Client",
"network_dhcp_information": "DHCP-Informationen", "network_dhcp_information": "DHCP-Informationen",
"network_dhcp_lease_renew": "DHCP-Lease erneuern",
"network_dhcp_lease_renew_confirm": "Mietvertrag verlängern",
"network_dhcp_lease_renew_confirm_description": "Dadurch wird eine neue IP-Adresse von Ihrem DHCP-Server angefordert. Während dieses Vorgangs kann die Netzwerkverbindung Ihres Geräts vorübergehend unterbrochen werden.", "network_dhcp_lease_renew_confirm_description": "Dadurch wird eine neue IP-Adresse von Ihrem DHCP-Server angefordert. Während dieses Vorgangs kann die Netzwerkverbindung Ihres Geräts vorübergehend unterbrochen werden.",
"network_dhcp_lease_renew_confirm_new_a": "Wenn Sie eine neue IP-Adresse erhalten", "network_dhcp_lease_renew_confirm": "Mietvertrag verlängern",
"network_dhcp_lease_renew_confirm_new_b": "Möglicherweise müssen Sie die Verbindung mit der neuen Adresse erneut herstellen",
"network_dhcp_lease_renew_failed": "Leasing konnte nicht erneuert werden: {error}", "network_dhcp_lease_renew_failed": "Leasing konnte nicht erneuert werden: {error}",
"network_dhcp_lease_renew_success": "DHCP-Lease erneuert", "network_dhcp_lease_renew_success": "DHCP-Lease erneuert",
"network_dhcp_lease_renew": "DHCP-Lease erneuern",
"network_domain_custom": "Brauch", "network_domain_custom": "Brauch",
"network_domain_description": "Netzwerkdomänensuffix für das Gerät", "network_domain_description": "Netzwerkdomänensuffix für das Gerät",
"network_domain_dhcp_provided": "DHCP bereitgestellt", "network_domain_dhcp_provided": "DHCP bereitgestellt",
@ -594,35 +588,21 @@
"network_hostname_description": "Gerätekennung im Netzwerk. Leer für Systemstandard", "network_hostname_description": "Gerätekennung im Netzwerk. Leer für Systemstandard",
"network_hostname_title": "Hostname", "network_hostname_title": "Hostname",
"network_http_proxy_description": "Proxyserver für ausgehende HTTP(S)-Anfragen vom Gerät. Leer für keine.", "network_http_proxy_description": "Proxyserver für ausgehende HTTP(S)-Anfragen vom Gerät. Leer für keine.",
"network_http_proxy_invalid": "Ungültige HTTP-Proxy-URL",
"network_http_proxy_title": "HTTP-Proxy", "network_http_proxy_title": "HTTP-Proxy",
"network_ipv4_address": "IPv4-Adresse",
"network_ipv4_dns": "IPv4 DNS",
"network_ipv4_gateway": "IPv4-Gateway",
"network_ipv4_mode_description": "Konfigurieren des IPv4-Modus", "network_ipv4_mode_description": "Konfigurieren des IPv4-Modus",
"network_ipv4_mode_dhcp": "DHCP", "network_ipv4_mode_dhcp": "DHCP",
"network_ipv4_mode_static": "Statisch",
"network_ipv4_mode_title": "IPv4-Modus", "network_ipv4_mode_title": "IPv4-Modus",
"network_ipv4_netmask": "IPv4-Netzmaske",
"network_ipv6_address": "IPv6-Adresse",
"network_ipv6_information": "IPv6-Informationen", "network_ipv6_information": "IPv6-Informationen",
"network_ipv6_mode_description": "Konfigurieren des IPv6-Modus", "network_ipv6_mode_description": "Konfigurieren des IPv6-Modus",
"network_ipv6_mode_dhcpv6": "DHCPv6",
"network_ipv6_mode_disabled": "Deaktiviert", "network_ipv6_mode_disabled": "Deaktiviert",
"network_ipv6_mode_link_local": "Nur Link-Local",
"network_ipv6_mode_slaac": "SLAAC", "network_ipv6_mode_slaac": "SLAAC",
"network_ipv6_mode_slaac_dhcpv6": "SLAAC + DHCPv6",
"network_ipv6_mode_static": "Statisch",
"network_ipv6_mode_title": "IPv6-Modus", "network_ipv6_mode_title": "IPv6-Modus",
"network_ipv6_netmask": "IPv6-Netzmaske",
"network_ipv6_no_addresses": "Keine IPv6-Adressen konfiguriert", "network_ipv6_no_addresses": "Keine IPv6-Adressen konfiguriert",
"network_ll_dp_all": "Alle", "network_ll_dp_all": "Alle",
"network_ll_dp_basic": "Basic", "network_ll_dp_basic": "Basic",
"network_ll_dp_description": "Steuern Sie, welche TLVs über das Link Layer Discovery Protocol gesendet werden", "network_ll_dp_description": "Steuern Sie, welche TLVs über das Link Layer Discovery Protocol gesendet werden",
"network_ll_dp_disabled": "Deaktiviert", "network_ll_dp_disabled": "Deaktiviert",
"network_ll_dp_title": "LLDP", "network_ll_dp_title": "LLDP",
"network_mac_address_copy_error": "MAC-Adresse konnte nicht kopiert werden",
"network_mac_address_copy_success": "MAC-Adresse { mac } in die Zwischenablage kopiert",
"network_mac_address_description": "Hardwarekennung für die Netzwerkschnittstelle", "network_mac_address_description": "Hardwarekennung für die Netzwerkschnittstelle",
"network_mac_address_title": "MAC-Adresse", "network_mac_address_title": "MAC-Adresse",
"network_mdns_auto": "Auto", "network_mdns_auto": "Auto",
@ -632,19 +612,9 @@
"network_mdns_ipv6_only": "Nur IPv6", "network_mdns_ipv6_only": "Nur IPv6",
"network_mdns_title": "mDNS", "network_mdns_title": "mDNS",
"network_no_dhcp_lease": "Keine DHCP-Lease-Informationen verfügbar", "network_no_dhcp_lease": "Keine DHCP-Lease-Informationen verfügbar",
"network_no_information_description": "Keine Netzwerkkonfiguration verfügbar",
"network_no_information_headline": "Netzwerkinformationen",
"network_pending_dhcp_mode_change_description": "Speichern Sie die Einstellungen, um den DHCP-Modus zu aktivieren und Leasinginformationen anzuzeigen",
"network_pending_dhcp_mode_change_headline": "Ausstehende Änderung des DHCP-IPv4-Modus",
"network_save_settings": "Einstellungen speichern",
"network_save_settings_apply_title": "Netzwerkeinstellungen anwenden",
"network_save_settings_confirm": "Änderungen übernehmen",
"network_save_settings_confirm_description": "Die folgenden Netzwerkeinstellungen werden angewendet. Diese Änderungen erfordern möglicherweise einen Neustart und können zu einer kurzen Unterbrechung der Verbindung führen.",
"network_save_settings_confirm_heading": "Konfigurationsänderungen",
"network_save_settings_failed": "Netzwerkeinstellungen konnten nicht gespeichert werden: {error}", "network_save_settings_failed": "Netzwerkeinstellungen konnten nicht gespeichert werden: {error}",
"network_save_settings_success": "Netzwerkeinstellungen gespeichert", "network_save_settings_success": "Netzwerkeinstellungen gespeichert",
"network_settings_invalid_ipv4_cidr": "Ungültige CIDR-Notation für IPv4-Adresse", "network_save_settings": "Einstellungen speichern",
"network_settings_load_error": "Netzwerkeinstellungen konnten nicht geladen werden: {error}",
"network_time_sync_description": "Konfigurieren der Zeitsynchronisierungseinstellungen", "network_time_sync_description": "Konfigurieren der Zeitsynchronisierungseinstellungen",
"network_time_sync_http_only": "Nur HTTP", "network_time_sync_http_only": "Nur HTTP",
"network_time_sync_ntp_and_http": "NTP und HTTP", "network_time_sync_ntp_and_http": "NTP und HTTP",
@ -670,8 +640,8 @@
"paste_modal_failed_paste": "Fehler beim Einfügen des Textes: {error}", "paste_modal_failed_paste": "Fehler beim Einfügen des Textes: {error}",
"paste_modal_invalid_chars_intro": "Die folgenden Zeichen werden nicht eingefügt:", "paste_modal_invalid_chars_intro": "Die folgenden Zeichen werden nicht eingefügt:",
"paste_modal_paste_from_host": "Vom Host einfügen", "paste_modal_paste_from_host": "Vom Host einfügen",
"paste_modal_paste_text": "Text einfügen",
"paste_modal_paste_text_description": "Fügen Sie Text von Ihrem Client in den Remote-Host ein", "paste_modal_paste_text_description": "Fügen Sie Text von Ihrem Client in den Remote-Host ein",
"paste_modal_paste_text": "Text einfügen",
"paste_modal_sending_using_layout": "Senden von Text mithilfe des Tastaturlayouts: {iso} - {name}", "paste_modal_sending_using_layout": "Senden von Text mithilfe des Tastaturlayouts: {iso} - {name}",
"peer_connection_closed": "Geschlossen", "peer_connection_closed": "Geschlossen",
"peer_connection_closing": "Schließen", "peer_connection_closing": "Schließen",
@ -698,20 +668,20 @@
"retry": "Wiederholen", "retry": "Wiederholen",
"saving": "Speichern…", "saving": "Speichern…",
"search_placeholder": "Suchen…", "search_placeholder": "Suchen…",
"serial_console": "Serielle Konsole",
"serial_console_baud_rate": "Baudrate", "serial_console_baud_rate": "Baudrate",
"serial_console_configure_description": "Konfigurieren Sie die Einstellungen Ihrer seriellen Konsole", "serial_console_configure_description": "Konfigurieren Sie die Einstellungen Ihrer seriellen Konsole",
"serial_console_data_bits": "Datenbits", "serial_console_data_bits": "Datenbits",
"serial_console_get_settings_error": "Die seriellen Konsoleneinstellungen konnten nicht abgerufen werden: {error}", "serial_console_get_settings_error": "Die seriellen Konsoleneinstellungen konnten nicht abgerufen werden: {error}",
"serial_console_open_console": "Konsole öffnen", "serial_console_open_console": "Konsole öffnen",
"serial_console_parity": "Parität",
"serial_console_parity_even": "Gerade Parität", "serial_console_parity_even": "Gerade Parität",
"serial_console_parity_mark": "Parität markieren", "serial_console_parity_mark": "Parität markieren",
"serial_console_parity_none": "Keine Parität", "serial_console_parity_none": "Keine Parität",
"serial_console_parity_odd": "Ungerade Parität", "serial_console_parity_odd": "Ungerade Parität",
"serial_console_parity_space": "Raumparität", "serial_console_parity_space": "Raumparität",
"serial_console_parity": "Parität",
"serial_console_set_settings_error": "Die Einstellungen der seriellen Konsole konnten nicht auf {settings} festgelegt werden: {error}", "serial_console_set_settings_error": "Die Einstellungen der seriellen Konsole konnten nicht auf {settings} festgelegt werden: {error}",
"serial_console_stop_bits": "Stoppbits", "serial_console_stop_bits": "Stoppbits",
"serial_console": "Serielle Konsole",
"setting_remote_description": "Beschreibung der Fernbedienung einstellen", "setting_remote_description": "Beschreibung der Fernbedienung einstellen",
"setting_remote_session_description": "Beschreibung der Remote-Sitzung festlegen ...", "setting_remote_session_description": "Beschreibung der Remote-Sitzung festlegen ...",
"setting_up_connection_to_device": "Verbindung zum Gerät wird eingerichtet …", "setting_up_connection_to_device": "Verbindung zum Gerät wird eingerichtet …",
@ -721,8 +691,8 @@
"settings_back_to_kvm": "Zurück zu KVM", "settings_back_to_kvm": "Zurück zu KVM",
"settings_general": "Allgemein", "settings_general": "Allgemein",
"settings_hardware": "Hardware", "settings_hardware": "Hardware",
"settings_keyboard": "Tastatur",
"settings_keyboard_macros": "Tastaturmakros", "settings_keyboard_macros": "Tastaturmakros",
"settings_keyboard": "Tastatur",
"settings_mouse": "Maus", "settings_mouse": "Maus",
"settings_network": "Netzwerk", "settings_network": "Netzwerk",
"settings_video": "Video", "settings_video": "Video",
@ -742,7 +712,6 @@
"updates_failed_check": "Fehler beim Suchen nach Updates: {error}", "updates_failed_check": "Fehler beim Suchen nach Updates: {error}",
"updates_failed_get_device_version": "Geräteversion konnte nicht abgerufen werden: {error}", "updates_failed_get_device_version": "Geräteversion konnte nicht abgerufen werden: {error}",
"updating_leave_device_on": "Bitte schalten Sie Ihr Gerät nicht aus…", "updating_leave_device_on": "Bitte schalten Sie Ihr Gerät nicht aus…",
"usb": "USB",
"usb_config_custom": "Brauch", "usb_config_custom": "Brauch",
"usb_config_default": "JetKVM-Standard", "usb_config_default": "JetKVM-Standard",
"usb_config_dell": "Dell Multimedia Pro-Tastatur", "usb_config_dell": "Dell Multimedia Pro-Tastatur",
@ -789,6 +758,7 @@
"usb_state_connecting": "Verbinden", "usb_state_connecting": "Verbinden",
"usb_state_disconnected": "Getrennt", "usb_state_disconnected": "Getrennt",
"usb_state_low_power_mode": "Energiesparmodus", "usb_state_low_power_mode": "Energiesparmodus",
"usb": "USB",
"user_interface_language_description": "Wählen Sie die Sprache aus, die in der JetKVM-Benutzeroberfläche verwendet werden soll", "user_interface_language_description": "Wählen Sie die Sprache aus, die in der JetKVM-Benutzeroberfläche verwendet werden soll",
"user_interface_language_title": "Schnittstellensprache", "user_interface_language_title": "Schnittstellensprache",
"video_brightness_description": "Helligkeitsstufe ( {value} x)", "video_brightness_description": "Helligkeitsstufe ( {value} x)",
@ -855,7 +825,6 @@
"video_title": "Video", "video_title": "Video",
"view_details": "Details anzeigen", "view_details": "Details anzeigen",
"virtual_keyboard_header": "Virtuelle Tastatur", "virtual_keyboard_header": "Virtuelle Tastatur",
"wake_on_lan": "Wake-On-LAN",
"wake_on_lan_add_device_device_name": "Gerätename", "wake_on_lan_add_device_device_name": "Gerätename",
"wake_on_lan_add_device_example_device_name": "Plex Media Server", "wake_on_lan_add_device_example_device_name": "Plex Media Server",
"wake_on_lan_add_device_mac_address": "MAC-Adresse", "wake_on_lan_add_device_mac_address": "MAC-Adresse",
@ -871,6 +840,7 @@
"wake_on_lan_failed_send_magic": "Das Senden des Magic Packets ist fehlgeschlagen.", "wake_on_lan_failed_send_magic": "Das Senden des Magic Packets ist fehlgeschlagen.",
"wake_on_lan_invalid_mac": "Ungültige MAC-Adresse", "wake_on_lan_invalid_mac": "Ungültige MAC-Adresse",
"wake_on_lan_magic_sent_success": "Magic Packet erfolgreich gesendet", "wake_on_lan_magic_sent_success": "Magic Packet erfolgreich gesendet",
"welcome_to_jetkvm": "Willkommen bei JetKVM", "wake_on_lan": "Wake-On-LAN",
"welcome_to_jetkvm_description": "Steuern Sie jeden Computer aus der Ferne" "welcome_to_jetkvm_description": "Steuern Sie jeden Computer aus der Ferne",
"welcome_to_jetkvm": "Willkommen bei JetKVM"
} }

View File

@ -107,10 +107,10 @@
"already_adopted_title": "Device Already Registered", "already_adopted_title": "Device Already Registered",
"appearance_description": "Choose your preferred color theme", "appearance_description": "Choose your preferred color theme",
"appearance_page_description": "Customize the look and feel of your JetKVM interface", "appearance_page_description": "Customize the look and feel of your JetKVM interface",
"appearance_theme": "Theme",
"appearance_theme_dark": "Dark", "appearance_theme_dark": "Dark",
"appearance_theme_light": "Light", "appearance_theme_light": "Light",
"appearance_theme_system": "System", "appearance_theme_system": "System",
"appearance_theme": "Theme",
"appearance_title": "Appearance", "appearance_title": "Appearance",
"attach": "Attach", "attach": "Attach",
"atx_power_control_get_state_error": "Failed to get ATX power state: {error}", "atx_power_control_get_state_error": "Failed to get ATX power state: {error}",
@ -121,85 +121,85 @@
"atx_power_control_reset_button": "Reset", "atx_power_control_reset_button": "Reset",
"atx_power_control_send_action_error": "Failed to send ATX power action {action}: {error}", "atx_power_control_send_action_error": "Failed to send ATX power action {action}: {error}",
"atx_power_control_short_power_button": "Short Press", "atx_power_control_short_power_button": "Short Press",
"auth_authentication_mode": "Please select an authentication mode",
"auth_authentication_mode_error": "An error occurred while setting the authentication mode", "auth_authentication_mode_error": "An error occurred while setting the authentication mode",
"auth_authentication_mode_invalid": "Invalid authentication mode", "auth_authentication_mode_invalid": "Invalid authentication mode",
"auth_connect_to_cloud": "Connect your JetKVM to the cloud", "auth_authentication_mode": "Please select an authentication mode",
"auth_connect_to_cloud_action": "Log in & Connect device", "auth_connect_to_cloud_action": "Log in & Connect device",
"auth_connect_to_cloud_description": "Unlock remote access and advanced features for your device", "auth_connect_to_cloud_description": "Unlock remote access and advanced features for your device",
"auth_connect_to_cloud": "Connect your JetKVM to the cloud",
"auth_header_cta_already_have_account": "Already have an account?", "auth_header_cta_already_have_account": "Already have an account?",
"auth_header_cta_dont_have_account": "Don't have an account?", "auth_header_cta_dont_have_account": "Don't have an account?",
"auth_header_cta_new_to_jetkvm": "New to JetKVM?", "auth_header_cta_new_to_jetkvm": "New to JetKVM?",
"auth_login": "Log in to your JetKVM account",
"auth_login_action": "Log in", "auth_login_action": "Log in",
"auth_login_description": "Log in to access and manage your devices securely", "auth_login_description": "Log in to access and manage your devices securely",
"auth_mode_local": "Local Authentication Method", "auth_login": "Log in to your JetKVM account",
"auth_mode_local_change_later": "You can always change your authentication method later in the settings.", "auth_mode_local_change_later": "You can always change your authentication method later in the settings.",
"auth_mode_local_description": "Select how you would like to secure your JetKVM device locally.", "auth_mode_local_description": "Select how you would like to secure your JetKVM device locally.",
"auth_mode_local_no_password": "No Password",
"auth_mode_local_no_password_description": "Quick access without password authentication.", "auth_mode_local_no_password_description": "Quick access without password authentication.",
"auth_mode_local_password": "Password", "auth_mode_local_no_password": "No Password",
"auth_mode_local_password_confirm_description": "Confirm your password", "auth_mode_local_password_confirm_description": "Confirm your password",
"auth_mode_local_password_confirm_label": "Confirm Password", "auth_mode_local_password_confirm_label": "Confirm Password",
"auth_mode_local_password_description": "Secure your device with a password for added protection.", "auth_mode_local_password_description": "Secure your device with a password for added protection.",
"auth_mode_local_password_do_not_match": "Passwords do not match", "auth_mode_local_password_do_not_match": "Passwords do not match",
"auth_mode_local_password_failed_set": "Failed to set password: {error}", "auth_mode_local_password_failed_set": "Failed to set password: {error}",
"auth_mode_local_password_note": "This password will be used to secure your device data and protect against unauthorized access.",
"auth_mode_local_password_note_local": "All data remains on your local device.", "auth_mode_local_password_note_local": "All data remains on your local device.",
"auth_mode_local_password_set": "Set a Password", "auth_mode_local_password_note": "This password will be used to secure your device data and protect against unauthorized access.",
"auth_mode_local_password_set_button": "Set Password", "auth_mode_local_password_set_button": "Set Password",
"auth_mode_local_password_set_description": "Create a strong password to secure your JetKVM device locally.", "auth_mode_local_password_set_description": "Create a strong password to secure your JetKVM device locally.",
"auth_mode_local_password_set_label": "Enter a password", "auth_mode_local_password_set_label": "Enter a password",
"auth_mode_local_password_set": "Set a Password",
"auth_mode_local_password": "Password",
"auth_mode_local": "Local Authentication Method",
"auth_signup_connect_to_cloud_action": "Signup & Connect device", "auth_signup_connect_to_cloud_action": "Signup & Connect device",
"auth_signup_create_account": "Create your JetKVM account",
"auth_signup_create_account_action": "Create Account", "auth_signup_create_account_action": "Create Account",
"auth_signup_create_account_description": "Create your account and start managing your devices with ease.", "auth_signup_create_account_description": "Create your account and start managing your devices with ease.",
"back": "Back", "auth_signup_create_account": "Create your JetKVM account",
"back_to_devices": "Back to Devices", "back_to_devices": "Back to Devices",
"back": "Back",
"cancel": "Cancel", "cancel": "Cancel",
"close": "Close", "close": "Close",
"cloud_kvms": "Cloud KVMs",
"cloud_kvms_description": "Manage your cloud KVMs and connect to them securely.", "cloud_kvms_description": "Manage your cloud KVMs and connect to them securely.",
"cloud_kvms_no_devices": "No devices found",
"cloud_kvms_no_devices_description": "You don't have any devices with enabled JetKVM Cloud yet.", "cloud_kvms_no_devices_description": "You don't have any devices with enabled JetKVM Cloud yet.",
"cloud_kvms_no_devices": "No devices found",
"cloud_kvms": "Cloud KVMs",
"confirm": "Confirm", "confirm": "Confirm",
"connect_to_kvm": "Connect to KVM", "connect_to_kvm": "Connect to KVM",
"connecting_to_device": "Connecting to device…", "connecting_to_device": "Connecting to device…",
"connection_established": "Connection established", "connection_established": "Connection established",
"connection_stats_badge_jitter": "Jitter",
"connection_stats_badge_jitter_buffer_avg_delay": "Jitter Buffer Avg. Delay", "connection_stats_badge_jitter_buffer_avg_delay": "Jitter Buffer Avg. Delay",
"connection_stats_connection": "Connection", "connection_stats_badge_jitter": "Jitter",
"connection_stats_connection_description": "The connection between the client and the JetKVM.", "connection_stats_connection_description": "The connection between the client and the JetKVM.",
"connection_stats_frames_per_second": "Frames per second", "connection_stats_connection": "Connection",
"connection_stats_frames_per_second_description": "Number of inbound video frames displayed per second.", "connection_stats_frames_per_second_description": "Number of inbound video frames displayed per second.",
"connection_stats_network_stability": "Network Stability", "connection_stats_frames_per_second": "Frames per second",
"connection_stats_network_stability_description": "How steady the flow of inbound video packets is across the network.", "connection_stats_network_stability_description": "How steady the flow of inbound video packets is across the network.",
"connection_stats_packets_lost": "Packets Lost", "connection_stats_network_stability": "Network Stability",
"connection_stats_packets_lost_description": "Count of lost inbound video RTP packets.", "connection_stats_packets_lost_description": "Count of lost inbound video RTP packets.",
"connection_stats_playback_delay": "Playback Delay", "connection_stats_packets_lost": "Packets Lost",
"connection_stats_playback_delay_description": "Delay added by the jitter buffer to smooth playback when frames arrive unevenly.", "connection_stats_playback_delay_description": "Delay added by the jitter buffer to smooth playback when frames arrive unevenly.",
"connection_stats_round_trip_time": "Round-Trip Time", "connection_stats_playback_delay": "Playback Delay",
"connection_stats_round_trip_time_description": "Round-trip time for the active ICE candidate pair between peers.", "connection_stats_round_trip_time_description": "Round-trip time for the active ICE candidate pair between peers.",
"connection_stats_round_trip_time": "Round-Trip Time",
"connection_stats_sidebar": "Connection Stats", "connection_stats_sidebar": "Connection Stats",
"connection_stats_video": "Video",
"connection_stats_video_description": "The video stream from the JetKVM to the client.", "connection_stats_video_description": "The video stream from the JetKVM to the client.",
"connection_stats_video": "Video",
"continue": "Continue", "continue": "Continue",
"creating_peer_connection": "Creating peer connection…", "creating_peer_connection": "Creating peer connection…",
"dc_power_control_current": "Current",
"dc_power_control_current_unit": "A", "dc_power_control_current_unit": "A",
"dc_power_control_current": "Current",
"dc_power_control_get_state_error": "Failed to get DC power state: {error}", "dc_power_control_get_state_error": "Failed to get DC power state: {error}",
"dc_power_control_power": "Power",
"dc_power_control_power_off_button": "Power Off", "dc_power_control_power_off_button": "Power Off",
"dc_power_control_power_off_state": "Power OFF", "dc_power_control_power_off_state": "Power OFF",
"dc_power_control_power_on_button": "Power On", "dc_power_control_power_on_button": "Power On",
"dc_power_control_power_on_state": "Power ON", "dc_power_control_power_on_state": "Power ON",
"dc_power_control_power_unit": "W", "dc_power_control_power_unit": "W",
"dc_power_control_power": "Power",
"dc_power_control_restore_last_state": "Last State", "dc_power_control_restore_last_state": "Last State",
"dc_power_control_restore_power_state": "Restore Power Loss", "dc_power_control_restore_power_state": "Restore Power Loss",
"dc_power_control_set_power_state_error": "Failed to send DC power state to {enabled}: {error}", "dc_power_control_set_power_state_error": "Failed to send DC power state to {enabled}: {error}",
"dc_power_control_set_restore_state_error": "Failed to send DC power restore state to {state}: {error}", "dc_power_control_set_restore_state_error": "Failed to send DC power restore state to {state}: {error}",
"dc_power_control_voltage": "Voltage",
"dc_power_control_voltage_unit": "V", "dc_power_control_voltage_unit": "V",
"dc_power_control_voltage": "Voltage",
"delete": "Delete", "delete": "Delete",
"deregister_button": "Deregister from Cloud", "deregister_button": "Deregister from Cloud",
"deregister_cloud_devices": "Cloud Devices", "deregister_cloud_devices": "Cloud Devices",
@ -226,12 +226,12 @@
"extension_popover_load_and_manage_extensions": "Load and manage your extensions", "extension_popover_load_and_manage_extensions": "Load and manage your extensions",
"extension_popover_set_error_notification": "Failed to set active extension: {error}", "extension_popover_set_error_notification": "Failed to set active extension: {error}",
"extension_popover_unload_extension": "Unload Extension", "extension_popover_unload_extension": "Unload Extension",
"extension_serial_console": "Serial Console",
"extension_serial_console_description": "Access your serial console extension", "extension_serial_console_description": "Access your serial console extension",
"extensions_atx_power_control": "ATX Power Control", "extension_serial_console": "Serial Console",
"extensions_atx_power_control_description": "Control the power state of your machine via ATX power control.", "extensions_atx_power_control_description": "Control the power state of your machine via ATX power control.",
"extensions_dc_power_control": "DC Power Control", "extensions_atx_power_control": "ATX Power Control",
"extensions_dc_power_control_description": "Control your DC Power extension", "extensions_dc_power_control_description": "Control your DC Power extension",
"extensions_dc_power_control": "DC Power Control",
"extensions_popover_extensions": "Extensions", "extensions_popover_extensions": "Extensions",
"gathering_ice_candidates": "Gathering ICE candidates…", "gathering_ice_candidates": "Gathering ICE candidates…",
"general_app_version": "App: {version}", "general_app_version": "App: {version}",
@ -241,8 +241,8 @@
"general_check_for_updates": "Check for Updates", "general_check_for_updates": "Check for Updates",
"general_page_description": "Configure device settings and update preferences", "general_page_description": "Configure device settings and update preferences",
"general_reboot_description": "Do you want to proceed with rebooting the system?", "general_reboot_description": "Do you want to proceed with rebooting the system?",
"general_reboot_device": "Reboot Device",
"general_reboot_device_description": "Power cycle the JetKVM", "general_reboot_device_description": "Power cycle the JetKVM",
"general_reboot_device": "Reboot Device",
"general_reboot_no_button": "No", "general_reboot_no_button": "No",
"general_reboot_title": "Reboot JetKVM", "general_reboot_title": "Reboot JetKVM",
"general_reboot_yes_button": "Yes", "general_reboot_yes_button": "Yes",
@ -295,9 +295,9 @@
"hardware_display_orientation_title": "Display Orientation", "hardware_display_orientation_title": "Display Orientation",
"hardware_display_wake_up_note": "The display will wake up when the connection state changes, or when touched.", "hardware_display_wake_up_note": "The display will wake up when the connection state changes, or when touched.",
"hardware_page_description": "Configure display settings and hardware options for your JetKVM device", "hardware_page_description": "Configure display settings and hardware options for your JetKVM device",
"hardware_time_10_minutes": "10 Minutes",
"hardware_time_1_hour": "1 Hour", "hardware_time_1_hour": "1 Hour",
"hardware_time_1_minute": "1 Minute", "hardware_time_1_minute": "1 Minute",
"hardware_time_10_minutes": "10 Minutes",
"hardware_time_30_minutes": "30 Minutes", "hardware_time_30_minutes": "30 Minutes",
"hardware_time_5_minutes": "5 Minutes", "hardware_time_5_minutes": "5 Minutes",
"hardware_time_never": "Never", "hardware_time_never": "Never",
@ -327,7 +327,6 @@
"invalid_password": "Invalid password", "invalid_password": "Invalid password",
"ip_address": "IP Address", "ip_address": "IP Address",
"ipv6_address_label": "Address", "ipv6_address_label": "Address",
"ipv6_gateway": "Gateway",
"ipv6_information": "IPv6 Information", "ipv6_information": "IPv6 Information",
"ipv6_link_local": "Link-local", "ipv6_link_local": "Link-local",
"ipv6_preferred_lifetime": "Preferred Lifetime", "ipv6_preferred_lifetime": "Preferred Lifetime",
@ -410,8 +409,8 @@
"log_in": "Log In", "log_in": "Log In",
"log_out": "Log out", "log_out": "Log out",
"logged_in_as": "Logged in as", "logged_in_as": "Logged in as",
"login_enter_password": "Enter your password",
"login_enter_password_description": "Enter your password to access your JetKVM.", "login_enter_password_description": "Enter your password to access your JetKVM.",
"login_enter_password": "Enter your password",
"login_error": "An error occurred while logging in", "login_error": "An error occurred while logging in",
"login_forgot_password": "Forgot password?", "login_forgot_password": "Forgot password?",
"login_password_label": "Password", "login_password_label": "Password",
@ -425,8 +424,8 @@
"macro_name_required": "Name is required", "macro_name_required": "Name is required",
"macro_name_too_long": "Name must be less than 50 characters", "macro_name_too_long": "Name must be less than 50 characters",
"macro_please_fix_validation_errors": "Please fix the validation errors", "macro_please_fix_validation_errors": "Please fix the validation errors",
"macro_save": "Save Macro",
"macro_save_error": "An error occurred while saving.", "macro_save_error": "An error occurred while saving.",
"macro_save": "Save Macro",
"macro_step_count": "{steps} / {max} steps", "macro_step_count": "{steps} / {max} steps",
"macro_step_duration_description": "Time to wait before executing the next step.", "macro_step_duration_description": "Time to wait before executing the next step.",
"macro_step_duration_label": "Step Duration", "macro_step_duration_label": "Step Duration",
@ -440,8 +439,8 @@
"macro_steps_description": "Keys/modifiers executed in sequence with a delay between each step.", "macro_steps_description": "Keys/modifiers executed in sequence with a delay between each step.",
"macro_steps_label": "Steps", "macro_steps_label": "Steps",
"macros_add_description": "Create a new keyboard macro", "macros_add_description": "Create a new keyboard macro",
"macros_add_new": "Add New Macro",
"macros_add_new_macro": "Add New Macro", "macros_add_new_macro": "Add New Macro",
"macros_add_new": "Add New Macro",
"macros_aria_add_new": "Add new macro", "macros_aria_add_new": "Add new macro",
"macros_aria_delete": "Delete macro {name}", "macros_aria_delete": "Delete macro {name}",
"macros_aria_duplicate": "Duplicate macro {name}", "macros_aria_duplicate": "Duplicate macro {name}",
@ -463,16 +462,16 @@
"macros_edit_button": "Edit", "macros_edit_button": "Edit",
"macros_edit_description": "Modify your keyboard macro", "macros_edit_description": "Modify your keyboard macro",
"macros_edit_title": "Edit Macro", "macros_edit_title": "Edit Macro",
"macros_failed_create": "Failed to create macro",
"macros_failed_create_error": "Failed to create macro: {error}", "macros_failed_create_error": "Failed to create macro: {error}",
"macros_failed_delete": "Failed to delete macro", "macros_failed_create": "Failed to create macro",
"macros_failed_delete_error": "Failed to delete macro: {error}", "macros_failed_delete_error": "Failed to delete macro: {error}",
"macros_failed_duplicate": "Failed to duplicate macro", "macros_failed_delete": "Failed to delete macro",
"macros_failed_duplicate_error": "Failed to duplicate macro: {error}", "macros_failed_duplicate_error": "Failed to duplicate macro: {error}",
"macros_failed_reorder": "Failed to reorder macros", "macros_failed_duplicate": "Failed to duplicate macro",
"macros_failed_reorder_error": "Failed to reorder macros: {error}", "macros_failed_reorder_error": "Failed to reorder macros: {error}",
"macros_failed_update": "Failed to update macro", "macros_failed_reorder": "Failed to reorder macros",
"macros_failed_update_error": "Failed to update macro: {error}", "macros_failed_update_error": "Failed to update macro: {error}",
"macros_failed_update": "Failed to update macro",
"macros_invalid_data": "Invalid macro data", "macros_invalid_data": "Invalid macro data",
"macros_loading": "Loading macros…", "macros_loading": "Loading macros…",
"macros_max_reached": "Max Reached", "macros_max_reached": "Max Reached",
@ -507,8 +506,8 @@
"mount_error_list_storage": "Error listing storage files: {error}", "mount_error_list_storage": "Error listing storage files: {error}",
"mount_error_title": "Mount Error", "mount_error_title": "Mount Error",
"mount_get_state_error": "Failed to get virtual media state: {error}", "mount_get_state_error": "Failed to get virtual media state: {error}",
"mount_jetkvm_storage": "JetKVM Storage Mount",
"mount_jetkvm_storage_description": "Mount previously uploaded files from the JetKVM storage", "mount_jetkvm_storage_description": "Mount previously uploaded files from the JetKVM storage",
"mount_jetkvm_storage": "JetKVM Storage Mount",
"mount_mode_cdrom": "CD/DVD", "mount_mode_cdrom": "CD/DVD",
"mount_mode_disk": "Disk", "mount_mode_disk": "Disk",
"mount_mounted_as": "Mounted as", "mount_mounted_as": "Mounted as",
@ -521,8 +520,8 @@
"mount_popular_images": "Popular images", "mount_popular_images": "Popular images",
"mount_streaming_from_url": "Streaming from URL", "mount_streaming_from_url": "Streaming from URL",
"mount_supported_formats": "Supported formats: ISO, IMG", "mount_supported_formats": "Supported formats: ISO, IMG",
"mount_unmount": "Unmount",
"mount_unmount_error": "Failed to unmount image: {error}", "mount_unmount_error": "Failed to unmount image: {error}",
"mount_unmount": "Unmount",
"mount_upload_description": "Select an image file to upload to JetKVM storage", "mount_upload_description": "Select an image file to upload to JetKVM storage",
"mount_upload_error": "Upload error: {error}", "mount_upload_error": "Upload error: {error}",
"mount_upload_failed_datachannel": "Failed to create data channel for file upload", "mount_upload_failed_datachannel": "Failed to create data channel for file upload",
@ -530,8 +529,8 @@
"mount_upload_successful": "Upload successful", "mount_upload_successful": "Upload successful",
"mount_upload_title": "Upload New Image", "mount_upload_title": "Upload New Image",
"mount_uploaded_has_been_uploaded": "{name} has been uploaded", "mount_uploaded_has_been_uploaded": "{name} has been uploaded",
"mount_uploading": "Uploading…",
"mount_uploading_with_name": "Uploading {name}", "mount_uploading_with_name": "Uploading {name}",
"mount_uploading": "Uploading…",
"mount_url_description": "Mount files from any public web address", "mount_url_description": "Mount files from any public web address",
"mount_url_input_label": "Image URL", "mount_url_input_label": "Image URL",
"mount_url_mount": "URL Mount", "mount_url_mount": "URL Mount",
@ -539,10 +538,10 @@
"mount_view_device_title": "Mount from JetKVM Storage", "mount_view_device_title": "Mount from JetKVM Storage",
"mount_view_url_description": "Enter an URL to the image file to mount", "mount_view_url_description": "Enter an URL to the image file to mount",
"mount_view_url_title": "Mount from URL", "mount_view_url_title": "Mount from URL",
"mount_virtual_media": "Virtual Media",
"mount_virtual_media_description": "Mount an image to boot from or install an operating system.", "mount_virtual_media_description": "Mount an image to boot from or install an operating system.",
"mount_virtual_media_source": "Virtual Media Source",
"mount_virtual_media_source_description": "Choose how you want to mount your virtual media", "mount_virtual_media_source_description": "Choose how you want to mount your virtual media",
"mount_virtual_media_source": "Virtual Media Source",
"mount_virtual_media": "Virtual Media",
"mouse_alt_finger": "Finger touching a screen", "mouse_alt_finger": "Finger touching a screen",
"mouse_alt_mouse": "Mouse icon", "mouse_alt_mouse": "Mouse icon",
"mouse_description": "Configure cursor behavior and interaction settings for your device", "mouse_description": "Configure cursor behavior and interaction settings for your device",
@ -559,10 +558,10 @@
"mouse_jiggler_light": "Light - 5m", "mouse_jiggler_light": "Light - 5m",
"mouse_jiggler_standard": "Standard - 1m", "mouse_jiggler_standard": "Standard - 1m",
"mouse_jiggler_title": "Jiggler", "mouse_jiggler_title": "Jiggler",
"mouse_mode_absolute": "Absolute",
"mouse_mode_absolute_description": "Most convenient", "mouse_mode_absolute_description": "Most convenient",
"mouse_mode_relative": "Relative", "mouse_mode_absolute": "Absolute",
"mouse_mode_relative_description": "Most compatible", "mouse_mode_relative_description": "Most compatible",
"mouse_mode_relative": "Relative",
"mouse_modes_description": "Choose the mouse input mode", "mouse_modes_description": "Choose the mouse input mode",
"mouse_modes_title": "Modes", "mouse_modes_title": "Modes",
"mouse_scroll_high": "High", "mouse_scroll_high": "High",
@ -575,17 +574,12 @@
"mouse_title": "Mouse", "mouse_title": "Mouse",
"network_custom_domain": "Custom Domain", "network_custom_domain": "Custom Domain",
"network_description": "Configure your network settings", "network_description": "Configure your network settings",
"network_dhcp_client_description": "Configure which DHCP client to use",
"network_dhcp_client_jetkvm": "JetKVM Internal",
"network_dhcp_client_title": "DHCP Client",
"network_dhcp_information": "DHCP Information", "network_dhcp_information": "DHCP Information",
"network_dhcp_lease_renew": "Renew DHCP Lease",
"network_dhcp_lease_renew_confirm": "Renew Lease",
"network_dhcp_lease_renew_confirm_description": "This will request a new IP address from your DHCP server. Your device may temporarily lose network connectivity during this process.", "network_dhcp_lease_renew_confirm_description": "This will request a new IP address from your DHCP server. Your device may temporarily lose network connectivity during this process.",
"network_dhcp_lease_renew_confirm_new_a": "If you receive a new IP address", "network_dhcp_lease_renew_confirm": "Renew Lease",
"network_dhcp_lease_renew_confirm_new_b": "you may need to reconnect using the new address",
"network_dhcp_lease_renew_failed": "Failed to renew lease: {error}", "network_dhcp_lease_renew_failed": "Failed to renew lease: {error}",
"network_dhcp_lease_renew_success": "DHCP lease renewed", "network_dhcp_lease_renew_success": "DHCP lease renewed",
"network_dhcp_lease_renew": "Renew DHCP Lease",
"network_domain_custom": "Custom", "network_domain_custom": "Custom",
"network_domain_description": "Network domain suffix for the device", "network_domain_description": "Network domain suffix for the device",
"network_domain_dhcp_provided": "DHCP provided", "network_domain_dhcp_provided": "DHCP provided",
@ -594,35 +588,21 @@
"network_hostname_description": "Device identifier on the network. Blank for system default", "network_hostname_description": "Device identifier on the network. Blank for system default",
"network_hostname_title": "Hostname", "network_hostname_title": "Hostname",
"network_http_proxy_description": "Proxy server for outgoing HTTP(S) requests from the device. Blank for none.", "network_http_proxy_description": "Proxy server for outgoing HTTP(S) requests from the device. Blank for none.",
"network_http_proxy_invalid": "Invalid HTTP proxy URL",
"network_http_proxy_title": "HTTP Proxy", "network_http_proxy_title": "HTTP Proxy",
"network_ipv4_address": "IPv4 Address",
"network_ipv4_dns": "IPv4 DNS",
"network_ipv4_gateway": "IPv4 Gateway",
"network_ipv4_mode_description": "Configure the IPv4 mode", "network_ipv4_mode_description": "Configure the IPv4 mode",
"network_ipv4_mode_dhcp": "DHCP", "network_ipv4_mode_dhcp": "DHCP",
"network_ipv4_mode_static": "Static",
"network_ipv4_mode_title": "IPv4 Mode", "network_ipv4_mode_title": "IPv4 Mode",
"network_ipv4_netmask": "IPv4 Netmask",
"network_ipv6_address": "IPv6 Address",
"network_ipv6_information": "IPv6 Information", "network_ipv6_information": "IPv6 Information",
"network_ipv6_mode_description": "Configure the IPv6 mode", "network_ipv6_mode_description": "Configure the IPv6 mode",
"network_ipv6_mode_dhcpv6": "DHCPv6",
"network_ipv6_mode_disabled": "Disabled", "network_ipv6_mode_disabled": "Disabled",
"network_ipv6_mode_link_local": "Link-local only",
"network_ipv6_mode_slaac": "SLAAC", "network_ipv6_mode_slaac": "SLAAC",
"network_ipv6_mode_slaac_dhcpv6": "SLAAC + DHCPv6",
"network_ipv6_mode_static": "Static",
"network_ipv6_mode_title": "IPv6 Mode", "network_ipv6_mode_title": "IPv6 Mode",
"network_ipv6_netmask": "IPv6 Netmask",
"network_ipv6_no_addresses": "No IPv6 addresses configured", "network_ipv6_no_addresses": "No IPv6 addresses configured",
"network_ll_dp_all": "All", "network_ll_dp_all": "All",
"network_ll_dp_basic": "Basic", "network_ll_dp_basic": "Basic",
"network_ll_dp_description": "Control which TLVs will be sent over Link Layer Discovery Protocol", "network_ll_dp_description": "Control which TLVs will be sent over Link Layer Discovery Protocol",
"network_ll_dp_disabled": "Disabled", "network_ll_dp_disabled": "Disabled",
"network_ll_dp_title": "LLDP", "network_ll_dp_title": "LLDP",
"network_mac_address_copy_error": "Failed to copy MAC address",
"network_mac_address_copy_success": "MAC address { mac } copied to clipboard",
"network_mac_address_description": "Hardware identifier for the network interface", "network_mac_address_description": "Hardware identifier for the network interface",
"network_mac_address_title": "MAC Address", "network_mac_address_title": "MAC Address",
"network_mdns_auto": "Auto", "network_mdns_auto": "Auto",
@ -632,19 +612,9 @@
"network_mdns_ipv6_only": "IPv6 only", "network_mdns_ipv6_only": "IPv6 only",
"network_mdns_title": "mDNS", "network_mdns_title": "mDNS",
"network_no_dhcp_lease": "No DHCP lease information available", "network_no_dhcp_lease": "No DHCP lease information available",
"network_no_information_description": "No network configuration available",
"network_no_information_headline": "Network Information",
"network_pending_dhcp_mode_change_description": "Save settings to enable DHCP mode and view lease information",
"network_pending_dhcp_mode_change_headline": "Pending DHCP IPv4 mode change",
"network_save_settings": "Save Settings",
"network_save_settings_apply_title": "Apply network settings",
"network_save_settings_confirm": "Apply changes",
"network_save_settings_confirm_description": "The following network settings will be applied. These changes may require a reboot and cause brief disconnection.",
"network_save_settings_confirm_heading": "Configuration changes",
"network_save_settings_failed": "Failed to save network settings: {error}", "network_save_settings_failed": "Failed to save network settings: {error}",
"network_save_settings_success": "Network settings saved", "network_save_settings_success": "Network settings saved",
"network_settings_invalid_ipv4_cidr": "Invalid CIDR notation for IPv4 address", "network_save_settings": "Save Settings",
"network_settings_load_error": "Failed to load network settings: {error}",
"network_time_sync_description": "Configure time synchronization settings", "network_time_sync_description": "Configure time synchronization settings",
"network_time_sync_http_only": "HTTP only", "network_time_sync_http_only": "HTTP only",
"network_time_sync_ntp_and_http": "NTP and HTTP", "network_time_sync_ntp_and_http": "NTP and HTTP",
@ -670,8 +640,8 @@
"paste_modal_failed_paste": "Failed to paste text: {error}", "paste_modal_failed_paste": "Failed to paste text: {error}",
"paste_modal_invalid_chars_intro": "The following characters won't be pasted:", "paste_modal_invalid_chars_intro": "The following characters won't be pasted:",
"paste_modal_paste_from_host": "Paste from host", "paste_modal_paste_from_host": "Paste from host",
"paste_modal_paste_text": "Paste text",
"paste_modal_paste_text_description": "Paste text from your client to the remote host", "paste_modal_paste_text_description": "Paste text from your client to the remote host",
"paste_modal_paste_text": "Paste text",
"paste_modal_sending_using_layout": "Sending text using keyboard layout: {iso}-{name}", "paste_modal_sending_using_layout": "Sending text using keyboard layout: {iso}-{name}",
"peer_connection_closed": "Closed", "peer_connection_closed": "Closed",
"peer_connection_closing": "Closing", "peer_connection_closing": "Closing",
@ -698,20 +668,20 @@
"retry": "Retry", "retry": "Retry",
"saving": "Saving…", "saving": "Saving…",
"search_placeholder": "Search…", "search_placeholder": "Search…",
"serial_console": "Serial Console",
"serial_console_baud_rate": "Baud Rate", "serial_console_baud_rate": "Baud Rate",
"serial_console_configure_description": "Configure your serial console settings", "serial_console_configure_description": "Configure your serial console settings",
"serial_console_data_bits": "Data Bits", "serial_console_data_bits": "Data Bits",
"serial_console_get_settings_error": "Failed to get serial console settings: {error}", "serial_console_get_settings_error": "Failed to get serial console settings: {error}",
"serial_console_open_console": "Open Console", "serial_console_open_console": "Open Console",
"serial_console_parity": "Parity",
"serial_console_parity_even": "Even Parity", "serial_console_parity_even": "Even Parity",
"serial_console_parity_mark": "Mark Parity", "serial_console_parity_mark": "Mark Parity",
"serial_console_parity_none": "No Parity", "serial_console_parity_none": "No Parity",
"serial_console_parity_odd": "Odd Parity", "serial_console_parity_odd": "Odd Parity",
"serial_console_parity_space": "Space Parity", "serial_console_parity_space": "Space Parity",
"serial_console_parity": "Parity",
"serial_console_set_settings_error": "Failed to set serial console settings to {settings}: {error}", "serial_console_set_settings_error": "Failed to set serial console settings to {settings}: {error}",
"serial_console_stop_bits": "Stop Bits", "serial_console_stop_bits": "Stop Bits",
"serial_console": "Serial Console",
"setting_remote_description": "Setting remote description", "setting_remote_description": "Setting remote description",
"setting_remote_session_description": "Setting remote session description...", "setting_remote_session_description": "Setting remote session description...",
"setting_up_connection_to_device": "Setting up connection to device...", "setting_up_connection_to_device": "Setting up connection to device...",
@ -721,8 +691,8 @@
"settings_back_to_kvm": "Back to KVM", "settings_back_to_kvm": "Back to KVM",
"settings_general": "General", "settings_general": "General",
"settings_hardware": "Hardware", "settings_hardware": "Hardware",
"settings_keyboard": "Keyboard",
"settings_keyboard_macros": "Keyboard Macros", "settings_keyboard_macros": "Keyboard Macros",
"settings_keyboard": "Keyboard",
"settings_mouse": "Mouse", "settings_mouse": "Mouse",
"settings_network": "Network", "settings_network": "Network",
"settings_video": "Video", "settings_video": "Video",
@ -742,7 +712,6 @@
"updates_failed_check": "Failed to check for updates: {error}", "updates_failed_check": "Failed to check for updates: {error}",
"updates_failed_get_device_version": "Failed to get device version: {error}", "updates_failed_get_device_version": "Failed to get device version: {error}",
"updating_leave_device_on": "Please don't turn off your device…", "updating_leave_device_on": "Please don't turn off your device…",
"usb": "USB",
"usb_config_custom": "Custom", "usb_config_custom": "Custom",
"usb_config_default": "JetKVM Default", "usb_config_default": "JetKVM Default",
"usb_config_dell": "Dell Multimedia Pro Keyboard", "usb_config_dell": "Dell Multimedia Pro Keyboard",
@ -789,6 +758,7 @@
"usb_state_connecting": "Connecting", "usb_state_connecting": "Connecting",
"usb_state_disconnected": "Disconnected", "usb_state_disconnected": "Disconnected",
"usb_state_low_power_mode": "Low power mode", "usb_state_low_power_mode": "Low power mode",
"usb": "USB",
"user_interface_language_description": "Select the language to use in the JetKVM user interface", "user_interface_language_description": "Select the language to use in the JetKVM user interface",
"user_interface_language_title": "Interface Language", "user_interface_language_title": "Interface Language",
"video_brightness_description": "Brightness level ({value}x)", "video_brightness_description": "Brightness level ({value}x)",
@ -855,7 +825,6 @@
"video_title": "Video", "video_title": "Video",
"view_details": "View Details", "view_details": "View Details",
"virtual_keyboard_header": "Virtual Keyboard", "virtual_keyboard_header": "Virtual Keyboard",
"wake_on_lan": "Wake On LAN",
"wake_on_lan_add_device_device_name": "Device Name", "wake_on_lan_add_device_device_name": "Device Name",
"wake_on_lan_add_device_example_device_name": "Plex Media Server", "wake_on_lan_add_device_example_device_name": "Plex Media Server",
"wake_on_lan_add_device_mac_address": "MAC Address", "wake_on_lan_add_device_mac_address": "MAC Address",
@ -871,6 +840,7 @@
"wake_on_lan_failed_send_magic": "Failed to send Magic Packet", "wake_on_lan_failed_send_magic": "Failed to send Magic Packet",
"wake_on_lan_invalid_mac": "Invalid MAC address", "wake_on_lan_invalid_mac": "Invalid MAC address",
"wake_on_lan_magic_sent_success": "Magic Packet sent successfully", "wake_on_lan_magic_sent_success": "Magic Packet sent successfully",
"welcome_to_jetkvm": "Welcome to JetKVM", "wake_on_lan": "Wake On LAN",
"welcome_to_jetkvm_description": "Control any computer remotely" "welcome_to_jetkvm_description": "Control any computer remotely",
"welcome_to_jetkvm": "Welcome to JetKVM"
} }

View File

@ -107,10 +107,10 @@
"already_adopted_title": "Dispositivo ya registrado", "already_adopted_title": "Dispositivo ya registrado",
"appearance_description": "Elige tu tema de color preferido", "appearance_description": "Elige tu tema de color preferido",
"appearance_page_description": "Personalice la apariencia de su interfaz JetKVM", "appearance_page_description": "Personalice la apariencia de su interfaz JetKVM",
"appearance_theme": "Tema",
"appearance_theme_dark": "Oscuro", "appearance_theme_dark": "Oscuro",
"appearance_theme_light": "Luz", "appearance_theme_light": "Luz",
"appearance_theme_system": "Sistema", "appearance_theme_system": "Sistema",
"appearance_theme": "Tema",
"appearance_title": "Apariencia", "appearance_title": "Apariencia",
"attach": "Adjuntar", "attach": "Adjuntar",
"atx_power_control_get_state_error": "No se pudo obtener el estado de energía ATX: {error}", "atx_power_control_get_state_error": "No se pudo obtener el estado de energía ATX: {error}",
@ -121,85 +121,85 @@
"atx_power_control_reset_button": "Reiniciar", "atx_power_control_reset_button": "Reiniciar",
"atx_power_control_send_action_error": "No se pudo enviar la acción de alimentación ATX {action} : {error}", "atx_power_control_send_action_error": "No se pudo enviar la acción de alimentación ATX {action} : {error}",
"atx_power_control_short_power_button": "Prensa corta", "atx_power_control_short_power_button": "Prensa corta",
"auth_authentication_mode": "Por favor seleccione un modo de autenticación",
"auth_authentication_mode_error": "Se produjo un error al configurar el modo de autenticación", "auth_authentication_mode_error": "Se produjo un error al configurar el modo de autenticación",
"auth_authentication_mode_invalid": "Modo de autenticación no válido", "auth_authentication_mode_invalid": "Modo de autenticación no válido",
"auth_connect_to_cloud": "Conecte su JetKVM a la nube", "auth_authentication_mode": "Por favor seleccione un modo de autenticación",
"auth_connect_to_cloud_action": "Iniciar sesión y conectar el dispositivo", "auth_connect_to_cloud_action": "Iniciar sesión y conectar el dispositivo",
"auth_connect_to_cloud_description": "Desbloquee el acceso remoto y las funciones avanzadas para su dispositivo", "auth_connect_to_cloud_description": "Desbloquee el acceso remoto y las funciones avanzadas para su dispositivo",
"auth_connect_to_cloud": "Conecte su JetKVM a la nube",
"auth_header_cta_already_have_account": "¿Ya tienes una cuenta?", "auth_header_cta_already_have_account": "¿Ya tienes una cuenta?",
"auth_header_cta_dont_have_account": "¿No tienes una cuenta?", "auth_header_cta_dont_have_account": "¿No tienes una cuenta?",
"auth_header_cta_new_to_jetkvm": "¿Eres nuevo en JetKVM?", "auth_header_cta_new_to_jetkvm": "¿Eres nuevo en JetKVM?",
"auth_login": "Inicie sesión en su cuenta JetKVM",
"auth_login_action": "Acceso", "auth_login_action": "Acceso",
"auth_login_description": "Inicie sesión para acceder y administrar sus dispositivos de forma segura", "auth_login_description": "Inicie sesión para acceder y administrar sus dispositivos de forma segura",
"auth_mode_local": "Método de autenticación local", "auth_login": "Inicie sesión en su cuenta JetKVM",
"auth_mode_local_change_later": "Siempre puedes cambiar tu método de autenticación más tarde en la configuración.", "auth_mode_local_change_later": "Siempre puedes cambiar tu método de autenticación más tarde en la configuración.",
"auth_mode_local_description": "Seleccione cómo desea proteger su dispositivo JetKVM localmente.", "auth_mode_local_description": "Seleccione cómo desea proteger su dispositivo JetKVM localmente.",
"auth_mode_local_no_password": "Sin contraseña",
"auth_mode_local_no_password_description": "Acceso rápido sin autenticación de contraseña.", "auth_mode_local_no_password_description": "Acceso rápido sin autenticación de contraseña.",
"auth_mode_local_password": "Contraseña", "auth_mode_local_no_password": "Sin contraseña",
"auth_mode_local_password_confirm_description": "Confirma tu contraseña", "auth_mode_local_password_confirm_description": "Confirma tu contraseña",
"auth_mode_local_password_confirm_label": "confirmar Contraseña", "auth_mode_local_password_confirm_label": "confirmar Contraseña",
"auth_mode_local_password_description": "Proteja su dispositivo con una contraseña para mayor protección.", "auth_mode_local_password_description": "Proteja su dispositivo con una contraseña para mayor protección.",
"auth_mode_local_password_do_not_match": "Las contraseñas no coinciden", "auth_mode_local_password_do_not_match": "Las contraseñas no coinciden",
"auth_mode_local_password_failed_set": "No se pudo establecer la contraseña: {error}", "auth_mode_local_password_failed_set": "No se pudo establecer la contraseña: {error}",
"auth_mode_local_password_note": "Esta contraseña se utilizará para proteger los datos de su dispositivo y contra accesos no autorizados.",
"auth_mode_local_password_note_local": "Todos los datos permanecen en su dispositivo local.", "auth_mode_local_password_note_local": "Todos los datos permanecen en su dispositivo local.",
"auth_mode_local_password_set": "Establecer una contraseña", "auth_mode_local_password_note": "Esta contraseña se utilizará para proteger los datos de su dispositivo y contra accesos no autorizados.",
"auth_mode_local_password_set_button": "Establecer contraseña", "auth_mode_local_password_set_button": "Establecer contraseña",
"auth_mode_local_password_set_description": "Cree una contraseña segura para proteger su dispositivo JetKVM localmente.", "auth_mode_local_password_set_description": "Cree una contraseña segura para proteger su dispositivo JetKVM localmente.",
"auth_mode_local_password_set_label": "Introduzca una contraseña", "auth_mode_local_password_set_label": "Introduzca una contraseña",
"auth_mode_local_password_set": "Establecer una contraseña",
"auth_mode_local_password": "Contraseña",
"auth_mode_local": "Método de autenticación local",
"auth_signup_connect_to_cloud_action": "Registrarse y conectar dispositivo", "auth_signup_connect_to_cloud_action": "Registrarse y conectar dispositivo",
"auth_signup_create_account": "Crea tu cuenta JetKVM",
"auth_signup_create_account_action": "Crear una cuenta", "auth_signup_create_account_action": "Crear una cuenta",
"auth_signup_create_account_description": "Crea tu cuenta y comienza a administrar tus dispositivos con facilidad.", "auth_signup_create_account_description": "Crea tu cuenta y comienza a administrar tus dispositivos con facilidad.",
"back": "Atrás", "auth_signup_create_account": "Crea tu cuenta JetKVM",
"back_to_devices": "Volver a Dispositivos", "back_to_devices": "Volver a Dispositivos",
"back": "Atrás",
"cancel": "Cancelar", "cancel": "Cancelar",
"close": "Cerca", "close": "Cerca",
"cloud_kvms": "KVM en la nube",
"cloud_kvms_description": "Administre sus KVM en la nube y conéctese a ellos de forma segura.", "cloud_kvms_description": "Administre sus KVM en la nube y conéctese a ellos de forma segura.",
"cloud_kvms_no_devices": "No se encontraron dispositivos",
"cloud_kvms_no_devices_description": "Aún no tienes ningún dispositivo con JetKVM Cloud habilitado.", "cloud_kvms_no_devices_description": "Aún no tienes ningún dispositivo con JetKVM Cloud habilitado.",
"cloud_kvms_no_devices": "No se encontraron dispositivos",
"cloud_kvms": "KVM en la nube",
"confirm": "Confirmar", "confirm": "Confirmar",
"connect_to_kvm": "Conectarse a KVM", "connect_to_kvm": "Conectarse a KVM",
"connecting_to_device": "Conectando al dispositivo…", "connecting_to_device": "Conectando al dispositivo…",
"connection_established": "Conexión establecida", "connection_established": "Conexión establecida",
"connection_stats_badge_jitter": "Estar nervioso",
"connection_stats_badge_jitter_buffer_avg_delay": "Retardo promedio del búfer de fluctuación", "connection_stats_badge_jitter_buffer_avg_delay": "Retardo promedio del búfer de fluctuación",
"connection_stats_connection": "Conexión", "connection_stats_badge_jitter": "Estar nervioso",
"connection_stats_connection_description": "La conexión entre el cliente y JetKVM.", "connection_stats_connection_description": "La conexión entre el cliente y JetKVM.",
"connection_stats_frames_per_second": "Fotogramas por segundo", "connection_stats_connection": "Conexión",
"connection_stats_frames_per_second_description": "Número de fotogramas de vídeo entrantes que se muestran por segundo.", "connection_stats_frames_per_second_description": "Número de fotogramas de vídeo entrantes que se muestran por segundo.",
"connection_stats_network_stability": "Estabilidad de la red", "connection_stats_frames_per_second": "Fotogramas por segundo",
"connection_stats_network_stability_description": "Qué tan constante es el flujo de paquetes de vídeo entrantes a través de la red.", "connection_stats_network_stability_description": "Qué tan constante es el flujo de paquetes de vídeo entrantes a través de la red.",
"connection_stats_packets_lost": "Paquetes perdidos", "connection_stats_network_stability": "Estabilidad de la red",
"connection_stats_packets_lost_description": "Recuento de paquetes de vídeo RTP entrantes perdidos.", "connection_stats_packets_lost_description": "Recuento de paquetes de vídeo RTP entrantes perdidos.",
"connection_stats_playback_delay": "Retraso de reproducción", "connection_stats_packets_lost": "Paquetes perdidos",
"connection_stats_playback_delay_description": "Retraso agregado por el buffer de fluctuación para suavizar la reproducción cuando los cuadros llegan de manera desigual.", "connection_stats_playback_delay_description": "Retraso agregado por el buffer de fluctuación para suavizar la reproducción cuando los cuadros llegan de manera desigual.",
"connection_stats_round_trip_time": "Tiempo de ida y vuelta", "connection_stats_playback_delay": "Retraso de reproducción",
"connection_stats_round_trip_time_description": "Tiempo de ida y vuelta para el par de candidatos ICE activos entre pares.", "connection_stats_round_trip_time_description": "Tiempo de ida y vuelta para el par de candidatos ICE activos entre pares.",
"connection_stats_round_trip_time": "Tiempo de ida y vuelta",
"connection_stats_sidebar": "Estadísticas de conexión", "connection_stats_sidebar": "Estadísticas de conexión",
"connection_stats_video": "Video",
"connection_stats_video_description": "La transmisión de vídeo desde JetKVM al cliente.", "connection_stats_video_description": "La transmisión de vídeo desde JetKVM al cliente.",
"connection_stats_video": "Video",
"continue": "Continuar", "continue": "Continuar",
"creating_peer_connection": "Creando conexión entre pares…", "creating_peer_connection": "Creando conexión entre pares…",
"dc_power_control_current": "Actual",
"dc_power_control_current_unit": "A", "dc_power_control_current_unit": "A",
"dc_power_control_current": "Actual",
"dc_power_control_get_state_error": "No se pudo obtener el estado de la alimentación de CC: {error}", "dc_power_control_get_state_error": "No se pudo obtener el estado de la alimentación de CC: {error}",
"dc_power_control_power": "Fuerza",
"dc_power_control_power_off_button": "Apagado", "dc_power_control_power_off_button": "Apagado",
"dc_power_control_power_off_state": "Apagado", "dc_power_control_power_off_state": "Apagado",
"dc_power_control_power_on_button": "Encendido", "dc_power_control_power_on_button": "Encendido",
"dc_power_control_power_on_state": "Encendido", "dc_power_control_power_on_state": "Encendido",
"dc_power_control_power_unit": "O", "dc_power_control_power_unit": "O",
"dc_power_control_power": "Fuerza",
"dc_power_control_restore_last_state": "Último estado", "dc_power_control_restore_last_state": "Último estado",
"dc_power_control_restore_power_state": "Restaurar pérdida de energía", "dc_power_control_restore_power_state": "Restaurar pérdida de energía",
"dc_power_control_set_power_state_error": "No se pudo enviar el estado de alimentación de CC a {enabled} : {error}", "dc_power_control_set_power_state_error": "No se pudo enviar el estado de alimentación de CC a {enabled} : {error}",
"dc_power_control_set_restore_state_error": "No se pudo enviar el estado de restauración de energía de CC a {state} : {error}", "dc_power_control_set_restore_state_error": "No se pudo enviar el estado de restauración de energía de CC a {state} : {error}",
"dc_power_control_voltage": "Voltaje",
"dc_power_control_voltage_unit": "V", "dc_power_control_voltage_unit": "V",
"dc_power_control_voltage": "Voltaje",
"delete": "Borrar", "delete": "Borrar",
"deregister_button": "Darse de baja de la nube", "deregister_button": "Darse de baja de la nube",
"deregister_cloud_devices": "Dispositivos en la nube", "deregister_cloud_devices": "Dispositivos en la nube",
@ -226,12 +226,12 @@
"extension_popover_load_and_manage_extensions": "Cargar y administrar sus extensiones", "extension_popover_load_and_manage_extensions": "Cargar y administrar sus extensiones",
"extension_popover_set_error_notification": "No se pudo establecer la extensión activa: {error}", "extension_popover_set_error_notification": "No se pudo establecer la extensión activa: {error}",
"extension_popover_unload_extension": "Extensión de descarga", "extension_popover_unload_extension": "Extensión de descarga",
"extension_serial_console": "Consola serial",
"extension_serial_console_description": "Acceda a la extensión de su consola serie", "extension_serial_console_description": "Acceda a la extensión de su consola serie",
"extensions_atx_power_control": "Control de alimentación ATX", "extension_serial_console": "Consola serial",
"extensions_atx_power_control_description": "Controle el estado de energía de su máquina a través del control de energía ATX.", "extensions_atx_power_control_description": "Controle el estado de energía de su máquina a través del control de energía ATX.",
"extensions_dc_power_control": "Control de potencia de CC", "extensions_atx_power_control": "Control de alimentación ATX",
"extensions_dc_power_control_description": "Controle su extensión de alimentación de CC", "extensions_dc_power_control_description": "Controle su extensión de alimentación de CC",
"extensions_dc_power_control": "Control de potencia de CC",
"extensions_popover_extensions": "Extensiones", "extensions_popover_extensions": "Extensiones",
"gathering_ice_candidates": "Reuniendo candidatos del ICE…", "gathering_ice_candidates": "Reuniendo candidatos del ICE…",
"general_app_version": "Aplicación: {version}", "general_app_version": "Aplicación: {version}",
@ -241,8 +241,8 @@
"general_check_for_updates": "Buscar actualizaciones", "general_check_for_updates": "Buscar actualizaciones",
"general_page_description": "Configurar los ajustes del dispositivo y actualizar las preferencias", "general_page_description": "Configurar los ajustes del dispositivo y actualizar las preferencias",
"general_reboot_description": "¿Desea continuar con el reinicio del sistema?", "general_reboot_description": "¿Desea continuar con el reinicio del sistema?",
"general_reboot_device": "Reiniciar el dispositivo",
"general_reboot_device_description": "Apague y encienda el JetKVM", "general_reboot_device_description": "Apague y encienda el JetKVM",
"general_reboot_device": "Reiniciar el dispositivo",
"general_reboot_no_button": "No", "general_reboot_no_button": "No",
"general_reboot_title": "Reiniciar JetKVM", "general_reboot_title": "Reiniciar JetKVM",
"general_reboot_yes_button": "Sí", "general_reboot_yes_button": "Sí",
@ -295,9 +295,9 @@
"hardware_display_orientation_title": "Orientación de la pantalla", "hardware_display_orientation_title": "Orientación de la pantalla",
"hardware_display_wake_up_note": "La pantalla se activará cuando cambie el estado de la conexión o cuando se toque.", "hardware_display_wake_up_note": "La pantalla se activará cuando cambie el estado de la conexión o cuando se toque.",
"hardware_page_description": "Configure los ajustes de pantalla y las opciones de hardware para su dispositivo JetKVM", "hardware_page_description": "Configure los ajustes de pantalla y las opciones de hardware para su dispositivo JetKVM",
"hardware_time_10_minutes": "10 minutos",
"hardware_time_1_hour": "1 hora", "hardware_time_1_hour": "1 hora",
"hardware_time_1_minute": "1 minuto", "hardware_time_1_minute": "1 minuto",
"hardware_time_10_minutes": "10 minutos",
"hardware_time_30_minutes": "30 minutos", "hardware_time_30_minutes": "30 minutos",
"hardware_time_5_minutes": "5 minutos", "hardware_time_5_minutes": "5 minutos",
"hardware_time_never": "Nunca", "hardware_time_never": "Nunca",
@ -327,7 +327,6 @@
"invalid_password": "Contraseña inválida", "invalid_password": "Contraseña inválida",
"ip_address": "Dirección IP", "ip_address": "Dirección IP",
"ipv6_address_label": "DIRECCIÓN", "ipv6_address_label": "DIRECCIÓN",
"ipv6_gateway": "Puerta",
"ipv6_information": "Información de IPv6", "ipv6_information": "Información de IPv6",
"ipv6_link_local": "Enlace local", "ipv6_link_local": "Enlace local",
"ipv6_preferred_lifetime": "Vida útil preferida", "ipv6_preferred_lifetime": "Vida útil preferida",
@ -410,8 +409,8 @@
"log_in": "Acceso", "log_in": "Acceso",
"log_out": "Finalizar la sesión", "log_out": "Finalizar la sesión",
"logged_in_as": "Inició sesión como", "logged_in_as": "Inició sesión como",
"login_enter_password": "Ingrese su contraseña",
"login_enter_password_description": "Introduzca su contraseña para acceder a su JetKVM.", "login_enter_password_description": "Introduzca su contraseña para acceder a su JetKVM.",
"login_enter_password": "Ingrese su contraseña",
"login_error": "Se produjo un error al iniciar sesión", "login_error": "Se produjo un error al iniciar sesión",
"login_forgot_password": "¿Has olvidado tu contraseña?", "login_forgot_password": "¿Has olvidado tu contraseña?",
"login_password_label": "Contraseña", "login_password_label": "Contraseña",
@ -425,8 +424,8 @@
"macro_name_required": "El nombre es obligatorio", "macro_name_required": "El nombre es obligatorio",
"macro_name_too_long": "El nombre debe tener menos de 50 caracteres.", "macro_name_too_long": "El nombre debe tener menos de 50 caracteres.",
"macro_please_fix_validation_errors": "Por favor corrija los errores de validación", "macro_please_fix_validation_errors": "Por favor corrija los errores de validación",
"macro_save": "Guardar macro",
"macro_save_error": "Se produjo un error al guardar.", "macro_save_error": "Se produjo un error al guardar.",
"macro_save": "Guardar macro",
"macro_step_count": "{steps} / {max} pasos", "macro_step_count": "{steps} / {max} pasos",
"macro_step_duration_description": "Tiempo de espera antes de ejecutar el siguiente paso.", "macro_step_duration_description": "Tiempo de espera antes de ejecutar el siguiente paso.",
"macro_step_duration_label": "Duración del paso", "macro_step_duration_label": "Duración del paso",
@ -440,8 +439,8 @@
"macro_steps_description": "Teclas/modificadores que se ejecutan en secuencia con un retraso entre cada paso.", "macro_steps_description": "Teclas/modificadores que se ejecutan en secuencia con un retraso entre cada paso.",
"macro_steps_label": "Pasos", "macro_steps_label": "Pasos",
"macros_add_description": "Crear una nueva macro de teclado", "macros_add_description": "Crear una nueva macro de teclado",
"macros_add_new": "Agregar nueva macro",
"macros_add_new_macro": "Agregar nueva macro", "macros_add_new_macro": "Agregar nueva macro",
"macros_add_new": "Agregar nueva macro",
"macros_aria_add_new": "Agregar nueva macro", "macros_aria_add_new": "Agregar nueva macro",
"macros_aria_delete": "Eliminar macro {name}", "macros_aria_delete": "Eliminar macro {name}",
"macros_aria_duplicate": "Macro duplicada {name}", "macros_aria_duplicate": "Macro duplicada {name}",
@ -463,16 +462,16 @@
"macros_edit_button": "Editar", "macros_edit_button": "Editar",
"macros_edit_description": "Modificar la macro del teclado", "macros_edit_description": "Modificar la macro del teclado",
"macros_edit_title": "Editar macro", "macros_edit_title": "Editar macro",
"macros_failed_create": "No se pudo crear la macro",
"macros_failed_create_error": "No se pudo crear la macro: {error}", "macros_failed_create_error": "No se pudo crear la macro: {error}",
"macros_failed_delete": "No se pudo eliminar la macro", "macros_failed_create": "No se pudo crear la macro",
"macros_failed_delete_error": "No se pudo eliminar la macro: {error}", "macros_failed_delete_error": "No se pudo eliminar la macro: {error}",
"macros_failed_duplicate": "No se pudo duplicar la macro", "macros_failed_delete": "No se pudo eliminar la macro",
"macros_failed_duplicate_error": "No se pudo duplicar la macro: {error}", "macros_failed_duplicate_error": "No se pudo duplicar la macro: {error}",
"macros_failed_reorder": "No se pudieron reordenar las macros", "macros_failed_duplicate": "No se pudo duplicar la macro",
"macros_failed_reorder_error": "No se pudieron reordenar las macros: {error}", "macros_failed_reorder_error": "No se pudieron reordenar las macros: {error}",
"macros_failed_update": "No se pudo actualizar la macro", "macros_failed_reorder": "No se pudieron reordenar las macros",
"macros_failed_update_error": "No se pudo actualizar la macro: {error}", "macros_failed_update_error": "No se pudo actualizar la macro: {error}",
"macros_failed_update": "No se pudo actualizar la macro",
"macros_invalid_data": "Datos de macro no válidos", "macros_invalid_data": "Datos de macro no válidos",
"macros_loading": "Cargando macros…", "macros_loading": "Cargando macros…",
"macros_max_reached": "Máximo alcanzado", "macros_max_reached": "Máximo alcanzado",
@ -507,8 +506,8 @@
"mount_error_list_storage": "Error al listar archivos de almacenamiento: {error}", "mount_error_list_storage": "Error al listar archivos de almacenamiento: {error}",
"mount_error_title": "Error de montaje", "mount_error_title": "Error de montaje",
"mount_get_state_error": "No se pudo obtener el estado del medio virtual: {error}", "mount_get_state_error": "No se pudo obtener el estado del medio virtual: {error}",
"mount_jetkvm_storage": "Soporte de almacenamiento JetKVM",
"mount_jetkvm_storage_description": "Montar archivos cargados previamente desde el almacenamiento JetKVM", "mount_jetkvm_storage_description": "Montar archivos cargados previamente desde el almacenamiento JetKVM",
"mount_jetkvm_storage": "Soporte de almacenamiento JetKVM",
"mount_mode_cdrom": "CD/DVD", "mount_mode_cdrom": "CD/DVD",
"mount_mode_disk": "Disco", "mount_mode_disk": "Disco",
"mount_mounted_as": "Montado como", "mount_mounted_as": "Montado como",
@ -521,8 +520,8 @@
"mount_popular_images": "Imágenes populares", "mount_popular_images": "Imágenes populares",
"mount_streaming_from_url": "Transmisión desde URL", "mount_streaming_from_url": "Transmisión desde URL",
"mount_supported_formats": "Formatos compatibles: ISO, IMG", "mount_supported_formats": "Formatos compatibles: ISO, IMG",
"mount_unmount": "Desmontar",
"mount_unmount_error": "Error al desmontar la imagen: {error}", "mount_unmount_error": "Error al desmontar la imagen: {error}",
"mount_unmount": "Desmontar",
"mount_upload_description": "Seleccione un archivo de imagen para cargar al almacenamiento de JetKVM", "mount_upload_description": "Seleccione un archivo de imagen para cargar al almacenamiento de JetKVM",
"mount_upload_error": "Error de carga: {error}", "mount_upload_error": "Error de carga: {error}",
"mount_upload_failed_datachannel": "No se pudo crear el canal de datos para la carga de archivos", "mount_upload_failed_datachannel": "No se pudo crear el canal de datos para la carga de archivos",
@ -530,8 +529,8 @@
"mount_upload_successful": "Subida exitosa", "mount_upload_successful": "Subida exitosa",
"mount_upload_title": "Subir nueva imagen", "mount_upload_title": "Subir nueva imagen",
"mount_uploaded_has_been_uploaded": "Se ha cargado {name}", "mount_uploaded_has_been_uploaded": "Se ha cargado {name}",
"mount_uploading": "Subiendo…",
"mount_uploading_with_name": "Subiendo {name}", "mount_uploading_with_name": "Subiendo {name}",
"mount_uploading": "Subiendo…",
"mount_url_description": "Montar archivos desde cualquier dirección web pública", "mount_url_description": "Montar archivos desde cualquier dirección web pública",
"mount_url_input_label": "URL de la imagen", "mount_url_input_label": "URL de la imagen",
"mount_url_mount": "Montaje de URL", "mount_url_mount": "Montaje de URL",
@ -539,10 +538,10 @@
"mount_view_device_title": "Montar desde el almacenamiento JetKVM", "mount_view_device_title": "Montar desde el almacenamiento JetKVM",
"mount_view_url_description": "Introduzca una URL al archivo de imagen que desea montar", "mount_view_url_description": "Introduzca una URL al archivo de imagen que desea montar",
"mount_view_url_title": "Montar desde URL", "mount_view_url_title": "Montar desde URL",
"mount_virtual_media": "Medios virtuales",
"mount_virtual_media_description": "Montar una imagen para arrancar o instalar un sistema operativo.", "mount_virtual_media_description": "Montar una imagen para arrancar o instalar un sistema operativo.",
"mount_virtual_media_source": "Fuente de medios virtuales",
"mount_virtual_media_source_description": "Elige cómo quieres montar tus medios virtuales", "mount_virtual_media_source_description": "Elige cómo quieres montar tus medios virtuales",
"mount_virtual_media_source": "Fuente de medios virtuales",
"mount_virtual_media": "Medios virtuales",
"mouse_alt_finger": "Dedo tocando una pantalla", "mouse_alt_finger": "Dedo tocando una pantalla",
"mouse_alt_mouse": "Icono del ratón", "mouse_alt_mouse": "Icono del ratón",
"mouse_description": "Configure el comportamiento del cursor y los ajustes de interacción para su dispositivo", "mouse_description": "Configure el comportamiento del cursor y los ajustes de interacción para su dispositivo",
@ -559,10 +558,10 @@
"mouse_jiggler_light": "Luz - 5m", "mouse_jiggler_light": "Luz - 5m",
"mouse_jiggler_standard": "Estándar - 1 m", "mouse_jiggler_standard": "Estándar - 1 m",
"mouse_jiggler_title": "Jiggler", "mouse_jiggler_title": "Jiggler",
"mouse_mode_absolute": "Absoluto",
"mouse_mode_absolute_description": "Lo más conveniente", "mouse_mode_absolute_description": "Lo más conveniente",
"mouse_mode_relative": "Relativo", "mouse_mode_absolute": "Absoluto",
"mouse_mode_relative_description": "Más compatible", "mouse_mode_relative_description": "Más compatible",
"mouse_mode_relative": "Relativo",
"mouse_modes_description": "Elija el modo de entrada del mouse", "mouse_modes_description": "Elija el modo de entrada del mouse",
"mouse_modes_title": "Modos", "mouse_modes_title": "Modos",
"mouse_scroll_high": "Alto", "mouse_scroll_high": "Alto",
@ -575,17 +574,12 @@
"mouse_title": "Ratón", "mouse_title": "Ratón",
"network_custom_domain": "Dominio personalizado", "network_custom_domain": "Dominio personalizado",
"network_description": "Configurar sus ajustes de red", "network_description": "Configurar sus ajustes de red",
"network_dhcp_client_description": "Configurar qué cliente DHCP utilizar",
"network_dhcp_client_jetkvm": "JetKVM interno",
"network_dhcp_client_title": "Cliente DHCP",
"network_dhcp_information": "Información de DHCP", "network_dhcp_information": "Información de DHCP",
"network_dhcp_lease_renew": "Renovar la concesión de DHCP",
"network_dhcp_lease_renew_confirm": "Renovar el contrato de arrendamiento",
"network_dhcp_lease_renew_confirm_description": "Esto solicitará una nueva dirección IP a su servidor DHCP. Es posible que su dispositivo pierda temporalmente la conexión a la red durante este proceso.", "network_dhcp_lease_renew_confirm_description": "Esto solicitará una nueva dirección IP a su servidor DHCP. Es posible que su dispositivo pierda temporalmente la conexión a la red durante este proceso.",
"network_dhcp_lease_renew_confirm_new_a": "Si recibe una nueva dirección IP", "network_dhcp_lease_renew_confirm": "Renovar el contrato de arrendamiento",
"network_dhcp_lease_renew_confirm_new_b": "Es posible que necesites volver a conectarte usando la nueva dirección",
"network_dhcp_lease_renew_failed": "No se pudo renovar el contrato de arrendamiento: {error}", "network_dhcp_lease_renew_failed": "No se pudo renovar el contrato de arrendamiento: {error}",
"network_dhcp_lease_renew_success": "Se renovó la concesión de DHCP", "network_dhcp_lease_renew_success": "Se renovó la concesión de DHCP",
"network_dhcp_lease_renew": "Renovar la concesión de DHCP",
"network_domain_custom": "Costumbre", "network_domain_custom": "Costumbre",
"network_domain_description": "Sufijo de dominio de red para el dispositivo", "network_domain_description": "Sufijo de dominio de red para el dispositivo",
"network_domain_dhcp_provided": "DHCP proporcionado", "network_domain_dhcp_provided": "DHCP proporcionado",
@ -594,35 +588,21 @@
"network_hostname_description": "Identificador del dispositivo en la red. En blanco para el valor predeterminado del sistema.", "network_hostname_description": "Identificador del dispositivo en la red. En blanco para el valor predeterminado del sistema.",
"network_hostname_title": "Nombre de host", "network_hostname_title": "Nombre de host",
"network_http_proxy_description": "Servidor proxy para solicitudes HTTP(S) salientes desde el dispositivo. En blanco si no hay ninguna.", "network_http_proxy_description": "Servidor proxy para solicitudes HTTP(S) salientes desde el dispositivo. En blanco si no hay ninguna.",
"network_http_proxy_invalid": "URL de proxy HTTP no válida",
"network_http_proxy_title": "Proxy HTTP", "network_http_proxy_title": "Proxy HTTP",
"network_ipv4_address": "Dirección IPv4",
"network_ipv4_dns": "DNS IPv4",
"network_ipv4_gateway": "Puerta de enlace IPv4",
"network_ipv4_mode_description": "Configurar el modo IPv4", "network_ipv4_mode_description": "Configurar el modo IPv4",
"network_ipv4_mode_dhcp": "DHCP", "network_ipv4_mode_dhcp": "DHCP",
"network_ipv4_mode_static": "Estático",
"network_ipv4_mode_title": "Modo IPv4", "network_ipv4_mode_title": "Modo IPv4",
"network_ipv4_netmask": "Máscara de red IPv4",
"network_ipv6_address": "Dirección IPv6",
"network_ipv6_information": "Información de IPv6", "network_ipv6_information": "Información de IPv6",
"network_ipv6_mode_description": "Configurar el modo IPv6", "network_ipv6_mode_description": "Configurar el modo IPv6",
"network_ipv6_mode_dhcpv6": "DHCPv6",
"network_ipv6_mode_disabled": "Desactivado", "network_ipv6_mode_disabled": "Desactivado",
"network_ipv6_mode_link_local": "Solo enlace local",
"network_ipv6_mode_slaac": "SLAAC", "network_ipv6_mode_slaac": "SLAAC",
"network_ipv6_mode_slaac_dhcpv6": "SLAAC + DHCPv6",
"network_ipv6_mode_static": "Estático",
"network_ipv6_mode_title": "Modo IPv6", "network_ipv6_mode_title": "Modo IPv6",
"network_ipv6_netmask": "Máscara de red IPv6",
"network_ipv6_no_addresses": "No hay direcciones IPv6 configuradas", "network_ipv6_no_addresses": "No hay direcciones IPv6 configuradas",
"network_ll_dp_all": "Todo", "network_ll_dp_all": "Todo",
"network_ll_dp_basic": "Básico", "network_ll_dp_basic": "Básico",
"network_ll_dp_description": "Controlar qué TLV se enviarán a través del Protocolo de descubrimiento de capa de enlace", "network_ll_dp_description": "Controlar qué TLV se enviarán a través del Protocolo de descubrimiento de capa de enlace",
"network_ll_dp_disabled": "Desactivado", "network_ll_dp_disabled": "Desactivado",
"network_ll_dp_title": "LLDP", "network_ll_dp_title": "LLDP",
"network_mac_address_copy_error": "No se pudo copiar la dirección MAC",
"network_mac_address_copy_success": "Dirección MAC { mac } copiada al portapapeles",
"network_mac_address_description": "Identificador de hardware para la interfaz de red", "network_mac_address_description": "Identificador de hardware para la interfaz de red",
"network_mac_address_title": "Dirección MAC", "network_mac_address_title": "Dirección MAC",
"network_mdns_auto": "Auto", "network_mdns_auto": "Auto",
@ -632,19 +612,9 @@
"network_mdns_ipv6_only": "Sólo IPv6", "network_mdns_ipv6_only": "Sólo IPv6",
"network_mdns_title": "mDNS", "network_mdns_title": "mDNS",
"network_no_dhcp_lease": "No hay información de arrendamiento de DHCP disponible", "network_no_dhcp_lease": "No hay información de arrendamiento de DHCP disponible",
"network_no_information_description": "No hay configuración de red disponible",
"network_no_information_headline": "Información de la red",
"network_pending_dhcp_mode_change_description": "Guardar la configuración para habilitar el modo DHCP y ver la información de arrendamiento",
"network_pending_dhcp_mode_change_headline": "Cambio de modo DHCP IPv4 pendiente",
"network_save_settings": "Guardar configuración",
"network_save_settings_apply_title": "Aplicar configuración de red",
"network_save_settings_confirm": "Aplicar cambios",
"network_save_settings_confirm_description": "Se aplicará la siguiente configuración de red. Estos cambios pueden requerir un reinicio y causar una breve desconexión.",
"network_save_settings_confirm_heading": "Cambios de configuración",
"network_save_settings_failed": "No se pudo guardar la configuración de red: {error}", "network_save_settings_failed": "No se pudo guardar la configuración de red: {error}",
"network_save_settings_success": "Configuración de red guardada", "network_save_settings_success": "Configuración de red guardada",
"network_settings_invalid_ipv4_cidr": "Notación CIDR no válida para la dirección IPv4", "network_save_settings": "Guardar configuración",
"network_settings_load_error": "No se pudo cargar la configuración de red: {error}",
"network_time_sync_description": "Configurar los ajustes de sincronización horaria", "network_time_sync_description": "Configurar los ajustes de sincronización horaria",
"network_time_sync_http_only": "Sólo HTTP", "network_time_sync_http_only": "Sólo HTTP",
"network_time_sync_ntp_and_http": "NTP y HTTP", "network_time_sync_ntp_and_http": "NTP y HTTP",
@ -670,8 +640,8 @@
"paste_modal_failed_paste": "No se pudo pegar el texto: {error}", "paste_modal_failed_paste": "No se pudo pegar el texto: {error}",
"paste_modal_invalid_chars_intro": "Los siguientes caracteres no se pegarán:", "paste_modal_invalid_chars_intro": "Los siguientes caracteres no se pegarán:",
"paste_modal_paste_from_host": "Pegar desde el host", "paste_modal_paste_from_host": "Pegar desde el host",
"paste_modal_paste_text": "Pegar texto",
"paste_modal_paste_text_description": "Pegue el texto de su cliente al host remoto", "paste_modal_paste_text_description": "Pegue el texto de su cliente al host remoto",
"paste_modal_paste_text": "Pegar texto",
"paste_modal_sending_using_layout": "Envío de texto mediante la distribución del teclado: {iso} - {name}", "paste_modal_sending_using_layout": "Envío de texto mediante la distribución del teclado: {iso} - {name}",
"peer_connection_closed": "Cerrado", "peer_connection_closed": "Cerrado",
"peer_connection_closing": "Cierre", "peer_connection_closing": "Cierre",
@ -698,20 +668,20 @@
"retry": "Rever", "retry": "Rever",
"saving": "Ahorro…", "saving": "Ahorro…",
"search_placeholder": "Buscar…", "search_placeholder": "Buscar…",
"serial_console": "Consola serial",
"serial_console_baud_rate": "Tasa de Baud", "serial_console_baud_rate": "Tasa de Baud",
"serial_console_configure_description": "Configure los ajustes de su consola serie", "serial_console_configure_description": "Configure los ajustes de su consola serie",
"serial_console_data_bits": "Bits de datos", "serial_console_data_bits": "Bits de datos",
"serial_console_get_settings_error": "No se pudo obtener la configuración de la consola serial: {error}", "serial_console_get_settings_error": "No se pudo obtener la configuración de la consola serial: {error}",
"serial_console_open_console": "Consola abierta", "serial_console_open_console": "Consola abierta",
"serial_console_parity": "Paridad",
"serial_console_parity_even": "Paridad uniforme", "serial_console_parity_even": "Paridad uniforme",
"serial_console_parity_mark": "Paridad de marca", "serial_console_parity_mark": "Paridad de marca",
"serial_console_parity_none": "Sin paridad", "serial_console_parity_none": "Sin paridad",
"serial_console_parity_odd": "Paridad impar", "serial_console_parity_odd": "Paridad impar",
"serial_console_parity_space": "Paridad espacial", "serial_console_parity_space": "Paridad espacial",
"serial_console_parity": "Paridad",
"serial_console_set_settings_error": "No se pudieron establecer los ajustes de la consola serial en {settings} : {error}", "serial_console_set_settings_error": "No se pudieron establecer los ajustes de la consola serial en {settings} : {error}",
"serial_console_stop_bits": "Bits de parada", "serial_console_stop_bits": "Bits de parada",
"serial_console": "Consola serial",
"setting_remote_description": "Configuración de la descripción remota", "setting_remote_description": "Configuración de la descripción remota",
"setting_remote_session_description": "Establecer la descripción de la sesión remota...", "setting_remote_session_description": "Establecer la descripción de la sesión remota...",
"setting_up_connection_to_device": "Configurando la conexión al dispositivo...", "setting_up_connection_to_device": "Configurando la conexión al dispositivo...",
@ -721,8 +691,8 @@
"settings_back_to_kvm": "Volver a KVM", "settings_back_to_kvm": "Volver a KVM",
"settings_general": "General", "settings_general": "General",
"settings_hardware": "Hardware", "settings_hardware": "Hardware",
"settings_keyboard": "Teclado",
"settings_keyboard_macros": "Macros del teclado", "settings_keyboard_macros": "Macros del teclado",
"settings_keyboard": "Teclado",
"settings_mouse": "Ratón", "settings_mouse": "Ratón",
"settings_network": "Red", "settings_network": "Red",
"settings_video": "Video", "settings_video": "Video",
@ -742,7 +712,6 @@
"updates_failed_check": "No se pudieron buscar actualizaciones: {error}", "updates_failed_check": "No se pudieron buscar actualizaciones: {error}",
"updates_failed_get_device_version": "No se pudo obtener la versión del dispositivo: {error}", "updates_failed_get_device_version": "No se pudo obtener la versión del dispositivo: {error}",
"updating_leave_device_on": "Por favor, no apagues tu dispositivo…", "updating_leave_device_on": "Por favor, no apagues tu dispositivo…",
"usb": "USB",
"usb_config_custom": "Costumbre", "usb_config_custom": "Costumbre",
"usb_config_default": "JetKVM predeterminado", "usb_config_default": "JetKVM predeterminado",
"usb_config_dell": "Teclado multimedia Dell Pro", "usb_config_dell": "Teclado multimedia Dell Pro",
@ -789,6 +758,7 @@
"usb_state_connecting": "Conectando", "usb_state_connecting": "Conectando",
"usb_state_disconnected": "Desconectado", "usb_state_disconnected": "Desconectado",
"usb_state_low_power_mode": "Modo de bajo consumo", "usb_state_low_power_mode": "Modo de bajo consumo",
"usb": "USB",
"user_interface_language_description": "Seleccione el idioma que se utilizará en la interfaz de usuario de JetKVM", "user_interface_language_description": "Seleccione el idioma que se utilizará en la interfaz de usuario de JetKVM",
"user_interface_language_title": "Lenguaje de interfaz", "user_interface_language_title": "Lenguaje de interfaz",
"video_brightness_description": "Nivel de brillo ( {value} x)", "video_brightness_description": "Nivel de brillo ( {value} x)",
@ -855,7 +825,6 @@
"video_title": "Video", "video_title": "Video",
"view_details": "Ver detalles", "view_details": "Ver detalles",
"virtual_keyboard_header": "Teclado virtual", "virtual_keyboard_header": "Teclado virtual",
"wake_on_lan": "Activación en LAN",
"wake_on_lan_add_device_device_name": "Nombre del dispositivo", "wake_on_lan_add_device_device_name": "Nombre del dispositivo",
"wake_on_lan_add_device_example_device_name": "Servidor multimedia Plex", "wake_on_lan_add_device_example_device_name": "Servidor multimedia Plex",
"wake_on_lan_add_device_mac_address": "Dirección MAC", "wake_on_lan_add_device_mac_address": "Dirección MAC",
@ -871,6 +840,7 @@
"wake_on_lan_failed_send_magic": "No se pudo enviar el paquete mágico", "wake_on_lan_failed_send_magic": "No se pudo enviar el paquete mágico",
"wake_on_lan_invalid_mac": "Dirección MAC no válida", "wake_on_lan_invalid_mac": "Dirección MAC no válida",
"wake_on_lan_magic_sent_success": "Paquete mágico enviado con éxito", "wake_on_lan_magic_sent_success": "Paquete mágico enviado con éxito",
"welcome_to_jetkvm": "Bienvenido a JetKVM", "wake_on_lan": "Activación en LAN",
"welcome_to_jetkvm_description": "Controla cualquier computadora de forma remota" "welcome_to_jetkvm_description": "Controla cualquier computadora de forma remota",
"welcome_to_jetkvm": "Bienvenido a JetKVM"
} }

View File

@ -107,10 +107,10 @@
"already_adopted_title": "Appareil déjà enregistré", "already_adopted_title": "Appareil déjà enregistré",
"appearance_description": "Choisissez votre thème de couleur préféré", "appearance_description": "Choisissez votre thème de couleur préféré",
"appearance_page_description": "Personnalisez l'apparence de votre interface JetKVM", "appearance_page_description": "Personnalisez l'apparence de votre interface JetKVM",
"appearance_theme": "Thème",
"appearance_theme_dark": "Sombre", "appearance_theme_dark": "Sombre",
"appearance_theme_light": "Lumière", "appearance_theme_light": "Lumière",
"appearance_theme_system": "Système", "appearance_theme_system": "Système",
"appearance_theme": "Thème",
"appearance_title": "Apparence", "appearance_title": "Apparence",
"attach": "Attacher", "attach": "Attacher",
"atx_power_control_get_state_error": "Échec de l'obtention de l'état d'alimentation ATX : {error}", "atx_power_control_get_state_error": "Échec de l'obtention de l'état d'alimentation ATX : {error}",
@ -121,85 +121,85 @@
"atx_power_control_reset_button": "Réinitialiser", "atx_power_control_reset_button": "Réinitialiser",
"atx_power_control_send_action_error": "Échec de l'envoi de l'action d'alimentation ATX {action} : {error}", "atx_power_control_send_action_error": "Échec de l'envoi de l'action d'alimentation ATX {action} : {error}",
"atx_power_control_short_power_button": "Appui court", "atx_power_control_short_power_button": "Appui court",
"auth_authentication_mode": "Veuillez sélectionner un mode d'authentification",
"auth_authentication_mode_error": "Une erreur s'est produite lors de la définition du mode d'authentification", "auth_authentication_mode_error": "Une erreur s'est produite lors de la définition du mode d'authentification",
"auth_authentication_mode_invalid": "Mode d'authentification non valide", "auth_authentication_mode_invalid": "Mode d'authentification non valide",
"auth_connect_to_cloud": "Connectez votre JetKVM au cloud", "auth_authentication_mode": "Veuillez sélectionner un mode d'authentification",
"auth_connect_to_cloud_action": "Connectez-vous et connectez l'appareil", "auth_connect_to_cloud_action": "Connectez-vous et connectez l'appareil",
"auth_connect_to_cloud_description": "Déverrouillez l'accès à distance et les fonctionnalités avancées de votre appareil", "auth_connect_to_cloud_description": "Déverrouillez l'accès à distance et les fonctionnalités avancées de votre appareil",
"auth_connect_to_cloud": "Connectez votre JetKVM au cloud",
"auth_header_cta_already_have_account": "Vous avez déjà un compte ?", "auth_header_cta_already_have_account": "Vous avez déjà un compte ?",
"auth_header_cta_dont_have_account": "Vous n'avez pas de compte ?", "auth_header_cta_dont_have_account": "Vous n'avez pas de compte ?",
"auth_header_cta_new_to_jetkvm": "Nouveau sur JetKVM ?", "auth_header_cta_new_to_jetkvm": "Nouveau sur JetKVM ?",
"auth_login": "Connectez-vous à votre compte JetKVM",
"auth_login_action": "Se connecter", "auth_login_action": "Se connecter",
"auth_login_description": "Connectez-vous pour accéder et gérer vos appareils en toute sécurité", "auth_login_description": "Connectez-vous pour accéder et gérer vos appareils en toute sécurité",
"auth_mode_local": "Méthode d'authentification locale", "auth_login": "Connectez-vous à votre compte JetKVM",
"auth_mode_local_change_later": "Vous pouvez toujours modifier votre méthode d'authentification ultérieurement dans les paramètres.", "auth_mode_local_change_later": "Vous pouvez toujours modifier votre méthode d'authentification ultérieurement dans les paramètres.",
"auth_mode_local_description": "Sélectionnez la manière dont vous souhaitez sécuriser votre périphérique JetKVM localement.", "auth_mode_local_description": "Sélectionnez la manière dont vous souhaitez sécuriser votre périphérique JetKVM localement.",
"auth_mode_local_no_password": "Pas de mot de passe",
"auth_mode_local_no_password_description": "Accès rapide sans authentification par mot de passe.", "auth_mode_local_no_password_description": "Accès rapide sans authentification par mot de passe.",
"auth_mode_local_password": "Mot de passe", "auth_mode_local_no_password": "Pas de mot de passe",
"auth_mode_local_password_confirm_description": "Confirmez votre mot de passe", "auth_mode_local_password_confirm_description": "Confirmez votre mot de passe",
"auth_mode_local_password_confirm_label": "Confirmez le mot de passe", "auth_mode_local_password_confirm_label": "Confirmez le mot de passe",
"auth_mode_local_password_description": "Sécurisez votre appareil avec un mot de passe pour une protection supplémentaire.", "auth_mode_local_password_description": "Sécurisez votre appareil avec un mot de passe pour une protection supplémentaire.",
"auth_mode_local_password_do_not_match": "Les mots de passe ne correspondent pas", "auth_mode_local_password_do_not_match": "Les mots de passe ne correspondent pas",
"auth_mode_local_password_failed_set": "Échec de la définition du mot de passe : {error}", "auth_mode_local_password_failed_set": "Échec de la définition du mot de passe : {error}",
"auth_mode_local_password_note": "Ce mot de passe sera utilisé pour sécuriser les données de votre appareil et les protéger contre tout accès non autorisé.",
"auth_mode_local_password_note_local": "Toutes les données restent sur votre appareil local.", "auth_mode_local_password_note_local": "Toutes les données restent sur votre appareil local.",
"auth_mode_local_password_set": "Définir un mot de passe", "auth_mode_local_password_note": "Ce mot de passe sera utilisé pour sécuriser les données de votre appareil et les protéger contre tout accès non autorisé.",
"auth_mode_local_password_set_button": "Définir le mot de passe", "auth_mode_local_password_set_button": "Définir le mot de passe",
"auth_mode_local_password_set_description": "Créez un mot de passe fort pour sécuriser votre périphérique JetKVM localement.", "auth_mode_local_password_set_description": "Créez un mot de passe fort pour sécuriser votre périphérique JetKVM localement.",
"auth_mode_local_password_set_label": "Entrez un mot de passe", "auth_mode_local_password_set_label": "Entrez un mot de passe",
"auth_mode_local_password_set": "Définir un mot de passe",
"auth_mode_local_password": "Mot de passe",
"auth_mode_local": "Méthode d'authentification locale",
"auth_signup_connect_to_cloud_action": "Inscription et connexion de l'appareil", "auth_signup_connect_to_cloud_action": "Inscription et connexion de l'appareil",
"auth_signup_create_account": "Créez votre compte JetKVM",
"auth_signup_create_account_action": "Créer un compte", "auth_signup_create_account_action": "Créer un compte",
"auth_signup_create_account_description": "Créez votre compte et commencez à gérer vos appareils en toute simplicité.", "auth_signup_create_account_description": "Créez votre compte et commencez à gérer vos appareils en toute simplicité.",
"back": "Dos", "auth_signup_create_account": "Créez votre compte JetKVM",
"back_to_devices": "Retour aux appareils", "back_to_devices": "Retour aux appareils",
"back": "Dos",
"cancel": "Annuler", "cancel": "Annuler",
"close": "Fermer", "close": "Fermer",
"cloud_kvms": "KVM Cloud",
"cloud_kvms_description": "Gérez vos KVM cloud et connectez-vous à eux en toute sécurité.", "cloud_kvms_description": "Gérez vos KVM cloud et connectez-vous à eux en toute sécurité.",
"cloud_kvms_no_devices": "Aucun appareil trouvé",
"cloud_kvms_no_devices_description": "Vous n'avez pas encore d'appareils avec JetKVM Cloud activé.", "cloud_kvms_no_devices_description": "Vous n'avez pas encore d'appareils avec JetKVM Cloud activé.",
"cloud_kvms_no_devices": "Aucun appareil trouvé",
"cloud_kvms": "KVM Cloud",
"confirm": "Confirmer", "confirm": "Confirmer",
"connect_to_kvm": "Se connecter à KVM", "connect_to_kvm": "Se connecter à KVM",
"connecting_to_device": "Connexion à l'appareil…", "connecting_to_device": "Connexion à l'appareil…",
"connection_established": "Connexion établie", "connection_established": "Connexion établie",
"connection_stats_badge_jitter": "Gigue",
"connection_stats_badge_jitter_buffer_avg_delay": "Délai moyen du tampon de gigue", "connection_stats_badge_jitter_buffer_avg_delay": "Délai moyen du tampon de gigue",
"connection_stats_connection": "Connexion", "connection_stats_badge_jitter": "Gigue",
"connection_stats_connection_description": "La connexion entre le client et le JetKVM.", "connection_stats_connection_description": "La connexion entre le client et le JetKVM.",
"connection_stats_frames_per_second": "Images par seconde", "connection_stats_connection": "Connexion",
"connection_stats_frames_per_second_description": "Nombre d'images vidéo entrantes affichées par seconde.", "connection_stats_frames_per_second_description": "Nombre d'images vidéo entrantes affichées par seconde.",
"connection_stats_network_stability": "Stabilité du réseau", "connection_stats_frames_per_second": "Images par seconde",
"connection_stats_network_stability_description": "La stabilité du flux de paquets vidéo entrants sur le réseau.", "connection_stats_network_stability_description": "La stabilité du flux de paquets vidéo entrants sur le réseau.",
"connection_stats_packets_lost": "Paquets perdus", "connection_stats_network_stability": "Stabilité du réseau",
"connection_stats_packets_lost_description": "Nombre de paquets vidéo RTP entrants perdus.", "connection_stats_packets_lost_description": "Nombre de paquets vidéo RTP entrants perdus.",
"connection_stats_playback_delay": "Délai de lecture", "connection_stats_packets_lost": "Paquets perdus",
"connection_stats_playback_delay_description": "Retard ajouté par le tampon de gigue pour fluidifier la lecture lorsque les images arrivent de manière inégale.", "connection_stats_playback_delay_description": "Retard ajouté par le tampon de gigue pour fluidifier la lecture lorsque les images arrivent de manière inégale.",
"connection_stats_round_trip_time": "Temps de trajet aller-retour", "connection_stats_playback_delay": "Délai de lecture",
"connection_stats_round_trip_time_description": "Temps de trajet aller-retour pour la paire de candidats ICE actifs entre pairs.", "connection_stats_round_trip_time_description": "Temps de trajet aller-retour pour la paire de candidats ICE actifs entre pairs.",
"connection_stats_round_trip_time": "Temps de trajet aller-retour",
"connection_stats_sidebar": "Statistiques de connexion", "connection_stats_sidebar": "Statistiques de connexion",
"connection_stats_video": "Vidéo",
"connection_stats_video_description": "Le flux vidéo du JetKVM vers le client.", "connection_stats_video_description": "Le flux vidéo du JetKVM vers le client.",
"connection_stats_video": "Vidéo",
"continue": "Continuer", "continue": "Continuer",
"creating_peer_connection": "Créer des liens entre pairs…", "creating_peer_connection": "Créer des liens entre pairs…",
"dc_power_control_current": "Actuel",
"dc_power_control_current_unit": "UN", "dc_power_control_current_unit": "UN",
"dc_power_control_current": "Actuel",
"dc_power_control_get_state_error": "Échec de l'obtention de l'état d'alimentation CC : {error}", "dc_power_control_get_state_error": "Échec de l'obtention de l'état d'alimentation CC : {error}",
"dc_power_control_power": "Pouvoir",
"dc_power_control_power_off_button": "Éteindre", "dc_power_control_power_off_button": "Éteindre",
"dc_power_control_power_off_state": "Éteindre", "dc_power_control_power_off_state": "Éteindre",
"dc_power_control_power_on_button": "Mise sous tension", "dc_power_control_power_on_button": "Mise sous tension",
"dc_power_control_power_on_state": "Mise sous tension", "dc_power_control_power_on_state": "Mise sous tension",
"dc_power_control_power_unit": "W", "dc_power_control_power_unit": "W",
"dc_power_control_power": "Pouvoir",
"dc_power_control_restore_last_state": "Dernier état", "dc_power_control_restore_last_state": "Dernier état",
"dc_power_control_restore_power_state": "Restaurer la perte de puissance", "dc_power_control_restore_power_state": "Restaurer la perte de puissance",
"dc_power_control_set_power_state_error": "Échec de l'envoi de l'état d'alimentation CC à {enabled} : {error}", "dc_power_control_set_power_state_error": "Échec de l'envoi de l'état d'alimentation CC à {enabled} : {error}",
"dc_power_control_set_restore_state_error": "Échec de l'envoi de l'état de restauration de l'alimentation CC à {state} : {error}", "dc_power_control_set_restore_state_error": "Échec de l'envoi de l'état de restauration de l'alimentation CC à {state} : {error}",
"dc_power_control_voltage": "Tension",
"dc_power_control_voltage_unit": "V", "dc_power_control_voltage_unit": "V",
"dc_power_control_voltage": "Tension",
"delete": "Supprimer", "delete": "Supprimer",
"deregister_button": "Se désinscrire du Cloud", "deregister_button": "Se désinscrire du Cloud",
"deregister_cloud_devices": "Appareils Cloud", "deregister_cloud_devices": "Appareils Cloud",
@ -226,12 +226,12 @@
"extension_popover_load_and_manage_extensions": "Chargez et gérez vos extensions", "extension_popover_load_and_manage_extensions": "Chargez et gérez vos extensions",
"extension_popover_set_error_notification": "Échec de la définition de l'extension active : {error}", "extension_popover_set_error_notification": "Échec de la définition de l'extension active : {error}",
"extension_popover_unload_extension": "Extension de déchargement", "extension_popover_unload_extension": "Extension de déchargement",
"extension_serial_console": "Console série",
"extension_serial_console_description": "Accédez à votre extension de console série", "extension_serial_console_description": "Accédez à votre extension de console série",
"extensions_atx_power_control": "Contrôle d'alimentation ATX", "extension_serial_console": "Console série",
"extensions_atx_power_control_description": "Contrôlez l'état d'alimentation de votre machine via le contrôle d'alimentation ATX.", "extensions_atx_power_control_description": "Contrôlez l'état d'alimentation de votre machine via le contrôle d'alimentation ATX.",
"extensions_dc_power_control": "Contrôle de l'alimentation CC", "extensions_atx_power_control": "Contrôle d'alimentation ATX",
"extensions_dc_power_control_description": "Contrôlez votre extension d'alimentation CC", "extensions_dc_power_control_description": "Contrôlez votre extension d'alimentation CC",
"extensions_dc_power_control": "Contrôle de l'alimentation CC",
"extensions_popover_extensions": "Extensions", "extensions_popover_extensions": "Extensions",
"gathering_ice_candidates": "Rassemblement des candidats de l'ICE…", "gathering_ice_candidates": "Rassemblement des candidats de l'ICE…",
"general_app_version": "Application : {version}", "general_app_version": "Application : {version}",
@ -241,8 +241,8 @@
"general_check_for_updates": "Vérifier les mises à jour", "general_check_for_updates": "Vérifier les mises à jour",
"general_page_description": "Configurer les paramètres de l'appareil et mettre à jour les préférences", "general_page_description": "Configurer les paramètres de l'appareil et mettre à jour les préférences",
"general_reboot_description": "Voulez-vous procéder au redémarrage du système ?", "general_reboot_description": "Voulez-vous procéder au redémarrage du système ?",
"general_reboot_device": "Redémarrer l'appareil",
"general_reboot_device_description": "Redémarrez le JetKVM", "general_reboot_device_description": "Redémarrez le JetKVM",
"general_reboot_device": "Redémarrer l'appareil",
"general_reboot_no_button": "Non", "general_reboot_no_button": "Non",
"general_reboot_title": "Redémarrer JetKVM", "general_reboot_title": "Redémarrer JetKVM",
"general_reboot_yes_button": "Oui", "general_reboot_yes_button": "Oui",
@ -295,9 +295,9 @@
"hardware_display_orientation_title": "Orientation de l'affichage", "hardware_display_orientation_title": "Orientation de l'affichage",
"hardware_display_wake_up_note": "L'écran se réveille lorsque l'état de connexion change ou lorsqu'il est touché.", "hardware_display_wake_up_note": "L'écran se réveille lorsque l'état de connexion change ou lorsqu'il est touché.",
"hardware_page_description": "Configurer les paramètres d'affichage et les options matérielles de votre périphérique JetKVM", "hardware_page_description": "Configurer les paramètres d'affichage et les options matérielles de votre périphérique JetKVM",
"hardware_time_10_minutes": "10 minutes",
"hardware_time_1_hour": "1 heure", "hardware_time_1_hour": "1 heure",
"hardware_time_1_minute": "1 minute", "hardware_time_1_minute": "1 minute",
"hardware_time_10_minutes": "10 minutes",
"hardware_time_30_minutes": "30 minutes", "hardware_time_30_minutes": "30 minutes",
"hardware_time_5_minutes": "5 minutes", "hardware_time_5_minutes": "5 minutes",
"hardware_time_never": "Jamais", "hardware_time_never": "Jamais",
@ -327,7 +327,6 @@
"invalid_password": "Mot de passe invalide", "invalid_password": "Mot de passe invalide",
"ip_address": "Adresse IP", "ip_address": "Adresse IP",
"ipv6_address_label": "Adresse", "ipv6_address_label": "Adresse",
"ipv6_gateway": "Porte",
"ipv6_information": "Informations IPv6", "ipv6_information": "Informations IPv6",
"ipv6_link_local": "Lien local", "ipv6_link_local": "Lien local",
"ipv6_preferred_lifetime": "Durée de vie préférée", "ipv6_preferred_lifetime": "Durée de vie préférée",
@ -410,8 +409,8 @@
"log_in": "Se connecter", "log_in": "Se connecter",
"log_out": "Se déconnecter", "log_out": "Se déconnecter",
"logged_in_as": "Connecté en tant que", "logged_in_as": "Connecté en tant que",
"login_enter_password": "Entrez votre mot de passe",
"login_enter_password_description": "Entrez votre mot de passe pour accéder à votre JetKVM.", "login_enter_password_description": "Entrez votre mot de passe pour accéder à votre JetKVM.",
"login_enter_password": "Entrez votre mot de passe",
"login_error": "Une erreur s'est produite lors de la connexion", "login_error": "Une erreur s'est produite lors de la connexion",
"login_forgot_password": "Mot de passe oublié?", "login_forgot_password": "Mot de passe oublié?",
"login_password_label": "Mot de passe", "login_password_label": "Mot de passe",
@ -425,8 +424,8 @@
"macro_name_required": "Le nom est obligatoire", "macro_name_required": "Le nom est obligatoire",
"macro_name_too_long": "Le nom doit comporter moins de 50 caractères", "macro_name_too_long": "Le nom doit comporter moins de 50 caractères",
"macro_please_fix_validation_errors": "Veuillez corriger les erreurs de validation", "macro_please_fix_validation_errors": "Veuillez corriger les erreurs de validation",
"macro_save": "Enregistrer la macro",
"macro_save_error": "Une erreur s'est produite lors de l'enregistrement.", "macro_save_error": "Une erreur s'est produite lors de l'enregistrement.",
"macro_save": "Enregistrer la macro",
"macro_step_count": "{steps} / {max} étapes", "macro_step_count": "{steps} / {max} étapes",
"macro_step_duration_description": "Il est temps dattendre avant dexécuter létape suivante.", "macro_step_duration_description": "Il est temps dattendre avant dexécuter létape suivante.",
"macro_step_duration_label": "Durée de l'étape", "macro_step_duration_label": "Durée de l'étape",
@ -440,8 +439,8 @@
"macro_steps_description": "Clés/modificateurs exécutés en séquence avec un délai entre chaque étape.", "macro_steps_description": "Clés/modificateurs exécutés en séquence avec un délai entre chaque étape.",
"macro_steps_label": "Mesures", "macro_steps_label": "Mesures",
"macros_add_description": "Créer une nouvelle macro de clavier", "macros_add_description": "Créer une nouvelle macro de clavier",
"macros_add_new": "Ajouter une nouvelle macro",
"macros_add_new_macro": "Ajouter une nouvelle macro", "macros_add_new_macro": "Ajouter une nouvelle macro",
"macros_add_new": "Ajouter une nouvelle macro",
"macros_aria_add_new": "Ajouter une nouvelle macro", "macros_aria_add_new": "Ajouter une nouvelle macro",
"macros_aria_delete": "Supprimer la macro {name}", "macros_aria_delete": "Supprimer la macro {name}",
"macros_aria_duplicate": "Macro dupliquée {name}", "macros_aria_duplicate": "Macro dupliquée {name}",
@ -463,16 +462,16 @@
"macros_edit_button": "Modifier", "macros_edit_button": "Modifier",
"macros_edit_description": "Modifiez votre macro de clavier", "macros_edit_description": "Modifiez votre macro de clavier",
"macros_edit_title": "Modifier la macro", "macros_edit_title": "Modifier la macro",
"macros_failed_create": "Échec de la création de la macro",
"macros_failed_create_error": "Échec de la création de la macro : {error}", "macros_failed_create_error": "Échec de la création de la macro : {error}",
"macros_failed_delete": "Échec de la suppression de la macro", "macros_failed_create": "Échec de la création de la macro",
"macros_failed_delete_error": "Échec de la suppression de la macro : {error}", "macros_failed_delete_error": "Échec de la suppression de la macro : {error}",
"macros_failed_duplicate": "Échec de la duplication de la macro", "macros_failed_delete": "Échec de la suppression de la macro",
"macros_failed_duplicate_error": "Échec de la duplication de la macro : {error}", "macros_failed_duplicate_error": "Échec de la duplication de la macro : {error}",
"macros_failed_reorder": "Échec de la réorganisation des macros", "macros_failed_duplicate": "Échec de la duplication de la macro",
"macros_failed_reorder_error": "Échec de la réorganisation des macros : {error}", "macros_failed_reorder_error": "Échec de la réorganisation des macros : {error}",
"macros_failed_update": "Échec de la mise à jour de la macro", "macros_failed_reorder": "Échec de la réorganisation des macros",
"macros_failed_update_error": "Échec de la mise à jour de la macro : {error}", "macros_failed_update_error": "Échec de la mise à jour de la macro : {error}",
"macros_failed_update": "Échec de la mise à jour de la macro",
"macros_invalid_data": "Données de macro non valides", "macros_invalid_data": "Données de macro non valides",
"macros_loading": "Chargement des macros…", "macros_loading": "Chargement des macros…",
"macros_max_reached": "Max atteint", "macros_max_reached": "Max atteint",
@ -507,8 +506,8 @@
"mount_error_list_storage": "Erreur lors de la liste des fichiers de stockage : {error}", "mount_error_list_storage": "Erreur lors de la liste des fichiers de stockage : {error}",
"mount_error_title": "Erreur de montage", "mount_error_title": "Erreur de montage",
"mount_get_state_error": "Échec de l'obtention de l'état du support virtuel : {error}", "mount_get_state_error": "Échec de l'obtention de l'état du support virtuel : {error}",
"mount_jetkvm_storage": "Support de stockage JetKVM",
"mount_jetkvm_storage_description": "Monter les fichiers précédemment téléchargés à partir du stockage JetKVM", "mount_jetkvm_storage_description": "Monter les fichiers précédemment téléchargés à partir du stockage JetKVM",
"mount_jetkvm_storage": "Support de stockage JetKVM",
"mount_mode_cdrom": "CD/DVD", "mount_mode_cdrom": "CD/DVD",
"mount_mode_disk": "Disque", "mount_mode_disk": "Disque",
"mount_mounted_as": "Monté comme", "mount_mounted_as": "Monté comme",
@ -521,8 +520,8 @@
"mount_popular_images": "Images populaires", "mount_popular_images": "Images populaires",
"mount_streaming_from_url": "Diffusion à partir d'une URL", "mount_streaming_from_url": "Diffusion à partir d'une URL",
"mount_supported_formats": "Formats pris en charge : ISO, IMG", "mount_supported_formats": "Formats pris en charge : ISO, IMG",
"mount_unmount": "Démonter",
"mount_unmount_error": "Échec du démontage de l'image : {error}", "mount_unmount_error": "Échec du démontage de l'image : {error}",
"mount_unmount": "Démonter",
"mount_upload_description": "Sélectionnez un fichier image à télécharger sur le stockage JetKVM", "mount_upload_description": "Sélectionnez un fichier image à télécharger sur le stockage JetKVM",
"mount_upload_error": "Erreur de téléchargement : {error}", "mount_upload_error": "Erreur de téléchargement : {error}",
"mount_upload_failed_datachannel": "Échec de la création du canal de données pour le téléchargement du fichier", "mount_upload_failed_datachannel": "Échec de la création du canal de données pour le téléchargement du fichier",
@ -530,8 +529,8 @@
"mount_upload_successful": "Téléchargement réussi", "mount_upload_successful": "Téléchargement réussi",
"mount_upload_title": "Télécharger une nouvelle image", "mount_upload_title": "Télécharger une nouvelle image",
"mount_uploaded_has_been_uploaded": "{name} a été téléchargé", "mount_uploaded_has_been_uploaded": "{name} a été téléchargé",
"mount_uploading": "Téléchargement en cours…",
"mount_uploading_with_name": "Téléchargement de {name}", "mount_uploading_with_name": "Téléchargement de {name}",
"mount_uploading": "Téléchargement en cours…",
"mount_url_description": "Monter des fichiers à partir de n'importe quelle adresse Web publique", "mount_url_description": "Monter des fichiers à partir de n'importe quelle adresse Web publique",
"mount_url_input_label": "URL de l'image", "mount_url_input_label": "URL de l'image",
"mount_url_mount": "Montage d'URL", "mount_url_mount": "Montage d'URL",
@ -539,10 +538,10 @@
"mount_view_device_title": "Montage à partir du stockage JetKVM", "mount_view_device_title": "Montage à partir du stockage JetKVM",
"mount_view_url_description": "Entrez une URL vers le fichier image à monter", "mount_view_url_description": "Entrez une URL vers le fichier image à monter",
"mount_view_url_title": "Monter à partir de l'URL", "mount_view_url_title": "Monter à partir de l'URL",
"mount_virtual_media": "Médias virtuels",
"mount_virtual_media_description": "Monter une image pour démarrer ou installer un système d'exploitation.", "mount_virtual_media_description": "Monter une image pour démarrer ou installer un système d'exploitation.",
"mount_virtual_media_source": "Source de média virtuel",
"mount_virtual_media_source_description": "Choisissez comment vous souhaitez monter votre support virtuel", "mount_virtual_media_source_description": "Choisissez comment vous souhaitez monter votre support virtuel",
"mount_virtual_media_source": "Source de média virtuel",
"mount_virtual_media": "Médias virtuels",
"mouse_alt_finger": "Doigt touchant un écran", "mouse_alt_finger": "Doigt touchant un écran",
"mouse_alt_mouse": "Icône de la souris", "mouse_alt_mouse": "Icône de la souris",
"mouse_description": "Configurer le comportement du curseur et les paramètres d'interaction pour votre appareil", "mouse_description": "Configurer le comportement du curseur et les paramètres d'interaction pour votre appareil",
@ -559,10 +558,10 @@
"mouse_jiggler_light": "Lumière - 5m", "mouse_jiggler_light": "Lumière - 5m",
"mouse_jiggler_standard": "Norme - 1 m", "mouse_jiggler_standard": "Norme - 1 m",
"mouse_jiggler_title": "Jiggler", "mouse_jiggler_title": "Jiggler",
"mouse_mode_absolute": "Absolu",
"mouse_mode_absolute_description": "Le plus pratique", "mouse_mode_absolute_description": "Le plus pratique",
"mouse_mode_relative": "Relatif", "mouse_mode_absolute": "Absolu",
"mouse_mode_relative_description": "Le plus compatible", "mouse_mode_relative_description": "Le plus compatible",
"mouse_mode_relative": "Relatif",
"mouse_modes_description": "Choisissez le mode de saisie de la souris", "mouse_modes_description": "Choisissez le mode de saisie de la souris",
"mouse_modes_title": "Modes", "mouse_modes_title": "Modes",
"mouse_scroll_high": "Haut", "mouse_scroll_high": "Haut",
@ -575,17 +574,12 @@
"mouse_title": "Souris", "mouse_title": "Souris",
"network_custom_domain": "Domaine personnalisé", "network_custom_domain": "Domaine personnalisé",
"network_description": "Configurez vos paramètres réseau", "network_description": "Configurez vos paramètres réseau",
"network_dhcp_client_description": "Configurer le client DHCP à utiliser",
"network_dhcp_client_jetkvm": "JetKVM interne",
"network_dhcp_client_title": "Client DHCP",
"network_dhcp_information": "Informations DHCP", "network_dhcp_information": "Informations DHCP",
"network_dhcp_lease_renew": "Renouveler le bail DHCP",
"network_dhcp_lease_renew_confirm": "Renouveler le bail",
"network_dhcp_lease_renew_confirm_description": "Cette opération demandera une nouvelle adresse IP à votre serveur DHCP. Votre appareil pourrait perdre temporairement sa connectivité réseau pendant cette opération.", "network_dhcp_lease_renew_confirm_description": "Cette opération demandera une nouvelle adresse IP à votre serveur DHCP. Votre appareil pourrait perdre temporairement sa connectivité réseau pendant cette opération.",
"network_dhcp_lease_renew_confirm_new_a": "Si vous recevez une nouvelle adresse IP", "network_dhcp_lease_renew_confirm": "Renouveler le bail",
"network_dhcp_lease_renew_confirm_new_b": "vous devrez peut-être vous reconnecter en utilisant la nouvelle adresse",
"network_dhcp_lease_renew_failed": "Échec du renouvellement du bail : {error}", "network_dhcp_lease_renew_failed": "Échec du renouvellement du bail : {error}",
"network_dhcp_lease_renew_success": "Renouvellement du bail DHCP", "network_dhcp_lease_renew_success": "Renouvellement du bail DHCP",
"network_dhcp_lease_renew": "Renouveler le bail DHCP",
"network_domain_custom": "Coutume", "network_domain_custom": "Coutume",
"network_domain_description": "Suffixe de domaine réseau pour l'appareil", "network_domain_description": "Suffixe de domaine réseau pour l'appareil",
"network_domain_dhcp_provided": "DHCP fourni", "network_domain_dhcp_provided": "DHCP fourni",
@ -594,35 +588,21 @@
"network_hostname_description": "Identifiant de l'appareil sur le réseau. Vide pour la valeur par défaut du système.", "network_hostname_description": "Identifiant de l'appareil sur le réseau. Vide pour la valeur par défaut du système.",
"network_hostname_title": "Nom d'hôte", "network_hostname_title": "Nom d'hôte",
"network_http_proxy_description": "Serveur proxy pour les requêtes HTTP(S) sortantes de l'appareil. Vide pour aucun.", "network_http_proxy_description": "Serveur proxy pour les requêtes HTTP(S) sortantes de l'appareil. Vide pour aucun.",
"network_http_proxy_invalid": "URL de proxy HTTP non valide",
"network_http_proxy_title": "Proxy HTTP", "network_http_proxy_title": "Proxy HTTP",
"network_ipv4_address": "Adresse IPv4",
"network_ipv4_dns": "DNS IPv4",
"network_ipv4_gateway": "Passerelle IPv4",
"network_ipv4_mode_description": "Configurer le mode IPv4", "network_ipv4_mode_description": "Configurer le mode IPv4",
"network_ipv4_mode_dhcp": "DHCP", "network_ipv4_mode_dhcp": "DHCP",
"network_ipv4_mode_static": "Statique",
"network_ipv4_mode_title": "Mode IPv4", "network_ipv4_mode_title": "Mode IPv4",
"network_ipv4_netmask": "Masque de réseau IPv4",
"network_ipv6_address": "Adresse IPv6",
"network_ipv6_information": "Informations IPv6", "network_ipv6_information": "Informations IPv6",
"network_ipv6_mode_description": "Configurer le mode IPv6", "network_ipv6_mode_description": "Configurer le mode IPv6",
"network_ipv6_mode_dhcpv6": "DHCPv6",
"network_ipv6_mode_disabled": "Désactivé", "network_ipv6_mode_disabled": "Désactivé",
"network_ipv6_mode_link_local": "Lien local uniquement",
"network_ipv6_mode_slaac": "SLAAC", "network_ipv6_mode_slaac": "SLAAC",
"network_ipv6_mode_slaac_dhcpv6": "SLAAC + DHCPv6",
"network_ipv6_mode_static": "Statique",
"network_ipv6_mode_title": "Mode IPv6", "network_ipv6_mode_title": "Mode IPv6",
"network_ipv6_netmask": "Masque de réseau IPv6",
"network_ipv6_no_addresses": "Aucune adresse IPv6 configurée", "network_ipv6_no_addresses": "Aucune adresse IPv6 configurée",
"network_ll_dp_all": "Tous", "network_ll_dp_all": "Tous",
"network_ll_dp_basic": "Basique", "network_ll_dp_basic": "Basique",
"network_ll_dp_description": "Contrôler les TLV qui seront envoyés via le protocole Link Layer Discovery", "network_ll_dp_description": "Contrôler les TLV qui seront envoyés via le protocole Link Layer Discovery",
"network_ll_dp_disabled": "Désactivé", "network_ll_dp_disabled": "Désactivé",
"network_ll_dp_title": "LLDP", "network_ll_dp_title": "LLDP",
"network_mac_address_copy_error": "Échec de la copie de l'adresse MAC",
"network_mac_address_copy_success": "Adresse MAC { mac } copiée dans le presse-papiers",
"network_mac_address_description": "Identifiant matériel de l'interface réseau", "network_mac_address_description": "Identifiant matériel de l'interface réseau",
"network_mac_address_title": "Adresse MAC", "network_mac_address_title": "Adresse MAC",
"network_mdns_auto": "Auto", "network_mdns_auto": "Auto",
@ -632,19 +612,9 @@
"network_mdns_ipv6_only": "IPv6 uniquement", "network_mdns_ipv6_only": "IPv6 uniquement",
"network_mdns_title": "mDNS", "network_mdns_title": "mDNS",
"network_no_dhcp_lease": "Aucune information de bail DHCP disponible", "network_no_dhcp_lease": "Aucune information de bail DHCP disponible",
"network_no_information_description": "Aucune configuration réseau disponible",
"network_no_information_headline": "Informations sur le réseau",
"network_pending_dhcp_mode_change_description": "Enregistrer les paramètres pour activer le mode DHCP et afficher les informations de bail",
"network_pending_dhcp_mode_change_headline": "Changement de mode DHCP IPv4 en attente",
"network_save_settings": "Enregistrer les paramètres",
"network_save_settings_apply_title": "Appliquer les paramètres réseau",
"network_save_settings_confirm": "Appliquer les modifications",
"network_save_settings_confirm_description": "Les paramètres réseau suivants seront appliqués. Ces modifications peuvent nécessiter un redémarrage et provoquer une brève déconnexion.",
"network_save_settings_confirm_heading": "Modifications de configuration",
"network_save_settings_failed": "Échec de l'enregistrement des paramètres réseau : {error}", "network_save_settings_failed": "Échec de l'enregistrement des paramètres réseau : {error}",
"network_save_settings_success": "Paramètres réseau enregistrés", "network_save_settings_success": "Paramètres réseau enregistrés",
"network_settings_invalid_ipv4_cidr": "Notation CIDR non valide pour l'adresse IPv4", "network_save_settings": "Enregistrer les paramètres",
"network_settings_load_error": "Échec du chargement des paramètres réseau : {error}",
"network_time_sync_description": "Configurer les paramètres de synchronisation de l'heure", "network_time_sync_description": "Configurer les paramètres de synchronisation de l'heure",
"network_time_sync_http_only": "HTTP uniquement", "network_time_sync_http_only": "HTTP uniquement",
"network_time_sync_ntp_and_http": "NTP et HTTP", "network_time_sync_ntp_and_http": "NTP et HTTP",
@ -670,8 +640,8 @@
"paste_modal_failed_paste": "Échec du collage du texte : {error}", "paste_modal_failed_paste": "Échec du collage du texte : {error}",
"paste_modal_invalid_chars_intro": "Les caractères suivants ne seront pas collés :", "paste_modal_invalid_chars_intro": "Les caractères suivants ne seront pas collés :",
"paste_modal_paste_from_host": "Coller depuis l'hôte", "paste_modal_paste_from_host": "Coller depuis l'hôte",
"paste_modal_paste_text": "Coller du texte",
"paste_modal_paste_text_description": "Collez le texte de votre client sur l'hôte distant", "paste_modal_paste_text_description": "Collez le texte de votre client sur l'hôte distant",
"paste_modal_paste_text": "Coller du texte",
"paste_modal_sending_using_layout": "Envoi de texte à l'aide de la disposition du clavier : {iso} - {name}", "paste_modal_sending_using_layout": "Envoi de texte à l'aide de la disposition du clavier : {iso} - {name}",
"peer_connection_closed": "Fermé", "peer_connection_closed": "Fermé",
"peer_connection_closing": "Clôture", "peer_connection_closing": "Clôture",
@ -698,20 +668,20 @@
"retry": "Réessayer", "retry": "Réessayer",
"saving": "Économie…", "saving": "Économie…",
"search_placeholder": "Rechercher…", "search_placeholder": "Rechercher…",
"serial_console": "Console série",
"serial_console_baud_rate": "Débit en bauds", "serial_console_baud_rate": "Débit en bauds",
"serial_console_configure_description": "Configurez les paramètres de votre console série", "serial_console_configure_description": "Configurez les paramètres de votre console série",
"serial_console_data_bits": "Bits de données", "serial_console_data_bits": "Bits de données",
"serial_console_get_settings_error": "Échec de l'obtention des paramètres de la console série : {error}", "serial_console_get_settings_error": "Échec de l'obtention des paramètres de la console série : {error}",
"serial_console_open_console": "Ouvrir la console", "serial_console_open_console": "Ouvrir la console",
"serial_console_parity": "Parité",
"serial_console_parity_even": "Parité égale", "serial_console_parity_even": "Parité égale",
"serial_console_parity_mark": "Marquer la parité", "serial_console_parity_mark": "Marquer la parité",
"serial_console_parity_none": "Pas de parité", "serial_console_parity_none": "Pas de parité",
"serial_console_parity_odd": "Parité impaire", "serial_console_parity_odd": "Parité impaire",
"serial_console_parity_space": "Parité spatiale", "serial_console_parity_space": "Parité spatiale",
"serial_console_parity": "Parité",
"serial_console_set_settings_error": "Échec de la définition des paramètres de la console série sur {settings} : {error}", "serial_console_set_settings_error": "Échec de la définition des paramètres de la console série sur {settings} : {error}",
"serial_console_stop_bits": "Bits d'arrêt", "serial_console_stop_bits": "Bits d'arrêt",
"serial_console": "Console série",
"setting_remote_description": "Description de la télécommande", "setting_remote_description": "Description de la télécommande",
"setting_remote_session_description": "Définition de la description de la session à distance...", "setting_remote_session_description": "Définition de la description de la session à distance...",
"setting_up_connection_to_device": "Configuration de la connexion à l'appareil...", "setting_up_connection_to_device": "Configuration de la connexion à l'appareil...",
@ -721,8 +691,8 @@
"settings_back_to_kvm": "Retour à KVM", "settings_back_to_kvm": "Retour à KVM",
"settings_general": "Général", "settings_general": "Général",
"settings_hardware": "Matériel", "settings_hardware": "Matériel",
"settings_keyboard": "Clavier",
"settings_keyboard_macros": "Macros de clavier", "settings_keyboard_macros": "Macros de clavier",
"settings_keyboard": "Clavier",
"settings_mouse": "Souris", "settings_mouse": "Souris",
"settings_network": "Réseau", "settings_network": "Réseau",
"settings_video": "Vidéo", "settings_video": "Vidéo",
@ -742,7 +712,6 @@
"updates_failed_check": "Échec de la vérification des mises à jour : {error}", "updates_failed_check": "Échec de la vérification des mises à jour : {error}",
"updates_failed_get_device_version": "Échec de l'obtention de la version de l'appareil : {error}", "updates_failed_get_device_version": "Échec de l'obtention de la version de l'appareil : {error}",
"updating_leave_device_on": "S'il vous plaît, n'éteignez pas votre appareil…", "updating_leave_device_on": "S'il vous plaît, n'éteignez pas votre appareil…",
"usb": "USB",
"usb_config_custom": "Coutume", "usb_config_custom": "Coutume",
"usb_config_default": "JetKVM par défaut", "usb_config_default": "JetKVM par défaut",
"usb_config_dell": "Clavier Dell Multimedia Pro", "usb_config_dell": "Clavier Dell Multimedia Pro",
@ -789,6 +758,7 @@
"usb_state_connecting": "De liaison", "usb_state_connecting": "De liaison",
"usb_state_disconnected": "Déconnecté", "usb_state_disconnected": "Déconnecté",
"usb_state_low_power_mode": "Mode basse consommation", "usb_state_low_power_mode": "Mode basse consommation",
"usb": "USB",
"user_interface_language_description": "Sélectionnez la langue à utiliser dans l'interface utilisateur de JetKVM", "user_interface_language_description": "Sélectionnez la langue à utiliser dans l'interface utilisateur de JetKVM",
"user_interface_language_title": "Langue de l'interface", "user_interface_language_title": "Langue de l'interface",
"video_brightness_description": "Niveau de luminosité ( {value} x)", "video_brightness_description": "Niveau de luminosité ( {value} x)",
@ -855,7 +825,6 @@
"video_title": "Vidéo", "video_title": "Vidéo",
"view_details": "Voir les détails", "view_details": "Voir les détails",
"virtual_keyboard_header": "Clavier virtuel", "virtual_keyboard_header": "Clavier virtuel",
"wake_on_lan": "Wake On LAN",
"wake_on_lan_add_device_device_name": "Nom de l'appareil", "wake_on_lan_add_device_device_name": "Nom de l'appareil",
"wake_on_lan_add_device_example_device_name": "Serveur multimédia Plex", "wake_on_lan_add_device_example_device_name": "Serveur multimédia Plex",
"wake_on_lan_add_device_mac_address": "Adresse MAC", "wake_on_lan_add_device_mac_address": "Adresse MAC",
@ -871,6 +840,7 @@
"wake_on_lan_failed_send_magic": "Échec de l'envoi du paquet magique", "wake_on_lan_failed_send_magic": "Échec de l'envoi du paquet magique",
"wake_on_lan_invalid_mac": "Adresse MAC invalide", "wake_on_lan_invalid_mac": "Adresse MAC invalide",
"wake_on_lan_magic_sent_success": "Paquet magique envoyé avec succès", "wake_on_lan_magic_sent_success": "Paquet magique envoyé avec succès",
"welcome_to_jetkvm": "Bienvenue chez JetKVM", "wake_on_lan": "Wake On LAN",
"welcome_to_jetkvm_description": "Contrôlez n'importe quel ordinateur à distance" "welcome_to_jetkvm_description": "Contrôlez n'importe quel ordinateur à distance",
"welcome_to_jetkvm": "Bienvenue chez JetKVM"
} }

View File

@ -107,10 +107,10 @@
"already_adopted_title": "Dispositivo già registrato", "already_adopted_title": "Dispositivo già registrato",
"appearance_description": "Scegli il tuo tema colore preferito", "appearance_description": "Scegli il tuo tema colore preferito",
"appearance_page_description": "Personalizza l'aspetto e le funzionalità della tua interfaccia JetKVM", "appearance_page_description": "Personalizza l'aspetto e le funzionalità della tua interfaccia JetKVM",
"appearance_theme": "Tema",
"appearance_theme_dark": "Buio", "appearance_theme_dark": "Buio",
"appearance_theme_light": "Leggero", "appearance_theme_light": "Leggero",
"appearance_theme_system": "Sistema", "appearance_theme_system": "Sistema",
"appearance_theme": "Tema",
"appearance_title": "Aspetto", "appearance_title": "Aspetto",
"attach": "Allegare", "attach": "Allegare",
"atx_power_control_get_state_error": "Impossibile ottenere lo stato di alimentazione ATX: {error}", "atx_power_control_get_state_error": "Impossibile ottenere lo stato di alimentazione ATX: {error}",
@ -121,85 +121,85 @@
"atx_power_control_reset_button": "Reset", "atx_power_control_reset_button": "Reset",
"atx_power_control_send_action_error": "Impossibile inviare l'azione di alimentazione ATX {action} : {error}", "atx_power_control_send_action_error": "Impossibile inviare l'azione di alimentazione ATX {action} : {error}",
"atx_power_control_short_power_button": "Pressione breve", "atx_power_control_short_power_button": "Pressione breve",
"auth_authentication_mode": "Seleziona una modalità di autenticazione",
"auth_authentication_mode_error": "Si è verificato un errore durante l'impostazione della modalità di autenticazione", "auth_authentication_mode_error": "Si è verificato un errore durante l'impostazione della modalità di autenticazione",
"auth_authentication_mode_invalid": "Modalità di autenticazione non valida", "auth_authentication_mode_invalid": "Modalità di autenticazione non valida",
"auth_connect_to_cloud": "Collega il tuo JetKVM al cloud", "auth_authentication_mode": "Seleziona una modalità di autenticazione",
"auth_connect_to_cloud_action": "Accedi e connetti il dispositivo", "auth_connect_to_cloud_action": "Accedi e connetti il dispositivo",
"auth_connect_to_cloud_description": "Sblocca l'accesso remoto e le funzionalità avanzate per il tuo dispositivo", "auth_connect_to_cloud_description": "Sblocca l'accesso remoto e le funzionalità avanzate per il tuo dispositivo",
"auth_connect_to_cloud": "Collega il tuo JetKVM al cloud",
"auth_header_cta_already_have_account": "Hai già un account?", "auth_header_cta_already_have_account": "Hai già un account?",
"auth_header_cta_dont_have_account": "Non hai un account?", "auth_header_cta_dont_have_account": "Non hai un account?",
"auth_header_cta_new_to_jetkvm": "Nuovo su JetKVM?", "auth_header_cta_new_to_jetkvm": "Nuovo su JetKVM?",
"auth_login": "Accedi al tuo account JetKVM",
"auth_login_action": "Login", "auth_login_action": "Login",
"auth_login_description": "Accedi per accedere e gestire i tuoi dispositivi in modo sicuro", "auth_login_description": "Accedi per accedere e gestire i tuoi dispositivi in modo sicuro",
"auth_mode_local": "Metodo di autenticazione locale", "auth_login": "Accedi al tuo account JetKVM",
"auth_mode_local_change_later": "Potrai sempre modificare il metodo di autenticazione in un secondo momento nelle impostazioni.", "auth_mode_local_change_later": "Potrai sempre modificare il metodo di autenticazione in un secondo momento nelle impostazioni.",
"auth_mode_local_description": "Seleziona come desideri proteggere localmente il tuo dispositivo JetKVM.", "auth_mode_local_description": "Seleziona come desideri proteggere localmente il tuo dispositivo JetKVM.",
"auth_mode_local_no_password": "Nessuna password",
"auth_mode_local_no_password_description": "Accesso rapido senza autenticazione tramite password.", "auth_mode_local_no_password_description": "Accesso rapido senza autenticazione tramite password.",
"auth_mode_local_password": "Password", "auth_mode_local_no_password": "Nessuna password",
"auth_mode_local_password_confirm_description": "Conferma la tua password", "auth_mode_local_password_confirm_description": "Conferma la tua password",
"auth_mode_local_password_confirm_label": "Conferma password", "auth_mode_local_password_confirm_label": "Conferma password",
"auth_mode_local_password_description": "Per una maggiore protezione, proteggi il tuo dispositivo con una password.", "auth_mode_local_password_description": "Per una maggiore protezione, proteggi il tuo dispositivo con una password.",
"auth_mode_local_password_do_not_match": "Le password non corrispondono", "auth_mode_local_password_do_not_match": "Le password non corrispondono",
"auth_mode_local_password_failed_set": "Impossibile impostare la password: {error}", "auth_mode_local_password_failed_set": "Impossibile impostare la password: {error}",
"auth_mode_local_password_note": "Questa password verrà utilizzata per proteggere i dati del tuo dispositivo e proteggerli da accessi non autorizzati.",
"auth_mode_local_password_note_local": "Tutti i dati rimangono sul tuo dispositivo locale.", "auth_mode_local_password_note_local": "Tutti i dati rimangono sul tuo dispositivo locale.",
"auth_mode_local_password_set": "Imposta una password", "auth_mode_local_password_note": "Questa password verrà utilizzata per proteggere i dati del tuo dispositivo e proteggerli da accessi non autorizzati.",
"auth_mode_local_password_set_button": "Imposta password", "auth_mode_local_password_set_button": "Imposta password",
"auth_mode_local_password_set_description": "Crea una password complessa per proteggere localmente il tuo dispositivo JetKVM.", "auth_mode_local_password_set_description": "Crea una password complessa per proteggere localmente il tuo dispositivo JetKVM.",
"auth_mode_local_password_set_label": "Inserisci una password", "auth_mode_local_password_set_label": "Inserisci una password",
"auth_mode_local_password_set": "Imposta una password",
"auth_mode_local_password": "Password",
"auth_mode_local": "Metodo di autenticazione locale",
"auth_signup_connect_to_cloud_action": "Registrati e connetti il dispositivo", "auth_signup_connect_to_cloud_action": "Registrati e connetti il dispositivo",
"auth_signup_create_account": "Crea il tuo account JetKVM",
"auth_signup_create_account_action": "Creare un account", "auth_signup_create_account_action": "Creare un account",
"auth_signup_create_account_description": "Crea il tuo account e inizia a gestire i tuoi dispositivi con facilità.", "auth_signup_create_account_description": "Crea il tuo account e inizia a gestire i tuoi dispositivi con facilità.",
"back": "Indietro", "auth_signup_create_account": "Crea il tuo account JetKVM",
"back_to_devices": "Torna ai dispositivi", "back_to_devices": "Torna ai dispositivi",
"back": "Indietro",
"cancel": "Cancellare", "cancel": "Cancellare",
"close": "Vicino", "close": "Vicino",
"cloud_kvms": "KVM cloud",
"cloud_kvms_description": "Gestisci i tuoi KVM cloud e connettiti ad essi in modo sicuro.", "cloud_kvms_description": "Gestisci i tuoi KVM cloud e connettiti ad essi in modo sicuro.",
"cloud_kvms_no_devices": "Nessun dispositivo trovato",
"cloud_kvms_no_devices_description": "Non hai ancora alcun dispositivo con JetKVM Cloud abilitato.", "cloud_kvms_no_devices_description": "Non hai ancora alcun dispositivo con JetKVM Cloud abilitato.",
"cloud_kvms_no_devices": "Nessun dispositivo trovato",
"cloud_kvms": "KVM cloud",
"confirm": "Confermare", "confirm": "Confermare",
"connect_to_kvm": "Connettiti a KVM", "connect_to_kvm": "Connettiti a KVM",
"connecting_to_device": "Connessione al dispositivo…", "connecting_to_device": "Connessione al dispositivo…",
"connection_established": "Connessione stabilita", "connection_established": "Connessione stabilita",
"connection_stats_badge_jitter": "tremolio",
"connection_stats_badge_jitter_buffer_avg_delay": "Ritardo medio del buffer di jitter", "connection_stats_badge_jitter_buffer_avg_delay": "Ritardo medio del buffer di jitter",
"connection_stats_connection": "Connessione", "connection_stats_badge_jitter": "tremolio",
"connection_stats_connection_description": "La connessione tra il client e JetKVM.", "connection_stats_connection_description": "La connessione tra il client e JetKVM.",
"connection_stats_frames_per_second": "Fotogrammi al secondo", "connection_stats_connection": "Connessione",
"connection_stats_frames_per_second_description": "Numero di fotogrammi video in entrata visualizzati al secondo.", "connection_stats_frames_per_second_description": "Numero di fotogrammi video in entrata visualizzati al secondo.",
"connection_stats_network_stability": "Stabilità della rete", "connection_stats_frames_per_second": "Fotogrammi al secondo",
"connection_stats_network_stability_description": "Quanto è costante il flusso di pacchetti video in entrata sulla rete.", "connection_stats_network_stability_description": "Quanto è costante il flusso di pacchetti video in entrata sulla rete.",
"connection_stats_packets_lost": "Pacchetti persi", "connection_stats_network_stability": "Stabilità della rete",
"connection_stats_packets_lost_description": "Conteggio dei pacchetti video RTP in entrata persi.", "connection_stats_packets_lost_description": "Conteggio dei pacchetti video RTP in entrata persi.",
"connection_stats_playback_delay": "Ritardo di riproduzione", "connection_stats_packets_lost": "Pacchetti persi",
"connection_stats_playback_delay_description": "Ritardo aggiunto dal buffer jitter per rendere più fluida la riproduzione quando i fotogrammi arrivano in modo non uniforme.", "connection_stats_playback_delay_description": "Ritardo aggiunto dal buffer jitter per rendere più fluida la riproduzione quando i fotogrammi arrivano in modo non uniforme.",
"connection_stats_round_trip_time": "Tempo di andata e ritorno", "connection_stats_playback_delay": "Ritardo di riproduzione",
"connection_stats_round_trip_time_description": "Tempo di andata e ritorno per la coppia di candidati ICE attivi tra pari.", "connection_stats_round_trip_time_description": "Tempo di andata e ritorno per la coppia di candidati ICE attivi tra pari.",
"connection_stats_round_trip_time": "Tempo di andata e ritorno",
"connection_stats_sidebar": "Statistiche di connessione", "connection_stats_sidebar": "Statistiche di connessione",
"connection_stats_video": "Video",
"connection_stats_video_description": "Il flusso video dal JetKVM al client.", "connection_stats_video_description": "Il flusso video dal JetKVM al client.",
"connection_stats_video": "Video",
"continue": "Continuare", "continue": "Continuare",
"creating_peer_connection": "Creazione di una connessione tra pari…", "creating_peer_connection": "Creazione di una connessione tra pari…",
"dc_power_control_current": "Attuale",
"dc_power_control_current_unit": "UN", "dc_power_control_current_unit": "UN",
"dc_power_control_current": "Attuale",
"dc_power_control_get_state_error": "Impossibile ottenere lo stato di alimentazione CC: {error}", "dc_power_control_get_state_error": "Impossibile ottenere lo stato di alimentazione CC: {error}",
"dc_power_control_power": "Energia",
"dc_power_control_power_off_button": "Spegnimento", "dc_power_control_power_off_button": "Spegnimento",
"dc_power_control_power_off_state": "Spegnimento", "dc_power_control_power_off_state": "Spegnimento",
"dc_power_control_power_on_button": "Accensione", "dc_power_control_power_on_button": "Accensione",
"dc_power_control_power_on_state": "Accensione", "dc_power_control_power_on_state": "Accensione",
"dc_power_control_power_unit": "O", "dc_power_control_power_unit": "O",
"dc_power_control_power": "Energia",
"dc_power_control_restore_last_state": "Ultimo stato", "dc_power_control_restore_last_state": "Ultimo stato",
"dc_power_control_restore_power_state": "Ripristinare la perdita di potenza", "dc_power_control_restore_power_state": "Ripristinare la perdita di potenza",
"dc_power_control_set_power_state_error": "Impossibile inviare lo stato di alimentazione CC a {enabled} : {error}", "dc_power_control_set_power_state_error": "Impossibile inviare lo stato di alimentazione CC a {enabled} : {error}",
"dc_power_control_set_restore_state_error": "Impossibile inviare lo stato di ripristino dell'alimentazione CC a {state} : {error}", "dc_power_control_set_restore_state_error": "Impossibile inviare lo stato di ripristino dell'alimentazione CC a {state} : {error}",
"dc_power_control_voltage": "Voltaggio",
"dc_power_control_voltage_unit": "V", "dc_power_control_voltage_unit": "V",
"dc_power_control_voltage": "Voltaggio",
"delete": "Eliminare", "delete": "Eliminare",
"deregister_button": "Annulla registrazione dal cloud", "deregister_button": "Annulla registrazione dal cloud",
"deregister_cloud_devices": "Dispositivi cloud", "deregister_cloud_devices": "Dispositivi cloud",
@ -226,12 +226,12 @@
"extension_popover_load_and_manage_extensions": "Carica e gestisci le tue estensioni", "extension_popover_load_and_manage_extensions": "Carica e gestisci le tue estensioni",
"extension_popover_set_error_notification": "Impossibile impostare l'estensione attiva: {error}", "extension_popover_set_error_notification": "Impossibile impostare l'estensione attiva: {error}",
"extension_popover_unload_extension": "Estensione di scaricamento", "extension_popover_unload_extension": "Estensione di scaricamento",
"extension_serial_console": "Console seriale",
"extension_serial_console_description": "Accedi all'estensione della tua console seriale", "extension_serial_console_description": "Accedi all'estensione della tua console seriale",
"extensions_atx_power_control": "Controllo di potenza ATX", "extension_serial_console": "Console seriale",
"extensions_atx_power_control_description": "Controlla lo stato di alimentazione del tuo computer tramite il controllo di alimentazione ATX.", "extensions_atx_power_control_description": "Controlla lo stato di alimentazione del tuo computer tramite il controllo di alimentazione ATX.",
"extensions_dc_power_control": "Controllo di potenza CC", "extensions_atx_power_control": "Controllo di potenza ATX",
"extensions_dc_power_control_description": "Controlla la tua estensione di alimentazione CC", "extensions_dc_power_control_description": "Controlla la tua estensione di alimentazione CC",
"extensions_dc_power_control": "Controllo di potenza CC",
"extensions_popover_extensions": "Estensioni", "extensions_popover_extensions": "Estensioni",
"gathering_ice_candidates": "Raduno dei candidati ICE…", "gathering_ice_candidates": "Raduno dei candidati ICE…",
"general_app_version": "App: {version}", "general_app_version": "App: {version}",
@ -241,8 +241,8 @@
"general_check_for_updates": "Controlla gli aggiornamenti", "general_check_for_updates": "Controlla gli aggiornamenti",
"general_page_description": "Configurare le impostazioni del dispositivo e aggiornare le preferenze", "general_page_description": "Configurare le impostazioni del dispositivo e aggiornare le preferenze",
"general_reboot_description": "Vuoi procedere con il riavvio del sistema?", "general_reboot_description": "Vuoi procedere con il riavvio del sistema?",
"general_reboot_device": "Riavvia il dispositivo",
"general_reboot_device_description": "Spegnere e riaccendere JetKVM", "general_reboot_device_description": "Spegnere e riaccendere JetKVM",
"general_reboot_device": "Riavvia il dispositivo",
"general_reboot_no_button": "NO", "general_reboot_no_button": "NO",
"general_reboot_title": "Riavviare JetKVM", "general_reboot_title": "Riavviare JetKVM",
"general_reboot_yes_button": "SÌ", "general_reboot_yes_button": "SÌ",
@ -295,9 +295,9 @@
"hardware_display_orientation_title": "Orientamento dello schermo", "hardware_display_orientation_title": "Orientamento dello schermo",
"hardware_display_wake_up_note": "Il display si riattiverà quando cambia lo stato della connessione o quando viene toccato.", "hardware_display_wake_up_note": "Il display si riattiverà quando cambia lo stato della connessione o quando viene toccato.",
"hardware_page_description": "Configura le impostazioni di visualizzazione e le opzioni hardware per il tuo dispositivo JetKVM", "hardware_page_description": "Configura le impostazioni di visualizzazione e le opzioni hardware per il tuo dispositivo JetKVM",
"hardware_time_10_minutes": "10 minuti",
"hardware_time_1_hour": "1 ora", "hardware_time_1_hour": "1 ora",
"hardware_time_1_minute": "1 minuto", "hardware_time_1_minute": "1 minuto",
"hardware_time_10_minutes": "10 minuti",
"hardware_time_30_minutes": "30 minuti", "hardware_time_30_minutes": "30 minuti",
"hardware_time_5_minutes": "5 minuti", "hardware_time_5_minutes": "5 minuti",
"hardware_time_never": "Mai", "hardware_time_never": "Mai",
@ -327,7 +327,6 @@
"invalid_password": "Password non valida", "invalid_password": "Password non valida",
"ip_address": "Indirizzo IP", "ip_address": "Indirizzo IP",
"ipv6_address_label": "Indirizzo", "ipv6_address_label": "Indirizzo",
"ipv6_gateway": "Portale",
"ipv6_information": "Informazioni IPv6", "ipv6_information": "Informazioni IPv6",
"ipv6_link_local": "Collegamento locale", "ipv6_link_local": "Collegamento locale",
"ipv6_preferred_lifetime": "Durata preferita", "ipv6_preferred_lifetime": "Durata preferita",
@ -410,8 +409,8 @@
"log_in": "Login", "log_in": "Login",
"log_out": "Disconnetti", "log_out": "Disconnetti",
"logged_in_as": "Accedi come", "logged_in_as": "Accedi come",
"login_enter_password": "Inserisci la tua password",
"login_enter_password_description": "Inserisci la tua password per accedere al tuo JetKVM.", "login_enter_password_description": "Inserisci la tua password per accedere al tuo JetKVM.",
"login_enter_password": "Inserisci la tua password",
"login_error": "Si è verificato un errore durante l'accesso", "login_error": "Si è verificato un errore durante l'accesso",
"login_forgot_password": "Ha dimenticato la password?", "login_forgot_password": "Ha dimenticato la password?",
"login_password_label": "Password", "login_password_label": "Password",
@ -425,8 +424,8 @@
"macro_name_required": "Il nome è obbligatorio", "macro_name_required": "Il nome è obbligatorio",
"macro_name_too_long": "Il nome deve contenere meno di 50 caratteri", "macro_name_too_long": "Il nome deve contenere meno di 50 caratteri",
"macro_please_fix_validation_errors": "Si prega di correggere gli errori di convalida", "macro_please_fix_validation_errors": "Si prega di correggere gli errori di convalida",
"macro_save": "Salva macro",
"macro_save_error": "Si è verificato un errore durante il salvataggio.", "macro_save_error": "Si è verificato un errore durante il salvataggio.",
"macro_save": "Salva macro",
"macro_step_count": "{steps} / {max} steps", "macro_step_count": "{steps} / {max} steps",
"macro_step_duration_description": "Tempo di attesa prima di eseguire il passaggio successivo.", "macro_step_duration_description": "Tempo di attesa prima di eseguire il passaggio successivo.",
"macro_step_duration_label": "Durata del passo", "macro_step_duration_label": "Durata del passo",
@ -440,8 +439,8 @@
"macro_steps_description": "Tasti/modificatori eseguiti in sequenza con un ritardo tra ogni passaggio.", "macro_steps_description": "Tasti/modificatori eseguiti in sequenza con un ritardo tra ogni passaggio.",
"macro_steps_label": "Passi", "macro_steps_label": "Passi",
"macros_add_description": "Crea una nuova macro della tastiera", "macros_add_description": "Crea una nuova macro della tastiera",
"macros_add_new": "Aggiungi nuova macro",
"macros_add_new_macro": "Aggiungi nuova macro", "macros_add_new_macro": "Aggiungi nuova macro",
"macros_add_new": "Aggiungi nuova macro",
"macros_aria_add_new": "Aggiungi nuova macro", "macros_aria_add_new": "Aggiungi nuova macro",
"macros_aria_delete": "Elimina macro {name}", "macros_aria_delete": "Elimina macro {name}",
"macros_aria_duplicate": "Macro duplicata {name}", "macros_aria_duplicate": "Macro duplicata {name}",
@ -463,16 +462,16 @@
"macros_edit_button": "Modificare", "macros_edit_button": "Modificare",
"macros_edit_description": "Modifica la macro della tastiera", "macros_edit_description": "Modifica la macro della tastiera",
"macros_edit_title": "Modifica macro", "macros_edit_title": "Modifica macro",
"macros_failed_create": "Impossibile creare la macro",
"macros_failed_create_error": "Impossibile creare la macro: {error}", "macros_failed_create_error": "Impossibile creare la macro: {error}",
"macros_failed_delete": "Impossibile eliminare la macro", "macros_failed_create": "Impossibile creare la macro",
"macros_failed_delete_error": "Impossibile eliminare la macro: {error}", "macros_failed_delete_error": "Impossibile eliminare la macro: {error}",
"macros_failed_duplicate": "Impossibile duplicare la macro", "macros_failed_delete": "Impossibile eliminare la macro",
"macros_failed_duplicate_error": "Impossibile duplicare la macro: {error}", "macros_failed_duplicate_error": "Impossibile duplicare la macro: {error}",
"macros_failed_reorder": "Impossibile riordinare le macro", "macros_failed_duplicate": "Impossibile duplicare la macro",
"macros_failed_reorder_error": "Impossibile riordinare le macro: {error}", "macros_failed_reorder_error": "Impossibile riordinare le macro: {error}",
"macros_failed_update": "Impossibile aggiornare la macro", "macros_failed_reorder": "Impossibile riordinare le macro",
"macros_failed_update_error": "Impossibile aggiornare la macro: {error}", "macros_failed_update_error": "Impossibile aggiornare la macro: {error}",
"macros_failed_update": "Impossibile aggiornare la macro",
"macros_invalid_data": "Dati macro non validi", "macros_invalid_data": "Dati macro non validi",
"macros_loading": "Caricamento macro in corso…", "macros_loading": "Caricamento macro in corso…",
"macros_max_reached": "Massimo raggiunto", "macros_max_reached": "Massimo raggiunto",
@ -507,8 +506,8 @@
"mount_error_list_storage": "Errore nell'elenco dei file di archiviazione: {error}", "mount_error_list_storage": "Errore nell'elenco dei file di archiviazione: {error}",
"mount_error_title": "Errore di montaggio", "mount_error_title": "Errore di montaggio",
"mount_get_state_error": "Impossibile ottenere lo stato del supporto virtuale: {error}", "mount_get_state_error": "Impossibile ottenere lo stato del supporto virtuale: {error}",
"mount_jetkvm_storage": "Montaggio di archiviazione JetKVM",
"mount_jetkvm_storage_description": "Montare i file caricati in precedenza dall'archiviazione JetKVM", "mount_jetkvm_storage_description": "Montare i file caricati in precedenza dall'archiviazione JetKVM",
"mount_jetkvm_storage": "Montaggio di archiviazione JetKVM",
"mount_mode_cdrom": "CD/DVD", "mount_mode_cdrom": "CD/DVD",
"mount_mode_disk": "Disco", "mount_mode_disk": "Disco",
"mount_mounted_as": "Montato come", "mount_mounted_as": "Montato come",
@ -521,8 +520,8 @@
"mount_popular_images": "Immagini popolari", "mount_popular_images": "Immagini popolari",
"mount_streaming_from_url": "Streaming da URL", "mount_streaming_from_url": "Streaming da URL",
"mount_supported_formats": "Formati supportati: ISO, IMG", "mount_supported_formats": "Formati supportati: ISO, IMG",
"mount_unmount": "Smontare",
"mount_unmount_error": "Impossibile smontare l'immagine: {error}", "mount_unmount_error": "Impossibile smontare l'immagine: {error}",
"mount_unmount": "Smontare",
"mount_upload_description": "Seleziona un file immagine da caricare nell'archiviazione JetKVM", "mount_upload_description": "Seleziona un file immagine da caricare nell'archiviazione JetKVM",
"mount_upload_error": "Errore di caricamento: {error}", "mount_upload_error": "Errore di caricamento: {error}",
"mount_upload_failed_datachannel": "Impossibile creare il canale dati per il caricamento del file", "mount_upload_failed_datachannel": "Impossibile creare il canale dati per il caricamento del file",
@ -530,8 +529,8 @@
"mount_upload_successful": "Caricamento riuscito", "mount_upload_successful": "Caricamento riuscito",
"mount_upload_title": "Carica nuova immagine", "mount_upload_title": "Carica nuova immagine",
"mount_uploaded_has_been_uploaded": "{name} è stato caricato", "mount_uploaded_has_been_uploaded": "{name} è stato caricato",
"mount_uploading": "Caricamento in corso…",
"mount_uploading_with_name": "Caricamento in corso {name}", "mount_uploading_with_name": "Caricamento in corso {name}",
"mount_uploading": "Caricamento in corso…",
"mount_url_description": "Montare file da qualsiasi indirizzo web pubblico", "mount_url_description": "Montare file da qualsiasi indirizzo web pubblico",
"mount_url_input_label": "URL dell'immagine", "mount_url_input_label": "URL dell'immagine",
"mount_url_mount": "Montaggio URL", "mount_url_mount": "Montaggio URL",
@ -539,10 +538,10 @@
"mount_view_device_title": "Monta da JetKVM Storage", "mount_view_device_title": "Monta da JetKVM Storage",
"mount_view_url_description": "Inserisci un URL al file immagine da montare", "mount_view_url_description": "Inserisci un URL al file immagine da montare",
"mount_view_url_title": "Monta da URL", "mount_view_url_title": "Monta da URL",
"mount_virtual_media": "Media virtuali",
"mount_virtual_media_description": "Montare un'immagine da cui avviare o installare un sistema operativo.", "mount_virtual_media_description": "Montare un'immagine da cui avviare o installare un sistema operativo.",
"mount_virtual_media_source": "Fonte multimediale virtuale",
"mount_virtual_media_source_description": "Scegli come vuoi montare i tuoi media virtuali", "mount_virtual_media_source_description": "Scegli come vuoi montare i tuoi media virtuali",
"mount_virtual_media_source": "Fonte multimediale virtuale",
"mount_virtual_media": "Media virtuali",
"mouse_alt_finger": "Dito che tocca uno schermo", "mouse_alt_finger": "Dito che tocca uno schermo",
"mouse_alt_mouse": "Icona del mouse", "mouse_alt_mouse": "Icona del mouse",
"mouse_description": "Configura il comportamento del cursore e le impostazioni di interazione per il tuo dispositivo", "mouse_description": "Configura il comportamento del cursore e le impostazioni di interazione per il tuo dispositivo",
@ -559,10 +558,10 @@
"mouse_jiggler_light": "Luce - 5m", "mouse_jiggler_light": "Luce - 5m",
"mouse_jiggler_standard": "Standard - 1m", "mouse_jiggler_standard": "Standard - 1m",
"mouse_jiggler_title": "Jiggler", "mouse_jiggler_title": "Jiggler",
"mouse_mode_absolute": "Assoluto",
"mouse_mode_absolute_description": "Il più conveniente", "mouse_mode_absolute_description": "Il più conveniente",
"mouse_mode_relative": "Relativo", "mouse_mode_absolute": "Assoluto",
"mouse_mode_relative_description": "Più compatibile", "mouse_mode_relative_description": "Più compatibile",
"mouse_mode_relative": "Relativo",
"mouse_modes_description": "Scegli la modalità di input del mouse", "mouse_modes_description": "Scegli la modalità di input del mouse",
"mouse_modes_title": "Modalità", "mouse_modes_title": "Modalità",
"mouse_scroll_high": "Alto", "mouse_scroll_high": "Alto",
@ -575,17 +574,12 @@
"mouse_title": "Topo", "mouse_title": "Topo",
"network_custom_domain": "Dominio personalizzato", "network_custom_domain": "Dominio personalizzato",
"network_description": "Configura le impostazioni di rete", "network_description": "Configura le impostazioni di rete",
"network_dhcp_client_description": "Configurare quale client DHCP utilizzare",
"network_dhcp_client_jetkvm": "JetKVM interno",
"network_dhcp_client_title": "Cliente DHCP",
"network_dhcp_information": "Informazioni DHCP", "network_dhcp_information": "Informazioni DHCP",
"network_dhcp_lease_renew": "Rinnova il contratto di locazione DHCP",
"network_dhcp_lease_renew_confirm": "Rinnovare il contratto di locazione",
"network_dhcp_lease_renew_confirm_description": "Verrà richiesto un nuovo indirizzo IP al server DHCP. Durante questo processo, il dispositivo potrebbe perdere temporaneamente la connettività di rete.", "network_dhcp_lease_renew_confirm_description": "Verrà richiesto un nuovo indirizzo IP al server DHCP. Durante questo processo, il dispositivo potrebbe perdere temporaneamente la connettività di rete.",
"network_dhcp_lease_renew_confirm_new_a": "Se ricevi un nuovo indirizzo IP", "network_dhcp_lease_renew_confirm": "Rinnovare il contratto di locazione",
"network_dhcp_lease_renew_confirm_new_b": "potrebbe essere necessario riconnettersi utilizzando il nuovo indirizzo",
"network_dhcp_lease_renew_failed": "Impossibile rinnovare il contratto di locazione: {error}", "network_dhcp_lease_renew_failed": "Impossibile rinnovare il contratto di locazione: {error}",
"network_dhcp_lease_renew_success": "Rinnovo del contratto di locazione DHCP", "network_dhcp_lease_renew_success": "Rinnovo del contratto di locazione DHCP",
"network_dhcp_lease_renew": "Rinnova il contratto di locazione DHCP",
"network_domain_custom": "Costume", "network_domain_custom": "Costume",
"network_domain_description": "Suffisso del dominio di rete per il dispositivo", "network_domain_description": "Suffisso del dominio di rete per il dispositivo",
"network_domain_dhcp_provided": "DHCP fornito", "network_domain_dhcp_provided": "DHCP fornito",
@ -594,35 +588,21 @@
"network_hostname_description": "Identificatore del dispositivo sulla rete. Vuoto per impostazione predefinita del sistema", "network_hostname_description": "Identificatore del dispositivo sulla rete. Vuoto per impostazione predefinita del sistema",
"network_hostname_title": "Nome host", "network_hostname_title": "Nome host",
"network_http_proxy_description": "Server proxy per le richieste HTTP(S) in uscita dal dispositivo. Vuoto per nessuna richiesta.", "network_http_proxy_description": "Server proxy per le richieste HTTP(S) in uscita dal dispositivo. Vuoto per nessuna richiesta.",
"network_http_proxy_invalid": "URL proxy HTTP non valido",
"network_http_proxy_title": "Proxy HTTP", "network_http_proxy_title": "Proxy HTTP",
"network_ipv4_address": "Indirizzo IPv4",
"network_ipv4_dns": "DNS IPv4",
"network_ipv4_gateway": "Gateway IPv4",
"network_ipv4_mode_description": "Configurare la modalità IPv4", "network_ipv4_mode_description": "Configurare la modalità IPv4",
"network_ipv4_mode_dhcp": "DHCP", "network_ipv4_mode_dhcp": "DHCP",
"network_ipv4_mode_static": "Statico",
"network_ipv4_mode_title": "Modalità IPv4", "network_ipv4_mode_title": "Modalità IPv4",
"network_ipv4_netmask": "Maschera di rete IPv4",
"network_ipv6_address": "Indirizzo IPv6",
"network_ipv6_information": "Informazioni IPv6", "network_ipv6_information": "Informazioni IPv6",
"network_ipv6_mode_description": "Configurare la modalità IPv6", "network_ipv6_mode_description": "Configurare la modalità IPv6",
"network_ipv6_mode_dhcpv6": "DHCPv6",
"network_ipv6_mode_disabled": "Disabili", "network_ipv6_mode_disabled": "Disabili",
"network_ipv6_mode_link_local": "Solo collegamento locale",
"network_ipv6_mode_slaac": "SLAAC", "network_ipv6_mode_slaac": "SLAAC",
"network_ipv6_mode_slaac_dhcpv6": "SLAAC + DHCPv6",
"network_ipv6_mode_static": "Statico",
"network_ipv6_mode_title": "Modalità IPv6", "network_ipv6_mode_title": "Modalità IPv6",
"network_ipv6_netmask": "Maschera di rete IPv6",
"network_ipv6_no_addresses": "Nessun indirizzo IPv6 configurato", "network_ipv6_no_addresses": "Nessun indirizzo IPv6 configurato",
"network_ll_dp_all": "Tutto", "network_ll_dp_all": "Tutto",
"network_ll_dp_basic": "Di base", "network_ll_dp_basic": "Di base",
"network_ll_dp_description": "Controlla quali TLV verranno inviati tramite Link Layer Discovery Protocol", "network_ll_dp_description": "Controlla quali TLV verranno inviati tramite Link Layer Discovery Protocol",
"network_ll_dp_disabled": "Disabili", "network_ll_dp_disabled": "Disabili",
"network_ll_dp_title": "LLDP", "network_ll_dp_title": "LLDP",
"network_mac_address_copy_error": "Impossibile copiare l'indirizzo MAC",
"network_mac_address_copy_success": "Indirizzo MAC { mac } copiato negli appunti",
"network_mac_address_description": "Identificatore hardware per l'interfaccia di rete", "network_mac_address_description": "Identificatore hardware per l'interfaccia di rete",
"network_mac_address_title": "Indirizzo MAC", "network_mac_address_title": "Indirizzo MAC",
"network_mdns_auto": "Auto", "network_mdns_auto": "Auto",
@ -632,19 +612,9 @@
"network_mdns_ipv6_only": "Solo IPv6", "network_mdns_ipv6_only": "Solo IPv6",
"network_mdns_title": "mDNS", "network_mdns_title": "mDNS",
"network_no_dhcp_lease": "Nessuna informazione disponibile sul lease DHCP", "network_no_dhcp_lease": "Nessuna informazione disponibile sul lease DHCP",
"network_no_information_description": "Nessuna configurazione di rete disponibile",
"network_no_information_headline": "Informazioni di rete",
"network_pending_dhcp_mode_change_description": "Salva le impostazioni per abilitare la modalità DHCP e visualizzare le informazioni di locazione",
"network_pending_dhcp_mode_change_headline": "In attesa di modifica della modalità DHCP IPv4",
"network_save_settings": "Salva impostazioni",
"network_save_settings_apply_title": "Applica le impostazioni di rete",
"network_save_settings_confirm": "Applica modifiche",
"network_save_settings_confirm_description": "Verranno applicate le seguenti impostazioni di rete. Queste modifiche potrebbero richiedere un riavvio e causare una breve disconnessione.",
"network_save_settings_confirm_heading": "Modifiche alla configurazione",
"network_save_settings_failed": "Impossibile salvare le impostazioni di rete: {error}", "network_save_settings_failed": "Impossibile salvare le impostazioni di rete: {error}",
"network_save_settings_success": "Impostazioni di rete salvate", "network_save_settings_success": "Impostazioni di rete salvate",
"network_settings_invalid_ipv4_cidr": "Notazione CIDR non valida per l'indirizzo IPv4", "network_save_settings": "Salva impostazioni",
"network_settings_load_error": "Impossibile caricare le impostazioni di rete: {error}",
"network_time_sync_description": "Configurare le impostazioni di sincronizzazione dell'ora", "network_time_sync_description": "Configurare le impostazioni di sincronizzazione dell'ora",
"network_time_sync_http_only": "Solo HTTP", "network_time_sync_http_only": "Solo HTTP",
"network_time_sync_ntp_and_http": "NTP e HTTP", "network_time_sync_ntp_and_http": "NTP e HTTP",
@ -670,8 +640,8 @@
"paste_modal_failed_paste": "Impossibile incollare il testo: {error}", "paste_modal_failed_paste": "Impossibile incollare il testo: {error}",
"paste_modal_invalid_chars_intro": "I seguenti caratteri non verranno incollati:", "paste_modal_invalid_chars_intro": "I seguenti caratteri non verranno incollati:",
"paste_modal_paste_from_host": "Incolla dall'host", "paste_modal_paste_from_host": "Incolla dall'host",
"paste_modal_paste_text": "Incolla il testo",
"paste_modal_paste_text_description": "Incolla il testo dal tuo client all'host remoto", "paste_modal_paste_text_description": "Incolla il testo dal tuo client all'host remoto",
"paste_modal_paste_text": "Incolla il testo",
"paste_modal_sending_using_layout": "Invio di testo tramite layout di tastiera: {iso} - {name}", "paste_modal_sending_using_layout": "Invio di testo tramite layout di tastiera: {iso} - {name}",
"peer_connection_closed": "Chiuso", "peer_connection_closed": "Chiuso",
"peer_connection_closing": "Chiusura", "peer_connection_closing": "Chiusura",
@ -698,20 +668,20 @@
"retry": "Riprova", "retry": "Riprova",
"saving": "Risparmio…", "saving": "Risparmio…",
"search_placeholder": "Ricerca…", "search_placeholder": "Ricerca…",
"serial_console": "Console seriale",
"serial_console_baud_rate": "Velocità in baud", "serial_console_baud_rate": "Velocità in baud",
"serial_console_configure_description": "Configura le impostazioni della tua console seriale", "serial_console_configure_description": "Configura le impostazioni della tua console seriale",
"serial_console_data_bits": "Bit di dati", "serial_console_data_bits": "Bit di dati",
"serial_console_get_settings_error": "Impossibile ottenere le impostazioni della console seriale: {error}", "serial_console_get_settings_error": "Impossibile ottenere le impostazioni della console seriale: {error}",
"serial_console_open_console": "Apri console", "serial_console_open_console": "Apri console",
"serial_console_parity": "Parità",
"serial_console_parity_even": "Parità pari", "serial_console_parity_even": "Parità pari",
"serial_console_parity_mark": "Segna parità", "serial_console_parity_mark": "Segna parità",
"serial_console_parity_none": "Nessuna parità", "serial_console_parity_none": "Nessuna parità",
"serial_console_parity_odd": "Parità dispari", "serial_console_parity_odd": "Parità dispari",
"serial_console_parity_space": "Parità spaziale", "serial_console_parity_space": "Parità spaziale",
"serial_console_parity": "Parità",
"serial_console_set_settings_error": "Impossibile impostare le impostazioni della console seriale su {settings} : {error}", "serial_console_set_settings_error": "Impossibile impostare le impostazioni della console seriale su {settings} : {error}",
"serial_console_stop_bits": "Bit di stop", "serial_console_stop_bits": "Bit di stop",
"serial_console": "Console seriale",
"setting_remote_description": "Impostazione della descrizione remota", "setting_remote_description": "Impostazione della descrizione remota",
"setting_remote_session_description": "Impostazione della descrizione della sessione remota...", "setting_remote_session_description": "Impostazione della descrizione della sessione remota...",
"setting_up_connection_to_device": "Impostazione della connessione al dispositivo...", "setting_up_connection_to_device": "Impostazione della connessione al dispositivo...",
@ -721,8 +691,8 @@
"settings_back_to_kvm": "Torna a KVM", "settings_back_to_kvm": "Torna a KVM",
"settings_general": "Generale", "settings_general": "Generale",
"settings_hardware": "Hardware", "settings_hardware": "Hardware",
"settings_keyboard": "Tastiera",
"settings_keyboard_macros": "Macro della tastiera", "settings_keyboard_macros": "Macro della tastiera",
"settings_keyboard": "Tastiera",
"settings_mouse": "Topo", "settings_mouse": "Topo",
"settings_network": "Rete", "settings_network": "Rete",
"settings_video": "Video", "settings_video": "Video",
@ -742,7 +712,6 @@
"updates_failed_check": "Impossibile verificare gli aggiornamenti: {error}", "updates_failed_check": "Impossibile verificare gli aggiornamenti: {error}",
"updates_failed_get_device_version": "Impossibile ottenere la versione del dispositivo: {error}", "updates_failed_get_device_version": "Impossibile ottenere la versione del dispositivo: {error}",
"updating_leave_device_on": "Per favore, non spegnere il tuo dispositivo…", "updating_leave_device_on": "Per favore, non spegnere il tuo dispositivo…",
"usb": "USB",
"usb_config_custom": "Costume", "usb_config_custom": "Costume",
"usb_config_default": "JetKVM predefinito", "usb_config_default": "JetKVM predefinito",
"usb_config_dell": "Tastiera Dell Multimedia Pro", "usb_config_dell": "Tastiera Dell Multimedia Pro",
@ -789,6 +758,7 @@
"usb_state_connecting": "Collegamento", "usb_state_connecting": "Collegamento",
"usb_state_disconnected": "Disconnesso", "usb_state_disconnected": "Disconnesso",
"usb_state_low_power_mode": "Modalità a basso consumo", "usb_state_low_power_mode": "Modalità a basso consumo",
"usb": "USB",
"user_interface_language_description": "Seleziona la lingua da utilizzare nell'interfaccia utente JetKVM", "user_interface_language_description": "Seleziona la lingua da utilizzare nell'interfaccia utente JetKVM",
"user_interface_language_title": "Lingua dell'interfaccia", "user_interface_language_title": "Lingua dell'interfaccia",
"video_brightness_description": "Livello di luminosità ( {value} x)", "video_brightness_description": "Livello di luminosità ( {value} x)",
@ -855,7 +825,6 @@
"video_title": "Video", "video_title": "Video",
"view_details": "Visualizza dettagli", "view_details": "Visualizza dettagli",
"virtual_keyboard_header": "Tastiera virtuale", "virtual_keyboard_header": "Tastiera virtuale",
"wake_on_lan": "Wake On LAN",
"wake_on_lan_add_device_device_name": "Nome del dispositivo", "wake_on_lan_add_device_device_name": "Nome del dispositivo",
"wake_on_lan_add_device_example_device_name": "Server multimediale Plex", "wake_on_lan_add_device_example_device_name": "Server multimediale Plex",
"wake_on_lan_add_device_mac_address": "Indirizzo MAC", "wake_on_lan_add_device_mac_address": "Indirizzo MAC",
@ -871,6 +840,7 @@
"wake_on_lan_failed_send_magic": "Impossibile inviare il pacchetto magico", "wake_on_lan_failed_send_magic": "Impossibile inviare il pacchetto magico",
"wake_on_lan_invalid_mac": "Indirizzo MAC non valido", "wake_on_lan_invalid_mac": "Indirizzo MAC non valido",
"wake_on_lan_magic_sent_success": "Pacchetto magico inviato con successo", "wake_on_lan_magic_sent_success": "Pacchetto magico inviato con successo",
"welcome_to_jetkvm": "Benvenuti a JetKVM", "wake_on_lan": "Wake On LAN",
"welcome_to_jetkvm_description": "Controlla qualsiasi computer da remoto" "welcome_to_jetkvm_description": "Controlla qualsiasi computer da remoto",
"welcome_to_jetkvm": "Benvenuti a JetKVM"
} }

View File

@ -107,10 +107,10 @@
"already_adopted_title": "Enheten er allerede registrert", "already_adopted_title": "Enheten er allerede registrert",
"appearance_description": "Velg ditt foretrukne fargetema", "appearance_description": "Velg ditt foretrukne fargetema",
"appearance_page_description": "Tilpass utseendet og følelsen til JetKVM-grensesnittet ditt", "appearance_page_description": "Tilpass utseendet og følelsen til JetKVM-grensesnittet ditt",
"appearance_theme": "Tema",
"appearance_theme_dark": "Mørk", "appearance_theme_dark": "Mørk",
"appearance_theme_light": "Lys", "appearance_theme_light": "Lys",
"appearance_theme_system": "System", "appearance_theme_system": "System",
"appearance_theme": "Tema",
"appearance_title": "Utseende", "appearance_title": "Utseende",
"attach": "Feste", "attach": "Feste",
"atx_power_control_get_state_error": "Klarte ikke å hente ATX-strømstatus: {error}", "atx_power_control_get_state_error": "Klarte ikke å hente ATX-strømstatus: {error}",
@ -121,85 +121,85 @@
"atx_power_control_reset_button": "Tilbakestill", "atx_power_control_reset_button": "Tilbakestill",
"atx_power_control_send_action_error": "Kunne ikke sende ATX-strømhandling {action} : {error}", "atx_power_control_send_action_error": "Kunne ikke sende ATX-strømhandling {action} : {error}",
"atx_power_control_short_power_button": "Kort trykk", "atx_power_control_short_power_button": "Kort trykk",
"auth_authentication_mode": "Vennligst velg en autentiseringsmodus",
"auth_authentication_mode_error": "Det oppsto en feil under angivelse av autentiseringsmodus", "auth_authentication_mode_error": "Det oppsto en feil under angivelse av autentiseringsmodus",
"auth_authentication_mode_invalid": "Ugyldig autentiseringsmodus", "auth_authentication_mode_invalid": "Ugyldig autentiseringsmodus",
"auth_connect_to_cloud": "Koble JetKVM-en din til skyen", "auth_authentication_mode": "Vennligst velg en autentiseringsmodus",
"auth_connect_to_cloud_action": "Logg inn og koble til enheten", "auth_connect_to_cloud_action": "Logg inn og koble til enheten",
"auth_connect_to_cloud_description": "Lås opp fjerntilgang og avanserte funksjoner for enheten din", "auth_connect_to_cloud_description": "Lås opp fjerntilgang og avanserte funksjoner for enheten din",
"auth_connect_to_cloud": "Koble JetKVM-en din til skyen",
"auth_header_cta_already_have_account": "Har du allerede en konto?", "auth_header_cta_already_have_account": "Har du allerede en konto?",
"auth_header_cta_dont_have_account": "Har du ikke en konto?", "auth_header_cta_dont_have_account": "Har du ikke en konto?",
"auth_header_cta_new_to_jetkvm": "Ny bruker av JetKVM?", "auth_header_cta_new_to_jetkvm": "Ny bruker av JetKVM?",
"auth_login": "Logg inn på JetKVM-kontoen din",
"auth_login_action": "Logg inn", "auth_login_action": "Logg inn",
"auth_login_description": "Logg inn for å få tilgang til og administrere enhetene dine på en sikker måte", "auth_login_description": "Logg inn for å få tilgang til og administrere enhetene dine på en sikker måte",
"auth_mode_local": "Lokal autentiseringsmetode", "auth_login": "Logg inn på JetKVM-kontoen din",
"auth_mode_local_change_later": "Du kan alltid endre autentiseringsmetoden din senere i innstillingene.", "auth_mode_local_change_later": "Du kan alltid endre autentiseringsmetoden din senere i innstillingene.",
"auth_mode_local_description": "Velg hvordan du vil sikre JetKVM-enheten din lokalt.", "auth_mode_local_description": "Velg hvordan du vil sikre JetKVM-enheten din lokalt.",
"auth_mode_local_no_password": "Ikke noe passord",
"auth_mode_local_no_password_description": "Rask tilgang uten passordgodkjenning.", "auth_mode_local_no_password_description": "Rask tilgang uten passordgodkjenning.",
"auth_mode_local_password": "Passord", "auth_mode_local_no_password": "Ikke noe passord",
"auth_mode_local_password_confirm_description": "Bekreft passordet ditt", "auth_mode_local_password_confirm_description": "Bekreft passordet ditt",
"auth_mode_local_password_confirm_label": "Bekreft passord", "auth_mode_local_password_confirm_label": "Bekreft passord",
"auth_mode_local_password_description": "Sikre enheten din med et passord for ekstra beskyttelse.", "auth_mode_local_password_description": "Sikre enheten din med et passord for ekstra beskyttelse.",
"auth_mode_local_password_do_not_match": "Passordene stemmer ikke overens", "auth_mode_local_password_do_not_match": "Passordene stemmer ikke overens",
"auth_mode_local_password_failed_set": "Klarte ikke å angi passord: {error}", "auth_mode_local_password_failed_set": "Klarte ikke å angi passord: {error}",
"auth_mode_local_password_note": "Dette passordet vil bli brukt til å sikre enhetsdataene dine og beskytte mot uautorisert tilgang.",
"auth_mode_local_password_note_local": "Alle dataene forblir på din lokale enhet.", "auth_mode_local_password_note_local": "Alle dataene forblir på din lokale enhet.",
"auth_mode_local_password_set": "Angi et passord", "auth_mode_local_password_note": "Dette passordet vil bli brukt til å sikre enhetsdataene dine og beskytte mot uautorisert tilgang.",
"auth_mode_local_password_set_button": "Angi passord", "auth_mode_local_password_set_button": "Angi passord",
"auth_mode_local_password_set_description": "Opprett et sterkt passord for å sikre JetKVM-enheten din lokalt.", "auth_mode_local_password_set_description": "Opprett et sterkt passord for å sikre JetKVM-enheten din lokalt.",
"auth_mode_local_password_set_label": "Skriv inn et passord", "auth_mode_local_password_set_label": "Skriv inn et passord",
"auth_mode_local_password_set": "Angi et passord",
"auth_mode_local_password": "Passord",
"auth_mode_local": "Lokal autentiseringsmetode",
"auth_signup_connect_to_cloud_action": "Registrer og koble til enhet", "auth_signup_connect_to_cloud_action": "Registrer og koble til enhet",
"auth_signup_create_account": "Opprett JetKVM-kontoen din",
"auth_signup_create_account_action": "Opprett konto", "auth_signup_create_account_action": "Opprett konto",
"auth_signup_create_account_description": "Opprett kontoen din og begynn å administrere enhetene dine med letthet.", "auth_signup_create_account_description": "Opprett kontoen din og begynn å administrere enhetene dine med letthet.",
"back": "Tilbake", "auth_signup_create_account": "Opprett JetKVM-kontoen din",
"back_to_devices": "Tilbake til Enheter", "back_to_devices": "Tilbake til Enheter",
"back": "Tilbake",
"cancel": "Kansellere", "cancel": "Kansellere",
"close": "Lukke", "close": "Lukke",
"cloud_kvms": "Cloud KVM-er",
"cloud_kvms_description": "Administrer skybaserte KVM-er og koble til dem sikkert.", "cloud_kvms_description": "Administrer skybaserte KVM-er og koble til dem sikkert.",
"cloud_kvms_no_devices": "Ingen enheter funnet",
"cloud_kvms_no_devices_description": "Du har ingen enheter med aktivert JetKVM Cloud ennå.", "cloud_kvms_no_devices_description": "Du har ingen enheter med aktivert JetKVM Cloud ennå.",
"cloud_kvms_no_devices": "Ingen enheter funnet",
"cloud_kvms": "Cloud KVM-er",
"confirm": "Bekrefte", "confirm": "Bekrefte",
"connect_to_kvm": "Koble til KVM", "connect_to_kvm": "Koble til KVM",
"connecting_to_device": "Kobler til enhet …", "connecting_to_device": "Kobler til enhet …",
"connection_established": "Forbindelse opprettet", "connection_established": "Forbindelse opprettet",
"connection_stats_badge_jitter": "Jitter",
"connection_stats_badge_jitter_buffer_avg_delay": "Jitterbuffer Gjns. forsinkelse", "connection_stats_badge_jitter_buffer_avg_delay": "Jitterbuffer Gjns. forsinkelse",
"connection_stats_connection": "Forbindelse", "connection_stats_badge_jitter": "Jitter",
"connection_stats_connection_description": "Forbindelsen mellom klienten og JetKVM-en.", "connection_stats_connection_description": "Forbindelsen mellom klienten og JetKVM-en.",
"connection_stats_frames_per_second": "Bilder per sekund", "connection_stats_connection": "Forbindelse",
"connection_stats_frames_per_second_description": "Antall innkommende videobilder som vises per sekund.", "connection_stats_frames_per_second_description": "Antall innkommende videobilder som vises per sekund.",
"connection_stats_network_stability": "Nettverksstabilitet", "connection_stats_frames_per_second": "Bilder per sekund",
"connection_stats_network_stability_description": "Hvor jevn flyten av innkommende videopakker er over nettverket.", "connection_stats_network_stability_description": "Hvor jevn flyten av innkommende videopakker er over nettverket.",
"connection_stats_packets_lost": "Pakker tapt", "connection_stats_network_stability": "Nettverksstabilitet",
"connection_stats_packets_lost_description": "Antall tapte innkommende RTP-videopakker.", "connection_stats_packets_lost_description": "Antall tapte innkommende RTP-videopakker.",
"connection_stats_playback_delay": "Avspillingsforsinkelse", "connection_stats_packets_lost": "Pakker tapt",
"connection_stats_playback_delay_description": "Forsinkelse lagt til av jitterbufferen for jevn avspilling når bilder ankommer ujevnt.", "connection_stats_playback_delay_description": "Forsinkelse lagt til av jitterbufferen for jevn avspilling når bilder ankommer ujevnt.",
"connection_stats_round_trip_time": "Tur-retur-tid", "connection_stats_playback_delay": "Avspillingsforsinkelse",
"connection_stats_round_trip_time_description": "Rundturstid for det aktive ICE-kandidatparet mellom jevnaldrende.", "connection_stats_round_trip_time_description": "Rundturstid for det aktive ICE-kandidatparet mellom jevnaldrende.",
"connection_stats_round_trip_time": "Tur-retur-tid",
"connection_stats_sidebar": "Tilkoblingsstatistikk", "connection_stats_sidebar": "Tilkoblingsstatistikk",
"connection_stats_video": "Video",
"connection_stats_video_description": "Videostrømmen fra JetKVM til klienten.", "connection_stats_video_description": "Videostrømmen fra JetKVM til klienten.",
"connection_stats_video": "Video",
"continue": "Fortsette", "continue": "Fortsette",
"creating_peer_connection": "Oppretter kontakt med andre personer …", "creating_peer_connection": "Oppretter kontakt med andre personer …",
"dc_power_control_current": "Nåværende",
"dc_power_control_current_unit": "EN", "dc_power_control_current_unit": "EN",
"dc_power_control_current": "Nåværende",
"dc_power_control_get_state_error": "Klarte ikke å hente likestrømsstatus: {error}", "dc_power_control_get_state_error": "Klarte ikke å hente likestrømsstatus: {error}",
"dc_power_control_power": "Makt",
"dc_power_control_power_off_button": "Slå av", "dc_power_control_power_off_button": "Slå av",
"dc_power_control_power_off_state": "Slå av", "dc_power_control_power_off_state": "Slå av",
"dc_power_control_power_on_button": "Slå på", "dc_power_control_power_on_button": "Slå på",
"dc_power_control_power_on_state": "Slå PÅ", "dc_power_control_power_on_state": "Slå PÅ",
"dc_power_control_power_unit": "V", "dc_power_control_power_unit": "V",
"dc_power_control_power": "Makt",
"dc_power_control_restore_last_state": "Siste stat", "dc_power_control_restore_last_state": "Siste stat",
"dc_power_control_restore_power_state": "Gjenopprett strømtap", "dc_power_control_restore_power_state": "Gjenopprett strømtap",
"dc_power_control_set_power_state_error": "Kunne ikke sende likestrømsstatus til {enabled} : {error}", "dc_power_control_set_power_state_error": "Kunne ikke sende likestrømsstatus til {enabled} : {error}",
"dc_power_control_set_restore_state_error": "Kunne ikke sende gjenopprettingsstatus for likestrøm til {state} : {error}", "dc_power_control_set_restore_state_error": "Kunne ikke sende gjenopprettingsstatus for likestrøm til {state} : {error}",
"dc_power_control_voltage": "Spenning",
"dc_power_control_voltage_unit": "V", "dc_power_control_voltage_unit": "V",
"dc_power_control_voltage": "Spenning",
"delete": "Slett", "delete": "Slett",
"deregister_button": "Avregistrer deg fra skyen", "deregister_button": "Avregistrer deg fra skyen",
"deregister_cloud_devices": "Skyenheter", "deregister_cloud_devices": "Skyenheter",
@ -226,12 +226,12 @@
"extension_popover_load_and_manage_extensions": "Last inn og administrer utvidelsene dine", "extension_popover_load_and_manage_extensions": "Last inn og administrer utvidelsene dine",
"extension_popover_set_error_notification": "Klarte ikke å angi aktiv utvidelse: {error}", "extension_popover_set_error_notification": "Klarte ikke å angi aktiv utvidelse: {error}",
"extension_popover_unload_extension": "Fjern utvidelse", "extension_popover_unload_extension": "Fjern utvidelse",
"extension_serial_console": "Seriell konsoll",
"extension_serial_console_description": "Få tilgang til seriekonsollutvidelsen din", "extension_serial_console_description": "Få tilgang til seriekonsollutvidelsen din",
"extensions_atx_power_control": "ATX-strømstyring", "extension_serial_console": "Seriell konsoll",
"extensions_atx_power_control_description": "Kontroller maskinens strømstatus via ATX-strømkontroll.", "extensions_atx_power_control_description": "Kontroller maskinens strømstatus via ATX-strømkontroll.",
"extensions_dc_power_control": "DC-strømkontroll", "extensions_atx_power_control": "ATX-strømstyring",
"extensions_dc_power_control_description": "Kontroller DC-strømutvidelsen din", "extensions_dc_power_control_description": "Kontroller DC-strømutvidelsen din",
"extensions_dc_power_control": "DC-strømkontroll",
"extensions_popover_extensions": "Utvidelser", "extensions_popover_extensions": "Utvidelser",
"gathering_ice_candidates": "Samler ICE-kandidater…", "gathering_ice_candidates": "Samler ICE-kandidater…",
"general_app_version": "App: {version}", "general_app_version": "App: {version}",
@ -241,8 +241,8 @@
"general_check_for_updates": "Se etter oppdateringer", "general_check_for_updates": "Se etter oppdateringer",
"general_page_description": "Konfigurer enhetsinnstillinger og oppdater preferanser", "general_page_description": "Konfigurer enhetsinnstillinger og oppdater preferanser",
"general_reboot_description": "Vil du fortsette med å starte systemet på nytt?", "general_reboot_description": "Vil du fortsette med å starte systemet på nytt?",
"general_reboot_device": "Start enheten på nytt",
"general_reboot_device_description": "Slå av og på JetKVM-en", "general_reboot_device_description": "Slå av og på JetKVM-en",
"general_reboot_device": "Start enheten på nytt",
"general_reboot_no_button": "Ingen", "general_reboot_no_button": "Ingen",
"general_reboot_title": "Start JetKVM på nytt", "general_reboot_title": "Start JetKVM på nytt",
"general_reboot_yes_button": "Ja", "general_reboot_yes_button": "Ja",
@ -295,9 +295,9 @@
"hardware_display_orientation_title": "Skjermretning", "hardware_display_orientation_title": "Skjermretning",
"hardware_display_wake_up_note": "Skjermen vil våkne når tilkoblingsstatusen endres, eller når den berøres.", "hardware_display_wake_up_note": "Skjermen vil våkne når tilkoblingsstatusen endres, eller når den berøres.",
"hardware_page_description": "Konfigurer skjerminnstillinger og maskinvarealternativer for JetKVM-enheten din", "hardware_page_description": "Konfigurer skjerminnstillinger og maskinvarealternativer for JetKVM-enheten din",
"hardware_time_10_minutes": "10 minutter",
"hardware_time_1_hour": "1 time", "hardware_time_1_hour": "1 time",
"hardware_time_1_minute": "1 minutt", "hardware_time_1_minute": "1 minutt",
"hardware_time_10_minutes": "10 minutter",
"hardware_time_30_minutes": "30 minutter", "hardware_time_30_minutes": "30 minutter",
"hardware_time_5_minutes": "5 minutter", "hardware_time_5_minutes": "5 minutter",
"hardware_time_never": "Aldri", "hardware_time_never": "Aldri",
@ -327,7 +327,6 @@
"invalid_password": "Ugyldig passord", "invalid_password": "Ugyldig passord",
"ip_address": "IP-adresse", "ip_address": "IP-adresse",
"ipv6_address_label": "Adresse", "ipv6_address_label": "Adresse",
"ipv6_gateway": "Inngangsport",
"ipv6_information": "IPv6-informasjon", "ipv6_information": "IPv6-informasjon",
"ipv6_link_local": "Link-local", "ipv6_link_local": "Link-local",
"ipv6_preferred_lifetime": "Foretrukket levetid", "ipv6_preferred_lifetime": "Foretrukket levetid",
@ -410,8 +409,8 @@
"log_in": "Logg inn", "log_in": "Logg inn",
"log_out": "Logg ut", "log_out": "Logg ut",
"logged_in_as": "Logget inn som", "logged_in_as": "Logget inn som",
"login_enter_password": "Skriv inn passordet ditt",
"login_enter_password_description": "Skriv inn passordet ditt for å få tilgang til JetKVM-en din.", "login_enter_password_description": "Skriv inn passordet ditt for å få tilgang til JetKVM-en din.",
"login_enter_password": "Skriv inn passordet ditt",
"login_error": "Det oppsto en feil under innlogging", "login_error": "Det oppsto en feil under innlogging",
"login_forgot_password": "Glemt passord?", "login_forgot_password": "Glemt passord?",
"login_password_label": "Passord", "login_password_label": "Passord",
@ -425,8 +424,8 @@
"macro_name_required": "Navn er obligatorisk", "macro_name_required": "Navn er obligatorisk",
"macro_name_too_long": "Navnet må være mindre enn 50 tegn", "macro_name_too_long": "Navnet må være mindre enn 50 tegn",
"macro_please_fix_validation_errors": "Vennligst rett opp valideringsfeilene", "macro_please_fix_validation_errors": "Vennligst rett opp valideringsfeilene",
"macro_save": "Lagre makro",
"macro_save_error": "Det oppsto en feil under lagring.", "macro_save_error": "Det oppsto en feil under lagring.",
"macro_save": "Lagre makro",
"macro_step_count": "{steps} / {max} trinn", "macro_step_count": "{steps} / {max} trinn",
"macro_step_duration_description": "Tid for å vente før man tar neste steg.", "macro_step_duration_description": "Tid for å vente før man tar neste steg.",
"macro_step_duration_label": "Stegvarighet", "macro_step_duration_label": "Stegvarighet",
@ -440,8 +439,8 @@
"macro_steps_description": "Taster/modifikatorer utføres i rekkefølge med en forsinkelse mellom hvert trinn.", "macro_steps_description": "Taster/modifikatorer utføres i rekkefølge med en forsinkelse mellom hvert trinn.",
"macro_steps_label": "Trinn", "macro_steps_label": "Trinn",
"macros_add_description": "Opprett en ny tastaturmakro", "macros_add_description": "Opprett en ny tastaturmakro",
"macros_add_new": "Legg til ny makro",
"macros_add_new_macro": "Legg til ny makro", "macros_add_new_macro": "Legg til ny makro",
"macros_add_new": "Legg til ny makro",
"macros_aria_add_new": "Legg til ny makro", "macros_aria_add_new": "Legg til ny makro",
"macros_aria_delete": "Slett makro {name}", "macros_aria_delete": "Slett makro {name}",
"macros_aria_duplicate": "Duplikatmakro {name}", "macros_aria_duplicate": "Duplikatmakro {name}",
@ -463,16 +462,16 @@
"macros_edit_button": "Redigere", "macros_edit_button": "Redigere",
"macros_edit_description": "Endre tastaturmakroen din", "macros_edit_description": "Endre tastaturmakroen din",
"macros_edit_title": "Rediger makro", "macros_edit_title": "Rediger makro",
"macros_failed_create": "Kunne ikke opprette makroen",
"macros_failed_create_error": "Klarte ikke å opprette makro: {error}", "macros_failed_create_error": "Klarte ikke å opprette makro: {error}",
"macros_failed_delete": "Kunne ikke slette makroen", "macros_failed_create": "Kunne ikke opprette makroen",
"macros_failed_delete_error": "Klarte ikke å slette makroen: {error}", "macros_failed_delete_error": "Klarte ikke å slette makroen: {error}",
"macros_failed_duplicate": "Kunne ikke duplisere makroen", "macros_failed_delete": "Kunne ikke slette makroen",
"macros_failed_duplicate_error": "Klarte ikke å duplisere makroen: {error}", "macros_failed_duplicate_error": "Klarte ikke å duplisere makroen: {error}",
"macros_failed_reorder": "Kunne ikke endre rekkefølgen på makroene", "macros_failed_duplicate": "Kunne ikke duplisere makroen",
"macros_failed_reorder_error": "Kunne ikke endre rekkefølgen på makroer: {error}", "macros_failed_reorder_error": "Kunne ikke endre rekkefølgen på makroer: {error}",
"macros_failed_update": "Kunne ikke oppdatere makroen", "macros_failed_reorder": "Kunne ikke endre rekkefølgen på makroene",
"macros_failed_update_error": "Kunne ikke oppdatere makroen: {error}", "macros_failed_update_error": "Kunne ikke oppdatere makroen: {error}",
"macros_failed_update": "Kunne ikke oppdatere makroen",
"macros_invalid_data": "Ugyldige makrodata", "macros_invalid_data": "Ugyldige makrodata",
"macros_loading": "Laster inn makroer …", "macros_loading": "Laster inn makroer …",
"macros_max_reached": "Maks nådd", "macros_max_reached": "Maks nådd",
@ -507,8 +506,8 @@
"mount_error_list_storage": "Feil ved oppføring av lagringsfiler: {error}", "mount_error_list_storage": "Feil ved oppføring av lagringsfiler: {error}",
"mount_error_title": "Monteringsfeil", "mount_error_title": "Monteringsfeil",
"mount_get_state_error": "Klarte ikke å hente status for virtuelle medier: {error}", "mount_get_state_error": "Klarte ikke å hente status for virtuelle medier: {error}",
"mount_jetkvm_storage": "JetKVM-lagringsmontering",
"mount_jetkvm_storage_description": "Monter tidligere opplastede filer fra JetKVM-lagringen", "mount_jetkvm_storage_description": "Monter tidligere opplastede filer fra JetKVM-lagringen",
"mount_jetkvm_storage": "JetKVM-lagringsmontering",
"mount_mode_cdrom": "CD/DVD", "mount_mode_cdrom": "CD/DVD",
"mount_mode_disk": "Disk", "mount_mode_disk": "Disk",
"mount_mounted_as": "Montert som", "mount_mounted_as": "Montert som",
@ -521,8 +520,8 @@
"mount_popular_images": "Populære bilder", "mount_popular_images": "Populære bilder",
"mount_streaming_from_url": "Strømming fra URL", "mount_streaming_from_url": "Strømming fra URL",
"mount_supported_formats": "Støttede formater: ISO, IMG", "mount_supported_formats": "Støttede formater: ISO, IMG",
"mount_unmount": "Avmonter",
"mount_unmount_error": "Klarte ikke å demontere bildet: {error}", "mount_unmount_error": "Klarte ikke å demontere bildet: {error}",
"mount_unmount": "Avmonter",
"mount_upload_description": "Velg en bildefil som skal lastes opp til JetKVM-lagring", "mount_upload_description": "Velg en bildefil som skal lastes opp til JetKVM-lagring",
"mount_upload_error": "Opplastingsfeil: {error}", "mount_upload_error": "Opplastingsfeil: {error}",
"mount_upload_failed_datachannel": "Kunne ikke opprette datakanal for filopplasting", "mount_upload_failed_datachannel": "Kunne ikke opprette datakanal for filopplasting",
@ -530,8 +529,8 @@
"mount_upload_successful": "Opplastingen var vellykket", "mount_upload_successful": "Opplastingen var vellykket",
"mount_upload_title": "Last opp nytt bilde", "mount_upload_title": "Last opp nytt bilde",
"mount_uploaded_has_been_uploaded": "{name} har blitt lastet opp", "mount_uploaded_has_been_uploaded": "{name} har blitt lastet opp",
"mount_uploading": "Laster opp…",
"mount_uploading_with_name": "Laster opp {name}", "mount_uploading_with_name": "Laster opp {name}",
"mount_uploading": "Laster opp…",
"mount_url_description": "Monter filer fra en hvilken som helst offentlig nettadresse", "mount_url_description": "Monter filer fra en hvilken som helst offentlig nettadresse",
"mount_url_input_label": "Bilde-URL", "mount_url_input_label": "Bilde-URL",
"mount_url_mount": "URL-montering", "mount_url_mount": "URL-montering",
@ -539,10 +538,10 @@
"mount_view_device_title": "Monter fra JetKVM-lagring", "mount_view_device_title": "Monter fra JetKVM-lagring",
"mount_view_url_description": "Skriv inn en URL til bildefilen som skal monteres", "mount_view_url_description": "Skriv inn en URL til bildefilen som skal monteres",
"mount_view_url_title": "Monter fra URL", "mount_view_url_title": "Monter fra URL",
"mount_virtual_media": "Virtuelle medier",
"mount_virtual_media_description": "Monter et image for å starte opp fra eller installere et operativsystem.", "mount_virtual_media_description": "Monter et image for å starte opp fra eller installere et operativsystem.",
"mount_virtual_media_source": "Virtuell mediekilde",
"mount_virtual_media_source_description": "Velg hvordan du vil montere virtuelle medier", "mount_virtual_media_source_description": "Velg hvordan du vil montere virtuelle medier",
"mount_virtual_media_source": "Virtuell mediekilde",
"mount_virtual_media": "Virtuelle medier",
"mouse_alt_finger": "Fingerberøring av en skjerm", "mouse_alt_finger": "Fingerberøring av en skjerm",
"mouse_alt_mouse": "Musikon", "mouse_alt_mouse": "Musikon",
"mouse_description": "Konfigurer markørens oppførsel og interaksjonsinnstillinger for enheten din", "mouse_description": "Konfigurer markørens oppførsel og interaksjonsinnstillinger for enheten din",
@ -559,10 +558,10 @@
"mouse_jiggler_light": "Lys - 5m", "mouse_jiggler_light": "Lys - 5m",
"mouse_jiggler_standard": "Standard - 1 m", "mouse_jiggler_standard": "Standard - 1 m",
"mouse_jiggler_title": "Jiggler", "mouse_jiggler_title": "Jiggler",
"mouse_mode_absolute": "Absolutt",
"mouse_mode_absolute_description": "Mest praktisk", "mouse_mode_absolute_description": "Mest praktisk",
"mouse_mode_relative": "Slektning", "mouse_mode_absolute": "Absolutt",
"mouse_mode_relative_description": "Mest kompatible", "mouse_mode_relative_description": "Mest kompatible",
"mouse_mode_relative": "Slektning",
"mouse_modes_description": "Velg museinndatamodus", "mouse_modes_description": "Velg museinndatamodus",
"mouse_modes_title": "Moduser", "mouse_modes_title": "Moduser",
"mouse_scroll_high": "Høy", "mouse_scroll_high": "Høy",
@ -575,17 +574,12 @@
"mouse_title": "Mus", "mouse_title": "Mus",
"network_custom_domain": "Tilpasset domene", "network_custom_domain": "Tilpasset domene",
"network_description": "Konfigurer nettverksinnstillingene dine", "network_description": "Konfigurer nettverksinnstillingene dine",
"network_dhcp_client_description": "Konfigurer hvilken DHCP-klient som skal brukes",
"network_dhcp_client_jetkvm": "JetKVM intern",
"network_dhcp_client_title": "DHCP-klient",
"network_dhcp_information": "DHCP-informasjon", "network_dhcp_information": "DHCP-informasjon",
"network_dhcp_lease_renew": "Forny DHCP-leieavtale",
"network_dhcp_lease_renew_confirm": "Forny leieavtalen",
"network_dhcp_lease_renew_confirm_description": "Dette vil be om en ny IP-adresse fra DHCP-serveren din. Enheten din kan midlertidig miste nettverkstilkoblingen under denne prosessen.", "network_dhcp_lease_renew_confirm_description": "Dette vil be om en ny IP-adresse fra DHCP-serveren din. Enheten din kan midlertidig miste nettverkstilkoblingen under denne prosessen.",
"network_dhcp_lease_renew_confirm_new_a": "Hvis du får en ny IP-adresse", "network_dhcp_lease_renew_confirm": "Forny leieavtalen",
"network_dhcp_lease_renew_confirm_new_b": "du må kanskje koble til på nytt med den nye adressen",
"network_dhcp_lease_renew_failed": "Kunne ikke fornye leieavtalen: {error}", "network_dhcp_lease_renew_failed": "Kunne ikke fornye leieavtalen: {error}",
"network_dhcp_lease_renew_success": "DHCP-leieavtale fornyet", "network_dhcp_lease_renew_success": "DHCP-leieavtale fornyet",
"network_dhcp_lease_renew": "Forny DHCP-leieavtale",
"network_domain_custom": "Skikk", "network_domain_custom": "Skikk",
"network_domain_description": "Nettverksdomenesuffiks for enheten", "network_domain_description": "Nettverksdomenesuffiks for enheten",
"network_domain_dhcp_provided": "DHCP levert", "network_domain_dhcp_provided": "DHCP levert",
@ -594,35 +588,21 @@
"network_hostname_description": "Enhetsidentifikator på nettverket. Blank for systemstandard", "network_hostname_description": "Enhetsidentifikator på nettverket. Blank for systemstandard",
"network_hostname_title": "Vertsnavn", "network_hostname_title": "Vertsnavn",
"network_http_proxy_description": "Proxy-server for utgående HTTP(S)-forespørsler fra enheten. Tomt hvis ingen.", "network_http_proxy_description": "Proxy-server for utgående HTTP(S)-forespørsler fra enheten. Tomt hvis ingen.",
"network_http_proxy_invalid": "Ugyldig HTTP-proxy-URL",
"network_http_proxy_title": "HTTP-proxy", "network_http_proxy_title": "HTTP-proxy",
"network_ipv4_address": "IPv4-adresse",
"network_ipv4_dns": "IPv4 DNS",
"network_ipv4_gateway": "IPv4-gateway",
"network_ipv4_mode_description": "Konfigurer IPv4-modusen", "network_ipv4_mode_description": "Konfigurer IPv4-modusen",
"network_ipv4_mode_dhcp": "DHCP", "network_ipv4_mode_dhcp": "DHCP",
"network_ipv4_mode_static": "Statisk",
"network_ipv4_mode_title": "IPv4-modus", "network_ipv4_mode_title": "IPv4-modus",
"network_ipv4_netmask": "IPv4-nettmaske",
"network_ipv6_address": "IPv6-adresse",
"network_ipv6_information": "IPv6-informasjon", "network_ipv6_information": "IPv6-informasjon",
"network_ipv6_mode_description": "Konfigurer IPv6-modusen", "network_ipv6_mode_description": "Konfigurer IPv6-modusen",
"network_ipv6_mode_dhcpv6": "DHCPv6",
"network_ipv6_mode_disabled": "Funksjonshemmet", "network_ipv6_mode_disabled": "Funksjonshemmet",
"network_ipv6_mode_link_local": "Kun lenkelokal",
"network_ipv6_mode_slaac": "SLAAC", "network_ipv6_mode_slaac": "SLAAC",
"network_ipv6_mode_slaac_dhcpv6": "SLAAC + DHCPv6",
"network_ipv6_mode_static": "Statisk",
"network_ipv6_mode_title": "IPv6-modus", "network_ipv6_mode_title": "IPv6-modus",
"network_ipv6_netmask": "IPv6-nettmaske",
"network_ipv6_no_addresses": "Ingen IPv6-adresser konfigurert", "network_ipv6_no_addresses": "Ingen IPv6-adresser konfigurert",
"network_ll_dp_all": "Alle", "network_ll_dp_all": "Alle",
"network_ll_dp_basic": "Grunnleggende", "network_ll_dp_basic": "Grunnleggende",
"network_ll_dp_description": "Kontroller hvilke TLV-er som skal sendes over Link Layer Discovery Protocol", "network_ll_dp_description": "Kontroller hvilke TLV-er som skal sendes over Link Layer Discovery Protocol",
"network_ll_dp_disabled": "Funksjonshemmet", "network_ll_dp_disabled": "Funksjonshemmet",
"network_ll_dp_title": "LLDP", "network_ll_dp_title": "LLDP",
"network_mac_address_copy_error": "Kunne ikke kopiere MAC-adressen",
"network_mac_address_copy_success": "MAC-adresse { mac } kopiert til utklippstavlen",
"network_mac_address_description": "Maskinvareidentifikator for nettverksgrensesnittet", "network_mac_address_description": "Maskinvareidentifikator for nettverksgrensesnittet",
"network_mac_address_title": "MAC-adresse", "network_mac_address_title": "MAC-adresse",
"network_mdns_auto": "Bil", "network_mdns_auto": "Bil",
@ -632,19 +612,9 @@
"network_mdns_ipv6_only": "Kun IPv6", "network_mdns_ipv6_only": "Kun IPv6",
"network_mdns_title": "mDNS", "network_mdns_title": "mDNS",
"network_no_dhcp_lease": "Ingen DHCP-leaseinformasjon tilgjengelig", "network_no_dhcp_lease": "Ingen DHCP-leaseinformasjon tilgjengelig",
"network_no_information_description": "Ingen nettverkskonfigurasjon tilgjengelig",
"network_no_information_headline": "Nettverksinformasjon",
"network_pending_dhcp_mode_change_description": "Lagre innstillinger for å aktivere DHCP-modus og vise leieavtaleinformasjon",
"network_pending_dhcp_mode_change_headline": "Venter på endring av DHCP IPv4-modus",
"network_save_settings": "Lagre innstillinger",
"network_save_settings_apply_title": "Bruk nettverksinnstillinger",
"network_save_settings_confirm": "Bruk endringer",
"network_save_settings_confirm_description": "Følgende nettverksinnstillinger vil bli brukt. Disse endringene kan kreve en omstart og forårsake en kortvarig frakobling.",
"network_save_settings_confirm_heading": "Konfigurasjonsendringer",
"network_save_settings_failed": "Kunne ikke lagre nettverksinnstillinger: {error}", "network_save_settings_failed": "Kunne ikke lagre nettverksinnstillinger: {error}",
"network_save_settings_success": "Nettverksinnstillinger lagret", "network_save_settings_success": "Nettverksinnstillinger lagret",
"network_settings_invalid_ipv4_cidr": "Ugyldig CIDR-notasjon for IPv4-adresse", "network_save_settings": "Lagre innstillinger",
"network_settings_load_error": "Kunne ikke laste inn nettverksinnstillinger: {error}",
"network_time_sync_description": "Konfigurer innstillinger for tidssynkronisering", "network_time_sync_description": "Konfigurer innstillinger for tidssynkronisering",
"network_time_sync_http_only": "Kun HTTP", "network_time_sync_http_only": "Kun HTTP",
"network_time_sync_ntp_and_http": "NTP og HTTP", "network_time_sync_ntp_and_http": "NTP og HTTP",
@ -670,8 +640,8 @@
"paste_modal_failed_paste": "Klarte ikke å lime inn tekst: {error}", "paste_modal_failed_paste": "Klarte ikke å lime inn tekst: {error}",
"paste_modal_invalid_chars_intro": "Følgende tegn vil ikke bli limt inn:", "paste_modal_invalid_chars_intro": "Følgende tegn vil ikke bli limt inn:",
"paste_modal_paste_from_host": "Lim inn fra verten", "paste_modal_paste_from_host": "Lim inn fra verten",
"paste_modal_paste_text": "Lim inn tekst",
"paste_modal_paste_text_description": "Lim inn tekst fra klienten din til den eksterne verten", "paste_modal_paste_text_description": "Lim inn tekst fra klienten din til den eksterne verten",
"paste_modal_paste_text": "Lim inn tekst",
"paste_modal_sending_using_layout": "Sende tekst ved hjelp av tastaturoppsett: {iso} - {name}", "paste_modal_sending_using_layout": "Sende tekst ved hjelp av tastaturoppsett: {iso} - {name}",
"peer_connection_closed": "Lukket", "peer_connection_closed": "Lukket",
"peer_connection_closing": "Lukking", "peer_connection_closing": "Lukking",
@ -698,20 +668,20 @@
"retry": "Prøv på nytt", "retry": "Prøv på nytt",
"saving": "Lagrer…", "saving": "Lagrer…",
"search_placeholder": "Søk…", "search_placeholder": "Søk…",
"serial_console": "Seriell konsoll",
"serial_console_baud_rate": "Baudhastighet", "serial_console_baud_rate": "Baudhastighet",
"serial_console_configure_description": "Konfigurer innstillingene for seriekonsollen", "serial_console_configure_description": "Konfigurer innstillingene for seriekonsollen",
"serial_console_data_bits": "Databiter", "serial_console_data_bits": "Databiter",
"serial_console_get_settings_error": "Klarte ikke å hente innstillinger for seriell konsoll: {error}", "serial_console_get_settings_error": "Klarte ikke å hente innstillinger for seriell konsoll: {error}",
"serial_console_open_console": "Åpne konsollen", "serial_console_open_console": "Åpne konsollen",
"serial_console_parity": "Paritet",
"serial_console_parity_even": "Paritet", "serial_console_parity_even": "Paritet",
"serial_console_parity_mark": "Mark Paritet", "serial_console_parity_mark": "Mark Paritet",
"serial_console_parity_none": "Ingen paritet", "serial_console_parity_none": "Ingen paritet",
"serial_console_parity_odd": "Oddeparitet", "serial_console_parity_odd": "Oddeparitet",
"serial_console_parity_space": "Romparitet", "serial_console_parity_space": "Romparitet",
"serial_console_parity": "Paritet",
"serial_console_set_settings_error": "Klarte ikke å sette innstillingene for seriell konsoll til {settings} : {error}", "serial_console_set_settings_error": "Klarte ikke å sette innstillingene for seriell konsoll til {settings} : {error}",
"serial_console_stop_bits": "Stoppbiter", "serial_console_stop_bits": "Stoppbiter",
"serial_console": "Seriell konsoll",
"setting_remote_description": "Innstilling av fjernkontrollbeskrivelse", "setting_remote_description": "Innstilling av fjernkontrollbeskrivelse",
"setting_remote_session_description": "Angi beskrivelse av ekstern økt...", "setting_remote_session_description": "Angi beskrivelse av ekstern økt...",
"setting_up_connection_to_device": "Setter opp tilkobling til enhet...", "setting_up_connection_to_device": "Setter opp tilkobling til enhet...",
@ -721,8 +691,8 @@
"settings_back_to_kvm": "Tilbake til KVM", "settings_back_to_kvm": "Tilbake til KVM",
"settings_general": "General", "settings_general": "General",
"settings_hardware": "Maskinvare", "settings_hardware": "Maskinvare",
"settings_keyboard": "Tastatur",
"settings_keyboard_macros": "Tastaturmakroer", "settings_keyboard_macros": "Tastaturmakroer",
"settings_keyboard": "Tastatur",
"settings_mouse": "Mus", "settings_mouse": "Mus",
"settings_network": "Nettverk", "settings_network": "Nettverk",
"settings_video": "Video", "settings_video": "Video",
@ -742,7 +712,6 @@
"updates_failed_check": "Klarte ikke å se etter oppdateringer: {error}", "updates_failed_check": "Klarte ikke å se etter oppdateringer: {error}",
"updates_failed_get_device_version": "Klarte ikke å hente enhetsversjon: {error}", "updates_failed_get_device_version": "Klarte ikke å hente enhetsversjon: {error}",
"updating_leave_device_on": "Vennligst ikke slå av enheten din ...", "updating_leave_device_on": "Vennligst ikke slå av enheten din ...",
"usb": "USB",
"usb_config_custom": "Skikk", "usb_config_custom": "Skikk",
"usb_config_default": "JetKVM-standard", "usb_config_default": "JetKVM-standard",
"usb_config_dell": "Dell Multimedia Pro-tastatur", "usb_config_dell": "Dell Multimedia Pro-tastatur",
@ -789,6 +758,7 @@
"usb_state_connecting": "Tilkobling", "usb_state_connecting": "Tilkobling",
"usb_state_disconnected": "Frakoblet", "usb_state_disconnected": "Frakoblet",
"usb_state_low_power_mode": "Lavstrømsmodus", "usb_state_low_power_mode": "Lavstrømsmodus",
"usb": "USB",
"user_interface_language_description": "Velg språket som skal brukes i JetKVM-brukergrensesnittet", "user_interface_language_description": "Velg språket som skal brukes i JetKVM-brukergrensesnittet",
"user_interface_language_title": "Grensesnittspråk", "user_interface_language_title": "Grensesnittspråk",
"video_brightness_description": "Lysstyrkenivå ( {value} x)", "video_brightness_description": "Lysstyrkenivå ( {value} x)",
@ -855,7 +825,6 @@
"video_title": "Video", "video_title": "Video",
"view_details": "Vis detaljer", "view_details": "Vis detaljer",
"virtual_keyboard_header": "Virtuelt tastatur", "virtual_keyboard_header": "Virtuelt tastatur",
"wake_on_lan": "Vekk på LAN",
"wake_on_lan_add_device_device_name": "Enhetsnavn", "wake_on_lan_add_device_device_name": "Enhetsnavn",
"wake_on_lan_add_device_example_device_name": "Plex Media Server", "wake_on_lan_add_device_example_device_name": "Plex Media Server",
"wake_on_lan_add_device_mac_address": "MAC-adresse", "wake_on_lan_add_device_mac_address": "MAC-adresse",
@ -871,6 +840,7 @@
"wake_on_lan_failed_send_magic": "Kunne ikke sende magisk pakke", "wake_on_lan_failed_send_magic": "Kunne ikke sende magisk pakke",
"wake_on_lan_invalid_mac": "Ugyldig MAC-adresse", "wake_on_lan_invalid_mac": "Ugyldig MAC-adresse",
"wake_on_lan_magic_sent_success": "Magisk pakke sendt", "wake_on_lan_magic_sent_success": "Magisk pakke sendt",
"welcome_to_jetkvm": "Velkommen til JetKVM", "wake_on_lan": "Vekk på LAN",
"welcome_to_jetkvm_description": "Kontroller hvilken som helst datamaskin eksternt" "welcome_to_jetkvm_description": "Kontroller hvilken som helst datamaskin eksternt",
"welcome_to_jetkvm": "Velkommen til JetKVM"
} }

View File

@ -107,10 +107,10 @@
"already_adopted_title": "Enheten är redan registrerad", "already_adopted_title": "Enheten är redan registrerad",
"appearance_description": "Välj ditt önskade färgtema", "appearance_description": "Välj ditt önskade färgtema",
"appearance_page_description": "Anpassa utseendet och känslan hos ditt JetKVM-gränssnitt", "appearance_page_description": "Anpassa utseendet och känslan hos ditt JetKVM-gränssnitt",
"appearance_theme": "Tema",
"appearance_theme_dark": "Mörk", "appearance_theme_dark": "Mörk",
"appearance_theme_light": "Ljus", "appearance_theme_light": "Ljus",
"appearance_theme_system": "System", "appearance_theme_system": "System",
"appearance_theme": "Tema",
"appearance_title": "Utseende", "appearance_title": "Utseende",
"attach": "Bifoga", "attach": "Bifoga",
"atx_power_control_get_state_error": "Misslyckades med att hämta ATX-strömstatus: {error}", "atx_power_control_get_state_error": "Misslyckades med att hämta ATX-strömstatus: {error}",
@ -121,85 +121,85 @@
"atx_power_control_reset_button": "Återställa", "atx_power_control_reset_button": "Återställa",
"atx_power_control_send_action_error": "Misslyckades med att skicka ATX-strömåtgärd {action} : {error}", "atx_power_control_send_action_error": "Misslyckades med att skicka ATX-strömåtgärd {action} : {error}",
"atx_power_control_short_power_button": "Kort tryck", "atx_power_control_short_power_button": "Kort tryck",
"auth_authentication_mode": "Välj ett autentiseringsläge",
"auth_authentication_mode_error": "Ett fel uppstod när autentiseringsläget ställdes in", "auth_authentication_mode_error": "Ett fel uppstod när autentiseringsläget ställdes in",
"auth_authentication_mode_invalid": "Ogiltigt autentiseringsläge", "auth_authentication_mode_invalid": "Ogiltigt autentiseringsläge",
"auth_connect_to_cloud": "Anslut din JetKVM till molnet", "auth_authentication_mode": "Välj ett autentiseringsläge",
"auth_connect_to_cloud_action": "Logga in och anslut enheten", "auth_connect_to_cloud_action": "Logga in och anslut enheten",
"auth_connect_to_cloud_description": "Lås upp fjärråtkomst och avancerade funktioner för din enhet", "auth_connect_to_cloud_description": "Lås upp fjärråtkomst och avancerade funktioner för din enhet",
"auth_connect_to_cloud": "Anslut din JetKVM till molnet",
"auth_header_cta_already_have_account": "Har du redan ett konto?", "auth_header_cta_already_have_account": "Har du redan ett konto?",
"auth_header_cta_dont_have_account": "Har du inget konto?", "auth_header_cta_dont_have_account": "Har du inget konto?",
"auth_header_cta_new_to_jetkvm": "Nybörjare på JetKVM?", "auth_header_cta_new_to_jetkvm": "Nybörjare på JetKVM?",
"auth_login": "Logga in på ditt JetKVM-konto",
"auth_login_action": "Logga in", "auth_login_action": "Logga in",
"auth_login_description": "Logga in för att få åtkomst till och hantera dina enheter säkert", "auth_login_description": "Logga in för att få åtkomst till och hantera dina enheter säkert",
"auth_mode_local": "Lokal autentiseringsmetod", "auth_login": "Logga in på ditt JetKVM-konto",
"auth_mode_local_change_later": "Du kan alltid ändra din autentiseringsmetod senare i inställningarna.", "auth_mode_local_change_later": "Du kan alltid ändra din autentiseringsmetod senare i inställningarna.",
"auth_mode_local_description": "Välj hur du vill säkra din JetKVM-enhet lokalt.", "auth_mode_local_description": "Välj hur du vill säkra din JetKVM-enhet lokalt.",
"auth_mode_local_no_password": "Inget lösenord",
"auth_mode_local_no_password_description": "Snabb åtkomst utan lösenordsautentisering.", "auth_mode_local_no_password_description": "Snabb åtkomst utan lösenordsautentisering.",
"auth_mode_local_password": "Lösenord", "auth_mode_local_no_password": "Inget lösenord",
"auth_mode_local_password_confirm_description": "Bekräfta ditt lösenord", "auth_mode_local_password_confirm_description": "Bekräfta ditt lösenord",
"auth_mode_local_password_confirm_label": "Bekräfta lösenord", "auth_mode_local_password_confirm_label": "Bekräfta lösenord",
"auth_mode_local_password_description": "Säkra din enhet med ett lösenord för extra skydd.", "auth_mode_local_password_description": "Säkra din enhet med ett lösenord för extra skydd.",
"auth_mode_local_password_do_not_match": "Lösenorden matchar inte", "auth_mode_local_password_do_not_match": "Lösenorden matchar inte",
"auth_mode_local_password_failed_set": "Misslyckades med att ange lösenord: {error}", "auth_mode_local_password_failed_set": "Misslyckades med att ange lösenord: {error}",
"auth_mode_local_password_note": "Detta lösenord kommer att användas för att säkra dina enhetsdata och skydda mot obehörig åtkomst.",
"auth_mode_local_password_note_local": "All data finns kvar på din lokala enhet.", "auth_mode_local_password_note_local": "All data finns kvar på din lokala enhet.",
"auth_mode_local_password_set": "Ange ett lösenord", "auth_mode_local_password_note": "Detta lösenord kommer att användas för att säkra dina enhetsdata och skydda mot obehörig åtkomst.",
"auth_mode_local_password_set_button": "Ange lösenord", "auth_mode_local_password_set_button": "Ange lösenord",
"auth_mode_local_password_set_description": "Skapa ett starkt lösenord för att säkra din JetKVM-enhet lokalt.", "auth_mode_local_password_set_description": "Skapa ett starkt lösenord för att säkra din JetKVM-enhet lokalt.",
"auth_mode_local_password_set_label": "Ange ett lösenord", "auth_mode_local_password_set_label": "Ange ett lösenord",
"auth_mode_local_password_set": "Ange ett lösenord",
"auth_mode_local_password": "Lösenord",
"auth_mode_local": "Lokal autentiseringsmetod",
"auth_signup_connect_to_cloud_action": "Registrera och anslut enhet", "auth_signup_connect_to_cloud_action": "Registrera och anslut enhet",
"auth_signup_create_account": "Skapa ditt JetKVM-konto",
"auth_signup_create_account_action": "Skapa konto", "auth_signup_create_account_action": "Skapa konto",
"auth_signup_create_account_description": "Skapa ditt konto och börja enkelt hantera dina enheter.", "auth_signup_create_account_description": "Skapa ditt konto och börja enkelt hantera dina enheter.",
"back": "Tillbaka", "auth_signup_create_account": "Skapa ditt JetKVM-konto",
"back_to_devices": "Tillbaka till Enheter", "back_to_devices": "Tillbaka till Enheter",
"back": "Tillbaka",
"cancel": "Avboka", "cancel": "Avboka",
"close": "Nära", "close": "Nära",
"cloud_kvms": "Moln-KVM:er",
"cloud_kvms_description": "Hantera dina moln-KVM:er och anslut till dem säkert.", "cloud_kvms_description": "Hantera dina moln-KVM:er och anslut till dem säkert.",
"cloud_kvms_no_devices": "Inga enheter hittades",
"cloud_kvms_no_devices_description": "Du har inga enheter med aktiverat JetKVM Cloud ännu.", "cloud_kvms_no_devices_description": "Du har inga enheter med aktiverat JetKVM Cloud ännu.",
"cloud_kvms_no_devices": "Inga enheter hittades",
"cloud_kvms": "Moln-KVM:er",
"confirm": "Bekräfta", "confirm": "Bekräfta",
"connect_to_kvm": "Anslut till KVM", "connect_to_kvm": "Anslut till KVM",
"connecting_to_device": "Ansluter till enhet…", "connecting_to_device": "Ansluter till enhet…",
"connection_established": "Anslutning upprättad", "connection_established": "Anslutning upprättad",
"connection_stats_badge_jitter": "Jitter",
"connection_stats_badge_jitter_buffer_avg_delay": "Genomsnittlig fördröjning för jitterbuffert", "connection_stats_badge_jitter_buffer_avg_delay": "Genomsnittlig fördröjning för jitterbuffert",
"connection_stats_connection": "Förbindelse", "connection_stats_badge_jitter": "Jitter",
"connection_stats_connection_description": "Anslutningen mellan klienten och JetKVM:n.", "connection_stats_connection_description": "Anslutningen mellan klienten och JetKVM:n.",
"connection_stats_frames_per_second": "Bildrutor per sekund", "connection_stats_connection": "Förbindelse",
"connection_stats_frames_per_second_description": "Antal inkommande videobildrutor som visas per sekund.", "connection_stats_frames_per_second_description": "Antal inkommande videobildrutor som visas per sekund.",
"connection_stats_network_stability": "Nätverksstabilitet", "connection_stats_frames_per_second": "Bildrutor per sekund",
"connection_stats_network_stability_description": "Hur jämnt flödet av inkommande videopaket är över nätverket.", "connection_stats_network_stability_description": "Hur jämnt flödet av inkommande videopaket är över nätverket.",
"connection_stats_packets_lost": "Paket förlorade", "connection_stats_network_stability": "Nätverksstabilitet",
"connection_stats_packets_lost_description": "Antal förlorade inkommande RTP-videopaket.", "connection_stats_packets_lost_description": "Antal förlorade inkommande RTP-videopaket.",
"connection_stats_playback_delay": "Uppspelningsfördröjning", "connection_stats_packets_lost": "Paket förlorade",
"connection_stats_playback_delay_description": "Fördröjning som läggs till av jitterbufferten för att jämna ut uppspelningen när bildrutor anländer ojämnt.", "connection_stats_playback_delay_description": "Fördröjning som läggs till av jitterbufferten för att jämna ut uppspelningen när bildrutor anländer ojämnt.",
"connection_stats_round_trip_time": "Tur- och returtid", "connection_stats_playback_delay": "Uppspelningsfördröjning",
"connection_stats_round_trip_time_description": "Tur- och returtid för det aktiva ICE-kandidatparet mellan peers.", "connection_stats_round_trip_time_description": "Tur- och returtid för det aktiva ICE-kandidatparet mellan peers.",
"connection_stats_round_trip_time": "Tur- och returtid",
"connection_stats_sidebar": "Anslutningsstatistik", "connection_stats_sidebar": "Anslutningsstatistik",
"connection_stats_video": "Video",
"connection_stats_video_description": "Videoströmmen från JetKVM till klienten.", "connection_stats_video_description": "Videoströmmen från JetKVM till klienten.",
"connection_stats_video": "Video",
"continue": "Fortsätta", "continue": "Fortsätta",
"creating_peer_connection": "Skapar peer-kontakt…", "creating_peer_connection": "Skapar peer-kontakt…",
"dc_power_control_current": "Nuvarande",
"dc_power_control_current_unit": "En", "dc_power_control_current_unit": "En",
"dc_power_control_current": "Nuvarande",
"dc_power_control_get_state_error": "Misslyckades med att hämta likströmsstatus: {error}", "dc_power_control_get_state_error": "Misslyckades med att hämta likströmsstatus: {error}",
"dc_power_control_power": "Driva",
"dc_power_control_power_off_button": "Stäng av", "dc_power_control_power_off_button": "Stäng av",
"dc_power_control_power_off_state": "Stäng av", "dc_power_control_power_off_state": "Stäng av",
"dc_power_control_power_on_button": "Slå på", "dc_power_control_power_on_button": "Slå på",
"dc_power_control_power_on_state": "Slå på", "dc_power_control_power_on_state": "Slå på",
"dc_power_control_power_unit": "V", "dc_power_control_power_unit": "V",
"dc_power_control_power": "Driva",
"dc_power_control_restore_last_state": "Senaste delstaten", "dc_power_control_restore_last_state": "Senaste delstaten",
"dc_power_control_restore_power_state": "Återställ strömförlust", "dc_power_control_restore_power_state": "Återställ strömförlust",
"dc_power_control_set_power_state_error": "Misslyckades med att skicka likströmsstatus till {enabled} : {error}", "dc_power_control_set_power_state_error": "Misslyckades med att skicka likströmsstatus till {enabled} : {error}",
"dc_power_control_set_restore_state_error": "Misslyckades med att skicka återställningsstatus för likström till {state} : {error}", "dc_power_control_set_restore_state_error": "Misslyckades med att skicka återställningsstatus för likström till {state} : {error}",
"dc_power_control_voltage": "Spänning",
"dc_power_control_voltage_unit": "V", "dc_power_control_voltage_unit": "V",
"dc_power_control_voltage": "Spänning",
"delete": "Radera", "delete": "Radera",
"deregister_button": "Avregistrera dig från molnet", "deregister_button": "Avregistrera dig från molnet",
"deregister_cloud_devices": "Molnenheter", "deregister_cloud_devices": "Molnenheter",
@ -226,12 +226,12 @@
"extension_popover_load_and_manage_extensions": "Ladda och hantera dina tillägg", "extension_popover_load_and_manage_extensions": "Ladda och hantera dina tillägg",
"extension_popover_set_error_notification": "Misslyckades med att ange aktivt tillägg: {error}", "extension_popover_set_error_notification": "Misslyckades med att ange aktivt tillägg: {error}",
"extension_popover_unload_extension": "Avlasta tillägg", "extension_popover_unload_extension": "Avlasta tillägg",
"extension_serial_console": "Seriell konsol",
"extension_serial_console_description": "Åtkomst till din seriella konsoltillägg", "extension_serial_console_description": "Åtkomst till din seriella konsoltillägg",
"extensions_atx_power_control": "ATX-strömkontroll", "extension_serial_console": "Seriell konsol",
"extensions_atx_power_control_description": "Styr din maskins strömförsörjning via ATX-strömkontroll.", "extensions_atx_power_control_description": "Styr din maskins strömförsörjning via ATX-strömkontroll.",
"extensions_dc_power_control": "DC-strömstyrning", "extensions_atx_power_control": "ATX-strömkontroll",
"extensions_dc_power_control_description": "Styr din DC-strömförlängning", "extensions_dc_power_control_description": "Styr din DC-strömförlängning",
"extensions_dc_power_control": "DC-strömstyrning",
"extensions_popover_extensions": "Tillägg", "extensions_popover_extensions": "Tillägg",
"gathering_ice_candidates": "Samlar ICE-kandidater…", "gathering_ice_candidates": "Samlar ICE-kandidater…",
"general_app_version": "App: {version}", "general_app_version": "App: {version}",
@ -241,8 +241,8 @@
"general_check_for_updates": "Kontrollera efter uppdateringar", "general_check_for_updates": "Kontrollera efter uppdateringar",
"general_page_description": "Konfigurera enhetsinställningar och uppdatera inställningar", "general_page_description": "Konfigurera enhetsinställningar och uppdatera inställningar",
"general_reboot_description": "Vill du fortsätta med att starta om systemet?", "general_reboot_description": "Vill du fortsätta med att starta om systemet?",
"general_reboot_device": "Starta om enheten",
"general_reboot_device_description": "Stäng av och på JetKVM:en", "general_reboot_device_description": "Stäng av och på JetKVM:en",
"general_reboot_device": "Starta om enheten",
"general_reboot_no_button": "Inga", "general_reboot_no_button": "Inga",
"general_reboot_title": "Starta om JetKVM", "general_reboot_title": "Starta om JetKVM",
"general_reboot_yes_button": "Ja", "general_reboot_yes_button": "Ja",
@ -295,9 +295,9 @@
"hardware_display_orientation_title": "Skärmorientering", "hardware_display_orientation_title": "Skärmorientering",
"hardware_display_wake_up_note": "Skärmen vaknar när anslutningsstatusen ändras eller när den berörs.", "hardware_display_wake_up_note": "Skärmen vaknar när anslutningsstatusen ändras eller när den berörs.",
"hardware_page_description": "Konfigurera skärminställningar och maskinvarualternativ för din JetKVM-enhet", "hardware_page_description": "Konfigurera skärminställningar och maskinvarualternativ för din JetKVM-enhet",
"hardware_time_10_minutes": "10 minuter",
"hardware_time_1_hour": "1 timme", "hardware_time_1_hour": "1 timme",
"hardware_time_1_minute": "1 minut", "hardware_time_1_minute": "1 minut",
"hardware_time_10_minutes": "10 minuter",
"hardware_time_30_minutes": "30 minuter", "hardware_time_30_minutes": "30 minuter",
"hardware_time_5_minutes": "5 minuter", "hardware_time_5_minutes": "5 minuter",
"hardware_time_never": "Aldrig", "hardware_time_never": "Aldrig",
@ -327,7 +327,6 @@
"invalid_password": "Ogiltigt lösenord", "invalid_password": "Ogiltigt lösenord",
"ip_address": "IP-adress", "ip_address": "IP-adress",
"ipv6_address_label": "Adress", "ipv6_address_label": "Adress",
"ipv6_gateway": "Inkörsport",
"ipv6_information": "IPv6-information", "ipv6_information": "IPv6-information",
"ipv6_link_local": "Länklokal", "ipv6_link_local": "Länklokal",
"ipv6_preferred_lifetime": "Föredragen livslängd", "ipv6_preferred_lifetime": "Föredragen livslängd",
@ -410,8 +409,8 @@
"log_in": "Logga in", "log_in": "Logga in",
"log_out": "Logga ut", "log_out": "Logga ut",
"logged_in_as": "Inloggad som", "logged_in_as": "Inloggad som",
"login_enter_password": "Ange ditt lösenord",
"login_enter_password_description": "Ange ditt lösenord för att komma åt din JetKVM.", "login_enter_password_description": "Ange ditt lösenord för att komma åt din JetKVM.",
"login_enter_password": "Ange ditt lösenord",
"login_error": "Ett fel uppstod vid inloggning", "login_error": "Ett fel uppstod vid inloggning",
"login_forgot_password": "Glömt lösenordet?", "login_forgot_password": "Glömt lösenordet?",
"login_password_label": "Lösenord", "login_password_label": "Lösenord",
@ -425,8 +424,8 @@
"macro_name_required": "Namn krävs", "macro_name_required": "Namn krävs",
"macro_name_too_long": "Namnet måste vara kortare än 50 tecken", "macro_name_too_long": "Namnet måste vara kortare än 50 tecken",
"macro_please_fix_validation_errors": "Vänligen åtgärda valideringsfelen", "macro_please_fix_validation_errors": "Vänligen åtgärda valideringsfelen",
"macro_save": "Spara makro",
"macro_save_error": "Ett fel uppstod när dokumentet skulle sparas.", "macro_save_error": "Ett fel uppstod när dokumentet skulle sparas.",
"macro_save": "Spara makro",
"macro_step_count": "{steps} / {max} steg", "macro_step_count": "{steps} / {max} steg",
"macro_step_duration_description": "Dags att vänta innan nästa steg genomförs.", "macro_step_duration_description": "Dags att vänta innan nästa steg genomförs.",
"macro_step_duration_label": "Steglängd", "macro_step_duration_label": "Steglängd",
@ -440,8 +439,8 @@
"macro_steps_description": "Tangenter/modifierare exekveras i sekvens med en fördröjning mellan varje steg.", "macro_steps_description": "Tangenter/modifierare exekveras i sekvens med en fördröjning mellan varje steg.",
"macro_steps_label": "Steg", "macro_steps_label": "Steg",
"macros_add_description": "Skapa ett nytt tangentbordsmakro", "macros_add_description": "Skapa ett nytt tangentbordsmakro",
"macros_add_new": "Lägg till nytt makro",
"macros_add_new_macro": "Lägg till nytt makro", "macros_add_new_macro": "Lägg till nytt makro",
"macros_add_new": "Lägg till nytt makro",
"macros_aria_add_new": "Lägg till nytt makro", "macros_aria_add_new": "Lägg till nytt makro",
"macros_aria_delete": "Ta bort makro {name}", "macros_aria_delete": "Ta bort makro {name}",
"macros_aria_duplicate": "Duplicera makro {name}", "macros_aria_duplicate": "Duplicera makro {name}",
@ -463,16 +462,16 @@
"macros_edit_button": "Redigera", "macros_edit_button": "Redigera",
"macros_edit_description": "Ändra ditt tangentbordsmakro", "macros_edit_description": "Ändra ditt tangentbordsmakro",
"macros_edit_title": "Redigera makro", "macros_edit_title": "Redigera makro",
"macros_failed_create": "Misslyckades med att skapa makrot",
"macros_failed_create_error": "Misslyckades med att skapa makrot: {error}", "macros_failed_create_error": "Misslyckades med att skapa makrot: {error}",
"macros_failed_delete": "Misslyckades med att ta bort makrot", "macros_failed_create": "Misslyckades med att skapa makrot",
"macros_failed_delete_error": "Misslyckades med att ta bort makrot: {error}", "macros_failed_delete_error": "Misslyckades med att ta bort makrot: {error}",
"macros_failed_duplicate": "Misslyckades med att duplicera makrot", "macros_failed_delete": "Misslyckades med att ta bort makrot",
"macros_failed_duplicate_error": "Misslyckades med att duplicera makrot: {error}", "macros_failed_duplicate_error": "Misslyckades med att duplicera makrot: {error}",
"macros_failed_reorder": "Misslyckades med att ändra ordningen på makrona", "macros_failed_duplicate": "Misslyckades med att duplicera makrot",
"macros_failed_reorder_error": "Misslyckades med att ändra ordning på makron: {error}", "macros_failed_reorder_error": "Misslyckades med att ändra ordning på makron: {error}",
"macros_failed_update": "Misslyckades med att uppdatera makrot", "macros_failed_reorder": "Misslyckades med att ändra ordningen på makrona",
"macros_failed_update_error": "Misslyckades med att uppdatera makrot: {error}", "macros_failed_update_error": "Misslyckades med att uppdatera makrot: {error}",
"macros_failed_update": "Misslyckades med att uppdatera makrot",
"macros_invalid_data": "Ogiltig makrodata", "macros_invalid_data": "Ogiltig makrodata",
"macros_loading": "Läser in makron…", "macros_loading": "Läser in makron…",
"macros_max_reached": "Max uppnått", "macros_max_reached": "Max uppnått",
@ -507,8 +506,8 @@
"mount_error_list_storage": "Fel vid lista av lagringsfiler: {error}", "mount_error_list_storage": "Fel vid lista av lagringsfiler: {error}",
"mount_error_title": "Monteringsfel", "mount_error_title": "Monteringsfel",
"mount_get_state_error": "Misslyckades med att hämta virtuellt medietillstånd: {error}", "mount_get_state_error": "Misslyckades med att hämta virtuellt medietillstånd: {error}",
"mount_jetkvm_storage": "JetKVM-lagringsmontering",
"mount_jetkvm_storage_description": "Montera tidigare uppladdade filer från JetKVM-lagringen", "mount_jetkvm_storage_description": "Montera tidigare uppladdade filer från JetKVM-lagringen",
"mount_jetkvm_storage": "JetKVM-lagringsmontering",
"mount_mode_cdrom": "CD/DVD", "mount_mode_cdrom": "CD/DVD",
"mount_mode_disk": "Disk", "mount_mode_disk": "Disk",
"mount_mounted_as": "Monterad som", "mount_mounted_as": "Monterad som",
@ -521,8 +520,8 @@
"mount_popular_images": "Populära bilder", "mount_popular_images": "Populära bilder",
"mount_streaming_from_url": "Streaming från URL", "mount_streaming_from_url": "Streaming från URL",
"mount_supported_formats": "Format som stöds: ISO, IMG", "mount_supported_formats": "Format som stöds: ISO, IMG",
"mount_unmount": "Avmontera",
"mount_unmount_error": "Misslyckades med att avmontera bilden: {error}", "mount_unmount_error": "Misslyckades med att avmontera bilden: {error}",
"mount_unmount": "Avmontera",
"mount_upload_description": "Välj en bildfil att ladda upp till JetKVM-lagring", "mount_upload_description": "Välj en bildfil att ladda upp till JetKVM-lagring",
"mount_upload_error": "Uppladdningsfel: {error}", "mount_upload_error": "Uppladdningsfel: {error}",
"mount_upload_failed_datachannel": "Misslyckades med att skapa datakanal för filuppladdning", "mount_upload_failed_datachannel": "Misslyckades med att skapa datakanal för filuppladdning",
@ -530,8 +529,8 @@
"mount_upload_successful": "Uppladdningen lyckades", "mount_upload_successful": "Uppladdningen lyckades",
"mount_upload_title": "Ladda upp ny bild", "mount_upload_title": "Ladda upp ny bild",
"mount_uploaded_has_been_uploaded": "{name} har laddats upp", "mount_uploaded_has_been_uploaded": "{name} har laddats upp",
"mount_uploading": "Laddar upp…",
"mount_uploading_with_name": "Laddar upp {name}", "mount_uploading_with_name": "Laddar upp {name}",
"mount_uploading": "Laddar upp…",
"mount_url_description": "Montera filer från valfri offentlig webbadress", "mount_url_description": "Montera filer från valfri offentlig webbadress",
"mount_url_input_label": "Bild-URL", "mount_url_input_label": "Bild-URL",
"mount_url_mount": "URL-montering", "mount_url_mount": "URL-montering",
@ -539,10 +538,10 @@
"mount_view_device_title": "Montera från JetKVM-lagring", "mount_view_device_title": "Montera från JetKVM-lagring",
"mount_view_url_description": "Ange en URL till bildfilen som ska monteras", "mount_view_url_description": "Ange en URL till bildfilen som ska monteras",
"mount_view_url_title": "Montera från URL", "mount_view_url_title": "Montera från URL",
"mount_virtual_media": "Virtuella medier",
"mount_virtual_media_description": "Montera en avbildning för att starta från eller installera ett operativsystem.", "mount_virtual_media_description": "Montera en avbildning för att starta från eller installera ett operativsystem.",
"mount_virtual_media_source": "Virtuell mediekälla",
"mount_virtual_media_source_description": "Välj hur du vill montera ditt virtuella media", "mount_virtual_media_source_description": "Välj hur du vill montera ditt virtuella media",
"mount_virtual_media_source": "Virtuell mediekälla",
"mount_virtual_media": "Virtuella medier",
"mouse_alt_finger": "Finger som rör vid en skärm", "mouse_alt_finger": "Finger som rör vid en skärm",
"mouse_alt_mouse": "Musikon", "mouse_alt_mouse": "Musikon",
"mouse_description": "Konfigurera markörens beteende och interaktionsinställningar för din enhet", "mouse_description": "Konfigurera markörens beteende och interaktionsinställningar för din enhet",
@ -559,10 +558,10 @@
"mouse_jiggler_light": "Ljus - 5m", "mouse_jiggler_light": "Ljus - 5m",
"mouse_jiggler_standard": "Standard - 1 m", "mouse_jiggler_standard": "Standard - 1 m",
"mouse_jiggler_title": "Jiggler", "mouse_jiggler_title": "Jiggler",
"mouse_mode_absolute": "Absolut",
"mouse_mode_absolute_description": "Mest bekvämt", "mouse_mode_absolute_description": "Mest bekvämt",
"mouse_mode_relative": "Relativ", "mouse_mode_absolute": "Absolut",
"mouse_mode_relative_description": "Mest kompatibla", "mouse_mode_relative_description": "Mest kompatibla",
"mouse_mode_relative": "Relativ",
"mouse_modes_description": "Välj musinmatningsläge", "mouse_modes_description": "Välj musinmatningsläge",
"mouse_modes_title": "Lägen", "mouse_modes_title": "Lägen",
"mouse_scroll_high": "Hög", "mouse_scroll_high": "Hög",
@ -575,17 +574,12 @@
"mouse_title": "Mus", "mouse_title": "Mus",
"network_custom_domain": "Anpassad domän", "network_custom_domain": "Anpassad domän",
"network_description": "Konfigurera dina nätverksinställningar", "network_description": "Konfigurera dina nätverksinställningar",
"network_dhcp_client_description": "Konfigurera vilken DHCP-klient som ska användas",
"network_dhcp_client_jetkvm": "JetKVM Intern",
"network_dhcp_client_title": "DHCP-klient",
"network_dhcp_information": "DHCP-information", "network_dhcp_information": "DHCP-information",
"network_dhcp_lease_renew": "Förnya DHCP-lease",
"network_dhcp_lease_renew_confirm": "Förnya hyresavtalet",
"network_dhcp_lease_renew_confirm_description": "Detta kommer att begära en ny IP-adress från din DHCP-server. Din enhet kan tillfälligt förlora nätverksanslutningen under denna process.", "network_dhcp_lease_renew_confirm_description": "Detta kommer att begära en ny IP-adress från din DHCP-server. Din enhet kan tillfälligt förlora nätverksanslutningen under denna process.",
"network_dhcp_lease_renew_confirm_new_a": "Om du får en ny IP-adress", "network_dhcp_lease_renew_confirm": "Förnya hyresavtalet",
"network_dhcp_lease_renew_confirm_new_b": "du kan behöva återansluta med den nya adressen",
"network_dhcp_lease_renew_failed": "Misslyckades med att förnya leasingavtalet: {error}", "network_dhcp_lease_renew_failed": "Misslyckades med att förnya leasingavtalet: {error}",
"network_dhcp_lease_renew_success": "DHCP-lease förnyad", "network_dhcp_lease_renew_success": "DHCP-lease förnyad",
"network_dhcp_lease_renew": "Förnya DHCP-lease",
"network_domain_custom": "Beställnings", "network_domain_custom": "Beställnings",
"network_domain_description": "Nätverksdomänsuffix för enheten", "network_domain_description": "Nätverksdomänsuffix för enheten",
"network_domain_dhcp_provided": "DHCP tillhandahålls", "network_domain_dhcp_provided": "DHCP tillhandahålls",
@ -594,35 +588,21 @@
"network_hostname_description": "Enhetsidentifierare i nätverket. Tomt för systemstandard", "network_hostname_description": "Enhetsidentifierare i nätverket. Tomt för systemstandard",
"network_hostname_title": "Värdnamn", "network_hostname_title": "Värdnamn",
"network_http_proxy_description": "Proxyserver för utgående HTTP(S)-förfrågningar från enheten. Tomt för inga.", "network_http_proxy_description": "Proxyserver för utgående HTTP(S)-förfrågningar från enheten. Tomt för inga.",
"network_http_proxy_invalid": "Ogiltig HTTP-proxy-URL",
"network_http_proxy_title": "HTTP-proxy", "network_http_proxy_title": "HTTP-proxy",
"network_ipv4_address": "IPv4-adress",
"network_ipv4_dns": "IPv4 DNS",
"network_ipv4_gateway": "IPv4-gateway",
"network_ipv4_mode_description": "Konfigurera IPv4-läget", "network_ipv4_mode_description": "Konfigurera IPv4-läget",
"network_ipv4_mode_dhcp": "DHCP", "network_ipv4_mode_dhcp": "DHCP",
"network_ipv4_mode_static": "Statisk",
"network_ipv4_mode_title": "IPv4-läge", "network_ipv4_mode_title": "IPv4-läge",
"network_ipv4_netmask": "IPv4-nätmask",
"network_ipv6_address": "IPv6-adress",
"network_ipv6_information": "IPv6-information", "network_ipv6_information": "IPv6-information",
"network_ipv6_mode_description": "Konfigurera IPv6-läget", "network_ipv6_mode_description": "Konfigurera IPv6-läget",
"network_ipv6_mode_dhcpv6": "DHCPv6",
"network_ipv6_mode_disabled": "Funktionshindrad", "network_ipv6_mode_disabled": "Funktionshindrad",
"network_ipv6_mode_link_local": "Endast länklokal",
"network_ipv6_mode_slaac": "SLAAC", "network_ipv6_mode_slaac": "SLAAC",
"network_ipv6_mode_slaac_dhcpv6": "SLAAC + DHCPv6",
"network_ipv6_mode_static": "Statisk",
"network_ipv6_mode_title": "IPv6-läge", "network_ipv6_mode_title": "IPv6-läge",
"network_ipv6_netmask": "IPv6-nätmask",
"network_ipv6_no_addresses": "Inga IPv6-adresser konfigurerade", "network_ipv6_no_addresses": "Inga IPv6-adresser konfigurerade",
"network_ll_dp_all": "Alla", "network_ll_dp_all": "Alla",
"network_ll_dp_basic": "Grundläggande", "network_ll_dp_basic": "Grundläggande",
"network_ll_dp_description": "Kontrollera vilka TLV:er som ska skickas via Link Layer Discovery Protocol", "network_ll_dp_description": "Kontrollera vilka TLV:er som ska skickas via Link Layer Discovery Protocol",
"network_ll_dp_disabled": "Funktionshindrad", "network_ll_dp_disabled": "Funktionshindrad",
"network_ll_dp_title": "LLDP", "network_ll_dp_title": "LLDP",
"network_mac_address_copy_error": "Misslyckades med att kopiera MAC-adressen",
"network_mac_address_copy_success": "MAC-adress { mac } kopierad till urklipp",
"network_mac_address_description": "Maskinvaruidentifierare för nätverksgränssnittet", "network_mac_address_description": "Maskinvaruidentifierare för nätverksgränssnittet",
"network_mac_address_title": "MAC-adress", "network_mac_address_title": "MAC-adress",
"network_mdns_auto": "Bil", "network_mdns_auto": "Bil",
@ -632,19 +612,9 @@
"network_mdns_ipv6_only": "Endast IPv6", "network_mdns_ipv6_only": "Endast IPv6",
"network_mdns_title": "mDNS", "network_mdns_title": "mDNS",
"network_no_dhcp_lease": "Ingen DHCP-leaseinformation tillgänglig", "network_no_dhcp_lease": "Ingen DHCP-leaseinformation tillgänglig",
"network_no_information_description": "Ingen nätverkskonfiguration tillgänglig",
"network_no_information_headline": "Nätverksinformation",
"network_pending_dhcp_mode_change_description": "Spara inställningar för att aktivera DHCP-läge och visa leasinginformation",
"network_pending_dhcp_mode_change_headline": "Väntar på ändring av DHCP IPv4-läge",
"network_save_settings": "Spara inställningar",
"network_save_settings_apply_title": "Tillämpa nätverksinställningar",
"network_save_settings_confirm": "Tillämpa ändringar",
"network_save_settings_confirm_description": "Följande nätverksinställningar kommer att tillämpas. Dessa ändringar kan kräva en omstart och orsaka en kortvarig frånkoppling.",
"network_save_settings_confirm_heading": "Konfigurationsändringar",
"network_save_settings_failed": "Misslyckades med att spara nätverksinställningar: {error}", "network_save_settings_failed": "Misslyckades med att spara nätverksinställningar: {error}",
"network_save_settings_success": "Nätverksinställningar sparade", "network_save_settings_success": "Nätverksinställningar sparade",
"network_settings_invalid_ipv4_cidr": "Ogiltig CIDR-notation för IPv4-adress", "network_save_settings": "Spara inställningar",
"network_settings_load_error": "Misslyckades med att läsa in nätverksinställningar: {error}",
"network_time_sync_description": "Konfigurera inställningar för tidssynkronisering", "network_time_sync_description": "Konfigurera inställningar för tidssynkronisering",
"network_time_sync_http_only": "Endast HTTP", "network_time_sync_http_only": "Endast HTTP",
"network_time_sync_ntp_and_http": "NTP och HTTP", "network_time_sync_ntp_and_http": "NTP och HTTP",
@ -670,8 +640,8 @@
"paste_modal_failed_paste": "Misslyckades med att klistra in text: {error}", "paste_modal_failed_paste": "Misslyckades med att klistra in text: {error}",
"paste_modal_invalid_chars_intro": "Följande tecken klistras inte in:", "paste_modal_invalid_chars_intro": "Följande tecken klistras inte in:",
"paste_modal_paste_from_host": "Klistra in från värd", "paste_modal_paste_from_host": "Klistra in från värd",
"paste_modal_paste_text": "Klistra in text",
"paste_modal_paste_text_description": "Klistra in text från din klient till fjärrdatorn", "paste_modal_paste_text_description": "Klistra in text från din klient till fjärrdatorn",
"paste_modal_paste_text": "Klistra in text",
"paste_modal_sending_using_layout": "Skicka text med tangentbordslayout: {iso} - {name}", "paste_modal_sending_using_layout": "Skicka text med tangentbordslayout: {iso} - {name}",
"peer_connection_closed": "Stängd", "peer_connection_closed": "Stängd",
"peer_connection_closing": "Stängning", "peer_connection_closing": "Stängning",
@ -698,20 +668,20 @@
"retry": "Försöka igen", "retry": "Försöka igen",
"saving": "Sparande…", "saving": "Sparande…",
"search_placeholder": "Söka…", "search_placeholder": "Söka…",
"serial_console": "Seriell konsol",
"serial_console_baud_rate": "Baudhastighet", "serial_console_baud_rate": "Baudhastighet",
"serial_console_configure_description": "Konfigurera dina seriella konsolinställningar", "serial_console_configure_description": "Konfigurera dina seriella konsolinställningar",
"serial_console_data_bits": "Databitar", "serial_console_data_bits": "Databitar",
"serial_console_get_settings_error": "Misslyckades med att hämta inställningar för seriekonsolen: {error}", "serial_console_get_settings_error": "Misslyckades med att hämta inställningar för seriekonsolen: {error}",
"serial_console_open_console": "Öppna konsolen", "serial_console_open_console": "Öppna konsolen",
"serial_console_parity": "Paritet",
"serial_console_parity_even": "Jämn paritet", "serial_console_parity_even": "Jämn paritet",
"serial_console_parity_mark": "Markera paritet", "serial_console_parity_mark": "Markera paritet",
"serial_console_parity_none": "Ingen paritet", "serial_console_parity_none": "Ingen paritet",
"serial_console_parity_odd": "Udda paritet", "serial_console_parity_odd": "Udda paritet",
"serial_console_parity_space": "Rymdparitet", "serial_console_parity_space": "Rymdparitet",
"serial_console_parity": "Paritet",
"serial_console_set_settings_error": "Misslyckades med att ställa in seriekonsolinställningarna till {settings} : {error}", "serial_console_set_settings_error": "Misslyckades med att ställa in seriekonsolinställningarna till {settings} : {error}",
"serial_console_stop_bits": "Stoppbitar", "serial_console_stop_bits": "Stoppbitar",
"serial_console": "Seriell konsol",
"setting_remote_description": "Ställa in fjärrkontrollens beskrivning", "setting_remote_description": "Ställa in fjärrkontrollens beskrivning",
"setting_remote_session_description": "Ställer in beskrivning av fjärrsession...", "setting_remote_session_description": "Ställer in beskrivning av fjärrsession...",
"setting_up_connection_to_device": "Konfigurerar anslutning till enhet...", "setting_up_connection_to_device": "Konfigurerar anslutning till enhet...",
@ -721,8 +691,8 @@
"settings_back_to_kvm": "Tillbaka till KVM", "settings_back_to_kvm": "Tillbaka till KVM",
"settings_general": "Allmän", "settings_general": "Allmän",
"settings_hardware": "Hårdvara", "settings_hardware": "Hårdvara",
"settings_keyboard": "Tangentbord",
"settings_keyboard_macros": "Tangentbordsmakron", "settings_keyboard_macros": "Tangentbordsmakron",
"settings_keyboard": "Tangentbord",
"settings_mouse": "Mus", "settings_mouse": "Mus",
"settings_network": "Nätverk", "settings_network": "Nätverk",
"settings_video": "Video", "settings_video": "Video",
@ -742,7 +712,6 @@
"updates_failed_check": "Misslyckades med att söka efter uppdateringar: {error}", "updates_failed_check": "Misslyckades med att söka efter uppdateringar: {error}",
"updates_failed_get_device_version": "Misslyckades med att hämta enhetsversionen: {error}", "updates_failed_get_device_version": "Misslyckades med att hämta enhetsversionen: {error}",
"updating_leave_device_on": "Stäng inte av din enhet…", "updating_leave_device_on": "Stäng inte av din enhet…",
"usb": "USB",
"usb_config_custom": "Beställnings", "usb_config_custom": "Beställnings",
"usb_config_default": "JetKVM-standard", "usb_config_default": "JetKVM-standard",
"usb_config_dell": "Dell Multimedia Pro-tangentbord", "usb_config_dell": "Dell Multimedia Pro-tangentbord",
@ -789,6 +758,7 @@
"usb_state_connecting": "Ansluter", "usb_state_connecting": "Ansluter",
"usb_state_disconnected": "Osammanhängande", "usb_state_disconnected": "Osammanhängande",
"usb_state_low_power_mode": "Lågströmsläge", "usb_state_low_power_mode": "Lågströmsläge",
"usb": "USB",
"user_interface_language_description": "Välj språket som ska användas i JetKVM-användargränssnittet", "user_interface_language_description": "Välj språket som ska användas i JetKVM-användargränssnittet",
"user_interface_language_title": "Gränssnittsspråk", "user_interface_language_title": "Gränssnittsspråk",
"video_brightness_description": "Ljusstyrka ( {value} x)", "video_brightness_description": "Ljusstyrka ( {value} x)",
@ -855,7 +825,6 @@
"video_title": "Video", "video_title": "Video",
"view_details": "Visa detaljer", "view_details": "Visa detaljer",
"virtual_keyboard_header": "Virtuellt tangentbord", "virtual_keyboard_header": "Virtuellt tangentbord",
"wake_on_lan": "Vakna på LAN",
"wake_on_lan_add_device_device_name": "Enhetsnamn", "wake_on_lan_add_device_device_name": "Enhetsnamn",
"wake_on_lan_add_device_example_device_name": "Plex Media Server", "wake_on_lan_add_device_example_device_name": "Plex Media Server",
"wake_on_lan_add_device_mac_address": "MAC-adress", "wake_on_lan_add_device_mac_address": "MAC-adress",
@ -871,6 +840,7 @@
"wake_on_lan_failed_send_magic": "Misslyckades med att skicka Magic Packet", "wake_on_lan_failed_send_magic": "Misslyckades med att skicka Magic Packet",
"wake_on_lan_invalid_mac": "Ogiltig MAC-adress", "wake_on_lan_invalid_mac": "Ogiltig MAC-adress",
"wake_on_lan_magic_sent_success": "Magiskt paket skickades", "wake_on_lan_magic_sent_success": "Magiskt paket skickades",
"welcome_to_jetkvm": "Välkommen till JetKVM", "wake_on_lan": "Vakna på LAN",
"welcome_to_jetkvm_description": "Styr vilken dator som helst på distans" "welcome_to_jetkvm_description": "Styr vilken dator som helst på distans",
"welcome_to_jetkvm": "Välkommen till JetKVM"
} }

View File

@ -107,10 +107,10 @@
"already_adopted_title": "设备已注册", "already_adopted_title": "设备已注册",
"appearance_description": "选择您喜欢的颜色主题", "appearance_description": "选择您喜欢的颜色主题",
"appearance_page_description": "自定义 JetKVM 界面的外观和感觉", "appearance_page_description": "自定义 JetKVM 界面的外观和感觉",
"appearance_theme": "主题",
"appearance_theme_dark": "黑暗的", "appearance_theme_dark": "黑暗的",
"appearance_theme_light": "光", "appearance_theme_light": "光",
"appearance_theme_system": "系统", "appearance_theme_system": "系统",
"appearance_theme": "主题",
"appearance_title": "外貌", "appearance_title": "外貌",
"attach": "附", "attach": "附",
"atx_power_control_get_state_error": "无法获取 ATX 电源状态:{error}", "atx_power_control_get_state_error": "无法获取 ATX 电源状态:{error}",
@ -121,85 +121,85 @@
"atx_power_control_reset_button": "重置", "atx_power_control_reset_button": "重置",
"atx_power_control_send_action_error": "无法发送 ATX 电源操作 {action} : {error}", "atx_power_control_send_action_error": "无法发送 ATX 电源操作 {action} : {error}",
"atx_power_control_short_power_button": "短按", "atx_power_control_short_power_button": "短按",
"auth_authentication_mode": "请选择身份验证方式",
"auth_authentication_mode_error": "设置身份验证模式时发生错误", "auth_authentication_mode_error": "设置身份验证模式时发生错误",
"auth_authentication_mode_invalid": "身份验证模式无效", "auth_authentication_mode_invalid": "身份验证模式无效",
"auth_connect_to_cloud": "将您的 JetKVM 连接到云端", "auth_authentication_mode": "请选择身份验证方式",
"auth_connect_to_cloud_action": "登录并连接设备", "auth_connect_to_cloud_action": "登录并连接设备",
"auth_connect_to_cloud_description": "解锁设备的远程访问和高级功能", "auth_connect_to_cloud_description": "解锁设备的远程访问和高级功能",
"auth_connect_to_cloud": "将您的 JetKVM 连接到云端",
"auth_header_cta_already_have_account": "已有账户?", "auth_header_cta_already_have_account": "已有账户?",
"auth_header_cta_dont_have_account": "沒有帳戶?", "auth_header_cta_dont_have_account": "沒有帳戶?",
"auth_header_cta_new_to_jetkvm": "JetKVM 新手?", "auth_header_cta_new_to_jetkvm": "JetKVM 新手?",
"auth_login": "登录您的 JetKVM 帐户",
"auth_login_action": "登录", "auth_login_action": "登录",
"auth_login_description": "登录以安全地访问和管理您的设备", "auth_login_description": "登录以安全地访问和管理您的设备",
"auth_mode_local": "本地身份验证方法", "auth_login": "登录您的 JetKVM 帐户",
"auth_mode_local_change_later": "您可以随时在设置中更改您的身份验证方法。", "auth_mode_local_change_later": "您可以随时在设置中更改您的身份验证方法。",
"auth_mode_local_description": "选择您希望如何在本地保护您的 JetKVM 设备。", "auth_mode_local_description": "选择您希望如何在本地保护您的 JetKVM 设备。",
"auth_mode_local_no_password": "没有密码",
"auth_mode_local_no_password_description": "无需密码验证即可快速访问。", "auth_mode_local_no_password_description": "无需密码验证即可快速访问。",
"auth_mode_local_password": "密码", "auth_mode_local_no_password": "没有密码",
"auth_mode_local_password_confirm_description": "确认您的密码", "auth_mode_local_password_confirm_description": "确认您的密码",
"auth_mode_local_password_confirm_label": "确认密码", "auth_mode_local_password_confirm_label": "确认密码",
"auth_mode_local_password_description": "使用密码保护您的设备以增强保护。", "auth_mode_local_password_description": "使用密码保护您的设备以增强保护。",
"auth_mode_local_password_do_not_match": "密码不匹配", "auth_mode_local_password_do_not_match": "密码不匹配",
"auth_mode_local_password_failed_set": "无法设置密码: {error}", "auth_mode_local_password_failed_set": "无法设置密码: {error}",
"auth_mode_local_password_note": "此密码将用于保护您的设备数据并防止未经授权的访问。",
"auth_mode_local_password_note_local": "所有数据都保留在您的本地设备上。", "auth_mode_local_password_note_local": "所有数据都保留在您的本地设备上。",
"auth_mode_local_password_set": "设置密码", "auth_mode_local_password_note": "此密码将用于保护您的设备数据并防止未经授权的访问。",
"auth_mode_local_password_set_button": "设置密码", "auth_mode_local_password_set_button": "设置密码",
"auth_mode_local_password_set_description": "创建一个强密码来本地保护您的 JetKVM 设备。", "auth_mode_local_password_set_description": "创建一个强密码来本地保护您的 JetKVM 设备。",
"auth_mode_local_password_set_label": "输入密码", "auth_mode_local_password_set_label": "输入密码",
"auth_mode_local_password_set": "设置密码",
"auth_mode_local_password": "密码",
"auth_mode_local": "本地身份验证方法",
"auth_signup_connect_to_cloud_action": "注册并连接设备", "auth_signup_connect_to_cloud_action": "注册并连接设备",
"auth_signup_create_account": "创建您的 JetKVM 帐户",
"auth_signup_create_account_action": "创建账户", "auth_signup_create_account_action": "创建账户",
"auth_signup_create_account_description": "创建您的帐户并开始轻松管理您的设备。", "auth_signup_create_account_description": "创建您的帐户并开始轻松管理您的设备。",
"back": "后退", "auth_signup_create_account": "创建您的 JetKVM 帐户",
"back_to_devices": "返回设备", "back_to_devices": "返回设备",
"back": "后退",
"cancel": "取消", "cancel": "取消",
"close": "关闭", "close": "关闭",
"cloud_kvms": "云 KVM",
"cloud_kvms_description": "管理您的云 KVM 并安全地连接到它们。", "cloud_kvms_description": "管理您的云 KVM 并安全地连接到它们。",
"cloud_kvms_no_devices": "未找到设备",
"cloud_kvms_no_devices_description": "您还没有任何启用 JetKVM Cloud 的设备。", "cloud_kvms_no_devices_description": "您还没有任何启用 JetKVM Cloud 的设备。",
"cloud_kvms_no_devices": "未找到设备",
"cloud_kvms": "云 KVM",
"confirm": "确认", "confirm": "确认",
"connect_to_kvm": "连接到 KVM", "connect_to_kvm": "连接到 KVM",
"connecting_to_device": "正在连接设备…", "connecting_to_device": "正在连接设备…",
"connection_established": "已建立连接", "connection_established": "已建立连接",
"connection_stats_badge_jitter": "抖动",
"connection_stats_badge_jitter_buffer_avg_delay": "抖动缓冲区平均延迟", "connection_stats_badge_jitter_buffer_avg_delay": "抖动缓冲区平均延迟",
"connection_stats_connection": "联系", "connection_stats_badge_jitter": "抖动",
"connection_stats_connection_description": "客户端与JetKVM之间的连接。", "connection_stats_connection_description": "客户端与JetKVM之间的连接。",
"connection_stats_frames_per_second": "每秒帧数", "connection_stats_connection": "联系",
"connection_stats_frames_per_second_description": "每秒显示的入站视频帧数。", "connection_stats_frames_per_second_description": "每秒显示的入站视频帧数。",
"connection_stats_network_stability": "网络稳定性", "connection_stats_frames_per_second": "每秒帧数",
"connection_stats_network_stability_description": "网络上传入的视频数据包的流动有多稳定。", "connection_stats_network_stability_description": "网络上传入的视频数据包的流动有多稳定。",
"connection_stats_packets_lost": "数据包丢失", "connection_stats_network_stability": "网络稳定性",
"connection_stats_packets_lost_description": "丢失的入站视频 RTP 数据包的数量。", "connection_stats_packets_lost_description": "丢失的入站视频 RTP 数据包的数量。",
"connection_stats_playback_delay": "播放延迟", "connection_stats_packets_lost": "数据包丢失",
"connection_stats_playback_delay_description": "当帧不均匀到达时,抖动缓冲区添加延迟以平滑播放。", "connection_stats_playback_delay_description": "当帧不均匀到达时,抖动缓冲区添加延迟以平滑播放。",
"connection_stats_round_trip_time": "往返时间", "connection_stats_playback_delay": "播放延迟",
"connection_stats_round_trip_time_description": "对等体之间活跃 ICE 候选对的往返时间。", "connection_stats_round_trip_time_description": "对等体之间活跃 ICE 候选对的往返时间。",
"connection_stats_round_trip_time": "往返时间",
"connection_stats_sidebar": "连接统计", "connection_stats_sidebar": "连接统计",
"connection_stats_video": "视频",
"connection_stats_video_description": "从 JetKVM 到客户端的视频流。", "connection_stats_video_description": "从 JetKVM 到客户端的视频流。",
"connection_stats_video": "视频",
"continue": "继续", "continue": "继续",
"creating_peer_connection": "正在创建对等连接...", "creating_peer_connection": "正在创建对等连接...",
"dc_power_control_current": "电流",
"dc_power_control_current_unit": "A", "dc_power_control_current_unit": "A",
"dc_power_control_current": "电流",
"dc_power_control_get_state_error": "无法获取直流电源状态:{error}", "dc_power_control_get_state_error": "无法获取直流电源状态:{error}",
"dc_power_control_power": "瓦特",
"dc_power_control_power_off_button": "关闭电源", "dc_power_control_power_off_button": "关闭电源",
"dc_power_control_power_off_state": "关闭电源", "dc_power_control_power_off_state": "关闭电源",
"dc_power_control_power_on_button": "开机", "dc_power_control_power_on_button": "开机",
"dc_power_control_power_on_state": "开启电源", "dc_power_control_power_on_state": "开启电源",
"dc_power_control_power_unit": "W", "dc_power_control_power_unit": "W",
"dc_power_control_power": "瓦特",
"dc_power_control_restore_last_state": "最后状态", "dc_power_control_restore_last_state": "最后状态",
"dc_power_control_restore_power_state": "恢复断电", "dc_power_control_restore_power_state": "恢复断电",
"dc_power_control_set_power_state_error": "无法将直流电源状态发送到 {enabled} : {error}", "dc_power_control_set_power_state_error": "无法将直流电源状态发送到 {enabled} : {error}",
"dc_power_control_set_restore_state_error": "无法将直流电源恢复状态发送到 {state} : {error}", "dc_power_control_set_restore_state_error": "无法将直流电源恢复状态发送到 {state} : {error}",
"dc_power_control_voltage": "电压",
"dc_power_control_voltage_unit": "V", "dc_power_control_voltage_unit": "V",
"dc_power_control_voltage": "电压",
"delete": "删除", "delete": "删除",
"deregister_button": "从云端注销", "deregister_button": "从云端注销",
"deregister_cloud_devices": "云设备", "deregister_cloud_devices": "云设备",
@ -226,12 +226,12 @@
"extension_popover_load_and_manage_extensions": "加载和管理您的扩展", "extension_popover_load_and_manage_extensions": "加载和管理您的扩展",
"extension_popover_set_error_notification": "无法设置活动扩展:{error}", "extension_popover_set_error_notification": "无法设置活动扩展:{error}",
"extension_popover_unload_extension": "卸载扩展", "extension_popover_unload_extension": "卸载扩展",
"extension_serial_console": "串行控制台",
"extension_serial_console_description": "访问串行控制台扩展", "extension_serial_console_description": "访问串行控制台扩展",
"extensions_atx_power_control": "ATX 电源控制", "extension_serial_console": "串行控制台",
"extensions_atx_power_control_description": "通过 ATX 电源控制来控制机器的电源状态。", "extensions_atx_power_control_description": "通过 ATX 电源控制来控制机器的电源状态。",
"extensions_dc_power_control": "直流电源控制", "extensions_atx_power_control": "ATX 电源控制",
"extensions_dc_power_control_description": "控制您的直流电源扩展", "extensions_dc_power_control_description": "控制您的直流电源扩展",
"extensions_dc_power_control": "直流电源控制",
"extensions_popover_extensions": "扩展", "extensions_popover_extensions": "扩展",
"gathering_ice_candidates": "召集 ICE 候选人……", "gathering_ice_candidates": "召集 ICE 候选人……",
"general_app_version": "应用程序: {version}", "general_app_version": "应用程序: {version}",
@ -241,8 +241,8 @@
"general_check_for_updates": "检查更新", "general_check_for_updates": "检查更新",
"general_page_description": "配置设备设置并更新首选项", "general_page_description": "配置设备设置并更新首选项",
"general_reboot_description": "您想继续重新启动系统吗?", "general_reboot_description": "您想继续重新启动系统吗?",
"general_reboot_device": "重启设备",
"general_reboot_device_description": "对 JetKVM 进行电源循环", "general_reboot_device_description": "对 JetKVM 进行电源循环",
"general_reboot_device": "重启设备",
"general_reboot_no_button": "不", "general_reboot_no_button": "不",
"general_reboot_title": "重启 JetKVM", "general_reboot_title": "重启 JetKVM",
"general_reboot_yes_button": "是的", "general_reboot_yes_button": "是的",
@ -295,9 +295,9 @@
"hardware_display_orientation_title": "显示方向", "hardware_display_orientation_title": "显示方向",
"hardware_display_wake_up_note": "当连接状态改变或被触摸时,显示屏将会唤醒。", "hardware_display_wake_up_note": "当连接状态改变或被触摸时,显示屏将会唤醒。",
"hardware_page_description": "为您的 JetKVM 设备配置显示设置和硬件选项", "hardware_page_description": "为您的 JetKVM 设备配置显示设置和硬件选项",
"hardware_time_10_minutes": "10分钟",
"hardware_time_1_hour": "1小时", "hardware_time_1_hour": "1小时",
"hardware_time_1_minute": "1分钟", "hardware_time_1_minute": "1分钟",
"hardware_time_10_minutes": "10分钟",
"hardware_time_30_minutes": "30分钟", "hardware_time_30_minutes": "30分钟",
"hardware_time_5_minutes": "5分钟", "hardware_time_5_minutes": "5分钟",
"hardware_time_never": "绝不", "hardware_time_never": "绝不",
@ -327,7 +327,6 @@
"invalid_password": "密码无效", "invalid_password": "密码无效",
"ip_address": "IP 地址", "ip_address": "IP 地址",
"ipv6_address_label": "地址", "ipv6_address_label": "地址",
"ipv6_gateway": "网关",
"ipv6_information": "IPv6 信息", "ipv6_information": "IPv6 信息",
"ipv6_link_local": "本地链路", "ipv6_link_local": "本地链路",
"ipv6_preferred_lifetime": "首选寿命", "ipv6_preferred_lifetime": "首选寿命",
@ -410,8 +409,8 @@
"log_in": "登录", "log_in": "登录",
"log_out": "登出", "log_out": "登出",
"logged_in_as": "登录身份", "logged_in_as": "登录身份",
"login_enter_password": "输入您的密码",
"login_enter_password_description": "输入您的密码以访问您的 JetKVM。", "login_enter_password_description": "输入您的密码以访问您的 JetKVM。",
"login_enter_password": "输入您的密码",
"login_error": "登录时发生错误", "login_error": "登录时发生错误",
"login_forgot_password": "忘记密码?", "login_forgot_password": "忘记密码?",
"login_password_label": "密码", "login_password_label": "密码",
@ -425,8 +424,8 @@
"macro_name_required": "姓名为必填项", "macro_name_required": "姓名为必填项",
"macro_name_too_long": "名称必须少于 50 个字符", "macro_name_too_long": "名称必须少于 50 个字符",
"macro_please_fix_validation_errors": "请修复验证错误", "macro_please_fix_validation_errors": "请修复验证错误",
"macro_save": "保存宏",
"macro_save_error": "保存时发生错误。", "macro_save_error": "保存时发生错误。",
"macro_save": "保存宏",
"macro_step_count": "{steps} / {max}步骤", "macro_step_count": "{steps} / {max}步骤",
"macro_step_duration_description": "执行下一步之前需要等待的时间。", "macro_step_duration_description": "执行下一步之前需要等待的时间。",
"macro_step_duration_label": "步骤持续时间", "macro_step_duration_label": "步骤持续时间",
@ -440,8 +439,8 @@
"macro_steps_description": "键/修饰键按顺序执行,每个步骤之间有延迟。", "macro_steps_description": "键/修饰键按顺序执行,每个步骤之间有延迟。",
"macro_steps_label": "步骤", "macro_steps_label": "步骤",
"macros_add_description": "创建新的键盘宏", "macros_add_description": "创建新的键盘宏",
"macros_add_new": "添加新宏",
"macros_add_new_macro": "添加新宏", "macros_add_new_macro": "添加新宏",
"macros_add_new": "添加新宏",
"macros_aria_add_new": "添加新宏", "macros_aria_add_new": "添加新宏",
"macros_aria_delete": "删除宏{name}", "macros_aria_delete": "删除宏{name}",
"macros_aria_duplicate": "重复宏{name}", "macros_aria_duplicate": "重复宏{name}",
@ -463,16 +462,16 @@
"macros_edit_button": "编辑", "macros_edit_button": "编辑",
"macros_edit_description": "修改键盘宏", "macros_edit_description": "修改键盘宏",
"macros_edit_title": "编辑宏", "macros_edit_title": "编辑宏",
"macros_failed_create": "无法创建宏",
"macros_failed_create_error": "无法创建宏: {error}", "macros_failed_create_error": "无法创建宏: {error}",
"macros_failed_delete": "删除宏失败", "macros_failed_create": "无法创建宏",
"macros_failed_delete_error": "无法删除宏: {error}", "macros_failed_delete_error": "无法删除宏: {error}",
"macros_failed_duplicate": "复制宏失败", "macros_failed_delete": "删除宏失败",
"macros_failed_duplicate_error": "无法复制宏: {error}", "macros_failed_duplicate_error": "无法复制宏: {error}",
"macros_failed_reorder": "无法重新排序宏", "macros_failed_duplicate": "复制宏失败",
"macros_failed_reorder_error": "无法重新排序宏: {error}", "macros_failed_reorder_error": "无法重新排序宏: {error}",
"macros_failed_update": "更新宏失败", "macros_failed_reorder": "无法重新排序宏",
"macros_failed_update_error": "无法更新宏: {error}", "macros_failed_update_error": "无法更新宏: {error}",
"macros_failed_update": "更新宏失败",
"macros_invalid_data": "无效的宏数据", "macros_invalid_data": "无效的宏数据",
"macros_loading": "正在加载宏...", "macros_loading": "正在加载宏...",
"macros_max_reached": "已达到最大值", "macros_max_reached": "已达到最大值",
@ -507,8 +506,8 @@
"mount_error_list_storage": "列出存储文件时出错: {error}", "mount_error_list_storage": "列出存储文件时出错: {error}",
"mount_error_title": "安装错误", "mount_error_title": "安装错误",
"mount_get_state_error": "无法获取虚拟媒体状态: {error}", "mount_get_state_error": "无法获取虚拟媒体状态: {error}",
"mount_jetkvm_storage": "JetKVM 存储支架",
"mount_jetkvm_storage_description": "从 JetKVM 存储挂载之前上传的文件", "mount_jetkvm_storage_description": "从 JetKVM 存储挂载之前上传的文件",
"mount_jetkvm_storage": "JetKVM 存储支架",
"mount_mode_cdrom": "CD/DVD", "mount_mode_cdrom": "CD/DVD",
"mount_mode_disk": "磁盘", "mount_mode_disk": "磁盘",
"mount_mounted_as": "安装为", "mount_mounted_as": "安装为",
@ -521,8 +520,8 @@
"mount_popular_images": "热门图片", "mount_popular_images": "热门图片",
"mount_streaming_from_url": "从 URL 流式传输", "mount_streaming_from_url": "从 URL 流式传输",
"mount_supported_formats": "支持的格式ISO、IMG", "mount_supported_formats": "支持的格式ISO、IMG",
"mount_unmount": "卸载",
"mount_unmount_error": "无法卸载映像: {error}", "mount_unmount_error": "无法卸载映像: {error}",
"mount_unmount": "卸载",
"mount_upload_description": "选择要上传到 JetKVM 存储的图像文件", "mount_upload_description": "选择要上传到 JetKVM 存储的图像文件",
"mount_upload_error": "上传错误: {error}", "mount_upload_error": "上传错误: {error}",
"mount_upload_failed_datachannel": "无法创建文件上传数据通道", "mount_upload_failed_datachannel": "无法创建文件上传数据通道",
@ -530,8 +529,8 @@
"mount_upload_successful": "上传成功", "mount_upload_successful": "上传成功",
"mount_upload_title": "上传新图片", "mount_upload_title": "上传新图片",
"mount_uploaded_has_been_uploaded": "{name}已上传", "mount_uploaded_has_been_uploaded": "{name}已上传",
"mount_uploading": "正在上传…",
"mount_uploading_with_name": "正在上传{name}", "mount_uploading_with_name": "正在上传{name}",
"mount_uploading": "正在上传…",
"mount_url_description": "从任何公共网址挂载文件", "mount_url_description": "从任何公共网址挂载文件",
"mount_url_input_label": "图片网址", "mount_url_input_label": "图片网址",
"mount_url_mount": "URL 挂载", "mount_url_mount": "URL 挂载",
@ -539,10 +538,10 @@
"mount_view_device_title": "从 JetKVM 存储挂载", "mount_view_device_title": "从 JetKVM 存储挂载",
"mount_view_url_description": "输入要挂载的镜像文件的 URL", "mount_view_url_description": "输入要挂载的镜像文件的 URL",
"mount_view_url_title": "从 URL 挂载", "mount_view_url_title": "从 URL 挂载",
"mount_virtual_media": "虚拟媒体",
"mount_virtual_media_description": "挂载映像以进行启动或安装操作系统。", "mount_virtual_media_description": "挂载映像以进行启动或安装操作系统。",
"mount_virtual_media_source": "虚拟媒体源",
"mount_virtual_media_source_description": "选择如何安装虚拟媒体", "mount_virtual_media_source_description": "选择如何安装虚拟媒体",
"mount_virtual_media_source": "虚拟媒体源",
"mount_virtual_media": "虚拟媒体",
"mouse_alt_finger": "手指触摸屏幕", "mouse_alt_finger": "手指触摸屏幕",
"mouse_alt_mouse": "鼠标图标", "mouse_alt_mouse": "鼠标图标",
"mouse_description": "为您的设备配置光标行为和交互设置", "mouse_description": "为您的设备配置光标行为和交互设置",
@ -559,10 +558,10 @@
"mouse_jiggler_light": "光 - 5米", "mouse_jiggler_light": "光 - 5米",
"mouse_jiggler_standard": "标准 - 1米", "mouse_jiggler_standard": "标准 - 1米",
"mouse_jiggler_title": "吉格勒", "mouse_jiggler_title": "吉格勒",
"mouse_mode_absolute": "绝对",
"mouse_mode_absolute_description": "最方便", "mouse_mode_absolute_description": "最方便",
"mouse_mode_relative": "相对的", "mouse_mode_absolute": "绝对",
"mouse_mode_relative_description": "最兼容", "mouse_mode_relative_description": "最兼容",
"mouse_mode_relative": "相对的",
"mouse_modes_description": "选择鼠标输入模式", "mouse_modes_description": "选择鼠标输入模式",
"mouse_modes_title": "模式", "mouse_modes_title": "模式",
"mouse_scroll_high": "高的", "mouse_scroll_high": "高的",
@ -575,17 +574,12 @@
"mouse_title": "老鼠", "mouse_title": "老鼠",
"network_custom_domain": "自定义域", "network_custom_domain": "自定义域",
"network_description": "配置您的网络设置", "network_description": "配置您的网络设置",
"network_dhcp_client_description": "配置要使用的 DHCP 客户端",
"network_dhcp_client_jetkvm": "JetKVM 内部",
"network_dhcp_client_title": "DHCP客户端",
"network_dhcp_information": "DHCP 信息", "network_dhcp_information": "DHCP 信息",
"network_dhcp_lease_renew": "续订 DHCP 租约",
"network_dhcp_lease_renew_confirm": "续租",
"network_dhcp_lease_renew_confirm_description": "这将从您的 DHCP 服务器请求新的 IP 地址。在此过程中,您的设备可能会暂时失去网络连接。", "network_dhcp_lease_renew_confirm_description": "这将从您的 DHCP 服务器请求新的 IP 地址。在此过程中,您的设备可能会暂时失去网络连接。",
"network_dhcp_lease_renew_confirm_new_a": "如果您收到新的 IP 地址", "network_dhcp_lease_renew_confirm": "续租",
"network_dhcp_lease_renew_confirm_new_b": "您可能需要使用新地址重新连接",
"network_dhcp_lease_renew_failed": "无法续订租约: {error}", "network_dhcp_lease_renew_failed": "无法续订租约: {error}",
"network_dhcp_lease_renew_success": "DHCP 租约已续订", "network_dhcp_lease_renew_success": "DHCP 租约已续订",
"network_dhcp_lease_renew": "续订 DHCP 租约",
"network_domain_custom": "风俗", "network_domain_custom": "风俗",
"network_domain_description": "设备的网络域后缀", "network_domain_description": "设备的网络域后缀",
"network_domain_dhcp_provided": "DHCP 提供", "network_domain_dhcp_provided": "DHCP 提供",
@ -594,35 +588,21 @@
"network_hostname_description": "网络上的设备标识符。系统默认为空白", "network_hostname_description": "网络上的设备标识符。系统默认为空白",
"network_hostname_title": "主机名", "network_hostname_title": "主机名",
"network_http_proxy_description": "设备发出 HTTP(S) 请求的代理服务器。空白表示无。", "network_http_proxy_description": "设备发出 HTTP(S) 请求的代理服务器。空白表示无。",
"network_http_proxy_invalid": "HTTP 代理 URL 无效",
"network_http_proxy_title": "HTTP 代理", "network_http_proxy_title": "HTTP 代理",
"network_ipv4_address": "IPv4 地址",
"network_ipv4_dns": "IPv4 域名服务器",
"network_ipv4_gateway": "IPv4 网关",
"network_ipv4_mode_description": "配置 IPv4 模式", "network_ipv4_mode_description": "配置 IPv4 模式",
"network_ipv4_mode_dhcp": "DHCP", "network_ipv4_mode_dhcp": "DHCP",
"network_ipv4_mode_static": "静止的",
"network_ipv4_mode_title": "IPv4 模式", "network_ipv4_mode_title": "IPv4 模式",
"network_ipv4_netmask": "IPv4 网络掩码",
"network_ipv6_address": "IPv6 地址",
"network_ipv6_information": "IPv6 信息", "network_ipv6_information": "IPv6 信息",
"network_ipv6_mode_description": "配置 IPv6 模式", "network_ipv6_mode_description": "配置 IPv6 模式",
"network_ipv6_mode_dhcpv6": "DHCPv6",
"network_ipv6_mode_disabled": "已禁用", "network_ipv6_mode_disabled": "已禁用",
"network_ipv6_mode_link_local": "仅限本地链路",
"network_ipv6_mode_slaac": "斯坦福直线加速器", "network_ipv6_mode_slaac": "斯坦福直线加速器",
"network_ipv6_mode_slaac_dhcpv6": "SLAAC + DHCPv6",
"network_ipv6_mode_static": "静止的",
"network_ipv6_mode_title": "IPv6模式", "network_ipv6_mode_title": "IPv6模式",
"network_ipv6_netmask": "IPv6 网络掩码",
"network_ipv6_no_addresses": "未配置 IPv6 地址", "network_ipv6_no_addresses": "未配置 IPv6 地址",
"network_ll_dp_all": "全部", "network_ll_dp_all": "全部",
"network_ll_dp_basic": "基本的", "network_ll_dp_basic": "基本的",
"network_ll_dp_description": "控制哪些 TLV 将通过链路层发现协议发送", "network_ll_dp_description": "控制哪些 TLV 将通过链路层发现协议发送",
"network_ll_dp_disabled": "已禁用", "network_ll_dp_disabled": "已禁用",
"network_ll_dp_title": "链路层发现协议", "network_ll_dp_title": "链路层发现协议",
"network_mac_address_copy_error": "复制 MAC 地址失败",
"network_mac_address_copy_success": "MAC 地址{ mac }已复制到剪贴板",
"network_mac_address_description": "网络接口的硬件标识符", "network_mac_address_description": "网络接口的硬件标识符",
"network_mac_address_title": "MAC 地址", "network_mac_address_title": "MAC 地址",
"network_mdns_auto": "汽车", "network_mdns_auto": "汽车",
@ -632,19 +612,9 @@
"network_mdns_ipv6_only": "仅限 IPv6", "network_mdns_ipv6_only": "仅限 IPv6",
"network_mdns_title": "移动DNS", "network_mdns_title": "移动DNS",
"network_no_dhcp_lease": "没有可用的 DHCP 租约信息", "network_no_dhcp_lease": "没有可用的 DHCP 租约信息",
"network_no_information_description": "没有可用的网络配置",
"network_no_information_headline": "网络信息",
"network_pending_dhcp_mode_change_description": "保存设置以启用 DHCP 模式并查看租约信息",
"network_pending_dhcp_mode_change_headline": "待处理的 DHCP IPv4 模式更改",
"network_save_settings": "保存设置",
"network_save_settings_apply_title": "应用网络设置",
"network_save_settings_confirm": "应用更改",
"network_save_settings_confirm_description": "将应用以下网络设置。这些更改可能需要重新启动并导致短暂断网。",
"network_save_settings_confirm_heading": "配置更改",
"network_save_settings_failed": "无法保存网络设置: {error}", "network_save_settings_failed": "无法保存网络设置: {error}",
"network_save_settings_success": "网络设置已保存", "network_save_settings_success": "网络设置已保存",
"network_settings_invalid_ipv4_cidr": "IPv4 地址的 CIDR 表示法无效", "network_save_settings": "保存设置",
"network_settings_load_error": "无法加载网络设置: {error}",
"network_time_sync_description": "配置时间同步设置", "network_time_sync_description": "配置时间同步设置",
"network_time_sync_http_only": "仅 HTTP", "network_time_sync_http_only": "仅 HTTP",
"network_time_sync_ntp_and_http": "NTP 和 HTTP", "network_time_sync_ntp_and_http": "NTP 和 HTTP",
@ -670,8 +640,8 @@
"paste_modal_failed_paste": "粘贴文本失败: {error}", "paste_modal_failed_paste": "粘贴文本失败: {error}",
"paste_modal_invalid_chars_intro": "以下字符将不会被粘贴:", "paste_modal_invalid_chars_intro": "以下字符将不会被粘贴:",
"paste_modal_paste_from_host": "从主机粘贴", "paste_modal_paste_from_host": "从主机粘贴",
"paste_modal_paste_text": "粘贴文本",
"paste_modal_paste_text_description": "将文本从客户端粘贴到远程主机", "paste_modal_paste_text_description": "将文本从客户端粘贴到远程主机",
"paste_modal_paste_text": "粘贴文本",
"paste_modal_sending_using_layout": "使用键盘布局发送文本: {iso} - {name}", "paste_modal_sending_using_layout": "使用键盘布局发送文本: {iso} - {name}",
"peer_connection_closed": "关闭", "peer_connection_closed": "关闭",
"peer_connection_closing": "结束语", "peer_connection_closing": "结束语",
@ -698,20 +668,20 @@
"retry": "重试", "retry": "重试",
"saving": "保存…", "saving": "保存…",
"search_placeholder": "搜索…", "search_placeholder": "搜索…",
"serial_console": "串行控制台",
"serial_console_baud_rate": "波特率", "serial_console_baud_rate": "波特率",
"serial_console_configure_description": "配置串行控制台设置", "serial_console_configure_description": "配置串行控制台设置",
"serial_console_data_bits": "数据位", "serial_console_data_bits": "数据位",
"serial_console_get_settings_error": "无法获取串行控制台设置: {error}", "serial_console_get_settings_error": "无法获取串行控制台设置: {error}",
"serial_console_open_console": "打开控制台", "serial_console_open_console": "打开控制台",
"serial_console_parity": "奇偶校验位",
"serial_console_parity_even": "偶校验", "serial_console_parity_even": "偶校验",
"serial_console_parity_mark": "Mark", "serial_console_parity_mark": "Mark",
"serial_console_parity_none": "无", "serial_console_parity_none": "无",
"serial_console_parity_odd": "奇校验", "serial_console_parity_odd": "奇校验",
"serial_console_parity_space": "Space", "serial_console_parity_space": "Space",
"serial_console_parity": "奇偶校验位",
"serial_console_set_settings_error": "无法将串行控制台设置设置为 {settings} : {error}", "serial_console_set_settings_error": "无法将串行控制台设置设置为 {settings} : {error}",
"serial_console_stop_bits": "停止位", "serial_console_stop_bits": "停止位",
"serial_console": "串行控制台",
"setting_remote_description": "设置远程描述", "setting_remote_description": "设置远程描述",
"setting_remote_session_description": "正在设置远程会话描述...", "setting_remote_session_description": "正在设置远程会话描述...",
"setting_up_connection_to_device": "正在设置与设备的连接...", "setting_up_connection_to_device": "正在设置与设备的连接...",
@ -721,8 +691,8 @@
"settings_back_to_kvm": "返回 KVM", "settings_back_to_kvm": "返回 KVM",
"settings_general": "一般的", "settings_general": "一般的",
"settings_hardware": "硬件", "settings_hardware": "硬件",
"settings_keyboard": "键盘",
"settings_keyboard_macros": "键盘宏", "settings_keyboard_macros": "键盘宏",
"settings_keyboard": "键盘",
"settings_mouse": "老鼠", "settings_mouse": "老鼠",
"settings_network": "网络", "settings_network": "网络",
"settings_video": "视频", "settings_video": "视频",
@ -742,7 +712,6 @@
"updates_failed_check": "无法检查更新: {error}", "updates_failed_check": "无法检查更新: {error}",
"updates_failed_get_device_version": "无法获取设备版本: {error}", "updates_failed_get_device_version": "无法获取设备版本: {error}",
"updating_leave_device_on": "请不要关闭您的设备……", "updating_leave_device_on": "请不要关闭您的设备……",
"usb": "USB",
"usb_config_custom": "风俗", "usb_config_custom": "风俗",
"usb_config_default": "JetKVM 默认", "usb_config_default": "JetKVM 默认",
"usb_config_dell": "戴尔多媒体专业键盘", "usb_config_dell": "戴尔多媒体专业键盘",
@ -789,6 +758,7 @@
"usb_state_connecting": "正在连接", "usb_state_connecting": "正在连接",
"usb_state_disconnected": "断开连接", "usb_state_disconnected": "断开连接",
"usb_state_low_power_mode": "低功耗模式", "usb_state_low_power_mode": "低功耗模式",
"usb": "USB",
"user_interface_language_description": "选择 JetKVM 用户界面使用的语言", "user_interface_language_description": "选择 JetKVM 用户界面使用的语言",
"user_interface_language_title": "界面语言", "user_interface_language_title": "界面语言",
"video_brightness_description": "亮度级别( {value} x", "video_brightness_description": "亮度级别( {value} x",
@ -855,7 +825,6 @@
"video_title": "视频", "video_title": "视频",
"view_details": "查看详情", "view_details": "查看详情",
"virtual_keyboard_header": "虚拟键盘", "virtual_keyboard_header": "虚拟键盘",
"wake_on_lan": "局域网唤醒",
"wake_on_lan_add_device_device_name": "设备名称", "wake_on_lan_add_device_device_name": "设备名称",
"wake_on_lan_add_device_example_device_name": "Plex媒体服务器", "wake_on_lan_add_device_example_device_name": "Plex媒体服务器",
"wake_on_lan_add_device_mac_address": "MAC 地址", "wake_on_lan_add_device_mac_address": "MAC 地址",
@ -871,6 +840,7 @@
"wake_on_lan_failed_send_magic": "发送魔术包失败", "wake_on_lan_failed_send_magic": "发送魔术包失败",
"wake_on_lan_invalid_mac": "无效的 MAC 地址", "wake_on_lan_invalid_mac": "无效的 MAC 地址",
"wake_on_lan_magic_sent_success": "魔包发送成功", "wake_on_lan_magic_sent_success": "魔包发送成功",
"welcome_to_jetkvm": "欢迎来到 JetKVM", "wake_on_lan": "局域网唤醒",
"welcome_to_jetkvm_description": "远程控制任何计算机" "welcome_to_jetkvm_description": "远程控制任何计算机",
"welcome_to_jetkvm": "欢迎来到 JetKVM"
} }

245
ui/package-lock.json generated
View File

@ -28,7 +28,6 @@
"react": "^19.2.0", "react": "^19.2.0",
"react-animate-height": "^3.2.3", "react-animate-height": "^3.2.3",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"react-hook-form": "^7.65.0",
"react-hot-toast": "^2.6.0", "react-hot-toast": "^2.6.0",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-router": "^7.9.4", "react-router": "^7.9.4",
@ -79,7 +78,7 @@
"vite-tsconfig-paths": "^5.1.4" "vite-tsconfig-paths": "^5.1.4"
}, },
"engines": { "engines": {
"node": "^22.20.0" "node": "^22.15.0"
} }
}, },
"node_modules/@alloc/quick-lru": { "node_modules/@alloc/quick-lru": {
@ -356,9 +355,9 @@
} }
}, },
"node_modules/@esbuild/aix-ppc64": { "node_modules/@esbuild/aix-ppc64": {
"version": "0.25.11", "version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz",
"integrity": "sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==", "integrity": "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@ -372,9 +371,9 @@
} }
}, },
"node_modules/@esbuild/android-arm": { "node_modules/@esbuild/android-arm": {
"version": "0.25.11", "version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.11.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.10.tgz",
"integrity": "sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==", "integrity": "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -388,9 +387,9 @@
} }
}, },
"node_modules/@esbuild/android-arm64": { "node_modules/@esbuild/android-arm64": {
"version": "0.25.11", "version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.11.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.10.tgz",
"integrity": "sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==", "integrity": "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -404,9 +403,9 @@
} }
}, },
"node_modules/@esbuild/android-x64": { "node_modules/@esbuild/android-x64": {
"version": "0.25.11", "version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.11.tgz", "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.10.tgz",
"integrity": "sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==", "integrity": "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -420,9 +419,9 @@
} }
}, },
"node_modules/@esbuild/darwin-arm64": { "node_modules/@esbuild/darwin-arm64": {
"version": "0.25.11", "version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.11.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.10.tgz",
"integrity": "sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==", "integrity": "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -436,9 +435,9 @@
} }
}, },
"node_modules/@esbuild/darwin-x64": { "node_modules/@esbuild/darwin-x64": {
"version": "0.25.11", "version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.11.tgz", "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz",
"integrity": "sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==", "integrity": "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -452,9 +451,9 @@
} }
}, },
"node_modules/@esbuild/freebsd-arm64": { "node_modules/@esbuild/freebsd-arm64": {
"version": "0.25.11", "version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.11.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.10.tgz",
"integrity": "sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==", "integrity": "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -468,9 +467,9 @@
} }
}, },
"node_modules/@esbuild/freebsd-x64": { "node_modules/@esbuild/freebsd-x64": {
"version": "0.25.11", "version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.11.tgz", "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.10.tgz",
"integrity": "sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==", "integrity": "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -484,9 +483,9 @@
} }
}, },
"node_modules/@esbuild/linux-arm": { "node_modules/@esbuild/linux-arm": {
"version": "0.25.11", "version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.11.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.10.tgz",
"integrity": "sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==", "integrity": "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
@ -500,9 +499,9 @@
} }
}, },
"node_modules/@esbuild/linux-arm64": { "node_modules/@esbuild/linux-arm64": {
"version": "0.25.11", "version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.11.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.10.tgz",
"integrity": "sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==", "integrity": "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -516,9 +515,9 @@
} }
}, },
"node_modules/@esbuild/linux-ia32": { "node_modules/@esbuild/linux-ia32": {
"version": "0.25.11", "version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.11.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.10.tgz",
"integrity": "sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==", "integrity": "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@ -532,9 +531,9 @@
} }
}, },
"node_modules/@esbuild/linux-loong64": { "node_modules/@esbuild/linux-loong64": {
"version": "0.25.11", "version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.11.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.10.tgz",
"integrity": "sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==", "integrity": "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==",
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
@ -548,9 +547,9 @@
} }
}, },
"node_modules/@esbuild/linux-mips64el": { "node_modules/@esbuild/linux-mips64el": {
"version": "0.25.11", "version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.11.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.10.tgz",
"integrity": "sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==", "integrity": "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==",
"cpu": [ "cpu": [
"mips64el" "mips64el"
], ],
@ -564,9 +563,9 @@
} }
}, },
"node_modules/@esbuild/linux-ppc64": { "node_modules/@esbuild/linux-ppc64": {
"version": "0.25.11", "version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.11.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz",
"integrity": "sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==", "integrity": "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
@ -580,9 +579,9 @@
} }
}, },
"node_modules/@esbuild/linux-riscv64": { "node_modules/@esbuild/linux-riscv64": {
"version": "0.25.11", "version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.11.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz",
"integrity": "sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==", "integrity": "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
@ -596,9 +595,9 @@
} }
}, },
"node_modules/@esbuild/linux-s390x": { "node_modules/@esbuild/linux-s390x": {
"version": "0.25.11", "version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.11.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.10.tgz",
"integrity": "sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==", "integrity": "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
@ -612,9 +611,9 @@
} }
}, },
"node_modules/@esbuild/linux-x64": { "node_modules/@esbuild/linux-x64": {
"version": "0.25.11", "version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.11.tgz", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz",
"integrity": "sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==", "integrity": "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -628,9 +627,9 @@
} }
}, },
"node_modules/@esbuild/netbsd-arm64": { "node_modules/@esbuild/netbsd-arm64": {
"version": "0.25.11", "version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.11.tgz", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.10.tgz",
"integrity": "sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==", "integrity": "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -644,9 +643,9 @@
} }
}, },
"node_modules/@esbuild/netbsd-x64": { "node_modules/@esbuild/netbsd-x64": {
"version": "0.25.11", "version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.11.tgz", "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.10.tgz",
"integrity": "sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==", "integrity": "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -660,9 +659,9 @@
} }
}, },
"node_modules/@esbuild/openbsd-arm64": { "node_modules/@esbuild/openbsd-arm64": {
"version": "0.25.11", "version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.11.tgz", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.10.tgz",
"integrity": "sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==", "integrity": "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -676,9 +675,9 @@
} }
}, },
"node_modules/@esbuild/openbsd-x64": { "node_modules/@esbuild/openbsd-x64": {
"version": "0.25.11", "version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.11.tgz", "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.10.tgz",
"integrity": "sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==", "integrity": "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -692,9 +691,9 @@
} }
}, },
"node_modules/@esbuild/openharmony-arm64": { "node_modules/@esbuild/openharmony-arm64": {
"version": "0.25.11", "version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.11.tgz", "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.10.tgz",
"integrity": "sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==", "integrity": "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -708,9 +707,9 @@
} }
}, },
"node_modules/@esbuild/sunos-x64": { "node_modules/@esbuild/sunos-x64": {
"version": "0.25.11", "version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.11.tgz", "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz",
"integrity": "sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==", "integrity": "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -724,9 +723,9 @@
} }
}, },
"node_modules/@esbuild/win32-arm64": { "node_modules/@esbuild/win32-arm64": {
"version": "0.25.11", "version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.11.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.10.tgz",
"integrity": "sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==", "integrity": "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
@ -740,9 +739,9 @@
} }
}, },
"node_modules/@esbuild/win32-ia32": { "node_modules/@esbuild/win32-ia32": {
"version": "0.25.11", "version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.11.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.10.tgz",
"integrity": "sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==", "integrity": "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
@ -756,9 +755,9 @@
} }
}, },
"node_modules/@esbuild/win32-x64": { "node_modules/@esbuild/win32-x64": {
"version": "0.25.11", "version": "0.25.10",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.11.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz",
"integrity": "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==", "integrity": "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
@ -3686,9 +3685,9 @@
} }
}, },
"node_modules/electron-to-chromium": { "node_modules/electron-to-chromium": {
"version": "1.5.237", "version": "1.5.235",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.237.tgz", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.235.tgz",
"integrity": "sha512-icUt1NvfhGLar5lSWH3tHNzablaA5js3HVHacQimfP8ViEBOQv+L7DKEuHdbTZ0SKCO1ogTJTIL1Gwk9S6Qvcg==", "integrity": "sha512-i/7ntLFwOdoHY7sgjlTIDo4Sl8EdoTjWIaKinYOVfC6bOp71bmwenyZthWHcasxgHDNWbWxvG9M3Ia116zIaYQ==",
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
@ -3887,9 +3886,9 @@
] ]
}, },
"node_modules/esbuild": { "node_modules/esbuild": {
"version": "0.25.11", "version": "0.25.10",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.11.tgz", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz",
"integrity": "sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==", "integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==",
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"bin": { "bin": {
@ -3899,32 +3898,32 @@
"node": ">=18" "node": ">=18"
}, },
"optionalDependencies": { "optionalDependencies": {
"@esbuild/aix-ppc64": "0.25.11", "@esbuild/aix-ppc64": "0.25.10",
"@esbuild/android-arm": "0.25.11", "@esbuild/android-arm": "0.25.10",
"@esbuild/android-arm64": "0.25.11", "@esbuild/android-arm64": "0.25.10",
"@esbuild/android-x64": "0.25.11", "@esbuild/android-x64": "0.25.10",
"@esbuild/darwin-arm64": "0.25.11", "@esbuild/darwin-arm64": "0.25.10",
"@esbuild/darwin-x64": "0.25.11", "@esbuild/darwin-x64": "0.25.10",
"@esbuild/freebsd-arm64": "0.25.11", "@esbuild/freebsd-arm64": "0.25.10",
"@esbuild/freebsd-x64": "0.25.11", "@esbuild/freebsd-x64": "0.25.10",
"@esbuild/linux-arm": "0.25.11", "@esbuild/linux-arm": "0.25.10",
"@esbuild/linux-arm64": "0.25.11", "@esbuild/linux-arm64": "0.25.10",
"@esbuild/linux-ia32": "0.25.11", "@esbuild/linux-ia32": "0.25.10",
"@esbuild/linux-loong64": "0.25.11", "@esbuild/linux-loong64": "0.25.10",
"@esbuild/linux-mips64el": "0.25.11", "@esbuild/linux-mips64el": "0.25.10",
"@esbuild/linux-ppc64": "0.25.11", "@esbuild/linux-ppc64": "0.25.10",
"@esbuild/linux-riscv64": "0.25.11", "@esbuild/linux-riscv64": "0.25.10",
"@esbuild/linux-s390x": "0.25.11", "@esbuild/linux-s390x": "0.25.10",
"@esbuild/linux-x64": "0.25.11", "@esbuild/linux-x64": "0.25.10",
"@esbuild/netbsd-arm64": "0.25.11", "@esbuild/netbsd-arm64": "0.25.10",
"@esbuild/netbsd-x64": "0.25.11", "@esbuild/netbsd-x64": "0.25.10",
"@esbuild/openbsd-arm64": "0.25.11", "@esbuild/openbsd-arm64": "0.25.10",
"@esbuild/openbsd-x64": "0.25.11", "@esbuild/openbsd-x64": "0.25.10",
"@esbuild/openharmony-arm64": "0.25.11", "@esbuild/openharmony-arm64": "0.25.10",
"@esbuild/sunos-x64": "0.25.11", "@esbuild/sunos-x64": "0.25.10",
"@esbuild/win32-arm64": "0.25.11", "@esbuild/win32-arm64": "0.25.10",
"@esbuild/win32-ia32": "0.25.11", "@esbuild/win32-ia32": "0.25.10",
"@esbuild/win32-x64": "0.25.11" "@esbuild/win32-x64": "0.25.10"
} }
}, },
"node_modules/esbuild-wasm": { "node_modules/esbuild-wasm": {
@ -4232,9 +4231,9 @@
} }
}, },
"node_modules/eslint-plugin-react-refresh": { "node_modules/eslint-plugin-react-refresh": {
"version": "0.4.24", "version": "0.4.23",
"resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.24.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.23.tgz",
"integrity": "sha512-nLHIW7TEq3aLrEYWpVaJ1dRgFR+wLDPN8e8FpYAql/bMV2oBEfC37K0gLEGgv9fy66juNShSMV8OkTqzltcG/w==", "integrity": "sha512-G4j+rv0NmbIR45kni5xJOrYvCtyD3/7LjpVH8MPPcudXDcNu8gv+4ATTDXTtbRR8rTCM5HxECvCSsRmxKnWDsA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peerDependencies": { "peerDependencies": {
@ -6477,22 +6476,6 @@
"react": "^19.2.0" "react": "^19.2.0"
} }
}, },
"node_modules/react-hook-form": {
"version": "7.65.0",
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.65.0.tgz",
"integrity": "sha512-xtOzDz063WcXvGWaHgLNrNzlsdFgtUWcb32E6WFaGTd7kPZG3EeDusjdZfUsPwKCKVXy1ZlntifaHZ4l8pAsmw==",
"license": "MIT",
"engines": {
"node": ">=18.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/react-hook-form"
},
"peerDependencies": {
"react": "^16.8.0 || ^17 || ^18 || ^19"
}
},
"node_modules/react-hot-toast": { "node_modules/react-hot-toast": {
"version": "2.6.0", "version": "2.6.0",
"resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.6.0.tgz", "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.6.0.tgz",

View File

@ -4,7 +4,7 @@
"version": "2025.10.14.2130", "version": "2025.10.14.2130",
"type": "module", "type": "module",
"engines": { "engines": {
"node": "^22.20.0" "node": "^22.15.0"
}, },
"scripts": { "scripts": {
"dev": "./dev_device.sh", "dev": "./dev_device.sh",
@ -42,7 +42,6 @@
"react-animate-height": "^3.2.3", "react-animate-height": "^3.2.3",
"react-dom": "^19.2.0", "react-dom": "^19.2.0",
"react-hot-toast": "^2.6.0", "react-hot-toast": "^2.6.0",
"react-hook-form": "^7.65.0",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"react-router": "^7.9.4", "react-router": "^7.9.4",
"react-simple-keyboard": "^3.8.130", "react-simple-keyboard": "^3.8.130",

View File

@ -1,5 +1,8 @@
import { CloseButton } from "@headlessui/react"; import {
import { LuCircleAlert, LuInfo, LuTriangleAlert } from "react-icons/lu"; CheckCircleIcon,
ExclamationTriangleIcon,
InformationCircleIcon,
} from "@heroicons/react/24/outline";
import { m } from "@localizations/messages.js"; import { m } from "@localizations/messages.js";
import { Button } from "@components/Button"; import { Button } from "@components/Button";
@ -22,23 +25,27 @@ interface ConfirmDialogProps {
const variantConfig = { const variantConfig = {
danger: { danger: {
icon: LuCircleAlert, icon: ExclamationTriangleIcon,
iconClass: "text-red-600 dark:text-red-400", iconClass: "text-red-600",
iconBgClass: "bg-red-100",
buttonTheme: "danger", buttonTheme: "danger",
}, },
success: { success: {
icon: LuCircleAlert, icon: CheckCircleIcon,
iconClass: "text-emerald-600 dark:text-emerald-400", iconClass: "text-green-600",
iconBgClass: "bg-green-100",
buttonTheme: "primary", buttonTheme: "primary",
}, },
warning: { warning: {
icon: LuTriangleAlert, icon: ExclamationTriangleIcon,
iconClass: "text-amber-600 dark:text-amber-400", iconClass: "text-yellow-600",
buttonTheme: "primary", iconBgClass: "bg-yellow-100",
buttonTheme: "lightDanger",
}, },
info: { info: {
icon: LuInfo, icon: InformationCircleIcon,
iconClass: "text-slate-700 dark:text-slate-300", iconClass: "text-blue-600",
iconBgClass: "bg-blue-100",
buttonTheme: "primary", buttonTheme: "primary",
}, },
} as Record< } as Record<
@ -46,6 +53,7 @@ const variantConfig = {
{ {
icon: React.ElementType; icon: React.ElementType;
iconClass: string; iconClass: string;
iconBgClass: string;
buttonTheme: "danger" | "primary" | "blank" | "light" | "lightDanger"; buttonTheme: "danger" | "primary" | "blank" | "light" | "lightDanger";
} }
>; >;
@ -61,40 +69,38 @@ export function ConfirmDialog({
onConfirm, onConfirm,
isConfirming = false, isConfirming = false,
}: ConfirmDialogProps) { }: ConfirmDialogProps) {
const { icon: Icon, iconClass, buttonTheme } = variantConfig[variant]; const { icon: Icon, iconClass, iconBgClass, buttonTheme } = variantConfig[variant];
const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
if (e.key === "Escape") {
e.stopPropagation();
onClose();
}
};
return ( return (
<div onKeyDown={handleKeyDown}>
<Modal open={open} onClose={onClose}> <Modal open={open} onClose={onClose}>
<div className="mx-auto max-w-md px-4 transition-all duration-300 ease-in-out"> <div className="mx-auto max-w-xl px-4 transition-all duration-300 ease-in-out">
<div className="pointer-events-auto relative w-full overflow-hidden rounded-lg border border-slate-200 bg-white shadow-sm transition-all dark:border-slate-800 dark:bg-slate-900"> <div className="pointer-events-auto relative w-full overflow-hidden rounded-lg bg-white p-6 text-left align-middle shadow-xl transition-all dark:bg-slate-800">
<div className="p-6"> <div className="space-y-4">
<div className="flex items-start gap-3.5"> <div className="sm:flex sm:items-start">
<Icon aria-hidden="true" className={cx("size-[18px] shrink-0 mt-[2px]", iconClass)} /> <div
<div className="flex-1 min-w-0 space-y-2"> className={cx(
<h2 className="font-semibold text-slate-950 dark:text-white"> "mx-auto flex size-12 shrink-0 items-center justify-center rounded-full sm:mx-0 sm:size-10",
iconBgClass,
)}
>
<Icon aria-hidden="true" className={cx("size-6", iconClass)} />
</div>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h2 className="text-lg leading-tight font-bold text-black dark:text-white">
{title} {title}
</h2> </h2>
<div className="text-sm text-slate-700 dark:text-slate-300"> <div className="mt-2 text-sm leading-snug text-slate-600 dark:text-slate-400">
{description} {description}
</div> </div>
</div> </div>
</div> </div>
<div className="mt-6 flex justify-end gap-2"> <div className="flex justify-end gap-x-2">
{cancelText && ( {cancelText && (
<CloseButton as={Button} size="SM" theme="blank" text={cancelText} /> <Button size="SM" theme="blank" text={cancelText} onClick={onClose} />
)} )}
<Button <Button
size="SM" size="SM"
type="button"
theme={buttonTheme} theme={buttonTheme}
text={isConfirming ? `${confirmText}...` : confirmText} text={isConfirming ? `${confirmText}...` : confirmText}
onClick={onConfirm} onClick={onConfirm}
@ -105,6 +111,5 @@ export function ConfirmDialog({
</div> </div>
</div> </div>
</Modal> </Modal>
</div>
); );
} }

View File

@ -6,48 +6,21 @@ import { LifeTimeLabel } from "@routes/devices.$id.settings.network";
import { NetworkState } from "@hooks/stores"; import { NetworkState } from "@hooks/stores";
import { m } from "@localizations/messages.js"; import { m } from "@localizations/messages.js";
import EmptyCard from "./EmptyCard";
export default function DhcpLeaseCard({ export default function DhcpLeaseCard({
networkState, networkState,
setShowRenewLeaseConfirm, setShowRenewLeaseConfirm,
}: { }: {
networkState: NetworkState | null; networkState: NetworkState;
setShowRenewLeaseConfirm: (show: boolean) => void; setShowRenewLeaseConfirm: (show: boolean) => void;
}) { }) {
const isDhcpLeaseEmpty = Object.keys(networkState?.dhcp_lease || {}).length === 0;
if (isDhcpLeaseEmpty) {
return (
<EmptyCard
headline="No DHCP Lease information"
description="We haven't received any DHCP lease information from the device yet."
/>
);
}
return ( return (
<GridCard> <GridCard>
<div className="animate-fadeIn p-4 text-black opacity-0 animation-duration-500 dark:text-white"> <div className="animate-fadeIn p-4 opacity-0 animation-duration-500 text-black dark:text-white">
<div className="space-y-3"> <div className="space-y-3">
<div className="flex items-center justify-between">
<h3 className="text-base font-bold text-slate-900 dark:text-white"> <h3 className="text-base font-bold text-slate-900 dark:text-white">
{m.dhcp_lease_header()} {m.dhcp_lease_header()}
</h3> </h3>
<div>
<Button
size="XS"
theme="light"
type="button"
className="text-red-500"
text="Renew DHCP Lease"
LeadingIcon={LuRefreshCcw}
onClick={() => setShowRenewLeaseConfirm(true)}
/>
</div>
</div>
<div className="flex gap-x-6 gap-y-2"> <div className="flex gap-x-6 gap-y-2">
<div className="flex-1 space-y-2"> <div className="flex-1 space-y-2">
{networkState?.dhcp_lease?.ip && ( {networkState?.dhcp_lease?.ip && (
@ -72,15 +45,13 @@ export default function DhcpLeaseCard({
</div> </div>
)} )}
{networkState?.dhcp_lease?.dns_servers && ( {networkState?.dhcp_lease?.dns && (
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20"> <div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
<span className="text-sm text-slate-600 dark:text-slate-400"> <span className="text-sm text-slate-600 dark:text-slate-400">
{m.dns_servers()} {m.dns_servers()}
</span> </span>
<span className="text-right text-sm font-medium"> <span className="text-right text-sm font-medium">
{networkState?.dhcp_lease?.dns_servers.map(dns => ( {networkState?.dhcp_lease?.dns.map(dns => <div key={dns}>{dns}</div>)}
<div key={dns}>{dns}</div>
))}
</span> </span>
</div> </div>
)} )}
@ -172,17 +143,6 @@ export default function DhcpLeaseCard({
</div> </div>
)} )}
{networkState?.dhcp_lease?.broadcast && (
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
<span className="text-sm text-slate-600 dark:text-slate-400">
Broadcast
</span>
<span className="text-sm font-medium">
{networkState?.dhcp_lease?.broadcast}
</span>
</div>
)}
{networkState?.dhcp_lease?.mtu && ( {networkState?.dhcp_lease?.mtu && (
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20"> <div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
<span className="text-sm text-slate-600 dark:text-slate-400">MTU</span> <span className="text-sm text-slate-600 dark:text-slate-400">MTU</span>
@ -233,14 +193,17 @@ export default function DhcpLeaseCard({
</span> </span>
</div> </div>
)} )}
{networkState?.dhcp_lease?.dhcp_client && (
<div className="flex justify-between border-t border-slate-800/10 pt-2 dark:border-slate-300/20">
<span className="text-sm text-slate-600 dark:text-slate-400">DHCP Client</span>
<span className="text-sm font-medium">{networkState?.dhcp_lease?.dhcp_client}</span>
</div> </div>
)}
</div> </div>
<div>
<Button
size="SM"
theme="light"
className="text-red-500"
text={m.dhcp_lease_renew()}
LeadingIcon={LuRefreshCcw}
onClick={() => setShowRenewLeaseConfirm(true)}
/>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,25 +1,12 @@
import { cx } from "@/cva.config";
import { NetworkState } from "@hooks/stores"; import { NetworkState } from "@hooks/stores";
import { GridCard } from "@components/Card"; import { GridCard } from "@components/Card";
import { LifeTimeLabel } from "@routes/devices.$id.settings.network"; import { LifeTimeLabel } from "@routes/devices.$id.settings.network";
import { m } from "@localizations/messages.js"; import { m } from "@localizations/messages.js";
export function FlagLabel({ flag, className }: { flag: string, className?: string }) {
const classes = cx(
"ml-2 rounded-sm bg-red-500 px-2 py-1 text-[10px] font-medium leading-none text-white dark:border",
"bg-red-500 text-white dark:border-red-700 dark:bg-red-800 dark:text-red-50",
className,
);
return <span className={classes}>
{flag}
</span>
}
export default function Ipv6NetworkCard({ export default function Ipv6NetworkCard({
networkState, networkState,
}: { }: {
networkState: NetworkState | undefined; networkState: NetworkState;
}) { }) {
return ( return (
<GridCard> <GridCard>
@ -30,6 +17,7 @@ export default function Ipv6NetworkCard({
</h3> </h3>
<div className="grid grid-cols-2 gap-x-6 gap-y-2"> <div className="grid grid-cols-2 gap-x-6 gap-y-2">
{networkState?.ipv6_link_local && (
<div className="flex flex-col justify-between"> <div className="flex flex-col justify-between">
<span className="text-sm text-slate-600 dark:text-slate-400"> <span className="text-sm text-slate-600 dark:text-slate-400">
{m.ipv6_link_local()} {m.ipv6_link_local()}
@ -38,22 +26,15 @@ export default function Ipv6NetworkCard({
{networkState?.ipv6_link_local} {networkState?.ipv6_link_local}
</span> </span>
</div> </div>
)}
<div className="flex flex-col justify-between">
<span className="text-sm text-slate-600 dark:text-slate-400">
{m.ipv6_gateway()}
</span>
<span className="text-sm font-medium">
{networkState?.ipv6_gateway}
</span>
</div>
</div> </div>
<div className="space-y-3 pt-2"> <div className="space-y-3 pt-2">
{networkState?.ipv6_addresses && networkState?.ipv6_addresses.length > 0 && ( {networkState?.ipv6_addresses && networkState?.ipv6_addresses.length > 0 && (
<div className="space-y-3"> <div className="space-y-3">
<h4 className="text-sm font-semibold">IPv6 Addresses</h4> <h4 className="text-sm font-semibold">IPv6 Addresses</h4>
{networkState.ipv6_addresses.map(addr => ( {networkState.ipv6_addresses.map(
addr => (
<div <div
key={addr.address} key={addr.address}
className="rounded-md rounded-l-none border border-slate-500/10 border-l-blue-700/50 bg-white p-4 pl-4 backdrop-blur-sm dark:bg-transparent" className="rounded-md rounded-l-none border border-slate-500/10 border-l-blue-700/50 bg-white p-4 pl-4 backdrop-blur-sm dark:bg-transparent"
@ -63,13 +44,7 @@ export default function Ipv6NetworkCard({
<span className="text-sm text-slate-600 dark:text-slate-400"> <span className="text-sm text-slate-600 dark:text-slate-400">
{m.ipv6_address_label()} {m.ipv6_address_label()}
</span> </span>
<span className="text-sm font-medium flex"> <span className="text-sm font-medium">{addr.address}</span>
<span className="flex-1">{addr.address}</span>
<span className="text-sm font-medium flex gap-x-1">
{addr.flag_deprecated ? <FlagLabel flag="Deprecated" /> : null}
{addr.flag_dad_failed ? <FlagLabel flag="DAD Failed" /> : null}
</span>
</span>
</div> </div>
{addr.valid_lifetime && ( {addr.valid_lifetime && (
@ -80,7 +55,7 @@ export default function Ipv6NetworkCard({
<span className="text-sm font-medium"> <span className="text-sm font-medium">
{addr.valid_lifetime === "" ? ( {addr.valid_lifetime === "" ? (
<span className="text-slate-400 dark:text-slate-600"> <span className="text-slate-400 dark:text-slate-600">
{m.not_applicable()} {m.not_available()}
</span> </span>
) : ( ) : (
<LifeTimeLabel lifetime={`${addr.valid_lifetime}`} /> <LifeTimeLabel lifetime={`${addr.valid_lifetime}`} />
@ -88,7 +63,6 @@ export default function Ipv6NetworkCard({
</span> </span>
</div> </div>
)} )}
{addr.preferred_lifetime && ( {addr.preferred_lifetime && (
<div className="flex flex-col justify-between"> <div className="flex flex-col justify-between">
<span className="text-sm text-slate-600 dark:text-slate-400"> <span className="text-sm text-slate-600 dark:text-slate-400">
@ -97,7 +71,7 @@ export default function Ipv6NetworkCard({
<span className="text-sm font-medium"> <span className="text-sm font-medium">
{addr.preferred_lifetime === "" ? ( {addr.preferred_lifetime === "" ? (
<span className="text-slate-400 dark:text-slate-600"> <span className="text-slate-400 dark:text-slate-600">
{m.not_applicable()} {m.not_available()}
</span> </span>
) : ( ) : (
<LifeTimeLabel lifetime={`${addr.preferred_lifetime}`} /> <LifeTimeLabel lifetime={`${addr.preferred_lifetime}`} />
@ -107,7 +81,8 @@ export default function Ipv6NetworkCard({
)} )}
</div> </div>
</div> </div>
))} ),
)}
</div> </div>
)} )}
</div> </div>

View File

@ -40,7 +40,7 @@ const basePresetDelays = [
]; ];
const PRESET_DELAYS = basePresetDelays.map(delay => { const PRESET_DELAYS = basePresetDelays.map(delay => {
if (Number.parseInt(delay.value, 10) === DEFAULT_DELAY) { if (parseInt(delay.value, 10) === DEFAULT_DELAY) {
return { ...delay, label: "Default" }; return { ...delay, label: "Default" };
} }
return delay; return delay;

View File

@ -3,19 +3,14 @@ import { ReactNode } from "react";
export function SettingsPageHeader({ export function SettingsPageHeader({
title, title,
description, description,
action,
}: { }: {
title: string | ReactNode; title: string | ReactNode;
description: string | ReactNode; description: string | ReactNode;
action?: ReactNode;
}) { }) {
return ( return (
<div className="flex items-center justify-between gap-x-2 select-none"> <div className="select-none">
<div className="flex flex-col gap-y-1"> <h2 className=" text-xl font-extrabold text-black dark:text-white">{title}</h2>
<h2 className="text-xl font-extrabold text-black dark:text-white">{title}</h2>
<div className="text-sm text-black dark:text-slate-300">{description}</div> <div className="text-sm text-black dark:text-slate-300">{description}</div>
</div> </div>
{action && <div className="">{action}</div>}
</div>
); );
} }

View File

@ -1,137 +0,0 @@
import { LuPlus, LuX } from "react-icons/lu";
import { useFieldArray, useFormContext } from "react-hook-form";
import { useEffect } from "react";
import validator from "validator";
import { cx } from "cva";
import { GridCard } from "@/components/Card";
import { Button } from "@/components/Button";
import { InputFieldWithLabel } from "@/components/InputField";
import { NetworkSettings } from "@/hooks/stores";
import { netMaskFromCidr4 } from "@/utils/ip";
export default function StaticIpv4Card() {
const formMethods = useFormContext<NetworkSettings>();
const { register, formState, watch, setValue } = formMethods;
const { fields, append, remove } = useFieldArray({ name: "ipv4_static.dns" });
useEffect(() => {
if (fields.length === 0) append("");
}, [append, fields.length]);
const dns = watch("ipv4_static.dns");
const ipv4StaticAddress = watch("ipv4_static.address");
const hideSubnetMask = ipv4StaticAddress?.includes("/");
useEffect(() => {
const parts = ipv4StaticAddress?.split("/", 2);
if (parts?.length !== 2) return;
const cidrNotation = parseInt(parts?.[1] ?? "");
if (isNaN(cidrNotation) || cidrNotation < 0 || cidrNotation > 32) return;
const mask = netMaskFromCidr4(cidrNotation);
setValue("ipv4_static.netmask", mask);
}, [ipv4StaticAddress, setValue]);
const validate = (value: string) => {
if (!validator.isIP(value)) return "Invalid IP address";
return true;
};
const validateIsIPOrCIDR4 = (value: string) => {
if (!validator.isIP(value, 4) && !validator.isIPRange(value, 4)) return "Invalid IP address or CIDR notation";
return true;
};
return (
<GridCard>
<div className="animate-fadeIn p-4 text-black opacity-0 animation-duration-500 dark:text-white">
<div className="space-y-4">
<h3 className="text-base font-bold text-slate-900 dark:text-white">
Static IPv4 Configuration
</h3>
<div className={cx("grid grid-cols-1 gap-4", hideSubnetMask ? "md:grid-cols-1" : "md:grid-cols-2")}>
<InputFieldWithLabel
label="IP Address"
type="text"
size="SM"
placeholder="192.168.1.100"
{
...register("ipv4_static.address", {
validate: (value: string | undefined) => validateIsIPOrCIDR4(value ?? "")
})}
error={formState.errors.ipv4_static?.address?.message}
/>
{!hideSubnetMask && <InputFieldWithLabel
label="Subnet Mask"
type="text"
size="SM"
placeholder="255.255.255.0"
{...register("ipv4_static.netmask", { validate: (value: string | undefined) => validate(value ?? "") })}
error={formState.errors.ipv4_static?.netmask?.message}
/>}
</div>
<InputFieldWithLabel
label="Gateway"
type="text"
size="SM"
placeholder="192.168.1.1"
{...register("ipv4_static.gateway", { validate: (value: string | undefined) => validate(value ?? "") })}
error={formState.errors.ipv4_static?.gateway?.message}
/>
{/* DNS server fields */}
<div className="space-y-4">
{fields.map((dns, index) => {
return (
<div key={dns.id}>
<div className="flex items-start gap-x-2">
<div className="flex-1">
<InputFieldWithLabel
label={index === 0 ? "DNS Server" : null}
type="text"
size="SM"
placeholder="1.1.1.1"
{...register(
`ipv4_static.dns.${index}`,
{ validate: (value: string | undefined) => validate(value ?? "") }
)}
error={formState.errors.ipv4_static?.dns?.[index]?.message}
/>
</div>
{index > 0 && (
<div className="flex-shrink-0">
<Button
size="SM"
theme="light"
type="button"
onClick={() => remove(index)}
LeadingIcon={LuX}
/>
</div>
)}
</div>
</div>
);
})}
</div>
<Button
size="SM"
theme="light"
onClick={() => append("", { shouldFocus: true })}
LeadingIcon={LuPlus}
type="button"
text="Add DNS Server"
disabled={dns?.[0] === ""}
/>
</div>
</div>
</GridCard>
);
}

View File

@ -1,117 +0,0 @@
import { LuPlus, LuX } from "react-icons/lu";
import { useFieldArray, useFormContext } from "react-hook-form";
import validator from "validator";
import { useEffect } from "react";
import { GridCard } from "@/components/Card";
import { Button } from "@/components/Button";
import { InputFieldWithLabel } from "@/components/InputField";
import { NetworkSettings } from "@/hooks/stores";
export default function StaticIpv6Card() {
const formMethods = useFormContext<NetworkSettings>();
const { register, formState, watch } = formMethods;
const { fields, append, remove } = useFieldArray({ name: "ipv6_static.dns" });
useEffect(() => {
if (fields.length === 0) append("");
}, [append, fields.length]);
const dns = watch("ipv6_static.dns");
const cidrValidation = (value: string) => {
if (value === "") return true;
// Check if it's a valid IPv6 address with CIDR notation
const parts = value.split("/");
if (parts.length !== 2) return "Please use CIDR notation (e.g., 2001:db8::1/64)";
const [address, prefix] = parts;
if (!validator.isIP(address, 6)) return "Invalid IPv6 address";
const prefixNum = parseInt(prefix);
if (isNaN(prefixNum) || prefixNum < 0 || prefixNum > 128) {
return "Prefix must be between 0 and 128";
}
return true;
};
const ipv6Validation = (value: string) => {
if (!validator.isIP(value, 6)) return "Invalid IPv6 address";
return true;
};
return (
<GridCard>
<div className="animate-fadeIn p-4 text-black opacity-0 animation-duration-500 dark:text-white">
<div className="space-y-4">
<h3 className="text-base font-bold text-slate-900 dark:text-white">
Static IPv6 Configuration
</h3>
<InputFieldWithLabel
label="IP Prefix"
type="text"
size="SM"
placeholder="2001:db8::1/64"
{...register("ipv6_static.prefix", { validate: (value: string | undefined) => cidrValidation(value ?? "") })}
error={formState.errors.ipv6_static?.prefix?.message}
/>
<InputFieldWithLabel
label="Gateway"
type="text"
size="SM"
placeholder="2001:db8::1"
{...register("ipv6_static.gateway", { validate: (value: string | undefined) => ipv6Validation(value ?? "") })}
error={formState.errors.ipv6_static?.gateway?.message}
/>
{/* DNS server fields */}
<div className="space-y-4">
{fields.map((dns, index) => {
return (
<div key={dns.id}>
<div className="flex items-start gap-x-2">
<div className="flex-1">
<InputFieldWithLabel
label={index === 0 ? "DNS Server" : null}
type="text"
size="SM"
placeholder="2001:4860:4860::8888"
{...register(`ipv6_static.dns.${index}`, { validate: (value: string | undefined) => ipv6Validation(value ?? "") })}
error={formState.errors.ipv6_static?.dns?.[index]?.message}
/>
</div>
{index > 0 && (
<div className="flex-shrink-0">
<Button
size="SM"
theme="light"
type="button"
onClick={() => remove(index)}
LeadingIcon={LuX}
/>
</div>
)}
</div>
</div>
);
})}
</div>
<Button
size="SM"
theme="light"
onClick={() => append("", { shouldFocus: true })}
LeadingIcon={LuPlus}
type="button"
text="Add DNS Server"
disabled={dns?.[0] === ""}
/>
</div>
</div>
</GridCard>
);
}

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState, useRef } from "react"; import React from "react";
import { ExclamationTriangleIcon } from "@heroicons/react/24/solid"; import { ExclamationTriangleIcon } from "@heroicons/react/24/solid";
import { ArrowPathIcon, ArrowRightIcon } from "@heroicons/react/16/solid"; import { ArrowPathIcon, ArrowRightIcon } from "@heroicons/react/16/solid";
import { motion, AnimatePresence } from "framer-motion"; import { motion, AnimatePresence } from "framer-motion";
@ -9,11 +9,6 @@ import { m } from "@localizations/messages.js";
import { Button, LinkButton } from "@components/Button"; import { Button, LinkButton } from "@components/Button";
import LoadingSpinner from "@components/LoadingSpinner"; import LoadingSpinner from "@components/LoadingSpinner";
import Card, { GridCard } from "@components/Card"; import Card, { GridCard } from "@components/Card";
import { useRTCStore, PostRebootAction } from "@/hooks/stores";
import LogoBlue from "@/assets/logo-blue.svg";
import LogoWhite from "@/assets/logo-white.svg";
import { isOnDevice } from "@/main";
interface OverlayContentProps { interface OverlayContentProps {
readonly children: React.ReactNode; readonly children: React.ReactNode;
@ -393,184 +388,3 @@ export function PointerLockBar({ show }: PointerLockBarProps) {
</AnimatePresence> </AnimatePresence>
); );
} }
interface RebootingOverlayProps {
readonly show: boolean;
readonly postRebootAction: PostRebootAction;
}
export function RebootingOverlay({ show, postRebootAction }: RebootingOverlayProps) {
const { peerConnectionState } = useRTCStore();
// Check if we've already seen the connection drop (confirms reboot actually started)
const [hasSeenDisconnect, setHasSeenDisconnect] = useState(
['disconnected', 'closed', 'failed'].includes(peerConnectionState ?? '')
);
// Track if we've timed out
const [hasTimedOut, setHasTimedOut] = useState(false);
// Monitor for disconnect after reboot is initiated
useEffect(() => {
if (!show) return;
if (hasSeenDisconnect) return;
if (['disconnected', 'closed', 'failed'].includes(peerConnectionState ?? '')) {
console.log('hasSeenDisconnect', hasSeenDisconnect);
setHasSeenDisconnect(true);
}
}, [show, peerConnectionState, hasSeenDisconnect]);
// Set timeout after 30 seconds
useEffect(() => {
if (!show) {
setHasTimedOut(false);
return;
}
const timeoutId = setTimeout(() => {
setHasTimedOut(true);
}, 30 * 1000);
return () => {
clearTimeout(timeoutId);
};
}, [show]);
// Poll suggested IP in device mode to detect when it's available
const abortControllerRef = useRef<AbortController | null>(null);
const isFetchingRef = useRef(false);
useEffect(() => {
// Only run in device mode with a postRebootAction
if (!isOnDevice || !postRebootAction || !show || !hasSeenDisconnect) {
return;
}
const checkPostRebootHealth = async () => {
// Don't start a new fetch if one is already in progress
if (isFetchingRef.current) {
return;
}
// Cancel any pending fetch
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
// Create new abort controller for this fetch
const abortController = new AbortController();
abortControllerRef.current = abortController;
isFetchingRef.current = true;
console.log('Checking post-reboot health endpoint:', postRebootAction.healthCheck);
const timeoutId = window.setTimeout(() => abortController.abort(), 2000);
try {
const response = await fetch(
postRebootAction.healthCheck,
{ signal: abortController.signal, }
);
if (response.ok) {
// Device is available, redirect to the specified URL
console.log('Device is available, redirecting to:', postRebootAction.redirectUrl);
window.location.href = postRebootAction.redirectUrl;
}
} catch (err) {
// Ignore errors - they're expected while device is rebooting
// Only log if it's not an abort error
if (err instanceof Error && err.name !== 'AbortError') {
console.debug('Error checking post-reboot health:', err);
}
} finally {
clearTimeout(timeoutId);
isFetchingRef.current = false;
}
};
// Start interval (check every 2 seconds)
const intervalId = setInterval(checkPostRebootHealth, 2000);
// Also check immediately
checkPostRebootHealth();
// Cleanup on unmount or when dependencies change
return () => {
clearInterval(intervalId);
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
isFetchingRef.current = false;
};
}, [show, postRebootAction, hasTimedOut, hasSeenDisconnect]);
return (
<AnimatePresence>
{show && (
<motion.div
className="aspect-video h-full w-full"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0, transition: { duration: 0 } }}
transition={{
duration: 0.4,
ease: "easeInOut",
}}
>
<OverlayContent>
<div className="flex flex-col items-start gap-y-4 w-full max-w-md">
<div className="h-[24px]">
<img src={LogoBlue} alt="" className="h-full dark:hidden" />
<img src={LogoWhite} alt="" className="hidden h-full dark:block" />
</div>
<div className="text-left text-sm text-slate-700 dark:text-slate-300">
<div className="space-y-4">
<div className="space-y-2 text-black dark:text-white">
<h2 className="text-xl font-bold">{hasTimedOut ? "Unable to Reconnect" : "Device is Rebooting"}</h2>
<p className="text-sm text-slate-700 dark:text-slate-300">
{hasTimedOut ? (
<>
The device may have restarted with a different IP address. Check the JetKVM&apos;s physical display to find the current IP address and reconnect.
</>
) : (
<>
Please wait while the device restarts. This usually takes 20-30 seconds.
</>
)}
</p>
</div>
<div className="flex items-center gap-x-2">
<Card>
<div className="flex items-center gap-x-2 p-4">
{!hasTimedOut ? (
<>
<LoadingSpinner className="h-4 w-4 text-blue-800 dark:text-blue-200" />
<p className="text-sm text-slate-700 dark:text-slate-300">
Waiting for device to restart...
</p>
</>
) : (
<div className="flex flex-col gap-y-2">
<div className="flex items-center gap-x-2">
<ExclamationTriangleIcon className="h-5 w-5 text-yellow-500" />
<p className="text-sm text-black dark:text-white">
Automatic Reconnection Timed Out
</p>
</div>
</div>
)}
</div>
</Card>
</div>
</div>
</div>
</div>
</OverlayContent>
</motion.div>
)}
</AnimatePresence>
);
}

View File

@ -23,7 +23,7 @@ import { keys } from "@/keyboardMappings";
import notifications from "@/notifications"; import notifications from "@/notifications";
import { m } from "@localizations/messages.js"; import { m } from "@localizations/messages.js";
export default function WebRTCVideo({ hasConnectionIssues }: { hasConnectionIssues: boolean }) { export default function WebRTCVideo() {
// Video and stream related refs and states // Video and stream related refs and states
const videoElm = useRef<HTMLVideoElement>(null); const videoElm = useRef<HTMLVideoElement>(null);
const { mediaStream, peerConnectionState } = useRTCStore(); const { mediaStream, peerConnectionState } = useRTCStore();
@ -528,10 +528,9 @@ export default function WebRTCVideo({ hasConnectionIssues }: { hasConnectionIssu
"max-h-full min-h-[384px] max-w-full min-w-[512px] bg-black/50 object-contain transition-all duration-1000", "max-h-full min-h-[384px] max-w-full min-w-[512px] bg-black/50 object-contain transition-all duration-1000",
{ {
"cursor-none": settings.isCursorHidden, "cursor-none": settings.isCursorHidden,
"!opacity-0": "opacity-0":
isVideoLoading || isVideoLoading ||
hdmiError || hdmiError ||
hasConnectionIssues ||
peerConnectionState !== "connected", peerConnectionState !== "connected",
"opacity-60!": showPointerLockBar, "opacity-60!": showPointerLockBar,
"animate-slideUpFade border border-slate-800/30 shadow-xs dark:border-slate-300/20": "animate-slideUpFade border border-slate-800/30 shadow-xs dark:border-slate-300/20":
@ -539,7 +538,7 @@ export default function WebRTCVideo({ hasConnectionIssues }: { hasConnectionIssu
}, },
)} )}
/> />
{peerConnection?.connectionState == "connected" && !hasConnectionIssues && ( {peerConnection?.connectionState == "connected" && (
<div <div
style={{ animationDuration: "500ms" }} style={{ animationDuration: "500ms" }}
className="animate-slideUpFade pointer-events-none absolute inset-0 flex items-center justify-center" className="animate-slideUpFade pointer-events-none absolute inset-0 flex items-center justify-center"

View File

@ -1,49 +0,0 @@
import { useCallback, useState } from "react";
export function useCopyToClipboard(resetInterval = 2000) {
const [isCopied, setIsCopied] = useState(false);
const copy = useCallback(async (text: string) => {
if (!text) return false;
let success = false;
if (navigator.clipboard && window.isSecureContext) {
try {
await navigator.clipboard.writeText(text);
success = true;
} catch (err) {
console.warn("Clipboard API failed:", err);
}
}
// Fallback for insecure contexts
if (!success) {
const textarea = document.createElement("textarea");
textarea.value = text;
textarea.style.position = "fixed";
textarea.style.opacity = "0";
document.body.appendChild(textarea);
textarea.focus();
textarea.select();
try {
success = document.execCommand("copy");
} catch (err) {
console.error("Fallback copy failed:", err);
success = false;
} finally {
document.body.removeChild(textarea);
}
}
setIsCopied(success);
if (success && resetInterval > 0) {
setTimeout(() => setIsCopied(false), resetInterval);
}
return success;
}, [resetInterval]);
return { copy, isCopied };
}

View File

@ -19,11 +19,6 @@ interface JsonRpcResponse {
id: number | string | null; id: number | string | null;
} }
export type PostRebootAction = {
healthCheck: string;
redirectUrl: string;
} | null;
// Utility function to append stats to a Map // Utility function to append stats to a Map
const appendStatToMap = <T extends { timestamp: number }>( const appendStatToMap = <T extends { timestamp: number }>(
stat: T, stat: T,
@ -74,11 +69,6 @@ export interface UIState {
terminalType: AvailableTerminalTypes; terminalType: AvailableTerminalTypes;
setTerminalType: (type: UIState["terminalType"]) => void; setTerminalType: (type: UIState["terminalType"]) => void;
rebootState: { isRebooting: boolean; postRebootAction: PostRebootAction } | null;
setRebootState: (
state: { isRebooting: boolean; postRebootAction: PostRebootAction } | null,
) => void;
} }
export const useUiStore = create<UIState>(set => ({ export const useUiStore = create<UIState>(set => ({
@ -92,8 +82,7 @@ export const useUiStore = create<UIState>(set => ({
setDisableVideoFocusTrap: (enabled: boolean) => set({ disableVideoFocusTrap: enabled }), setDisableVideoFocusTrap: (enabled: boolean) => set({ disableVideoFocusTrap: enabled }),
isWakeOnLanModalVisible: false, isWakeOnLanModalVisible: false,
setWakeOnLanModalVisibility: (enabled: boolean) => setWakeOnLanModalVisibility: (enabled: boolean) => set({ isWakeOnLanModalVisible: enabled }),
set({ isWakeOnLanModalVisible: enabled }),
toggleSidebarView: view => toggleSidebarView: view =>
set(state => { set(state => {
@ -107,9 +96,6 @@ export const useUiStore = create<UIState>(set => ({
isAttachedVirtualKeyboardVisible: true, isAttachedVirtualKeyboardVisible: true,
setAttachedVirtualKeyboardVisibility: (enabled: boolean) => setAttachedVirtualKeyboardVisibility: (enabled: boolean) =>
set({ isAttachedVirtualKeyboardVisible: enabled }), set({ isAttachedVirtualKeyboardVisible: enabled }),
rebootState: null,
setRebootState: state => set({ rebootState: state }),
})); }));
export interface RTCState { export interface RTCState {
@ -691,7 +677,6 @@ export interface DhcpLease {
timezone?: string; timezone?: string;
routers?: string[]; routers?: string[];
dns?: string[]; dns?: string[];
dns_servers?: string[];
ntp_servers?: string[]; ntp_servers?: string[];
lpr_servers?: string[]; lpr_servers?: string[];
_time_servers?: string[]; _time_servers?: string[];
@ -709,7 +694,6 @@ export interface DhcpLease {
message?: string; message?: string;
tftp?: string; tftp?: string;
bootfile?: string; bootfile?: string;
dhcp_client?: string;
} }
export interface IPv6Address { export interface IPv6Address {
@ -718,15 +702,6 @@ export interface IPv6Address {
valid_lifetime: string; valid_lifetime: string;
preferred_lifetime: string; preferred_lifetime: string;
scope: string; scope: string;
flags: number;
flag_secondary?: boolean;
flag_permanent?: boolean;
flag_temporary?: boolean;
flag_stable_privacy?: boolean;
flag_deprecated?: boolean;
flag_optimistic?: boolean;
flag_dad_failed?: boolean;
flag_tentative?: boolean;
} }
export interface NetworkState { export interface NetworkState {
@ -737,9 +712,7 @@ export interface NetworkState {
ipv6?: string; ipv6?: string;
ipv6_addresses?: IPv6Address[]; ipv6_addresses?: IPv6Address[];
ipv6_link_local?: string; ipv6_link_local?: string;
ipv6_gateway?: string;
dhcp_lease?: DhcpLease; dhcp_lease?: DhcpLease;
hostname?: string;
setNetworkState: (state: NetworkState) => void; setNetworkState: (state: NetworkState) => void;
setDhcpLease: (lease: NetworkState["dhcp_lease"]) => void; setDhcpLease: (lease: NetworkState["dhcp_lease"]) => void;
@ -764,28 +737,12 @@ export type TimeSyncMode =
| "custom" | "custom"
| "unknown"; | "unknown";
export interface IPv4StaticConfig {
address: string;
netmask: string;
gateway: string;
dns: string[];
}
export interface IPv6StaticConfig {
prefix: string;
gateway: string;
dns: string[];
}
export interface NetworkSettings { export interface NetworkSettings {
dhcp_client: string; hostname: string;
hostname: string | null; domain: string;
domain: string | null; http_proxy: string;
http_proxy: string | null;
ipv4_mode: IPv4Mode; ipv4_mode: IPv4Mode;
ipv4_static?: IPv4StaticConfig;
ipv6_mode: IPv6Mode; ipv6_mode: IPv6Mode;
ipv6_static?: IPv6StaticConfig;
lldp_mode: LLDPMode; lldp_mode: LLDPMode;
lldp_tx_tlvs: string[]; lldp_tx_tlvs: string[];
mdns_mode: mDNSMode; mdns_mode: mDNSMode;

Some files were not shown because too many files have changed in this diff Show More