[client/ui] Refresh profile list in tray + UI on CLI profile add/remove

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.
This commit is contained in:
Zoltán Papp
2026-06-04 16:03:22 +02:00
parent cba4a8a63b
commit e298747203
2 changed files with 50 additions and 0 deletions

View File

@@ -1864,6 +1864,8 @@ func (s *Server) AddProfile(ctx context.Context, msg *proto.AddProfileRequest) (
return nil, fmt.Errorf("failed to create profile: %w", err)
}
s.publishProfileListChanged(msg.ProfileName)
return &proto.AddProfileResponse{}, nil
}
@@ -1885,9 +1887,32 @@ func (s *Server) RemoveProfile(ctx context.Context, msg *proto.RemoveProfileRequ
return nil, fmt.Errorf("failed to remove profile: %w", err)
}
s.publishProfileListChanged(msg.ProfileName)
return &proto.RemoveProfileResponse{}, nil
}
// publishProfileListChanged nudges the desktop UI to refresh its profile list
// after a CLI-driven add/remove. The daemon exposes no dedicated
// profile-changed RPC event, and a profile add/remove doesn't move the
// connection status, so the UI's SubscribeStatus path never fires for it (and
// the tray's status-string guard would swallow it anyway). Instead we publish
// a marked INFO/SYSTEM event over SubscribeEvents: the UI's dispatchSystemEvent
// recognises the metadata "kind" marker and translates it into its internal
// profile-changed signal that both the tray menu and the React profile views
// already subscribe to (see client/ui/services/daemon_feed.go,
// MetadataKindProfileListChanged). userMessage is intentionally empty so this
// stays a silent refresh signal rather than a user-facing notification.
func (s *Server) publishProfileListChanged(profileName string) {
s.statusRecorder.PublishEvent(
proto.SystemEvent_INFO,
proto.SystemEvent_SYSTEM,
"Profile list changed",
"",
map[string]string{"kind": "profile-list-changed", "profile": profileName},
)
}
// ListProfiles lists all profiles in the daemon.
func (s *Server) ListProfiles(ctx context.Context, msg *proto.ListProfilesRequest) (*proto.ListProfilesResponse, error) {
s.mutex.Lock()

View File

@@ -46,6 +46,21 @@ const (
// tray (Go side) so the frontend stays passive on this flow.
EventSessionWarning = "netbird:session:warning"
// MetadataKindProfileListChanged is the SystemEvent.metadata["kind"]
// marker the daemon stamps on the INFO/SYSTEM event it publishes after a
// CLI-driven AddProfile / RemoveProfile (the daemon emits no dedicated
// profile RPC event). dispatchSystemEvent recognises it and re-emits the
// existing EventProfileChanged so the tray and React profile views refresh
// — closing the gap the SubscribeStatus path can't, since a profile
// add/remove doesn't change the daemon's status string (the tray's
// iconChanged guard would swallow it). The daemon side hard-codes the same
// string literal in client/server/server.go (client/server cannot import
// this UI package).
MetadataKindProfileListChanged = "profile-list-changed"
// metadataKindKey is the SystemEvent.metadata key the "kind" marker lives
// under. Kept in sync with the daemon-side literal in client/server.
metadataKindKey = "kind"
// StatusDaemonUnavailable is the synthetic Status the UI emits when the
// daemon's gRPC socket is unreachable (daemon not running, socket
// permission, etc.). Real daemon statuses come straight from
@@ -526,6 +541,16 @@ func (s *DaemonFeed) subscribeAndStreamEvents(ctx context.Context) error {
func (s *DaemonFeed) dispatchSystemEvent(ev *proto.SystemEvent) {
se := systemEventFromProto(ev)
log.Infof("backend event: system severity=%s category=%s msg=%q", se.Severity, se.Category, se.UserMessage)
// A CLI-driven profile add/remove publishes a marked SYSTEM event purely
// to nudge the UI's profile views. Translate it into the existing
// EventProfileChanged (which the tray's loadProfiles and React's
// ProfileContext.refresh already subscribe to) and stop — it's an internal
// refresh signal, not a user-facing notification, so it must not reach the
// Recent Events list or fire an OS toast.
if se.Metadata[metadataKindKey] == MetadataKindProfileListChanged {
s.emitter.Emit(EventProfileChanged, ProfileRef{})
return
}
s.emitter.Emit(EventDaemonNotification, se)
if warn, ok := authsession.WarningFromMetadata(se.Metadata); ok {
s.emitter.Emit(EventSessionWarning, warn)