From af40ee52f81547bb0adf249c5b7feb457ec8f5e5 Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Tue, 12 May 2026 21:40:29 +0200 Subject: [PATCH] [client/ui] Auto-reconnect tray profile switch when daemon was active MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Picking a profile from the tray submenu only ran SwitchProfile on the daemon, so the in-flight retry loop kept dialing the previous profile's management server. The fix is to follow up Switch with Down+Up, but only when the daemon was actively trying to be online — Connected or Connecting. Idle / NeedsLogin / LoginFailed / SessionExpired stay as deliberate waiting points so a profile pick doesn't surprise the user with an SSO redirect or flip an intentionally offline daemon online. The decision table lives in the switchProfile godoc. --- client/ui/tray.go | 47 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/client/ui/tray.go b/client/ui/tray.go index d5d3a515c..539a945b7 100644 --- a/client/ui/tray.go +++ b/client/ui/tray.go @@ -753,7 +753,33 @@ func (t *Tray) loadProfiles() { // switchProfile runs the daemon RPC in a goroutine so the menu click // returns immediately, then reloads the submenu to move the checkmark. +// +// Reconnect policy by previous daemon status: +// +// ┌─────────────────┬──────────────────────┬───────────────────────────────────┐ +// │ Previous status │ Tray action │ Rationale │ +// ├─────────────────┼──────────────────────┼───────────────────────────────────┤ +// │ Connected │ Switch + Down + Up │ Reconnect with the new profile. │ +// │ Connecting │ Switch + Down + Up │ Stop the retry loop still dialing │ +// │ │ │ the old management server, then │ +// │ │ │ restart with new config. │ +// │ Idle │ Switch only │ User chose to be offline; don't │ +// │ │ │ silently flip the daemon online. │ +// │ NeedsLogin │ Switch only │ Login needs interactive SSO; let │ +// │ LoginFailed │ Switch only │ the user trigger the next step. │ +// │ SessionExpired │ Switch only │ │ +// └─────────────────┴──────────────────────┴───────────────────────────────────┘ +// +// Rule of thumb: auto-reconnect only when the daemon was actively trying +// to be online (Connected or Connecting). Any other state is a deliberate +// waiting point — keep the user in control of the next action. func (t *Tray) switchProfile(name string) { + t.mu.Lock() + prevStatus := t.lastStatus + t.mu.Unlock() + wasActive := strings.EqualFold(prevStatus, statusConnected) || + strings.EqualFold(prevStatus, statusConnecting) + go func() { ctx, cancel := context.WithCancel(context.Background()) defer cancel() @@ -763,7 +789,8 @@ func (t *Tray) switchProfile(name string) { log.Errorf("get current user: %v", err) return } - log.Infof("tray switchProfile: sending SwitchProfile RPC profile=%q user=%q", name, username) + log.Infof("tray switchProfile: sending SwitchProfile RPC profile=%q user=%q prevStatus=%q wasActive=%v", + name, username, prevStatus, wasActive) if err := t.svc.Profiles.Switch(ctx, services.ProfileRef{ ProfileName: name, Username: username, @@ -773,6 +800,24 @@ func (t *Tray) switchProfile(name string) { return } log.Infof("tray switchProfile: SwitchProfile RPC succeeded profile=%q", name) + + if wasActive { + // Stop the in-flight (or established) connection that's still + // pointing at the previous profile's management server, then + // bring it back up against the new profile. + log.Infof("tray switchProfile: was active (%s), reconnecting with new profile %q", prevStatus, name) + if err := t.svc.Connection.Down(ctx); err != nil { + log.Errorf("tray switchProfile: Down failed: %v", err) + } + if err := t.svc.Connection.Up(ctx, services.UpParams{ + ProfileName: name, + Username: username, + }); err != nil { + log.Errorf("tray switchProfile: Up failed: %v", err) + t.notifyError(fmt.Sprintf("Failed to reconnect with %s", name)) + } + } + t.loadProfiles() }() }