From 32f62f3ed8d69ea759333a4e68d2ffa59d5f6f10 Mon Sep 17 00:00:00 2001 From: Eduard Gert Date: Mon, 18 May 2026 10:38:13 +0200 Subject: [PATCH] add profile switched event --- client/ui/main.go | 1 + client/ui/services/peers.go | 7 +++++++ client/ui/services/profileswitcher.go | 12 ++++++++++++ 3 files changed, 20 insertions(+) diff --git a/client/ui/main.go b/client/ui/main.go index 2270be8c0..db2ea70ec 100644 --- a/client/ui/main.go +++ b/client/ui/main.go @@ -59,6 +59,7 @@ func (s *stringList) Set(v string) error { func init() { application.RegisterEvent[services.Status](services.EventStatus) application.RegisterEvent[services.SystemEvent](services.EventSystem) + application.RegisterEvent[services.ProfileRef](services.EventProfileChanged) application.RegisterEvent[updater.State](updater.EventStateChanged) application.RegisterEvent[preferences.UIPreferences](preferences.EventPreferencesChanged) } diff --git a/client/ui/services/peers.go b/client/ui/services/peers.go index cbc7c35a7..5503b2134 100644 --- a/client/ui/services/peers.go +++ b/client/ui/services/peers.go @@ -27,6 +27,13 @@ const ( // forwarded here to updater.Holder.OnSystemEvent so the typed update // state can be maintained without a second daemon subscription. EventSystem = "netbird:event" + // EventProfileChanged fires after ProfileSwitcher.SwitchActive completes + // a daemon-side switch. The payload is the new ProfileRef. Both tray + // and React subscribers refresh their profile views off this so a flip + // driven from one surface (tray menu, settings page) paints in the + // others without polling. The daemon itself does not emit a profile + // event, so this is the only signal that closes the gap. + EventProfileChanged = "netbird:profile:changed" // StatusDaemonUnavailable is the synthetic Status the UI emits when the // daemon's gRPC socket is unreachable (daemon not running, socket diff --git a/client/ui/services/profileswitcher.go b/client/ui/services/profileswitcher.go index e2cb492df..55ca0efad 100644 --- a/client/ui/services/profileswitcher.go +++ b/client/ui/services/profileswitcher.go @@ -57,6 +57,9 @@ type ProfileSwitcher struct { } // NewProfileSwitcher creates a ProfileSwitcher backed by the given services. +// EventProfileChanged is emitted via peers.emitter (same package), so React +// refreshes after a tray-driven switch and vice versa — the daemon does +// not emit a dedicated profile event. func NewProfileSwitcher(profiles *Profiles, connection *Connection, peers *Peers) *ProfileSwitcher { return &ProfileSwitcher{profiles: profiles, connection: connection, peers: peers} } @@ -124,5 +127,14 @@ func (s *ProfileSwitcher) SwitchActive(ctx context.Context, p ProfileRef) error } } + // Fan out the switch to every UI surface. The daemon does not emit a + // profile event, so without this the React ProfileContext stays on the + // old profile after a tray-initiated switch (and the tray's profile + // submenu would lag a React-initiated one, except the tray rebuilds on + // every status transition). + if s.peers != nil && s.peers.emitter != nil { + s.peers.emitter.Emit(EventProfileChanged, p) + } + return nil }