From 66aa3d8cff36b54ec6a52617ab7a32d625fda83e Mon Sep 17 00:00:00 2001 From: mlsmaycon Date: Sun, 12 Jul 2026 21:45:16 +0200 Subject: [PATCH 1/7] Make Agent Network menu available via dashboard_features.agent_network Wire the new dashboard_features account setting into the dashboard at the API level: add it to the Account settings interface (so settings updates round-trip it) and have useAgentNetworkMode treat dashboard_features.agent_network === true as enabling the Agent Network menu alongside the full dashboard. Unlike agent_network_only it does not focus the dashboard, and it does not trigger the onboarding flow. No settings-menu UI yet; the field is set via the API. --- src/interfaces/Account.ts | 7 +++++++ src/modules/agent-network/useAgentNetworkMode.ts | 6 +++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/interfaces/Account.ts b/src/interfaces/Account.ts index 38b96493..0632fb1a 100644 --- a/src/interfaces/Account.ts +++ b/src/interfaces/Account.ts @@ -36,10 +36,17 @@ export interface Account { ipv6_enabled_groups?: string[]; network_range_v6?: string; agent_network_only?: boolean; + dashboard_features?: AccountDashboardFeatures; }; onboarding?: AccountOnboarding; } +// AccountDashboardFeatures holds per-account dashboard section visibility +// overrides. Omitted keys follow the default dashboard behavior. +export interface AccountDashboardFeatures { + agent_network?: boolean; +} + export interface AccountOnboarding { onboarding_flow_pending: boolean; signup_form_pending: boolean; diff --git a/src/modules/agent-network/useAgentNetworkMode.ts b/src/modules/agent-network/useAgentNetworkMode.ts index cb93fdb2..63b038c2 100644 --- a/src/modules/agent-network/useAgentNetworkMode.ts +++ b/src/modules/agent-network/useAgentNetworkMode.ts @@ -54,7 +54,11 @@ export const useAgentNetworkMode = () => { const setting = account?.settings?.agent_network_only; const only = setting ?? (isAgentNetworkSignupPending(account) || isAgentNetworkOnly()); - const enabled = only || isAgentNetworkEnabled(); + // dashboard_features.agent_network makes the Agent Network menu available + // alongside the full dashboard (unlike "only", which hides everything else). + const featureEnabled = + account?.settings?.dashboard_features?.agent_network === true; + const enabled = only || featureEnabled || isAgentNetworkEnabled(); const loading = permission.accounts.read ? isLoading : false; return { only, enabled, loading } as const; }, [accounts, isLoading, permission.accounts.read]); From 992c4de581292fb69f2d0df9b6d192ccd870ce46 Mon Sep 17 00:00:00 2001 From: mlsmaycon Date: Sun, 12 Jul 2026 21:54:10 +0200 Subject: [PATCH 2/7] Add e2e coverage for dashboard_features.agent_network menu availability --- e2e/tests/agent-network-focused-view.spec.ts | 25 ++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/e2e/tests/agent-network-focused-view.spec.ts b/e2e/tests/agent-network-focused-view.spec.ts index 2a0d559c..ab673cef 100644 --- a/e2e/tests/agent-network-focused-view.spec.ts +++ b/e2e/tests/agent-network-focused-view.spec.ts @@ -22,6 +22,9 @@ type AccountMock = { // undefined leaves the setting absent (as an un-onboarded account would be). agentNetworkOnly?: boolean; signupFormPending?: boolean; + // dashboardFeaturesAgentNetwork sets settings.dashboard_features.agent_network, + // which makes the Agent Network menu available without focusing the dashboard. + dashboardFeaturesAgentNetwork?: boolean; }; // mockAccounts rewrites GET /accounts to inject the focused-view setting and @@ -51,6 +54,11 @@ function mockAccounts( } else { body[0].settings.agent_network_only = applied; } + if (initial.dashboardFeaturesAgentNetwork !== undefined) { + body[0].settings.dashboard_features = { + agent_network: initial.dashboardFeaturesAgentNetwork, + }; + } body[0].onboarding = { ...(body[0].onboarding ?? {}), signup_form_pending: !!initial.signupFormPending, @@ -179,6 +187,23 @@ test.describe.serial("Agent Network focused view @agent-network", () => { } }); + test("dashboard_features.agent_network makes the menu available without focusing", async ({ + browser, + }) => { + const { page, close } = await openWithAccount(browser, { + dashboardFeaturesAgentNetwork: true, + }); + try { + // The Agent Network menu is available... + await expect(navItem(page, "Agent Network")).toBeVisible(); + // ...but the dashboard is not focused: a regular section that focused + // mode hides (Reverse Proxy) is still present. + await expect(navItem(page, "Reverse Proxy")).toBeVisible(); + } finally { + await close(); + } + }); + test("applies the focused view optimistically for a pending netbird.ai signup and persists it", async ({ browser, }) => { From b30d16284f53aab0bbf83b776f22f17a4c7ab862 Mon Sep 17 00:00:00 2001 From: mlsmaycon Date: Sun, 12 Jul 2026 22:06:43 +0200 Subject: [PATCH 3/7] Set dashboard_features for existing netbird.ai accounts The signup-source flow only handled new accounts (agent_network_only) and discarded the source for existing accounts, so nothing set dashboard_features. Branch on account existence: new accounts get the focused view, existing accounts get the Agent Network menu made available via dashboard_features.agent_network. Adds e2e coverage for the existing-account write. --- e2e/tests/agent-network-focused-view.spec.ts | 23 +++++++++++ src/cloud/contexts/NetBirdCloudProvider.tsx | 40 +++++++++++++------- 2 files changed, 50 insertions(+), 13 deletions(-) diff --git a/e2e/tests/agent-network-focused-view.spec.ts b/e2e/tests/agent-network-focused-view.spec.ts index ab673cef..56e965b5 100644 --- a/e2e/tests/agent-network-focused-view.spec.ts +++ b/e2e/tests/agent-network-focused-view.spec.ts @@ -229,6 +229,29 @@ test.describe.serial("Agent Network focused view @agent-network", () => { } }); + test("existing netbird.ai account gets dashboard_features, not the focused view", async ({ + browser, + }) => { + // An existing account (signup form already submitted) with the netbird.ai + // source should have the Agent Network menu made available via + // dashboard_features, not the focused agent_network_only view. + const { page, captured, close } = await openWithAccount( + browser, + { signupFormPending: false }, + { signupSource: true }, + ); + try { + await expect + .poll( + () => captured.putBody?.settings?.dashboard_features?.agent_network, + ) + .toBe(true); + expect(captured.putBody?.settings?.agent_network_only).not.toBe(true); + } finally { + await close(); + } + }); + test("exposes the focused-view toggle in client settings", async ({ browser, }) => { diff --git a/src/cloud/contexts/NetBirdCloudProvider.tsx b/src/cloud/contexts/NetBirdCloudProvider.tsx index cc96efd8..e54c1743 100644 --- a/src/cloud/contexts/NetBirdCloudProvider.tsx +++ b/src/cloud/contexts/NetBirdCloudProvider.tsx @@ -51,32 +51,44 @@ export const NetBirdCloudProvider = () => { } catch (e) {} }, []); - // Apply the netbird.ai signup source once the account is available. Only - // new accounts (signup form still pending) are switched to the Agent - // Network focused view — a stale flag from an existing user is discarded. + // Apply the netbird.ai signup source once the account is available. New + // accounts (signup form still pending) get the focused view + // (agent_network_only); existing accounts just get the Agent Network menu + // made available (dashboard_features.agent_network), leaving the rest of + // their dashboard intact. useEffect(() => { if (!account || signupSourceApplied.current) return; try { const source = localStorage.getItem(SIGNUP_SOURCE_LOCAL_STORAGE_KEY); if (source !== AGENT_NETWORK_SIGNUP_SOURCE) return; - if ( - account.onboarding?.signup_form_pending !== true || - account.settings?.agent_network_only === true - ) { + const isNewAccount = account.onboarding?.signup_form_pending === true; + const alreadyApplied = isNewAccount + ? account.settings?.agent_network_only === true + : account.settings?.dashboard_features?.agent_network === true; + + if (alreadyApplied) { localStorage.removeItem(SIGNUP_SOURCE_LOCAL_STORAGE_KEY); return; } signupSourceApplied.current = true; + const settings = isNewAccount + ? { ...account.settings, agent_network_only: true } + : { + ...account.settings, + dashboard_features: { + ...account.settings?.dashboard_features, + agent_network: true, + }, + }; notify({ title: "Agent Network", - description: "Agent Network focused view enabled for your account.", + description: isNewAccount + ? "Agent Network focused view enabled for your account." + : "Agent Network enabled for your account.", promise: accountRequest( - { - id: account.id, - settings: { ...account.settings, agent_network_only: true }, - }, + { id: account.id, settings }, "/" + account.id, ).then(async () => { // Revalidate before clearing the source key so the persisted @@ -87,7 +99,9 @@ export const NetBirdCloudProvider = () => { await mutate("/accounts"); localStorage.removeItem(SIGNUP_SOURCE_LOCAL_STORAGE_KEY); }), - loadingMessage: "Enabling Agent Network focused view...", + loadingMessage: isNewAccount + ? "Enabling Agent Network focused view..." + : "Enabling Agent Network...", }); } catch (e) {} }, [account]); From a78e66e0aacfd7f8577cc2e41b0b942ff051bb2d Mon Sep 17 00:00:00 2001 From: mlsmaycon Date: Sun, 12 Jul 2026 22:13:58 +0200 Subject: [PATCH 4/7] Also enable Agent Network menu for new netbird.ai accounts New accounts got agent_network_only (focused) but not dashboard_features.agent_network, so turning the focused view off later would remove the Agent Network menu entirely. Set both on new accounts so disabling focus degrades to menu-available rather than losing access. --- e2e/tests/agent-network-focused-view.spec.ts | 6 +++++- src/cloud/contexts/NetBirdCloudProvider.tsx | 21 +++++++++++--------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/e2e/tests/agent-network-focused-view.spec.ts b/e2e/tests/agent-network-focused-view.spec.ts index 56e965b5..bffebee3 100644 --- a/e2e/tests/agent-network-focused-view.spec.ts +++ b/e2e/tests/agent-network-focused-view.spec.ts @@ -217,10 +217,14 @@ test.describe.serial("Agent Network focused view @agent-network", () => { await expect(navItem(page, "Agent Network")).toBeVisible(); await expect(navItem(page, "Networks")).toHaveCount(0); - // The signup source is persisted as the account setting. + // The signup source is persisted: focused view plus the Agent Network + // menu flag, so turning focus off later keeps access to Agent Network. await expect .poll(() => captured.putBody?.settings?.agent_network_only) .toBe(true); + expect( + captured.putBody?.settings?.dashboard_features?.agent_network, + ).toBe(true); // The focused view is retained after the write settles. await expect(navItem(page, "Networks")).toHaveCount(0); diff --git a/src/cloud/contexts/NetBirdCloudProvider.tsx b/src/cloud/contexts/NetBirdCloudProvider.tsx index e54c1743..0bf4c6b1 100644 --- a/src/cloud/contexts/NetBirdCloudProvider.tsx +++ b/src/cloud/contexts/NetBirdCloudProvider.tsx @@ -73,15 +73,18 @@ export const NetBirdCloudProvider = () => { } signupSourceApplied.current = true; - const settings = isNewAccount - ? { ...account.settings, agent_network_only: true } - : { - ...account.settings, - dashboard_features: { - ...account.settings?.dashboard_features, - agent_network: true, - }, - }; + // Always enable the Agent Network menu (dashboard_features). New accounts + // additionally get the focused view (agent_network_only). Keeping the + // menu flag set means that if a focused account later turns the focused + // view off, it keeps access to Agent Network rather than losing the menu. + const settings = { + ...account.settings, + dashboard_features: { + ...account.settings?.dashboard_features, + agent_network: true, + }, + ...(isNewAccount ? { agent_network_only: true } : {}), + }; notify({ title: "Agent Network", description: isNewAccount From fd2aac42030c18de65a73773d3b1777ecaec4a34 Mon Sep 17 00:00:00 2001 From: mlsmaycon Date: Mon, 13 Jul 2026 11:59:08 +0200 Subject: [PATCH 5/7] Match badged nav labels by substring in dashboard_features e2e --- e2e/tests/agent-network-focused-view.spec.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/e2e/tests/agent-network-focused-view.spec.ts b/e2e/tests/agent-network-focused-view.spec.ts index bffebee3..6f7a43df 100644 --- a/e2e/tests/agent-network-focused-view.spec.ts +++ b/e2e/tests/agent-network-focused-view.spec.ts @@ -118,6 +118,13 @@ function navItem(page: Page, text: string) { .getByText(text, { exact: true }); } +// navItemContaining matches a sidebar entry by a substring of its label. Some +// entries (Agent Network, Reverse Proxy) render a "Beta" badge inside the label +// when the dashboard is not focused, so exact text matching would miss them. +function navItemContaining(page: Page, text: string) { + return page.getByTestId("left-navigation-item").filter({ hasText: text }); +} + // Regular dashboard sections that the focused view hides (gated on // !agentNetworkOnly in Navigation.tsx). const FOCUS_HIDDEN_NAV = ["Networks", "Reverse Proxy", "DNS", "Activity"]; @@ -194,11 +201,12 @@ test.describe.serial("Agent Network focused view @agent-network", () => { dashboardFeaturesAgentNetwork: true, }); try { - // The Agent Network menu is available... - await expect(navItem(page, "Agent Network")).toBeVisible(); + // The Agent Network menu is available (the label carries a "Beta" badge + // outside focused mode, so match on the label substring)... + await expect(navItemContaining(page, "Agent Network")).toBeVisible(); // ...but the dashboard is not focused: a regular section that focused // mode hides (Reverse Proxy) is still present. - await expect(navItem(page, "Reverse Proxy")).toBeVisible(); + await expect(navItemContaining(page, "Reverse Proxy")).toBeVisible(); } finally { await close(); } From 32b7ceb50840a4ba24b973401074f894e4cb8c61 Mon Sep 17 00:00:00 2001 From: mlsmaycon Date: Mon, 13 Jul 2026 15:42:11 +0200 Subject: [PATCH 6/7] Treat agent network deployment config as a floor over account setting --- src/modules/agent-network/useAgentNetworkMode.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/modules/agent-network/useAgentNetworkMode.ts b/src/modules/agent-network/useAgentNetworkMode.ts index 63b038c2..bd7f1e27 100644 --- a/src/modules/agent-network/useAgentNetworkMode.ts +++ b/src/modules/agent-network/useAgentNetworkMode.ts @@ -48,12 +48,15 @@ export const useAgentNetworkMode = () => { return useMemo(() => { const account = accounts?.[0]; - // An explicit account setting (true or false) always wins so a user can - // opt out even when the deployment config enables focused mode. Only when - // the setting is absent do signup optimism and deployment config apply. + // Deployment config is a floor: NETBIRD_AGENT_NETWORK_ONLY focuses the + // dashboard regardless of the per-account setting. The management API always + // serializes agent_network_only (defaulting to false), so a false value + // can't be read as "unset" — without this floor it would silently override + // the env flag. When the config flag is off (e.g. cloud) the per-account + // setting decides, so a user can still opt in via signup or the toggle. const setting = account?.settings?.agent_network_only; const only = - setting ?? (isAgentNetworkSignupPending(account) || isAgentNetworkOnly()); + isAgentNetworkOnly() || (setting ?? isAgentNetworkSignupPending(account)); // dashboard_features.agent_network makes the Agent Network menu available // alongside the full dashboard (unlike "only", which hides everything else). const featureEnabled = From 94cf3ae40dc9af616ace8f108a7f78344f31b827 Mon Sep 17 00:00:00 2001 From: mlsmaycon Date: Mon, 13 Jul 2026 16:05:34 +0200 Subject: [PATCH 7/7] Add agent network env x signup-source e2e matrix with a config override --- e2e/tests/agent-network-focused-view.spec.ts | 85 +++++++++++++++++++- src/utils/netbird.ts | 27 +++++++ 2 files changed, 111 insertions(+), 1 deletion(-) diff --git a/e2e/tests/agent-network-focused-view.spec.ts b/e2e/tests/agent-network-focused-view.spec.ts index 6f7a43df..337003aa 100644 --- a/e2e/tests/agent-network-focused-view.spec.ts +++ b/e2e/tests/agent-network-focused-view.spec.ts @@ -17,6 +17,11 @@ import { loginToApp, navigateTo } from "../helpers/auth"; const SIGNUP_SOURCE_KEY = "netbird-signup-source"; const AGENT_NETWORK_SOURCE = "netbird.ai"; +// Drives the NETBIRD_AGENT_NETWORK_ONLY / _ENABLED deployment flags against the +// test build (see testAgentNetworkOverride in utils/netbird.ts). +const AGENT_NETWORK_CONFIG_KEY = "netbird-test-agent-network"; + +type AgentNetworkConfig = "only" | "enabled" | "off"; type AccountMock = { // undefined leaves the setting absent (as an un-onboarded account would be). @@ -86,7 +91,10 @@ function mockAccounts( async function openWithAccount( browser: Browser, account: AccountMock, - opts: { signupSource?: boolean } = {}, + opts: { + signupSource?: boolean; + agentNetworkConfig?: AgentNetworkConfig; + } = {}, ): Promise<{ page: Page; captured: { putBody?: any }; @@ -105,6 +113,16 @@ async function openWithAccount( [SIGNUP_SOURCE_KEY, AGENT_NETWORK_SOURCE], ); } + if (opts.agentNetworkConfig) { + await context.addInitScript( + ([key, value]) => { + try { + window.localStorage.setItem(key as string, value as string); + } catch (e) {} + }, + [AGENT_NETWORK_CONFIG_KEY, opts.agentNetworkConfig], + ); + } const page = await context.newPage(); const captured: { putBody?: any } = {}; mockAccounts(page, account, captured); @@ -264,6 +282,71 @@ test.describe.serial("Agent Network focused view @agent-network", () => { } }); + // Matrix of the two deployment flags (NETBIRD_AGENT_NETWORK_ONLY / _ENABLED, + // "off" = neither) crossed with the netbird.ai signup source, with no explicit + // account settings. The API defaults agent_network_only to false, so the + // deployment flags must still take effect. A present source implies a new + // signup (signup_form_pending), which focuses the dashboard optimistically + // regardless of the flags. + type Expectation = "hidden" | "available" | "focused"; + + const ENV_SIGNUP_MATRIX: { + env: AgentNetworkConfig; + signupSource: boolean; + expected: Expectation; + }[] = [ + { env: "off", signupSource: false, expected: "hidden" }, + { env: "off", signupSource: true, expected: "focused" }, + { env: "enabled", signupSource: false, expected: "available" }, + { env: "enabled", signupSource: true, expected: "focused" }, + { env: "only", signupSource: false, expected: "focused" }, + { env: "only", signupSource: true, expected: "focused" }, + ]; + + async function expectAgentNetworkState(page: Page, expected: Expectation) { + // Peers always renders for an owner, confirming the sidebar loaded. + await expect(navItem(page, "Peers")).toBeVisible(); + + if (expected === "hidden") { + await expect(navItemContaining(page, "Agent Network")).toHaveCount(0); + // The regular dashboard stays intact. + await expect(navItemContaining(page, "Reverse Proxy")).toBeVisible(); + return; + } + + await expect(navItemContaining(page, "Agent Network")).toBeVisible(); + + if (expected === "focused") { + for (const label of FOCUS_HIDDEN_NAV) { + await expect(navItem(page, label)).toHaveCount(0); + } + return; + } + + // available: the menu shows alongside the regular dashboard. + await expect(navItemContaining(page, "Reverse Proxy")).toBeVisible(); + } + + for (const { env, signupSource, expected } of ENV_SIGNUP_MATRIX) { + const sourceLabel = signupSource ? "netbird.ai" : "none"; + test(`env=${env} signup_source=${sourceLabel} -> ${expected}`, async ({ + browser, + }) => { + const { page, close } = await openWithAccount( + browser, + // A source present means a fresh netbird.ai signup (form still pending); + // otherwise a plain account with no agent-network settings. + { signupFormPending: signupSource }, + { agentNetworkConfig: env, signupSource }, + ); + try { + await expectAgentNetworkState(page, expected); + } finally { + await close(); + } + }); + } + test("exposes the focused-view toggle in client settings", async ({ browser, }) => { diff --git a/src/utils/netbird.ts b/src/utils/netbird.ts index 0a5c7559..92e5fc00 100644 --- a/src/utils/netbird.ts +++ b/src/utils/netbird.ts @@ -71,10 +71,35 @@ export const hasLicensedFlag = () => { return config.licensed || isNetBirdCloud(); }; +// testAgentNetworkOverride lets e2e tests drive the deployment flags +// NETBIRD_AGENT_NETWORK_ONLY / NETBIRD_AGENT_NETWORK_ENABLED against the test +// build via localStorage. "off" forces both flags off regardless of the build +// config. Inert outside test builds, where the APP_ENV check is replaced at +// compile time and tree-shaken away. +export const testAgentNetworkOverride = (): + | "only" + | "enabled" + | "off" + | undefined => { + if (process.env.APP_ENV !== "test") return undefined; + if (typeof window === "undefined") return undefined; + try { + const value = window.localStorage.getItem("netbird-test-agent-network"); + if (value === "only" || value === "enabled" || value === "off") { + return value; + } + } catch (e) { + return undefined; + } + return undefined; +}; + // isAgentNetworkOnly returns true for the dedicated Agent Network surface: // the regular UI (network routing, DNS, reverse proxy, activity) is hidden and // the Agent Network menu shows without a Beta badge. export const isAgentNetworkOnly = () => { + const override = testAgentNetworkOverride(); + if (override) return override === "only"; return config.agentNetworkOnly; }; @@ -91,6 +116,8 @@ export const pkgsDownloadUrl = (path: string) => // (Providers, Policies, Usage & Logs) is available — in either the dedicated // "only" mode or alongside the regular UI (where it carries a Beta badge). export const isAgentNetworkEnabled = () => { + const override = testAgentNetworkOverride(); + if (override) return override === "only" || override === "enabled"; return config.agentNetworkEnabled || config.agentNetworkOnly; };