mirror of
https://github.com/netbirdio/netbird.git
synced 2026-05-14 20:59:54 +00:00
[client/ui] Unify profile-switching logic in ProfileSwitcher service
Both the tray and the React Profiles page previously had separate switching logic: the tray applied a status-aware reconnect policy (Down for error states, Up only when previously Connected/Connecting), while the React page always called Switch + Up unconditionally with no Down for LoginFailed/NeedsLogin/SessionExpired. Introduce a single ProfileSwitcher service that encapsulates the full reconnect policy. SwitchActive queries the current daemon status, calls Switch, and launches Down/Up in a background goroutine so the caller returns immediately after the Switch RPC completes. Both the tray and the React Profiles page now delegate to this service. Export the daemon status string constants (StatusConnected, etc.) from the services package so tray.go no longer duplicates them as private constants.
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
// @ts-check
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
@@ -14,17 +15,28 @@ import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Cr
|
||||
// @ts-ignore: Unused imports
|
||||
import * as $models from "./models.js";
|
||||
|
||||
export function Down(): $CancellablePromise<void> {
|
||||
/**
|
||||
* @returns {$CancellablePromise<void>}
|
||||
*/
|
||||
export function Down() {
|
||||
return $Call.ByID(70044537);
|
||||
}
|
||||
|
||||
export function Login(p: $models.LoginParams): $CancellablePromise<$models.LoginResult> {
|
||||
return $Call.ByID(252661358, p).then(($result: any) => {
|
||||
/**
|
||||
* @param {$models.LoginParams} p
|
||||
* @returns {$CancellablePromise<$models.LoginResult>}
|
||||
*/
|
||||
export function Login(p) {
|
||||
return $Call.ByID(252661358, p).then(/** @type {($result: any) => any} */(($result) => {
|
||||
return $$createType0($result);
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
export function Logout(p: $models.LogoutParams): $CancellablePromise<void> {
|
||||
/**
|
||||
* @param {$models.LogoutParams} p
|
||||
* @returns {$CancellablePromise<void>}
|
||||
*/
|
||||
export function Logout(p) {
|
||||
return $Call.ByID(3824847887, p);
|
||||
}
|
||||
|
||||
@@ -34,16 +46,26 @@ export function Logout(p: $models.LogoutParams): $CancellablePromise<void> {
|
||||
* the same way as the legacy UI — WebKitGTK's window.open is blocked by the
|
||||
* embedded webview, and asking the user to copy/paste defeats the point of
|
||||
* SSO. Honors $BROWSER first, then falls back to the platform default.
|
||||
* @param {string} url
|
||||
* @returns {$CancellablePromise<void>}
|
||||
*/
|
||||
export function OpenURL(url: string): $CancellablePromise<void> {
|
||||
export function OpenURL(url) {
|
||||
return $Call.ByID(3786555598, url);
|
||||
}
|
||||
|
||||
export function Up(p: $models.UpParams): $CancellablePromise<void> {
|
||||
/**
|
||||
* @param {$models.UpParams} p
|
||||
* @returns {$CancellablePromise<void>}
|
||||
*/
|
||||
export function Up(p) {
|
||||
return $Call.ByID(3381092588, p);
|
||||
}
|
||||
|
||||
export function WaitSSOLogin(p: $models.WaitSSOParams): $CancellablePromise<string> {
|
||||
/**
|
||||
* @param {$models.WaitSSOParams} p
|
||||
* @returns {$CancellablePromise<string>}
|
||||
*/
|
||||
export function WaitSSOLogin(p) {
|
||||
return $Call.ByID(1751351500, p);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-check
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
@@ -14,19 +15,30 @@ import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Cr
|
||||
// @ts-ignore: Unused imports
|
||||
import * as $models from "./models.js";
|
||||
|
||||
export function Bundle(p: $models.DebugBundleParams): $CancellablePromise<$models.DebugBundleResult> {
|
||||
return $Call.ByID(617551238, p).then(($result: any) => {
|
||||
/**
|
||||
* @param {$models.DebugBundleParams} p
|
||||
* @returns {$CancellablePromise<$models.DebugBundleResult>}
|
||||
*/
|
||||
export function Bundle(p) {
|
||||
return $Call.ByID(617551238, p).then(/** @type {($result: any) => any} */(($result) => {
|
||||
return $$createType0($result);
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
export function GetLogLevel(): $CancellablePromise<$models.LogLevel> {
|
||||
return $Call.ByID(3832950014).then(($result: any) => {
|
||||
/**
|
||||
* @returns {$CancellablePromise<$models.LogLevel>}
|
||||
*/
|
||||
export function GetLogLevel() {
|
||||
return $Call.ByID(3832950014).then(/** @type {($result: any) => any} */(($result) => {
|
||||
return $$createType1($result);
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
export function SetLogLevel(lvl: $models.LogLevel): $CancellablePromise<void> {
|
||||
/**
|
||||
* @param {$models.LogLevel} lvl
|
||||
* @returns {$CancellablePromise<void>}
|
||||
*/
|
||||
export function SetLogLevel(lvl) {
|
||||
return $Call.ByID(4122411498, lvl);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-check
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
@@ -17,11 +18,12 @@ import * as $models from "./models.js";
|
||||
/**
|
||||
* List returns the current set of forwarding rules from the daemon's
|
||||
* reverse proxy. The frontend renders these as the "exposed services" list.
|
||||
* @returns {$CancellablePromise<$models.ForwardingRule[]>}
|
||||
*/
|
||||
export function List(): $CancellablePromise<$models.ForwardingRule[]> {
|
||||
return $Call.ByID(3831092172).then(($result: any) => {
|
||||
export function List() {
|
||||
return $Call.ByID(3831092172).then(/** @type {($result: any) => any} */(($result) => {
|
||||
return $$createType1($result);
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-check
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
@@ -6,6 +7,7 @@ import * as Debug from "./debug.js";
|
||||
import * as Forwarding from "./forwarding.js";
|
||||
import * as Networks from "./networks.js";
|
||||
import * as Peers from "./peers.js";
|
||||
import * as ProfileSwitcher from "./profileswitcher.js";
|
||||
import * as Profiles from "./profiles.js";
|
||||
import * as Settings from "./settings.js";
|
||||
import * as Update from "./update.js";
|
||||
@@ -15,6 +17,7 @@ export {
|
||||
Forwarding,
|
||||
Networks,
|
||||
Peers,
|
||||
ProfileSwitcher,
|
||||
Profiles,
|
||||
Settings,
|
||||
Update
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,4 @@
|
||||
// @ts-check
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
@@ -14,17 +15,28 @@ import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Cr
|
||||
// @ts-ignore: Unused imports
|
||||
import * as $models from "./models.js";
|
||||
|
||||
export function Deselect(p: $models.SelectNetworksParams): $CancellablePromise<void> {
|
||||
/**
|
||||
* @param {$models.SelectNetworksParams} p
|
||||
* @returns {$CancellablePromise<void>}
|
||||
*/
|
||||
export function Deselect(p) {
|
||||
return $Call.ByID(3382210947, p);
|
||||
}
|
||||
|
||||
export function List(): $CancellablePromise<$models.Network[]> {
|
||||
return $Call.ByID(1550842096).then(($result: any) => {
|
||||
/**
|
||||
* @returns {$CancellablePromise<$models.Network[]>}
|
||||
*/
|
||||
export function List() {
|
||||
return $Call.ByID(1550842096).then(/** @type {($result: any) => any} */(($result) => {
|
||||
return $$createType1($result);
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
export function Select(p: $models.SelectNetworksParams): $CancellablePromise<void> {
|
||||
/**
|
||||
* @param {$models.SelectNetworksParams} p
|
||||
* @returns {$CancellablePromise<void>}
|
||||
*/
|
||||
export function Select(p) {
|
||||
return $Call.ByID(1339338400, p);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-check
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
@@ -17,11 +18,12 @@ import * as $models from "./models.js";
|
||||
|
||||
/**
|
||||
* Get returns the current daemon status snapshot.
|
||||
* @returns {$CancellablePromise<$models.Status>}
|
||||
*/
|
||||
export function Get(): $CancellablePromise<$models.Status> {
|
||||
return $Call.ByID(3266051360).then(($result: any) => {
|
||||
export function Get() {
|
||||
return $Call.ByID(3266051360).then(/** @type {($result: any) => any} */(($result) => {
|
||||
return $$createType0($result);
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -37,8 +39,9 @@ export function Get(): $CancellablePromise<$models.Status> {
|
||||
*
|
||||
* Safe to call once at boot; both loops self-restart on stream errors
|
||||
* via exponential backoff.
|
||||
* @returns {$CancellablePromise<void>}
|
||||
*/
|
||||
export function Watch(): $CancellablePromise<void> {
|
||||
export function Watch() {
|
||||
return $Call.ByID(2799871735);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-check
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
@@ -14,35 +15,55 @@ import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Cr
|
||||
// @ts-ignore: Unused imports
|
||||
import * as $models from "./models.js";
|
||||
|
||||
export function Add(p: $models.ProfileRef): $CancellablePromise<void> {
|
||||
/**
|
||||
* @param {$models.ProfileRef} p
|
||||
* @returns {$CancellablePromise<void>}
|
||||
*/
|
||||
export function Add(p) {
|
||||
return $Call.ByID(722930578, p);
|
||||
}
|
||||
|
||||
export function GetActive(): $CancellablePromise<$models.ActiveProfile> {
|
||||
return $Call.ByID(3458449443).then(($result: any) => {
|
||||
/**
|
||||
* @returns {$CancellablePromise<$models.ActiveProfile>}
|
||||
*/
|
||||
export function GetActive() {
|
||||
return $Call.ByID(3458449443).then(/** @type {($result: any) => any} */(($result) => {
|
||||
return $$createType0($result);
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
export function List(username: string): $CancellablePromise<$models.Profile[]> {
|
||||
return $Call.ByID(3702185167, username).then(($result: any) => {
|
||||
/**
|
||||
* @param {string} username
|
||||
* @returns {$CancellablePromise<$models.Profile[]>}
|
||||
*/
|
||||
export function List(username) {
|
||||
return $Call.ByID(3702185167, username).then(/** @type {($result: any) => any} */(($result) => {
|
||||
return $$createType2($result);
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
export function Remove(p: $models.ProfileRef): $CancellablePromise<void> {
|
||||
/**
|
||||
* @param {$models.ProfileRef} p
|
||||
* @returns {$CancellablePromise<void>}
|
||||
*/
|
||||
export function Remove(p) {
|
||||
return $Call.ByID(2365690315, p);
|
||||
}
|
||||
|
||||
export function Switch(p: $models.ProfileRef): $CancellablePromise<void> {
|
||||
/**
|
||||
* @param {$models.ProfileRef} p
|
||||
* @returns {$CancellablePromise<void>}
|
||||
*/
|
||||
export function Switch(p) {
|
||||
return $Call.ByID(3209858855, p);
|
||||
}
|
||||
|
||||
/**
|
||||
* Username returns the OS username the daemon expects for profile lookups.
|
||||
* The frontend calls this once at boot and reuses the result.
|
||||
* @returns {$CancellablePromise<string>}
|
||||
*/
|
||||
export function Username(): $CancellablePromise<string> {
|
||||
export function Username() {
|
||||
return $Call.ByID(262345647);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
// @ts-check
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* ProfileSwitcher encapsulates the full profile-switching reconnect policy so
|
||||
* both the tray and the React frontend use identical logic.
|
||||
*
|
||||
* Reconnect policy:
|
||||
*
|
||||
* ┌─────────────────┬──────────────────────┬────────────────────────────────────┐
|
||||
* │ Previous status │ Action │ Rationale │
|
||||
* ├─────────────────┼──────────────────────┼────────────────────────────────────┤
|
||||
* │ Connected │ Switch + Down + Up │ Reconnect with the new profile. │
|
||||
* │ Connecting │ Switch + Down + Up │ Stop old retry loop, restart. │
|
||||
* │ NeedsLogin │ Switch + Down │ Clear stale error; user logs in. │
|
||||
* │ LoginFailed │ Switch + Down │ Clear stale error; user logs in. │
|
||||
* │ SessionExpired │ Switch + Down │ Clear stale error; user logs in. │
|
||||
* │ Idle │ Switch only │ User chose offline; don't connect. │
|
||||
* └─────────────────┴──────────────────────┴────────────────────────────────────┘
|
||||
* @module
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as $models from "./models.js";
|
||||
|
||||
/**
|
||||
* SwitchActive switches to the named profile applying the reconnect policy.
|
||||
* It returns after the Switch RPC completes so the caller can refresh its UI
|
||||
* immediately; Down and Up run in a background goroutine.
|
||||
* @param {$models.ProfileRef} p
|
||||
* @returns {$CancellablePromise<void>}
|
||||
*/
|
||||
export function SwitchActive(p) {
|
||||
return $Call.ByID(4025913103, p);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-check
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
@@ -14,19 +15,30 @@ import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Cr
|
||||
// @ts-ignore: Unused imports
|
||||
import * as $models from "./models.js";
|
||||
|
||||
export function GetConfig(p: $models.ConfigParams): $CancellablePromise<$models.Config> {
|
||||
return $Call.ByID(59246988, p).then(($result: any) => {
|
||||
/**
|
||||
* @param {$models.ConfigParams} p
|
||||
* @returns {$CancellablePromise<$models.Config>}
|
||||
*/
|
||||
export function GetConfig(p) {
|
||||
return $Call.ByID(59246988, p).then(/** @type {($result: any) => any} */(($result) => {
|
||||
return $$createType0($result);
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
export function GetFeatures(): $CancellablePromise<$models.Features> {
|
||||
return $Call.ByID(2056724965).then(($result: any) => {
|
||||
/**
|
||||
* @returns {$CancellablePromise<$models.Features>}
|
||||
*/
|
||||
export function GetFeatures() {
|
||||
return $Call.ByID(2056724965).then(/** @type {($result: any) => any} */(($result) => {
|
||||
return $$createType1($result);
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
export function SetConfig(p: $models.SetConfigParams): $CancellablePromise<void> {
|
||||
/**
|
||||
* @param {$models.SetConfigParams} p
|
||||
* @returns {$CancellablePromise<void>}
|
||||
*/
|
||||
export function SetConfig(p) {
|
||||
return $Call.ByID(26939944, p);
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-check
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
@@ -14,10 +15,13 @@ import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Cr
|
||||
// @ts-ignore: Unused imports
|
||||
import * as $models from "./models.js";
|
||||
|
||||
export function GetInstallerResult(): $CancellablePromise<$models.UpdateResult> {
|
||||
return $Call.ByID(2533624807).then(($result: any) => {
|
||||
/**
|
||||
* @returns {$CancellablePromise<$models.UpdateResult>}
|
||||
*/
|
||||
export function GetInstallerResult() {
|
||||
return $Call.ByID(2533624807).then(/** @type {($result: any) => any} */(($result) => {
|
||||
return $$createType0($result);
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -26,15 +30,19 @@ export function GetInstallerResult(): $CancellablePromise<$models.UpdateResult>
|
||||
* Fyne UI's app.Quit() in showInstallerResult. Schedules the actual exit
|
||||
* off the calling goroutine so the JS-side caller's response can return
|
||||
* before the runtime tears down.
|
||||
* @returns {$CancellablePromise<void>}
|
||||
*/
|
||||
export function Quit(): $CancellablePromise<void> {
|
||||
export function Quit() {
|
||||
return $Call.ByID(409602657);
|
||||
}
|
||||
|
||||
export function Trigger(): $CancellablePromise<$models.UpdateResult> {
|
||||
return $Call.ByID(166270378).then(($result: any) => {
|
||||
/**
|
||||
* @returns {$CancellablePromise<$models.UpdateResult>}
|
||||
*/
|
||||
export function Trigger() {
|
||||
return $Call.ByID(166270378).then(/** @type {($result: any) => any} */(($result) => {
|
||||
return $$createType0($result);
|
||||
});
|
||||
}));
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
@@ -1,3 +1,4 @@
|
||||
// @ts-check
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
// @ts-check
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import { Create as $Create } from "@wailsio/runtime";
|
||||
|
||||
/**
|
||||
* NotificationAction represents an action button for a notification.
|
||||
*/
|
||||
export class NotificationAction {
|
||||
/**
|
||||
* Creates a new NotificationAction instance.
|
||||
* @param {Partial<NotificationAction>} [$$source = {}] - The source object to create the NotificationAction.
|
||||
*/
|
||||
constructor($$source = {}) {
|
||||
if (/** @type {any} */(false)) {
|
||||
/**
|
||||
* @member
|
||||
* @type {string | undefined}
|
||||
*/
|
||||
this["id"] = undefined;
|
||||
}
|
||||
if (/** @type {any} */(false)) {
|
||||
/**
|
||||
* @member
|
||||
* @type {string | undefined}
|
||||
*/
|
||||
this["title"] = undefined;
|
||||
}
|
||||
if (/** @type {any} */(false)) {
|
||||
/**
|
||||
* (macOS-specific)
|
||||
* @member
|
||||
* @type {boolean | undefined}
|
||||
*/
|
||||
this["destructive"] = undefined;
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new NotificationAction instance from a string or object.
|
||||
* @param {any} [$$source = {}]
|
||||
* @returns {NotificationAction}
|
||||
*/
|
||||
static createFrom($$source = {}) {
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
return new NotificationAction(/** @type {Partial<NotificationAction>} */($$parsedSource));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NotificationCategory groups actions for notifications.
|
||||
*/
|
||||
export class NotificationCategory {
|
||||
/**
|
||||
* Creates a new NotificationCategory instance.
|
||||
* @param {Partial<NotificationCategory>} [$$source = {}] - The source object to create the NotificationCategory.
|
||||
*/
|
||||
constructor($$source = {}) {
|
||||
if (/** @type {any} */(false)) {
|
||||
/**
|
||||
* @member
|
||||
* @type {string | undefined}
|
||||
*/
|
||||
this["id"] = undefined;
|
||||
}
|
||||
if (/** @type {any} */(false)) {
|
||||
/**
|
||||
* @member
|
||||
* @type {NotificationAction[] | undefined}
|
||||
*/
|
||||
this["actions"] = undefined;
|
||||
}
|
||||
if (/** @type {any} */(false)) {
|
||||
/**
|
||||
* @member
|
||||
* @type {boolean | undefined}
|
||||
*/
|
||||
this["hasReplyField"] = undefined;
|
||||
}
|
||||
if (/** @type {any} */(false)) {
|
||||
/**
|
||||
* @member
|
||||
* @type {string | undefined}
|
||||
*/
|
||||
this["replyPlaceholder"] = undefined;
|
||||
}
|
||||
if (/** @type {any} */(false)) {
|
||||
/**
|
||||
* @member
|
||||
* @type {string | undefined}
|
||||
*/
|
||||
this["replyButtonTitle"] = undefined;
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new NotificationCategory instance from a string or object.
|
||||
* @param {any} [$$source = {}]
|
||||
* @returns {NotificationCategory}
|
||||
*/
|
||||
static createFrom($$source = {}) {
|
||||
const $$createField1_0 = $$createType1;
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
if ("actions" in $$parsedSource) {
|
||||
$$parsedSource["actions"] = $$createField1_0($$parsedSource["actions"]);
|
||||
}
|
||||
return new NotificationCategory(/** @type {Partial<NotificationCategory>} */($$parsedSource));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NotificationOptions contains configuration for a notification
|
||||
*/
|
||||
export class NotificationOptions {
|
||||
/**
|
||||
* Creates a new NotificationOptions instance.
|
||||
* @param {Partial<NotificationOptions>} [$$source = {}] - The source object to create the NotificationOptions.
|
||||
*/
|
||||
constructor($$source = {}) {
|
||||
if (!("id" in $$source)) {
|
||||
/**
|
||||
* @member
|
||||
* @type {string}
|
||||
*/
|
||||
this["id"] = "";
|
||||
}
|
||||
if (!("title" in $$source)) {
|
||||
/**
|
||||
* @member
|
||||
* @type {string}
|
||||
*/
|
||||
this["title"] = "";
|
||||
}
|
||||
if (/** @type {any} */(false)) {
|
||||
/**
|
||||
* (macOS and Linux only)
|
||||
* @member
|
||||
* @type {string | undefined}
|
||||
*/
|
||||
this["subtitle"] = undefined;
|
||||
}
|
||||
if (/** @type {any} */(false)) {
|
||||
/**
|
||||
* @member
|
||||
* @type {string | undefined}
|
||||
*/
|
||||
this["body"] = undefined;
|
||||
}
|
||||
if (/** @type {any} */(false)) {
|
||||
/**
|
||||
* @member
|
||||
* @type {string | undefined}
|
||||
*/
|
||||
this["categoryId"] = undefined;
|
||||
}
|
||||
if (/** @type {any} */(false)) {
|
||||
/**
|
||||
* @member
|
||||
* @type {{ [_ in string]?: any } | undefined}
|
||||
*/
|
||||
this["data"] = undefined;
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new NotificationOptions instance from a string or object.
|
||||
* @param {any} [$$source = {}]
|
||||
* @returns {NotificationOptions}
|
||||
*/
|
||||
static createFrom($$source = {}) {
|
||||
const $$createField5_0 = $$createType2;
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
if ("data" in $$parsedSource) {
|
||||
$$parsedSource["data"] = $$createField5_0($$parsedSource["data"]);
|
||||
}
|
||||
return new NotificationOptions(/** @type {Partial<NotificationOptions>} */($$parsedSource));
|
||||
}
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = NotificationAction.createFrom;
|
||||
const $$createType1 = $Create.Array($$createType0);
|
||||
const $$createType2 = $Create.Map($Create.Any, $Create.Any);
|
||||
@@ -1,107 +0,0 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import { Create as $Create } from "@wailsio/runtime";
|
||||
|
||||
/**
|
||||
* NotificationAction represents an action button for a notification.
|
||||
*/
|
||||
export class NotificationAction {
|
||||
"id"?: string;
|
||||
"title"?: string;
|
||||
|
||||
/**
|
||||
* (macOS-specific)
|
||||
*/
|
||||
"destructive"?: boolean;
|
||||
|
||||
/** Creates a new NotificationAction instance. */
|
||||
constructor($$source: Partial<NotificationAction> = {}) {
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new NotificationAction instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): NotificationAction {
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
return new NotificationAction($$parsedSource as Partial<NotificationAction>);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NotificationCategory groups actions for notifications.
|
||||
*/
|
||||
export class NotificationCategory {
|
||||
"id"?: string;
|
||||
"actions"?: NotificationAction[];
|
||||
"hasReplyField"?: boolean;
|
||||
"replyPlaceholder"?: string;
|
||||
"replyButtonTitle"?: string;
|
||||
|
||||
/** Creates a new NotificationCategory instance. */
|
||||
constructor($$source: Partial<NotificationCategory> = {}) {
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new NotificationCategory instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): NotificationCategory {
|
||||
const $$createField1_0 = $$createType1;
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
if ("actions" in $$parsedSource) {
|
||||
$$parsedSource["actions"] = $$createField1_0($$parsedSource["actions"]);
|
||||
}
|
||||
return new NotificationCategory($$parsedSource as Partial<NotificationCategory>);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* NotificationOptions contains configuration for a notification
|
||||
*/
|
||||
export class NotificationOptions {
|
||||
"id": string;
|
||||
"title": string;
|
||||
|
||||
/**
|
||||
* (macOS and Linux only)
|
||||
*/
|
||||
"subtitle"?: string;
|
||||
"body"?: string;
|
||||
"categoryId"?: string;
|
||||
"data"?: { [_ in string]?: any };
|
||||
|
||||
/** Creates a new NotificationOptions instance. */
|
||||
constructor($$source: Partial<NotificationOptions> = {}) {
|
||||
if (!("id" in $$source)) {
|
||||
this["id"] = "";
|
||||
}
|
||||
if (!("title" in $$source)) {
|
||||
this["title"] = "";
|
||||
}
|
||||
|
||||
Object.assign(this, $$source);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new NotificationOptions instance from a string or object.
|
||||
*/
|
||||
static createFrom($$source: any = {}): NotificationOptions {
|
||||
const $$createField5_0 = $$createType2;
|
||||
let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source;
|
||||
if ("data" in $$parsedSource) {
|
||||
$$parsedSource["data"] = $$createField5_0($$parsedSource["data"]);
|
||||
}
|
||||
return new NotificationOptions($$parsedSource as Partial<NotificationOptions>);
|
||||
}
|
||||
}
|
||||
|
||||
// Private type creation functions
|
||||
const $$createType0 = NotificationAction.createFrom;
|
||||
const $$createType1 = $Create.Array($$createType0);
|
||||
const $$createType2 = $Create.Map($Create.Any, $Create.Any);
|
||||
@@ -0,0 +1,101 @@
|
||||
// @ts-check
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* Service represents the notifications service
|
||||
* @module
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as $models from "./models.js";
|
||||
|
||||
/**
|
||||
* @returns {$CancellablePromise<boolean>}
|
||||
*/
|
||||
export function CheckNotificationAuthorization() {
|
||||
return $Call.ByID(2216952893);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {$models.NotificationCategory} category
|
||||
* @returns {$CancellablePromise<void>}
|
||||
*/
|
||||
export function RegisterNotificationCategory(category) {
|
||||
return $Call.ByID(2917562919, category);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {$CancellablePromise<void>}
|
||||
*/
|
||||
export function RemoveAllDeliveredNotifications() {
|
||||
return $Call.ByID(3956282340);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {$CancellablePromise<void>}
|
||||
*/
|
||||
export function RemoveAllPendingNotifications() {
|
||||
return $Call.ByID(108821341);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} identifier
|
||||
* @returns {$CancellablePromise<void>}
|
||||
*/
|
||||
export function RemoveDeliveredNotification(identifier) {
|
||||
return $Call.ByID(975691940, identifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} identifier
|
||||
* @returns {$CancellablePromise<void>}
|
||||
*/
|
||||
export function RemoveNotification(identifier) {
|
||||
return $Call.ByID(3966653866, identifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} categoryID
|
||||
* @returns {$CancellablePromise<void>}
|
||||
*/
|
||||
export function RemoveNotificationCategory(categoryID) {
|
||||
return $Call.ByID(2032615554, categoryID);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} identifier
|
||||
* @returns {$CancellablePromise<void>}
|
||||
*/
|
||||
export function RemovePendingNotification(identifier) {
|
||||
return $Call.ByID(3729049703, identifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Public methods that delegate to the implementation.
|
||||
* @returns {$CancellablePromise<boolean>}
|
||||
*/
|
||||
export function RequestNotificationAuthorization() {
|
||||
return $Call.ByID(3933442950);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {$models.NotificationOptions} options
|
||||
* @returns {$CancellablePromise<void>}
|
||||
*/
|
||||
export function SendNotification(options) {
|
||||
return $Call.ByID(3968228732, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {$models.NotificationOptions} options
|
||||
* @returns {$CancellablePromise<void>}
|
||||
*/
|
||||
export function SendNotificationWithActions(options) {
|
||||
return $Call.ByID(1886542847, options);
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
/**
|
||||
* Service represents the notifications service
|
||||
* @module
|
||||
*/
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Create } from "@wailsio/runtime";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore: Unused imports
|
||||
import * as $models from "./models.js";
|
||||
|
||||
export function CheckNotificationAuthorization(): $CancellablePromise<boolean> {
|
||||
return $Call.ByID(2216952893);
|
||||
}
|
||||
|
||||
export function RegisterNotificationCategory(category: $models.NotificationCategory): $CancellablePromise<void> {
|
||||
return $Call.ByID(2917562919, category);
|
||||
}
|
||||
|
||||
export function RemoveAllDeliveredNotifications(): $CancellablePromise<void> {
|
||||
return $Call.ByID(3956282340);
|
||||
}
|
||||
|
||||
export function RemoveAllPendingNotifications(): $CancellablePromise<void> {
|
||||
return $Call.ByID(108821341);
|
||||
}
|
||||
|
||||
export function RemoveDeliveredNotification(identifier: string): $CancellablePromise<void> {
|
||||
return $Call.ByID(975691940, identifier);
|
||||
}
|
||||
|
||||
export function RemoveNotification(identifier: string): $CancellablePromise<void> {
|
||||
return $Call.ByID(3966653866, identifier);
|
||||
}
|
||||
|
||||
export function RemoveNotificationCategory(categoryID: string): $CancellablePromise<void> {
|
||||
return $Call.ByID(2032615554, categoryID);
|
||||
}
|
||||
|
||||
export function RemovePendingNotification(identifier: string): $CancellablePromise<void> {
|
||||
return $Call.ByID(3729049703, identifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Public methods that delegate to the implementation.
|
||||
*/
|
||||
export function RequestNotificationAuthorization(): $CancellablePromise<boolean> {
|
||||
return $Call.ByID(3933442950);
|
||||
}
|
||||
|
||||
export function SendNotification(options: $models.NotificationOptions): $CancellablePromise<void> {
|
||||
return $Call.ByID(3968228732, options);
|
||||
}
|
||||
|
||||
export function SendNotificationWithActions(options: $models.NotificationOptions): $CancellablePromise<void> {
|
||||
return $Call.ByID(1886542847, options);
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { Plus, RefreshCw } from "lucide-react";
|
||||
import {
|
||||
Profiles as ProfilesSvc,
|
||||
Connection,
|
||||
ProfileSwitcher,
|
||||
} from "../../bindings/github.com/netbirdio/netbird/client/ui/services";
|
||||
import type { Profile } from "../../bindings/github.com/netbirdio/netbird/client/ui/services/models.js";
|
||||
import { Button } from "../components/Button";
|
||||
@@ -33,8 +34,7 @@ export default function Profiles() {
|
||||
|
||||
const select = async (name: string) => {
|
||||
try {
|
||||
await ProfilesSvc.Switch({ profileName: name, username });
|
||||
await Connection.Up({ profileName: name, username });
|
||||
await ProfileSwitcher.SwitchActive({ profileName: name, username });
|
||||
await refresh();
|
||||
} catch (e) {
|
||||
setError(String(e));
|
||||
|
||||
@@ -103,6 +103,7 @@ func main() {
|
||||
peers := services.NewPeers(conn, app.Event)
|
||||
update := services.NewUpdate(conn)
|
||||
notifier := notifications.New()
|
||||
profileSwitcher := services.NewProfileSwitcher(profiles, connection, peers)
|
||||
|
||||
app.RegisterService(application.NewService(connection))
|
||||
app.RegisterService(application.NewService(settings))
|
||||
@@ -113,6 +114,7 @@ func main() {
|
||||
app.RegisterService(application.NewService(update))
|
||||
app.RegisterService(application.NewService(peers))
|
||||
app.RegisterService(application.NewService(notifier))
|
||||
app.RegisterService(application.NewService(profileSwitcher))
|
||||
|
||||
window := app.Window.NewWithOptions(application.WebviewWindowOptions{
|
||||
Title: "NetBird",
|
||||
@@ -146,12 +148,13 @@ func main() {
|
||||
startStatusNotifierWatcher()
|
||||
|
||||
tray = NewTray(app, window, TrayServices{
|
||||
Connection: connection,
|
||||
Settings: settings,
|
||||
Profiles: profiles,
|
||||
Peers: peers,
|
||||
Notifier: notifier,
|
||||
Update: update,
|
||||
Connection: connection,
|
||||
Settings: settings,
|
||||
Profiles: profiles,
|
||||
Peers: peers,
|
||||
Notifier: notifier,
|
||||
Update: update,
|
||||
ProfileSwitcher: profileSwitcher,
|
||||
})
|
||||
listenForShowSignal(context.Background(), tray)
|
||||
|
||||
|
||||
@@ -37,6 +37,15 @@ const (
|
||||
// permission, etc.). Real daemon statuses come straight from
|
||||
// internal.Status* — none of those collide with this label.
|
||||
StatusDaemonUnavailable = "DaemonUnavailable"
|
||||
|
||||
// Daemon connection status strings — mirror internal.Status* in
|
||||
// client/internal/state.go.
|
||||
StatusConnected = "Connected"
|
||||
StatusConnecting = "Connecting"
|
||||
StatusIdle = "Idle"
|
||||
StatusNeedsLogin = "NeedsLogin"
|
||||
StatusLoginFailed = "LoginFailed"
|
||||
StatusSessionExpired = "SessionExpired"
|
||||
)
|
||||
|
||||
// Emitter is what peers.Watch needs from the host application: a simple
|
||||
|
||||
82
client/ui/services/profileswitcher.go
Normal file
82
client/ui/services/profileswitcher.go
Normal file
@@ -0,0 +1,82 @@
|
||||
//go:build !android && !ios && !freebsd && !js
|
||||
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
// ProfileSwitcher encapsulates the full profile-switching reconnect policy so
|
||||
// both the tray and the React frontend use identical logic.
|
||||
//
|
||||
// Reconnect policy:
|
||||
//
|
||||
// ┌─────────────────┬──────────────────────┬────────────────────────────────────┐
|
||||
// │ Previous status │ Action │ Rationale │
|
||||
// ├─────────────────┼──────────────────────┼────────────────────────────────────┤
|
||||
// │ Connected │ Switch + Down + Up │ Reconnect with the new profile. │
|
||||
// │ Connecting │ Switch + Down + Up │ Stop old retry loop, restart. │
|
||||
// │ NeedsLogin │ Switch + Down │ Clear stale error; user logs in. │
|
||||
// │ LoginFailed │ Switch + Down │ Clear stale error; user logs in. │
|
||||
// │ SessionExpired │ Switch + Down │ Clear stale error; user logs in. │
|
||||
// │ Idle │ Switch only │ User chose offline; don't connect. │
|
||||
// └─────────────────┴──────────────────────┴────────────────────────────────────┘
|
||||
type ProfileSwitcher struct {
|
||||
profiles *Profiles
|
||||
connection *Connection
|
||||
peers *Peers
|
||||
}
|
||||
|
||||
// NewProfileSwitcher creates a ProfileSwitcher backed by the given services.
|
||||
func NewProfileSwitcher(profiles *Profiles, connection *Connection, peers *Peers) *ProfileSwitcher {
|
||||
return &ProfileSwitcher{profiles: profiles, connection: connection, peers: peers}
|
||||
}
|
||||
|
||||
// SwitchActive switches to the named profile applying the reconnect policy.
|
||||
// It returns after the Switch RPC completes so the caller can refresh its UI
|
||||
// immediately; Down and Up run in a background goroutine.
|
||||
func (s *ProfileSwitcher) SwitchActive(ctx context.Context, p ProfileRef) error {
|
||||
prevStatus := ""
|
||||
if st, err := s.peers.Get(ctx); err == nil {
|
||||
prevStatus = st.Status
|
||||
} else {
|
||||
log.Warnf("profileswitcher: get status: %v", err)
|
||||
}
|
||||
|
||||
wasActive := strings.EqualFold(prevStatus, StatusConnected) ||
|
||||
strings.EqualFold(prevStatus, StatusConnecting)
|
||||
needsDown := wasActive ||
|
||||
strings.EqualFold(prevStatus, StatusNeedsLogin) ||
|
||||
strings.EqualFold(prevStatus, StatusLoginFailed) ||
|
||||
strings.EqualFold(prevStatus, StatusSessionExpired)
|
||||
|
||||
log.Infof("profileswitcher: switch profile=%q prevStatus=%q wasActive=%v needsDown=%v",
|
||||
p.ProfileName, prevStatus, wasActive, needsDown)
|
||||
|
||||
if err := s.profiles.Switch(ctx, p); err != nil {
|
||||
return fmt.Errorf("switch profile %q: %w", p.ProfileName, err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
bgCtx := context.Background()
|
||||
if needsDown {
|
||||
if err := s.connection.Down(bgCtx); err != nil {
|
||||
log.Errorf("profileswitcher: Down: %v", err)
|
||||
}
|
||||
}
|
||||
if wasActive {
|
||||
if err := s.connection.Up(bgCtx, UpParams{
|
||||
ProfileName: p.ProfileName,
|
||||
Username: p.Username,
|
||||
}); err != nil {
|
||||
log.Errorf("profileswitcher: Up %s: %v", p.ProfileName, err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -75,25 +75,9 @@ const (
|
||||
notifyIDTrayError = "netbird-tray-error"
|
||||
notifyIDSessionExpired = "netbird-session-expired"
|
||||
|
||||
// Daemon status strings mirroring internal.Status* — kept in sync
|
||||
// with client/internal/state.go.
|
||||
statusConnected = "Connected"
|
||||
statusConnecting = "Connecting"
|
||||
statusIdle = "Idle"
|
||||
statusError = "Error"
|
||||
// Daemon status string for an SSO session that has expired and needs
|
||||
// re-authentication. Mirrors internal.StatusSessionExpired.
|
||||
statusSessionExpired = "SessionExpired"
|
||||
// statusNeedsLogin is what the daemon publishes before the user has
|
||||
// completed an SSO authentication on this profile. Mirrors
|
||||
// internal.StatusNeedsLogin.
|
||||
statusNeedsLogin = "NeedsLogin"
|
||||
// statusLoginFailed is what the daemon publishes when a login attempt
|
||||
// failed with a non-auth error (management unreachable, init error,
|
||||
// etc.). The CLI groups it with NeedsLogin/SessionExpired and prompts
|
||||
// the user to run "netbird up", so we mirror that here. Mirrors
|
||||
// internal.StatusLoginFailed.
|
||||
statusLoginFailed = "LoginFailed"
|
||||
// statusError is a tray-only synthetic label used for the error icon;
|
||||
// it does not come from the daemon and is not exported.
|
||||
statusError = "Error"
|
||||
|
||||
// External URLs.
|
||||
urlGitHubRepo = "https://github.com/netbirdio/netbird"
|
||||
@@ -108,12 +92,13 @@ const (
|
||||
// linter's parameter-count threshold and so adding another service later
|
||||
// is a one-line struct change instead of a NewTray signature break.
|
||||
type TrayServices struct {
|
||||
Connection *services.Connection
|
||||
Settings *services.Settings
|
||||
Profiles *services.Profiles
|
||||
Peers *services.Peers
|
||||
Notifier *notifications.NotificationService
|
||||
Update *services.Update
|
||||
Connection *services.Connection
|
||||
Settings *services.Settings
|
||||
Profiles *services.Profiles
|
||||
Peers *services.Peers
|
||||
Notifier *notifications.NotificationService
|
||||
Update *services.Update
|
||||
ProfileSwitcher *services.ProfileSwitcher
|
||||
}
|
||||
|
||||
type Tray struct {
|
||||
@@ -466,15 +451,15 @@ func (t *Tray) onUpdateProgress(ev *application.CustomEvent) {
|
||||
// otherwise spam Shell_NotifyIcon and the log.
|
||||
func (t *Tray) applyStatus(st services.Status) {
|
||||
t.mu.Lock()
|
||||
connected := strings.EqualFold(st.Status, statusConnected)
|
||||
connected := strings.EqualFold(st.Status, services.StatusConnected)
|
||||
iconChanged := connected != t.connected || st.Status != t.lastStatus
|
||||
// Detect the transition into SessionExpired: the daemon emits the
|
||||
// state on every Status snapshot for as long as the session stays
|
||||
// expired, so without this guard we would re-fire the notification
|
||||
// on every push. Mirrors the legacy Fyne client's sendNotification
|
||||
// flag in onSessionExpire.
|
||||
sessionExpiredEnter := strings.EqualFold(st.Status, statusSessionExpired) &&
|
||||
!strings.EqualFold(t.lastStatus, statusSessionExpired)
|
||||
sessionExpiredEnter := strings.EqualFold(st.Status, services.StatusSessionExpired) &&
|
||||
!strings.EqualFold(t.lastStatus, services.StatusSessionExpired)
|
||||
daemonVersionChanged := st.DaemonVersion != "" && st.DaemonVersion != t.lastDaemonVersion
|
||||
t.connected = connected
|
||||
t.lastStatus = st.Status
|
||||
@@ -489,11 +474,11 @@ func (t *Tray) applyStatus(st services.Status) {
|
||||
|
||||
if iconChanged {
|
||||
t.applyIcon()
|
||||
needsLogin := strings.EqualFold(st.Status, statusNeedsLogin) ||
|
||||
strings.EqualFold(st.Status, statusSessionExpired) ||
|
||||
strings.EqualFold(st.Status, statusLoginFailed)
|
||||
needsLogin := strings.EqualFold(st.Status, services.StatusNeedsLogin) ||
|
||||
strings.EqualFold(st.Status, services.StatusSessionExpired) ||
|
||||
strings.EqualFold(st.Status, services.StatusLoginFailed)
|
||||
daemonUnavailable := strings.EqualFold(st.Status, services.StatusDaemonUnavailable)
|
||||
connecting := strings.EqualFold(st.Status, statusConnecting)
|
||||
connecting := strings.EqualFold(st.Status, services.StatusConnecting)
|
||||
if t.statusItem != nil {
|
||||
// When the daemon needs re-authentication the status row turns
|
||||
// into the actionable Login entry — Connect would only fail.
|
||||
@@ -503,7 +488,7 @@ func (t *Tray) applyStatus(st services.Status) {
|
||||
switch {
|
||||
case daemonUnavailable:
|
||||
label = menuStatusDaemonUnavailable
|
||||
case strings.EqualFold(st.Status, statusIdle):
|
||||
case strings.EqualFold(st.Status, services.StatusIdle):
|
||||
label = menuStatusDisconnected
|
||||
}
|
||||
t.statusItem.SetLabel(label)
|
||||
@@ -603,14 +588,14 @@ func (t *Tray) applyStatusIndicator(status string) {
|
||||
|
||||
func statusIndicatorBitmap(status string) []byte {
|
||||
switch {
|
||||
case strings.EqualFold(status, statusConnected):
|
||||
case strings.EqualFold(status, services.StatusConnected):
|
||||
return iconMenuDotConnected
|
||||
case strings.EqualFold(status, statusConnecting):
|
||||
case strings.EqualFold(status, services.StatusConnecting):
|
||||
return iconMenuDotConnecting
|
||||
case strings.EqualFold(status, statusNeedsLogin),
|
||||
strings.EqualFold(status, statusSessionExpired):
|
||||
case strings.EqualFold(status, services.StatusNeedsLogin),
|
||||
strings.EqualFold(status, services.StatusSessionExpired):
|
||||
return iconMenuDotLogin
|
||||
case strings.EqualFold(status, statusLoginFailed),
|
||||
case strings.EqualFold(status, services.StatusLoginFailed),
|
||||
strings.EqualFold(status, statusError):
|
||||
return iconMenuDotError
|
||||
case strings.EqualFold(status, services.StatusDaemonUnavailable):
|
||||
@@ -648,12 +633,12 @@ func (t *Tray) iconForState() (icon, dark []byte) {
|
||||
statusLabel := t.lastStatus
|
||||
t.mu.Unlock()
|
||||
|
||||
connecting := strings.EqualFold(statusLabel, statusConnecting)
|
||||
connecting := strings.EqualFold(statusLabel, services.StatusConnecting)
|
||||
errored := strings.EqualFold(statusLabel, statusError) ||
|
||||
strings.EqualFold(statusLabel, services.StatusDaemonUnavailable)
|
||||
needsLogin := strings.EqualFold(statusLabel, statusNeedsLogin) ||
|
||||
strings.EqualFold(statusLabel, statusSessionExpired) ||
|
||||
strings.EqualFold(statusLabel, statusLoginFailed)
|
||||
needsLogin := strings.EqualFold(statusLabel, services.StatusNeedsLogin) ||
|
||||
strings.EqualFold(statusLabel, services.StatusSessionExpired) ||
|
||||
strings.EqualFold(statusLabel, services.StatusLoginFailed)
|
||||
|
||||
if runtime.GOOS == "darwin" {
|
||||
switch {
|
||||
@@ -799,78 +784,30 @@ 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 + Down │ Clean stale error state so the new │
|
||||
// │ LoginFailed │ Switch + Down │ profile starts from Idle. User │
|
||||
// │ SessionExpired │ Switch + Down │ initiates login manually. │
|
||||
// └─────────────────┴──────────────────────┴────────────────────────────────────┘
|
||||
//
|
||||
// Rule of thumb: auto-reconnect only when the daemon was actively trying to be
|
||||
// online (Connected or Connecting). Login-error states get Down so stale errors
|
||||
// are cleared, but no Up — the user initiates login on the new profile manually.
|
||||
// switchProfile delegates to ProfileSwitcher.SwitchActive in a goroutine so
|
||||
// the menu click returns immediately. The menu is refreshed as soon as the
|
||||
// Switch RPC completes (before Down/Up finishes in the background).
|
||||
func (t *Tray) switchProfile(name string) {
|
||||
t.mu.Lock()
|
||||
prevStatus := t.lastStatus
|
||||
t.mu.Unlock()
|
||||
wasActive := strings.EqualFold(prevStatus, statusConnected) ||
|
||||
strings.EqualFold(prevStatus, statusConnecting)
|
||||
needsDown := wasActive ||
|
||||
strings.EqualFold(prevStatus, statusNeedsLogin) ||
|
||||
strings.EqualFold(prevStatus, statusLoginFailed) ||
|
||||
strings.EqualFold(prevStatus, statusSessionExpired)
|
||||
|
||||
go func() {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
username, err := t.svc.Profiles.Username()
|
||||
if err != nil {
|
||||
log.Errorf("get current user: %v", err)
|
||||
log.Errorf("tray switchProfile: get current user: %v", err)
|
||||
return
|
||||
}
|
||||
log.Infof("tray switchProfile: sending SwitchProfile RPC profile=%q user=%q prevStatus=%q wasActive=%v needsDown=%v",
|
||||
name, username, prevStatus, wasActive, needsDown)
|
||||
if err := t.svc.Profiles.Switch(ctx, services.ProfileRef{
|
||||
if err := t.svc.ProfileSwitcher.SwitchActive(ctx, services.ProfileRef{
|
||||
ProfileName: name,
|
||||
Username: username,
|
||||
}); err != nil {
|
||||
log.Errorf("tray switchProfile: SwitchProfile RPC failed profile=%q err=%v", name, err)
|
||||
log.Errorf("tray switchProfile: %v", err)
|
||||
t.notifyError(fmt.Sprintf("Failed to switch to %s", name))
|
||||
return
|
||||
}
|
||||
log.Infof("tray switchProfile: SwitchProfile RPC succeeded profile=%q", name)
|
||||
|
||||
if needsDown {
|
||||
log.Infof("tray switchProfile: calling Down to clean up previous state (%s)", prevStatus)
|
||||
if err := t.svc.Connection.Down(ctx); err != nil {
|
||||
log.Errorf("tray switchProfile: Down failed: %v", err)
|
||||
}
|
||||
}
|
||||
if wasActive {
|
||||
// Bring the connection back up against the new profile's management server.
|
||||
log.Infof("tray switchProfile: was active (%s), reconnecting with new profile %q", prevStatus, name)
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
// SwitchActive returns after the Switch RPC — active_profile.json is
|
||||
// updated; Down/Up run in the background. Refresh menu now so the
|
||||
// checkmark moves immediately.
|
||||
t.loadProfiles()
|
||||
}()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user