Skip to content

[client] Add a Reconnect entry to the tray menu - #6976

Draft
atj393 wants to merge 2 commits into
netbirdio:mainfrom
atj393:feature/tray-reconnect
Draft

[client] Add a Reconnect entry to the tray menu#6976
atj393 wants to merge 2 commits into
netbirdio:mainfrom
atj393:feature/tray-reconnect

Conversation

@atj393

@atj393 atj393 commented Jul 30, 2026

Copy link
Copy Markdown

Opened as a draft on purpose. This implements the idea in discussion #6943, which has no maintainer reply yet. Per CONTRIBUTING.md new features should be agreed with the NetBird team first, so this is a proposal to react to, not a merge request. Happy to change the approach, the placement, or drop it entirely.

Describe your changes

Adds a Reconnect entry to the desktop tray menu: one click that runs Down and then Up, replacing the right-click → Disconnect → wait → right-click → Connect dance users repeat after a sleep/hibernate wake, a Wi-Fi ↔ ethernet ↔ LTE roam, or when peers read as connected but no traffic flows.

Existing Connect and Disconnect behaviour is unchanged, and no gRPC, daemon, or CLI surface is touched — the daemon has no reconnect RPC and this does not add one. The sequencing lives in the UI, where ProfileSwitcher already does the same thing.

client/ui/services/connection.goConnection.Reconnect(ctx) owns the sequence, next to the Up/Down it composes:

  • Down is synchronous, so Up goes out only once the daemon reports the teardown finished.
  • A failed Down aborts instead of stacking an Up on a session that is still up.
  • A ctx cancelled between the legs aborts too — a Disconnect or a quit racing the round trip must not be undone by a late Up.

client/ui/tray.go — the tray's share: the entry, the in-flight guard, the transitional paint, the failure toast.

  • The entry sits directly below Disconnect and shares its visibility gate (connected || connecting); with no session to tear down, a reconnect is just Connect.
  • reconnectCancel is non-nil exactly while a round trip is in flight. It does double duty: handleReconnect and relayoutMenu read it so a second click cannot send a Down into the in-flight Up, and handleDisconnect / handleQuit call it so the queued Up cannot resurrect the session the user just asked to drop. A click-time SetEnabled(false) cannot stand in for the first duty — buildMenu recreates every item on relayout, and the status pushes the round trip itself produces are what triggers those relayouts. For the same reason the row is re-enabled by a relayout rather than by SetEnabled on a by-then-detached item.
  • The round trip is bounded by a 30s timeout, matching DaemonFeed's own suppression window, so a daemon that never answers Down cannot grey the entry out for the rest of the session.
  • A click while a profile switch is in flight is ignored rather than cancelling the switch: a switch is already a DownUp, cancelling one midway can leave the daemon and the CLI's on-disk profile state disagreeing, and this Up carries no profile of its own.

Transitional feedback reuses DaemonFeed.BeginProfileSwitch() instead of inventing a "Reconnecting…" state that would have to fight the daemon's status pushes. It already does exactly what a DownUp needs: emits an optimistic Connecting, suppresses the stale Connected/Idle blink during teardown, and arms the login-watch so an Up landing in NeedsLogin (an expired session, say) opens browser-login instead of stalling. handleReconnect clears pendingConnectLogin when arming it, so the two SSO-handoff mechanisms cannot both fire on one NeedsLogin push. On failure the optimistic paint is released and the tray repaints from a fresh Get() snapshot — a failed Down leaves the daemon exactly as it was, and SubscribeStatus only pushes on state changes, so waiting for a push would strand the tray on Connecting.

i18ntray.menu.reconnect and notify.error.reconnect added to en with descriptions and translated in all nine other bundles, following each locale's existing Connect/Disconnect wording (de "Neu verbinden", ru "Переподключиться", ja "再接続", …) and the phrasing already used by settings.network.monitor.label.

Known gap

The failure repaint above fixes the tray. The React window is fed by the same suppressed stream, so after a failed Down it can sit on the optimistic Connecting until the next real state change. Fixing it properly means a DaemonFeed method that clears suppression and re-emits a snapshot — see question 1.

Open questions for reviewers

  1. BeginProfileSwitch / CancelProfileSwitch naming. They are named for the flow they were written for, but the mechanism is the DownUp transition this shares, and DaemonFeed's suppression-table doc comment is now describing a flow it no longer exclusively serves. I left the names alone to keep the diff small. If you would rather have BeginTransition / CancelTransition — ideally with the cancel re-emitting a snapshot, closing the gap above — I am glad to do it in this PR.
  2. connecting state. The entry stays enabled while connecting, so a stuck attempt can be restarted. Say the word if you would rather it were connected-only.
  3. Main window. Deliberately tray-only, matching the discussion. The React Status page could get the same action for parity.

Issue ticket number and link

Discussion: #6943

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)
  • This change does not modify the public API, gRPC protocols, functionality behavior, CLI / service flags, or introduce a new feature — OR I have discussed it with the NetBird team beforehand (link the issue / Slack thread in the description). See CONTRIBUTING.md.

Last box left unchecked deliberately: this is a new user-facing feature and discussion #6943 has no maintainer reply yet, which is why the PR is a draft.

client/ui/services/connection_test.go covers Reconnect's three orderings — Down then Up on the happy path, no Up after a failed Down, and no Up after a cancel lands mid-teardown. The tray wiring itself is untested: client/ui has no harness for buildMenu / handleConnect / handleDisconnect (it needs a live application.App), and the package does not even compile without a built frontend/dist. Glad to add coverage there if you have a preferred way to fake the Wails menu.

Verified: gofmt, go build ./client/ui/..., go vet ./client/ui/..., go test ./client/ui/... clean on Windows/amd64, Go 1.26.5.
Not verified: no runtime click-through. I have no Wails/pnpm toolchain or running daemon here, so the menu has not been exercised on any of the three platforms — worth a manual pass before this is taken seriously.

Documentation

Select exactly one:

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

A self-explanatory tray entry with no new setting or flag. If the docs enumerate the tray menu somewhere I missed, point me at it and I will open the matching netbirdio/docs PR.

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

n/a

Collapses the Disconnect -> wait -> Connect sequence users repeat after a
sleep/wake, a network roam, or peers reading connected while no traffic
flows, into a single tray click. Connect and Disconnect are unchanged.

Connection.Reconnect owns the ordering: Down is synchronous, so Up goes
out only once the daemon reports the teardown finished, a failed Down
aborts instead of stacking an Up on a session that is still up, and a
context cancelled between the legs aborts as well.

The tray keeps its own share - the in-flight guard, the transitional
paint and the failure toast. reconnectCancel doubles as guard and abort
handle: a second click cannot send a Down into the in-flight Up, and a
Disconnect or quit is not undone by the Up queued behind it. The
transitional paint reuses DaemonFeed's switch suppression, which already
hides the stale Connected/Idle blink a Down emits and arms the SSO
login-watch for the Up that follows.
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: bf57d265-59a4-48ab-a6f1-a72510e079b1

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@CLAassistant

CLAassistant commented Jul 30, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@sonarqubecloud

Copy link
Copy Markdown

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.

2 participants