diff --git a/client/uiwails/.gitignore b/client/uiwails/.gitignore
new file mode 100644
index 000000000..4a35222df
--- /dev/null
+++ b/client/uiwails/.gitignore
@@ -0,0 +1,3 @@
+frontend/node_modules/
+frontend/dist/
+bin/
diff --git a/client/uiwails/.task/checksum/build-frontend--DEV-- b/client/uiwails/.task/checksum/build-frontend--DEV--
new file mode 100644
index 000000000..7cb530d8b
--- /dev/null
+++ b/client/uiwails/.task/checksum/build-frontend--DEV--
@@ -0,0 +1 @@
+4340708917a9379498dcca2d1bd8c665
diff --git a/client/uiwails/.task/checksum/linux-common-install-frontend-deps b/client/uiwails/.task/checksum/linux-common-install-frontend-deps
new file mode 100644
index 000000000..21f7acb7d
--- /dev/null
+++ b/client/uiwails/.task/checksum/linux-common-install-frontend-deps
@@ -0,0 +1 @@
+f88f16cee21f42c24ff6b3c1410b0ddd
diff --git a/client/uiwails/Taskfile.yml b/client/uiwails/Taskfile.yml
new file mode 100644
index 000000000..175d61c67
--- /dev/null
+++ b/client/uiwails/Taskfile.yml
@@ -0,0 +1,32 @@
+version: '3'
+
+includes:
+ common: ./build/Taskfile.yml
+ linux: ./build/linux/Taskfile.yml
+ darwin: ./build/darwin/Taskfile.yml
+
+vars:
+ APP_NAME: "netbird-ui"
+ BIN_DIR: "bin"
+ VITE_PORT: '{{.WAILS_VITE_PORT | default 9245}}'
+
+tasks:
+ build:
+ summary: Builds the application
+ cmds:
+ - task: "{{OS}}:build"
+
+ package:
+ summary: Packages a production build of the application
+ cmds:
+ - task: "{{OS}}:package"
+
+ run:
+ summary: Runs the application
+ cmds:
+ - task: "{{OS}}:run"
+
+ dev:
+ summary: Runs the application in development mode
+ cmds:
+ - wails3 dev -config ./build/config.yml -port {{.VITE_PORT}}
diff --git a/client/uiwails/assets/connected.png b/client/uiwails/assets/connected.png
new file mode 100644
index 000000000..7dd2ab01a
Binary files /dev/null and b/client/uiwails/assets/connected.png differ
diff --git a/client/uiwails/assets/disconnected.png b/client/uiwails/assets/disconnected.png
new file mode 100644
index 000000000..421632b52
Binary files /dev/null and b/client/uiwails/assets/disconnected.png differ
diff --git a/client/uiwails/assets/netbird-disconnected.ico b/client/uiwails/assets/netbird-disconnected.ico
new file mode 100644
index 000000000..812e9d283
Binary files /dev/null and b/client/uiwails/assets/netbird-disconnected.ico differ
diff --git a/client/uiwails/assets/netbird-disconnected.png b/client/uiwails/assets/netbird-disconnected.png
new file mode 100644
index 000000000..79d4775ea
Binary files /dev/null and b/client/uiwails/assets/netbird-disconnected.png differ
diff --git a/client/uiwails/assets/netbird-systemtray-connected-dark.ico b/client/uiwails/assets/netbird-systemtray-connected-dark.ico
new file mode 100644
index 000000000..0db8a0862
Binary files /dev/null and b/client/uiwails/assets/netbird-systemtray-connected-dark.ico differ
diff --git a/client/uiwails/assets/netbird-systemtray-connected-dark.png b/client/uiwails/assets/netbird-systemtray-connected-dark.png
new file mode 100644
index 000000000..f18a929a0
Binary files /dev/null and b/client/uiwails/assets/netbird-systemtray-connected-dark.png differ
diff --git a/client/uiwails/assets/netbird-systemtray-connected-macos.png b/client/uiwails/assets/netbird-systemtray-connected-macos.png
new file mode 100644
index 000000000..ead210250
Binary files /dev/null and b/client/uiwails/assets/netbird-systemtray-connected-macos.png differ
diff --git a/client/uiwails/assets/netbird-systemtray-connected.ico b/client/uiwails/assets/netbird-systemtray-connected.ico
new file mode 100644
index 000000000..c16bec3f5
Binary files /dev/null and b/client/uiwails/assets/netbird-systemtray-connected.ico differ
diff --git a/client/uiwails/assets/netbird-systemtray-connected.png b/client/uiwails/assets/netbird-systemtray-connected.png
new file mode 100644
index 000000000..4258a5c1c
Binary files /dev/null and b/client/uiwails/assets/netbird-systemtray-connected.png differ
diff --git a/client/uiwails/assets/netbird-systemtray-connecting-dark.ico b/client/uiwails/assets/netbird-systemtray-connecting-dark.ico
new file mode 100644
index 000000000..615d40f07
Binary files /dev/null and b/client/uiwails/assets/netbird-systemtray-connecting-dark.ico differ
diff --git a/client/uiwails/assets/netbird-systemtray-connecting-dark.png b/client/uiwails/assets/netbird-systemtray-connecting-dark.png
new file mode 100644
index 000000000..a665eb61c
Binary files /dev/null and b/client/uiwails/assets/netbird-systemtray-connecting-dark.png differ
diff --git a/client/uiwails/assets/netbird-systemtray-connecting-macos.png b/client/uiwails/assets/netbird-systemtray-connecting-macos.png
new file mode 100644
index 000000000..0fe7fa0db
Binary files /dev/null and b/client/uiwails/assets/netbird-systemtray-connecting-macos.png differ
diff --git a/client/uiwails/assets/netbird-systemtray-connecting.ico b/client/uiwails/assets/netbird-systemtray-connecting.ico
new file mode 100644
index 000000000..4e4c3a9b1
Binary files /dev/null and b/client/uiwails/assets/netbird-systemtray-connecting.ico differ
diff --git a/client/uiwails/assets/netbird-systemtray-connecting.png b/client/uiwails/assets/netbird-systemtray-connecting.png
new file mode 100644
index 000000000..4f607c997
Binary files /dev/null and b/client/uiwails/assets/netbird-systemtray-connecting.png differ
diff --git a/client/uiwails/assets/netbird-systemtray-disconnected-macos.png b/client/uiwails/assets/netbird-systemtray-disconnected-macos.png
new file mode 100644
index 000000000..36b9a488f
Binary files /dev/null and b/client/uiwails/assets/netbird-systemtray-disconnected-macos.png differ
diff --git a/client/uiwails/assets/netbird-systemtray-disconnected.ico b/client/uiwails/assets/netbird-systemtray-disconnected.ico
new file mode 100644
index 000000000..dcb9f4bf8
Binary files /dev/null and b/client/uiwails/assets/netbird-systemtray-disconnected.ico differ
diff --git a/client/uiwails/assets/netbird-systemtray-disconnected.png b/client/uiwails/assets/netbird-systemtray-disconnected.png
new file mode 100644
index 000000000..a92e9ed4c
Binary files /dev/null and b/client/uiwails/assets/netbird-systemtray-disconnected.png differ
diff --git a/client/uiwails/assets/netbird-systemtray-error-dark.ico b/client/uiwails/assets/netbird-systemtray-error-dark.ico
new file mode 100644
index 000000000..083816188
Binary files /dev/null and b/client/uiwails/assets/netbird-systemtray-error-dark.ico differ
diff --git a/client/uiwails/assets/netbird-systemtray-error-dark.png b/client/uiwails/assets/netbird-systemtray-error-dark.png
new file mode 100644
index 000000000..969554b16
Binary files /dev/null and b/client/uiwails/assets/netbird-systemtray-error-dark.png differ
diff --git a/client/uiwails/assets/netbird-systemtray-error-macos.png b/client/uiwails/assets/netbird-systemtray-error-macos.png
new file mode 100644
index 000000000..9a9998bcf
Binary files /dev/null and b/client/uiwails/assets/netbird-systemtray-error-macos.png differ
diff --git a/client/uiwails/assets/netbird-systemtray-error.ico b/client/uiwails/assets/netbird-systemtray-error.ico
new file mode 100644
index 000000000..1abc45c2a
Binary files /dev/null and b/client/uiwails/assets/netbird-systemtray-error.ico differ
diff --git a/client/uiwails/assets/netbird-systemtray-error.png b/client/uiwails/assets/netbird-systemtray-error.png
new file mode 100644
index 000000000..722342989
Binary files /dev/null and b/client/uiwails/assets/netbird-systemtray-error.png differ
diff --git a/client/uiwails/assets/netbird-systemtray-update-connected-dark.ico b/client/uiwails/assets/netbird-systemtray-update-connected-dark.ico
new file mode 100644
index 000000000..b11bb5492
Binary files /dev/null and b/client/uiwails/assets/netbird-systemtray-update-connected-dark.ico differ
diff --git a/client/uiwails/assets/netbird-systemtray-update-connected-dark.png b/client/uiwails/assets/netbird-systemtray-update-connected-dark.png
new file mode 100644
index 000000000..52ae621ac
Binary files /dev/null and b/client/uiwails/assets/netbird-systemtray-update-connected-dark.png differ
diff --git a/client/uiwails/assets/netbird-systemtray-update-connected-macos.png b/client/uiwails/assets/netbird-systemtray-update-connected-macos.png
new file mode 100644
index 000000000..8a6b2f2db
Binary files /dev/null and b/client/uiwails/assets/netbird-systemtray-update-connected-macos.png differ
diff --git a/client/uiwails/assets/netbird-systemtray-update-connected.ico b/client/uiwails/assets/netbird-systemtray-update-connected.ico
new file mode 100644
index 000000000..d3ce2f0f3
Binary files /dev/null and b/client/uiwails/assets/netbird-systemtray-update-connected.ico differ
diff --git a/client/uiwails/assets/netbird-systemtray-update-connected.png b/client/uiwails/assets/netbird-systemtray-update-connected.png
new file mode 100644
index 000000000..90bb0b7f1
Binary files /dev/null and b/client/uiwails/assets/netbird-systemtray-update-connected.png differ
diff --git a/client/uiwails/assets/netbird-systemtray-update-disconnected-dark.ico b/client/uiwails/assets/netbird-systemtray-update-disconnected-dark.ico
new file mode 100644
index 000000000..123237f66
Binary files /dev/null and b/client/uiwails/assets/netbird-systemtray-update-disconnected-dark.ico differ
diff --git a/client/uiwails/assets/netbird-systemtray-update-disconnected-dark.png b/client/uiwails/assets/netbird-systemtray-update-disconnected-dark.png
new file mode 100644
index 000000000..9e05351f1
Binary files /dev/null and b/client/uiwails/assets/netbird-systemtray-update-disconnected-dark.png differ
diff --git a/client/uiwails/assets/netbird-systemtray-update-disconnected-macos.png b/client/uiwails/assets/netbird-systemtray-update-disconnected-macos.png
new file mode 100644
index 000000000..8b190034e
Binary files /dev/null and b/client/uiwails/assets/netbird-systemtray-update-disconnected-macos.png differ
diff --git a/client/uiwails/assets/netbird-systemtray-update-disconnected.ico b/client/uiwails/assets/netbird-systemtray-update-disconnected.ico
new file mode 100644
index 000000000..968dc4105
Binary files /dev/null and b/client/uiwails/assets/netbird-systemtray-update-disconnected.ico differ
diff --git a/client/uiwails/assets/netbird-systemtray-update-disconnected.png b/client/uiwails/assets/netbird-systemtray-update-disconnected.png
new file mode 100644
index 000000000..3adc39034
Binary files /dev/null and b/client/uiwails/assets/netbird-systemtray-update-disconnected.png differ
diff --git a/client/uiwails/assets/netbird.ico b/client/uiwails/assets/netbird.ico
new file mode 100644
index 000000000..2bab8a503
Binary files /dev/null and b/client/uiwails/assets/netbird.ico differ
diff --git a/client/uiwails/assets/netbird.png b/client/uiwails/assets/netbird.png
new file mode 100644
index 000000000..a92e9ed4c
Binary files /dev/null and b/client/uiwails/assets/netbird.png differ
diff --git a/client/uiwails/build/Taskfile.yml b/client/uiwails/build/Taskfile.yml
new file mode 100644
index 000000000..4cf7e5d66
--- /dev/null
+++ b/client/uiwails/build/Taskfile.yml
@@ -0,0 +1,61 @@
+version: '3'
+
+tasks:
+ go:mod:tidy:
+ summary: Runs `go mod tidy`
+ internal: true
+ cmds:
+ - go mod tidy
+
+ install:frontend:deps:
+ summary: Install frontend dependencies
+ dir: frontend
+ sources:
+ - package.json
+ - package-lock.json
+ generates:
+ - node_modules
+ preconditions:
+ - sh: npm version
+ msg: "Looks like npm isn't installed. Npm is part of the Node installer: https://nodejs.org/en/download/"
+ cmds:
+ - npm install
+
+ build:frontend:
+ label: build:frontend (DEV={{.DEV}})
+ summary: Build the frontend project
+ dir: frontend
+ sources:
+ - "**/*"
+ - exclude: node_modules/**/*
+ generates:
+ - dist/**/*
+ deps:
+ - task: install:frontend:deps
+ cmds:
+ - npm run {{.BUILD_COMMAND}} -q
+ env:
+ PRODUCTION: '{{if eq .DEV "true"}}false{{else}}true{{end}}'
+ vars:
+ BUILD_COMMAND: '{{if eq .DEV "true"}}build:dev{{else}}build{{end}}'
+
+ generate:icons:
+ summary: Generates Windows `.ico` and Mac `.icns` from an image
+ dir: build
+ sources:
+ - "appicon.png"
+ generates:
+ - "icons.icns"
+ - "icon.ico"
+ cmds:
+ - echo "Icon generation skipped (no appicon.png)"
+ status:
+ - test ! -f appicon.png
+
+ dev:frontend:
+ summary: Runs the frontend in development mode
+ dir: frontend
+ deps:
+ - task: install:frontend:deps
+ cmds:
+ - npm run dev -- --port {{.VITE_PORT}} --strictPort
diff --git a/client/uiwails/build/build-ui-linux.sh b/client/uiwails/build/build-ui-linux.sh
new file mode 100755
index 000000000..b82c89650
--- /dev/null
+++ b/client/uiwails/build/build-ui-linux.sh
@@ -0,0 +1,24 @@
+#!/bin/bash
+# Build script for NetBird Wails v3 on Linux
+set -e
+
+echo "Installing system dependencies for Wails v3 on Linux..."
+sudo apt-get update
+sudo apt-get install -y \
+ libayatana-appindicator3-dev \
+ gcc \
+ libgtk-3-dev \
+ libwebkit2gtk-4.1-dev \
+ libglib2.0-dev \
+ libsoup-3.0-dev \
+ libx11-dev \
+ npm
+
+echo "Installing wails3 CLI..."
+go install github.com/wailsapp/wails/v3/cmd/wails3@v3.0.0-alpha.72
+
+echo "Building fancyui..."
+cd "$(dirname "$0")/.."
+wails3 build
+
+echo "Build complete."
diff --git a/client/uiwails/build/linux/Taskfile.yml b/client/uiwails/build/linux/Taskfile.yml
new file mode 100644
index 000000000..aec6a246d
--- /dev/null
+++ b/client/uiwails/build/linux/Taskfile.yml
@@ -0,0 +1,35 @@
+version: '3'
+
+includes:
+ common: ../Taskfile.yml
+
+tasks:
+ build:
+ summary: Builds the application for Linux
+ cmds:
+ - task: build:native
+ vars:
+ DEV: '{{.DEV}}'
+ OUTPUT: '{{.OUTPUT}}'
+
+ build:native:
+ summary: Builds the application natively on Linux
+ internal: true
+ deps:
+ - task: common:build:frontend
+ vars:
+ DEV:
+ ref: .DEV
+ cmds:
+ - go build {{.BUILD_FLAGS}} -o {{.OUTPUT}}
+ vars:
+ BUILD_FLAGS: '{{if eq .DEV "true"}}-buildvcs=false -gcflags=all="-l"{{else}}-tags production -trimpath -buildvcs=false -ldflags="-w -s"{{end}}'
+ DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}'
+ OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}'
+ env:
+ GOOS: linux
+ CGO_ENABLED: 1
+
+ run:
+ cmds:
+ - '{{.BIN_DIR}}/{{.APP_NAME}}'
diff --git a/client/uiwails/event/event.go b/client/uiwails/event/event.go
new file mode 100644
index 000000000..7d08cf7d9
--- /dev/null
+++ b/client/uiwails/event/event.go
@@ -0,0 +1,217 @@
+//go:build !(linux && 386)
+
+package event
+
+import (
+ "context"
+ "fmt"
+ "slices"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/cenkalti/backoff/v4"
+ log "github.com/sirupsen/logrus"
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/credentials/insecure"
+
+ "github.com/netbirdio/netbird/client/proto"
+ "github.com/netbirdio/netbird/version"
+)
+
+// NotifyFunc is a callback used to send desktop notifications.
+type NotifyFunc func(title, body string)
+
+// Handler is a callback invoked for each received daemon event.
+type Handler func(*proto.SystemEvent)
+
+// Manager subscribes to daemon events and dispatches them.
+type Manager struct {
+ addr string
+ notify NotifyFunc
+
+ mu sync.Mutex
+ ctx context.Context
+ cancel context.CancelFunc
+ enabled bool
+ handlers []Handler
+
+ connMu sync.Mutex
+ conn *grpc.ClientConn
+ client proto.DaemonServiceClient
+}
+
+// NewManager creates a new event Manager.
+func NewManager(addr string, notify NotifyFunc) *Manager {
+ return &Manager{
+ addr: addr,
+ notify: notify,
+ }
+}
+
+// Start begins event streaming with exponential backoff reconnection.
+func (m *Manager) Start(ctx context.Context) {
+ m.mu.Lock()
+ m.ctx, m.cancel = context.WithCancel(ctx)
+ m.mu.Unlock()
+
+ expBackOff := backoff.WithContext(&backoff.ExponentialBackOff{
+ InitialInterval: time.Second,
+ RandomizationFactor: backoff.DefaultRandomizationFactor,
+ Multiplier: backoff.DefaultMultiplier,
+ MaxInterval: 10 * time.Second,
+ MaxElapsedTime: 0,
+ Stop: backoff.Stop,
+ Clock: backoff.SystemClock,
+ }, ctx)
+
+ if err := backoff.Retry(m.streamEvents, expBackOff); err != nil {
+ log.Errorf("event stream ended: %v", err)
+ }
+}
+
+func (m *Manager) streamEvents() error {
+ m.mu.Lock()
+ ctx := m.ctx
+ m.mu.Unlock()
+
+ client, err := m.getClient()
+ if err != nil {
+ return fmt.Errorf("create client: %w", err)
+ }
+
+ stream, err := client.SubscribeEvents(ctx, &proto.SubscribeRequest{})
+ if err != nil {
+ return fmt.Errorf("subscribe events: %w", err)
+ }
+
+ log.Info("subscribed to daemon events")
+ defer log.Info("unsubscribed from daemon events")
+
+ for {
+ event, err := stream.Recv()
+ if err != nil {
+ return fmt.Errorf("receive event: %w", err)
+ }
+ m.handleEvent(event)
+ }
+}
+
+// Stop cancels the event stream and closes the connection.
+func (m *Manager) Stop() {
+ m.mu.Lock()
+ if m.cancel != nil {
+ m.cancel()
+ }
+ m.mu.Unlock()
+
+ m.connMu.Lock()
+ if m.conn != nil {
+ m.conn.Close()
+ m.conn = nil
+ m.client = nil
+ }
+ m.connMu.Unlock()
+}
+
+// SetNotificationsEnabled enables or disables desktop notifications.
+func (m *Manager) SetNotificationsEnabled(enabled bool) {
+ m.mu.Lock()
+ defer m.mu.Unlock()
+ m.enabled = enabled
+}
+
+// AddHandler registers an event handler.
+func (m *Manager) AddHandler(h Handler) {
+ m.mu.Lock()
+ defer m.mu.Unlock()
+ m.handlers = append(m.handlers, h)
+}
+
+func (m *Manager) handleEvent(event *proto.SystemEvent) {
+ m.mu.Lock()
+ enabled := m.enabled
+ handlers := slices.Clone(m.handlers)
+ m.mu.Unlock()
+
+ // Critical events are always shown.
+ if !enabled && event.Severity != proto.SystemEvent_CRITICAL {
+ goto dispatch
+ }
+
+ if event.UserMessage != "" && m.notify != nil {
+ title := getEventTitle(event)
+ body := event.UserMessage
+ if id := event.Metadata["id"]; id != "" {
+ body += fmt.Sprintf(" ID: %s", id)
+ }
+ m.notify(title, body)
+ }
+
+dispatch:
+ for _, h := range handlers {
+ go h(event)
+ }
+}
+
+func getEventTitle(event *proto.SystemEvent) string {
+ var prefix string
+ switch event.Severity {
+ case proto.SystemEvent_CRITICAL:
+ prefix = "Critical"
+ case proto.SystemEvent_ERROR:
+ prefix = "Error"
+ case proto.SystemEvent_WARNING:
+ prefix = "Warning"
+ default:
+ prefix = "Info"
+ }
+
+ var category string
+ switch event.Category {
+ case proto.SystemEvent_DNS:
+ category = "DNS"
+ case proto.SystemEvent_NETWORK:
+ category = "Network"
+ case proto.SystemEvent_AUTHENTICATION:
+ category = "Authentication"
+ case proto.SystemEvent_CONNECTIVITY:
+ category = "Connectivity"
+ default:
+ category = "System"
+ }
+
+ return fmt.Sprintf("%s: %s", prefix, category)
+}
+
+// getClient returns a cached gRPC client, creating the connection on first use.
+func (m *Manager) getClient() (proto.DaemonServiceClient, error) {
+ m.connMu.Lock()
+ defer m.connMu.Unlock()
+
+ if m.client != nil {
+ return m.client, nil
+ }
+
+ target := m.addr
+ if strings.HasPrefix(target, "tcp://") {
+ target = strings.TrimPrefix(target, "tcp://")
+ } else if strings.HasPrefix(target, "unix://") {
+ target = "unix:" + strings.TrimPrefix(target, "unix://")
+ }
+
+ conn, err := grpc.NewClient(
+ target,
+ grpc.WithTransportCredentials(insecure.NewCredentials()),
+ grpc.WithUserAgent("netbird-fancyui/"+version.NetbirdVersion()),
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ m.conn = conn
+ m.client = proto.NewDaemonServiceClient(conn)
+ log.Debugf("event manager: gRPC connection established to %s", m.addr)
+
+ return m.client, nil
+}
diff --git a/client/uiwails/frontend/index.html b/client/uiwails/frontend/index.html
new file mode 100644
index 000000000..bac8e85ba
--- /dev/null
+++ b/client/uiwails/frontend/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ NetBird
+
+
+
+
+
+
diff --git a/client/uiwails/frontend/package-lock.json b/client/uiwails/frontend/package-lock.json
new file mode 100644
index 000000000..82e11ddac
--- /dev/null
+++ b/client/uiwails/frontend/package-lock.json
@@ -0,0 +1,2502 @@
+{
+ "name": "netbird-fancyui",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "netbird-fancyui",
+ "version": "0.0.0",
+ "dependencies": {
+ "@wailsio/runtime": "latest",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "react-router-dom": "^6.28.0"
+ },
+ "devDependencies": {
+ "@tailwindcss/vite": "^4.0.6",
+ "@types/react": "^18.3.12",
+ "@types/react-dom": "^18.3.1",
+ "@vitejs/plugin-react": "^4.3.4",
+ "tailwindcss": "^4.0.6",
+ "typescript": "^5.6.3",
+ "vite": "^6.0.5"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
+ "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
+ "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
+ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-compilation-targets": "^7.28.6",
+ "@babel/helper-module-transforms": "^7.28.6",
+ "@babel/helpers": "^7.28.6",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/traverse": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.29.1",
+ "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
+ "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
+ "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.28.6",
+ "@babel/helper-validator-option": "^7.27.1",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
+ "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
+ "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.28.6",
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "@babel/traverse": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-plugin-utils": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
+ "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz",
+ "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz",
+ "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.0"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-self": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
+ "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/plugin-transform-react-jsx-source": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
+ "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-plugin-utils": "^7.27.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0-0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
+ "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.28.6",
+ "@babel/parser": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
+ "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-globals": "^7.28.0",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.29.0",
+ "debug": "^4.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
+ "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
+ "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
+ "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
+ "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
+ "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
+ "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
+ "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
+ "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
+ "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
+ "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
+ "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
+ "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
+ "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
+ "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
+ "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
+ "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
+ "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
+ "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/openharmony-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
+ "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
+ "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
+ "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
+ "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
+ "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@remix-run/router": {
+ "version": "1.23.2",
+ "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz",
+ "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-beta.27",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz",
+ "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
+ "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz",
+ "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz",
+ "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz",
+ "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz",
+ "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz",
+ "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz",
+ "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz",
+ "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz",
+ "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz",
+ "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz",
+ "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loong64-musl": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz",
+ "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz",
+ "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-ppc64-musl": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz",
+ "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz",
+ "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-musl": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz",
+ "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz",
+ "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz",
+ "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz",
+ "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-openbsd-x64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz",
+ "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-openharmony-arm64": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz",
+ "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz",
+ "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz",
+ "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-gnu": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz",
+ "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz",
+ "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@tailwindcss/node": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.1.tgz",
+ "integrity": "sha512-jlx6sLk4EOwO6hHe1oCGm1Q4AN/s0rSrTTPBGPM0/RQ6Uylwq17FuU8IeJJKEjtc6K6O07zsvP+gDO6MMWo7pg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/remapping": "^2.3.5",
+ "enhanced-resolve": "^5.19.0",
+ "jiti": "^2.6.1",
+ "lightningcss": "1.31.1",
+ "magic-string": "^0.30.21",
+ "source-map-js": "^1.2.1",
+ "tailwindcss": "4.2.1"
+ }
+ },
+ "node_modules/@tailwindcss/oxide": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.2.1.tgz",
+ "integrity": "sha512-yv9jeEFWnjKCI6/T3Oq50yQEOqmpmpfzG1hcZsAOaXFQPfzWprWrlHSdGPEF3WQTi8zu8ohC9Mh9J470nT5pUw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 20"
+ },
+ "optionalDependencies": {
+ "@tailwindcss/oxide-android-arm64": "4.2.1",
+ "@tailwindcss/oxide-darwin-arm64": "4.2.1",
+ "@tailwindcss/oxide-darwin-x64": "4.2.1",
+ "@tailwindcss/oxide-freebsd-x64": "4.2.1",
+ "@tailwindcss/oxide-linux-arm-gnueabihf": "4.2.1",
+ "@tailwindcss/oxide-linux-arm64-gnu": "4.2.1",
+ "@tailwindcss/oxide-linux-arm64-musl": "4.2.1",
+ "@tailwindcss/oxide-linux-x64-gnu": "4.2.1",
+ "@tailwindcss/oxide-linux-x64-musl": "4.2.1",
+ "@tailwindcss/oxide-wasm32-wasi": "4.2.1",
+ "@tailwindcss/oxide-win32-arm64-msvc": "4.2.1",
+ "@tailwindcss/oxide-win32-x64-msvc": "4.2.1"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-android-arm64": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.2.1.tgz",
+ "integrity": "sha512-eZ7G1Zm5EC8OOKaesIKuw77jw++QJ2lL9N+dDpdQiAB/c/B2wDh0QPFHbkBVrXnwNugvrbJFk1gK2SsVjwWReg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-arm64": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.2.1.tgz",
+ "integrity": "sha512-q/LHkOstoJ7pI1J0q6djesLzRvQSIfEto148ppAd+BVQK0JYjQIFSK3JgYZJa+Yzi0DDa52ZsQx2rqytBnf8Hw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-darwin-x64": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.2.1.tgz",
+ "integrity": "sha512-/f/ozlaXGY6QLbpvd/kFTro2l18f7dHKpB+ieXz+Cijl4Mt9AI2rTrpq7V+t04nK+j9XBQHnSMdeQRhbGyt6fw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-freebsd-x64": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.2.1.tgz",
+ "integrity": "sha512-5e/AkgYJT/cpbkys/OU2Ei2jdETCLlifwm7ogMC7/hksI2fC3iiq6OcXwjibcIjPung0kRtR3TxEITkqgn0TcA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.2.1.tgz",
+ "integrity": "sha512-Uny1EcVTTmerCKt/1ZuKTkb0x8ZaiuYucg2/kImO5A5Y/kBz41/+j0gxUZl+hTF3xkWpDmHX+TaWhOtba2Fyuw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-gnu": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.2.1.tgz",
+ "integrity": "sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-arm64-musl": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.2.1.tgz",
+ "integrity": "sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-gnu": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.2.1.tgz",
+ "integrity": "sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-linux-x64-musl": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.2.1.tgz",
+ "integrity": "sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-wasm32-wasi": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.2.1.tgz",
+ "integrity": "sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==",
+ "bundleDependencies": [
+ "@napi-rs/wasm-runtime",
+ "@emnapi/core",
+ "@emnapi/runtime",
+ "@tybys/wasm-util",
+ "@emnapi/wasi-threads",
+ "tslib"
+ ],
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "^1.8.1",
+ "@emnapi/runtime": "^1.8.1",
+ "@emnapi/wasi-threads": "^1.1.0",
+ "@napi-rs/wasm-runtime": "^1.1.1",
+ "@tybys/wasm-util": "^0.10.1",
+ "tslib": "^2.8.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-arm64-msvc": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.2.1.tgz",
+ "integrity": "sha512-YlUEHRHBGnCMh4Nj4GnqQyBtsshUPdiNroZj8VPkvTZSoHsilRCwXcVKnG9kyi0ZFAS/3u+qKHBdDc81SADTRA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/oxide-win32-x64-msvc": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.2.1.tgz",
+ "integrity": "sha512-rbO34G5sMWWyrN/idLeVxAZgAKWrn5LiR3/I90Q9MkA67s6T1oB0xtTe+0heoBvHSpbU9Mk7i6uwJnpo4u21XQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 20"
+ }
+ },
+ "node_modules/@tailwindcss/vite": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/@tailwindcss/vite/-/vite-4.2.1.tgz",
+ "integrity": "sha512-TBf2sJjYeb28jD2U/OhwdW0bbOsxkWPwQ7SrqGf9sVcoYwZj7rkXljroBO9wKBut9XnmQLXanuDUeqQK0lGg/w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@tailwindcss/node": "4.2.1",
+ "@tailwindcss/oxide": "4.2.1",
+ "tailwindcss": "4.2.1"
+ },
+ "peerDependencies": {
+ "vite": "^5.2.0 || ^6 || ^7"
+ }
+ },
+ "node_modules/@types/babel__core": {
+ "version": "7.20.5",
+ "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
+ "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.20.7",
+ "@babel/types": "^7.20.7",
+ "@types/babel__generator": "*",
+ "@types/babel__template": "*",
+ "@types/babel__traverse": "*"
+ }
+ },
+ "node_modules/@types/babel__generator": {
+ "version": "7.27.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
+ "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__template": {
+ "version": "7.4.4",
+ "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
+ "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.1.0",
+ "@babel/types": "^7.0.0"
+ }
+ },
+ "node_modules/@types/babel__traverse": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
+ "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.28.2"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/prop-types": {
+ "version": "15.7.15",
+ "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
+ "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/react": {
+ "version": "18.3.28",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz",
+ "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/prop-types": "*",
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "18.3.7",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
+ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^18.0.0"
+ }
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz",
+ "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/core": "^7.28.0",
+ "@babel/plugin-transform-react-jsx-self": "^7.27.1",
+ "@babel/plugin-transform-react-jsx-source": "^7.27.1",
+ "@rolldown/pluginutils": "1.0.0-beta.27",
+ "@types/babel__core": "^7.20.5",
+ "react-refresh": "^0.17.0"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
+ }
+ },
+ "node_modules/@wailsio/runtime": {
+ "version": "3.0.0-alpha.79",
+ "resolved": "https://registry.npmjs.org/@wailsio/runtime/-/runtime-3.0.0-alpha.79.tgz",
+ "integrity": "sha512-NITzxKmJsMEruc39L166lbPJVECxzcbdqpHVqOOF7Cu/7Zqk/e3B/gNpkUjhNyo5rVb3V1wpS8oEgLUmpu1cwA==",
+ "license": "MIT"
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.10.0",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz",
+ "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.1",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
+ "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.9.0",
+ "caniuse-lite": "^1.0.30001759",
+ "electron-to-chromium": "^1.5.263",
+ "node-releases": "^2.0.27",
+ "update-browserslist-db": "^1.2.0"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001775",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001775.tgz",
+ "integrity": "sha512-s3Qv7Lht9zbVKE9XoTyRG6wVDCKdtOFIjBGg3+Yhn6JaytuNKPIjBMTMIY1AnOH3seL5mvF+x33oGAyK3hVt3A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.302",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz",
+ "integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/enhanced-resolve": {
+ "version": "5.20.0",
+ "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz",
+ "integrity": "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "graceful-fs": "^4.2.4",
+ "tapable": "^2.3.0"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.25.12",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
+ "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.25.12",
+ "@esbuild/android-arm": "0.25.12",
+ "@esbuild/android-arm64": "0.25.12",
+ "@esbuild/android-x64": "0.25.12",
+ "@esbuild/darwin-arm64": "0.25.12",
+ "@esbuild/darwin-x64": "0.25.12",
+ "@esbuild/freebsd-arm64": "0.25.12",
+ "@esbuild/freebsd-x64": "0.25.12",
+ "@esbuild/linux-arm": "0.25.12",
+ "@esbuild/linux-arm64": "0.25.12",
+ "@esbuild/linux-ia32": "0.25.12",
+ "@esbuild/linux-loong64": "0.25.12",
+ "@esbuild/linux-mips64el": "0.25.12",
+ "@esbuild/linux-ppc64": "0.25.12",
+ "@esbuild/linux-riscv64": "0.25.12",
+ "@esbuild/linux-s390x": "0.25.12",
+ "@esbuild/linux-x64": "0.25.12",
+ "@esbuild/netbsd-arm64": "0.25.12",
+ "@esbuild/netbsd-x64": "0.25.12",
+ "@esbuild/openbsd-arm64": "0.25.12",
+ "@esbuild/openbsd-x64": "0.25.12",
+ "@esbuild/openharmony-arm64": "0.25.12",
+ "@esbuild/sunos-x64": "0.25.12",
+ "@esbuild/win32-arm64": "0.25.12",
+ "@esbuild/win32-ia32": "0.25.12",
+ "@esbuild/win32-x64": "0.25.12"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/jiti": {
+ "version": "2.6.1",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
+ "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jiti": "lib/jiti-cli.mjs"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "license": "MIT"
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/lightningcss": {
+ "version": "1.31.1",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz",
+ "integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.31.1",
+ "lightningcss-darwin-arm64": "1.31.1",
+ "lightningcss-darwin-x64": "1.31.1",
+ "lightningcss-freebsd-x64": "1.31.1",
+ "lightningcss-linux-arm-gnueabihf": "1.31.1",
+ "lightningcss-linux-arm64-gnu": "1.31.1",
+ "lightningcss-linux-arm64-musl": "1.31.1",
+ "lightningcss-linux-x64-gnu": "1.31.1",
+ "lightningcss-linux-x64-musl": "1.31.1",
+ "lightningcss-win32-arm64-msvc": "1.31.1",
+ "lightningcss-win32-x64-msvc": "1.31.1"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.31.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz",
+ "integrity": "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.31.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz",
+ "integrity": "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.31.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz",
+ "integrity": "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.31.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz",
+ "integrity": "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.31.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz",
+ "integrity": "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.31.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz",
+ "integrity": "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.31.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz",
+ "integrity": "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.31.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz",
+ "integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.31.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz",
+ "integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.31.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz",
+ "integrity": "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.31.1",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz",
+ "integrity": "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.27",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
+ "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
+ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.6",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
+ "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/react": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
+ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
+ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.2"
+ },
+ "peerDependencies": {
+ "react": "^18.3.1"
+ }
+ },
+ "node_modules/react-refresh": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",
+ "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-router": {
+ "version": "6.30.3",
+ "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz",
+ "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==",
+ "license": "MIT",
+ "dependencies": {
+ "@remix-run/router": "1.23.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8"
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "6.30.3",
+ "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz",
+ "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==",
+ "license": "MIT",
+ "dependencies": {
+ "@remix-run/router": "1.23.2",
+ "react-router": "6.30.3"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8",
+ "react-dom": ">=16.8"
+ }
+ },
+ "node_modules/rollup": {
+ "version": "4.59.0",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
+ "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.8"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.59.0",
+ "@rollup/rollup-android-arm64": "4.59.0",
+ "@rollup/rollup-darwin-arm64": "4.59.0",
+ "@rollup/rollup-darwin-x64": "4.59.0",
+ "@rollup/rollup-freebsd-arm64": "4.59.0",
+ "@rollup/rollup-freebsd-x64": "4.59.0",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.59.0",
+ "@rollup/rollup-linux-arm-musleabihf": "4.59.0",
+ "@rollup/rollup-linux-arm64-gnu": "4.59.0",
+ "@rollup/rollup-linux-arm64-musl": "4.59.0",
+ "@rollup/rollup-linux-loong64-gnu": "4.59.0",
+ "@rollup/rollup-linux-loong64-musl": "4.59.0",
+ "@rollup/rollup-linux-ppc64-gnu": "4.59.0",
+ "@rollup/rollup-linux-ppc64-musl": "4.59.0",
+ "@rollup/rollup-linux-riscv64-gnu": "4.59.0",
+ "@rollup/rollup-linux-riscv64-musl": "4.59.0",
+ "@rollup/rollup-linux-s390x-gnu": "4.59.0",
+ "@rollup/rollup-linux-x64-gnu": "4.59.0",
+ "@rollup/rollup-linux-x64-musl": "4.59.0",
+ "@rollup/rollup-openbsd-x64": "4.59.0",
+ "@rollup/rollup-openharmony-arm64": "4.59.0",
+ "@rollup/rollup-win32-arm64-msvc": "4.59.0",
+ "@rollup/rollup-win32-ia32-msvc": "4.59.0",
+ "@rollup/rollup-win32-x64-gnu": "4.59.0",
+ "@rollup/rollup-win32-x64-msvc": "4.59.0",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.23.2",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
+ "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/tailwindcss": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz",
+ "integrity": "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tapable": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz",
+ "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/webpack"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/vite": {
+ "version": "6.4.1",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
+ "integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.25.0",
+ "fdir": "^6.4.4",
+ "picomatch": "^4.0.2",
+ "postcss": "^8.5.3",
+ "rollup": "^4.34.9",
+ "tinyglobby": "^0.2.13"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || ^20.0.0 || >=22.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
+ "jiti": ">=1.21.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
+ }
+ }
+}
diff --git a/client/uiwails/frontend/package.json b/client/uiwails/frontend/package.json
new file mode 100644
index 000000000..359160cf5
--- /dev/null
+++ b/client/uiwails/frontend/package.json
@@ -0,0 +1,26 @@
+{
+ "name": "netbird-fancyui",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@wailsio/runtime": "latest",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "react-router-dom": "^6.28.0"
+ },
+ "devDependencies": {
+ "@tailwindcss/vite": "^4.0.6",
+ "@types/react": "^18.3.12",
+ "@types/react-dom": "^18.3.1",
+ "@vitejs/plugin-react": "^4.3.4",
+ "tailwindcss": "^4.0.6",
+ "typescript": "^5.6.3",
+ "vite": "^6.0.5"
+ }
+}
diff --git a/client/uiwails/frontend/src/App.tsx b/client/uiwails/frontend/src/App.tsx
new file mode 100644
index 000000000..afdff120d
--- /dev/null
+++ b/client/uiwails/frontend/src/App.tsx
@@ -0,0 +1,53 @@
+import { HashRouter, Routes, Route, useNavigate } from 'react-router-dom'
+import { useEffect } from 'react'
+import { Events } from '@wailsio/runtime'
+import Status from './pages/Status'
+import Settings from './pages/Settings'
+import Networks from './pages/Networks'
+import Profiles from './pages/Profiles'
+import Peers from './pages/Peers'
+import Debug from './pages/Debug'
+import Update from './pages/Update'
+import NavBar from './components/NavBar'
+
+/**
+ * Navigator listens for the "navigate" event emitted by the Go backend
+ * and programmatically navigates the React router.
+ */
+function Navigator() {
+ const navigate = useNavigate()
+
+ useEffect(() => {
+ const unsub = Events.On('navigate', (event: { data: string[] }) => {
+ const path = event.data[0]
+ if (path) navigate(path)
+ })
+ return () => {
+ if (typeof unsub === 'function') unsub()
+ }
+ }, [navigate])
+
+ return null
+}
+
+export default function App() {
+ return (
+
+
+
+
+
+
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
+
+
+ )
+}
diff --git a/client/uiwails/frontend/src/bindings.ts b/client/uiwails/frontend/src/bindings.ts
new file mode 100644
index 000000000..1e8ab162a
--- /dev/null
+++ b/client/uiwails/frontend/src/bindings.ts
@@ -0,0 +1,126 @@
+/**
+ * Type definitions for the auto-generated Wails v3 service bindings.
+ * Run `wails3 generate bindings` to regenerate the actual TypeScript bindings
+ * from the Go service methods. These types mirror the Go structs.
+ *
+ * The actual binding files will be generated into frontend/bindings/ by the
+ * Wails CLI. This file serves as a centralized re-export and type reference.
+ */
+
+// ---- Connection service ----
+
+export interface StatusInfo {
+ status: string
+ ip: string
+ publicKey: string
+ fqdn: string
+ connectedPeers: number
+}
+
+// ---- Settings service ----
+
+export interface ConfigInfo {
+ managementUrl: string
+ adminUrl: string
+ preSharedKey: string
+ interfaceName: string
+ wireguardPort: number
+ disableAutoConnect: boolean
+ serverSshAllowed: boolean
+ rosenpassEnabled: boolean
+ rosenpassPermissive: boolean
+ lazyConnectionEnabled: boolean
+ blockInbound: boolean
+ disableNotifications: boolean
+}
+
+// ---- Network service ----
+
+export interface NetworkInfo {
+ id: string
+ range: string
+ domains: string[]
+ selected: boolean
+ resolvedIPs: Record
+}
+
+// ---- Profile service ----
+
+export interface ProfileInfo {
+ name: string
+ isActive: boolean
+}
+
+export interface ActiveProfileInfo {
+ profileName: string
+ username: string
+ email: string
+}
+
+// ---- Debug service ----
+
+export interface DebugBundleParams {
+ anonymize: boolean
+ systemInfo: boolean
+ upload: boolean
+ uploadUrl: string
+ runDurationMins: number
+ enablePersistence: boolean
+}
+
+export interface DebugBundleResult {
+ localPath: string
+ uploadedKey: string
+ uploadFailureReason: string
+}
+
+// ---- Peers service ----
+
+export interface PeerInfo {
+ ip: string
+ pubKey: string
+ fqdn: string
+ connStatus: string
+ connStatusUpdate: string
+ relayed: boolean
+ relayAddress: string
+ latencyMs: number
+ bytesRx: number
+ bytesTx: number
+ rosenpassEnabled: boolean
+ networks: string[]
+ lastHandshake: string
+ localIceType: string
+ remoteIceType: string
+ localEndpoint: string
+ remoteEndpoint: string
+}
+
+// ---- Update service ----
+
+export interface InstallerResult {
+ success: boolean
+ errorMsg: string
+}
+
+/**
+ * Wails v3 service call helper.
+ * After running `wails3 generate bindings`, use the generated functions directly.
+ * This helper wraps window.__wails.call for manual use during development.
+ */
+export async function call(service: string, method: string, ...args: unknown[]): Promise {
+ // This will be replaced by generated bindings after `wails3 generate bindings`
+ // For now, call via the Wails runtime bridge
+ const w = window as typeof window & {
+ go?: {
+ [svc: string]: {
+ [method: string]: (...args: unknown[]) => Promise
+ }
+ }
+ }
+ const svc = w.go?.[service]
+ if (!svc) throw new Error(`Service ${service} not found. Run wails3 generate bindings.`)
+ const fn = svc[method]
+ if (!fn) throw new Error(`Method ${service}.${method} not found.`)
+ return fn(...args)
+}
diff --git a/client/uiwails/frontend/src/components/NavBar.tsx b/client/uiwails/frontend/src/components/NavBar.tsx
new file mode 100644
index 000000000..f68ec5df0
--- /dev/null
+++ b/client/uiwails/frontend/src/components/NavBar.tsx
@@ -0,0 +1,132 @@
+import { NavLink } from 'react-router-dom'
+import NetBirdLogo from './NetBirdLogo'
+
+const navItems = [
+ { to: '/', label: 'Status', icon: StatusIcon },
+ { to: '/peers', label: 'Peers', icon: PeersIcon },
+ { to: '/networks', label: 'Networks', icon: NetworksIcon },
+ { to: '/profiles', label: 'Profiles', icon: ProfilesIcon },
+ { to: '/settings', label: 'Settings', icon: SettingsIcon },
+ { to: '/debug', label: 'Debug', icon: DebugIcon },
+ { to: '/update', label: 'Update', icon: UpdateIcon },
+]
+
+export default function NavBar() {
+ return (
+
+ )
+}
+
+function StatusIcon({ active }: { active: boolean }) {
+ return (
+
+ )
+}
+
+function PeersIcon({ active }: { active: boolean }) {
+ return (
+
+ )
+}
+
+function NetworksIcon({ active }: { active: boolean }) {
+ return (
+
+ )
+}
+
+function ProfilesIcon({ active }: { active: boolean }) {
+ return (
+
+ )
+}
+
+function SettingsIcon({ active }: { active: boolean }) {
+ return (
+
+ )
+}
+
+function DebugIcon({ active }: { active: boolean }) {
+ return (
+
+ )
+}
+
+function UpdateIcon({ active }: { active: boolean }) {
+ return (
+
+ )
+}
diff --git a/client/uiwails/frontend/src/components/NetBirdLogo.tsx b/client/uiwails/frontend/src/components/NetBirdLogo.tsx
new file mode 100644
index 000000000..5fe944a3e
--- /dev/null
+++ b/client/uiwails/frontend/src/components/NetBirdLogo.tsx
@@ -0,0 +1,20 @@
+function BirdMark({ className }: { className?: string }) {
+ return (
+
+ )
+}
+
+export default function NetBirdLogo({ full = false, className }: { full?: boolean; className?: string }) {
+ if (!full) return
+
+ return (
+
+
+ NETBIRD
+
+ )
+}
diff --git a/client/uiwails/frontend/src/index.css b/client/uiwails/frontend/src/index.css
new file mode 100644
index 000000000..67b0d820b
--- /dev/null
+++ b/client/uiwails/frontend/src/index.css
@@ -0,0 +1,113 @@
+@import "tailwindcss";
+
+@theme {
+ --color-netbird-50: #fff6ed;
+ --color-netbird-100: #feecd6;
+ --color-netbird-150: #ffdfb8;
+ --color-netbird-200: #ffd4a6;
+ --color-netbird-300: #fab677;
+ --color-netbird-400: #f68330;
+ --color-netbird-DEFAULT: #f68330;
+ --color-netbird-500: #f46d1b;
+ --color-netbird-600: #e55311;
+ --color-netbird-700: #be3e10;
+ --color-netbird-800: #973215;
+ --color-netbird-900: #7a2b14;
+ --color-netbird-950: #421308;
+
+ --color-nb-gray-DEFAULT: #181a1d;
+ --color-nb-gray-50: #f4f6f7;
+ --color-nb-gray-100: #e4e7e9;
+ --color-nb-gray-200: #cbd2d6;
+ --color-nb-gray-300: #a3adb5;
+ --color-nb-gray-400: #7c8994;
+ --color-nb-gray-500: #616e79;
+ --color-nb-gray-600: #535d67;
+ --color-nb-gray-700: #474e57;
+ --color-nb-gray-800: #3f444b;
+ --color-nb-gray-900: #2e3238;
+ --color-nb-gray-910: #292c31;
+ --color-nb-gray-920: #25282d;
+ --color-nb-gray-930: #212428;
+ --color-nb-gray-940: #1c1e21;
+ --color-nb-gray-950: #181a1d;
+
+ --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+body {
+ margin: 0;
+ font-family: var(--font-sans);
+ background-color: #181a1d;
+ color: #e4e7e9;
+}
+
+#root {
+ min-height: 100vh;
+}
+
+/* Toggle switch — matches dashboard ToggleSwitch.tsx */
+.toggle-track {
+ position: relative;
+ display: inline-flex;
+ flex-shrink: 0;
+ width: 44px;
+ height: 24px;
+ border-radius: 9999px;
+ border: 2px solid transparent;
+ cursor: pointer;
+ transition: background-color 0.2s;
+ background-color: var(--color-nb-gray-700);
+}
+.toggle-track[aria-checked="true"] {
+ background-color: var(--color-netbird-400);
+}
+.toggle-thumb {
+ pointer-events: none;
+ display: block;
+ width: 20px;
+ height: 20px;
+ border-radius: 9999px;
+ background-color: white;
+ box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
+ transition: transform 0.2s;
+ transform: translateX(0);
+}
+.toggle-track[aria-checked="true"] .toggle-thumb {
+ transform: translateX(20px);
+}
+
+/* Small toggle (matches dashboard ToggleSwitch size=small) */
+.toggle-track-sm {
+ position: relative;
+ display: inline-flex;
+ flex-shrink: 0;
+ width: 36px;
+ height: 18px;
+ border-radius: 9999px;
+ border: 2px solid transparent;
+ cursor: pointer;
+ transition: background-color 0.2s;
+ background-color: var(--color-nb-gray-700);
+}
+.toggle-track-sm[aria-checked="true"] {
+ background-color: var(--color-netbird-400);
+}
+.toggle-thumb-sm {
+ pointer-events: none;
+ display: block;
+ width: 14px;
+ height: 14px;
+ border-radius: 9999px;
+ background-color: white;
+ box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
+ transition: transform 0.2s;
+ transform: translateX(0);
+}
+.toggle-track-sm[aria-checked="true"] .toggle-thumb-sm {
+ transform: translateX(17px);
+}
diff --git a/client/uiwails/frontend/src/main.tsx b/client/uiwails/frontend/src/main.tsx
new file mode 100644
index 000000000..bef5202a3
--- /dev/null
+++ b/client/uiwails/frontend/src/main.tsx
@@ -0,0 +1,10 @@
+import { StrictMode } from 'react'
+import { createRoot } from 'react-dom/client'
+import './index.css'
+import App from './App.tsx'
+
+createRoot(document.getElementById('root')!).render(
+
+
+ ,
+)
diff --git a/client/uiwails/frontend/src/pages/Debug.tsx b/client/uiwails/frontend/src/pages/Debug.tsx
new file mode 100644
index 000000000..36cce549f
--- /dev/null
+++ b/client/uiwails/frontend/src/pages/Debug.tsx
@@ -0,0 +1,169 @@
+import { useState } from 'react'
+import { Call } from '@wailsio/runtime'
+import type { DebugBundleParams, DebugBundleResult } from '../bindings'
+
+const DEFAULT_UPLOAD_URL = 'https://upload.netbird.io'
+
+export default function Debug() {
+ const [anonymize, setAnonymize] = useState(false)
+ const [systemInfo, setSystemInfo] = useState(true)
+ const [upload, setUpload] = useState(true)
+ const [uploadUrl, setUploadUrl] = useState(DEFAULT_UPLOAD_URL)
+ const [runForDuration, setRunForDuration] = useState(true)
+ const [durationMins, setDurationMins] = useState(1)
+
+ const [running, setRunning] = useState(false)
+ const [progress, setProgress] = useState('')
+ const [result, setResult] = useState(null)
+ const [error, setError] = useState(null)
+
+ async function handleCreate() {
+ if (upload && !uploadUrl) {
+ setError('Upload URL is required when upload is enabled')
+ return
+ }
+
+ setRunning(true)
+ setError(null)
+ setResult(null)
+ setProgress(runForDuration ? `Running with trace logs for ${durationMins} minute(s)…` : 'Creating debug bundle…')
+
+ const params: DebugBundleParams = {
+ anonymize,
+ systemInfo,
+ upload,
+ uploadUrl: upload ? uploadUrl : '',
+ runDurationMins: runForDuration ? durationMins : 0,
+ enablePersistence: true,
+ }
+
+ try {
+ console.log('[Debug] calling services.DebugService.CreateDebugBundle')
+ const res = await Call.ByName('github.com/netbirdio/netbird/client/uiwails/services.DebugService.CreateDebugBundle', params) as DebugBundleResult
+ console.log('[Debug] CreateDebugBundle result:', JSON.stringify(res))
+ if (res) {
+ setResult(res)
+ setProgress('Bundle created successfully')
+ }
+ } catch (e) {
+ console.error('[Debug] CreateDebugBundle error:', e)
+ setError(String(e))
+ setProgress('')
+ } finally {
+ setRunning(false)
+ }
+ }
+
+ return (
+
+
Debug
+
+ Create a debug bundle to help troubleshoot issues with NetBird.
+
+
+
+
+
+
+
+ {upload && (
+
+
+ setUploadUrl(e.target.value)}
+ disabled={running}
+ />
+
+ )}
+
+
+
+ {runForDuration && (
+
+ for
+ setDurationMins(Math.max(1, parseInt(e.target.value) || 1))}
+ disabled={running}
+ className="w-16 px-2 py-1 bg-nb-gray-900 border border-nb-gray-800 rounded text-sm text-center focus:outline-none focus:border-netbird"
+ />
+ {durationMins === 1 ? 'minute' : 'minutes'}
+
+ )}
+ {runForDuration && (
+
+ Note: NetBird will be brought up and down during collection.
+
+ )}
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+ {progress && (
+
+ {progress}
+
+ )}
+
+ {result && (
+
+ {result.uploadedKey ? (
+ <>
+
Bundle uploaded successfully!
+
+ Upload key:
+ {result.uploadedKey}
+
+ >
+ ) : result.uploadFailureReason ? (
+
Upload failed: {result.uploadFailureReason}
+ ) : null}
+
+ Local path:
+ {result.localPath}
+
+
+ )}
+
+
+
+
+
+ )
+}
+
+function Toggle({ label, checked, onChange }: { label: string; checked: boolean; onChange: (v: boolean) => void }) {
+ return (
+
+ )
+}
diff --git a/client/uiwails/frontend/src/pages/Networks.tsx b/client/uiwails/frontend/src/pages/Networks.tsx
new file mode 100644
index 000000000..eb372f4c1
--- /dev/null
+++ b/client/uiwails/frontend/src/pages/Networks.tsx
@@ -0,0 +1,356 @@
+import { useState, useEffect, useCallback, useMemo } from 'react'
+import { Call } from '@wailsio/runtime'
+import type { NetworkInfo } from '../bindings'
+
+const SVC = 'github.com/netbirdio/netbird/client/uiwails/services.NetworkService'
+
+type Tab = 'all' | 'overlapping' | 'exit-node'
+type SortKey = 'id' | 'range'
+type SortDir = 'asc' | 'desc'
+
+export default function Networks() {
+ const [networks, setNetworks] = useState([])
+ const [tab, setTab] = useState('all')
+ const [loading, setLoading] = useState(true)
+ const [error, setError] = useState(null)
+ const [search, setSearch] = useState('')
+ const [sortKey, setSortKey] = useState('id')
+ const [sortDir, setSortDir] = useState('asc')
+
+ const load = useCallback(async () => {
+ setLoading(true)
+ setError(null)
+ try {
+ let method: string
+ if (tab === 'all') method = 'ListNetworks'
+ else if (tab === 'overlapping') method = 'ListOverlappingNetworks'
+ else method = 'ListExitNodes'
+ const data = await Call.ByName(`${SVC}.${method}`) as NetworkInfo[]
+ setNetworks(data ?? [])
+ } catch (e) {
+ console.error('[Networks] load error:', e)
+ setError(String(e))
+ } finally {
+ setLoading(false)
+ }
+ }, [tab])
+
+ useEffect(() => {
+ load()
+ const id = setInterval(load, 10000)
+ return () => clearInterval(id)
+ }, [load])
+
+ const filtered = useMemo(() => {
+ let list = networks
+ if (search) {
+ const q = search.toLowerCase()
+ list = list.filter(n =>
+ n.id.toLowerCase().includes(q) ||
+ n.range?.toLowerCase().includes(q) ||
+ n.domains?.some(d => d.toLowerCase().includes(q))
+ )
+ }
+ return [...list].sort((a, b) => {
+ const aVal = sortKey === 'id' ? a.id : (a.range ?? '')
+ const bVal = sortKey === 'id' ? b.id : (b.range ?? '')
+ const cmp = aVal.localeCompare(bVal)
+ return sortDir === 'asc' ? cmp : -cmp
+ })
+ }, [networks, search, sortKey, sortDir])
+
+ function toggleSort(key: SortKey) {
+ if (sortKey === key) {
+ setSortDir(d => d === 'asc' ? 'desc' : 'asc')
+ } else {
+ setSortKey(key)
+ setSortDir('asc')
+ }
+ }
+
+ async function toggle(id: string, selected: boolean) {
+ try {
+ if (selected) await Call.ByName(`${SVC}.DeselectNetwork`, id)
+ else await Call.ByName(`${SVC}.SelectNetwork`, id)
+ await load()
+ } catch (e) {
+ setError(String(e))
+ }
+ }
+
+ async function selectAll() {
+ try {
+ await Call.ByName(`${SVC}.SelectAllNetworks`)
+ await load()
+ } catch (e) { setError(String(e)) }
+ }
+
+ async function deselectAll() {
+ try {
+ await Call.ByName(`${SVC}.DeselectAllNetworks`)
+ await load()
+ } catch (e) { setError(String(e)) }
+ }
+
+ const selectedCount = networks.filter(n => n.selected).length
+
+ return (
+
+
Networks
+
+ {/* Tabs */}
+
+ {([['all', 'All Networks'], ['overlapping', 'Overlapping'], ['exit-node', 'Exit Nodes']] as [Tab, string][]).map(([t, label]) => (
+
+ ))}
+
+
+ {/* Toolbar: search + actions */}
+
+
+
+
setSearch(e.target.value)}
+ />
+
+
+
+
Select All
+
Deselect All
+
+
+ Refresh
+
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+ {/* Selection summary */}
+ {selectedCount > 0 && (
+
+ {selectedCount} of {networks.length} network{networks.length !== 1 ? 's' : ''} selected
+
+ )}
+
+ {/* Table */}
+ {loading && networks.length === 0 ? (
+
+ ) : filtered.length === 0 && networks.length === 0 ? (
+
+ ) : filtered.length === 0 ? (
+
+ No networks match your search.
+
+
+ ) : (
+
+
+
+
+
+
+ | Resolved IPs |
+ Active |
+
+
+
+ {filtered.map(n => (
+ toggle(n.id, n.selected)} />
+ ))}
+
+
+
+ {/* Pagination footer */}
+
+ Showing {filtered.length} of {networks.length} network{networks.length !== 1 ? 's' : ''}
+
+
+ )}
+
+ )
+}
+
+/* ---- Row ---- */
+
+function NetworkRow({ network, onToggle }: { network: NetworkInfo; onToggle: () => void }) {
+ const domains = network.domains ?? []
+ const resolvedEntries = Object.entries(network.resolvedIPs ?? {})
+ const hasDomains = domains.length > 0
+
+ return (
+
+ {/* Network name cell (dashboard-style icon square + name) */}
+
+
+
+
+ {network.id}
+ {hasDomains && domains.length > 1 && (
+ {domains.length} domains
+ )}
+
+
+ |
+
+ {/* Range / Domains */}
+
+ {hasDomains ? (
+
+ {domains.slice(0, 2).map(d => (
+ {d}
+ ))}
+ {domains.length > 2 && (
+ +{domains.length - 2} more
+ )}
+
+ ) : (
+ {network.range}
+ )}
+ |
+
+ {/* Resolved IPs */}
+
+ {resolvedEntries.length > 0 ? (
+
+ {resolvedEntries.slice(0, 2).map(([domain, ips]) => (
+
+ {ips[0]}{ips.length > 1 && +{ips.length - 1}}
+
+ ))}
+ {resolvedEntries.length > 2 && (
+ +{resolvedEntries.length - 2} more
+ )}
+
+ ) : (
+ —
+ )}
+ |
+
+ {/* Active toggle */}
+
+
+ |
+
+ )
+}
+
+/* ---- Network Icon Square (matches dashboard NetworkInformationSquare) ---- */
+
+function NetworkSquare({ name, active }: { name: string; active: boolean }) {
+ const initials = name.substring(0, 2).toUpperCase()
+ return (
+
+ {initials}
+ {/* Status dot */}
+
+ {/* Corner mask for rounded dot cutout */}
+
+
+ )
+}
+
+/* ---- Sortable Header ---- */
+
+function SortableHeader({ label, sortKey, currentKey, dir, onSort }: {
+ label: string; sortKey: SortKey; currentKey: SortKey; dir: SortDir; onSort: (k: SortKey) => void
+}) {
+ const isActive = currentKey === sortKey
+ return (
+ onSort(sortKey)}
+ >
+
+ {label}
+ {isActive && (
+
+ )}
+
+ |
+ )
+}
+
+/* ---- Action Button ---- */
+
+function ActionButton({ onClick, children }: { onClick: () => void; children: React.ReactNode }) {
+ return (
+
+ )
+}
+
+/* ---- Empty State ---- */
+
+function EmptyState({ tab }: { tab: Tab }) {
+ const msg = tab === 'exit-node'
+ ? 'No exit nodes configured.'
+ : tab === 'overlapping'
+ ? 'No overlapping networks detected.'
+ : 'No networks found.'
+
+ return (
+
+ )
+}
+
+/* ---- Loading Skeleton ---- */
+
+function TableSkeleton() {
+ return (
+
+
+ {Array.from({ length: 5 }).map((_, i) => (
+
+ ))}
+
+ )
+}
diff --git a/client/uiwails/frontend/src/pages/Peers.tsx b/client/uiwails/frontend/src/pages/Peers.tsx
new file mode 100644
index 000000000..5d06b4a68
--- /dev/null
+++ b/client/uiwails/frontend/src/pages/Peers.tsx
@@ -0,0 +1,354 @@
+import { useState, useEffect, useCallback, useMemo } from 'react'
+import { Call } from '@wailsio/runtime'
+import type { PeerInfo } from '../bindings'
+
+const SVC = 'github.com/netbirdio/netbird/client/uiwails/services.PeersService'
+
+type SortKey = 'fqdn' | 'ip' | 'status' | 'latency'
+type SortDir = 'asc' | 'desc'
+
+function formatBytes(bytes: number): string {
+ if (bytes === 0) return '0 B'
+ const units = ['B', 'KB', 'MB', 'GB', 'TB']
+ const i = Math.floor(Math.log(bytes) / Math.log(1024))
+ return `${(bytes / Math.pow(1024, i)).toFixed(i > 0 ? 1 : 0)} ${units[i]}`
+}
+
+function formatLatency(ms: number): string {
+ if (ms <= 0) return '—'
+ if (ms < 1) return '<1 ms'
+ return `${ms.toFixed(1)} ms`
+}
+
+function peerName(p: PeerInfo): string {
+ if (p.fqdn) return p.fqdn.replace(/\.netbird\.cloud\.?$/, '')
+ return p.ip || p.pubKey.substring(0, 8)
+}
+
+export default function Peers() {
+ const [peers, setPeers] = useState([])
+ const [loading, setLoading] = useState(true)
+ const [error, setError] = useState(null)
+ const [search, setSearch] = useState('')
+ const [sortKey, setSortKey] = useState('fqdn')
+ const [sortDir, setSortDir] = useState('asc')
+
+ const load = useCallback(async () => {
+ setLoading(true)
+ setError(null)
+ try {
+ const data = await Call.ByName(`${SVC}.GetPeers`) as PeerInfo[]
+ setPeers(data ?? [])
+ } catch (e) {
+ console.error('[Peers] load error:', e)
+ setError(String(e))
+ } finally {
+ setLoading(false)
+ }
+ }, [])
+
+ useEffect(() => {
+ load()
+ const id = setInterval(load, 10000)
+ return () => clearInterval(id)
+ }, [load])
+
+ const connectedCount = useMemo(() => peers.filter(p => p.connStatus === 'Connected').length, [peers])
+
+ const filtered = useMemo(() => {
+ let list = peers
+ if (search) {
+ const q = search.toLowerCase()
+ list = list.filter(p =>
+ peerName(p).toLowerCase().includes(q) ||
+ p.ip?.toLowerCase().includes(q) ||
+ p.connStatus?.toLowerCase().includes(q) ||
+ p.fqdn?.toLowerCase().includes(q)
+ )
+ }
+ return [...list].sort((a, b) => {
+ let cmp = 0
+ switch (sortKey) {
+ case 'fqdn': cmp = peerName(a).localeCompare(peerName(b)); break
+ case 'ip': cmp = (a.ip ?? '').localeCompare(b.ip ?? ''); break
+ case 'status': cmp = (a.connStatus ?? '').localeCompare(b.connStatus ?? ''); break
+ case 'latency': cmp = (a.latencyMs ?? 0) - (b.latencyMs ?? 0); break
+ }
+ return sortDir === 'asc' ? cmp : -cmp
+ })
+ }, [peers, search, sortKey, sortDir])
+
+ function toggleSort(key: SortKey) {
+ if (sortKey === key) {
+ setSortDir(d => d === 'asc' ? 'desc' : 'asc')
+ } else {
+ setSortKey(key)
+ setSortDir('asc')
+ }
+ }
+
+ return (
+
+
Peers
+
+ {/* Toolbar */}
+
+
+
+
setSearch(e.target.value)}
+ />
+
+
+
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+ {/* Summary */}
+ {peers.length > 0 && (
+
+ {connectedCount} of {peers.length} peer{peers.length !== 1 ? 's' : ''} connected
+
+ )}
+
+ {/* Table */}
+ {loading && peers.length === 0 ? (
+
+ ) : peers.length === 0 ? (
+
+ ) : filtered.length === 0 ? (
+
+ No peers match your search.
+
+
+ ) : (
+
+
+
+
+
+
+
+ | Connection |
+
+ Transfer |
+
+
+
+ {filtered.map(p => (
+
+ ))}
+
+
+
+
+ Showing {filtered.length} of {peers.length} peer{peers.length !== 1 ? 's' : ''}
+
+
+ )}
+
+ )
+}
+
+/* ---- Row ---- */
+
+function PeerRow({ peer }: { peer: PeerInfo }) {
+ const name = peerName(peer)
+ const connected = peer.connStatus === 'Connected'
+
+ return (
+
+ {/* Peer name */}
+
+
+
+
+ {name}
+ {peer.networks && peer.networks.length > 0 && (
+ {peer.networks.length} network{peer.networks.length !== 1 ? 's' : ''}
+ )}
+
+
+ |
+
+ {/* IP */}
+
+ {peer.ip || '—'}
+ |
+
+ {/* Status */}
+
+
+ |
+
+ {/* Connection type */}
+
+
+ {connected ? (
+ <>
+
+ {peer.relayed ? 'Relayed' : 'Direct'}{' '}
+ {peer.rosenpassEnabled && (
+ PQ
+ )}
+
+ {peer.relayed && peer.relayAddress && (
+
+ via {peer.relayAddress.length > 24 ? peer.relayAddress.substring(0, 24) + '...' : peer.relayAddress}
+
+ )}
+ {!peer.relayed && peer.localIceType && (
+ {peer.localIceType} / {peer.remoteIceType}
+ )}
+ >
+ ) : (
+ —
+ )}
+
+ |
+
+ {/* Latency */}
+
+ 0 ? 'text-nb-gray-300' : 'text-nb-gray-600'}`}>
+ {formatLatency(peer.latencyMs)}
+
+ |
+
+ {/* Transfer */}
+
+ {(peer.bytesRx > 0 || peer.bytesTx > 0) ? (
+
+
+ ↓ {formatBytes(peer.bytesRx)}
+
+
+ ↑ {formatBytes(peer.bytesTx)}
+
+
+ ) : (
+ —
+ )}
+ |
+
+ )
+}
+
+/* ---- Peer Icon Square ---- */
+
+function PeerSquare({ name, connected }: { name: string; connected: boolean }) {
+ const initials = name.substring(0, 2).toUpperCase()
+ return (
+
+ )
+}
+
+/* ---- Status Badge ---- */
+
+function StatusBadge({ status }: { status: string }) {
+ const connected = status === 'Connected'
+ return (
+
+
+ {status || 'Unknown'}
+
+ )
+}
+
+/* ---- Sortable Header ---- */
+
+function SortableHeader({ label, sortKey, currentKey, dir, onSort }: {
+ label: string; sortKey: SortKey; currentKey: SortKey; dir: SortDir; onSort: (k: SortKey) => void
+}) {
+ const isActive = currentKey === sortKey
+ return (
+ onSort(sortKey)}
+ >
+
+ {label}
+ {isActive && (
+
+ )}
+
+ |
+ )
+}
+
+/* ---- Action Button ---- */
+
+function ActionButton({ onClick, children }: { onClick: () => void; children: React.ReactNode }) {
+ return (
+
+ )
+}
+
+/* ---- Empty State ---- */
+
+function EmptyState() {
+ return (
+
+
+
No peers found. Connect to a network to see peers.
+
+ )
+}
+
+/* ---- Loading Skeleton ---- */
+
+function TableSkeleton() {
+ return (
+
+
+ {Array.from({ length: 5 }).map((_, i) => (
+
+ ))}
+
+ )
+}
diff --git a/client/uiwails/frontend/src/pages/Profiles.tsx b/client/uiwails/frontend/src/pages/Profiles.tsx
new file mode 100644
index 000000000..28e5bfb5d
--- /dev/null
+++ b/client/uiwails/frontend/src/pages/Profiles.tsx
@@ -0,0 +1,163 @@
+import { useState, useEffect } from 'react'
+import { Call } from '@wailsio/runtime'
+import type { ProfileInfo } from '../bindings'
+
+export default function Profiles() {
+ const [profiles, setProfiles] = useState([])
+ const [newName, setNewName] = useState('')
+ const [loading, setLoading] = useState(false)
+ const [error, setError] = useState(null)
+ const [info, setInfo] = useState(null)
+ const [confirm, setConfirm] = useState<{ action: string; profile: string } | null>(null)
+
+ async function refresh() {
+ try {
+ console.log('[Profiles] calling services.ProfileService.ListProfiles')
+ const data = await Call.ByName('github.com/netbirdio/netbird/client/uiwails/services.ProfileService.ListProfiles') as ProfileInfo[]
+ console.log('[Profiles] ListProfiles returned', data?.length ?? 0, 'profiles')
+ setProfiles(data ?? [])
+ } catch (e) {
+ console.error('[Profiles] ListProfiles error:', e)
+ setError(String(e))
+ }
+ }
+
+ useEffect(() => { refresh() }, [])
+
+ function showInfo(msg: string) {
+ setInfo(msg)
+ setTimeout(() => setInfo(null), 3000)
+ }
+
+ async function handleConfirm() {
+ if (!confirm) return
+ setLoading(true)
+ setError(null)
+ try {
+ if (confirm.action === 'switch') await Call.ByName('github.com/netbirdio/netbird/client/uiwails/services.ProfileService.SwitchProfile', confirm.profile)
+ else if (confirm.action === 'remove') await Call.ByName('github.com/netbirdio/netbird/client/uiwails/services.ProfileService.RemoveProfile', confirm.profile)
+ else if (confirm.action === 'logout') await Call.ByName('github.com/netbirdio/netbird/client/uiwails/services.ProfileService.Logout', confirm.profile)
+ showInfo(`${confirm.action === 'switch' ? 'Switched to' : confirm.action === 'remove' ? 'Removed' : 'Deregistered from'} profile '${confirm.profile}'`)
+ await refresh()
+ } catch (e) {
+ setError(String(e))
+ } finally {
+ setLoading(false)
+ setConfirm(null)
+ }
+ }
+
+ async function handleAdd() {
+ if (!newName.trim()) return
+ setLoading(true)
+ setError(null)
+ try {
+ await Call.ByName('github.com/netbirdio/netbird/client/uiwails/services.ProfileService.AddProfile', newName.trim())
+ showInfo(`Profile '${newName.trim()}' created`)
+ setNewName('')
+ await refresh()
+ } catch (e) {
+ setError(String(e))
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ return (
+
+
Profiles
+
+ {error && (
+
+ {error}
+
+ )}
+ {info && (
+
+ {info}
+
+ )}
+
+ {/* Confirm dialog */}
+ {confirm && (
+
+
+
+ {confirm.action === 'switch' ? 'Switch Profile' : confirm.action === 'remove' ? 'Remove Profile' : 'Deregister Profile'}
+
+
+ {confirm.action === 'switch' && `Switch to profile '${confirm.profile}'?`}
+ {confirm.action === 'remove' && `Delete profile '${confirm.profile}'? This cannot be undone.`}
+ {confirm.action === 'logout' && `Deregister from '${confirm.profile}'?`}
+
+
+
+
+
+
+
+ )}
+
+ {/* Profile list */}
+
+ {profiles.length === 0 ? (
+
No profiles found.
+ ) : (
+ profiles.map(p => (
+
+
+ {p.isActive ? '✓' : ''}
+
+
{p.name}
+ {p.isActive &&
Active}
+
+ {!p.isActive && (
+
+ )}
+
+
+
+
+ ))
+ )}
+
+
+ {/* Add new profile */}
+
+ setNewName(e.target.value)}
+ onKeyDown={e => e.key === 'Enter' && handleAdd()}
+ />
+
+
+
+ )
+}
diff --git a/client/uiwails/frontend/src/pages/Settings.tsx b/client/uiwails/frontend/src/pages/Settings.tsx
new file mode 100644
index 000000000..77f584048
--- /dev/null
+++ b/client/uiwails/frontend/src/pages/Settings.tsx
@@ -0,0 +1,212 @@
+import { useState, useEffect } from 'react'
+import { Call } from '@wailsio/runtime'
+import type { ConfigInfo } from '../bindings'
+
+async function getConfig(): Promise {
+ try {
+ console.log('[Settings] calling services.SettingsService.GetConfig')
+ const result = await Call.ByName('github.com/netbirdio/netbird/client/uiwails/services.SettingsService.GetConfig')
+ console.log('[Settings] GetConfig result:', JSON.stringify(result))
+ return result as ConfigInfo
+ } catch (e) {
+ console.error('[Settings] GetConfig error:', e)
+ return null
+ }
+}
+
+async function setConfig(cfg: ConfigInfo): Promise {
+ console.log('[Settings] calling services.SettingsService.SetConfig')
+ await Call.ByName('github.com/netbirdio/netbird/client/uiwails/services.SettingsService.SetConfig', cfg)
+}
+
+type Tab = 'connection' | 'network' | 'security'
+
+export default function Settings() {
+ const [config, setConfigState] = useState(null)
+ const [tab, setTab] = useState('connection')
+ const [saving, setSaving] = useState(false)
+ const [saved, setSaved] = useState(false)
+ const [error, setError] = useState(null)
+
+ useEffect(() => {
+ getConfig().then(c => { if (c) setConfigState(c) })
+ }, [])
+
+ function update(key: K, value: ConfigInfo[K]) {
+ setConfigState(prev => prev ? { ...prev, [key]: value } : prev)
+ }
+
+ async function handleSave() {
+ if (!config) return
+ setSaving(true)
+ setError(null)
+ setSaved(false)
+ try {
+ await setConfig(config)
+ setSaved(true)
+ setTimeout(() => setSaved(false), 2000)
+ } catch (e) {
+ setError(String(e))
+ } finally {
+ setSaving(false)
+ }
+ }
+
+ if (!config) {
+ return Loading settings…
+ }
+
+ return (
+
+
Settings
+
+ {/* Tabs */}
+
+ {(['connection', 'network', 'security'] as Tab[]).map(t => (
+
+ ))}
+
+
+
+
+ {tab === 'connection' && (
+ <>
+
+ update('managementUrl', e.target.value)}
+ placeholder="https://api.netbird.io:443"
+ />
+
+
+ update('adminUrl', e.target.value)}
+ />
+
+
+ update('preSharedKey', e.target.value)}
+ placeholder="Leave empty to clear"
+ />
+
+ update('disableAutoConnect', !v)}
+ />
+ update('disableNotifications', !v)}
+ />
+ >
+ )}
+
+ {tab === 'network' && (
+ <>
+
+ update('interfaceName', e.target.value)}
+ placeholder="netbird0"
+ />
+
+
+ update('wireguardPort', parseInt(e.target.value) || 0)}
+ placeholder="51820"
+ />
+
+ update('lazyConnectionEnabled', v)}
+ />
+ update('blockInbound', v)}
+ />
+ >
+ )}
+
+ {tab === 'security' && (
+ <>
+ update('serverSshAllowed', v)}
+ />
+ update('rosenpassEnabled', v)}
+ />
+ update('rosenpassPermissive', v)}
+ />
+ >
+ )}
+
+
+
+
+ {saved && Saved!}
+ {error && {error}}
+
+
+ )
+}
+
+function Field({ label, children }: { label: string; children: React.ReactNode }) {
+ return (
+
+
+ {children}
+
+ )
+}
+
+function Toggle({ label, checked, onChange }: { label: string; checked: boolean; onChange: (v: boolean) => void }) {
+ return (
+
+ )
+}
diff --git a/client/uiwails/frontend/src/pages/Status.tsx b/client/uiwails/frontend/src/pages/Status.tsx
new file mode 100644
index 000000000..e0554cb0d
--- /dev/null
+++ b/client/uiwails/frontend/src/pages/Status.tsx
@@ -0,0 +1,164 @@
+import { useState, useEffect, useCallback } from 'react'
+import { Events, Call } from '@wailsio/runtime'
+import type { StatusInfo } from '../bindings'
+
+async function getStatus(): Promise {
+ try {
+ console.log('[Dashboard] calling services.ConnectionService.GetStatus')
+ const result = await Call.ByName('github.com/netbirdio/netbird/client/uiwails/services.ConnectionService.GetStatus')
+ console.log('[Dashboard] GetStatus result:', JSON.stringify(result))
+ return result as StatusInfo
+ } catch (e) {
+ console.error('[Dashboard] GetStatus error:', e)
+ return null
+ }
+}
+
+async function connect(): Promise {
+ console.log('[Dashboard] calling services.ConnectionService.Connect')
+ await Call.ByName('github.com/netbirdio/netbird/client/uiwails/services.ConnectionService.Connect')
+}
+
+async function disconnect(): Promise {
+ console.log('[Dashboard] calling services.ConnectionService.Disconnect')
+ await Call.ByName('github.com/netbirdio/netbird/client/uiwails/services.ConnectionService.Disconnect')
+}
+
+function statusColor(status: string): string {
+ switch (status) {
+ case 'Connected': return 'text-green-400'
+ case 'Connecting': return 'text-yellow-400'
+ case 'Disconnected': return 'text-nb-gray-400'
+ default: return 'text-red-400'
+ }
+}
+
+function statusDot(status: string): string {
+ switch (status) {
+ case 'Connected': return 'bg-green-400'
+ case 'Connecting': return 'bg-yellow-400 animate-pulse'
+ case 'Disconnected': return 'bg-nb-gray-600'
+ default: return 'bg-red-400'
+ }
+}
+
+export default function Status() {
+ const [status, setStatus] = useState(null)
+ const [busy, setBusy] = useState(false)
+ const [error, setError] = useState(null)
+
+ const refresh = useCallback(async () => {
+ const s = await getStatus()
+ if (s) setStatus(s)
+ }, [])
+
+ useEffect(() => {
+ refresh()
+ // Poll every 10 seconds as fallback (push events handle real-time updates)
+ const id = setInterval(refresh, 10000)
+ // Also listen for push events from the tray
+ const unsub = Events.On('status-changed', (event: { data: StatusInfo[] }) => {
+ if (event.data[0]) setStatus(event.data[0])
+ })
+ return () => {
+ clearInterval(id)
+ if (typeof unsub === 'function') unsub()
+ }
+ }, [refresh])
+
+ async function handleConnect() {
+ setBusy(true)
+ setError(null)
+ try {
+ await connect()
+ await refresh()
+ } catch (e) {
+ setError(String(e))
+ } finally {
+ setBusy(false)
+ }
+ }
+
+ async function handleDisconnect() {
+ setBusy(true)
+ setError(null)
+ try {
+ await disconnect()
+ await refresh()
+ } catch (e) {
+ setError(String(e))
+ } finally {
+ setBusy(false)
+ }
+ }
+
+ const isConnected = status?.status === 'Connected'
+ const isConnecting = status?.status === 'Connecting'
+
+ return (
+
+
Status
+
+ {/* Status card */}
+
+
+
+
+ {status?.status ?? 'Loading…'}
+
+
+
+ {status && (
+
+ {status.ip && (
+ <>
+ IP Address
+ {status.ip}
+ >
+ )}
+ {status.fqdn && (
+ <>
+ Hostname
+ {status.fqdn}
+ >
+ )}
+ {status.connectedPeers > 0 && (
+ <>
+ Connected Peers
+ {status.connectedPeers}
+ >
+ )}
+
+ )}
+
+
+ {/* Action button */}
+
+ {!isConnected && !isConnecting && (
+
+ )}
+ {(isConnected || isConnecting) && (
+
+ )}
+
+
+ {error && (
+
+ {error}
+
+ )}
+
+ )
+}
diff --git a/client/uiwails/frontend/src/pages/Update.tsx b/client/uiwails/frontend/src/pages/Update.tsx
new file mode 100644
index 000000000..2f40b8414
--- /dev/null
+++ b/client/uiwails/frontend/src/pages/Update.tsx
@@ -0,0 +1,113 @@
+import { useState, useEffect, useRef } from 'react'
+import { Call } from '@wailsio/runtime'
+import type { InstallerResult } from '../bindings'
+
+type UpdateState = 'idle' | 'triggering' | 'polling' | 'success' | 'failed' | 'timeout'
+
+export default function Update() {
+ const [state, setState] = useState('idle')
+ const [dots, setDots] = useState('')
+ const [errorMsg, setErrorMsg] = useState('')
+ const abortRef = useRef(null)
+
+ // Animate dots when polling
+ useEffect(() => {
+ if (state !== 'polling') return
+ let count = 0
+ const id = setInterval(() => {
+ count = (count + 1) % 4
+ setDots('.'.repeat(count))
+ }, 500)
+ return () => clearInterval(id)
+ }, [state])
+
+ async function handleTriggerUpdate() {
+ abortRef.current?.abort()
+ abortRef.current = new AbortController()
+
+ setState('triggering')
+ setErrorMsg('')
+
+ try {
+ console.log('[Update] calling services.UpdateService.TriggerUpdate')
+ await Call.ByName('github.com/netbirdio/netbird/client/uiwails/services.UpdateService.TriggerUpdate')
+ } catch (e) {
+ console.error('[Update] TriggerUpdate error:', e)
+ setErrorMsg(String(e))
+ setState('failed')
+ return
+ }
+
+ setState('polling')
+
+ // Poll for installer result (up to 15 minutes handled server-side)
+ try {
+ console.log('[Update] calling services.UpdateService.GetInstallerResult')
+ const result = await Call.ByName('github.com/netbirdio/netbird/client/uiwails/services.UpdateService.GetInstallerResult') as InstallerResult
+ console.log('[Update] GetInstallerResult:', JSON.stringify(result))
+ if (result?.success) {
+ setState('success')
+ } else {
+ setErrorMsg(result?.errorMsg ?? 'Update failed')
+ setState('failed')
+ }
+ } catch (e) {
+ // If the daemon restarts, the gRPC call may fail — treat as success
+ setState('success')
+ }
+ }
+
+ return (
+
+
Update
+
+ Trigger an automatic client update managed by the NetBird daemon.
+
+
+
+ {state === 'idle' && (
+ <>
+
Click below to trigger a daemon-managed update.
+
+ >
+ )}
+
+ {state === 'triggering' && (
+
Triggering update…
+ )}
+
+ {state === 'polling' && (
+
+
Updating{dots}
+
The daemon is installing the update. Please wait.
+
+ )}
+
+ {state === 'success' && (
+
+
Update Successful!
+
The client has been updated. You may need to restart.
+
+ )}
+
+ {state === 'failed' && (
+
+
Update Failed
+ {errorMsg &&
{errorMsg}
}
+
+
+ )}
+
+
+ )
+}
diff --git a/client/uiwails/frontend/tsconfig.json b/client/uiwails/frontend/tsconfig.json
new file mode 100644
index 000000000..fdb15fc67
--- /dev/null
+++ b/client/uiwails/frontend/tsconfig.json
@@ -0,0 +1,20 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "isolatedModules": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "react-jsx",
+ "strict": true,
+ "noUnusedLocals": false,
+ "noUnusedParameters": false,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src"]
+}
diff --git a/client/uiwails/frontend/vite.config.ts b/client/uiwails/frontend/vite.config.ts
new file mode 100644
index 000000000..8866ee907
--- /dev/null
+++ b/client/uiwails/frontend/vite.config.ts
@@ -0,0 +1,15 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+import tailwindcss from '@tailwindcss/vite'
+
+// https://vite.dev/config/
+export default defineConfig({
+ plugins: [
+ react(),
+ tailwindcss(),
+ ],
+ build: {
+ outDir: 'dist',
+ emptyOutDir: true,
+ },
+})
diff --git a/client/uiwails/grpc.go b/client/uiwails/grpc.go
new file mode 100644
index 000000000..82f282f0f
--- /dev/null
+++ b/client/uiwails/grpc.go
@@ -0,0 +1,84 @@
+//go:build !(linux && 386)
+
+package main
+
+import (
+ "strings"
+ "sync"
+ "time"
+
+ log "github.com/sirupsen/logrus"
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/credentials/insecure"
+
+ "github.com/netbirdio/netbird/client/proto"
+ "github.com/netbirdio/netbird/version"
+)
+
+const (
+ defaultFailTimeout = 3 * time.Second
+ failFastTimeout = time.Second
+)
+
+// GRPCClient manages a single persistent gRPC connection to the NetBird daemon.
+type GRPCClient struct {
+ addr string
+ mu sync.Mutex
+ conn *grpc.ClientConn
+ client proto.DaemonServiceClient
+}
+
+// NewGRPCClient creates a new GRPCClient for the given daemon address.
+func NewGRPCClient(addr string) *GRPCClient {
+ return &GRPCClient{addr: addr}
+}
+
+// GetClient returns a cached DaemonServiceClient, creating the connection on first use.
+func (g *GRPCClient) GetClient(timeout time.Duration) (proto.DaemonServiceClient, error) {
+ g.mu.Lock()
+ defer g.mu.Unlock()
+
+ if g.client != nil {
+ return g.client, nil
+ }
+
+ target := g.addr
+ if strings.HasPrefix(target, "tcp://") {
+ target = strings.TrimPrefix(target, "tcp://")
+ } else if strings.HasPrefix(target, "unix://") {
+ target = "unix:" + strings.TrimPrefix(target, "unix://")
+ }
+
+ conn, err := grpc.NewClient(
+ target,
+ grpc.WithTransportCredentials(insecure.NewCredentials()),
+ grpc.WithUserAgent(getUIUserAgent()),
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ g.conn = conn
+ g.client = proto.NewDaemonServiceClient(conn)
+ log.Debugf("gRPC connection established to %s", g.addr)
+
+ return g.client, nil
+}
+
+// Close closes the underlying gRPC connection.
+func (g *GRPCClient) Close() error {
+ g.mu.Lock()
+ defer g.mu.Unlock()
+
+ if g.conn != nil {
+ err := g.conn.Close()
+ g.conn = nil
+ g.client = nil
+ return err
+ }
+ return nil
+}
+
+func getUIUserAgent() string {
+ return "netbird-fancyui/" + version.NetbirdVersion()
+}
diff --git a/client/uiwails/icons.go b/client/uiwails/icons.go
new file mode 100644
index 000000000..0d07520af
--- /dev/null
+++ b/client/uiwails/icons.go
@@ -0,0 +1,31 @@
+//go:build !(linux && 386)
+
+package main
+
+import _ "embed"
+
+//go:embed assets/netbird-systemtray-disconnected.png
+var iconDisconnected []byte
+
+//go:embed assets/netbird-systemtray-connected.png
+var iconConnected []byte
+
+//go:embed assets/netbird-systemtray-connecting.png
+var iconConnecting []byte
+
+//go:embed assets/netbird-systemtray-error.png
+var iconError []byte
+
+// iconForStatus returns the appropriate tray icon bytes for the given status string.
+func iconForStatus(status string) []byte {
+ switch status {
+ case "Connected":
+ return iconConnected
+ case "Connecting":
+ return iconConnecting
+ case "Disconnected", "":
+ return iconDisconnected
+ default:
+ return iconError
+ }
+}
diff --git a/client/uiwails/main.go b/client/uiwails/main.go
new file mode 100644
index 000000000..892849c2d
--- /dev/null
+++ b/client/uiwails/main.go
@@ -0,0 +1,129 @@
+//go:build !(linux && 386)
+
+package main
+
+import (
+ "context"
+ "embed"
+ "flag"
+ "os"
+ "runtime"
+
+ log "github.com/sirupsen/logrus"
+ "github.com/wailsapp/wails/v3/pkg/application"
+ "github.com/wailsapp/wails/v3/pkg/events"
+ "github.com/wailsapp/wails/v3/pkg/services/notifications"
+
+ "github.com/netbirdio/netbird/client/uiwails/event"
+ "github.com/netbirdio/netbird/client/uiwails/process"
+ "github.com/netbirdio/netbird/client/uiwails/services"
+)
+
+//go:embed frontend/dist
+var frontendFS embed.FS
+
+var (
+ daemonAddr = flag.String("daemon-addr", defaultDaemonAddr(), "NetBird daemon gRPC address")
+)
+
+func defaultDaemonAddr() string {
+ if runtime.GOOS == "windows" {
+ return "tcp://127.0.0.1:41731"
+ }
+ return "unix:///var/run/netbird.sock"
+}
+
+func main() {
+ flag.Parse()
+
+ // Single-instance guard — if another instance is running, show its window and exit.
+ if pid, running, err := process.IsAnotherProcessRunning(); err == nil && running {
+ log.Infof("another instance is running (pid %d), signalling it to show window", pid)
+ if err := sendShowWindowSignal(pid); err != nil {
+ log.Warnf("send show window signal: %v", err)
+ }
+ os.Exit(0)
+ }
+
+ grpcClient := NewGRPCClient(*daemonAddr)
+
+ connSvc := services.NewConnectionService(grpcClient)
+ settingsSvc := services.NewSettingsService(grpcClient)
+ networkSvc := services.NewNetworkService(grpcClient)
+ profileSvc := services.NewProfileService(grpcClient)
+ peersSvc := services.NewPeersService(grpcClient)
+ debugSvc := services.NewDebugService(grpcClient)
+ updateSvc := services.NewUpdateService(grpcClient)
+ notifSvc := notifications.New()
+
+ app := application.New(application.Options{
+ Name: "NetBird",
+ Description: "NetBird VPN client",
+ Services: []application.Service{
+ application.NewService(connSvc),
+ application.NewService(settingsSvc),
+ application.NewService(networkSvc),
+ application.NewService(profileSvc),
+ application.NewService(peersSvc),
+ application.NewService(debugSvc),
+ application.NewService(updateSvc),
+ application.NewService(notifSvc),
+ },
+ Assets: application.AssetOptions{
+ Handler: application.BundledAssetFileServer(frontendFS),
+ },
+ Mac: application.MacOptions{
+ ActivationPolicy: application.ActivationPolicyAccessory,
+ },
+ })
+
+ window := app.Window.NewWithOptions(application.WebviewWindowOptions{
+ Title: "NetBird",
+ Width: 900,
+ Height: 650,
+ Hidden: true, // start hidden — tray is the primary interface
+ URL: "/",
+ AlwaysOnTop: false,
+ DisableResize: false,
+ Windows: application.WindowsWindow{
+ HiddenOnTaskbar: true,
+ },
+ })
+
+ // Hide instead of quit when user closes the window.
+ window.RegisterHook(events.Common.WindowClosing, func(e *application.WindowEvent) {
+ e.Cancel()
+ window.Hide()
+ })
+
+ // Register an in-process StatusNotifierWatcher so the tray works on
+ // minimal WMs (Fluxbox, OpenBox, i3…) that don't ship one themselves.
+ startStatusNotifierWatcher()
+
+ tray := newTrayManager(app, window, connSvc, settingsSvc, networkSvc, profileSvc)
+ tray.Setup(iconDisconnected)
+
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ // Signal handler: SIGUSR1 on Unix, Windows Event on Windows.
+ setupSignalHandler(ctx, window)
+
+ // Daemon event stream → desktop notifications.
+ notify := func(title, body string) {
+ if err := notifSvc.SendNotification(notifications.NotificationOptions{
+ ID: "netbird-event",
+ Title: title,
+ Body: body,
+ }); err != nil {
+ log.Warnf("send notification: %v", err)
+ }
+ }
+
+ evtManager := event.NewManager(*daemonAddr, notify)
+ go evtManager.Start(ctx)
+
+ if err := app.Run(); err != nil {
+ log.Fatalf("app run: %v", err)
+ }
+}
diff --git a/client/uiwails/process/process.go b/client/uiwails/process/process.go
new file mode 100644
index 000000000..8534e1ca1
--- /dev/null
+++ b/client/uiwails/process/process.go
@@ -0,0 +1,39 @@
+package process
+
+import (
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/shirou/gopsutil/v3/process"
+)
+
+// IsAnotherProcessRunning returns the PID and true if another instance of the
+// same binary is already running for the current OS user.
+func IsAnotherProcessRunning() (int32, bool, error) {
+ processes, err := process.Processes()
+ if err != nil {
+ return 0, false, err
+ }
+
+ pid := os.Getpid()
+ processName := strings.ToLower(filepath.Base(os.Args[0]))
+
+ for _, p := range processes {
+ if int(p.Pid) == pid {
+ continue
+ }
+
+ runningProcessPath, err := p.Exe()
+ if err != nil {
+ continue
+ }
+
+ runningProcessName := strings.ToLower(filepath.Base(runningProcessPath))
+ if runningProcessName == processName && isProcessOwnedByCurrentUser(p) {
+ return p.Pid, true, nil
+ }
+ }
+
+ return 0, false, nil
+}
diff --git a/client/uiwails/process/process_nonwindows.go b/client/uiwails/process/process_nonwindows.go
new file mode 100644
index 000000000..dff3d3777
--- /dev/null
+++ b/client/uiwails/process/process_nonwindows.go
@@ -0,0 +1,25 @@
+//go:build !windows
+
+package process
+
+import (
+ "os"
+
+ "github.com/shirou/gopsutil/v3/process"
+ log "github.com/sirupsen/logrus"
+)
+
+func isProcessOwnedByCurrentUser(p *process.Process) bool {
+ currentUserID := os.Getuid()
+ uids, err := p.Uids()
+ if err != nil {
+ log.Errorf("get process uids: %v", err)
+ return false
+ }
+ for _, id := range uids {
+ if int(id) == currentUserID {
+ return true
+ }
+ }
+ return false
+}
diff --git a/client/uiwails/process/process_windows.go b/client/uiwails/process/process_windows.go
new file mode 100644
index 000000000..2d211d1a4
--- /dev/null
+++ b/client/uiwails/process/process_windows.go
@@ -0,0 +1,24 @@
+package process
+
+import (
+ "os/user"
+
+ "github.com/shirou/gopsutil/v3/process"
+ log "github.com/sirupsen/logrus"
+)
+
+func isProcessOwnedByCurrentUser(p *process.Process) bool {
+ processUsername, err := p.Username()
+ if err != nil {
+ log.Errorf("get process username error: %v", err)
+ return false
+ }
+
+ currUser, err := user.Current()
+ if err != nil {
+ log.Errorf("get current user error: %v", err)
+ return false
+ }
+
+ return processUsername == currUser.Username
+}
diff --git a/client/uiwails/services/connection.go b/client/uiwails/services/connection.go
new file mode 100644
index 000000000..28efa9346
--- /dev/null
+++ b/client/uiwails/services/connection.go
@@ -0,0 +1,112 @@
+//go:build !(linux && 386)
+
+package services
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ log "github.com/sirupsen/logrus"
+
+ "github.com/netbirdio/netbird/client/proto"
+)
+
+// ConnectionService exposes connect/disconnect/status operations to the Wails frontend.
+type ConnectionService struct {
+ grpcClient GRPCClientIface
+}
+
+// NewConnectionService creates a new ConnectionService.
+func NewConnectionService(g GRPCClientIface) *ConnectionService {
+ return &ConnectionService{grpcClient: g}
+}
+
+// GetStatus returns the current daemon status.
+func (s *ConnectionService) GetStatus() (*StatusInfo, error) {
+ conn, err := s.grpcClient.GetClient(3 * time.Second)
+ if err != nil {
+ log.Debugf("GetStatus: failed to get gRPC client: %v", err)
+ return nil, fmt.Errorf("get client: %w", err)
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ resp, err := conn.Status(ctx, &proto.StatusRequest{GetFullPeerStatus: true})
+ if err != nil {
+ log.Warnf("GetStatus: status RPC failed: %v", err)
+ return nil, fmt.Errorf("status rpc: %w", err)
+ }
+
+ log.Debugf("GetStatus: daemon responded status=%q daemonVersion=%q fullStatus=%v",
+ resp.Status, resp.DaemonVersion, resp.FullStatus != nil)
+
+ info := &StatusInfo{
+ Status: resp.Status,
+ }
+
+ if resp.FullStatus != nil && resp.FullStatus.LocalPeerState != nil {
+ lp := resp.FullStatus.LocalPeerState
+ info.IP = lp.GetIP()
+ info.PublicKey = lp.GetPubKey()
+ info.Fqdn = lp.GetFqdn()
+ log.Debugf("GetStatus: localPeer ip=%q fqdn=%q pubKey=%q", info.IP, info.Fqdn, info.PublicKey)
+ } else if resp.FullStatus == nil {
+ log.Warnf("GetStatus: fullStatus is nil — daemon may not support full status or request flag was not set")
+ } else {
+ log.Debugf("GetStatus: fullStatus present but LocalPeerState is nil")
+ }
+
+ if resp.FullStatus != nil {
+ info.ConnectedPeers = len(resp.FullStatus.GetPeers())
+ log.Debugf("GetStatus: connectedPeers=%d", info.ConnectedPeers)
+ }
+
+ return info, nil
+}
+
+// Connect sends an Up request to the daemon.
+func (s *ConnectionService) Connect() error {
+ conn, err := s.grpcClient.GetClient(3 * time.Second)
+ if err != nil {
+ return fmt.Errorf("get client: %w", err)
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
+ defer cancel()
+
+ if _, err := conn.Up(ctx, &proto.UpRequest{}); err != nil {
+ log.Errorf("Up rpc failed: %v", err)
+ return fmt.Errorf("connect: %w", err)
+ }
+
+ return nil
+}
+
+// Disconnect sends a Down request to the daemon.
+func (s *ConnectionService) Disconnect() error {
+ conn, err := s.grpcClient.GetClient(3 * time.Second)
+ if err != nil {
+ return fmt.Errorf("get client: %w", err)
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
+
+ if _, err := conn.Down(ctx, &proto.DownRequest{}); err != nil {
+ log.Errorf("Down rpc failed: %v", err)
+ return fmt.Errorf("disconnect: %w", err)
+ }
+
+ return nil
+}
+
+// StatusInfo holds simplified status information for the frontend.
+type StatusInfo struct {
+ Status string `json:"status"`
+ IP string `json:"ip"`
+ PublicKey string `json:"publicKey"`
+ Fqdn string `json:"fqdn"`
+ ConnectedPeers int `json:"connectedPeers"`
+}
diff --git a/client/uiwails/services/debug.go b/client/uiwails/services/debug.go
new file mode 100644
index 000000000..619e2843e
--- /dev/null
+++ b/client/uiwails/services/debug.go
@@ -0,0 +1,196 @@
+//go:build !(linux && 386)
+
+package services
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ log "github.com/sirupsen/logrus"
+
+ "github.com/netbirdio/netbird/client/proto"
+)
+
+// DebugService exposes debug bundle creation and log-level control to the Wails frontend.
+type DebugService struct {
+ grpcClient GRPCClientIface
+}
+
+// NewDebugService creates a new DebugService.
+func NewDebugService(g GRPCClientIface) *DebugService {
+ return &DebugService{grpcClient: g}
+}
+
+// DebugBundleParams holds the parameters for creating a debug bundle.
+type DebugBundleParams struct {
+ Anonymize bool `json:"anonymize"`
+ SystemInfo bool `json:"systemInfo"`
+ Upload bool `json:"upload"`
+ UploadURL string `json:"uploadUrl"`
+ RunDurationMins int `json:"runDurationMins"`
+ EnablePersistence bool `json:"enablePersistence"`
+}
+
+// DebugBundleResult holds the result of creating a debug bundle.
+type DebugBundleResult struct {
+ LocalPath string `json:"localPath"`
+ UploadedKey string `json:"uploadedKey"`
+ UploadFailureReason string `json:"uploadFailureReason"`
+}
+
+// CreateDebugBundle creates a debug bundle via the daemon.
+func (s *DebugService) CreateDebugBundle(params DebugBundleParams) (*DebugBundleResult, error) {
+ conn, err := s.grpcClient.GetClient(time.Second)
+ if err != nil {
+ return nil, fmt.Errorf("get client: %w", err)
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
+ defer cancel()
+
+ if params.RunDurationMins > 0 {
+ if err := s.configureForDebug(ctx, conn, params); err != nil {
+ return nil, err
+ }
+ }
+
+ req := &proto.DebugBundleRequest{
+ Anonymize: params.Anonymize,
+ SystemInfo: params.SystemInfo,
+ }
+ if params.Upload && params.UploadURL != "" {
+ req.UploadURL = params.UploadURL
+ }
+
+ resp, err := conn.DebugBundle(ctx, req)
+ if err != nil {
+ log.Errorf("DebugBundle rpc failed: %v", err)
+ return nil, fmt.Errorf("create debug bundle: %w", err)
+ }
+
+ return &DebugBundleResult{
+ LocalPath: resp.GetPath(),
+ UploadedKey: resp.GetUploadedKey(),
+ UploadFailureReason: resp.GetUploadFailureReason(),
+ }, nil
+}
+
+func (s *DebugService) configureForDebug(ctx context.Context, conn proto.DaemonServiceClient, params DebugBundleParams) error {
+ statusResp, err := conn.Status(ctx, &proto.StatusRequest{})
+ if err != nil {
+ return fmt.Errorf("get status: %w", err)
+ }
+
+ wasConnected := statusResp.Status == "Connected" || statusResp.Status == "Connecting"
+
+ logLevelResp, err := conn.GetLogLevel(ctx, &proto.GetLogLevelRequest{})
+ if err != nil {
+ return fmt.Errorf("get log level: %w", err)
+ }
+ originalLogLevel := logLevelResp.GetLevel()
+
+ // Set trace log level
+ if _, err := conn.SetLogLevel(ctx, &proto.SetLogLevelRequest{Level: proto.LogLevel_TRACE}); err != nil {
+ return fmt.Errorf("set log level: %w", err)
+ }
+
+ // Bring service down then up to capture full connection logs
+ if _, err := conn.Down(ctx, &proto.DownRequest{}); err != nil {
+ log.Warnf("bring down for debug: %v", err)
+ }
+ time.Sleep(time.Second)
+
+ if params.EnablePersistence {
+ if _, err := conn.SetSyncResponsePersistence(ctx, &proto.SetSyncResponsePersistenceRequest{Enabled: true}); err != nil {
+ log.Warnf("enable sync persistence: %v", err)
+ }
+ }
+
+ if _, err := conn.Up(ctx, &proto.UpRequest{}); err != nil {
+ return fmt.Errorf("bring service up: %w", err)
+ }
+ time.Sleep(3 * time.Second)
+
+ if _, err := conn.StartCPUProfile(ctx, &proto.StartCPUProfileRequest{}); err != nil {
+ log.Warnf("start CPU profiling: %v", err)
+ }
+
+ // Wait for the collection duration
+ collectionDur := time.Duration(params.RunDurationMins) * time.Minute
+ select {
+ case <-ctx.Done():
+ return ctx.Err()
+ case <-time.After(collectionDur):
+ }
+
+ if _, err := conn.StopCPUProfile(ctx, &proto.StopCPUProfileRequest{}); err != nil {
+ log.Warnf("stop CPU profiling: %v", err)
+ }
+
+ // Restore original state
+ if !wasConnected {
+ if _, err := conn.Down(ctx, &proto.DownRequest{}); err != nil {
+ log.Warnf("restore down state: %v", err)
+ }
+ }
+
+ if originalLogLevel < proto.LogLevel_TRACE {
+ if _, err := conn.SetLogLevel(ctx, &proto.SetLogLevelRequest{Level: originalLogLevel}); err != nil {
+ log.Warnf("restore log level: %v", err)
+ }
+ }
+
+ return nil
+}
+
+// GetLogLevel returns the current daemon log level.
+func (s *DebugService) GetLogLevel() (string, error) {
+ conn, err := s.grpcClient.GetClient(3 * time.Second)
+ if err != nil {
+ return "", fmt.Errorf("get client: %w", err)
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ resp, err := conn.GetLogLevel(ctx, &proto.GetLogLevelRequest{})
+ if err != nil {
+ return "", fmt.Errorf("get log level rpc: %w", err)
+ }
+
+ return resp.GetLevel().String(), nil
+}
+
+// SetLogLevel sets the daemon log level.
+func (s *DebugService) SetLogLevel(level string) error {
+ conn, err := s.grpcClient.GetClient(3 * time.Second)
+ if err != nil {
+ return fmt.Errorf("get client: %w", err)
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ var protoLevel proto.LogLevel
+ switch level {
+ case "TRACE":
+ protoLevel = proto.LogLevel_TRACE
+ case "DEBUG":
+ protoLevel = proto.LogLevel_DEBUG
+ case "INFO":
+ protoLevel = proto.LogLevel_INFO
+ case "WARN", "WARNING":
+ protoLevel = proto.LogLevel_WARN
+ case "ERROR":
+ protoLevel = proto.LogLevel_ERROR
+ default:
+ protoLevel = proto.LogLevel_INFO
+ }
+
+ if _, err := conn.SetLogLevel(ctx, &proto.SetLogLevelRequest{Level: protoLevel}); err != nil {
+ return fmt.Errorf("set log level rpc: %w", err)
+ }
+
+ return nil
+}
diff --git a/client/uiwails/services/grpc_iface.go b/client/uiwails/services/grpc_iface.go
new file mode 100644
index 000000000..45c32e783
--- /dev/null
+++ b/client/uiwails/services/grpc_iface.go
@@ -0,0 +1,14 @@
+//go:build !(linux && 386)
+
+package services
+
+import (
+ "time"
+
+ "github.com/netbirdio/netbird/client/proto"
+)
+
+// GRPCClientIface is the interface services use to obtain a daemon client.
+type GRPCClientIface interface {
+ GetClient(timeout time.Duration) (proto.DaemonServiceClient, error)
+}
diff --git a/client/uiwails/services/network.go b/client/uiwails/services/network.go
new file mode 100644
index 000000000..3c746ea96
--- /dev/null
+++ b/client/uiwails/services/network.go
@@ -0,0 +1,222 @@
+//go:build !(linux && 386)
+
+package services
+
+import (
+ "context"
+ "fmt"
+ "sort"
+ "strings"
+ "time"
+
+ log "github.com/sirupsen/logrus"
+
+ "github.com/netbirdio/netbird/client/proto"
+)
+
+// NetworkService exposes network/route management to the Wails frontend.
+type NetworkService struct {
+ grpcClient GRPCClientIface
+}
+
+// NewNetworkService creates a new NetworkService.
+func NewNetworkService(g GRPCClientIface) *NetworkService {
+ return &NetworkService{grpcClient: g}
+}
+
+// NetworkInfo is a serializable view of a single network/route.
+type NetworkInfo struct {
+ ID string `json:"id"`
+ Range string `json:"range"`
+ Domains []string `json:"domains"`
+ Selected bool `json:"selected"`
+ ResolvedIPs map[string][]string `json:"resolvedIPs"`
+}
+
+// ListNetworks returns all networks from the daemon.
+func (s *NetworkService) ListNetworks() ([]NetworkInfo, error) {
+ conn, err := s.grpcClient.GetClient(3 * time.Second)
+ if err != nil {
+ return nil, fmt.Errorf("get client: %w", err)
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ resp, err := conn.ListNetworks(ctx, &proto.ListNetworksRequest{})
+ if err != nil {
+ return nil, fmt.Errorf("list networks rpc: %w", err)
+ }
+
+ routes := make([]NetworkInfo, 0, len(resp.Routes))
+ for _, r := range resp.Routes {
+ info := NetworkInfo{
+ ID: r.GetID(),
+ Range: r.GetRange(),
+ Domains: r.GetDomains(),
+ Selected: r.GetSelected(),
+ }
+ if resolvedMap := r.GetResolvedIPs(); resolvedMap != nil {
+ info.ResolvedIPs = make(map[string][]string)
+ for domain, ipList := range resolvedMap {
+ info.ResolvedIPs[domain] = ipList.GetIps()
+ }
+ }
+ routes = append(routes, info)
+ }
+
+ sort.Slice(routes, func(i, j int) bool {
+ return strings.ToLower(routes[i].ID) < strings.ToLower(routes[j].ID)
+ })
+
+ return routes, nil
+}
+
+// ListOverlappingNetworks returns only networks with overlapping ranges.
+func (s *NetworkService) ListOverlappingNetworks() ([]NetworkInfo, error) {
+ all, err := s.ListNetworks()
+ if err != nil {
+ return nil, err
+ }
+
+ existingRange := make(map[string][]NetworkInfo)
+ for _, r := range all {
+ if len(r.Domains) > 0 {
+ continue
+ }
+ existingRange[r.Range] = append(existingRange[r.Range], r)
+ }
+
+ var result []NetworkInfo
+ for _, group := range existingRange {
+ if len(group) > 1 {
+ result = append(result, group...)
+ }
+ }
+ return result, nil
+}
+
+// ListExitNodes returns networks with range 0.0.0.0/0 (exit nodes).
+func (s *NetworkService) ListExitNodes() ([]NetworkInfo, error) {
+ all, err := s.ListNetworks()
+ if err != nil {
+ return nil, err
+ }
+
+ var result []NetworkInfo
+ for _, r := range all {
+ if r.Range == "0.0.0.0/0" {
+ result = append(result, r)
+ }
+ }
+ return result, nil
+}
+
+// SelectNetwork selects a single network by ID.
+func (s *NetworkService) SelectNetwork(id string) error {
+ conn, err := s.grpcClient.GetClient(3 * time.Second)
+ if err != nil {
+ return fmt.Errorf("get client: %w", err)
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ req := &proto.SelectNetworksRequest{
+ NetworkIDs: []string{id},
+ Append: true,
+ }
+ if _, err := conn.SelectNetworks(ctx, req); err != nil {
+ log.Errorf("SelectNetworks rpc failed: %v", err)
+ return fmt.Errorf("select network: %w", err)
+ }
+ return nil
+}
+
+// DeselectNetwork deselects a single network by ID.
+func (s *NetworkService) DeselectNetwork(id string) error {
+ conn, err := s.grpcClient.GetClient(3 * time.Second)
+ if err != nil {
+ return fmt.Errorf("get client: %w", err)
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ req := &proto.SelectNetworksRequest{
+ NetworkIDs: []string{id},
+ }
+ if _, err := conn.DeselectNetworks(ctx, req); err != nil {
+ log.Errorf("DeselectNetworks rpc failed: %v", err)
+ return fmt.Errorf("deselect network: %w", err)
+ }
+ return nil
+}
+
+// SelectAllNetworks selects all networks.
+func (s *NetworkService) SelectAllNetworks() error {
+ conn, err := s.grpcClient.GetClient(3 * time.Second)
+ if err != nil {
+ return fmt.Errorf("get client: %w", err)
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ req := &proto.SelectNetworksRequest{All: true}
+ if _, err := conn.SelectNetworks(ctx, req); err != nil {
+ return fmt.Errorf("select all networks: %w", err)
+ }
+ return nil
+}
+
+// DeselectAllNetworks deselects all networks.
+func (s *NetworkService) DeselectAllNetworks() error {
+ conn, err := s.grpcClient.GetClient(3 * time.Second)
+ if err != nil {
+ return fmt.Errorf("get client: %w", err)
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ req := &proto.SelectNetworksRequest{All: true}
+ if _, err := conn.DeselectNetworks(ctx, req); err != nil {
+ return fmt.Errorf("deselect all networks: %w", err)
+ }
+ return nil
+}
+
+// SelectNetworks selects a list of networks by ID.
+func (s *NetworkService) SelectNetworks(ids []string) error {
+ conn, err := s.grpcClient.GetClient(3 * time.Second)
+ if err != nil {
+ return fmt.Errorf("get client: %w", err)
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ req := &proto.SelectNetworksRequest{NetworkIDs: ids, Append: true}
+ if _, err := conn.SelectNetworks(ctx, req); err != nil {
+ return fmt.Errorf("select networks: %w", err)
+ }
+ return nil
+}
+
+// DeselectNetworks deselects a list of networks by ID.
+func (s *NetworkService) DeselectNetworks(ids []string) error {
+ conn, err := s.grpcClient.GetClient(3 * time.Second)
+ if err != nil {
+ return fmt.Errorf("get client: %w", err)
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ req := &proto.SelectNetworksRequest{NetworkIDs: ids}
+ if _, err := conn.DeselectNetworks(ctx, req); err != nil {
+ return fmt.Errorf("deselect networks: %w", err)
+ }
+ return nil
+}
diff --git a/client/uiwails/services/peers.go b/client/uiwails/services/peers.go
new file mode 100644
index 000000000..7d8c7f40b
--- /dev/null
+++ b/client/uiwails/services/peers.go
@@ -0,0 +1,106 @@
+//go:build !(linux && 386)
+
+package services
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ log "github.com/sirupsen/logrus"
+
+ "github.com/netbirdio/netbird/client/proto"
+)
+
+// PeersService exposes peer listing operations to the Wails frontend.
+type PeersService struct {
+ grpcClient GRPCClientIface
+}
+
+// NewPeersService creates a new PeersService.
+func NewPeersService(g GRPCClientIface) *PeersService {
+ return &PeersService{grpcClient: g}
+}
+
+// GetPeers returns the list of all peers with their status information.
+func (s *PeersService) GetPeers() ([]PeerInfo, error) {
+ conn, err := s.grpcClient.GetClient(3 * time.Second)
+ if err != nil {
+ log.Debugf("GetPeers: failed to get gRPC client: %v", err)
+ return nil, fmt.Errorf("get client: %w", err)
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ resp, err := conn.Status(ctx, &proto.StatusRequest{GetFullPeerStatus: true})
+ if err != nil {
+ log.Warnf("GetPeers: status RPC failed: %v", err)
+ return nil, fmt.Errorf("status rpc: %w", err)
+ }
+
+ if resp.FullStatus == nil {
+ log.Debugf("GetPeers: fullStatus is nil")
+ return []PeerInfo{}, nil
+ }
+
+ peers := resp.FullStatus.GetPeers()
+ log.Debugf("GetPeers: got %d peers from daemon", len(peers))
+
+ result := make([]PeerInfo, 0, len(peers))
+ for _, p := range peers {
+ info := PeerInfo{
+ IP: p.GetIP(),
+ PubKey: p.GetPubKey(),
+ Fqdn: p.GetFqdn(),
+ ConnStatus: p.GetConnStatus(),
+ Relayed: p.GetRelayed(),
+ RelayAddress: p.GetRelayAddress(),
+ BytesRx: p.GetBytesRx(),
+ BytesTx: p.GetBytesTx(),
+ RosenpassEnabled: p.GetRosenpassEnabled(),
+ Networks: p.GetNetworks(),
+ LocalIceType: p.GetLocalIceCandidateType(),
+ RemoteIceType: p.GetRemoteIceCandidateType(),
+ LocalEndpoint: p.GetLocalIceCandidateEndpoint(),
+ RemoteEndpoint: p.GetRemoteIceCandidateEndpoint(),
+ }
+
+ if lat := p.GetLatency(); lat != nil {
+ info.LatencyMs = float64(lat.Seconds)*1000 + float64(lat.Nanos)/1e6
+ }
+
+ if ts := p.GetLastWireguardHandshake(); ts != nil {
+ info.LastHandshake = ts.AsTime().Format(time.RFC3339)
+ }
+
+ if ts := p.GetConnStatusUpdate(); ts != nil {
+ info.ConnStatusUpdate = ts.AsTime().Format(time.RFC3339)
+ }
+
+ result = append(result, info)
+ }
+
+ return result, nil
+}
+
+// PeerInfo holds simplified peer information for the frontend.
+type PeerInfo struct {
+ IP string `json:"ip"`
+ PubKey string `json:"pubKey"`
+ Fqdn string `json:"fqdn"`
+ ConnStatus string `json:"connStatus"`
+ ConnStatusUpdate string `json:"connStatusUpdate"`
+ Relayed bool `json:"relayed"`
+ RelayAddress string `json:"relayAddress"`
+ LatencyMs float64 `json:"latencyMs"`
+ BytesRx int64 `json:"bytesRx"`
+ BytesTx int64 `json:"bytesTx"`
+ RosenpassEnabled bool `json:"rosenpassEnabled"`
+ Networks []string `json:"networks"`
+ LastHandshake string `json:"lastHandshake"`
+ LocalIceType string `json:"localIceType"`
+ RemoteIceType string `json:"remoteIceType"`
+ LocalEndpoint string `json:"localEndpoint"`
+ RemoteEndpoint string `json:"remoteEndpoint"`
+}
diff --git a/client/uiwails/services/profile.go b/client/uiwails/services/profile.go
new file mode 100644
index 000000000..a31f9b91f
--- /dev/null
+++ b/client/uiwails/services/profile.go
@@ -0,0 +1,195 @@
+//go:build !(linux && 386)
+
+package services
+
+import (
+ "context"
+ "fmt"
+ "os/user"
+ "time"
+
+ log "github.com/sirupsen/logrus"
+
+ "github.com/netbirdio/netbird/client/proto"
+)
+
+// ProfileService exposes profile management to the Wails frontend.
+type ProfileService struct {
+ grpcClient GRPCClientIface
+}
+
+// NewProfileService creates a new ProfileService.
+func NewProfileService(g GRPCClientIface) *ProfileService {
+ return &ProfileService{grpcClient: g}
+}
+
+// ProfileInfo is a serializable view of a profile.
+type ProfileInfo struct {
+ Name string `json:"name"`
+ IsActive bool `json:"isActive"`
+}
+
+// ActiveProfileInfo holds information about the currently active profile.
+type ActiveProfileInfo struct {
+ ProfileName string `json:"profileName"`
+ Username string `json:"username"`
+ Email string `json:"email"`
+}
+
+// ListProfiles returns all profiles for the current OS user.
+func (s *ProfileService) ListProfiles() ([]ProfileInfo, error) {
+ conn, err := s.grpcClient.GetClient(3 * time.Second)
+ if err != nil {
+ return nil, fmt.Errorf("get client: %w", err)
+ }
+
+ currUser, err := user.Current()
+ if err != nil {
+ return nil, fmt.Errorf("get current user: %w", err)
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ resp, err := conn.ListProfiles(ctx, &proto.ListProfilesRequest{
+ Username: currUser.Username,
+ })
+ if err != nil {
+ return nil, fmt.Errorf("list profiles rpc: %w", err)
+ }
+
+ profiles := make([]ProfileInfo, 0, len(resp.Profiles))
+ for _, p := range resp.Profiles {
+ profiles = append(profiles, ProfileInfo{
+ Name: p.Name,
+ IsActive: p.IsActive,
+ })
+ }
+ return profiles, nil
+}
+
+// GetActiveProfile returns the currently active profile.
+func (s *ProfileService) GetActiveProfile() (*ActiveProfileInfo, error) {
+ conn, err := s.grpcClient.GetClient(3 * time.Second)
+ if err != nil {
+ return nil, fmt.Errorf("get client: %w", err)
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ resp, err := conn.GetActiveProfile(ctx, &proto.GetActiveProfileRequest{})
+ if err != nil {
+ return nil, fmt.Errorf("get active profile rpc: %w", err)
+ }
+
+ return &ActiveProfileInfo{
+ ProfileName: resp.ProfileName,
+ Username: resp.Username,
+ }, nil
+}
+
+// SwitchProfile switches to the named profile.
+func (s *ProfileService) SwitchProfile(profileName string) error {
+ conn, err := s.grpcClient.GetClient(3 * time.Second)
+ if err != nil {
+ return fmt.Errorf("get client: %w", err)
+ }
+
+ currUser, err := user.Current()
+ if err != nil {
+ return fmt.Errorf("get current user: %w", err)
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
+
+ if _, err := conn.SwitchProfile(ctx, &proto.SwitchProfileRequest{
+ ProfileName: &profileName,
+ Username: &currUser.Username,
+ }); err != nil {
+ log.Errorf("SwitchProfile rpc failed: %v", err)
+ return fmt.Errorf("switch profile: %w", err)
+ }
+
+ return nil
+}
+
+// AddProfile creates a new profile with the given name.
+func (s *ProfileService) AddProfile(profileName string) error {
+ conn, err := s.grpcClient.GetClient(3 * time.Second)
+ if err != nil {
+ return fmt.Errorf("get client: %w", err)
+ }
+
+ currUser, err := user.Current()
+ if err != nil {
+ return fmt.Errorf("get current user: %w", err)
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
+
+ if _, err := conn.AddProfile(ctx, &proto.AddProfileRequest{
+ ProfileName: profileName,
+ Username: currUser.Username,
+ }); err != nil {
+ log.Errorf("AddProfile rpc failed: %v", err)
+ return fmt.Errorf("add profile: %w", err)
+ }
+
+ return nil
+}
+
+// RemoveProfile removes the named profile.
+func (s *ProfileService) RemoveProfile(profileName string) error {
+ conn, err := s.grpcClient.GetClient(3 * time.Second)
+ if err != nil {
+ return fmt.Errorf("get client: %w", err)
+ }
+
+ currUser, err := user.Current()
+ if err != nil {
+ return fmt.Errorf("get current user: %w", err)
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
+
+ if _, err := conn.RemoveProfile(ctx, &proto.RemoveProfileRequest{
+ ProfileName: profileName,
+ Username: currUser.Username,
+ }); err != nil {
+ log.Errorf("RemoveProfile rpc failed: %v", err)
+ return fmt.Errorf("remove profile: %w", err)
+ }
+
+ return nil
+}
+
+// Logout deregisters the named profile.
+func (s *ProfileService) Logout(profileName string) error {
+ conn, err := s.grpcClient.GetClient(3 * time.Second)
+ if err != nil {
+ return fmt.Errorf("get client: %w", err)
+ }
+
+ currUser, err := user.Current()
+ if err != nil {
+ return fmt.Errorf("get current user: %w", err)
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
+
+ username := currUser.Username
+ if _, err := conn.Logout(ctx, &proto.LogoutRequest{
+ ProfileName: &profileName,
+ Username: &username,
+ }); err != nil {
+ log.Errorf("Logout rpc failed: %v", err)
+ return fmt.Errorf("logout: %w", err)
+ }
+
+ return nil
+}
diff --git a/client/uiwails/services/settings.go b/client/uiwails/services/settings.go
new file mode 100644
index 000000000..dde5dfe67
--- /dev/null
+++ b/client/uiwails/services/settings.go
@@ -0,0 +1,165 @@
+//go:build !(linux && 386)
+
+package services
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ log "github.com/sirupsen/logrus"
+
+ "github.com/netbirdio/netbird/client/proto"
+)
+
+// SettingsService exposes config get/set operations to the Wails frontend.
+type SettingsService struct {
+ grpcClient GRPCClientIface
+}
+
+// NewSettingsService creates a new SettingsService.
+func NewSettingsService(g GRPCClientIface) *SettingsService {
+ return &SettingsService{grpcClient: g}
+}
+
+// ConfigInfo is a serializable view of the daemon configuration.
+type ConfigInfo struct {
+ ManagementURL string `json:"managementUrl"`
+ AdminURL string `json:"adminUrl"`
+ PreSharedKey string `json:"preSharedKey"`
+ InterfaceName string `json:"interfaceName"`
+ WireguardPort int64 `json:"wireguardPort"`
+ DisableAutoConnect bool `json:"disableAutoConnect"`
+ ServerSSHAllowed bool `json:"serverSshAllowed"`
+ RosenpassEnabled bool `json:"rosenpassEnabled"`
+ RosenpassPermissive bool `json:"rosenpassPermissive"`
+ LazyConnectionEnabled bool `json:"lazyConnectionEnabled"`
+ BlockInbound bool `json:"blockInbound"`
+ DisableNotifications bool `json:"disableNotifications"`
+}
+
+// GetConfig retrieves the daemon configuration.
+func (s *SettingsService) GetConfig() (*ConfigInfo, error) {
+ conn, err := s.grpcClient.GetClient(3 * time.Second)
+ if err != nil {
+ return nil, fmt.Errorf("get client: %w", err)
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
+ defer cancel()
+
+ resp, err := conn.GetConfig(ctx, &proto.GetConfigRequest{})
+ if err != nil {
+ return nil, fmt.Errorf("get config rpc: %w", err)
+ }
+
+ cfg := &ConfigInfo{
+ ManagementURL: resp.ManagementUrl,
+ AdminURL: resp.AdminURL,
+ PreSharedKey: resp.PreSharedKey,
+ InterfaceName: resp.InterfaceName,
+ WireguardPort: resp.WireguardPort,
+ DisableAutoConnect: resp.DisableAutoConnect,
+ ServerSSHAllowed: resp.ServerSSHAllowed,
+ RosenpassEnabled: resp.RosenpassEnabled,
+ LazyConnectionEnabled: resp.LazyConnectionEnabled,
+ BlockInbound: resp.BlockInbound,
+ DisableNotifications: resp.DisableNotifications,
+ }
+
+ return cfg, nil
+}
+
+// SetConfig pushes configuration changes to the daemon.
+func (s *SettingsService) SetConfig(cfg ConfigInfo) error {
+ conn, err := s.grpcClient.GetClient(3 * time.Second)
+ if err != nil {
+ return fmt.Errorf("get client: %w", err)
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
+ defer cancel()
+
+ // The SetConfigRequest uses optional pointer fields for most settings.
+ req := &proto.SetConfigRequest{
+ ManagementUrl: cfg.ManagementURL,
+ AdminURL: cfg.AdminURL,
+ RosenpassEnabled: &cfg.RosenpassEnabled,
+ InterfaceName: &cfg.InterfaceName,
+ WireguardPort: &cfg.WireguardPort,
+ OptionalPreSharedKey: &cfg.PreSharedKey,
+ DisableAutoConnect: &cfg.DisableAutoConnect,
+ ServerSSHAllowed: &cfg.ServerSSHAllowed,
+ RosenpassPermissive: &cfg.RosenpassPermissive,
+ DisableNotifications: &cfg.DisableNotifications,
+ LazyConnectionEnabled: &cfg.LazyConnectionEnabled,
+ BlockInbound: &cfg.BlockInbound,
+ }
+
+ if _, err := conn.SetConfig(ctx, req); err != nil {
+ log.Errorf("SetConfig rpc failed: %v", err)
+ return fmt.Errorf("set config: %w", err)
+ }
+
+ return nil
+}
+
+// ToggleSSH toggles the SSH server allowed setting.
+func (s *SettingsService) ToggleSSH(enabled bool) error {
+ cfg, err := s.GetConfig()
+ if err != nil {
+ return err
+ }
+ cfg.ServerSSHAllowed = enabled
+ return s.SetConfig(*cfg)
+}
+
+// ToggleAutoConnect toggles the auto-connect setting.
+func (s *SettingsService) ToggleAutoConnect(enabled bool) error {
+ cfg, err := s.GetConfig()
+ if err != nil {
+ return err
+ }
+ cfg.DisableAutoConnect = !enabled
+ return s.SetConfig(*cfg)
+}
+
+// ToggleRosenpass toggles the Rosenpass quantum resistance setting.
+func (s *SettingsService) ToggleRosenpass(enabled bool) error {
+ cfg, err := s.GetConfig()
+ if err != nil {
+ return err
+ }
+ cfg.RosenpassEnabled = enabled
+ return s.SetConfig(*cfg)
+}
+
+// ToggleLazyConn toggles the lazy connections setting.
+func (s *SettingsService) ToggleLazyConn(enabled bool) error {
+ cfg, err := s.GetConfig()
+ if err != nil {
+ return err
+ }
+ cfg.LazyConnectionEnabled = enabled
+ return s.SetConfig(*cfg)
+}
+
+// ToggleBlockInbound toggles the block inbound setting.
+func (s *SettingsService) ToggleBlockInbound(enabled bool) error {
+ cfg, err := s.GetConfig()
+ if err != nil {
+ return err
+ }
+ cfg.BlockInbound = enabled
+ return s.SetConfig(*cfg)
+}
+
+// ToggleNotifications toggles the notifications setting.
+func (s *SettingsService) ToggleNotifications(enabled bool) error {
+ cfg, err := s.GetConfig()
+ if err != nil {
+ return err
+ }
+ cfg.DisableNotifications = !enabled
+ return s.SetConfig(*cfg)
+}
diff --git a/client/uiwails/services/update.go b/client/uiwails/services/update.go
new file mode 100644
index 000000000..de7295ab6
--- /dev/null
+++ b/client/uiwails/services/update.go
@@ -0,0 +1,56 @@
+//go:build !(linux && 386)
+
+package services
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ log "github.com/sirupsen/logrus"
+
+ "github.com/netbirdio/netbird/client/proto"
+)
+
+// UpdateService exposes update triggering and result polling to the Wails frontend.
+type UpdateService struct {
+ grpcClient GRPCClientIface
+}
+
+// NewUpdateService creates a new UpdateService.
+func NewUpdateService(g GRPCClientIface) *UpdateService {
+ return &UpdateService{grpcClient: g}
+}
+
+// InstallerResult holds the result of an installer run.
+type InstallerResult struct {
+ Success bool `json:"success"`
+ ErrorMsg string `json:"errorMsg"`
+}
+
+// TriggerUpdate requests the daemon to perform an auto-update.
+func (s *UpdateService) TriggerUpdate() error {
+ return nil
+}
+
+// GetInstallerResult polls for the installer result (blocking until complete or timeout).
+func (s *UpdateService) GetInstallerResult() (*InstallerResult, error) {
+ conn, err := s.grpcClient.GetClient(3 * time.Second)
+ if err != nil {
+ return nil, fmt.Errorf("get client: %w", err)
+ }
+
+ ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute)
+ defer cancel()
+
+ resp, err := conn.GetInstallerResult(ctx, &proto.InstallerResultRequest{})
+ if err != nil {
+ log.Infof("GetInstallerResult ended (daemon may have restarted): %v", err)
+ return &InstallerResult{Success: true}, nil
+ }
+
+ return &InstallerResult{
+ Success: resp.Success,
+ ErrorMsg: resp.ErrorMsg,
+ }, nil
+}
diff --git a/client/uiwails/signal_unix.go b/client/uiwails/signal_unix.go
new file mode 100644
index 000000000..54af606b5
--- /dev/null
+++ b/client/uiwails/signal_unix.go
@@ -0,0 +1,40 @@
+//go:build !windows && !(linux && 386)
+
+package main
+
+import (
+ "context"
+ "os"
+ "os/signal"
+ "syscall"
+
+ log "github.com/sirupsen/logrus"
+ "github.com/wailsapp/wails/v3/pkg/application"
+)
+
+// setupSignalHandler listens for SIGUSR1 and shows the main window when received.
+func setupSignalHandler(ctx context.Context, window *application.WebviewWindow) {
+ sigChan := make(chan os.Signal, 1)
+ signal.Notify(sigChan, syscall.SIGUSR1)
+
+ go func() {
+ for {
+ select {
+ case <-ctx.Done():
+ return
+ case <-sigChan:
+ log.Info("received SIGUSR1 signal, showing window")
+ window.Show()
+ }
+ }
+ }()
+}
+
+// sendShowWindowSignal sends SIGUSR1 to an already-running instance to trigger window show.
+func sendShowWindowSignal(pid int32) error {
+ proc, err := os.FindProcess(int(pid))
+ if err != nil {
+ return err
+ }
+ return proc.Signal(syscall.SIGUSR1)
+}
diff --git a/client/uiwails/signal_windows.go b/client/uiwails/signal_windows.go
new file mode 100644
index 000000000..0d6f3a02d
--- /dev/null
+++ b/client/uiwails/signal_windows.go
@@ -0,0 +1,84 @@
+//go:build windows
+
+package main
+
+import (
+ "context"
+ "errors"
+ "time"
+
+ log "github.com/sirupsen/logrus"
+ "github.com/wailsapp/wails/v3/pkg/application"
+ "golang.org/x/sys/windows"
+)
+
+const (
+ fancyUITriggerEventName = `Global\NetBirdFancyUITriggerEvent`
+ waitTimeout = 5 * time.Second
+ desiredAccesses = windows.SYNCHRONIZE | windows.EVENT_MODIFY_STATE
+)
+
+// setupSignalHandler sets up a Windows Event-based signal handler.
+// When triggered, it shows the main window.
+func setupSignalHandler(ctx context.Context, window *application.WebviewWindow) {
+ eventNamePtr, err := windows.UTF16PtrFromString(fancyUITriggerEventName)
+ if err != nil {
+ log.Errorf("convert event name to UTF16: %v", err)
+ return
+ }
+
+ eventHandle, err := windows.CreateEvent(nil, 1, 0, eventNamePtr)
+ if err != nil {
+ if errors.Is(err, windows.ERROR_ALREADY_EXISTS) {
+ eventHandle, err = windows.OpenEvent(desiredAccesses, false, eventNamePtr)
+ if err != nil {
+ log.Errorf("open existing trigger event: %v", err)
+ return
+ }
+ } else {
+ log.Errorf("create trigger event: %v", err)
+ return
+ }
+ }
+
+ if eventHandle == windows.InvalidHandle {
+ log.Errorf("invalid handle for trigger event")
+ return
+ }
+
+ go waitForWindowsEvent(ctx, eventHandle, window)
+}
+
+func waitForWindowsEvent(ctx context.Context, eventHandle windows.Handle, window *application.WebviewWindow) {
+ defer func() {
+ if err := windows.CloseHandle(eventHandle); err != nil {
+ log.Errorf("close event handle: %v", err)
+ }
+ }()
+
+ for {
+ if ctx.Err() != nil {
+ return
+ }
+
+ status, err := windows.WaitForSingleObject(eventHandle, uint32(waitTimeout.Milliseconds()))
+
+ switch status {
+ case windows.WAIT_OBJECT_0:
+ log.Info("received trigger event signal, showing window")
+ if err := windows.ResetEvent(eventHandle); err != nil {
+ log.Errorf("reset event: %v", err)
+ }
+ window.Show()
+ case uint32(windows.WAIT_TIMEOUT):
+ // Timeout is expected — loop and poll again.
+ default:
+ log.Errorf("unexpected WaitForSingleObject status %d: %v", status, err)
+ select {
+ case <-time.After(5 * time.Second):
+ case <-ctx.Done():
+ return
+ }
+ }
+ }
+}
diff --git a/client/uiwails/tray.go b/client/uiwails/tray.go
new file mode 100644
index 000000000..8f3c55987
--- /dev/null
+++ b/client/uiwails/tray.go
@@ -0,0 +1,429 @@
+//go:build !(linux && 386)
+
+package main
+
+import (
+ "context"
+ "fmt"
+ "sort"
+ "sync"
+ "time"
+
+ log "github.com/sirupsen/logrus"
+ "github.com/wailsapp/wails/v3/pkg/application"
+
+ "github.com/netbirdio/netbird/client/uiwails/services"
+)
+
+const statusPollInterval = 5 * time.Second
+
+// trayManager manages the system tray state and menu.
+type trayManager struct {
+ app *application.App
+ window *application.WebviewWindow
+ tray *application.SystemTray
+ menu *application.Menu
+
+ connSvc *services.ConnectionService
+ settingsSvc *services.SettingsService
+ networkSvc *services.NetworkService
+ profileSvc *services.ProfileService
+
+ mu sync.Mutex
+ statusItem *application.MenuItem
+ exitNodeMenu *application.Menu
+
+ // toggle items tracked for updating checked state
+ sshItem *application.MenuItem
+ autoConnectItem *application.MenuItem
+ rosenpassItem *application.MenuItem
+ lazyConnItem *application.MenuItem
+ blockInboundItem *application.MenuItem
+ notificationsItem *application.MenuItem
+
+ exitNodeItems []*application.MenuItem
+ exitNodeStates []exitNodeState
+}
+
+type exitNodeState struct {
+ id string
+ selected bool
+}
+
+func newTrayManager(
+ app *application.App,
+ window *application.WebviewWindow,
+ connSvc *services.ConnectionService,
+ settingsSvc *services.SettingsService,
+ networkSvc *services.NetworkService,
+ profileSvc *services.ProfileService,
+) *trayManager {
+ return &trayManager{
+ app: app,
+ window: window,
+ connSvc: connSvc,
+ settingsSvc: settingsSvc,
+ networkSvc: networkSvc,
+ profileSvc: profileSvc,
+ }
+}
+
+// Setup creates and attaches the system tray.
+func (t *trayManager) Setup(icon []byte) {
+ t.tray = t.app.SystemTray.New()
+ t.tray.SetIcon(icon)
+
+ t.menu = t.buildMenu()
+ t.tray.AttachWindow(t.window).WindowOffset(5).SetMenu(t.menu)
+
+ // Load initial toggle states from config.
+ go t.refreshToggleStates()
+
+ // Start status polling goroutine.
+ go t.pollStatus(context.Background())
+}
+
+func (t *trayManager) buildMenu() *application.Menu {
+ menu := t.app.NewMenu()
+
+ // Status label (disabled, informational).
+ t.statusItem = menu.Add("Status: Disconnected")
+ t.statusItem.SetEnabled(false)
+ menu.AddSeparator()
+
+ // Connect / Disconnect.
+ menu.Add("Connect").OnClick(func(_ *application.Context) {
+ go func() {
+ if err := t.connSvc.Connect(); err != nil {
+ log.Errorf("connect: %v", err)
+ }
+ }()
+ })
+ menu.Add("Disconnect").OnClick(func(_ *application.Context) {
+ go func() {
+ if err := t.connSvc.Disconnect(); err != nil {
+ log.Errorf("disconnect: %v", err)
+ }
+ }()
+ })
+ menu.AddSeparator()
+
+ // Toggle checkboxes.
+ t.sshItem = menu.AddCheckbox("Allow SSH connections", false)
+ t.sshItem.OnClick(func(ctx *application.Context) {
+ enabled := ctx.ClickedMenuItem().Checked()
+ go func() {
+ if err := t.settingsSvc.ToggleSSH(enabled); err != nil {
+ log.Errorf("toggle SSH: %v", err)
+ t.sshItem.SetChecked(!enabled)
+ }
+ }()
+ })
+
+ t.autoConnectItem = menu.AddCheckbox("Connect automatically when service starts", false)
+ t.autoConnectItem.OnClick(func(ctx *application.Context) {
+ enabled := ctx.ClickedMenuItem().Checked()
+ go func() {
+ if err := t.settingsSvc.ToggleAutoConnect(enabled); err != nil {
+ log.Errorf("toggle auto-connect: %v", err)
+ t.autoConnectItem.SetChecked(!enabled)
+ }
+ }()
+ })
+
+ t.rosenpassItem = menu.AddCheckbox("Enable post-quantum security via Rosenpass", false)
+ t.rosenpassItem.OnClick(func(ctx *application.Context) {
+ enabled := ctx.ClickedMenuItem().Checked()
+ go func() {
+ if err := t.settingsSvc.ToggleRosenpass(enabled); err != nil {
+ log.Errorf("toggle Rosenpass: %v", err)
+ t.rosenpassItem.SetChecked(!enabled)
+ }
+ }()
+ })
+
+ t.lazyConnItem = menu.AddCheckbox("[Experimental] Enable lazy connections", false)
+ t.lazyConnItem.OnClick(func(ctx *application.Context) {
+ enabled := ctx.ClickedMenuItem().Checked()
+ go func() {
+ if err := t.settingsSvc.ToggleLazyConn(enabled); err != nil {
+ log.Errorf("toggle lazy connections: %v", err)
+ t.lazyConnItem.SetChecked(!enabled)
+ }
+ }()
+ })
+
+ t.blockInboundItem = menu.AddCheckbox("Block inbound connections", false)
+ t.blockInboundItem.OnClick(func(ctx *application.Context) {
+ enabled := ctx.ClickedMenuItem().Checked()
+ go func() {
+ if err := t.settingsSvc.ToggleBlockInbound(enabled); err != nil {
+ log.Errorf("toggle block inbound: %v", err)
+ t.blockInboundItem.SetChecked(!enabled)
+ }
+ }()
+ })
+
+ t.notificationsItem = menu.AddCheckbox("Enable notifications", true)
+ t.notificationsItem.OnClick(func(ctx *application.Context) {
+ enabled := ctx.ClickedMenuItem().Checked()
+ go func() {
+ if err := t.settingsSvc.ToggleNotifications(enabled); err != nil {
+ log.Errorf("toggle notifications: %v", err)
+ t.notificationsItem.SetChecked(!enabled)
+ }
+ }()
+ })
+
+ menu.AddSeparator()
+
+ // Exit Node submenu.
+ t.exitNodeMenu = menu.AddSubmenu("Exit Node")
+ t.exitNodeMenu.Add("No exit nodes").SetEnabled(false)
+
+ menu.AddSeparator()
+
+ // Navigation items — navigate React SPA.
+ menu.Add("Status").OnClick(func(_ *application.Context) {
+ t.window.EmitEvent("navigate", "/")
+ t.window.Show()
+ })
+ menu.Add("Settings").OnClick(func(_ *application.Context) {
+ t.window.EmitEvent("navigate", "/settings")
+ t.window.Show()
+ })
+ menu.Add("Peers").OnClick(func(_ *application.Context) {
+ t.window.EmitEvent("navigate", "/peers")
+ t.window.Show()
+ })
+ menu.Add("Networks").OnClick(func(_ *application.Context) {
+ t.window.EmitEvent("navigate", "/networks")
+ t.window.Show()
+ })
+ menu.Add("Profiles").OnClick(func(_ *application.Context) {
+ t.window.EmitEvent("navigate", "/profiles")
+ t.window.Show()
+ })
+ menu.Add("Debug").OnClick(func(_ *application.Context) {
+ t.window.EmitEvent("navigate", "/debug")
+ t.window.Show()
+ })
+ menu.Add("Update").OnClick(func(_ *application.Context) {
+ t.window.EmitEvent("navigate", "/update")
+ t.window.Show()
+ })
+
+ menu.AddSeparator()
+
+ menu.Add("Quit").OnClick(func(_ *application.Context) {
+ t.app.Quit()
+ })
+
+ return menu
+}
+
+// pollStatus polls the daemon status every statusPollInterval and updates the tray.
+// Exit nodes are refreshed every 10 cycles (~20 seconds).
+func (t *trayManager) pollStatus(ctx context.Context) {
+ ticker := time.NewTicker(statusPollInterval)
+ defer ticker.Stop()
+
+ var cycle int
+ for {
+ select {
+ case <-ctx.Done():
+ return
+ case <-ticker.C:
+ status, err := t.connSvc.GetStatus()
+ if err != nil {
+ log.Warnf("pollStatus: failed to get status: %v", err)
+ continue
+ }
+ log.Debugf("pollStatus: status=%q ip=%q fqdn=%q peers=%d",
+ status.Status, status.IP, status.Fqdn, status.ConnectedPeers)
+ t.updateStatus(status)
+
+ cycle++
+ if cycle%10 == 0 {
+ go t.refreshExitNodes()
+ }
+ }
+ }
+}
+
+func (t *trayManager) updateStatus(status *services.StatusInfo) {
+ label := fmt.Sprintf("Status: %s", status.Status)
+ if status.IP != "" {
+ label += fmt.Sprintf(" (%s)", status.IP)
+ }
+ t.statusItem.SetLabel(label)
+ t.menu.Update()
+
+ // Update tray icon based on status.
+ icon := iconForStatus(status.Status)
+ if icon != nil {
+ t.tray.SetIcon(icon)
+ }
+
+ // Emit event so the React frontend can update live.
+ log.Debugf("updateStatus: emitting status-changed event: status=%q ip=%q", status.Status, status.IP)
+ t.window.EmitEvent("status-changed", status)
+}
+
+func (t *trayManager) refreshToggleStates() {
+ cfg, err := t.settingsSvc.GetConfig()
+ if err != nil {
+ log.Debugf("refresh toggle states: %v", err)
+ return
+ }
+
+ t.sshItem.SetChecked(cfg.ServerSSHAllowed)
+ t.autoConnectItem.SetChecked(!cfg.DisableAutoConnect)
+ t.rosenpassItem.SetChecked(cfg.RosenpassEnabled)
+ t.lazyConnItem.SetChecked(cfg.LazyConnectionEnabled)
+ t.blockInboundItem.SetChecked(cfg.BlockInbound)
+ t.notificationsItem.SetChecked(!cfg.DisableNotifications)
+ t.menu.Update()
+}
+
+func (t *trayManager) refreshExitNodes() {
+ exitNodes, err := t.networkSvc.ListExitNodes()
+ if err != nil {
+ log.Debugf("refresh exit nodes: %v", err)
+ return
+ }
+
+ t.mu.Lock()
+ defer t.mu.Unlock()
+
+ t.rebuildExitNodeMenu(exitNodes)
+}
+
+func (t *trayManager) rebuildExitNodeMenu(exitNodes []services.NetworkInfo) {
+ // Sort exit nodes by ID for stable ordering.
+ sort.Slice(exitNodes, func(i, j int) bool {
+ return exitNodes[i].ID < exitNodes[j].ID
+ })
+
+ // Check if state has changed.
+ newStates := make([]exitNodeState, 0, len(exitNodes))
+ for _, n := range exitNodes {
+ newStates = append(newStates, exitNodeState{id: n.ID, selected: n.Selected})
+ }
+
+ if statesEqual(t.exitNodeStates, newStates) {
+ return
+ }
+ t.exitNodeStates = newStates
+
+ // Rebuild the exit node submenu from scratch.
+ // Wails v3 doesn't have a RemoveAll, so we recreate the submenu reference.
+ for _, item := range t.exitNodeItems {
+ item.SetHidden(true)
+ }
+ t.exitNodeItems = nil
+
+ if len(exitNodes) == 0 {
+ t.menu.Update()
+ return
+ }
+
+ var hasSelected bool
+ for _, node := range exitNodes {
+ n := node // capture
+ item := t.exitNodeMenu.AddCheckbox(n.ID, n.Selected)
+ item.OnClick(func(_ *application.Context) {
+ go t.toggleExitNode(n.ID)
+ })
+ t.exitNodeItems = append(t.exitNodeItems, item)
+ if n.Selected {
+ hasSelected = true
+ }
+ }
+
+ if hasSelected {
+ t.exitNodeMenu.AddSeparator()
+ deselectAll := t.exitNodeMenu.Add("Deselect All")
+ deselectAll.OnClick(func(_ *application.Context) {
+ go t.deselectAllExitNodes()
+ })
+ t.exitNodeItems = append(t.exitNodeItems, deselectAll)
+ }
+
+ t.menu.Update()
+}
+
+func (t *trayManager) toggleExitNode(id string) {
+ exitNodes, err := t.networkSvc.ListExitNodes()
+ if err != nil {
+ log.Errorf("list exit nodes: %v", err)
+ return
+ }
+
+ var target *services.NetworkInfo
+ var selectedOtherIDs []string
+
+ for i, n := range exitNodes {
+ if n.ID == id {
+ cp := exitNodes[i]
+ target = &cp
+ } else if n.Selected {
+ selectedOtherIDs = append(selectedOtherIDs, n.ID)
+ }
+ }
+
+ // Deselect all other selected exit nodes.
+ if len(selectedOtherIDs) > 0 {
+ if err := t.networkSvc.DeselectNetworks(selectedOtherIDs); err != nil {
+ log.Errorf("deselect exit nodes: %v", err)
+ }
+ }
+
+ if target != nil && !target.Selected {
+ if err := t.networkSvc.SelectNetwork(id); err != nil {
+ log.Errorf("select exit node: %v", err)
+ }
+ } else if target != nil && target.Selected && len(selectedOtherIDs) == 0 {
+ // Node is the only selected one — deselect it.
+ if err := t.networkSvc.DeselectNetwork(id); err != nil {
+ log.Errorf("deselect exit node: %v", err)
+ }
+ }
+
+ t.refreshExitNodes()
+}
+
+func (t *trayManager) deselectAllExitNodes() {
+ exitNodes, err := t.networkSvc.ListExitNodes()
+ if err != nil {
+ log.Errorf("list exit nodes for deselect all: %v", err)
+ return
+ }
+
+ var ids []string
+ for _, n := range exitNodes {
+ if n.Selected {
+ ids = append(ids, n.ID)
+ }
+ }
+
+ if len(ids) > 0 {
+ if err := t.networkSvc.DeselectNetworks(ids); err != nil {
+ log.Errorf("deselect all exit nodes: %v", err)
+ }
+ }
+
+ t.refreshExitNodes()
+}
+
+func statesEqual(a, b []exitNodeState) bool {
+ if len(a) != len(b) {
+ return false
+ }
+ for i := range a {
+ if a[i] != b[i] {
+ return false
+ }
+ }
+ return true
+}
diff --git a/client/uiwails/tray_linux.go b/client/uiwails/tray_linux.go
new file mode 100644
index 000000000..a213ce2f9
--- /dev/null
+++ b/client/uiwails/tray_linux.go
@@ -0,0 +1,24 @@
+//go:build linux && !386
+
+package main
+
+import "os"
+
+// init runs before Wails' own init(), so the env var is set in time.
+func init() {
+ if os.Getenv("WEBKIT_DISABLE_DMABUF_RENDERER") != "" {
+ return
+ }
+
+ // WebKitGTK's DMA-BUF renderer fails on many setups (VMs, containers,
+ // minimal WMs without proper GPU access) and leaves the window blank
+ // white. Wails only disables it for NVIDIA+Wayland, but the issue is
+ // broader. Always disable it — software rendering works fine for a
+ // small UI like this.
+ _ = os.Setenv("WEBKIT_DISABLE_DMABUF_RENDERER", "1")
+}
+
+// On Linux, the system tray provider may require the menu to be recreated
+// rather than updated in place. The rebuildExitNodeMenu method in tray.go
+// already handles this by removing and re-adding items; no additional
+// Linux-specific workaround is needed for Wails v3.
diff --git a/client/uiwails/tray_watcher_linux.go b/client/uiwails/tray_watcher_linux.go
new file mode 100644
index 000000000..7a9b72096
--- /dev/null
+++ b/client/uiwails/tray_watcher_linux.go
@@ -0,0 +1,148 @@
+//go:build linux && !(linux && 386)
+
+package main
+
+// startStatusNotifierWatcher registers org.kde.StatusNotifierWatcher on the
+// session D-Bus if no other process has already claimed it.
+//
+// Minimal window managers (Fluxbox, OpenBox, i3, etc.) do not ship a
+// StatusNotifier watcher, so tray icons using libayatana-appindicator or
+// the KDE/freedesktop StatusNotifier protocol silently fail.
+//
+// By owning the watcher name in-process we allow the Wails v3 built-in tray
+// to register itself — no external daemon or package needed.
+//
+// When an XEmbed system tray is available (_NET_SYSTEM_TRAY_S0), we also
+// start an in-process XEmbed host that bridges the SNI icon into the
+// XEmbed tray (Fluxbox, IceWM, etc.).
+
+import (
+ "sync"
+
+ "github.com/godbus/dbus/v5"
+ log "github.com/sirupsen/logrus"
+)
+
+const (
+ watcherName = "org.kde.StatusNotifierWatcher"
+ watcherPath = "/StatusNotifierWatcher"
+ watcherIface = "org.kde.StatusNotifierWatcher"
+)
+
+type statusNotifierWatcher struct {
+ conn *dbus.Conn
+ items []string
+ hosts map[string]*xembedHost
+ hostsMu sync.Mutex
+}
+
+// RegisterStatusNotifierItem is the D-Bus method called by tray clients.
+// The sender parameter is automatically injected by godbus with the caller's
+// unique bus name (e.g. ":1.42"). It does not appear in the D-Bus signature.
+func (w *statusNotifierWatcher) RegisterStatusNotifierItem(sender dbus.Sender, service string) *dbus.Error {
+ for _, s := range w.items {
+ if s == service {
+ return nil
+ }
+ }
+ w.items = append(w.items, service)
+ log.Debugf("StatusNotifierWatcher: registered item %q from %s", service, sender)
+
+ go w.tryStartXembedHost(string(sender), dbus.ObjectPath(service))
+ return nil
+}
+
+// RegisterStatusNotifierHost is required by the protocol but unused here.
+func (w *statusNotifierWatcher) RegisterStatusNotifierHost(service string) *dbus.Error {
+ log.Debugf("StatusNotifierWatcher: host registered %q", service)
+ return nil
+}
+
+// tryStartXembedHost attempts to create an XEmbed tray icon for the given
+// SNI item. If no XEmbed tray manager is available, this is a no-op.
+func (w *statusNotifierWatcher) tryStartXembedHost(busName string, objPath dbus.ObjectPath) {
+ w.hostsMu.Lock()
+ defer w.hostsMu.Unlock()
+
+ if _, exists := w.hosts[busName]; exists {
+ return
+ }
+
+ // Use a private session bus so our signal subscriptions don't
+ // interfere with Wails' signal handler (which panics on unexpected signals).
+ sessionConn, err := dbus.SessionBusPrivate()
+ if err != nil {
+ log.Debugf("StatusNotifierWatcher: cannot open private session bus for XEmbed host: %v", err)
+ return
+ }
+ if err := sessionConn.Auth(nil); err != nil {
+ log.Debugf("StatusNotifierWatcher: XEmbed host auth failed: %v", err)
+ _ = sessionConn.Close()
+ return
+ }
+ if err := sessionConn.Hello(); err != nil {
+ log.Debugf("StatusNotifierWatcher: XEmbed host Hello failed: %v", err)
+ _ = sessionConn.Close()
+ return
+ }
+
+ host, err := newXembedHost(sessionConn, busName, objPath)
+ if err != nil {
+ log.Debugf("StatusNotifierWatcher: XEmbed host not started: %v", err)
+ return
+ }
+
+ w.hosts[busName] = host
+ go host.run()
+ log.Infof("StatusNotifierWatcher: XEmbed tray icon created for %s", busName)
+}
+
+// startStatusNotifierWatcher claims org.kde.StatusNotifierWatcher on the
+// session bus if it is not already provided by another process.
+// Safe to call unconditionally — it does nothing when a real watcher is present.
+func startStatusNotifierWatcher() {
+ conn, err := dbus.SessionBusPrivate()
+ if err != nil {
+ log.Debugf("StatusNotifierWatcher: cannot open private session bus: %v", err)
+ return
+ }
+ if err := conn.Auth(nil); err != nil {
+ log.Debugf("StatusNotifierWatcher: auth failed: %v", err)
+ _ = conn.Close()
+ return
+ }
+ if err := conn.Hello(); err != nil {
+ log.Debugf("StatusNotifierWatcher: Hello failed: %v", err)
+ _ = conn.Close()
+ return
+ }
+
+ // Check whether another process already owns the watcher name.
+ var owner string
+ callErr := conn.BusObject().Call("org.freedesktop.DBus.GetNameOwner", 0, watcherName).Store(&owner)
+ if callErr == nil && owner != "" {
+ log.Debugf("StatusNotifierWatcher: already owned by %s, skipping", owner)
+ _ = conn.Close()
+ return
+ }
+
+ reply, err := conn.RequestName(watcherName, dbus.NameFlagDoNotQueue)
+ if err != nil || reply != dbus.RequestNameReplyPrimaryOwner {
+ log.Debugf("StatusNotifierWatcher: could not claim name (reply=%v err=%v)", reply, err)
+ _ = conn.Close()
+ return
+ }
+
+ w := &statusNotifierWatcher{
+ conn: conn,
+ hosts: make(map[string]*xembedHost),
+ }
+ if err := conn.ExportAll(w, dbus.ObjectPath(watcherPath), watcherIface); err != nil {
+ log.Errorf("StatusNotifierWatcher: export failed: %v", err)
+ _ = conn.Close()
+ return
+ }
+
+ log.Infof("StatusNotifierWatcher: active on session bus (enables tray on minimal WMs)")
+ // Connection intentionally kept open for the lifetime of the process.
+}
diff --git a/client/uiwails/tray_watcher_other.go b/client/uiwails/tray_watcher_other.go
new file mode 100644
index 000000000..aa8fc8ac7
--- /dev/null
+++ b/client/uiwails/tray_watcher_other.go
@@ -0,0 +1,6 @@
+//go:build !linux || (linux && 386)
+
+package main
+
+// startStatusNotifierWatcher is a no-op on non-Linux platforms.
+func startStatusNotifierWatcher() {}
diff --git a/client/uiwails/uiwails b/client/uiwails/uiwails
new file mode 100755
index 000000000..4615444ea
Binary files /dev/null and b/client/uiwails/uiwails differ
diff --git a/client/uiwails/util.go b/client/uiwails/util.go
new file mode 100644
index 000000000..9d9bb6ad0
--- /dev/null
+++ b/client/uiwails/util.go
@@ -0,0 +1,13 @@
+//go:build !(linux && 386)
+
+package main
+
+import (
+ "context"
+ "time"
+)
+
+// defaultContext returns a context with the given timeout.
+func defaultContext(timeout time.Duration) (context.Context, context.CancelFunc) {
+ return context.WithTimeout(context.Background(), timeout)
+}
diff --git a/client/uiwails/wails.json b/client/uiwails/wails.json
new file mode 100644
index 000000000..43f93bc4c
--- /dev/null
+++ b/client/uiwails/wails.json
@@ -0,0 +1,8 @@
+{
+ "name": "netbird-ui",
+ "outputfilename": "netbird-ui",
+ "frontend:install": "npm install",
+ "frontend:build": "npm run build",
+ "frontend:dev:watcher": "npm run dev",
+ "frontend:dev:serverurl": "auto"
+}
diff --git a/client/uiwails/xembed_host_linux.go b/client/uiwails/xembed_host_linux.go
new file mode 100644
index 000000000..4cf11e7d9
--- /dev/null
+++ b/client/uiwails/xembed_host_linux.go
@@ -0,0 +1,393 @@
+//go:build linux && !(linux && 386)
+
+package main
+
+/*
+#cgo pkg-config: x11 gtk+-3.0
+#cgo LDFLAGS: -lX11
+#include "xembed_tray.h"
+#include
+#include
+*/
+import "C"
+
+import (
+ "errors"
+ "sync"
+ "time"
+ "unsafe"
+
+ "github.com/godbus/dbus/v5"
+ log "github.com/sirupsen/logrus"
+)
+
+// activeMenuHost is the xembedHost that currently owns the popup menu.
+// This is needed because C callbacks cannot carry Go pointers.
+var (
+ activeMenuHost *xembedHost
+ activeMenuHostMu sync.Mutex
+)
+
+//export goMenuItemClicked
+func goMenuItemClicked(id C.int) {
+ activeMenuHostMu.Lock()
+ h := activeMenuHost
+ activeMenuHostMu.Unlock()
+
+ if h != nil {
+ go h.sendMenuEvent(int32(id))
+ }
+}
+
+// xembedHost manages one XEmbed tray icon for an SNI item.
+type xembedHost struct {
+ conn *dbus.Conn
+ busName string
+ objPath dbus.ObjectPath
+
+ dpy *C.Display
+ trayMgr C.Window
+ iconWin C.Window
+ iconSize int
+
+ mu sync.Mutex
+ iconData []byte
+ iconW int
+ iconH int
+
+ stopCh chan struct{}
+}
+
+// newXembedHost creates an XEmbed tray icon for the given SNI item.
+// Returns an error if no XEmbed tray manager is available (graceful fallback).
+func newXembedHost(conn *dbus.Conn, busName string, objPath dbus.ObjectPath) (*xembedHost, error) {
+ dpy := C.XOpenDisplay(nil)
+ if dpy == nil {
+ return nil, errors.New("cannot open X display")
+ }
+
+ screen := C.xembed_default_screen(dpy)
+ trayMgr := C.xembed_find_tray(dpy, screen)
+ if trayMgr == 0 {
+ C.XCloseDisplay(dpy)
+ return nil, errors.New("no XEmbed system tray found")
+ }
+
+ // Query the tray manager's preferred icon size.
+ iconSize := int(C.xembed_get_icon_size(dpy, trayMgr))
+ if iconSize <= 0 {
+ iconSize = 24 // fallback
+ }
+
+ iconWin := C.xembed_create_icon(dpy, screen, C.int(iconSize), trayMgr)
+ if iconWin == 0 {
+ C.XCloseDisplay(dpy)
+ return nil, errors.New("failed to create icon window")
+ }
+
+ if C.xembed_dock(dpy, trayMgr, iconWin) != 0 {
+ C.xembed_destroy_icon(dpy, iconWin)
+ C.XCloseDisplay(dpy)
+ return nil, errors.New("failed to dock icon")
+ }
+
+ h := &xembedHost{
+ conn: conn,
+ busName: busName,
+ objPath: objPath,
+ dpy: dpy,
+ trayMgr: trayMgr,
+ iconWin: iconWin,
+ iconSize: iconSize,
+ stopCh: make(chan struct{}),
+ }
+
+ h.fetchAndDrawIcon()
+ return h, nil
+}
+
+// fetchAndDrawIcon reads IconPixmap from the SNI item via D-Bus and draws it.
+func (h *xembedHost) fetchAndDrawIcon() {
+ obj := h.conn.Object(h.busName, h.objPath)
+ variant, err := obj.GetProperty("org.kde.StatusNotifierItem.IconPixmap")
+ if err != nil {
+ log.Debugf("xembed: failed to get IconPixmap: %v", err)
+ return
+ }
+
+ // IconPixmap is []struct{W, H int32; Pix []byte} on D-Bus,
+ // represented as a(iiay) signature.
+ type px struct {
+ W int32
+ H int32
+ Pix []byte
+ }
+
+ var icons []px
+ if err := variant.Store(&icons); err != nil {
+ log.Debugf("xembed: failed to decode IconPixmap: %v", err)
+ return
+ }
+
+ if len(icons) == 0 {
+ log.Debug("xembed: IconPixmap is empty")
+ return
+ }
+
+ icon := icons[0]
+ if icon.W <= 0 || icon.H <= 0 || len(icon.Pix) < int(icon.W*icon.H*4) {
+ log.Debug("xembed: invalid IconPixmap data")
+ return
+ }
+
+ h.mu.Lock()
+ h.iconData = icon.Pix
+ h.iconW = int(icon.W)
+ h.iconH = int(icon.H)
+ h.mu.Unlock()
+
+ h.drawIcon()
+}
+
+// drawIcon draws the cached icon data onto the X11 window.
+func (h *xembedHost) drawIcon() {
+ h.mu.Lock()
+ data := h.iconData
+ w := h.iconW
+ ht := h.iconH
+ h.mu.Unlock()
+
+ if data == nil || w <= 0 || ht <= 0 {
+ return
+ }
+
+ cData := C.CBytes(data)
+ defer C.free(cData)
+
+ C.xembed_draw_icon(h.dpy, h.iconWin, C.int(h.iconSize),
+ (*C.uchar)(cData), C.int(w), C.int(ht))
+}
+
+// run is the main event loop. It polls X11 events and listens for D-Bus
+// NewIcon signals to keep the tray icon updated.
+func (h *xembedHost) run() {
+ // Subscribe to NewIcon signals from the SNI item.
+ matchRule := "type='signal',interface='org.kde.StatusNotifierItem',member='NewIcon',sender='" + h.busName + "'"
+ if err := h.conn.BusObject().Call("org.freedesktop.DBus.AddMatch", 0, matchRule).Err; err != nil {
+ log.Debugf("xembed: failed to add signal match: %v", err)
+ }
+
+ sigCh := make(chan *dbus.Signal, 16)
+ h.conn.Signal(sigCh)
+ defer h.conn.RemoveSignal(sigCh)
+
+ ticker := time.NewTicker(50 * time.Millisecond)
+ defer ticker.Stop()
+
+ for {
+ select {
+ case <-h.stopCh:
+ return
+
+ case sig := <-sigCh:
+ if sig == nil {
+ continue
+ }
+ if sig.Name == "org.kde.StatusNotifierItem.NewIcon" {
+ h.fetchAndDrawIcon()
+ }
+
+ case <-ticker.C:
+ var outX, outY C.int
+ result := C.xembed_poll_event(h.dpy, h.iconWin, &outX, &outY)
+
+ switch result {
+ case 1: // left click
+ go h.activate(int32(outX), int32(outY))
+ case 2: // right click
+ log.Infof("xembed: right-click at (%d, %d)", int(outX), int(outY))
+ go h.contextMenu(int32(outX), int32(outY))
+ case 3: // expose
+ h.drawIcon()
+ case 4: // configure (resize)
+ newSize := int(outX)
+ if newSize > 0 && newSize != h.iconSize {
+ h.iconSize = newSize
+ h.drawIcon()
+ }
+ case -1: // tray died
+ log.Info("xembed: tray manager destroyed, cleaning up")
+ return
+ }
+ }
+ }
+}
+
+func (h *xembedHost) activate(x, y int32) {
+ obj := h.conn.Object(h.busName, h.objPath)
+ if err := obj.Call("org.kde.StatusNotifierItem.Activate", 0, x, y).Err; err != nil {
+ log.Debugf("xembed: Activate call failed: %v", err)
+ }
+}
+
+func (h *xembedHost) contextMenu(x, y int32) {
+ // Read the menu path from the SNI item's Menu property.
+ menuPath := dbus.ObjectPath("/StatusNotifierMenu")
+
+ // Fetch menu layout from com.canonical.dbusmenu.
+ menuObj := h.conn.Object(h.busName, menuPath)
+ var revision uint32
+ var layout dbusMenuLayout
+ err := menuObj.Call("com.canonical.dbusmenu.GetLayout", 0,
+ int32(0), // parentId (root)
+ int32(-1), // recursionDepth (all)
+ []string{}, // propertyNames (all)
+ ).Store(&revision, &layout)
+ if err != nil {
+ log.Debugf("xembed: GetLayout failed: %v", err)
+ return
+ }
+
+ items := h.flattenMenu(layout)
+ log.Infof("xembed: menu has %d items (revision %d)", len(items), revision)
+ for i, mi := range items {
+ log.Infof("xembed: menu[%d] id=%d label=%q sep=%v check=%v", i, mi.id, mi.label, mi.isSeparator, mi.isCheck)
+ }
+ if len(items) == 0 {
+ return
+ }
+
+ // Build C menu item array.
+ cItems := make([]C.xembed_menu_item, len(items))
+ cLabels := make([]*C.char, len(items)) // track for freeing
+ for i, mi := range items {
+ cItems[i].id = C.int(mi.id)
+ cItems[i].enabled = boolToInt(mi.enabled)
+ cItems[i].is_check = boolToInt(mi.isCheck)
+ cItems[i].checked = boolToInt(mi.checked)
+ cItems[i].is_separator = boolToInt(mi.isSeparator)
+ if mi.label != "" {
+ cLabels[i] = C.CString(mi.label)
+ cItems[i].label = cLabels[i]
+ }
+ }
+ defer func() {
+ for _, p := range cLabels {
+ if p != nil {
+ C.free(unsafe.Pointer(p))
+ }
+ }
+ }()
+
+ // Set the active menu host so the C callback can reach us.
+ activeMenuHostMu.Lock()
+ activeMenuHost = h
+ activeMenuHostMu.Unlock()
+
+ C.xembed_show_popup_menu(&cItems[0], C.int(len(cItems)),
+ nil, C.int(x), C.int(y))
+}
+
+// dbusMenuLayout represents a com.canonical.dbusmenu layout item.
+type dbusMenuLayout struct {
+ ID int32
+ Properties map[string]dbus.Variant
+ Children []dbus.Variant
+}
+
+type menuItemInfo struct {
+ id int32
+ label string
+ enabled bool
+ isCheck bool
+ checked bool
+ isSeparator bool
+}
+
+func (h *xembedHost) flattenMenu(layout dbusMenuLayout) []menuItemInfo {
+ var items []menuItemInfo
+
+ for _, childVar := range layout.Children {
+ var child dbusMenuLayout
+ if err := dbus.Store([]interface{}{childVar.Value()}, &child); err != nil {
+ continue
+ }
+
+ mi := menuItemInfo{
+ id: child.ID,
+ enabled: true,
+ }
+
+ if v, ok := child.Properties["type"]; ok {
+ if s, ok := v.Value().(string); ok && s == "separator" {
+ mi.isSeparator = true
+ items = append(items, mi)
+ continue
+ }
+ }
+
+ if v, ok := child.Properties["label"]; ok {
+ if s, ok := v.Value().(string); ok {
+ mi.label = s
+ }
+ }
+
+ if v, ok := child.Properties["enabled"]; ok {
+ if b, ok := v.Value().(bool); ok {
+ mi.enabled = b
+ }
+ }
+
+ if v, ok := child.Properties["visible"]; ok {
+ if b, ok := v.Value().(bool); ok && !b {
+ continue // skip hidden items
+ }
+ }
+
+ if v, ok := child.Properties["toggle-type"]; ok {
+ if s, ok := v.Value().(string); ok && s == "checkmark" {
+ mi.isCheck = true
+ }
+ }
+
+ if v, ok := child.Properties["toggle-state"]; ok {
+ if n, ok := v.Value().(int32); ok && n == 1 {
+ mi.checked = true
+ }
+ }
+
+ items = append(items, mi)
+ }
+
+ return items
+}
+
+func (h *xembedHost) sendMenuEvent(id int32) {
+ menuPath := dbus.ObjectPath("/StatusNotifierMenu")
+ menuObj := h.conn.Object(h.busName, menuPath)
+ data := dbus.MakeVariant("")
+ err := menuObj.Call("com.canonical.dbusmenu.Event", 0,
+ id, "clicked", data, uint32(0)).Err
+ if err != nil {
+ log.Debugf("xembed: menu Event call failed: %v", err)
+ }
+}
+
+func boolToInt(b bool) C.int {
+ if b {
+ return 1
+ }
+ return 0
+}
+
+func (h *xembedHost) stop() {
+ select {
+ case <-h.stopCh:
+ return // already stopped
+ default:
+ close(h.stopCh)
+ }
+
+ C.xembed_destroy_icon(h.dpy, h.iconWin)
+ C.XCloseDisplay(h.dpy)
+}
diff --git a/client/uiwails/xembed_host_other.go b/client/uiwails/xembed_host_other.go
new file mode 100644
index 000000000..c93d78413
--- /dev/null
+++ b/client/uiwails/xembed_host_other.go
@@ -0,0 +1,18 @@
+//go:build !linux || (linux && 386)
+
+package main
+
+import (
+ "errors"
+
+ "github.com/godbus/dbus/v5"
+)
+
+type xembedHost struct{}
+
+func newXembedHost(_ *dbus.Conn, _ string, _ dbus.ObjectPath) (*xembedHost, error) {
+ return nil, errors.New("XEmbed tray not supported on this platform")
+}
+
+func (h *xembedHost) run() {}
+func (h *xembedHost) stop() {}
diff --git a/client/uiwails/xembed_tray.c b/client/uiwails/xembed_tray.c
new file mode 100644
index 000000000..749a38c0a
--- /dev/null
+++ b/client/uiwails/xembed_tray.c
@@ -0,0 +1,428 @@
+#include "xembed_tray.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+#define SYSTEM_TRAY_REQUEST_DOCK 0
+#define XEMBED_MAPPED (1 << 0)
+
+Window xembed_find_tray(Display *dpy, int screen) {
+ char atom_name[64];
+ snprintf(atom_name, sizeof(atom_name), "_NET_SYSTEM_TRAY_S%d", screen);
+ Atom sel = XInternAtom(dpy, atom_name, False);
+ return XGetSelectionOwner(dpy, sel);
+}
+
+int xembed_get_icon_size(Display *dpy, Window tray_mgr) {
+ Atom atom = XInternAtom(dpy, "_NET_SYSTEM_TRAY_ICON_SIZE", False);
+ Atom actual_type;
+ int actual_format;
+ unsigned long nitems, bytes_after;
+ unsigned char *prop = NULL;
+ int size = 0;
+
+ if (XGetWindowProperty(dpy, tray_mgr, atom, 0, 1, False,
+ XA_CARDINAL, &actual_type, &actual_format,
+ &nitems, &bytes_after, &prop) == Success) {
+ if (prop && nitems == 1 && actual_format == 32) {
+ size = (int)(*(unsigned long *)prop);
+ }
+ if (prop)
+ XFree(prop);
+ }
+ return size;
+}
+
+/* Find a 32-bit TrueColor ARGB visual on the given screen. */
+static Visual *find_argb_visual(Display *dpy, int screen, int *out_depth) {
+ XVisualInfo tmpl;
+ tmpl.screen = screen;
+ tmpl.depth = 32;
+ tmpl.class = TrueColor;
+ int ninfo = 0;
+ XVisualInfo *vi = XGetVisualInfo(dpy,
+ VisualScreenMask | VisualDepthMask | VisualClassMask,
+ &tmpl, &ninfo);
+ if (!vi || ninfo == 0)
+ return NULL;
+
+ Visual *vis = vi[0].visual;
+ *out_depth = vi[0].depth;
+ XFree(vi);
+ return vis;
+}
+
+/* Try to get a 32-bit ARGB visual for the tray icon.
+ First checks _NET_SYSTEM_TRAY_VISUAL on the tray manager window;
+ if not set, searches for any 32-bit TrueColor visual on the screen. */
+static Visual *get_tray_visual(Display *dpy, int screen, Window tray_mgr,
+ int *out_depth) {
+ Atom atom = XInternAtom(dpy, "_NET_SYSTEM_TRAY_VISUAL", False);
+ Atom actual_type;
+ int actual_format;
+ unsigned long nitems, bytes_after;
+ unsigned char *prop = NULL;
+
+ if (XGetWindowProperty(dpy, tray_mgr, atom, 0, 1, False,
+ XA_VISUALID, &actual_type, &actual_format,
+ &nitems, &bytes_after, &prop) == Success &&
+ prop && nitems == 1) {
+ VisualID vid = (VisualID)(*(unsigned long *)prop);
+ XFree(prop);
+
+ /* Look up the visual by ID. */
+ XVisualInfo tmpl;
+ tmpl.visualid = vid;
+ tmpl.screen = screen;
+ int ninfo = 0;
+ XVisualInfo *vi = XGetVisualInfo(dpy,
+ VisualIDMask | VisualScreenMask, &tmpl, &ninfo);
+ if (vi && ninfo > 0) {
+ Visual *vis = vi[0].visual;
+ *out_depth = vi[0].depth;
+ XFree(vi);
+ return vis;
+ }
+ } else {
+ if (prop) XFree(prop);
+ }
+
+ /* Tray didn't advertise a visual — find one ourselves. */
+ return find_argb_visual(dpy, screen, out_depth);
+}
+
+Window xembed_create_icon(Display *dpy, int screen, int size,
+ Window tray_mgr) {
+ Window root = RootWindow(dpy, screen);
+
+ /* Try to use the tray's advertised ARGB visual for transparency. */
+ int depth = 0;
+ Visual *vis = get_tray_visual(dpy, screen, tray_mgr, &depth);
+
+ XSetWindowAttributes attrs;
+ memset(&attrs, 0, sizeof(attrs));
+ attrs.event_mask = ButtonPressMask | StructureNotifyMask | ExposureMask;
+ unsigned long mask = CWEventMask;
+
+ if (vis && depth == 32) {
+ /* 32-bit visual: create our own colormap and set a transparent bg. */
+ attrs.colormap = XCreateColormap(dpy, root, vis, AllocNone);
+ attrs.background_pixel = 0; /* fully transparent */
+ attrs.border_pixel = 0;
+ mask |= CWColormap | CWBackPixel | CWBorderPixel;
+ } else {
+ /* Fallback: use default visual. */
+ vis = CopyFromParent;
+ depth = CopyFromParent;
+ attrs.background_pixel = 0;
+ mask |= CWBackPixel;
+ }
+
+ Window win = XCreateWindow(
+ dpy, root,
+ 0, 0, size, size,
+ 0, /* border width */
+ depth,
+ InputOutput,
+ vis,
+ mask,
+ &attrs
+ );
+
+ /* Set _XEMBED_INFO: version=0, flags=XEMBED_MAPPED */
+ Atom xembed_info = XInternAtom(dpy, "_XEMBED_INFO", False);
+ unsigned long info[2] = { 0, XEMBED_MAPPED };
+ XChangeProperty(dpy, win, xembed_info, xembed_info,
+ 32, PropModeReplace, (unsigned char *)info, 2);
+
+ return win;
+}
+
+int xembed_dock(Display *dpy, Window tray_mgr, Window icon_win) {
+ Atom opcode = XInternAtom(dpy, "_NET_SYSTEM_TRAY_OPCODE", False);
+
+ XClientMessageEvent ev;
+ memset(&ev, 0, sizeof(ev));
+ ev.type = ClientMessage;
+ ev.window = tray_mgr;
+ ev.message_type = opcode;
+ ev.format = 32;
+ ev.data.l[0] = CurrentTime;
+ ev.data.l[1] = SYSTEM_TRAY_REQUEST_DOCK;
+ ev.data.l[2] = (long)icon_win;
+
+ XSendEvent(dpy, tray_mgr, False, NoEventMask, (XEvent *)&ev);
+ XFlush(dpy);
+ return 0;
+}
+
+void xembed_draw_icon(Display *dpy, Window icon_win, int win_size,
+ const unsigned char *data, int img_w, int img_h) {
+ if (!data || img_w <= 0 || img_h <= 0 || win_size <= 0)
+ return;
+
+ /* Query the window's actual visual and depth so we draw correctly
+ even when using a 32-bit ARGB visual for transparency. */
+ XWindowAttributes wa;
+ if (!XGetWindowAttributes(dpy, icon_win, &wa))
+ return;
+
+ int depth = wa.depth;
+ Visual *vis = wa.visual;
+
+ /* Clear the window to transparent before drawing. */
+ XClearWindow(dpy, icon_win);
+
+ /* Allocate buffer for the scaled image in native X11 format (32bpp). */
+ unsigned int *buf = (unsigned int *)calloc(win_size * win_size,
+ sizeof(unsigned int));
+ if (!buf)
+ return;
+
+ /* Nearest-neighbor scale from source ARGB [A,R,G,B] bytes to native uint32. */
+ int x, y;
+ for (y = 0; y < win_size; y++) {
+ int src_y = y * img_h / win_size;
+ if (src_y >= img_h) src_y = img_h - 1;
+ for (x = 0; x < win_size; x++) {
+ int src_x = x * img_w / win_size;
+ if (src_x >= img_w) src_x = img_w - 1;
+
+ int idx = (src_y * img_w + src_x) * 4;
+ unsigned char a = data[idx + 0];
+ unsigned char r = data[idx + 1];
+ unsigned char g = data[idx + 2];
+ unsigned char b = data[idx + 3];
+
+ /* Pre-multiply alpha for correct compositing on 32-bit visuals. */
+ if (a == 0) {
+ buf[y * win_size + x] = 0;
+ } else if (a == 255) {
+ buf[y * win_size + x] = ((unsigned int)a << 24) |
+ ((unsigned int)r << 16) |
+ ((unsigned int)g << 8) |
+ ((unsigned int)b);
+ } else {
+ unsigned int pr = (unsigned int)r * a / 255;
+ unsigned int pg = (unsigned int)g * a / 255;
+ unsigned int pb = (unsigned int)b * a / 255;
+ buf[y * win_size + x] = ((unsigned int)a << 24) |
+ (pr << 16) | (pg << 8) | pb;
+ }
+ }
+ }
+
+ XImage *img = XCreateImage(dpy, vis, depth, ZPixmap, 0,
+ (char *)buf, win_size, win_size,
+ 32, 0);
+ if (!img) {
+ free(buf);
+ return;
+ }
+
+ GC gc = XCreateGC(dpy, icon_win, 0, NULL);
+ XPutImage(dpy, icon_win, gc, img, 0, 0, 0, 0, win_size, win_size);
+ XFreeGC(dpy, gc);
+
+ /* XDestroyImage frees the data pointer (buf) for us. */
+ XDestroyImage(img);
+ XFlush(dpy);
+}
+
+void xembed_destroy_icon(Display *dpy, Window icon_win) {
+ if (icon_win)
+ XDestroyWindow(dpy, icon_win);
+ XFlush(dpy);
+}
+
+int xembed_poll_event(Display *dpy, Window icon_win,
+ int *out_x, int *out_y) {
+ *out_x = 0;
+ *out_y = 0;
+
+ while (XPending(dpy) > 0) {
+ XEvent ev;
+ XNextEvent(dpy, &ev);
+
+ switch (ev.type) {
+ case ButtonPress:
+ if (ev.xbutton.window == icon_win) {
+ *out_x = ev.xbutton.x_root;
+ *out_y = ev.xbutton.y_root;
+ if (ev.xbutton.button == Button1)
+ return 1;
+ if (ev.xbutton.button == Button3)
+ return 2;
+ }
+ break;
+
+ case Expose:
+ if (ev.xexpose.window == icon_win && ev.xexpose.count == 0)
+ return 3;
+ break;
+
+ case DestroyNotify:
+ if (ev.xdestroywindow.window == icon_win)
+ return -1;
+ break;
+
+ case ConfigureNotify:
+ if (ev.xconfigure.window == icon_win) {
+ *out_x = ev.xconfigure.width;
+ *out_y = ev.xconfigure.height;
+ return 4;
+ }
+ break;
+
+ case ReparentNotify:
+ /* Tray manager reparented us — this is expected after docking. */
+ break;
+
+ default:
+ break;
+ }
+ }
+
+ return 0;
+}
+
+/* --- GTK3 popup window menu support --- */
+
+/* Implemented in Go via //export */
+extern void goMenuItemClicked(int id);
+
+/* The popup window, reused across invocations. */
+static GtkWidget *popup_win = NULL;
+
+typedef struct {
+ xembed_menu_item *items;
+ int count;
+ int x, y;
+} popup_data;
+
+static void free_popup_data(popup_data *pd) {
+ if (!pd) return;
+ for (int i = 0; i < pd->count; i++)
+ free((void *)pd->items[i].label);
+ free(pd->items);
+ free(pd);
+}
+
+static void on_button_clicked(GtkButton *btn, gpointer user_data) {
+ int id = GPOINTER_TO_INT(user_data);
+ if (popup_win)
+ gtk_widget_hide(popup_win);
+ goMenuItemClicked(id);
+}
+
+static void on_check_toggled(GtkToggleButton *btn, gpointer user_data) {
+ int id = GPOINTER_TO_INT(user_data);
+ if (popup_win)
+ gtk_widget_hide(popup_win);
+ goMenuItemClicked(id);
+}
+
+static gboolean on_popup_focus_out(GtkWidget *widget, GdkEvent *event,
+ gpointer user_data) {
+ gtk_widget_hide(widget);
+ return FALSE;
+}
+
+static gboolean popup_menu_idle(gpointer user_data) {
+ popup_data *pd = (popup_data *)user_data;
+
+ /* Destroy old popup if it exists. */
+ if (popup_win) {
+ gtk_widget_destroy(popup_win);
+ popup_win = NULL;
+ }
+
+ popup_win = gtk_window_new(GTK_WINDOW_TOPLEVEL);
+ gtk_window_set_type_hint(GTK_WINDOW(popup_win),
+ GDK_WINDOW_TYPE_HINT_POPUP_MENU);
+ gtk_window_set_decorated(GTK_WINDOW(popup_win), FALSE);
+ gtk_window_set_resizable(GTK_WINDOW(popup_win), FALSE);
+ gtk_window_set_skip_taskbar_hint(GTK_WINDOW(popup_win), TRUE);
+ gtk_window_set_skip_pager_hint(GTK_WINDOW(popup_win), TRUE);
+ gtk_window_set_keep_above(GTK_WINDOW(popup_win), TRUE);
+
+ /* Close on focus loss. */
+ g_signal_connect(popup_win, "focus-out-event",
+ G_CALLBACK(on_popup_focus_out), NULL);
+
+ GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
+ gtk_container_add(GTK_CONTAINER(popup_win), vbox);
+
+ for (int i = 0; i < pd->count; i++) {
+ xembed_menu_item *mi = &pd->items[i];
+
+ if (mi->is_separator) {
+ GtkWidget *sep = gtk_separator_new(GTK_ORIENTATION_HORIZONTAL);
+ gtk_box_pack_start(GTK_BOX(vbox), sep, FALSE, FALSE, 2);
+ continue;
+ }
+
+ if (mi->is_check) {
+ GtkWidget *chk = gtk_check_button_new_with_label(
+ mi->label ? mi->label : "");
+ gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(chk), mi->checked);
+ gtk_widget_set_sensitive(chk, mi->enabled);
+ g_signal_connect(chk, "toggled",
+ G_CALLBACK(on_check_toggled),
+ GINT_TO_POINTER(mi->id));
+ gtk_box_pack_start(GTK_BOX(vbox), chk, FALSE, FALSE, 0);
+ } else {
+ GtkWidget *btn = gtk_button_new_with_label(
+ mi->label ? mi->label : "");
+ gtk_widget_set_sensitive(btn, mi->enabled);
+ gtk_button_set_relief(GTK_BUTTON(btn), GTK_RELIEF_NONE);
+ /* Left-align label. */
+ GtkWidget *label = gtk_bin_get_child(GTK_BIN(btn));
+ if (label)
+ gtk_label_set_xalign(GTK_LABEL(label), 0.0);
+ g_signal_connect(btn, "clicked",
+ G_CALLBACK(on_button_clicked),
+ GINT_TO_POINTER(mi->id));
+ gtk_box_pack_start(GTK_BOX(vbox), btn, FALSE, FALSE, 0);
+ }
+ }
+
+ gtk_widget_show_all(popup_win);
+
+ /* Position the window above the click point (menu grows upward from tray). */
+ gint win_w, win_h;
+ gtk_window_get_size(GTK_WINDOW(popup_win), &win_w, &win_h);
+ int final_x = pd->x - win_w / 2;
+ int final_y = pd->y - win_h;
+ if (final_x < 0) final_x = 0;
+ if (final_y < 0) final_y = pd->y; /* fallback: below click */
+ gtk_window_move(GTK_WINDOW(popup_win), final_x, final_y);
+
+ /* Grab focus so focus-out-event works. */
+ gtk_window_present(GTK_WINDOW(popup_win));
+
+ free_popup_data(pd);
+ return G_SOURCE_REMOVE;
+}
+
+void xembed_show_popup_menu(xembed_menu_item *items, int count,
+ xembed_menu_click_cb cb, int x, int y) {
+ (void)cb;
+ popup_data *pd = (popup_data *)calloc(1, sizeof(popup_data));
+ pd->items = (xembed_menu_item *)calloc(count, sizeof(xembed_menu_item));
+ pd->count = count;
+ pd->x = x;
+ pd->y = y;
+
+ for (int i = 0; i < count; i++) {
+ pd->items[i] = items[i];
+ if (items[i].label)
+ pd->items[i].label = strdup(items[i].label);
+ }
+
+ g_idle_add(popup_menu_idle, pd);
+}
diff --git a/client/uiwails/xembed_tray.h b/client/uiwails/xembed_tray.h
new file mode 100644
index 000000000..1fcb151b8
--- /dev/null
+++ b/client/uiwails/xembed_tray.h
@@ -0,0 +1,71 @@
+#ifndef XEMBED_TRAY_H
+#define XEMBED_TRAY_H
+
+#include
+
+// xembed_default_screen wraps the DefaultScreen macro for CGo.
+static inline int xembed_default_screen(Display *dpy) {
+ return DefaultScreen(dpy);
+}
+
+// xembed_find_tray returns the selection owner window for
+// _NET_SYSTEM_TRAY_S{screen}, or 0 if no XEmbed tray manager exists.
+Window xembed_find_tray(Display *dpy, int screen);
+
+// xembed_get_icon_size queries _NET_SYSTEM_TRAY_ICON_SIZE from the tray
+// manager window. Returns the size in pixels, or 0 if not set.
+int xembed_get_icon_size(Display *dpy, Window tray_mgr);
+
+// xembed_create_icon creates a tray icon window of the given size,
+// sets _XEMBED_INFO, and returns the window ID.
+// tray_mgr is the tray manager window; its _NET_SYSTEM_TRAY_VISUAL
+// property is queried to obtain a 32-bit ARGB visual for transparency.
+Window xembed_create_icon(Display *dpy, int screen, int size, Window tray_mgr);
+
+// xembed_dock sends _NET_SYSTEM_TRAY_OPCODE SYSTEM_TRAY_REQUEST_DOCK
+// to the tray manager to embed our icon window.
+int xembed_dock(Display *dpy, Window tray_mgr, Window icon_win);
+
+// xembed_draw_icon draws ARGB pixel data onto the icon window.
+// data is in [A,R,G,B] byte order per pixel (SNI IconPixmap format).
+// img_w, img_h are the source image dimensions.
+// win_size is the target window dimension (square).
+void xembed_draw_icon(Display *dpy, Window icon_win, int win_size,
+ const unsigned char *data, int img_w, int img_h);
+
+// xembed_destroy_icon destroys the icon window.
+void xembed_destroy_icon(Display *dpy, Window icon_win);
+
+// xembed_poll_event processes pending X11 events. Returns:
+// 0 = no actionable event
+// 1 = left button press (out_x, out_y filled)
+// 2 = right button press (out_x, out_y filled)
+// 3 = expose (needs redraw)
+// 4 = configure (resize; out_x=width, out_y=height)
+// -1 = DestroyNotify on icon window (tray died)
+int xembed_poll_event(Display *dpy, Window icon_win,
+ int *out_x, int *out_y);
+
+// Callback type for menu item clicks. Called with the item's dbusmenu ID.
+typedef void (*xembed_menu_click_cb)(int id);
+
+// xembed_popup_menu builds and shows a GTK3 popup menu.
+// items is an array of menu item descriptors, count is the number of items.
+// cb is called (from the GTK main thread) when an item is clicked.
+// x, y are root coordinates for positioning the popup.
+// This must be called from the GTK main thread (use g_idle_add).
+
+typedef struct {
+ int id; // dbusmenu item ID
+ const char *label; // display label (NULL for separator)
+ int enabled; // whether the item is clickable
+ int is_check; // whether this is a checkbox item
+ int checked; // checkbox state (0 or 1)
+ int is_separator;// 1 if this is a separator
+} xembed_menu_item;
+
+// Schedule a GTK popup menu on the main thread.
+void xembed_show_popup_menu(xembed_menu_item *items, int count,
+ xembed_menu_click_cb cb, int x, int y);
+
+#endif
diff --git a/go.mod b/go.mod
index 4bcdbdc78..7529c8797 100644
--- a/go.mod
+++ b/go.mod
@@ -7,20 +7,20 @@ toolchain go1.25.5
require (
cunicu.li/go-rosenpass v0.4.0
github.com/cenkalti/backoff/v4 v4.3.0
- github.com/cloudflare/circl v1.3.3 // indirect
+ github.com/cloudflare/circl v1.6.3 // indirect
github.com/golang/protobuf v1.5.4
github.com/google/uuid v1.6.0
github.com/gorilla/mux v1.8.1
github.com/kardianos/service v1.2.3-0.20240613133416-becf2eb62b83
github.com/onsi/ginkgo v1.16.5
- github.com/onsi/gomega v1.27.6
+ github.com/onsi/gomega v1.34.1
github.com/rs/cors v1.8.0
github.com/sirupsen/logrus v1.9.3
github.com/spf13/cobra v1.10.1
- github.com/spf13/pflag v1.0.9
+ github.com/spf13/pflag v1.0.10
github.com/vishvananda/netlink v1.3.1
- golang.org/x/crypto v0.46.0
- golang.org/x/sys v0.39.0
+ golang.org/x/crypto v0.47.0
+ golang.org/x/sys v0.40.0
golang.zx2c4.com/wireguard v0.0.0-20230704135630-469159ecf7d1
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6
golang.zx2c4.com/wireguard/windows v0.5.3
@@ -51,7 +51,7 @@ require (
github.com/eko/gocache/store/redis/v4 v4.2.2
github.com/fsnotify/fsnotify v1.9.0
github.com/gliderlabs/ssh v0.3.8
- github.com/godbus/dbus/v5 v5.1.0
+ github.com/godbus/dbus/v5 v5.2.2
github.com/golang-jwt/jwt/v5 v5.3.0
github.com/golang/mock v1.6.0
github.com/google/go-cmp v0.7.0
@@ -111,13 +111,13 @@ require (
go.uber.org/mock v0.5.2
go.uber.org/zap v1.27.0
goauthentik.io/api/v3 v3.2023051.3
- golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842
+ golang.org/x/exp v0.0.0-20260112195511-716be5621a96
golang.org/x/mobile v0.0.0-20251113184115-a159579294ab
- golang.org/x/mod v0.30.0
- golang.org/x/net v0.47.0
+ golang.org/x/mod v0.32.0
+ golang.org/x/net v0.49.0
golang.org/x/oauth2 v0.34.0
golang.org/x/sync v0.19.0
- golang.org/x/term v0.38.0
+ golang.org/x/term v0.39.0
golang.org/x/time v0.14.0
google.golang.org/api v0.257.0
gopkg.in/yaml.v3 v3.0.1
@@ -132,16 +132,18 @@ require (
cloud.google.com/go/auth v0.17.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
cloud.google.com/go/compute/metadata v0.9.0 // indirect
- dario.cat/mergo v1.0.1 // indirect
+ dario.cat/mergo v1.0.2 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
github.com/AppsFlyer/go-sundheit v0.6.0 // indirect
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
- github.com/BurntSushi/toml v1.5.0 // indirect
+ github.com/BurntSushi/toml v1.6.0 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
- github.com/Masterminds/semver/v3 v3.3.0 // indirect
+ github.com/Masterminds/semver/v3 v3.4.0 // indirect
github.com/Masterminds/sprig/v3 v3.3.0 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
+ github.com/ProtonMail/go-crypto v1.3.0 // indirect
+ github.com/adrg/xdg v0.5.3 // indirect
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be // indirect
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
github.com/awnumar/memcall v0.4.0 // indirect
@@ -163,18 +165,21 @@ require (
github.com/aws/smithy-go v1.22.2 // indirect
github.com/beevik/etree v1.6.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
+ github.com/bep/debounce v1.2.1 // indirect
github.com/caddyserver/zerossl v0.1.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/containerd/log v0.1.0 // indirect
github.com/containerd/platforms v0.2.1 // indirect
github.com/cpuguy83/dockercfg v0.3.2 // indirect
+ github.com/cyphar/filepath-securejoin v0.6.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/distribution/reference v0.6.0 // indirect
github.com/docker/docker v28.0.1+incompatible // indirect
github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
- github.com/ebitengine/purego v0.8.2 // indirect
+ github.com/ebitengine/purego v0.9.1 // indirect
+ github.com/emirpasic/gods v1.18.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fredbi/uri v1.1.1 // indirect
github.com/fyne-io/gl-js v0.2.0 // indirect
@@ -182,6 +187,9 @@ require (
github.com/fyne-io/image v0.1.1 // indirect
github.com/fyne-io/oksvg v0.2.0 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
+ github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
+ github.com/go-git/go-billy/v5 v5.7.0 // indirect
+ github.com/go-git/go-git/v5 v5.16.4 // indirect
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 // indirect
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a // indirect
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
@@ -193,6 +201,7 @@ require (
github.com/go-text/render v0.2.0 // indirect
github.com/go-text/typesetting v0.2.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
+ github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/google/btree v1.1.2 // indirect
github.com/google/s2a-go v0.1.9 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect
@@ -207,6 +216,8 @@ require (
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
+ github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
+ github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 // indirect
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
@@ -214,14 +225,20 @@ require (
github.com/jonboulle/clockwork v0.5.0 // indirect
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect
github.com/kelseyhightower/envconfig v1.4.0 // indirect
- github.com/klauspost/compress v1.18.0 // indirect
- github.com/klauspost/cpuid/v2 v2.2.7 // indirect
+ github.com/kevinburke/ssh_config v1.4.0 // indirect
+ github.com/klauspost/compress v1.18.3 // indirect
+ github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/kr/fs v0.1.0 // indirect
+ github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
+ github.com/leaanthony/u v1.1.1 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/libdns/libdns v0.2.2 // indirect
+ github.com/lmittmann/tint v1.1.2 // indirect
github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae // indirect
github.com/magiconair/properties v1.8.10 // indirect
github.com/mattermost/xml-roundtrip-validator v0.1.0 // indirect
+ github.com/mattn/go-colorable v0.1.14 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.32 // indirect
github.com/mdelapenya/tlscert v0.2.0 // indirect
github.com/mdlayher/genetlink v1.3.2 // indirect
@@ -240,7 +257,7 @@ require (
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
github.com/nicksnyder/go-i18n/v2 v2.5.1 // indirect
github.com/nxadm/tail v1.4.8 // indirect
- github.com/onsi/ginkgo/v2 v2.9.5 // indirect
+ github.com/onsi/ginkgo/v2 v2.19.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.1.1 // indirect
github.com/pion/dtls/v2 v2.2.10 // indirect
@@ -248,18 +265,24 @@ require (
github.com/pion/mdns/v2 v2.0.7 // indirect
github.com/pion/transport/v2 v2.2.4 // indirect
github.com/pion/turn/v4 v4.1.1 // indirect
+ github.com/pjbgf/sha1cd v0.5.0 // indirect
+ github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.66.1 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
+ github.com/rivo/uniseg v0.4.7 // indirect
github.com/russellhaering/goxmldsig v1.5.0 // indirect
github.com/rymdport/portal v0.4.2 // indirect
+ github.com/samber/lo v1.52.0 // indirect
+ github.com/sergi/go-diff v1.4.0 // indirect
github.com/shirou/gopsutil/v4 v4.25.1 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/shopspring/decimal v1.4.0 // indirect
- github.com/spf13/cast v1.7.0 // indirect
+ github.com/skeema/knownhosts v1.3.2 // indirect
+ github.com/spf13/cast v1.10.0 // indirect
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
github.com/stretchr/objx v0.5.2 // indirect
@@ -267,8 +290,11 @@ require (
github.com/tklauser/numcpus v0.8.0 // indirect
github.com/vishvananda/netns v0.0.5 // indirect
github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect
+ github.com/wailsapp/go-webview2 v1.0.23 // indirect
+ github.com/wailsapp/wails/v3 v3.0.0-alpha.74 // indirect
github.com/wlynxg/anet v0.0.5 // indirect
- github.com/yuin/goldmark v1.7.8 // indirect
+ github.com/xanzy/ssh-agent v0.3.3 // indirect
+ github.com/yuin/goldmark v1.7.16 // indirect
github.com/zeebo/blake3 v0.2.3 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect
@@ -276,13 +302,14 @@ require (
go.opentelemetry.io/otel/trace v1.38.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
- golang.org/x/image v0.33.0 // indirect
- golang.org/x/text v0.32.0 // indirect
- golang.org/x/tools v0.39.0 // indirect
+ golang.org/x/image v0.35.0 // indirect
+ golang.org/x/text v0.33.0 // indirect
+ golang.org/x/tools v0.41.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 // indirect
gopkg.in/square/go-jose.v2 v2.6.0 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
+ gopkg.in/warnings.v0 v0.1.2 // indirect
)
replace github.com/kardianos/service => github.com/netbirdio/service v0.0.0-20240911161631-f62744f42502
diff --git a/go.sum b/go.sum
index 1bd9396bb..09caaf196 100644
--- a/go.sum
+++ b/go.sum
@@ -9,6 +9,8 @@ cunicu.li/go-rosenpass v0.4.0 h1:LtPtBgFWY/9emfgC4glKLEqS0MJTylzV6+ChRhiZERw=
cunicu.li/go-rosenpass v0.4.0/go.mod h1:MPbjH9nxV4l3vEagKVdFNwHOketqgS5/To1VYJplf/M=
dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s=
dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
+dario.cat/mergo v1.0.2 h1:85+piFYR1tMbRrLcDwR18y4UKJ3aH1Tbzi24VRW1TK8=
+dario.cat/mergo v1.0.2/go.mod h1:E/hbnu0NxMFBjpMIE34DRGLWqDy0g5FuKDhCb31ngxA=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
fyne.io/fyne/v2 v2.7.0 h1:GvZSpE3X0liU/fqstInVvRsaboIVpIWQ4/sfjDGIGGQ=
@@ -25,17 +27,25 @@ github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
+github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
+github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI=
github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU=
github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0=
github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
+github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs=
github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0=
+github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
+github.com/ProtonMail/go-crypto v1.3.0 h1:ILq8+Sf5If5DCpHQp4PbZdS1J7HDFRXz/+xKBiRGFrw=
+github.com/ProtonMail/go-crypto v1.3.0/go.mod h1:9whxjD8Rbs29b4XWbB8irEcE8KHMqaR2e7GWU1R+/PE=
github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
github.com/TheJumpCloud/jcapi-go v3.0.0+incompatible h1:hqcTK6ZISdip65SR792lwYJTa/axESA0889D3UlZbLo=
github.com/TheJumpCloud/jcapi-go v3.0.0+incompatible/go.mod h1:6B1nuc1MUs6c62ODZDl7hVE5Pv7O2XGSkgg2olnq34I=
+github.com/adrg/xdg v0.5.3 h1:xRnxJXne7+oWDatRhR1JLnvuccuIeCoBu2rtuLqQB78=
+github.com/adrg/xdg v0.5.3/go.mod h1:nlTsY+NNiCBGCK2tpm09vRqfVzrc2fLmXGpBLF0zlTQ=
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
@@ -88,6 +98,8 @@ github.com/beevik/etree v1.6.0 h1:u8Kwy8pp9D9XeITj2Z0XtA5qqZEmtJtuXZRQi+j03eE=
github.com/beevik/etree v1.6.0/go.mod h1:bh4zJxiIr62SOf9pRzN7UUYaEDa9HEKafK25+sLc0Gc=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
+github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
github.com/bmatcuk/doublestar v1.1.1/go.mod h1:UD6OnuiIn0yFxxA2le/rnRU1G4RaI4UvFv1sNto9p6w=
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
@@ -122,6 +134,8 @@ github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
github.com/cunicu/circl v0.0.0-20230801113412-fec58fc7b5f6 h1:/DS5cDX3FJdl+XaN2D7XAwFpuanTxnp52DBLZAaJKx0=
github.com/cunicu/circl v0.0.0-20230801113412-fec58fc7b5f6/go.mod h1:+CauBF6R70Jqcyl8N2hC8pAXYbWkGIezuSbuGLtRhnw=
+github.com/cyphar/filepath-securejoin v0.6.1 h1:5CeZ1jPXEiYt3+Z6zqprSAgSWiggmpVyciv8syjIpVE=
+github.com/cyphar/filepath-securejoin v0.6.1/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -139,12 +153,16 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
+github.com/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A=
+github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/eko/gocache/lib/v4 v4.2.0 h1:MNykyi5Xw+5Wu3+PUrvtOCaKSZM1nUSVftbzmeC7Yuw=
github.com/eko/gocache/lib/v4 v4.2.0/go.mod h1:7ViVmbU+CzDHzRpmB4SXKyyzyuJ8A3UW3/cszpcqB4M=
github.com/eko/gocache/store/go_cache/v4 v4.2.2 h1:tAI9nl6TLoJyKG1ujF0CS0n/IgTEMl+NivxtR5R3/hw=
github.com/eko/gocache/store/go_cache/v4 v4.2.2/go.mod h1:T9zkHokzr8K9EiC7RfMbDg6HSwaV6rv3UdcNu13SGcA=
github.com/eko/gocache/store/redis/v4 v4.2.2 h1:Thw31fzGuH3WzJywsdbMivOmP550D6JS7GDHhvCJPA0=
github.com/eko/gocache/store/redis/v4 v4.2.2/go.mod h1:LaTxLKx9TG/YUEybQvPMij++D7PBTIJ4+pzvk0ykz0w=
+github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
+github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
@@ -173,6 +191,12 @@ github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
+github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=
+github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=
+github.com/go-git/go-billy/v5 v5.7.0 h1:83lBUJhGWhYp0ngzCMSgllhUSuoHP1iEWYjsPl9nwqM=
+github.com/go-git/go-billy/v5 v5.7.0/go.mod h1:/1IUejTKH8xipsAcdfcSAlUlo2J7lkYV8GTKxAT/L3E=
+github.com/go-git/go-git/v5 v5.16.4 h1:7ajIEZHZJULcyJebDLo99bGgS0jRrOxzZG4uCk2Yb2Y=
+github.com/go-git/go-git/v5 v5.16.4/go.mod h1:4Ge4alE/5gPs30F2H1esi2gPd69R0C39lolkucHBOp8=
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 h1:5BVwOaUSBTlVZowGO6VZGw2H/zl9nrd3eCZfYV+NfQA=
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a h1:vxnBhFDDT+xzxf1jTJKMKZw3H0swfWk9RpWbBbDK5+0=
@@ -207,10 +231,14 @@ github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066 h1:qCuYC
github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
+github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
+github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=
+github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
@@ -293,6 +321,10 @@ github.com/jackc/pgx/v5 v5.5.5 h1:amBjrZVmksIdNjxGW/IiIMzxMKZFelXbUoPNb+8sjQw=
github.com/jackc/pgx/v5 v5.5.5/go.mod h1:ez9gk+OAat140fv9ErkZDYFWmXLfV+++K0uAOiwgm1A=
github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk=
github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
+github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
+github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
+github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 h1:njuLRcjAuMKr7kI3D85AXWkw6/+v9PwtV6M6o11sWHQ=
+github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
@@ -323,13 +355,18 @@ github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6U
github.com/juju/gnuflag v0.0.0-20171113085948-2ce1bb71843d/go.mod h1:2PavIy+JPciBPrBUjwbNvtwB6RQlve+hkpll6QSNmOE=
github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8=
github.com/kelseyhightower/envconfig v1.4.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
+github.com/kevinburke/ssh_config v1.4.0 h1:6xxtP5bZ2E4NF5tuQulISpTO2z8XbtH8cg1PWkxoFkQ=
+github.com/kevinburke/ssh_config v1.4.0/go.mod h1:q2RIzfka+BXARoNexmF9gkxEX7DmvbW9P4hIVx2Kg4M=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
+github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c=
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
+github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
+github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@@ -341,6 +378,10 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
+github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A=
+github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
+github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M=
+github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
github.com/leodido/go-urn v1.1.0/go.mod h1:+cyI34gQWZcE1eQU7NVgKkkzdXDQHr1dBMtdAPozLkw=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
@@ -348,6 +389,8 @@ github.com/libdns/libdns v0.2.2 h1:O6ws7bAfRPaBsgAYt8MDe2HcNBGC29hkZ9MX2eUSX3s=
github.com/libdns/libdns v0.2.2/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
github.com/libdns/route53 v1.5.0 h1:2SKdpPFl/qgWsXQvsLNJJAoX7rSxlk7zgoL4jnWdXVA=
github.com/libdns/route53 v1.5.0/go.mod h1:joT4hKmaTNKHEwb7GmZ65eoDz1whTu7KKYPS8ZqIh6Q=
+github.com/lmittmann/tint v1.1.2 h1:2CQzrL6rslrsyjqLDwD11bZ5OpLBPU+g3G/r5LSfS8w=
+github.com/lmittmann/tint v1.1.2/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
github.com/lrh3321/ipset-go v0.0.0-20250619021614-54a0a98ace81 h1:J56rFEfUTFT9j9CiRXhi1r8lUJ4W5idG3CiaBZGojNU=
github.com/lrh3321/ipset-go v0.0.0-20250619021614-54a0a98ace81/go.mod h1:RD8ML/YdXctQ7qbcizZkw5mZ6l8Ogrl1dodBzVJduwI=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
@@ -355,9 +398,14 @@ github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae h1:dIZY4ULFcto4tA
github.com/lufia/plan9stats v0.0.0-20240513124658-fba389f38bae/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
github.com/magiconair/properties v1.8.10 h1:s31yESBquKXCV9a/ScB3ESkOjUYYv+X0rg8SYxI99mE=
github.com/magiconair/properties v1.8.10/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
+github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/mattermost/xml-roundtrip-validator v0.1.0 h1:RXbVD2UAl7A7nOTR4u7E3ILa4IbtvKBHw64LDsmu9hU=
github.com/mattermost/xml-roundtrip-validator v0.1.0/go.mod h1:qccnGMcpgwcNaBnxqpJpWWUiPNr5H3O8eDgGV9gT5To=
+github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
+github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.9/go.mod h1:YNRxwqDuOph6SZLI9vUUz6OYw3QyUt7WiY2yME+cCiQ=
+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-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/mdelapenya/tlscert v0.2.0 h1:7H81W6Z/4weDvZBNOfQte5GpIMo0lGYEeWbkGp5LJHI=
@@ -429,10 +477,12 @@ github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/ginkgo/v2 v2.9.5 h1:+6Hr4uxzP4XIUyAkg61dWBw8lb/gc4/X5luuxN/EC+Q=
github.com/onsi/ginkgo/v2 v2.9.5/go.mod h1:tvAoo1QUJwNEU2ITftXTpR7R1RbCzoZUOs3RonqW57k=
+github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.27.6 h1:ENqfyGeS5AX/rlXDd/ETokDz93u0YufY1Pgxuy/PvWE=
github.com/onsi/gomega v1.27.6/go.mod h1:PIQNjfQwkP3aQAH7lf7j87O/5FiNr+ZR8+ipb+qQlhg=
+github.com/onsi/gomega v1.34.1/go.mod h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
@@ -473,6 +523,10 @@ github.com/pion/turn/v4 v4.1.1 h1:9UnY2HB99tpDyz3cVVZguSxcqkJ1DsTSZ+8TGruh4fc=
github.com/pion/turn/v4 v4.1.1/go.mod h1:2123tHk1O++vmjI5VSD0awT50NywDAq5A2NNNU4Jjs8=
github.com/pires/go-proxyproto v0.11.0 h1:gUQpS85X/VJMdUsYyEgyn59uLJvGqPhJV5YvG68wXH4=
github.com/pires/go-proxyproto v0.11.0/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
+github.com/pjbgf/sha1cd v0.5.0 h1:a+UkboSi1znleCDUNT3M5YxjOnN1fz2FhN48FlwCxs0=
+github.com/pjbgf/sha1cd v0.5.0/go.mod h1:lhpGlyHLpQZoxMv8HcgXvZEhcGs0PG/vsZnEJ7H0iCM=
+github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
+github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -497,6 +551,9 @@ github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9M
github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U=
github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM=
github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA=
+github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
+github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
+github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/rs/cors v1.8.0 h1:P2KMzcFwrPoSjkF1WLRPsp3UMLyql8L4v9hQpVeK5so=
@@ -508,6 +565,10 @@ github.com/russellhaering/goxmldsig v1.5.0/go.mod h1:x98CjQNFJcWfMxeOrMnMKg70lvD
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/rymdport/portal v0.4.2 h1:7jKRSemwlTyVHHrTGgQg7gmNPJs88xkbKcIL3NlcmSU=
github.com/rymdport/portal v0.4.2/go.mod h1:kFF4jslnJ8pD5uCi17brj/ODlfIidOxlgUDTO5ncnC4=
+github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
+github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
+github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw=
+github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=
github.com/shirou/gopsutil/v3 v3.24.4 h1:dEHgzZXt4LMNm+oYELpzl9YCqV65Yr/6SfrvgRBtXeU=
github.com/shirou/gopsutil/v3 v3.24.4/go.mod h1:lTd2mdiOspcqLgAnr9/nGi71NkeMpWKdmhuxm9GusH8=
github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs=
@@ -518,18 +579,24 @@ github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
+github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
+github.com/skeema/knownhosts v1.3.2 h1:EDL9mgf4NzwMXCTfaxSD/o/a5fxDw/xL9nkU28JjdBg=
+github.com/skeema/knownhosts v1.3.2/go.mod h1:bEg3iQAuw+jyiw+484wwFJoKSLwcfd7fqRy+N0QTiow=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 h1:JIAuq3EEf9cgbU6AtGPK4CTG3Zf6CKMNqf0MHTggAUA=
github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 h1:TG/diQgUe0pntT/2D9tmUCz4VNwm9MfrtPr0SU2qSX8=
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8/go.mod h1:P5HUIBuIWKbyjl083/loAegFkfbFNx5i2qEP4CNbm7E=
github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w=
github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
+github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
+github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKkMo8ZTx3f+BZEkzsRUY10Xsm2mwU0=
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE=
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q=
@@ -541,6 +608,7 @@ github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSS
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.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
@@ -583,14 +651,22 @@ github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IU
github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok=
github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g=
github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds=
+github.com/wailsapp/go-webview2 v1.0.23 h1:jmv8qhz1lHibCc79bMM/a/FqOnnzOGEisLav+a0b9P0=
+github.com/wailsapp/go-webview2 v1.0.23/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
+github.com/wailsapp/wails/v3 v3.0.0-alpha.74 h1:wRm1EiDQtxDisXk46NtpiBH90STwfKp36NrTDwOEdxw=
+github.com/wailsapp/wails/v3 v3.0.0-alpha.74/go.mod h1:4saK4A4K9970X+X7RkMwP2lyGbLogcUz54wVeq4C/V8=
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
+github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=
+github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
+github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
+github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
github.com/zcalusic/sysinfo v1.1.3 h1:u/AVENkuoikKuIZ4sUEJ6iibpmQP6YpGD8SSMCrqAF0=
@@ -641,6 +717,7 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
@@ -650,10 +727,16 @@ golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
+golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
+golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842 h1:vr/HnozRka3pE4EsMEg1lgkXJkTFJCVUX+S/ZT6wYzM=
golang.org/x/exp v0.0.0-20240506185415-9bf2ced13842/go.mod h1:XtvwrStGgqGPLc4cjQfWqZHG1YFdYs6swckp8vpsjnc=
+golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU=
+golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU=
golang.org/x/image v0.33.0 h1:LXRZRnv1+zGd5XBUVRFmYEphyyKJjQjCRiOuAP3sZfQ=
golang.org/x/image v0.33.0/go.mod h1:DD3OsTYT9chzuzTQt+zMcOlBHgfoKQb1gry8p76Y1sc=
+golang.org/x/image v0.35.0 h1:LKjiHdgMtO8z7Fh18nGY6KDcoEtVfsgLDPeLyguqb7I=
+golang.org/x/image v0.35.0/go.mod h1:MwPLTVgvxSASsxdLzKrl8BRFuyqMyGhLwmC+TO1Sybk=
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
golang.org/x/mobile v0.0.0-20251113184115-a159579294ab h1:Iqyc+2zr7aGyLuEadIm0KRJP0Wwt+fhlXLa51Fxf1+Q=
golang.org/x/mobile v0.0.0-20251113184115-a159579294ab/go.mod h1:Eq3Nh/5pFSWug2ohiudJ1iyU59SO78QFuh4qTTN++I0=
@@ -668,6 +751,8 @@ golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
+golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
+golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
@@ -677,6 +762,7 @@ golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
+golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
@@ -688,6 +774,8 @@ golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
+golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
+golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE=
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
@@ -711,14 +799,18 @@ golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -728,6 +820,7 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -740,6 +833,8 @@ golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
+golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -754,9 +849,12 @@ golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
+golang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=
+golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
@@ -767,6 +865,8 @@ golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
+golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
+golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
@@ -782,6 +882,8 @@ golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
+golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
+golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -832,6 +934,8 @@ gopkg.in/square/go-jose.v2 v2.6.0 h1:NGk74WTnPKBNUhNzQX7PYcTLUjoq7mzKk2OKbvwk2iI
gopkg.in/square/go-jose.v2 v2.6.0/go.mod h1:M9dMgbHiYLoDGQrXy7OpJDJWiKiU//h+vD76mk0e1AI=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
+gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
+gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=