Files
netbird/client/ui/frontend/WAILS-API.md
2026-05-15 16:22:14 +02:00

293 lines
15 KiB
Markdown

# Wails Go API reference (frontend)
Reference for every binding method and model shape exposed to the frontend. Generated from `client/ui/services/*.go` via `wails3 generate bindings -clean=true -ts` — regenerate after any Go-side change. Authoritative source is always `bindings/github.com/netbirdio/netbird/client/ui/services/*.ts`.
Every method returns `$CancellablePromise<T>` (a Wails3 wrapper around `Promise`). Call `.cancel()` to abort the underlying gRPC call; in practice we just `await` and let it run.
## Imports
```ts
// Services
import {
Connection, Peers, ProfileSwitcher, Profiles,
Settings, Networks, Forwarding, Debug, Update, WindowManager,
I18n, Preferences,
} from "@bindings/services";
// Models (types-only)
import type {
Status, PeerStatus, PeerLink, LocalPeer, SystemEvent,
Profile, ProfileRef, ActiveProfile,
Config, ConfigParams, SetConfigParams, Features,
Network, SelectNetworksParams,
ForwardingRule, PortInfo, PortRange,
LoginParams, LoginResult, LogoutParams, WaitSSOParams, UpParams,
DebugBundleParams, DebugBundleResult, LogLevel,
UpdateResult, UpdateAvailable, UpdateProgress,
} from "@bindings/services/models.js";
// i18n / preferences models live in sibling packages, not services/models
import { LanguageCode, type Language } from "@bindings/i18n/models.js";
import type { UIPreferences } from "@bindings/preferences/models.js";
```
## Push events
Subscribe with `Events.On(name, handler)` from `@wailsio/runtime`. Handlers receive `{ data: <payload> }`.
| Event | Payload | Fires on |
|---|---|---|
| `netbird:status` | `Status` | Daemon SubscribeStatus snapshot — connection-state change, peer-list change, address change, mgmt/signal flip. Synthetic `StatusDaemonUnavailable` is emitted when the gRPC socket is unreachable, and a synthetic `Connecting` is emitted at the start of an active profile switch. |
| `netbird:event` | `SystemEvent` | One push per daemon SubscribeEvents item (DNS / network / authentication / connectivity / system). Used by the tray for OS toasts; the TS side reads events through `Status.events` instead. |
| `netbird:update:available` | `UpdateAvailable` | Daemon detected a new version (fan-out of the `new_version_available` metadata key). |
| `netbird:preferences:changed` | `{ language: string }` | Fires after every successful `Preferences.SetLanguage` (including the caller's own window). `src/i18n/index.ts` subscribes and calls `i18next.changeLanguage`. |
| `netbird:update:progress` | `UpdateProgress` | Daemon enforced-update install progress (`action: "show"` etc.). |
| `browser-login:cancel` | (none) | Either the user closed the `BrowserLogin` window (Go-emitted) or the page's Cancel button (frontend-emitted). |
| `trigger-login` | (none) | Reserved by the tray for asking the frontend to start an SSO flow; not currently wired on the TS side. |
The two stream loops behind `netbird:status` and `netbird:event` start automatically — `main.go` calls `peers.Watch(context.Background())` at boot. `Peers.Watch` is still exported but the frontend doesn't need to invoke it.
## `Connection`
```ts
Connection.Login(p: LoginParams): Promise<LoginResult>
Connection.WaitSSOLogin(p: WaitSSOParams): Promise<string> // returns email
Connection.Up(p: UpParams): Promise<void> // async on the daemon
Connection.Down(): Promise<void>
Connection.Logout(p: LogoutParams): Promise<void>
Connection.OpenURL(url: string): Promise<void> // honors $BROWSER
```
`Login` Down-resets the daemon first to dislodge a stale `WaitSSOLogin` (so a previously abandoned SSO flow doesn't fail the next attempt). `Up` always uses async mode — status flows back through `netbird:status`. **Do not call `Up` on an `Idle` / `NeedsLogin` daemon** — the daemon's internal 50s `waitForUp` will block and return `DeadlineExceeded`.
Full SSO sequence: `Login` → if `result.needsSsoLogin`, open `result.verificationUriComplete` via `OpenURL` + `WindowManager.OpenBrowserLogin(uri)``WaitSSOLogin({ userCode })``Up({})`. The canonical implementation is `startLogin()` in `layouts/ConnectionStatusSwitch.tsx`.
## `Peers`
```ts
Peers.Get(): Promise<Status> // one-shot snapshot
Peers.Watch(): Promise<void> // already invoked from main.go
Peers.BeginProfileSwitch(): Promise<void>
Peers.CancelProfileSwitch(): Promise<void>
```
`BeginProfileSwitch` and `CancelProfileSwitch` are normally driven by `ProfileSwitcher` / the tray, not the frontend.
## `ProfileSwitcher`
```ts
ProfileSwitcher.SwitchActive(p: ProfileRef): Promise<void>
```
The single entry point both tray and frontend should use for profile flips. Applies the reconnect policy below, mirrors the switch into the user-side `profilemanager` (so the CLI's `netbird up` reads a consistent active profile), and drives the optimistic-Connecting paint via `Peers.BeginProfileSwitch`.
Reconnect policy (driven by `prevStatus` captured at entry):
| Previous status | Action | Optimistic UI | Suppressed events until new flow |
|---|---|---|---|
| Connected | Switch + Down + Up | Connecting (synthetic) | Connected, Idle |
| Connecting | Switch + Down + Up | Connecting (unchanged) | Connected, Idle |
| NeedsLogin / LoginFailed / SessionExpired | Switch + Down | (no change) | — |
| Idle | Switch only | (no change) | — |
## `Profiles`
```ts
Profiles.Username(): Promise<string> // current OS username
Profiles.List(username: string): Promise<Profile[]>
Profiles.GetActive(): Promise<ActiveProfile>
Profiles.Switch(p: ProfileRef): Promise<void> // raw daemon RPC; prefer ProfileSwitcher.SwitchActive
Profiles.Add(p: ProfileRef): Promise<void>
Profiles.Remove(p: ProfileRef): Promise<void>
```
`Profile.email` is populated by the **UI process** reading the per-profile state file (`~/Library/Application Support/netbird/<name>.state.json` on macOS), not by the daemon — the daemon runs as root and can't read user-owned files.
## `Settings`
```ts
Settings.GetConfig(p: ConfigParams): Promise<Config>
Settings.SetConfig(p: SetConfigParams): Promise<void> // partial update
Settings.GetFeatures(): Promise<Features> // operator-disabled UI sections
```
`SetConfig` is a partial update: only fields you set are pushed to the daemon. `profileName` + `username` are always required; the typed fields in `SetConfigParams` are optional (`field?: T | null`). `managementUrl` and `adminUrl` are always-string for historical reasons.
**PSK mask quirk:** `GetConfig` returns existing pre-shared keys as `"**********"`. If you send the mask back, `wgtypes.ParseKey` fails on the next connect. `SettingsContext.save` drops the field when it equals `"**********"`. See `modules/settings/SettingsContext.tsx`.
`SetConfigParams` carries one field that `Config` does not: `disableFirewall`. There's no current GET path for it.
## `Networks`
```ts
Networks.List(): Promise<Network[]>
Networks.Select(p: SelectNetworksParams): Promise<void>
Networks.Deselect(p: SelectNetworksParams): Promise<void>
```
`SelectNetworksParams.append=true` merges into the existing selection; `false` replaces. `all=true` ignores `networkIds` and targets every network (Select-All / Deselect-All).
Exit-node filter: `range === "0.0.0.0/0" || range === "::/0"`. Domain network: `domains.length > 0`. CIDR overlap check is client-side.
## `Forwarding`
```ts
Forwarding.List(): Promise<ForwardingRule[]>
```
`PortInfo` is a daemon-side oneof — exactly one of `port?: number` or `range?: PortRange` is populated. `protocol` is the lowercase daemon string (`"tcp"` / `"udp"`).
## `Debug`
```ts
Debug.GetLogLevel(): Promise<LogLevel>
Debug.SetLogLevel(lvl: LogLevel): Promise<void>
Debug.Bundle(p: DebugBundleParams): Promise<DebugBundleResult>
Debug.RevealFile(path: string): Promise<void> // OS file-manager focus
```
**Log level case sensitivity bug:** `proto.LogLevel_value` is keyed on uppercase enum names (`"TRACE"`, `"DEBUG"`, `"INFO"`, `"WARN"`, `"ERROR"`, `"PANIC"`, `"FATAL"`, `"UNKNOWN"`). `Debug.SetLogLevel` calls `proto.LogLevel_value[lvl.Level]` and falls back to `INFO` on miss. `useDebugBundle` currently passes `"trace"` (lowercase), which silently maps to `INFO` — the trace-capture flow doesn't actually raise the log level today. To raise to trace, pass `{ level: "TRACE" }`. Fix on the cleanup list.
`Debug.Bundle` uploads when `uploadUrl != ""`. Result fields: `path` (local copy), `uploadedKey` (set on success), `uploadFailureReason` (set on upload failure — the local copy is still saved).
## `Update`
```ts
Update.Trigger(): Promise<UpdateResult> // start the install
Update.GetInstallerResult(): Promise<UpdateResult> // poll the outcome (long-running)
Update.Quit(): Promise<void> // 100ms later, app.Quit()
```
Typical enforced-update flow on the `/update` route: call `Trigger` once, then poll `GetInstallerResult` every 2s with a 15-minute total timeout. On `success: true` call `Quit`. On `success: false` show `errorMsg`. If the gRPC poll itself starts failing for `DAEMON_DOWN_GRACE_MS` (5s), treat that as success and quit too — the installer commonly takes the daemon offline mid-upgrade. See `pages/Update.tsx` for the canonical implementation.
## `WindowManager`
```ts
WindowManager.OpenSettings(): Promise<void>
WindowManager.OpenBrowserLogin(uri: string): Promise<void> // uri appended as ?uri=…
WindowManager.CloseBrowserLogin(): Promise<void>
```
Both auxiliary windows are created on first open and destroyed on close (mutex-guarded singleton). The BrowserLogin window's red-X close fires the `browser-login:cancel` event so `startLogin()` can tear down the pending daemon `WaitSSOLogin`.
## `I18n`
```ts
I18n.Languages(): Promise<Language[]> // from _index.json
I18n.Bundle(code: LanguageCode): Promise<Record<string,string>> // full key→text map
```
Source of truth is `frontend/src/i18n/locales/`. The frontend's i18next bootstrap doesn't need `I18n.Bundle` at runtime (bundles are statically imported by Vite), but the language picker reads `I18n.Languages()` so the list matches `_index.json` without duplicating it in TS.
## `Preferences`
```ts
Preferences.Get(): Promise<UIPreferences> // { language: string }
Preferences.SetLanguage(code: LanguageCode): Promise<void> // rejects on unknown code
```
`SetLanguage` validates against the loaded `i18n.Bundle`, persists to `os.UserConfigDir()/netbird/ui-preferences.json`, and emits `netbird:preferences:changed`. The frontend's `src/i18n/index.ts` listens to that event and calls `i18next.changeLanguage` so a flip in any window paints in all of them. Missing preferences file → defaults to `en`, written on first read.
## Daemon `Status.status` values
Mirror `internal.Status*` in `client/internal/state.go` plus the synthetic UI label:
| Value | Meaning |
|---|---|
| `"Idle"` | Tunnel down (Up never invoked or Down completed) |
| `"Connecting"` | Up in progress |
| `"Connected"` | Tunnel up |
| `"NeedsLogin"` | Fresh install or token cleared; needs Login → SSO → Up |
| `"LoginFailed"` | Previous Login attempt errored |
| `"SessionExpired"` | SSO token expired; needs re-Login |
| `"DaemonUnavailable"` | **Synthetic** — UI side, emitted when the daemon gRPC socket is unreachable. Not a real daemon enum. |
The tray also reads a tray-only synthetic `"Error"` for icon purposes; the frontend doesn't see that.
## Model field reference
`Status`:
```ts
{ status, daemonVersion: string;
management: PeerLink; signal: PeerLink;
local: LocalPeer;
peers: PeerStatus[];
events: SystemEvent[]; }
```
`PeerLink`: `{ url: string; connected: boolean; error?: string }`.
`LocalPeer`: `{ ip, pubKey, fqdn: string; networks: string[] }`.
`PeerStatus`:
```ts
{ ip, pubKey, fqdn, connStatus: string;
connStatusUpdateUnix: number;
relayed: boolean;
localIceCandidateType, remoteIceCandidateType: string; // pion: "host"|"srflx"|"prflx"|"relay"|""
localIceCandidateEndpoint, remoteIceCandidateEndpoint: string;
bytesRx, bytesTx, latencyMs, lastHandshakeUnix: number;
relayAddress: string; // set when relayed=true
rosenpassEnabled: boolean;
networks: string[]; }
```
`SystemEvent`:
```ts
{ id: string;
severity: string; // "info"|"warning"|"error"|"critical" (lowercased proto enum, "SystemEvent_" prefix stripped)
category: string; // "network"|"dns"|"authentication"|"connectivity"|"system" (same casing rules)
message: string; // technical / log line
userMessage: string; // human-friendly — render this
timestamp: number; // unix seconds
metadata: Record<string, string>; } // keys: "new_version_available", "enforced", "id", "network", "version", "progress_window", …
```
`Profile`: `{ name: string; isActive: boolean; email: string }`.
`Config` (read-only mirror, all required):
```ts
{ managementUrl, adminUrl, configFile, logFile, preSharedKey, interfaceName: string;
wireguardPort, mtu, sshJwtCacheTtl: number;
disableAutoConnect, serverSshAllowed,
rosenpassEnabled, rosenpassPermissive,
disableNotifications, lazyConnectionEnabled, blockInbound,
networkMonitor, disableClientRoutes, disableServerRoutes,
disableDns, disableIpv6, blockLanAccess,
enableSshRoot, enableSshSftp,
enableSshLocalPortForwarding, enableSshRemotePortForwarding,
disableSshAuth: boolean; }
```
`SetConfigParams` has all `Config` fields as `field?: T | null` (partial update), plus the write-only `disableFirewall?: boolean | null`, plus `profileName` / `username` / `managementUrl` / `adminUrl` as required strings.
`Features`: `{ disableProfiles, disableUpdateSettings, disableNetworks: boolean }`.
`Network`: `{ id, range: string; selected: boolean; domains: string[]; resolvedIps: Record<string, string[]> }`.
`ForwardingRule`: `{ protocol: string; destinationPort: PortInfo; translatedAddress, translatedHostname: string; translatedPort: PortInfo }`.
`PortInfo`: `{ port?: number | null; range?: PortRange | null }` (exactly one populated).
`PortRange`: `{ start, end: number }` (inclusive).
`LoginParams`: `{ profileName, username, managementUrl, setupKey, preSharedKey, hostname, hint: string }`.
`LoginResult`: `{ needsSsoLogin: boolean; userCode, verificationUri, verificationUriComplete: string }`.
`WaitSSOParams`: `{ userCode, hostname: string }`. Resolves to the user's email.
`UpParams` / `LogoutParams` / `ProfileRef` / `ConfigParams` / `ActiveProfile`: all `{ profileName, username: string }` (different names but same shape — kept distinct by Wails for clarity).
`DebugBundleParams`: `{ anonymize, systemInfo: boolean; uploadUrl: string; logFileCount: number }`.
`DebugBundleResult`: `{ path, uploadedKey, uploadFailureReason: string }`.
`LogLevel`: `{ level: string }`**uppercase** proto enum name (`"TRACE"`, `"DEBUG"`, `"INFO"`, `"WARN"`, `"ERROR"`, `"PANIC"`, `"FATAL"`).
`UpdateResult`: `{ success: boolean; errorMsg: string }`.
`UpdateAvailable`: `{ version: string; enforced: boolean }`.
`UpdateProgress`: `{ action: string; version: string }`.