Skip to content

feat: add optional heuristic deployment maturity indicator#5358

Open
ravii4 wants to merge 6 commits intonetbirdio:mainfrom
ravii4:feat/deployment-maturity-indicator
Open

feat: add optional heuristic deployment maturity indicator#5358
ravii4 wants to merge 6 commits intonetbirdio:mainfrom
ravii4:feat/deployment-maturity-indicator

Conversation

@ravii4
Copy link

@ravii4 ravii4 commented Feb 17, 2026

This PR introduces an optional, heuristic deployment_maturity indicator for self-hosted deployments.

The indicator provides an informational signal about deployment stage (exploration → functional → operational → production) derived from:

  • Peer count
  • Policy count
  • Account age

The field is:

  • Fully optional
  • Feature-flag gated
  • Informational only
  • Non-enforcing
  • Backwards compatible

(No routing, policy, enforcement, or persistence behavior is modified.)


What’s Included

1. Core Evaluation Logic

  • Adds EvaluateDeploymentMaturity in management/server/types
  • Uses simple, transparent thresholds
  • Includes unit tests
  • Includes golden testdata update

(The logic is deterministic and purely heuristic.)

2. API Wiring (Flag-Gated)

  • Adds config flag: EnableDeploymentMaturity

When enabled:

  • deployment_maturity is included in the Account response

When disabled:

  • Field is omitted entirely

  • OpenAPI schema updated

  • Generated types updated

(The field is optional and does not change existing API contracts.)

3. UI Component

  • Adds DeploymentMaturityCard component
  • Informational display only
  • Not auto-wired into existing dashboard flows
  • No UX behavior changes introduced in this PR

Backwards Compatibility

  • Field is optional
  • Disabled by default
  • No breaking API changes
  • No database schema changes
  • No changes to policy evaluation or enforcement

Testing

Validated locally using:

go fmt ./...
go vet ./management/server/...
go test ./management/server/...

@CLAassistant
Copy link

CLAassistant commented Feb 17, 2026

CLA assistant check
All committers have signed the CLA.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 17, 2026

📝 Walkthrough

Walkthrough

Replaces NewAPIHandler's positional parameters with a single APIHandlerDeps struct, adds an optional deployment maturity feature (config flag, heuristic, API field, tests, OpenAPI/types update), wires the feature into account handlers, and adds a frontend card component.

Changes

Cohort / File(s) Summary
Deployment Maturity Core
management/server/types/deployment_maturity.go, management/server/types/deployment_maturity_test.go
Adds DeploymentMaturity type, four stage constants, and EvaluateDeploymentMaturity(peerCount, policyCount, activeDays) heuristic plus table-driven tests.
Config & Boot
management/internals/server/config/config.go, management/internals/server/boot.go
Adds EnableDeploymentMaturity bool to Config and passes it into API handler construction via APIHandlerDeps.EnableDeploymentMaturity.
HTTP Handler / Dependency Injection
management/server/http/handler.go, management/server/http/testing/testing_tools/channel/channel.go, management/internals/server/boot.go
Introduces APIHandlerDeps struct and changes NewAPIHandler(ctx, deps APIHandlerDeps) signature; refactors handler wiring, middleware, integrations and endpoint registration to use deps fields (including EnableDeploymentMaturity).
Accounts Endpoint
management/server/http/handlers/accounts/accounts_handler.go
Adds enableDeploymentMaturity flag, computeDeploymentMaturity logic, and threads optional deployment_maturity into account responses and endpoint constructors.
API Schema & Types
shared/management/http/api/openapi.yml, shared/management/http/api/types.gen.go
Adds deployment_maturity enum property to OpenAPI Account schema and nullable DeploymentMaturity *string json:"deployment_maturity,omitempty"`` to generated Account type.
Frontend UI
proxy/web/src/components/DeploymentMaturityCard.tsx
New React component DeploymentMaturityCard renders stage-specific title/description; returns null when stage is null.
Testing / Call Sites
management/server/http/testing/testing_tools/channel/channel.go
Updated test helper to construct and pass APIHandlerDeps to NewAPIHandler; several nils used for optional fields.
Misc / Non-functional
client/internal/engine.go, shared/relay/client/early_msg_buffer.go
Minor import reorder and whitespace/formatting changes only.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client (UI / API consumer)
    participant API as API Handler
    participant AccountMgr as AccountManager
    participant PeersMgr as PeersManager
    participant Policies as PoliciesManager

    rect rgba(200,200,255,0.5)
    Client->>API: GET /api/accounts
    API->>AccountMgr: Fetch account(s) metadata
    API->>PeersMgr: Fetch peers for account
    API->>Policies: Fetch policy count for account
    end

    rect rgba(200,255,200,0.5)
    API->>API: EvaluateDeploymentMaturity(peers, policies, activeDays)
    API->>Client: Respond with account(s) + optional deployment_maturity
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • pascal-fischer
  • crn4

Poem

🐰 I sniffed the peers beneath the starlit sky,
Counted policies and days that flutter by,
From tiny test to production's light,
A little card will show the sight,
Hop, compute, and share — hooray for maturity!

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 23.08% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive The description provides comprehensive coverage of changes, rationale, and testing, but does not follow the repository template structure with explicit sections and checklist items. Consider reformatting the description to follow the template structure: use explicit 'Describe your changes', 'Issue ticket number and link', 'Stack', and 'Checklist' sections with checkboxes, and address the 'Documentation' section requirement.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and concisely describes the main change: adding an optional, heuristic deployment maturity indicator feature.

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

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

❤️ Share

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

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (6)
shared/management/http/api/openapi.yml (1)

253-257: Mark deployment_maturity as read-only to reflect server-computed data.

This field is derived heuristically and not client-settable, so adding readOnly: true clarifies API intent for consumers.

✅ Suggested schema tweak
         deployment_maturity:
           description: Optional deployment maturity stage derived from peer count, policy count, and account age. This field is informational only and does not affect account behavior.
           type: string
           enum: [ exploration, functional, operational, production ]
           example: operational
+          readOnly: true
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@shared/management/http/api/openapi.yml` around lines 253 - 257, The
deployment_maturity property in the OpenAPI schema is server-computed and should
be marked read-only; update the deployment_maturity schema to include readOnly:
true so clients know they cannot set it (keep the description, type, enum, and
example intact). Target the deployment_maturity property definition in the
OpenAPI YAML (the schema where description/type/enum/example are defined) and
add readOnly: true.
proxy/web/src/components/DeploymentMaturityCard.tsx (2)

6-6: Consider deriving DeploymentMaturityStage from generated API types.

This local type duplicates the enum values defined server-side. If the API types are code-generated (e.g., from OpenAPI), importing or deriving from them would prevent drift.

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

In `@proxy/web/src/components/DeploymentMaturityCard.tsx` at line 6, The local
string union type DeploymentMaturityStage duplicates server-side enum
values—replace it by importing or deriving the type from the generated API types
(e.g., the OpenAPI/SDK types) so it stays in sync; find usages of
DeploymentMaturityStage in DeploymentMaturityCard and change the type annotation
to the imported/generated enum/type (and update any runtime checks or mapping to
use the generated values) to avoid duplication and drift.

17-33: Hoist static lookup objects outside the component.

titleByStage and descriptionByStage are constant and don't depend on props or state, yet they're re-allocated on every render. Move them to module scope.

♻️ Proposed refactor
+const titleByStage: Record<DeploymentMaturityStage, string> = {
+  exploration: "Exploration deployment",
+  functional: "Functional deployment",
+  operational: "Operational deployment",
+  production: "Production deployment",
+};
+
+const descriptionByStage: Record<DeploymentMaturityStage, string> = {
+  exploration:
+    "This deployment is suitable for initial testing. Review core setup, access policies, and peer coverage before relying on it for daily work.",
+  functional:
+    "This deployment covers basic connectivity. Review routing, DNS, and access policies to ensure they match how your team actually works.",
+  operational:
+    "This deployment supports day-to-day use. Periodically review audit events, policy changes, and onboarding flows to keep it predictable.",
+  production:
+    "This deployment is ready for sustained production use. Keep an eye on change management, observability, and backup procedures.",
+};
+
 export const DeploymentMaturityCard = ({ stage }: DeploymentMaturityCardProps) => {
   if (!stage) {
     return null;
   }
 
-  const titleByStage: Record<DeploymentMaturityStage, string> = {
-    exploration: "Exploration deployment",
-    ...
-  };
-
-  const descriptionByStage: Record<DeploymentMaturityStage, string> = {
-    exploration: "...",
-    ...
-  };
-
   const titleText = titleByStage[stage];
   const descriptionText = descriptionByStage[stage];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@proxy/web/src/components/DeploymentMaturityCard.tsx` around lines 17 - 33,
Move the two constant lookup objects titleByStage and descriptionByStage out of
the DeploymentMaturityCard component and into module scope so they are allocated
once; declare them at the top-level of the file (keeping their type
Record<DeploymentMaturityStage, string> and the same keys/values), remove the
in-component declarations, and ensure the component uses the top-level
titleByStage and descriptionByStage identifiers unchanged.
management/server/http/handlers/accounts/accounts_handler.go (1)

141-171: Two extra DB round-trips on every account request when enabled.

GetPeers and ListPolicies are called on every GET /accounts and PUT /accounts/{id} when the flag is on. For large deployments these can be expensive. The inline NOTE (lines 150-152) already acknowledges this — consider caching the maturity result (e.g., in-memory with a short TTL or recompute on peer/policy mutations) before enabling this in production environments.

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

In `@management/server/http/handlers/accounts/accounts_handler.go` around lines
141 - 171, computeDeploymentMaturity currently triggers two DB calls
(h.accountManager.GetPeers and h.accountManager.ListPolicies) on every account
request when enableDeploymentMaturity is true; change this to cache the computed
maturity per account to avoid repeated round-trips: add an in-memory cache field
on the handler (map or sync.Map with TTL/expiry and mutex or use an existing
cache lib), have computeDeploymentMaturity first check the cache by accountID
and return cached value if fresh, otherwise perform the GetPeers/ListPolicies
calls, store the computed value with an expiry, and ensure cache
invalidation/update when peers or policies mutate (hook into peer/policy
mutation handlers to delete or refresh the accountID entry).
management/server/types/deployment_maturity.go (1)

18-31: Consider extracting magic numbers into named constants.

The threshold values (3, 8, 5 for peers; 1, 3, 2 for policies; 14 for days) are scattered through the conditionals. Named constants would make the heuristic easier to tune and reason about.

♻️ Proposed refactor
+const (
+	minPeersForFunctional    = 3
+	minPoliciesForFunctional = 1
+	minPeersForOperational   = 5
+	minPoliciesForOperational = 2
+	minPeersForProduction    = 8
+	minPoliciesForProduction = 3
+	minDaysForProduction     = 14
+)
+
 func EvaluateDeploymentMaturity(peerCount int, policyCount int, activeDays int) DeploymentMaturity {
-	if peerCount < 3 || policyCount < 1 {
+	if peerCount < minPeersForFunctional || policyCount < minPoliciesForFunctional {
 		return DeploymentMaturityExploration
 	}
 
-	if peerCount >= 8 && policyCount >= 3 && activeDays >= 14 {
+	if peerCount >= minPeersForProduction && policyCount >= minPoliciesForProduction && activeDays >= minDaysForProduction {
 		return DeploymentMaturityProduction
 	}
 
-	if peerCount >= 5 && policyCount >= 2 {
+	if peerCount >= minPeersForOperational && policyCount >= minPoliciesForOperational {
 		return DeploymentMaturityOperational
 	}
 
 	return DeploymentMaturityFunctional
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@management/server/types/deployment_maturity.go` around lines 18 - 31, The
EvaluateDeploymentMaturity function uses hard-coded threshold numbers (3, 8, 5
for peerCount; 1, 3, 2 for policyCount; 14 for activeDays); extract these magic
numbers into well-named package-level constants (e.g., minPeersExploration,
minPeersOperational, minPeersProduction, minPoliciesExploration,
minPoliciesOperational, minPoliciesProduction, minActiveDaysProduction) and
replace the numeric literals in EvaluateDeploymentMaturity with those constants
so the heuristic is easier to read and tune and to centralize thresholds for
tests and documentation.
management/server/types/deployment_maturity_test.go (1)

5-24: Use t.Run with named subtests for better diagnostics and add boundary cases.

The current test lacks subtest names, making failures hard to identify. Also, only 5 cases are tested — boundary conditions at the exact thresholds are missing (e.g., {3, 1, 0} for the Functional boundary, {8, 3, 13} for Production just-miss, {5, 2, 0} for Operational boundary, negative inputs).

♻️ Proposed improvement
 func TestEvaluateDeploymentMaturity(t *testing.T) {
 	tests := []struct {
+		name     string
 		peers    int
 		policies int
 		days     int
 		expected DeploymentMaturity
 	}{
-		{1, 0, 1, DeploymentMaturityExploration},
-		{3, 1, 1, DeploymentMaturityFunctional},
-		{5, 2, 5, DeploymentMaturityOperational},
-		{8, 3, 14, DeploymentMaturityProduction},
-		{8, 3, 5, DeploymentMaturityOperational},
+		{"below_peer_threshold", 1, 0, 1, DeploymentMaturityExploration},
+		{"below_policy_threshold", 5, 0, 30, DeploymentMaturityExploration},
+		{"functional_boundary", 3, 1, 0, DeploymentMaturityFunctional},
+		{"functional_typical", 3, 1, 1, DeploymentMaturityFunctional},
+		{"operational_boundary", 5, 2, 0, DeploymentMaturityOperational},
+		{"operational_typical", 5, 2, 5, DeploymentMaturityOperational},
+		{"production_boundary", 8, 3, 14, DeploymentMaturityProduction},
+		{"production_miss_days", 8, 3, 13, DeploymentMaturityOperational},
+		{"high_peers_low_days", 8, 3, 5, DeploymentMaturityOperational},
 	}
 
 	for _, tt := range tests {
-		stage := EvaluateDeploymentMaturity(tt.peers, tt.policies, tt.days)
-		if stage != tt.expected {
-			t.Errorf("expected %s, got %s", tt.expected, stage)
-		}
+		t.Run(tt.name, func(t *testing.T) {
+			stage := EvaluateDeploymentMaturity(tt.peers, tt.policies, tt.days)
+			if stage != tt.expected {
+				t.Errorf("EvaluateDeploymentMaturity(%d, %d, %d) = %s, want %s",
+					tt.peers, tt.policies, tt.days, stage, tt.expected)
+			}
+		})
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@management/server/types/deployment_maturity_test.go` around lines 5 - 24,
Replace the table-driven loop in TestEvaluateDeploymentMaturity with named
subtests using t.Run so failures show which case failed (use a descriptive name
per case like "peers=3_policies=1_days=0"), and expand the tests slice to
include boundary and edge cases such as {3,1,0} (Functional boundary), {8,3,13}
(just-miss Production), {5,2,0} (Operational boundary) and some negative inputs
to validate robustness; keep calling EvaluateDeploymentMaturity for each subtest
and assert equality with the expected DeploymentMaturity so each case reports
independently.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@management/server/http/handlers/accounts/accounts_handler.go`:
- Around line 141-171: computeDeploymentMaturity currently triggers two DB calls
(h.accountManager.GetPeers and h.accountManager.ListPolicies) on every account
request when enableDeploymentMaturity is true; change this to cache the computed
maturity per account to avoid repeated round-trips: add an in-memory cache field
on the handler (map or sync.Map with TTL/expiry and mutex or use an existing
cache lib), have computeDeploymentMaturity first check the cache by accountID
and return cached value if fresh, otherwise perform the GetPeers/ListPolicies
calls, store the computed value with an expiry, and ensure cache
invalidation/update when peers or policies mutate (hook into peer/policy
mutation handlers to delete or refresh the accountID entry).

In `@management/server/types/deployment_maturity_test.go`:
- Around line 5-24: Replace the table-driven loop in
TestEvaluateDeploymentMaturity with named subtests using t.Run so failures show
which case failed (use a descriptive name per case like
"peers=3_policies=1_days=0"), and expand the tests slice to include boundary and
edge cases such as {3,1,0} (Functional boundary), {8,3,13} (just-miss
Production), {5,2,0} (Operational boundary) and some negative inputs to validate
robustness; keep calling EvaluateDeploymentMaturity for each subtest and assert
equality with the expected DeploymentMaturity so each case reports
independently.

In `@management/server/types/deployment_maturity.go`:
- Around line 18-31: The EvaluateDeploymentMaturity function uses hard-coded
threshold numbers (3, 8, 5 for peerCount; 1, 3, 2 for policyCount; 14 for
activeDays); extract these magic numbers into well-named package-level constants
(e.g., minPeersExploration, minPeersOperational, minPeersProduction,
minPoliciesExploration, minPoliciesOperational, minPoliciesProduction,
minActiveDaysProduction) and replace the numeric literals in
EvaluateDeploymentMaturity with those constants so the heuristic is easier to
read and tune and to centralize thresholds for tests and documentation.

In `@proxy/web/src/components/DeploymentMaturityCard.tsx`:
- Line 6: The local string union type DeploymentMaturityStage duplicates
server-side enum values—replace it by importing or deriving the type from the
generated API types (e.g., the OpenAPI/SDK types) so it stays in sync; find
usages of DeploymentMaturityStage in DeploymentMaturityCard and change the type
annotation to the imported/generated enum/type (and update any runtime checks or
mapping to use the generated values) to avoid duplication and drift.
- Around line 17-33: Move the two constant lookup objects titleByStage and
descriptionByStage out of the DeploymentMaturityCard component and into module
scope so they are allocated once; declare them at the top-level of the file
(keeping their type Record<DeploymentMaturityStage, string> and the same
keys/values), remove the in-component declarations, and ensure the component
uses the top-level titleByStage and descriptionByStage identifiers unchanged.

In `@shared/management/http/api/openapi.yml`:
- Around line 253-257: The deployment_maturity property in the OpenAPI schema is
server-computed and should be marked read-only; update the deployment_maturity
schema to include readOnly: true so clients know they cannot set it (keep the
description, type, enum, and example intact). Target the deployment_maturity
property definition in the OpenAPI YAML (the schema where
description/type/enum/example are defined) and add readOnly: true.

@ravii4 ravii4 closed this Feb 17, 2026
@ravii4 ravii4 reopened this Feb 17, 2026
@ravii4
Copy link
Author

ravii4 commented Feb 17, 2026

Quality Gate Failed Quality Gate failed

Failed conditions 1 New issue 1 New Code Smells (required ≤ 0)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

This constructor already exceeded the parameter threshold before this PR.
This change only adds a single boolean flag for the optional deployment maturity feature.

Refactoring NewAPIHandler into a dependency struct would be a larger architectural change and is outside the scope of this PR.

@ravii4 ravii4 closed this Feb 17, 2026
@ravii4
Copy link
Author

ravii4 commented Feb 17, 2026

license signed

@ravii4 ravii4 reopened this Feb 17, 2026
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
management/server/http/handler.go (1)

68-92: Consider adding a Validate() method (or early nil guards) to distinguish required vs optional deps.

The struct has 22 fields with no annotation or enforcement separating required fields (AccountManager, AuthManager, AppMetrics, etc.) from optional ones (ReverseProxyManager, ProxyGRPCServer, etc.). A misconfigured caller will see a panic deep inside handler initialization rather than a clear error at the call site.

♻️ Suggested addition
+// Validate checks that all required fields are non-nil.
+func (d APIHandlerDeps) Validate() error {
+    if d.AccountManager == nil {
+        return fmt.Errorf("AccountManager is required")
+    }
+    if d.AuthManager == nil {
+        return fmt.Errorf("AuthManager is required")
+    }
+    if d.AppMetrics == nil {
+        return fmt.Errorf("AppMetrics is required")
+    }
+    // … other required fields
+    return nil
+}

Then call deps.Validate() at the top of NewAPIHandler.

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

In `@management/server/http/handler.go` around lines 68 - 92, Add a Validate
method on APIHandlerDeps that checks required fields (e.g., AccountManager,
AuthManager, AppMetrics, PermissionsManager, NetworksManager, ResourceManager,
etc.) for nil and returns a descriptive error listing missing deps; keep truly
optional fields (e.g., ReverseProxyManager, ProxyGRPCServer,
ReverseProxyDomainManager) out of the failure conditions. Update NewAPIHandler
to call deps.Validate() immediately and propagate the error so callers get a
clear failure instead of panics; additionally, where appropriate add defensive
nil-guards inside methods that rely on optional deps (e.g., ReverseProxyManager,
ProxyGRPCServer) to avoid runtime panics when those are intentionally unset.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@management/server/http/handler.go`:
- Around line 202-204: The call to reverseproxymanager.RegisterEndpoints passes
deps.ReverseProxyAccessLogs without nil-checking, which can lead to a panic when
accesslogsmanager.GetAllAccessLogs is later invoked; update the guard in the
handler so that RegisterEndpoints is only called when deps.ReverseProxyManager,
deps.ReverseProxyDomainManager, and deps.ReverseProxyAccessLogs are all non-nil
(or alternatively pass a safe non-nil stub), i.e. add
deps.ReverseProxyAccessLogs != nil to the existing if condition that surrounds
reverseproxymanager.RegisterEndpoints so the access logs manager is validated
before being stored/used.

---

Nitpick comments:
In `@management/server/http/handler.go`:
- Around line 68-92: Add a Validate method on APIHandlerDeps that checks
required fields (e.g., AccountManager, AuthManager, AppMetrics,
PermissionsManager, NetworksManager, ResourceManager, etc.) for nil and returns
a descriptive error listing missing deps; keep truly optional fields (e.g.,
ReverseProxyManager, ProxyGRPCServer, ReverseProxyDomainManager) out of the
failure conditions. Update NewAPIHandler to call deps.Validate() immediately and
propagate the error so callers get a clear failure instead of panics;
additionally, where appropriate add defensive nil-guards inside methods that
rely on optional deps (e.g., ReverseProxyManager, ProxyGRPCServer) to avoid
runtime panics when those are intentionally unset.

Comment on lines 202 to 204
if deps.ReverseProxyManager != nil && deps.ReverseProxyDomainManager != nil {
reverseproxymanager.RegisterEndpoints(deps.ReverseProxyManager, *deps.ReverseProxyDomainManager, deps.ReverseProxyAccessLogs, router)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find RegisterEndpoints function in reverseproxymanager
rg -n "func RegisterEndpoints" --type=go -A 20 -B 2

Repository: netbirdio/netbird

Length of output: 11526


deps.ReverseProxyAccessLogs must be checked for nil before passing to RegisterEndpoints.

The guard on line 202 ensures ReverseProxyManager and ReverseProxyDomainManager are non-nil, but deps.ReverseProxyAccessLogs is passed unchecked to RegisterEndpoints, which passes it directly to accesslogsmanager.RegisterEndpoints (line 32 of manager/api.go). The accesslogs handler stores this manager without validation, and calling the /events/proxy endpoint with a nil manager will panic when GetAllAccessLogs is invoked.

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

In `@management/server/http/handler.go` around lines 202 - 204, The call to
reverseproxymanager.RegisterEndpoints passes deps.ReverseProxyAccessLogs without
nil-checking, which can lead to a panic when accesslogsmanager.GetAllAccessLogs
is later invoked; update the guard in the handler so that RegisterEndpoints is
only called when deps.ReverseProxyManager, deps.ReverseProxyDomainManager, and
deps.ReverseProxyAccessLogs are all non-nil (or alternatively pass a safe
non-nil stub), i.e. add deps.ReverseProxyAccessLogs != nil to the existing if
condition that surrounds reverseproxymanager.RegisterEndpoints so the access
logs manager is validated before being stored/used.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@management/server/http/handler.go`:
- Around line 253-261: The call to reverseproxymanager.RegisterEndpoints in
registerReverseProxyAndOAuth passes deps.ReverseProxyAccessLogs without checking
it for nil; update the guard to ensure deps.ReverseProxyAccessLogs is non-nil
(in addition to deps.ReverseProxyManager and deps.ReverseProxyDomainManager)
before invoking reverseproxymanager.RegisterEndpoints so it never receives a nil
access-logs dependency (or alternatively handle a nil-safe value inside
RegisterEndpoints), referencing registerReverseProxyAndOAuth,
APIHandlerDeps.ReverseProxyAccessLogs and reverseproxymanager.RegisterEndpoints
to locate the change.

@ravii4 ravii4 force-pushed the feat/deployment-maturity-indicator branch from cbad720 to 0299073 Compare February 18, 2026 14:36
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
proxy/web/src/components/DeploymentMaturityCard.tsx (1)

17-33: Consider hoisting static lookup maps outside the component.

titleByStage and descriptionByStage are constant objects recreated on every render. Moving them to module scope avoids unnecessary allocations.

♻️ Suggested change
+const titleByStage: Record<DeploymentMaturityStage, string> = {
+  exploration: "Exploration deployment",
+  functional: "Functional deployment",
+  operational: "Operational deployment",
+  production: "Production deployment",
+};
+
+const descriptionByStage: Record<DeploymentMaturityStage, string> = {
+  exploration:
+    "This deployment is suitable for initial testing. Review core setup, access policies, and peer coverage before relying on it for daily work.",
+  functional:
+    "This deployment covers basic connectivity. Review routing, DNS, and access policies to ensure they match how your team actually works.",
+  operational:
+    "This deployment supports day-to-day use. Periodically review audit events, policy changes, and onboarding flows to keep it predictable.",
+  production:
+    "This deployment is ready for sustained production use. Keep an eye on change management, observability, and backup procedures.",
+};
+
 export const DeploymentMaturityCard = ({ stage }: DeploymentMaturityCardProps) => {
   if (!stage) {
     return null;
   }
 
-  const titleByStage: Record<DeploymentMaturityStage, string> = {
-    exploration: "Exploration deployment",
-    functional: "Functional deployment",
-    operational: "Operational deployment",
-    production: "Production deployment",
-  };
-
-  const descriptionByStage: Record<DeploymentMaturityStage, string> = {
-    exploration:
-      "This deployment is suitable for initial testing. ...",
-    functional:
-      "This deployment covers basic connectivity. ...",
-    operational:
-      "This deployment supports day-to-day use. ...",
-    production:
-      "This deployment is ready for sustained production use. ...",
-  };
-
   const titleText = titleByStage[stage];
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@proxy/web/src/components/DeploymentMaturityCard.tsx` around lines 17 - 33,
Move the two static lookup objects titleByStage and descriptionByStage out of
the DeploymentMaturityCard component into module scope as top-level constants
(e.g., declare const titleByStage: Record<DeploymentMaturityStage,string> = ...
and similarly for descriptionByStage) so they are created once, then update the
component to reference these top-level constants without changing their keys or
values; ensure the symbol names remain titleByStage and descriptionByStage so
existing usages inside DeploymentMaturityCard continue to work.
management/server/types/deployment_maturity_test.go (1)

5-24: Use t.Run subtests and include inputs in error messages for easier debugging.

Without subtests, a failure reports only expected vs got, with no indication of which case failed. Adding names also lets you run individual cases with -run.

♻️ Suggested improvement
 func TestEvaluateDeploymentMaturity(t *testing.T) {
 	tests := []struct {
+		name     string
 		peers    int
 		policies int
 		days     int
 		expected DeploymentMaturity
 	}{
-		{1, 0, 1, DeploymentMaturityExploration},
-		{3, 1, 1, DeploymentMaturityFunctional},
-		{5, 2, 5, DeploymentMaturityOperational},
-		{8, 3, 14, DeploymentMaturityProduction},
-		{8, 3, 5, DeploymentMaturityOperational},
+		{"below_min_peers", 1, 0, 1, DeploymentMaturityExploration},
+		{"functional_threshold", 3, 1, 1, DeploymentMaturityFunctional},
+		{"operational_threshold", 5, 2, 5, DeploymentMaturityOperational},
+		{"production_threshold", 8, 3, 14, DeploymentMaturityProduction},
+		{"production_peers_but_few_days", 8, 3, 5, DeploymentMaturityOperational},
 	}
 
 	for _, tt := range tests {
-		stage := EvaluateDeploymentMaturity(tt.peers, tt.policies, tt.days)
-		if stage != tt.expected {
-			t.Errorf("expected %s, got %s", tt.expected, stage)
-		}
+		t.Run(tt.name, func(t *testing.T) {
+			stage := EvaluateDeploymentMaturity(tt.peers, tt.policies, tt.days)
+			if stage != tt.expected {
+				t.Errorf("EvaluateDeploymentMaturity(%d, %d, %d) = %s, want %s",
+					tt.peers, tt.policies, tt.days, stage, tt.expected)
+			}
+		})
 	}
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@management/server/types/deployment_maturity_test.go` around lines 5 - 24,
Update TestEvaluateDeploymentMaturity to use t.Run subtests: for each case in
the tests slice call t.Run with a descriptive name that includes the inputs
(e.g., fmt.Sprintf("peers=%d_policies=%d_days=%d", peers, policies, days)),
invoke EvaluateDeploymentMaturity inside the subtest, and on failure call
t.Errorf including the input values as well as expected and actual (so the error
message shows peers, policies, days, expected, got). Ensure you reference the
existing TestEvaluateDeploymentMaturity test table and the
EvaluateDeploymentMaturity function when making the change.
management/server/http/handlers/accounts/accounts_handler.go (1)

141-171: Maturity computation adds two queries per account request — acceptable when gated, but worth monitoring.

The inline comment (Lines 150-152) correctly flags the per-request overhead. Since the feature is flag-gated and disabled by default, this is fine for now. If enabled for accounts with large peer/policy sets, the cost of GetPeers + ListPolicies on every GET/PUT could become noticeable.

Consider caching the maturity result (e.g., recompute on peer/policy mutation and store alongside account metadata) if usage telemetry shows this becoming a hot path.

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

In `@management/server/http/handlers/accounts/accounts_handler.go` around lines
141 - 171, computeDeploymentMaturity currently issues GetPeers and ListPolicies
on every account request when enableDeploymentMaturity is true, causing two
extra queries per request; change this to read a cached maturity value from
account metadata (meta) when present and only call EvaluateDeploymentMaturity +
persist the computed value on peer/policy mutation events (i.e., in
accountManager methods that add/remove peers or create/update/delete policies).
Specifically, update computeDeploymentMaturity to prefer a cached field on
types.AccountMeta (e.g., Maturity or similar) and avoid calling
h.accountManager.GetPeers/ListPolicies during reads; implement recomputation
logic that calls types.EvaluateDeploymentMaturity and writes the new value back
to account metadata from the mutation handlers so subsequent calls to
computeDeploymentMaturity return the cached string without extra queries.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@management/server/http/handler.go`:
- Around line 253-261: The guard in registerReverseProxyAndOAuth only checks
deps.ReverseProxyManager and deps.ReverseProxyDomainManager but not
deps.ReverseProxyAccessLogs, so add a nil check for ReverseProxyAccessLogs
before calling reverseproxymanager.RegisterEndpoints; update the if condition to
require deps.ReverseProxyAccessLogs != nil (in addition to ReverseProxyManager
!= nil and ReverseProxyDomainManager != nil) so RegisterEndpoints is only
invoked when all three dependencies exist, preventing downstream nil dereference
in handlers like /events/proxy.

---

Nitpick comments:
In `@management/server/http/handlers/accounts/accounts_handler.go`:
- Around line 141-171: computeDeploymentMaturity currently issues GetPeers and
ListPolicies on every account request when enableDeploymentMaturity is true,
causing two extra queries per request; change this to read a cached maturity
value from account metadata (meta) when present and only call
EvaluateDeploymentMaturity + persist the computed value on peer/policy mutation
events (i.e., in accountManager methods that add/remove peers or
create/update/delete policies). Specifically, update computeDeploymentMaturity
to prefer a cached field on types.AccountMeta (e.g., Maturity or similar) and
avoid calling h.accountManager.GetPeers/ListPolicies during reads; implement
recomputation logic that calls types.EvaluateDeploymentMaturity and writes the
new value back to account metadata from the mutation handlers so subsequent
calls to computeDeploymentMaturity return the cached string without extra
queries.

In `@management/server/types/deployment_maturity_test.go`:
- Around line 5-24: Update TestEvaluateDeploymentMaturity to use t.Run subtests:
for each case in the tests slice call t.Run with a descriptive name that
includes the inputs (e.g., fmt.Sprintf("peers=%d_policies=%d_days=%d", peers,
policies, days)), invoke EvaluateDeploymentMaturity inside the subtest, and on
failure call t.Errorf including the input values as well as expected and actual
(so the error message shows peers, policies, days, expected, got). Ensure you
reference the existing TestEvaluateDeploymentMaturity test table and the
EvaluateDeploymentMaturity function when making the change.

In `@proxy/web/src/components/DeploymentMaturityCard.tsx`:
- Around line 17-33: Move the two static lookup objects titleByStage and
descriptionByStage out of the DeploymentMaturityCard component into module scope
as top-level constants (e.g., declare const titleByStage:
Record<DeploymentMaturityStage,string> = ... and similarly for
descriptionByStage) so they are created once, then update the component to
reference these top-level constants without changing their keys or values;
ensure the symbol names remain titleByStage and descriptionByStage so existing
usages inside DeploymentMaturityCard continue to work.

@ravii4 ravii4 force-pushed the feat/deployment-maturity-indicator branch from 0299073 to 9039ebf Compare February 18, 2026 14:45
@sonarqubecloud
Copy link

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 18, 2026

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
{"name":"HttpError","status":401,"request":{"method":"PATCH","url":"https://api.github.com/repos/netbirdio/netbird/issues/comments/3912252795","headers":{"accept":"application/vnd.github.v3+json","user-agent":"octokit.js/0.0.0-development octokit-core.js/7.0.6 Node.js/24","authorization":"token [REDACTED]","content-type":"application/json; charset=utf-8"},"body":{"body":"<!-- This is an auto-generated comment: summarize by coderabbit.ai -->\n<!-- walkthrough_start -->\n\n<details>\n<summary>📝 Walkthrough</summary>\n\n## Walkthrough\n\nReplaces NewAPIHandler's positional parameters with a single APIHandlerDeps struct, adds an optional deployment maturity feature (config flag, heuristic, API field, tests, OpenAPI/types update), wires the feature into account handlers, and adds a frontend card component.\n\n## Changes\n\n|Cohort / File(s)|Summary|\n|---|---|\n|**Deployment Maturity Core** <br> `management/server/types/deployment_maturity.go`, `management/server/types/deployment_maturity_test.go`|Adds `DeploymentMaturity` type, four stage constants, and `EvaluateDeploymentMaturity(peerCount, policyCount, activeDays)` heuristic plus table-driven tests.|\n|**Config & Boot** <br> `management/internals/server/config/config.go`, `management/internals/server/boot.go`|Adds `EnableDeploymentMaturity bool` to `Config` and passes it into API handler construction via `APIHandlerDeps.EnableDeploymentMaturity`.|\n|**HTTP Handler / Dependency Injection** <br> `management/server/http/handler.go`, `management/server/http/testing/testing_tools/channel/channel.go`, `management/internals/server/boot.go`|Introduces `APIHandlerDeps` struct and changes `NewAPIHandler(ctx, deps APIHandlerDeps)` signature; refactors handler wiring, middleware, integrations and endpoint registration to use deps fields (including `EnableDeploymentMaturity`).|\n|**Accounts Endpoint** <br> `management/server/http/handlers/accounts/accounts_handler.go`|Adds `enableDeploymentMaturity` flag, `computeDeploymentMaturity` logic, and threads optional `deployment_maturity` into account responses and endpoint constructors.|\n|**API Schema & Types** <br> `shared/management/http/api/openapi.yml`, `shared/management/http/api/types.gen.go`|Adds `deployment_maturity` enum property to OpenAPI Account schema and nullable `DeploymentMaturity *string `json:\"deployment_maturity,omitempty\"`` to generated Account type.|\n|**Frontend UI** <br> `proxy/web/src/components/DeploymentMaturityCard.tsx`|New React component `DeploymentMaturityCard` renders stage-specific title/description; returns null when stage is null.|\n|**Testing / Call Sites** <br> `management/server/http/testing/testing_tools/channel/channel.go`|Updated test helper to construct and pass `APIHandlerDeps` to NewAPIHandler; several nils used for optional fields.|\n|**Misc / Non-functional** <br> `client/internal/engine.go`, `shared/relay/client/early_msg_buffer.go`|Minor import reorder and whitespace/formatting changes only.|\n\n## Sequence Diagram(s)\n\n```mermaid\nsequenceDiagram\n    participant Client as Client (UI / API consumer)\n    participant API as API Handler\n    participant AccountMgr as AccountManager\n    participant PeersMgr as PeersManager\n    participant Policies as PoliciesManager\n\n    rect rgba(200,200,255,0.5)\n    Client->>API: GET /api/accounts\n    API->>AccountMgr: Fetch account(s) metadata\n    API->>PeersMgr: Fetch peers for account\n    API->>Policies: Fetch policy count for account\n    end\n\n    rect rgba(200,255,200,0.5)\n    API->>API: EvaluateDeploymentMaturity(peers, policies, activeDays)\n    API->>Client: Respond with account(s) + optional deployment_maturity\n    end\n```\n\n## Estimated code review effort\n\n🎯 4 (Complex) | ⏱️ ~45 minutes\n\n## Possibly related PRs\n\n- netbirdio/netbird#4774 — Modifies NewAPIHandler call sites and handler wiring; overlaps on dependency restructuring.  \n- netbirdio/netbird#4849 — Also changes NewAPIHandler signature and wiring (adds managers); touches same initialization surface.\n\n## Suggested reviewers\n\n- pascal-fischer  \n- crn4\n\n## Poem\n\n> 🐰 I sniffed the peers beneath the starlit sky,  \n> Counted policies and days that flutter by,  \n> From tiny test to production's light,  \n> A little card will show the sight,  \n> Hop, compute, and share — hooray for maturity!\n\n</details>\n\n<!-- walkthrough_end -->\n\n\n<!-- pre_merge_checks_walkthrough_start -->\n\n<details>\n<summary>🚥 Pre-merge checks | ✅ 1 | ❌ 2</summary>\n\n### ❌ Failed checks (1 warning, 1 inconclusive)\n\n|     Check name     | Status         | Explanation                                                                                                                                                                                 | Resolution                                                                                                                                                                                                                                      |\n| :----------------: | :------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| Docstring Coverage | ⚠️ Warning     | Docstring coverage is 23.08% which is insufficient. The required threshold is 80.00%.                                                                                                       | Write docstrings for the functions missing them to satisfy the coverage threshold.                                                                                                                                                              |\n|  Description check | ❓ Inconclusive | PR description provides comprehensive technical details but lacks required template sections: issue ticket number/link, stack information, checklist completion, and documentation section. | Add missing template sections: issue ticket link, stack/branch-stack info, complete the checklist for issue type and testing, and explicitly address the Documentation section (indicate if docs are needed and provide PR link if applicable). |\n\n<details>\n<summary>✅ Passed checks (1 passed)</summary>\n\n|  Check name | Status   | Explanation                                                                                                                        |\n| :---------: | :------- | :--------------------------------------------------------------------------------------------------------------------------------- |\n| Title check | ✅ Passed | The title accurately and concisely describes the main change: adding an optional, heuristic deployment maturity indicator feature. |\n\n</details>\n\n<sub>✏️ Tip: You can configure your own custom pre-merge checks in the settings.</sub>\n\n</details>\n\n<!-- pre_merge_checks_walkthrough_end -->\n\n<!-- finishing_touch_checkbox_start -->\n\n<details>\n<summary>✨ Finishing Touches</summary>\n\n- [ ] <!-- {\"checkboxId\": \"7962f53c-55bc-4827-bfbf-6a18da830691\"} --> 📝 Generate docstrings\n<details>\n<summary>🧪 Generate unit tests (beta)</summary>\n\n- [ ] <!-- {\"checkboxId\": \"f47ac10b-58cc-4372-a567-0e02b2c3d479\", \"radioGroupId\": \"utg-output-choice-group-unknown_comment_id\"} -->   Create PR with unit tests\n- [ ] <!-- {\"checkboxId\": \"07f1e7d6-8a8e-4e23-9900-8731c2c87f58\", \"radioGroupId\": \"utg-output-choice-group-unknown_comment_id\"} -->   Post copyable unit tests in a comment\n\n</details>\n\n</details>\n\n<!-- finishing_touch_checkbox_end -->\n\n<!-- tips_start -->\n\n---\n\nThanks for using [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=netbirdio/netbird&utm_content=5358)! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.\n\n<details>\n<summary>❤️ Share</summary>\n\n- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)\n- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)\n- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)\n- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)\n\n</details>\n\n<sub>Comment `@coderabbitai help` to get the list of available commands and usage tips.</sub>\n\n<!-- tips_end -->\n\n<!-- internal state start -->\n\n\n<!-- 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 -->\n\n<!-- internal state end -->"},"request":{"retryCount":1}},"response":{"url":"https://api.github.com/repos/netbirdio/netbird/issues/comments/3912252795","status":401,"headers":{"access-control-allow-origin":"*","access-control-expose-headers":"ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset","connection":"close","content-security-policy":"default-src 'none'","content-type":"application/json; charset=utf-8","date":"Wed, 18 Feb 2026 15:04:31 GMT","referrer-policy":"origin-when-cross-origin, strict-origin-when-cross-origin","server":"github.com","strict-transport-security":"max-age=31536000; includeSubdomains; preload","vary":"Accept-Encoding, Accept, X-Requested-With","x-content-type-options":"nosniff","x-frame-options":"deny","x-github-media-type":"github.v3; format=json","x-github-request-id":"0467:A1F52:55D77CC:17638277:6995D4FF","x-xss-protection":"0"},"data":{"message":"Bad credentials","documentation_url":"https://docs.github.com/rest","status":"401"}}}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

Comments