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. + +
+ ) : ( +
+ + + + + + + + + + + {filtered.map(n => ( + toggle(n.id, n.selected)} /> + ))} + +
Resolved IPsActive
+ + {/* 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 && ( + + {dir === 'asc' ? : } + + )} + + + ) +} + +/* ---- 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 ( +
+
+ + + +
+

{msg}

+
+ ) +} + +/* ---- 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)} + /> +
+ +
+ + + + + Refresh + +
+
+ + {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. + +
+ ) : ( +
+ + + + + + + + + + + + + {filtered.map(p => ( + + ))} + +
ConnectionTransfer
+ +
+ 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 ( +
+ {initials} +
+
+
+ ) +} + +/* ---- 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 && ( + + {dir === 'asc' ? : } + + )} + + + ) +} + +/* ---- 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=