Compare commits

...

13 Commits

Author SHA1 Message Date
Zoltán Papp
0148d926d5 [client/ui-wails] Use original Fyne tray PNGs and drop the .ico split
The SVG-derived tray icons + multi-resolution .ico path looked correct on
disk but Wails3's Shell_NotifyIcon update never landed on the running
Windows tray — the icon stayed frozen on the .exe resource regardless of
how many times we called SetIcon. Single-PNG fed through the same path
updates correctly, so revert to the source-of-truth PNGs that ship with
the legacy Fyne UI and remove the icons_windows.go / tray_icon_*.go
split. The 6 colored tray PNGs and 6 macOS-template PNGs come from
client/ui/assets verbatim. Generation pipeline (assets/svg/) is gone.
2026-04-29 18:54:51 +02:00
Zoltán Papp
8f16a19b8f [client/ui-wails] Add windows:build:console task for log debugging
The default Windows build links the binary as a GUI subsystem app, so
stdout/stderr is detached from the launching terminal — invisible logrus
output makes tray and event-stream bugs hard to chase. Add a sibling task
that links as console subsystem and writes a separately-named binary so
the production output is preserved.

Usage:
  CGO_ENABLED=1 task windows:build:console
  bin\netbird-ui-console.exe   # logs print to the launching cmd/PowerShell
2026-04-29 16:21:45 +02:00
Zoltán Papp
504dceedf3 [client] Add Wails3 + React desktop UI scaffold
Stage 1 of the client/ui (Fyne) replacement. Adds a new client/ui-wails
module that runs on Linux/macOS/Windows from a single React + Vite +
Tailwind frontend driven by a thin gRPC services layer in Go.

- Single-module integration (no submodule): merge Wails3 into root go.mod
  with build tags !android !ios !freebsd !js so cross-compiles on those
  targets exclude the package automatically.
- Seven gRPC-bound services: Connection, Settings, Networks, Profiles,
  Debug, Update, Peers. Peers bridges Status polling and SubscribeEvents
  to the Wails event bus (netbird:status, netbird:event).
- Tray + window shell mirrors the Fyne menu 1:1 with hide-on-close,
  SIGUSR1 / Windows named-event for external "show window" triggers.
- React pages cover functional parity for Status, Settings (3 tabs),
  Networks (3 tabs), Profiles, Debug, Update, QuickActions, LoginUrl.
- SVG-sourced tray icons (12 source SVGs incl. macOS template variants)
  rasterized to PNG via task common:generate:tray:icons.
- Linux launcher sets WEBKIT_DISABLE_DMABUF_RENDERER=1 in the .desktop
  Exec= line and in task linux:run so the app renders correctly under
  RDP, VirtualBox, KVM, and bare WMs (Fluxbox/dwm) without DRM access.
2026-04-29 11:10:23 +02:00
Viktor Liu
e5474e199f [client] Use WinRT COM for Windows toasts (#6013)
* Use WinRT COM for Windows toasts instead of fyne's PowerShell path

* Quote autostart path and split HKCU registry into per-user component
2026-04-28 20:54:06 +02:00
Bethuel Mmbaga
db44848e2d [management] Drop netmap calculation on peer read (#6006) 2026-04-28 18:25:56 +03:00
EL OUAZIZI Walid
9417ce3b3a fix(getting-started): Infinite healthcheck loop with existing traefik (#5871) 2026-04-28 17:22:51 +02:00
Zoltan Papp
8fc4265995 [relay] evict foreign client cache on disconnect (#6015)
* [relay] evict foreign client cache on disconnect

When a foreign relay's TCP connection drops, the manager's
onServerDisconnected handler only triggered reconnect logic for the
home server; the disconnected foreign entry stayed in the relayClients
cache. Subsequent OpenConn calls reused the closed client until the
60-second cleanup tick evicted it, breaking peer connectivity through
that relay for up to a minute.

Evict the foreign entry from the cache on disconnect so the next
OpenConn dials a fresh client.

Also:
- Make the reconnect backoff cap configurable via WithMaxBackoffInterval
  ManagerOption; the previous hard-coded 60s constant forced
  TestAutoReconnect to sleep ~61s. Test now polls Ready() and finishes
  in ~2s.
- Add NB_HOME_RELAY_SERVERS env var that overrides the relay URL list
  received from management, so a peer can be pinned to a specific home
  relay (used by the netbird-conn-lab Edge 4 reproducer).

* [client] treat empty NB_HOME_RELAY_SERVERS as unset

Returning (urls=[], ok=true) when the env var contained only separators or
whitespace caused callers to wipe the mgmt-provided relay list, leaving the
peer with no relays. Treat a parsed-empty result the same as an unset env.
2026-04-28 15:04:48 +02:00
Zoltan Papp
9c50819f20 Don't mark management disconnected on transient job stream errors (#6005)
The JOB stream is a separate channel from the SYNC stream. Server-side
EOF or other transient errors on the JOB stream do not indicate that
the management connection is unhealthy — the SYNC stream remains the
authoritative state signal.

Previously, a JOB stream EOF would call notifyDisconnected and the
client would emit OnConnecting to the UI. The backoff retry would
reconnect the JOB stream, but handleJobStream never calls notifyConnected
on success, so the UI was stuck on "Connecting" until the next SYNC
event or health check.

Keep notifyDisconnected for codes.PermissionDenied since IsLoginRequired
relies on managementError to detect expired auth.
2026-04-28 15:04:41 +02:00
Bethuel Mmbaga
6f0eff3ba0 [management] Handle single-string JWT group claim from IdPs (#6014) 2026-04-28 14:48:28 +03:00
Bethuel Mmbaga
f8745723fc [management] Add Microsoft AD FS support for embedded Dex identity providers (#6008) 2026-04-28 12:42:19 +03:00
Vlad
154b81645a [management] removed legacy network map code (#5565) 2026-04-27 16:02:54 +02:00
Maycon Santos
34167c8a16 [misc] Update release pipeline version (#5995) 2026-04-27 10:55:38 +02:00
Maycon Santos
d6f08e4840 [misc] Update sign pipeline version (#5981) 2026-04-24 13:13:27 +02:00
154 changed files with 9314 additions and 5605 deletions

View File

@@ -9,7 +9,7 @@ on:
pull_request:
env:
SIGN_PIPE_VER: "v0.1.2"
SIGN_PIPE_VER: "v0.1.4"
GORELEASER_VER: "v2.14.3"
PRODUCT_NAME: "NetBird"
COPYRIGHT: "NetBird GmbH"

View File

@@ -200,6 +200,7 @@ Pop $0
!macroend
Function .onInit
SetRegView 64
StrCpy $INSTDIR "${INSTALL_DIR}"
ReadRegStr $R0 HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\$(^NAME)" "UninstallString"
${If} $R0 != ""
@@ -214,6 +215,10 @@ ${If} $R0 != ""
${EndIf}
FunctionEnd
Function un.onInit
SetRegView 64
FunctionEnd
######################################################################
Section -MainProgram
${INSTALL_TYPE}
@@ -228,6 +233,7 @@ Section -MainProgram
!else
File /r "..\\dist\\netbird_windows_amd64\\"
!endif
File "..\\client\\ui\\assets\\netbird.png"
SectionEnd
######################################################################
@@ -247,9 +253,11 @@ WriteRegStr ${REG_ROOT} "${UI_REG_APP_PATH}" "" "$INSTDIR\${UI_APP_EXE}"
; Create autostart registry entry based on checkbox
DetailPrint "Autostart enabled: $AutostartEnabled"
${If} $AutostartEnabled == "1"
WriteRegStr HKCU "${AUTOSTART_REG_KEY}" "${APP_NAME}" "$INSTDIR\${UI_APP_EXE}.exe"
WriteRegStr HKLM "${AUTOSTART_REG_KEY}" "${APP_NAME}" '"$INSTDIR\${UI_APP_EXE}.exe"'
DetailPrint "Added autostart registry entry: $INSTDIR\${UI_APP_EXE}.exe"
${Else}
DeleteRegValue HKLM "${AUTOSTART_REG_KEY}" "${APP_NAME}"
; Legacy: pre-HKLM installs wrote to HKCU; clean that up too.
DeleteRegValue HKCU "${AUTOSTART_REG_KEY}" "${APP_NAME}"
DetailPrint "Autostart not enabled by user"
${EndIf}
@@ -283,6 +291,8 @@ ExecWait `taskkill /im ${UI_APP_EXE}.exe /f`
; Remove autostart registry entry
DetailPrint "Removing autostart registry entry if exists..."
DeleteRegValue HKLM "${AUTOSTART_REG_KEY}" "${APP_NAME}"
; Legacy: pre-HKLM installs wrote to HKCU; clean that up too.
DeleteRegValue HKCU "${AUTOSTART_REG_KEY}" "${APP_NAME}"
; Handle data deletion based on checkbox
@@ -321,6 +331,7 @@ DetailPrint "Removing registry keys..."
DeleteRegKey ${REG_ROOT} "${REG_APP_PATH}"
DeleteRegKey ${REG_ROOT} "${UNINSTALL_PATH}"
DeleteRegKey ${REG_ROOT} "${UI_REG_APP_PATH}"
DeleteRegKey HKCU "Software\Classes\AppUserModelId\${APP_NAME}"
DetailPrint "Removing application directory from PATH..."
EnVar::SetHKLM

View File

@@ -333,6 +333,10 @@ func (c *ConnectClient) run(mobileDependency MobileDependency, runningChan chan
c.statusRecorder.MarkSignalConnected()
relayURLs, token := parseRelayInfo(loginResp)
if override, ok := peer.OverrideRelayURLs(); ok {
log.Infof("overriding relay URLs from %s: %v", peer.EnvKeyNBHomeRelayServers, override)
relayURLs = override
}
peerConfig := loginResp.GetPeerConfig()
engineConfig, err := createEngineConfig(myPrivateKey, c.config, peerConfig, logPath)

View File

@@ -944,7 +944,12 @@ func (e *Engine) handleRelayUpdate(update *mgmProto.RelayConfig) error {
return fmt.Errorf("update relay token: %w", err)
}
e.relayManager.UpdateServerURLs(update.Urls)
urls := update.Urls
if override, ok := peer.OverrideRelayURLs(); ok {
log.Infof("overriding relay URLs from %s: %v", peer.EnvKeyNBHomeRelayServers, override)
urls = override
}
e.relayManager.UpdateServerURLs(urls)
// Just in case the agent started with an MGM server where the relay was disabled but was later enabled.
// We can ignore all errors because the guard will manage the reconnection retries.

View File

@@ -7,7 +7,8 @@ import (
)
const (
EnvKeyNBForceRelay = "NB_FORCE_RELAY"
EnvKeyNBForceRelay = "NB_FORCE_RELAY"
EnvKeyNBHomeRelayServers = "NB_HOME_RELAY_SERVERS"
)
func IsForceRelayed() bool {
@@ -16,3 +17,28 @@ func IsForceRelayed() bool {
}
return strings.EqualFold(os.Getenv(EnvKeyNBForceRelay), "true")
}
// OverrideRelayURLs returns the relay server URL list set in
// NB_HOME_RELAY_SERVERS (comma-separated) and a boolean indicating whether
// the override is active. When the env var is unset, the boolean is false
// and the caller should keep the list received from the management server.
// Intended for lab/debug scenarios where a peer must pin to a specific home
// relay regardless of what management offers.
func OverrideRelayURLs() ([]string, bool) {
raw := os.Getenv(EnvKeyNBHomeRelayServers)
if raw == "" {
return nil, false
}
parts := strings.Split(raw, ",")
urls := make([]string, 0, len(parts))
for _, p := range parts {
p = strings.TrimSpace(p)
if p != "" {
urls = append(urls, p)
}
}
if len(urls) == 0 {
return nil, false
}
return urls, true
}

View File

@@ -18,10 +18,17 @@
<Component Id="NetbirdFiles" Guid="db3165de-cc6e-4922-8396-9d892950e23e" Bitness="always64">
<File ProcessorArchitecture="$(var.ProcessorArchitecture)" Source=".\dist\netbird_windows_$(var.ArchSuffix)\netbird.exe" KeyPath="yes" />
<File ProcessorArchitecture="$(var.ProcessorArchitecture)" Source=".\dist\netbird_windows_$(var.ArchSuffix)\netbird-ui.exe">
<Shortcut Id="NetbirdDesktopShortcut" Directory="DesktopFolder" Name="NetBird" WorkingDirectory="NetbirdInstallDir" Icon="NetbirdIcon" />
<Shortcut Id="NetbirdStartMenuShortcut" Directory="StartMenuFolder" Name="NetBird" WorkingDirectory="NetbirdInstallDir" Icon="NetbirdIcon" />
<Shortcut Id="NetbirdDesktopShortcut" Directory="DesktopFolder" Name="NetBird" WorkingDirectory="NetbirdInstallDir" Icon="NetbirdIcon">
<ShortcutProperty Key="System.AppUserModel.ID" Value="NetBird" />
<ShortcutProperty Key="System.AppUserModel.ToastActivatorCLSID" Value="{0E1B4DE7-E148-432B-9814-544F941826EC}" />
</Shortcut>
<Shortcut Id="NetbirdStartMenuShortcut" Directory="StartMenuFolder" Name="NetBird" WorkingDirectory="NetbirdInstallDir" Icon="NetbirdIcon">
<ShortcutProperty Key="System.AppUserModel.ID" Value="NetBird" />
<ShortcutProperty Key="System.AppUserModel.ToastActivatorCLSID" Value="{0E1B4DE7-E148-432B-9814-544F941826EC}" />
</Shortcut>
</File>
<File ProcessorArchitecture="$(var.ProcessorArchitecture)" Source=".\dist\netbird_windows_$(var.ArchSuffix)\wintun.dll" />
<File Id="NetbirdToastIcon" Name="netbird.png" Source=".\client\ui\assets\netbird.png" />
<?if $(var.ArchSuffix) = "amd64" ?>
<File ProcessorArchitecture="$(var.ProcessorArchitecture)" Source=".\dist\netbird_windows_$(var.ArchSuffix)\opengl32.dll" />
<?endif ?>
@@ -46,8 +53,19 @@
</Directory>
</StandardDirectory>
<!-- Per-user component: HKCU keypath (auto GUID via "*"), separate from
the per-machine NetbirdFiles component to satisfy ICE57. -->
<StandardDirectory Id="ProgramMenuFolder">
<Component Id="NetbirdAumidRegistry" Guid="*">
<RegistryKey Root="HKCU" Key="Software\Classes\AppUserModelId\NetBird" ForceDeleteOnUninstall="yes">
<RegistryValue Name="InstalledByMSI" Type="integer" Value="1" KeyPath="yes" />
</RegistryKey>
</Component>
</StandardDirectory>
<ComponentGroup Id="NetbirdFilesComponent">
<ComponentRef Id="NetbirdFiles" />
<ComponentRef Id="NetbirdAumidRegistry" />
</ComponentGroup>
<util:CloseApplication Id="CloseNetBird" CloseMessage="no" Target="netbird.exe" RebootPrompt="no" />

7
client/ui-wails/.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
.task
bin
frontend/dist
frontend/node_modules
frontend/bindings
build/linux/appimage/build
build/windows/nsis/MicrosoftEdgeWebview2Setup.exe

100
client/ui-wails/README.md Normal file
View File

@@ -0,0 +1,100 @@
# NetBird desktop UI (Wails3 + React)
Replaces `client/ui` (Fyne). One binary on Windows / macOS / Linux,
talks to the NetBird daemon over gRPC, renders a React frontend in a
WebView.
## Prerequisites
- Go ≥ 1.25, Node ≥ 20, **pnpm** (`corepack enable && corepack prepare pnpm@latest --activate`)
- `wails3` CLI: `go install github.com/wailsapp/wails/v3/cmd/wails3@latest`
- `task`: `go install github.com/go-task/task/v3/cmd/task@latest`
- A running NetBird daemon (default: `unix:///var/run/netbird.sock`,
Windows `tcp://127.0.0.1:41731`)
- Linux only: `libwebkit2gtk-4.1-dev`, `libgtk-3-dev`,
`libayatana-appindicator3-dev`
## Develop without rebuilding
```bash
cd client/ui-wails
task dev
```
`task dev` runs Vite (port 9245) + the Go binary + a `*.go` watcher.
Frontend edits hot-reload instantly. Go edits trigger a rebuild and
relaunch. Pass daemon flags after `--`:
```bash
task dev -- --daemon-addr=tcp://127.0.0.1:41731
```
For pure UI work (no native window, fastest loop):
```bash
cd frontend && pnpm dev
```
## Production build
```bash
task build
```
Output in `bin/`. Frontend assets are embedded into the binary.
### Cross-compile Windows from Linux
Install the mingw-w64 toolchain once:
```bash
sudo apt install gcc-mingw-w64-x86-64 # Debian/Ubuntu
sudo dnf install mingw64-gcc # Fedora
sudo pacman -S mingw-w64-gcc # Arch
```
Then:
```bash
CGO_ENABLED=1 task windows:build
```
Produces `bin/netbird-ui.exe`. macOS cross-compile from Linux is not
supported (signing and notarization need a real Mac).
### Windows console build (logs in the terminal)
Default `windows:build` links the binary as a Windows GUI app, which
detaches from the launching console — `logrus` output, `fmt.Println`,
and panics go nowhere visible. To debug tray/event/daemon issues:
```bash
CGO_ENABLED=1 task windows:build:console
```
Produces `bin/netbird-ui-console.exe`. Run it from `cmd.exe` /
PowerShell / Windows Terminal and stdout/stderr land in that
terminal. Same flag works on a native Windows build (drop the
`CGO_ENABLED=1` if your toolchain already has it set).
## Regenerating bindings
When a Go service signature changes:
```bash
wails3 generate bindings
```
`task dev` does this automatically on `*.go` save.
## Tray icons
Source SVGs live in `assets/svg/` (state.svg + state-macos.svg). After editing
any SVG, rasterize to the PNGs the Go side embeds:
```bash
task common:generate:tray:icons
```
Requires Inkscape. Commit the resulting `assets/*.png` files alongside the
SVG change so CI doesn't need Inkscape installed.

View File

@@ -0,0 +1,58 @@
version: '3'
includes:
common: ./build/Taskfile.yml
windows: ./build/windows/Taskfile.yml
darwin: ./build/darwin/Taskfile.yml
linux: ./build/linux/Taskfile.yml
vars:
APP_NAME: "netbird-ui"
BIN_DIR: "bin"
VITE_PORT: '{{.WAILS_VITE_PORT | default 9245}}'
tasks:
build:
summary: Builds the application
cmds:
- task: "{{OS}}:build"
package:
summary: Packages a production build of the application
cmds:
- task: "{{OS}}:package"
run:
summary: Runs the application
cmds:
- task: "{{OS}}:run"
dev:
summary: Runs the application in development mode
cmds:
- wails3 dev -config ./build/config.yml -port {{.VITE_PORT}}
setup:docker:
summary: Builds Docker image for cross-compilation (~800MB download)
cmds:
- task: common:setup:docker
build:server:
summary: Builds the application in server mode (no GUI, HTTP server only)
cmds:
- task: common:build:server
run:server:
summary: Runs the application in server mode
cmds:
- task: common:run:server
build:docker:
summary: Builds a Docker image for server mode deployment
cmds:
- task: common:build:docker
run:docker:
summary: Builds and runs the Docker image
cmds:
- task: common:run:docker

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

@@ -0,0 +1,14 @@
<!--
NetBird base mark, centered in a 32×32 viewBox with badge-friendly margins.
Preserved across every state icon as required by the design plan; state
badges sit on top in the bottom-right 12×12 area (x=18..30, y=18..30).
The mark itself is taken verbatim from dashboard/src/assets/netbird.svg
(three orange/red paths) and translated into the 32×32 grid.
-->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
<g id="netbird-mark" transform="translate(2 5) scale(0.8)">
<path d="M21.4631 0.523438C17.8173 0.857913 16.0028 2.95675 15.3171 4.01871L4.66406 22.4734H17.5163L30.1929 0.523438H21.4631Z" fill="#F68330"/>
<path d="M17.5265 22.4737L0 3.88525C0 3.88525 19.8177 -1.44128 21.7493 15.1738L17.5265 22.4737Z" fill="#F68330"/>
<path d="M14.9236 4.70563L9.54688 14.0208L17.5158 22.4747L21.7385 15.158C21.0696 9.44682 18.2851 6.32784 14.9236 4.69727" fill="#F05252"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 932 B

View File

@@ -0,0 +1,17 @@
<!--
App icon source. Rasterized to build/appicon.png by
`task common:generate:icons`, which then drives `wails3 generate icons`
to produce the per-platform .ico / .icns artifacts.
The mark fills ~90% of the canvas width (with vertical centering) so
Windows Explorer and macOS Finder render a recognisable bird at small
sizes. The mark's native aspect (31:23) is wider than tall, so width is
the binding dimension.
-->
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="1024" viewBox="0 0 1024 1024">
<g transform="translate(37 170) scale(29.7)">
<path d="M21.4631 0.523438C17.8173 0.857913 16.0028 2.95675 15.3171 4.01871L4.66406 22.4734H17.5163L30.1929 0.523438H21.4631Z" fill="#F68330"/>
<path d="M17.5265 22.4737L0 3.88525C0 3.88525 19.8177 -1.44128 21.7493 15.1738L17.5265 22.4737Z" fill="#F68330"/>
<path d="M14.9236 4.70563L9.54688 14.0208L17.5158 22.4747L21.7385 15.158C21.0696 9.44682 18.2851 6.32784 14.9236 4.69727" fill="#F05252"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 997 B

View File

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
<g transform="translate(0.5 4.5)" fill="black">
<path d="M21.4631 0.523438C17.8173 0.857913 16.0028 2.95675 15.3171 4.01871L4.66406 22.4734H17.5163L30.1929 0.523438H21.4631Z"/>
<path d="M17.5265 22.4737L0 3.88525C0 3.88525 19.8177 -1.44128 21.7493 15.1738L17.5265 22.4737Z"/>
<path d="M14.9236 4.70563L9.54688 14.0208L17.5158 22.4747L21.7385 15.158C21.0696 9.44682 18.2851 6.32784 14.9236 4.69727"/>
</g>
<circle cx="25" cy="25" r="7" fill="white"/>
<circle cx="25" cy="25" r="6" fill="black"/>
<path d="M22 25 L24 27 L28 23" stroke="white" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 723 B

View File

@@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
<!-- Mark fills the canvas. Badge overlaps the bottom-right corner so most
of the mark is still visible at 16 px tray sizes. -->
<g transform="translate(0.5 4.5) scale(1.0)">
<path d="M21.4631 0.523438C17.8173 0.857913 16.0028 2.95675 15.3171 4.01871L4.66406 22.4734H17.5163L30.1929 0.523438H21.4631Z" fill="#F68330"/>
<path d="M17.5265 22.4737L0 3.88525C0 3.88525 19.8177 -1.44128 21.7493 15.1738L17.5265 22.4737Z" fill="#F68330"/>
<path d="M14.9236 4.70563L9.54688 14.0208L17.5158 22.4747L21.7385 15.158C21.0696 9.44682 18.2851 6.32784 14.9236 4.69727" fill="#F05252"/>
</g>
<!-- connected badge: green check, ~25% canvas, with a thin white halo so
the green disc reads cleanly on top of the orange mark. -->
<circle cx="25" cy="25" r="7" fill="white"/>
<circle cx="25" cy="25" r="6" fill="#0E9F6E"/>
<path d="M22 25 L24 27 L28 23" stroke="white" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
<g transform="translate(0.5 4.5)" fill="black">
<path d="M21.4631 0.523438C17.8173 0.857913 16.0028 2.95675 15.3171 4.01871L4.66406 22.4734H17.5163L30.1929 0.523438H21.4631Z"/>
<path d="M17.5265 22.4737L0 3.88525C0 3.88525 19.8177 -1.44128 21.7493 15.1738L17.5265 22.4737Z"/>
<path d="M14.9236 4.70563L9.54688 14.0208L17.5158 22.4747L21.7385 15.158C21.0696 9.44682 18.2851 6.32784 14.9236 4.69727"/>
</g>
<circle cx="25" cy="25" r="7" fill="white"/>
<circle cx="25" cy="25" r="6" fill="none" stroke="black" stroke-width="1.8" stroke-dasharray="2.5 2.5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 678 B

View File

@@ -0,0 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
<g transform="translate(0.5 4.5) scale(1.0)">
<path d="M21.4631 0.523438C17.8173 0.857913 16.0028 2.95675 15.3171 4.01871L4.66406 22.4734H17.5163L30.1929 0.523438H21.4631Z" fill="#F68330"/>
<path d="M17.5265 22.4737L0 3.88525C0 3.88525 19.8177 -1.44128 21.7493 15.1738L17.5265 22.4737Z" fill="#F68330"/>
<path d="M14.9236 4.70563L9.54688 14.0208L17.5158 22.4747L21.7385 15.158C21.0696 9.44682 18.2851 6.32784 14.9236 4.69727" fill="#F05252"/>
</g>
<circle cx="25" cy="25" r="7" fill="white"/>
<circle cx="25" cy="25" r="6" fill="none" stroke="#F68330" stroke-width="1.8" stroke-dasharray="2.5 2.5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 723 B

View File

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
<g transform="translate(0.5 4.5)" fill="black" opacity="0.5">
<path d="M21.4631 0.523438C17.8173 0.857913 16.0028 2.95675 15.3171 4.01871L4.66406 22.4734H17.5163L30.1929 0.523438H21.4631Z"/>
<path d="M17.5265 22.4737L0 3.88525C0 3.88525 19.8177 -1.44128 21.7493 15.1738L17.5265 22.4737Z"/>
<path d="M14.9236 4.70563L9.54688 14.0208L17.5158 22.4747L21.7385 15.158C21.0696 9.44682 18.2851 6.32784 14.9236 4.69727"/>
</g>
<circle cx="25" cy="25" r="7" fill="white"/>
<circle cx="25" cy="25" r="6" fill="none" stroke="black" stroke-width="1.6"/>
<line x1="21.5" y1="25" x2="28.5" y2="25" stroke="black" stroke-width="1.6" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 745 B

View File

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
<g transform="translate(0.5 4.5) scale(1.0)" opacity="0.45">
<path d="M21.4631 0.523438C17.8173 0.857913 16.0028 2.95675 15.3171 4.01871L4.66406 22.4734H17.5163L30.1929 0.523438H21.4631Z" fill="#F68330"/>
<path d="M17.5265 22.4737L0 3.88525C0 3.88525 19.8177 -1.44128 21.7493 15.1738L17.5265 22.4737Z" fill="#F68330"/>
<path d="M14.9236 4.70563L9.54688 14.0208L17.5158 22.4747L21.7385 15.158C21.0696 9.44682 18.2851 6.32784 14.9236 4.69727" fill="#F05252"/>
</g>
<circle cx="25" cy="25" r="7" fill="white"/>
<circle cx="25" cy="25" r="6" fill="none" stroke="#7c8994" stroke-width="1.6"/>
<line x1="21.5" y1="25" x2="28.5" y2="25" stroke="#7c8994" stroke-width="1.6" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 793 B

View File

@@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
<g transform="translate(0.5 4.5)" fill="black" opacity="0.7">
<path d="M21.4631 0.523438C17.8173 0.857913 16.0028 2.95675 15.3171 4.01871L4.66406 22.4734H17.5163L30.1929 0.523438H21.4631Z"/>
<path d="M17.5265 22.4737L0 3.88525C0 3.88525 19.8177 -1.44128 21.7493 15.1738L17.5265 22.4737Z"/>
<path d="M14.9236 4.70563L9.54688 14.0208L17.5158 22.4747L21.7385 15.158C21.0696 9.44682 18.2851 6.32784 14.9236 4.69727"/>
</g>
<circle cx="25" cy="25" r="7" fill="white"/>
<circle cx="25" cy="25" r="6" fill="black"/>
<line x1="25" y1="21.5" x2="25" y2="26" stroke="white" stroke-width="1.8" stroke-linecap="round"/>
<circle cx="25" cy="28.4" r="1.0" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 761 B

View File

@@ -0,0 +1,11 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
<g transform="translate(0.5 4.5) scale(1.0)" opacity="0.7">
<path d="M21.4631 0.523438C17.8173 0.857913 16.0028 2.95675 15.3171 4.01871L4.66406 22.4734H17.5163L30.1929 0.523438H21.4631Z" fill="#F68330"/>
<path d="M17.5265 22.4737L0 3.88525C0 3.88525 19.8177 -1.44128 21.7493 15.1738L17.5265 22.4737Z" fill="#F68330"/>
<path d="M14.9236 4.70563L9.54688 14.0208L17.5158 22.4747L21.7385 15.158C21.0696 9.44682 18.2851 6.32784 14.9236 4.69727" fill="#F05252"/>
</g>
<circle cx="25" cy="25" r="7" fill="white"/>
<circle cx="25" cy="25" r="6" fill="#E02424"/>
<line x1="25" y1="21.5" x2="25" y2="26" stroke="white" stroke-width="1.8" stroke-linecap="round"/>
<circle cx="25" cy="28.4" r="1.0" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 806 B

View File

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
<g transform="translate(0.5 4.5)" fill="black">
<path d="M21.4631 0.523438C17.8173 0.857913 16.0028 2.95675 15.3171 4.01871L4.66406 22.4734H17.5163L30.1929 0.523438H21.4631Z"/>
<path d="M17.5265 22.4737L0 3.88525C0 3.88525 19.8177 -1.44128 21.7493 15.1738L17.5265 22.4737Z"/>
<path d="M14.9236 4.70563L9.54688 14.0208L17.5158 22.4747L21.7385 15.158C21.0696 9.44682 18.2851 6.32784 14.9236 4.69727"/>
</g>
<circle cx="25" cy="25" r="7" fill="white"/>
<circle cx="25" cy="25" r="6" fill="black"/>
<path d="M25 22 L25 28 M22.5 24.5 L25 22 L27.5 24.5" stroke="white" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 745 B

View File

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
<g transform="translate(0.5 4.5) scale(1.0)">
<path d="M21.4631 0.523438C17.8173 0.857913 16.0028 2.95675 15.3171 4.01871L4.66406 22.4734H17.5163L30.1929 0.523438H21.4631Z" fill="#F68330"/>
<path d="M17.5265 22.4737L0 3.88525C0 3.88525 19.8177 -1.44128 21.7493 15.1738L17.5265 22.4737Z" fill="#F68330"/>
<path d="M14.9236 4.70563L9.54688 14.0208L17.5158 22.4747L21.7385 15.158C21.0696 9.44682 18.2851 6.32784 14.9236 4.69727" fill="#F05252"/>
</g>
<circle cx="25" cy="25" r="7" fill="white"/>
<circle cx="25" cy="25" r="6" fill="#1C64F2"/>
<path d="M25 22 L25 28 M22.5 24.5 L25 22 L27.5 24.5" stroke="white" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 790 B

View File

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
<g transform="translate(0.5 4.5)" fill="black" opacity="0.5">
<path d="M21.4631 0.523438C17.8173 0.857913 16.0028 2.95675 15.3171 4.01871L4.66406 22.4734H17.5163L30.1929 0.523438H21.4631Z"/>
<path d="M17.5265 22.4737L0 3.88525C0 3.88525 19.8177 -1.44128 21.7493 15.1738L17.5265 22.4737Z"/>
<path d="M14.9236 4.70563L9.54688 14.0208L17.5158 22.4747L21.7385 15.158C21.0696 9.44682 18.2851 6.32784 14.9236 4.69727"/>
</g>
<circle cx="25" cy="25" r="7" fill="white"/>
<circle cx="25" cy="25" r="6" fill="black"/>
<path d="M25 22 L25 28 M22.5 24.5 L25 22 L27.5 24.5" stroke="white" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 759 B

View File

@@ -0,0 +1,10 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
<g transform="translate(0.5 4.5) scale(1.0)" opacity="0.45">
<path d="M21.4631 0.523438C17.8173 0.857913 16.0028 2.95675 15.3171 4.01871L4.66406 22.4734H17.5163L30.1929 0.523438H21.4631Z" fill="#F68330"/>
<path d="M17.5265 22.4737L0 3.88525C0 3.88525 19.8177 -1.44128 21.7493 15.1738L17.5265 22.4737Z" fill="#F68330"/>
<path d="M14.9236 4.70563L9.54688 14.0208L17.5158 22.4747L21.7385 15.158C21.0696 9.44682 18.2851 6.32784 14.9236 4.69727" fill="#F05252"/>
</g>
<circle cx="25" cy="25" r="7" fill="white"/>
<circle cx="25" cy="25" r="6" fill="#1C64F2"/>
<path d="M25 22 L25 28 M22.5 24.5 L25 22 L27.5 24.5" stroke="white" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" fill="none"/>
</svg>

After

Width:  |  Height:  |  Size: 805 B

View File

@@ -0,0 +1,295 @@
version: '3'
tasks:
go:mod:tidy:
summary: Runs `go mod tidy`
internal: true
cmds:
- go mod tidy
install:frontend:deps:
summary: Install frontend dependencies
dir: frontend
sources:
- package.json
- pnpm-lock.yaml
generates:
- node_modules
preconditions:
- sh: pnpm --version
msg: "Looks like pnpm isn't installed. Install with: corepack enable && corepack prepare pnpm@latest --activate"
cmds:
- pnpm install
build:frontend:
label: build:frontend (DEV={{.DEV}})
summary: Build the frontend project
dir: frontend
sources:
- "**/*"
- exclude: node_modules/**/*
generates:
- dist/**/*
deps:
- task: install:frontend:deps
- task: generate:bindings
vars:
BUILD_FLAGS:
ref: .BUILD_FLAGS
cmds:
- pnpm run {{.BUILD_COMMAND}}
env:
PRODUCTION: '{{if eq .DEV "true"}}false{{else}}true{{end}}'
vars:
BUILD_COMMAND: '{{if eq .DEV "true"}}build:dev{{else}}build{{end}}'
frontend:vendor:puppertino:
summary: Fetches Puppertino CSS into frontend/public for consistent mobile styling
sources:
- frontend/public/puppertino/puppertino.css
generates:
- frontend/public/puppertino/puppertino.css
cmds:
- |
set -euo pipefail
mkdir -p frontend/public/puppertino
# If bundled Puppertino exists, prefer it. Otherwise, try to fetch, but don't fail build on error.
if [ ! -f frontend/public/puppertino/puppertino.css ]; then
echo "No bundled Puppertino found. Attempting to fetch from GitHub..."
if curl -fsSL https://raw.githubusercontent.com/codedgar/Puppertino/main/dist/css/full.css -o frontend/public/puppertino/puppertino.css; then
curl -fsSL https://raw.githubusercontent.com/codedgar/Puppertino/main/LICENSE -o frontend/public/puppertino/LICENSE || true
echo "Puppertino CSS downloaded to frontend/public/puppertino/puppertino.css"
else
echo "Warning: Could not fetch Puppertino CSS. Proceeding without download since template may bundle it."
fi
else
echo "Using bundled Puppertino at frontend/public/puppertino/puppertino.css"
fi
# Ensure index.html includes Puppertino CSS and button classes
INDEX_HTML=frontend/index.html
if [ -f "$INDEX_HTML" ]; then
if ! grep -q 'href="/puppertino/puppertino.css"' "$INDEX_HTML"; then
# Insert Puppertino link tag after style.css link
awk '
/href="\/style.css"\/?/ && !x { print; print " <link rel=\"stylesheet\" href=\"/puppertino/puppertino.css\"/>"; x=1; next }1
' "$INDEX_HTML" > "$INDEX_HTML.tmp" && mv "$INDEX_HTML.tmp" "$INDEX_HTML"
fi
# Replace default .btn with Puppertino primary button classes if present
sed -E -i'' 's/class=\"btn\"/class=\"p-btn p-prim-col\"/g' "$INDEX_HTML" || true
fi
generate:bindings:
label: generate:bindings (BUILD_FLAGS={{.BUILD_FLAGS}})
summary: Generates bindings for the frontend
deps:
- task: go:mod:tidy
sources:
- "**/*.[jt]s"
- exclude: frontend/**/*
- frontend/bindings/**/* # Rerun when switching between dev/production mode causes changes in output
- "**/*.go"
- go.mod
- go.sum
generates:
- frontend/bindings/**/*
cmds:
- wails3 generate bindings -f '{{.BUILD_FLAGS}}' -clean=true -ts
generate:icons:
summary: Generates Windows `.ico` and Mac `.icns` from an image; on macOS, `-iconcomposerinput appicon.icon -macassetdir darwin` also produces `Assets.car` from a `.icon` file (skipped on other platforms).
dir: build
sources:
- "appicon.png"
- "appicon.icon"
generates:
- "darwin/icons.icns"
- "windows/icon.ico"
cmds:
- wails3 generate icons -input appicon.png -macfilename darwin/icons.icns -windowsfilename windows/icon.ico -iconcomposerinput appicon.icon -macassetdir darwin
generate:tray:icons:
summary: Rebuild Windows multi-res .ico files from the per-state PNGs.
desc: |
The colored tray PNGs (assets/netbird-systemtray-<state>.png) and the
macOS template variants are committed to the repo as the canonical
source. This task only regenerates the Windows multi-resolution .ico
files from those PNGs by downscaling each to 16/24/32/48 px and
packing them with icotool, so Shell_NotifyIcon picks the frame
matching the user's DPI instead of downscaling a single large PNG.
Run after replacing any of the colored PNGs (e.g. when copying a new
version of the icons from client/ui/assets). The SVG sources in
assets/svg/ are kept for reference but are not built by default.
dir: assets
sources:
- "netbird-systemtray-connected.png"
- "netbird-systemtray-disconnected.png"
- "netbird-systemtray-connecting.png"
- "netbird-systemtray-error.png"
- "netbird-systemtray-update-connected.png"
- "netbird-systemtray-update-disconnected.png"
generates:
- "netbird-systemtray-*.ico"
preconditions:
- sh: command -v magick >/dev/null 2>&1 || command -v convert >/dev/null 2>&1
msg: "ImageMagick is required to downscale PNGs (apt install imagemagick)"
- sh: command -v icotool >/dev/null 2>&1
msg: "icotool is required to pack tray .ico files (apt install icoutils)"
cmds:
- |
set -euo pipefail
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
resize=$(command -v magick || echo convert)
for state in connected disconnected connecting error update-connected update-disconnected; do
for sz in 16 24 32 48; do
"$resize" "netbird-systemtray-$state.png" -resize ${sz}x${sz} "$tmp/$state-$sz.png"
done
icotool -c -o "netbird-systemtray-$state.ico" \
"$tmp/$state-16.png" "$tmp/$state-24.png" "$tmp/$state-32.png" "$tmp/$state-48.png"
done
dev:frontend:
summary: Runs the frontend in development mode
dir: frontend
deps:
- task: install:frontend:deps
cmds:
- pnpm exec vite --port {{.VITE_PORT}} --strictPort
update:build-assets:
summary: Updates the build assets
dir: build
cmds:
- wails3 update build-assets -name "{{.APP_NAME}}" -binaryname "{{.APP_NAME}}" -config config.yml -dir .
build:server:
summary: Builds the application in server mode (no GUI, HTTP server only)
desc: |
Builds the application with the server build tag enabled.
Server mode runs as a pure HTTP server without native GUI dependencies.
Usage: task build:server
deps:
- task: build:frontend
vars:
BUILD_FLAGS:
ref: .BUILD_FLAGS
cmds:
- go build -tags server {{.BUILD_FLAGS}} -o {{.BIN_DIR}}/{{.APP_NAME}}-server{{exeExt}}
vars:
BUILD_FLAGS: "{{.BUILD_FLAGS}}"
run:server:
summary: Builds and runs the application in server mode
deps:
- task: build:server
cmds:
- ./{{.BIN_DIR}}/{{.APP_NAME}}-server{{exeExt}}
build:docker:
summary: Builds a Docker image for server mode deployment
desc: |
Creates a minimal Docker image containing the server mode binary.
The image is based on distroless for security and small size.
Usage: task build:docker [TAG=myapp:latest]
cmds:
- docker build -t {{.TAG | default (printf "%s:latest" .APP_NAME)}} -f build/docker/Dockerfile.server .
vars:
TAG: "{{.TAG}}"
preconditions:
- sh: docker info > /dev/null 2>&1
msg: "Docker is required. Please install Docker first."
- sh: test -f build/docker/Dockerfile.server
msg: "Dockerfile.server not found. Run 'wails3 update build-assets' to generate it."
run:docker:
summary: Builds and runs the Docker image
desc: |
Builds the Docker image and runs it, exposing port 8080.
Usage: task run:docker [TAG=myapp:latest] [PORT=8080]
Note: The internal container port is always 8080. The PORT variable
only changes the host port mapping. Ensure your app uses port 8080
or modify the Dockerfile to match your ServerOptions.Port setting.
deps:
- task: build:docker
vars:
TAG:
ref: .TAG
cmds:
- docker run --rm -p {{.PORT | default "8080"}}:8080 {{.TAG | default (printf "%s:latest" .APP_NAME)}}
vars:
TAG: "{{.TAG}}"
PORT: "{{.PORT}}"
setup:docker:
summary: Builds Docker image for cross-compilation (~800MB download)
desc: |
Builds the Docker image needed for cross-compiling to any platform.
Run this once to enable cross-platform builds from any OS.
cmds:
- docker build -t wails-cross -f build/docker/Dockerfile.cross build/docker/
preconditions:
- sh: docker info > /dev/null 2>&1
msg: "Docker is required. Please install Docker first."
ios:device:list:
summary: Lists connected iOS devices (UDIDs)
cmds:
- xcrun xcdevice list
ios:run:device:
summary: Build, install, and launch on a physical iPhone using Apple tools (xcodebuild/devicectl)
vars:
PROJECT: '{{.PROJECT}}' # e.g., build/ios/xcode/<YourProject>.xcodeproj
SCHEME: '{{.SCHEME}}' # e.g., ios.dev
CONFIG: '{{.CONFIG | default "Debug"}}'
DERIVED: '{{.DERIVED | default "build/ios/DerivedData"}}'
UDID: '{{.UDID}}' # from `task ios:device:list`
BUNDLE_ID: '{{.BUNDLE_ID}}' # e.g., com.yourco.wails.ios.dev
TEAM_ID: '{{.TEAM_ID}}' # optional, if your project is not already set up for signing
preconditions:
- sh: xcrun -f xcodebuild
msg: "xcodebuild not found. Please install Xcode."
- sh: xcrun -f devicectl
msg: "devicectl not found. Please update to Xcode 15+ (which includes devicectl)."
- sh: test -n '{{.PROJECT}}'
msg: "Set PROJECT to your .xcodeproj path (e.g., PROJECT=build/ios/xcode/App.xcodeproj)."
- sh: test -n '{{.SCHEME}}'
msg: "Set SCHEME to your app scheme (e.g., SCHEME=ios.dev)."
- sh: test -n '{{.UDID}}'
msg: "Set UDID to your device UDID (see: task ios:device:list)."
- sh: test -n '{{.BUNDLE_ID}}'
msg: "Set BUNDLE_ID to your app's bundle identifier (e.g., com.yourco.wails.ios.dev)."
cmds:
- |
set -euo pipefail
echo "Building for device: UDID={{.UDID}} SCHEME={{.SCHEME}} PROJECT={{.PROJECT}}"
XCB_ARGS=(
-project "{{.PROJECT}}"
-scheme "{{.SCHEME}}"
-configuration "{{.CONFIG}}"
-destination "id={{.UDID}}"
-derivedDataPath "{{.DERIVED}}"
-allowProvisioningUpdates
-allowProvisioningDeviceRegistration
)
# Optionally inject signing identifiers if provided
if [ -n '{{.TEAM_ID}}' ]; then XCB_ARGS+=(DEVELOPMENT_TEAM={{.TEAM_ID}}); fi
if [ -n '{{.BUNDLE_ID}}' ]; then XCB_ARGS+=(PRODUCT_BUNDLE_IDENTIFIER={{.BUNDLE_ID}}); fi
xcodebuild "${XCB_ARGS[@]}" build | xcpretty || true
# If xcpretty isn't installed, run without it
if [ "${PIPESTATUS[0]}" -ne 0 ]; then
xcodebuild "${XCB_ARGS[@]}" build
fi
# Find built .app
APP_PATH=$(find "{{.DERIVED}}/Build/Products" -type d -name "*.app" -maxdepth 3 | head -n 1)
if [ -z "$APP_PATH" ]; then
echo "Could not locate built .app under {{.DERIVED}}/Build/Products" >&2
exit 1
fi
echo "Installing: $APP_PATH"
xcrun devicectl device install app --device "{{.UDID}}" "$APP_PATH"
echo "Launching: {{.BUNDLE_ID}}"
xcrun devicectl device process launch --device "{{.UDID}}" --stderr console --stdout console "{{.BUNDLE_ID}}"

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!--
macOS Icon Composer source. icon.json references this SVG by name and
applies its own scale/translation/fill, so we leave the artwork in its
native 31×23 viewBox.
-->
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="0 0 31 23">
<path d="M21.4631 0.523438C17.8173 0.857913 16.0028 2.95675 15.3171 4.01871L4.66406 22.4734H17.5163L30.1929 0.523438H21.4631Z" fill="#F68330"/>
<path d="M17.5265 22.4737L0 3.88525C0 3.88525 19.8177 -1.44128 21.7493 15.1738L17.5265 22.4737Z" fill="#F68330"/>
<path d="M14.9236 4.70563L9.54688 14.0208L17.5158 22.4747L21.7385 15.158C21.0696 9.44682 18.2851 6.32784 14.9236 4.69727" fill="#F05252"/>
</svg>

After

Width:  |  Height:  |  Size: 732 B

View File

@@ -0,0 +1,51 @@
{
"fill" : {
"automatic-gradient" : "extended-gray:1.00000,1.00000"
},
"groups" : [
{
"layers" : [
{
"fill-specializations" : [
{
"appearance" : "dark",
"value" : {
"solid" : "srgb:0.92143,0.92145,0.92144,1.00000"
}
},
{
"appearance" : "tinted",
"value" : {
"solid" : "srgb:0.83742,0.83744,0.83743,1.00000"
}
}
],
"image-name" : "wails_icon_vector.svg",
"name" : "wails_icon_vector",
"position" : {
"scale" : 1.25,
"translation-in-points" : [
36.890625,
4.96875
]
}
}
],
"shadow" : {
"kind" : "neutral",
"opacity" : 0.5
},
"specular" : true,
"translucency" : {
"enabled" : true,
"value" : 0.5
}
}
],
"supported-platforms" : {
"circles" : [
"watchOS"
],
"squares" : "shared"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@@ -0,0 +1,78 @@
# This file contains the configuration for this project.
# When you update `info` or `fileAssociations`, run `wails3 task common:update:build-assets` to update the assets.
# Note that this will overwrite any changes you have made to the assets.
version: '3'
# This information is used to generate the build assets.
info:
companyName: "My Company" # The name of the company
productName: "My Product" # The name of the application
productIdentifier: "com.mycompany.myproduct" # The unique product identifier
description: "A program that does X" # The application description
copyright: "(c) 2025, My Company" # Copyright text
comments: "Some Product Comments" # Comments
version: "0.0.1" # The application version
# cfBundleIconName: "appicon" # The macOS icon name in Assets.car icon bundles (optional)
# # Should match the name of your .icon file without the extension
# # If not set and Assets.car exists, defaults to "appicon"
# iOS build configuration (uncomment to customise iOS project generation)
# Note: Keys under `ios` OVERRIDE values under `info` when set.
# ios:
# # The iOS bundle identifier used in the generated Xcode project (CFBundleIdentifier)
# bundleID: "com.mycompany.myproduct"
# # The display name shown under the app icon (CFBundleDisplayName/CFBundleName)
# displayName: "My Product"
# # The app version to embed in Info.plist (CFBundleShortVersionString/CFBundleVersion)
# version: "0.0.1"
# # The company/organisation name for templates and project settings
# company: "My Company"
# # Additional comments to embed in Info.plist metadata
# comments: "Some Product Comments"
# Dev mode configuration
dev_mode:
root_path: .
log_level: warn
debounce: 1000
ignore:
dir:
- .git
- node_modules
- frontend
- bin
file:
- .DS_Store
- .gitignore
- .gitkeep
watched_extension:
- "*.go"
- "*.js" # Watch for changes to JS/TS files included using the //wails:include directive.
- "*.ts" # The frontend directory will be excluded entirely by the setting above.
git_ignore: true
executes:
- cmd: wails3 build DEV=true
type: blocking
- cmd: wails3 task common:dev:frontend
type: background
- cmd: wails3 task run
type: primary
# File Associations
# More information at: https://v3.wails.io/noit/done/yet
fileAssociations:
# - ext: wails
# name: Wails
# description: Wails Application File
# iconName: wailsFileIcon
# role: Editor
# - ext: jpg
# name: JPEG
# description: Image File
# iconName: jpegFileIcon
# role: Editor
# mimeType: image/jpeg # (optional)
# Other data
other:
- name: My Other Data

View File

@@ -0,0 +1,34 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleName</key>
<string>NetBird</string>
<key>CFBundleExecutable</key>
<string>netbird-ui</string>
<key>CFBundleIdentifier</key>
<string>io.netbird.client</string>
<key>CFBundleVersion</key>
<string>0.0.1</string>
<key>CFBundleGetInfoString</key>
<string>This is a comment</string>
<key>CFBundleShortVersionString</key>
<string>0.0.1</string>
<key>CFBundleIconFile</key>
<string>icons</string>
<key>CFBundleIconName</key>
<string>appicon</string>
<key>LSMinimumSystemVersion</key>
<string>10.15.0</string>
<key>NSHighResolutionCapable</key>
<string>true</string>
<key>NSHumanReadableCopyright</key>
<string>© 2026, My Company</string>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
</dict>
</plist>

View File

@@ -0,0 +1,29 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleName</key>
<string>NetBird</string>
<key>CFBundleExecutable</key>
<string>netbird-ui</string>
<key>CFBundleIdentifier</key>
<string>io.netbird.client</string>
<key>CFBundleVersion</key>
<string>0.0.1</string>
<key>CFBundleGetInfoString</key>
<string>This is a comment</string>
<key>CFBundleShortVersionString</key>
<string>0.0.1</string>
<key>CFBundleIconFile</key>
<string>icons</string>
<key>CFBundleIconName</key>
<string>appicon</string>
<key>LSMinimumSystemVersion</key>
<string>10.15.0</string>
<key>NSHighResolutionCapable</key>
<string>true</string>
<key>NSHumanReadableCopyright</key>
<string>© 2026, My Company</string>
</dict>
</plist>

View File

@@ -0,0 +1,208 @@
version: '3'
includes:
common: ../Taskfile.yml
vars:
# Signing configuration - edit these values for your project
# SIGN_IDENTITY: "Developer ID Application: Your Company (TEAMID)"
# KEYCHAIN_PROFILE: "my-notarize-profile"
# ENTITLEMENTS: "build/darwin/entitlements.plist"
# Docker image for cross-compilation (used when building on non-macOS)
CROSS_IMAGE: wails-cross
tasks:
build:
summary: Builds the application
cmds:
- task: '{{if eq OS "darwin"}}build:native{{else}}build:docker{{end}}'
vars:
ARCH: '{{.ARCH}}'
DEV: '{{.DEV}}'
OUTPUT: '{{.OUTPUT}}'
EXTRA_TAGS: '{{.EXTRA_TAGS}}'
vars:
DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}'
OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}'
build:native:
summary: Builds the application natively on macOS
internal: true
deps:
- task: common:go:mod:tidy
- task: common:build:frontend
vars:
BUILD_FLAGS:
ref: .BUILD_FLAGS
DEV:
ref: .DEV
- task: common:generate:icons
cmds:
- go build {{.BUILD_FLAGS}} -o {{.OUTPUT}}
vars:
BUILD_FLAGS: '{{if eq .DEV "true"}}{{if .EXTRA_TAGS}}-tags {{.EXTRA_TAGS}} {{end}}-buildvcs=false -gcflags=all="-l"{{else}}-tags production{{if .EXTRA_TAGS}},{{.EXTRA_TAGS}}{{end}} -trimpath -buildvcs=false -ldflags="-w -s"{{end}}'
DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}'
OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}'
env:
GOOS: darwin
CGO_ENABLED: 1
GOARCH: '{{.ARCH | default ARCH}}'
CGO_CFLAGS: "-mmacosx-version-min=10.15"
CGO_LDFLAGS: "-mmacosx-version-min=10.15"
MACOSX_DEPLOYMENT_TARGET: "10.15"
build:docker:
summary: Cross-compiles for macOS using Docker (for Linux/Windows hosts)
internal: true
deps:
- task: common:build:frontend
- task: common:generate:icons
preconditions:
- sh: docker info > /dev/null 2>&1
msg: "Docker is required for cross-compilation. Please install Docker."
- sh: docker image inspect {{.CROSS_IMAGE}} > /dev/null 2>&1
msg: |
Docker image '{{.CROSS_IMAGE}}' not found.
Build it first: wails3 task setup:docker
cmds:
- docker run --rm -v "{{.ROOT_DIR}}:/app" {{.GO_CACHE_MOUNT}} {{.REPLACE_MOUNTS}} -e APP_NAME="{{.APP_NAME}}" {{if .EXTRA_TAGS}}-e EXTRA_TAGS="{{.EXTRA_TAGS}}"{{end}} {{.CROSS_IMAGE}} darwin {{.DOCKER_ARCH}}
- docker run --rm -v "{{.ROOT_DIR}}:/app" alpine chown -R $(id -u):$(id -g) /app/bin
- mkdir -p {{.BIN_DIR}}
- mv "bin/{{.APP_NAME}}-darwin-{{.DOCKER_ARCH}}" "{{.OUTPUT}}"
vars:
DOCKER_ARCH: '{{if eq .ARCH "arm64"}}arm64{{else if eq .ARCH "amd64"}}amd64{{else}}arm64{{end}}'
DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}'
OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}'
# Mount Go module cache for faster builds
GO_CACHE_MOUNT:
sh: 'echo "-v ${GOPATH:-$HOME/go}/pkg/mod:/go/pkg/mod"'
# Extract replace directives from go.mod and create -v mounts for each
# Handles both relative (=> ../) and absolute (=> /) paths
REPLACE_MOUNTS:
sh: |
grep -E '^replace .* => ' go.mod 2>/dev/null | while read -r line; do
path=$(echo "$line" | sed -E 's/^replace .* => //' | tr -d '\r')
# Convert relative paths to absolute
if [ "${path#/}" = "$path" ]; then
path="$(cd "$(dirname "$path")" 2>/dev/null && pwd)/$(basename "$path")"
fi
# Only mount if directory exists
if [ -d "$path" ]; then
echo "-v $path:$path:ro"
fi
done | tr '\n' ' '
build:universal:
summary: Builds darwin universal binary (arm64 + amd64)
deps:
- task: build
vars:
ARCH: amd64
OUTPUT: "{{.BIN_DIR}}/{{.APP_NAME}}-amd64"
- task: build
vars:
ARCH: arm64
OUTPUT: "{{.BIN_DIR}}/{{.APP_NAME}}-arm64"
cmds:
- task: '{{if eq OS "darwin"}}build:universal:lipo:native{{else}}build:universal:lipo:go{{end}}'
build:universal:lipo:native:
summary: Creates universal binary using native lipo (macOS)
internal: true
cmds:
- lipo -create -output "{{.BIN_DIR}}/{{.APP_NAME}}" "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" "{{.BIN_DIR}}/{{.APP_NAME}}-arm64"
- rm "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" "{{.BIN_DIR}}/{{.APP_NAME}}-arm64"
build:universal:lipo:go:
summary: Creates universal binary using wails3 tool lipo (Linux/Windows)
internal: true
cmds:
- wails3 tool lipo -output "{{.BIN_DIR}}/{{.APP_NAME}}" -input "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" -input "{{.BIN_DIR}}/{{.APP_NAME}}-arm64"
- rm -f "{{.BIN_DIR}}/{{.APP_NAME}}-amd64" "{{.BIN_DIR}}/{{.APP_NAME}}-arm64"
package:
summary: Packages the application into a `.app` bundle
deps:
- task: build
cmds:
- task: create:app:bundle
package:universal:
summary: Packages darwin universal binary (arm64 + amd64)
deps:
- task: build:universal
cmds:
- task: create:app:bundle
create:app:bundle:
summary: Creates an `.app` bundle
cmds:
- mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/MacOS"
- mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources"
- cp build/darwin/icons.icns "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources"
- |
if [ -f build/darwin/Assets.car ]; then
cp build/darwin/Assets.car "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/Resources"
fi
- cp "{{.BIN_DIR}}/{{.APP_NAME}}" "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents/MacOS"
- cp build/darwin/Info.plist "{{.BIN_DIR}}/{{.APP_NAME}}.app/Contents"
- task: '{{if eq OS "darwin"}}codesign:adhoc{{else}}codesign:skip{{end}}'
codesign:adhoc:
summary: Ad-hoc signs the app bundle (macOS only)
internal: true
cmds:
- codesign --force --deep --sign - "{{.BIN_DIR}}/{{.APP_NAME}}.app"
codesign:skip:
summary: Skips codesigning when cross-compiling
internal: true
cmds:
- 'echo "Skipping codesign (not available on {{OS}}). Sign the .app on macOS before distribution."'
run:
cmds:
- mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/MacOS"
- mkdir -p "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Resources"
- cp build/darwin/icons.icns "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Resources"
- |
if [ -f build/darwin/Assets.car ]; then
cp build/darwin/Assets.car "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Resources"
fi
- cp "{{.BIN_DIR}}/{{.APP_NAME}}" "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/MacOS"
- cp "build/darwin/Info.dev.plist" "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/Info.plist"
- codesign --force --deep --sign - "{{.BIN_DIR}}/{{.APP_NAME}}.dev.app"
- '{{.BIN_DIR}}/{{.APP_NAME}}.dev.app/Contents/MacOS/{{.APP_NAME}}'
sign:
summary: Signs the application bundle with Developer ID
desc: |
Signs the .app bundle for distribution.
Configure SIGN_IDENTITY in the vars section at the top of this file.
deps:
- task: package
cmds:
- wails3 tool sign --input "{{.BIN_DIR}}/{{.APP_NAME}}.app" --identity "{{.SIGN_IDENTITY}}" {{if .ENTITLEMENTS}}--entitlements {{.ENTITLEMENTS}}{{end}}
preconditions:
- sh: '[ -n "{{.SIGN_IDENTITY}}" ]'
msg: "SIGN_IDENTITY is required. Set it in the vars section at the top of build/darwin/Taskfile.yml"
sign:notarize:
summary: Signs and notarizes the application bundle
desc: |
Signs the .app bundle and submits it for notarization.
Configure SIGN_IDENTITY and KEYCHAIN_PROFILE in the vars section at the top of this file.
Setup (one-time):
wails3 signing credentials --apple-id "you@email.com" --team-id "TEAMID" --password "app-specific-password" --profile "my-profile"
deps:
- task: package
cmds:
- wails3 tool sign --input "{{.BIN_DIR}}/{{.APP_NAME}}.app" --identity "{{.SIGN_IDENTITY}}" {{if .ENTITLEMENTS}}--entitlements {{.ENTITLEMENTS}}{{end}} --notarize --keychain-profile {{.KEYCHAIN_PROFILE}}
preconditions:
- sh: '[ -n "{{.SIGN_IDENTITY}}" ]'
msg: "SIGN_IDENTITY is required. Set it in the vars section at the top of build/darwin/Taskfile.yml"
- sh: '[ -n "{{.KEYCHAIN_PROFILE}}" ]'
msg: "KEYCHAIN_PROFILE is required. Set it in the vars section at the top of build/darwin/Taskfile.yml"

Binary file not shown.

View File

@@ -0,0 +1,203 @@
# Cross-compile Wails v3 apps to any platform
#
# Darwin: Zig + macOS SDK
# Linux: Native GCC when host matches target, Zig for cross-arch
# Windows: Zig + bundled mingw
#
# Usage:
# docker build -t wails-cross -f Dockerfile.cross .
# docker run --rm -v $(pwd):/app wails-cross darwin arm64
# docker run --rm -v $(pwd):/app wails-cross darwin amd64
# docker run --rm -v $(pwd):/app wails-cross linux amd64
# docker run --rm -v $(pwd):/app wails-cross linux arm64
# docker run --rm -v $(pwd):/app wails-cross windows amd64
# docker run --rm -v $(pwd):/app wails-cross windows arm64
FROM golang:1.25-bookworm
ARG TARGETARCH
# Install base tools, GCC, and GTK/WebKit dev packages
RUN apt-get update && apt-get install -y --no-install-recommends \
curl xz-utils nodejs npm pkg-config gcc libc6-dev \
libgtk-3-dev libwebkit2gtk-4.1-dev \
libgtk-4-dev libwebkitgtk-6.0-dev \
&& rm -rf /var/lib/apt/lists/*
# Install Zig - automatically selects correct binary for host architecture
ARG ZIG_VERSION=0.14.0
RUN ZIG_ARCH=$(case "${TARGETARCH}" in arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \
curl -L "https://ziglang.org/download/${ZIG_VERSION}/zig-linux-${ZIG_ARCH}-${ZIG_VERSION}.tar.xz" \
| tar -xJ -C /opt \
&& ln -s /opt/zig-linux-${ZIG_ARCH}-${ZIG_VERSION}/zig /usr/local/bin/zig
# Download macOS SDK (required for darwin targets)
ARG MACOS_SDK_VERSION=14.5
RUN curl -L "https://github.com/joseluisq/macosx-sdks/releases/download/${MACOS_SDK_VERSION}/MacOSX${MACOS_SDK_VERSION}.sdk.tar.xz" \
| tar -xJ -C /opt \
&& mv /opt/MacOSX${MACOS_SDK_VERSION}.sdk /opt/macos-sdk
ENV MACOS_SDK_PATH=/opt/macos-sdk
# Create Zig CC wrappers for cross-compilation targets
# Darwin and Windows use Zig; Linux uses native GCC (run with --platform for cross-arch)
# Darwin arm64
COPY <<'ZIGWRAP' /usr/local/bin/zcc-darwin-arm64
#!/bin/sh
ARGS=""
SKIP_NEXT=0
for arg in "$@"; do
if [ $SKIP_NEXT -eq 1 ]; then
SKIP_NEXT=0
continue
fi
case "$arg" in
-target) SKIP_NEXT=1 ;;
-mmacosx-version-min=*) ;;
*) ARGS="$ARGS $arg" ;;
esac
done
exec zig cc -fno-sanitize=all -target aarch64-macos-none -isysroot /opt/macos-sdk -I/opt/macos-sdk/usr/include -L/opt/macos-sdk/usr/lib -F/opt/macos-sdk/System/Library/Frameworks -w $ARGS
ZIGWRAP
RUN chmod +x /usr/local/bin/zcc-darwin-arm64
# Darwin amd64
COPY <<'ZIGWRAP' /usr/local/bin/zcc-darwin-amd64
#!/bin/sh
ARGS=""
SKIP_NEXT=0
for arg in "$@"; do
if [ $SKIP_NEXT -eq 1 ]; then
SKIP_NEXT=0
continue
fi
case "$arg" in
-target) SKIP_NEXT=1 ;;
-mmacosx-version-min=*) ;;
*) ARGS="$ARGS $arg" ;;
esac
done
exec zig cc -fno-sanitize=all -target x86_64-macos-none -isysroot /opt/macos-sdk -I/opt/macos-sdk/usr/include -L/opt/macos-sdk/usr/lib -F/opt/macos-sdk/System/Library/Frameworks -w $ARGS
ZIGWRAP
RUN chmod +x /usr/local/bin/zcc-darwin-amd64
# Windows amd64 - uses Zig's bundled mingw
COPY <<'ZIGWRAP' /usr/local/bin/zcc-windows-amd64
#!/bin/sh
ARGS=""
SKIP_NEXT=0
for arg in "$@"; do
if [ $SKIP_NEXT -eq 1 ]; then
SKIP_NEXT=0
continue
fi
case "$arg" in
-target) SKIP_NEXT=1 ;;
-Wl,*) ;;
*) ARGS="$ARGS $arg" ;;
esac
done
exec zig cc -target x86_64-windows-gnu $ARGS
ZIGWRAP
RUN chmod +x /usr/local/bin/zcc-windows-amd64
# Windows arm64 - uses Zig's bundled mingw
COPY <<'ZIGWRAP' /usr/local/bin/zcc-windows-arm64
#!/bin/sh
ARGS=""
SKIP_NEXT=0
for arg in "$@"; do
if [ $SKIP_NEXT -eq 1 ]; then
SKIP_NEXT=0
continue
fi
case "$arg" in
-target) SKIP_NEXT=1 ;;
-Wl,*) ;;
*) ARGS="$ARGS $arg" ;;
esac
done
exec zig cc -target aarch64-windows-gnu $ARGS
ZIGWRAP
RUN chmod +x /usr/local/bin/zcc-windows-arm64
# Build script
COPY <<'SCRIPT' /usr/local/bin/build.sh
#!/bin/sh
set -e
OS=${1:-darwin}
ARCH=${2:-arm64}
case "${OS}-${ARCH}" in
darwin-arm64|darwin-aarch64)
export CC=zcc-darwin-arm64
export GOARCH=arm64
export GOOS=darwin
;;
darwin-amd64|darwin-x86_64)
export CC=zcc-darwin-amd64
export GOARCH=amd64
export GOOS=darwin
;;
linux-arm64|linux-aarch64)
export CC=gcc
export GOARCH=arm64
export GOOS=linux
;;
linux-amd64|linux-x86_64)
export CC=gcc
export GOARCH=amd64
export GOOS=linux
;;
windows-arm64|windows-aarch64)
export CC=zcc-windows-arm64
export GOARCH=arm64
export GOOS=windows
;;
windows-amd64|windows-x86_64)
export CC=zcc-windows-amd64
export GOARCH=amd64
export GOOS=windows
;;
*)
echo "Usage: <os> <arch>"
echo " os: darwin, linux, windows"
echo " arch: amd64, arm64"
exit 1
;;
esac
export CGO_ENABLED=1
export CGO_CFLAGS="-w"
# Build frontend if exists and not already built (host may have built it)
if [ -d "frontend" ] && [ -f "frontend/package.json" ] && [ ! -d "frontend/dist" ]; then
(cd frontend && npm install --silent && npm run build --silent)
fi
# Build
APP=${APP_NAME:-$(basename $(pwd))}
mkdir -p bin
EXT=""
LDFLAGS="-s -w"
if [ "$GOOS" = "windows" ]; then
EXT=".exe"
LDFLAGS="-s -w -H windowsgui"
fi
TAGS="production"
if [ -n "$EXTRA_TAGS" ]; then
TAGS="${TAGS},${EXTRA_TAGS}"
fi
go build -tags "$TAGS" -trimpath -buildvcs=false -ldflags="$LDFLAGS" -o bin/${APP}-${GOOS}-${GOARCH}${EXT} .
echo "Built: bin/${APP}-${GOOS}-${GOARCH}${EXT}"
SCRIPT
RUN chmod +x /usr/local/bin/build.sh
WORKDIR /app
ENTRYPOINT ["/usr/local/bin/build.sh"]
CMD ["darwin", "arm64"]

View File

@@ -0,0 +1,41 @@
# Wails Server Mode Dockerfile
# Multi-stage build for minimal image size
# Build stage
FROM golang:alpine AS builder
WORKDIR /app
# Install build dependencies
RUN apk add --no-cache git
# Copy source code
COPY . .
# Remove local replace directive if present (for production builds)
RUN sed -i '/^replace/d' go.mod || true
# Download dependencies
RUN go mod tidy
# Build the server binary
RUN go build -tags server -ldflags="-s -w" -o server .
# Runtime stage - minimal image
FROM gcr.io/distroless/static-debian12
# Copy the binary
COPY --from=builder /app/server /server
# Copy frontend assets
COPY --from=builder /app/frontend/dist /frontend/dist
# Expose the default port
EXPOSE 8080
# Bind to all interfaces (required for Docker)
# Can be overridden at runtime with -e WAILS_SERVER_HOST=...
ENV WAILS_SERVER_HOST=0.0.0.0
# Run the server
ENTRYPOINT ["/server"]

View File

@@ -0,0 +1,235 @@
version: '3'
includes:
common: ../Taskfile.yml
vars:
# Signing configuration - edit these values for your project
# PGP_KEY: "path/to/signing-key.asc"
# SIGN_ROLE: "builder" # Options: origin, maint, archive, builder
#
# Password is stored securely in system keychain. Run: wails3 setup signing
# Docker image for cross-compilation (used when building on non-Linux or no CC available)
CROSS_IMAGE: wails-cross
tasks:
build:
summary: Builds the application for Linux
cmds:
# Linux requires CGO - use Docker when:
# 1. Cross-compiling from non-Linux, OR
# 2. No C compiler is available, OR
# 3. Target architecture differs from host architecture (cross-arch compilation)
- task: '{{if and (eq OS "linux") (eq .HAS_CC "true") (eq .TARGET_ARCH ARCH)}}build:native{{else}}build:docker{{end}}'
vars:
ARCH: '{{.ARCH}}'
DEV: '{{.DEV}}'
OUTPUT: '{{.OUTPUT}}'
EXTRA_TAGS: '{{.EXTRA_TAGS}}'
vars:
DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}'
OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}'
# Determine target architecture (defaults to host ARCH if not specified)
TARGET_ARCH: '{{.ARCH | default ARCH}}'
# Check if a C compiler is available (gcc or clang)
HAS_CC:
sh: '(command -v gcc >/dev/null 2>&1 || command -v clang >/dev/null 2>&1) && echo "true" || echo "false"'
build:native:
summary: Builds the application natively on Linux
internal: true
deps:
- task: common:go:mod:tidy
- task: common:build:frontend
vars:
BUILD_FLAGS:
ref: .BUILD_FLAGS
DEV:
ref: .DEV
- task: common:generate:icons
- task: generate:dotdesktop
cmds:
- go build {{.BUILD_FLAGS}} -o {{.OUTPUT}}
vars:
BUILD_FLAGS: '{{if eq .DEV "true"}}{{if .EXTRA_TAGS}}-tags {{.EXTRA_TAGS}} {{end}}-buildvcs=false -gcflags=all="-l"{{else}}-tags production{{if .EXTRA_TAGS}},{{.EXTRA_TAGS}}{{end}} -trimpath -buildvcs=false -ldflags="-w -s"{{end}}'
DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}'
OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}'
env:
GOOS: linux
CGO_ENABLED: 1
GOARCH: '{{.ARCH | default ARCH}}'
build:docker:
summary: Builds for Linux using Docker (for non-Linux hosts or when no C compiler available)
internal: true
deps:
- task: common:build:frontend
- task: common:generate:icons
- task: generate:dotdesktop
preconditions:
- sh: docker info > /dev/null 2>&1
msg: "Docker is required for cross-compilation to Linux. Please install Docker."
- sh: docker image inspect {{.CROSS_IMAGE}} > /dev/null 2>&1
msg: |
Docker image '{{.CROSS_IMAGE}}' not found.
Build it first: wails3 task setup:docker
cmds:
- docker run --rm -v "{{.ROOT_DIR}}:/app" {{.GO_CACHE_MOUNT}} {{.REPLACE_MOUNTS}} -e APP_NAME="{{.APP_NAME}}" {{if .EXTRA_TAGS}}-e EXTRA_TAGS="{{.EXTRA_TAGS}}"{{end}} "{{.CROSS_IMAGE}}" linux {{.DOCKER_ARCH}}
- docker run --rm -v "{{.ROOT_DIR}}:/app" alpine chown -R $(id -u):$(id -g) /app/bin
- mkdir -p {{.BIN_DIR}}
- mv "bin/{{.APP_NAME}}-linux-{{.DOCKER_ARCH}}" "{{.OUTPUT}}"
vars:
DOCKER_ARCH: '{{.ARCH | default "amd64"}}'
DEFAULT_OUTPUT: '{{.BIN_DIR}}/{{.APP_NAME}}'
OUTPUT: '{{ .OUTPUT | default .DEFAULT_OUTPUT }}'
# Mount Go module cache for faster builds
GO_CACHE_MOUNT:
sh: 'echo "-v ${GOPATH:-$HOME/go}/pkg/mod:/go/pkg/mod"'
# Extract replace directives from go.mod and create -v mounts for each
REPLACE_MOUNTS:
sh: |
grep -E '^replace .* => ' go.mod 2>/dev/null | while read -r line; do
path=$(echo "$line" | sed -E 's/^replace .* => //' | tr -d '\r')
# Convert relative paths to absolute
if [ "${path#/}" = "$path" ]; then
path="$(cd "$(dirname "$path")" 2>/dev/null && pwd)/$(basename "$path")"
fi
# Only mount if directory exists
if [ -d "$path" ]; then
echo "-v $path:$path:ro"
fi
done | tr '\n' ' '
package:
summary: Packages the application for Linux
deps:
- task: build
cmds:
- task: create:appimage
- task: create:deb
- task: create:rpm
- task: create:aur
create:appimage:
summary: Creates an AppImage
dir: build/linux/appimage
deps:
- task: build
- task: generate:dotdesktop
cmds:
- cp "{{.APP_BINARY}}" "{{.APP_NAME}}"
- cp ../../appicon.png "{{.APP_NAME}}.png"
- wails3 generate appimage -binary "{{.APP_NAME}}" -icon {{.ICON}} -desktopfile {{.DESKTOP_FILE}} -outputdir {{.OUTPUT_DIR}} -builddir {{.ROOT_DIR}}/build/linux/appimage/build
vars:
APP_NAME: '{{.APP_NAME}}'
APP_BINARY: '../../../bin/{{.APP_NAME}}'
ICON: '{{.APP_NAME}}.png'
DESKTOP_FILE: '../{{.APP_NAME}}.desktop'
OUTPUT_DIR: '../../../bin'
create:deb:
summary: Creates a deb package
deps:
- task: build
cmds:
- task: generate:dotdesktop
- task: generate:deb
create:rpm:
summary: Creates a rpm package
deps:
- task: build
cmds:
- task: generate:dotdesktop
- task: generate:rpm
create:aur:
summary: Creates a arch linux packager package
deps:
- task: build
cmds:
- task: generate:dotdesktop
- task: generate:aur
generate:deb:
summary: Creates a deb package
cmds:
- wails3 tool package -name "{{.APP_NAME}}" -format deb -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin
generate:rpm:
summary: Creates a rpm package
cmds:
- wails3 tool package -name "{{.APP_NAME}}" -format rpm -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin
generate:aur:
summary: Creates a arch linux packager package
cmds:
- wails3 tool package -name "{{.APP_NAME}}" -format archlinux -config ./build/linux/nfpm/nfpm.yaml -out {{.ROOT_DIR}}/bin
generate:dotdesktop:
summary: Generates a `.desktop` file
dir: build
cmds:
- mkdir -p {{.ROOT_DIR}}/build/linux/appimage
- wails3 generate .desktop -name "{{.APP_NAME}}" -exec "{{.EXEC}}" -icon "{{.ICON}}" -outputfile "{{.ROOT_DIR}}/build/linux/{{.APP_NAME}}.desktop" -categories "{{.CATEGORIES}}"
# Wrap Exec= with `env WEBKIT_DISABLE_DMABUF_RENDERER=1 ...` so launches
# from any desktop environment use the working renderer. See build/linux/Taskfile.yml :run for the matching dev-mode env block.
- sed -i -E 's|^Exec=([^ ]+)(.*)$|Exec=env WEBKIT_DISABLE_DMABUF_RENDERER=1 \1\2|' {{.ROOT_DIR}}/build/linux/{{.APP_NAME}}.desktop
vars:
APP_NAME: '{{.APP_NAME}}'
EXEC: '{{.APP_NAME}}'
ICON: '{{.APP_NAME}}'
CATEGORIES: 'Development;'
OUTPUTFILE: '{{.ROOT_DIR}}/build/linux/{{.APP_NAME}}.desktop'
run:
cmds:
- '{{.BIN_DIR}}/{{.APP_NAME}}'
env:
# WebKitGTK 2.50's default DMA-BUF renderer fails on RDP, VirtualBox/QEMU,
# and some bare WMs (Fluxbox, dwm) where DRM dumb-buffer access is
# restricted. Disabling it falls back to the GLES2/cairo path which works
# everywhere. Production launchers must set this too.
WEBKIT_DISABLE_DMABUF_RENDERER: "1"
sign:deb:
summary: Signs the DEB package
desc: |
Signs the .deb package with a PGP key.
Configure PGP_KEY in the vars section at the top of this file.
Password is retrieved from system keychain (run: wails3 setup signing)
deps:
- task: create:deb
cmds:
- wails3 tool sign --input "{{.BIN_DIR}}/{{.APP_NAME}}*.deb" --pgp-key {{.PGP_KEY}} {{if .SIGN_ROLE}}--role {{.SIGN_ROLE}}{{end}}
preconditions:
- sh: '[ -n "{{.PGP_KEY}}" ]'
msg: "PGP_KEY is required. Set it in the vars section at the top of build/linux/Taskfile.yml"
sign:rpm:
summary: Signs the RPM package
desc: |
Signs the .rpm package with a PGP key.
Configure PGP_KEY in the vars section at the top of this file.
Password is retrieved from system keychain (run: wails3 setup signing)
deps:
- task: create:rpm
cmds:
- wails3 tool sign --input "{{.BIN_DIR}}/{{.APP_NAME}}*.rpm" --pgp-key {{.PGP_KEY}}
preconditions:
- sh: '[ -n "{{.PGP_KEY}}" ]'
msg: "PGP_KEY is required. Set it in the vars section at the top of build/linux/Taskfile.yml"
sign:packages:
summary: Signs all Linux packages (DEB and RPM)
desc: |
Signs both .deb and .rpm packages with a PGP key.
Configure PGP_KEY in the vars section at the top of this file.
Password is retrieved from system keychain (run: wails3 setup signing)
cmds:
- task: sign:deb
- task: sign:rpm
preconditions:
- sh: '[ -n "{{.PGP_KEY}}" ]'
msg: "PGP_KEY is required. Set it in the vars section at the top of build/linux/Taskfile.yml"

View File

@@ -0,0 +1,35 @@
#!/usr/bin/env bash
# Copyright (c) 2018-Present Lea Anthony
# SPDX-License-Identifier: MIT
# Fail script on any error
set -euxo pipefail
# Define variables
APP_DIR="${APP_NAME}.AppDir"
# Create AppDir structure
mkdir -p "${APP_DIR}/usr/bin"
cp -r "${APP_BINARY}" "${APP_DIR}/usr/bin/"
cp "${ICON_PATH}" "${APP_DIR}/"
cp "${DESKTOP_FILE}" "${APP_DIR}/"
if [[ $(uname -m) == *x86_64* ]]; then
# Download linuxdeploy and make it executable
wget -q -4 -N https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage
chmod +x linuxdeploy-x86_64.AppImage
# Run linuxdeploy to bundle the application
./linuxdeploy-x86_64.AppImage --appdir "${APP_DIR}" --output appimage
else
# Download linuxdeploy and make it executable (arm64)
wget -q -4 -N https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-aarch64.AppImage
chmod +x linuxdeploy-aarch64.AppImage
# Run linuxdeploy to bundle the application (arm64)
./linuxdeploy-aarch64.AppImage --appdir "${APP_DIR}" --output appimage
fi
# Rename the generated AppImage
mv "${APP_NAME}*.AppImage" "${APP_NAME}.AppImage"

View File

@@ -0,0 +1,13 @@
[Desktop Entry]
Version=1.0
Name=NetBird
Comment=NetBird desktop client
# The Exec line includes %u to pass the URL to the application
Exec=/usr/local/bin/netbird-ui %u
Terminal=false
Type=Application
Icon=netbird-ui
Categories=Utility;
StartupWMClass=netbird-ui

View File

@@ -0,0 +1,10 @@
[Desktop Entry]
Type=Application
Name=netbird-ui
Exec=netbird-ui
Icon=netbird-ui
Categories=Development;
Terminal=false
Keywords=wails
Version=1.0
StartupNotify=false

View File

@@ -0,0 +1,67 @@
# Feel free to remove those if you don't want/need to use them.
# Make sure to check the documentation at https://nfpm.goreleaser.com
#
# The lines below are called `modelines`. See `:help modeline`
name: "netbird-ui"
arch: ${GOARCH}
platform: "linux"
version: "0.0.1"
section: "default"
priority: "extra"
maintainer: ${GIT_COMMITTER_NAME} <${GIT_COMMITTER_EMAIL}>
description: "NetBird desktop client"
vendor: "NetBird"
homepage: "https://wails.io"
license: "MIT"
release: "1"
contents:
- src: "./bin/netbird-ui"
dst: "/usr/local/bin/netbird-ui"
- src: "./build/appicon.png"
dst: "/usr/share/icons/hicolor/128x128/apps/netbird-ui.png"
- src: "./build/linux/netbird-ui.desktop"
dst: "/usr/share/applications/netbird-ui.desktop"
# Default dependencies for Debian 12/Ubuntu 22.04+ with WebKit 4.1
depends:
- libgtk-3-0
- libwebkit2gtk-4.1-0
# Distribution-specific overrides for different package formats and WebKit versions
overrides:
# RPM packages for RHEL/CentOS/AlmaLinux/Rocky Linux (WebKit 4.0)
rpm:
depends:
- gtk3
- webkit2gtk4.1
# Arch Linux packages (WebKit 4.1)
archlinux:
depends:
- gtk3
- webkit2gtk-4.1
# scripts section to ensure desktop database is updated after install
scripts:
postinstall: "./build/linux/nfpm/scripts/postinstall.sh"
# You can also add preremove, postremove if needed
# preremove: "./build/linux/nfpm/scripts/preremove.sh"
# postremove: "./build/linux/nfpm/scripts/postremove.sh"
# replaces:
# - foobar
# provides:
# - bar
# depends:
# - gtk3
# - libwebkit2gtk
# recommends:
# - whatever
# suggests:
# - something-else
# conflicts:
# - not-foo
# - not-bar
# changelog: "changelog.yaml"

View File

@@ -0,0 +1,21 @@
#!/bin/sh
# Update desktop database for .desktop file changes
# This makes the application appear in application menus and registers its capabilities.
if command -v update-desktop-database >/dev/null 2>&1; then
echo "Updating desktop database..."
update-desktop-database -q /usr/share/applications
else
echo "Warning: update-desktop-database command not found. Desktop file may not be immediately recognized." >&2
fi
# Update MIME database for custom URL schemes (x-scheme-handler)
# This ensures the system knows how to handle your custom protocols.
if command -v update-mime-database >/dev/null 2>&1; then
echo "Updating MIME database..."
update-mime-database -n /usr/share/mime
else
echo "Warning: update-mime-database command not found. Custom URL schemes may not be immediately recognized." >&2
fi
exit 0

View File

@@ -0,0 +1 @@
#!/bin/bash

View File

@@ -0,0 +1 @@
#!/bin/bash

View File

@@ -0,0 +1 @@
#!/bin/bash

View File

@@ -0,0 +1,236 @@
version: '3'
includes:
common: ../Taskfile.yml
vars:
# Signing configuration - edit these values for your project
# SIGN_CERTIFICATE: "path/to/certificate.pfx"
# SIGN_THUMBPRINT: "certificate-thumbprint" # Alternative to SIGN_CERTIFICATE
# TIMESTAMP_SERVER: "http://timestamp.digicert.com"
#
# Password is stored securely in system keychain. Run: wails3 setup signing
# Docker image for cross-compilation with CGO (used when CGO_ENABLED=1 on non-Windows)
CROSS_IMAGE: wails-cross
tasks:
build:
summary: Builds the application for Windows
cmds:
# CGO Windows builds from Linux use mingw-w64 (lighter than docker).
# Docker is only needed if mingw-w64 is unavailable.
- task: build:native
vars:
ARCH: '{{.ARCH}}'
DEV: '{{.DEV}}'
EXTRA_TAGS: '{{.EXTRA_TAGS}}'
vars:
CGO_ENABLED: '{{.CGO_ENABLED | default "0"}}'
build:console:
summary: Builds a console-attached Windows binary so logs go to the terminal.
desc: |
Same as `windows:build` but links against the console PE subsystem
instead of windowsgui, so stdout/stderr (logrus, panics) print to the
terminal that launched the .exe. Useful for chasing tray, event-stream,
or daemon-RPC bugs that have no other feedback channel on Windows.
Output is bin/netbird-ui-console.exe — kept distinct so the production
binary built by `windows:build` isn't shadowed.
Cross-compile from Linux works the same way:
CGO_ENABLED=1 task windows:build:console
deps:
- task: common:go:mod:tidy
- task: common:build:frontend
vars:
BUILD_FLAGS:
ref: .BUILD_FLAGS
DEV:
ref: .DEV
- task: common:generate:icons
preconditions:
- sh: '[ "{{OS}}" = "windows" ] || [ "{{.CGO_ENABLED}}" != "1" ] || command -v {{.CC}}'
msg: "{{.CC}} not found. Install with: sudo apt-get install gcc-mingw-w64-x86-64 (Debian/Ubuntu) / sudo dnf install mingw64-gcc (Fedora)"
cmds:
- task: generate:syso
- go build {{.BUILD_FLAGS}} -o "{{.BIN_DIR}}/{{.APP_NAME}}-console.exe"
- cmd: powershell Remove-item *.syso
platforms: [windows]
- cmd: rm -f *.syso
platforms: [linux, darwin]
vars:
# Identical to build:native's flags except no -H windowsgui, so the
# binary attaches to the launching console.
BUILD_FLAGS: '-tags production{{if .EXTRA_TAGS}},{{.EXTRA_TAGS}}{{end}} -trimpath -buildvcs=false -ldflags="-w -s"'
CGO_ENABLED: '{{.CGO_ENABLED | default "0"}}'
CC: '{{.CC | default "x86_64-w64-mingw32-gcc"}}'
env:
GOOS: windows
CGO_ENABLED: '{{.CGO_ENABLED}}'
GOARCH: '{{.ARCH | default ARCH}}'
CC: '{{.CC}}'
build:native:
summary: Builds for Windows natively, or cross-compiles from Linux/macOS via mingw-w64.
internal: true
deps:
- task: common:go:mod:tidy
- task: common:build:frontend
vars:
BUILD_FLAGS:
ref: .BUILD_FLAGS
DEV:
ref: .DEV
- task: common:generate:icons
preconditions:
# When cross-compiling with CGO from a non-Windows host, the mingw-w64
# cross-gcc must be present. Native Windows builds skip this check.
- sh: '[ "{{OS}}" = "windows" ] || [ "{{.CGO_ENABLED}}" != "1" ] || command -v {{.CC}}'
msg: "{{.CC}} not found. Install with: sudo apt-get install gcc-mingw-w64-x86-64 (Debian/Ubuntu) / sudo dnf install mingw64-gcc (Fedora)"
cmds:
- task: generate:syso
- go build {{.BUILD_FLAGS}} -o "{{.BIN_DIR}}/{{.APP_NAME}}.exe"
- cmd: powershell Remove-item *.syso
platforms: [windows]
- cmd: rm -f *.syso
platforms: [linux, darwin]
vars:
BUILD_FLAGS: '{{if eq .DEV "true"}}{{if .EXTRA_TAGS}}-tags {{.EXTRA_TAGS}} {{end}}-buildvcs=false -gcflags=all="-l"{{else}}-tags production{{if .EXTRA_TAGS}},{{.EXTRA_TAGS}}{{end}} -trimpath -buildvcs=false -ldflags="-w -s -H windowsgui"{{end}}'
CGO_ENABLED: '{{.CGO_ENABLED | default "0"}}'
CC: '{{.CC | default "x86_64-w64-mingw32-gcc"}}'
env:
GOOS: windows
CGO_ENABLED: '{{.CGO_ENABLED}}'
GOARCH: '{{.ARCH | default ARCH}}'
CC: '{{.CC}}'
build:docker:
summary: Cross-compiles for Windows using Docker with Zig (for CGO builds on non-Windows)
internal: true
deps:
- task: common:build:frontend
- task: common:generate:icons
preconditions:
- sh: docker info > /dev/null 2>&1
msg: "Docker is required for CGO cross-compilation. Please install Docker."
- sh: docker image inspect {{.CROSS_IMAGE}} > /dev/null 2>&1
msg: |
Docker image '{{.CROSS_IMAGE}}' not found.
Build it first: wails3 task setup:docker
cmds:
- task: generate:syso
- docker run --rm -v "{{.ROOT_DIR}}:/app" {{.GO_CACHE_MOUNT}} {{.REPLACE_MOUNTS}} -e APP_NAME="{{.APP_NAME}}" {{if .EXTRA_TAGS}}-e EXTRA_TAGS="{{.EXTRA_TAGS}}"{{end}} {{.CROSS_IMAGE}} windows {{.DOCKER_ARCH}}
- docker run --rm -v "{{.ROOT_DIR}}:/app" alpine chown -R $(id -u):$(id -g) /app/bin
- rm -f *.syso
vars:
DOCKER_ARCH: '{{.ARCH | default "amd64"}}'
# Mount Go module cache for faster builds
GO_CACHE_MOUNT:
sh: 'echo "-v ${GOPATH:-$HOME/go}/pkg/mod:/go/pkg/mod"'
# Extract replace directives from go.mod and create -v mounts for each
REPLACE_MOUNTS:
sh: |
grep -E '^replace .* => ' go.mod 2>/dev/null | while read -r line; do
path=$(echo "$line" | sed -E 's/^replace .* => //' | tr -d '\r')
# Convert relative paths to absolute
if [ "${path#/}" = "$path" ]; then
path="$(cd "$(dirname "$path")" 2>/dev/null && pwd)/$(basename "$path")"
fi
# Only mount if directory exists
if [ -d "$path" ]; then
echo "-v $path:$path:ro"
fi
done | tr '\n' ' '
package:
summary: Packages the application
cmds:
- task: '{{if eq (.FORMAT | default "nsis") "msix"}}create:msix:package{{else}}create:nsis:installer{{end}}'
vars:
FORMAT: '{{.FORMAT | default "nsis"}}'
generate:syso:
summary: Generates Windows `.syso` file
dir: build
cmds:
- wails3 generate syso -arch {{.ARCH}} -icon windows/icon.ico -manifest windows/wails.exe.manifest -info windows/info.json -out ../wails_windows_{{.ARCH}}.syso
vars:
ARCH: '{{.ARCH | default ARCH}}'
create:nsis:installer:
summary: Creates an NSIS installer
dir: build/windows/nsis
deps:
- task: build
cmds:
# Create the Microsoft WebView2 bootstrapper if it doesn't exist
- wails3 generate webview2bootstrapper -dir "{{.ROOT_DIR}}/build/windows/nsis"
- |
{{if eq OS "windows"}}
makensis -DARG_WAILS_{{.ARG_FLAG}}_BINARY="{{.ROOT_DIR}}\{{.BIN_DIR}}\{{.APP_NAME}}.exe" project.nsi
{{else}}
makensis -DARG_WAILS_{{.ARG_FLAG}}_BINARY="{{.ROOT_DIR}}/{{.BIN_DIR}}/{{.APP_NAME}}.exe" project.nsi
{{end}}
vars:
ARCH: '{{.ARCH | default ARCH}}'
ARG_FLAG: '{{if eq .ARCH "amd64"}}AMD64{{else}}ARM64{{end}}'
create:msix:package:
summary: Creates an MSIX package
deps:
- task: build
cmds:
- |-
wails3 tool msix \
--config "{{.ROOT_DIR}}/wails.json" \
--name "{{.APP_NAME}}" \
--executable "{{.ROOT_DIR}}/{{.BIN_DIR}}/{{.APP_NAME}}.exe" \
--arch "{{.ARCH}}" \
--out "{{.ROOT_DIR}}/{{.BIN_DIR}}/{{.APP_NAME}}-{{.ARCH}}.msix" \
{{if .CERT_PATH}}--cert "{{.CERT_PATH}}"{{end}} \
{{if .PUBLISHER}}--publisher "{{.PUBLISHER}}"{{end}} \
{{if .USE_MSIX_TOOL}}--use-msix-tool{{else}}--use-makeappx{{end}}
vars:
ARCH: '{{.ARCH | default ARCH}}'
CERT_PATH: '{{.CERT_PATH | default ""}}'
PUBLISHER: '{{.PUBLISHER | default ""}}'
USE_MSIX_TOOL: '{{.USE_MSIX_TOOL | default "false"}}'
install:msix:tools:
summary: Installs tools required for MSIX packaging
cmds:
- wails3 tool msix-install-tools
run:
cmds:
- '{{.BIN_DIR}}/{{.APP_NAME}}.exe'
sign:
summary: Signs the Windows executable
desc: |
Signs the .exe with an Authenticode certificate.
Configure SIGN_CERTIFICATE or SIGN_THUMBPRINT in the vars section at the top of this file.
Password is retrieved from system keychain (run: wails3 setup signing)
deps:
- task: build
cmds:
- wails3 tool sign --input "{{.BIN_DIR}}/{{.APP_NAME}}.exe" {{if .SIGN_CERTIFICATE}}--certificate {{.SIGN_CERTIFICATE}}{{end}} {{if .SIGN_THUMBPRINT}}--thumbprint {{.SIGN_THUMBPRINT}}{{end}} {{if .TIMESTAMP_SERVER}}--timestamp {{.TIMESTAMP_SERVER}}{{end}}
preconditions:
- sh: '[ -n "{{.SIGN_CERTIFICATE}}" ] || [ -n "{{.SIGN_THUMBPRINT}}" ]'
msg: "Either SIGN_CERTIFICATE or SIGN_THUMBPRINT is required. Set it in the vars section at the top of build/windows/Taskfile.yml"
sign:installer:
summary: Signs the NSIS installer
desc: |
Creates and signs the NSIS installer.
Configure SIGN_CERTIFICATE or SIGN_THUMBPRINT in the vars section at the top of this file.
Password is retrieved from system keychain (run: wails3 setup signing)
deps:
- task: create:nsis:installer
cmds:
- wails3 tool sign --input "build/windows/nsis/{{.APP_NAME}}-installer.exe" {{if .SIGN_CERTIFICATE}}--certificate {{.SIGN_CERTIFICATE}}{{end}} {{if .SIGN_THUMBPRINT}}--thumbprint {{.SIGN_THUMBPRINT}}{{end}} {{if .TIMESTAMP_SERVER}}--timestamp {{.TIMESTAMP_SERVER}}{{end}}
preconditions:
- sh: '[ -n "{{.SIGN_CERTIFICATE}}" ] || [ -n "{{.SIGN_THUMBPRINT}}" ]'
msg: "Either SIGN_CERTIFICATE or SIGN_THUMBPRINT is required. Set it in the vars section at the top of build/windows/Taskfile.yml"

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -0,0 +1,15 @@
{
"fixed": {
"file_version": "0.0.1"
},
"info": {
"0000": {
"ProductVersion": "0.0.1",
"CompanyName": "NetBird",
"FileDescription": "NetBird desktop client",
"LegalCopyright": "© 2026, My Company",
"ProductName": "NetBird",
"Comments": "This is a comment"
}
}
}

View File

@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<Package
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:uap3="http://schemas.microsoft.com/appx/manifest/uap/windows10/3"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
xmlns:desktop="http://schemas.microsoft.com/appx/manifest/desktop/windows10"
IgnorableNamespaces="uap3">
<Identity
Name="io.netbird.client"
Publisher="CN=NetBird"
Version="0.0.1.0"
ProcessorArchitecture="x64" />
<Properties>
<DisplayName>NetBird</DisplayName>
<PublisherDisplayName>NetBird</PublisherDisplayName>
<Description>NetBird desktop client</Description>
<Logo>Assets\StoreLogo.png</Logo>
</Properties>
<Dependencies>
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
</Dependencies>
<Resources>
<Resource Language="en-us" />
</Resources>
<Applications>
<Application Id="io.netbird.client" Executable="netbird-ui" EntryPoint="Windows.FullTrustApplication">
<uap:VisualElements
DisplayName="NetBird"
Description="NetBird desktop client"
BackgroundColor="transparent"
Square150x150Logo="Assets\Square150x150Logo.png"
Square44x44Logo="Assets\Square44x44Logo.png">
<uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png" />
<uap:SplashScreen Image="Assets\SplashScreen.png" />
</uap:VisualElements>
<Extensions>
<desktop:Extension Category="windows.fullTrustProcess" Executable="netbird-ui" />
</Extensions>
</Application>
</Applications>
<Capabilities>
<rescap:Capability Name="runFullTrust" />
</Capabilities>
</Package>

View File

@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<MsixPackagingToolTemplate
xmlns="http://schemas.microsoft.com/msix/packaging/msixpackagingtool/template/2022">
<Settings
AllowTelemetry="false"
ApplyACLsToPackageFiles="true"
GenerateCommandLineFile="true"
AllowPromptForPassword="false">
</Settings>
<Installer
Path="netbird-ui"
Arguments=""
InstallLocation="C:\Program Files\NetBird\NetBird">
</Installer>
<PackageInformation
PackageName="NetBird"
PackageDisplayName="NetBird"
PublisherName="CN=NetBird"
PublisherDisplayName="NetBird"
Version="0.0.1.0"
PackageDescription="NetBird desktop client">
<Capabilities>
<Capability Name="runFullTrust" />
</Capabilities>
<Applications>
<Application
Id="io.netbird.client"
Description="NetBird desktop client"
DisplayName="NetBird"
ExecutableName="netbird-ui"
EntryPoint="Windows.FullTrustApplication">
</Application>
</Applications>
<Resources>
<Resource Language="en-us" />
</Resources>
<Dependencies>
<TargetDeviceFamily Name="Windows.Desktop" MinVersion="10.0.17763.0" MaxVersionTested="10.0.19041.0" />
</Dependencies>
<Properties>
<Framework>false</Framework>
<DisplayName>NetBird</DisplayName>
<PublisherDisplayName>NetBird</PublisherDisplayName>
<Description>NetBird desktop client</Description>
<Logo>Assets\AppIcon.png</Logo>
</Properties>
</PackageInformation>
<SaveLocation PackagePath="netbird-ui.msix" />
<PackageIntegrity>
<CertificatePath></CertificatePath>
</PackageIntegrity>
</MsixPackagingToolTemplate>

View File

@@ -0,0 +1,114 @@
Unicode true
####
## Please note: Template replacements don't work in this file. They are provided with default defines like
## mentioned underneath.
## If the keyword is not defined, "wails_tools.nsh" will populate them.
## If they are defined here, "wails_tools.nsh" will not touch them. This allows you to use this project.nsi manually
## from outside of Wails for debugging and development of the installer.
##
## For development first make a wails nsis build to populate the "wails_tools.nsh":
## > wails build --target windows/amd64 --nsis
## Then you can call makensis on this file with specifying the path to your binary:
## For a AMD64 only installer:
## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app.exe
## For a ARM64 only installer:
## > makensis -DARG_WAILS_ARM64_BINARY=..\..\bin\app.exe
## For a installer with both architectures:
## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app-amd64.exe -DARG_WAILS_ARM64_BINARY=..\..\bin\app-arm64.exe
####
## The following information is taken from the wails_tools.nsh file, but they can be overwritten here.
####
## !define INFO_PROJECTNAME "my-project" # Default "netbird-ui"
## !define INFO_COMPANYNAME "My Company" # Default "NetBird"
## !define INFO_PRODUCTNAME "My Product Name" # Default "NetBird"
## !define INFO_PRODUCTVERSION "1.0.0" # Default "0.0.1"
## !define INFO_COPYRIGHT "(c) Now, My Company" # Default "© 2026, My Company"
###
## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe"
## !define UNINST_KEY_NAME "UninstKeyInRegistry" # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
####
## !define REQUEST_EXECUTION_LEVEL "admin" # Default "admin" see also https://nsis.sourceforge.io/Docs/Chapter4.html
####
## Include the wails tools
####
!include "wails_tools.nsh"
# The version information for this two must consist of 4 parts
VIProductVersion "${INFO_PRODUCTVERSION}.0"
VIFileVersion "${INFO_PRODUCTVERSION}.0"
VIAddVersionKey "CompanyName" "${INFO_COMPANYNAME}"
VIAddVersionKey "FileDescription" "${INFO_PRODUCTNAME} Installer"
VIAddVersionKey "ProductVersion" "${INFO_PRODUCTVERSION}"
VIAddVersionKey "FileVersion" "${INFO_PRODUCTVERSION}"
VIAddVersionKey "LegalCopyright" "${INFO_COPYRIGHT}"
VIAddVersionKey "ProductName" "${INFO_PRODUCTNAME}"
# Enable HiDPI support. https://nsis.sourceforge.io/Reference/ManifestDPIAware
ManifestDPIAware true
!include "MUI.nsh"
!define MUI_ICON "..\icon.ico"
!define MUI_UNICON "..\icon.ico"
# !define MUI_WELCOMEFINISHPAGE_BITMAP "resources\leftimage.bmp" #Include this to add a bitmap on the left side of the Welcome Page. Must be a size of 164x314
!define MUI_FINISHPAGE_NOAUTOCLOSE # Wait on the INSTFILES page so the user can take a look into the details of the installation steps
!define MUI_ABORTWARNING # This will warn the user if they exit from the installer.
!insertmacro MUI_PAGE_WELCOME # Welcome to the installer page.
# !insertmacro MUI_PAGE_LICENSE "resources\eula.txt" # Adds a EULA page to the installer
!insertmacro MUI_PAGE_DIRECTORY # In which folder install page.
!insertmacro MUI_PAGE_INSTFILES # Installing page.
!insertmacro MUI_PAGE_FINISH # Finished installation page.
!insertmacro MUI_UNPAGE_INSTFILES # Uninstalling page
!insertmacro MUI_LANGUAGE "English" # Set the Language of the installer
## The following two statements can be used to sign the installer and the uninstaller. The path to the binaries are provided in %1
#!uninstfinalize 'signtool --file "%1"'
#!finalize 'signtool --file "%1"'
Name "${INFO_PRODUCTNAME}"
OutFile "..\..\..\bin\${INFO_PROJECTNAME}-${ARCH}-installer.exe" # Name of the installer's file.
InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}" # Default installing folder ($PROGRAMFILES is Program Files folder).
ShowInstDetails show # This will always show the installation details.
Function .onInit
!insertmacro wails.checkArchitecture
FunctionEnd
Section
!insertmacro wails.setShellContext
!insertmacro wails.webview2runtime
SetOutPath $INSTDIR
!insertmacro wails.files
CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
!insertmacro wails.associateFiles
!insertmacro wails.associateCustomProtocols
!insertmacro wails.writeUninstaller
SectionEnd
Section "uninstall"
!insertmacro wails.setShellContext
RMDir /r "$AppData\${PRODUCT_EXECUTABLE}" # Remove the WebView2 DataPath
RMDir /r $INSTDIR
Delete "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk"
Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk"
!insertmacro wails.unassociateFiles
!insertmacro wails.unassociateCustomProtocols
!insertmacro wails.deleteUninstaller
SectionEnd

View File

@@ -0,0 +1,236 @@
# DO NOT EDIT - Generated automatically by `wails build`
!include "x64.nsh"
!include "WinVer.nsh"
!include "FileFunc.nsh"
!ifndef INFO_PROJECTNAME
!define INFO_PROJECTNAME "netbird-ui"
!endif
!ifndef INFO_COMPANYNAME
!define INFO_COMPANYNAME "NetBird"
!endif
!ifndef INFO_PRODUCTNAME
!define INFO_PRODUCTNAME "NetBird"
!endif
!ifndef INFO_PRODUCTVERSION
!define INFO_PRODUCTVERSION "0.0.1"
!endif
!ifndef INFO_COPYRIGHT
!define INFO_COPYRIGHT "© 2026, My Company"
!endif
!ifndef PRODUCT_EXECUTABLE
!define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe"
!endif
!ifndef UNINST_KEY_NAME
!define UNINST_KEY_NAME "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
!endif
!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINST_KEY_NAME}"
!ifndef REQUEST_EXECUTION_LEVEL
!define REQUEST_EXECUTION_LEVEL "admin"
!endif
RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}"
!ifdef ARG_WAILS_AMD64_BINARY
!define SUPPORTS_AMD64
!endif
!ifdef ARG_WAILS_ARM64_BINARY
!define SUPPORTS_ARM64
!endif
!ifdef SUPPORTS_AMD64
!ifdef SUPPORTS_ARM64
!define ARCH "amd64_arm64"
!else
!define ARCH "amd64"
!endif
!else
!ifdef SUPPORTS_ARM64
!define ARCH "arm64"
!else
!error "Wails: Undefined ARCH, please provide at least one of ARG_WAILS_AMD64_BINARY or ARG_WAILS_ARM64_BINARY"
!endif
!endif
!macro wails.checkArchitecture
!ifndef WAILS_WIN10_REQUIRED
!define WAILS_WIN10_REQUIRED "This product is only supported on Windows 10 (Server 2016) and later."
!endif
!ifndef WAILS_ARCHITECTURE_NOT_SUPPORTED
!define WAILS_ARCHITECTURE_NOT_SUPPORTED "This product can't be installed on the current Windows architecture. Supports: ${ARCH}"
!endif
${If} ${AtLeastWin10}
!ifdef SUPPORTS_AMD64
${if} ${IsNativeAMD64}
Goto ok
${EndIf}
!endif
!ifdef SUPPORTS_ARM64
${if} ${IsNativeARM64}
Goto ok
${EndIf}
!endif
IfSilent silentArch notSilentArch
silentArch:
SetErrorLevel 65
Abort
notSilentArch:
MessageBox MB_OK "${WAILS_ARCHITECTURE_NOT_SUPPORTED}"
Quit
${else}
IfSilent silentWin notSilentWin
silentWin:
SetErrorLevel 64
Abort
notSilentWin:
MessageBox MB_OK "${WAILS_WIN10_REQUIRED}"
Quit
${EndIf}
ok:
!macroend
!macro wails.files
!ifdef SUPPORTS_AMD64
${if} ${IsNativeAMD64}
File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_AMD64_BINARY}"
${EndIf}
!endif
!ifdef SUPPORTS_ARM64
${if} ${IsNativeARM64}
File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_ARM64_BINARY}"
${EndIf}
!endif
!macroend
!macro wails.writeUninstaller
WriteUninstaller "$INSTDIR\uninstall.exe"
SetRegView 64
WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}"
WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}"
WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}"
WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}"
WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\""
WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S"
${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2
IntFmt $0 "0x%08X" $0
WriteRegDWORD HKLM "${UNINST_KEY}" "EstimatedSize" "$0"
!macroend
!macro wails.deleteUninstaller
Delete "$INSTDIR\uninstall.exe"
SetRegView 64
DeleteRegKey HKLM "${UNINST_KEY}"
!macroend
!macro wails.setShellContext
${If} ${REQUEST_EXECUTION_LEVEL} == "admin"
SetShellVarContext all
${else}
SetShellVarContext current
${EndIf}
!macroend
# Install webview2 by launching the bootstrapper
# See https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution#online-only-deployment
!macro wails.webview2runtime
!ifndef WAILS_INSTALL_WEBVIEW_DETAILPRINT
!define WAILS_INSTALL_WEBVIEW_DETAILPRINT "Installing: WebView2 Runtime"
!endif
SetRegView 64
# If the admin key exists and is not empty then webview2 is already installed
ReadRegStr $0 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
${If} $0 != ""
Goto ok
${EndIf}
${If} ${REQUEST_EXECUTION_LEVEL} == "user"
# If the installer is run in user level, check the user specific key exists and is not empty then webview2 is already installed
ReadRegStr $0 HKCU "Software\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
${If} $0 != ""
Goto ok
${EndIf}
${EndIf}
SetDetailsPrint both
DetailPrint "${WAILS_INSTALL_WEBVIEW_DETAILPRINT}"
SetDetailsPrint listonly
InitPluginsDir
CreateDirectory "$pluginsdir\webview2bootstrapper"
SetOutPath "$pluginsdir\webview2bootstrapper"
File "MicrosoftEdgeWebview2Setup.exe"
ExecWait '"$pluginsdir\webview2bootstrapper\MicrosoftEdgeWebview2Setup.exe" /silent /install'
SetDetailsPrint both
ok:
!macroend
# Copy of APP_ASSOCIATE and APP_UNASSOCIATE macros from here https://gist.github.com/nikku/281d0ef126dbc215dd58bfd5b3a5cd5b
!macro APP_ASSOCIATE EXT FILECLASS DESCRIPTION ICON COMMANDTEXT COMMAND
; Backup the previously associated file class
ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" ""
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0"
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}"
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}`
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}`
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" "open"
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open" "" `${COMMANDTEXT}`
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open\command" "" `${COMMAND}`
!macroend
!macro APP_UNASSOCIATE EXT FILECLASS
; Backup the previously associated file class
ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" `${FILECLASS}_backup`
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "$R0"
DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}`
!macroend
!macro wails.associateFiles
; Create file associations
!macroend
!macro wails.unassociateFiles
; Delete app associations
!macroend
!macro CUSTOM_PROTOCOL_ASSOCIATE PROTOCOL DESCRIPTION ICON COMMAND
DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}"
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "" "${DESCRIPTION}"
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "URL Protocol" ""
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\DefaultIcon" "" "${ICON}"
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell" "" ""
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open" "" ""
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open\command" "" "${COMMAND}"
!macroend
!macro CUSTOM_PROTOCOL_UNASSOCIATE PROTOCOL
DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}"
!macroend
!macro wails.associateCustomProtocols
; Create custom protocols associations
!macroend
!macro wails.unassociateCustomProtocols
; Delete app custom protocol associations
!macroend

View File

@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
<assemblyIdentity type="win32" name="io.netbird.client" version="0.0.1" processorArchitecture="*"/>
<dependency>
<dependentAssembly>
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
</dependentAssembly>
</dependency>
<asmv3:application>
<asmv3:windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware> <!-- fallback for Windows 7 and 8 -->
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">permonitorv2,permonitor</dpiAwareness> <!-- falls back to per-monitor if per-monitor v2 is not supported -->
</asmv3:windowsSettings>
</asmv3:application>
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
<security>
<requestedPrivileges>
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
</requestedPrivileges>
</security>
</trustInfo>
</assembly>

View File

@@ -0,0 +1,93 @@
Copyright 2020 The Inter Project Authors (https://github.com/rsms/inter)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>NetBird</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

View File

@@ -0,0 +1,33 @@
{
"name": "netbird-ui",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite",
"build:dev": "tsc && vite build --minify false --mode development",
"build": "tsc && vite build --mode production",
"preview": "vite preview",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@wailsio/runtime": "latest",
"clsx": "^2.1.1",
"lucide-react": "^0.469.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^7.1.3",
"tailwind-merge": "^2.6.0"
},
"devDependencies": {
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.20",
"postcss": "^8.5.1",
"tailwindcss": "^3.4.17",
"tailwindcss-animate": "^1.0.7",
"typescript": "^5.7.3",
"vite": "^6.0.7"
}
}

1758
client/ui-wails/frontend/pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

Binary file not shown.

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

View File

@@ -0,0 +1,157 @@
:root {
font-family: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
"Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
font-size: 16px;
line-height: 24px;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: rgba(27, 38, 54, 1);
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
}
@font-face {
font-family: "Inter";
font-style: normal;
font-weight: 400;
src: local(""),
url("./Inter-Medium.ttf") format("truetype");
}
h3 {
font-size: 3em;
line-height: 1.1;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
button {
width: 60px;
height: 30px;
line-height: 30px;
border-radius: 3px;
border: none;
margin: 0 0 0 20px;
padding: 0 8px;
cursor: pointer;
}
.result {
height: 20px;
line-height: 20px;
}
body {
margin: 0;
display: flex;
place-items: center;
place-content: center;
min-width: 320px;
min-height: 100vh;
}
.container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
#app {
max-width: 1280px;
margin: 0 auto;
padding: 2rem;
text-align: center;
}
.logo {
height: 6em;
padding: 1.5em;
will-change: filter;
}
.logo:hover {
filter: drop-shadow(0 0 2em #e80000aa);
}
.logo.react:hover {
filter: drop-shadow(0 0 2em #61dafbaa);
}
.result {
height: 20px;
line-height: 20px;
margin: 1.5rem auto;
text-align: center;
}
.footer {
margin-top: 1rem;
align-content: center;
text-align: center;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}
.input-box .btn:hover {
background-image: linear-gradient(to top, #cfd9df 0%, #e2ebf0 100%);
color: #333333;
}
.input-box .input {
border: none;
border-radius: 3px;
outline: none;
height: 30px;
line-height: 30px;
padding: 0 10px;
color: black;
background-color: rgba(240, 240, 240, 1);
-webkit-font-smoothing: antialiased;
}
.input-box .input:hover {
border: none;
background-color: rgba(255, 255, 255, 1);
}
.input-box .input:focus {
border: none;
background-color: rgba(255, 255, 255, 1);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

View File

@@ -0,0 +1,32 @@
import { HashRouter, Navigate, Route, Routes } from "react-router-dom";
import Layout from "./Layout";
import Status from "./pages/Status";
import Settings from "./pages/Settings";
import Networks from "./pages/Networks";
import Peers from "./pages/Peers";
import Profiles from "./pages/Profiles";
import Debug from "./pages/Debug";
import Update from "./pages/Update";
import QuickActions from "./pages/QuickActions";
import LoginUrl from "./pages/LoginUrl";
export default function App() {
return (
<HashRouter>
<Routes>
<Route path="/quick" element={<QuickActions />} />
<Route path="/login" element={<LoginUrl />} />
<Route path="/update" element={<Update />} />
<Route element={<Layout />}>
<Route index element={<Status />} />
<Route path="peers" element={<Peers />} />
<Route path="networks" element={<Networks />} />
<Route path="profiles" element={<Profiles />} />
<Route path="settings" element={<Settings />} />
<Route path="debug" element={<Debug />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Route>
</Routes>
</HashRouter>
);
}

View File

@@ -0,0 +1,45 @@
import { NavLink, Outlet } from "react-router-dom";
import { Activity, Bug, Network, Settings as SettingsIcon, Share2, Users } from "lucide-react";
import { cn } from "./lib/cn";
const nav = [
{ to: "/", label: "Status", icon: Activity, end: true },
{ to: "/peers", label: "Peers", icon: Share2 },
{ to: "/networks", label: "Networks", icon: Network },
{ to: "/profiles", label: "Profiles", icon: Users },
{ to: "/settings", label: "Settings", icon: SettingsIcon },
{ to: "/debug", label: "Debug", icon: Bug },
];
export default function Layout() {
return (
<div className="flex h-full">
<aside className="w-48 shrink-0 border-r border-nb-gray-200 bg-nb-gray-50 dark:border-nb-gray-800 dark:bg-nb-gray-940">
<div className="px-4 py-5 text-lg font-semibold text-netbird">NetBird</div>
<nav className="px-2">
{nav.map(({ to, label, icon: Icon, end }) => (
<NavLink
key={to}
to={to}
end={end}
className={({ isActive }) =>
cn(
"flex items-center gap-2 rounded-md px-3 py-2 text-sm",
isActive
? "bg-netbird/10 text-netbird"
: "text-nb-gray-700 hover:bg-nb-gray-100 dark:text-nb-gray-300 dark:hover:bg-nb-gray-900",
)
}
>
<Icon className="h-4 w-4" strokeWidth={1.5} />
{label}
</NavLink>
))}
</nav>
</aside>
<main className="flex-1 overflow-auto">
<Outlet />
</main>
</div>
);
}

View File

@@ -0,0 +1,42 @@
import { ButtonHTMLAttributes, forwardRef } from "react";
import { cn } from "../lib/cn";
type Variant = "primary" | "secondary" | "ghost" | "danger";
type Size = "sm" | "md";
const variants: Record<Variant, string> = {
primary: "bg-netbird text-white hover:bg-netbird-500 disabled:bg-nb-gray-300",
secondary:
"bg-nb-gray-100 text-nb-gray-900 hover:bg-nb-gray-200 dark:bg-nb-gray-900 dark:text-nb-gray-50 dark:hover:bg-nb-gray-800",
ghost:
"bg-transparent text-nb-gray-700 hover:bg-nb-gray-100 dark:text-nb-gray-200 dark:hover:bg-nb-gray-900",
danger: "bg-red-600 text-white hover:bg-red-500",
};
const sizes: Record<Size, string> = {
sm: "h-7 px-2 text-xs",
md: "h-9 px-3 text-sm",
};
interface Props extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: Variant;
size?: Size;
}
export const Button = forwardRef<HTMLButtonElement, Props>(function Button(
{ variant = "primary", size = "md", className, ...rest },
ref,
) {
return (
<button
ref={ref}
className={cn(
"inline-flex items-center justify-center gap-2 rounded-md font-medium transition-colors disabled:cursor-not-allowed disabled:opacity-60",
variants[variant],
sizes[size],
className,
)}
{...rest}
/>
);
});

View File

@@ -0,0 +1,14 @@
import { HTMLAttributes } from "react";
import { cn } from "../lib/cn";
export function Card({ className, ...rest }: HTMLAttributes<HTMLDivElement>) {
return (
<div
className={cn(
"rounded-lg border border-nb-gray-200 bg-white p-4 dark:border-nb-gray-800 dark:bg-nb-gray-925",
className,
)}
{...rest}
/>
);
}

View File

@@ -0,0 +1,33 @@
import { InputHTMLAttributes, forwardRef } from "react";
import { cn } from "../lib/cn";
interface Props extends InputHTMLAttributes<HTMLInputElement> {
label?: string;
}
export const Input = forwardRef<HTMLInputElement, Props>(function Input(
{ label, className, id, ...rest },
ref,
) {
const inputId = id ?? label?.toLowerCase().replace(/\s+/g, "-");
return (
<div className="flex flex-col gap-1">
{label && (
<label htmlFor={inputId} className="text-xs font-medium text-nb-gray-600 dark:text-nb-gray-300">
{label}
</label>
)}
<input
id={inputId}
ref={ref}
className={cn(
"h-9 rounded-md border border-nb-gray-300 bg-white px-3 text-sm",
"focus:border-netbird focus:outline-none focus:ring-1 focus:ring-netbird",
"dark:border-nb-gray-700 dark:bg-nb-gray-925 dark:text-nb-gray-50",
className,
)}
{...rest}
/>
</div>
);
});

View File

@@ -0,0 +1,42 @@
import { cn } from "../lib/cn";
interface Props {
checked: boolean;
onChange: (checked: boolean) => void;
disabled?: boolean;
label?: string;
description?: string;
}
export function Switch({ checked, onChange, disabled, label, description }: Props) {
return (
<label className={cn("flex items-start gap-3", disabled && "opacity-60")}>
<button
type="button"
role="switch"
aria-checked={checked}
disabled={disabled}
onClick={() => onChange(!checked)}
className={cn(
"mt-0.5 inline-flex h-5 w-9 shrink-0 items-center rounded-full transition-colors",
checked ? "bg-netbird" : "bg-nb-gray-300 dark:bg-nb-gray-700",
)}
>
<span
className={cn(
"inline-block h-4 w-4 transform rounded-full bg-white transition-transform",
checked ? "translate-x-4" : "translate-x-0.5",
)}
/>
</button>
{(label || description) && (
<span className="flex flex-col">
{label && <span className="text-sm font-medium">{label}</span>}
{description && (
<span className="text-xs text-nb-gray-500">{description}</span>
)}
</span>
)}
</label>
);
}

View File

@@ -0,0 +1,40 @@
import { ReactNode, useState } from "react";
import { cn } from "../lib/cn";
interface Tab {
value: string;
label: string;
content: ReactNode;
}
interface Props {
tabs: Tab[];
initial?: string;
}
export function Tabs({ tabs, initial }: Props) {
const [active, setActive] = useState(initial ?? tabs[0]?.value);
return (
<div className="flex h-full flex-col">
<div className="flex shrink-0 gap-1 border-b border-nb-gray-200 dark:border-nb-gray-800">
{tabs.map((t) => (
<button
key={t.value}
onClick={() => setActive(t.value)}
className={cn(
"border-b-2 px-3 py-2 text-sm font-medium transition-colors",
active === t.value
? "border-netbird text-netbird"
: "border-transparent text-nb-gray-500 hover:text-nb-gray-800 dark:hover:text-nb-gray-200",
)}
>
{t.label}
</button>
))}
</div>
<div className="flex-1 overflow-auto">
{tabs.find((t) => t.value === active)?.content}
</div>
</div>
);
}

View File

@@ -0,0 +1,36 @@
import { useEffect, useState } from "react";
import { Events } from "@wailsio/runtime";
import { Peers } from "../../bindings/github.com/netbirdio/netbird/client/ui-wails/services";
import type { Status } from "../../bindings/github.com/netbirdio/netbird/client/ui-wails/services/models.js";
const EVENT_STATUS = "netbird:status";
// useStatus loads the current daemon status once and re-renders whenever the
// peers service emits a fresh snapshot over the Wails event bus.
export function useStatus(): { status: Status | null; error: string | null } {
const [status, setStatus] = useState<Status | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
let cancelled = false;
Peers.Get()
.then((s) => {
if (!cancelled) setStatus(s);
})
.catch((e: unknown) => {
if (!cancelled) setError(String(e));
});
const off = Events.On(EVENT_STATUS, (ev: { data: Status }) => {
setStatus(ev.data);
setError(null);
});
return () => {
cancelled = true;
off();
};
}, []);
return { status, error };
}

View File

@@ -0,0 +1,17 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
html,
body,
#root {
height: 100%;
}
body {
@apply bg-white text-nb-gray-900 antialiased;
}
.dark body {
@apply bg-nb-gray-950 text-nb-gray-50;
}

View File

@@ -0,0 +1,6 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

View File

@@ -0,0 +1,10 @@
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
import "./index.css";
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);

View File

@@ -0,0 +1,105 @@
import { useState } from "react";
import { Debug as DebugSvc } from "../../bindings/github.com/netbirdio/netbird/client/ui-wails/services";
import type { DebugBundleResult } from "../../bindings/github.com/netbirdio/netbird/client/ui-wails/services/models.js";
import { Button } from "../components/Button";
import { Input } from "../components/Input";
import { Switch } from "../components/Switch";
import { Card } from "../components/Card";
export default function Debug() {
const [anonymize, setAnonymize] = useState(true);
const [systemInfo, setSystemInfo] = useState(true);
const [upload, setUpload] = useState(false);
const [uploadUrl, setUploadUrl] = useState("");
const [logFiles, setLogFiles] = useState(0);
const [running, setRunning] = useState(false);
const [result, setResult] = useState<DebugBundleResult | null>(null);
const [error, setError] = useState<string | null>(null);
const run = async () => {
setRunning(true);
setResult(null);
setError(null);
try {
const r = await DebugSvc.Bundle({
anonymize,
systemInfo,
uploadUrl: upload ? uploadUrl : "",
logFileCount: logFiles,
});
setResult(r);
} catch (e) {
setError(String(e));
} finally {
setRunning(false);
}
};
return (
<div className="space-y-4 p-6">
<h1 className="text-xl font-semibold">Debug bundle</h1>
<Card className="space-y-4">
<Switch
checked={anonymize}
onChange={setAnonymize}
label="Anonymize"
description="Replace IPs and identifiers in the bundle."
/>
<Switch
checked={systemInfo}
onChange={setSystemInfo}
label="Include system information"
/>
<Switch
checked={upload}
onChange={setUpload}
label="Upload on create"
/>
{upload && (
<Input
label="Upload URL"
value={uploadUrl}
onChange={(e) => setUploadUrl(e.target.value)}
/>
)}
<Input
label="Log file count"
type="number"
value={logFiles}
onChange={(e) => setLogFiles(Number(e.target.value))}
/>
<div className="pt-2">
<Button onClick={run} disabled={running}>
{running ? "Generating…" : "Create bundle"}
</Button>
</div>
</Card>
{error && <p className="text-sm text-red-500">{error}</p>}
{result && (
<Card>
{result.path && (
<p className="text-sm">
<span className="text-nb-gray-500">Path:</span>{" "}
<span className="font-mono">{result.path}</span>
</p>
)}
{result.uploadedKey && (
<p className="text-sm">
<span className="text-nb-gray-500">Uploaded key:</span>{" "}
<span className="font-mono">{result.uploadedKey}</span>
</p>
)}
{result.uploadFailureReason && (
<p className="text-sm text-red-500">
Upload failed: {result.uploadFailureReason}
</p>
)}
</Card>
)}
</div>
);
}

View File

@@ -0,0 +1,34 @@
import { useEffect, useState } from "react";
import { ExternalLink } from "lucide-react";
import { Button } from "../components/Button";
export default function LoginUrl() {
const [url, setUrl] = useState<string>("");
useEffect(() => {
const params = new URLSearchParams(window.location.hash.split("?")[1] ?? "");
setUrl(params.get("url") ?? "");
}, []);
if (!url) {
return (
<div className="flex h-full items-center justify-center p-6 text-sm text-nb-gray-500">
No login URL provided.
</div>
);
}
return (
<div className="flex h-full flex-col items-center justify-center gap-4 p-6 text-center">
<h1 className="text-xl font-semibold">Continue in your browser</h1>
<p className="max-w-sm text-sm text-nb-gray-500">
Open the following URL to finish signing in.
</p>
<Button onClick={() => window.open(url, "_blank")}>
<ExternalLink className="h-4 w-4" strokeWidth={1.5} />
Open URL
</Button>
<p className="max-w-sm break-all font-mono text-xs text-nb-gray-500">{url}</p>
</div>
);
}

View File

@@ -0,0 +1,159 @@
import { useCallback, useEffect, useMemo, useState } from "react";
import { RefreshCw } from "lucide-react";
import { Networks as NetworksSvc } from "../../bindings/github.com/netbirdio/netbird/client/ui-wails/services";
import type { Network } from "../../bindings/github.com/netbirdio/netbird/client/ui-wails/services/models.js";
import { Button } from "../components/Button";
import { Tabs } from "../components/Tabs";
export default function Networks() {
const [routes, setRoutes] = useState<Network[]>([]);
const [error, setError] = useState<string | null>(null);
const [loading, setLoading] = useState(false);
const refresh = useCallback(async () => {
setLoading(true);
try {
const list = await NetworksSvc.List();
setRoutes(list);
setError(null);
} catch (e) {
setError(String(e));
} finally {
setLoading(false);
}
}, []);
useEffect(() => {
refresh();
}, [refresh]);
const toggle = async (id: string, selected: boolean) => {
try {
if (selected) {
await NetworksSvc.Deselect({ networkIds: [id], append: false, all: false });
} else {
await NetworksSvc.Select({ networkIds: [id], append: true, all: false });
}
await refresh();
} catch (e) {
setError(String(e));
}
};
const setAll = async (ids: string[], on: boolean) => {
try {
if (on) {
await NetworksSvc.Select({ networkIds: ids, append: false, all: true });
} else {
await NetworksSvc.Deselect({ networkIds: ids, append: false, all: true });
}
await refresh();
} catch (e) {
setError(String(e));
}
};
const overlapping = useMemo(() => filterOverlapping(routes), [routes]);
const exitNodes = useMemo(() => routes.filter((r) => r.range === "0.0.0.0/0"), [routes]);
return (
<div className="flex h-full flex-col p-6">
<div className="mb-3 flex items-center justify-between">
<h1 className="text-xl font-semibold">Networks</h1>
<Button variant="secondary" size="sm" onClick={refresh} disabled={loading}>
<RefreshCw className={`h-3.5 w-3.5 ${loading ? "animate-spin" : ""}`} strokeWidth={1.5} />
Refresh
</Button>
</div>
{error && (
<p className="mb-2 text-sm text-red-500">{error}</p>
)}
<div className="flex-1 overflow-hidden">
<Tabs
tabs={[
{
value: "all",
label: `All (${routes.length})`,
content: <NetworkList routes={routes} onToggle={toggle} onSetAll={setAll} />,
},
{
value: "overlap",
label: `Overlapping (${overlapping.length})`,
content: <NetworkList routes={overlapping} onToggle={toggle} onSetAll={setAll} />,
},
{
value: "exit",
label: `Exit-node (${exitNodes.length})`,
content: <NetworkList routes={exitNodes} onToggle={toggle} onSetAll={setAll} />,
},
]}
/>
</div>
</div>
);
}
function NetworkList({
routes,
onToggle,
onSetAll,
}: {
routes: Network[];
onToggle: (id: string, selected: boolean) => void;
onSetAll: (ids: string[], on: boolean) => void;
}) {
if (routes.length === 0) {
return <p className="p-4 text-sm text-nb-gray-500">No networks.</p>;
}
const ids = routes.map((r) => r.id);
return (
<div className="flex h-full flex-col">
<div className="flex shrink-0 gap-2 border-b border-nb-gray-200 px-4 py-2 dark:border-nb-gray-800">
<Button size="sm" variant="ghost" onClick={() => onSetAll(ids, true)}>
Select all
</Button>
<Button size="sm" variant="ghost" onClick={() => onSetAll(ids, false)}>
Deselect all
</Button>
</div>
<ul className="flex-1 overflow-auto divide-y divide-nb-gray-200 dark:divide-nb-gray-800">
{routes.map((r) => (
<li key={r.id} className="flex items-start gap-3 px-4 py-3">
<input
type="checkbox"
checked={r.selected}
onChange={() => onToggle(r.id, r.selected)}
className="mt-1 h-4 w-4 accent-netbird"
/>
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium">{r.id}</p>
<p className="truncate font-mono text-xs text-nb-gray-500">{r.range}</p>
{r.domains.length > 0 && (
<p className="mt-0.5 truncate text-xs text-nb-gray-500">
{r.domains.join(", ")}
</p>
)}
</div>
</li>
))}
</ul>
</div>
);
}
function filterOverlapping(routes: Network[]): Network[] {
const byRange = new Map<string, Network[]>();
for (const r of routes) {
if (r.domains.length > 0) continue;
const arr = byRange.get(r.range) ?? [];
arr.push(r);
byRange.set(r.range, arr);
}
const out: Network[] = [];
for (const arr of byRange.values()) {
if (arr.length > 1) out.push(...arr);
}
return out;
}

View File

@@ -0,0 +1,211 @@
import { useMemo, useState } from "react";
import { ChevronDown, ChevronRight, Network, ShieldCheck, Zap } from "lucide-react";
import { useStatus } from "../hooks/useStatus";
import type { PeerStatus } from "../../bindings/github.com/netbirdio/netbird/client/ui-wails/services/models.js";
import { Card } from "../components/Card";
import { Input } from "../components/Input";
import { cn } from "../lib/cn";
export default function Peers() {
const { status } = useStatus();
const [filter, setFilter] = useState("");
const [expanded, setExpanded] = useState<string | null>(null);
const peers = useMemo(() => {
const all = status?.peers ?? [];
if (!filter.trim()) return all;
const q = filter.trim().toLowerCase();
return all.filter(
(p) =>
p.fqdn.toLowerCase().includes(q) ||
p.ip.toLowerCase().includes(q) ||
p.networks.some((n) => n.toLowerCase().includes(q)),
);
}, [status?.peers, filter]);
return (
<div className="flex h-full flex-col p-6">
<div className="mb-3 flex items-center justify-between">
<h1 className="text-xl font-semibold">
Peers
<span className="ml-2 text-sm font-normal text-nb-gray-500">
{status?.peers?.length ?? 0}
</span>
</h1>
<div className="w-64">
<Input
placeholder="Filter by FQDN / IP / network"
value={filter}
onChange={(e) => setFilter(e.target.value)}
/>
</div>
</div>
{peers.length === 0 ? (
<Card className="text-sm text-nb-gray-500">
{status?.peers?.length === 0
? "No peers visible from this client."
: "No peers match the filter."}
</Card>
) : (
<ul className="flex-1 divide-y divide-nb-gray-200 overflow-auto rounded-lg border border-nb-gray-200 dark:divide-nb-gray-800 dark:border-nb-gray-800">
{peers.map((p) => (
<PeerRow
key={p.pubKey}
peer={p}
expanded={expanded === p.pubKey}
onToggle={() => setExpanded(expanded === p.pubKey ? null : p.pubKey)}
/>
))}
</ul>
)}
</div>
);
}
function PeerRow({
peer,
expanded,
onToggle,
}: {
peer: PeerStatus;
expanded: boolean;
onToggle: () => void;
}) {
return (
<li>
<button
onClick={onToggle}
className="flex w-full items-center gap-3 px-4 py-3 text-left hover:bg-nb-gray-50 dark:hover:bg-nb-gray-940"
>
<ChevronIcon expanded={expanded} />
<StateBadge state={peer.connStatus} />
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium">{peer.fqdn || "—"}</p>
<p className="truncate font-mono text-xs text-nb-gray-500">{peer.ip}</p>
</div>
<RouteIcon relayed={peer.relayed} connected={peer.connStatus === "Connected"} />
{peer.rosenpassEnabled && (
<ShieldCheck className="h-4 w-4 text-green-500" strokeWidth={1.5} />
)}
<span className="w-16 text-right text-xs text-nb-gray-500">
{peer.connStatus === "Connected" && peer.latencyMs > 0
? `${peer.latencyMs} ms`
: ""}
</span>
</button>
{expanded && <PeerDetails peer={peer} />}
</li>
);
}
function PeerDetails({ peer }: { peer: PeerStatus }) {
return (
<div className="grid grid-cols-2 gap-x-6 gap-y-2 bg-nb-gray-50 px-12 py-3 text-xs dark:bg-nb-gray-940">
<Detail label="Public key" value={peer.pubKey} mono />
<Detail label="Last handshake" value={fmtRelative(peer.lastHandshakeUnix)} />
<Detail label="Status since" value={fmtRelative(peer.connStatusUpdateUnix)} />
<Detail
label="Bytes rx / tx"
value={`${fmtBytes(peer.bytesRx)} / ${fmtBytes(peer.bytesTx)}`}
/>
<Detail
label="Local candidate"
value={
peer.localIceCandidateType
? `${peer.localIceCandidateType} (${peer.localIceCandidateEndpoint || "—"})`
: "—"
}
mono
/>
<Detail
label="Remote candidate"
value={
peer.remoteIceCandidateType
? `${peer.remoteIceCandidateType} (${peer.remoteIceCandidateEndpoint || "—"})`
: "—"
}
mono
/>
{peer.relayed && (
<Detail label="Relay" value={peer.relayAddress || "—"} mono />
)}
{peer.networks.length > 0 && (
<Detail label="Networks" value={peer.networks.join(", ")} />
)}
</div>
);
}
function Detail({ label, value, mono }: { label: string; value: string; mono?: boolean }) {
return (
<div className="flex min-w-0 gap-2">
<span className="shrink-0 text-nb-gray-500">{label}</span>
<span
className={cn(
"min-w-0 truncate text-nb-gray-700 dark:text-nb-gray-200",
mono && "font-mono",
)}
>
{value}
</span>
</div>
);
}
function ChevronIcon({ expanded }: { expanded: boolean }) {
const Icon = expanded ? ChevronDown : ChevronRight;
return <Icon className="h-4 w-4 shrink-0 text-nb-gray-400" strokeWidth={1.5} />;
}
function StateBadge({ state }: { state: string }) {
const cls = "h-2 w-2 rounded-full shrink-0";
switch (state) {
case "Connected":
return <span className={cn(cls, "bg-green-500")} title="Connected" />;
case "Connecting":
return <span className={cn(cls, "bg-netbird animate-pulse")} title="Connecting" />;
case "Idle":
return <span className={cn(cls, "bg-yellow-500")} title="Idle" />;
default:
return <span className={cn(cls, "bg-nb-gray-400")} title={state || "Disconnected"} />;
}
}
function RouteIcon({ relayed, connected }: { relayed: boolean; connected: boolean }) {
if (!connected) {
return <span className="w-4 shrink-0" />;
}
if (relayed) {
return (
<Network
className="h-4 w-4 text-yellow-600"
strokeWidth={1.5}
>
<title>Relayed</title>
</Network>
);
}
return (
<Zap className="h-4 w-4 text-green-600" strokeWidth={1.5}>
<title>P2P</title>
</Zap>
);
}
function fmtBytes(n: number): string {
if (n < 1024) return `${n} B`;
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
if (n < 1024 * 1024 * 1024) return `${(n / 1024 / 1024).toFixed(1)} MB`;
return `${(n / 1024 / 1024 / 1024).toFixed(2)} GB`;
}
function fmtRelative(unixSec: number): string {
if (!unixSec) return "—";
const ageSec = Math.max(0, Math.floor(Date.now() / 1000) - unixSec);
if (ageSec < 60) return `${ageSec}s ago`;
if (ageSec < 3600) return `${Math.floor(ageSec / 60)}m ago`;
if (ageSec < 86400) return `${Math.floor(ageSec / 3600)}h ago`;
return `${Math.floor(ageSec / 86400)}d ago`;
}

View File

@@ -0,0 +1,173 @@
import { FormEvent, useCallback, useEffect, useState } from "react";
import { Plus, RefreshCw } from "lucide-react";
import {
Profiles as ProfilesSvc,
Connection,
} from "../../bindings/github.com/netbirdio/netbird/client/ui-wails/services";
import type { Profile } from "../../bindings/github.com/netbirdio/netbird/client/ui-wails/services/models.js";
import { Button } from "../components/Button";
import { Input } from "../components/Input";
import { Card } from "../components/Card";
export default function Profiles() {
const [username, setUsername] = useState("");
const [profiles, setProfiles] = useState<Profile[]>([]);
const [error, setError] = useState<string | null>(null);
const [adding, setAdding] = useState(false);
const refresh = useCallback(async () => {
try {
const u = username || (await ProfilesSvc.Username());
if (!username) setUsername(u);
const list = await ProfilesSvc.List(u);
setProfiles(list);
setError(null);
} catch (e) {
setError(String(e));
}
}, [username]);
useEffect(() => {
refresh();
}, [refresh]);
const select = async (name: string) => {
try {
await ProfilesSvc.Switch({ profileName: name, username });
await Connection.Up({ profileName: name, username });
await refresh();
} catch (e) {
setError(String(e));
}
};
const deregister = async (name: string) => {
try {
await Connection.Logout({ profileName: name, username });
await refresh();
} catch (e) {
setError(String(e));
}
};
const remove = async (name: string) => {
if (name === "default") return;
try {
await ProfilesSvc.Remove({ profileName: name, username });
await refresh();
} catch (e) {
setError(String(e));
}
};
return (
<div className="space-y-4 p-6">
<div className="flex items-center justify-between">
<h1 className="text-xl font-semibold">Profiles</h1>
<div className="flex gap-2">
<Button variant="secondary" size="sm" onClick={refresh}>
<RefreshCw className="h-3.5 w-3.5" strokeWidth={1.5} /> Refresh
</Button>
<Button size="sm" onClick={() => setAdding(true)}>
<Plus className="h-3.5 w-3.5" strokeWidth={1.5} /> Add
</Button>
</div>
</div>
{error && <p className="text-sm text-red-500">{error}</p>}
<div className="space-y-2">
{profiles.map((p) => (
<Card key={p.name} className="flex items-center gap-3">
<input
type="radio"
name="active-profile"
checked={p.isActive}
onChange={() => select(p.name)}
className="h-4 w-4 accent-netbird"
/>
<div className="flex-1">
<p className="text-sm font-medium">{p.name}</p>
{p.isActive && <p className="text-xs text-nb-gray-500">Active</p>}
</div>
<Button size="sm" variant="ghost" onClick={() => deregister(p.name)}>
Deregister
</Button>
<Button
size="sm"
variant="danger"
disabled={p.name === "default"}
onClick={() => remove(p.name)}
>
Remove
</Button>
</Card>
))}
{profiles.length === 0 && (
<p className="text-sm text-nb-gray-500">No profiles.</p>
)}
</div>
{adding && (
<AddDialog
username={username}
onClose={() => setAdding(false)}
onAdded={async () => {
setAdding(false);
await refresh();
}}
/>
)}
</div>
);
}
function AddDialog({
username,
onClose,
onAdded,
}: {
username: string;
onClose: () => void;
onAdded: () => void;
}) {
const [name, setName] = useState("");
const [err, setErr] = useState<string | null>(null);
const submit = async (e: FormEvent) => {
e.preventDefault();
if (!name.trim()) return;
try {
await ProfilesSvc.Add({ profileName: name.trim(), username });
onAdded();
} catch (e) {
setErr(String(e));
}
};
return (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40">
<form
onSubmit={submit}
className="w-80 rounded-lg border border-nb-gray-200 bg-white p-4 shadow-lg dark:border-nb-gray-800 dark:bg-nb-gray-925"
>
<h2 className="mb-3 text-base font-semibold">New profile</h2>
<Input
autoFocus
label="Name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
{err && <p className="mt-2 text-xs text-red-500">{err}</p>}
<div className="mt-4 flex justify-end gap-2">
<Button type="button" variant="ghost" size="sm" onClick={onClose}>
Cancel
</Button>
<Button type="submit" size="sm">
Add
</Button>
</div>
</form>
</div>
);
}

View File

@@ -0,0 +1,40 @@
import { CheckCircle2, Circle, Loader2, Power } from "lucide-react";
import { useStatus } from "../hooks/useStatus";
import { Connection } from "../../bindings/github.com/netbirdio/netbird/client/ui-wails/services";
import { Button } from "../components/Button";
import { cn } from "../lib/cn";
export default function QuickActions() {
const { status } = useStatus();
const state = status?.status ?? "Disconnected";
const connected = state === "Connected";
const connecting = state === "Connecting";
return (
<div className="flex h-full flex-col items-center justify-center gap-4 p-6">
<Icon state={state} />
<p className="text-lg font-medium">{state}</p>
{connected ? (
<Button variant="secondary" onClick={() => Connection.Down()}>
Disconnect
</Button>
) : (
<Button onClick={() => Connection.Up({ profileName: "", username: "" })} disabled={connecting}>
<Power className="h-4 w-4" strokeWidth={1.5} /> Connect
</Button>
)}
</div>
);
}
function Icon({ state }: { state: string }) {
const cls = "h-12 w-12";
switch (state) {
case "Connected":
return <CheckCircle2 className={cn(cls, "text-green-500")} strokeWidth={1.5} />;
case "Connecting":
return <Loader2 className={cn(cls, "animate-spin text-netbird")} strokeWidth={1.5} />;
default:
return <Circle className={cn(cls, "text-nb-gray-400")} strokeWidth={1.5} />;
}
}

View File

@@ -0,0 +1,240 @@
import { useCallback, useEffect, useState } from "react";
import {
Settings as SettingsSvc,
Profiles as ProfilesSvc,
} from "../../bindings/github.com/netbirdio/netbird/client/ui-wails/services";
import type { Config } from "../../bindings/github.com/netbirdio/netbird/client/ui-wails/services/models.js";
import { Button } from "../components/Button";
import { Input } from "../components/Input";
import { Switch } from "../components/Switch";
import { Tabs } from "../components/Tabs";
interface Ctx {
cfg: Config;
setField: <K extends keyof Config>(k: K, v: Config[K]) => void;
}
export default function Settings() {
const [username, setUsername] = useState("");
const [profile, setProfile] = useState("");
const [cfg, setCfg] = useState<Config | null>(null);
const [error, setError] = useState<string | null>(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 <div className="p-6 text-sm text-nb-gray-500">Loading</div>;
}
const ctx: Ctx = { cfg, setField };
return (
<div className="flex h-full flex-col">
<div className="flex items-center justify-between border-b border-nb-gray-200 px-6 py-3 dark:border-nb-gray-800">
<h1 className="text-xl font-semibold">Settings</h1>
<Button onClick={save} disabled={saving}>
{saving ? "Saving…" : "Save"}
</Button>
</div>
{error && <p className="px-6 py-2 text-sm text-red-500">{error}</p>}
<div className="flex-1 overflow-hidden">
<Tabs
tabs={[
{ value: "conn", label: "Connection", content: <ConnectionTab {...ctx} /> },
{ value: "net", label: "Network", content: <NetworkTab {...ctx} /> },
{ value: "ssh", label: "SSH", content: <SSHTab {...ctx} /> },
]}
/>
</div>
</div>
);
}
function ConnectionTab({ cfg, setField }: Ctx) {
return (
<div className="grid max-w-2xl gap-4 p-6">
<Input
label="Management URL"
value={cfg.managementUrl}
onChange={(e) => setField("managementUrl", e.target.value)}
/>
<Input
label="Pre-shared key"
type="password"
value={cfg.preSharedKey}
onChange={(e) => setField("preSharedKey", e.target.value)}
/>
<Input
label="Interface name"
value={cfg.interfaceName}
onChange={(e) => setField("interfaceName", e.target.value)}
/>
<div className="grid grid-cols-2 gap-4">
<Input
label="WireGuard port"
type="number"
value={cfg.wireguardPort}
onChange={(e) => setField("wireguardPort", Number(e.target.value))}
/>
<Input
label="MTU"
type="number"
value={cfg.mtu}
onChange={(e) => setField("mtu", Number(e.target.value))}
/>
</div>
<Switch
checked={cfg.rosenpassEnabled}
onChange={(v) => setField("rosenpassEnabled", v)}
label="Rosenpass (post-quantum)"
/>
<Switch
checked={cfg.rosenpassPermissive}
onChange={(v) => setField("rosenpassPermissive", v)}
label="Rosenpass permissive mode"
/>
</div>
);
}
function NetworkTab({ cfg, setField }: Ctx) {
return (
<div className="grid max-w-xl gap-4 p-6">
<Switch
checked={cfg.networkMonitor}
onChange={(v) => setField("networkMonitor", v)}
label="Network monitor"
/>
<Switch
checked={cfg.disableDns}
onChange={(v) => setField("disableDns", v)}
label="Disable DNS"
/>
<Switch
checked={cfg.disableClientRoutes}
onChange={(v) => setField("disableClientRoutes", v)}
label="Disable client routes"
/>
<Switch
checked={cfg.disableServerRoutes}
onChange={(v) => setField("disableServerRoutes", v)}
label="Disable server routes"
/>
<Switch
checked={cfg.blockLanAccess}
onChange={(v) => setField("blockLanAccess", v)}
label="Block LAN access"
/>
<Switch
checked={cfg.blockInbound}
onChange={(v) => setField("blockInbound", v)}
label="Block inbound connections"
/>
</div>
);
}
function SSHTab({ cfg, setField }: Ctx) {
return (
<div className="grid max-w-xl gap-4 p-6">
<Switch
checked={cfg.serverSshAllowed}
onChange={(v) => setField("serverSshAllowed", v)}
label="Server SSH allowed"
/>
<Switch
checked={cfg.enableSshRoot}
onChange={(v) => setField("enableSshRoot", v)}
label="SSH root login"
/>
<Switch
checked={cfg.enableSshSftp}
onChange={(v) => setField("enableSshSftp", v)}
label="SFTP"
/>
<Switch
checked={cfg.enableSshLocalPortForwarding}
onChange={(v) => setField("enableSshLocalPortForwarding", v)}
label="Local port forwarding"
/>
<Switch
checked={cfg.enableSshRemotePortForwarding}
onChange={(v) => setField("enableSshRemotePortForwarding", v)}
label="Remote port forwarding"
/>
<Switch
checked={cfg.disableSshAuth}
onChange={(v) => setField("disableSshAuth", v)}
label="Disable SSH auth"
/>
<Input
label="JWT cache TTL (seconds)"
type="number"
value={cfg.sshJwtCacheTtl}
onChange={(e) => setField("sshJwtCacheTtl", Number(e.target.value))}
/>
</div>
);
}

View File

@@ -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 (
<div className="space-y-4 p-6">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<StateIcon state={connState} />
<div>
<h1 className="text-xl font-semibold leading-none">{connState}</h1>
<p className="mt-1 text-sm text-nb-gray-500">
{status?.local.fqdn || "—"}
</p>
</div>
</div>
<div className="flex gap-2">
<Button onClick={connect} disabled={connected || connecting}>
<Power className="h-4 w-4" strokeWidth={1.5} /> Connect
</Button>
<Button onClick={disconnect} variant="secondary" disabled={!connected}>
Disconnect
</Button>
</div>
</div>
{error && (
<div className="flex items-start gap-2 rounded-md border border-red-200 bg-red-50 p-3 text-sm text-red-800 dark:border-red-800 dark:bg-red-950 dark:text-red-200">
<AlertTriangle className="mt-0.5 h-4 w-4" strokeWidth={1.5} />
<span>{error}</span>
</div>
)}
<div className="grid grid-cols-2 gap-4">
<InfoCard label="Local IP" value={status?.local.ip || "—"} />
<InfoCard label="Peers" value={String(status?.peers?.length ?? 0)} />
<LinkCard label="Management" link={status?.management} />
<LinkCard label="Signal" link={status?.signal} />
</div>
<Card>
<h2 className="mb-3 text-sm font-semibold text-nb-gray-700 dark:text-nb-gray-200">
Recent events
</h2>
{(() => {
const events = dedupEvents(status?.events ?? []).slice(0, 8);
if (events.length === 0) {
return <p className="text-sm text-nb-gray-500">No recent events.</p>;
}
return (
<ul className="space-y-2 text-sm">
{events.map((e, i) => (
<li key={`${e.id}-${i}`} className="flex gap-2">
<span className="shrink-0 font-mono text-xs text-nb-gray-500">
{e.severity}
</span>
<span className="text-nb-gray-700 dark:text-nb-gray-200">
{e.userMessage || e.message}
</span>
</li>
))}
</ul>
);
})()}
</Card>
</div>
);
}
function StateIcon({ state }: { state: string }) {
const cls = "h-7 w-7";
switch (state) {
case "Connected":
return <CheckCircle2 className={cn(cls, "text-green-500")} strokeWidth={1.5} />;
case "Connecting":
return <Loader2 className={cn(cls, "animate-spin text-netbird")} strokeWidth={1.5} />;
case "Error":
return <AlertTriangle className={cn(cls, "text-red-500")} strokeWidth={1.5} />;
default:
return <Circle className={cn(cls, "text-nb-gray-400")} strokeWidth={1.5} />;
}
}
function InfoCard({ label, value }: { label: string; value: string }) {
return (
<Card>
<p className="text-xs uppercase tracking-wide text-nb-gray-500">{label}</p>
<p className="mt-1 truncate font-mono text-sm">{value}</p>
</Card>
);
}
// 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<string>();
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 (
<Card>
<div className="flex items-center justify-between">
<p className="text-xs uppercase tracking-wide text-nb-gray-500">{label}</p>
<span
className={cn(
"h-2 w-2 rounded-full",
link?.connected ? "bg-green-500" : "bg-nb-gray-400",
)}
/>
</div>
<p className="mt-1 truncate text-xs text-nb-gray-600 dark:text-nb-gray-300">
{link?.url || "—"}
</p>
{link?.error && (
<p className="mt-1 truncate text-xs text-red-500">{link.error}</p>
)}
</Card>
);
}

View File

@@ -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<string | null>(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 (
<div className="flex h-full items-center justify-center p-6">
<div className="text-center">
{done ? (
<h1 className="text-xl font-semibold text-green-500">Update complete</h1>
) : error ? (
<h1 className="text-xl font-semibold text-red-500">{error}</h1>
) : (
<>
<Loader2 className="mx-auto mb-3 h-8 w-8 animate-spin text-netbird" strokeWidth={1.5} />
<h1 className="text-xl font-semibold">Updating</h1>
<p className="mt-1 text-sm text-nb-gray-500">
Please don't close this window.
</p>
</>
)}
</div>
</div>
);
}

View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@@ -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;

View File

@@ -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"],
}

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