From f2d5892aa5af4e8eaddc8cd9b55bd70841106694 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Fri, 17 Apr 2026 12:16:03 +0100 Subject: [PATCH] Skip onboarding overflow bonus on uncapped plans MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The free-tier onboarding overflow adds +1 to MaxMonitoredSystems for 14 days after initial setup. Once rc.2 made self-hosted core monitoring uncapped (MaxMonitoredSystems = 0 on Free), the bonus math silently converted "unlimited" into a hard cap of 1 — the UI then surfaced "Over plan by N. N monitored, 1 included." on healthy installs. Guard the addition on limit > 0 at all three call sites (ledger path, commercial entitlement payload, runtime capabilities payload) so the bonus only extends plans that actually have a cap. Refs #1429 --- .../api/monitored_system_limit_enforcement.go | 6 +++-- ...monitored_system_limit_enforcement_test.go | 24 +++++++++++++++++++ internal/api/subscription_entitlements.go | 16 +++++++++---- 3 files changed, 39 insertions(+), 7 deletions(-) diff --git a/internal/api/monitored_system_limit_enforcement.go b/internal/api/monitored_system_limit_enforcement.go index 631333bb3..8b62e47ab 100644 --- a/internal/api/monitored_system_limit_enforcement.go +++ b/internal/api/monitored_system_limit_enforcement.go @@ -85,8 +85,10 @@ func maxMonitoredSystemsLimitForContext(ctx context.Context) int { limit := status.MaxMonitoredSystems - // Apply onboarding overflow bonus for free-tier orgs. - if status.Tier == licenseTierFreeValue { + // Apply onboarding overflow bonus for free-tier orgs. The bonus only + // makes sense on plans that actually have a cap — adding +1 to an + // uncapped limit (0) would convert "unlimited" into a cap of 1. + if status.Tier == licenseTierFreeValue && limit > 0 { var overflowGrantedAt *int64 // Try evaluator first (covers hosted path with DatabaseSource). diff --git a/internal/api/monitored_system_limit_enforcement_test.go b/internal/api/monitored_system_limit_enforcement_test.go index 16ac70020..7bb9d57a7 100644 --- a/internal/api/monitored_system_limit_enforcement_test.go +++ b/internal/api/monitored_system_limit_enforcement_test.go @@ -550,6 +550,30 @@ func TestVMwareAdmissionEnforcementSkipsUsageAndInventoryForDisabledConnections( ) } +func TestOnboardingOverflowBonusSkipsUncappedPlans(t *testing.T) { + enforcement := readAPIPackageFile(t, "monitored_system_limit_enforcement.go") + overflowSegment := requireSourceSegment( + t, + enforcement, + "func maxMonitoredSystemsLimitForContext", + "\n}\n", + ) + requireSnippetBefore( + t, + overflowSegment, + "limit > 0", + "overflowBonusFromLicensing", + ) + + entitlements := readAPIPackageFile(t, "subscription_entitlements.go") + requireSnippetCountAtLeast( + t, + entitlements, + "if status.MaxMonitoredSystems > 0 {", + 2, + ) +} + func TestMonitoredSystemCountNilMonitor(t *testing.T) { got := monitoredSystemCount(nil) if got != 0 { diff --git a/internal/api/subscription_entitlements.go b/internal/api/subscription_entitlements.go index bfa346c3c..eeb214589 100644 --- a/internal/api/subscription_entitlements.go +++ b/internal/api/subscription_entitlements.go @@ -40,11 +40,15 @@ func (h *LicenseHandlers) buildCommercialEntitlementPayload( usage := h.entitlementUsageSnapshot(ctx) trialEndsAtUnix := trialEndsAtUnixFromService(svc) - // Onboarding overflow: +1 agent for 14 days on free tier. + // Onboarding overflow: +1 agent for 14 days on free tier. Only applies + // on plans that have an actual cap — adding the bonus to an uncapped + // limit (0) would surface as a cap of 1 to the UI. overflowGrantedAt := h.ensureOnboardingOverflow(ctx, status.Tier) now := time.Now() - if bonus := overflowBonusFromLicensing(status.Tier, overflowGrantedAt, now); bonus > 0 { - status.MaxMonitoredSystems += bonus + if status.MaxMonitoredSystems > 0 { + if bonus := overflowBonusFromLicensing(status.Tier, overflowGrantedAt, now); bonus > 0 { + status.MaxMonitoredSystems += bonus + } } payload := buildEntitlementPayloadWithUsage(status, svc.SubscriptionState(), usage, trialEndsAtUnix) @@ -123,8 +127,10 @@ func (h *LicenseHandlers) HandleRuntimeCapabilities(w http.ResponseWriter, r *ht overflowGrantedAt := h.ensureOnboardingOverflow(r.Context(), status.Tier) now := time.Now() - if bonus := overflowBonusFromLicensing(status.Tier, overflowGrantedAt, now); bonus > 0 { - status.MaxMonitoredSystems += bonus + if status.MaxMonitoredSystems > 0 { + if bonus := overflowBonusFromLicensing(status.Tier, overflowGrantedAt, now); bonus > 0 { + status.MaxMonitoredSystems += bonus + } } payload := buildRuntimeCapabilitiesPayloadWithUsage(status, svc.SubscriptionState(), usage)