(null);
+ const [saving, setSaving] = useState(false);
+
+ const load = useCallback(async () => {
+ try {
+ const u = await ProfilesSvc.Username();
+ const active = await ProfilesSvc.GetActive();
+ const profileName = active.profileName || "default";
+ setUsername(u);
+ setProfile(profileName);
+ const c = await SettingsSvc.GetConfig({ profileName, username: u });
+ setCfg(c);
+ setError(null);
+ } catch (e) {
+ setError(String(e));
+ }
+ }, []);
+
+ useEffect(() => {
+ load();
+ }, [load]);
+
+ const setField: Ctx["setField"] = (k, v) => {
+ setCfg((c) => (c ? { ...c, [k]: v } : c));
+ };
+
+ const save = async () => {
+ if (!cfg) return;
+ setSaving(true);
+ try {
+ await SettingsSvc.SetConfig({
+ profileName: profile,
+ username,
+ managementUrl: cfg.managementUrl,
+ adminUrl: cfg.adminUrl,
+ interfaceName: cfg.interfaceName,
+ wireguardPort: cfg.wireguardPort,
+ mtu: cfg.mtu,
+ preSharedKey: cfg.preSharedKey,
+ disableAutoConnect: cfg.disableAutoConnect,
+ serverSshAllowed: cfg.serverSshAllowed,
+ rosenpassEnabled: cfg.rosenpassEnabled,
+ rosenpassPermissive: cfg.rosenpassPermissive,
+ disableNotifications: cfg.disableNotifications,
+ lazyConnectionEnabled: cfg.lazyConnectionEnabled,
+ blockInbound: cfg.blockInbound,
+ networkMonitor: cfg.networkMonitor,
+ disableClientRoutes: cfg.disableClientRoutes,
+ disableServerRoutes: cfg.disableServerRoutes,
+ disableDns: cfg.disableDns,
+ blockLanAccess: cfg.blockLanAccess,
+ enableSshRoot: cfg.enableSshRoot,
+ enableSshSftp: cfg.enableSshSftp,
+ enableSshLocalPortForwarding: cfg.enableSshLocalPortForwarding,
+ enableSshRemotePortForwarding: cfg.enableSshRemotePortForwarding,
+ disableSshAuth: cfg.disableSshAuth,
+ sshJwtCacheTtl: cfg.sshJwtCacheTtl,
+ });
+ setError(null);
+ } catch (e) {
+ setError(String(e));
+ } finally {
+ setSaving(false);
+ }
+ };
+
+ if (!cfg) {
+ return Loading…
;
+ }
+
+ const ctx: Ctx = { cfg, setField };
+
+ return (
+
+
+
Settings
+
+
+ {error &&
{error}
}
+
+ },
+ { value: "net", label: "Network", content: },
+ { value: "ssh", label: "SSH", content: },
+ ]}
+ />
+
+
+ );
+}
+
+function ConnectionTab({ cfg, setField }: Ctx) {
+ return (
+
+ );
+}
+
+function NetworkTab({ cfg, setField }: Ctx) {
+ return (
+
+ setField("networkMonitor", v)}
+ label="Network monitor"
+ />
+ setField("disableDns", v)}
+ label="Disable DNS"
+ />
+ setField("disableClientRoutes", v)}
+ label="Disable client routes"
+ />
+ setField("disableServerRoutes", v)}
+ label="Disable server routes"
+ />
+ setField("blockLanAccess", v)}
+ label="Block LAN access"
+ />
+ setField("blockInbound", v)}
+ label="Block inbound connections"
+ />
+
+ );
+}
+
+function SSHTab({ cfg, setField }: Ctx) {
+ return (
+
+ setField("serverSshAllowed", v)}
+ label="Server SSH allowed"
+ />
+ setField("enableSshRoot", v)}
+ label="SSH root login"
+ />
+ setField("enableSshSftp", v)}
+ label="SFTP"
+ />
+ setField("enableSshLocalPortForwarding", v)}
+ label="Local port forwarding"
+ />
+ setField("enableSshRemotePortForwarding", v)}
+ label="Remote port forwarding"
+ />
+ setField("disableSshAuth", v)}
+ label="Disable SSH auth"
+ />
+ setField("sshJwtCacheTtl", Number(e.target.value))}
+ />
+
+ );
+}
diff --git a/client/ui-wails/frontend/src/pages/Status.tsx b/client/ui-wails/frontend/src/pages/Status.tsx
new file mode 100644
index 000000000..ec0533568
--- /dev/null
+++ b/client/ui-wails/frontend/src/pages/Status.tsx
@@ -0,0 +1,161 @@
+import { CheckCircle2, Circle, Loader2, AlertTriangle, Power } from "lucide-react";
+import { useStatus } from "../hooks/useStatus";
+import { Connection } from "../../bindings/github.com/netbirdio/netbird/client/ui-wails/services";
+import type { SystemEvent } from "../../bindings/github.com/netbirdio/netbird/client/ui-wails/services/models.js";
+import { Button } from "../components/Button";
+import { Card } from "../components/Card";
+import { cn } from "../lib/cn";
+
+export default function Status() {
+ const { status, error } = useStatus();
+
+ const connState = status?.status ?? "Disconnected";
+ const connected = connState === "Connected";
+ const connecting = connState === "Connecting";
+
+ const connect = () => Connection.Up({ profileName: "", username: "" }).catch(console.error);
+ const disconnect = () => Connection.Down().catch(console.error);
+
+ return (
+
+
+
+
+
+
{connState}
+
+ {status?.local.fqdn || "—"}
+
+
+
+
+
+
+
+
+
+ {error && (
+
+ )}
+
+
+
+
+
+
+
+
+
+
+ Recent events
+
+ {(() => {
+ const events = dedupEvents(status?.events ?? []).slice(0, 8);
+ if (events.length === 0) {
+ return No recent events.
;
+ }
+ return (
+
+ {events.map((e, i) => (
+ -
+
+ {e.severity}
+
+
+ {e.userMessage || e.message}
+
+
+ ))}
+
+ );
+ })()}
+
+
+ );
+}
+
+function StateIcon({ state }: { state: string }) {
+ const cls = "h-7 w-7";
+ switch (state) {
+ case "Connected":
+ return ;
+ case "Connecting":
+ return ;
+ case "Error":
+ return ;
+ default:
+ return ;
+ }
+}
+
+function InfoCard({ label, value }: { label: string; value: string }) {
+ return (
+
+ {label}
+ {value}
+
+ );
+}
+
+// dedupEvents collapses repeated daemon events that carry the same logical
+// content. The daemon emits one "new_version_available" event per check tick,
+// so its 10-event ring buffer fills with duplicates after a quiet hour. Same
+// goes for periodic "DNS unreachable" or "auth retry" events. We key by
+// message + a small set of identity-bearing metadata fields and keep the
+// newest occurrence (the events array is already in publish order).
+function dedupEvents(events: SystemEvent[]): SystemEvent[] {
+ const seen = new Set();
+ const out: SystemEvent[] = [];
+ for (let i = events.length - 1; i >= 0; i--) {
+ const e = events[i];
+ const md = e.metadata ?? {};
+ const key = [
+ e.severity,
+ e.category,
+ e.userMessage || e.message,
+ md["new_version_available"] ?? "",
+ md["enforced"] ?? "",
+ ].join("|");
+ // eslint-disable-next-line no-console
+ console.log("[dedup]", { key, event: e });
+ if (seen.has(key)) continue;
+ seen.add(key);
+ out.unshift(e);
+ }
+ return out;
+}
+
+function LinkCard({
+ label,
+ link,
+}: {
+ label: string;
+ link?: { url: string; connected: boolean; error?: string };
+}) {
+ return (
+
+
+
+ {link?.url || "—"}
+
+ {link?.error && (
+ {link.error}
+ )}
+
+ );
+}
diff --git a/client/ui-wails/frontend/src/pages/Update.tsx b/client/ui-wails/frontend/src/pages/Update.tsx
new file mode 100644
index 000000000..04d9eb245
--- /dev/null
+++ b/client/ui-wails/frontend/src/pages/Update.tsx
@@ -0,0 +1,61 @@
+import { useEffect, useState } from "react";
+import { Loader2 } from "lucide-react";
+import { Update as UpdateSvc } from "../../bindings/github.com/netbirdio/netbird/client/ui-wails/services";
+
+const TIMEOUT_MS = 15 * 60 * 1000;
+
+export default function Update() {
+ const [done, setDone] = useState(false);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ let cancelled = false;
+ UpdateSvc.Trigger().catch((e) => !cancelled && setError(String(e)));
+
+ const start = Date.now();
+ const timer = setInterval(async () => {
+ if (Date.now() - start > TIMEOUT_MS) {
+ setError("Update timed out.");
+ clearInterval(timer);
+ return;
+ }
+ try {
+ const r = await UpdateSvc.GetInstallerResult();
+ if (r.success) {
+ setDone(true);
+ clearInterval(timer);
+ } else if (r.errorMsg) {
+ setError(r.errorMsg);
+ clearInterval(timer);
+ }
+ } catch {
+ // installer not finished yet
+ }
+ }, 2000);
+
+ return () => {
+ cancelled = true;
+ clearInterval(timer);
+ };
+ }, []);
+
+ return (
+
+
+ {done ? (
+
Update complete
+ ) : error ? (
+
{error}
+ ) : (
+ <>
+
+
Updating…
+
+ Please don't close this window.
+
+ >
+ )}
+
+
+ );
+}
diff --git a/client/ui-wails/frontend/src/vite-env.d.ts b/client/ui-wails/frontend/src/vite-env.d.ts
new file mode 100644
index 000000000..11f02fe2a
--- /dev/null
+++ b/client/ui-wails/frontend/src/vite-env.d.ts
@@ -0,0 +1 @@
+///
diff --git a/client/ui-wails/frontend/tailwind.config.ts b/client/ui-wails/frontend/tailwind.config.ts
new file mode 100644
index 000000000..9dc4f1178
--- /dev/null
+++ b/client/ui-wails/frontend/tailwind.config.ts
@@ -0,0 +1,44 @@
+import type { Config } from "tailwindcss";
+
+const config: Config = {
+ content: ["./index.html", "./src/**/*.{ts,tsx}"],
+ darkMode: "class",
+ theme: {
+ extend: {
+ colors: {
+ netbird: {
+ DEFAULT: "#f68330",
+ 50: "#fff6ed",
+ 100: "#feecd6",
+ 200: "#ffd4a6",
+ 300: "#fab677",
+ 400: "#f68330",
+ 500: "#f46d1b",
+ 600: "#e55311",
+ 700: "#be3e10",
+ 800: "#973215",
+ 900: "#7a2b14",
+ },
+ "nb-gray": {
+ DEFAULT: "#181A1D",
+ 50: "#f4f6f7",
+ 100: "#e4e7e9",
+ 200: "#cbd2d6",
+ 300: "#a3adb5",
+ 400: "#7c8994",
+ 500: "#616e79",
+ 600: "#535d67",
+ 700: "#474e57",
+ 800: "#3f444b",
+ 900: "#2e3238",
+ 925: "#1e2123",
+ 940: "#1c1e21",
+ 950: "#181a1d",
+ },
+ },
+ },
+ },
+ plugins: [require("tailwindcss-animate")],
+};
+
+export default config;
diff --git a/client/ui-wails/frontend/tsconfig.json b/client/ui-wails/frontend/tsconfig.json
new file mode 100644
index 000000000..ae81ea6d5
--- /dev/null
+++ b/client/ui-wails/frontend/tsconfig.json
@@ -0,0 +1,25 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": false,
+ "noImplicitAny": false,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src", "bindings"],
+}
diff --git a/client/ui-wails/frontend/vite.config.ts b/client/ui-wails/frontend/vite.config.ts
new file mode 100644
index 000000000..f58d1fa4a
--- /dev/null
+++ b/client/ui-wails/frontend/vite.config.ts
@@ -0,0 +1,12 @@
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react";
+import wails from "@wailsio/runtime/plugins/vite";
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [react(), wails("./bindings")],
+ server: {
+ port: 9245,
+ strictPort: true,
+ },
+});
diff --git a/client/ui-wails/grpc.go b/client/ui-wails/grpc.go
new file mode 100644
index 000000000..1ee3e5518
--- /dev/null
+++ b/client/ui-wails/grpc.go
@@ -0,0 +1,57 @@
+//go:build !android && !ios && !freebsd && !js
+
+package main
+
+import (
+ "fmt"
+ "runtime"
+ "strings"
+ "sync"
+
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/credentials/insecure"
+
+ "github.com/netbirdio/netbird/client/proto"
+ "github.com/netbirdio/netbird/client/ui/desktop"
+)
+
+// Conn is a lazy, lock-protected gRPC connection to the NetBird daemon.
+// One Conn instance is shared by all services so they reuse the same channel.
+type Conn struct {
+ addr string
+
+ mu sync.Mutex
+ client proto.DaemonServiceClient
+}
+
+func NewConn(addr string) *Conn {
+ return &Conn{addr: addr}
+}
+
+func (c *Conn) Client() (proto.DaemonServiceClient, error) {
+ c.mu.Lock()
+ defer c.mu.Unlock()
+ if c.client != nil {
+ return c.client, nil
+ }
+
+ cc, err := grpc.NewClient(
+ strings.TrimPrefix(c.addr, "tcp://"),
+ grpc.WithTransportCredentials(insecure.NewCredentials()),
+ grpc.WithUserAgent(desktop.GetUIUserAgent()),
+ )
+ if err != nil {
+ return nil, fmt.Errorf("dial daemon: %w", err)
+ }
+ c.client = proto.NewDaemonServiceClient(cc)
+ return c.client, nil
+}
+
+// DaemonAddr returns the default daemon gRPC address for the current OS.
+// Linux/macOS use a Unix socket; Windows uses TCP loopback.
+func DaemonAddr() string {
+ if runtime.GOOS == "windows" {
+ return "tcp://127.0.0.1:41731"
+ }
+ return "unix:///var/run/netbird.sock"
+}
diff --git a/client/ui-wails/icons.go b/client/ui-wails/icons.go
new file mode 100644
index 000000000..3f4ec97f2
--- /dev/null
+++ b/client/ui-wails/icons.go
@@ -0,0 +1,49 @@
+//go:build !android && !ios && !freebsd && !js
+
+package main
+
+import _ "embed"
+
+// Tray icons embedded from the legacy Fyne UI's asset set so the rewrite has
+// something to render until Stage 3 produces SVG sources. Each pair is a
+// light-mode PNG and its dark-mode variant; macOS template variants live
+// alongside for menubar use.
+
+//go:embed assets/netbird-systemtray-connected.png
+var iconConnected []byte
+
+//go:embed assets/netbird-systemtray-connected-dark.png
+var iconConnectedDark []byte
+
+//go:embed assets/netbird-systemtray-disconnected.png
+var iconDisconnected []byte
+
+//go:embed assets/netbird-systemtray-connecting.png
+var iconConnecting []byte
+
+//go:embed assets/netbird-systemtray-error.png
+var iconError []byte
+
+//go:embed assets/netbird-systemtray-update-connected.png
+var iconUpdateConnected []byte
+
+//go:embed assets/netbird-systemtray-update-disconnected.png
+var iconUpdateDisconnected []byte
+
+//go:embed assets/netbird-systemtray-connected-macos.png
+var iconConnectedMacOS []byte
+
+//go:embed assets/netbird-systemtray-disconnected-macos.png
+var iconDisconnectedMacOS []byte
+
+//go:embed assets/netbird-systemtray-connecting-macos.png
+var iconConnectingMacOS []byte
+
+//go:embed assets/netbird-systemtray-error-macos.png
+var iconErrorMacOS []byte
+
+//go:embed assets/netbird-systemtray-update-connected-macos.png
+var iconUpdateConnectedMacOS []byte
+
+//go:embed assets/netbird-systemtray-update-disconnected-macos.png
+var iconUpdateDisconnectedMacOS []byte
diff --git a/client/ui-wails/icons_windows.go b/client/ui-wails/icons_windows.go
new file mode 100644
index 000000000..1e0763770
--- /dev/null
+++ b/client/ui-wails/icons_windows.go
@@ -0,0 +1,29 @@
+//go:build windows
+
+package main
+
+import _ "embed"
+
+// Windows tray icons. Wails3 hands these to Shell_NotifyIcon via
+// CreateIconFromResourceEx, which picks the frame matching SM_CXSMICON
+// (16/32 px depending on DPI). A single high-res PNG forces the OS to
+// downscale and the result is fuzzy at tray size — multi-frame .ico files
+// avoid that by embedding 16/24/32/48 px raster frames in one resource.
+
+//go:embed assets/netbird-systemtray-connected.ico
+var winIconConnected []byte
+
+//go:embed assets/netbird-systemtray-disconnected.ico
+var winIconDisconnected []byte
+
+//go:embed assets/netbird-systemtray-connecting.ico
+var winIconConnecting []byte
+
+//go:embed assets/netbird-systemtray-error.ico
+var winIconError []byte
+
+//go:embed assets/netbird-systemtray-update-connected.ico
+var winIconUpdateConnected []byte
+
+//go:embed assets/netbird-systemtray-update-disconnected.ico
+var winIconUpdateDisconnected []byte
diff --git a/client/ui-wails/main.go b/client/ui-wails/main.go
new file mode 100644
index 000000000..480b3b21f
--- /dev/null
+++ b/client/ui-wails/main.go
@@ -0,0 +1,101 @@
+//go:build !android && !ios && !freebsd && !js
+
+package main
+
+import (
+ "context"
+ "embed"
+ "flag"
+ "log"
+
+ "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/ui-wails/services"
+)
+
+//go:embed all:frontend/dist
+var assets embed.FS
+
+func init() {
+ application.RegisterEvent[services.Status](services.EventStatus)
+ application.RegisterEvent[services.SystemEvent](services.EventSystem)
+ application.RegisterEvent[services.UpdateAvailable](services.EventUpdateAvailable)
+ application.RegisterEvent[services.UpdateProgress](services.EventUpdateProgress)
+}
+
+func main() {
+ daemonAddr := flag.String("daemon-addr", DaemonAddr(), "Daemon gRPC address: unix:///path or tcp://host:port")
+ flag.Parse()
+
+ conn := NewConn(*daemonAddr)
+
+ // tray is captured in the SingleInstance callback below; the var is
+ // declared before app.New so the closure has a stable reference.
+ var tray *Tray
+
+ app := application.New(application.Options{
+ Name: "netbird-ui",
+ Description: "NetBird desktop client",
+ Assets: application.AssetOptions{
+ Handler: application.AssetFileServerFS(assets),
+ },
+ Mac: application.MacOptions{
+ ApplicationShouldTerminateAfterLastWindowClosed: false,
+ },
+ SingleInstance: &application.SingleInstanceOptions{
+ UniqueID: "io.netbird.ui",
+ OnSecondInstanceLaunch: func(_ application.SecondInstanceData) {
+ if tray != nil {
+ tray.ShowWindow()
+ }
+ },
+ },
+ })
+
+ connection := services.NewConnection(conn)
+ settings := services.NewSettings(conn)
+ profiles := services.NewProfiles(conn)
+ peers := services.NewPeers(conn, app.Event)
+ notifier := notifications.New()
+
+ app.RegisterService(application.NewService(connection))
+ app.RegisterService(application.NewService(settings))
+ app.RegisterService(application.NewService(services.NewNetworks(conn)))
+ app.RegisterService(application.NewService(profiles))
+ app.RegisterService(application.NewService(services.NewDebug(conn)))
+ app.RegisterService(application.NewService(services.NewUpdate(conn)))
+ app.RegisterService(application.NewService(peers))
+ app.RegisterService(application.NewService(notifier))
+
+ window := app.Window.NewWithOptions(application.WebviewWindowOptions{
+ Title: "NetBird",
+ Width: 960,
+ Height: 640,
+ Hidden: false,
+ BackgroundColour: application.NewRGB(24, 26, 29),
+ URL: "/",
+ Mac: application.MacWindow{
+ InvisibleTitleBarHeight: 38,
+ Backdrop: application.MacBackdropTranslucent,
+ TitleBar: application.MacTitleBarHiddenInset,
+ },
+ })
+
+ // Intercept the window close to hide instead of quit. The user reaches
+ // "really quit" via tray -> Quit.
+ window.RegisterHook(events.Common.WindowClosing, func(e *application.WindowEvent) {
+ e.Cancel()
+ window.Hide()
+ })
+
+ tray = NewTray(app, window, connection, settings, profiles, peers, notifier)
+ listenForShowSignal(context.Background(), tray)
+
+ peers.Watch(context.Background())
+
+ if err := app.Run(); err != nil {
+ log.Fatal(err)
+ }
+}
diff --git a/client/ui-wails/services/conn.go b/client/ui-wails/services/conn.go
new file mode 100644
index 000000000..531abe7d9
--- /dev/null
+++ b/client/ui-wails/services/conn.go
@@ -0,0 +1,13 @@
+//go:build !android && !ios && !freebsd && !js
+
+package services
+
+import "github.com/netbirdio/netbird/client/proto"
+
+// DaemonConn returns a lazy gRPC client to the NetBird daemon.
+// All services receive a DaemonConn so they share a single connection.
+type DaemonConn interface {
+ Client() (proto.DaemonServiceClient, error)
+}
+
+func ptrStr(s string) *string { return &s }
diff --git a/client/ui-wails/services/connection.go b/client/ui-wails/services/connection.go
new file mode 100644
index 000000000..282bd04f7
--- /dev/null
+++ b/client/ui-wails/services/connection.go
@@ -0,0 +1,146 @@
+//go:build !android && !ios && !freebsd && !js
+
+package services
+
+import (
+ "context"
+
+ "github.com/netbirdio/netbird/client/proto"
+)
+
+// LoginParams carries the fields the UI sets when starting a login.
+type LoginParams struct {
+ ProfileName string `json:"profileName"`
+ Username string `json:"username"`
+ ManagementURL string `json:"managementUrl"`
+ SetupKey string `json:"setupKey"`
+ PreSharedKey string `json:"preSharedKey"`
+ Hostname string `json:"hostname"`
+ Hint string `json:"hint"`
+}
+
+// LoginResult is the daemon's reply to a Login call.
+type LoginResult struct {
+ NeedsSSOLogin bool `json:"needsSsoLogin"`
+ UserCode string `json:"userCode"`
+ VerificationURI string `json:"verificationUri"`
+ VerificationURIComplete string `json:"verificationUriComplete"`
+}
+
+// WaitSSOParams carries the fields the UI passes to WaitSSOLogin.
+type WaitSSOParams struct {
+ UserCode string `json:"userCode"`
+ Hostname string `json:"hostname"`
+}
+
+// UpParams selects the profile the daemon should bring up.
+type UpParams struct {
+ ProfileName string `json:"profileName"`
+ Username string `json:"username"`
+}
+
+// LogoutParams selects the profile the daemon should log out.
+type LogoutParams struct {
+ ProfileName string `json:"profileName"`
+ Username string `json:"username"`
+}
+
+// Connection groups the daemon RPCs that drive login / connect / disconnect.
+type Connection struct {
+ conn DaemonConn
+}
+
+func NewConnection(conn DaemonConn) *Connection {
+ return &Connection{conn: conn}
+}
+
+func (s *Connection) Login(ctx context.Context, p LoginParams) (LoginResult, error) {
+ cli, err := s.conn.Client()
+ if err != nil {
+ return LoginResult{}, err
+ }
+ req := &proto.LoginRequest{
+ ManagementUrl: p.ManagementURL,
+ SetupKey: p.SetupKey,
+ Hostname: p.Hostname,
+ }
+ if p.ProfileName != "" {
+ req.ProfileName = ptrStr(p.ProfileName)
+ }
+ if p.Username != "" {
+ req.Username = ptrStr(p.Username)
+ }
+ if p.PreSharedKey != "" {
+ req.OptionalPreSharedKey = ptrStr(p.PreSharedKey)
+ }
+ if p.Hint != "" {
+ req.Hint = ptrStr(p.Hint)
+ }
+
+ resp, err := cli.Login(ctx, req)
+ if err != nil {
+ return LoginResult{}, err
+ }
+ return LoginResult{
+ NeedsSSOLogin: resp.GetNeedsSSOLogin(),
+ UserCode: resp.GetUserCode(),
+ VerificationURI: resp.GetVerificationURI(),
+ VerificationURIComplete: resp.GetVerificationURIComplete(),
+ }, nil
+}
+
+func (s *Connection) WaitSSOLogin(ctx context.Context, p WaitSSOParams) (string, error) {
+ cli, err := s.conn.Client()
+ if err != nil {
+ return "", err
+ }
+ resp, err := cli.WaitSSOLogin(ctx, &proto.WaitSSOLoginRequest{
+ UserCode: p.UserCode,
+ Hostname: p.Hostname,
+ })
+ if err != nil {
+ return "", err
+ }
+ return resp.GetEmail(), nil
+}
+
+func (s *Connection) Up(ctx context.Context, p UpParams) error {
+ cli, err := s.conn.Client()
+ if err != nil {
+ return err
+ }
+ req := &proto.UpRequest{}
+ if p.ProfileName != "" {
+ req.ProfileName = ptrStr(p.ProfileName)
+ }
+ if p.Username != "" {
+ req.Username = ptrStr(p.Username)
+ }
+ _, err = cli.Up(ctx, req)
+ return err
+}
+
+func (s *Connection) Down(ctx context.Context) error {
+ cli, err := s.conn.Client()
+ if err != nil {
+ return err
+ }
+ _, err = cli.Down(ctx, &proto.DownRequest{})
+ return err
+}
+
+func (s *Connection) Logout(ctx context.Context, p LogoutParams) error {
+ cli, err := s.conn.Client()
+ if err != nil {
+ return err
+ }
+ req := &proto.LogoutRequest{}
+ if p.ProfileName != "" {
+ req.ProfileName = ptrStr(p.ProfileName)
+ }
+ if p.Username != "" {
+ req.Username = ptrStr(p.Username)
+ }
+ _, err = cli.Logout(ctx, req)
+ return err
+}
diff --git a/client/ui-wails/services/debug.go b/client/ui-wails/services/debug.go
new file mode 100644
index 000000000..71ea6138a
--- /dev/null
+++ b/client/ui-wails/services/debug.go
@@ -0,0 +1,88 @@
+//go:build !android && !ios && !freebsd && !js
+
+package services
+
+import (
+ "context"
+
+ "github.com/netbirdio/netbird/client/proto"
+)
+
+// DebugBundleParams configures what the daemon collects when generating a
+// debug bundle.
+type DebugBundleParams struct {
+ Anonymize bool `json:"anonymize"`
+ SystemInfo bool `json:"systemInfo"`
+ UploadURL string `json:"uploadUrl"`
+ LogFileCount uint32 `json:"logFileCount"`
+}
+
+// DebugBundleResult mirrors DebugBundleResponse — Path is set on local-only
+// bundles, UploadedKey on successful uploads, UploadFailureReason on failed
+// uploads.
+type DebugBundleResult struct {
+ Path string `json:"path"`
+ UploadedKey string `json:"uploadedKey"`
+ UploadFailureReason string `json:"uploadFailureReason"`
+}
+
+// LogLevel is a single log-level value the daemon understands ("error",
+// "warn", "info", "debug", "trace").
+type LogLevel struct {
+ Level string `json:"level"`
+}
+
+// Debug groups debug / log-level / packet-trace RPCs.
+type Debug struct {
+ conn DaemonConn
+}
+
+func NewDebug(conn DaemonConn) *Debug {
+ return &Debug{conn: conn}
+}
+
+func (s *Debug) Bundle(ctx context.Context, p DebugBundleParams) (DebugBundleResult, error) {
+ cli, err := s.conn.Client()
+ if err != nil {
+ return DebugBundleResult{}, err
+ }
+ resp, err := cli.DebugBundle(ctx, &proto.DebugBundleRequest{
+ Anonymize: p.Anonymize,
+ SystemInfo: p.SystemInfo,
+ UploadURL: p.UploadURL,
+ LogFileCount: p.LogFileCount,
+ })
+ if err != nil {
+ return DebugBundleResult{}, err
+ }
+ return DebugBundleResult{
+ Path: resp.GetPath(),
+ UploadedKey: resp.GetUploadedKey(),
+ UploadFailureReason: resp.GetUploadFailureReason(),
+ }, nil
+}
+
+func (s *Debug) GetLogLevel(ctx context.Context) (LogLevel, error) {
+ cli, err := s.conn.Client()
+ if err != nil {
+ return LogLevel{}, err
+ }
+ resp, err := cli.GetLogLevel(ctx, &proto.GetLogLevelRequest{})
+ if err != nil {
+ return LogLevel{}, err
+ }
+ return LogLevel{Level: resp.GetLevel().String()}, nil
+}
+
+func (s *Debug) SetLogLevel(ctx context.Context, lvl LogLevel) error {
+ cli, err := s.conn.Client()
+ if err != nil {
+ return err
+ }
+ level, ok := proto.LogLevel_value[lvl.Level]
+ if !ok {
+ level = int32(proto.LogLevel_INFO)
+ }
+ _, err = cli.SetLogLevel(ctx, &proto.SetLogLevelRequest{Level: proto.LogLevel(level)})
+ return err
+}
diff --git a/client/ui-wails/services/network.go b/client/ui-wails/services/network.go
new file mode 100644
index 000000000..44257e120
--- /dev/null
+++ b/client/ui-wails/services/network.go
@@ -0,0 +1,92 @@
+//go:build !android && !ios && !freebsd && !js
+
+package services
+
+import (
+ "context"
+
+ "github.com/netbirdio/netbird/client/proto"
+)
+
+// Network is one routed network the daemon offers to the client.
+type Network struct {
+ ID string `json:"id"`
+ Range string `json:"range"`
+ Selected bool `json:"selected"`
+ Domains []string `json:"domains"`
+ ResolvedIPs map[string][]string `json:"resolvedIps"`
+}
+
+// SelectNetworksParams selects which networks to enable / disable.
+// All means "every available network" (used by Select-All / Deselect-All buttons);
+// Append means "leave the existing selection in place and merge these IDs in".
+type SelectNetworksParams struct {
+ NetworkIDs []string `json:"networkIds"`
+ Append bool `json:"append"`
+ All bool `json:"all"`
+}
+
+// Networks groups the daemon RPCs that read and toggle routed networks.
+type Networks struct {
+ conn DaemonConn
+}
+
+func NewNetworks(conn DaemonConn) *Networks {
+ return &Networks{conn: conn}
+}
+
+func (s *Networks) List(ctx context.Context) ([]Network, error) {
+ cli, err := s.conn.Client()
+ if err != nil {
+ return nil, err
+ }
+ resp, err := cli.ListNetworks(ctx, &proto.ListNetworksRequest{})
+ if err != nil {
+ return nil, err
+ }
+ out := make([]Network, 0, len(resp.GetRoutes()))
+ for _, n := range resp.GetRoutes() {
+ out = append(out, networkFromProto(n))
+ }
+ return out, nil
+}
+
+func (s *Networks) Select(ctx context.Context, p SelectNetworksParams) error {
+ cli, err := s.conn.Client()
+ if err != nil {
+ return err
+ }
+ _, err = cli.SelectNetworks(ctx, &proto.SelectNetworksRequest{
+ NetworkIDs: p.NetworkIDs,
+ Append: p.Append,
+ All: p.All,
+ })
+ return err
+}
+
+func (s *Networks) Deselect(ctx context.Context, p SelectNetworksParams) error {
+ cli, err := s.conn.Client()
+ if err != nil {
+ return err
+ }
+ _, err = cli.DeselectNetworks(ctx, &proto.SelectNetworksRequest{
+ NetworkIDs: p.NetworkIDs,
+ Append: p.Append,
+ All: p.All,
+ })
+ return err
+}
+
+func networkFromProto(n *proto.Network) Network {
+ resolved := make(map[string][]string, len(n.GetResolvedIPs()))
+ for k, v := range n.GetResolvedIPs() {
+ resolved[k] = append([]string{}, v.GetIps()...)
+ }
+ return Network{
+ ID: n.GetID(),
+ Range: n.GetRange(),
+ Selected: n.GetSelected(),
+ Domains: append([]string{}, n.GetDomains()...),
+ ResolvedIPs: resolved,
+ }
+}
diff --git a/client/ui-wails/services/peers.go b/client/ui-wails/services/peers.go
new file mode 100644
index 000000000..093371e39
--- /dev/null
+++ b/client/ui-wails/services/peers.go
@@ -0,0 +1,328 @@
+//go:build !android && !ios && !freebsd && !js
+
+package services
+
+import (
+ "context"
+ "fmt"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/cenkalti/backoff/v4"
+ log "github.com/sirupsen/logrus"
+
+ "github.com/netbirdio/netbird/client/proto"
+)
+
+// PollInterval is how often Watch falls back to Status polling when the
+// SubscribeEvents stream is unavailable. Matches the Fyne UI's 2-second cadence.
+const PollInterval = 2 * time.Second
+
+const (
+ // EventStatus is emitted to the frontend whenever a fresh Status snapshot
+ // is captured (from a poll or a stream-driven refresh).
+ EventStatus = "netbird:status"
+ // EventSystem is emitted for each SubscribeEvents message (DNS, network,
+ // auth, connectivity categories).
+ EventSystem = "netbird:event"
+ // EventUpdateAvailable fires when the daemon detects a new version. The
+ // metadata's enforced flag is propagated as part of the payload.
+ EventUpdateAvailable = "netbird:update:available"
+ // EventUpdateProgress fires when the daemon is about to start (or has
+ // started) installing an update — Mode 2 enforced flow. The UI opens the
+ // progress window in response.
+ EventUpdateProgress = "netbird:update:progress"
+)
+
+// Emitter is what peers.Watch needs from the host application: a simple
+// "send this name and payload to the frontend" hook. The Wails app.Event
+// satisfies this with its Emit method.
+type Emitter interface {
+ Emit(name string, data ...any) bool
+}
+
+// UpdateAvailable carries the new_version_available metadata.
+type UpdateAvailable struct {
+ Version string `json:"version"`
+ Enforced bool `json:"enforced"`
+}
+
+// UpdateProgress carries the progress_window metadata.
+type UpdateProgress struct {
+ Action string `json:"action"`
+ Version string `json:"version"`
+}
+
+// SystemEvent is the frontend-facing shape of a daemon SystemEvent.
+type SystemEvent struct {
+ ID string `json:"id"`
+ Severity string `json:"severity"`
+ Category string `json:"category"`
+ Message string `json:"message"`
+ UserMessage string `json:"userMessage"`
+ Timestamp int64 `json:"timestamp"`
+ Metadata map[string]string `json:"metadata"`
+}
+
+// PeerStatus is the frontend-facing shape of a daemon PeerState. Carries
+// enough detail for the dashboard's compact peer row plus the on-click
+// troubleshooting expansion (ICE candidate types, endpoints, handshake age).
+type PeerStatus struct {
+ IP string `json:"ip"`
+ PubKey string `json:"pubKey"`
+ ConnStatus string `json:"connStatus"`
+ ConnStatusUpdateUnix int64 `json:"connStatusUpdateUnix"`
+ Relayed bool `json:"relayed"`
+ LocalIceCandidateType string `json:"localIceCandidateType"`
+ RemoteIceCandidateType string `json:"remoteIceCandidateType"`
+ LocalIceCandidateEndpoint string `json:"localIceCandidateEndpoint"`
+ RemoteIceCandidateEndpoint string `json:"remoteIceCandidateEndpoint"`
+ Fqdn string `json:"fqdn"`
+ BytesRx int64 `json:"bytesRx"`
+ BytesTx int64 `json:"bytesTx"`
+ LatencyMs int64 `json:"latencyMs"`
+ RelayAddress string `json:"relayAddress"`
+ LastHandshakeUnix int64 `json:"lastHandshakeUnix"`
+ RosenpassEnabled bool `json:"rosenpassEnabled"`
+ Networks []string `json:"networks"`
+}
+
+// PeerLink is one of the named connections between this peer and its mgmt
+// or signal server.
+type PeerLink struct {
+ URL string `json:"url"`
+ Connected bool `json:"connected"`
+ Error string `json:"error,omitempty"`
+}
+
+// LocalPeer mirrors LocalPeerState — what this client looks like on the mesh.
+type LocalPeer struct {
+ IP string `json:"ip"`
+ PubKey string `json:"pubKey"`
+ Fqdn string `json:"fqdn"`
+ Networks []string `json:"networks"`
+}
+
+// Status is the snapshot the frontend renders on the dashboard.
+type Status struct {
+ Status string `json:"status"`
+ DaemonVersion string `json:"daemonVersion"`
+ Management PeerLink `json:"management"`
+ Signal PeerLink `json:"signal"`
+ Local LocalPeer `json:"local"`
+ Peers []PeerStatus `json:"peers"`
+ Events []SystemEvent `json:"events"`
+}
+
+// Peers serves the dashboard data: one polled Status RPC and a long-running
+// SubscribeEvents stream that re-emits every event over the Wails event bus.
+type Peers struct {
+ conn DaemonConn
+ emitter Emitter
+
+ mu sync.Mutex
+ cancel context.CancelFunc
+ streamWg sync.WaitGroup
+}
+
+func NewPeers(conn DaemonConn, emitter Emitter) *Peers {
+ return &Peers{conn: conn, emitter: emitter}
+}
+
+// Watch starts the background loop: a poll-then-stream pair that runs until
+// ctx (or the service shutdown) cancels it. Safe to call once at boot.
+func (s *Peers) Watch(ctx context.Context) {
+ s.mu.Lock()
+ if s.cancel != nil {
+ s.mu.Unlock()
+ return
+ }
+ ctx, cancel := context.WithCancel(ctx)
+ s.cancel = cancel
+ s.mu.Unlock()
+
+ s.streamWg.Add(2)
+ go s.pollLoop(ctx)
+ go s.streamLoop(ctx)
+}
+
+// ServiceShutdown is the Wails service hook fired on app exit.
+func (s *Peers) ServiceShutdown() error {
+ s.mu.Lock()
+ cancel := s.cancel
+ s.cancel = nil
+ s.mu.Unlock()
+ if cancel != nil {
+ cancel()
+ }
+ s.streamWg.Wait()
+ return nil
+}
+
+// Get returns the current daemon status snapshot.
+func (s *Peers) Get(ctx context.Context) (Status, error) {
+ cli, err := s.conn.Client()
+ if err != nil {
+ return Status{}, err
+ }
+ resp, err := cli.Status(ctx, &proto.StatusRequest{GetFullPeerStatus: true})
+ if err != nil {
+ return Status{}, err
+ }
+ return statusFromProto(resp), nil
+}
+
+func (s *Peers) pollLoop(ctx context.Context) {
+ defer s.streamWg.Done()
+ ticker := time.NewTicker(PollInterval)
+ defer ticker.Stop()
+
+ for {
+ st, err := s.Get(ctx)
+ if err == nil {
+ s.emitter.Emit(EventStatus, st)
+ } else if ctx.Err() == nil {
+ log.Debugf("status poll: %v", err)
+ }
+
+ select {
+ case <-ctx.Done():
+ return
+ case <-ticker.C:
+ }
+ }
+}
+
+func (s *Peers) streamLoop(ctx context.Context) {
+ defer s.streamWg.Done()
+
+ bo := 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)
+
+ op := func() error {
+ cli, err := s.conn.Client()
+ if err != nil {
+ return fmt.Errorf("get client: %w", err)
+ }
+ stream, err := cli.SubscribeEvents(ctx, &proto.SubscribeRequest{})
+ if err != nil {
+ return fmt.Errorf("subscribe: %w", err)
+ }
+ for {
+ ev, err := stream.Recv()
+ if err != nil {
+ if ctx.Err() != nil {
+ return ctx.Err()
+ }
+ return fmt.Errorf("stream recv: %w", err)
+ }
+ s.emitter.Emit(EventSystem, systemEventFromProto(ev))
+ s.fanOutUpdateEvents(ev)
+ }
+ }
+
+ if err := backoff.Retry(op, bo); err != nil && ctx.Err() == nil {
+ log.Errorf("event stream ended: %v", err)
+ }
+}
+
+func statusFromProto(resp *proto.StatusResponse) Status {
+ full := resp.GetFullStatus()
+ mgmt := full.GetManagementState()
+ sig := full.GetSignalState()
+ local := full.GetLocalPeerState()
+
+ st := Status{
+ Status: resp.GetStatus(),
+ DaemonVersion: resp.GetDaemonVersion(),
+ Management: PeerLink{
+ URL: mgmt.GetURL(),
+ Connected: mgmt.GetConnected(),
+ Error: mgmt.GetError(),
+ },
+ Signal: PeerLink{
+ URL: sig.GetURL(),
+ Connected: sig.GetConnected(),
+ Error: sig.GetError(),
+ },
+ Local: LocalPeer{
+ IP: local.GetIP(),
+ PubKey: local.GetPubKey(),
+ Fqdn: local.GetFqdn(),
+ Networks: append([]string{}, local.GetNetworks()...),
+ },
+ }
+
+ for _, p := range full.GetPeers() {
+ st.Peers = append(st.Peers, PeerStatus{
+ IP: p.GetIP(),
+ PubKey: p.GetPubKey(),
+ ConnStatus: p.GetConnStatus(),
+ ConnStatusUpdateUnix: p.GetConnStatusUpdate().GetSeconds(),
+ Relayed: p.GetRelayed(),
+ LocalIceCandidateType: p.GetLocalIceCandidateType(),
+ RemoteIceCandidateType: p.GetRemoteIceCandidateType(),
+ LocalIceCandidateEndpoint: p.GetLocalIceCandidateEndpoint(),
+ RemoteIceCandidateEndpoint: p.GetRemoteIceCandidateEndpoint(),
+ Fqdn: p.GetFqdn(),
+ BytesRx: p.GetBytesRx(),
+ BytesTx: p.GetBytesTx(),
+ LatencyMs: p.GetLatency().AsDuration().Milliseconds(),
+ RelayAddress: p.GetRelayAddress(),
+ LastHandshakeUnix: p.GetLastWireguardHandshake().GetSeconds(),
+ RosenpassEnabled: p.GetRosenpassEnabled(),
+ Networks: append([]string{}, p.GetNetworks()...),
+ })
+ }
+ for _, e := range full.GetEvents() {
+ st.Events = append(st.Events, systemEventFromProto(e))
+ }
+ return st
+}
+
+// fanOutUpdateEvents inspects the daemon SystemEvent for update-related
+// metadata keys and re-emits them as dedicated Wails events. This lets the
+// tray and React update window listen for a single, narrow event instead of
+// re-checking metadata on every system event they receive.
+func (s *Peers) fanOutUpdateEvents(ev *proto.SystemEvent) {
+ md := ev.GetMetadata()
+ if md == nil {
+ return
+ }
+ if v, ok := md["new_version_available"]; ok {
+ _, enforced := md["enforced"]
+ s.emitter.Emit(EventUpdateAvailable, UpdateAvailable{Version: v, Enforced: enforced})
+ }
+ if action, ok := md["progress_window"]; ok {
+ s.emitter.Emit(EventUpdateProgress, UpdateProgress{
+ Action: action,
+ Version: md["version"],
+ })
+ }
+}
+
+func systemEventFromProto(e *proto.SystemEvent) SystemEvent {
+ out := SystemEvent{
+ ID: e.GetId(),
+ Severity: strings.ToLower(strings.TrimPrefix(e.GetSeverity().String(), "SystemEvent_")),
+ Category: strings.ToLower(strings.TrimPrefix(e.GetCategory().String(), "SystemEvent_")),
+ Message: e.GetMessage(),
+ UserMessage: e.GetUserMessage(),
+ Metadata: map[string]string{},
+ }
+ if ts := e.GetTimestamp(); ts != nil {
+ out.Timestamp = ts.GetSeconds()
+ }
+ for k, v := range e.GetMetadata() {
+ out.Metadata[k] = v
+ }
+ return out
+}
diff --git a/client/ui-wails/services/profile.go b/client/ui-wails/services/profile.go
new file mode 100644
index 000000000..7efcf46bc
--- /dev/null
+++ b/client/ui-wails/services/profile.go
@@ -0,0 +1,118 @@
+//go:build !android && !ios && !freebsd && !js
+
+package services
+
+import (
+ "context"
+ "os/user"
+
+ "github.com/netbirdio/netbird/client/proto"
+)
+
+// Profile is one named daemon profile.
+type Profile struct {
+ Name string `json:"name"`
+ IsActive bool `json:"isActive"`
+}
+
+// ProfileRef identifies a profile by name+username.
+type ProfileRef struct {
+ ProfileName string `json:"profileName"`
+ Username string `json:"username"`
+}
+
+// ActiveProfile is the result of GetActiveProfile.
+type ActiveProfile struct {
+ ProfileName string `json:"profileName"`
+ Username string `json:"username"`
+}
+
+// Profiles groups the daemon RPCs that manage named profiles.
+type Profiles struct {
+ conn DaemonConn
+}
+
+func NewProfiles(conn DaemonConn) *Profiles {
+ return &Profiles{conn: conn}
+}
+
+// Username returns the OS username the daemon expects for profile lookups.
+// The frontend calls this once at boot and reuses the result.
+func (s *Profiles) Username() (string, error) {
+ u, err := user.Current()
+ if err != nil {
+ return "", err
+ }
+ return u.Username, nil
+}
+
+func (s *Profiles) List(ctx context.Context, username string) ([]Profile, error) {
+ cli, err := s.conn.Client()
+ if err != nil {
+ return nil, err
+ }
+ resp, err := cli.ListProfiles(ctx, &proto.ListProfilesRequest{Username: username})
+ if err != nil {
+ return nil, err
+ }
+ out := make([]Profile, 0, len(resp.GetProfiles()))
+ for _, p := range resp.GetProfiles() {
+ out = append(out, Profile{Name: p.GetName(), IsActive: p.GetIsActive()})
+ }
+ return out, nil
+}
+
+func (s *Profiles) GetActive(ctx context.Context) (ActiveProfile, error) {
+ cli, err := s.conn.Client()
+ if err != nil {
+ return ActiveProfile{}, err
+ }
+ resp, err := cli.GetActiveProfile(ctx, &proto.GetActiveProfileRequest{})
+ if err != nil {
+ return ActiveProfile{}, err
+ }
+ return ActiveProfile{
+ ProfileName: resp.GetProfileName(),
+ Username: resp.GetUsername(),
+ }, nil
+}
+
+func (s *Profiles) Switch(ctx context.Context, p ProfileRef) error {
+ cli, err := s.conn.Client()
+ if err != nil {
+ return err
+ }
+ req := &proto.SwitchProfileRequest{}
+ if p.ProfileName != "" {
+ req.ProfileName = ptrStr(p.ProfileName)
+ }
+ if p.Username != "" {
+ req.Username = ptrStr(p.Username)
+ }
+ _, err = cli.SwitchProfile(ctx, req)
+ return err
+}
+
+func (s *Profiles) Add(ctx context.Context, p ProfileRef) error {
+ cli, err := s.conn.Client()
+ if err != nil {
+ return err
+ }
+ _, err = cli.AddProfile(ctx, &proto.AddProfileRequest{
+ ProfileName: p.ProfileName,
+ Username: p.Username,
+ })
+ return err
+}
+
+func (s *Profiles) Remove(ctx context.Context, p ProfileRef) error {
+ cli, err := s.conn.Client()
+ if err != nil {
+ return err
+ }
+ _, err = cli.RemoveProfile(ctx, &proto.RemoveProfileRequest{
+ ProfileName: p.ProfileName,
+ Username: p.Username,
+ })
+ return err
+}
diff --git a/client/ui-wails/services/settings.go b/client/ui-wails/services/settings.go
new file mode 100644
index 000000000..c5d24232a
--- /dev/null
+++ b/client/ui-wails/services/settings.go
@@ -0,0 +1,192 @@
+//go:build !android && !ios && !freebsd && !js
+
+package services
+
+import (
+ "context"
+
+ "github.com/netbirdio/netbird/client/proto"
+)
+
+// ConfigParams selects which profile/user to read or write config for.
+type ConfigParams struct {
+ ProfileName string `json:"profileName"`
+ Username string `json:"username"`
+}
+
+// Config is the daemon configuration the UI exposes in the settings window.
+// Pointer fields mark "set" vs "unset" so the UI can omit a value to keep the
+// daemon's current setting (matching SetConfigRequest's optional semantics).
+type Config struct {
+ ManagementURL string `json:"managementUrl"`
+ AdminURL string `json:"adminUrl"`
+ ConfigFile string `json:"configFile"`
+ LogFile string `json:"logFile"`
+ PreSharedKey string `json:"preSharedKey"`
+ InterfaceName string `json:"interfaceName"`
+ WireguardPort int64 `json:"wireguardPort"`
+ MTU int64 `json:"mtu"`
+ DisableAutoConnect bool `json:"disableAutoConnect"`
+ ServerSSHAllowed bool `json:"serverSshAllowed"`
+ RosenpassEnabled bool `json:"rosenpassEnabled"`
+ RosenpassPermissive bool `json:"rosenpassPermissive"`
+ DisableNotifications bool `json:"disableNotifications"`
+ LazyConnectionEnabled bool `json:"lazyConnectionEnabled"`
+ BlockInbound bool `json:"blockInbound"`
+ NetworkMonitor bool `json:"networkMonitor"`
+ DisableClientRoutes bool `json:"disableClientRoutes"`
+ DisableServerRoutes bool `json:"disableServerRoutes"`
+ DisableDNS bool `json:"disableDns"`
+ BlockLANAccess bool `json:"blockLanAccess"`
+ EnableSSHRoot bool `json:"enableSshRoot"`
+ EnableSSHSFTP bool `json:"enableSshSftp"`
+ EnableSSHLocalPortForwarding bool `json:"enableSshLocalPortForwarding"`
+ EnableSSHRemotePortForwarding bool `json:"enableSshRemotePortForwarding"`
+ DisableSSHAuth bool `json:"disableSshAuth"`
+ SSHJWTCacheTTL int32 `json:"sshJwtCacheTtl"`
+}
+
+// SetConfigParams is a partial update — only fields with non-nil pointers
+// are sent to the daemon. The frontend uses this to flip individual toggles.
+type SetConfigParams struct {
+ ProfileName string `json:"profileName"`
+ Username string `json:"username"`
+ ManagementURL string `json:"managementUrl"`
+ AdminURL string `json:"adminUrl"`
+ InterfaceName *string `json:"interfaceName,omitempty"`
+ WireguardPort *int64 `json:"wireguardPort,omitempty"`
+ MTU *int64 `json:"mtu,omitempty"`
+ PreSharedKey *string `json:"preSharedKey,omitempty"`
+ DisableAutoConnect *bool `json:"disableAutoConnect,omitempty"`
+ ServerSSHAllowed *bool `json:"serverSshAllowed,omitempty"`
+ RosenpassEnabled *bool `json:"rosenpassEnabled,omitempty"`
+ RosenpassPermissive *bool `json:"rosenpassPermissive,omitempty"`
+ DisableNotifications *bool `json:"disableNotifications,omitempty"`
+ LazyConnectionEnabled *bool `json:"lazyConnectionEnabled,omitempty"`
+ BlockInbound *bool `json:"blockInbound,omitempty"`
+ NetworkMonitor *bool `json:"networkMonitor,omitempty"`
+ DisableClientRoutes *bool `json:"disableClientRoutes,omitempty"`
+ DisableServerRoutes *bool `json:"disableServerRoutes,omitempty"`
+ DisableDNS *bool `json:"disableDns,omitempty"`
+ DisableFirewall *bool `json:"disableFirewall,omitempty"`
+ BlockLANAccess *bool `json:"blockLanAccess,omitempty"`
+ EnableSSHRoot *bool `json:"enableSshRoot,omitempty"`
+ EnableSSHSFTP *bool `json:"enableSshSftp,omitempty"`
+ EnableSSHLocalPortForwarding *bool `json:"enableSshLocalPortForwarding,omitempty"`
+ EnableSSHRemotePortForwarding *bool `json:"enableSshRemotePortForwarding,omitempty"`
+ DisableSSHAuth *bool `json:"disableSshAuth,omitempty"`
+ SSHJWTCacheTTL *int32 `json:"sshJwtCacheTtl,omitempty"`
+}
+
+// Features reports which UI surfaces the daemon has disabled. The Fyne UI uses
+// these flags to grey out menu items the operator turned off server-side.
+type Features struct {
+ DisableProfiles bool `json:"disableProfiles"`
+ DisableUpdateSettings bool `json:"disableUpdateSettings"`
+ DisableNetworks bool `json:"disableNetworks"`
+}
+
+// Settings groups the daemon RPCs that read and write the daemon config.
+type Settings struct {
+ conn DaemonConn
+}
+
+func NewSettings(conn DaemonConn) *Settings {
+ return &Settings{conn: conn}
+}
+
+func (s *Settings) GetConfig(ctx context.Context, p ConfigParams) (Config, error) {
+ cli, err := s.conn.Client()
+ if err != nil {
+ return Config{}, err
+ }
+ resp, err := cli.GetConfig(ctx, &proto.GetConfigRequest{
+ ProfileName: p.ProfileName,
+ Username: p.Username,
+ })
+ if err != nil {
+ return Config{}, err
+ }
+ return Config{
+ ManagementURL: resp.GetManagementUrl(),
+ AdminURL: resp.GetAdminURL(),
+ ConfigFile: resp.GetConfigFile(),
+ LogFile: resp.GetLogFile(),
+ PreSharedKey: resp.GetPreSharedKey(),
+ InterfaceName: resp.GetInterfaceName(),
+ WireguardPort: resp.GetWireguardPort(),
+ MTU: resp.GetMtu(),
+ DisableAutoConnect: resp.GetDisableAutoConnect(),
+ ServerSSHAllowed: resp.GetServerSSHAllowed(),
+ RosenpassEnabled: resp.GetRosenpassEnabled(),
+ RosenpassPermissive: resp.GetRosenpassPermissive(),
+ DisableNotifications: resp.GetDisableNotifications(),
+ LazyConnectionEnabled: resp.GetLazyConnectionEnabled(),
+ BlockInbound: resp.GetBlockInbound(),
+ NetworkMonitor: resp.GetNetworkMonitor(),
+ DisableClientRoutes: resp.GetDisableClientRoutes(),
+ DisableServerRoutes: resp.GetDisableServerRoutes(),
+ DisableDNS: resp.GetDisableDns(),
+ BlockLANAccess: resp.GetBlockLanAccess(),
+ EnableSSHRoot: resp.GetEnableSSHRoot(),
+ EnableSSHSFTP: resp.GetEnableSSHSFTP(),
+ EnableSSHLocalPortForwarding: resp.GetEnableSSHLocalPortForwarding(),
+ EnableSSHRemotePortForwarding: resp.GetEnableSSHRemotePortForwarding(),
+ DisableSSHAuth: resp.GetDisableSSHAuth(),
+ SSHJWTCacheTTL: resp.GetSshJWTCacheTTL(),
+ }, nil
+}
+
+func (s *Settings) SetConfig(ctx context.Context, p SetConfigParams) error {
+ cli, err := s.conn.Client()
+ if err != nil {
+ return err
+ }
+ req := &proto.SetConfigRequest{
+ ProfileName: p.ProfileName,
+ Username: p.Username,
+ ManagementUrl: p.ManagementURL,
+ AdminURL: p.AdminURL,
+ InterfaceName: p.InterfaceName,
+ WireguardPort: p.WireguardPort,
+ Mtu: p.MTU,
+ OptionalPreSharedKey: p.PreSharedKey,
+ DisableAutoConnect: p.DisableAutoConnect,
+ ServerSSHAllowed: p.ServerSSHAllowed,
+ RosenpassEnabled: p.RosenpassEnabled,
+ RosenpassPermissive: p.RosenpassPermissive,
+ DisableNotifications: p.DisableNotifications,
+ LazyConnectionEnabled: p.LazyConnectionEnabled,
+ BlockInbound: p.BlockInbound,
+ NetworkMonitor: p.NetworkMonitor,
+ DisableClientRoutes: p.DisableClientRoutes,
+ DisableServerRoutes: p.DisableServerRoutes,
+ DisableDns: p.DisableDNS,
+ DisableFirewall: p.DisableFirewall,
+ BlockLanAccess: p.BlockLANAccess,
+ EnableSSHRoot: p.EnableSSHRoot,
+ EnableSSHSFTP: p.EnableSSHSFTP,
+ EnableSSHLocalPortForwarding: p.EnableSSHLocalPortForwarding,
+ EnableSSHRemotePortForwarding: p.EnableSSHRemotePortForwarding,
+ DisableSSHAuth: p.DisableSSHAuth,
+ SshJWTCacheTTL: p.SSHJWTCacheTTL,
+ }
+ _, err = cli.SetConfig(ctx, req)
+ return err
+}
+
+func (s *Settings) GetFeatures(ctx context.Context) (Features, error) {
+ cli, err := s.conn.Client()
+ if err != nil {
+ return Features{}, err
+ }
+ resp, err := cli.GetFeatures(ctx, &proto.GetFeaturesRequest{})
+ if err != nil {
+ return Features{}, err
+ }
+ return Features{
+ DisableProfiles: resp.GetDisableProfiles(),
+ DisableUpdateSettings: resp.GetDisableUpdateSettings(),
+ DisableNetworks: resp.GetDisableNetworks(),
+ }, nil
+}
diff --git a/client/ui-wails/services/update.go b/client/ui-wails/services/update.go
new file mode 100644
index 000000000..e7f9ad9c9
--- /dev/null
+++ b/client/ui-wails/services/update.go
@@ -0,0 +1,55 @@
+//go:build !android && !ios && !freebsd && !js
+
+package services
+
+import (
+ "context"
+
+ "github.com/netbirdio/netbird/client/proto"
+)
+
+// UpdateResult mirrors TriggerUpdateResponse: Success false carries an error
+// message in ErrorMsg.
+type UpdateResult struct {
+ Success bool `json:"success"`
+ ErrorMsg string `json:"errorMsg"`
+}
+
+// Update groups the RPCs that drive the enforced-update install flow.
+type Update struct {
+ conn DaemonConn
+}
+
+func NewUpdate(conn DaemonConn) *Update {
+ return &Update{conn: conn}
+}
+
+func (s *Update) Trigger(ctx context.Context) (UpdateResult, error) {
+ cli, err := s.conn.Client()
+ if err != nil {
+ return UpdateResult{}, err
+ }
+ resp, err := cli.TriggerUpdate(ctx, &proto.TriggerUpdateRequest{})
+ if err != nil {
+ return UpdateResult{}, err
+ }
+ return UpdateResult{
+ Success: resp.GetSuccess(),
+ ErrorMsg: resp.GetErrorMsg(),
+ }, nil
+}
+
+func (s *Update) GetInstallerResult(ctx context.Context) (UpdateResult, error) {
+ cli, err := s.conn.Client()
+ if err != nil {
+ return UpdateResult{}, err
+ }
+ resp, err := cli.GetInstallerResult(ctx, &proto.InstallerResultRequest{})
+ if err != nil {
+ return UpdateResult{}, err
+ }
+ return UpdateResult{
+ Success: resp.GetSuccess(),
+ ErrorMsg: resp.GetErrorMsg(),
+ }, nil
+}
diff --git a/client/ui-wails/signal_unix.go b/client/ui-wails/signal_unix.go
new file mode 100644
index 000000000..a5a9205c0
--- /dev/null
+++ b/client/ui-wails/signal_unix.go
@@ -0,0 +1,33 @@
+//go:build !windows && !android && !ios && !freebsd && !js
+
+package main
+
+import (
+ "context"
+ "os"
+ "os/signal"
+ "syscall"
+
+ log "github.com/sirupsen/logrus"
+)
+
+// listenForShowSignal opens the main window when the process receives SIGUSR1.
+// External tools (the daemon, the installer, or another `netbird-ui` invocation)
+// can poke this channel by signalling the running pid.
+func listenForShowSignal(ctx context.Context, tray *Tray) {
+ sigCh := make(chan os.Signal, 1)
+ signal.Notify(sigCh, syscall.SIGUSR1)
+
+ go func() {
+ for {
+ select {
+ case <-ctx.Done():
+ signal.Stop(sigCh)
+ return
+ case <-sigCh:
+ log.Debug("SIGUSR1 received, showing window")
+ tray.ShowWindow()
+ }
+ }
+ }()
+}
diff --git a/client/ui-wails/signal_windows.go b/client/ui-wails/signal_windows.go
new file mode 100644
index 000000000..22f1623cf
--- /dev/null
+++ b/client/ui-wails/signal_windows.go
@@ -0,0 +1,81 @@
+//go:build windows
+
+package main
+
+import (
+ "context"
+ "errors"
+ "time"
+
+ log "github.com/sirupsen/logrus"
+ "golang.org/x/sys/windows"
+)
+
+const (
+ quickActionsTriggerEventName = `Global\NetBirdQuickActionsTriggerEvent`
+ waitTimeout = 5 * time.Second
+ desiredAccesses = windows.SYNCHRONIZE | windows.EVENT_MODIFY_STATE
+
+ // WaitForSingleObject returns this when the timeout elapses without the
+ // object being signalled. golang.org/x/sys/windows does not expose it.
+ waitTimeoutCode uint32 = 0x00000102
+)
+
+// listenForShowSignal opens the main window when an external process pulses
+// the named event Global\NetBirdQuickActionsTriggerEvent. Mirrors the trigger
+// the legacy Fyne UI used so the installer and CLI integrations keep working.
+func listenForShowSignal(ctx context.Context, tray *Tray) {
+ namePtr, err := windows.UTF16PtrFromString(quickActionsTriggerEventName)
+ if err != nil {
+ log.Errorf("trigger event name: %v", err)
+ return
+ }
+
+ handle, err := windows.CreateEvent(nil, 1, 0, namePtr)
+ if err != nil {
+ if !errors.Is(err, windows.ERROR_ALREADY_EXISTS) {
+ log.Errorf("create trigger event %q: %v", quickActionsTriggerEventName, err)
+ return
+ }
+ handle, err = windows.OpenEvent(desiredAccesses, false, namePtr)
+ if err != nil {
+ log.Errorf("open trigger event %q: %v", quickActionsTriggerEventName, err)
+ return
+ }
+ }
+
+ if handle == windows.InvalidHandle {
+ log.Errorf("invalid handle for trigger event %q", quickActionsTriggerEventName)
+ return
+ }
+
+ go waitForTrigger(ctx, handle, tray)
+}
+
+func waitForTrigger(ctx context.Context, handle windows.Handle, tray *Tray) {
+ defer func() {
+ if err := windows.CloseHandle(handle); err != nil {
+ log.Errorf("close trigger event handle: %v", err)
+ }
+ }()
+
+ timeoutMs := uint32(waitTimeout / time.Millisecond)
+ for {
+ if ctx.Err() != nil {
+ return
+ }
+ ev, err := windows.WaitForSingleObject(handle, timeoutMs)
+ switch {
+ case err != nil:
+ log.Errorf("wait trigger event: %v", err)
+ return
+ case ev == waitTimeoutCode:
+ continue
+ case ev == windows.WAIT_OBJECT_0:
+ if err := windows.ResetEvent(handle); err != nil {
+ log.Errorf("reset trigger event: %v", err)
+ }
+ tray.ShowWindow()
+ }
+ }
+}
diff --git a/client/ui-wails/tray.go b/client/ui-wails/tray.go
new file mode 100644
index 000000000..91a3625e6
--- /dev/null
+++ b/client/ui-wails/tray.go
@@ -0,0 +1,565 @@
+//go:build !android && !ios && !freebsd && !js
+
+package main
+
+import (
+ "context"
+ "fmt"
+ "runtime"
+ "sort"
+ "strings"
+ "sync"
+
+ log "github.com/sirupsen/logrus"
+ "github.com/wailsapp/wails/v3/pkg/application"
+ "github.com/wailsapp/wails/v3/pkg/services/notifications"
+
+ "github.com/netbirdio/netbird/client/ui-wails/services"
+)
+
+// Tray builds and updates the systray menu. It mirrors the layout of the Fyne
+// systray 1:1 and routes clicks back to the gRPC services. Dynamic state
+// (status icon, exit-node submenu) is driven by the netbird:status event.
+type Tray struct {
+ app *application.App
+ tray *application.SystemTray
+ window *application.WebviewWindow
+ connection *services.Connection
+ settings *services.Settings
+ profiles *services.Profiles
+ peers *services.Peers
+ notifier *notifications.NotificationService
+
+ statusItem *application.MenuItem
+ upItem *application.MenuItem
+ downItem *application.MenuItem
+ exitNodeItem *application.MenuItem
+ networksItem *application.MenuItem
+ allowSSHItem *application.MenuItem
+ autoConnItem *application.MenuItem
+ rosenpassItem *application.MenuItem
+ lazyConnItem *application.MenuItem
+ blockInItem *application.MenuItem
+ notifyItem *application.MenuItem
+
+ mu sync.Mutex
+ connected bool
+ hasUpdate bool
+ exitNodes []string
+ lastStatus string
+ notificationsEnabled bool
+ activeProfile string
+ activeUsername string
+}
+
+func NewTray(
+ app *application.App,
+ window *application.WebviewWindow,
+ connection *services.Connection,
+ settings *services.Settings,
+ profiles *services.Profiles,
+ peers *services.Peers,
+ notifier *notifications.NotificationService,
+) *Tray {
+ t := &Tray{
+ app: app,
+ window: window,
+ connection: connection,
+ settings: settings,
+ profiles: profiles,
+ peers: peers,
+ notifier: notifier,
+ notificationsEnabled: true,
+ }
+ t.tray = app.SystemTray.New()
+ t.applyIcon()
+ t.tray.SetTooltip("NetBird")
+ t.tray.SetMenu(t.buildMenu())
+ t.tray.AttachWindow(window)
+ t.tray.OnClick(func() { t.toggleWindow() })
+
+ app.Event.On(services.EventStatus, t.onStatusEvent)
+ app.Event.On(services.EventSystem, t.onSystemEvent)
+ app.Event.On(services.EventUpdateAvailable, t.onUpdateAvailable)
+ app.Event.On(services.EventUpdateProgress, t.onUpdateProgress)
+
+ go t.loadConfig()
+ return t
+}
+
+// ShowWindow brings the main window forward — used by SIGUSR1 / Windows event.
+func (t *Tray) ShowWindow() {
+ if t.window == nil {
+ return
+ }
+ t.window.Show()
+}
+
+func (t *Tray) buildMenu() *application.Menu {
+ menu := application.NewMenu()
+
+ t.statusItem = menu.Add("Disconnected").SetEnabled(false)
+
+ menu.AddSeparator()
+ t.upItem = menu.Add("Connect").OnClick(func(*application.Context) { t.handleConnect() })
+ t.downItem = menu.Add("Disconnect").OnClick(func(*application.Context) { t.handleDisconnect() })
+ t.downItem.SetEnabled(false)
+
+ menu.AddSeparator()
+
+ settingsSub := menu.AddSubmenu("Settings")
+ t.allowSSHItem = settingsSub.AddCheckbox("Allow SSH", false).OnClick(func(*application.Context) {
+ t.flipFlag("ssh", t.allowSSHItem.Checked())
+ })
+ t.autoConnItem = settingsSub.AddCheckbox("Connect on Startup", false).OnClick(func(*application.Context) {
+ t.flipFlag("auto", t.autoConnItem.Checked())
+ })
+ t.rosenpassItem = settingsSub.AddCheckbox("Enable Quantum-Resistance", false).OnClick(func(*application.Context) {
+ t.flipFlag("rosenpass", t.rosenpassItem.Checked())
+ })
+ t.lazyConnItem = settingsSub.AddCheckbox("Enable Lazy Connections", false).OnClick(func(*application.Context) {
+ t.flipFlag("lazy", t.lazyConnItem.Checked())
+ })
+ t.blockInItem = settingsSub.AddCheckbox("Block Inbound Connections", false).OnClick(func(*application.Context) {
+ t.flipFlag("blockin", t.blockInItem.Checked())
+ })
+ t.notifyItem = settingsSub.AddCheckbox("Notifications", true).OnClick(func(*application.Context) {
+ t.flipFlag("notify", t.notifyItem.Checked())
+ })
+ settingsSub.AddSeparator()
+ settingsSub.Add("Advanced Settings").OnClick(func(*application.Context) { t.openRoute("/settings") })
+ settingsSub.Add("Create Debug Bundle").OnClick(func(*application.Context) { t.openRoute("/debug") })
+
+ t.exitNodeItem = menu.Add("Exit Node").SetEnabled(false)
+
+ t.networksItem = menu.Add("Networks").OnClick(func(*application.Context) { t.openRoute("/networks") })
+
+ menu.AddSeparator()
+
+ about := menu.AddSubmenu("About")
+ about.Add("GitHub").OnClick(func(*application.Context) {
+ _ = t.app.Browser.OpenURL("https://github.com/netbirdio/netbird")
+ })
+ about.Add("Documentation").SetEnabled(false)
+
+ menu.AddSeparator()
+ menu.Add("Quit").OnClick(func(*application.Context) { t.app.Quit() })
+
+ return menu
+}
+
+func (t *Tray) toggleWindow() {
+ if t.window == nil {
+ return
+ }
+ if t.window.IsVisible() {
+ t.window.Hide()
+ return
+ }
+ t.window.Show()
+}
+
+func (t *Tray) openRoute(route string) {
+ if t.window == nil {
+ return
+ }
+ t.window.Show()
+ t.window.SetURL("/#" + route)
+}
+
+func (t *Tray) handleConnect() {
+ t.upItem.SetEnabled(false)
+ go func() {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ if err := t.connection.Up(ctx, services.UpParams{}); err != nil {
+ log.Errorf("connect: %v", err)
+ t.notifyError("Failed to connect")
+ t.upItem.SetEnabled(true)
+ }
+ }()
+}
+
+func (t *Tray) handleDisconnect() {
+ t.downItem.SetEnabled(false)
+ go func() {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ if err := t.connection.Down(ctx); err != nil {
+ log.Errorf("disconnect: %v", err)
+ t.notifyError("Failed to disconnect")
+ t.downItem.SetEnabled(true)
+ }
+ }()
+}
+
+// flipFlag pushes a partial SetConfig for one tray-toggled boolean. On
+// failure the tray checkbox is reverted to keep it in sync with the daemon
+// and an error notification is fired so the user knows the change didn't
+// stick. The "notify" flag also updates the in-process gate that decides
+// whether daemon SystemEvents become OS notifications.
+func (t *Tray) flipFlag(name string, checked bool) {
+ go func() {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ t.mu.Lock()
+ profile, username := t.activeProfile, t.activeUsername
+ t.mu.Unlock()
+
+ req := services.SetConfigParams{ProfileName: profile, Username: username}
+ var (
+ label string
+ item *application.MenuItem
+ )
+ switch name {
+ case "ssh":
+ req.ServerSSHAllowed = ptrBool(checked)
+ label, item = "SSH", t.allowSSHItem
+ case "auto":
+ // "Connect on Startup" is the inverse of disableAutoConnect.
+ req.DisableAutoConnect = ptrBool(!checked)
+ label, item = "auto-connect", t.autoConnItem
+ case "rosenpass":
+ req.RosenpassEnabled = ptrBool(checked)
+ label, item = "Rosenpass", t.rosenpassItem
+ case "lazy":
+ req.LazyConnectionEnabled = ptrBool(checked)
+ label, item = "lazy connection", t.lazyConnItem
+ case "blockin":
+ req.BlockInbound = ptrBool(checked)
+ label, item = "block inbound", t.blockInItem
+ case "notify":
+ req.DisableNotifications = ptrBool(!checked)
+ label, item = "notifications", t.notifyItem
+ default:
+ log.Debugf("tray flipFlag: unknown flag %q", name)
+ return
+ }
+
+ if err := t.settings.SetConfig(ctx, req); err != nil {
+ log.Errorf("set %s: %v", label, err)
+ t.notifyError("Failed to update " + label + " settings")
+ if item != nil {
+ item.SetChecked(!checked) // revert
+ }
+ return
+ }
+
+ if name == "notify" {
+ t.mu.Lock()
+ t.notificationsEnabled = checked
+ t.mu.Unlock()
+ }
+ }()
+}
+
+func ptrBool(b bool) *bool { return &b }
+
+func (t *Tray) onStatusEvent(ev *application.CustomEvent) {
+ st, ok := ev.Data.(services.Status)
+ if !ok {
+ return
+ }
+ t.applyStatus(st)
+}
+
+// onSystemEvent fires an OS notification for daemon SystemEvents that carry
+// a user-facing message, mirroring the legacy event.Manager behaviour: gated
+// by the user's "Notifications" toggle, with CRITICAL events bypassing the
+// gate. The narrowly-scoped EventUpdate* events are skipped here because
+// onUpdateAvailable already produces a richer notification for them.
+func (t *Tray) onSystemEvent(ev *application.CustomEvent) {
+ se, ok := ev.Data.(services.SystemEvent)
+ if !ok || se.UserMessage == "" {
+ return
+ }
+ if _, isUpdate := se.Metadata["new_version_available"]; isUpdate {
+ return
+ }
+
+ critical := se.Severity == "critical"
+ t.mu.Lock()
+ enabled := t.notificationsEnabled
+ t.mu.Unlock()
+ if !enabled && !critical {
+ return
+ }
+
+ body := se.UserMessage
+ if id := se.Metadata["id"]; id != "" {
+ body += fmt.Sprintf(" ID: %s", id)
+ }
+ t.notify(eventTitle(se), body, "netbird-event-"+se.ID)
+}
+
+// onUpdateAvailable runs when the daemon reports a new netbird version. It
+// flips the tray's hasUpdate flag (icon swap) and posts an OS notification.
+// The notification is what the legacy Fyne UI used to alert the user.
+func (t *Tray) onUpdateAvailable(ev *application.CustomEvent) {
+ upd, ok := ev.Data.(services.UpdateAvailable)
+ if !ok {
+ return
+ }
+ t.mu.Lock()
+ t.hasUpdate = true
+ t.mu.Unlock()
+ t.applyIcon()
+
+ body := fmt.Sprintf("NetBird %s is available.", upd.Version)
+ if upd.Enforced {
+ body += " Your administrator requires this update."
+ }
+ if err := t.notifier.SendNotification(notifications.NotificationOptions{
+ ID: "netbird-update-" + upd.Version,
+ Title: "NetBird update available",
+ Body: body,
+ }); err != nil {
+ log.Debugf("send update notification: %v", err)
+ }
+}
+
+// onUpdateProgress runs when the daemon enters the install phase of an
+// enforced update. The Fyne UI used to spawn a separate process with the
+// update window; here the window is already in-process, so we just route to
+// the /update page and bring it forward.
+func (t *Tray) onUpdateProgress(ev *application.CustomEvent) {
+ prog, ok := ev.Data.(services.UpdateProgress)
+ if !ok || prog.Action != "show" {
+ return
+ }
+ if t.window == nil {
+ return
+ }
+ url := "/#/update"
+ if prog.Version != "" {
+ url += "?version=" + prog.Version
+ }
+ t.window.SetURL(url)
+ t.window.Show()
+}
+
+// applyStatus updates the tray icon, status label, exit-node submenu, and
+// connect/disconnect enablement based on the latest daemon snapshot.
+func (t *Tray) applyStatus(st services.Status) {
+ t.mu.Lock()
+ connected := strings.EqualFold(st.Status, "Connected")
+ t.connected = connected
+ t.lastStatus = st.Status
+
+ exitNodes := exitNodesFromStatus(st)
+ exitNodesChanged := !equalStrings(exitNodes, t.exitNodes)
+ t.exitNodes = exitNodes
+ t.mu.Unlock()
+
+ t.applyIcon()
+ if t.statusItem != nil {
+ t.statusItem.SetLabel(st.Status)
+ }
+ if t.upItem != nil {
+ t.upItem.SetEnabled(!connected)
+ }
+ if t.downItem != nil {
+ t.downItem.SetEnabled(connected)
+ }
+ if exitNodesChanged {
+ t.rebuildExitNodes(exitNodes)
+ }
+}
+
+func (t *Tray) rebuildExitNodes(nodes []string) {
+ if t.exitNodeItem == nil {
+ return
+ }
+ if len(nodes) == 0 {
+ t.exitNodeItem.SetEnabled(false)
+ return
+ }
+ sub := application.NewMenu()
+ for _, fqdn := range nodes {
+ sub.AddCheckbox(fqdn, false)
+ }
+ t.exitNodeItem.SetEnabled(true)
+}
+
+func (t *Tray) applyIcon() {
+ if runtime.GOOS == "windows" {
+ t.mu.Lock()
+ ico := trayIcon(t.connected, t.hasUpdate, t.lastStatus)
+ t.mu.Unlock()
+ if ico != nil {
+ t.tray.SetIcon(ico)
+ }
+ return
+ }
+
+ icon, dark := t.iconForState()
+ if runtime.GOOS == "darwin" {
+ t.tray.SetTemplateIcon(icon)
+ return
+ }
+ t.tray.SetIcon(icon)
+ if dark != nil {
+ t.tray.SetDarkModeIcon(dark)
+ }
+}
+
+func (t *Tray) iconForState() (icon, dark []byte) {
+ t.mu.Lock()
+ connected := t.connected
+ hasUpdate := t.hasUpdate
+ statusLabel := t.lastStatus
+ t.mu.Unlock()
+
+ connecting := strings.EqualFold(statusLabel, "Connecting")
+ errored := strings.EqualFold(statusLabel, "Error")
+
+ if runtime.GOOS == "darwin" {
+ switch {
+ case connecting:
+ return iconConnectingMacOS, nil
+ case errored:
+ return iconErrorMacOS, nil
+ case connected && hasUpdate:
+ return iconUpdateConnectedMacOS, nil
+ case connected:
+ return iconConnectedMacOS, nil
+ case hasUpdate:
+ return iconUpdateDisconnectedMacOS, nil
+ default:
+ return iconDisconnectedMacOS, nil
+ }
+ }
+
+ switch {
+ case connecting:
+ return iconConnecting, nil
+ case errored:
+ return iconError, nil
+ case connected && hasUpdate:
+ return iconUpdateConnected, nil
+ case connected:
+ return iconConnected, iconConnectedDark
+ case hasUpdate:
+ return iconUpdateDisconnected, nil
+ default:
+ return iconDisconnected, nil
+ }
+}
+
+// loadConfig syncs the tray-submenu checkboxes with the daemon's stored
+// config and seeds the notifications gate. Called once at startup from a
+// goroutine so a slow or unreachable daemon does not block menu construction.
+func (t *Tray) loadConfig() {
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ active, err := t.profiles.GetActive(ctx)
+ if err != nil {
+ log.Debugf("get active profile: %v", err)
+ return
+ }
+ cfg, err := t.settings.GetConfig(ctx, services.ConfigParams(active))
+ if err != nil {
+ log.Debugf("get config: %v", err)
+ return
+ }
+
+ t.mu.Lock()
+ t.activeProfile = active.ProfileName
+ t.activeUsername = active.Username
+ t.notificationsEnabled = !cfg.DisableNotifications
+ t.mu.Unlock()
+
+ if t.allowSSHItem != nil {
+ t.allowSSHItem.SetChecked(cfg.ServerSSHAllowed)
+ }
+ if t.autoConnItem != nil {
+ t.autoConnItem.SetChecked(!cfg.DisableAutoConnect)
+ }
+ if t.rosenpassItem != nil {
+ t.rosenpassItem.SetChecked(cfg.RosenpassEnabled)
+ }
+ if t.lazyConnItem != nil {
+ t.lazyConnItem.SetChecked(cfg.LazyConnectionEnabled)
+ }
+ if t.blockInItem != nil {
+ t.blockInItem.SetChecked(cfg.BlockInbound)
+ }
+ if t.notifyItem != nil {
+ t.notifyItem.SetChecked(!cfg.DisableNotifications)
+ }
+}
+
+// notify wraps the Wails notification service with the tray's standard
+// id-prefix scheme and swallows errors (notifications are best-effort).
+func (t *Tray) notify(title, body, id string) {
+ if t.notifier == nil {
+ return
+ }
+ if err := t.notifier.SendNotification(notifications.NotificationOptions{
+ ID: id,
+ Title: title,
+ Body: body,
+ }); err != nil {
+ log.Debugf("notify %q: %v", title, err)
+ }
+}
+
+// notifyError fires a generic "Error" notification for tray-driven action
+// failures. Each tray click site already logs the underlying error; this
+// adds the user-visible toast.
+func (t *Tray) notifyError(message string) {
+ t.notify("Error", message, "netbird-tray-error")
+}
+
+func exitNodesFromStatus(st services.Status) []string {
+ seen := map[string]struct{}{}
+ out := []string{}
+ for _, p := range st.Peers {
+ if p.Fqdn == "" {
+ continue
+ }
+ if _, ok := seen[p.Fqdn]; ok {
+ continue
+ }
+ seen[p.Fqdn] = struct{}{}
+ out = append(out, p.Fqdn)
+ }
+ sort.Strings(out)
+ return out
+}
+
+func equalStrings(a, b []string) bool {
+ if len(a) != len(b) {
+ return false
+ }
+ for i := range a {
+ if a[i] != b[i] {
+ return false
+ }
+ }
+ return true
+}
+
+// eventTitle composes a notification title from a SystemEvent's severity and
+// category — "Critical: DNS", "Warning: Authentication", etc. — matching the
+// format the legacy Fyne event.Manager produced.
+func eventTitle(e services.SystemEvent) string {
+ prefix := titleCase(e.Severity)
+ if prefix == "" {
+ prefix = "Info"
+ }
+ category := titleCase(e.Category)
+ if category == "" {
+ category = "System"
+ }
+ return prefix + ": " + category
+}
+
+func titleCase(s string) string {
+ if s == "" {
+ return ""
+ }
+ return strings.ToUpper(s[:1]) + strings.ToLower(s[1:])
+}
diff --git a/client/ui-wails/tray_icon_other.go b/client/ui-wails/tray_icon_other.go
new file mode 100644
index 000000000..6e2489929
--- /dev/null
+++ b/client/ui-wails/tray_icon_other.go
@@ -0,0 +1,8 @@
+//go:build !windows && !android && !ios && !freebsd && !js
+
+package main
+
+// trayIcon is unused on non-Windows hosts — Linux feeds setIcon a PNG and
+// macOS uses SetTemplateIcon. This stub exists so the compiler is happy and
+// callers don't need build tags around references.
+func trayIcon(_, _ bool, _ string) []byte { return nil }
diff --git a/client/ui-wails/tray_icon_windows.go b/client/ui-wails/tray_icon_windows.go
new file mode 100644
index 000000000..8cd7ec606
--- /dev/null
+++ b/client/ui-wails/tray_icon_windows.go
@@ -0,0 +1,27 @@
+//go:build windows
+
+package main
+
+import "strings"
+
+// trayIcon returns the Windows-tray .ico bytes for the given state. The
+// other-platform implementation in tray_icon_other.go returns the colored
+// PNG instead. Splitting it this way keeps the Linux/macOS paths free of
+// .ico artifacts in their //go:embed search and avoids loading large icon
+// resources where they aren't used.
+func trayIcon(connected, hasUpdate bool, statusLabel string) []byte {
+ switch {
+ case strings.EqualFold(statusLabel, "Connecting"):
+ return winIconConnecting
+ case strings.EqualFold(statusLabel, "Error"):
+ return winIconError
+ case connected && hasUpdate:
+ return winIconUpdateConnected
+ case connected:
+ return winIconConnected
+ case hasUpdate:
+ return winIconUpdateDisconnected
+ default:
+ return winIconDisconnected
+ }
+}
diff --git a/go.mod b/go.mod
index 1958a3278..8ed0ba38e 100644
--- a/go.mod
+++ b/go.mod
@@ -5,17 +5,17 @@ go 1.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.4
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.49.0
golang.org/x/sys v0.42.0
@@ -53,7 +53,7 @@ require (
github.com/fsnotify/fsnotify v1.9.0
github.com/gliderlabs/ssh v0.3.8
github.com/go-jose/go-jose/v4 v4.1.3
- 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
@@ -104,6 +104,7 @@ require (
github.com/ti-mo/conntrack v0.5.1
github.com/ti-mo/netfilter v0.5.2
github.com/vmihailenco/msgpack/v5 v5.4.1
+ github.com/wailsapp/wails/v3 v3.0.0-alpha.78
github.com/yusufpapurcu/wmi v1.2.4
github.com/zcalusic/sysinfo v1.1.3
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0
@@ -114,7 +115,7 @@ 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-20250620022241-b7579e27df2b
+ golang.org/x/exp v0.0.0-20260112195511-716be5621a96
golang.org/x/mobile v0.0.0-20251113184115-a159579294ab
golang.org/x/mod v0.33.0
golang.org/x/net v0.52.0
@@ -135,16 +136,18 @@ require (
cloud.google.com/go/auth v0.20.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.1 // 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/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect
@@ -166,6 +169,7 @@ require (
github.com/aws/smithy-go v1.23.0 // 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/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
@@ -173,13 +177,15 @@ require (
github.com/containerd/platforms v0.2.1 // indirect
github.com/cpuguy83/dockercfg v0.3.2 // indirect
github.com/crowdsecurity/go-cs-lib v0.0.25 // indirect
+ github.com/cyphar/filepath-securejoin v0.6.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // 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.6.0 // indirect
github.com/docker/go-units v0.5.0 // indirect
- github.com/ebitengine/purego v0.8.4 // 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
@@ -187,6 +193,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-ldap/ldap/v3 v3.4.12 // indirect
@@ -208,6 +217,7 @@ require (
github.com/goccy/go-yaml v1.18.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
+ github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect
github.com/google/btree v1.1.2 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/s2a-go v0.1.9 // indirect
@@ -225,6 +235,8 @@ require (
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/jackpal/go-nat-pmp v1.0.2 // 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
@@ -233,16 +245,22 @@ require (
github.com/josharian/intern v1.0.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.10 // 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/koron/go-ssdp v0.0.4 // 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/mailru/easyjson v0.9.0 // 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
@@ -263,7 +281,6 @@ require (
github.com/nicksnyder/go-i18n/v2 v2.5.1 // indirect
github.com/nxadm/tail v1.4.11 // indirect
github.com/oklog/ulid v1.3.1 // indirect
- github.com/onsi/ginkgo/v2 v2.9.5 // 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
@@ -271,6 +288,8 @@ 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.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
@@ -278,12 +297,16 @@ require (
github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/otlptranslator v1.0.0 // indirect
github.com/prometheus/procfs v0.19.2 // indirect
+ github.com/rivo/uniseg v0.4.7 // indirect
github.com/russellhaering/goxmldsig v1.6.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.8 // indirect
github.com/shoenig/go-m1cpu v0.2.1 // 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
@@ -291,8 +314,10 @@ require (
github.com/tklauser/numcpus v0.10.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/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.mongodb.org/mongo-driver v1.17.9 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
@@ -301,13 +326,14 @@ require (
go.opentelemetry.io/otel/trace v1.43.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect
- golang.org/x/image v0.33.0 // indirect
+ golang.org/x/image v0.35.0 // indirect
golang.org/x/text v0.35.0 // indirect
golang.org/x/tools v0.42.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // 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
gopkg.in/yaml.v2 v2.4.0 // indirect
)
diff --git a/go.sum b/go.sum
index 2abf55142..de267d08f 100644
--- a/go.sum
+++ b/go.sum
@@ -7,8 +7,8 @@ cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdB
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
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.1 h1:YpjwWWlNmGIDyXOn8zLzqiD+9TyIlPhGFG96P39uBpw=
filippo.io/edwards25519 v1.1.1/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
fyne.io/fyne/v2 v2.7.0 h1:GvZSpE3X0liU/fqstInVvRsaboIVpIWQ4/sfjDGIGGQ=
@@ -25,23 +25,30 @@ github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
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 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=
+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/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=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
+github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
+github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/awnumar/memcall v0.4.0 h1:B7hgZYdfH6Ot1Goaz8jGne/7i8xD4taZie/PNSFZ29g=
@@ -90,6 +97,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=
@@ -132,6 +141,8 @@ github.com/crowdsecurity/go-cs-lib v0.0.25 h1:Ov6VPW9yV+OPsbAIQk1iTkEWhwkpaG0v3l
github.com/crowdsecurity/go-cs-lib v0.0.25/go.mod h1:X0GMJY2CxdA1S09SpuqIKaWQsvRGxXmecUp9cP599dE=
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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
@@ -148,14 +159,18 @@ github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pM
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
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.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
-github.com/ebitengine/purego v0.8.4/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/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=
+github.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=
+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=
@@ -185,12 +200,22 @@ 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-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
+github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
+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=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
+github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e h1:Lf/gRkoycfOBPa42vU2bbgPurFong6zXeFtPoxholzU=
+github.com/go-json-experiment/json v0.0.0-20251027170946-4849db3c2f7e/go.mod h1:uNVvRXArCGbZ508SxYYTC5v1JWoz2voff5pm25jU1Ok=
github.com/go-ldap/ldap/v3 v3.4.12 h1:1b81mv7MagXZ7+1r7cLTWmyuTqVqdwbtJSjC0DAp9s4=
github.com/go-ldap/ldap/v3 v3.4.12/go.mod h1:+SPAGcTtOfmGsCb3h1RFiq4xpp4N636G75OEace8lNo=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
@@ -226,9 +251,10 @@ github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZs
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
+github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
-github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
-github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
+github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
+github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-text/render v0.2.0 h1:LBYoTmp5jYiJ4NPqDc2pz17MLmA3wHw1dZSVGcOdeAc=
github.com/go-text/render v0.2.0/go.mod h1:CkiqfukRGKJA5vZZISkjSYrcdtgKQWRa2HIzvwNN5SU=
github.com/go-text/typesetting v0.2.1 h1:x0jMOGyO3d1qFAPI0j4GSsh7M0Q3Ypjzr4+CEVg82V8=
@@ -237,14 +263,16 @@ 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/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
-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/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
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=
@@ -281,8 +309,8 @@ github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
github.com/google/nftables v0.3.0 h1:bkyZ0cbpVeMHXOrtlFc8ISmfVqq5gPJukoYieyVmITg=
github.com/google/nftables v0.3.0/go.mod h1:BCp9FsrbF1Fn/Yu6CLUc9GGZFw/+hsxfluNXXmxBfRM=
-github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y=
-github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
+github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6 h1:k7nVchz72niMH6YLQNvHSdIE7iqsQxK1P41mySCvssg=
+github.com/google/pprof v0.0.0-20240424215950-a892ee059fd6/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0=
github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@@ -334,6 +362,10 @@ 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/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus=
github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc=
+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=
@@ -366,13 +398,15 @@ 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 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw=
+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.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
-github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
+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/koron/go-ssdp v0.0.4 h1:1IDwrghSKYM7yLf7XCzbByg2sJ/JcNOZRXS2jczTwz0=
github.com/koron/go-ssdp v0.0.4/go.mod h1:oDXq+E5IL5q0U8uSBcoAXzTzInwy5lEgC91HoKtbmZk=
github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8=
@@ -386,6 +420,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=
@@ -395,6 +433,8 @@ github.com/libdns/route53 v1.5.0 h1:2SKdpPFl/qgWsXQvsLNJJAoX7rSxlk7zgoL4jnWdXVA=
github.com/libdns/route53 v1.5.0/go.mod h1:joT4hKmaTNKHEwb7GmZ65eoDz1whTu7KKYPS8ZqIh6Q=
github.com/libp2p/go-nat v0.2.0 h1:Tyz+bUFAYqGyJ/ppPPymMGbIgNRH+WqC5QrT5fKrrGk=
github.com/libp2p/go-nat v0.2.0/go.mod h1:3MJr+GRpRkyT65EpVPBstXLvOlAPzUVlG6Pwg9ohLJk=
+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=
@@ -402,9 +442,16 @@ 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/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
+github.com/matryer/is v1.4.1/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=
@@ -481,12 +528,12 @@ github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+W
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
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 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA=
+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 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
+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=
@@ -527,6 +574,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=
@@ -554,6 +605,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=
@@ -565,6 +619,10 @@ github.com/russellhaering/goxmldsig v1.6.0/go.mod h1:TrnaquDcYxWXfJrOjeMBTX4mLBe
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.8 h1:NnAsw9lN7587WHxjJA9ryDnqhJpFH6A+wagYWTOH970=
@@ -577,18 +635,22 @@ github.com/shoenig/test v1.7.0 h1:eWcHtTXa6QLnBvm0jgEabMRN/uJ4DMV3M8xUGgRkZmk=
github.com/shoenig/test v1.7.0/go.mod h1:UxJ6u/x2v/TNs/LoLxBNJRV9DiwBBKYxXSyczsBHFoI=
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.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
+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 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
+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=
@@ -600,6 +662,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=
@@ -642,14 +705,20 @@ 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.78 h1:31nJb4N8X+SIBZ88RNkptFA1eUnBOH805tDV0sN7Vpk=
+github.com/wailsapp/wails/v3 v3.0.0-alpha.78/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=
@@ -702,6 +771,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=
@@ -711,10 +781,10 @@ 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.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
-golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
-golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
-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/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.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=
@@ -738,6 +808,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=
@@ -772,23 +843,29 @@ 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=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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=
@@ -818,6 +895,7 @@ golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
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=
@@ -893,6 +971,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=