diff --git a/docs/release-control/v6/internal/subsystems/cloud-paid.md b/docs/release-control/v6/internal/subsystems/cloud-paid.md index 7f24d6ea4..580f504bb 100644 --- a/docs/release-control/v6/internal/subsystems/cloud-paid.md +++ b/docs/release-control/v6/internal/subsystems/cloud-paid.md @@ -1167,23 +1167,21 @@ mounts. That same counted-unit boundary also owns the disclosure rule for retail copy: default billing and pricing surfaces should use concise monitored-system copy, while the full counted-unit definition appears only behind explicit disclosure -such as `View counting rules` instead of sitting as always-visible explanatory -chrome. -When a self-hosted billing arrival is explicitly about monitored-system -capacity, `ProLicensePlanSection.tsx` may still open that disclosure by -default, but the same top-level-root definition must remain the canonical -counted-unit explanation rather than introducing a second plan-local wording. -The same boundary also owns where monitored-system capacity truth lives. The -canonical persistent explanation belongs in the self-hosted Pro plan surface: -`ProLicensePanel.tsx`, `ProLicensePlanSection.tsx`, and -`useProLicensePanelState.ts` must render a dedicated capacity section with the -current monitored count, included plan limit, status posture, and any -over-plan or continuity explanation. Customer-facing copy there should -describe the current admission posture in plain language, not raw -`current / limit` math. When a plan is full, the section must explain that -existing monitoring continues while new monitored systems are blocked; when an -installation is already above the current plan, it must explain that Pulse is -in an over-plan frozen state rather than implying a hard runtime blackout. +such as `View counting rules` on the usage-owned monitored-system surfaces +instead of sitting as persistent plan-tab chrome. +The same boundary also owns where monitored-system capacity truth lives. A +dedicated self-hosted Pro plan-surface capacity section is only canonical +when Pulse is reconciling or enforcing a finite monitored-system limit, such +as bounded migration continuity, grandfathered floors, or other explicit +carry-forward limits. Uncapped self-hosted plans should not keep a +`Monitoring capacity` section alive just to restate that monitoring is +unlimited; those plan surfaces should describe core monitoring as unlimited in +their summary model and leave counted-unit explanation plus current usage +inspection to the usage-owned ledger/disclosure path. When a finite plan is +full, the section must explain that existing monitoring continues while new +monitored systems are blocked; when an installation is already above the +current plan, it must explain that Pulse is in an over-plan frozen state +rather than implying a hard runtime blackout. Community overflow/setup-slot messaging must still explain the included monitored systems plus the temporary setup slot in customer terms rather than compressing the contract into slash-style quota strings that imply Pulse is diff --git a/docs/release-control/v6/internal/subsystems/registry.json b/docs/release-control/v6/internal/subsystems/registry.json index df8032093..604adbb34 100644 --- a/docs/release-control/v6/internal/subsystems/registry.json +++ b/docs/release-control/v6/internal/subsystems/registry.json @@ -2190,6 +2190,7 @@ "frontend-modern/src/components/Settings/MonitoredSystemLedgerPanel.tsx", "frontend-modern/src/components/Settings/ProLicensePanel.tsx", "frontend-modern/src/components/Settings/ProLicensePlanSection.tsx", + "frontend-modern/src/components/Settings/selfHostedBillingPresentation.ts", "frontend-modern/src/components/Settings/SelfHostedCommercialRecoverySection.tsx", "frontend-modern/src/components/Settings/useProLicensePanelState.ts" ], diff --git a/frontend-modern/src/components/Settings/ProLicensePlanSection.tsx b/frontend-modern/src/components/Settings/ProLicensePlanSection.tsx index a8f374d3c..dd8edc0e3 100644 --- a/frontend-modern/src/components/Settings/ProLicensePlanSection.tsx +++ b/frontend-modern/src/components/Settings/ProLicensePlanSection.tsx @@ -1,7 +1,6 @@ import { Component, For, Show } from 'solid-js'; import RefreshCw from 'lucide-solid/icons/refresh-cw'; import { UpgradeLink } from '@/components/shared/UpgradeLink'; -import { MonitoredSystemDefinitionDisclosure } from '@/components/Commercial/MonitoredSystemDefinitionDisclosure'; import { getUpgradeActionDestination } from '@/stores/licenseCommercial'; import { licenseEntitlementsLoadError } from '@/stores/licenseEntitlements'; import { @@ -118,12 +117,6 @@ export const ProLicensePlanSection: Component = (pro -
- -
{(notice) => (
diff --git a/frontend-modern/src/components/Settings/__tests__/ProLicensePanel.test.tsx b/frontend-modern/src/components/Settings/__tests__/ProLicensePanel.test.tsx index aae83636b..4535f0376 100644 --- a/frontend-modern/src/components/Settings/__tests__/ProLicensePanel.test.tsx +++ b/frontend-modern/src/components/Settings/__tests__/ProLicensePanel.test.tsx @@ -296,8 +296,17 @@ describe('ProLicensePanel', () => { }); expect(screen.getByText('V5 Lifetime Grandfathered')).toBeInTheDocument(); + expect( + screen.getByText( + 'Review your active plan, expiry, and the paid capabilities that come with it.', + ), + ).toBeInTheDocument(); expect(screen.queryByText('Included Monitored Systems')).not.toBeInTheDocument(); expect(screen.getByText('Max Guests')).toBeInTheDocument(); + expect(screen.getByText('Core Monitoring')).toBeInTheDocument(); + expect(screen.queryByText('Monitored Systems')).not.toBeInTheDocument(); + expect(screen.queryByText('Capacity Status')).not.toBeInTheDocument(); + expect(screen.queryByText('Monitoring capacity')).not.toBeInTheDocument(); expect(screen.getAllByText('Unlimited').length).toBeGreaterThan(0); expect(screen.queryByText('5 / 12')).not.toBeInTheDocument(); }); @@ -343,17 +352,14 @@ describe('ProLicensePanel', () => { ), ).toBeInTheDocument(); expect( - within(screen.getByText('Monitored Systems').parentElement as HTMLElement).getByText( - 'Unlimited', - ), - ).toBeInTheDocument(); - expect( - within(screen.getByText('Capacity Status').parentElement as HTMLElement).getByText( + within(screen.getByText('Core Monitoring').parentElement as HTMLElement).getByText( 'Unlimited', ), ).toBeInTheDocument(); + expect(screen.queryByText('Capacity Status')).not.toBeInTheDocument(); expect(screen.queryByText('Included Monitored Systems')).not.toBeInTheDocument(); expect(screen.getByText('Max Guests')).toBeInTheDocument(); + expect(screen.queryByText('Monitoring capacity')).not.toBeInTheDocument(); expect(screen.getAllByText('Unlimited').length).toBeGreaterThan(0); expect(screen.queryByText('Plan Monitored System Limit')).not.toBeInTheDocument(); @@ -756,32 +762,20 @@ describe('ProLicensePanel', () => { 'Community keeps core monitoring free. Compare Relay and Pro in Pulse Account, then return here with Pulse Pro activated automatically.', ), ).toBeInTheDocument(); - expect(screen.getByRole('button', { name: 'Hide counting rules' })).toBeInTheDocument(); - expect( - screen.getByText( - /a monitored system is a top-level monitored root such as a docker host, kubernetes cluster, proxmox node/i, - ), - ).toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'Hide counting rules' })).not.toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'View counting rules' })).not.toBeInTheDocument(); expect(screen.getByRole('link', { name: 'Compare plans' })).toHaveAttribute( 'href', getSelfHostedPurchaseStartUrl('self_hosted_plan'), ); }); - it('keeps monitored-system counting guidance available on the plan surface', async () => { + it('keeps monitored-system counting guidance out of the plan surface', async () => { renderPanel(); - expect( - screen.getByText( - 'Pulse counts top-level monitored systems. Child resources underneath them are included.', - ), - ).toBeInTheDocument(); - expect(screen.getByRole('button', { name: 'View counting rules' })).toBeInTheDocument(); - expect( - screen.queryByText( - /a monitored system is a top-level monitored root such as a docker host, kubernetes cluster, proxmox node/i, - ), - ).not.toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'View counting rules' })).not.toBeInTheDocument(); + expect(screen.queryByRole('button', { name: 'Hide counting rules' })).not.toBeInTheDocument(); + expect(screen.queryByText('Monitoring capacity')).not.toBeInTheDocument(); }); it('navigates between plan and usage focus states through the billing subtabs', async () => { @@ -966,7 +960,7 @@ describe('ProLicensePanel', () => { expect(proLicensePlanSectionSource).toContain('getNoActiveProLicenseState'); expect(proLicensePlanSectionSource).toContain('getTrialEndedProLicenseNotice'); expect(proLicensePlanSectionSource).toContain('getInactiveProUpsellNotice'); - expect(proLicensePlanSectionSource).toContain('MonitoredSystemDefinitionDisclosure'); + expect(proLicensePlanSectionSource).not.toContain('MonitoredSystemDefinitionDisclosure'); expect(proLicensePlanSectionSource).toContain('trialStartTitle'); expect(proLicensePlanSectionSource).toContain('trialStartIdleActionLabel'); expect(proLicensePlanSectionSource).not.toContain( diff --git a/frontend-modern/src/components/Settings/selfHostedBillingPresentation.ts b/frontend-modern/src/components/Settings/selfHostedBillingPresentation.ts index d9779fb90..b0d095701 100644 --- a/frontend-modern/src/components/Settings/selfHostedBillingPresentation.ts +++ b/frontend-modern/src/components/Settings/selfHostedBillingPresentation.ts @@ -44,7 +44,8 @@ export const SELF_HOSTED_PRO_BILLING_PRESENTATION: SelfHostedProBillingPresentat planTabLabel: 'Plan', usageTabLabel: 'Usage', planSectionTitle: 'Plan', - planSectionDescription: 'Review your active plan, expiry, included limits, and paid capabilities.', + planSectionDescription: + 'Review your active plan, expiry, and the paid capabilities that come with it.', usageSectionTitle: 'Usage', hiddenShellTitle: 'Demo mode', hiddenShellDescription: 'Commercial settings are hidden for this session.', diff --git a/frontend-modern/src/utils/__tests__/monitoredSystemPresentation.test.ts b/frontend-modern/src/utils/__tests__/monitoredSystemPresentation.test.ts index 70f0f73b0..2548a3ea1 100644 --- a/frontend-modern/src/utils/__tests__/monitoredSystemPresentation.test.ts +++ b/frontend-modern/src/utils/__tests__/monitoredSystemPresentation.test.ts @@ -302,6 +302,22 @@ describe('monitoredSystemPresentation', () => { }); }); + it('does not build a monitored-system capacity section for uncapped plans', () => { + expect( + buildMonitoredSystemCapacitySectionModel(undefined, { + mode: 'unlimited', + urgency: 'ok', + current: 12, + limit: 0, + current_available: true, + available_slots: 0, + overage: 0, + blocks_new_systems: false, + existing_monitoring_continues: true, + }), + ).toBeNull(); + }); + it('centralizes monitored-system limit availability and capacity presentation', () => { const unavailableLimit = { current: 0, diff --git a/frontend-modern/src/utils/commercialBillingModel.ts b/frontend-modern/src/utils/commercialBillingModel.ts index 65cd344cc..3c7822ecf 100644 --- a/frontend-modern/src/utils/commercialBillingModel.ts +++ b/frontend-modern/src/utils/commercialBillingModel.ts @@ -60,20 +60,36 @@ const hasFiniteSelfHostedLimit = (value: string | number) => export const buildSelfHostedCommercialPlanModel = ( input: SelfHostedCommercialModelInput, ): CommercialPlanViewModel => ({ - summary: [ - { - label: 'Monitored Systems', - value: input.monitoredSystemsSummary, - }, - { - label: 'Capacity Status', - value: input.capacityStatusSummary, - }, - { - label: 'Plan Status', - value: input.statusLabel, - }, - ], + summary: + !input.monitoredSystemContinuity && !hasFiniteSelfHostedLimit(input.maxMonitoredSystems) + ? [ + { + label: 'Core Monitoring', + value: 'Unlimited', + }, + { + label: 'Plan Status', + value: input.statusLabel, + }, + { + label: 'Guests', + value: input.maxGuests, + }, + ] + : [ + { + label: 'Monitored Systems', + value: input.monitoredSystemsSummary, + }, + { + label: 'Capacity Status', + value: input.capacityStatusSummary, + }, + { + label: 'Plan Status', + value: input.statusLabel, + }, + ], details: [ { label: 'Tier', diff --git a/frontend-modern/src/utils/monitoredSystemPresentation.ts b/frontend-modern/src/utils/monitoredSystemPresentation.ts index 0101503c2..3af677b7c 100644 --- a/frontend-modern/src/utils/monitoredSystemPresentation.ts +++ b/frontend-modern/src/utils/monitoredSystemPresentation.ts @@ -467,13 +467,14 @@ export function buildMonitoredSystemCapacitySectionModel( if (!resolved) { return null; } + if (resolved.limit <= 0) { + return null; + } const includedValue = - resolved.mode === 'unlimited' - ? MONITORED_SYSTEM_LEDGER_PRESENTATION.unlimitedLimitLabel - : resolved.limit > 0 - ? String(resolved.limit) - : MONITORED_SYSTEM_LEDGER_PRESENTATION.remainingCapacityUnavailableLabel; + resolved.limit > 0 + ? String(resolved.limit) + : MONITORED_SYSTEM_LEDGER_PRESENTATION.remainingCapacityUnavailableLabel; const stats = [ { diff --git a/tests/integration/tests/55-self-hosted-upgrade-return.spec.ts b/tests/integration/tests/55-self-hosted-upgrade-return.spec.ts index e68d4f8b9..6e9bf6964 100644 --- a/tests/integration/tests/55-self-hosted-upgrade-return.spec.ts +++ b/tests/integration/tests/55-self-hosted-upgrade-return.spec.ts @@ -28,9 +28,7 @@ const MONITORED_SYSTEM_FEATURE = "self_hosted_plan"; const MONITORED_SYSTEM_ENTITLEMENTS = { capabilities: [], - limits: [ - { key: "max_monitored_systems", limit: 5, current: 16, state: "enforced" }, - ], + limits: [], subscription_state: "expired", upgrade_reasons: [], tier: "free", @@ -42,14 +40,11 @@ const MONITORED_SYSTEM_ENTITLEMENTS = { kubernetes_clusters: 0, }, has_migration_gap: false, - overflow_days_remaining: 14, }; const MONITORED_SYSTEM_RUNTIME_CAPABILITIES = { capabilities: [], - limits: [ - { key: "max_monitored_systems", limit: 5, current: 16, state: "enforced" }, - ], + limits: [], hosted_mode: false, max_history_days: 90, }; @@ -65,7 +60,6 @@ const MONITORED_SYSTEM_COMMERCIAL_POSTURE = { kubernetes_clusters: 0, }, has_migration_gap: false, - overflow_days_remaining: 14, }; const MONITORED_SYSTEM_LEDGER = { @@ -97,7 +91,7 @@ const MONITORED_SYSTEM_LEDGER = { }, ], total: 16, - limit: 5, + limit: 0, }; function fulfillJSON(route: Route, payload: unknown, status = 200) { @@ -365,12 +359,9 @@ async function openMonitoredSystemUpgradeArrival(page: Page) { "Community keeps core monitoring free. Compare Relay and Pro in Pulse Account, then return here with Pulse Pro activated automatically.", ), ).toBeVisible(); - await expect(page.getByRole("button", { name: "Hide counting rules" })).toBeVisible(); - await expect( - page.getByText( - "A monitored system is a top-level monitored root such as a Docker host, Kubernetes cluster, Proxmox node, standalone host, or TrueNAS system. Each root counts once no matter how Pulse collects it. Child resources like VMs, containers, pods, disks, backups, and services underneath that root are included.", - ), - ).toBeVisible(); + await expect(page.getByRole("button", { name: "Hide counting rules" })).toHaveCount(0); + await expect(page.getByRole("button", { name: "View counting rules" })).toHaveCount(0); + await expect(page.getByText("Monitoring capacity")).toHaveCount(0); } test.describe("Self-hosted upgrade return flow", () => {