Compare commits

...
Author SHA1 Message Date
TechHutTV d0cca1aa31 Darker light-mode danger-text 2026-09-16 11:26:30 -07:00
Brandon Hopkins 6a26f546c5 Hydration-safe theme provider, review contrast fixes 2026-09-15 08:17:15 -07:00
Brandon Hopkins 5776a7dd5c Tokenize hard-coded colors, fix dark-only literals in light mode 2026-09-15 07:27:08 -07:00
Brandon Hopkins 208176ba9d Align colors with desktop UI 2026-09-15 07:14:08 -07:00
Brandon Hopkins 8a1ba01ef2 Merge remote-tracking branch 'origin/main' into ui-accessibility 2026-09-15 06:58:25 -07:00
Maycon SantosandClaude Fable 5.1 da81cb7f50 Pick a private-capable proxy cluster when bootstrapping the agent network (#780)
* Pick a private-capable proxy cluster when bootstrapping the agent network

The connect-provider wizard bootstraps the account's agent network
endpoint from a proxy cluster it auto-picks off /reverse-proxies/domains,
taking the first validated free domain. That list includes clusters that
cannot host the endpoint: the endpoint is a private service, reachable
only from connected peers and authorised by their tunnel identity, which
needs a cluster with at least one connected embedded proxy — the same
supports_private flag the Reverse Proxy modal gates NetBird-Only Access
on. Management refuses a bootstrap onto a cluster reporting it false, so
the wizard could propose a pick the API rejects, and the endpoint it
assigns is immutable.

Filter the candidates on supports_private before auto-picking, and give
the "nothing to pick" callout a second message for the case where
clusters exist but none of them has an embedded proxy — a different
problem from having no proxy at all, and a different fix.

Only an explicit false disqualifies a cluster: a management build that
predates the flag reports nothing at all, and dropping every cluster
there would block setup on a backend that would have accepted it.

Callout takes an optional data-testid so a spec can assert on the state
it announces rather than on its copy.

* Derive the bootstrap cluster instead of holding it in state

Nothing picks the bootstrap cluster: there is no selector, so the state
could only ever mirror the first usable entry of the domains list, with
an effect writing it back on arrival and a render pass to show it. Derive
it from that list instead, which drops the state, the effect and the two
resets that kept them in sync.

* Declare the e2e proxy private so a bootstrap cluster exists

The connect-provider wizard now offers only clusters whose proxies report
the private capability, because the agent network endpoint is a private
service and management refuses to bootstrap it anywhere else. The e2e
environment's reverse-proxy ran with NB_PROXY_PRIVATE at its default of
false, so with that filter in place no cluster qualified: the wizard
showed the no-cluster callout, Continue stayed disabled, and every spec
that walks through the wizard failed on the Models tab it could no longer
reach — agentgateway, Kimi and the refused-save spec.

Declare the capability on the primary proxy. The no-ports proxy stays
centralised, so the pick also has to skip past a cluster that does not
qualify, which is the case the wizard filter exists for.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sa3DsBDP3VciAi4PPG17L6

* Send the no-cluster callout to the agent network Clusters section

The callout told users to connect a proxy under Reverse Proxy, the generic
services page. Agent Network has its own Clusters tab under Configuration,
which is where a user setting up the agent network expects to manage the
clusters it runs on, so both messages link there instead. The tab and the
callout gate on the same permission (services read), so every role that
sees the callout can follow the link.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sa3DsBDP3VciAi4PPG17L6

* Say private capabilities, not embedded proxy, in the no-cluster callout

The requirement is a cluster with private capabilities, which is what the
supports_private flag reports. Tying the message to an embedded proxy named
one way of getting there as if it were the requirement itself, and pointed
users at the wrong fix. The callout now states the requirement and where to
connect such a cluster.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Sa3DsBDP3VciAi4PPG17L6

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-14 22:19:37 +02:00
Jack Carter f4e80d37c4 feat: restore the Active toggle and keep it visible on disabled rows (#803)
Disabled routing peers, network resources, policies and legacy routes were
only distinguishable by a faded row, and the fade also dimmed the controls
needed to re-enable them. Bring back the Active toggle column that was
hidden in #654, and add a cellClassName hook to DataTable with a shared
fadeDisabledRowCells helper that fades every cell of a disabled row except
the toggle and actions cells.
2026-09-14 15:43:50 +02:00
Pascal Fischer de045582e5 mark is cloud false unless explicitly overridden (#799) 2026-09-12 09:07:45 +02:00
Eduard GertandClaude Opus 5 6a85748b91 chore: update Next.js to 16.3.4 and refresh dependencies (#797)
Bump next from 16.3.0 to 16.3.4 and refresh the transitive lockfile
entries that came with it (@next/* swc binaries, sharp 0.35.4,
rolldown, vitest, playwright).

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-11 14:47:14 +02:00
Nicolas Frati a9a91905fa Add rootless dashboard container image (#791) 2026-09-09 23:00:19 +02:00
Eduard Gert 3459a4db28 chore: remove dead code and unused dependencies (#787)
Remove the unreachable FirewallGPT module and the abandoned traffic-event
detail prototypes, along with the dependencies they were the sole consumers of.

FirewallGPT was disabled in 9d8d9ee4 by removing its trigger button, leaving
the modal mounted but impossible to open. TrafficEventDetails was never
imported anywhere, and it was the only consumer of TrafficEventChart.

Dependency changes:

- Drop @faker-js/faker, whose only usage was mock chart data in the removed
  TrafficEventChart. This also resolves GHSA-qxc2-j82w-r537 without needing
  the 9 -> 10 major upgrade.
- Drop react-confetti-explosion, only used by the removed
  FirewallGPTSuccessModal.
- Bump @types/node to 20.19.43 so it satisfies the peer range of the vite
  version pulled in by vitest, silencing an ERESOLVE warning on install.
  Stays on the 20.x line to match the Node 20 used in CI.
- Add an allowScripts deny policy for the four packages with install scripts.
  Both @axa-fr postinstalls are bare echo statements, fsevents ships a
  prebuilt binary, and the unrs-resolver postinstall only verifies a binding
  that optionalDependencies already installs.
2026-09-09 11:38:38 +02:00
Maycon Santos cba2613e68 Add anonymization level and upload URL to the debug bundle modal (#753)
Surfaces the two bundle parameters added to the management API: an
anonymization level (Default/Strict, shown when anonymization is enabled)
and an optional upload service URL that overrides the default upload
server. The upload URL is validated as an https URL before the job can be
submitted; both fields are omitted from the request when left at their
defaults so the peer applies its own.
2026-09-03 20:05:02 +02:00
Maycon Santos fe32179dc1 Pin the group view's group, and give agentgateway a reachable upstream (#788)
Stabilize the flaky Control Center and agentgateway e2e specs (#788)

  Three separate causes behind the recent e2e failures.

  The group view ranked every group in the account by peer count to decide what
  to display, so with two workers it could land on a group another spec had just
  created. openGroupView() selects the group by name instead.

  Management now checks a provider's upstream URL when the provider is saved and
  rejects it unless the URL returns a model list, so the agentgateway spec's
  placeholder host stopped working. The test environment runs a stub that answers
  the check.

  The Backspace test blurred the group panel's search input before that input had
  taken focus, so it reclaimed focus and swallowed the key.
2026-09-03 18:38:12 +02:00
Eduard Gert 1bad86b277 Show peer-sourced policies in the Control Center peer view (#786)
The peer view matched policies only by source group membership, so a
policy whose source is the peer itself never appeared in that peer's
view. There was no group fallback either: a rule carrying a
sourceResource has its sources nulled on save, so the group check could
only ever return false for exactly these policies.

Such policies are reachable from the canvas itself, since dragging a
connection off a peer node writes a sourceResource rather than a group.
A user could create a peer-sourced policy, deploy it, drill into that
same peer and not see it.

Match rule.sourceResource against the peer id alongside the group check.
The rendering side needs no change, as select-peer-node is the peer and
its edge to the policy is drawn unconditionally. Draft mode is fixed
transitively: useDraft harvests visible policies from the live view's
nodes, and already draws a sourceResource peer as a source card.
2026-09-03 14:29:06 +02:00
Maycon Santos 3c72444e83 Keep a refused provider save on screen, and its discovered prices (#772)
A refused save closed the modal anyway, throwing away the key just typed — and
the API never returns one. Both paths now stop before closing. The failure was
also reported twice and, because notify() only reddens through its promise path,
both times as a success.

Load and submit now spin, say what they are waiting on, and stop taking clicks;
dismissal is refused mid-save, since the write lands regardless and would
otherwise close a session reopened in the meantime.

Discovery carries the record id and the form's URL together, so changing an
endpoint no longer demands the key back. A 422 from it was reported as
unreachable rather than unsupported — useApiCall rejects with { code, message }
and the hook read status, leaving the catalog fallback unreachable.

Discovered models arrived at zero: 0/0 was hardcoded for anything the catalog
lacked by exact id, which stopped being true once discovery returned real rates.
Bedrock felt all of it — its geography-prefixed ids never match by string.

A Playwright spec covers the refusal end to end.
2026-09-02 21:47:47 +02:00
Nicolas Frati 386fb12013 fix(e2e): stabilize agentgateway provider test (#782) 2026-09-02 19:09:36 +02:00
Daneyon Hansen e3cc74522c feat(agent-network): add agentgateway provider support (#774)
* feat(agent-network): add agentgateway provider UI

Signed-off-by: Daneyon Hansen <daneyon.hansen@solo.io>

* fix(agent-network): address agentgateway review feedback

Signed-off-by: Daneyon Hansen <daneyon.hansen@solo.io>

* fix(agent-network): align with updated dashboard lint

Signed-off-by: Daneyon Hansen <daneyon.hansen@solo.io>

---------

Signed-off-by: Daneyon Hansen <daneyon.hansen@solo.io>
2026-09-02 01:01:13 +02:00
Maycon Santosandbraginini 1537d31f0d Add Agent Network Admin and Usage Viewer roles, gate Agent Network per submodule (#764)
* Add Agent Network Admin and Usage Viewer roles, gate Agent Network per submodule

The management server now ships two delegated roles (agent_network_admin,
usage_viewer) and reports agent_network submodule permissions
(agent_network.providers, .policies, .usage, .logs, .settings, ...) in
/users/current. The dashboard still gated the whole Agent Network section
on services.read, so the new roles saw either everything or nothing.

- register the agent_network module keys so PermissionsProvider fills
  denied defaults and typed lookups work
- gate the Agent Network sidebar and pages per submodule: providers,
  policies, usage (usage or logs read), configuration (settings)
- show the section when any child is permitted instead of services.read
- add both roles to the role selector, user/invite tables and badges

* Gate Usage and Clusters tabs by their own permissions

A caller holding only agent_network.logs.read landed on the Usage tab it
cannot read, and one holding only agent_network.usage.read could open
Access Logs. Show each tab only with its matching read permission and
fall back to the first permitted tab on deep links.

The Clusters tab is a reverse-proxy surface: its table and controls run
on the services permission, so agent_network.settings alone (e.g.
agent_network_admin) no longer surfaces it.

* Fall back to a known configuration tab for unknown query values

An unknown ?tab= value selected no content pane and left the
configuration page blank; unknown or unauthorized values now fall back
to Global Limits.

* Derive the configuration tab from the URL query

The ?tab= query was mirrored into local state through an effect, which
can lag a render behind the URL and duplicates the source of truth.
Derive the active tab from the query directly; trigger clicks already
push the query themselves, and onChange keeps pushing it for Radix's
keyboard navigation, which fires without a click.

* Derive the usage tab from the URL query

Same treatment as the configuration page: the ?tab= query was mirrored
into local state through an effect, duplicating the source of truth.
Derive the active tab from the query; onTabChange keeps pushing it so
clicks, deep links and back/forward all resolve the same way.

* Group user roles into General and Agent Network tabs

  Tab the role dropdown by product surface, gate the Agent Network
  roles and tab on the Agent Network surface being available, and
  disable the role field for the owner unless the caller is an owner.

* Point agent network learn-more links at their docs pages

* Skip the domains lookup when the role can't read it

* Flatten the role selector and add a caller-scoped My Setup page

The role dropdown split General and Agent Network roles into tabs,
which reads as two products inside one field. Return to the single
flat list from main, keeping the surface gate (Agent Network roles
hidden when the surface is unavailable) and the owner protections.

Regular users had no way to see which providers and models their
policies allow or where to point their tools. Add
/agent-network/my-setup backed by the caller-scoped me/setup and
me/consumption endpoints: the endpoint with copy, per-provider
available models, the Configure Your Agent instructions flattened
inline from the connect modal, and the caller's own usage counters.
The sidebar surfaces it for any role — limited view included —
whenever the caller's setup is configured, and stays hidden when
not, which is indistinguishable from no access by design.

* Fix delegated-role API errors and reuse the usage overview for My Usage

Field testing surfaced three problems. The policy and budget modals'
group selector fetched /networks/resources unconditionally, so an
agent_network_admin without the networks permission got a 403 and an
error toast on the policies page; gate the fetch on networks read.

Restricted users got no navigation at all, so the limited view could
never reach My Setup; render the sidebar for restricted users when
their setup is configured or their role reads an agent_network surface.
Roles below account admin also cannot read the account feature flag
(accounts read), so holding an explicit agent_network grant now counts
as proof the surface exists, while callers with accounts read keep the
flag as the source of truth.

My Usage rendered raw per-window counter rows, nothing like the admin
usage view; render it with the same chart-and-table component as the
admin overview, fed from the caller-scoped me/usage/overview endpoint
that returns the identical bucket shape filtered to the current user.

* Reuse the providers-page agent config on My Setup and self-scope Usage & Logs

My Setup now presents the endpoint exactly like the providers page — the
API Base URL badge with Copy and Agent Config opening the same modal
(provider ids come from the caller's me/setup answer instead of the
admin-permissioned provider context) — and drops the My Usage tab: the
regular Usage & Logs page now serves callers without the account-wide
grants too, showing their own data (the server pins the filters to them),
with the identity and provider filters trimmed since they'd be overridden.

usage_viewer gets working filters: the feature-flag fallback moves into
useAgentNetworkMode, AIProvidersProvider gates each fetch on its
submodule read grant so partially-granted roles don't fire 403s, and
GroupsProvider serves restricted roles that hold the groups read grant.
The limited view also gains the Peers link (the add-your-device page).

* Hide provider write actions from read-only viewers

usage_viewer now reads the provider list, but the page still offered every
write flow: Connect Provider, row-click edit, and the enable/disable/delete
menu. Clicking any of them opened the wizard, which - unable to read the
settings row - warned that Agent Network isn't set up even on configured
accounts. Each action is now gated on its own operation grant: the connect
button needs providers create, row-click edit needs update, and the menu
items need update/delete respectively (no menu at all without either).

* Restore the grouped role selector tabs

Bring back the General and Agent Network tabs in the role dropdown
from 3fbeb1e, undoing the flattening: the tabbed grouping keeps the
list readable as more surfaces ship their own delegated roles. The
surface gate (Agent Network roles hidden when the surface is
unavailable) and the owner protections are unchanged.

* Offer the provider and model filters to self-scoped callers

The providers endpoint now self-scopes on the server, so a configured
plain user fetches it too and the caller's own authorized providers back
the Provider and Model filter options on the self-scoped Usage & Logs
view. The identity filters stay hidden — the server pins user and group
to the caller regardless.

* Give the agent config a single home on Connect Agent

  The endpoint card carried an Agent Config action on both the providers
  page and the caller-scoped self-service page, so the same connect
  snippets had two entry points. EndpointBadge now renders it only when
  the caller passes providerIds, which is just the self-service page; the
  providers page keeps the URL and Copy. The context-backed
  AgentConnectModal wrapper is then unused, so it goes.

  Rename that page from My Setup to Connect Agent in the sidebar,
  breadcrumb and heading: My Setup was the only first-person label in the
  navigation, and Connect Agent stays clear of the Configuration sibling.

* Simplify Bedrock config

* Inline the agent config on the Connect page

  Drop the Agent Config modal: its snippets now render inline under the
  endpoint card, so AgentConnectModal is just AgentConnectTabs and
  EndpointBadge is back to the URL and Copy. Move the route to
  /agent-network/connect and swap the hand-rolled provider list for the
  standard DataTable. Follow the backend rename of me/setup to
  agent-config.

* Show a policy's allowed models in the policies table

  A policy's model restrictions live on the guardrails attached to it, so
  the new Models column unions the enabled model allowlists: one chip for
  a single model, a count with the names on hover past that, and a
  Restrict badge opening the guardrails tab when nothing narrows the
  policy. Clicking the column lands on that tab too.

  Chip the models on the Connect Agent table instead of comma-joining
  them, and capitalize the model counts across all three tables to match
  the "2 Groups" chips on Peers.

* Point the Kimi e2e config assertions at the Connect Agent page

The Kimi-gated config surfaces moved: the Agent Config modal is gone and
the snippets render inline on /agent-network/connect, so the spec stopped
finding the button it used to click on the providers page. Navigate to the
Connect page instead.

That page's answer is caller-scoped — it only offers providers the
caller's own policies authorize — so the spec now grants the owner the
freshly connected provider through a dedicated group + policy, torn down
again (group detached from the owner first) before and after the run. The
backing GET /agent-network/agent-config ships with newer management
builds; on builds that predate it the suite skips, same as the existing
catalog and settings-bootstrap probes.

* Hand the agent config to every account member

  The agent-config answer withheld the endpoint from callers no policy
  covers, so "account not set up" and "you have no access" were the same
  shape. The dashboard shows every user the same connection config, so
  Configured now tracks the account: once it has an endpoint every member
  gets it, with an empty providers list for those no policy covers. That
  list stays caller-scoped, and the endpoint authorizes nothing on its own
  since the gateway still refuses every request no policy permits.

  Rename the files and symbols the endpoint outgrew when it became
  /agent-network/agent-config: me_handler.go, setup.go and types/setup.go
  become agent_config.go, GetSetupForUser becomes GetAgentConfigForUser,
  and the AgentNetworkMeSetup schema becomes AgentNetworkAgentConfig.
  Regenerating types.gen.go from the untouched spec first reproduced it
  byte for byte, so the generated diff is only these renames.

* Let the surface switch decide the Agent Network section

  Connect Agent and Usage & Logs were visible on the caller's own setup
  alone, so a deployment with Agent Network off still showed them, and the
  route tree then 404d the links the nav had just rendered. The switch now
  decides first.

  The caller's agent config still stands in for the switch, but only for
  callers who cannot read the config themselves - otherwise an account
  holding an endpoint kept the surface on for its owner regardless.

* Collapse the two User types the merge left behind

  Both sides of the merge declared a User type, in different regions of the
  helper, so the merge took both without ever reporting a conflict and
  Playwright refused to parse the file. One type now carries the union of
  the fields the /users answer returns.

* Fix SNAPSHOT version display

* Fix e2e tests

* Fix e2e tests

* Fix version display

* Fix e2e

* Fix e2e

* Fix e2e

---------

Co-authored-by: braginini <bangvalo@gmail.com>
2026-09-01 14:35:18 +02:00
TechHutTV e8df6c153c More fixes on posture check elements 2026-08-28 13:41:04 +02:00
TechHutTV 8023ff5047 Light mode fixes; borders, keycaps, callouts 2026-08-28 11:15:25 +02:00
TechHutTV 32ecc374bb Fix posture check icons, no change on switch 2026-08-28 10:37:55 +02:00
TechHutTV f3a4f22e1e Pinned posture check icon colors; darkened dialog borders 2026-08-28 10:21:36 +02:00
TechHutTV 968ca333c1 Tokenized canvas edge strockes plus tests 2026-08-27 23:32:47 +02:00
TechHutTV 1e77129bdf Lighter-light mode hover colors for badges/dropdown 2026-08-27 23:11:31 +02:00
TechHutTV 0dfdaf62c6 Fix button hover 2026-08-27 18:47:49 +02:00
TechHutTV 2f74430616 Banner colors and settings sidebar fix 2026-08-27 18:37:24 +02:00
TechHutTV b3cf73950e Fix button seperation 2026-08-27 18:33:37 +02:00
TechHutTV addea360a1 Add useTheme to testing 2026-08-27 18:21:44 +02:00
TechHutTV 4a597a7fdd Merge remote-tracking branch 'origin/main' into ui-accessibility 2026-08-27 17:43:54 +02:00
TechHutTV ba88b838f9 Merge remote-tracking branch 'origin/main' into ui-accessibility 2026-08-27 17:36:51 +02:00
Brandon Hopkins 4f905a4368 Add bulk deletion for Access Control groups (#752)
* Add multi-select bulk deletion for Access Control groups

* Add indeterminate group selection and permission-safe column visibility

* Background rule for checkbox component

* Better show when disabled
2026-08-27 17:32:25 +02:00
Eduard Gert 5cf8995c65 Detect Entra connection from auth service SSO connections (#776) 2026-08-27 11:51:27 +02:00
Eduard Gert ca16b4868b Add control center draft mode (#744) 2026-08-27 10:08:22 +02:00
Shaban Mohammedsaani Hassan d3ff3bafca fix: use --restart=unless-stopped instead of --rm in Docker install command (#760)
The generated Docker command used --rm, which removes the container on
stop. Combined with -d, the NetBird client would not survive a host
reboot despite the persistent netbird-client volume. Replace with
--restart=unless-stopped so the client reconnects automatically.
2026-08-27 09:51:35 +02:00
4ca8cc09a4 Fix typo in PeerExpirationToggle component (#665)
* Fix typo in PeerExpirationToggle component

* Fix the same typo in PeerActionCell

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Eduard Gert <kontakt@eduardgert.de>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-26 14:52:28 +02:00
Nicolas Frati 9e6d512ed1 ci: skip Docker publishing for community PRs (#775) 2026-08-26 14:40:03 +02:00
Viktor Liu 7b60d87652 Add a workflow bumping the WASM client on netbird releases (#770)
* Add a workflow bumping the WASM client on netbird releases

* Cover the pinned WASM client with an instantiation test

* Connect the pinned WASM client to management in the e2e suite

* Validate the dispatched tag before it reaches the shell and satisfy lint

* Sort the spec imports

* Clean up the registered peer even when the connect test fails

* Drive the dashboard's own remote-access flow instead of the client directly

* Mint the flow test's target peer by exposing a service

* Limit the wasm check to what the test environment can prove

* Give the wasm download and compile a realistic budget

* Hold the wasm client to the dashboard's initialization deadline
2026-08-26 14:18:55 +02:00
Maycon Santos d72f417ff3 Keep the prices discovery returns for a loaded model (#773)
Loading a provider's models merged anything the catalog did not already carry
by exact id, and hardcoded both rates to zero on the way in. The comment gave
the reason: the discovery response carried no prices. That stopped being true
when the endpoint began returning the same rates the proxy bills with.

Bedrock feels all of it. Its listing returns geography-prefixed ids —
eu.anthropic.claude-opus-4-7 — while the catalog holds the bare
anthropic.claude-opus-4-7, so no Bedrock model ever matches by string and every
one of them takes this branch. Selecting one filled its row with $0, and the
form then warned that the model had no cost set, while the API had reported a
rate for it. Usage against it records nothing.

Exact-id matching stays. Collapsing a geography-prefixed id onto its catalog
entry would hand back the bare form, and only the prefixed one is invocable at
AWS.
2026-08-24 10:54:42 +02:00
Maycon Santos 3be6dda6a7 Load a provider's models from the vendor in the provider modal (#767)
The model picker only offered NetBird's compiled-in catalog, which goes stale as
vendors retire models and cannot reflect what an account is actually entitled to.

Adds a "Load models from provider" action backed by
`POST /agent-network/catalog/providers/models` (netbirdio/netbird#7246). Editing a
saved provider reuses the stored credential, so the masked key never reaches the
browser; creating a new one sends the key being typed. Discovered models merge into
the catalog options, with catalog entries winning a collision because they carry
pricing. A model the backend reports as unpriced is flagged and needs confirmation
before saving, so nothing is registered at a silent zero.

Two implementation notes: the unpriced warning renders once at list level rather
than per row, and the hook uses `ignoreError: true` so a provider-specific failure
is reported inline in the modal instead of as a global toast.
2026-08-23 20:22:35 +02:00
Jack Carter 98ae943bbc feat: add required Country field to Agent Network signup form (#766)
The HubSpot form behind the self-hosted Agent Network signup now has a
required company-scoped Country/Region field, so submissions without it
are rejected. Adds a searchable Country dropdown to the signup step and
includes the value as an objectTypeId 0-2 field.

Uses a static country list instead of /locations/countries since the
geolocation database may be absent on self-hosted deployments and the
HubSpot property expects full country names, not ISO codes.
2026-08-19 10:37:13 +02:00
Misha Bragin 99f86b6421 fix(setup): match the v0.75 client UI in the management URL step (#765)
The desktop client reworked its UI in v0.75: the management server is now
  asked for on the first-run "Set up NetBird" screen and lives under
  Settings > General, so the "Settings > Advanced Settings" path the Windows
  and macOS tabs described no longer exists. Move the copy into a shared
  ManagementUrlStep, cover the already-installed case, and say "menu bar"
  instead of "system tray" on macOS.

  Fixes #732

* Remove unnecessary comment
2026-08-18 16:01:58 +02:00
Viktor Liu 0ceda40180 Show the SSH enable commands with the privileges the daemon now requires (#737) 2026-08-16 19:29:16 +02:00
Misha Bragin f4cf7458be fix(agent-network): show real provider name in access log (#758) 2026-08-16 16:44:16 +02:00
Misha Bragin 9e1c29b769 fix(version): ignore build metadata when detecting available updates (#759) 2026-08-14 14:29:42 +02:00
Eduard GertandClaude Opus 5 fb05f2c172 chore(docker): upgrade base image to Alpine 3.24 (#756)
Alpine 3.14 reached end of life in May 2023 and has received no security
fixes for roughly three years. nginx is installed from the Alpine repos
rather than pinned, so the base bump also moves nginx 1.20.2 -> 1.30.4,
certbot 1.16.0 -> 5.6.0 and supervisor 4.2.2 -> 4.3.0.

Alpine 3.24 carries security support until June 2028.

Verified by building and running this Dockerfile on 3.14, 3.23 and 3.24 and
comparing: the served index.html is byte-identical across all three, the CSP
response header hashes identically, `nginx -t` passes on the existing config,
and supervisord reaches the same state. /etc/crontabs/root and
/etc/nginx/http.d/ still exist on 3.24, so the `rm /etc/crontabs/root` step and
the config COPY paths are unaffected. Every certbot flag used by init_cert.sh
is still accepted by certbot 5.x.

Certificate issuance itself could not be exercised without a real domain, so
the certbot 1.x -> 5.x jump should get one staging issuance before rollout.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 11:12:03 +02:00
Eduard GertandClaude Opus 5 6cf828ce8a Upgrade Next.js to 16.3 (#755)
* chore: upgrade to Next.js 16.3

Turbopack is now the default for both `next dev` and `next build`, so the
separate `turbo` script is redundant and has been dropped.

`next build` rewrites tsconfig `moduleResolution` to "bundler", which Next 16
expects.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore: migrate ESLint to flat config

`next lint` was removed in Next 16, so `npm run lint` had been failing with
"Invalid project directory provided, no such directory: ./lint" since that
upgrade. eslint-config-next 16 also ships flat config only, which the old
.eslintrc.json could not consume.

Ignores .claude/ so stray worktree copies of the repo are not linted.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: replace ip-cidr with ip-address to clear CVE advisories

ip-cidr pulled a vulnerable ip-address (GHSA-mwp4-54f8-5fhr, high: leading-zero
octets decoded as decimal while resolvers read them as octal, enabling SSRF;
GHSA-v2v4-37r5-5v8g, moderate: XSS in Address6 HTML methods). npm reported "no
fix available" because even the latest ip-cidr pins ip-address ^9.0.5, still
inside the vulnerable range.

ip-address 10.x was already a direct dependency and covers every use: its
isValid rejects leading-zero octets and validates mask ranges natively, and
isInSubnet replaces the one `new cidr().contains()` call site.

The new validators were differential-tested against the real ip-cidr over a
67-input corpus (134 checks) before removal, with no divergences; containment
was checked across 14 v4/v6/boundary/cross-family cases. `npm audit` now
reports 0 vulnerabilities.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* refactor: replace next-themes with an in-repo ThemeProvider

next-themes renders an inline <script> to set the theme before paint. It was
loaded through dynamic(ssr: false), so that script rendered on the client, where
React 19 never executes script tags and warns:

  Encountered a script tag while rendering React component.

Upgrading would not have helped; 0.4.6 still renders the script
unconditionally. The script was also useless here: injected after hydration, it
could not prevent a flash, and postbuild.js hoists inline scripts out of the
export anyway.

The replacement keeps the same `netbird-theme` storage key and three-mode
support, and sets the theme class on <html> in the layout instead, so the
static export ships the correct theme in its first byte with no script at all.

Dropping the dynamic(ssr: false) wrapper removed an implicit prerender bailout
for the whole provider tree, which exposed isNetBirdCloud() reading
window.location during the build-time render; it is now guarded. The exported
HTML is byte-identical either way, since the page already bails to client
rendering.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: key the router children slot in the dashboard layout

React logged "Each child in a list should have a unique key prop. Check the
render method of DashboardPageContent." The App Router's segment element for
`children` arrives with a null key, which React flags once it is reconciled
alongside the Navigation sibling. Wrapping it in a keyed Fragment resolves it.

Verified in the browser both ways: with the Fragment the console is clean,
without it the warning returns.

Also applies the pending simple-import-sort fix for this file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: match IPv4-mapped IPv6 hosts against IPv4 CIDRs

Regression from replacing ip-cidr. Its contains() normalised an IPv4-mapped
IPv6 address to the embedded v4 before the subnet check, so
contains("::ffff:10.0.0.5") on 10.0.0.0/24 returned true; isHostInCIDR returned
false. Verified against ip-cidr@3.1.0 directly.

This is reachable: isValidIP accepts "::ffff:10.0.0.5", so a mapped host entered
in ReverseProxyAddressInput against an IPv4 resource CIDR was wrongly rejected.

An IPv6 network still rejects a mapped host, so families do not leak. Checked
across 20 cases (6 mapped-host, 14 pre-existing), with the mapped expectations
taken from measured ip-cidr output.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: derive skeleton colors from the resolved theme

SkeletonTheme had the dark palette hardcoded, so a light theme would render
dark skeletons. The light values are the ones already defined on the
feature-light-theme branch rather than new invented colors.

resolvedTheme is tracked in state and updated from the existing apply(), so the
palette also follows the OS preference under "system".

Latent today, since DarkModeToggle disables both Light and System.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 11:11:48 +02:00
Eduard GertandClaude Opus 5 83cb07a44c Fix icon alignment in collapsed navigation and network tab Add buttons (#748)
* fix: center icons in collapsed navigation rail

Nav items have 24px of left padding (px-3 on the li plus px-3 on the
button), so with 16px icons the icon center lands at 32px. The 70px rail
centered at 35px, leaving icons 3px off. Narrow the rail to 64px so the
two line up, and match the content offset in PageContainer.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix: use one plus icon across network tab Add buttons

The Resources and Routing Peers Add buttons used tabler's IconCirclePlus
while the Services tab used lucide's PlusCircle. Both were size 16, but
the two icon sets draw at different inner scales, so the glyphs read as
different sizes. Standardize on lucide.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-13 11:11:33 +02:00
Viktor Liu bbfa2d3a79 Bump default wasm client to v0.76.3 (#754) 2026-08-11 17:17:00 +02:00
Brad Ison a92816b9b8 Bootstrap Agent Network settings via the explicit POST (#746)
Adopts the new Agent Network settings contract:

- The management API replaced the implicit settings bootstrap — a
  `bootstrap_cluster` hint on the first provider create — with an
  explicit POST /agent-network/settings carrying the proxy cluster
  address. The settings response now carries endpoint +
  proxy_address + dedicated instead of cluster + subdomain.

- The provider wizard keeps its UX (auto-picking a validated cluster,
  gating the first create on having one) but now bootstraps the
  settings row with the explicit POST right before the first provider
  create. A bootstrap failure keeps the wizard open and surfaces the
  error — previously a failed bootstrap was swallowed server-side and
  the account silently ended up with a provider but no endpoint. A 409
  (concurrent bootstrap won) is treated as success, since the row
  exists either way.

- Types updated accordingly; bootstrapAgentNetworkSettings is exposed
  on the providers context. The onboarding provider step reuses the
  same wizard, so it is covered by the same change.

Without this, the dashboard against a new management silently stops
bootstrapping accounts: provider create succeeds (the old hint is an
ignored unknown field), but no endpoint is ever assigned.
2026-08-11 11:32:27 +02:00
Bethuel Mmbaga 0caf754897 Update Entra ID SCIM setup wizard for the new Azure portal UI (#750) 2026-08-10 11:12:40 +02:00
Misha Bragin 49582fff74 Trim numeric build suffix from sidebar version display. (#749)
Self-hosted management versions can carry a numeric build suffix
  (e.g. 0.76.3-31256681241) that overflows the sidebar box. Show the
  semver only and move the full string into the hover tooltip alongside
  the latest version. Pre-release labels like -rc.1 are preserved.
2026-08-09 19:20:49 +02:00
Jack Carter af7745a03b chore: remove solutions engineer booking modal from onboarding (#745)
The onboarding survey opened a HubSpot booking modal for accounts with
50+ planned users. Removes the modal, its onboarding-call experiment
variants, the trigger in Onboarding.tsx, and the now-unused avatar.

Keeps src/cloud/cloud-hooks/useExperiment.ts, which this was the only
consumer of, for a future experiment.
2026-08-06 10:48:31 +02:00
Maycon Santos 19d82cc2ab Handle agent-network settings defaults response (#739)
* Handle agent-network settings defaults response

The management API now answers the settings GET with a defaults object
before the account is bootstrapped (empty cluster/subdomain/endpoint)
instead of a JSON null body. Normalize that shape to the existing null
settings signal so every consumer keeps a single not-bootstrapped check,
while still tolerating the null body and 404 from older backends.

* Pin the settings wire-shape normalization in a Playwright spec

The providers page must render the connect-first empty state for every
unbootstrapped settings shape the management API has answered with over
time (defaults object with an empty endpoint, JSON null body, 404) and
the endpoint badge once bootstrapped. The settings route is mocked per
test so the spec is independent of the backend build and of account
state left behind by other suites.

* Address review: no stale settings on 404, close context on setup failure

useFetchApi keeps the previous SWR data alongside the error, so a later
404 could expose stale settings through the memo; it now yields null
when the error is a 404 while still riding out transient failures on
the kept data. The wire-shapes spec closes its browser context when a
setup step throws before the close callback is handed to the test.
2026-08-04 01:59:34 +02:00
Misha Bragin b66cc4ccb7 Cache pricing + custom model entry (#736) 2026-08-01 15:56:24 +02:00
Zoltan Papp 1d408f2e7e Add openSUSE zypper instructions with GPG key import to the Linux tab (#735)
Zypper ignores the gpgkey= entry of a .repo file, so the yum snippet would
leave the repository key uninstalled on openSUSE. The new distro entry adds
the repository with GPG checking enabled and imports the key on refresh.
2026-07-28 17:50:09 +02:00
Maycon Santos 9fab9d7a83 Cover RPM-based distros and desktop app dependencies in the Linux tab (#733)
* Cover RPM-based distros and desktop app dependencies in the Linux tab

The manual-install section of the add-peer modal only covered Ubuntu via
APT. Replace the single Ubuntu accordion with a distro selector covering
Debian/Ubuntu (APT), Fedora (DNF), RHEL/AlmaLinux/Rocky (DNF), and Amazon
Linux (YUM).

The desktop app links GTK 4.10+ and WebKitGTK 6.0 but the released
netbird-ui packages do not declare them, so each desktop-app install line
names the packages explicitly and a callout states the minimum release
(Ubuntu 24.04, Debian 13, Fedora 43, RHEL 10 with EPEL). Distros without
the required stack get CLI-only instructions.

* Add e2e coverage for the Linux distro selector
2026-07-28 03:03:46 +09:00
Maycon Santosandbraginini 6ca91287fc Show agent-network prompt-cache tokens and cost on hover (#731)
Co-authored-by: braginini <bangvalo@gmail.com>
2026-07-26 21:43:33 +02:00
Misha Bragin 5a5e04ad35 Add 0.75 announcement (#727) 2026-07-23 21:05:52 +02:00
Maycon Santos 598ecb196d remove rc releases logic for agent networks (#725) 2026-07-23 20:29:09 +02:00
Maycon Santosandbraginini 7c8352822d Add Kimi (Moonshot AI) provider support to Agent Network UI (#724)
* Add Kimi (Moonshot AI) provider support to Agent Network UI

Pairs with the management catalog's new kimi_api entry:

- kimi_api in the AIProviderId union and the access-log provider-id
  normalization list.
- Provider modal: api.moonshot.ai host placeholder, upstream-URL help
  explaining the OpenAI-shape (/v1) vs Anthropic-shape (/anthropic)
  path choice, sk-... key placeholder.
- Configure Your Agent: Kimi backend option in the Claude Code tab
  (JSON + shell modes), a Kimi variant in the Codex tab using
  wire_api="chat", and a new Kimi CLI tab with the ~/.kimi/config.toml
  provider setup. All Kimi surfaces render only when a kimi_api
  provider is connected, and pre-select themselves when Kimi is the
  only provider of its shape.
- Onboarding defaults to the Claude Code tab for Kimi providers.

* Add Kimi provider e2e spec

Walks the Kimi lifecycle against the real backend: select kimi_api from
the catalog (prefilled host, catalog model pricing), connect it, then
assert the Kimi-gated Configure Your Agent surfaces (Kimi CLI tab,
Claude Code kimi-k3 config, Codex wire_api=chat variant) and clean up
via new agent-network API helpers. Skips when the management build
under test has no kimi_api catalog entry, so it can merge ahead of the
backend and activate once the management-cloud image carries it.

* Remove Codex option for Kimi and align Claude Code config with Moonshot guide

Codex has no clear support against Moonshot's upstream, so the Kimi
variant is dropped from the Codex tab. The Claude Code Kimi snippet now
pins all default model tiers and the subagent model to kimi-k3 and
disables tool search, per Moonshot's Claude Code guide.

* Carry the /anthropic shape prefix in the agent base URL for Kimi

The Kimi provider keeps its default https://api.moonshot.ai upstream;
Claude Code and Kimi CLI select the Anthropic shape by appending
/anthropic to the endpoint base URL, which rides through to Moonshot.
One provider now serves both API shapes.

* Kimi CLI uses the bare endpoint base URL

Only Claude Code carries the /anthropic prefix; Kimi CLI's anthropic
provider type points at the bare endpoint.

* Kimi tooltip

* Make the Claude Code provider dropdown width flexible and tidy Kimi notes

* Make the Claude Code provider dropdown width flexible and tidy Kimi notes

* Restored the note on the Kimi CLI tab

---------

Co-authored-by: braginini <bangvalo@gmail.com>
2026-07-23 19:24:41 +02:00
Maycon Santos e4f5046ff2 [agent-network] per-provider disable-metadata toggle (#715) 2026-07-20 14:46:17 +02:00
Eduard GertandClaude Opus 4.8 d5f61efc1a Fix network resource policy creation with single peer source (#721)
When creating a network resource and adding a policy with a single peer
(sourceResource) as the source, the payload included both an empty
`sources: []` array and `sourceResource`, causing the backend to reject
it with "specify either sources or source resources not both".

`createPolicyForResource` always computed `sources` as an array while the
`...rule` spread carried `sourceResource` through. This mirrors the
existing destinations handling: gate `sources` on `rule.sourceResource`
so it becomes undefined (and is dropped during serialization) when a
source resource is set.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-20 11:16:36 +02:00
Maycon Santos 3503141c56 Adjust license cache ttl to 10 minutes (#718) 2026-07-20 11:03:14 +02:00
Misha Bragin a27e4654cf Fix IdP cards spacing (#717) 2026-07-16 14:41:59 +02:00
Misha Bragin b9569f48d5 Add clusters view to agent network (#714) 2026-07-15 23:22:05 +02:00
Viktor Liu 9694bb753d Show RDP for Linux and FreeBSD peers and harden the browser RDP client (#710) 2026-07-15 12:29:54 +02:00
Maycon Santos 377d239a01 Make Agent Network menu available via dashboard_features (#709)
* 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.

* Add e2e coverage for dashboard_features.agent_network menu availability

* 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.

* 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.

* Match badged nav labels by substring in dashboard_features e2e

* Treat agent network deployment config as a floor over account setting

* Add agent network env x signup-source e2e matrix with a config override
2026-07-13 17:17:36 +02:00
Maycon Santos a266fff89d Focus dashboard on Agent Network for netbird.ai signups (#708)
Accounts that sign up through netbird.ai are focused on the Agent Network
  surface and taken through the Agent Network onboarding. Reversible by the user
  and a no-op for every other account.
  
  - Capture ?signup_source=netbird.ai pre-auth (survives the OIDC redirect); once
    authenticated, mark new accounts with settings.agent_network_only, awaiting
    /accounts revalidation before clearing the source so the view never flips.
  - useAgentNetworkMode() resolves the mode with nullish semantics: an explicit
    account setting (true or false) wins over deployment config; when absent, a
    pending netbird.ai signup or NETBIRD_AGENT_NETWORK_ONLY/_ENABLED decides. 
    Purely additive — unchanged behavior when the setting is absent. 
  - Navigation, the agent-network route guard, control-center, and fetch gates
    read the resolver instead of the config flags.
  - Onboarding: the form is chosen from signup_source synchronously, so netbird.ai
    arrivals see the Agent Network signup form (now shown on cloud) and never the
    regular form, even under a slow backend.
  - Client Settings gains an "Agent Network focused view" opt-out toggle.
  - E2E coverage for focused-view menu/routes and onboarding form selection.
2026-07-12 21:16:05 +02:00
Maycon Santos 8ed69f21c5 [reverse proxy] Cluster one click deploy (#707) 2026-07-12 15:05:02 +02:00
Eduard Gert 58d1035b16 Remove experimental flag from lazy connections (#706) 2026-07-10 14:29:18 +02:00
Pascal Fischer 991234785c new traffic filters (#704) 2026-07-09 14:26:40 +02:00
Eduard Gert f2ad889fdc Revert using tenants own currency and instead use MSP currency (#703) 2026-07-08 10:47:17 +02:00
Jack CarterandEduard Gert 1f219c4ffc Default billing to EUR, preserve USD for active subscribers (#702)
* feat: default billing to EUR only, preserve USD for active subscribers

Removes the USD/EUR currency selector from Plans & Billing. New
subscriptions, MSP tenants, and distributor customers now default to
EUR; any existing active USD subscription keeps its real currency so
its price stays accurate.

Also fixes a latent bug where MSP/distributor per-tenant pricing was
computed off the admin's own account currency instead of each
tenant's, pins AWS Marketplace checkout to USD since it's a separate
billing rail, and removes an unused dead TotalCost stub from the MSP
Tenants table.

* move currency helper into provider

* force usd for aws

* fix flaky setup key test

---------

Co-authored-by: Eduard Gert <kontakt@eduardgert.de>
2026-07-07 16:45:31 +02:00
Viktor Liu 87b3a08adc Surface readable RDP errors, keep client connected on retry, bump wasm to v0.74.2 (#701) 2026-07-07 14:11:59 +02:00
Maycon SantosandClaude Opus 4.8 cfd0037cef Show management API request id in error notifications (#698)
* Show management API request id in error notifications

Newer management servers return an X-Request-Id header on every response,
including errors. Surface it in the error toast so it can be quoted in
support tickets.

- api.tsx: read X-Request-Id from the response and attach it to the
  rejected ErrorResponse (both the JSON-body and status-text paths).
- Notification.tsx: render a copyable "Request ID" line whenever an error
  carries one, covering the promise-based mutation notifications.
- ErrorBoundary.tsx: thread the request id through for GET/fetch errors.

The line is only shown when the header is present, so older servers that
do not send it are unaffected. Adds an e2e spec covering both cases.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016B2yLBeDK3kvX2FvhqcWk8

* docs: add request-id error toast screenshots for PR

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016B2yLBeDK3kvX2FvhqcWk8

* test: expose X-Request-Id via CORS in e2e mock

The dashboard reads the request id from a cross-origin response, so the
browser only surfaces the header to JS when the response lists it in
Access-Control-Expose-Headers. Mirror a CORS-enabled management server in
the mock so the test exercises the dashboard logic rather than the header
being filtered out cross-origin.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016B2yLBeDK3kvX2FvhqcWk8

* test: assert error toast via data-testid instead of text

Add a notification-title testid to the toast and target it with
getByTestId in the request-id spec, following the repo convention of
selecting by data-testid rather than brittle visible text.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 14:02:16 +02:00
Pascal Fischer ac7d578bd7 aggregated traffic events (#693) 2026-07-07 11:36:25 +02:00
Misha Bragin 8d41de995c Fix Skip TLS doc link and model pricing format (#699) 2026-07-03 07:03:42 +02:00
Misha Bragin 30dd75d95d Feature/add skip tls verification (#697) 2026-07-01 22:25:05 +02:00
Zoltan Papp bfa064a19d Add Metrics settings tab with metrics push toggle (#613)
* Add Metrics settings tab with metrics push toggle

Add a dedicated Metrics tab to Settings with a toggle for enabling
anonymous performance metrics collection. Moves metrics out of the
Clients tab into its own tab with description and docs link.

* Update docs link
2026-07-01 19:00:00 +02:00
Misha Bragin db73d18370 Feature/add provider session (#695) 2026-07-01 16:32:44 +02:00
Misha Bragin ba32178425 Fix peer table page reset when switching kind (#696) 2026-07-01 16:09:15 +02:00
4ea4fd4a4c Reload nginx after patching CSP header (#687)
* fix(docker): reload nginx after patching CSP header

nginx is started by supervisord (priority 100) before init_react_envs.sh
(priority 201) and is never reloaded afterwards, so it keeps serving the
static default.conf CSP that has no connect-src. Over plain HTTP this blocks
OIDC discovery and the management API, breaking browser SSH/RDP auth.

Reload nginx at the end of the init script so the dynamically built CSP
header takes effect.

* Update docker/init_react_envs.sh

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* fix(docker): log nginx -t output on failed CSP reload

When the reload never succeeds, run nginx -t before exiting so the logs show
whether the patched config is invalid or nginx never came up.

* conditionally emit `upgrade-insecure-requests` in CSP

---------

Co-authored-by: Viktor Liu <17948409+lixmal@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: mlsmaycon <mlsmaycon@gmail.com>
2026-07-01 15:32:22 +02:00
Bethuel Mmbaga deaae9159c Show reason for peer login expiration events (#694) 2026-07-01 12:32:11 +03:00
Bethuel Mmbaga d7fd24244d Prevent gated useFetchApi hooks from overwriting shared cache entries (#692) 2026-06-30 17:44:38 +03:00
Misha Bragin 9230b3e4e3 Add Claude provider selector and Bedrock (#691) 2026-06-30 11:50:15 +02:00
Brandon Hopkins a8275e54d5 Audit timeline contrast fixes plus nameserver gradient inversion 2026-06-29 21:28:23 -07:00
Brandon Hopkins f77d0dc48e Theme provider review fixes: leak guard, token colors, dev misuse error 2026-06-29 21:28:23 -07:00
Brandon Hopkins 67cd3ddf08 Fix High-availability badge hover and Beta badge 2026-06-29 21:28:23 -07:00
Brandon Hopkins d90fe688f5 clean up badge colors 2026-06-29 21:28:23 -07:00
Brandon Hopkins 6dd3dfa153 Modal footer tint, darker modal/input borders in light mode 2026-06-29 21:28:23 -07:00
Brandon Hopkins 68128dffb7 minor fixes and color cleanup 2026-06-29 21:28:23 -07:00
Brandon Hopkins daff0659d4 theme switcher in profile dropdown 2026-06-29 21:28:05 -07:00
Brandon Hopkins 019320a481 StatusPicker sync 2026-06-29 21:26:44 -07:00
Brandon Hopkins d1a2d407b3 next-themes is removed and replaced with a self-owned provider 2026-06-29 21:26:28 -07:00
TechHutTV 41043bb10c Fix light mode announcement banner 2026-06-29 21:26:28 -07:00
TechHutTV d4f6adb807 Fixed invisible light-mode filter text on peers page 2026-06-29 21:26:28 -07:00
Brandon Hopkins ccf417e103 light mode in AnnouncementBanner.tsx 2026-06-29 21:26:28 -07:00
TechHutTV 6a6a3b86c4 Light-mode color fixes plus CodeRabbit-audited nb-gray token migration 2026-06-29 21:26:28 -07:00
TechHutTV 9849e0a70c fixing color for shared UI components 2026-06-29 21:22:16 -07:00
TechHutTV 9249464d5c light mode for control center 2026-06-29 21:22:16 -07:00
TechHutTV 06daecf89a fix callout colors 2026-06-29 21:22:16 -07:00
TechHutTV d58d50c4ec fix light sub-text and directional arrows 2026-06-29 21:22:16 -07:00
TechHutTV 97007db62e light mode draft with switcher 2026-06-29 21:22:15 -07:00
Misha Bragin 19ad8d6806 Prefill Vertex endpoint (#689) 2026-06-29 10:53:07 +02:00
Misha Bragin 38fc2970e6 Support Vertex keyfile upload and agent config (#688) 2026-06-28 22:34:54 +02:00
Misha Bragin 4ec4d29e84 The empty-state peer card now renders AddPeerDropdown 2026-06-28 12:12:11 +02:00
Misha Bragin 944d1b5bf6 Agent network (#684) 2026-06-27 19:11:41 +02:00
Max Bruch c325c170ab Add origin URL of AUTH_AUTHORITY to CSP_DOMAINS #679 (#680) 2026-06-26 13:11:52 +02:00
Bethuel Mmbaga 76529fc089 Preserve inactivity expiration on partial peer updates (#676) 2026-06-23 17:30:06 +03:00
Maycon Santos 8c22e64d02 Restrict cloud/licensed-only API calls in open-source mode (#675)
Gate premium endpoints so the open-source dashboard stops calling
endpoints the open-source management server does not serve:

- MSP/Distributor: only fetch /integrations/msp, /integrations/msp/switcher
  and /integrations/msp/reseller on NetBird Cloud
- EDR bypass: only fetch /peers/edr/bypassed when licensed
- Event Streaming: gate /integrations/event-streaming GETs on the license
  in addition to the existing permission check
- useIsLicensed: cache the licensed-only probe result (per API origin, 1h
  TTL) so it no longer re-fires on every page load
- Team > Users: show the Identity Provider Sync tile in open-source mode so
  IdP sync remains discoverable
2026-06-23 15:59:15 +02:00
Maycon Santos 7653e3411c Merge NetBird cloud edition into the dashboard (#674)
Brings the unified dashboard into the open-source repo. Premium features
ship in the open code, gated at runtime via NETBIRD_CLOUD and
NETBIRD_LICENSED, with upgrade prompts for unlicensed self-hosted
deployments. Adds the cloud-only feature areas (billing, integrations,
MSP, traffic events, notifications) and the Playwright e2e suite.
2026-06-21 16:01:08 +02:00
Maycon Santos 86b6e23981 Update announcements.json (#673) 2026-06-20 16:43:04 +02:00
Eduard Gert 389a0c0785 update banner (#672) 2026-06-20 15:59:23 +02:00
Misha Bragin bf6601782d DNS Zones & Setup modal improvements (#669) 2026-06-15 16:02:33 +02:00
Max 5eac5162e0 Insert link to remote jobs documentation (#664)
Replaced general link to docs homepage with exact Documentation link for Remote Jobs
2026-06-12 09:39:36 +02:00
Brandon Hopkins 1d8d32dd12 Edit banner to IPv6 kh link (#662) 2026-06-05 12:54:59 -07:00
Brandon Hopkins 578d890bb5 Add Logout URL to the Identity Provider dialog (#657)
* Add Logout URL section

* Condensed the URL section, added logout note
2026-06-05 08:28:32 -07:00
Maycon Santos e8f0f20455 Update reverse proxy modals (#661)
* add multi-environment deployment options for reverse proxy setup

* refactor reverse proxy modal to handle state updates more robustly
2026-06-04 20:43:45 +02:00
Misha Bragin 8c94090e3d Group Networks and Routes under Network Routing (#660) 2026-06-04 19:44:13 +02:00
Misha Bragin 1917df6f60 Improve Table Filters Layout (#654) 2026-06-04 18:24:53 +02:00
Misha Bragin 358b477ded Move setup keys to Settings (#653) 2026-05-27 17:17:04 +02:00
Maycon Santosandcoderabbitai[bot] f535fe2667 Feature/private service expose (#646)
* feat: private service expose

Dashboard surface for the netbird private-service feature: the
reverse-proxy modal gains a Private toggle and the target modal gains
a Direct-upstream option with custom upstream host, both feeding the
backend's Service.private + target.direct_upstream / target.host
fields. The Proxy Events page wraps its table in PeersProvider so
peer-name fallback resolution works for tunnel-peer callers.

Reverse-proxy modal changes (ReverseProxyModal.tsx):
- Private toggle that pivots the modal between standalone and cluster
  target types and auto-injects the cluster target.

Reverse-proxy target modal changes:
- Direct upstream toggle (target dials via the host stack instead of
  the embedded NetBird client).
- Custom upstream host input revealed when Direct upstream is on.
- New ReverseProxyClusterTargetSelector + ReverseProxyAddressInput.
- useReverseProxyTargetOptions updated for the new option shape.

Proxy Events table:
- Reuses across the reverse-proxy events surface; UserCell falls back
  to the peer name when no user is attached to the call.

* feat(reverse-proxy): move NetBird-only access to auth tab + access groups

Restructures the Private (NetBird-only) flow so the auth model is
clearer:

- Removes the 'Private (NetBird-only)' toggle from the Service main
  page. Service mode is the only primary choice now.
- Adds a 'NetBird-only access' toggle on the Authentication tab, gated
  on serviceMode=HTTP and selectedDomain.supports_private===true. When
  the cluster doesn't support it, the toggle is disabled with an
  inline note explaining why.
- Adds an Access Groups picker (PeerGroupSelector) inline on the auth
  tab when NetBird-only is on. Replaces the previous bearer-auth
  distribution_groups overload — these groups go on the new
  service.access_groups field on the wire.
- When NetBird-only is enabled, all other auth modes (SSO, password,
  PIN, headers, link) are hidden — the inbound peer's WireGuard
  identity is the only auth path.
- Adds a 'Direct upstream' toggle to the Advanced settings tab,
  gated on isPrivate + cluster supports private. The toggle is
  service-level in the UI; on save it patches the (single) cluster
  target's options.direct_upstream.
- togglePrivate now also clears bearer/password/pin/header/link
  state when entering private mode (strict mutual exclusivity).

* fix(reverse-proxy): tighten NetBird-only flow per review

Five fixes from the first cut:

1. Auth-tab design mismatch. The NetBird-only block was an inline
   FancyToggleSwitch + inline PeerGroupSelector. It now follows the
   same SettingCard.Item pattern as SSO/Password/PIN/Headers: a
   clickable row showing enabled state that opens a dedicated modal
   (AuthNetBirdOnlyModal) where the access groups are picked.

2. Trimmed wordy NetBird-only description down to one line.

3. Trimmed wordy Direct upstream help text.

4. Removed the per-target Direct upstream toggle from the target
   modal. Direct upstream now lives only at the service level (under
   Advanced settings) for private services. Cluster targets still
   imply direct_upstream via the existing sanitizeTargets path, so
   the wire stays correct.

5. togglePrivate no longer drops the user's existing targets when
   entering private mode. The previous behavior silently dropped any
   non-cluster targets the operator had configured, leaving the
   targets list empty and the Save button disabled. Now: targets
   stay put, canSaveService gates on 'all targets are cluster type'
   so the Save button accurately reflects what the backend will
   accept, and an inline warning explains what to fix when the
   constraint isn't met.

The AuthNetBirdOnlyModal requires at least one access group before
Enable becomes clickable, mirroring the SSO modal's pattern.

* fix(reverse-proxy): allow any target type on private services

The cluster-target restriction was a holdover from the previous
auth-by-bearer-groups model where only cluster targets exposed a
proxy peer that could host the ACL. With the new access_groups path
the ACL is server-side and works regardless of how the proxy reaches
the upstream.

- Drop the inline 'Private services only support cluster targets'
  warning.
- canSaveService no longer requires all targets to be cluster type.
- Service-level Direct upstream now applies to every target (cluster,
  peer, host, domain) when private, so the operator can mix target
  types and still control the dial path globally.
- Tightened NetBird-only description to '...connected peers in the
  selected NetBird groups.' (per review).
- Tightened Direct upstream help to '...reachable without a Wireguard
  connection.' (per review).

A future iteration may add a dedicated cluster-only mode with its own
guided flow; for now the operator picks whatever target types suit
their topology.

* fix(reverse-proxy): count NetBird-only access as protection

The 'No Protection Configured' popup fired even on private services
because isUnprotected only checked the password/PIN/bearer/header/link
auth modes. NetBird-only access is also a form of protection (tunnel
identity + access groups), so include isPrivate in the gate.

* fix(reverse-proxy): show NetBird-only auth and resolve access group names

Two display bugs for private services:

- ReverseProxyModal seeded accessGroups with {id}-only stubs, which
  made useGroupHelper skip its ID→Group resolution and render empty
  pills in the NetBird-only modal when editing an existing service.
  Pass the raw string[] so useGroupHelper resolves full Group objects
  with names against the GroupsProvider.

- ReverseProxyAuthCell only inspected password/pin/bearer/headers
  auth flags, so a NetBird-only service displayed "No Auth" in the
  services table. Add a NetBird-only entry that counts toward the
  auth badge, renders as "NetBird Only" with the CircleUser icon
  when it's the single auth, and lists the access groups (name +
  user count) in the hover.

* feat(reverse-proxy): unify proxy-cluster target into the peer/resource selector

Operators picking a target for a service now see Proxy Clusters as a
third tab alongside Peers and Resources, gated on at least one cluster
advertising supports_private. When the service already has a proxy
cluster the tab lists only that cluster; when it doesn't, picking a
cluster commits it as the service's domain. The dedicated private-mode
cluster picker is removed in favour of this unified flow.

Selecting a cluster target now also auto-flips the service to
NetBird-only, since cluster targets are only reachable over the
WireGuard overlay and SSO/password/PIN advertise an auth path no
public client could exercise. The Access Control tab carries a note
explaining that an allow rule for the account's NetBird network range
is applied automatically alongside any operator-configured rules.

PeerGroupSelector gains opt-in showClusters/clusters/selectedCluster/
onClusterChange props plus a Proxy Clusters tab in its TabsList; the
tabOrder union widens to include "clusters". All call sites that don't
pass showClusters render unchanged.

* fix(reverse-proxy): polish private-service copy and lock Direct Upstream for cluster targets

Title-case NetBird-Only Access and Direct Upstream so the labels read
consistently across the auth tab, the dedicated modal, and the
services-table auth cell.

Cluster targets are reached via the embedded proxy's host network
stack and have no WireGuard endpoint to fall back to, so toggling
Direct Upstream off would silently break them. When any target is a
cluster the toggle is forced on, the FancyToggleSwitch is disabled,
and the helper text explains why. The save path writes the locked-on
value to every target's options.direct_upstream so the persisted
state matches what the UI shows.

* fix(reverse-proxy): support dynamic placeholder and restrict input to IPv4 or IPv6 when required

* fix(reverse-proxy): rename "Agent" to "Peer" in events user cell tooltip

* feat(reverse-proxy): gate NetBird-Only + Direct Upstream on cluster capability

When the selected cluster doesn't have at least one connected embedded
proxy (`netbird proxy`), the NetBird-Only Access and Direct Upstream
controls are visually disabled with a tooltip explaining why, instead of
silently no-op (NetBird-Only) or hidden (Direct Upstream).

- ReverseProxyCluster interface gains the optional `private` flag that
  the management API now exposes on /api/reverse-proxies/clusters.
- ClustersFeaturesCell renders a new "Private" badge for clusters whose
  `private === true`, matching the other capability badges.
- SettingCard.Item gains a `disabled` prop: opacity-50 + cursor-not-allowed,
  click/keyboard guarded, Add/Edit button greyed out. No visual change
  for existing usages (default false).
- ReverseProxyModal:
  - NetBird-Only Access card is now wrapped in a FullTooltip that
    activates only when the cluster lacks support; the SettingCard.Item
    is rendered with `disabled` in that case. Description text stops
    switching based on cluster state — the tooltip explains the gate.
  - Direct Upstream toggle is no longer hidden when supports_private is
    false; it stays visible and disabled (alongside the existing
    hasClusterTarget locked-on case), wrapped in a tooltip explaining
    the cluster requirement. Existing private services whose cluster
    lost the capability now show the control disabled rather than
    vanishing.

* disable button when no group were selected

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

* Feature/private service expose update (#651)

* feat(reverse-proxy): refine UI state and copy for NetBird-Only and Direct Upstream controls

- Default `Direct Upstream` to `false` for peer/resource targets; enhance tooltip descriptions.
- Improve conditional rendering of `NetBird-Only Access` setting with appropriate tooltips for unsupported clusters.
- Add cluster badge interaction in `PeerGroupSelector` and dynamic placeholder handling.
- Simplify `ReverseProxyTargetSelector` messaging for proxy forwarding options.
- Enhance `ClustersFeaturesCell` with richer description for the "Private" badge.

* refactor(reverse-proxy): improve tooltip and modal text for clarity

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-05-26 21:37:11 +02:00
Misha Bragin 52f7020a0f Fix broken SSH and RDP (#650) 2026-05-26 20:14:36 +02:00
Misha Bragin a604643f9a Feature/simplified layout (#649)
Splits the Peers sidebar entry into User Devices (/peers/users) and Servers (/peers/servers), with /peers redirecting to User Devices and a shared kind filter splitting peers
by whether the owner is a real user vs a service/no-user. The Servers page description and an inline link replace what was the old "Setup Keys" sidebar item under Peers.
2026-05-25 13:09:30 +02:00
Maycon SantosandEduard Gert 42cd088c5d rebuild self-hosted page as Clusters with type features (#641)
* feat(reverse-proxy): rebuild self-hosted page as Clusters with type + features

The Self-Hosted Proxies page was account-only by design but the
underlying API already returned every cluster the account could see.
Lifting that filter and renaming the page surfaces shared clusters
too — operators can see what NetBird-deployed clusters are reachable
alongside their own self-hosted ones, with online status and feature
support visible per row.

ReverseProxyCluster matches the new backend shape: `type`
(account/shared), `online`, and the three capability flags. The
`isSelfHostedCluster` provider hook now compares against `type ===
account` instead of a deprecated boolean.

Page folder renamed self-hosted-proxies → clusters (history-preserving
git mv). Table columns: Cluster (with an EphemeralPeerIndicator-style
icon next to the name marking account vs shared and a colored dot for
online status), Connected Proxies (plain numeric), Features (one
tooltip-backed badge per supported capability), Actions (Delete only
on account-owned rows; shared clusters render an empty action cell).

Empty state shows when the list is fully empty with a doc link in the
page header. Sidebar entry restored under Reverse Proxy.

* Update record in modal, update doc link, update modal title

* update reverse proxy documentation links to latest anchors

* update cluster modal description to "proxy cluster" instead of "self-hosted cluster"

---------

Co-authored-by: Eduard Gert <kontakt@eduardgert.de>
2026-05-20 11:48:38 +02:00
Maycon Santos 7400ac806e remove self-hosted proxies menu item (#640) 2026-05-14 17:50:07 +02:00
Viktor Liu 240ff5af9a Fix IPv6 input across reverse proxy, routes and resources (#638) 2026-05-14 16:43:02 +02:00
Eduard Gert dc86c30463 Add self-hosted proxies (#636)
* Add self-hosted proxies

* fix selfhosted badge for domain
2026-05-12 15:22:12 +02:00
Nicolas Frati e58f75ae3c Enable MFA for local users toggle (#615)
* implement enable mfa for local users toggle

* fix visibility check

* Added beta badge to MFA auth toggle
2026-05-08 16:51:17 +02:00
Viktor Liu dc1adebd27 Add IPv6 overlay settings and peer display (#594) 2026-05-07 15:20:12 +02:00
Bethuel Mmbaga d76cbd1122 Add Microsoft AD FS support for embedded Dex identity providers (#625) 2026-04-28 12:42:48 +03:00
Eduard Gert 01330e0f58 Fix missing peer context in group network routes tab (#620) 2026-04-23 17:05:05 +02:00
Viktor Liu e9ac1a1a23 Add CrowdSec IP reputation (#600) 2026-04-21 12:29:37 +02:00
raghvendra b53802a5c5 fix: prevent storage clear and logout on failed account deletion (#611) 2026-04-13 09:07:14 +02:00
Eduard Gert 9addc18956 Fix reverse proxy mode selection (#606)
* Fix reverse proxy mode selection

* Fix isNetBirdHosted

* Fix activity description
2026-04-09 09:52:35 +02:00
970 changed files with 119583 additions and 8859 deletions

No files matched your search

+12
View File
@@ -0,0 +1,12 @@
# The image only needs the pre-built static export (out/) and the docker/
# config files. Keep the build context small by dropping everything heavy and
# irrelevant — without this, `docker build .` ships node_modules and .next.
node_modules
.next
.git
.github
e2e
src
public
*.tar.gz
*.tar
-13
View File
@@ -1,13 +0,0 @@
{
"extends": ["next/core-web-vitals","prettier"],
"plugins": ["simple-import-sort"],
"rules": {
"simple-import-sort/imports": [
"warn",
{
"groups": [["^\\u0000", "^@?\\w", "^[^.]", "^\\."]]
}
],
"simple-import-sort/exports": "warn"
}
}
+7
View File
@@ -10,3 +10,10 @@ Select exactly one:
Paste the PR link from https://github.com/netbirdio/docs here:
https://github.com/netbirdio/docs/pull/__
## E2E tests
Optional: override the image tags used by the Playwright e2e workflow.
Defaults to `main` when omitted.
management-cloud-tag: main
reverse-proxy-tag: main
+78 -17
View File
@@ -7,17 +7,24 @@ on:
- "**"
pull_request:
# Cancel in-progress runs on the same ref (PR or branch) when a new commit
# arrives, so we don't waste CI building superseded commits.
concurrency:
group: build-and-push-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
IMAGE_NAME: netbirdio/dashboard
DOCKERHUB_IMAGE: netbirdio/dashboard
GHCR_IMAGE: ghcr.io/netbirdio/dashboard-cloud
jobs:
build_n_push:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v5
- name: setup-node
uses: actions/setup-node@v3
uses: actions/setup-node@v5
with:
node-version: '20'
cache: 'npm'
@@ -32,7 +39,7 @@ jobs:
with:
token: ${{ secrets.GITHUB_TOKEN }}
repository: netbirdio/IronRDP
latest: true
tag: v0.0.2
fileName: "*.ts"
out-file-path: 'public/ironrdp-pkg'
@@ -41,7 +48,7 @@ jobs:
with:
token: ${{ secrets.GITHUB_TOKEN }}
repository: netbirdio/IronRDP
latest: true
tag: v0.0.2
fileName: "*.js"
out-file-path: 'public/ironrdp-pkg'
@@ -50,7 +57,7 @@ jobs:
with:
token: ${{ secrets.GITHUB_TOKEN }}
repository: netbirdio/IronRDP
latest: true
tag: v0.0.2
fileName: "ironrdp_web_bg.wasm"
out-file-path: 'public/ironrdp-pkg'
@@ -69,29 +76,83 @@ jobs:
NEXT_PUBLIC_DASHBOARD_VERSION: ${{ steps.version.outputs.version }}
-
name: Set up QEMU
uses: docker/setup-qemu-action@v2
uses: docker/setup-qemu-action@v3
-
name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
uses: docker/setup-buildx-action@v3
-
name: Docker meta
id: meta
uses: docker/metadata-action@v4
uses: docker/metadata-action@v5
with:
images: ${{ env.IMAGE_NAME }}
-
name: Login to DockerHub
uses: docker/login-action@v2
images: |
${{ env.DOCKERHUB_IMAGE }}
${{ env.GHCR_IMAGE }}
flavor: |
latest=false
tags: |
type=schedule
type=ref,event=branch
type=ref,event=tag
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/') }}
type=ref,event=pr
type=sha
- name: Docker meta (rootless)
id: meta_rootless
uses: docker/metadata-action@v5
with:
images: |
${{ env.DOCKERHUB_IMAGE }}
${{ env.GHCR_IMAGE }}
flavor: |
latest=false
suffix=-rootless,onlatest=true
tags: |
type=schedule
type=ref,event=branch
type=ref,event=tag
type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/') }}
type=ref,event=pr
type=sha
- name: Log in to Docker Hub
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
uses: docker/login-action@v3
with:
username: ${{ secrets.NB_DOCKER_USER }}
password: ${{ secrets.NB_DOCKER_TOKEN }}
-
name: Docker build and push
uses: docker/build-push-action@v3
- name: Log in to the GitHub Container registry
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
file: docker/Dockerfile
push: true
push: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
platforms: linux/amd64,linux/arm64,linux/arm
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
- name: Build and push rootless Docker image
uses: docker/build-push-action@v5
with:
context: .
file: docker/Dockerfile.rootless
push: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }}
platforms: linux/amd64,linux/arm64,linux/arm
tags: ${{ steps.meta_rootless.outputs.tags }}
labels: ${{ steps.meta_rootless.outputs.labels }}
- if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository
run: |
echo '### Pushed tags' >> $GITHUB_STEP_SUMMARY
echo '${{ steps.meta.outputs.tags }}' >> $GITHUB_STEP_SUMMARY
echo '${{ steps.meta_rootless.outputs.tags }}' >> $GITHUB_STEP_SUMMARY
+81
View File
@@ -0,0 +1,81 @@
name: Bump netbird wasm client
on:
workflow_dispatch:
inputs:
tag:
description: 'NetBird release tag (e.g. v0.78.0)'
required: true
type: string
concurrency:
group: ${{ github.workflow }}-${{ inputs.tag }}
cancel-in-progress: false
jobs:
bump:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: main
token: ${{ secrets.NETBIRD_DEV_BOT_GITHUB_TOKEN }}
- name: Configure git
run: |
git config --global user.email "dev@netbird.io"
git config --global user.name "netbirddev"
# The tag rides in env and is validated before use, so a crafted dispatch
# input cannot expand into shell source or reach the sed expression.
- name: Update the default wasm path
id: bump
env:
TAG: ${{ inputs.tag }}
run: |
if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Refusing tag '$TAG': expected vMAJOR.MINOR.PATCH"
exit 1
fi
sed -i "s|https://pkgs.netbird.io/wasm/client/v[0-9.]*|https://pkgs.netbird.io/wasm/client/${TAG}|" src/utils/config.ts
branch="bump-netbird-wasm-${TAG#v}"
echo "branch=${branch}" >> "$GITHUB_OUTPUT"
# The branch name is derived from the tag, so a re-dispatch of the
# same release would collide with the branch and pull request an
# earlier run left behind.
if git ls-remote --exit-code --heads origin "${branch}" >/dev/null 2>&1; then
echo "skip=true" >> "$GITHUB_OUTPUT"
echo "Branch ${branch} already exists, ${TAG} was handled by an earlier run"
exit 0
fi
git checkout -b "${branch}"
git add src/utils/config.ts
if git diff --cached --quiet; then
echo "skip=true" >> "$GITHUB_OUTPUT"
echo "Default wasm path already at ${TAG}, nothing to do"
exit 0
fi
echo "skip=false" >> "$GITHUB_OUTPUT"
git commit -m "Bump the WASM client to ${TAG}"
git push --set-upstream origin "${branch}"
- name: Create pull request
if: steps.bump.outputs.skip != 'true'
env:
GH_TOKEN: ${{ secrets.NETBIRD_DEV_BOT_GITHUB_TOKEN }}
TAG: ${{ inputs.tag }}
BRANCH: ${{ steps.bump.outputs.branch }}
run: |
gh pr create \
--base main \
--head "$BRANCH" \
--title "Bump the WASM client to ${TAG}" \
--body "Automated wasm path bump triggered by netbird release ${TAG}."
+206
View File
@@ -0,0 +1,206 @@
name: Playwright E2E Tests
on:
push:
branches: [main]
pull_request:
# `edited` is included so that updating the PR description (e.g. to set
# `management-cloud-tag: <tag>` or `reverse-proxy-tag: <tag>`) re-triggers
# the e2e run with the new tag.
types: [opened, synchronize, reopened, edited]
workflow_dispatch:
inputs:
management-cloud-tag:
description: 'Management Cloud image tag'
required: true
type: string
default: 'main'
reverse-proxy-tag:
description: 'Reverse Proxy image tag'
required: true
type: string
default: 'main'
env:
REGISTRY: ghcr.io
jobs:
playwright-run:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
with:
ref: ${{ github.event.pull_request.head.sha || github.ref }}
fetch-depth: 0
- name: setup-node
uses: actions/setup-node@v5
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm install
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium
- name: Log in to the Container registry
uses: docker/login-action@v4
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.CI_DOCKER_PULL_GITHUB_TOKEN }}
- run: echo '{}' > .local-config.json
- name: Install jq
run: sudo apt-get install jq
- name: Resolve management-cloud image tag
id: management_tag
env:
INPUT_TAG: ${{ inputs.management-cloud-tag }}
PR_BODY: ${{ github.event.pull_request.body }}
run: |
# Use workflow_dispatch input if provided, otherwise parse PR body.
# Falls back to `main` when not specified.
if [ -n "$INPUT_TAG" ]; then
TAG="$INPUT_TAG"
else
TAG=$(printf '%s' "$PR_BODY" \
| grep -iE '^[[:space:]]*management-cloud-tag:[[:space:]]*[A-Za-z0-9._-]+[[:space:]]*$' \
| head -n1 \
| sed -E 's/^[[:space:]]*[Mm]anagement-cloud-tag:[[:space:]]*([A-Za-z0-9._-]+)[[:space:]]*$/\1/' \
|| true)
if [ -z "$TAG" ]; then
TAG="main"
fi
fi
echo "Using management-cloud tag: $TAG"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
- name: Resolve reverse-proxy image tag
id: reverse_proxy_tag
env:
INPUT_TAG: ${{ inputs.reverse-proxy-tag }}
PR_BODY: ${{ github.event.pull_request.body }}
run: |
if [ -n "$INPUT_TAG" ]; then
TAG="$INPUT_TAG"
else
TAG=$(printf '%s' "$PR_BODY" \
| grep -iE '^[[:space:]]*reverse-proxy-tag:[[:space:]]*[A-Za-z0-9._-]+[[:space:]]*$' \
| head -n1 \
| sed -E 's/^[[:space:]]*[Rr]everse-proxy-tag:[[:space:]]*([A-Za-z0-9._-]+)[[:space:]]*$/\1/' \
|| true)
if [ -z "$TAG" ]; then
TAG="main"
fi
fi
echo "Using reverse-proxy tag: $TAG"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
- name: Restore geolocation database cache
id: geo-cache
uses: actions/cache/restore@v4
with:
path: e2e/environment/geo-cache
key: geolite-dbs-v1
- name: Build dashboard and set up test environment
env:
MANAGEMENT_IMAGE_TAG: ${{ steps.management_tag.outputs.tag }}
REVERSE_PROXY_IMAGE_TAG: ${{ steps.reverse_proxy_tag.outputs.tag }}
# The docker environment (image pulls, Zitadel bootstrap) is
# network-bound and the Next build is CPU-bound, so they overlap.
# The build reads .test-config.json via the @/config/test alias,
# and create-test-env.sh renders that file only after the Zitadel
# bootstrap — so the build waits for it before starting.
run: |
(cd ./e2e/environment && bash create-test-env.sh) > env-setup.log 2>&1 &
ENV_PID=$!
until [ -s .test-config.json ] && jq empty .test-config.json 2>/dev/null; do
if ! kill -0 "$ENV_PID" 2>/dev/null; then break; fi
sleep 2
done
if ! [ -s .test-config.json ]; then
echo "::error::create-test-env.sh exited before rendering .test-config.json"
cat env-setup.log
exit 1
fi
# Lowest CPU priority: the containers must win the scheduler, or
# the build can starve management past the proxy-registration wait.
if ! nice -n 19 env APP_ENV=test npx next build; then
kill "$ENV_PID" 2>/dev/null || true
exit 1
fi
if ! wait "$ENV_PID"; then
echo "::error::Test environment setup failed"
cat env-setup.log
exit 1
fi
cat env-setup.log
# Saved right after env setup rather than in a post step, so a later
# test failure doesn't lose the cache.
- name: Save geolocation database cache
if: always() && steps.geo-cache.outputs.cache-hit != 'true' && hashFiles('e2e/environment/geo-cache/**') != ''
uses: actions/cache/save@v4
with:
path: e2e/environment/geo-cache
key: geolite-dbs-v1
- name: Run Playwright tests
id: playwright
run: |
set -o pipefail
npx playwright test --config=e2e/playwright.config.ts 2>&1 | tee playwright-output.log
- name: Append Playwright summary to job summary
if: always() && hashFiles('e2e/test-results/results.json') != ''
run: |
if [ -f e2e/test-results/results.json ]; then
passed=$(jq '.stats.expected // 0' e2e/test-results/results.json)
failed=$(jq '.stats.unexpected // 0' e2e/test-results/results.json)
skipped=$(jq '.stats.skipped // 0' e2e/test-results/results.json)
duration=$(jq '.stats.duration // 0' e2e/test-results/results.json)
{
echo '### Playwright results'
echo ''
echo "| Passed | Failed | Skipped | Duration |"
echo "|--------|--------|---------|----------|"
echo "| $passed | $failed | $skipped | ${duration}ms |"
} >> "$GITHUB_STEP_SUMMARY"
fi
- name: Collect container logs
if: failure()
run: |
cd e2e/environment
docker compose logs management --tail=500 --no-color > management.log 2>&1 || true
docker compose logs reverse-proxy --tail=500 --no-color > reverse-proxy.log 2>&1 || true
- uses: actions/upload-artifact@v7
if: ${{ !cancelled() }}
with:
name: playwright-report
path: e2e/playwright-report/
- uses: actions/upload-artifact@v7
if: ${{ failure() }}
with:
name: playwright-traces
path: e2e/test-results/
- uses: actions/upload-artifact@v7
if: ${{ failure() }}
with:
name: management-logs
path: e2e/environment/management.log
- uses: actions/upload-artifact@v7
if: ${{ failure() }}
with:
name: reverse-proxy-logs
path: e2e/environment/reverse-proxy.log
@@ -0,0 +1,34 @@
name: Sync deploy templates to S3
on:
push:
branches: [main]
paths:
- "templates/reverse-proxy/**"
permissions:
id-token: write
contents: read
jobs:
sync:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
persist-credentials: false
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ secrets.DEPLOY_TEMPLATES_ROLE_ARN }}
aws-region: eu-central-1
- name: Validate CloudFormation template
run: |
aws cloudformation validate-template \
--template-body file://templates/reverse-proxy/netbird-proxy-cfn.yaml
- name: Upload CloudFormation template
run: |
aws s3 cp templates/reverse-proxy/netbird-proxy-cfn.yaml \
s3://netbird-deploy-templates/templates/netbird-proxy-cfn.yaml
+78
View File
@@ -0,0 +1,78 @@
name: Test cloud deploy config
on:
pull_request:
paths:
- "docker/init_react_envs.sh"
- "templates/reverse-proxy/**"
- "src/modules/reverse-proxy/clusters/ClusterCloudDeploy.tsx"
- ".github/workflows/test-cloud-deploy.yml"
permissions:
contents: read
jobs:
csp-connect-src:
name: CSP allows cloud provider APIs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install nginx
run: |
sudo apt-get update
sudo apt-get install -y nginx gettext-base
- name: Prepare nginx layout expected by the init script
run: |
sudo mkdir -p /etc/nginx/http.d /usr/share/nginx/html
echo 'add_header Content-Security-Policy "placeholder" always;' | sudo tee /etc/nginx/http.d/default.conf
sudo touch /usr/share/nginx/html/OidcTrustedDomains.js.tmpl
sudo systemctl start nginx
- name: Run init script with production-like env
env:
AUTH_AUTHORITY: https://auth.example.com
AUTH_CLIENT_ID: test-client
AUTH_AUDIENCE: test-audience
AUTH_SUPPORTED_SCOPES: openid profile email
USE_AUTH0: "false"
NETBIRD_MGMT_API_ENDPOINT: https://api.example.com
run: sudo -E bash docker/init_react_envs.sh
- name: Assert provider APIs are in connect-src
run: |
CSP=$(grep -o 'Content-Security-Policy "[^"]*"' /etc/nginx/http.d/default.conf)
echo "Generated CSP: $CSP"
CONNECT_SRC=$(echo "$CSP" | sed 's/.*connect-src \([^;]*\);.*/\1/')
echo "connect-src: $CONNECT_SRC"
status=0
for origin in https://api.hetzner.cloud https://api.digitalocean.com; do
if echo "$CONNECT_SRC" | grep -qF "$origin"; then
echo "OK: $origin"
else
echo "MISSING in connect-src: $origin"
status=1
fi
done
exit $status
cfn-template-url:
name: CloudFormation template URL is live
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Extract template URL from component and check it
run: |
URL=$(grep -o 'https://[^"]*netbird-proxy-cfn\.yaml' src/modules/reverse-proxy/clusters/ClusterCloudDeploy.tsx)
echo "CFN_TEMPLATE_URL: $URL"
test -n "$URL"
# Non-blocking: the template is published by sync-deploy-templates on
# the default branch, so it may not exist yet for a PR that only edits
# the URL. Warn instead of failing the check.
if curl -sfI "$URL" >/dev/null; then
echo "OK: template URL is live"
else
echo "::warning::CloudFormation template URL is not reachable yet: $URL"
fi
+32
View File
@@ -0,0 +1,32 @@
name: Unit Tests
on:
push:
branches:
- main
pull_request:
# Cancel superseded runs on the same ref so a new push doesn't queue behind an
# outdated one (mirrors build_and_push.yml).
concurrency:
group: unit-tests-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
vitest:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- name: setup-node
uses: actions/setup-node@v5
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run unit tests
run: npm run test:unit
+14 -3
View File
@@ -35,10 +35,19 @@ yarn-error.log*
*.tsbuildinfo
next-env.d.ts
# local UI sandbox (throwaway preview harness — never commit)
/src/app/sandbox/
# config
.local-config.json
.local*config*.json
.test-config.json
cypress.env.json
e2e/playwright.env.json
e2e/fixtures/auth/*.json
e2e/test-results/
e2e/playwright-report/
/test-results/
/playwright-report/
playwright-report*.zip
.configs/.local-config.zitadel.json
.configs/.staging-config.json
.configs/.temp-config.json
@@ -47,4 +56,6 @@ cypress.env.json
/public/ironrdp-pkg/
/public/netbird.wasm
.idea
src/.local-config*
src/.local-config*
# local mock data (throwaway preview — never commit)
/src/utils/mockData.ts
+1 -1
View File
@@ -1,3 +1,3 @@
Mikhail Bragin (https://github.com/braginini)
Maycon Santos (https://github.com/mlsmaycon)
Wiretrustee UG (haftungsbeschränkt)
NetBird GmbH
+2 -2
View File
@@ -1,8 +1,8 @@
[
{
"tag": "New",
"text": "NetBird Reverse Proxy - Expose internal services to the public with automatic TLS and optional authentication.",
"link": "https://docs.netbird.io/manage/reverse-proxy",
"text": "The new NetBird desktop app is here - now available in the v0.75 release.",
"link": "https://netbird.io/knowledge-hub/netbird-v0-75-new-desktop-app",
"linkText": "Learn more",
"variant": "important",
"isExternal": true,
Executable
+89
View File
@@ -0,0 +1,89 @@
#!/usr/bin/env bash
#
# Build the netbird-dashboard Docker image for linux/amd64 and export it as a
# portable tarball that can be loaded and run on a remote server.
#
# The dashboard image is nginx serving a Next.js *static export*: the Dockerfile
# only COPYs the pre-built out/ directory, so we build out/ on the host first
# (the static files are arch-independent) and then assemble the amd64 image.
#
# Usage:
# ./build.sh # build + export netbird-dashboard.tar.gz
# IMAGE_TAG=v1 ./build.sh # override the image tag
# SKIP_BUILD=1 ./build.sh # reuse an existing out/ (skip npm build)
# ROOTLESS=1 ./build.sh # build the amd64-rootless image variant
#
set -euo pipefail
# Always run relative to this script's directory (the project root).
cd "$(dirname "$0")"
IMAGE_NAME="${IMAGE_NAME:-netbird-dashboard}"
IMAGE_TAG="${IMAGE_TAG:-amd64}"
DOCKERFILE="docker/Dockerfile"
CONTAINER_PORT=80
OUTPUT_SUFFIX=""
case "${ROOTLESS:-0}" in
0) ;;
1)
DOCKERFILE="docker/Dockerfile.rootless"
CONTAINER_PORT=8080
OUTPUT_SUFFIX="-rootless"
[[ "${IMAGE_TAG}" == *-rootless ]] || IMAGE_TAG="${IMAGE_TAG}-rootless"
;;
*)
echo "ERROR: ROOTLESS must be 0 or 1." >&2
exit 1
;;
esac
IMAGE="${IMAGE_NAME}:${IMAGE_TAG}"
OUTPUT="${OUTPUT:-${IMAGE_NAME}${OUTPUT_SUFFIX}.tar.gz}"
# Build the static export (out/) unless told to reuse an existing one. Next 16
# no longer runs ESLint during `next build`; types are checked by the build via
# tsc (verify separately with `npx tsc --noEmit`).
if [[ "${SKIP_BUILD:-0}" != "1" ]]; then
if [[ ! -d node_modules ]]; then
echo "==> Installing dependencies (npm ci)"
npm ci
fi
echo "==> Building static export (next build) into out/"
npx next build
fi
if [[ ! -d out ]]; then
echo "ERROR: out/ not found — the static export did not build." >&2
exit 1
fi
echo "==> Building ${IMAGE} for linux/amd64"
docker build \
--platform linux/amd64 \
-f "${DOCKERFILE}" \
-t "${IMAGE}" \
.
echo "==> Exporting ${IMAGE} to ${OUTPUT}"
docker save "${IMAGE}" | gzip > "${OUTPUT}"
echo "==> Done: ${OUTPUT} ($(du -h "${OUTPUT}" | cut -f1))"
echo
echo "Copy and run on the server:"
echo " scp ${OUTPUT} user@your-server:/tmp/"
echo " docker load < /tmp/${OUTPUT##*/}"
echo
echo "Run (nginx serves on container port ${CONTAINER_PORT}). Set the runtime env vars the"
echo "dashboard templates at startup — for the Agent Network onboarding test,"
echo "flip NETBIRD_AGENT_NETWORK_ONLY=true:"
echo " docker run -d --name ${IMAGE_NAME} --restart unless-stopped \\"
echo " -p 8080:${CONTAINER_PORT} \\"
echo " -e NETBIRD_MGMT_API_ENDPOINT=https://your-mgmt:443 \\"
echo " -e AUTH_AUTHORITY=https://your-idp/realms/netbird \\"
echo " -e AUTH_CLIENT_ID=netbird-dashboard \\"
echo " -e AUTH_AUDIENCE=netbird-dashboard \\"
echo " -e AUTH_SUPPORTED_SCOPES='openid profile email' \\"
echo " -e USE_AUTH0=false \\"
echo " -e NETBIRD_AGENT_NETWORK_ONLY=true \\"
echo " ${IMAGE}"
+11 -1
View File
@@ -14,5 +14,15 @@
"hotjarTrackID": "$NETBIRD_HOTJAR_TRACK_ID",
"googleAnalyticsID": "$NETBIRD_GOOGLE_ANALYTICS_ID",
"googleTagManagerID": "$NETBIRD_GOOGLE_TAG_MANAGER_ID",
"wasmPath": "$NETBIRD_WASM_PATH"
"authServiceUrl": "$NETBIRD_AUTH_SERVICE_URL",
"wasmPath": "$NETBIRD_WASM_PATH",
"licensed": "$NETBIRD_LICENSED",
"cloud": "$NETBIRD_CLOUD",
"agentNetworkOnly": "$NETBIRD_AGENT_NETWORK_ONLY",
"agentNetworkEnabled": "$NETBIRD_AGENT_NETWORK_ENABLED",
"hubspotPortalId": "$NETBIRD_HUBSPOT_PORTAL_ID",
"hubspotSignupFormId": "$NETBIRD_HUBSPOT_SIGNUP_FORM_ID",
"hubspotOnboardingFormId": "$NETBIRD_HUBSPOT_ONBOARDING_FORM_ID",
"hubspotSurveyFormId": "$NETBIRD_HUBSPOT_SURVEY_FORM_ID",
"analyticsExcludedEmails": "$NETBIRD_ANALYTICS_EXCLUDED_EMAILS"
}
-15
View File
@@ -1,15 +0,0 @@
import { defineConfig } from "cypress";
export default defineConfig({
e2e: {
baseUrl: "http://localhost:3000",
},
component: {
devServer: {
framework: "next",
bundler: "webpack",
},
},
viewportWidth: 1920,
viewportHeight: 1080,
});
-13
View File
@@ -1,13 +0,0 @@
describe("Click all tabs in peer modal", () => {
it("passes", () => {
cy.visit("/install");
cy.get("div").contains("Linux").click();
cy.get("[data-cy=copy-to-clipboard]").click();
cy.get("div").contains("Windows").click();
cy.get("[data-cy=copy-to-clipboard]").click();
cy.get("div").contains("Android").click();
cy.get("[data-cy=copy-to-clipboard]").click();
cy.get("div").contains("Docker").click();
cy.get("[data-cy=copy-to-clipboard]").click();
});
});
-5
View File
@@ -1,5 +0,0 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}
-37
View File
@@ -1,37 +0,0 @@
/// <reference types="cypress" />
// ***********************************************
// This example commands.ts shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add('login', (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This will overwrite an existing command --
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
//
// declare global {
// namespace Cypress {
// interface Chainable {
// login(email: string, password: string): Chainable<void>
// drag(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
// dismiss(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
// }
// }
// }
-20
View File
@@ -1,20 +0,0 @@
// ***********************************************************
// This example support/e2e.ts is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands'
// Alternatively you can use CommonJS syntax:
// require('./commands')
-9
View File
@@ -1,9 +0,0 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["es5", "dom"],
"baseUrl": "http://localhost:3000",
"types": ["cypress", "node"],
},
"include": ["**/*.ts"]
}
Loaded 20 of 970 files, more files were not shown because too many files have changed in this diff. Show more