The Wails notifications service connects to the D-Bus session bus in its
ServiceStartup, which Wails runs synchronously inside app.Run. daemonFeed.Watch
was started before app.Run, so the first daemon SubscribeEvents message (which
replays the cached available-update state) fanned out to the tray's update-state
listener and fired an OS notification before that startup ran. The notifier's
*dbus.Conn was still nil, so SendNotification nil-dereferenced deep in godbus and
the panic was fatal to the whole process (observed on Linux Mint).
Move daemonFeed.Watch into the ApplicationStarted hook so it runs after the
service-startup loop, and route every notification send through a new
safeSendNotification helper that recovers from a panic and logs it, so a broken
or unavailable notification bus degrades to a skipped toast instead of crashing.
Use the shared logrus logger alias and carry the JS origin in a dedicated
"ui" log field instead of inlining a [ui ...] tag in the message, keeping
frontend logs distinct from the Go-caller source.
The Wails v3 tray is a pure DBus StatusNotifierItem implementation and no
longer links libappindicator (a Fyne-era dependency). Drop the
libayatana-appindicator runtime and build deps, and move the rpm package
and dev-dependency docs onto the GTK4 / WebKitGTK 6.0 stack that the
default (non-gtk3) build actually links against.
Replace the doc comments above the empty no-op stubs (bindTrayClick,
startTrayTheme, noopSessionWatcher.Dismiss/Close) with short in-body
comments explaining why each is empty.
SwitchProfile now publishes the same profile-list-changed event that
AddProfile/RemoveProfile already emit. The daemon emits no dedicated
profile RPC event, and the React ProfileContext only refreshes on
EventProfileChanged (unlike the tray, which also re-fetches on every
status-string transition via loadProfiles). So a CLI-driven
"netbird down; profile select X; netbird up" refreshed the tray (the
down/up status flips trigger loadProfiles) but left the React profile
dropdown stale, since the select path never surfaced an event.
Publishing the marked INFO/SYSTEM event from SwitchProfile closes that
gap: dispatchSystemEvent re-emits EventProfileChanged, which
ProfileContext.refresh already subscribes to. No proto change.
* [client] Surface session deadline rejections via SystemEvent and add timer arm debug logs
When sessionwatch.Watcher.Update rejects a deadline (pre-epoch, too far
in the future, or past the clock-skew tolerance) it silently zeroes the
status recorder, leaving the UI with no "expires in" row and no
indication of why. Publish a SystemEvent_ERROR on the AUTHENTICATION
channel so the rejection appears in the UI event feed and the user
knows re-login may be required.
Also add Debugf log lines in armTimerLocked so that warning and
final-warning timer fire-times are visible in logs without having to
add instrumentation after the fact.
https://claude.ai/code/session_01Y3bQoNgcVjTD4zDTvv7a8u
* [client] Remove verbose arm-timer debug logs from sessionwatch
The per-arm Debugf lines added noise on every deadline update.
Rejection logging already happens at the call site in engine_authsession.go;
the watcher itself needs no extra instrumentation.
https://claude.ai/code/session_01Y3bQoNgcVjTD4zDTvv7a8u
* [client] Leave userMessage empty on deadline-rejected event; use metadata key
Daemon-layer PublishEvent userMessage strings are not localized — the UI
reads metadata keys and builds its own locale-aware copy (same pattern
as the session-warning events in event.go). Drop the hardcoded English
sentence from the deadline-rejected event and instead surface the
rejection reason via a new MetaSessionDeadlineRejected metadata key so
the UI can detect and localize it.
https://claude.ai/code/session_01Y3bQoNgcVjTD4zDTvv7a8u
* [client] Revert silent deadline-rejected event; restore userMessage
MetaSessionDeadlineRejected had no UI consumer: the tray only does
metadata-driven localisation for MetaSessionWarning events; all other
SystemEvents display userMessage directly (tray_events.go). Leaving
userMessage empty made the rejection invisible to the user.
Restore the English userMessage so the generic event path shows
something, and remove the unused MetaSessionDeadlineRejected constant.
https://claude.ai/code/session_01Y3bQoNgcVjTD4zDTvv7a8u
* [client] Localize session deadline rejected notification via metadata key
Follow the same pattern as session-warning events: the daemon emits an
empty userMessage and puts the signal in a typed metadata key
(MetaSessionDeadlineRejected); the UI tray detects the key and builds a
locale-aware OS notification from i18n strings.
Changes:
- sessionwatch/event.go: add MetaSessionDeadlineRejected constant
- engine_authsession.go: empty userMessage, use the new metadata key
- ui/authsession/warning.go: re-export MetaDeadlineRejected for UI consumers
- ui/tray_events.go: gate on isDeadlineRejected alongside isSessionWarning;
new branch calls t.notify with localized title/body
- i18n locales (en/de/hu): add notify.sessionDeadlineRejected.{title,body}
https://claude.ai/code/session_01Y3bQoNgcVjTD4zDTvv7a8u
---------
Co-authored-by: Claude <noreply@anthropic.com>
logoutFromProfile failed hard when the management server returned NotFound
(peer already deleted from the dashboard), blocking both profile logout and
profile removal. Treat NotFound as success — the peer is already gone, so
deregistering it is already satisfied.
Also drop the user-side per-profile state file on logout. The account email is
sourced from <profile>.state.json (written by the CLI after SSO login), which
the root daemon can't reach, so logout left a stale email showing in the UI.
Connection.Logout now removes it from the UI process after a successful logout;
the next SSO login recreates it.
On minimal window managers (fluxbox et al, the in-process XEmbed-tray
path) the WM neither centers small windows nor restores their position
across a hide -> show round-trip, so the main, Settings, and dialog
windows opened in the top-left corner instead of centered.
These windows are created Hidden, so Wails' Linux/GTK4 backend skips its
post-Show centering pass (gated on !Hidden) and InitialPosition has no
effect on an unrealized window. Re-center from Go after Show, gated on
the minimal-WM environment via a recenterOnShow predicate (set to
xembedTrayAvailable on Linux, nil on macOS/Windows where the WM handles
placement). centerWhenReady polls from a background goroutine until the
move actually lands -- Center() moves via raw X11, which no-ops while the
GdkSurface is still nil and GTK4 realizes it asynchronously after Show().
Also reorder xembed_host_linux.go so the static helpers (xembedTrayAvailable,
goMenuItemClicked) sit at the end, after the constructor and methods.
Previously the SyncResponse was persisted to syncStore before
updateNetworkMap() ran. If applying the network map failed, the engine
persisted state it never applied, so GetLatestSyncResponse() could return
stale/unapplied state. Move the persistence into the post-apply success
path so the persisted response always reflects what the engine applied.
The Profiles and Exit Node submenus (and the About version/Update rows)
stopped reflecting changes on KDE/Plasma: after the first profile switch
the menu froze on its initial snapshot, and "Manage Profiles" — plus the
profile rows themselves — stopped responding to clicks entirely.
Root cause (confirmed via dbus-monitor): Plasma's StatusNotifierItem host
caches a submenu's layout the first time it is opened (GetLayout for that
submenu id) and never re-fetches it on a LayoutUpdated(parent=0) signal.
The old submenu.Clear()+Add() repaint allocated fresh monotonic item ids
each time but reused the same submenu container id, so Plasma kept showing
the stale snapshot and, on click, sent the stale ids back — which the
rebuilt itemMap no longer knew, silently no-op'ing the click.
Fix: route every dynamic tray-menu change through a new relayoutMenu that
rebuilds the whole tree (buildMenu + repaint cached state + a single
SetMenu), allocating brand-new submenu container ids. Plasma treats those
as unseen and re-queries them on next open, fixing both the stale paint
and the dead clicks. loadProfiles/refreshExitNodes now cache their rows
and drive relayoutMenu; the update row goes through a new onMenuChange
hook; the daemon-version row relayouts too. relayoutMenu is serialised by
menuMu and the fill*Submenu helpers are pure UI (no fetch, no SetMenu) so
it never recurses. The whole-tree SetMenu also subsumes the prior darwin
detached-NSMenu workaround.
* Adds heuristic to detect an edge case on Linux where a system has configured logrotate as a separate service to rotate log files which would mangle our client log files. If we detect logrotate being configured for netbird, we disable our rotation.
* Adds new env var to disable log rotation: NB_LOG_DISABLE_ROTATION
* Adds compressed and plain logrotate files to debug bundle.
* Replaces lumberjack with timberjack (maintained fork with bug fixes and extra features).
* Clarifies which daemon version is running in the bundle stats.
* Change logging for client service status to console
The daemon emits no dedicated profile-changed RPC event, and a profile
add/remove doesn't move the connection status, so the UI's SubscribeStatus
path never fired for CLI-driven `netbird profile add|remove` (and the tray's
iconChanged guard would swallow it anyway). The tray menu and the React
profile list stayed stale until the next status-string transition.
AddProfile/RemoveProfile now publish a marked INFO/SYSTEM event over
SubscribeEvents (metadata kind=profile-list-changed, empty userMessage so it
stays silent). The UI's dispatchSystemEvent recognises the marker and
re-emits the existing EventProfileChanged, which the tray's loadProfiles and
React's ProfileContext.refresh already subscribe to — so both surfaces
refresh from a single signal that originates in the shared daemon handler
(covering both CLI and UI-initiated removals). No proto change.
Also drop a stray, build-breaking `app.Updater` line in main.go.
startLogin held both guards (the module-level loginInFlight and the
caller's React-level loginGuard) across the post-failure errorDialog
await. The native Windows MessageBox disables its parent for its whole
lifetime while the main window's WindowClosing hook hides instead of
closing, so the dialog promise can outlive the click — and even a clean
dismissal kept the guards held until the promise settled. Until then
every later Connect click and tray trigger-login was silently dropped at
the guard check, so the only way back was a client restart.
Release both guards the instant the flow itself settles, before the
dialog: startLogin now takes an onSettled callback fired in its finally
(driveLogin releases loginGuard through it), and the errorDialog await
moved out of the try/finally so no guard is ever gated on the dialog.