diff --git a/.vscode/settings.json b/.vscode/settings.json
index ba3550bf..41aeee58 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -10,5 +10,6 @@
]
},
"git.ignoreLimitWarning": true,
- "cmake.sourceDirectory": "/workspaces/kvm-static-ip/internal/native/cgo"
+ "cmake.sourceDirectory": "/workspaces/kvm-static-ip/internal/native/cgo",
+ "cmake.ignoreCMakeListsMissing": true
}
\ No newline at end of file
diff --git a/cmd/main.go b/cmd/main.go
index d9636088..9a1e1899 100644
--- a/cmd/main.go
+++ b/cmd/main.go
@@ -74,7 +74,12 @@ func supervise() error {
// run the child binary
cmd := exec.Command(binPath)
- cmd.Env = append(os.Environ(), []string{envChildID + "=" + kvm.GetBuiltAppVersion()}...)
+ lastFilePath := filepath.Join(errorDumpDir, errorDumpLastFile)
+
+ cmd.Env = append(os.Environ(), []string{
+ fmt.Sprintf("%s=%s", envChildID, kvm.GetBuiltAppVersion()),
+ fmt.Sprintf("JETKVM_LAST_ERROR_PATH=%s", lastFilePath),
+ }...)
cmd.Args = os.Args
logFile, err := os.CreateTemp("", "jetkvm-stdout.log")
diff --git a/failsafe.go b/failsafe.go
new file mode 100644
index 00000000..3c6b3d3a
--- /dev/null
+++ b/failsafe.go
@@ -0,0 +1,107 @@
+package kvm
+
+import (
+ "fmt"
+ "os"
+ "strings"
+ "sync"
+)
+
+const (
+ failsafeDefaultLastCrashPath = "/userdata/jetkvm/crashdump/last-crash.log"
+ failsafeFile = "/userdata/jetkvm/.enablefailsafe"
+ failsafeLastCrashEnv = "JETKVM_LAST_ERROR_PATH"
+ failsafeEnv = "JETKVM_FORCE_FAILSAFE"
+)
+
+var (
+ failsafeOnce sync.Once
+ failsafeCrashLog = ""
+ failsafeModeActive = false
+ failsafeModeReason = ""
+)
+
+type FailsafeModeNotification struct {
+ Active bool `json:"active"`
+ Reason string `json:"reason"`
+}
+
+// this function has side effects and can be only executed once
+func checkFailsafeReason() {
+ failsafeOnce.Do(func() {
+ // check if the failsafe environment variable is set
+ if os.Getenv(failsafeEnv) == "1" {
+ failsafeModeActive = true
+ failsafeModeReason = "failsafe_env_set"
+ return
+ }
+
+ // check if the failsafe file exists
+ if _, err := os.Stat(failsafeFile); err == nil {
+ failsafeModeActive = true
+ failsafeModeReason = "failsafe_file_exists"
+ _ = os.Remove(failsafeFile)
+ return
+ }
+
+ // get the last crash log path from the environment variable
+ lastCrashPath := os.Getenv(failsafeLastCrashEnv)
+ if lastCrashPath == "" {
+ lastCrashPath = failsafeDefaultLastCrashPath
+ }
+
+ // check if the last crash log file exists
+ l := failsafeLogger.With().Str("path", lastCrashPath).Logger()
+ fi, err := os.Lstat(lastCrashPath)
+ if err != nil {
+ if !os.IsNotExist(err) {
+ l.Warn().Err(err).Msg("failed to stat last crash log")
+ }
+ return
+ }
+
+ if fi.Mode()&os.ModeSymlink != os.ModeSymlink {
+ l.Warn().Msg("last crash log is not a symlink, ignoring")
+ return
+ }
+
+ // open the last crash log file and find if it contains the string "panic"
+ content, err := os.ReadFile(lastCrashPath)
+ if err != nil {
+ l.Warn().Err(err).Msg("failed to read last crash log")
+ return
+ }
+
+ // unlink the last crash log file
+ failsafeCrashLog = string(content)
+ _ = os.Remove(lastCrashPath)
+
+ // TODO: read the goroutine stack trace and check which goroutine is panicking
+ if strings.Contains(failsafeCrashLog, "runtime.cgocall") {
+ failsafeModeActive = true
+ failsafeModeReason = "video"
+ return
+ }
+ })
+}
+
+func notifyFailsafeMode(session *Session) {
+ if !failsafeModeActive || session == nil {
+ return
+ }
+
+ jsonRpcLogger.Info().Str("reason", failsafeModeReason).Msg("sending failsafe mode notification")
+
+ writeJSONRPCEvent("failsafeMode", FailsafeModeNotification{
+ Active: true,
+ Reason: failsafeModeReason,
+ }, session)
+}
+
+func rpcGetFailsafeLogs() (string, error) {
+ if !failsafeModeActive {
+ return "", fmt.Errorf("failsafe mode is not active")
+ }
+
+ return failsafeCrashLog, nil
+}
diff --git a/internal/native/cgo_linux.go b/internal/native/cgo_linux.go
index 850da0e8..be1a5a36 100644
--- a/internal/native/cgo_linux.go
+++ b/internal/native/cgo_linux.go
@@ -1,5 +1,8 @@
//go:build linux
+// TODO: use a generator to generate the cgo code for the native functions
+// there's too much boilerplate code to write manually
+
package native
import (
@@ -46,7 +49,17 @@ static inline void jetkvm_cgo_setup_rpc_handler() {
*/
import "C"
-var cgoLock sync.Mutex
+var (
+ cgoLock sync.Mutex
+ cgoDisabled bool
+)
+
+func setCgoDisabled(disabled bool) {
+ cgoLock.Lock()
+ defer cgoLock.Unlock()
+
+ cgoDisabled = disabled
+}
//export jetkvm_go_video_state_handler
func jetkvm_go_video_state_handler(state *C.jetkvm_video_state_t) {
@@ -91,6 +104,10 @@ func jetkvm_go_rpc_handler(method *C.cchar_t, params *C.cchar_t) {
var eventCodeToNameMap = map[int]string{}
func uiEventCodeToName(code int) string {
+ if cgoDisabled {
+ return ""
+ }
+
name, ok := eventCodeToNameMap[code]
if !ok {
cCode := C.int(code)
@@ -103,6 +120,10 @@ func uiEventCodeToName(code int) string {
}
func setUpNativeHandlers() {
+ if cgoDisabled {
+ return
+ }
+
cgoLock.Lock()
defer cgoLock.Unlock()
@@ -114,6 +135,10 @@ func setUpNativeHandlers() {
}
func uiInit(rotation uint16) {
+ if cgoDisabled {
+ return
+ }
+
cgoLock.Lock()
defer cgoLock.Unlock()
@@ -123,6 +148,10 @@ func uiInit(rotation uint16) {
}
func uiTick() {
+ if cgoDisabled {
+ return
+ }
+
cgoLock.Lock()
defer cgoLock.Unlock()
@@ -130,6 +159,10 @@ func uiTick() {
}
func videoInit(factor float64) error {
+ if cgoDisabled {
+ return nil
+ }
+
cgoLock.Lock()
defer cgoLock.Unlock()
@@ -143,6 +176,10 @@ func videoInit(factor float64) error {
}
func videoShutdown() {
+ if cgoDisabled {
+ return
+ }
+
cgoLock.Lock()
defer cgoLock.Unlock()
@@ -150,6 +187,10 @@ func videoShutdown() {
}
func videoStart() {
+ if cgoDisabled {
+ return
+ }
+
cgoLock.Lock()
defer cgoLock.Unlock()
@@ -157,6 +198,10 @@ func videoStart() {
}
func videoStop() {
+ if cgoDisabled {
+ return
+ }
+
cgoLock.Lock()
defer cgoLock.Unlock()
@@ -164,6 +209,10 @@ func videoStop() {
}
func videoLogStatus() string {
+ if cgoDisabled {
+ return ""
+ }
+
cgoLock.Lock()
defer cgoLock.Unlock()
@@ -174,6 +223,10 @@ func videoLogStatus() string {
}
func uiSetVar(name string, value string) {
+ if cgoDisabled {
+ return
+ }
+
cgoLock.Lock()
defer cgoLock.Unlock()
@@ -187,6 +240,10 @@ func uiSetVar(name string, value string) {
}
func uiGetVar(name string) string {
+ if cgoDisabled {
+ return ""
+ }
+
cgoLock.Lock()
defer cgoLock.Unlock()
@@ -197,6 +254,10 @@ func uiGetVar(name string) string {
}
func uiSwitchToScreen(screen string) {
+ if cgoDisabled {
+ return
+ }
+
cgoLock.Lock()
defer cgoLock.Unlock()
@@ -206,6 +267,10 @@ func uiSwitchToScreen(screen string) {
}
func uiGetCurrentScreen() string {
+ if cgoDisabled {
+ return ""
+ }
+
cgoLock.Lock()
defer cgoLock.Unlock()
@@ -214,6 +279,10 @@ func uiGetCurrentScreen() string {
}
func uiObjAddState(objName string, state string) (bool, error) {
+ if cgoDisabled {
+ return false, nil
+ }
+
cgoLock.Lock()
defer cgoLock.Unlock()
@@ -226,6 +295,10 @@ func uiObjAddState(objName string, state string) (bool, error) {
}
func uiObjClearState(objName string, state string) (bool, error) {
+ if cgoDisabled {
+ return false, nil
+ }
+
cgoLock.Lock()
defer cgoLock.Unlock()
@@ -238,6 +311,10 @@ func uiObjClearState(objName string, state string) (bool, error) {
}
func uiGetLVGLVersion() string {
+ if cgoDisabled {
+ return ""
+ }
+
cgoLock.Lock()
defer cgoLock.Unlock()
@@ -246,6 +323,10 @@ func uiGetLVGLVersion() string {
// TODO: use Enum instead of string but it's not a hot path and performance is not a concern now
func uiObjAddFlag(objName string, flag string) (bool, error) {
+ if cgoDisabled {
+ return false, nil
+ }
+
cgoLock.Lock()
defer cgoLock.Unlock()
@@ -258,6 +339,10 @@ func uiObjAddFlag(objName string, flag string) (bool, error) {
}
func uiObjClearFlag(objName string, flag string) (bool, error) {
+ if cgoDisabled {
+ return false, nil
+ }
+
cgoLock.Lock()
defer cgoLock.Unlock()
@@ -278,6 +363,10 @@ func uiObjShow(objName string) (bool, error) {
}
func uiObjSetOpacity(objName string, opacity int) (bool, error) {
+ if cgoDisabled {
+ return false, nil
+ }
+
cgoLock.Lock()
defer cgoLock.Unlock()
@@ -289,6 +378,10 @@ func uiObjSetOpacity(objName string, opacity int) (bool, error) {
}
func uiObjFadeIn(objName string, duration uint32) (bool, error) {
+ if cgoDisabled {
+ return false, nil
+ }
+
cgoLock.Lock()
defer cgoLock.Unlock()
@@ -301,6 +394,10 @@ func uiObjFadeIn(objName string, duration uint32) (bool, error) {
}
func uiObjFadeOut(objName string, duration uint32) (bool, error) {
+ if cgoDisabled {
+ return false, nil
+ }
+
cgoLock.Lock()
defer cgoLock.Unlock()
@@ -313,6 +410,10 @@ func uiObjFadeOut(objName string, duration uint32) (bool, error) {
}
func uiLabelSetText(objName string, text string) (bool, error) {
+ if cgoDisabled {
+ return false, nil
+ }
+
cgoLock.Lock()
defer cgoLock.Unlock()
@@ -330,6 +431,10 @@ func uiLabelSetText(objName string, text string) (bool, error) {
}
func uiImgSetSrc(objName string, src string) (bool, error) {
+ if cgoDisabled {
+ return false, nil
+ }
+
cgoLock.Lock()
defer cgoLock.Unlock()
@@ -345,6 +450,10 @@ func uiImgSetSrc(objName string, src string) (bool, error) {
}
func uiDispSetRotation(rotation uint16) (bool, error) {
+ if cgoDisabled {
+ return false, nil
+ }
+
cgoLock.Lock()
defer cgoLock.Unlock()
@@ -357,6 +466,10 @@ func uiDispSetRotation(rotation uint16) (bool, error) {
}
func videoGetStreamQualityFactor() (float64, error) {
+ if cgoDisabled {
+ return 0, nil
+ }
+
cgoLock.Lock()
defer cgoLock.Unlock()
@@ -365,6 +478,10 @@ func videoGetStreamQualityFactor() (float64, error) {
}
func videoSetStreamQualityFactor(factor float64) error {
+ if cgoDisabled {
+ return nil
+ }
+
cgoLock.Lock()
defer cgoLock.Unlock()
@@ -373,6 +490,10 @@ func videoSetStreamQualityFactor(factor float64) error {
}
func videoGetEDID() (string, error) {
+ if cgoDisabled {
+ return "", nil
+ }
+
cgoLock.Lock()
defer cgoLock.Unlock()
@@ -381,6 +502,10 @@ func videoGetEDID() (string, error) {
}
func videoSetEDID(edid string) error {
+ if cgoDisabled {
+ return nil
+ }
+
cgoLock.Lock()
defer cgoLock.Unlock()
diff --git a/internal/native/native.go b/internal/native/native.go
index 3b1cc0b4..cb8761cf 100644
--- a/internal/native/native.go
+++ b/internal/native/native.go
@@ -9,6 +9,7 @@ import (
)
type Native struct {
+ disable bool
ready chan struct{}
l *zerolog.Logger
lD *zerolog.Logger
@@ -27,6 +28,7 @@ type Native struct {
}
type NativeOptions struct {
+ Disable bool
SystemVersion *semver.Version
AppVersion *semver.Version
DisplayRotation uint16
@@ -74,6 +76,7 @@ func NewNative(opts NativeOptions) *Native {
}
return &Native{
+ disable: opts.Disable,
ready: make(chan struct{}),
l: nativeLogger,
lD: displayLogger,
@@ -92,6 +95,12 @@ func NewNative(opts NativeOptions) *Native {
}
func (n *Native) Start() {
+ if n.disable {
+ nativeLogger.Warn().Msg("native is disabled, skipping initialization")
+ setCgoDisabled(true)
+ return
+ }
+
// set up singleton
setInstance(n)
setUpNativeHandlers()
diff --git a/jsonrpc.go b/jsonrpc.go
index ebb6d672..1f29dc03 100644
--- a/jsonrpc.go
+++ b/jsonrpc.go
@@ -932,6 +932,10 @@ func rpcSetCloudUrl(apiUrl string, appUrl string) error {
disconnectCloud(fmt.Errorf("cloud url changed from %s to %s", currentCloudURL, apiUrl))
}
+ if publicIPState != nil {
+ publicIPState.SetCloudflareEndpoint(apiUrl)
+ }
+
if err := SaveConfig(); err != nil {
return fmt.Errorf("failed to save config: %w", err)
}
@@ -1311,4 +1315,7 @@ var rpcHandlers = map[string]RPCHandler{
"setKeyboardMacros": {Func: setKeyboardMacros, Params: []string{"params"}},
"getLocalLoopbackOnly": {Func: rpcGetLocalLoopbackOnly},
"setLocalLoopbackOnly": {Func: rpcSetLocalLoopbackOnly, Params: []string{"enabled"}},
+ "getFailSafeLogs": {Func: rpcGetFailsafeLogs},
+ "getPublicIPAddresses": {Func: rpcGetPublicIPAddresses, Params: []string{"refresh"}},
+ "checkPublicIPAddresses": {Func: rpcCheckPublicIPAddresses},
}
diff --git a/log.go b/log.go
index 2047bbfa..9cd9188e 100644
--- a/log.go
+++ b/log.go
@@ -11,6 +11,7 @@ func ErrorfL(l *zerolog.Logger, format string, err error, args ...any) error {
var (
logger = logging.GetSubsystemLogger("jetkvm")
+ failsafeLogger = logging.GetSubsystemLogger("failsafe")
networkLogger = logging.GetSubsystemLogger("network")
cloudLogger = logging.GetSubsystemLogger("cloud")
websocketLogger = logging.GetSubsystemLogger("websocket")
diff --git a/main.go b/main.go
index bcc2d73d..d7ecc3e1 100644
--- a/main.go
+++ b/main.go
@@ -15,6 +15,12 @@ var appCtx context.Context
func Main() {
logger.Log().Msg("JetKVM Starting Up")
+
+ checkFailsafeReason()
+ if failsafeModeActive {
+ logger.Warn().Str("reason", failsafeModeReason).Msg("failsafe mode activated")
+ }
+
LoadConfig()
var cancel context.CancelFunc
@@ -50,6 +56,7 @@ func Main() {
// Initialize network
if err := initNetwork(); err != nil {
logger.Error().Err(err).Msg("failed to initialize network")
+ // TODO: reset config to default
os.Exit(1)
}
@@ -60,7 +67,6 @@ func Main() {
// Initialize mDNS
if err := initMdns(); err != nil {
logger.Error().Err(err).Msg("failed to initialize mDNS")
- os.Exit(1)
}
initPrometheus()
@@ -126,6 +132,7 @@ func Main() {
// As websocket client already checks if the cloud token is set, we can start it here.
go RunWebsocketClient()
+ initPublicIPState()
initSerialPort()
sigs := make(chan os.Signal, 1)
diff --git a/native.go b/native.go
index 4a523bce..81a0e50d 100644
--- a/native.go
+++ b/native.go
@@ -17,6 +17,7 @@ var (
func initNative(systemVersion *semver.Version, appVersion *semver.Version) {
nativeInstance = native.NewNative(native.NativeOptions{
+ Disable: failsafeModeActive,
SystemVersion: systemVersion,
AppVersion: appVersion,
DisplayRotation: config.GetDisplayRotation(),
diff --git a/network.go b/network.go
index 846f41f1..3a2b24f9 100644
--- a/network.go
+++ b/network.go
@@ -3,12 +3,17 @@ package kvm
import (
"context"
"fmt"
+ "net"
+ "net/http"
"reflect"
+ "time"
"github.com/jetkvm/kvm/internal/confparser"
"github.com/jetkvm/kvm/internal/mdns"
"github.com/jetkvm/kvm/internal/network/types"
+ "github.com/jetkvm/kvm/pkg/myip"
"github.com/jetkvm/kvm/pkg/nmlite"
+ "github.com/jetkvm/kvm/pkg/nmlite/link"
)
const (
@@ -17,6 +22,7 @@ const (
var (
networkManager *nmlite.NetworkManager
+ publicIPState *myip.PublicIPState
)
type RpcNetworkSettings struct {
@@ -104,6 +110,13 @@ func triggerTimeSyncOnNetworkStateChange() {
}()
}
+func setPublicIPReadyState(ipv4Ready, ipv6Ready bool) {
+ if publicIPState == nil {
+ return
+ }
+ publicIPState.SetIPv4AndIPv6(ipv4Ready, ipv6Ready)
+}
+
func networkStateChanged(_ string, state types.InterfaceState) {
// do not block the main thread
go waitCtrlAndRequestDisplayUpdate(true, "network_state_changed")
@@ -117,6 +130,8 @@ func networkStateChanged(_ string, state types.InterfaceState) {
triggerTimeSyncOnNetworkStateChange()
}
+ setPublicIPReadyState(state.IPv4Ready, state.IPv6Ready)
+
// always restart mDNS when the network state changes
if mDNS != nil {
restartMdns()
@@ -164,6 +179,40 @@ func initNetwork() error {
return nil
}
+func initPublicIPState() {
+ // the feature will be only enabled if the cloud has been adopted
+ // due to privacy reasons
+
+ // but it will be initialized anyway to avoid nil pointer dereferences
+ ps := myip.NewPublicIPState(&myip.PublicIPStateConfig{
+ Logger: networkLogger,
+ CloudflareEndpoint: config.CloudURL,
+ APIEndpoint: "",
+ IPv4: false,
+ IPv6: false,
+ HttpClientGetter: func(family int) *http.Client {
+ transport := http.DefaultTransport.(*http.Transport).Clone()
+ transport.Proxy = config.NetworkConfig.GetTransportProxyFunc()
+ transport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {
+ netType := network
+ switch family {
+ case link.AfInet:
+ netType = "tcp4"
+ case link.AfInet6:
+ netType = "tcp6"
+ }
+ return (&net.Dialer{}).DialContext(ctx, netType, addr)
+ }
+
+ return &http.Client{
+ Transport: transport,
+ Timeout: 30 * time.Second,
+ }
+ },
+ })
+ publicIPState = ps
+}
+
func setHostname(nm *nmlite.NetworkManager, hostname, domain string) error {
if nm == nil {
return nil
@@ -312,3 +361,25 @@ func rpcToggleDHCPClient() error {
return rpcReboot(true)
}
+
+func rpcGetPublicIPAddresses(refresh bool) ([]myip.PublicIP, error) {
+ if publicIPState == nil {
+ return nil, fmt.Errorf("public IP state not initialized")
+ }
+
+ if refresh {
+ if err := publicIPState.ForceUpdate(); err != nil {
+ return nil, err
+ }
+ }
+
+ return publicIPState.GetAddresses(), nil
+}
+
+func rpcCheckPublicIPAddresses() error {
+ if publicIPState == nil {
+ return fmt.Errorf("public IP state not initialized")
+ }
+
+ return publicIPState.ForceUpdate()
+}
diff --git a/pkg/myip/check.go b/pkg/myip/check.go
new file mode 100644
index 00000000..86d3ba50
--- /dev/null
+++ b/pkg/myip/check.go
@@ -0,0 +1,160 @@
+package myip
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "net"
+ "net/http"
+ "net/url"
+ "strconv"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/jetkvm/kvm/pkg/nmlite/link"
+)
+
+func (ps *PublicIPState) request(ctx context.Context, url string, family int) ([]byte, error) {
+ client := ps.httpClient(family)
+
+ req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
+ if err != nil {
+ return nil, fmt.Errorf("error creating request: %w", err)
+ }
+
+ resp, err := client.Do(req)
+ if err != nil {
+ return nil, fmt.Errorf("error sending request: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
+ }
+
+ body, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return nil, fmt.Errorf("error reading response body: %w", err)
+ }
+
+ return body, err
+}
+
+// checkCloudflare uses cdn-cgi/trace to get the public IP address
+func (ps *PublicIPState) checkCloudflare(ctx context.Context, family int) (*PublicIP, error) {
+ u, err := url.JoinPath(ps.cloudflareEndpoint, "/cdn-cgi/trace")
+ if err != nil {
+ return nil, fmt.Errorf("error joining path: %w", err)
+ }
+
+ body, err := ps.request(ctx, u, family)
+ if err != nil {
+ return nil, err
+ }
+
+ values := make(map[string]string)
+ for line := range strings.SplitSeq(string(body), "\n") {
+ key, value, ok := strings.Cut(line, "=")
+ if !ok {
+ continue
+ }
+ values[key] = value
+ }
+
+ ps.lastUpdated = time.Now()
+ if ts, ok := values["ts"]; ok {
+ if ts, err := strconv.ParseFloat(ts, 64); err == nil {
+ ps.lastUpdated = time.Unix(int64(ts), 0)
+ }
+ }
+
+ ipStr, ok := values["ip"]
+ if !ok {
+ return nil, fmt.Errorf("no IP address found")
+ }
+
+ ip := net.ParseIP(ipStr)
+ if ip == nil {
+ return nil, fmt.Errorf("invalid IP address: %s", ipStr)
+ }
+
+ return &PublicIP{
+ IPAddress: ip,
+ LastUpdated: ps.lastUpdated,
+ }, nil
+}
+
+// checkAPI uses the API endpoint to get the public IP address
+func (ps *PublicIPState) checkAPI(_ context.Context, _ int) (*PublicIP, error) {
+ return nil, fmt.Errorf("not implemented")
+}
+
+// checkIPs checks both IPv4 and IPv6 public IP addresses in parallel
+// and updates the IPAddresses slice with the results
+func (ps *PublicIPState) checkIPs(ctx context.Context, checkIPv4, checkIPv6 bool) error {
+ var wg sync.WaitGroup
+ var mu sync.Mutex
+ var ips []PublicIP
+ var errors []error
+
+ checkFamily := func(family int, familyName string) {
+ wg.Add(1)
+ go func(f int, name string) {
+ defer wg.Done()
+
+ ip, err := ps.checkIPForFamily(ctx, f)
+ mu.Lock()
+ defer mu.Unlock()
+ if err != nil {
+ errors = append(errors, fmt.Errorf("%s check failed: %w", name, err))
+ return
+ }
+ if ip != nil {
+ ips = append(ips, *ip)
+ }
+ }(family, familyName)
+ }
+
+ if checkIPv4 {
+ checkFamily(link.AfInet, "IPv4")
+ }
+
+ if checkIPv6 {
+ checkFamily(link.AfInet6, "IPv6")
+ }
+
+ wg.Wait()
+
+ if len(ips) > 0 {
+ ps.mu.Lock()
+ defer ps.mu.Unlock()
+
+ ps.addresses = ips
+ ps.lastUpdated = time.Now()
+ }
+
+ if len(errors) > 0 && len(ips) == 0 {
+ return errors[0]
+ }
+
+ return nil
+}
+
+func (ps *PublicIPState) checkIPForFamily(ctx context.Context, family int) (*PublicIP, error) {
+ if ps.apiEndpoint != "" {
+ ip, err := ps.checkAPI(ctx, family)
+ if err == nil && ip != nil {
+ return ip, nil
+ }
+ }
+
+ if ps.cloudflareEndpoint != "" {
+ ip, err := ps.checkCloudflare(ctx, family)
+ if err == nil && ip != nil {
+ return ip, nil
+ }
+ }
+
+ return nil, fmt.Errorf("all IP check methods failed for family %d", family)
+}
diff --git a/pkg/myip/ip.go b/pkg/myip/ip.go
new file mode 100644
index 00000000..15afc24e
--- /dev/null
+++ b/pkg/myip/ip.go
@@ -0,0 +1,196 @@
+package myip
+
+import (
+ "context"
+ "fmt"
+ "net"
+ "net/http"
+ "net/url"
+ "sync"
+ "time"
+
+ "github.com/jetkvm/kvm/internal/logging"
+ "github.com/rs/zerolog"
+)
+
+type PublicIP struct {
+ IPAddress net.IP `json:"ip"`
+ LastUpdated time.Time `json:"last_updated"`
+}
+
+type HttpClientGetter func(family int) *http.Client
+
+type PublicIPState struct {
+ addresses []PublicIP
+ lastUpdated time.Time
+
+ cloudflareEndpoint string // cdn-cgi/trace domain
+ apiEndpoint string // api endpoint
+ ipv4 bool
+ ipv6 bool
+ httpClient HttpClientGetter
+ logger *zerolog.Logger
+
+ timer *time.Timer
+ ctx context.Context
+ cancel context.CancelFunc
+ mu sync.Mutex
+}
+
+type PublicIPStateConfig struct {
+ CloudflareEndpoint string
+ APIEndpoint string
+ IPv4 bool
+ IPv6 bool
+ HttpClientGetter HttpClientGetter
+ Logger *zerolog.Logger
+}
+
+func stripURLPath(s string) string {
+ parsed, err := url.Parse(s)
+ if err != nil {
+ return ""
+ }
+ scheme := parsed.Scheme
+ if scheme != "http" && scheme != "https" {
+ scheme = "https"
+ }
+
+ return fmt.Sprintf("%s://%s", scheme, parsed.Host)
+}
+
+// NewPublicIPState creates a new PublicIPState
+func NewPublicIPState(config *PublicIPStateConfig) *PublicIPState {
+ if config.Logger == nil {
+ config.Logger = logging.GetSubsystemLogger("publicip")
+ }
+
+ ctx, cancel := context.WithCancel(context.Background())
+ ps := &PublicIPState{
+ addresses: make([]PublicIP, 0),
+ lastUpdated: time.Now(),
+ cloudflareEndpoint: stripURLPath(config.CloudflareEndpoint),
+ apiEndpoint: config.APIEndpoint,
+ ipv4: config.IPv4,
+ ipv6: config.IPv6,
+ httpClient: config.HttpClientGetter,
+ ctx: ctx,
+ cancel: cancel,
+ logger: config.Logger,
+ }
+ // Start the timer automatically
+ ps.Start()
+ return ps
+}
+
+// SetFamily sets if we need to track IPv4 and IPv6 public IP addresses
+func (ps *PublicIPState) SetIPv4AndIPv6(ipv4, ipv6 bool) {
+ ps.mu.Lock()
+ defer ps.mu.Unlock()
+
+ ps.ipv4 = ipv4
+ ps.ipv6 = ipv6
+}
+
+// SetCloudflareEndpoint sets the Cloudflare endpoint
+func (ps *PublicIPState) SetCloudflareEndpoint(endpoint string) {
+ ps.mu.Lock()
+ defer ps.mu.Unlock()
+
+ ps.cloudflareEndpoint = stripURLPath(endpoint)
+}
+
+// SetAPIEndpoint sets the API endpoint
+func (ps *PublicIPState) SetAPIEndpoint(endpoint string) {
+ ps.mu.Lock()
+ defer ps.mu.Unlock()
+
+ ps.apiEndpoint = endpoint
+}
+
+// GetAddresses returns the public IP addresses
+func (ps *PublicIPState) GetAddresses() []PublicIP {
+ ps.mu.Lock()
+ defer ps.mu.Unlock()
+
+ return ps.addresses
+}
+
+// Start starts the timer loop to check public IP addresses periodically
+func (ps *PublicIPState) Start() {
+ ps.mu.Lock()
+ defer ps.mu.Unlock()
+
+ // Stop any existing timer
+ if ps.timer != nil {
+ ps.timer.Stop()
+ }
+
+ if ps.cancel != nil {
+ ps.cancel()
+ }
+
+ // Create new context and cancel function
+ ps.ctx, ps.cancel = context.WithCancel(context.Background())
+
+ // Start the timer loop in a goroutine
+ go ps.timerLoop(ps.ctx)
+}
+
+// Stop stops the timer loop
+func (ps *PublicIPState) Stop() {
+ ps.mu.Lock()
+ defer ps.mu.Unlock()
+
+ if ps.cancel != nil {
+ ps.cancel()
+ ps.cancel = nil
+ }
+
+ if ps.timer != nil {
+ ps.timer.Stop()
+ ps.timer = nil
+ }
+}
+
+// ForceUpdate forces an update of the public IP addresses
+func (ps *PublicIPState) ForceUpdate() error {
+ return ps.checkIPs(context.Background(), true, true)
+}
+
+// timerLoop runs the periodic IP check loop
+func (ps *PublicIPState) timerLoop(ctx context.Context) {
+ timer := time.NewTimer(5 * time.Minute)
+ defer timer.Stop()
+
+ // Store timer reference for Stop() to access
+ ps.mu.Lock()
+ ps.timer = timer
+ checkIPv4 := ps.ipv4
+ checkIPv6 := ps.ipv6
+ ps.mu.Unlock()
+
+ // Perform initial check immediately
+ checkIPs := func() {
+ if err := ps.checkIPs(ctx, checkIPv4, checkIPv6); err != nil {
+ ps.logger.Error().Err(err).Msg("failed to check public IP addresses")
+ }
+ }
+
+ checkIPs()
+
+ for {
+ select {
+ case <-timer.C:
+ // Perform the check
+ checkIPs()
+
+ // Reset the timer for the next check
+ timer.Reset(5 * time.Minute)
+
+ case <-ctx.Done():
+ // Timer was stopped
+ return
+ }
+ }
+}
diff --git a/ui/localization/messages/en.json b/ui/localization/messages/en.json
index 6a0c1ed9..dbbd6ff5 100644
--- a/ui/localization/messages/en.json
+++ b/ui/localization/messages/en.json
@@ -896,5 +896,11 @@
"wake_on_lan_invalid_mac": "Invalid MAC address",
"wake_on_lan_magic_sent_success": "Magic Packet sent successfully",
"welcome_to_jetkvm": "Welcome to JetKVM",
- "welcome_to_jetkvm_description": "Control any computer remotely"
+ "welcome_to_jetkvm_description": "Control any computer remotely","connection_stats_remote_ip_address": "Remote IP Address",
+ "connection_stats_remote_ip_address_description": "The IP address of the remote device.",
+ "connection_stats_remote_ip_address_copy_error": "Failed to copy remote IP address",
+ "connection_stats_remote_ip_address_copy_success": "Remote IP address { ip } copied to clipboard",
+ "public_ip_card_header": "Public IP addresses",
+ "public_ip_card_refresh": "Refresh",
+ "public_ip_card_refresh_error": "Failed to refresh public IP addresses: {error}"
}
diff --git a/ui/package-lock.json b/ui/package-lock.json
index acdb64a4..7e4938e5 100644
--- a/ui/package-lock.json
+++ b/ui/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "kvm-ui",
- "version": "2025.11.05.2130",
+ "version": "2025.11.07.2130",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "kvm-ui",
- "version": "2025.11.05.2130",
+ "version": "2025.11.07.2130",
"dependencies": {
"@headlessui/react": "^2.2.9",
"@headlessui/tailwindcss": "^0.2.2",
@@ -52,9 +52,9 @@
"@inlang/plugin-message-format": "^4.0.0",
"@inlang/sdk": "^2.4.9",
"@tailwindcss/forms": "^0.5.10",
- "@tailwindcss/postcss": "^4.1.16",
+ "@tailwindcss/postcss": "^4.1.17",
"@tailwindcss/typography": "^0.5.19",
- "@tailwindcss/vite": "^4.1.16",
+ "@tailwindcss/vite": "^4.1.17",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"@types/semver": "^7.7.1",
@@ -74,7 +74,7 @@
"postcss": "^8.5.6",
"prettier": "^3.6.2",
"prettier-plugin-tailwindcss": "^0.7.1",
- "tailwindcss": "^4.1.16",
+ "tailwindcss": "^4.1.17",
"typescript": "^5.9.3",
"vite": "^7.2.0",
"vite-tsconfig-paths": "^5.1.4"
@@ -1437,9 +1437,9 @@
"license": "MIT"
},
"node_modules/@rollup/rollup-android-arm-eabi": {
- "version": "4.52.5",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz",
- "integrity": "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==",
+ "version": "4.53.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.1.tgz",
+ "integrity": "sha512-bxZtughE4VNVJlL1RdoSE545kc4JxL7op57KKoi59/gwuU5rV6jLWFXXc8jwgFoT6vtj+ZjO+Z2C5nrY0Cl6wA==",
"cpu": [
"arm"
],
@@ -1450,9 +1450,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
- "version": "4.52.5",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.5.tgz",
- "integrity": "sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==",
+ "version": "4.53.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.1.tgz",
+ "integrity": "sha512-44a1hreb02cAAfAKmZfXVercPFaDjqXCK+iKeVOlJ9ltvnO6QqsBHgKVPTu+MJHSLLeMEUbeG2qiDYgbFPU48g==",
"cpu": [
"arm64"
],
@@ -1463,9 +1463,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
- "version": "4.52.5",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.5.tgz",
- "integrity": "sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==",
+ "version": "4.53.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.1.tgz",
+ "integrity": "sha512-usmzIgD0rf1syoOZ2WZvy8YpXK5G1V3btm3QZddoGSa6mOgfXWkkv+642bfUUldomgrbiLQGrPryb7DXLovPWQ==",
"cpu": [
"arm64"
],
@@ -1476,9 +1476,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
- "version": "4.52.5",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.5.tgz",
- "integrity": "sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==",
+ "version": "4.53.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.1.tgz",
+ "integrity": "sha512-is3r/k4vig2Gt8mKtTlzzyaSQ+hd87kDxiN3uDSDwggJLUV56Umli6OoL+/YZa/KvtdrdyNfMKHzL/P4siOOmg==",
"cpu": [
"x64"
],
@@ -1489,9 +1489,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
- "version": "4.52.5",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.5.tgz",
- "integrity": "sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==",
+ "version": "4.53.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.1.tgz",
+ "integrity": "sha512-QJ1ksgp/bDJkZB4daldVmHaEQkG4r8PUXitCOC2WRmRaSaHx5RwPoI3DHVfXKwDkB+Sk6auFI/+JHacTekPRSw==",
"cpu": [
"arm64"
],
@@ -1502,9 +1502,9 @@
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
- "version": "4.52.5",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.5.tgz",
- "integrity": "sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==",
+ "version": "4.53.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.1.tgz",
+ "integrity": "sha512-J6ma5xgAzvqsnU6a0+jgGX/gvoGokqpkx6zY4cWizRrm0ffhHDpJKQgC8dtDb3+MqfZDIqs64REbfHDMzxLMqQ==",
"cpu": [
"x64"
],
@@ -1515,9 +1515,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
- "version": "4.52.5",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.5.tgz",
- "integrity": "sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==",
+ "version": "4.53.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.1.tgz",
+ "integrity": "sha512-JzWRR41o2U3/KMNKRuZNsDUAcAVUYhsPuMlx5RUldw0E4lvSIXFUwejtYz1HJXohUmqs/M6BBJAUBzKXZVddbg==",
"cpu": [
"arm"
],
@@ -1528,9 +1528,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
- "version": "4.52.5",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.5.tgz",
- "integrity": "sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==",
+ "version": "4.53.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.1.tgz",
+ "integrity": "sha512-L8kRIrnfMrEoHLHtHn+4uYA52fiLDEDyezgxZtGUTiII/yb04Krq+vk3P2Try+Vya9LeCE9ZHU8CXD6J9EhzHQ==",
"cpu": [
"arm"
],
@@ -1541,9 +1541,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
- "version": "4.52.5",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.5.tgz",
- "integrity": "sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==",
+ "version": "4.53.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.1.tgz",
+ "integrity": "sha512-ysAc0MFRV+WtQ8li8hi3EoFi7us6d1UzaS/+Dp7FYZfg3NdDljGMoVyiIp6Ucz7uhlYDBZ/zt6XI0YEZbUO11Q==",
"cpu": [
"arm64"
],
@@ -1554,9 +1554,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
- "version": "4.52.5",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.5.tgz",
- "integrity": "sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==",
+ "version": "4.53.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.1.tgz",
+ "integrity": "sha512-UV6l9MJpDbDZZ/fJvqNcvO1PcivGEf1AvKuTcHoLjVZVFeAMygnamCTDikCVMRnA+qJe+B3pSbgX2+lBMqgBhA==",
"cpu": [
"arm64"
],
@@ -1567,9 +1567,9 @@
]
},
"node_modules/@rollup/rollup-linux-loong64-gnu": {
- "version": "4.52.5",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.5.tgz",
- "integrity": "sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==",
+ "version": "4.53.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.1.tgz",
+ "integrity": "sha512-UDUtelEprkA85g95Q+nj3Xf0M4hHa4DiJ+3P3h4BuGliY4NReYYqwlc0Y8ICLjN4+uIgCEvaygYlpf0hUj90Yg==",
"cpu": [
"loong64"
],
@@ -1580,9 +1580,9 @@
]
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
- "version": "4.52.5",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.5.tgz",
- "integrity": "sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==",
+ "version": "4.53.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.1.tgz",
+ "integrity": "sha512-vrRn+BYhEtNOte/zbc2wAUQReJXxEx2URfTol6OEfY2zFEUK92pkFBSXRylDM7aHi+YqEPJt9/ABYzmcrS4SgQ==",
"cpu": [
"ppc64"
],
@@ -1593,9 +1593,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
- "version": "4.52.5",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.5.tgz",
- "integrity": "sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==",
+ "version": "4.53.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.1.tgz",
+ "integrity": "sha512-gto/1CxHyi4A7YqZZNznQYrVlPSaodOBPKM+6xcDSCMVZN/Fzb4K+AIkNz/1yAYz9h3Ng+e2fY9H6bgawVq17w==",
"cpu": [
"riscv64"
],
@@ -1606,9 +1606,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
- "version": "4.52.5",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.5.tgz",
- "integrity": "sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==",
+ "version": "4.53.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.1.tgz",
+ "integrity": "sha512-KZ6Vx7jAw3aLNjFR8eYVcQVdFa/cvBzDNRFM3z7XhNNunWjA03eUrEwJYPk0G8V7Gs08IThFKcAPS4WY/ybIrQ==",
"cpu": [
"riscv64"
],
@@ -1619,9 +1619,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
- "version": "4.52.5",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.5.tgz",
- "integrity": "sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==",
+ "version": "4.53.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.1.tgz",
+ "integrity": "sha512-HvEixy2s/rWNgpwyKpXJcHmE7om1M89hxBTBi9Fs6zVuLU4gOrEMQNbNsN/tBVIMbLyysz/iwNiGtMOpLAOlvA==",
"cpu": [
"s390x"
],
@@ -1632,9 +1632,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
- "version": "4.52.5",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.5.tgz",
- "integrity": "sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==",
+ "version": "4.53.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.1.tgz",
+ "integrity": "sha512-E/n8x2MSjAQgjj9IixO4UeEUeqXLtiA7pyoXCFYLuXpBA/t2hnbIdxHfA7kK9BFsYAoNU4st1rHYdldl8dTqGA==",
"cpu": [
"x64"
],
@@ -1645,9 +1645,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
- "version": "4.52.5",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.5.tgz",
- "integrity": "sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==",
+ "version": "4.53.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.1.tgz",
+ "integrity": "sha512-IhJ087PbLOQXCN6Ui/3FUkI9pWNZe/Z7rEIVOzMsOs1/HSAECCvSZ7PkIbkNqL/AZn6WbZvnoVZw/qwqYMo4/w==",
"cpu": [
"x64"
],
@@ -1658,9 +1658,9 @@
]
},
"node_modules/@rollup/rollup-openharmony-arm64": {
- "version": "4.52.5",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.5.tgz",
- "integrity": "sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==",
+ "version": "4.53.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.1.tgz",
+ "integrity": "sha512-0++oPNgLJHBblreu0SFM7b3mAsBJBTY0Ksrmu9N6ZVrPiTkRgda52mWR7TKhHAsUb9noCjFvAw9l6ZO1yzaVbA==",
"cpu": [
"arm64"
],
@@ -1671,9 +1671,9 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
- "version": "4.52.5",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.5.tgz",
- "integrity": "sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==",
+ "version": "4.53.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.1.tgz",
+ "integrity": "sha512-VJXivz61c5uVdbmitLkDlbcTk9Or43YC2QVLRkqp86QoeFSqI81bNgjhttqhKNMKnQMWnecOCm7lZz4s+WLGpQ==",
"cpu": [
"arm64"
],
@@ -1684,9 +1684,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
- "version": "4.52.5",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.5.tgz",
- "integrity": "sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==",
+ "version": "4.53.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.1.tgz",
+ "integrity": "sha512-NmZPVTUOitCXUH6erJDzTQ/jotYw4CnkMDjCYRxNHVD9bNyfrGoIse684F9okwzKCV4AIHRbUkeTBc9F2OOH5Q==",
"cpu": [
"ia32"
],
@@ -1697,9 +1697,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
- "version": "4.52.5",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.5.tgz",
- "integrity": "sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==",
+ "version": "4.53.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.1.tgz",
+ "integrity": "sha512-2SNj7COIdAf6yliSpLdLG8BEsp5lgzRehgfkP0Av8zKfQFKku6JcvbobvHASPJu4f3BFxej5g+HuQPvqPhHvpQ==",
"cpu": [
"x64"
],
@@ -1710,9 +1710,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
- "version": "4.52.5",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.5.tgz",
- "integrity": "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==",
+ "version": "4.53.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.1.tgz",
+ "integrity": "sha512-rLarc1Ofcs3DHtgSzFO31pZsCh8g05R2azN1q3fF+H423Co87My0R+tazOEvYVKXSLh8C4LerMK41/K7wlklcg==",
"cpu": [
"x64"
],
@@ -2006,9 +2006,9 @@
}
},
"node_modules/@tailwindcss/node": {
- "version": "4.1.16",
- "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.16.tgz",
- "integrity": "sha512-BX5iaSsloNuvKNHRN3k2RcCuTEgASTo77mofW0vmeHkfrDWaoFAFvNHpEgtu0eqyypcyiBkDWzSMxJhp3AUVcw==",
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.17.tgz",
+ "integrity": "sha512-csIkHIgLb3JisEFQ0vxr2Y57GUNYh447C8xzwj89U/8fdW8LhProdxvnVH6U8M2Y73QKiTIH+LWbK3V2BBZsAg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2016,39 +2016,39 @@
"enhanced-resolve": "^5.18.3",
"jiti": "^2.6.1",
"lightningcss": "1.30.2",
- "magic-string": "^0.30.19",
+ "magic-string": "^0.30.21",
"source-map-js": "^1.2.1",
- "tailwindcss": "4.1.16"
+ "tailwindcss": "4.1.17"
}
},
"node_modules/@tailwindcss/oxide": {
- "version": "4.1.16",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.16.tgz",
- "integrity": "sha512-2OSv52FRuhdlgyOQqgtQHuCgXnS8nFSYRp2tJ+4WZXKgTxqPy7SMSls8c3mPT5pkZ17SBToGM5LHEJBO7miEdg==",
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.17.tgz",
+ "integrity": "sha512-F0F7d01fmkQhsTjXezGBLdrl1KresJTcI3DB8EkScCldyKp3Msz4hub4uyYaVnk88BAS1g5DQjjF6F5qczheLA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 10"
},
"optionalDependencies": {
- "@tailwindcss/oxide-android-arm64": "4.1.16",
- "@tailwindcss/oxide-darwin-arm64": "4.1.16",
- "@tailwindcss/oxide-darwin-x64": "4.1.16",
- "@tailwindcss/oxide-freebsd-x64": "4.1.16",
- "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.16",
- "@tailwindcss/oxide-linux-arm64-gnu": "4.1.16",
- "@tailwindcss/oxide-linux-arm64-musl": "4.1.16",
- "@tailwindcss/oxide-linux-x64-gnu": "4.1.16",
- "@tailwindcss/oxide-linux-x64-musl": "4.1.16",
- "@tailwindcss/oxide-wasm32-wasi": "4.1.16",
- "@tailwindcss/oxide-win32-arm64-msvc": "4.1.16",
- "@tailwindcss/oxide-win32-x64-msvc": "4.1.16"
+ "@tailwindcss/oxide-android-arm64": "4.1.17",
+ "@tailwindcss/oxide-darwin-arm64": "4.1.17",
+ "@tailwindcss/oxide-darwin-x64": "4.1.17",
+ "@tailwindcss/oxide-freebsd-x64": "4.1.17",
+ "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.17",
+ "@tailwindcss/oxide-linux-arm64-gnu": "4.1.17",
+ "@tailwindcss/oxide-linux-arm64-musl": "4.1.17",
+ "@tailwindcss/oxide-linux-x64-gnu": "4.1.17",
+ "@tailwindcss/oxide-linux-x64-musl": "4.1.17",
+ "@tailwindcss/oxide-wasm32-wasi": "4.1.17",
+ "@tailwindcss/oxide-win32-arm64-msvc": "4.1.17",
+ "@tailwindcss/oxide-win32-x64-msvc": "4.1.17"
}
},
"node_modules/@tailwindcss/oxide-android-arm64": {
- "version": "4.1.16",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.16.tgz",
- "integrity": "sha512-8+ctzkjHgwDJ5caq9IqRSgsP70xhdhJvm+oueS/yhD5ixLhqTw9fSL1OurzMUhBwE5zK26FXLCz2f/RtkISqHA==",
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.17.tgz",
+ "integrity": "sha512-BMqpkJHgOZ5z78qqiGE6ZIRExyaHyuxjgrJ6eBO5+hfrfGkuya0lYfw8fRHG77gdTjWkNWEEm+qeG2cDMxArLQ==",
"cpu": [
"arm64"
],
@@ -2063,9 +2063,9 @@
}
},
"node_modules/@tailwindcss/oxide-darwin-arm64": {
- "version": "4.1.16",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.16.tgz",
- "integrity": "sha512-C3oZy5042v2FOALBZtY0JTDnGNdS6w7DxL/odvSny17ORUnaRKhyTse8xYi3yKGyfnTUOdavRCdmc8QqJYwFKA==",
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.17.tgz",
+ "integrity": "sha512-EquyumkQweUBNk1zGEU/wfZo2qkp/nQKRZM8bUYO0J+Lums5+wl2CcG1f9BgAjn/u9pJzdYddHWBiFXJTcxmOg==",
"cpu": [
"arm64"
],
@@ -2080,9 +2080,9 @@
}
},
"node_modules/@tailwindcss/oxide-darwin-x64": {
- "version": "4.1.16",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.16.tgz",
- "integrity": "sha512-vjrl/1Ub9+JwU6BP0emgipGjowzYZMjbWCDqwA2Z4vCa+HBSpP4v6U2ddejcHsolsYxwL5r4bPNoamlV0xDdLg==",
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.17.tgz",
+ "integrity": "sha512-gdhEPLzke2Pog8s12oADwYu0IAw04Y2tlmgVzIN0+046ytcgx8uZmCzEg4VcQh+AHKiS7xaL8kGo/QTiNEGRog==",
"cpu": [
"x64"
],
@@ -2097,9 +2097,9 @@
}
},
"node_modules/@tailwindcss/oxide-freebsd-x64": {
- "version": "4.1.16",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.16.tgz",
- "integrity": "sha512-TSMpPYpQLm+aR1wW5rKuUuEruc/oOX3C7H0BTnPDn7W/eMw8W+MRMpiypKMkXZfwH8wqPIRKppuZoedTtNj2tg==",
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.17.tgz",
+ "integrity": "sha512-hxGS81KskMxML9DXsaXT1H0DyA+ZBIbyG/sSAjWNe2EDl7TkPOBI42GBV3u38itzGUOmFfCzk1iAjDXds8Oh0g==",
"cpu": [
"x64"
],
@@ -2114,9 +2114,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
- "version": "4.1.16",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.16.tgz",
- "integrity": "sha512-p0GGfRg/w0sdsFKBjMYvvKIiKy/LNWLWgV/plR4lUgrsxFAoQBFrXkZ4C0w8IOXfslB9vHK/JGASWD2IefIpvw==",
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.17.tgz",
+ "integrity": "sha512-k7jWk5E3ldAdw0cNglhjSgv501u7yrMf8oeZ0cElhxU6Y2o7f8yqelOp3fhf7evjIS6ujTI3U8pKUXV2I4iXHQ==",
"cpu": [
"arm"
],
@@ -2131,9 +2131,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
- "version": "4.1.16",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.16.tgz",
- "integrity": "sha512-DoixyMmTNO19rwRPdqviTrG1rYzpxgyYJl8RgQvdAQUzxC1ToLRqtNJpU/ATURSKgIg6uerPw2feW0aS8SNr/w==",
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.17.tgz",
+ "integrity": "sha512-HVDOm/mxK6+TbARwdW17WrgDYEGzmoYayrCgmLEw7FxTPLcp/glBisuyWkFz/jb7ZfiAXAXUACfyItn+nTgsdQ==",
"cpu": [
"arm64"
],
@@ -2148,9 +2148,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-arm64-musl": {
- "version": "4.1.16",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.16.tgz",
- "integrity": "sha512-H81UXMa9hJhWhaAUca6bU2wm5RRFpuHImrwXBUvPbYb+3jo32I9VIwpOX6hms0fPmA6f2pGVlybO6qU8pF4fzQ==",
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.17.tgz",
+ "integrity": "sha512-HvZLfGr42i5anKtIeQzxdkw/wPqIbpeZqe7vd3V9vI3RQxe3xU1fLjss0TjyhxWcBaipk7NYwSrwTwK1hJARMg==",
"cpu": [
"arm64"
],
@@ -2165,9 +2165,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-x64-gnu": {
- "version": "4.1.16",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.16.tgz",
- "integrity": "sha512-ZGHQxDtFC2/ruo7t99Qo2TTIvOERULPl5l0K1g0oK6b5PGqjYMga+FcY1wIUnrUxY56h28FxybtDEla+ICOyew==",
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.17.tgz",
+ "integrity": "sha512-M3XZuORCGB7VPOEDH+nzpJ21XPvK5PyjlkSFkFziNHGLc5d6g3di2McAAblmaSUNl8IOmzYwLx9NsE7bplNkwQ==",
"cpu": [
"x64"
],
@@ -2182,9 +2182,9 @@
}
},
"node_modules/@tailwindcss/oxide-linux-x64-musl": {
- "version": "4.1.16",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.16.tgz",
- "integrity": "sha512-Oi1tAaa0rcKf1Og9MzKeINZzMLPbhxvm7rno5/zuP1WYmpiG0bEHq4AcRUiG2165/WUzvxkW4XDYCscZWbTLZw==",
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.17.tgz",
+ "integrity": "sha512-k7f+pf9eXLEey4pBlw+8dgfJHY4PZ5qOUFDyNf7SI6lHjQ9Zt7+NcscjpwdCEbYi6FI5c2KDTDWyf2iHcCSyyQ==",
"cpu": [
"x64"
],
@@ -2199,9 +2199,9 @@
}
},
"node_modules/@tailwindcss/oxide-wasm32-wasi": {
- "version": "4.1.16",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.16.tgz",
- "integrity": "sha512-B01u/b8LteGRwucIBmCQ07FVXLzImWESAIMcUU6nvFt/tYsQ6IHz8DmZ5KtvmwxD+iTYBtM1xwoGXswnlu9v0Q==",
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.17.tgz",
+ "integrity": "sha512-cEytGqSSoy7zK4JRWiTCx43FsKP/zGr0CsuMawhH67ONlH+T79VteQeJQRO/X7L0juEUA8ZyuYikcRBf0vsxhg==",
"bundleDependencies": [
"@napi-rs/wasm-runtime",
"@emnapi/core",
@@ -2217,8 +2217,8 @@
"license": "MIT",
"optional": true,
"dependencies": {
- "@emnapi/core": "^1.5.0",
- "@emnapi/runtime": "^1.5.0",
+ "@emnapi/core": "^1.6.0",
+ "@emnapi/runtime": "^1.6.0",
"@emnapi/wasi-threads": "^1.1.0",
"@napi-rs/wasm-runtime": "^1.0.7",
"@tybys/wasm-util": "^0.10.1",
@@ -2229,9 +2229,9 @@
}
},
"node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
- "version": "4.1.16",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.16.tgz",
- "integrity": "sha512-zX+Q8sSkGj6HKRTMJXuPvOcP8XfYON24zJBRPlszcH1Np7xuHXhWn8qfFjIujVzvH3BHU+16jBXwgpl20i+v9A==",
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.17.tgz",
+ "integrity": "sha512-JU5AHr7gKbZlOGvMdb4722/0aYbU+tN6lv1kONx0JK2cGsh7g148zVWLM0IKR3NeKLv+L90chBVYcJ8uJWbC9A==",
"cpu": [
"arm64"
],
@@ -2246,9 +2246,9 @@
}
},
"node_modules/@tailwindcss/oxide-win32-x64-msvc": {
- "version": "4.1.16",
- "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.16.tgz",
- "integrity": "sha512-m5dDFJUEejbFqP+UXVstd4W/wnxA4F61q8SoL+mqTypId2T2ZpuxosNSgowiCnLp2+Z+rivdU0AqpfgiD7yCBg==",
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.17.tgz",
+ "integrity": "sha512-SKWM4waLuqx0IH+FMDUw6R66Hu4OuTALFgnleKbqhgGU30DY20NORZMZUKgLRjQXNN2TLzKvh48QXTig4h4bGw==",
"cpu": [
"x64"
],
@@ -2263,17 +2263,17 @@
}
},
"node_modules/@tailwindcss/postcss": {
- "version": "4.1.16",
- "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.16.tgz",
- "integrity": "sha512-Qn3SFGPXYQMKR/UtqS+dqvPrzEeBZHrFA92maT4zijCVggdsXnDBMsPFJo1eArX3J+O+Gi+8pV4PkqjLCNBk3A==",
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.17.tgz",
+ "integrity": "sha512-+nKl9N9mN5uJ+M7dBOOCzINw94MPstNR/GtIhz1fpZysxL/4a+No64jCBD6CPN+bIHWFx3KWuu8XJRrj/572Dw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
- "@tailwindcss/node": "4.1.16",
- "@tailwindcss/oxide": "4.1.16",
+ "@tailwindcss/node": "4.1.17",
+ "@tailwindcss/oxide": "4.1.17",
"postcss": "^8.4.41",
- "tailwindcss": "4.1.16"
+ "tailwindcss": "4.1.17"
}
},
"node_modules/@tailwindcss/typography": {
@@ -2290,15 +2290,15 @@
}
},
"node_modules/@tailwindcss/vite": {
- "version": "4.1.16",
- "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.16.tgz",
- "integrity": "sha512-bbguNBcDxsRmi9nnlWJxhfDWamY3lmcyACHcdO1crxfzuLpOhHLLtEIN/nCbbAtj5rchUgQD17QVAKi1f7IsKg==",
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.1.17.tgz",
+ "integrity": "sha512-4+9w8ZHOiGnpcGI6z1TVVfWaX/koK7fKeSYF3qlYg2xpBtbteP2ddBxiarL+HVgfSJGeK5RIxRQmKm4rTJJAwA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@tailwindcss/node": "4.1.16",
- "@tailwindcss/oxide": "4.1.16",
- "tailwindcss": "4.1.16"
+ "@tailwindcss/node": "4.1.17",
+ "@tailwindcss/oxide": "4.1.17",
+ "tailwindcss": "4.1.17"
},
"peerDependencies": {
"vite": "^5.2.0 || ^6 || ^7"
@@ -3221,9 +3221,9 @@
}
},
"node_modules/caniuse-lite": {
- "version": "1.0.30001753",
- "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001753.tgz",
- "integrity": "sha512-Bj5H35MD/ebaOV4iDLqPEtiliTN29qkGtEHCwawWn4cYm+bPJM2NsaP30vtZcnERClMzp52J4+aw2UNbK4o+zw==",
+ "version": "1.0.30001754",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001754.tgz",
+ "integrity": "sha512-x6OeBXueoAceOmotzx3PO4Zpt4rzpeIFsSr6AAePTZxSkXiYDUmpypEl7e2+8NCd9bD7bXjqyef8CJYPC1jfxg==",
"dev": true,
"funding": [
{
@@ -3695,9 +3695,9 @@
}
},
"node_modules/electron-to-chromium": {
- "version": "1.5.245",
- "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.245.tgz",
- "integrity": "sha512-rdmGfW47ZhL/oWEJAY4qxRtdly2B98ooTJ0pdEI4jhVLZ6tNf8fPtov2wS1IRKwFJT92le3x4Knxiwzl7cPPpQ==",
+ "version": "1.5.248",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.248.tgz",
+ "integrity": "sha512-zsur2yunphlyAO4gIubdJEXCK6KOVvtpiuDfCIqbM9FjcnMYiyn0ICa3hWfPr0nc41zcLWobgy1iL7VvoOyA2Q==",
"dev": true,
"license": "ISC"
},
@@ -6743,9 +6743,9 @@
}
},
"node_modules/rollup": {
- "version": "4.52.5",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz",
- "integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==",
+ "version": "4.53.1",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.1.tgz",
+ "integrity": "sha512-n2I0V0lN3E9cxxMqBCT3opWOiQBzRN7UG60z/WDKqdX2zHUS/39lezBcsckZFsV6fUTSnfqI7kHf60jDAPGKug==",
"license": "MIT",
"dependencies": {
"@types/estree": "1.0.8"
@@ -6758,28 +6758,28 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
- "@rollup/rollup-android-arm-eabi": "4.52.5",
- "@rollup/rollup-android-arm64": "4.52.5",
- "@rollup/rollup-darwin-arm64": "4.52.5",
- "@rollup/rollup-darwin-x64": "4.52.5",
- "@rollup/rollup-freebsd-arm64": "4.52.5",
- "@rollup/rollup-freebsd-x64": "4.52.5",
- "@rollup/rollup-linux-arm-gnueabihf": "4.52.5",
- "@rollup/rollup-linux-arm-musleabihf": "4.52.5",
- "@rollup/rollup-linux-arm64-gnu": "4.52.5",
- "@rollup/rollup-linux-arm64-musl": "4.52.5",
- "@rollup/rollup-linux-loong64-gnu": "4.52.5",
- "@rollup/rollup-linux-ppc64-gnu": "4.52.5",
- "@rollup/rollup-linux-riscv64-gnu": "4.52.5",
- "@rollup/rollup-linux-riscv64-musl": "4.52.5",
- "@rollup/rollup-linux-s390x-gnu": "4.52.5",
- "@rollup/rollup-linux-x64-gnu": "4.52.5",
- "@rollup/rollup-linux-x64-musl": "4.52.5",
- "@rollup/rollup-openharmony-arm64": "4.52.5",
- "@rollup/rollup-win32-arm64-msvc": "4.52.5",
- "@rollup/rollup-win32-ia32-msvc": "4.52.5",
- "@rollup/rollup-win32-x64-gnu": "4.52.5",
- "@rollup/rollup-win32-x64-msvc": "4.52.5",
+ "@rollup/rollup-android-arm-eabi": "4.53.1",
+ "@rollup/rollup-android-arm64": "4.53.1",
+ "@rollup/rollup-darwin-arm64": "4.53.1",
+ "@rollup/rollup-darwin-x64": "4.53.1",
+ "@rollup/rollup-freebsd-arm64": "4.53.1",
+ "@rollup/rollup-freebsd-x64": "4.53.1",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.53.1",
+ "@rollup/rollup-linux-arm-musleabihf": "4.53.1",
+ "@rollup/rollup-linux-arm64-gnu": "4.53.1",
+ "@rollup/rollup-linux-arm64-musl": "4.53.1",
+ "@rollup/rollup-linux-loong64-gnu": "4.53.1",
+ "@rollup/rollup-linux-ppc64-gnu": "4.53.1",
+ "@rollup/rollup-linux-riscv64-gnu": "4.53.1",
+ "@rollup/rollup-linux-riscv64-musl": "4.53.1",
+ "@rollup/rollup-linux-s390x-gnu": "4.53.1",
+ "@rollup/rollup-linux-x64-gnu": "4.53.1",
+ "@rollup/rollup-linux-x64-musl": "4.53.1",
+ "@rollup/rollup-openharmony-arm64": "4.53.1",
+ "@rollup/rollup-win32-arm64-msvc": "4.53.1",
+ "@rollup/rollup-win32-ia32-msvc": "4.53.1",
+ "@rollup/rollup-win32-x64-gnu": "4.53.1",
+ "@rollup/rollup-win32-x64-msvc": "4.53.1",
"fsevents": "~2.3.2"
}
},
@@ -7230,11 +7230,10 @@
}
},
"node_modules/tailwindcss": {
- "version": "4.1.16",
- "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.16.tgz",
- "integrity": "sha512-pONL5awpaQX4LN5eiv7moSiSPd/DLDzKVRJz8Q9PgzmAdd1R4307GQS2ZpfiN7ZmekdQrfhZZiSE5jkLR4WNaA==",
- "license": "MIT",
- "peer": true
+ "version": "4.1.17",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.17.tgz",
+ "integrity": "sha512-j9Ee2YjuQqYT9bbRTfTZht9W/ytp5H+jJpZKiYdP/bpnXARAuELt9ofP0lPnmHjbga7SNQIxdTAXCmtKVYjN+Q==",
+ "license": "MIT"
},
"node_modules/tapable": {
"version": "2.3.0",
@@ -7650,9 +7649,9 @@
}
},
"node_modules/vite": {
- "version": "7.2.0",
- "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.0.tgz",
- "integrity": "sha512-C/Naxf8H0pBx1PA4BdpT+c/5wdqI9ILMdwjSMILw7tVIh3JsxzZqdeTLmmdaoh5MYUEOyBnM9K3o0DzoZ/fe+w==",
+ "version": "7.2.2",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz",
+ "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==",
"license": "MIT",
"peer": true,
"dependencies": {
diff --git a/ui/package.json b/ui/package.json
index 36495177..2ca7cbe1 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -1,7 +1,7 @@
{
"name": "kvm-ui",
"private": true,
- "version": "2025.11.05.2130",
+ "version": "2025.11.07.2130",
"type": "module",
"engines": {
"node": "^22.20.0"
@@ -71,9 +71,9 @@
"@inlang/plugin-message-format": "^4.0.0",
"@inlang/sdk": "^2.4.9",
"@tailwindcss/forms": "^0.5.10",
- "@tailwindcss/postcss": "^4.1.16",
+ "@tailwindcss/postcss": "^4.1.17",
"@tailwindcss/typography": "^0.5.19",
- "@tailwindcss/vite": "^4.1.16",
+ "@tailwindcss/vite": "^4.1.17",
"@types/react": "^19.2.2",
"@types/react-dom": "^19.2.2",
"@types/semver": "^7.7.1",
@@ -93,7 +93,7 @@
"postcss": "^8.5.6",
"prettier": "^3.6.2",
"prettier-plugin-tailwindcss": "^0.7.1",
- "tailwindcss": "^4.1.16",
+ "tailwindcss": "^4.1.17",
"typescript": "^5.9.3",
"vite": "^7.2.0",
"vite-tsconfig-paths": "^5.1.4"
diff --git a/ui/src/components/FailSafeModeBanner.tsx b/ui/src/components/FailSafeModeBanner.tsx
new file mode 100644
index 00000000..04fddc67
--- /dev/null
+++ b/ui/src/components/FailSafeModeBanner.tsx
@@ -0,0 +1,30 @@
+import { LuTriangleAlert } from "react-icons/lu";
+
+import Card from "@components/Card";
+
+interface FailsafeModeBannerProps {
+ reason: string;
+}
+
+export function FailsafeModeBanner({ reason }: FailsafeModeBannerProps) {
+ const getReasonMessage = () => {
+ switch (reason) {
+ case "video":
+ return "Failsafe Mode Active: Video-related settings are currently unavailable";
+ default:
+ return "Failsafe Mode Active: Some settings may be unavailable";
+ }
+ };
+
+ return (
+
+ {getReasonMessage()}
+
{message}
+