diff --git a/docs/release-control/v6/internal/subsystems/cloud-paid.md b/docs/release-control/v6/internal/subsystems/cloud-paid.md index 85c082bc4..2f3d4b260 100644 --- a/docs/release-control/v6/internal/subsystems/cloud-paid.md +++ b/docs/release-control/v6/internal/subsystems/cloud-paid.md @@ -239,9 +239,11 @@ signal entirely, the settings surface should degrade to a safe customer-facing fallback instead of an unexplained placeholder glyph. That same billing support boundary now also owns the shared monitored-system presentation helper. `frontend-modern/src/utils/monitoredSystemPresentation.ts` -is the canonical owner for ledger labels, safe fallback summaries, and -source/type attribution wording, so the settings panel must consume that -helper instead of redefining customer-facing monitored-system copy inline. +is the canonical owner for monitored-system brief/disclosure copy, ledger +labels, safe fallback summaries, and source/type attribution wording, so the +settings panel, Pro usage section, and counting-rules disclosure must consume +that helper instead of redefining customer-facing monitored-system copy inline +or keeping a parallel copy in generic self-hosted plan utilities. Frontend billing/admin surfaces must not synthesize `plan_version` from subscription lifecycle state. When a hosted billing record lacks a plan label, the UI must preserve that absence instead of fabricating values like `active` diff --git a/frontend-modern/src/components/Commercial/MonitoredSystemDefinitionDisclosure.tsx b/frontend-modern/src/components/Commercial/MonitoredSystemDefinitionDisclosure.tsx index 03dbb8d7e..ee0dd7245 100644 --- a/frontend-modern/src/components/Commercial/MonitoredSystemDefinitionDisclosure.tsx +++ b/frontend-modern/src/components/Commercial/MonitoredSystemDefinitionDisclosure.tsx @@ -1,9 +1,8 @@ import { Show, createSignal, type Component } from 'solid-js'; import { - SELF_HOSTED_MONITORED_SYSTEMS_DEFINITION, - SELF_HOSTED_MONITORED_SYSTEMS_DISCLOSURE_LABEL, - SELF_HOSTED_MONITORED_SYSTEMS_HIDE_LABEL, -} from '@/utils/selfHostedPlans'; + getMonitoredSystemDisclosureDefinition, + getMonitoredSystemDisclosureToggleLabel, +} from '@/utils/monitoredSystemPresentation'; interface MonitoredSystemDefinitionDisclosureProps { summary?: string; @@ -34,12 +33,12 @@ export const MonitoredSystemDefinitionDisclosure: Component< aria-expanded={open()} onClick={() => setOpen((current) => !current)} > - {open() ? SELF_HOSTED_MONITORED_SYSTEMS_HIDE_LABEL : SELF_HOSTED_MONITORED_SYSTEMS_DISCLOSURE_LABEL} + {getMonitoredSystemDisclosureToggleLabel(open())}

- {SELF_HOSTED_MONITORED_SYSTEMS_DEFINITION} + {getMonitoredSystemDisclosureDefinition()}

diff --git a/frontend-modern/src/components/Settings/MonitoredSystemLedgerPanel.tsx b/frontend-modern/src/components/Settings/MonitoredSystemLedgerPanel.tsx index 627331160..b979f207a 100644 --- a/frontend-modern/src/components/Settings/MonitoredSystemLedgerPanel.tsx +++ b/frontend-modern/src/components/Settings/MonitoredSystemLedgerPanel.tsx @@ -25,14 +25,12 @@ import { PulseLogoIcon } from '@/components/icons/PulseLogoIcon'; import { formatMonitoredSystemLatestIncludedSignalSentence, formatMonitoredSystemSurfaceAttribution, + getMonitoredSystemLedgerDescription, getMonitoredSystemCountingDetailsToggleLabel, getMonitoredSystemExplanationFallbackSummary, getMonitoredSystemLedgerPresentation, getMonitoredSystemStatusFallbackSummary, } from '@/utils/monitoredSystemPresentation'; -import { - SELF_HOSTED_MONITORED_SYSTEM_LEDGER_DESCRIPTION, -} from '@/utils/selfHostedPlans'; import { MonitoredSystemDefinitionDisclosure } from '@/components/Commercial/MonitoredSystemDefinitionDisclosure'; interface MonitoredSystemLedgerPanelProps { @@ -289,7 +287,7 @@ export function MonitoredSystemLedgerPanel(props: MonitoredSystemLedgerPanelProp return ( } bodyClass="space-y-4" > diff --git a/frontend-modern/src/components/Settings/ProLicensePanel.tsx b/frontend-modern/src/components/Settings/ProLicensePanel.tsx index 528e98584..9b0f03e57 100644 --- a/frontend-modern/src/components/Settings/ProLicensePanel.tsx +++ b/frontend-modern/src/components/Settings/ProLicensePanel.tsx @@ -6,7 +6,7 @@ import { CommercialBillingShell, CommercialSection } from './CommercialBillingSe import { ProLicensePlanSection } from './ProLicensePlanSection'; import { SelfHostedCommercialActivationSection } from './SelfHostedCommercialActivationSection'; import { useProLicensePanelState } from './useProLicensePanelState'; -import { SELF_HOSTED_MONITORED_SYSTEMS_BRIEF } from '@/utils/selfHostedPlans'; +import { getMonitoredSystemBriefSummary } from '@/utils/monitoredSystemPresentation'; export const ProLicensePanel: Component = () => { const state = useProLicensePanelState(); @@ -52,7 +52,7 @@ export const ProLicensePanel: Component = () => { diff --git a/frontend-modern/src/components/Settings/__tests__/ProLicensePanel.test.tsx b/frontend-modern/src/components/Settings/__tests__/ProLicensePanel.test.tsx index 5d5663165..62a30884b 100644 --- a/frontend-modern/src/components/Settings/__tests__/ProLicensePanel.test.tsx +++ b/frontend-modern/src/components/Settings/__tests__/ProLicensePanel.test.tsx @@ -159,9 +159,17 @@ describe('ProLicensePanel', () => { expect( screen.queryByText(/a monitored system is a top-level machine or cluster/i), ).not.toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'View counting rules' })).toHaveAttribute( + 'aria-expanded', + 'false', + ); fireEvent.click(screen.getByRole('button', { name: 'View counting rules' })); + expect(screen.getByRole('button', { name: 'Hide counting rules' })).toHaveAttribute( + 'aria-expanded', + 'true', + ); expect( screen.getByText(/a monitored system is a top-level machine or cluster/i), ).toBeInTheDocument(); diff --git a/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts b/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts index cd976b97b..5e22b87a9 100644 --- a/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts +++ b/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts @@ -80,6 +80,7 @@ import relaySettingsPanelSource from '../RelaySettingsPanel.tsx?raw'; import relayPairingSectionSource from '../RelayPairingSection.tsx?raw'; import monitoredSystemLedgerPanelSource from '../MonitoredSystemLedgerPanel.tsx?raw'; import proLicensePanelSource from '../ProLicensePanel.tsx?raw'; +import monitoredSystemDefinitionDisclosureSource from '@/components/Commercial/MonitoredSystemDefinitionDisclosure.tsx?raw'; import proLicensePlanSectionSource from '../ProLicensePlanSection.tsx?raw'; import commercialBillingSectionsSource from '../CommercialBillingSections.tsx?raw'; import selfHostedCommercialActivationSectionSource from '../SelfHostedCommercialActivationSection.tsx?raw'; @@ -510,6 +511,15 @@ describe('Settings architecture guardrails', () => { expect(monitoredSystemLedgerPanelSource).toContain('getMonitoredSystemCountingDetailsToggleLabel'); expect(monitoredSystemLedgerPanelSource).not.toContain('No monitored systems counted.'); expect(monitoredSystemLedgerPanelSource).not.toContain('Current status'); + expect(monitoredSystemLedgerPanelSource).toContain('getMonitoredSystemLedgerDescription'); + expect(proLicensePanelSource).toContain('@/utils/monitoredSystemPresentation'); + expect(proLicensePanelSource).toContain('getMonitoredSystemBriefSummary'); + expect(monitoredSystemDefinitionDisclosureSource).toContain( + '@/utils/monitoredSystemPresentation', + ); + expect(monitoredSystemDefinitionDisclosureSource).toContain( + 'getMonitoredSystemDisclosureToggleLabel', + ); expect(proLicensePanelStateSource).toContain('buildSelfHostedCommercialPlanModel'); expect(proLicensePanelStateSource).toContain('loadLicenseStatus(true)'); expect(proLicensePlanSectionSource).toContain('CommercialStatGrid'); @@ -518,6 +528,12 @@ describe('Settings architecture guardrails', () => { expect(monitoredSystemPresentationSource).toContain( 'export function getMonitoredSystemCountingDetailsToggleLabel', ); + expect(monitoredSystemPresentationSource).toContain( + 'export function getMonitoredSystemBriefSummary', + ); + expect(monitoredSystemPresentationSource).toContain( + 'export function getMonitoredSystemDisclosureToggleLabel', + ); expect(selfHostedCommercialActivationSectionSource).toContain('License / Activation Key'); expect(selfHostedCommercialActivationSectionSource).toContain('Start 14-day Pro Trial'); expect(organizationBillingPanelSource).toContain('./CommercialBillingSections'); diff --git a/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts b/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts index 7b64a7ed6..dc7901c06 100644 --- a/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts +++ b/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts @@ -61,6 +61,7 @@ import trialBannerModelSource from '@/components/shared/trialBannerModel.ts?raw' import monitoredSystemLimitWarningBannerSource from '@/components/shared/MonitoredSystemLimitWarningBanner.tsx?raw'; import monitoredSystemLimitWarningBannerModelSource from '@/components/shared/monitoredSystemLimitWarningBannerModel.ts?raw'; import monitoredSystemLedgerPanelSource from '@/components/Settings/MonitoredSystemLedgerPanel.tsx?raw'; +import monitoredSystemDefinitionDisclosureSource from '@/components/Commercial/MonitoredSystemDefinitionDisclosure.tsx?raw'; import infrastructureSummaryTableSource from '@/components/shared/InfrastructureSummaryTable.tsx?raw'; import infrastructureSummaryTableRowSource from '@/components/shared/InfrastructureSummaryTableRow.tsx?raw'; import interactiveSparklineSource from '@/components/shared/InteractiveSparkline.tsx?raw'; @@ -3001,8 +3002,15 @@ describe('frontend resource type boundaries', () => { expect(monitoredSystemLedgerPanelSource).toContain( 'getMonitoredSystemCountingDetailsToggleLabel', ); + expect(monitoredSystemLedgerPanelSource).toContain('getMonitoredSystemLedgerDescription'); expect(monitoredSystemLedgerPanelSource).not.toContain('No monitored systems counted.'); expect(monitoredSystemLedgerPanelSource).not.toContain('Current status'); + expect(monitoredSystemDefinitionDisclosureSource).toContain( + '@/utils/monitoredSystemPresentation', + ); + expect(monitoredSystemDefinitionDisclosureSource).toContain( + 'getMonitoredSystemDisclosureToggleLabel', + ); expect(monitoredSystemPresentationSource).toContain( 'export function getMonitoredSystemLedgerPresentation', ); @@ -3012,6 +3020,12 @@ describe('frontend resource type boundaries', () => { expect(monitoredSystemPresentationSource).toContain( 'export function formatMonitoredSystemLatestIncludedSignalSentence', ); + expect(monitoredSystemPresentationSource).toContain( + 'export function getMonitoredSystemBriefSummary', + ); + expect(monitoredSystemPresentationSource).toContain( + 'export function getMonitoredSystemDisclosureToggleLabel', + ); expect(whatsNewModalSource).toContain('useWhatsNewModalState'); expect(whatsNewModalSource).toContain('WHATS_NEW_FEATURE_CARDS'); expect(whatsNewModalSource).not.toContain('createLocalStorageBooleanSignal'); diff --git a/frontend-modern/src/utils/__tests__/monitoredSystemPresentation.test.ts b/frontend-modern/src/utils/__tests__/monitoredSystemPresentation.test.ts index 5b26492fa..e87db39ce 100644 --- a/frontend-modern/src/utils/__tests__/monitoredSystemPresentation.test.ts +++ b/frontend-modern/src/utils/__tests__/monitoredSystemPresentation.test.ts @@ -1,10 +1,14 @@ import { describe, expect, it } from 'vitest'; import { + getMonitoredSystemBriefSummary, formatMonitoredSystemLatestIncludedSignalSentence, formatMonitoredSystemSurfaceAttribution, getMonitoredSystemCountingDetailsToggleLabel, + getMonitoredSystemDisclosureDefinition, + getMonitoredSystemDisclosureToggleLabel, getMonitoredSystemExplanationFallbackSummary, + getMonitoredSystemLedgerDescription, getMonitoredSystemLedgerPresentation, getMonitoredSystemSourceLabel, getMonitoredSystemStatusFallbackSummary, @@ -14,8 +18,15 @@ import { describe('monitoredSystemPresentation', () => { it('returns canonical ledger labels and fallback copy', () => { expect(getMonitoredSystemLedgerPresentation()).toEqual({ + briefSummary: 'Billing is based on monitored systems. Child resources are included.', sectionTitle: 'Monitored Systems', panelTitle: 'Monitored System Ledger', + disclosureButtonLabel: 'View counting rules', + disclosureHideLabel: 'Hide counting rules', + disclosureDefinition: + 'A monitored system is a top-level machine or cluster Pulse actively monitors. Each system counts once no matter how Pulse collects it. Child resources like VMs, containers, pods, disks, backups, and services are included.', + ledgerDescription: + 'Review the monitored systems currently counted against your Pulse Pro plan limit.', tableNameLabel: 'Name', tableStatusLabel: 'Status', tableLatestIncludedSignalLabel: 'Latest Included Signal', @@ -30,6 +41,15 @@ describe('monitoredSystemPresentation', () => { fallbackStatusSummary: 'Pulse cannot determine a canonical runtime status for this monitored system yet.', }); + expect(getMonitoredSystemBriefSummary()).toBe( + 'Billing is based on monitored systems. Child resources are included.', + ); + expect(getMonitoredSystemDisclosureToggleLabel(false)).toBe('View counting rules'); + expect(getMonitoredSystemDisclosureToggleLabel(true)).toBe('Hide counting rules'); + expect(getMonitoredSystemDisclosureDefinition()).toContain('top-level machine or cluster'); + expect(getMonitoredSystemLedgerDescription()).toBe( + 'Review the monitored systems currently counted against your Pulse Pro plan limit.', + ); expect(getMonitoredSystemCountingDetailsToggleLabel(false)).toBe('View counting details'); expect(getMonitoredSystemCountingDetailsToggleLabel(true)).toBe('Hide counting details'); expect(getMonitoredSystemExplanationFallbackSummary()).toBe( diff --git a/frontend-modern/src/utils/__tests__/selfHostedPlans.test.ts b/frontend-modern/src/utils/__tests__/selfHostedPlans.test.ts index 2118a2105..c27d16ae9 100644 --- a/frontend-modern/src/utils/__tests__/selfHostedPlans.test.ts +++ b/frontend-modern/src/utils/__tests__/selfHostedPlans.test.ts @@ -2,25 +2,11 @@ import { describe, expect, it } from 'vitest'; import { SELF_HOSTED_FEATURE_ROWS, - SELF_HOSTED_MONITORED_SYSTEMS_BRIEF, - SELF_HOSTED_MONITORED_SYSTEMS_DEFINITION, SELF_HOSTED_PLAN_BY_TIER, SELF_HOSTED_PLAN_DEFINITIONS, } from '../selfHostedPlans'; describe('selfHostedPlans', () => { - it('keeps the monitored-system wording concise by default and explicit on disclosure', () => { - expect(SELF_HOSTED_MONITORED_SYSTEMS_BRIEF).toBe( - 'Billing is based on monitored systems. Child resources are included.', - ); - expect(SELF_HOSTED_MONITORED_SYSTEMS_DEFINITION).toContain( - 'top-level machine or cluster', - ); - expect(SELF_HOSTED_MONITORED_SYSTEMS_DEFINITION).toContain('counts once'); - expect(SELF_HOSTED_MONITORED_SYSTEMS_DEFINITION).toContain('VMs'); - expect(SELF_HOSTED_MONITORED_SYSTEMS_DEFINITION).toContain('services'); - }); - it('keeps self-hosted plan limits aligned across tier cards and comparison rows', () => { expect(SELF_HOSTED_PLAN_DEFINITIONS.map((tier) => tier.name)).toEqual([ 'Community', diff --git a/frontend-modern/src/utils/monitoredSystemPresentation.ts b/frontend-modern/src/utils/monitoredSystemPresentation.ts index 72e558302..d7fd04aa7 100644 --- a/frontend-modern/src/utils/monitoredSystemPresentation.ts +++ b/frontend-modern/src/utils/monitoredSystemPresentation.ts @@ -9,8 +9,15 @@ const titleCaseWords = (value: string): string => .join(' '); const MONITORED_SYSTEM_LEDGER_PRESENTATION = { + briefSummary: 'Billing is based on monitored systems. Child resources are included.', sectionTitle: 'Monitored Systems', panelTitle: 'Monitored System Ledger', + disclosureButtonLabel: 'View counting rules', + disclosureHideLabel: 'Hide counting rules', + disclosureDefinition: + 'A monitored system is a top-level machine or cluster Pulse actively monitors. Each system counts once no matter how Pulse collects it. Child resources like VMs, containers, pods, disks, backups, and services are included.', + ledgerDescription: + 'Review the monitored systems currently counted against your Pulse Pro plan limit.', tableNameLabel: 'Name', tableStatusLabel: 'Status', tableLatestIncludedSignalLabel: 'Latest Included Signal', @@ -30,6 +37,24 @@ export function getMonitoredSystemLedgerPresentation() { return MONITORED_SYSTEM_LEDGER_PRESENTATION; } +export function getMonitoredSystemBriefSummary(): string { + return MONITORED_SYSTEM_LEDGER_PRESENTATION.briefSummary; +} + +export function getMonitoredSystemDisclosureToggleLabel(open: boolean): string { + return open + ? MONITORED_SYSTEM_LEDGER_PRESENTATION.disclosureHideLabel + : MONITORED_SYSTEM_LEDGER_PRESENTATION.disclosureButtonLabel; +} + +export function getMonitoredSystemDisclosureDefinition(): string { + return MONITORED_SYSTEM_LEDGER_PRESENTATION.disclosureDefinition; +} + +export function getMonitoredSystemLedgerDescription(): string { + return MONITORED_SYSTEM_LEDGER_PRESENTATION.ledgerDescription; +} + export function getMonitoredSystemCountingDetailsToggleLabel(expanded: boolean): string { return expanded ? MONITORED_SYSTEM_LEDGER_PRESENTATION.countingDetailsExpandedLabel diff --git a/frontend-modern/src/utils/selfHostedPlans.ts b/frontend-modern/src/utils/selfHostedPlans.ts index b3dadc04f..8849df6af 100644 --- a/frontend-modern/src/utils/selfHostedPlans.ts +++ b/frontend-modern/src/utils/selfHostedPlans.ts @@ -19,19 +19,6 @@ export interface SelfHostedFeatureRow { proPlus: boolean | string; } -export const SELF_HOSTED_MONITORED_SYSTEMS_BRIEF = - 'Billing is based on monitored systems. Child resources are included.'; - -export const SELF_HOSTED_MONITORED_SYSTEMS_DISCLOSURE_LABEL = 'View counting rules'; - -export const SELF_HOSTED_MONITORED_SYSTEMS_HIDE_LABEL = 'Hide counting rules'; - -export const SELF_HOSTED_MONITORED_SYSTEMS_DEFINITION = - 'A monitored system is a top-level machine or cluster Pulse actively monitors. Each system counts once no matter how Pulse collects it. Child resources like VMs, containers, pods, disks, backups, and services are included.'; - -export const SELF_HOSTED_MONITORED_SYSTEM_LEDGER_DESCRIPTION = - 'Review the monitored systems currently counted against your Pulse Pro plan limit.'; - export const SELF_HOSTED_PLAN_DEFINITIONS: readonly SelfHostedPlanDefinition[] = [ { tier: 'community',