Skip to content

Phase 1: ConnectionModeSelect replaces Lazy toggle (issue netbirdio/netbird#5989)#627

Open
MichaelUray wants to merge 1 commit intonetbirdio:mainfrom
MichaelUray:feat/connection-mode-phase1
Open

Phase 1: ConnectionModeSelect replaces Lazy toggle (issue netbirdio/netbird#5989)#627
MichaelUray wants to merge 1 commit intonetbirdio:mainfrom
MichaelUray:feat/connection-mode-phase1

Conversation

@MichaelUray
Copy link
Copy Markdown

@MichaelUray MichaelUray commented May 1, 2026

Summary

Companion to backend PR netbirdio/netbird#6047.

Replaces the binary "Lazy Connection" toggle in the Client Settings tab with a connection-mode dropdown. Phase 1 shows two values (P2P, P2P Lazy) plus a conditional relay-timeout input that only appears in P2P-Lazy mode.

The other two modes (relay-forced, p2p-dynamic) remain admin-only via the management API in Phase 1:

  • relay-forced is a power-user / compliance choice that should not be one click away in the UI.
  • p2p-dynamic is reserved at the proto / DB level but its daemon implementation is Phase 2; surfacing it in the dropdown before the daemon honours it would be misleading.

Backwards-compat

The save callback writes both the new fields AND mirrors p2p-lazy onto the legacy lazy_connection_enabled boolean -- so admins running an older backend or older clients continue to see consistent behaviour.

The dropdown's initial value comes from connection_mode if set, falling back to the legacy lazy_connection_enabled boolean via resolveLegacyLazyBool. Mode-change preserves the persisted relay-timeout (per spec section 5.3): users only lose their entered timeout if they explicitly clear the input.

Test plan

  • TypeScript strict mode passes for the changed files
  • Manual test against the Phase-1 backend on a production instance
  • Backwards-compat verified: 32 connected peers across 12 router versions + Windows + Android + iOS continued to function with no disconnect during the cutover

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features
    • Added connection mode selector with options for relay-forced, P2P, P2P Lazy, and P2P Dynamic modes.
    • Added relay timeout and P2P timeout configuration settings for fine-tuned connection control.
    • Settings are now persistently saved and backward compatible with legacy configurations.

Phase 1 of issue netbirdio/netbird#5989. The dashboard now exposes the
new connection_mode field as a 2-value dropdown (P2P, P2P Lazy) with a
conditional relay-timeout input that only appears in P2P-Lazy mode.

The two other modes (relay-forced, p2p-dynamic) remain admin-only via
the management API in Phase 1; relay-forced because it is a power-user
compliance choice that should not be one click away in the UI, and
p2p-dynamic because its daemon-side implementation is reserved for
Phase 2.

Backwards-compat:
- The legacy lazy_connection_enabled boolean is written alongside the
  new mode (true when mode == p2p-lazy, false otherwise) so older
  daemon versions that only know the boolean keep behaving identically
  to today.
- Existing accounts with no connection_mode in the DB seed the dropdown
  via the same legacy-bool fallback used on the server side
  (resolveLegacyLazyBool helper).

Mode-change preserves the persisted relay-timeout value (per spec
section 5.3): users only lose their entered timeout if they explicitly
clear the input, not by switching modes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 1, 2026

📝 Walkthrough

Walkthrough

This pull request replaces the legacy lazy_connection_enabled boolean toggle with a new connection-mode configuration system. It adds three optional fields to the Account interface, introduces a new modeOptions module for centralized mode configuration and mapping helpers, and updates the settings UI component to use a dropdown selector with associated timeout inputs.

Changes

Cohort / File(s) Summary
Interface Definitions
src/interfaces/Account.ts
Added three optional fields to Account.settings.extra: connection_mode (enum: "relay-forced", "p2p", "p2p-lazy", "p2p-dynamic", or null), relay_timeout_seconds (number or null), and p2p_timeout_seconds (number or null).
Configuration Module
src/modules/settings/connectionmode/modeOptions.ts
New module defining connection mode values, per-mode metadata (label, visibility, timeout toggles), default timeout constants, and utility functions to map between legacy lazy_connection_enabled boolean and new connection_mode enum values.
Settings UI Component
src/modules/settings/ClientSettingsTab.tsx
Replaced boolean lazy-connection toggle with connection-mode dropdown and optional relay-timeout input. Added state initialization from both new and legacy fields with fallback logic, change handlers with validation, and conditional UI visibility based on mode metadata.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

Suggested reviewers

  • pappz

Poem

🐰 Hops excitedly

No more toggles, just a dropdown so neat,
Connection modes now make the config complete!
P2P lazy, dynamic, relay-forced too,
With timeouts to tune—the old boolean's adieu! 🎉

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is largely complete with comprehensive summary, backwards-compatibility details, and test plan, but the required documentation section from the template is entirely missing. Complete the documentation section by selecting one checkbox option and providing either a docs PR URL or explaining why documentation is not needed.
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically summarizes the main change: replacing a lazy toggle with a connection-mode selector in Phase 1, and includes the relevant issue reference.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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
Review rate limit: 6/8 reviews remaining, refill in 14 minutes and 41 seconds.

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

@CLAassistant
Copy link
Copy Markdown

CLAassistant commented May 1, 2026

CLA assistant check
All committers have signed the CLA.

Copy link
Copy Markdown

@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

🧹 Nitpick comments (1)
src/modules/settings/ClientSettingsTab.tsx (1)

236-245: 💤 Low value

Consider adding visual feedback for invalid timeout input.

When the user enters an invalid value (non-integer, negative), the handler silently returns without feedback. The user won't know why their input wasn't accepted. Since changes save immediately, consider either:

  1. Showing an inline error message (similar to versionError handling)
  2. Using a controlled input that prevents invalid characters

This is minor given the Phase 1 scope and the clear placeholder guidance.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/settings/ClientSettingsTab.tsx` around lines 236 - 245,
handleRelayTimeoutChange currently silently ignores invalid timeout input; add
visible inline feedback by introducing a local error state (e.g.,
relayTimeoutError) and update it inside handleRelayTimeoutChange: when trimmed
=== "" or parsed is valid clear relayTimeoutError and call
saveConnectionMode(connectionMode, parsed|null); when parsed is invalid set
relayTimeoutError to a short message (e.g., "Timeout must be a non‑negative
integer") and do not call saveConnectionMode. Also update the input element to
show the error text (similar to versionError handling) and consider adding
input-level guards (type="number" and min=0 or inputMode/pattern) to reduce
invalid keystrokes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/modules/settings/ClientSettingsTab.tsx`:
- Around line 439-451: The relay timeout Input rendered under
currentMeta.showsRelayTimeout lacks an accessible label; update the Input
component (the instance using value derived from relayTimeoutSeconds,
placeholder DEFAULT_RELAY_TIMEOUT_SECONDS, onChange handleRelayTimeoutChange,
and disabled tied to permission.settings.update) to include an accessible
label—e.g., add an aria-label or aria-labelledby that clearly describes the
field (such as "Relay timeout in seconds") so screen readers can identify the
input while keeping the existing customPrefix icon.
- Around line 200-227: The saveConnectionMode function currently performs an
optimistic update via setConnectionMode and setRelayTimeoutSeconds before
saveRequest.put completes; capture the previous values (e.g., const prevMode =
connectionMode; const prevTimeout = relayTimeoutSeconds) then perform the
optimistic set, call saveRequest.put(...).then(() => mutate("/accounts")), and
in the .catch() revert state with setConnectionMode(prevMode) and
setRelayTimeoutSeconds(prevTimeout) and surface an error notify; ensure the
notify promise uses the saveRequest.put promise (so rollback runs on failure)
and keep references to saveConnectionMode, setConnectionMode,
setRelayTimeoutSeconds, saveRequest.put, and mutate("/accounts") when
implementing the rollback.

---

Nitpick comments:
In `@src/modules/settings/ClientSettingsTab.tsx`:
- Around line 236-245: handleRelayTimeoutChange currently silently ignores
invalid timeout input; add visible inline feedback by introducing a local error
state (e.g., relayTimeoutError) and update it inside handleRelayTimeoutChange:
when trimmed === "" or parsed is valid clear relayTimeoutError and call
saveConnectionMode(connectionMode, parsed|null); when parsed is invalid set
relayTimeoutError to a short message (e.g., "Timeout must be a non‑negative
integer") and do not call saveConnectionMode. Also update the input element to
show the error text (similar to versionError handling) and consider adding
input-level guards (type="number" and min=0 or inputMode/pattern) to reduce
invalid keystrokes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4175f2d5-ee22-4e71-8b4b-58e3b961f66c

📥 Commits

Reviewing files that changed from the base of the PR and between d76cbd1 and 824d58a.

📒 Files selected for processing (3)
  • src/interfaces/Account.ts
  • src/modules/settings/ClientSettingsTab.tsx
  • src/modules/settings/connectionmode/modeOptions.ts

Comment on lines +200 to 227
// Phase 1 (#5989): persist mode + timeout, AND mirror onto the legacy
// lazy_connection_enabled boolean so older daemon versions stay in sync.
const saveConnectionMode = async (
nextMode: ConnectionModeValue,
nextRelayTimeout: number | null,
) => {
setConnectionMode(nextMode);
setRelayTimeoutSeconds(nextRelayTimeout);

notify({
title: "Lazy Connections",
description: `Lazy Connections successfully ${
toggle ? "enabled" : "disabled"
}.`,
title: "Connection Mode",
description: "Connection mode updated.",
promise: saveRequest
.put({
id: account.id,
settings: {
...account.settings,
lazy_connection_enabled: toggle,
connection_mode: nextMode,
relay_timeout_seconds: nextRelayTimeout,
lazy_connection_enabled: modeImpliesLegacyLazy(nextMode),
},
})
.then(() => {
setLazyConnection(toggle);
mutate("/accounts");
}),
loadingMessage: "Updating Lazy Connections setting...",
loadingMessage: "Updating connection mode...",
});
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Optimistic state update without rollback on failure.

Lines 206-207 update local state before the API call completes. If saveRequest.put() fails, the UI will show the new values while the server retains the old ones.

Consider capturing the previous state and reverting on error:

🛡️ Proposed fix to add rollback on failure
   const saveConnectionMode = async (
     nextMode: ConnectionModeValue,
     nextRelayTimeout: number | null,
   ) => {
+    const prevMode = connectionMode;
+    const prevTimeout = relayTimeoutSeconds;
     setConnectionMode(nextMode);
     setRelayTimeoutSeconds(nextRelayTimeout);

     notify({
       title: "Connection Mode",
       description: "Connection mode updated.",
       promise: saveRequest
         .put({
           id: account.id,
           settings: {
             ...account.settings,
             connection_mode: nextMode,
             relay_timeout_seconds: nextRelayTimeout,
             lazy_connection_enabled: modeImpliesLegacyLazy(nextMode),
           },
         })
         .then(() => {
           mutate("/accounts");
+        })
+        .catch((err) => {
+          setConnectionMode(prevMode);
+          setRelayTimeoutSeconds(prevTimeout);
+          throw err;
         }),
       loadingMessage: "Updating connection mode...",
     });
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/settings/ClientSettingsTab.tsx` around lines 200 - 227, The
saveConnectionMode function currently performs an optimistic update via
setConnectionMode and setRelayTimeoutSeconds before saveRequest.put completes;
capture the previous values (e.g., const prevMode = connectionMode; const
prevTimeout = relayTimeoutSeconds) then perform the optimistic set, call
saveRequest.put(...).then(() => mutate("/accounts")), and in the .catch() revert
state with setConnectionMode(prevMode) and setRelayTimeoutSeconds(prevTimeout)
and surface an error notify; ensure the notify promise uses the saveRequest.put
promise (so rollback runs on failure) and keep references to saveConnectionMode,
setConnectionMode, setRelayTimeoutSeconds, saveRequest.put, and
mutate("/accounts") when implementing the rollback.

Comment on lines +439 to +451
{currentMeta.showsRelayTimeout && (
<Input
value={
relayTimeoutSeconds === null
? ""
: String(relayTimeoutSeconds)
}
customPrefix={<ClockFadingIcon size={14} />}
placeholder={String(DEFAULT_RELAY_TIMEOUT_SECONDS)}
onChange={(e) => handleRelayTimeoutChange(e.target.value)}
disabled={!permission.settings.update}
/>
)}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add accessible label for the relay timeout input.

The timeout input uses only an icon as customPrefix with no visible or accessible label. Screen reader users won't understand the field's purpose.

♿ Proposed fix to add aria-label
               <Input
                 value={
                   relayTimeoutSeconds === null
                     ? ""
                     : String(relayTimeoutSeconds)
                 }
                 customPrefix={<ClockFadingIcon size={14} />}
                 placeholder={String(DEFAULT_RELAY_TIMEOUT_SECONDS)}
                 onChange={(e) => handleRelayTimeoutChange(e.target.value)}
                 disabled={!permission.settings.update}
+                aria-label="Relay timeout in seconds"
               />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
{currentMeta.showsRelayTimeout && (
<Input
value={
relayTimeoutSeconds === null
? ""
: String(relayTimeoutSeconds)
}
customPrefix={<ClockFadingIcon size={14} />}
placeholder={String(DEFAULT_RELAY_TIMEOUT_SECONDS)}
onChange={(e) => handleRelayTimeoutChange(e.target.value)}
disabled={!permission.settings.update}
/>
)}
{currentMeta.showsRelayTimeout && (
<Input
value={
relayTimeoutSeconds === null
? ""
: String(relayTimeoutSeconds)
}
customPrefix={<ClockFadingIcon size={14} />}
placeholder={String(DEFAULT_RELAY_TIMEOUT_SECONDS)}
onChange={(e) => handleRelayTimeoutChange(e.target.value)}
disabled={!permission.settings.update}
aria-label="Relay timeout in seconds"
/>
)}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/modules/settings/ClientSettingsTab.tsx` around lines 439 - 451, The relay
timeout Input rendered under currentMeta.showsRelayTimeout lacks an accessible
label; update the Input component (the instance using value derived from
relayTimeoutSeconds, placeholder DEFAULT_RELAY_TIMEOUT_SECONDS, onChange
handleRelayTimeoutChange, and disabled tied to permission.settings.update) to
include an accessible label—e.g., add an aria-label or aria-labelledby that
clearly describes the field (such as "Relay timeout in seconds") so screen
readers can identify the input while keeping the existing customPrefix icon.

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