Skip to content

Conversation

@doromaraujo
Copy link
Collaborator

@doromaraujo doromaraujo commented Oct 29, 2025

Describe your changes

Adds a window that will pop up when running NetBird on desktop after it is already running.
It sends a SIGUSR1 to a previously set up signal handler (platform-dependent) that will handle opening the window itself.

The window only contains the NetBird logo with a single button indicating the action to connect or disconnect.
When connected, the NetBird logo is colored, when disconnected, the logo is greyed out.

On connection change, the window closes on its own.

Issue ticket number and link

Stack

Checklist

  • Is it a bug fix
  • Is a typo/documentation fix
  • Is a feature enhancement
  • It is a refactor
  • Created tests that fail without the change (if possible)

By submitting this pull request, you confirm that you have read and agree to the terms of the Contributor License Agreement.

Documentation

Select exactly one:

  • I added/updated documentation for this change
  • Documentation is not needed for this change (explain why)

Docs PR URL (required if "docs added" is checked)

Paste the PR link from https://github.com/netbirdio/docs here:

https://github.com/netbirdio/docs/pull/__

Summary by CodeRabbit

  • New Features

    • Quick Actions UI window for expedited connection management
    • --quick-actions command-line mode
    • Disconnected-state icon and visual indicator
  • Improvements

    • Better daemon detection with clearer messaging and show-window signaling
    • Cross-platform signal/event handling to trigger the Quick Actions window
    • Faster icon refresh on theme/settings changes
    • Upload success flow now integrates clipboard behavior with the app context
  • Chores

    • Updated project dependencies

lixmal and others added 20 commits October 20, 2025 15:49
It now displays the NetBird logo and a single button
with a round icon
To use its clipboard rather than the window's when showing
the upload success dialog
Settings now accept a function callback
@doromaraujo doromaraujo requested a review from mlsmaycon October 29, 2025 19:54

toggleConnectionButton.OnTapped = func() {
go func() {
uiState.toggleAction()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we set this in a goroutine? That looks like it can race with something else.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's the command to be used (connect, disconnect) when user taps the button. It's updated via the UI channel; it's also the only place where it is set.

return
}

uiChan := make(chan quickActionsUiState, 1)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This channel should be closed, e.g. in a defer. Consider using sync.Once to avoid panics if it can be closed in more than one code path

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The goroutine that uses it is now making use of ctx.Done to return from it. More than one place in the view model send UI states to it.

provider clientConnectionStatusProvider
connect clientCommand
disconnect clientCommand
uiChan chan quickActionsUiState
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This field is accessed from multiple goroutines but it's not protected

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Which field? What protection is needed?

@mlsmaycon
Copy link
Collaborator

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 7, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 7, 2025

Walkthrough

Adds a Quick Actions Fyne UI with platform-specific signal handlers to spawn/show it, a --quick-actions CLI flag, embedded quick-action and disconnected icons, periodic daemon status polling and connect/disconnect commands, and signal-sending logic to request existing daemons to display the UI.

Changes

Cohort / File(s) Change Summary
Quick Actions UI Core
client/ui/quickactions.go
New Fyne-based quick actions UI: UI state/view-model, periodic (1s) gRPC status polling, connect/disconnect command types, icon loading/resizing, and showQuickActionsUI to build/update the window.
Quick Actions Assets (auto-generated)
client/ui/quickactions_assets.go
Embeds assets/connected.png and assets/disconnected.png as byte slices and Fyne StaticResource values.
Signal Handling — Unix
client/ui/signal_unix.go
Adds SIGUSR1 handler that calls openQuickActions, implements openQuickActions to re-exec the binary with --quick-actions/--daemon-addr, and sendShowWindowSignal(pid) to send SIGUSR1.
Signal Handling — Windows
client/ui/signal_windows.go
Adds named-event signaling (Global\NetBirdQuickActionsTriggerEvent), setupSignalHandler, waitForEvent, openQuickActions, sendShowWindowSignal, and related helpers to open/set/reset the event and manage spawned process output.
Main UI Client
client/ui/client_ui.go
Adds --quick-actions CLI flag and plumbing through flag parsing and newServiceClientArgs, adds wQuickActions window field and showQuickActions path, calls setupSignalHandler(client.ctx) at startup, and attempts to signal existing daemon to show the window when another process is detected.
Debug Dialog Signature
client/ui/debug.go
Changes showUploadSuccessDialog signature from (w fyne.Window, localPath, uploadedKey string) to (a fyne.App, w fyne.Window, localPath, uploadedKey string) and updates call sites to pass the app for clipboard operations.
Icons
client/ui/icons.go, client/ui/icons_windows.go
Adds embedded netbird-disconnected assets (assets/netbird-disconnected.png and assets/netbird-disconnected.ico) as new package-level variables alongside existing icons.
Dependencies
go.mod
Bumps multiple module versions (including fyne/v2, systray, fsnotify, testify, BurntSushi/toml, go-i18n, goldmark, golang.org/x/image, etc.).

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant ExistingDaemon as Existing Daemon
    participant QuickProc as QuickActions Proc
    participant gRPC as gRPC Daemon

    User->>ExistingDaemon: sendShowWindowSignal / trigger event
    ExistingDaemon->>ExistingDaemon: platform signal handler receives (SIGUSR1 / named event)
    ExistingDaemon->>QuickProc: openQuickActions() → exec binary --quick-actions --daemon-addr
    activate QuickProc
    QuickProc->>QuickProc: showQuickActionsUI()
    loop every 1s
        QuickProc->>gRPC: Status()
        gRPC-->>QuickProc: Status response
        QuickProc->>QuickProc: update UI state (button, hint, icon)
    end
    User->>QuickProc: Click toggle
    alt was connected
        QuickProc->>gRPC: Disconnect command
    else was disconnected
        QuickProc->>gRPC: Connect command
    end
    gRPC-->>QuickProc: Ack/status update
    QuickProc->>QuickProc: refresh UI
    deactivate QuickProc
Loading
sequenceDiagram
    participant Launcher as Initial Client Start
    participant ServiceClient as serviceClient
    participant SignalHandler as Platform Signal Handler

    Launcher->>ServiceClient: start()
    ServiceClient->>ServiceClient: parseFlags (includes --quick-actions)
    ServiceClient->>SignalHandler: setupSignalHandler(ctx)
    alt Another daemon running
        ServiceClient->>SignalHandler: sendShowWindowSignal(pid)
        SignalHandler->>ExistingDaemon: OS-level signal / named event
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

  • Focus review on:
    • client/ui/quickactions.go — goroutine lifecycle, channel usage, UI updates from background thread, error handling for status polling and command execution.
    • client/ui/signal_windows.go — Windows API handle management, event creation/opening, waiting loop and context cancellation, and sendShowWindowSignal correctness.
    • client/ui/signal_unix.go — process re-exec details, stdio handling, and PID/signal correctness.
    • client/ui/client_ui.go — CLI flag plumbing and startup ordering (ensuring signal handler setup before signaling).
    • client/ui/debug.go — updated showUploadSuccessDialog signature and all adjusted call sites.

Poem

🐇
I thumped a key and rang a bell,
A tiny window woke to tell,
Icons blinked, a toggle bright,
Connect, disconnect — quick delight.
Rabbit hops, the actions swell.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 46.15% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title '[client] Add quick actions window' directly and concisely describes the main feature addition in the changeset.
Description check ✅ Passed The description explains the feature well with implementation details, marks appropriate checklist items, and addresses documentation needs, though no issue ticket number is provided.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 62e69d5 and da6d4e0.

📒 Files selected for processing (1)
  • client/ui/signal_windows.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • client/ui/signal_windows.go
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (18)
  • GitHub Check: Relay / Unit (386)
  • GitHub Check: Relay / Unit (amd64, -race)
  • GitHub Check: Management / Unit (amd64, sqlite)
  • GitHub Check: Client / Unit (386)
  • GitHub Check: Client / Unit (amd64)
  • GitHub Check: Management / Unit (amd64, mysql)
  • GitHub Check: Management / Unit (amd64, postgres)
  • GitHub Check: Client (Docker) / Unit
  • GitHub Check: Linux
  • GitHub Check: Windows
  • GitHub Check: Darwin
  • GitHub Check: Client / Unit
  • GitHub Check: Android / Build
  • GitHub Check: release
  • GitHub Check: release_ui_darwin
  • GitHub Check: release_ui
  • GitHub Check: JS / Lint
  • GitHub Check: Client / Unit

Comment @coderabbitai help to get the list of available commands and usage tips.

buttonText = disconnectedLabelText
buttonIcon = disconnectedCircle.Resource
logo = disconnectedImage
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about the other states? How do we handle those?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Other states won't be displayed in this UI. It's either "Connect " when disconnected or "Disconnect" when connected, and when clicked the button will be disabled until it either switches connection status or an error occurs.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (3)
client/ui/quickactions.go (1)

101-163: Fix the pause/resume loop to avoid pegging the CPU and freezing the UI

Once executeCommand sends to pauseChan, the poller flips getConnectionStatus to false and your default branch immediately hits continue. Because there is no matching resume send (the only call is commented out) the loop stays “paused” forever, yet it now busy-spins with no sleep, burning a core and never emitting another state. On top of that, if the connect/disconnect command returns an error the UI stays disabled and the user can’t retry.

Please rework this section to block on a ticker while paused and ensure you always resume the poller (both after success and on error). A concrete fix could look like:

-	go func() {
-		getConnectionStatus := true
-		for {
-			select {
-			case <-viewModel.pauseChan:
-				getConnectionStatus = false
-				log.Debug("uiChan paused.")
-			case <-viewModel.resumeChan:
-				getConnectionStatus = true
-				log.Debug("uiChan resumed.")
-			default:
-				if !getConnectionStatus {
-					continue
-				}
-
-				time.Sleep(500 * time.Millisecond)
-			}
-		}
-	}()
+	go func() {
+		ticker := time.NewTicker(500 * time.Millisecond)
+		defer ticker.Stop()
+		paused := false
+		for {
+			select {
+			case <-viewModel.pauseChan:
+				paused = true
+				log.Debug("uiChan paused.")
+			case <-viewModel.resumeChan:
+				paused = false
+				log.Debug("uiChan resumed.")
+			case <-ticker.C:
+				if paused {
+					continue
+				}
+
+			}
+		}
+	}()

and in executeCommand:

	q.pauseChan <- struct{}{}
	err := command.execute()
	if err != nil {
		log.Errorf("Status: Error - %v", err)
-	} else {
-		uiState = newQuickActionsUiState()
-		uiState.isConnectionChanged = true
-		q.uiChan <- uiState
-		//q.resumeChan <- struct{}{}
-	}
+		q.resumeChan <- struct{}{}
+		return
+	}
+	uiState = newQuickActionsUiState()
+	uiState.isConnectionChanged = true
+	q.uiChan <- uiState
+	q.resumeChan <- struct{}{}

Without these fixes the new window can hang the process and leaves users with a dead quick-actions button.

client/ui/signal_windows.go (2)

63-65: Align the log message with the quick actions handler.

The log still refers to a “Debug handler,” which is confusing when this code wires up the quick actions event listener. Please update the message to reflect the actual handler name.

-	log.Infof("Debug handler waiting for signal on event: %s", quickActionsTriggerEventName)
+	log.Infof("Quick actions handler waiting for signal on event: %s", quickActionsTriggerEventName)

159-170: Close the event handle and return contextual errors.

We still leak the event handle here—after SetEvent we never call CloseHandle, so every trigger leaves a live handle behind. While fixing that, please wrap the returned errors with context (this also addresses the prior feedback on this block). Remember to add the missing fmt import.

-	eventHandle, err := windows.OpenEvent(desiredAccesses, false, eventNamePtr)
-	if err != nil {
-		return err
-	}
-
-	err = windows.SetEvent(eventHandle)
-	if err != nil {
-		log.Printf("Error setting event: %v", err)
-		return err
-	}
-
-	return nil
+	eventHandle, err := windows.OpenEvent(desiredAccesses, false, eventNamePtr)
+	if err != nil {
+		return fmt.Errorf("open quick actions event '%s': %w", quickActionsTriggerEventName, err)
+	}
+	defer func() {
+		if cerr := windows.CloseHandle(eventHandle); cerr != nil {
+			log.Errorf("failed to close quick actions event handle '%s': %v", quickActionsTriggerEventName, cerr)
+		}
+	}()
+
+	if err := windows.SetEvent(eventHandle); err != nil {
+		return fmt.Errorf("set quick actions event '%s': %w", quickActionsTriggerEventName, err)
+	}
+
+	return nil
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 229e003 and 3d55baa.

⛔ Files ignored due to path filters (3)
  • client/ui/assets/netbird-disconnected.ico is excluded by !**/*.ico
  • client/ui/assets/netbird-disconnected.png is excluded by !**/*.png
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (9)
  • client/ui/client_ui.go (7 hunks)
  • client/ui/debug.go (4 hunks)
  • client/ui/icons.go (1 hunks)
  • client/ui/icons_windows.go (1 hunks)
  • client/ui/quickactions.go (1 hunks)
  • client/ui/quickactions_assets.go (1 hunks)
  • client/ui/signal_unix.go (1 hunks)
  • client/ui/signal_windows.go (1 hunks)
  • go.mod (9 hunks)
🧰 Additional context used
🪛 GitHub Check: JS / Lint
client/ui/client_ui.go

[failure] 124-124:
client.setDefaultFonts undefined (type *serviceClient has no field or method setDefaultFonts) (typecheck)

🔇 Additional comments (3)
go.mod (3)

31-32: Direct dependency updates align with quick actions feature addition.

The Fyne UI framework (v2.7.0), systray, fsnotify, and testify updates are appropriate for the PR's quick actions window implementation. The versions are reasonable and stable.

Also applies to: 46-46, 85-85


32-32: I need to verify that this commit contains the required signal handling improvements for the quick actions feature. Let me check the actual commit details from the repository.

Verify that the systray commit contains the signal handling improvements described.

The pseudo-version commit ca66a66d8b58cfe0f18d2a1f999f2f9d47364606 successfully resolves via Go's module system (confirmed: timestamp 2025-06-03T11:35:21Z), establishing that the commit exists and is accessible. However, I cannot verify through public sources that this commit contains the claimed "signal handling improvements for quick actions feature" as:

  1. The term "quick actions" does not appear in systray's public documentation or issue tracker
  2. Web searches for systray-related signal handling improvements return no relevant results
  3. The specific changes in this commit are not accessible through public channels

Since the commit's existence is confirmed but its contents cannot be independently verified, manually inspect the systray repository commit directly to confirm the signal handling improvements before merging.


160-165: No actionable changes required; Fyne transitive dependencies verified safe.

Verification confirms:

  • No known vulnerabilities detected in Fyne and related transitive dependencies (go-indexeddb, safejs, rymdport/portal, etc.)
  • WASM dependencies properly constrained via //go:build js tags and isolated to client/wasm/ package tree
  • Desktop code paths use //go:build !js constraints, ensuring WASM-only dependencies are excluded from desktop binaries
  • Architecture correctly separates WebAssembly and desktop code, preventing bloat or unintended linking

The dependency additions from Fyne v2.7.0 upgrade are well-integrated and safe for production use.

Comment on lines +122 to 125
client.setupSignalHandler(client.ctx)

client.setDefaultFonts()
systray.Run(client.onTrayReady, client.onTrayExit)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Restore the missing setDefaultFonts implementation

client.setDefaultFonts() is invoked here, but no definition is provided in any of the included build targets, and the static analysis/typecheck run already fails with “client.setDefaultFonts undefined”. The PR will not compile until the function is (re)introduced or this call is removed/guarded. Please add the missing implementation (with the appropriate build tags) or drop the call so the build succeeds.

🧰 Tools
🪛 GitHub Check: JS / Lint

[failure] 124-124:
client.setDefaultFonts undefined (type *serviceClient has no field or method setDefaultFonts) (typecheck)

To address racing conditions, it also replaces
usage of pause and resume channels with an
atomic bool.
Also, format error when trying to set event on
sendShowWindowSignal
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (2)
client/ui/quickactions.go (2)

209-221: Still unresolved: nil logo causes UI corruption for unhandled states.

The variables logo, buttonText, and buttonIcon remain uninitialized for connection states other than "Connected" and "Idle" (e.g., "Connecting", "Disconnected", "Error"). When an unhandled state arrives, logo stays nil, and line 248's assignment content.Objects[1] = logo corrupts the UI or risks a panic.

Initialize these variables to the disconnected assets before the conditional checks:

-	var logo *canvas.Image
-	var buttonText string
-	var buttonIcon fyne.Resource
+	logo := disconnectedImage
+	buttonText := disconnectedLabelText
+	buttonIcon := disconnectedCircleRes
 
 	if uiState.connectionStatus == string(internal.StatusConnected) {
 		buttonText = connectedLabelText
 		buttonIcon = connectedCircleRes
 		logo = connectedImage
 	} else if uiState.connectionStatus == string(internal.StatusIdle) {
-		buttonText = disconnectedLabelText
-		buttonIcon = disconnectedCircleRes
-		logo = disconnectedImage
+		// keep defaults
 	}

This ensures any unhandled state displays the greyed-out logo and safe button values.


281-282: Still unresolved: channel is never closed, preventing clean shutdown.

The uiChan is created but never closed. When vmCtx is cancelled (via window close at line 260), the watchConnectionStatus goroutine stops sending, but the channel remains open indefinitely. The consumer goroutine at lines 313-337 will block forever on the channel receive (line 319) after context cancellation since it won't receive the close signal.

Close the channel when the view model's work is done:

 	uiChan := make(chan quickActionsUiState, 1)
+	defer close(uiChan)
 	newQuickActionsViewModel(vmCtx, daemonClientConnectionStatusProvider{client: client}, connCmd, disConnCmd, uiChan)

Alternatively, close it inside watchConnectionStatus when the context is cancelled. Consider using sync.Once if multiple paths could close the channel.

🧹 Nitpick comments (1)
client/ui/quickactions.go (1)

313-337: Consider extracting the UI update loop to a named method.

The anonymous goroutine spans 25 lines and could be extracted to a method like watchAndApplyUIStates(ctx, uiChan, ...) to improve readability and make the showQuickActionsUI method more scannable.

Based on learnings.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3d55baa and 69cb1aa.

📒 Files selected for processing (2)
  • client/ui/quickactions.go (1 hunks)
  • client/ui/signal_windows.go (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • client/ui/signal_windows.go
🧰 Additional context used
🧬 Code graph analysis (1)
client/ui/quickactions.go (1)
client/proto/daemon_grpc.pb.go (1)
  • DaemonServiceClient (20-67)
🔇 Additional comments (7)
client/ui/quickactions.go (7)

1-5: LGTM!

The build constraints and asset bundling directives are appropriate for this cross-platform UI feature.


26-86: LGTM!

The type definitions follow clean architecture patterns (provider abstraction, command pattern) and the use of atomic.Bool at line 85 properly handles concurrent access to isWatchingConnectionStatus.


88-104: LGTM!

The view model initialization is well-structured: it sends an initial UI state for immediate feedback and properly passes context to the background polling goroutine.


106-129: LGTM!

The status update logic is sound: it queries the daemon, handles errors gracefully by sending a disabled state, and appropriately assigns the toggle action based on connection status.


131-145: LGTM!

The polling loop correctly implements context cancellation, properly cleans up the ticker, and respects the atomic flag to pause updates during command execution.


147-167: LGTM!

The command execution properly disables polling during the operation and handles both success (closing window) and error (re-enabling polling) paths. The next ticker cycle will refresh the UI after an error, providing eventual consistency.


169-189: LGTM!

The helper functions are well-implemented: getSystemTrayName provides platform-specific terminology, and getNetBirdImage properly configures the image size and fill mode.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (2)
client/ui/quickactions.go (2)

285-286: Close the UI channel to prevent goroutine leak.

The uiChan channel is never closed, which can prevent the UI update goroutine (lines 317-343) from terminating gracefully. While context cancellation stops the view model from sending, the receiver should be notified that no more values will arrive.

Close the channel when the view model context is done:

 	uiChan := make(chan quickActionsUiState, 1)
+	defer close(uiChan)
 	newQuickActionsViewModel(vmCtx, daemonClientConnectionStatusProvider{client: client}, connCmd, disConnCmd, uiChan)

Alternatively, if the view model should own the channel lifecycle, close it within the view model's cleanup logic and ensure proper synchronization with sync.Once to avoid double-close panics.


213-225: Default to disconnected assets for unhandled connection statuses.

The variables logo, buttonText, and buttonIcon are only initialized for "Connected" (line 217) and "Idle" (line 221) statuses. Other possible statuses like "Connecting", "Disconnected", or "Error" will leave these variables nil/empty. While the nil checks at lines 228, 232, and 251 prevent crashes, they result in no visual updates for unhandled statuses, leaving the window in a stale state.

A previous review comment suggested this was addressed, but the issue persists in the current code.

Initialize these variables with disconnected defaults at the top of the method:

 func (s *serviceClient) applyQuickActionsUiState(
 	uiState quickActionsUiState,
 	components quickActionsUiComponents,
 ) bool {
 	if uiState.isConnectionChanged {
 		fyne.DoAndWait(func() {
 			s.wQuickActions.Close()
 		})
 		return true
 	}
 
-	var logo *canvas.Image
-	var buttonText string
-	var buttonIcon fyne.Resource
+	// Default to disconnected state for unknown statuses
+	logo := components.disconnectedImage
+	buttonText := components.disconnectedLabelText
+	buttonIcon := components.disconnectedCircleRes
 
 	if uiState.connectionStatus == string(internal.StatusConnected) {
 		buttonText = components.connectedLabelText
 		buttonIcon = components.connectedCircleRes
 		logo = components.connectedImage
-	} else if uiState.connectionStatus == string(internal.StatusIdle) {
-		buttonText = components.disconnectedLabelText
-		buttonIcon = components.disconnectedCircleRes
-		logo = components.disconnectedImage
 	}
🧹 Nitpick comments (4)
go.mod (1)

162-167: Verify transitive dependency scope for quick actions feature.

Multiple new indirect dependencies have been added, likely from Fyne and related UI libraries (lines 162–167 for Fyne ecosystem, 183–184 for IndexedDB/SafeJS, 191 for localization, 195 for bitmap, 214 for image resize, 215 for i18n, 231 for XDG portal, 241 for markdown, 250 for image processing). While many are expected for a new UI feature, confirm none are unnecessary bloat.

Consider documenting in the PR description or a comment which dependencies were added directly for the quick actions feature vs. which are transitive. This helps maintain awareness of supply chain scope. For example:

  • Direct dependencies: fyne/v2 (UI), systray (system integration)
  • Transitive/indirect: Fyne ecosystem libs, image processing, localization, XDG portal (Linux integration)

This isn't a blocker, but transparency aids future maintenance.

Also applies to: 175-175, 183-184, 191-191, 195-195, 214-215, 231-231, 241-241, 250-250

client/ui/quickactions.go (3)

147-167: Clarify the empty connection status during command execution.

At line 151, connectionStatus is set to an empty string, which doesn't match either "Connected" or "Idle". When this state reaches applyQuickActionsUiState (lines 217-225), the variables logo, buttonText, and buttonIcon will remain uninitialized (nil/empty), though the UI update logic checks for nil before applying. However, this makes the intent unclear.

Consider either:

  1. Explicitly setting connectionStatus to the current known status, or
  2. Adding a comment explaining that empty status intentionally leaves the logo unchanged during command execution

242-246: Consider caching the OnTapped handler to avoid repeated reassignment.

The OnTapped handler is reassigned on every UI state update (potentially every second during polling). While functionally correct, this is inefficient when toggleAction hasn't changed.

You could track whether toggleAction has changed and only reassign the handler when necessary, though the current approach is simple and works.


317-343: Consider extracting the UI update goroutine to a named function.

The inline goroutine spanning lines 317-343 handles UI state updates. Extracting it to a named method would improve readability and testability.

For example:

// In showQuickActionsUI, replace the inline goroutine:
go s.runQuickActionsUIUpdateLoop(vmCtx, uiChan, components)

// Add the method:
func (s *serviceClient) runQuickActionsUIUpdateLoop(
	ctx context.Context,
	uiChan <-chan quickActionsUiState,
	components quickActionsUiComponents,
) {
	for {
		select {
		case <-ctx.Done():
			return
		case uiState, ok := <-uiChan:
			if !ok {
				return
			}
			if s.applyQuickActionsUiState(uiState, components) {
				return
			}
		}
	}
}

This addresses a previous reviewer's suggestion for improved code organization.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 69cb1aa and 62e69d5.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (2)
  • client/ui/quickactions.go (1 hunks)
  • go.mod (9 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
client/ui/quickactions.go (1)
client/proto/daemon_grpc.pb.go (1)
  • DaemonServiceClient (20-67)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (13)
  • GitHub Check: release_ui_darwin
  • GitHub Check: release
  • GitHub Check: release_ui
  • GitHub Check: Client / Unit
  • GitHub Check: Linux
  • GitHub Check: Windows
  • GitHub Check: Darwin
  • GitHub Check: Android / Build
  • GitHub Check: iOS / Build
  • GitHub Check: Client / Unit
  • GitHub Check: Build Cache
  • GitHub Check: JS / Lint
  • GitHub Check: Client / Unit
🔇 Additional comments (8)
go.mod (2)

31-32: Clarify why systray uses a commit hash instead of the stable release.

The latest stable versions are fyne.io/fyne/v2 v2.7.0 and fyne.io/systray v1.11.0. Line 31 correctly uses fyne/v2 v2.7.0, but line 32 uses v1.11.1-0.20250603113521-ca66a66d8b58—a pseudo-version pointing to a specific commit rather than the stable v1.11.0 release.

Please confirm:

  • Is this a fork or custom branch? If so, document the rationale and maintenance status.
  • If it's a required fix from upstream that isn't in v1.11.0 yet, reference the issue/PR explaining why the commit is necessary.

31-31: I need to search for specific security advisories on each of the three updated dependencies. The previous query returned general Go ecosystem vulnerabilities but not the specific packages mentioned. Let me check each dependency:

No known security vulnerabilities found in updated dependencies.

Verification via GitHub Advisory Database and targeted security searches confirms that fyne.io/fyne/v2 v2.7.0, fsnotify v1.9.0, and testify v1.11.1 have no known CVEs or published security advisories. These are all stable releases without reported security issues.

client/ui/quickactions.go (6)

1-39: LGTM: Well-structured state management.

The quickActionsUiState struct and constructor provide a clean foundation for managing UI state with appropriate default values.


49-58: Verify that 400ms timeout is sufficient for Status RPC.

The 400ms timeout for the Status gRPC call is quite aggressive. On slower systems, under load, or with network latency, this could result in frequent timeout errors and a degraded user experience.

Consider whether a longer timeout (e.g., 1-2 seconds) would be more appropriate, or verify through testing that 400ms is sufficient across expected deployment scenarios.


60-78: LGTM: Clean command pattern.

The command interface and implementations provide a nice abstraction for connect/disconnect actions.


80-104: LGTM: View model properly uses atomic operations.

The view model correctly uses atomic.Bool for the isWatchingConnectionStatus flag, ensuring thread-safe access from multiple goroutines. The goroutine started at line 103 is properly managed via context cancellation.


106-145: LGTM: Proper ticker usage and context handling.

The polling mechanism correctly uses a ticker with deferred cleanup and proper context cancellation, addressing previous reviewer concerns. The atomic flag prevents unnecessary status checks when commands are executing.


169-189: LGTM: Clean helper functions.

Both utility functions are straightforward and correctly implemented.

@sonarqubecloud
Copy link

sonarqubecloud bot commented Nov 7, 2025

@doromaraujo doromaraujo merged commit 3176b53 into netbirdio:main Nov 13, 2025
38 checks passed
@doromaraujo doromaraujo deleted the feature/quick-settings-show-popup-macos branch November 13, 2025 13:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants