@@ -337,19 +192,6 @@ export function MonitoredSystemLedgerPanel(props: MonitoredSystemLedgerPanelProp
{/* Loaded content */}
-
-
-
-
0}
fallback={{presentation.emptyState}
}
diff --git a/frontend-modern/src/components/Settings/OrganizationBillingPanel.tsx b/frontend-modern/src/components/Settings/OrganizationBillingPanel.tsx
index c9fb07455..48b6009cb 100644
--- a/frontend-modern/src/components/Settings/OrganizationBillingPanel.tsx
+++ b/frontend-modern/src/components/Settings/OrganizationBillingPanel.tsx
@@ -31,7 +31,7 @@ export const OrganizationBillingPanel: Component
}
>
@@ -55,9 +55,9 @@ export const OrganizationBillingPanel: Component
-
+
diff --git a/frontend-modern/src/components/Settings/ProLicensePanel.tsx b/frontend-modern/src/components/Settings/ProLicensePanel.tsx
index c7426f9fd..44f173713 100644
--- a/frontend-modern/src/components/Settings/ProLicensePanel.tsx
+++ b/frontend-modern/src/components/Settings/ProLicensePanel.tsx
@@ -1,13 +1,11 @@
import { Show, type Component } from 'solid-js';
import RefreshCw from 'lucide-solid/icons/refresh-cw';
-import { MonitoredSystemLedgerPanel } from './MonitoredSystemLedgerPanel';
import { CommercialBillingShell, CommercialSection } from './CommercialBillingSections';
import { ProLicensePlanSection } from './ProLicensePlanSection';
import { SelfHostedCommercialRecoverySection } from './SelfHostedCommercialRecoverySection';
import { useProLicensePanelState } from './useProLicensePanelState';
import { SELF_HOSTED_PRO_BILLING_PRESENTATION } from './selfHostedBillingPresentation';
import { Subtabs } from '@/components/shared/Subtabs';
-import { getMonitoredSystemBriefSummary } from '@/utils/monitoredSystemPresentation';
import {
presentationPolicyHidesCommercialSurfaces,
sessionPresentationPolicyResolved,
@@ -15,7 +13,6 @@ import {
import {
SELF_HOSTED_PRO_BILLING_PLAN_SECTION_ID,
SELF_HOSTED_PRO_BILLING_RECOVERY_SECTION_ID,
- SELF_HOSTED_PRO_BILLING_USAGE_SECTION_ID,
} from '@/utils/pricingHandoff';
const ProLicensePolicyLoadingPanel: Component = () => (
@@ -78,14 +75,6 @@ const ProLicensePanelContent: Component = () => {
value: 'plan',
label: SELF_HOSTED_PRO_BILLING_PRESENTATION.planTabLabel,
},
- ...(state.showUsageSection()
- ? [
- {
- value: 'usage',
- label: SELF_HOSTED_PRO_BILLING_PRESENTATION.usageTabLabel,
- },
- ]
- : []),
]}
/>
@@ -106,8 +95,6 @@ const ProLicensePanelContent: Component = () => {
grandfatheredPriceNotice={state.grandfatheredPriceNotice()}
hasLicenseDetails={state.hasLicenseDetails()}
loading={state.loading()}
- monitoredSystemCapacitySection={state.monitoredSystemCapacitySection()}
- monitoredSystemContinuityNotice={state.monitoredSystemContinuityNotice()}
onReload={() => void state.loadPanelData()}
planSelectionPrompt={state.planSelectionPrompt()}
planComparisonSummary={state.planComparisonSummary()}
@@ -131,22 +118,6 @@ const ProLicensePanelContent: Component = () => {
-
-
-
-
-
-
diff --git a/frontend-modern/src/components/Settings/ProLicensePlanSection.tsx b/frontend-modern/src/components/Settings/ProLicensePlanSection.tsx
index 6f0762f50..d371d3fb5 100644
--- a/frontend-modern/src/components/Settings/ProLicensePlanSection.tsx
+++ b/frontend-modern/src/components/Settings/ProLicensePlanSection.tsx
@@ -21,17 +21,6 @@ interface ActionNotice extends Notice {
actionDestination: UpgradeDestination;
}
-interface MonitoredSystemCapacitySection {
- stats: Array<{ label: string; value: string }>;
- statusMessage: string;
- detailMessage?: string;
- explanation?: {
- label: string;
- body: string;
- };
- reviewUsageDestination: UpgradeDestination;
-}
-
interface ProLicensePlanSectionProps {
activationSuccessSummary: {
title: string;
@@ -74,8 +63,6 @@ interface ProLicensePlanSectionProps {
grandfatheredPriceNotice: Notice | null;
hasLicenseDetails: boolean;
loading: boolean;
- monitoredSystemCapacitySection: MonitoredSystemCapacitySection | null;
- monitoredSystemContinuityNotice: Notice | null;
onReload: () => void;
planComparisonSummary: {
cards: Array<{
@@ -314,51 +301,6 @@ export const ProLicensePlanSection: Component = (pro
)}
-
- {(notice) => (
-
-
{notice().title}
-
{notice().body}
-
- )}
-
-
- {(section) => (
-
-
-
Monitored-system policy
-
{section().statusMessage}
-
- {(detail) => {detail()}
}
-
-
-
-
-
-
-
-
- {(explanation) => (
-
-
- {explanation().label}
-
- {explanation().body}
-
- )}
-
-
-
-
- Review monitored systems
-
-
-
- )}
-
Could not load license status
diff --git a/frontend-modern/src/components/Settings/Settings.tsx b/frontend-modern/src/components/Settings/Settings.tsx
index 795c9ae19..b0abc2f54 100644
--- a/frontend-modern/src/components/Settings/Settings.tsx
+++ b/frontend-modern/src/components/Settings/Settings.tsx
@@ -18,10 +18,7 @@ import { useSystemSettingsState } from './useSystemSettingsState';
import { useSettingsNavigation } from './useSettingsNavigation';
import { getSettingsLoadingState } from '@/utils/settingsShellPresentation';
-import {
- getRuntimeLimit,
- loadRuntimeCapabilities,
-} from '@/stores/license';
+import { getRuntimeLimit, loadRuntimeCapabilities } from '@/stores/license';
import { isPro } from '@/stores/licenseCommercial';
interface SettingsProps {
@@ -50,9 +47,7 @@ const Settings: Component
= (props) => {
searchQuery,
setSearchQuery,
} = useSettingsShellState({ activeTab });
- const organizationMonitoredSystemUsage = createMemo(
- () => getRuntimeLimit('max_monitored_systems')?.current ?? 0,
- );
+ const organizationMonitoredSystemUsage = createMemo(() => 0);
const organizationGuestUsage = createMemo(() => getRuntimeLimit('max_guests')?.current ?? 0);
const discoverySettings = useDiscoverySettingsState();
diff --git a/frontend-modern/src/components/Settings/__tests__/MonitoredSystemAdmissionPreview.test.tsx b/frontend-modern/src/components/Settings/__tests__/MonitoredSystemImpactPreview.test.tsx
similarity index 73%
rename from frontend-modern/src/components/Settings/__tests__/MonitoredSystemAdmissionPreview.test.tsx
rename to frontend-modern/src/components/Settings/__tests__/MonitoredSystemImpactPreview.test.tsx
index dd1f602f7..f36fcbb92 100644
--- a/frontend-modern/src/components/Settings/__tests__/MonitoredSystemAdmissionPreview.test.tsx
+++ b/frontend-modern/src/components/Settings/__tests__/MonitoredSystemImpactPreview.test.tsx
@@ -1,7 +1,7 @@
import { cleanup, render, screen } from '@solidjs/testing-library';
import { afterEach, describe, expect, it } from 'vitest';
-import { MonitoredSystemAdmissionPreview } from '../MonitoredSystemAdmissionPreview';
+import { MonitoredSystemImpactPreview } from '../MonitoredSystemImpactPreview';
import type { MonitoredSystemLedgerPreviewResponse } from '@/api/monitoredSystemLedger';
const buildPreview = (
@@ -10,8 +10,6 @@ const buildPreview = (
current_count: 4,
projected_count: 4,
additional_count: 0,
- limit: 10,
- would_exceed_limit: false,
effect: 'no_change',
current_systems: [],
projected_systems: [],
@@ -20,13 +18,13 @@ const buildPreview = (
...overrides,
});
-describe('MonitoredSystemAdmissionPreview', () => {
+describe('MonitoredSystemImpactPreview', () => {
afterEach(() => {
cleanup();
});
it('describes unchanged usage when a preview has no count impact', () => {
- render(() => );
+ render(() => );
expect(
screen.getByText('This change keeps monitored-system count unchanged'),
@@ -40,7 +38,7 @@ describe('MonitoredSystemAdmissionPreview', () => {
it('describes removed systems when a preview reduces monitored-system usage', () => {
render(() => (
- {
).toBeInTheDocument();
});
- it('describes continuity review failures without slash quota copy', () => {
+ it('describes added systems without capacity review copy', () => {
render(() => (
-
));
- expect(
- screen.getByText('This change needs continuity review before saving'),
- ).toBeInTheDocument();
+ expect(screen.getByText('This change adds monitored systems')).toBeInTheDocument();
expect(
screen.getByText(
- 'Pulse currently counts 9 monitored systems. Saving this change would bring the count to 11 monitored systems (+2), above the current verified baseline of 10 monitored systems.',
+ 'Pulse currently counts 9 monitored systems. Saving this change would bring the count to 11 monitored systems (+2).',
),
).toBeInTheDocument();
});
diff --git a/frontend-modern/src/components/Settings/__tests__/MonitoredSystemLedgerPanel.test.tsx b/frontend-modern/src/components/Settings/__tests__/MonitoredSystemLedgerPanel.test.tsx
index 62d061497..506f5e9f2 100644
--- a/frontend-modern/src/components/Settings/__tests__/MonitoredSystemLedgerPanel.test.tsx
+++ b/frontend-modern/src/components/Settings/__tests__/MonitoredSystemLedgerPanel.test.tsx
@@ -84,16 +84,7 @@ describe('MonitoredSystemLedgerPanel', () => {
it('hides monitored-system usage in demo mode without requesting the ledger', () => {
presentationPolicyHidesCommercialSurfacesMock.mockReturnValue(true);
- render(() => (
-
- ));
+ render(() => );
expect(explainMock).not.toHaveBeenCalled();
expect(screen.getByText('Monitored-system usage is hidden in demo mode')).toBeInTheDocument();
@@ -168,7 +159,6 @@ describe('MonitoredSystemLedgerPanel', () => {
},
],
total: 1,
- limit: 5,
}),
);
@@ -216,24 +206,13 @@ describe('MonitoredSystemLedgerPanel', () => {
}),
);
- render(() => (
-
- ));
+ render(() => );
await waitFor(() => {
expect(screen.getByText('Verifying monitored-system inventory')).toBeInTheDocument();
});
- expect(screen.getByText('Verifying…')).toBeInTheDocument();
+ expect(screen.getByText('Verifying')).toBeInTheDocument();
expect(
screen.getByText(
'Pulse is still collecting the first provider-owned inventory baseline. The monitored-system ledger will appear after that baseline completes.',
@@ -245,40 +224,6 @@ describe('MonitoredSystemLedgerPanel', () => {
).not.toBeInTheDocument();
});
- it('surfaces monitored-system continuity context from entitlements', async () => {
- explainMock.mockResolvedValue(
- explainResponse({
- systems: [],
- total: 7,
- limit: 12,
- }),
- );
-
- render(() => (
-
- ));
-
- await waitFor(() => {
- expect(screen.getByText('7 monitored systems')).toBeInTheDocument();
- });
-
- expect(screen.getByText('Legacy continuity')).toBeInTheDocument();
- expect(screen.getByText('Plan baseline')).toBeInTheDocument();
- expect(screen.getByText('Current baseline')).toBeInTheDocument();
- expect(screen.getByText('Observed legacy estate')).toBeInTheDocument();
- expect(screen.getByText('Verification')).toBeInTheDocument();
- expect(screen.getByText('5')).toBeInTheDocument();
- expect(screen.getAllByText('12')).toHaveLength(2);
- expect(screen.getByText('Pending')).toBeInTheDocument();
- });
-
it('renders ledger data on successful load', async () => {
explainMock.mockResolvedValue(
explainResponse({
@@ -356,7 +301,6 @@ describe('MonitoredSystemLedgerPanel', () => {
},
],
total: 2,
- limit: 10,
}),
);
@@ -372,7 +316,7 @@ describe('MonitoredSystemLedgerPanel', () => {
expect(screen.getAllByText('server-b (PBS Server via PBS)').length).toBeGreaterThan(0);
expect(
screen.getByText(
- 'Review the top-level monitored systems Pulse has identified for reporting, migration continuity, and support context.',
+ 'Review the top-level monitored systems Pulse has identified for reporting and support context.',
),
).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'View counting rules' })).toBeInTheDocument();
@@ -452,7 +396,6 @@ describe('MonitoredSystemLedgerPanel', () => {
},
],
total: 1,
- limit: 10,
}),
);
@@ -501,7 +444,6 @@ describe('MonitoredSystemLedgerPanel', () => {
},
],
total: 1,
- limit: 10,
}),
);
@@ -542,7 +484,6 @@ describe('MonitoredSystemLedgerPanel', () => {
},
],
total: 1,
- limit: 10,
}),
);
@@ -566,7 +507,6 @@ describe('MonitoredSystemLedgerPanel', () => {
explainResponse({
systems: [],
total: 2,
- limit: 5,
}),
);
diff --git a/frontend-modern/src/components/Settings/__tests__/OrganizationBillingPanel.test.tsx b/frontend-modern/src/components/Settings/__tests__/OrganizationBillingPanel.test.tsx
index 8c1279a95..32dd8b124 100644
--- a/frontend-modern/src/components/Settings/__tests__/OrganizationBillingPanel.test.tsx
+++ b/frontend-modern/src/components/Settings/__tests__/OrganizationBillingPanel.test.tsx
@@ -79,7 +79,6 @@ describe('OrganizationBillingPanel', () => {
is_lifetime: false,
days_remaining: 30,
features: [],
- max_monitored_systems: 12,
max_guests: 5,
email: 'owner@example.com',
expires_at: '2026-04-01T00:00:00Z',
@@ -92,18 +91,18 @@ describe('OrganizationBillingPanel', () => {
cleanup();
});
- it('renders organization usage from the canonical max_monitored_systems limit only', async () => {
+ it('renders monitored-system usage as not metered', async () => {
render(() => );
await waitFor(() => {
- expect(screen.getByText('5 / 12')).toBeInTheDocument();
+ expect(screen.getByText('5 / Not metered')).toBeInTheDocument();
});
expect(getStatusMock).toHaveBeenCalledTimes(1);
expect(screen.getByText('Plan')).toBeInTheDocument();
- expect(screen.getByText('Usage')).toBeInTheDocument();
+ expect(screen.getAllByText('Usage')).toHaveLength(2);
expect(screen.getByText('Cloud')).toBeInTheDocument();
- expect(screen.getByText('5 / 12')).toBeInTheDocument();
+ expect(screen.getByText('5 / Not metered')).toBeInTheDocument();
expect(screen.getByText('2 / 5')).toBeInTheDocument();
expect(errorMock).not.toHaveBeenCalled();
});
diff --git a/frontend-modern/src/components/Settings/__tests__/ProLicensePanel.test.tsx b/frontend-modern/src/components/Settings/__tests__/ProLicensePanel.test.tsx
index 54e8242f7..8d167fc1e 100644
--- a/frontend-modern/src/components/Settings/__tests__/ProLicensePanel.test.tsx
+++ b/frontend-modern/src/components/Settings/__tests__/ProLicensePanel.test.tsx
@@ -268,7 +268,6 @@ describe('ProLicensePanel', () => {
expect(screen.queryByText(/trial/i)).not.toBeInTheDocument();
expect(screen.queryByText(/monitoring room/i)).not.toBeInTheDocument();
expect(screen.queryByText(/monitoring capacity/i)).not.toBeInTheDocument();
- expect(screen.queryByText(/monitored-system capacity/i)).not.toBeInTheDocument();
});
it('does not surface a trial-ended banner for retired self-hosted trial state', async () => {
@@ -327,24 +326,6 @@ describe('ProLicensePanel', () => {
licensed_email: 'owner@example.com',
is_lifetime: false,
trial_eligible: false,
- monitored_system_continuity: {
- plan_limit: 12,
- grandfathered_floor: 23,
- effective_limit: 23,
- capture_pending: false,
- },
- monitored_system_capacity: {
- mode: 'at_limit_blocking_new',
- urgency: 'enforced',
- current: 23,
- limit: 23,
- current_available: true,
- available_slots: 0,
- overage: 0,
- reason: 'limit_reached',
- blocks_new_systems: true,
- existing_monitoring_continues: true,
- },
};
renderPanel();
@@ -579,8 +560,6 @@ describe('ProLicensePanel', () => {
key: 'max_monitored_systems',
limit: 10,
current: 0,
- current_available: false,
- current_unavailable_reason: 'supplemental_inventory_unsettled',
state: 'ok',
},
],
@@ -590,24 +569,6 @@ describe('ProLicensePanel', () => {
plan_version: 'legacy_migration_fallback',
licensed_email: 'owner@example.com',
trial_eligible: false,
- monitored_system_continuity: {
- plan_limit: 10,
- effective_limit: 10,
- capture_pending: true,
- },
- monitored_system_capacity: {
- mode: 'usage_unavailable',
- urgency: 'ok',
- current: 0,
- limit: 10,
- current_available: false,
- current_unavailable_reason: 'supplemental_inventory_unsettled',
- available_slots: 0,
- overage: 0,
- reason: 'legacy_migration_capture_pending',
- blocks_new_systems: false,
- existing_monitoring_continues: false,
- },
};
renderPanel();
@@ -641,7 +602,6 @@ describe('ProLicensePanel', () => {
key: 'max_monitored_systems',
limit: 10,
current: 23,
- current_available: true,
state: 'enforced',
},
],
@@ -651,23 +611,6 @@ describe('ProLicensePanel', () => {
plan_version: 'legacy_migration_fallback',
licensed_email: 'owner@example.com',
trial_eligible: false,
- monitored_system_continuity: {
- plan_limit: 10,
- effective_limit: 10,
- capture_pending: true,
- },
- monitored_system_capacity: {
- mode: 'over_limit_frozen',
- urgency: 'enforced',
- current: 23,
- limit: 10,
- current_available: true,
- available_slots: 0,
- overage: 13,
- reason: 'legacy_migration_capture_pending',
- blocks_new_systems: true,
- existing_monitoring_continues: true,
- },
};
renderPanel();
@@ -702,7 +645,6 @@ describe('ProLicensePanel', () => {
key: 'max_monitored_systems',
limit: 23,
current: 23,
- current_available: true,
state: 'enforced',
},
],
@@ -712,25 +654,6 @@ describe('ProLicensePanel', () => {
plan_version: 'legacy_migration_fallback',
licensed_email: 'owner@example.com',
trial_eligible: false,
- monitored_system_continuity: {
- plan_limit: 10,
- grandfathered_floor: 23,
- effective_limit: 23,
- capture_pending: false,
- captured_at: 1_768_000_000,
- },
- monitored_system_capacity: {
- mode: 'at_limit_blocking_new',
- urgency: 'enforced',
- current: 23,
- limit: 23,
- current_available: true,
- available_slots: 0,
- overage: 0,
- reason: 'limit_reached',
- blocks_new_systems: true,
- existing_monitoring_continues: true,
- },
};
renderPanel();
@@ -1177,7 +1100,7 @@ describe('ProLicensePanel', () => {
expect(proLicensePanelStateSource).toContain('useLocation');
expect(proLicensePanelStateSource).toContain('resolveSelfHostedBillingSection');
expect(proLicensePanelStateSource).toContain('getSelfHostedBillingPlanIntent');
- expect(proLicensePanelStateSource).toContain('getSelfHostedBillingUsageDetail');
+ expect(proLicensePanelStateSource).not.toContain('getSelfHostedBillingUsageDetail');
expect(proLicensePanelStateSource).toContain('const setActiveSection = (section: string) => {');
expect(proLicensePanelStateSource).toContain('loadLicenseEntitlements(true)');
expect(proLicensePanelStateSource).toContain('loadCommercialPosture(true)');
@@ -1229,6 +1152,6 @@ describe('ProLicensePanel', () => {
expect(selfHostedCommercialRecoverySectionSource).not.toContain('Start 14-day Pro Trial');
expect(selfHostedCommercialRecoverySectionSource).not.toContain('Legacy v5 license detected');
expect(proLicensePanelSource).toContain('id={SELF_HOSTED_PRO_BILLING_PLAN_SECTION_ID}');
- expect(proLicensePanelSource).toContain('id={SELF_HOSTED_PRO_BILLING_USAGE_SECTION_ID}');
+ expect(proLicensePanelSource).not.toContain('SELF_HOSTED_PRO_BILLING_USAGE_SECTION_ID');
});
});
diff --git a/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts b/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts
index 95f2259df..3c689bd70 100644
--- a/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts
+++ b/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts
@@ -35,7 +35,7 @@ import infrastructureSourcePickerSource from '../InfrastructureSourcePicker.tsx?
import infrastructureWorkspaceModelSource from '../infrastructureWorkspaceModel.ts?raw';
import agentProfileSettingsSource from '../agentProfileSettings.ts?raw';
import connectionsTableSource from '../ConnectionsTable.tsx?raw';
-import monitoredSystemAdmissionPreviewSource from '../MonitoredSystemAdmissionPreview.tsx?raw';
+import monitoredSystemImpactPreviewSource from '../MonitoredSystemImpactPreview.tsx?raw';
import connectionEditorSource from '../ConnectionEditor/ConnectionEditor.tsx?raw';
import addressProbeStepSource from '../ConnectionEditor/AddressProbeStep.tsx?raw';
import connectionEditorStateSource from '../ConnectionEditor/useConnectionEditor.ts?raw';
@@ -66,7 +66,6 @@ import infrastructureOnboardingPresentationSource from '../../../utils/infrastru
import selfHostedBillingPresentationSource from '../selfHostedBillingPresentation.ts?raw';
import systemSettingsPresentationSource from '../../../utils/systemSettingsPresentation.ts?raw';
import auditLogPresentationSource from '../../../utils/auditLogPresentation.ts?raw';
-import monitoredSystemLimitWarningBannerSource from '../../shared/MonitoredSystemLimitWarningBanner.tsx?raw';
const settingsRuntimeSources = import.meta.glob(['../*.tsx', '../ConnectionEditor/**/*.tsx'], {
query: '?raw',
@@ -224,7 +223,6 @@ describe('settings architecture guardrails', () => {
auditLogStateSource,
auditWebhookPanelSource,
auditWebhookStateSource,
- monitoredSystemLimitWarningBannerSource,
rbacFeatureGateSectionSource,
rbacFeatureGateStateSource,
reportingPanelSource,
@@ -246,9 +244,6 @@ describe('settings architecture guardrails', () => {
expect(rbacFeatureGateSectionSource).not.toContain('Custom Roles (Pro)');
expect(rbacFeatureGateSectionSource).not.toContain('Centralized Access Control (Pro)');
expect(ssoProvidersPanelSource).not.toContain('Add SAML (Pro)');
- expect(monitoredSystemLimitWarningBannerSource).not.toContain(
- 'Unlimited self-hosted monitoring',
- );
});
it('keeps SAML SSO available without a self-hosted Pro upsell boundary', () => {
@@ -446,18 +441,18 @@ describe('settings architecture guardrails', () => {
expect(infrastructureSourceManagerSource).toContain('Infrastructure systems');
expect(infrastructureSourceManagerSource).toContain('Run discovery');
expect(infrastructureSourceManagerSource).toContain('Discovery settings');
- expect(monitoredSystemAdmissionPreviewSource).toContain(
- 'getMonitoredSystemAdmissionPreviewTitle',
+ expect(monitoredSystemImpactPreviewSource).toContain(
+ 'getMonitoredSystemImpactPreviewTitle',
);
- expect(monitoredSystemAdmissionPreviewSource).toContain(
- 'formatMonitoredSystemAdmissionPreviewSummary',
+ expect(monitoredSystemImpactPreviewSource).toContain(
+ 'formatMonitoredSystemImpactPreviewSummary',
);
- expect(monitoredSystemAdmissionPreviewSource).not.toContain('Current usage');
- expect(monitoredSystemAdmissionPreviewSource).not.toContain(' / ');
- expect(monitoredSystemAdmissionPreviewSource).not.toContain(
- 'reuses your current monitored-system capacity',
+ expect(monitoredSystemImpactPreviewSource).not.toContain('Current usage');
+ expect(monitoredSystemImpactPreviewSource).not.toContain(' / ');
+ expect(monitoredSystemImpactPreviewSource).not.toContain(
+ 'reuses your current monitored-system allowance',
);
- expect(monitoredSystemAdmissionPreviewSource).not.toContain('frees monitored-system capacity');
+ expect(monitoredSystemImpactPreviewSource).not.toContain('frees monitored-system allowance');
expect(infrastructureSourceManagerSource).toContain(
"Add, discover, and verify the platform APIs plus Pulse Agent telemetry that make up Pulse's infrastructure model.",
);
diff --git a/frontend-modern/src/components/Settings/__tests__/useTrueNASSettingsPanelState.test.tsx b/frontend-modern/src/components/Settings/__tests__/useTrueNASSettingsPanelState.test.tsx
index 457ba3f07..54a3a2260 100644
--- a/frontend-modern/src/components/Settings/__tests__/useTrueNASSettingsPanelState.test.tsx
+++ b/frontend-modern/src/components/Settings/__tests__/useTrueNASSettingsPanelState.test.tsx
@@ -36,8 +36,6 @@ const safeTrueNASPreview = () => ({
current_count: 1,
projected_count: 1,
additional_count: 0,
- limit: 10,
- would_exceed_limit: false,
effect: 'attaches_existing',
current_systems: [],
projected_systems: [
@@ -227,8 +225,11 @@ describe('useTrueNASSettingsPanelState', () => {
expect(notificationStore.success).toHaveBeenCalledWith('TrueNAS connection successful');
});
- it('requires a successful monitored-system preview before saving', async () => {
- vi.mocked(TrueNASAPI.listConnections).mockResolvedValueOnce([] as never);
+ it('saves a connection without requiring a monitored-system preview', async () => {
+ vi.mocked(TrueNASAPI.listConnections)
+ .mockResolvedValueOnce([] as never)
+ .mockResolvedValueOnce([] as never);
+ vi.mocked(TrueNASAPI.createConnection).mockResolvedValueOnce({} as never);
const { result } = renderHook(() => useTrueNASSettingsPanelState());
await waitFor(() => expect(result.loading()).toBe(false));
@@ -239,13 +240,15 @@ describe('useTrueNASSettingsPanelState', () => {
apiKey: 'secret',
});
- expect(result.monitoredSystemAdmissionSaveBlocked()).toBe(true);
await result.saveCurrentForm();
- expect(TrueNASAPI.createConnection).not.toHaveBeenCalled();
- expect(notificationStore.error).toHaveBeenCalledWith(
- 'Pulse must preview the monitored-system impact for this platform connection before it can be saved.',
+ expect(TrueNASAPI.createConnection).toHaveBeenCalledWith(
+ expect.objectContaining({
+ host: 'tower.local',
+ apiKey: 'secret',
+ }),
);
+ expect(notificationStore.success).toHaveBeenCalledWith('TrueNAS connection added');
});
it('previews monitored-system impact through the canonical TrueNAS preview path', async () => {
@@ -254,8 +257,6 @@ describe('useTrueNASSettingsPanelState', () => {
current_count: 4,
projected_count: 4,
additional_count: 0,
- limit: 10,
- would_exceed_limit: false,
effect: 'attaches_existing',
current_systems: [
{
@@ -303,10 +304,13 @@ describe('useTrueNASSettingsPanelState', () => {
expect(result.monitoredSystemPreview()?.effect).toBe('attaches_existing');
});
- it('blocks save when monitored-system usage is temporarily unavailable during preview', async () => {
- vi.mocked(TrueNASAPI.listConnections).mockResolvedValueOnce([] as never);
+ it('allows save when monitored-system impact preview is temporarily unavailable', async () => {
+ vi.mocked(TrueNASAPI.listConnections)
+ .mockResolvedValueOnce([] as never)
+ .mockResolvedValueOnce([] as never);
+ vi.mocked(TrueNASAPI.createConnection).mockResolvedValueOnce({} as never);
vi.mocked(TrueNASAPI.previewConnection).mockRejectedValueOnce(
- Object.assign(new Error('Unable to verify monitored-system capacity right now'), {
+ Object.assign(new Error('Unable to verify monitored-system grouping right now'), {
status: 503,
code: 'monitored_system_usage_unavailable',
details: {
@@ -328,48 +332,25 @@ describe('useTrueNASSettingsPanelState', () => {
expect(preview).toBeNull();
expect(result.monitoredSystemPreview()).toBeNull();
- expect(result.monitoredSystemAdmissionSaveBlocked()).toBe(true);
expect(result.monitoredSystemPreviewErrorTitle()).toBe(
'Monitored-system verification is temporarily unavailable',
);
expect(result.monitoredSystemPreviewError()).toBe(
- 'Pulse is still settling provider-owned inventory for this platform connection, so the monitored-system check is not safe yet. Retry preview after the first baseline finishes.',
+ 'Pulse is still settling provider-owned inventory for this platform connection. You can still save the connection and review the impact after the first baseline finishes.',
);
await result.saveCurrentForm();
- expect(TrueNASAPI.createConnection).not.toHaveBeenCalled();
- expect(notificationStore.error).toHaveBeenLastCalledWith(
- 'Pulse is still settling provider-owned inventory for this platform connection, so the monitored-system check is not safe yet. Retry preview after the first baseline finishes.',
- );
+ expect(TrueNASAPI.createConnection).toHaveBeenCalled();
+ expect(notificationStore.success).toHaveBeenCalledWith('TrueNAS connection added');
});
- it('reuses the canonical monitored-system preview when a save is denied by the backend', async () => {
+ it('surfaces backend save errors without reopening retired cap-preview handling', async () => {
vi.mocked(TrueNASAPI.listConnections).mockResolvedValueOnce([] as never);
vi.mocked(TrueNASAPI.previewConnection).mockResolvedValueOnce(safeTrueNASPreview() as never);
vi.mocked(TrueNASAPI.createConnection).mockRejectedValueOnce(
- Object.assign(new Error('Monitored-system capacity reached (10/9)'), {
- status: 402,
- feature: 'max_monitored_systems',
- monitored_system_preview: {
- current_count: 9,
- projected_count: 10,
- additional_count: 1,
- limit: 9,
- would_exceed_limit: true,
- effect: 'creates_new',
- current_systems: [],
- projected_systems: [
- {
- name: 'tower',
- type: 'truenas-system',
- status: 'online',
- source: 'truenas',
- },
- ],
- current_system: null,
- projected_system: null,
- },
+ Object.assign(new Error('TrueNAS connection save failed'), {
+ status: 500,
}),
);
@@ -384,20 +365,19 @@ describe('useTrueNASSettingsPanelState', () => {
await result.previewCurrentForm();
await result.saveCurrentForm();
- expect(notificationStore.error).toHaveBeenCalledWith('Monitored-system capacity reached (10/9)');
+ expect(notificationStore.error).toHaveBeenCalledWith('TrueNAS connection save failed');
expect(result.monitoredSystemPreview()).toMatchObject({
- would_exceed_limit: true,
- projected_count: 10,
- effect: 'creates_new',
+ projected_count: 1,
+ effect: 'attaches_existing',
});
expect(result.dialogOpen()).toBe(true);
});
- it('surfaces monitored-system usage unavailability when save races a stale preview', async () => {
+ it('treats save-time monitored-system usage unavailability as an ordinary save error', async () => {
vi.mocked(TrueNASAPI.listConnections).mockResolvedValueOnce([] as never);
vi.mocked(TrueNASAPI.previewConnection).mockResolvedValueOnce(safeTrueNASPreview() as never);
vi.mocked(TrueNASAPI.createConnection).mockRejectedValueOnce(
- Object.assign(new Error('Unable to verify monitored-system capacity right now'), {
+ Object.assign(new Error('Unable to verify monitored-system grouping right now'), {
status: 503,
code: 'monitored_system_usage_unavailable',
details: {
@@ -417,13 +397,13 @@ describe('useTrueNASSettingsPanelState', () => {
await result.previewCurrentForm();
await result.saveCurrentForm();
- expect(result.monitoredSystemPreview()).toBeNull();
- expect(result.monitoredSystemAdmissionSaveBlocked()).toBe(true);
- expect(result.monitoredSystemPreviewError()).toBe(
- 'Pulse has settled provider-owned inventory and is rebuilding the canonical monitored-system view, so this connection cannot be saved yet. Retry preview in a moment.',
- );
+ expect(result.monitoredSystemPreview()).toMatchObject({
+ projected_count: 1,
+ effect: 'attaches_existing',
+ });
+ expect(result.monitoredSystemPreviewError()).toBeNull();
expect(notificationStore.error).toHaveBeenCalledWith(
- 'Pulse has settled provider-owned inventory and is rebuilding the canonical monitored-system view, so this connection cannot be saved yet. Retry preview in a moment.',
+ 'Unable to verify monitored-system grouping right now',
);
});
});
diff --git a/frontend-modern/src/components/Settings/__tests__/useVMwareSettingsPanelState.test.tsx b/frontend-modern/src/components/Settings/__tests__/useVMwareSettingsPanelState.test.tsx
index f8d3f9da3..f32934b94 100644
--- a/frontend-modern/src/components/Settings/__tests__/useVMwareSettingsPanelState.test.tsx
+++ b/frontend-modern/src/components/Settings/__tests__/useVMwareSettingsPanelState.test.tsx
@@ -35,8 +35,6 @@ const safeVMwarePreview = () => ({
current_count: 1,
projected_count: 1,
additional_count: 0,
- limit: 10,
- would_exceed_limit: false,
effect: 'attaches_existing',
current_systems: [],
projected_systems: [
@@ -224,8 +222,11 @@ describe('useVMwareSettingsPanelState', () => {
expect(notificationStore.success).toHaveBeenCalledWith('VMware connection successful');
});
- it('requires a successful monitored-system preview before saving', async () => {
- vi.mocked(VMwareAPI.listConnections).mockResolvedValueOnce([] as never);
+ it('saves a connection without requiring a monitored-system preview', async () => {
+ vi.mocked(VMwareAPI.listConnections)
+ .mockResolvedValueOnce([] as never)
+ .mockResolvedValueOnce([] as never);
+ vi.mocked(VMwareAPI.createConnection).mockResolvedValueOnce({} as never);
const { result } = renderHook(() => useVMwareSettingsPanelState());
await waitFor(() => expect(result.loading()).toBe(false));
@@ -237,13 +238,16 @@ describe('useVMwareSettingsPanelState', () => {
password: 'secret',
});
- expect(result.monitoredSystemAdmissionSaveBlocked()).toBe(true);
await result.saveCurrentForm();
- expect(VMwareAPI.createConnection).not.toHaveBeenCalled();
- expect(notificationStore.error).toHaveBeenCalledWith(
- 'Pulse must preview the monitored-system impact for this platform connection before it can be saved.',
+ expect(VMwareAPI.createConnection).toHaveBeenCalledWith(
+ expect.objectContaining({
+ host: 'vcsa.lab.local',
+ username: 'administrator@vsphere.local',
+ password: 'secret',
+ }),
);
+ expect(notificationStore.success).toHaveBeenCalledWith('VMware connection added');
});
it('surfaces categorized draft test guidance from structured backend failures', async () => {
@@ -420,8 +424,6 @@ describe('useVMwareSettingsPanelState', () => {
current_count: 1,
projected_count: 3,
additional_count: 2,
- limit: 5,
- would_exceed_limit: false,
effect: 'creates_multiple',
current_systems: [],
projected_systems: [
@@ -461,10 +463,13 @@ describe('useVMwareSettingsPanelState', () => {
expect(result.monitoredSystemPreview()?.projected_count).toBe(3);
});
- it('blocks save when monitored-system usage is temporarily unavailable during preview', async () => {
- vi.mocked(VMwareAPI.listConnections).mockResolvedValueOnce([] as never);
+ it('allows save when monitored-system impact preview is temporarily unavailable', async () => {
+ vi.mocked(VMwareAPI.listConnections)
+ .mockResolvedValueOnce([] as never)
+ .mockResolvedValueOnce([] as never);
+ vi.mocked(VMwareAPI.createConnection).mockResolvedValueOnce({} as never);
vi.mocked(VMwareAPI.previewConnection).mockRejectedValueOnce(
- Object.assign(new Error('Unable to verify monitored-system capacity right now'), {
+ Object.assign(new Error('Unable to verify monitored-system grouping right now'), {
status: 503,
code: 'monitored_system_usage_unavailable',
details: {
@@ -488,61 +493,25 @@ describe('useVMwareSettingsPanelState', () => {
expect(preview).toBeNull();
expect(result.connectionFailure()).toBeNull();
expect(result.monitoredSystemPreview()).toBeNull();
- expect(result.monitoredSystemAdmissionSaveBlocked()).toBe(true);
expect(result.monitoredSystemPreviewErrorTitle()).toBe(
'Monitored-system verification is temporarily unavailable',
);
expect(result.monitoredSystemPreviewError()).toBe(
- 'Pulse is still settling provider-owned inventory for this platform connection, so the monitored-system check is not safe yet. Retry preview after the first baseline finishes.',
+ 'Pulse is still settling provider-owned inventory for this platform connection. You can still save the connection and review the impact after the first baseline finishes.',
);
await result.saveCurrentForm();
- expect(VMwareAPI.createConnection).not.toHaveBeenCalled();
- expect(notificationStore.error).toHaveBeenLastCalledWith(
- 'Pulse is still settling provider-owned inventory for this platform connection, so the monitored-system check is not safe yet. Retry preview after the first baseline finishes.',
- );
+ expect(VMwareAPI.createConnection).toHaveBeenCalled();
+ expect(notificationStore.success).toHaveBeenCalledWith('VMware connection added');
});
- it('reuses the canonical monitored-system preview when a save is denied by the backend', async () => {
+ it('surfaces backend save errors without reopening retired cap-preview handling', async () => {
vi.mocked(VMwareAPI.listConnections).mockResolvedValueOnce([] as never);
vi.mocked(VMwareAPI.previewConnection).mockResolvedValueOnce(safeVMwarePreview() as never);
vi.mocked(VMwareAPI.createConnection).mockRejectedValueOnce(
- Object.assign(new Error('Monitored-system capacity reached (7/6)'), {
- status: 402,
- feature: 'max_monitored_systems',
- monitored_system_preview: {
- current_count: 5,
- projected_count: 7,
- additional_count: 2,
- limit: 6,
- would_exceed_limit: true,
- effect: 'mixed_existing_and_new',
- current_systems: [
- {
- name: 'esxi-01',
- type: 'agent',
- status: 'online',
- source: 'agent',
- },
- ],
- projected_systems: [
- {
- name: 'esxi-01',
- type: 'agent',
- status: 'online',
- source: 'vmware',
- },
- {
- name: 'esxi-02',
- type: 'agent',
- status: 'online',
- source: 'vmware',
- },
- ],
- current_system: null,
- projected_system: null,
- },
+ Object.assign(new Error('VMware connection save failed'), {
+ status: 500,
}),
);
@@ -558,20 +527,19 @@ describe('useVMwareSettingsPanelState', () => {
await result.previewCurrentForm();
await result.saveCurrentForm();
- expect(notificationStore.error).toHaveBeenCalledWith('Monitored-system capacity reached (7/6)');
+ expect(notificationStore.error).toHaveBeenCalledWith('VMware connection save failed');
expect(result.monitoredSystemPreview()).toMatchObject({
- would_exceed_limit: true,
- projected_count: 7,
- effect: 'mixed_existing_and_new',
+ projected_count: 1,
+ effect: 'attaches_existing',
});
expect(result.dialogOpen()).toBe(true);
});
- it('surfaces monitored-system usage unavailability when save races a stale preview', async () => {
+ it('treats save-time monitored-system usage unavailability as an ordinary save error', async () => {
vi.mocked(VMwareAPI.listConnections).mockResolvedValueOnce([] as never);
vi.mocked(VMwareAPI.previewConnection).mockResolvedValueOnce(safeVMwarePreview() as never);
vi.mocked(VMwareAPI.createConnection).mockRejectedValueOnce(
- Object.assign(new Error('Unable to verify monitored-system capacity right now'), {
+ Object.assign(new Error('Unable to verify monitored-system grouping right now'), {
status: 503,
code: 'monitored_system_usage_unavailable',
details: {
@@ -593,13 +561,13 @@ describe('useVMwareSettingsPanelState', () => {
await result.saveCurrentForm();
expect(result.connectionFailure()).toBeNull();
- expect(result.monitoredSystemPreview()).toBeNull();
- expect(result.monitoredSystemAdmissionSaveBlocked()).toBe(true);
- expect(result.monitoredSystemPreviewError()).toBe(
- 'Pulse has settled provider-owned inventory and is rebuilding the canonical monitored-system view, so this connection cannot be saved yet. Retry preview in a moment.',
- );
+ expect(result.monitoredSystemPreview()).toMatchObject({
+ projected_count: 1,
+ effect: 'attaches_existing',
+ });
+ expect(result.monitoredSystemPreviewError()).toBeNull();
expect(notificationStore.error).toHaveBeenCalledWith(
- 'Pulse has settled provider-owned inventory and is rebuilding the canonical monitored-system view, so this connection cannot be saved yet. Retry preview in a moment.',
+ 'Unable to verify monitored-system grouping right now',
);
});
});
diff --git a/frontend-modern/src/components/Settings/useProLicensePanelState.ts b/frontend-modern/src/components/Settings/useProLicensePanelState.ts
index cb31b5b82..928c9c785 100644
--- a/frontend-modern/src/components/Settings/useProLicensePanelState.ts
+++ b/frontend-modern/src/components/Settings/useProLicensePanelState.ts
@@ -18,12 +18,10 @@ import {
getSelfHostedPlanComparisonPresentation,
getSelfHostedCurrentPlanPresentation,
getLicenseFeatureLabel,
- getMonitoredSystemContinuityNotice,
getPurchaseActivationNotice,
getSelfHostedCurrentPlanStatusPresentation,
getLicenseSubscriptionStatusPresentation,
getLicenseTierLabel,
- getDisplayableMonitoredSystemContinuity,
isDisplayableLicenseFeature,
} from '@/utils/licensePresentation';
import {
@@ -31,10 +29,8 @@ import {
getSelfHostedBillingPlanDetail,
getSelfHostedBillingPlanIntent,
getSelfHostedBillingPurchaseArrival,
- getSelfHostedBillingUsageDetail,
resolveSelfHostedBillingSection,
resolveSelfHostedPurchaseStartDestination,
- SELF_HOSTED_PRO_BILLING_COUNTING_RULES_DETAIL,
SELF_HOSTED_PRO_BILLING_PLAN_SELECTION_INTENT,
SELF_HOSTED_PRO_BILLING_RECOVERY_DETAIL,
SELF_HOSTED_PRO_BILLING_PURCHASE_ACTIVATED,
@@ -47,23 +43,14 @@ import {
SELF_HOSTED_PRO_BILLING_PLAN_INTENT_QUERY_PARAM,
SELF_HOSTED_PRO_BILLING_PLAN_ROUTE,
SELF_HOSTED_PRO_BILLING_PURCHASE_QUERY_PARAM,
- SELF_HOSTED_PRO_BILLING_USAGE_HREF,
- SELF_HOSTED_PRO_BILLING_USAGE_ROUTE,
type SelfHostedBillingPlanIntent,
type SelfHostedBillingSection,
} from '@/utils/pricingHandoff';
import {
buildSelfHostedCommercialPlanModel,
LIFETIME_DAYS_REMAINING_LABEL,
- SELF_HOSTED_NOT_METERED_LABEL,
} from '@/utils/commercialBillingModel';
import { getSelfHostedPlanDefinitionForBillingTier } from '@/utils/selfHostedPlans';
-import {
- buildMonitoredSystemCapacitySectionModel,
- getMonitoredSystemLimitCapacityStatusSummary,
- getMonitoredSystemLimitUsageSummary,
- resolveMonitoredSystemCapacityStatus,
-} from '@/utils/monitoredSystemPresentation';
import { resolveUpgradeDestination, type UpgradeDestination } from '@/utils/upgradeNavigation';
import { SELF_HOSTED_PRO_BILLING_PRESENTATION } from './selfHostedBillingPresentation';
@@ -148,104 +135,30 @@ export function useProLicensePanelState() {
resolveSelfHostedBillingSection(location.pathname, location.search, location.hash),
);
- const limitStatus = (key: string) => entitlements()?.limits?.find((entry) => entry.key === key);
- const monitoredSystemContinuity = createMemo(() => entitlements()?.monitored_system_continuity);
const selfHostedPlanDefinition = createMemo(() =>
getSelfHostedPlanDefinitionForBillingTier(entitlements()?.tier),
);
- const usesCanonicalSelfHostedPlan = createMemo(() => Boolean(selfHostedPlanDefinition()));
- const monitoredSystemLimitStatus = createMemo(() =>
- usesCanonicalSelfHostedPlan() ? undefined : limitStatus('max_monitored_systems'),
- );
- const monitoredSystemCapacity = createMemo(() =>
- usesCanonicalSelfHostedPlan() ? undefined : entitlements()?.monitored_system_capacity,
- );
- const displayableMonitoredSystemContinuity = createMemo(() => {
- if (usesCanonicalSelfHostedPlan()) {
- return null;
- }
- return getDisplayableMonitoredSystemContinuity({
- continuity: monitoredSystemContinuity(),
- planVersion: entitlements()?.plan_version,
- isLifetime: entitlements()?.is_lifetime,
- subscriptionState: entitlements()?.subscription_state,
- });
- });
-
- const showUsageSection = createMemo(() => {
- if (!panelDataSettled()) {
- return true;
- }
- if (usesCanonicalSelfHostedPlan()) {
- return false;
- }
-
- const continuity = displayableMonitoredSystemContinuity();
- if (continuity) {
- if (continuity.capture_pending) {
- return true;
- }
- if (typeof continuity.plan_limit === 'number' && continuity.plan_limit > 0) {
- return true;
- }
- if (typeof continuity.effective_limit === 'number' && continuity.effective_limit > 0) {
- return true;
- }
- if (
- typeof continuity.grandfathered_floor === 'number' &&
- continuity.grandfathered_floor > 0
- ) {
- return true;
- }
- }
-
- const resolved = resolveMonitoredSystemCapacityStatus(
- monitoredSystemCapacity(),
- monitoredSystemLimitStatus(),
- );
- return Boolean(resolved && resolved.limit > 0);
- });
const activeSection = createMemo(() => {
- if (!panelDataSettled()) {
- return requestedSection();
- }
- if (requestedSection() === 'usage' && !showUsageSection()) {
- return 'plan';
- }
- return requestedSection();
+ return requestedSection() === 'usage' ? 'plan' : requestedSection();
});
createEffect(() => {
if (!panelDataSettled()) {
return;
}
- if (requestedSection() !== 'usage' || showUsageSection()) {
+ if (requestedSection() !== 'usage') {
return;
}
navigate(SELF_HOSTED_PRO_BILLING_PLAN_ROUTE, { replace: true, scroll: false });
});
const setActiveSection = (section: string) => {
- if (section !== 'plan' && section !== 'usage') {
+ if (section !== 'plan') {
return;
}
- if (section === 'usage' && panelDataSettled() && !showUsageSection()) {
- navigate(SELF_HOSTED_PRO_BILLING_PLAN_ROUTE, { replace: false, scroll: false });
- return;
- }
- const nextPath =
- section === 'usage'
- ? SELF_HOSTED_PRO_BILLING_USAGE_ROUTE
- : SELF_HOSTED_PRO_BILLING_PLAN_ROUTE;
- navigate(nextPath, { replace: false, scroll: false });
+ navigate(SELF_HOSTED_PRO_BILLING_PLAN_ROUTE, { replace: false, scroll: false });
};
- const showCountingRulesByDefault = createMemo(
- () =>
- activeSection() === 'usage' &&
- getSelfHostedBillingUsageDetail(location.search) ===
- SELF_HOSTED_PRO_BILLING_COUNTING_RULES_DETAIL,
- );
const showRecoveryByDefault = createMemo(
() =>
activeSection() === 'plan' &&
@@ -336,58 +249,7 @@ export function useProLicensePanelState() {
return segments.length === 3 && segments.every((segment) => segment.length > 0);
});
- const monitoredSystemUsageSummary = createMemo(() => {
- const limit = monitoredSystemLimitStatus();
- const capacity = monitoredSystemCapacity();
- if (!limit && !capacity && usesCanonicalSelfHostedPlan()) {
- return SELF_HOSTED_NOT_METERED_LABEL;
- }
- return getMonitoredSystemLimitUsageSummary(limit, capacity);
- });
- const monitoredSystemCapacityStatusSummary = createMemo(() => {
- const limit = monitoredSystemLimitStatus();
- const capacity = monitoredSystemCapacity();
- if (!limit && !capacity && usesCanonicalSelfHostedPlan()) {
- return SELF_HOSTED_NOT_METERED_LABEL;
- }
- return getMonitoredSystemLimitCapacityStatusSummary(limit, capacity);
- });
const currentRetailPlanDefinition = createMemo(() => selfHostedPlanDefinition());
- const monitoredSystemContinuityNotice = createMemo(() => {
- const continuity = displayableMonitoredSystemContinuity();
- if (!continuity) {
- return null;
- }
- return getMonitoredSystemContinuityNotice(
- continuity,
- monitoredSystemLimitStatus(),
- monitoredSystemCapacity(),
- {
- planVersion: entitlements()?.plan_version,
- isLifetime: entitlements()?.is_lifetime,
- subscriptionState: entitlements()?.subscription_state,
- },
- );
- });
- const monitoredSystemCapacitySection = createMemo(() => {
- const section = buildMonitoredSystemCapacitySectionModel(
- monitoredSystemLimitStatus(),
- monitoredSystemCapacity(),
- );
- if (!section) {
- return null;
- }
- return {
- ...section,
- reviewUsageDestination: resolveUpgradeDestination(SELF_HOSTED_PRO_BILLING_USAGE_HREF),
- };
- });
- const continuityCapturedAt = createMemo(() => {
- const capturedAt = displayableMonitoredSystemContinuity()?.captured_at;
- return typeof capturedAt === 'number' && capturedAt > 0
- ? formatUnixDate(capturedAt)
- : undefined;
- });
const purchaseActivationNotice = createMemo(() => {
if (purchaseActivationResult().trim().toLowerCase() === 'activated') {
@@ -466,16 +328,7 @@ export function useProLicensePanelState() {
planTerms: formattedPlanTerms() || undefined,
expires: displayedExpiry(),
daysRemaining: displayedDaysRemaining() ?? 'Unknown',
- monitoredSystemsSummary: monitoredSystemUsageSummary(),
- capacityStatusSummary: monitoredSystemCapacityStatusSummary(),
- maxMonitoredSystems:
- typeof monitoredSystemLimitStatus()?.limit === 'number' &&
- monitoredSystemLimitStatus()!.limit > 0
- ? monitoredSystemLimitStatus()!.limit
- : SELF_HOSTED_NOT_METERED_LABEL,
retailPlanDefinition: currentRetailPlanDefinition(),
- monitoredSystemContinuity: displayableMonitoredSystemContinuity() ?? null,
- continuityCapturedAt: continuityCapturedAt(),
}),
);
const currentPlanSummary = createMemo(() => {
@@ -573,11 +426,6 @@ export function useProLicensePanelState() {
commercialPlanModel,
currentPlanSummary,
planComparisonSummary,
- monitoredSystemCapacity,
- monitoredSystemLimitStatus,
- displayableMonitoredSystemContinuity,
- monitoredSystemCapacitySection,
- monitoredSystemContinuityNotice,
entitlements,
formattedFeatures,
grandfatheredPriceNotice,
@@ -593,8 +441,6 @@ export function useProLicensePanelState() {
purchaseActivationAction,
setActiveSection,
setLicenseKey,
- showUsageSection,
- showCountingRulesByDefault,
showRecoveryByDefault,
statusPresentation,
};
diff --git a/frontend-modern/src/components/Settings/useTrueNASSettingsPanelState.ts b/frontend-modern/src/components/Settings/useTrueNASSettingsPanelState.ts
index f39973527..1cf791ee2 100644
--- a/frontend-modern/src/components/Settings/useTrueNASSettingsPanelState.ts
+++ b/frontend-modern/src/components/Settings/useTrueNASSettingsPanelState.ts
@@ -5,7 +5,6 @@ import type { MonitoredSystemLedgerPreviewResponse } from '@/api/monitoredSystem
import {
apiErrorCode,
apiErrorDetailField,
- apiErrorMonitoredSystemPreview,
apiErrorStatus,
} from '@/api/responseUtils';
import {
@@ -15,9 +14,8 @@ import {
type TrueNASConnectionInput,
} from '@/api/truenas';
import {
- buildMonitoredSystemAdmissionPreviewUnavailableState,
- getMonitoredSystemAdmissionPreviewSaveBlockedMessage,
- type MonitoredSystemAdmissionPreviewUnavailableState,
+ buildMonitoredSystemImpactPreviewUnavailableState,
+ type MonitoredSystemImpactPreviewUnavailableState,
} from '@/utils/monitoredSystemPresentation';
type TrueNASAuthMode = 'apiKey' | 'userpass';
@@ -131,8 +129,8 @@ const getErrorMessage = (error: unknown, fallback: string): string => {
return fallback;
};
-const monitoredSystemAdmissionPreviewUnavailableStateFromError = (error: unknown) => {
- return buildMonitoredSystemAdmissionPreviewUnavailableState({
+const monitoredSystemImpactPreviewUnavailableStateFromError = (error: unknown) => {
+ return buildMonitoredSystemImpactPreviewUnavailableState({
code: apiErrorCode(error),
reason: apiErrorDetailField(error, 'reason'),
});
@@ -191,7 +189,7 @@ export function useTrueNASSettingsPanelState() {
const [monitoredSystemPreview, setMonitoredSystemPreview] =
createSignal(null);
const [monitoredSystemPreviewUnavailableState, setMonitoredSystemPreviewUnavailableState] =
- createSignal(null);
+ createSignal(null);
const [monitoredSystemPreviewGenericError, setMonitoredSystemPreviewGenericError] = createSignal<
string | null
>(null);
@@ -201,18 +199,6 @@ export function useTrueNASSettingsPanelState() {
const monitoredSystemPreviewError = createMemo(
() => monitoredSystemPreviewUnavailableState()?.message ?? monitoredSystemPreviewGenericError(),
);
- const monitoredSystemAdmissionSaveBlockedMessage = createMemo(() =>
- getMonitoredSystemAdmissionPreviewSaveBlockedMessage({
- preview: monitoredSystemPreview(),
- unavailableState: monitoredSystemPreviewUnavailableState(),
- error: monitoredSystemPreviewGenericError(),
- loading: previewing(),
- }),
- );
- const monitoredSystemAdmissionSaveBlocked = createMemo(
- () => monitoredSystemAdmissionSaveBlockedMessage() !== null,
- );
-
const editingConnection = createMemo(
() => connections().find((connection) => connection.id === editingConnectionId()) ?? null,
);
@@ -352,7 +338,7 @@ export function useTrueNASSettingsPanelState() {
setMonitoredSystemPreviewGenericError(null);
return preview;
} catch (error) {
- const unavailableState = monitoredSystemAdmissionPreviewUnavailableStateFromError(error);
+ const unavailableState = monitoredSystemImpactPreviewUnavailableStateFromError(error);
setMonitoredSystemPreview(null);
if (unavailableState) {
setMonitoredSystemPreviewUnavailableState(unavailableState);
@@ -371,11 +357,6 @@ export function useTrueNASSettingsPanelState() {
};
const saveCurrentForm = async () => {
- const blockedMessage = monitoredSystemAdmissionSaveBlockedMessage();
- if (blockedMessage) {
- notificationStore.error(blockedMessage);
- return;
- }
setSaving(true);
try {
const payload = buildConnectionInput(form());
@@ -389,22 +370,8 @@ export function useTrueNASSettingsPanelState() {
resetDialogState();
await loadConnections();
} catch (error) {
- const unavailableState = monitoredSystemAdmissionPreviewUnavailableStateFromError(error);
- const preview = apiErrorMonitoredSystemPreview(error);
- if (preview) {
- setMonitoredSystemPreview(preview);
- setMonitoredSystemPreviewUnavailableState(null);
- setMonitoredSystemPreviewGenericError(null);
- } else if (unavailableState) {
- setMonitoredSystemPreview(null);
- setMonitoredSystemPreviewUnavailableState(unavailableState);
- setMonitoredSystemPreviewGenericError(null);
- } else {
- setMonitoredSystemPreviewUnavailableState(null);
- }
- const message = unavailableState
- ? unavailableState.message
- : getErrorMessage(error, 'Failed to save TrueNAS connection');
+ setMonitoredSystemPreviewUnavailableState(null);
+ const message = getErrorMessage(error, 'Failed to save TrueNAS connection');
notificationStore.error(message);
logger.error('[TrueNAS Settings] Save failed', error);
} finally {
@@ -450,7 +417,6 @@ export function useTrueNASSettingsPanelState() {
openEditDialog,
pendingDeleteConnection,
previewCurrentForm,
- monitoredSystemAdmissionSaveBlocked,
monitoredSystemPreview,
monitoredSystemPreviewError,
monitoredSystemPreviewErrorTitle,
diff --git a/frontend-modern/src/components/Settings/useVMwareSettingsPanelState.ts b/frontend-modern/src/components/Settings/useVMwareSettingsPanelState.ts
index 94c9ce175..a7fff097b 100644
--- a/frontend-modern/src/components/Settings/useVMwareSettingsPanelState.ts
+++ b/frontend-modern/src/components/Settings/useVMwareSettingsPanelState.ts
@@ -9,15 +9,13 @@ import {
import {
apiErrorCode,
apiErrorDetailField,
- apiErrorMonitoredSystemPreview,
apiErrorStatus,
} from '@/api/responseUtils';
import { notificationStore } from '@/stores/notifications';
import { logger } from '@/utils/logger';
import {
- buildMonitoredSystemAdmissionPreviewUnavailableState,
- getMonitoredSystemAdmissionPreviewSaveBlockedMessage,
- type MonitoredSystemAdmissionPreviewUnavailableState,
+ buildMonitoredSystemImpactPreviewUnavailableState,
+ type MonitoredSystemImpactPreviewUnavailableState,
} from '@/utils/monitoredSystemPresentation';
import {
buildVMwareConnectionFailurePresentation,
@@ -99,8 +97,8 @@ const getErrorMessage = (error: unknown, fallback: string): string => {
const getVMwareErrorMessage = (error: unknown, fallback: string): string =>
apiErrorDetailField(error, 'error') ?? getErrorMessage(error, fallback);
-const monitoredSystemAdmissionPreviewUnavailableStateFromError = (error: unknown) => {
- return buildMonitoredSystemAdmissionPreviewUnavailableState({
+const monitoredSystemImpactPreviewUnavailableStateFromError = (error: unknown) => {
+ return buildMonitoredSystemImpactPreviewUnavailableState({
code: apiErrorCode(error),
reason: apiErrorDetailField(error, 'reason'),
});
@@ -147,7 +145,7 @@ export function useVMwareSettingsPanelState() {
const [monitoredSystemPreview, setMonitoredSystemPreview] =
createSignal(null);
const [monitoredSystemPreviewUnavailableState, setMonitoredSystemPreviewUnavailableState] =
- createSignal(null);
+ createSignal(null);
const [monitoredSystemPreviewGenericError, setMonitoredSystemPreviewGenericError] = createSignal<
string | null
>(null);
@@ -157,18 +155,6 @@ export function useVMwareSettingsPanelState() {
const monitoredSystemPreviewError = createMemo(
() => monitoredSystemPreviewUnavailableState()?.message ?? monitoredSystemPreviewGenericError(),
);
- const monitoredSystemAdmissionSaveBlockedMessage = createMemo(() =>
- getMonitoredSystemAdmissionPreviewSaveBlockedMessage({
- preview: monitoredSystemPreview(),
- unavailableState: monitoredSystemPreviewUnavailableState(),
- error: monitoredSystemPreviewGenericError(),
- loading: previewing(),
- }),
- );
- const monitoredSystemAdmissionSaveBlocked = createMemo(
- () => monitoredSystemAdmissionSaveBlockedMessage() !== null,
- );
-
const editingConnection = createMemo(
() => connections().find((connection) => connection.id === editingConnectionId()) ?? null,
);
@@ -328,7 +314,7 @@ export function useVMwareSettingsPanelState() {
setMonitoredSystemPreviewGenericError(null);
return preview;
} catch (error) {
- const unavailableState = monitoredSystemAdmissionPreviewUnavailableStateFromError(error);
+ const unavailableState = monitoredSystemImpactPreviewUnavailableStateFromError(error);
if (unavailableState) {
setMonitoredSystemPreview(null);
setMonitoredSystemPreviewUnavailableState(unavailableState);
@@ -359,11 +345,6 @@ export function useVMwareSettingsPanelState() {
};
const saveCurrentForm = async () => {
- const blockedMessage = monitoredSystemAdmissionSaveBlockedMessage();
- if (blockedMessage) {
- notificationStore.error(blockedMessage);
- return;
- }
setSaving(true);
try {
const payload = buildConnectionInput(form());
@@ -377,22 +358,8 @@ export function useVMwareSettingsPanelState() {
resetDialogState();
await loadConnections();
} catch (error) {
- const unavailableState = monitoredSystemAdmissionPreviewUnavailableStateFromError(error);
- const preview = apiErrorMonitoredSystemPreview(error);
- if (preview) {
- setMonitoredSystemPreview(preview);
- setMonitoredSystemPreviewUnavailableState(null);
- setMonitoredSystemPreviewGenericError(null);
- } else if (unavailableState) {
- setMonitoredSystemPreview(null);
- setMonitoredSystemPreviewUnavailableState(unavailableState);
- setMonitoredSystemPreviewGenericError(null);
- } else {
- setMonitoredSystemPreviewUnavailableState(null);
- }
- const message = unavailableState
- ? unavailableState.message
- : getErrorMessage(error, 'Failed to save VMware connection');
+ setMonitoredSystemPreviewUnavailableState(null);
+ const message = getErrorMessage(error, 'Failed to save VMware connection');
notificationStore.error(message);
logger.error('[VMware Settings] Save failed', error);
} finally {
@@ -439,7 +406,6 @@ export function useVMwareSettingsPanelState() {
openEditDialog,
pendingDeleteConnection,
previewCurrentForm,
- monitoredSystemAdmissionSaveBlocked,
monitoredSystemPreview,
monitoredSystemPreviewError,
monitoredSystemPreviewErrorTitle,
diff --git a/frontend-modern/src/components/shared/MonitoredSystemLimitWarningBanner.tsx b/frontend-modern/src/components/shared/MonitoredSystemLimitWarningBanner.tsx
deleted file mode 100644
index 66f5f57f7..000000000
--- a/frontend-modern/src/components/shared/MonitoredSystemLimitWarningBanner.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import { Component, Show } from 'solid-js';
-import {
- MONITORED_SYSTEM_LIMIT_INSTALL_COLLECTORS_LABEL,
- MONITORED_SYSTEM_LIMIT_REVIEW_POLICY_LABEL,
-} from './monitoredSystemLimitWarningBannerModel';
-import { UpgradeLink } from './UpgradeLink';
-import { useMonitoredSystemLimitWarningBannerState } from './useMonitoredSystemLimitWarningBannerState';
-
-export const MonitoredSystemLimitWarningBanner: Component = () => {
- const state = useMonitoredSystemLimitWarningBannerState();
-
- return (
-
-
-
- {state.monitoredSystemSummary()}
-
- {MONITORED_SYSTEM_LIMIT_REVIEW_POLICY_LABEL}
-
-
-
- {MONITORED_SYSTEM_LIMIT_INSTALL_COLLECTORS_LABEL}
-
-
-
-
-
- );
-};
diff --git a/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts b/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts
index 9fdbb7215..4dc5f08ab 100644
--- a/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts
+++ b/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts
@@ -81,8 +81,6 @@ import infrastructureSummaryTableRowSource from '@/components/shared/Infrastruct
import infrastructureSelectorModelSource from '@/components/shared/infrastructureSelectorModel.ts?raw';
import infrastructureSummaryTableModelSource from '@/components/shared/infrastructureSummaryTableModel.ts?raw';
import infrastructureSummaryTableStateSource from '@/components/shared/useInfrastructureSummaryTableState.ts?raw';
-import monitoredSystemLimitWarningBannerSource from '@/components/shared/MonitoredSystemLimitWarningBanner.tsx?raw';
-import monitoredSystemLimitWarningBannerModelSource from '@/components/shared/monitoredSystemLimitWarningBannerModel.ts?raw';
import selectionCardGroupSource from '@/components/shared/SelectionCardGroup.tsx?raw';
import selectionCardGroupModelSource from '@/components/shared/selectionCardGroupModel.ts?raw';
import summaryMetricCardSource from '@/components/shared/SummaryMetricCard.tsx?raw';
@@ -115,7 +113,6 @@ import searchTipsPopoverStateSource from '@/components/shared/useSearchTipsPopov
import tooltipStateSource from '@/components/shared/useTooltipState.ts?raw';
import upgradeNavigationHookSource from '@/components/shared/useUpgradeNavigation.ts?raw';
import interactiveSparklineStateSource from '@/components/shared/useInteractiveSparklineState.ts?raw';
-import monitoredSystemLimitWarningBannerStateSource from '@/components/shared/useMonitoredSystemLimitWarningBannerState.ts?raw';
import selectionCardGroupStateSource from '@/components/shared/useSelectionCardGroupState.ts?raw';
import webInterfaceUrlFieldSource from '@/components/shared/WebInterfaceUrlField.tsx?raw';
import webInterfaceUrlFieldModelSource from '@/components/shared/webInterfaceUrlFieldModel.ts?raw';
@@ -866,105 +863,6 @@ describe('shared primitive guardrails', () => {
expect(tlsVerificationWarningBannerSource).not.toContain('CalloutCard');
});
- it('keeps shared fleet limit banner copy on the monitored-system commercial term', () => {
- expect(monitoredSystemLimitWarningBannerModelSource).toContain(
- '@/utils/monitoredSystemPresentation',
- );
- expect(monitoredSystemLimitWarningBannerModelSource).toContain(
- 'formatMonitoredSystemLimitSummary',
- );
- expect(monitoredSystemLimitWarningBannerModelSource).toContain(
- 'formatMonitoredSystemMigrationMessage',
- );
- expect(monitoredSystemLimitWarningBannerModelSource).toContain(
- 'getMonitoredSystemLimitInstallCollectorsLabel',
- );
- expect(monitoredSystemLimitWarningBannerModelSource).not.toContain('Monitored systems:');
- expect(monitoredSystemLimitWarningBannerModelSource).not.toContain('monitored-system cap');
- expect(monitoredSystemLimitWarningBannerModelSource).not.toContain('Install v6 collectors');
- expect(monitoredSystemLimitWarningBannerModelSource).not.toContain('v6 Unified Agents:');
- expect(monitoredSystemLimitWarningBannerModelSource).not.toContain(
- 'do not count toward Unified Agents.',
- );
- expect(monitoredSystemLimitWarningBannerModelSource).not.toContain('Install v6 Unified Agents');
- });
-
- it('keeps monitored system limit warning banner on shell, runtime, and model owners', () => {
- expect(monitoredSystemLimitWarningBannerSource).toContain(
- 'useMonitoredSystemLimitWarningBannerState',
- );
- expect(monitoredSystemLimitWarningBannerSource).toContain('UpgradeLink');
- expect(monitoredSystemLimitWarningBannerSource).toContain(
- 'MONITORED_SYSTEM_LIMIT_REVIEW_POLICY_LABEL',
- );
- expect(monitoredSystemLimitWarningBannerSource).not.toContain('createEffect');
- expect(monitoredSystemLimitWarningBannerSource).not.toContain('createMemo');
- expect(monitoredSystemLimitWarningBannerSource).not.toContain('loadRuntimeCapabilities');
- expect(monitoredSystemLimitWarningBannerSource).not.toContain('trackUpgradeMetricEvent');
- expect(monitoredSystemLimitWarningBannerSource).not.toContain('legacyConnections()');
-
- expect(monitoredSystemLimitWarningBannerStateSource).toContain(
- 'export function useMonitoredSystemLimitWarningBannerState',
- );
- expect(monitoredSystemLimitWarningBannerStateSource).not.toContain('createEffect');
- expect(monitoredSystemLimitWarningBannerStateSource).toContain('createMemo');
- expect(monitoredSystemLimitWarningBannerStateSource).toContain('loadRuntimeCapabilities');
- expect(monitoredSystemLimitWarningBannerStateSource).toContain(
- 'getRuntimeMonitoredSystemCapacity',
- );
- expect(monitoredSystemLimitWarningBannerStateSource).toContain('isHostedModeEnabled');
- expect(monitoredSystemLimitWarningBannerStateSource).toContain(
- 'presentationPolicyHidesCommercialSurfaces',
- );
- expect(monitoredSystemLimitWarningBannerStateSource).toContain(
- 'presentationPolicyHidesUpgradePrompts',
- );
- expect(monitoredSystemLimitWarningBannerStateSource).not.toContain('trackUpgradeMetricEvent');
- expect(monitoredSystemLimitWarningBannerStateSource).toContain('hasMigrationGap');
- expect(monitoredSystemLimitWarningBannerStateSource).not.toContain(
- 'scopeSelfHostedBillingDestination',
- );
- expect(monitoredSystemLimitWarningBannerStateSource).not.toContain(
- 'SELF_HOSTED_PRO_BILLING_PLAN_SELECTION_INTENT',
- );
- expect(monitoredSystemLimitWarningBannerStateSource).not.toContain(
- 'SELF_HOSTED_PRO_BILLING_MONITORED_SYSTEM_INTENT',
- );
- expect(monitoredSystemLimitWarningBannerStateSource).toContain('reviewPolicyDestination');
- expect(monitoredSystemLimitWarningBannerStateSource).not.toContain('handleUpgradeClick');
- expect(monitoredSystemLimitWarningBannerStateSource).not.toContain("fetch('/api/health'");
-
- expect(monitoredSystemLimitWarningBannerModelSource).toContain(
- 'getMonitoredSystemBannerToneClass',
- );
- expect(monitoredSystemLimitWarningBannerModelSource).not.toContain(
- 'getMonitoredSystemLimitUpgradeLabel',
- );
- expect(monitoredSystemLimitWarningBannerModelSource).toContain(
- 'getMonitoredSystemLimitInstallCollectorsLabel',
- );
- expect(monitoredSystemLimitWarningBannerModelSource).toContain(
- 'type MonitoredSystemCapacityStatus',
- );
- expect(monitoredSystemLimitWarningBannerModelSource).toContain(
- 'isMonitoredSystemLimitUsageAvailable',
- );
- expect(monitoredSystemLimitWarningBannerModelSource).toContain(
- 'formatMonitoredSystemLimitSummary',
- );
- expect(monitoredSystemLimitWarningBannerModelSource).not.toContain(
- 'current_available !== false',
- );
- expect(monitoredSystemLimitWarningBannerModelSource).not.toContain('current / limit');
- expect(monitoredSystemLimitWarningBannerModelSource).not.toContain('0 remaining');
- expect(monitoredSystemLimitWarningBannerModelSource).toContain(
- 'SELF_HOSTED_PRO_BILLING_USAGE_HREF',
- );
- expect(monitoredSystemLimitWarningBannerModelSource).not.toContain(
- 'SELF_HOSTED_PRO_BILLING_PLAN_HREF',
- );
- });
-
it('keeps shared tag badges in the shared primitive boundary', () => {
expect(tagBadgesSource).toContain("from '@/components/shared/Tooltip'");
expect(guestRowSource).toContain("from '@/components/shared/TagBadges'");
diff --git a/frontend-modern/src/components/shared/__tests__/MonitoredSystemLimitWarningBanner.test.tsx b/frontend-modern/src/components/shared/__tests__/MonitoredSystemLimitWarningBanner.test.tsx
deleted file mode 100644
index 73aa7ea5e..000000000
--- a/frontend-modern/src/components/shared/__tests__/MonitoredSystemLimitWarningBanner.test.tsx
+++ /dev/null
@@ -1,334 +0,0 @@
-import { cleanup, render, screen } from '@solidjs/testing-library';
-import { Route, Router } from '@solidjs/router';
-import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
-import monitoredSystemLimitWarningBannerSource from '@/components/shared/MonitoredSystemLimitWarningBanner.tsx?raw';
-import monitoredSystemLimitWarningBannerModelSource from '@/components/shared/monitoredSystemLimitWarningBannerModel.ts?raw';
-import monitoredSystemLimitWarningBannerStateSource from '@/components/shared/useMonitoredSystemLimitWarningBannerState.ts?raw';
-
-type MockEntitlements = {
- overflow_days_remaining?: number;
-};
-const mockEntitlements = vi.hoisted(() =>
- vi.fn<() => MockEntitlements>(() => ({ overflow_days_remaining: undefined })),
-);
-type MockLimitRecord = {
- key: string;
- limit: number;
- current: number;
- current_available?: boolean;
- current_unavailable_reason?: string;
- state: string;
-};
-const mockGetLimit = vi.hoisted(() =>
- vi.fn<(key: string) => MockLimitRecord | undefined>(() => undefined),
-);
-const mockGetMonitoredSystemCapacity = vi.hoisted(() => vi.fn(() => undefined));
-const mockIsHostedModeEnabled = vi.hoisted(() => vi.fn(() => false));
-const mockHasMigrationGap = vi.hoisted(() => vi.fn(() => false));
-const mockLegacyConnections = vi.hoisted(() =>
- vi.fn(() => ({
- proxmox_nodes: 0,
- docker_hosts: 0,
- kubernetes_clusters: 0,
- })),
-);
-const mockLoadRuntimeLicenseStatus = vi.hoisted(() => vi.fn());
-const mockPresentationPolicyHidesCommercialSurfaces = vi.hoisted(() => vi.fn(() => false));
-const mockPresentationPolicyHidesUpgradePrompts = vi.hoisted(() => vi.fn(() => false));
-const mockGetUpgradeActionDestination = vi.hoisted(() => vi.fn());
-const mockGetUpgradeActionUrlOrFallback = vi.hoisted(() => vi.fn());
-
-vi.mock('@/stores/license', () => ({
- getRuntimeLimit: (key: string) => mockGetLimit(key),
- getRuntimeMonitoredSystemCapacity: () => mockGetMonitoredSystemCapacity(),
- isHostedModeEnabled: () => mockIsHostedModeEnabled(),
- loadRuntimeCapabilities: (force?: boolean) => mockLoadRuntimeLicenseStatus(force),
-}));
-
-vi.mock('@/stores/licenseCommercial', () => ({
- commercialOverflowDaysRemaining: () => mockEntitlements().overflow_days_remaining ?? null,
- getUpgradeActionDestination: (key: string) => mockGetUpgradeActionDestination(key),
- getUpgradeActionUrlOrFallback: (key: string) => mockGetUpgradeActionUrlOrFallback(key),
- hasMigrationGap: mockHasMigrationGap,
- legacyConnections: mockLegacyConnections,
-}));
-
-vi.mock('@/stores/sessionPresentationPolicy', () => ({
- presentationPolicyHidesCommercialSurfaces: () => mockPresentationPolicyHidesCommercialSurfaces(),
- presentationPolicyHidesUpgradePrompts: () => mockPresentationPolicyHidesUpgradePrompts(),
-}));
-
-describe('MonitoredSystemLimitWarningBanner', () => {
- beforeEach(() => {
- localStorage.clear();
- mockEntitlements.mockReturnValue({ overflow_days_remaining: undefined });
- mockGetLimit.mockReturnValue({
- key: 'max_monitored_systems',
- limit: 6,
- current: 0,
- state: 'ok',
- });
- mockGetMonitoredSystemCapacity.mockReset();
- mockGetMonitoredSystemCapacity.mockReturnValue(undefined);
- mockIsHostedModeEnabled.mockReset();
- mockIsHostedModeEnabled.mockReturnValue(false);
- mockHasMigrationGap.mockReturnValue(false);
- mockLegacyConnections.mockReturnValue({
- proxmox_nodes: 0,
- docker_hosts: 0,
- kubernetes_clusters: 0,
- });
- mockPresentationPolicyHidesCommercialSurfaces.mockReset();
- mockPresentationPolicyHidesCommercialSurfaces.mockReturnValue(false);
- mockPresentationPolicyHidesUpgradePrompts.mockReset();
- mockPresentationPolicyHidesUpgradePrompts.mockReturnValue(false);
- mockLoadRuntimeLicenseStatus.mockReset();
- mockLoadRuntimeLicenseStatus.mockResolvedValue(undefined);
- mockGetUpgradeActionDestination.mockReset();
- mockGetUpgradeActionUrlOrFallback.mockReset();
- mockGetUpgradeActionDestination.mockReturnValue({
- href: '/settings/system/billing',
- external: false,
- });
- mockGetUpgradeActionUrlOrFallback.mockReturnValue(
- '/settings/system/billing/usage?details=counting-rules',
- );
- });
-
- afterEach(() => {
- cleanup();
- vi.clearAllMocks();
- });
-
- it('keeps monitored system limit warning banner on shell, runtime, and model owners', () => {
- expect(monitoredSystemLimitWarningBannerSource).toContain(
- 'useMonitoredSystemLimitWarningBannerState',
- );
- expect(monitoredSystemLimitWarningBannerSource).toContain(
- 'MONITORED_SYSTEM_LIMIT_REVIEW_POLICY_LABEL',
- );
- expect(monitoredSystemLimitWarningBannerSource).not.toContain('createEffect');
- expect(monitoredSystemLimitWarningBannerSource).not.toContain('createMemo');
- expect(monitoredSystemLimitWarningBannerSource).not.toContain('loadRuntimeCapabilities');
- expect(monitoredSystemLimitWarningBannerSource).not.toContain('trackUpgradeMetricEvent');
- expect(monitoredSystemLimitWarningBannerSource).not.toContain('legacyConnections()');
-
- expect(monitoredSystemLimitWarningBannerStateSource).toContain(
- 'export function useMonitoredSystemLimitWarningBannerState',
- );
- expect(monitoredSystemLimitWarningBannerStateSource).not.toContain('createEffect');
- expect(monitoredSystemLimitWarningBannerStateSource).toContain('createMemo');
- expect(monitoredSystemLimitWarningBannerStateSource).toContain('loadRuntimeCapabilities');
- expect(monitoredSystemLimitWarningBannerStateSource).not.toContain('loadCommercialPosture');
- expect(monitoredSystemLimitWarningBannerStateSource).toContain(
- 'presentationPolicyHidesCommercialSurfaces',
- );
- expect(monitoredSystemLimitWarningBannerStateSource).toContain('isHostedModeEnabled');
- expect(monitoredSystemLimitWarningBannerStateSource).toContain(
- 'presentationPolicyHidesUpgradePrompts',
- );
- expect(monitoredSystemLimitWarningBannerStateSource).not.toContain('trackUpgradeMetricEvent');
- expect(monitoredSystemLimitWarningBannerStateSource).toContain('hasMigrationGap');
- expect(monitoredSystemLimitWarningBannerStateSource).not.toContain(
- 'scopeSelfHostedBillingDestination',
- );
- expect(monitoredSystemLimitWarningBannerStateSource).not.toContain(
- 'SELF_HOSTED_PRO_BILLING_PLAN_SELECTION_INTENT',
- );
- expect(monitoredSystemLimitWarningBannerStateSource).toContain('reviewPolicyDestination');
- expect(monitoredSystemLimitWarningBannerStateSource).not.toContain('handleUpgradeClick');
-
- expect(monitoredSystemLimitWarningBannerModelSource).toContain(
- '@/utils/monitoredSystemPresentation',
- );
- expect(monitoredSystemLimitWarningBannerModelSource).toContain(
- 'getMonitoredSystemBannerToneClass',
- );
- expect(monitoredSystemLimitWarningBannerModelSource).not.toContain(
- 'getMonitoredSystemLimitUpgradeLabel',
- );
- expect(monitoredSystemLimitWarningBannerModelSource).toContain(
- 'getMonitoredSystemLimitInstallCollectorsLabel',
- );
- expect(monitoredSystemLimitWarningBannerModelSource).toContain(
- 'isMonitoredSystemLimitUsageAvailable',
- );
- expect(monitoredSystemLimitWarningBannerModelSource).not.toContain(
- 'current_available !== false',
- );
- expect(monitoredSystemLimitWarningBannerModelSource).not.toContain('Upgrade to add more');
- expect(monitoredSystemLimitWarningBannerModelSource).not.toContain('Install v6 collectors');
- });
-
- it('stays hidden for non-urgent pure v6 installs', async () => {
- const mod = await import('../MonitoredSystemLimitWarningBanner');
- render(() => (
-
-
-
- ));
-
- expect(mockLoadRuntimeLicenseStatus).toHaveBeenCalled();
- expect(screen.queryByText(/Monitored systems:/i)).not.toBeInTheDocument();
- });
-
- it('shows migration guidance when legacy connections exist', async () => {
- mockHasMigrationGap.mockReturnValue(true);
- mockLegacyConnections.mockReturnValue({
- proxmox_nodes: 2,
- docker_hosts: 1,
- kubernetes_clusters: 0,
- });
-
- const mod = await import('../MonitoredSystemLimitWarningBanner');
- render(() => (
-
-
-
- ));
-
- expect(screen.queryByText(/Monitored systems:/i)).not.toBeInTheDocument();
- });
-
- it('keeps urgent limit warnings visible even without migration gap', async () => {
- mockIsHostedModeEnabled.mockReturnValue(true);
- mockGetLimit.mockReturnValue({
- key: 'max_monitored_systems',
- limit: 6,
- current: 5,
- state: 'warning',
- });
-
- const mod = await import('../MonitoredSystemLimitWarningBanner');
- render(() => (
-
-
-
- ));
-
- expect(screen.getByText('5 monitored systems.')).toBeInTheDocument();
- expect(screen.getByText('Review continuity')).toHaveAttribute(
- 'href',
- '/settings/system/billing/usage',
- );
- expect(screen.queryByText('Review options')).not.toBeInTheDocument();
- expect(screen.queryByText('Install v6 collectors')).not.toBeInTheDocument();
- });
-
- it('stays hidden while canonical monitored-system usage is unavailable', async () => {
- mockGetLimit.mockReturnValue({
- key: 'max_monitored_systems',
- limit: 6,
- current: 6,
- current_available: false,
- current_unavailable_reason: 'supplemental_inventory_unsettled',
- state: 'enforced',
- });
-
- const mod = await import('../MonitoredSystemLimitWarningBanner');
- render(() => (
-
-
-
- ));
-
- expect(screen.queryByText(/Monitored systems:/i)).not.toBeInTheDocument();
- expect(screen.queryByText('Review options')).not.toBeInTheDocument();
- });
-
- it('keeps urgent limit warnings visible with migration context', async () => {
- mockIsHostedModeEnabled.mockReturnValue(true);
- mockHasMigrationGap.mockReturnValue(true);
- mockEntitlements.mockReturnValue({ overflow_days_remaining: 14 });
- mockGetLimit.mockReturnValue({
- key: 'max_monitored_systems',
- limit: 6,
- current: 5,
- state: 'warning',
- });
- mockLegacyConnections.mockReturnValue({
- proxmox_nodes: 2,
- docker_hosts: 1,
- kubernetes_clusters: 0,
- });
-
- const mod = await import('../MonitoredSystemLimitWarningBanner');
- render(() => (
-
-
-
- ));
-
- expect(screen.getByText('5 monitored systems.')).toBeInTheDocument();
- expect(screen.getByText('Install v6 collectors')).toHaveAttribute('href', '/settings');
- expect(screen.getByText('Review continuity')).toHaveAttribute(
- 'href',
- '/settings/system/billing/usage',
- );
- expect(screen.queryByText('Review options')).not.toBeInTheDocument();
- });
-
- it('stays hidden in demo mode even when usage is urgent', async () => {
- mockIsHostedModeEnabled.mockReturnValue(true);
- mockPresentationPolicyHidesCommercialSurfaces.mockReturnValue(true);
- mockGetLimit.mockReturnValue({
- key: 'max_monitored_systems',
- limit: 6,
- current: 5,
- state: 'warning',
- });
-
- const mod = await import('../MonitoredSystemLimitWarningBanner');
- render(() => (
-
-
-
- ));
-
- expect(
- screen.queryByText('5 monitored systems.'),
- ).not.toBeInTheDocument();
- });
-
- it('stays hidden when self-hosted upgrade prompts are suppressed', async () => {
- mockIsHostedModeEnabled.mockReturnValue(true);
- mockPresentationPolicyHidesUpgradePrompts.mockReturnValue(true);
- mockGetLimit.mockReturnValue({
- key: 'max_monitored_systems',
- limit: 6,
- current: 5,
- state: 'warning',
- });
-
- const mod = await import('../MonitoredSystemLimitWarningBanner');
- render(() => (
-
-
-
- ));
-
- expect(
- screen.queryByText('5 monitored systems.'),
- ).not.toBeInTheDocument();
- });
-
- it('stays hidden for self-hosted installs even when stale continuity metadata is urgent', async () => {
- mockGetLimit.mockReturnValue({
- key: 'max_monitored_systems',
- limit: 6,
- current: 5,
- state: 'warning',
- });
-
- const mod = await import('../MonitoredSystemLimitWarningBanner');
- render(() => (
-
-
-
- ));
-
- expect(
- screen.queryByText('5 monitored systems.'),
- ).not.toBeInTheDocument();
- });
-});
diff --git a/frontend-modern/src/components/shared/monitoredSystemLimitWarningBannerModel.ts b/frontend-modern/src/components/shared/monitoredSystemLimitWarningBannerModel.ts
deleted file mode 100644
index dbfea248a..000000000
--- a/frontend-modern/src/components/shared/monitoredSystemLimitWarningBannerModel.ts
+++ /dev/null
@@ -1,98 +0,0 @@
-import {
- formatMonitoredSystemLegacyConnectionBreakdown,
- formatMonitoredSystemLimitSummary,
- formatMonitoredSystemMigrationMessage,
- formatMonitoredSystemOverflowSummary,
- getMonitoredSystemLimitInstallCollectorsLabel,
- getMonitoredSystemLimitReviewPolicyLabel,
- isMonitoredSystemLimitUrgent as isCanonicalMonitoredSystemLimitUrgent,
- isMonitoredSystemLimitUsageAvailable as isCanonicalMonitoredSystemLimitUsageAvailable,
- type MonitoredSystemCapacityStatus,
- type MonitoredSystemLimitUsageStatus,
- type MonitoredSystemLegacyConnectionCounts,
-} from '@/utils/monitoredSystemPresentation';
-import { SELF_HOSTED_PRO_BILLING_USAGE_HREF } from '@/utils/pricingHandoff';
-
-type LimitState = MonitoredSystemLimitUsageStatus & {
- current: number;
- limit: number;
- state?: string;
-};
-
-export const MONITORED_SYSTEM_LIMIT_KEY = 'max_monitored_systems';
-export const MONITORED_SYSTEM_LIMIT_REVIEW_POLICY_HREF = SELF_HOSTED_PRO_BILLING_USAGE_HREF;
-export const MONITORED_SYSTEM_LIMIT_INSTALL_COLLECTORS_HREF = '/settings';
-export const MONITORED_SYSTEM_LIMIT_REVIEW_POLICY_LABEL =
- getMonitoredSystemLimitReviewPolicyLabel();
-export const MONITORED_SYSTEM_LIMIT_INSTALL_COLLECTORS_LABEL =
- getMonitoredSystemLimitInstallCollectorsLabel();
-
-export function isMonitoredSystemLimitUsageAvailable(limit: LimitState | undefined): boolean {
- return isCanonicalMonitoredSystemLimitUsageAvailable(limit);
-}
-
-export function isMonitoredSystemLimitUrgent(
- limit: LimitState | undefined,
- capacity?: MonitoredSystemCapacityStatus | null,
-): boolean {
- return isCanonicalMonitoredSystemLimitUrgent(limit, capacity);
-}
-
-export function shouldShowMonitoredSystemLimitBanner(
- limit: LimitState | undefined,
- capacity?: MonitoredSystemCapacityStatus | null,
-): boolean {
- return Boolean(limit || capacity) && isMonitoredSystemLimitUrgent(limit, capacity);
-}
-
-export function getMonitoredSystemSummary(
- limit: LimitState | undefined,
- capacity?: MonitoredSystemCapacityStatus | null,
-): string {
- if (!limit && !capacity) return '';
- if (limit && !isMonitoredSystemLimitUsageAvailable(limit)) return '';
- return formatMonitoredSystemLimitSummary(
- limit ?? {
- current: capacity?.current ?? 0,
- limit: capacity?.limit ?? 0,
- current_available: capacity?.current_available ?? true,
- current_unavailable_reason: capacity?.current_unavailable_reason,
- state: capacity?.urgency ?? 'ok',
- },
- capacity,
- );
-}
-
-export function getMonitoredSystemLegacyConnectionTotal(
- counts: MonitoredSystemLegacyConnectionCounts,
-): number {
- return counts.proxmox_nodes + counts.docker_hosts + counts.kubernetes_clusters;
-}
-
-export function getMonitoredSystemLegacyBreakdown(
- counts: MonitoredSystemLegacyConnectionCounts,
-): string {
- return formatMonitoredSystemLegacyConnectionBreakdown(counts);
-}
-
-export function getMonitoredSystemMigrationMessage(
- counts: MonitoredSystemLegacyConnectionCounts,
-): string {
- return formatMonitoredSystemMigrationMessage(counts);
-}
-
-export function getMonitoredSystemOverflowSummary(
- daysRemaining: number | null | undefined,
-): string {
- return formatMonitoredSystemOverflowSummary(daysRemaining ?? undefined);
-}
-
-export function getMonitoredSystemBannerToneClass(isUrgent: boolean): string {
- return isUrgent
- ? 'border-amber-200 bg-amber-50 text-amber-900 dark:border-amber-900 dark:bg-amber-900 dark:text-amber-100'
- : 'border-sky-200 bg-sky-50 text-sky-950 dark:border-sky-900 dark:bg-sky-950 dark:text-sky-100';
-}
-
-export function getMonitoredSystemMigrationTextClass(isUrgent: boolean): string {
- return isUrgent ? 'text-amber-800 dark:text-amber-200' : 'text-sky-800 dark:text-sky-200';
-}
diff --git a/frontend-modern/src/components/shared/useMonitoredSystemLimitWarningBannerState.ts b/frontend-modern/src/components/shared/useMonitoredSystemLimitWarningBannerState.ts
deleted file mode 100644
index d24a53c32..000000000
--- a/frontend-modern/src/components/shared/useMonitoredSystemLimitWarningBannerState.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-import { createMemo, onMount } from 'solid-js';
-import {
- presentationPolicyHidesCommercialSurfaces,
- presentationPolicyHidesUpgradePrompts,
-} from '@/stores/sessionPresentationPolicy';
-import {
- getRuntimeMonitoredSystemCapacity,
- getRuntimeLimit,
- isHostedModeEnabled,
- loadRuntimeCapabilities,
-} from '@/stores/license';
-import { hasMigrationGap } from '@/stores/licenseCommercial';
-import { resolveUpgradeDestination } from '@/utils/upgradeNavigation';
-import {
- getMonitoredSystemBannerToneClass,
- getMonitoredSystemSummary,
- isMonitoredSystemLimitUrgent,
- MONITORED_SYSTEM_LIMIT_INSTALL_COLLECTORS_HREF,
- MONITORED_SYSTEM_LIMIT_KEY,
- MONITORED_SYSTEM_LIMIT_REVIEW_POLICY_HREF,
- shouldShowMonitoredSystemLimitBanner,
-} from './monitoredSystemLimitWarningBannerModel';
-
-export function useMonitoredSystemLimitWarningBannerState() {
- onMount(() => {
- void loadRuntimeCapabilities();
- });
-
- const monitoredSystemLimit = createMemo(() => getRuntimeLimit(MONITORED_SYSTEM_LIMIT_KEY));
- const monitoredSystemCapacity = createMemo(() => getRuntimeMonitoredSystemCapacity());
- const isUrgent = createMemo(() =>
- isMonitoredSystemLimitUrgent(monitoredSystemLimit(), monitoredSystemCapacity()),
- );
- const showBanner = createMemo(
- () =>
- isHostedModeEnabled() &&
- !presentationPolicyHidesCommercialSurfaces() &&
- !presentationPolicyHidesUpgradePrompts() &&
- shouldShowMonitoredSystemLimitBanner(monitoredSystemLimit(), monitoredSystemCapacity()),
- );
- const migrationGap = createMemo(() => hasMigrationGap());
- const monitoredSystemSummary = createMemo(() =>
- getMonitoredSystemSummary(monitoredSystemLimit(), monitoredSystemCapacity()),
- );
- const toneClass = createMemo(() => getMonitoredSystemBannerToneClass(isUrgent()));
- const reviewPolicyDestination = createMemo(() =>
- resolveUpgradeDestination(MONITORED_SYSTEM_LIMIT_REVIEW_POLICY_HREF),
- );
- const installCollectorsDestination = createMemo(() =>
- resolveUpgradeDestination(MONITORED_SYSTEM_LIMIT_INSTALL_COLLECTORS_HREF),
- );
-
- return {
- installCollectorsDestination,
- isUrgent,
- reviewPolicyDestination,
- migrationGap,
- monitoredSystemSummary,
- showBanner,
- toneClass,
- };
-}
diff --git a/frontend-modern/src/hooks/__tests__/useDeployWizard.test.ts b/frontend-modern/src/hooks/__tests__/useDeployWizard.test.ts
index 2797edbf2..70720a712 100644
--- a/frontend-modern/src/hooks/__tests__/useDeployWizard.test.ts
+++ b/frontend-modern/src/hooks/__tests__/useDeployWizard.test.ts
@@ -3,7 +3,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
/* ── hoisted mocks ──────────────────────────────────────────── */
-const { apiMock, loggerMock, getLimitMock } = vi.hoisted(() => ({
+const { apiMock, loggerMock } = vi.hoisted(() => ({
apiMock: {
getCandidates: vi.fn(),
createPreflight: vi.fn(),
@@ -14,12 +14,10 @@ const { apiMock, loggerMock, getLimitMock } = vi.hoisted(() => ({
retryJob: vi.fn(),
},
loggerMock: { info: vi.fn(), warn: vi.fn() },
- getLimitMock: vi.fn(),
}));
vi.mock('@/api/agentDeploy', () => ({ AgentDeployAPI: apiMock }));
vi.mock('@/utils/logger', () => ({ logger: loggerMock }));
-vi.mock('@/stores/license', () => ({ getRuntimeLimit: getLimitMock }));
// Stub useDeployStream — wizard only needs the returned state, not real SSE.
vi.mock('@/hooks/useDeployStream', () => ({
@@ -49,7 +47,6 @@ const defaultOpts = { clusterId: 'cluster-1', clusterName: 'My Cluster' };
beforeEach(() => {
vi.clearAllMocks();
- getLimitMock.mockReturnValue(null);
// Default: getCandidates returns empty (called immediately on creation).
apiMock.getCandidates.mockResolvedValue({ nodes: [], sourceAgents: [] });
});
@@ -490,26 +487,5 @@ describe('useDeployWizard', () => {
dispose();
});
- it('maxAgentSlots returns monitored-system capacity from runtime limits', async () => {
- getLimitMock.mockReturnValue({ limit: 10 });
- apiMock.getCandidates.mockResolvedValue({ nodes: [], sourceAgents: [] });
-
- const { wizard, dispose } = withRoot(() => useDeployWizard(defaultOpts));
- await vi.waitFor(() => expect(wizard.candidatesLoading()).toBe(false));
-
- expect(wizard.maxAgentSlots()).toBe(10);
- dispose();
- });
-
- it('maxAgentSlots returns 0 when no monitored-system capacity limit exists', async () => {
- getLimitMock.mockReturnValue(null);
- apiMock.getCandidates.mockResolvedValue({ nodes: [], sourceAgents: [] });
-
- const { wizard, dispose } = withRoot(() => useDeployWizard(defaultOpts));
- await vi.waitFor(() => expect(wizard.candidatesLoading()).toBe(false));
-
- expect(wizard.maxAgentSlots()).toBe(0);
- dispose();
- });
});
});
diff --git a/frontend-modern/src/hooks/useDeployWizard.ts b/frontend-modern/src/hooks/useDeployWizard.ts
index 9b220b605..7f7ba2b7d 100644
--- a/frontend-modern/src/hooks/useDeployWizard.ts
+++ b/frontend-modern/src/hooks/useDeployWizard.ts
@@ -8,7 +8,6 @@
import { createSignal, createMemo, onCleanup } from 'solid-js';
import { AgentDeployAPI } from '@/api/agentDeploy';
import { useDeployStream } from '@/hooks/useDeployStream';
-import { getRuntimeLimit } from '@/stores/license';
import type {
CandidateNode,
SourceAgentInfo,
@@ -114,18 +113,11 @@ export function useDeployWizard(opts: UseDeployWizardOptions) {
);
const skippedTargets = createMemo(() =>
- jobTargets().filter(
- (t) => t.status === 'skipped_already_agent' || t.status === 'skipped_license',
- ),
+ jobTargets().filter((t) => t.status === 'skipped_already_agent'),
);
const canceledTargets = createMemo(() => jobTargets().filter((t) => t.status === 'canceled'));
- const maxAgentSlots = createMemo(() => {
- const limit = getRuntimeLimit('max_monitored_systems');
- return limit?.limit ?? 0;
- });
-
const isOperationActive = createMemo(() => step() === 'preflight' || step() === 'deploying');
// -- Event handlers --
@@ -608,9 +600,6 @@ export function useDeployWizard(opts: UseDeployWizardOptions) {
skippedTargets,
canceledTargets,
- // License
- maxAgentSlots,
-
// Operation states
startingPreflight,
startingDeploy,
diff --git a/frontend-modern/src/stores/__tests__/license.test.ts b/frontend-modern/src/stores/__tests__/license.test.ts
index bd27ba04b..c28670be9 100644
--- a/frontend-modern/src/stores/__tests__/license.test.ts
+++ b/frontend-modern/src/stores/__tests__/license.test.ts
@@ -426,14 +426,14 @@ describe('license stores', () => {
expect(getUpgradeActionUrlOrFallback('relay')).toBe(getUpgradeFallbackDestination('relay'));
});
- it('routes monitored-system limit fallbacks to the neutral plan surface', async () => {
+ it('routes monitored-system limit fallbacks through self-hosted purchase compatibility', async () => {
vi.mocked(LicenseAPI.getCommercialPosture).mockResolvedValue({
...mockCommercialPosture,
upgrade_reasons: [{ key: 'reason1', reason: 'Reason 1', action_url: '/upgrade/reason1' }],
});
await loadCommercialPosture(true);
expect(getUpgradeActionUrlOrFallback('max_monitored_systems')).toBe(
- '/settings/system/billing/plan',
+ getUpgradeFallbackDestination('max_monitored_systems'),
);
});
@@ -472,7 +472,7 @@ describe('license stores', () => {
upgrade_reasons: [
{
key: 'max_monitored_systems',
- reason: 'Expand monitored-system capacity',
+ reason: 'Legacy monitored-system alias',
action_url: '/upgrade/max-monitored-systems',
},
],
diff --git a/frontend-modern/src/stores/license.ts b/frontend-modern/src/stores/license.ts
index e2aad4e60..52e2e36e9 100644
--- a/frontend-modern/src/stores/license.ts
+++ b/frontend-modern/src/stores/license.ts
@@ -1,9 +1,5 @@
import { createSignal } from 'solid-js';
-import {
- LicenseAPI,
- type LicenseRuntimeCapabilities,
- type MonitoredSystemCapacityStatus,
-} from '@/api/license';
+import { LicenseAPI, type LicenseRuntimeCapabilities } from '@/api/license';
import { eventBus } from '@/stores/events';
import { logger } from '@/utils/logger';
@@ -70,10 +66,6 @@ export function getRuntimeLimit(key: string) {
return current.limits.find((limit) => limit.key === key);
}
-export function getRuntimeMonitoredSystemCapacity(): MonitoredSystemCapacityStatus | undefined {
- return runtimeCapabilities()?.monitored_system_capacity;
-}
-
/** Default max history days when runtime capabilities aren't loaded yet. */
const DEFAULT_MAX_HISTORY_DAYS = 7;
diff --git a/frontend-modern/src/types/agentDeploy.ts b/frontend-modern/src/types/agentDeploy.ts
index 23c6fe6d9..ba74222b5 100644
--- a/frontend-modern/src/types/agentDeploy.ts
+++ b/frontend-modern/src/types/agentDeploy.ts
@@ -24,7 +24,6 @@ export type DeployTargetStatus =
| 'failed_retryable'
| 'failed_permanent'
| 'skipped_already_agent'
- | 'skipped_license'
| 'canceled';
/** Event type classification for deployment audit log entries. */
@@ -138,7 +137,6 @@ export interface CreateJobResponse {
jobId: string;
acceptedTargets: string[];
skippedTargets: SkippedTarget[];
- reservedLicenseSlots: number;
eventsUrl: string;
}
diff --git a/frontend-modern/src/utils/__tests__/apiClient.error.test.ts b/frontend-modern/src/utils/__tests__/apiClient.error.test.ts
index 3c7de00d2..8c4e5b94e 100644
--- a/frontend-modern/src/utils/__tests__/apiClient.error.test.ts
+++ b/frontend-modern/src/utils/__tests__/apiClient.error.test.ts
@@ -52,32 +52,13 @@ describe('apiClient structured error extraction', () => {
});
});
- it('preserves monitored-system preview payloads from canonical 402 errors', async () => {
+ it('preserves generic feature metadata from structured commercial errors', async () => {
const error = await apiErrorFromResponse(
new Response(
JSON.stringify({
error: 'license_required',
- message: 'Monitored-system capacity reached (6/5)',
- feature: 'max_monitored_systems',
- monitored_system_preview: {
- current_count: 5,
- projected_count: 6,
- additional_count: 1,
- limit: 5,
- would_exceed_limit: true,
- effect: 'creates_new',
- current_systems: [],
- projected_systems: [
- {
- name: 'backup',
- type: 'truenas-system',
- status: 'online',
- source: 'truenas',
- },
- ],
- current_system: null,
- projected_system: null,
- },
+ message: 'Relay access requires a paid feature',
+ feature: 'relay',
}),
{
status: 402,
@@ -88,14 +69,9 @@ describe('apiClient structured error extraction', () => {
);
expect(error).toMatchObject({
- message: 'Monitored-system capacity reached (6/5)',
+ message: 'Relay access requires a paid feature',
status: 402,
- feature: 'max_monitored_systems',
- monitored_system_preview: {
- current_count: 5,
- projected_count: 6,
- would_exceed_limit: true,
- },
+ feature: 'relay',
});
});
});
diff --git a/frontend-modern/src/utils/__tests__/apiClient.org.test.ts b/frontend-modern/src/utils/__tests__/apiClient.org.test.ts
index f7a5bacc8..4074f6827 100644
--- a/frontend-modern/src/utils/__tests__/apiClient.org.test.ts
+++ b/frontend-modern/src/utils/__tests__/apiClient.org.test.ts
@@ -69,32 +69,13 @@ describe('apiClient org context', () => {
expect(headers['X-Pulse-Org-ID']).toBe('tenant-ledger');
});
- it('preserves hosted org context and preview payloads on monitored-system denials', async () => {
+ it('preserves hosted org context on structured commercial errors', async () => {
mockFetch.mockResolvedValue(
new Response(
JSON.stringify({
error: 'license_required',
- message: 'Monitored-system capacity reached (6/5)',
- feature: 'max_monitored_systems',
- monitored_system_preview: {
- current_count: 5,
- projected_count: 6,
- additional_count: 1,
- limit: 5,
- would_exceed_limit: true,
- effect: 'creates_new',
- current_systems: [],
- projected_systems: [
- {
- name: 'backup',
- type: 'truenas-system',
- status: 'online',
- source: 'truenas',
- },
- ],
- current_system: null,
- projected_system: null,
- },
+ message: 'Relay access requires a paid feature',
+ feature: 'relay',
}),
{
status: 402,
@@ -107,14 +88,9 @@ describe('apiClient org context', () => {
await expect(
apiFetchJSON('/api/truenas/connections', { method: 'POST', body: '{}' }),
).rejects.toMatchObject({
- message: 'Monitored-system capacity reached (6/5)',
+ message: 'Relay access requires a paid feature',
status: 402,
- feature: 'max_monitored_systems',
- monitored_system_preview: {
- current_count: 5,
- projected_count: 6,
- would_exceed_limit: true,
- },
+ feature: 'relay',
});
const [, options] = mockFetch.mock.calls[0] as [string, RequestInit];
diff --git a/frontend-modern/src/utils/__tests__/commercialBillingModel.test.ts b/frontend-modern/src/utils/__tests__/commercialBillingModel.test.ts
index bdaa91936..e971be557 100644
--- a/frontend-modern/src/utils/__tests__/commercialBillingModel.test.ts
+++ b/frontend-modern/src/utils/__tests__/commercialBillingModel.test.ts
@@ -1,9 +1,6 @@
import { describe, expect, it } from 'vitest';
-import {
- buildSelfHostedCommercialPlanModel,
- SELF_HOSTED_NOT_METERED_LABEL,
-} from '../commercialBillingModel';
+import { buildSelfHostedCommercialPlanModel } from '../commercialBillingModel';
import { SELF_HOSTED_PLAN_BY_TIER } from '../selfHostedPlans';
const createBaseInput = () => ({
@@ -13,9 +10,6 @@ const createBaseInput = () => ({
planTerms: 'Pro Monthly',
expires: '12/31/2026',
daysRemaining: 123,
- monitoredSystemsSummary: SELF_HOSTED_NOT_METERED_LABEL,
- capacityStatusSummary: SELF_HOSTED_NOT_METERED_LABEL,
- maxMonitoredSystems: SELF_HOSTED_NOT_METERED_LABEL,
});
describe('commercialBillingModel', () => {
@@ -55,55 +49,4 @@ describe('commercialBillingModel', () => {
expect(JSON.stringify(model)).not.toContain('Unlimited');
});
- it('keeps bounded monitored-system details as legacy support context', () => {
- const model = buildSelfHostedCommercialPlanModel({
- ...createBaseInput(),
- monitoredSystemsSummary: '7 monitored systems',
- capacityStatusSummary: 'Continuity review',
- maxMonitoredSystems: 10,
- retailPlanDefinition: null,
- });
-
- expect(model.summary).toEqual([
- { label: 'Monitored Systems', value: '7 monitored systems' },
- { label: 'Continuity Status', value: 'Continuity review' },
- { label: 'Plan Status', value: 'Active' },
- ]);
- expect(model.details.map((item) => item.label)).toContain('Recorded Monitoring Baseline');
- expect(model.details.map((item) => item.label)).not.toContain('Guest Capacity');
- });
-
- it('labels captured legacy continuity as baselines instead of current limits', () => {
- const model = buildSelfHostedCommercialPlanModel({
- ...createBaseInput(),
- monitoredSystemsSummary: '23 monitored systems',
- capacityStatusSummary: 'Continuity review',
- maxMonitoredSystems: SELF_HOSTED_NOT_METERED_LABEL,
- retailPlanDefinition: null,
- monitoredSystemContinuity: {
- plan_limit: 10,
- grandfathered_floor: 23,
- effective_limit: 23,
- capture_pending: false,
- },
- });
-
- expect(model.summary).toEqual([
- { label: 'Monitored Systems', value: '23 monitored systems' },
- { label: 'Continuity Status', value: 'Continuity review' },
- { label: 'Plan Status', value: 'Active' },
- ]);
- expect(model.details.map((item) => item.label)).toEqual([
- 'Tier',
- 'Licensed Email',
- 'Plan Terms',
- 'Expires',
- 'Days Remaining',
- 'Plan Baseline',
- 'Current Baseline',
- 'Observed Legacy Estate',
- 'Continuity Verification',
- ]);
- expect(JSON.stringify(model)).not.toContain('Monitored System Limit');
- });
});
diff --git a/frontend-modern/src/utils/__tests__/deployStatusPresentation.test.ts b/frontend-modern/src/utils/__tests__/deployStatusPresentation.test.ts
index 6e125e3dd..e9377860b 100644
--- a/frontend-modern/src/utils/__tests__/deployStatusPresentation.test.ts
+++ b/frontend-modern/src/utils/__tests__/deployStatusPresentation.test.ts
@@ -15,7 +15,6 @@ describe('deployStatusPresentation', () => {
['failed_retryable', 'Failed'],
['failed_permanent', 'Failed'],
['skipped_already_agent', 'Already monitored'],
- ['skipped_license', 'Workspace capacity'],
['canceled', 'Canceled'],
];
diff --git a/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts b/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts
index 5e061a84a..219ae6757 100644
--- a/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts
+++ b/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts
@@ -55,8 +55,6 @@ import searchTipsPopoverModelSource from '@/components/shared/searchTipsPopoverM
import tooltipSource from '@/components/shared/Tooltip.tsx?raw';
import tooltipModelSource from '@/components/shared/tooltipModel.ts?raw';
import upgradeLinkSource from '@/components/shared/UpgradeLink.tsx?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';
@@ -90,7 +88,6 @@ import searchTipsPopoverStateSource from '@/components/shared/useSearchTipsPopov
import tooltipStateSource from '@/components/shared/useTooltipState.ts?raw';
import upgradeNavigationHookSource from '@/components/shared/useUpgradeNavigation.ts?raw';
import interactiveSparklineStateSource from '@/components/shared/useInteractiveSparklineState.ts?raw';
-import monitoredSystemLimitWarningBannerStateSource from '@/components/shared/useMonitoredSystemLimitWarningBannerState.ts?raw';
import infrastructureSummaryTableStateSource from '@/components/shared/useInfrastructureSummaryTableState.ts?raw';
import selectionCardGroupStateSource from '@/components/shared/useSelectionCardGroupState.ts?raw';
import resourceBadgePresentationSource from '@/utils/resourceBadgePresentation.ts?raw';
@@ -192,7 +189,6 @@ import workloadUrlSyncModelSource from '@/components/Workloads/workloadUrlSyncMo
import workloadsWorkloadRouteStateSource from '@/components/Workloads/useWorkloadRouteState.ts?raw';
import workloadsWorkloadUrlSyncSource from '@/components/Workloads/useWorkloadUrlSync.ts?raw';
import workloadsStateSource from '@/components/Workloads/useWorkloadsState.ts?raw';
-import workloadsFilterStateSource from '@/components/Workloads/useWorkloadsFilterState.ts?raw';
import workloadTopologySource from '@/components/Workloads/workloadTopology.ts?raw';
import groupedTableWindowingSource from '@/components/Workloads/useGroupedTableWindowing.ts?raw';
import thresholdSliderModelSource from '@/components/Workloads/thresholdSliderModel.ts?raw';
@@ -252,11 +248,7 @@ import storageDetailKeyValueRowSource from '@/components/Storage/StorageDetailKe
import storageDetailMetricCardSource from '@/components/Storage/StorageDetailMetricCard.tsx?raw';
import diskLiveMetricSource from '@/components/Storage/DiskLiveMetric.tsx?raw';
import useDiskLiveMetricModelSource from '@/components/Storage/useDiskLiveMetricModel.ts?raw';
-import storageControlsSource from '@/components/Storage/StorageControls.tsx?raw';
-import useStorageControlsModelSource from '@/components/Storage/useStorageControlsModel.ts?raw';
-import storageFilterSource from '@/components/Storage/StorageFilter.tsx?raw';
import storagePageControlsSource from '@/components/Storage/StoragePageControls.tsx?raw';
-import useStoragePageControlsModelSource from '@/components/Storage/useStoragePageControlsModel.ts?raw';
import storageCephSectionSource from '@/components/Storage/StorageCephSection.tsx?raw';
import storageContentCardSource from '@/components/Storage/StorageContentCard.tsx?raw';
import storagePageSource from '@/components/Storage/Storage.tsx?raw';
@@ -284,7 +276,6 @@ import useDiskDetailModelSource from '@/components/Storage/useDiskDetailModel.ts
import storagePoolRowSource from '@/components/Storage/StoragePoolRow.tsx?raw';
import storageExpansionStateSource from '@/components/Storage/useStorageExpansionState.ts?raw';
import storageFilterStateSource from '@/components/Storage/useStorageFilterState.ts?raw';
-import useStorageFilterToolbarModelSource from '@/components/Storage/useStorageFilterToolbarModel.ts?raw';
import storagePageFiltersSource from '@/components/Storage/useStoragePageFilters.ts?raw';
import storagePageDataSource from '@/components/Storage/useStoragePageData.ts?raw';
import storagePageModelSource from '@/components/Storage/useStoragePageModel.ts?raw';
@@ -838,14 +829,12 @@ describe('frontend resource type boundaries', () => {
expect(workloadSelectionModelSource).toContain('resolveWorkloadResourceSelection');
expect(workloadSelectionModelSource).toContain('workloadsHasHoveredWorkload');
expect(workloadsStateSource).not.toContain('const guestId = () => {');
- expect(workloadsFilterSource).toContain('useWorkloadsFilterState');
+ expect(workloadsFilterSource).not.toContain('useWorkloadsFilterState');
expect(workloadsFilterSource).not.toContain('const [filtersOpen, setFiltersOpen] =');
- expect(workloadsFilterSource).not.toContain('useBreakpoint');
+ expect(workloadsFilterSource).toContain('useBreakpoint');
expect(workloadsFilterSource).not.toContain("props.setSortKey('name')");
- expect(workloadsFilterStateSource).toContain('countActiveWorkloadsFilters');
- expect(workloadsFilterStateSource).not.toContain('props.containerRuntimeFilter?.onChange');
- expect(workloadsFilterStateSource).toContain('useBreakpoint');
- expect(workloadsFilterStateSource).toContain('DEFAULT_WORKLOADS_SORT_KEY');
+ expect(workloadsFilterSource).toContain('hasActiveWorkloadsFilters');
+ expect(workloadsFilterSource).toContain('DEFAULT_WORKLOADS_SORT_KEY');
expect(workloadsFilterModelSource).toContain('export const countActiveWorkloadsFilters');
expect(workloadsFilterModelSource).toContain('export const hasActiveWorkloadsFilters');
expect(workloadsFilterModelSource).toContain(
@@ -1139,9 +1128,9 @@ describe('frontend resource type boundaries', () => {
expect(recoverySource).not.toContain(
"chartRangeDays() === range\n ? 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-200'",
);
- expect(recoverySource).toContain('getRecoveryTimelineColumnButtonClass');
- expect(recoverySource).toContain('getRecoveryArtifactColumnHeaderClass');
- expect(recoverySource).toContain('getRecoveryArtifactRowClass');
+ expect(recoveryActivitySectionSource).toContain('getRecoveryTimelineColumnButtonClass');
+ expect(recoveryHistoryTableSource).toContain('getRecoveryArtifactColumnHeaderClass');
+ expect(recoveryHistoryTableSource).toContain('getRecoveryArtifactRowClass');
expect(recoverySource).toContain('recoveryDateKeyFromTimestamp');
expect(recoverySource).toContain('parseRecoveryDateKey');
expect(recoverySource).toContain('getRecoveryPrettyDateLabel');
@@ -1152,10 +1141,9 @@ describe('frontend resource type boundaries', () => {
expect(recoveryProtectedInventorySectionSource).toContain(
'getRecoveryRollupInventoryStatusTextClass',
);
- expect(recoverySource).toContain('getRecoverySpecialOutcomeTextClass');
- expect(recoverySource).toContain('getRecoveryBreadcrumbLinkClass');
- expect(recoverySource).toContain('getRecoveryFilterPanelClearClass');
- expect(recoverySource).toContain('getRecoveryEmptyStateActionClass');
+ expect(recoveryProtectedInventorySectionSource).toContain('getRecoverySpecialOutcomeTextClass');
+ expect(recoveryHistorySectionSource).toContain('getRecoveryBreadcrumbLinkClass');
+ expect(recoveryHistoryTableSource).toContain('getRecoveryEmptyStateActionClass');
expect(recoverySource).toContain('getRecoveryProtectedItemsLoadingState');
expect(recoverySource).toContain('getRecoveryProtectedItemsFailureState');
expect(recoverySource).toContain('getRecoveryActivityLoadingState');
@@ -1164,31 +1152,29 @@ describe('frontend resource type boundaries', () => {
expect(recoverySource).toContain('getRecoveryPointsFailureState');
expect(recoverySource).toContain('getRecoveryProtectedItemsEmptyState');
expect(recoverySource).toContain('getRecoveryHistoryEmptyState');
- expect(recoverySource).toContain('getRecoveryDrawerCloseButtonClass');
+ expect(recoveryHistoryTableSource).toContain('getRecoveryDrawerCloseButtonClass');
expect(recoverySource).toContain('getRecoveryRollupItemLabel');
expect(recoverySource).toContain('getRecoveryPointItemLabel');
expect(recoverySource).toContain('getRecoveryPointRepositoryLabel');
expect(recoverySource).toContain('getRecoveryPointDetailsSummary');
expect(recoverySource).toContain('getRecoveryPointTimestampMs');
expect(recoverySource).toContain('normalizeRecoveryModeQueryValue');
- expect(recoverySource).toContain('getRecoveryTimelineAxisLabelClass');
+ expect(recoveryActivitySectionSource).toContain('getRecoveryTimelineAxisLabelClass');
expect(recoverySource).toContain('getRecoveryTimelineLabelEvery');
expect(recoverySource).toContain('getRecoveryGroupNoTimestampLabel');
- expect(recoverySource).toContain('getRecoveryProtectedSearchPlaceholder');
- expect(recoverySource).toContain('getRecoveryHistorySearchPlaceholder');
+ expect(recoveryProtectedInventorySectionSource).toContain('getRecoveryProtectedSearchPlaceholder');
+ expect(recoveryHistorySectionSource).toContain('getRecoveryHistorySearchPlaceholder');
expect(recoverySource).toContain('getRecoverySearchHistoryEmptyMessage');
expect(recoverySource).not.toContain('Search protected items...');
expect(recoverySource).not.toContain('Search recovery history...');
expect(recoverySource).not.toContain('Recent searches appear here.');
- expect(recoverySource).toContain('RECOVERY_TIMELINE_LEGEND_ITEM_CLASS');
- expect(recoverySource).toContain('getRecoveryEventTimeTextClass');
+ expect(recoveryActivitySectionSource).toContain('RECOVERY_TIMELINE_LEGEND_ITEM_CLASS');
+ expect(recoveryHistoryTableSource).toContain('getRecoveryEventTimeTextClass');
expect(recoverySource).not.toContain('getRecoverySubjectTypeBadgeClass');
expect(recoverySource).not.toContain('getRecoverySubjectTypeLabel');
- expect(recoverySource).toContain('getRecoveryRollupAgeTextClass');
- expect(recoverySource).toContain('RECOVERY_ADVANCED_FILTER_LABEL_CLASS');
- expect(recoverySource).toContain('RECOVERY_ADVANCED_FILTER_FIELD_CLASS');
- expect(recoverySource).toContain('RECOVERY_GROUP_HEADER_ROW_CLASS');
- expect(recoverySource).toContain('RECOVERY_GROUP_HEADER_TEXT_CLASS');
+ expect(recoveryProtectedInventorySectionSource).toContain('getRecoveryRollupAgeTextClass');
+ expect(recoveryHistoryTableSource).toContain('RECOVERY_GROUP_HEADER_ROW_CLASS');
+ expect(recoveryHistoryTableSource).toContain('RECOVERY_GROUP_HEADER_TEXT_CLASS');
expect(recoverySource).not.toContain(
"isSelected\n ? 'bg-blue-100 dark:bg-blue-900'\n : 'hover:bg-surface-hover'",
);
@@ -1493,7 +1479,7 @@ describe('frontend resource type boundaries', () => {
);
expect(auditLogPanelSource).toContain('@/utils/upgradePresentation');
expect(auditWebhookPanelSource).toContain('@/utils/upgradePresentation');
- expect(ssoProvidersPanelSource).toContain('@/utils/upgradePresentation');
+ expect(ssoProvidersPanelSource).not.toContain('@/utils/upgradePresentation');
expect(upgradePresentationSource).toContain('export const UPGRADE_ACTION_LABEL');
expect(upgradePresentationSource).not.toContain('UPGRADE_TRIAL_LABEL');
expect(upgradePresentationSource).not.toContain('UPGRADE_TRIAL_LINK_CLASS');
@@ -1920,39 +1906,17 @@ describe('frontend resource type boundaries', () => {
expect(storagePageSource).toContain('StorageContentCard');
expect(storagePageSource).toContain('StoragePageBanners');
expect(storagePageSource).toContain('StoragePageSummary');
- expect(storageControlsSource).toContain('export const StorageControls');
- expect(storageControlsSource).toContain('StorageFilter');
- expect(storageControlsSource).toContain('Subtabs');
- expect(storageControlsSource).toContain('useStorageControlsModel');
- expect(storageControlsSource).toContain('STORAGE_CONTROLS_NODE_SELECT_CLASS');
- expect(storageControlsSource).toContain('STORAGE_CONTROLS_NODE_DIVIDER_CLASS');
- expect(storageControlsSource).toContain('DEFAULT_STORAGE_SORT_OPTIONS');
- expect(storageControlsSource).not.toContain('STORAGE_VIEW_OPTIONS');
- expect(storageControlsSource).not.toContain(
- 'props.setSelectedNodeId(event.currentTarget.value)',
- );
- expect(storageControlsSource).not.toContain('focus:ring-blue-500');
- expect(useStorageControlsModelSource).toContain('STORAGE_VIEW_OPTIONS');
- expect(useStorageControlsModelSource).toContain('handleNodeFilterChange');
- expect(useStorageControlsModelSource).toContain('handleViewChange');
- expect(storageFilterSource).toContain('useStorageFilterToolbarModel');
- expect(storageFilterSource).toContain('STORAGE_FILTER_SORT_SELECT_CLASS');
- expect(storageFilterSource).toContain('STORAGE_FILTER_SORT_DIRECTION_BUTTON_CLASS');
- expect(storageFilterSource).not.toContain('const activeFilterCount = createMemo(() => {');
- expect(storageFilterSource).not.toContain('const sourceOptions = (): StorageSourceOption[] =>');
- expect(storageFilterSource).not.toContain('props.setSortKey(DEFAULT_STORAGE_SORT_KEY)');
- expect(storageFilterSource).not.toContain("props.sortDirection() === 'asc' ? 'desc' : 'asc'");
- expect(storageFilterSource).not.toContain(
- "props.sortDirection() === 'asc' ? 'rotate-180' : ''",
- );
- expect(storageFilterSource).not.toContain('focus:ring-blue-500');
- expect(useStorageFilterToolbarModelSource).toContain('countActiveStorageFilters');
- expect(useStorageFilterToolbarModelSource).toContain('hasActiveStorageFilters');
- expect(useStorageFilterToolbarModelSource).toContain('DEFAULT_STORAGE_SORT_KEY');
- expect(useStorageFilterToolbarModelSource).toContain('DEFAULT_STORAGE_SOURCE_FILTER');
- expect(useStorageFilterToolbarModelSource).toContain('getStorageSortDirectionTitle');
- expect(useStorageFilterToolbarModelSource).toContain('getStorageSortDirectionIconClass');
- expect(useStorageFilterToolbarModelSource).toContain('getNextStorageSortDirection');
+ expect(storagePageControlsSource).toContain('FilterBar');
+ expect(storagePageControlsSource).toContain('Subtabs');
+ expect(storagePageControlsSource).toContain('STORAGE_VIEW_OPTIONS');
+ expect(storagePageControlsSource).toContain('hasActiveStorageFilters');
+ expect(storagePageControlsSource).toContain('DEFAULT_STORAGE_SORT_OPTIONS');
+ expect(storagePageControlsSource).toContain('getNextStorageSortDirection');
+ expect(storagePageControlsSource).toContain('STORAGE_FILTER_SORT_SELECT_CLASS');
+ expect(storagePageControlsSource).toContain('STORAGE_FILTER_SORT_DIRECTION_BUTTON_CLASS');
+ expect(storagePageControlsSource).not.toContain('StorageControls');
+ expect(storagePageControlsSource).not.toContain('useStoragePageControlsModel');
+ expect(storagePageControlsSource).not.toContain('focus:ring-blue-500');
expect(storageFilterPresentationSource).toContain('export const getStorageSortDirectionTitle');
expect(storageFilterPresentationSource).toContain(
'export const getStorageSortDirectionIconClass',
@@ -1964,15 +1928,11 @@ describe('frontend resource type boundaries', () => {
'export const STORAGE_FILTER_SORT_DIRECTION_BUTTON_CLASS',
);
expect(storagePageControlsSource).toContain('export const StoragePageControls');
- expect(storagePageControlsSource).toContain('StorageControls');
- expect(storagePageControlsSource).toContain('useStoragePageControlsModel');
- expect(storagePageControlsSource).not.toContain('normalizeStorageSortKey');
+ expect(storagePageControlsSource).toContain('normalizeStorageSortKey');
expect(storagePageControlsSource).not.toContain(
"props.view() === 'pools' ? props.storageFilterGroupBy : undefined",
);
- expect(storagePageControlsSource).not.toContain("props.view() !== 'pools'");
- expect(useStoragePageControlsModelSource).toContain('normalizeStorageSortKey');
- expect(useStoragePageControlsModelSource).toContain("options.view() === 'pools'");
+ expect(storagePageControlsSource).toContain('const sortDisabled = () => !isPoolsView()');
expect(storageCephSectionSource).toContain('export const StorageCephSection');
expect(storageCephSectionSource).toContain('useStorageCephSectionModel');
expect(storageCephSectionSource).toContain('StorageCephSummaryCard');
@@ -2927,33 +2887,6 @@ describe('frontend resource type boundaries', () => {
expect(selectionCardGroupModelSource).toContain('resolveSelectionCardTone');
expect(selectionCardGroupModelSource).toContain('getSelectionCardButtonClass');
expect(selectionCardGroupModelSource).toContain("compact: 'grid grid-cols-2 gap-2'");
- expect(monitoredSystemLimitWarningBannerSource).toContain(
- 'useMonitoredSystemLimitWarningBannerState',
- );
- expect(monitoredSystemLimitWarningBannerSource).toContain(
- 'MONITORED_SYSTEM_LIMIT_REVIEW_POLICY_LABEL',
- );
- expect(monitoredSystemLimitWarningBannerSource).not.toContain('createEffect');
- expect(monitoredSystemLimitWarningBannerSource).not.toContain('createMemo');
- expect(monitoredSystemLimitWarningBannerSource).not.toContain('loadRuntimeCapabilities');
- expect(monitoredSystemLimitWarningBannerSource).not.toContain('trackUpgradeMetricEvent');
- expect(monitoredSystemLimitWarningBannerSource).not.toContain('legacyConnections()');
- expect(monitoredSystemLimitWarningBannerStateSource).not.toContain('createEffect');
- expect(monitoredSystemLimitWarningBannerStateSource).toContain('createMemo');
- expect(monitoredSystemLimitWarningBannerStateSource).toContain('loadRuntimeCapabilities');
- expect(monitoredSystemLimitWarningBannerStateSource).not.toContain('trackUpgradeMetricEvent');
- expect(monitoredSystemLimitWarningBannerStateSource).toContain('hasMigrationGap');
- expect(monitoredSystemLimitWarningBannerStateSource).toContain('reviewPolicyDestination');
- expect(monitoredSystemLimitWarningBannerStateSource).not.toContain('handleUpgradeClick');
- expect(monitoredSystemLimitWarningBannerModelSource).toContain(
- 'getMonitoredSystemBannerToneClass',
- );
- expect(monitoredSystemLimitWarningBannerModelSource).not.toContain(
- 'MONITORED_SYSTEM_LIMIT_UPGRADE_LABEL',
- );
- expect(monitoredSystemLimitWarningBannerModelSource).toContain(
- 'MONITORED_SYSTEM_LIMIT_INSTALL_COLLECTORS_LABEL',
- );
expect(monitoredSystemLedgerPanelSource).toContain('@/utils/monitoredSystemPresentation');
expect(monitoredSystemLedgerPanelSource).toContain('getMonitoredSystemLedgerPresentation');
expect(monitoredSystemLedgerPanelSource).toContain(
diff --git a/frontend-modern/src/utils/__tests__/licensePresentation.test.ts b/frontend-modern/src/utils/__tests__/licensePresentation.test.ts
index 4bb2002cc..00bf4536d 100644
--- a/frontend-modern/src/utils/__tests__/licensePresentation.test.ts
+++ b/frontend-modern/src/utils/__tests__/licensePresentation.test.ts
@@ -18,14 +18,11 @@ import {
getLicenseSubscriptionStatusPresentation,
getSelfHostedPlanLabel,
getLicenseTierLabel,
- getDisplayableMonitoredSystemContinuity,
- getMonitoredSystemContinuityNotice,
getNoActiveSelfHostedActivationState,
getOrganizationBillingLicenseStatusLabel,
getPurchaseActivationNotice,
isDisplayableLicenseFeature,
isGrandfatheredRecurringV5PlanVersion,
- isUncappedGrandfatheredPlanVersion,
SELF_HOSTED_RECOVERY_PRESENTATION,
} from '@/utils/licensePresentation';
import { SELF_HOSTED_PRO_BILLING_PRESENTATION } from '@/components/Settings/selfHostedBillingPresentation';
@@ -186,10 +183,6 @@ describe('licensePresentation', () => {
expect(isGrandfatheredRecurringV5PlanVersion('v5_pro_monthly_grandfathered')).toBe(true);
expect(isGrandfatheredRecurringV5PlanVersion('v5_pro_annual_grandfathered')).toBe(true);
expect(isGrandfatheredRecurringV5PlanVersion('v5_lifetime_grandfathered')).toBe(false);
- expect(isUncappedGrandfatheredPlanVersion('v5_pro_monthly_grandfathered', false)).toBe(true);
- expect(isUncappedGrandfatheredPlanVersion('v5_pro_annual_grandfathered', false)).toBe(true);
- expect(isUncappedGrandfatheredPlanVersion(undefined, true)).toBe(true);
- expect(isUncappedGrandfatheredPlanVersion('pro', false)).toBe(false);
expect(
getGrandfatheredPriceContinuityNotice('v5_pro_monthly_grandfathered', 'active'),
).toMatchObject({
@@ -214,21 +207,6 @@ describe('licensePresentation', () => {
).toBeNull();
});
- it('hides monitored-system continuity when self-hosted effective capacity is uncapped', () => {
- expect(
- getDisplayableMonitoredSystemContinuity({
- continuity: {
- plan_limit: 10,
- effective_limit: 0,
- capture_pending: true,
- },
- planVersion: 'legacy_migration_fallback',
- isLifetime: false,
- subscriptionState: 'active',
- }),
- ).toBeNull();
- });
-
it('builds entitlement-first current-plan presentation for community, paid, and grandfathered installs', () => {
expect(
getSelfHostedCurrentPlanPresentation({
@@ -265,12 +243,6 @@ describe('licensePresentation', () => {
capabilities: ['relay', 'ai_autofix'],
limits: [{ key: 'max_monitored_systems', limit: 10, current: 23, state: 'enforced' }],
upgrade_reasons: [],
- monitored_system_continuity: {
- plan_limit: 10,
- grandfathered_floor: 23,
- effective_limit: 23,
- capture_pending: false,
- },
},
displayableCapabilities: ['Pulse Relay (Remote Access)', 'Safe Remediation Workflows'],
}),
@@ -347,12 +319,6 @@ describe('licensePresentation', () => {
capabilities: ['relay'],
limits: [],
upgrade_reasons: [],
- monitored_system_continuity: {
- plan_limit: 10,
- grandfathered_floor: 23,
- effective_limit: 23,
- capture_pending: false,
- },
},
displayableCapabilities: ['Pulse Relay (Remote Access)'],
}),
@@ -670,129 +636,6 @@ describe('licensePresentation', () => {
).toBeNull();
});
- it('returns monitored-system continuity notices for pending verification and captured grandfathering', () => {
- expect(
- getMonitoredSystemContinuityNotice(
- {
- plan_limit: 10,
- effective_limit: 10,
- capture_pending: true,
- },
- {
- current_available: false,
- current_unavailable_reason: 'supplemental_inventory_unsettled',
- },
- {
- mode: 'usage_unavailable',
- urgency: 'ok',
- current: 0,
- limit: 10,
- current_available: false,
- current_unavailable_reason: 'supplemental_inventory_unsettled',
- available_slots: 0,
- overage: 0,
- blocks_new_systems: false,
- existing_monitoring_continues: false,
- },
- ),
- ).toMatchObject({
- title: 'Legacy continuity verification pending',
- body: expect.stringContaining('legacy v5 monitoring continuity'),
- tone: expect.stringContaining('amber'),
- });
- expect(
- getMonitoredSystemContinuityNotice(
- {
- plan_limit: 10,
- effective_limit: 10,
- capture_pending: true,
- },
- {
- current: 23,
- limit: 10,
- current_available: true,
- state: 'enforced',
- },
- {
- mode: 'over_limit_frozen',
- urgency: 'enforced',
- current: 23,
- limit: 10,
- current_available: true,
- available_slots: 0,
- overage: 13,
- reason: 'legacy_migration_capture_pending',
- blocks_new_systems: true,
- existing_monitoring_continues: true,
- },
- ),
- ).toMatchObject({
- title: 'Legacy continuity verification pending',
- body: expect.stringContaining('identified 23 monitored systems'),
- tone: expect.stringContaining('amber'),
- });
- expect(
- getMonitoredSystemContinuityNotice(
- {
- plan_limit: 10,
- grandfathered_floor: 23,
- effective_limit: 23,
- capture_pending: false,
- captured_at: 123,
- },
- {
- current_available: true,
- },
- {
- mode: 'at_limit_blocking_new',
- urgency: 'enforced',
- current: 23,
- limit: 23,
- current_available: true,
- available_slots: 0,
- overage: 0,
- reason: 'limit_reached',
- blocks_new_systems: true,
- existing_monitoring_continues: true,
- },
- {
- planVersion: 'v5_pro_monthly_grandfathered',
- isLifetime: false,
- subscriptionState: 'active',
- },
- ),
- ).toBeNull();
- expect(
- getMonitoredSystemContinuityNotice(
- {
- plan_limit: 10,
- grandfathered_floor: 23,
- effective_limit: 23,
- capture_pending: false,
- captured_at: 123,
- },
- {
- current_available: true,
- },
- {
- mode: 'at_limit_blocking_new',
- urgency: 'enforced',
- current: 23,
- limit: 23,
- current_available: true,
- available_slots: 0,
- overage: 0,
- reason: 'limit_reached',
- blocks_new_systems: true,
- existing_monitoring_continues: true,
- },
- ),
- ).toMatchObject({
- title: 'Legacy monitoring continuity',
- tone: expect.stringContaining('green'),
- });
- });
-
it('returns canonical purchase activation notices', () => {
expect(getPurchaseActivationNotice('activated')).toMatchObject({
title: 'Plan activated',
diff --git a/frontend-modern/src/utils/__tests__/monitoredSystemPresentation.test.ts b/frontend-modern/src/utils/__tests__/monitoredSystemPresentation.test.ts
index b049336e8..56625b1c0 100644
--- a/frontend-modern/src/utils/__tests__/monitoredSystemPresentation.test.ts
+++ b/frontend-modern/src/utils/__tests__/monitoredSystemPresentation.test.ts
@@ -1,36 +1,20 @@
import { describe, expect, it } from 'vitest';
-import proLicensePanelStateSource from '@/components/Settings/useProLicensePanelState.ts?raw';
-import monitoredSystemLedgerPanelSource from '@/components/Settings/MonitoredSystemLedgerPanel.tsx?raw';
-import monitoredSystemLimitWarningBannerModelSource from '@/components/shared/monitoredSystemLimitWarningBannerModel.ts?raw';
-import commercialBillingModelSource from '@/utils/commercialBillingModel.ts?raw';
-import licensePresentationSource from '@/utils/licensePresentation.ts?raw';
-import monitoredSystemPresentationSource from '@/utils/monitoredSystemPresentation.ts?raw';
import {
- buildMonitoredSystemCapacitySectionModel,
- buildMonitoredSystemAdmissionPreviewUnavailableState,
- formatMonitoredSystemAdmissionPreviewSummary,
- formatMonitoredSystemAdmissionPreviewUnavailableMessage,
+ buildMonitoredSystemImpactPreviewUnavailableState,
+ formatMonitoredSystemImpactPreviewSummary,
+ formatMonitoredSystemImpactPreviewUnavailableMessage,
formatMonitoredSystemGroupedSourcesLabel,
- formatMonitoredSystemLegacyConnectionBreakdown,
- formatMonitoredSystemUsageUnavailableMessage,
- getMonitoredSystemBriefSummary,
- formatMonitoredSystemLimitSummary,
formatMonitoredSystemLedgerUnavailableMessage,
formatMonitoredSystemLatestIncludedSignalSentence,
- formatMonitoredSystemMigrationMessage,
- formatMonitoredSystemOverflowSummary,
formatMonitoredSystemSurfaceAttribution,
- getMonitoredSystemAdmissionPreviewRequiredState,
- getMonitoredSystemAdmissionPreviewSaveBlockedMessage,
- getMonitoredSystemAdmissionPreviewTitle,
- getMonitoredSystemAdmissionPreviewUnavailableTitle,
+ getMonitoredSystemImpactPreviewTitle,
+ getMonitoredSystemImpactPreviewUnavailableTitle,
+ getMonitoredSystemBriefSummary,
getMonitoredSystemCountingDetailsToggleLabel,
getMonitoredSystemDisclosureDefinition,
getMonitoredSystemDisclosureToggleLabel,
getMonitoredSystemExplanationFallbackSummary,
- getMonitoredSystemLimitCapacityStatusSummary,
- getMonitoredSystemLimitContextSummary,
getMonitoredSystemLedgerDescription,
getMonitoredSystemLedgerErrorState,
getMonitoredSystemLedgerHiddenState,
@@ -38,466 +22,107 @@ import {
getMonitoredSystemLedgerPresentation,
getMonitoredSystemLedgerPolicyLoadingState,
getMonitoredSystemLedgerUnavailableState,
- getMonitoredSystemLimitInstallCollectorsLabel,
- getMonitoredSystemLimitReviewPolicyLabel,
- getMonitoredSystemLimitUnavailableReason,
- getMonitoredSystemLimitUsageSummary,
getMonitoredSystemSourceLabel,
- resolveMonitoredSystemCapacityStatus,
getMonitoredSystemStatusFallbackSummary,
getMonitoredSystemSurfaceTypeLabel,
- isMonitoredSystemAdmissionPreviewResolvedSafely,
- isMonitoredSystemLimitUrgent,
- isMonitoredSystemLimitUsageAvailable,
} from '@/utils/monitoredSystemPresentation';
describe('monitoredSystemPresentation', () => {
- it('returns canonical ledger labels and fallback copy', () => {
- expect(getMonitoredSystemLedgerPresentation()).toEqual({
- briefSummary:
- 'Pulse counts top-level monitored systems. Child resources underneath them 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 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.',
- ledgerDescription:
- 'Review the top-level monitored systems Pulse has identified for reporting, migration continuity, and support context.',
- tableNameLabel: 'Name',
- tableStatusLabel: 'Status',
- tableLatestIncludedSignalLabel: 'Latest Included Signal',
- countedSystemBadgeLabel: 'Counts as 1 monitored system',
- groupedSourcesHeading: 'Grouped sources',
- countingExplanationHeading: 'Why this counts',
- continuityHeading: 'Legacy continuity',
- continuityPlanLimitLabel: 'Plan baseline',
- continuityEffectiveLimitLabel: 'Current baseline',
- continuityGrandfatheredFloorLabel: 'Observed legacy estate',
- continuityCaptureLabel: 'Verification',
- continuityCapturePendingLabel: 'Pending',
- continuityCaptureCapturedLabel: 'Captured',
- usageVerifyingLabel: 'Verifying…',
- remainingCapacityUnavailableLabel: 'Unavailable',
- unlimitedLimitLabel: 'Not metered',
- loadingState: {
- text: 'Loading monitored system usage…',
- },
- errorState: {
- title: 'Monitored system usage is temporarily unavailable.',
- retryingLabel: 'Trying again…',
- retryLabel: 'Try again',
- },
- unavailableState: {
- title: 'Verifying monitored-system inventory',
- fallbackMessage:
- 'Pulse cannot currently verify monitored-system usage for this installation. Refresh after the monitoring runtime settles.',
- unsettledMessage:
- 'Pulse is still collecting the first provider-owned inventory baseline. The monitored-system ledger will appear after that baseline completes.',
- rebuildPendingMessage:
- 'Pulse has collected provider-owned inventory and is rebuilding the canonical monitored-system ledger. Usage will appear when that rebuild finishes.',
- },
- policyLoadingState: {
- title: 'Checking monitored-system visibility',
- message:
- 'Pulse waits for the session visibility state before loading monitored-system usage details.',
- },
- hiddenState: {
- title: 'Monitored-system usage is hidden in demo mode',
- message:
- 'The public demo uses sample infrastructure data, so Pulse hides counted-system totals and billing actions instead of creating a demo license.',
- },
- countingDetailsCollapsedLabel: 'View counting details',
- countingDetailsExpandedLabel: 'Hide counting details',
- currentStatusHeading: 'Current status',
- latestIncludedSignalSummaryLabel: 'Latest included signal',
- includedCollectionPathsHeading: 'Included collection paths',
- emptyState: 'No monitored systems counted.',
- noIncludedSignalLabel: 'No included signal yet.',
- fallbackExplanationSummary:
- 'Pulse counts this top-level collection path as one monitored system.',
- statusSummaryByStatus: {
- online: 'All included top-level collection paths currently report online status.',
- warning:
- 'At least one included top-level collection path is degraded, so Pulse marks this monitored system as warning.',
- offline:
- 'At least one included source is offline or disconnected, so Pulse marks this monitored system as offline.',
- unknown: 'Pulse cannot determine a canonical runtime status for this monitored system yet.',
- },
- limitBanner: {
- reviewPolicyLabel: 'Review continuity',
- installCollectorsLabel: 'Install v6 collectors',
- overflowSummaryPrefix: 'A temporary setup slot is active',
- legacyConnectionSuffix:
- 'that are folded into the canonical monitored-system ledger when the same top-level system is discovered canonically.',
- },
- admissionPreview: {
- requiredTitle: 'Preview monitored-system impact before saving',
- requiredMessage:
- 'Pulse must preview the monitored-system impact for this platform connection before it can be saved.',
- fallbackTitle: 'Monitored-system impact',
- exceedsPolicyTitle: 'This change needs continuity review before saving',
- addsSystemsTitle: 'This change adds monitored systems',
- removesSystemsTitle: 'This change removes monitored systems',
- unchangedTitle: 'This change keeps monitored-system count unchanged',
- unavailableTitle: 'Monitored-system verification is temporarily unavailable',
- unavailableFallbackMessage:
- 'Pulse cannot verify monitored-system impact right now, so this connection cannot be saved yet. Retry preview in a moment.',
- unavailableUnsettledMessage:
- 'Pulse is still settling provider-owned inventory for this platform connection, so the monitored-system check is not safe yet. Retry preview after the first baseline finishes.',
- unavailableRebuildPendingMessage:
- 'Pulse has settled provider-owned inventory and is rebuilding the canonical monitored-system view, so this connection cannot be saved yet. Retry preview in a moment.',
- saveBlockedLimitMessage: 'This change needs monitored-system review before saving',
- saveBlockedLoadingMessage: 'Wait for the monitored-system impact preview to finish',
- },
- });
+ it('returns canonical ledger labels and fallback copy without capacity policy copy', () => {
expect(getMonitoredSystemBriefSummary()).toBe(
'Pulse counts top-level monitored systems. Child resources underneath them are included.',
);
+ expect(getMonitoredSystemLedgerDescription()).toBe(
+ 'Review the top-level monitored systems Pulse has identified for reporting and support context.',
+ );
expect(getMonitoredSystemDisclosureToggleLabel(false)).toBe('View counting rules');
expect(getMonitoredSystemDisclosureToggleLabel(true)).toBe('Hide counting rules');
- expect(getMonitoredSystemDisclosureDefinition()).toContain('Docker host');
- expect(getMonitoredSystemDisclosureDefinition()).toContain('Proxmox node');
- expect(getMonitoredSystemLedgerDescription()).toBe(
- 'Review the top-level monitored systems Pulse has identified for reporting, migration continuity, and support context.',
- );
- expect(getMonitoredSystemLedgerLoadingState()).toEqual({
- text: 'Loading monitored system usage…',
- });
- expect(getMonitoredSystemLedgerErrorState()).toEqual({
- title: 'Monitored system usage is temporarily unavailable.',
- retryingLabel: 'Trying again…',
- retryLabel: 'Try again',
- });
- expect(getMonitoredSystemLedgerUnavailableState()).toEqual({
- title: 'Verifying monitored-system inventory',
- fallbackMessage:
- 'Pulse cannot currently verify monitored-system usage for this installation. Refresh after the monitoring runtime settles.',
- unsettledMessage:
- 'Pulse is still collecting the first provider-owned inventory baseline. The monitored-system ledger will appear after that baseline completes.',
- rebuildPendingMessage:
- 'Pulse has collected provider-owned inventory and is rebuilding the canonical monitored-system ledger. Usage will appear when that rebuild finishes.',
- });
- expect(getMonitoredSystemLedgerPolicyLoadingState()).toEqual({
- title: 'Checking monitored-system visibility',
- message:
- 'Pulse waits for the session visibility state before loading monitored-system usage details.',
- });
- expect(getMonitoredSystemLedgerHiddenState()).toEqual({
- title: 'Monitored-system usage is hidden in demo mode',
- message:
- 'The public demo uses sample infrastructure data, so Pulse hides counted-system totals and billing actions instead of creating a demo license.',
- });
+ expect(getMonitoredSystemDisclosureDefinition()).toContain('Each root counts once');
expect(getMonitoredSystemCountingDetailsToggleLabel(false)).toBe('View counting details');
expect(getMonitoredSystemCountingDetailsToggleLabel(true)).toBe('Hide counting details');
expect(getMonitoredSystemExplanationFallbackSummary()).toBe(
'Pulse counts this top-level collection path as one monitored system.',
);
+ expect(getMonitoredSystemLedgerLoadingState().text).toContain('Loading monitored system usage');
+ expect(getMonitoredSystemLedgerErrorState().retryLabel).toBe('Try again');
+ expect(getMonitoredSystemLedgerUnavailableState().title).toBe(
+ 'Verifying monitored-system inventory',
+ );
+ expect(getMonitoredSystemLedgerPolicyLoadingState().title).toBe(
+ 'Checking monitored-system visibility',
+ );
+ expect(getMonitoredSystemLedgerHiddenState().title).toBe(
+ 'Monitored-system usage is hidden in demo mode',
+ );
+ expect(JSON.stringify(getMonitoredSystemLedgerPresentation())).not.toContain('capacity');
+ expect(JSON.stringify(getMonitoredSystemLedgerPresentation())).not.toContain('limit');
+ });
+
+ it('returns status, source, type, and attribution labels', () => {
expect(getMonitoredSystemStatusFallbackSummary('online')).toBe(
'All included top-level collection paths currently report online status.',
);
- expect(getMonitoredSystemStatusFallbackSummary('warning')).toBe(
- 'At least one included top-level collection path is degraded, so Pulse marks this monitored system as warning.',
- );
- expect(getMonitoredSystemStatusFallbackSummary('offline')).toBe(
- 'At least one included source is offline or disconnected, so Pulse marks this monitored system as offline.',
- );
- expect(getMonitoredSystemStatusFallbackSummary()).toBe(
- 'Pulse cannot determine a canonical runtime status for this monitored system yet.',
- );
+ expect(getMonitoredSystemStatusFallbackSummary('warning')).toContain('degraded');
+ expect(getMonitoredSystemStatusFallbackSummary('offline')).toContain('offline or disconnected');
+ expect(getMonitoredSystemStatusFallbackSummary()).toContain('cannot determine');
+ expect(getMonitoredSystemSourceLabel('vmware')).toBe('VMware');
+ expect(getMonitoredSystemSourceLabel('unknown')).toBe('');
+ expect(getMonitoredSystemSurfaceTypeLabel('docker-host')).toBe('Docker Host');
+ expect(getMonitoredSystemSurfaceTypeLabel('custom-system')).toBe('Custom System');
+ expect(
+ formatMonitoredSystemSurfaceAttribution({
+ name: 'esx-a',
+ type: 'host',
+ source: 'vmware',
+ }),
+ ).toBe('esx-a (Host via VMware)');
+ expect(formatMonitoredSystemGroupedSourcesLabel(1)).toBe('1 grouped source');
+ expect(formatMonitoredSystemGroupedSourcesLabel(3)).toBe('3 grouped sources');
+ expect(
+ formatMonitoredSystemLatestIncludedSignalSentence({
+ attribution: 'esx-a (Host via VMware)',
+ relative: '2m ago',
+ }),
+ ).toBe('Latest included signal: esx-a (Host via VMware), reported 2m ago.');
});
- it('keeps monitored-system usage availability on the shared presentation helper', () => {
- expect(monitoredSystemPresentationSource).toContain(
- 'export function isMonitoredSystemLimitUsageAvailable',
- );
- expect(monitoredSystemPresentationSource).toContain(
- 'export function getMonitoredSystemLimitUsageSummary',
- );
- expect(monitoredSystemPresentationSource).toContain(
- 'export function getMonitoredSystemLimitCapacityStatusSummary',
- );
- expect(monitoredSystemPresentationSource).toContain(
- 'export function resolveMonitoredSystemCapacityStatus',
- );
-
- for (const source of [
- licensePresentationSource,
- monitoredSystemLedgerPanelSource,
- monitoredSystemLimitWarningBannerModelSource,
- proLicensePanelStateSource,
- ]) {
- expect(source).not.toContain('current_available !== false');
- }
- expect(proLicensePanelStateSource).not.toContain("'Verifying…'");
- expect(proLicensePanelStateSource).not.toContain("'Unavailable'");
-
- const paidLimitPhrases = [
- 'active monitored-system policy',
- 'additional monitored-system admissions',
- 'finite policy',
- 'policy boundary',
- 'capacity is available',
- 'Grandfathered monitored-system floor',
- 'effective monitored-system limit',
- 'Plan Monitored System Limit',
- 'Effective Monitored System Limit',
- 'Included Monitored Systems',
- 'remaining before',
- 'Over policy',
- ];
- for (const source of [
- commercialBillingModelSource,
- licensePresentationSource,
- monitoredSystemLedgerPanelSource,
- monitoredSystemLimitWarningBannerModelSource,
- monitoredSystemPresentationSource,
- proLicensePanelStateSource,
- ]) {
- for (const phrase of paidLimitPhrases) {
- expect(source).not.toContain(phrase);
- }
- }
- });
-
- it('returns canonical monitored-system limit warning copy', () => {
- expect(getMonitoredSystemLimitReviewPolicyLabel()).toBe('Review continuity');
- expect(getMonitoredSystemLimitInstallCollectorsLabel()).toBe('Install v6 collectors');
- expect(formatMonitoredSystemLimitSummary({ current: 5, limit: 6 })).toBe(
- '5 monitored systems.',
- );
- expect(formatMonitoredSystemLimitSummary({ current: 16, limit: 5, state: 'enforced' })).toBe(
- 'Continuity review needed. 16 monitored systems.',
- );
- expect(
- formatMonitoredSystemLimitSummary(
- { current: 16, limit: 5, state: 'enforced' },
- {
- mode: 'over_limit_frozen',
- urgency: 'enforced',
- current: 16,
- limit: 5,
- current_available: true,
- available_slots: 0,
- overage: 11,
- reason: 'legacy_migration_capture_pending',
- blocks_new_systems: true,
- existing_monitoring_continues: true,
- },
- ),
- ).toBe('Continuity verification pending. 16 monitored systems.');
- expect(
- formatMonitoredSystemLegacyConnectionBreakdown({
- proxmox_nodes: 2,
- docker_hosts: 1,
- kubernetes_clusters: 0,
- }),
- ).toBe('2 Proxmox nodes, 1 Docker host');
- expect(
- formatMonitoredSystemMigrationMessage({
- proxmox_nodes: 2,
- docker_hosts: 1,
- kubernetes_clusters: 0,
- }),
- ).toBe(
- 'You also have 3 resources connected via API or legacy collectors (2 Proxmox nodes, 1 Docker host) that are folded into the canonical monitored-system ledger when the same top-level system is discovered canonically.',
- );
- expect(formatMonitoredSystemOverflowSummary(14)).toBe(
- 'A temporary setup slot is active (14d remaining)',
- );
- expect(formatMonitoredSystemOverflowSummary(undefined)).toBe('');
- });
-
- it('builds a monitored-system capacity section model for the plan surface', () => {
- expect(
- buildMonitoredSystemCapacitySectionModel({
- current: 16,
- limit: 5,
- current_available: true,
- state: 'enforced',
- }),
- ).toEqual({
- stats: [
- { label: 'Monitored', value: '16 monitored systems' },
- { label: 'Baseline', value: '5' },
- { label: 'Status', value: 'Continuity review' },
- ],
- statusMessage: 'Existing monitoring remains visible. New top-level additions need review.',
- detailMessage:
- 'Review the legacy continuity state before adding another top-level monitored system.',
- explanation: {
- label: 'Why does this need review?',
- body: 'Pulse has already identified 16 monitored systems for this installation. Existing monitoring remains visible, but new top-level additions are paused until this legacy continuity state is reviewed.',
- },
- });
- });
-
- it('does not build a monitored-system capacity section for unmetered or healthy self-hosted states', () => {
- 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();
- expect(
- buildMonitoredSystemCapacitySectionModel({
- current: 7,
- limit: 10,
- current_available: true,
- state: 'ok',
- }),
- ).toBeNull();
- });
-
- it('centralizes monitored-system limit availability and capacity presentation', () => {
- const unavailableLimit = {
- current: 0,
- limit: 10,
- current_available: false,
- current_unavailable_reason: 'supplemental_inventory_unsettled',
- state: 'enforced',
- };
-
- expect(isMonitoredSystemLimitUsageAvailable(unavailableLimit)).toBe(false);
- expect(getMonitoredSystemLimitUnavailableReason(unavailableLimit)).toBe(
- 'supplemental_inventory_unsettled',
- );
- expect(getMonitoredSystemLimitUsageSummary(unavailableLimit)).toBe('Verifying…');
- expect(getMonitoredSystemLimitCapacityStatusSummary(unavailableLimit)).toBe('Unavailable');
- expect(getMonitoredSystemLimitContextSummary(unavailableLimit)).toBe(
- 'Pulse is still collecting the first provider-owned inventory baseline. The monitored-system ledger will appear after that baseline completes.',
- );
- expect(isMonitoredSystemLimitUrgent(unavailableLimit)).toBe(false);
- expect(
- formatMonitoredSystemUsageUnavailableMessage(unavailableLimit.current_unavailable_reason),
- ).toBe(
- 'Pulse is still collecting the first provider-owned inventory baseline. The monitored-system ledger will appear after that baseline completes.',
- );
-
- expect(
- getMonitoredSystemLimitUsageSummary({
- current: 7,
- limit: 10,
- current_available: true,
- }),
- ).toBe('7 monitored systems');
- expect(
- getMonitoredSystemLimitCapacityStatusSummary({
- current: 7,
- limit: 10,
- current_available: true,
- }),
- ).toBe('Healthy');
- expect(
- getMonitoredSystemLimitCapacityStatusSummary({
- current: 7,
- limit: 0,
- current_available: true,
- }),
- ).toBe('Not metered');
- expect(
- getMonitoredSystemLimitContextSummary({
- current: 10,
- limit: 10,
- current_available: true,
- state: 'enforced',
- }),
- ).toBe(
- 'Existing monitoring remains visible. New top-level additions are paused until this legacy continuity state is reviewed.',
- );
- expect(
- getMonitoredSystemLimitContextSummary({
- current: 16,
- limit: 5,
- current_available: true,
- state: 'enforced',
- }),
- ).toBe(
- 'Existing monitoring remains visible. New top-level additions are paused until this legacy continuity state is reviewed.',
- );
- expect(
- getMonitoredSystemLimitContextSummary(
- {
- current: 16,
- limit: 5,
- current_available: true,
- state: 'enforced',
- },
- {
- mode: 'over_limit_frozen',
- urgency: 'enforced',
- current: 16,
- limit: 5,
- current_available: true,
- available_slots: 0,
- overage: 11,
- reason: 'legacy_migration_capture_pending',
- blocks_new_systems: true,
- existing_monitoring_continues: true,
- },
- ),
- ).toBe(
- 'Pulse is verifying legacy v5 continuity for this installation. Existing monitoring remains visible while new top-level additions wait for verification to finish.',
- );
- expect(
- isMonitoredSystemLimitUrgent({
- current: 9,
- limit: 10,
- current_available: true,
- state: 'warning',
- }),
- ).toBe(true);
- expect(
- resolveMonitoredSystemCapacityStatus(undefined, {
- current: 16,
- limit: 5,
- current_available: true,
- state: 'enforced',
- }),
- ).toMatchObject({
- mode: 'over_limit_frozen',
- overage: 11,
- reason: 'preexisting_usage',
- blocks_new_systems: true,
- existing_monitoring_continues: true,
- });
- });
-
- it('returns canonical monitored-system admission unavailable copy', () => {
- expect(getMonitoredSystemAdmissionPreviewRequiredState()).toEqual({
- title: 'Preview monitored-system impact before saving',
- message:
- 'Pulse must preview the monitored-system impact for this platform connection before it can be saved.',
- });
- expect(getMonitoredSystemAdmissionPreviewUnavailableTitle()).toBe(
+ it('returns impact preview copy without quota math', () => {
+ expect(getMonitoredSystemImpactPreviewUnavailableTitle()).toBe(
'Monitored-system verification is temporarily unavailable',
);
- expect(
- formatMonitoredSystemAdmissionPreviewUnavailableMessage('supplemental_inventory_unsettled'),
- ).toBe(
- 'Pulse is still settling provider-owned inventory for this platform connection, so the monitored-system check is not safe yet. Retry preview after the first baseline finishes.',
+ expect(getMonitoredSystemImpactPreviewTitle(null)).toBe('Monitored-system impact');
+ expect(getMonitoredSystemImpactPreviewTitle({ current_count: 4, projected_count: 5 })).toBe(
+ 'This change adds monitored systems',
+ );
+ expect(getMonitoredSystemImpactPreviewTitle({ current_count: 4, projected_count: 3 })).toBe(
+ 'This change removes monitored systems',
+ );
+ expect(getMonitoredSystemImpactPreviewTitle({ current_count: 4, projected_count: 4 })).toBe(
+ 'This change keeps monitored-system count unchanged',
);
expect(
- formatMonitoredSystemAdmissionPreviewUnavailableMessage(
+ formatMonitoredSystemImpactPreviewSummary({
+ current_count: 9,
+ projected_count: 11,
+ }),
+ ).toBe(
+ 'Pulse currently counts 9 monitored systems. Saving this change would bring the count to 11 monitored systems (+2).',
+ );
+ });
+
+ it('returns monitored-system unavailable copy', () => {
+ expect(formatMonitoredSystemLedgerUnavailableMessage('supplemental_inventory_unsettled')).toBe(
+ 'Pulse is still collecting the first provider-owned inventory baseline. The monitored-system ledger will appear after that baseline completes.',
+ );
+ expect(
+ formatMonitoredSystemImpactPreviewUnavailableMessage(
'supplemental_inventory_rebuild_pending',
),
).toBe(
- 'Pulse has settled provider-owned inventory and is rebuilding the canonical monitored-system view, so this connection cannot be saved yet. Retry preview in a moment.',
+ 'Pulse has settled provider-owned inventory and is rebuilding the canonical monitored-system view. You can still save the connection and review the impact in a moment.',
);
expect(
- formatMonitoredSystemAdmissionPreviewUnavailableMessage('monitor_state_unavailable'),
- ).toBe(
- 'Pulse cannot verify monitored-system impact right now, so this connection cannot be saved yet. Retry preview in a moment.',
- );
- expect(
- buildMonitoredSystemAdmissionPreviewUnavailableState({
+ buildMonitoredSystemImpactPreviewUnavailableState({
code: 'monitored_system_usage_unavailable',
reason: ' supplemental_inventory_unsettled ',
}),
@@ -505,156 +130,7 @@ describe('monitoredSystemPresentation', () => {
reason: 'supplemental_inventory_unsettled',
title: 'Monitored-system verification is temporarily unavailable',
message:
- 'Pulse is still settling provider-owned inventory for this platform connection, so the monitored-system check is not safe yet. Retry preview after the first baseline finishes.',
+ 'Pulse is still settling provider-owned inventory for this platform connection. You can still save the connection and review the impact after the first baseline finishes.',
});
- expect(
- buildMonitoredSystemAdmissionPreviewUnavailableState({
- code: 'provider_failed',
- reason: 'supplemental_inventory_unsettled',
- }),
- ).toBeNull();
- expect(
- isMonitoredSystemAdmissionPreviewResolvedSafely({
- preview: { would_exceed_limit: false },
- }),
- ).toBe(true);
- expect(
- isMonitoredSystemAdmissionPreviewResolvedSafely({
- preview: { would_exceed_limit: true },
- }),
- ).toBe(false);
- expect(getMonitoredSystemAdmissionPreviewSaveBlockedMessage({ preview: null })).toBe(
- 'Pulse must preview the monitored-system impact for this platform connection before it can be saved.',
- );
- expect(
- getMonitoredSystemAdmissionPreviewSaveBlockedMessage({
- preview: { would_exceed_limit: true },
- }),
- ).toBe('This change needs monitored-system review before saving');
- expect(
- getMonitoredSystemAdmissionPreviewSaveBlockedMessage({
- preview: { would_exceed_limit: false },
- }),
- ).toBeNull();
- });
-
- it('returns neutral monitored-system admission preview titles', () => {
- expect(getMonitoredSystemAdmissionPreviewTitle(null)).toBe('Monitored-system impact');
- expect(
- getMonitoredSystemAdmissionPreviewTitle({
- current_count: 4,
- projected_count: 5,
- would_exceed_limit: false,
- }),
- ).toBe('This change adds monitored systems');
- expect(
- getMonitoredSystemAdmissionPreviewTitle({
- current_count: 4,
- projected_count: 3,
- would_exceed_limit: false,
- }),
- ).toBe('This change removes monitored systems');
- expect(
- getMonitoredSystemAdmissionPreviewTitle({
- current_count: 4,
- projected_count: 4,
- would_exceed_limit: false,
- }),
- ).toBe('This change keeps monitored-system count unchanged');
- expect(
- getMonitoredSystemAdmissionPreviewTitle({
- current_count: 4,
- projected_count: 11,
- would_exceed_limit: true,
- }),
- ).toBe('This change needs continuity review before saving');
- });
-
- it('formats monitored-system admission preview summaries without quota math', () => {
- expect(
- formatMonitoredSystemAdmissionPreviewSummary({
- current_count: 4,
- projected_count: 4,
- limit: 10,
- }),
- ).toBe(
- 'Pulse currently counts 4 monitored systems. Saving this change would keep the count at 4 monitored systems.',
- );
- expect(
- formatMonitoredSystemAdmissionPreviewSummary({
- current_count: 9,
- projected_count: 11,
- limit: 10,
- would_exceed_limit: true,
- }),
- ).toBe(
- 'Pulse currently counts 9 monitored systems. Saving this change would bring the count to 11 monitored systems (+2), above the current verified baseline of 10 monitored systems.',
- );
- });
-
- it('returns canonical monitored-system ledger unavailable copy', () => {
- expect(formatMonitoredSystemLedgerUnavailableMessage('supplemental_inventory_unsettled')).toBe(
- 'Pulse is still collecting the first provider-owned inventory baseline. The monitored-system ledger will appear after that baseline completes.',
- );
- expect(
- formatMonitoredSystemLedgerUnavailableMessage('supplemental_inventory_rebuild_pending'),
- ).toBe(
- 'Pulse has collected provider-owned inventory and is rebuilding the canonical monitored-system ledger. Usage will appear when that rebuild finishes.',
- );
- expect(formatMonitoredSystemLedgerUnavailableMessage('monitor_state_unavailable')).toBe(
- 'Pulse cannot currently verify monitored-system usage for this installation. Refresh after the monitoring runtime settles.',
- );
- });
-
- it('returns customer-facing source and type labels', () => {
- expect(getMonitoredSystemSourceLabel('agent')).toBe('Agent');
- expect(getMonitoredSystemSourceLabel('multiple')).toBe('Multiple Sources');
- expect(getMonitoredSystemSourceLabel('pbs')).toBe('PBS');
- expect(getMonitoredSystemSourceLabel('vmware')).toBe('VMware');
- expect(getMonitoredSystemSourceLabel('')).toBe('');
- expect(getMonitoredSystemSurfaceTypeLabel('agent')).toBe('Host');
- expect(getMonitoredSystemSurfaceTypeLabel('docker-host')).toBe('Docker Host');
- expect(getMonitoredSystemSurfaceTypeLabel('proxmox-node')).toBe('Proxmox Node');
- expect(getMonitoredSystemSurfaceTypeLabel(undefined)).toBe('System');
- expect(getMonitoredSystemSurfaceTypeLabel('custom_cluster')).toBe('Custom Cluster');
- });
-
- it('formats included signal attribution and summary sentences', () => {
- expect(formatMonitoredSystemGroupedSourcesLabel(1)).toBe('1 grouped source');
- expect(formatMonitoredSystemGroupedSourcesLabel(2)).toBe('2 grouped sources');
- expect(
- formatMonitoredSystemSurfaceAttribution({
- name: 'tower',
- type: 'pbs-server',
- source: 'pbs',
- }),
- ).toBe('tower (PBS Server via PBS)');
- expect(
- formatMonitoredSystemSurfaceAttribution({
- name: 'tower',
- type: 'host',
- source: 'host',
- }),
- ).toBe('tower (Host)');
- expect(
- formatMonitoredSystemSurfaceAttribution({
- name: 'esxi-01',
- type: 'host',
- source: 'vmware',
- }),
- ).toBe('esxi-01 (Host via VMware)');
- expect(
- formatMonitoredSystemSurfaceAttribution({
- name: 'tower',
- type: 'truenas-system',
- source: 'multiple',
- }),
- ).toBe('tower (TrueNAS System via Multiple Sources)');
- expect(
- formatMonitoredSystemLatestIncludedSignalSentence({
- attribution: 'tower (PBS Server via PBS)',
- relative: '2m ago',
- }),
- ).toBe('Latest included signal: tower (PBS Server via PBS), reported 2m ago.');
});
});
diff --git a/frontend-modern/src/utils/__tests__/pricingHandoff.test.ts b/frontend-modern/src/utils/__tests__/pricingHandoff.test.ts
index 63633cc79..6aca2f955 100644
--- a/frontend-modern/src/utils/__tests__/pricingHandoff.test.ts
+++ b/frontend-modern/src/utils/__tests__/pricingHandoff.test.ts
@@ -34,9 +34,9 @@ import {
} from '@/utils/pricingHandoff';
describe('pricingHandoff', () => {
- it('retires legacy monitored-system pricing links onto the neutral plan surface', () => {
+ it('does not keep a special in-product monitored-system pricing route', () => {
expect(getUpgradeFallbackDestination('max_monitored_systems')).toBe(
- SELF_HOSTED_PRO_BILLING_PLAN_HREF,
+ getSelfHostedPurchaseStartUrl('max_monitored_systems'),
);
});
@@ -118,9 +118,9 @@ describe('pricingHandoff', () => {
);
});
- it('keeps retired monitored-system pricing handoffs on the neutral Plans surface', () => {
+ it('treats legacy monitored-system pricing links as external purchase-start compatibility', () => {
expect(getPricingRouteDestination('?feature=max_monitored_systems')).toBe(
- SELF_HOSTED_PRO_BILLING_PLAN_HREF,
+ getSelfHostedPurchaseStartUrl('max_monitored_systems'),
);
});
diff --git a/frontend-modern/src/utils/apiClient.ts b/frontend-modern/src/utils/apiClient.ts
index d992b86a7..ba181e54e 100644
--- a/frontend-modern/src/utils/apiClient.ts
+++ b/frontend-modern/src/utils/apiClient.ts
@@ -83,7 +83,6 @@ type APIErrorShape = Error & {
detail?: string;
details?: APIErrorDetails;
feature?: string;
- monitored_system_preview?: unknown;
requiredScope?: string;
upgrade_url?: string;
status?: number;
@@ -118,7 +117,6 @@ async function createAPIErrorFromResponse(
let errorDetail: string | undefined;
let errorDetails: APIErrorDetails | undefined;
let errorFeature: string | undefined;
- let errorMonitoredSystemPreview: unknown;
let errorRequiredScope: string | undefined;
let errorUpgradeUrl: string | undefined;
try {
@@ -133,12 +131,6 @@ async function createAPIErrorFromResponse(
}
errorCode = sanitizeBoundedText(jsonError.code, MAX_API_ERROR_CODE_LENGTH) ?? undefined;
errorDetails = sanitizeAPIErrorDetails(jsonError.details);
- if (
- jsonError.monitored_system_preview &&
- typeof jsonError.monitored_system_preview === 'object'
- ) {
- errorMonitoredSystemPreview = jsonError.monitored_system_preview;
- }
errorRequiredScope = sanitizeBoundedText(jsonError.requiredScope, 128) ?? undefined;
errorFeature = sanitizeBoundedText(jsonError.feature, 128) ?? undefined;
errorUpgradeUrl = sanitizeBoundedText(jsonError.upgrade_url, 2048) ?? undefined;
@@ -174,9 +166,6 @@ async function createAPIErrorFromResponse(
if (errorFeature) {
err.feature = errorFeature;
}
- if (errorMonitoredSystemPreview) {
- err.monitored_system_preview = errorMonitoredSystemPreview;
- }
if (errorUpgradeUrl) {
err.upgrade_url = errorUpgradeUrl;
}
diff --git a/frontend-modern/src/utils/commercialBillingModel.ts b/frontend-modern/src/utils/commercialBillingModel.ts
index ccfa5e154..2cfdf3c37 100644
--- a/frontend-modern/src/utils/commercialBillingModel.ts
+++ b/frontend-modern/src/utils/commercialBillingModel.ts
@@ -1,5 +1,4 @@
import type { LicenseStatus } from '@/api/license';
-import type { MonitoredSystemContinuityStatus } from '@/api/license';
import type { SelfHostedPlanDefinition } from '@/utils/selfHostedPlans';
export interface CommercialStatValue {
@@ -30,21 +29,16 @@ export interface SelfHostedCommercialModelInput {
planTerms?: string;
expires: string;
daysRemaining: string | number;
- monitoredSystemsSummary: string | number;
- capacityStatusSummary: string | number;
- maxMonitoredSystems: string | number;
retailPlanDefinition?: Pick<
SelfHostedPlanDefinition,
'billingExtrasSummary' | 'metricHistoryDays'
> | null;
- monitoredSystemContinuity?: MonitoredSystemContinuityStatus | null;
- continuityCapturedAt?: string;
}
export interface HostedCommercialModelInput {
status?: Pick<
LicenseStatus,
- 'email' | 'is_lifetime' | 'expires_at' | 'max_monitored_systems' | 'max_guests'
+ 'email' | 'is_lifetime' | 'expires_at' | 'max_guests'
> | null;
tierLabel: string;
licenseStatusLabel: string;
@@ -61,13 +55,6 @@ export const LIFETIME_DAYS_REMAINING_LABEL = 'Permanent';
const asUnlimitedLimit = (value?: number) =>
typeof value === 'number' && value > 0 ? value : undefined;
-const hasFiniteSelfHostedLimit = (value: string | number) =>
- typeof value === 'number'
- ? value > 0
- : !['unlimited', SELF_HOSTED_NOT_METERED_LABEL.toLowerCase()].includes(
- value.trim().toLowerCase(),
- );
-
const buildSelfHostedBaseDetails = (
input: SelfHostedCommercialModelInput,
): CommercialPlanViewModel['details'] => [
@@ -121,75 +108,17 @@ export const buildSelfHostedCommercialPlanModel = (
}
return {
- summary:
- !input.monitoredSystemContinuity && !hasFiniteSelfHostedLimit(input.maxMonitoredSystems)
- ? [
- {
- label: 'Core Monitoring',
- value: 'Included',
- },
- {
- label: 'Plan Status',
- value: input.statusLabel,
- },
- ]
- : [
- {
- label: 'Monitored Systems',
- value: input.monitoredSystemsSummary,
- },
- {
- label: 'Continuity Status',
- value: input.capacityStatusSummary,
- },
- {
- label: 'Plan Status',
- value: input.statusLabel,
- },
- ],
- details: [
- ...buildSelfHostedBaseDetails(input),
- ...(input.monitoredSystemContinuity
- ? [
- {
- label: 'Plan Baseline',
- value:
- input.monitoredSystemContinuity.plan_limit > 0
- ? input.monitoredSystemContinuity.plan_limit
- : SELF_HOSTED_NOT_METERED_LABEL,
- },
- {
- label: 'Current Baseline',
- value:
- input.monitoredSystemContinuity.effective_limit > 0
- ? input.monitoredSystemContinuity.effective_limit
- : SELF_HOSTED_NOT_METERED_LABEL,
- },
- ...(typeof input.monitoredSystemContinuity.grandfathered_floor === 'number' &&
- input.monitoredSystemContinuity.grandfathered_floor > 0
- ? [
- {
- label: 'Observed Legacy Estate',
- value: input.monitoredSystemContinuity.grandfathered_floor,
- },
- ]
- : []),
- {
- label: 'Continuity Verification',
- value: input.monitoredSystemContinuity.capture_pending
- ? 'Pending'
- : input.continuityCapturedAt || 'Captured',
- },
- ]
- : hasFiniteSelfHostedLimit(input.maxMonitoredSystems)
- ? [
- {
- label: 'Recorded Monitoring Baseline',
- value: input.maxMonitoredSystems,
- },
- ]
- : []),
+ summary: [
+ {
+ label: 'Core Monitoring',
+ value: 'Included',
+ },
+ {
+ label: 'Plan Status',
+ value: input.statusLabel,
+ },
],
+ details: buildSelfHostedBaseDetails(input),
};
};
@@ -233,7 +162,6 @@ export const buildHostedCommercialUsageModel = (
{
label: 'Monitored Systems',
current: input.nodeUsage,
- limit: asUnlimitedLimit(input.status?.max_monitored_systems),
accentClass: 'bg-blue-600 dark:bg-blue-500',
},
{
diff --git a/frontend-modern/src/utils/deployStatusPresentation.ts b/frontend-modern/src/utils/deployStatusPresentation.ts
index ddbd62a9b..3431169bb 100644
--- a/frontend-modern/src/utils/deployStatusPresentation.ts
+++ b/frontend-modern/src/utils/deployStatusPresentation.ts
@@ -43,10 +43,6 @@ const DEPLOY_STATUS_PRESENTATION: Record {
- if (isLifetime) {
- return true;
- }
- return isGrandfatheredRecurringV5PlanVersion(planVersion);
-};
-
const isActiveOrGraceSubscription = (subscriptionState?: string | null): boolean => {
const normalized = (subscriptionState || '').trim().toLowerCase();
return normalized === 'active' || normalized === 'grace';
};
-export const hasActiveUncappedSelfHostedContinuity = ({
- planVersion,
- isLifetime,
- subscriptionState,
-}: {
- planVersion?: string | null;
- isLifetime?: boolean | null;
- subscriptionState?: string | null;
-}): boolean => {
- if (isLifetime) {
- return true;
- }
- return (
- isActiveOrGraceSubscription(subscriptionState) &&
- isGrandfatheredRecurringV5PlanVersion(planVersion)
- );
-};
-
-export const getDisplayableMonitoredSystemContinuity = ({
- continuity,
- planVersion,
- isLifetime,
- subscriptionState,
-}: {
- continuity?: MonitoredSystemContinuityStatus | null;
- planVersion?: string | null;
- isLifetime?: boolean | null;
- subscriptionState?: string | null;
-}): MonitoredSystemContinuityStatus | null => {
- if (!continuity) {
- return null;
- }
- if (hasActiveUncappedSelfHostedContinuity({ planVersion, isLifetime, subscriptionState })) {
- return null;
- }
- if (typeof continuity.effective_limit === 'number' && continuity.effective_limit <= 0) {
- return null;
- }
- return continuity;
-};
-
export const getLicenseTierLabel = (tier?: string | null): string => {
const normalized = (tier || '').trim().toLowerCase();
if (!normalized) return 'Unknown';
@@ -271,78 +211,6 @@ export const getGrandfatheredPriceContinuityNotice = (
};
};
-export const getMonitoredSystemContinuityNotice = (
- continuity?: MonitoredSystemContinuityStatus | null,
- limit?: MonitoredSystemLimitUsageStatus | null,
- capacity?: MonitoredSystemCapacityStatus | null,
- context?: {
- planVersion?: string | null;
- isLifetime?: boolean | null;
- subscriptionState?: string | null;
- },
-): LicenseInlineNotice | null => {
- const displayContinuity = getDisplayableMonitoredSystemContinuity({
- continuity,
- planVersion: context?.planVersion,
- isLifetime: context?.isLifetime,
- subscriptionState: context?.subscriptionState,
- });
- if (!displayContinuity) {
- return null;
- }
- const resolvedCapacity = resolveMonitoredSystemCapacityStatus(capacity, limit);
-
- if (displayContinuity.capture_pending) {
- if (!resolvedCapacity?.current_available) {
- return {
- tone: 'border-amber-200 dark:border-amber-900 bg-amber-50 dark:bg-amber-900 text-amber-900 dark:text-amber-100',
- title: 'Legacy continuity verification pending',
- body: `Pulse is still verifying legacy v5 monitoring continuity for this installation. ${formatMonitoredSystemUsageUnavailableMessage(
- getMonitoredSystemLimitUnavailableReason(limit, capacity),
- )}`,
- };
- }
-
- if (resolvedCapacity.mode === 'over_limit_frozen') {
- return {
- tone: 'border-amber-200 dark:border-amber-900 bg-amber-50 dark:bg-amber-900 text-amber-900 dark:text-amber-100',
- title: 'Legacy continuity verification pending',
- body: `Pulse is still verifying legacy v5 monitoring continuity for this installation. Pulse has already identified ${resolvedCapacity.current} monitored systems for continuity reporting, and existing monitoring remains visible while new top-level additions wait for verification to finish.`,
- };
- }
-
- return {
- tone: 'border-amber-200 dark:border-amber-900 bg-amber-50 dark:bg-amber-900 text-amber-900 dark:text-amber-100',
- title: 'Legacy continuity verification pending',
- body: 'Pulse is still verifying legacy v5 monitoring continuity for this installation. Existing monitoring remains visible while Pulse finalizes the continuity baseline.',
- };
- }
-
- if (!isMonitoredSystemLimitUsageAvailable(limit)) {
- return {
- tone: 'border-amber-200 dark:border-amber-900 bg-amber-50 dark:bg-amber-900 text-amber-900 dark:text-amber-100',
- title: 'Monitored-system usage unavailable',
- body: formatMonitoredSystemUsageUnavailableMessage(
- getMonitoredSystemLimitUnavailableReason(limit, capacity),
- ),
- };
- }
-
- if (
- typeof displayContinuity.grandfathered_floor === 'number' &&
- displayContinuity.grandfathered_floor > 0 &&
- displayContinuity.effective_limit > displayContinuity.plan_limit
- ) {
- return {
- tone: 'border-green-200 dark:border-green-900 bg-green-50 dark:bg-green-900 text-green-900 dark:text-green-100',
- title: 'Legacy monitoring continuity',
- body: `This migrated v5 installation keeps its observed legacy estate available for continuity reporting on this instance. Pulse recorded ${displayContinuity.grandfathered_floor} monitored systems during migration.`,
- };
- }
-
- return null;
-};
-
const getSelfHostedUnlockedFeatures = ({
entitlements,
displayableCapabilities,
@@ -492,11 +360,6 @@ export const getSelfHostedCurrentPlanPresentation = ({
const normalizedTier = (current.tier || '').trim().toLowerCase();
const planLabel = getSelfHostedPlanLabel(current.tier);
const planDefinition = getSelfHostedPlanDefinitionForBillingTier(current.tier);
- const hasUncappedContinuity = hasActiveUncappedSelfHostedContinuity({
- planVersion: current.plan_version,
- isLifetime: current.is_lifetime,
- subscriptionState: current.subscription_state,
- });
const unlockedFeatures = getSelfHostedUnlockedFeatures({
entitlements: current,
displayableCapabilities,
@@ -518,38 +381,13 @@ export const getSelfHostedCurrentPlanPresentation = ({
supplementalDetails.push(
'This migrated v5 subscription keeps its existing recurring price until cancellation. Self-hosted monitoring and child-resource volume are not metered in current v6 self-hosted packaging.',
);
- } else if (hasUncappedContinuity && current.is_lifetime) {
+ } else if (current.is_lifetime) {
supplementalBadges.push('Grandfathered lifetime');
supplementalDetails.push(
'This migrated lifetime install remains valid permanently, and self-hosted monitoring plus child-resource volume are not metered in current v6 self-hosted packaging.',
);
}
- const continuity = planDefinition
- ? null
- : getDisplayableMonitoredSystemContinuity({
- continuity: current.monitored_system_continuity,
- planVersion: current.plan_version,
- isLifetime: current.is_lifetime,
- subscriptionState: current.subscription_state,
- });
- if (continuity?.capture_pending) {
- supplementalBadges.push('Continuity pending');
- supplementalDetails.push(
- 'Pulse is still verifying legacy v5 monitoring continuity for this migrated installation.',
- );
- } else if (
- continuity &&
- typeof continuity.grandfathered_floor === 'number' &&
- continuity.grandfathered_floor > 0 &&
- continuity.effective_limit > continuity.plan_limit
- ) {
- supplementalBadges.push('Legacy continuity');
- supplementalDetails.push(
- 'This migrated installation keeps the observed legacy estate available for continuity reporting on this instance.',
- );
- }
-
if (normalizedState === 'trial') {
return {
title: `Current plan: ${planLabel} Trial`,
diff --git a/frontend-modern/src/utils/monitoredSystemPresentation.ts b/frontend-modern/src/utils/monitoredSystemPresentation.ts
index aae9a171d..a09a24608 100644
--- a/frontend-modern/src/utils/monitoredSystemPresentation.ts
+++ b/frontend-modern/src/utils/monitoredSystemPresentation.ts
@@ -18,23 +18,13 @@ const MONITORED_SYSTEM_LEDGER_PRESENTATION = {
disclosureDefinition:
'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.',
ledgerDescription:
- 'Review the top-level monitored systems Pulse has identified for reporting, migration continuity, and support context.',
+ 'Review the top-level monitored systems Pulse has identified for reporting and support context.',
tableNameLabel: 'Name',
tableStatusLabel: 'Status',
tableLatestIncludedSignalLabel: 'Latest Included Signal',
countedSystemBadgeLabel: 'Counts as 1 monitored system',
groupedSourcesHeading: 'Grouped sources',
countingExplanationHeading: 'Why this counts',
- continuityHeading: 'Legacy continuity',
- continuityPlanLimitLabel: 'Plan baseline',
- continuityEffectiveLimitLabel: 'Current baseline',
- continuityGrandfatheredFloorLabel: 'Observed legacy estate',
- continuityCaptureLabel: 'Verification',
- continuityCapturePendingLabel: 'Pending',
- continuityCaptureCapturedLabel: 'Captured',
- usageVerifyingLabel: 'Verifying…',
- remainingCapacityUnavailableLabel: 'Unavailable',
- unlimitedLimitLabel: 'Not metered',
loadingState: {
text: 'Loading monitored system usage…',
},
@@ -79,119 +69,43 @@ const MONITORED_SYSTEM_LEDGER_PRESENTATION = {
'At least one included source is offline or disconnected, so Pulse marks this monitored system as offline.',
unknown: 'Pulse cannot determine a canonical runtime status for this monitored system yet.',
},
- limitBanner: {
- reviewPolicyLabel: 'Review continuity',
- installCollectorsLabel: 'Install v6 collectors',
- overflowSummaryPrefix: 'A temporary setup slot is active',
- legacyConnectionSuffix:
- 'that are folded into the canonical monitored-system ledger when the same top-level system is discovered canonically.',
- },
- admissionPreview: {
- requiredTitle: 'Preview monitored-system impact before saving',
- requiredMessage:
- 'Pulse must preview the monitored-system impact for this platform connection before it can be saved.',
+ impactPreview: {
fallbackTitle: 'Monitored-system impact',
- exceedsPolicyTitle: 'This change needs continuity review before saving',
addsSystemsTitle: 'This change adds monitored systems',
removesSystemsTitle: 'This change removes monitored systems',
unchangedTitle: 'This change keeps monitored-system count unchanged',
unavailableTitle: 'Monitored-system verification is temporarily unavailable',
unavailableFallbackMessage:
- 'Pulse cannot verify monitored-system impact right now, so this connection cannot be saved yet. Retry preview in a moment.',
+ 'Pulse cannot verify monitored-system impact right now. You can still save the connection and review the impact after inventory refreshes.',
unavailableUnsettledMessage:
- 'Pulse is still settling provider-owned inventory for this platform connection, so the monitored-system check is not safe yet. Retry preview after the first baseline finishes.',
+ 'Pulse is still settling provider-owned inventory for this platform connection. You can still save the connection and review the impact after the first baseline finishes.',
unavailableRebuildPendingMessage:
- 'Pulse has settled provider-owned inventory and is rebuilding the canonical monitored-system view, so this connection cannot be saved yet. Retry preview in a moment.',
- saveBlockedLimitMessage: 'This change needs monitored-system review before saving',
- saveBlockedLoadingMessage: 'Wait for the monitored-system impact preview to finish',
+ 'Pulse has settled provider-owned inventory and is rebuilding the canonical monitored-system view. You can still save the connection and review the impact in a moment.',
},
} as const;
const MONITORED_SYSTEM_USAGE_UNAVAILABLE_ERROR_CODE = 'monitored_system_usage_unavailable';
-export type MonitoredSystemLegacyConnectionCounts = {
- proxmox_nodes: number;
- docker_hosts: number;
- kubernetes_clusters: number;
-};
-
-export type MonitoredSystemLimitUsageStatus = {
- current?: number | null;
- limit?: number | null;
- current_available?: boolean | null;
- current_unavailable_reason?: string | null;
- state?: string | null;
-};
-
-export type MonitoredSystemCapacityStatus = {
- mode?: string | null;
- urgency?: string | null;
- current?: number | null;
- limit?: number | null;
- current_available?: boolean | null;
- current_unavailable_reason?: string | null;
- available_slots?: number | null;
- overage?: number | null;
- reason?: string | null;
- blocks_new_systems?: boolean | null;
- existing_monitoring_continues?: boolean | null;
-};
-
-type ResolvedMonitoredSystemCapacityStatus = {
- mode: string;
- urgency: string;
- current: number;
- limit: number;
- current_available: boolean;
- current_unavailable_reason?: string;
- available_slots: number;
- overage: number;
- reason?: string;
- blocks_new_systems: boolean;
- existing_monitoring_continues: boolean;
-};
-
-export type MonitoredSystemAdmissionPreviewUnavailableState = {
+export type MonitoredSystemImpactPreviewUnavailableState = {
reason: string | null;
title: string;
message: string;
};
-export type MonitoredSystemCapacitySectionModel = {
- stats: Array<{ label: string; value: string }>;
- statusMessage: string;
- detailMessage?: string;
- explanation?: {
- label: string;
- body: string;
- };
-};
-
-export type MonitoredSystemAdmissionPreviewSaveState = {
- preview?: { would_exceed_limit?: boolean | null } | null;
- unavailableState?: MonitoredSystemAdmissionPreviewUnavailableState | null;
- error?: string | null;
- loading?: boolean | null;
-};
-
-export type MonitoredSystemAdmissionPreviewTitleInput = {
+export type MonitoredSystemImpactPreviewTitleInput = {
current_count?: number | null;
projected_count?: number | null;
- would_exceed_limit?: boolean | null;
};
-export type MonitoredSystemAdmissionPreviewSummaryInput =
- MonitoredSystemAdmissionPreviewTitleInput & {
- limit?: number | null;
- };
+export type MonitoredSystemImpactPreviewSummaryInput = MonitoredSystemImpactPreviewTitleInput;
-const normalizeAdmissionPreviewCount = (count: number | null | undefined): number =>
+const normalizeImpactPreviewCount = (count: number | null | undefined): number =>
typeof count === 'number' && Number.isFinite(count) ? Math.max(0, count) : 0;
-const formatAdmissionPreviewCount = (count: number): string =>
+const formatImpactPreviewCount = (count: number): string =>
`${count} monitored ${count === 1 ? 'system' : 'systems'}`;
-const formatAdmissionPreviewDelta = (delta: number): string =>
+const formatImpactPreviewDelta = (delta: number): string =>
delta > 0 ? `+${delta}` : `${delta}`;
export function getMonitoredSystemLedgerPresentation() {
@@ -251,340 +165,6 @@ export function formatMonitoredSystemLedgerUnavailableMessage(reason?: string |
return formatMonitoredSystemUsageUnavailableMessage(reason);
}
-export function isMonitoredSystemLimitUsageAvailable(
- limit?: MonitoredSystemLimitUsageStatus | null,
-): boolean {
- return limit?.current_available !== false;
-}
-
-export function getMonitoredSystemLimitUnavailableReason(
- limit?: MonitoredSystemLimitUsageStatus | null,
- capacity?: MonitoredSystemCapacityStatus | null,
-): string | undefined {
- const resolved = resolveMonitoredSystemCapacityStatus(capacity, limit);
- if (resolved?.current_available) return undefined;
- return resolved?.current_unavailable_reason?.trim() || undefined;
-}
-
-function deriveMonitoredSystemLimitUrgency(current: number, limit: number): string {
- if (limit <= 0) return 'ok';
- if (current >= limit) return 'enforced';
- if (limit > 1 && limit <= 10) {
- return current >= limit - 1 ? 'warning' : 'ok';
- }
- return current * 10 >= limit * 9 ? 'warning' : 'ok';
-}
-
-function deriveMonitoredSystemCapacityStatus(
- limit?: MonitoredSystemLimitUsageStatus | null,
-): ResolvedMonitoredSystemCapacityStatus | undefined {
- if (!limit) return undefined;
-
- const currentAvailable = isMonitoredSystemLimitUsageAvailable(limit);
- const current = typeof limit.current === 'number' ? limit.current : 0;
- const planLimit = typeof limit.limit === 'number' ? limit.limit : 0;
- const urgency =
- normalizeMonitoredSystemValue(limit.state ?? undefined) ||
- deriveMonitoredSystemLimitUrgency(current, planLimit);
-
- if (!currentAvailable) {
- return {
- mode: 'usage_unavailable',
- urgency: 'ok',
- current: 0,
- limit: planLimit,
- current_available: false,
- current_unavailable_reason: limit.current_unavailable_reason?.trim() || undefined,
- available_slots: 0,
- overage: 0,
- reason: undefined,
- blocks_new_systems: false,
- existing_monitoring_continues: false,
- };
- }
-
- if (planLimit <= 0) {
- return {
- mode: 'unlimited',
- urgency: 'ok',
- current,
- limit: 0,
- current_available: true,
- available_slots: 0,
- overage: 0,
- reason: undefined,
- blocks_new_systems: false,
- existing_monitoring_continues: true,
- };
- }
-
- if (current > planLimit) {
- return {
- mode: 'over_limit_frozen',
- urgency: 'enforced',
- current,
- limit: planLimit,
- current_available: true,
- available_slots: 0,
- overage: current - planLimit,
- reason: 'preexisting_usage',
- blocks_new_systems: true,
- existing_monitoring_continues: true,
- };
- }
-
- if (current === planLimit) {
- return {
- mode: 'at_limit_blocking_new',
- urgency: 'enforced',
- current,
- limit: planLimit,
- current_available: true,
- available_slots: 0,
- overage: 0,
- reason: 'limit_reached',
- blocks_new_systems: true,
- existing_monitoring_continues: true,
- };
- }
-
- return {
- mode: 'within_limit',
- urgency,
- current,
- limit: planLimit,
- current_available: true,
- available_slots: planLimit - current,
- overage: 0,
- reason: undefined,
- blocks_new_systems: false,
- existing_monitoring_continues: true,
- };
-}
-
-export function resolveMonitoredSystemCapacityStatus(
- capacity?: MonitoredSystemCapacityStatus | null,
- limit?: MonitoredSystemLimitUsageStatus | null,
-): ResolvedMonitoredSystemCapacityStatus | undefined {
- const fallback = deriveMonitoredSystemCapacityStatus(limit);
- if (!capacity) {
- return fallback;
- }
-
- const current =
- typeof capacity.current === 'number' ? capacity.current : (fallback?.current ?? 0);
- const planLimit = typeof capacity.limit === 'number' ? capacity.limit : (fallback?.limit ?? 0);
- const currentAvailable =
- typeof capacity.current_available === 'boolean'
- ? capacity.current_available
- : (fallback?.current_available ?? true);
- const mode =
- normalizeMonitoredSystemValue(capacity.mode ?? undefined) ||
- fallback?.mode ||
- 'usage_unavailable';
- const urgency =
- normalizeMonitoredSystemValue(capacity.urgency ?? undefined) ||
- fallback?.urgency ||
- deriveMonitoredSystemLimitUrgency(current, planLimit);
- const reason =
- normalizeMonitoredSystemValue(capacity.reason ?? undefined) || fallback?.reason || undefined;
-
- return {
- mode,
- urgency,
- current,
- limit: planLimit,
- current_available: currentAvailable,
- current_unavailable_reason:
- capacity.current_unavailable_reason?.trim() ||
- fallback?.current_unavailable_reason ||
- undefined,
- available_slots:
- typeof capacity.available_slots === 'number'
- ? capacity.available_slots
- : (fallback?.available_slots ?? Math.max(planLimit - current, 0)),
- overage:
- typeof capacity.overage === 'number'
- ? capacity.overage
- : (fallback?.overage ?? Math.max(current - planLimit, 0)),
- reason,
- blocks_new_systems:
- typeof capacity.blocks_new_systems === 'boolean'
- ? capacity.blocks_new_systems
- : (fallback?.blocks_new_systems ?? false),
- existing_monitoring_continues:
- typeof capacity.existing_monitoring_continues === 'boolean'
- ? capacity.existing_monitoring_continues
- : (fallback?.existing_monitoring_continues ?? currentAvailable),
- };
-}
-
-function formatMonitoredSystemCount(value: number): string {
- return `${value} monitored system${value === 1 ? '' : 's'}`;
-}
-
-export function getMonitoredSystemLimitUsageSummary(
- limit?: MonitoredSystemLimitUsageStatus | null,
- capacity?: MonitoredSystemCapacityStatus | null,
-): string {
- const resolved = resolveMonitoredSystemCapacityStatus(capacity, limit);
- if (!resolved || !resolved.current_available) {
- return MONITORED_SYSTEM_LEDGER_PRESENTATION.usageVerifyingLabel;
- }
- return formatMonitoredSystemCount(resolved.current);
-}
-
-export function getMonitoredSystemLimitCapacityStatusSummary(
- limit?: MonitoredSystemLimitUsageStatus | null,
- capacity?: MonitoredSystemCapacityStatus | null,
-): string {
- const resolved = resolveMonitoredSystemCapacityStatus(capacity, limit);
- if (!resolved || !resolved.current_available) {
- return MONITORED_SYSTEM_LEDGER_PRESENTATION.remainingCapacityUnavailableLabel;
- }
-
- switch (resolved.mode) {
- case 'unlimited':
- return MONITORED_SYSTEM_LEDGER_PRESENTATION.unlimitedLimitLabel;
- case 'over_limit_frozen':
- return 'Continuity review';
- case 'at_limit_blocking_new':
- return 'Continuity review';
- default:
- return 'Healthy';
- }
-}
-
-export function getMonitoredSystemLimitContextSummary(
- limit?: MonitoredSystemLimitUsageStatus | null,
- capacity?: MonitoredSystemCapacityStatus | null,
-): string {
- const resolved = resolveMonitoredSystemCapacityStatus(capacity, limit);
- if (!resolved) {
- return '';
- }
- if (!resolved.current_available) {
- return formatMonitoredSystemUsageUnavailableMessage(resolved.current_unavailable_reason);
- }
-
- switch (resolved.mode) {
- case 'unlimited':
- return 'Self-hosted monitoring is included for this instance.';
- case 'over_limit_frozen':
- if (resolved.reason === 'legacy_migration_capture_pending') {
- return 'Pulse is verifying legacy v5 continuity for this installation. Existing monitoring remains visible while new top-level additions wait for verification to finish.';
- }
- return 'Existing monitoring remains visible. New top-level additions are paused until this legacy continuity state is reviewed.';
- case 'at_limit_blocking_new':
- return 'Existing monitoring remains visible. New top-level additions are paused until this legacy continuity state is reviewed.';
- default:
- return '';
- }
-}
-
-export function buildMonitoredSystemCapacitySectionModel(
- limit?: MonitoredSystemLimitUsageStatus | null,
- capacity?: MonitoredSystemCapacityStatus | null,
-): MonitoredSystemCapacitySectionModel | null {
- const resolved = resolveMonitoredSystemCapacityStatus(capacity, limit);
- if (!resolved) {
- return null;
- }
- if (resolved.limit <= 0) {
- return null;
- }
- if (
- resolved.current_available &&
- resolved.mode !== 'over_limit_frozen' &&
- resolved.mode !== 'at_limit_blocking_new'
- ) {
- return null;
- }
-
- const includedValue =
- resolved.limit > 0
- ? String(resolved.limit)
- : MONITORED_SYSTEM_LEDGER_PRESENTATION.remainingCapacityUnavailableLabel;
-
- const stats = [
- {
- label: 'Monitored',
- value: getMonitoredSystemLimitUsageSummary(limit, capacity),
- },
- {
- label: 'Baseline',
- value: includedValue,
- },
- {
- label: 'Status',
- value: getMonitoredSystemLimitCapacityStatusSummary(limit, capacity),
- },
- ];
-
- if (!resolved.current_available) {
- return {
- stats,
- statusMessage: 'Pulse is verifying monitored-system usage for this installation.',
- detailMessage: formatMonitoredSystemUsageUnavailableMessage(
- resolved.current_unavailable_reason,
- ),
- };
- }
-
- switch (resolved.mode) {
- case 'unlimited':
- return {
- stats,
- statusMessage: 'Self-hosted monitoring is included for this instance.',
- };
- case 'at_limit_blocking_new':
- return {
- stats,
- statusMessage: 'Existing monitoring remains visible. New top-level additions need review.',
- detailMessage:
- 'Review the legacy continuity state before adding another top-level monitored system.',
- };
- case 'over_limit_frozen':
- if (resolved.reason === 'legacy_migration_capture_pending') {
- return {
- stats,
- statusMessage:
- 'Existing monitoring remains visible. New top-level additions wait for verification.',
- detailMessage: 'Pulse is still verifying legacy v5 continuity for this installation.',
- explanation: {
- label: 'Why is continuity still pending?',
- body: `Pulse is still verifying legacy v5 continuity for this installation. It has already identified ${resolved.current} monitored systems for continuity reporting, and existing monitoring remains visible while new top-level additions wait for verification to finish.`,
- },
- };
- }
- return {
- stats,
- statusMessage: 'Existing monitoring remains visible. New top-level additions need review.',
- detailMessage:
- 'Review the legacy continuity state before adding another top-level monitored system.',
- explanation: {
- label: 'Why does this need review?',
- body: `Pulse has already identified ${resolved.current} monitored systems for this installation. Existing monitoring remains visible, but new top-level additions are paused until this legacy continuity state is reviewed.`,
- },
- };
- default:
- return {
- stats,
- statusMessage: 'Current monitored-system accounting is healthy.',
- };
- }
-}
-
-export function isMonitoredSystemLimitUrgent(
- limit?: MonitoredSystemLimitUsageStatus | null,
- capacity?: MonitoredSystemCapacityStatus | null,
-): boolean {
- const resolved = resolveMonitoredSystemCapacityStatus(capacity, limit);
- if (!resolved?.current_available) return false;
- const state = normalizeMonitoredSystemValue(resolved.urgency);
- return state === 'warning' || state === 'enforced';
-}
-
export function getMonitoredSystemCountingDetailsToggleLabel(expanded: boolean): string {
return expanded
? MONITORED_SYSTEM_LEDGER_PRESENTATION.countingDetailsExpandedLabel
@@ -601,39 +181,20 @@ export function getMonitoredSystemStatusFallbackSummary(
return MONITORED_SYSTEM_LEDGER_PRESENTATION.statusSummaryByStatus[status];
}
-export function getMonitoredSystemLimitReviewPolicyLabel(): string {
- return MONITORED_SYSTEM_LEDGER_PRESENTATION.limitBanner.reviewPolicyLabel;
+export function getMonitoredSystemImpactPreviewUnavailableTitle(): string {
+ return MONITORED_SYSTEM_LEDGER_PRESENTATION.impactPreview.unavailableTitle;
}
-export function getMonitoredSystemLimitInstallCollectorsLabel(): string {
- return MONITORED_SYSTEM_LEDGER_PRESENTATION.limitBanner.installCollectorsLabel;
-}
-
-export function getMonitoredSystemAdmissionPreviewUnavailableTitle(): string {
- return MONITORED_SYSTEM_LEDGER_PRESENTATION.admissionPreview.unavailableTitle;
-}
-
-export function getMonitoredSystemAdmissionPreviewRequiredState(): {
- title: string;
- message: string;
-} {
- return {
- title: MONITORED_SYSTEM_LEDGER_PRESENTATION.admissionPreview.requiredTitle,
- message: MONITORED_SYSTEM_LEDGER_PRESENTATION.admissionPreview.requiredMessage,
- };
-}
-
-export function getMonitoredSystemAdmissionPreviewTitle(
- preview: MonitoredSystemAdmissionPreviewTitleInput | null | undefined,
+export function getMonitoredSystemImpactPreviewTitle(
+ preview: MonitoredSystemImpactPreviewTitleInput | null | undefined,
): string {
- const presentation = MONITORED_SYSTEM_LEDGER_PRESENTATION.admissionPreview;
+ const presentation = MONITORED_SYSTEM_LEDGER_PRESENTATION.impactPreview;
if (!preview) return presentation.fallbackTitle;
- if (preview.would_exceed_limit) return presentation.exceedsPolicyTitle;
- const current = normalizeAdmissionPreviewCount(preview.current_count);
+ const current = normalizeImpactPreviewCount(preview.current_count);
const projected =
typeof preview.projected_count === 'number' && Number.isFinite(preview.projected_count)
- ? normalizeAdmissionPreviewCount(preview.projected_count)
+ ? normalizeImpactPreviewCount(preview.projected_count)
: current;
const delta = projected - current;
@@ -642,48 +203,43 @@ export function getMonitoredSystemAdmissionPreviewTitle(
return presentation.unchangedTitle;
}
-export function formatMonitoredSystemAdmissionPreviewSummary(
- preview: MonitoredSystemAdmissionPreviewSummaryInput,
+export function formatMonitoredSystemImpactPreviewSummary(
+ preview: MonitoredSystemImpactPreviewSummaryInput,
): string {
- const current = normalizeAdmissionPreviewCount(preview.current_count);
+ const current = normalizeImpactPreviewCount(preview.current_count);
const projected =
typeof preview.projected_count === 'number' && Number.isFinite(preview.projected_count)
- ? normalizeAdmissionPreviewCount(preview.projected_count)
+ ? normalizeImpactPreviewCount(preview.projected_count)
: current;
- const limit = normalizeAdmissionPreviewCount(preview.limit);
const delta = projected - current;
- const policySuffix =
- preview.would_exceed_limit && limit > 0
- ? `, above the current verified baseline of ${formatAdmissionPreviewCount(limit)}`
- : '';
- const currentSummary = `Pulse currently counts ${formatAdmissionPreviewCount(current)}.`;
+ const currentSummary = `Pulse currently counts ${formatImpactPreviewCount(current)}.`;
if (delta !== 0) {
- return `${currentSummary} Saving this change would bring the count to ${formatAdmissionPreviewCount(
+ return `${currentSummary} Saving this change would bring the count to ${formatImpactPreviewCount(
projected,
- )} (${formatAdmissionPreviewDelta(delta)})${policySuffix}.`;
+ )} (${formatImpactPreviewDelta(delta)}).`;
}
- return `${currentSummary} Saving this change would keep the count at ${formatAdmissionPreviewCount(
+ return `${currentSummary} Saving this change would keep the count at ${formatImpactPreviewCount(
projected,
- )}${policySuffix}.`;
+ )}.`;
}
-export function formatMonitoredSystemAdmissionPreviewUnavailableMessage(reason?: string): string {
+export function formatMonitoredSystemImpactPreviewUnavailableMessage(reason?: string): string {
switch (normalizeMonitoredSystemValue(reason)) {
case 'supplemental_inventory_unsettled':
- return MONITORED_SYSTEM_LEDGER_PRESENTATION.admissionPreview.unavailableUnsettledMessage;
+ return MONITORED_SYSTEM_LEDGER_PRESENTATION.impactPreview.unavailableUnsettledMessage;
case 'supplemental_inventory_rebuild_pending':
- return MONITORED_SYSTEM_LEDGER_PRESENTATION.admissionPreview.unavailableRebuildPendingMessage;
+ return MONITORED_SYSTEM_LEDGER_PRESENTATION.impactPreview.unavailableRebuildPendingMessage;
default:
- return MONITORED_SYSTEM_LEDGER_PRESENTATION.admissionPreview.unavailableFallbackMessage;
+ return MONITORED_SYSTEM_LEDGER_PRESENTATION.impactPreview.unavailableFallbackMessage;
}
}
-export function buildMonitoredSystemAdmissionPreviewUnavailableState(input: {
+export function buildMonitoredSystemImpactPreviewUnavailableState(input: {
code?: string | null;
reason?: string | null;
-}): MonitoredSystemAdmissionPreviewUnavailableState | null {
+}): MonitoredSystemImpactPreviewUnavailableState | null {
if (normalizeMonitoredSystemValue(input.code) !== MONITORED_SYSTEM_USAGE_UNAVAILABLE_ERROR_CODE) {
return null;
}
@@ -691,120 +247,11 @@ export function buildMonitoredSystemAdmissionPreviewUnavailableState(input: {
const reason = input.reason?.trim() || null;
return {
reason,
- title: getMonitoredSystemAdmissionPreviewUnavailableTitle(),
- message: formatMonitoredSystemAdmissionPreviewUnavailableMessage(reason ?? undefined),
+ title: getMonitoredSystemImpactPreviewUnavailableTitle(),
+ message: formatMonitoredSystemImpactPreviewUnavailableMessage(reason ?? undefined),
};
}
-export function isMonitoredSystemAdmissionPreviewResolvedSafely(
- state: MonitoredSystemAdmissionPreviewSaveState,
-): boolean {
- return (
- !state.loading &&
- Boolean(state.preview) &&
- state.preview?.would_exceed_limit !== true &&
- !state.unavailableState &&
- !state.error?.trim()
- );
-}
-
-export function getMonitoredSystemAdmissionPreviewSaveBlockedMessage(
- state: MonitoredSystemAdmissionPreviewSaveState,
-): string | null {
- if (isMonitoredSystemAdmissionPreviewResolvedSafely(state)) {
- return null;
- }
-
- if (state.loading) {
- return MONITORED_SYSTEM_LEDGER_PRESENTATION.admissionPreview.saveBlockedLoadingMessage;
- }
- if (state.unavailableState) {
- return state.unavailableState.message;
- }
- if (state.preview?.would_exceed_limit) {
- return MONITORED_SYSTEM_LEDGER_PRESENTATION.admissionPreview.saveBlockedLimitMessage;
- }
-
- const error = state.error?.trim();
- if (error) {
- return error;
- }
-
- return MONITORED_SYSTEM_LEDGER_PRESENTATION.admissionPreview.requiredMessage;
-}
-
-export function formatMonitoredSystemLimitSummary(
- limit: {
- current: number;
- limit: number;
- current_available?: boolean | null;
- current_unavailable_reason?: string | null;
- state?: string | null;
- },
- capacity?: MonitoredSystemCapacityStatus | null,
-): string {
- const resolved = resolveMonitoredSystemCapacityStatus(capacity, limit);
- if (!resolved || !resolved.current_available) {
- return MONITORED_SYSTEM_LEDGER_PRESENTATION.usageVerifyingLabel;
- }
-
- switch (resolved.mode) {
- case 'over_limit_frozen':
- if (resolved.reason === 'legacy_migration_capture_pending') {
- return `Continuity verification pending. ${formatMonitoredSystemCount(
- resolved.current,
- )}.`;
- }
- return `Continuity review needed. ${formatMonitoredSystemCount(resolved.current)}.`;
- case 'at_limit_blocking_new':
- return `Continuity review needed. ${formatMonitoredSystemCount(resolved.current)}.`;
- case 'unlimited':
- return `${formatMonitoredSystemCount(resolved.current)}.`;
- default:
- return `${formatMonitoredSystemCount(resolved.current)}.`;
- }
-}
-
-export function formatMonitoredSystemLegacyConnectionBreakdown(
- counts: MonitoredSystemLegacyConnectionCounts,
-): string {
- const parts: string[] = [];
-
- if (counts.proxmox_nodes > 0) {
- parts.push(`${counts.proxmox_nodes} Proxmox ${counts.proxmox_nodes === 1 ? 'node' : 'nodes'}`);
- }
- if (counts.docker_hosts > 0) {
- parts.push(`${counts.docker_hosts} Docker ${counts.docker_hosts === 1 ? 'host' : 'hosts'}`);
- }
- if (counts.kubernetes_clusters > 0) {
- parts.push(
- `${counts.kubernetes_clusters} Kubernetes ${
- counts.kubernetes_clusters === 1 ? 'cluster' : 'clusters'
- }`,
- );
- }
-
- return parts.join(', ');
-}
-
-export function formatMonitoredSystemMigrationMessage(
- counts: MonitoredSystemLegacyConnectionCounts,
-): string {
- const total = counts.proxmox_nodes + counts.docker_hosts + counts.kubernetes_clusters;
- if (total <= 0) return '';
-
- const noun = total === 1 ? 'resource' : 'resources';
- const breakdown = formatMonitoredSystemLegacyConnectionBreakdown(counts);
- return `You also have ${total} ${noun} connected via API or legacy collectors${
- breakdown ? ` (${breakdown})` : ''
- } ${MONITORED_SYSTEM_LEDGER_PRESENTATION.limitBanner.legacyConnectionSuffix}`;
-}
-
-export function formatMonitoredSystemOverflowSummary(daysRemaining: number | undefined): string {
- if (!daysRemaining) return '';
- return `${MONITORED_SYSTEM_LEDGER_PRESENTATION.limitBanner.overflowSummaryPrefix} (${daysRemaining}d remaining)`;
-}
-
export function formatMonitoredSystemLatestIncludedSignalSentence(signal: {
attribution: string;
relative: string;
diff --git a/frontend-modern/src/utils/pricingHandoff.ts b/frontend-modern/src/utils/pricingHandoff.ts
index ac30b6c57..9bdb21be5 100644
--- a/frontend-modern/src/utils/pricingHandoff.ts
+++ b/frontend-modern/src/utils/pricingHandoff.ts
@@ -51,7 +51,6 @@ export const SELF_HOSTED_PRO_BILLING_PLAN_SELECTION_HREF = `${SELF_HOSTED_PRO_BI
const IN_PRODUCT_PRICING_DESTINATIONS: Record = {
self_hosted_plan: SELF_HOSTED_PRO_BILLING_PLAN_SELECTION_HREF,
- max_monitored_systems: SELF_HOSTED_PRO_BILLING_PLAN_HREF,
// Paid self-hosted feature keys: route to the owned billing plan page instead
// of the Pulse Account purchase-start handoff, which fails for local instances
// without PublicURL.
diff --git a/internal/api/agent_ingest.go b/internal/api/agent_ingest.go
index 6f6ba178a..c5a6047d2 100644
--- a/internal/api/agent_ingest.go
+++ b/internal/api/agent_ingest.go
@@ -83,16 +83,6 @@ func (h *UnifiedAgentHandlers) HandleReport(w http.ResponseWriter, r *http.Reque
}
tokenRecord := getAPITokenRecordFromRequest(r)
- if enforceMonitoredSystemLimitForHostReport(
- w,
- r.Context(),
- h.getMonitor(r.Context()),
- report,
- tokenRecord,
- ) {
- return
- }
-
host, err := h.getMonitor(r.Context()).ApplyHostReport(report, tokenRecord)
if err != nil {
writeErrorResponse(w, http.StatusBadRequest, "invalid_report", err.Error(), nil)
diff --git a/internal/api/billing_state_handlers_test.go b/internal/api/billing_state_handlers_test.go
index 604daf4b1..b9c1754f5 100644
--- a/internal/api/billing_state_handlers_test.go
+++ b/internal/api/billing_state_handlers_test.go
@@ -195,8 +195,8 @@ func TestBillingStateGetHostedNonDefaultFallsBackToDefaultBillingState(t *testin
if payload.PlanVersion != "msp_starter" {
t.Fatalf("expected plan_version %q, got %q", "msp_starter", payload.PlanVersion)
}
- if !reflect.DeepEqual(payload.Limits, map[string]int64{"max_monitored_systems": 50}) {
- t.Fatalf("expected limits to inherit default hosted billing state, got %v", payload.Limits)
+ if len(payload.Limits) != 0 {
+ t.Fatalf("expected retired monitored-system volume keys to be scrubbed from inherited hosted billing state, got %v", payload.Limits)
}
}
diff --git a/internal/api/code_standards_test.go b/internal/api/code_standards_test.go
index 4822ca570..864716bfa 100644
--- a/internal/api/code_standards_test.go
+++ b/internal/api/code_standards_test.go
@@ -202,14 +202,13 @@ func TestPaidDomainBoundaryAudit(t *testing.T) {
// Use an exact allowlist to prevent new paid implementations from
// hiding behind the enterprise_extension_ prefix.
knownBinderFiles := map[string]bool{
- "enterprise_extension_ai_alert_analysis.go": true,
- "enterprise_extension_ai_autofix.go": true,
- "enterprise_extension_ai_investigation.go": true,
- "enterprise_extension_audit_admin.go": true,
- "enterprise_extension_monitored_system_admission.go": true,
- "enterprise_extension_rbac_admin.go": true,
- "enterprise_extension_reporting_admin.go": true,
- "enterprise_extension_sso_admin.go": true,
+ "enterprise_extension_ai_alert_analysis.go": true,
+ "enterprise_extension_ai_autofix.go": true,
+ "enterprise_extension_ai_investigation.go": true,
+ "enterprise_extension_audit_admin.go": true,
+ "enterprise_extension_rbac_admin.go": true,
+ "enterprise_extension_reporting_admin.go": true,
+ "enterprise_extension_sso_admin.go": true,
}
// These reporting files are intentionally public shared surfaces rather than
diff --git a/internal/api/config_handlers_add_test.go b/internal/api/config_handlers_add_test.go
index 5996fbf7e..50988a265 100644
--- a/internal/api/config_handlers_add_test.go
+++ b/internal/api/config_handlers_add_test.go
@@ -252,7 +252,7 @@ func TestHandleAddNode_PBSTurnkeyTokenCreationUsesCanonicalPulseURL(t *testing.T
}
}
-func TestHandleAddNode_BlocksNewCountedSystemAtLimit(t *testing.T) {
+func TestHandleAddNode_AllowsNewCountedSystemWithCapsRetired(t *testing.T) {
stubAutoRegisterNetworkDeps(t)
setMaxMonitoredSystemsLicenseForTests(t, 1)
@@ -307,8 +307,8 @@ func TestHandleAddNode_BlocksNewCountedSystemAtLimit(t *testing.T) {
rec := httptest.NewRecorder()
handler.HandleAddNode(rec, req)
- if rec.Code != http.StatusPaymentRequired {
- t.Fatalf("expected 402 once monitored-system cap is full, got %d: %s", rec.Code, rec.Body.String())
+ if rec.Code != http.StatusCreated {
+ t.Fatalf("expected 201 with monitored-system caps retired, got %d: %s", rec.Code, rec.Body.String())
}
}
@@ -363,7 +363,7 @@ func TestHandleAddNode_AllowsCanonicalOverlapAtLimit(t *testing.T) {
}
}
-func TestHandleAddNode_FailsClosedWhenUsageUnavailable(t *testing.T) {
+func TestHandleAddNode_AllowsWhenUsageUnavailableWithCapsRetired(t *testing.T) {
stubAutoRegisterNetworkDeps(t)
setMaxMonitoredSystemsLicenseForTests(t, 1)
@@ -386,8 +386,8 @@ func TestHandleAddNode_FailsClosedWhenUsageUnavailable(t *testing.T) {
rec := httptest.NewRecorder()
handler.HandleAddNode(rec, req)
- if rec.Code != http.StatusServiceUnavailable {
- t.Fatalf("expected 503 when monitored-system usage cannot be resolved, got %d: %s", rec.Code, rec.Body.String())
+ if rec.Code != http.StatusCreated {
+ t.Fatalf("expected 201 when monitored-system usage is unavailable and caps are retired, got %d: %s", rec.Code, rec.Body.String())
}
}
diff --git a/internal/api/config_handlers_auto_register_test.go b/internal/api/config_handlers_auto_register_test.go
index 288448ada..250e979ff 100644
--- a/internal/api/config_handlers_auto_register_test.go
+++ b/internal/api/config_handlers_auto_register_test.go
@@ -1014,7 +1014,7 @@ func TestHandleAutoRegisterSelectsReachableFallbackCandidateHost(t *testing.T) {
}
}
-func TestHandleAutoRegister_BlocksNewCountedSystemAtLimit(t *testing.T) {
+func TestHandleAutoRegister_AllowsNewCountedSystemWithCapsRetired(t *testing.T) {
stubAutoRegisterNetworkDeps(t)
setMaxMonitoredSystemsLicenseForTests(t, 1)
@@ -1086,8 +1086,8 @@ func TestHandleAutoRegister_BlocksNewCountedSystemAtLimit(t *testing.T) {
rec := httptest.NewRecorder()
handler.HandleAutoRegister(rec, req)
- if rec.Code != http.StatusPaymentRequired {
- t.Fatalf("expected 402 once monitored-system cap is full, got %d: %s", rec.Code, rec.Body.String())
+ if rec.Code != http.StatusOK {
+ t.Fatalf("expected 200 with monitored-system caps retired, got %d: %s", rec.Code, rec.Body.String())
}
}
diff --git a/internal/api/config_node_handlers.go b/internal/api/config_node_handlers.go
index af6797d2a..c532dedc5 100644
--- a/internal/api/config_node_handlers.go
+++ b/internal/api/config_node_handlers.go
@@ -12,7 +12,6 @@ import (
"github.com/rcourtman/pulse-go-rewrite/internal/config"
"github.com/rcourtman/pulse-go-rewrite/internal/mock"
- "github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
"github.com/rcourtman/pulse-go-rewrite/internal/websocket"
"github.com/rcourtman/pulse-go-rewrite/pkg/pbs"
"github.com/rcourtman/pulse-go-rewrite/pkg/pmg"
@@ -520,15 +519,6 @@ func (h *ConfigHandlers) handleAddNode(w http.ResponseWriter, r *http.Request) {
pve.PhysicalDiskPollingMinutes = *req.PhysicalDiskPollingMinutes
}
- if enforceMonitoredSystemLimitForConfigRegistration(w, r.Context(), h.getConfig(r.Context()), h.getMonitor(r.Context()), unifiedresources.MonitoredSystemCandidate{
- Source: unifiedresources.SourceProxmox,
- Type: unifiedresources.ResourceTypeAgent,
- Name: displayName,
- Hostname: pulseTokenHostCandidate(host),
- HostURL: host,
- }) {
- return
- }
h.getConfig(r.Context()).PVEInstances = append(h.getConfig(r.Context()).PVEInstances, pve)
h.normalizePVEConfigState(r.Context())
@@ -653,15 +643,6 @@ func (h *ConfigHandlers) handleAddNode(w http.ResponseWriter, r *http.Request) {
MonitorGarbageJobs: monitorGarbageJobs,
TemperatureMonitoringEnabled: req.TemperatureMonitoringEnabled,
}
- if enforceMonitoredSystemLimitForConfigRegistration(w, r.Context(), h.getConfig(r.Context()), h.getMonitor(r.Context()), unifiedresources.MonitoredSystemCandidate{
- Source: unifiedresources.SourcePBS,
- Type: unifiedresources.ResourceTypePBS,
- Name: pbsDisplayName,
- Hostname: pulseTokenHostCandidate(host),
- HostURL: host,
- }) {
- return
- }
h.getConfig(r.Context()).PBSInstances = append(h.getConfig(r.Context()).PBSInstances, pbs)
} else if req.Type == "pmg" {
host := normalizedHost
@@ -744,15 +725,6 @@ func (h *ConfigHandlers) handleAddNode(w http.ResponseWriter, r *http.Request) {
MonitorDomainStats: monitorDomainStats,
TemperatureMonitoringEnabled: req.TemperatureMonitoringEnabled,
}
- if enforceMonitoredSystemLimitForConfigRegistration(w, r.Context(), h.getConfig(r.Context()), h.getMonitor(r.Context()), unifiedresources.MonitoredSystemCandidate{
- Source: unifiedresources.SourcePMG,
- Type: unifiedresources.ResourceTypePMG,
- Name: pmgDisplayName,
- Hostname: pulseTokenHostCandidate(host),
- HostURL: host,
- }) {
- return
- }
h.getConfig(r.Context()).PMGInstances = append(h.getConfig(r.Context()).PMGInstances, pmgInstance)
}
@@ -780,76 +752,6 @@ func (h *ConfigHandlers) handleAddNode(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(map[string]string{"status": "success"})
}
-func proxmoxMonitoredSystemCandidate(instance config.PVEInstance) unifiedresources.MonitoredSystemCandidate {
- return unifiedresources.MonitoredSystemCandidate{
- Source: unifiedresources.SourceProxmox,
- Type: unifiedresources.ResourceTypeAgent,
- Name: instance.Name,
- Hostname: pulseTokenHostCandidate(instance.Host),
- HostURL: instance.Host,
- }
-}
-
-func pbsMonitoredSystemCandidate(instance config.PBSInstance) unifiedresources.MonitoredSystemCandidate {
- return unifiedresources.MonitoredSystemCandidate{
- Source: unifiedresources.SourcePBS,
- Type: unifiedresources.ResourceTypePBS,
- Name: instance.Name,
- Hostname: pulseTokenHostCandidate(instance.Host),
- HostURL: instance.Host,
- }
-}
-
-func pmgMonitoredSystemCandidate(instance config.PMGInstance) unifiedresources.MonitoredSystemCandidate {
- return unifiedresources.MonitoredSystemCandidate{
- Source: unifiedresources.SourcePMG,
- Type: unifiedresources.ResourceTypePMG,
- Name: instance.Name,
- Hostname: pulseTokenHostCandidate(instance.Host),
- HostURL: instance.Host,
- }
-}
-
-func proxmoxMonitoredSystemReplacement(instance config.PVEInstance) unifiedresources.MonitoredSystemReplacement {
- name := strings.TrimSpace(instance.Name)
- host := strings.TrimSpace(instance.Host)
- hostname := pulseTokenHostCandidate(instance.Host)
- return unifiedresources.MonitoredSystemReplacement{
- Source: unifiedresources.SourceProxmox,
- Selector: unifiedresources.MonitoredSystemReplacementSelector{
- Name: name,
- Hostname: hostname,
- HostURL: host,
- },
- }
-}
-
-func pbsMonitoredSystemReplacement(instance config.PBSInstance) unifiedresources.MonitoredSystemReplacement {
- name := strings.TrimSpace(instance.Name)
- host := strings.TrimSpace(instance.Host)
- hostname := pulseTokenHostCandidate(instance.Host)
- return unifiedresources.MonitoredSystemReplacement{
- Source: unifiedresources.SourcePBS,
- Selector: unifiedresources.MonitoredSystemReplacementSelector{
- ResourceID: name,
- Hostname: hostname,
- HostURL: host,
- },
- }
-}
-
-func pmgMonitoredSystemReplacement(instance config.PMGInstance) unifiedresources.MonitoredSystemReplacement {
- name := strings.TrimSpace(instance.Name)
- hostname := pulseTokenHostCandidate(instance.Host)
- return unifiedresources.MonitoredSystemReplacement{
- Source: unifiedresources.SourcePMG,
- Selector: unifiedresources.MonitoredSystemReplacementSelector{
- ResourceID: name,
- Hostname: hostname,
- },
- }
-}
-
// HandleTestConnection tests a node connection without saving
func (h *ConfigHandlers) handleTestConnection(w http.ResponseWriter, r *http.Request) {
@@ -1275,15 +1177,6 @@ func (h *ConfigHandlers) handleUpdateNode(w http.ResponseWriter, r *http.Request
updated.Disabled = !*req.Enabled
}
- if enforceMonitoredSystemLimitForConfigReplacement(
- w,
- r.Context(),
- h.getMonitor(r.Context()),
- proxmoxMonitoredSystemReplacement(current),
- proxmoxMonitoredSystemCandidate(updated),
- ) {
- return
- }
*pve = updated
} else if nodeType == "pbs" && index < len(h.getConfig(r.Context()).PBSInstances) {
pbs := &h.getConfig(r.Context()).PBSInstances[index]
@@ -1382,15 +1275,6 @@ func (h *ConfigHandlers) handleUpdateNode(w http.ResponseWriter, r *http.Request
updated.Disabled = !*req.Enabled
}
- if enforceMonitoredSystemLimitForConfigReplacement(
- w,
- r.Context(),
- h.getMonitor(r.Context()),
- pbsMonitoredSystemReplacement(current),
- pbsMonitoredSystemCandidate(updated),
- ) {
- return
- }
*pbs = updated
} else if nodeType == "pmg" && index < len(h.getConfig(r.Context()).PMGInstances) {
pmgInst := &h.getConfig(r.Context()).PMGInstances[index]
@@ -1473,15 +1357,6 @@ func (h *ConfigHandlers) handleUpdateNode(w http.ResponseWriter, r *http.Request
updated.Disabled = !*req.Enabled
}
- if enforceMonitoredSystemLimitForConfigReplacement(
- w,
- r.Context(),
- h.getMonitor(r.Context()),
- pmgMonitoredSystemReplacement(current),
- pmgMonitoredSystemCandidate(updated),
- ) {
- return
- }
*pmgInst = updated
} else {
http.Error(w, "Node not found", http.StatusNotFound)
diff --git a/internal/api/config_setup_handlers.go b/internal/api/config_setup_handlers.go
index 54ea711b1..68c677f8c 100644
--- a/internal/api/config_setup_handlers.go
+++ b/internal/api/config_setup_handlers.go
@@ -20,7 +20,6 @@ import (
"github.com/rcourtman/pulse-go-rewrite/internal/config"
"github.com/rcourtman/pulse-go-rewrite/internal/system"
- "github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
"github.com/rcourtman/pulse-go-rewrite/internal/websocket"
internalauth "github.com/rcourtman/pulse-go-rewrite/pkg/auth"
"github.com/rs/zerolog/log"
@@ -1273,15 +1272,6 @@ func (h *ConfigHandlers) handleCanonicalAutoRegister(w http.ResponseWriter, r *h
http.Error(w, "agent token auth permits token updates for existing nodes only", http.StatusForbidden)
return
}
- if enforceMonitoredSystemLimitForConfigRegistration(w, r.Context(), h.getConfig(r.Context()), h.getMonitor(r.Context()), unifiedresources.MonitoredSystemCandidate{
- Source: unifiedresources.SourceProxmox,
- Type: unifiedresources.ResourceTypeAgent,
- Name: serverName,
- Hostname: pulseTokenHostCandidate(host),
- HostURL: host,
- }) {
- return
- }
h.getConfig(r.Context()).PVEInstances = append(h.getConfig(r.Context()).PVEInstances, pveNode)
h.normalizePVEConfigState(r.Context())
}
@@ -1358,15 +1348,6 @@ func (h *ConfigHandlers) handleCanonicalAutoRegister(w http.ResponseWriter, r *h
http.Error(w, "agent token auth permits token updates for existing nodes only", http.StatusForbidden)
return
}
- if enforceMonitoredSystemLimitForConfigRegistration(w, r.Context(), h.getConfig(r.Context()), h.getMonitor(r.Context()), unifiedresources.MonitoredSystemCandidate{
- Source: unifiedresources.SourcePBS,
- Type: unifiedresources.ResourceTypePBS,
- Name: serverName,
- Hostname: pulseTokenHostCandidate(host),
- HostURL: host,
- }) {
- return
- }
h.getConfig(r.Context()).PBSInstances = append(h.getConfig(r.Context()).PBSInstances, pbsNode)
}
}
diff --git a/internal/api/contract_test.go b/internal/api/contract_test.go
index b0e043178..c413efdca 100644
--- a/internal/api/contract_test.go
+++ b/internal/api/contract_test.go
@@ -44,7 +44,6 @@ import (
agentshost "github.com/rcourtman/pulse-go-rewrite/pkg/agents/host"
authpkg "github.com/rcourtman/pulse-go-rewrite/pkg/auth"
"github.com/rcourtman/pulse-go-rewrite/pkg/cloudauth"
- "github.com/rcourtman/pulse-go-rewrite/pkg/extensions"
pkglicensing "github.com/rcourtman/pulse-go-rewrite/pkg/licensing"
licensetestsupport "github.com/rcourtman/pulse-go-rewrite/pkg/licensing/testsupport"
"github.com/rcourtman/pulse-go-rewrite/pkg/metrics"
@@ -3690,7 +3689,6 @@ func TestContract_MonitoredSystemLedgerJSONSnapshot(t *testing.T) {
},
},
Total: 1,
- Limit: 5,
}
got, err := json.Marshal(payload)
@@ -3744,21 +3742,18 @@ func TestContract_MonitoredSystemLedgerJSONSnapshot(t *testing.T) {
}
}
],
- "total":1,
- "limit":5
- }`
+ "total":1
+ }`
assertJSONSnapshot(t, got, want)
}
func TestContract_MonitoredSystemLedgerPreviewJSONSnapshot(t *testing.T) {
payload := MonitoredSystemLedgerPreviewResponse{
- CurrentCount: 1,
- ProjectedCount: 1,
- AdditionalCount: 0,
- Limit: 5,
- WouldExceedLimit: false,
- Effect: "attaches_existing",
+ CurrentCount: 1,
+ ProjectedCount: 1,
+ AdditionalCount: 0,
+ Effect: "attaches_existing",
CurrentSystems: []MonitoredSystemLedgerEntry{
{
Name: "Tower",
@@ -3889,11 +3884,9 @@ func TestContract_MonitoredSystemLedgerPreviewJSONSnapshot(t *testing.T) {
}
const want = `{
- "current_count":1,
- "projected_count":1,
- "additional_count":0,
- "limit":5,
- "would_exceed_limit":false,
+ "current_count":1,
+ "projected_count":1,
+ "additional_count":0,
"effect":"attaches_existing",
"current_systems":[
{
@@ -4073,7 +4066,6 @@ func TestContract_MonitoredSystemLedgerDoesNotEmitCompatibilityAliases(t *testin
payload := MonitoredSystemLedgerResponse{
Systems: []MonitoredSystemLedgerEntry{entry},
Total: 1,
- Limit: 5,
}
got, err := json.Marshal(payload)
@@ -4104,10 +4096,9 @@ func TestContract_MonitoredSystemLedgerDoesNotEmitCompatibilityAliases(t *testin
"surfaces":[]
}
}
- ],
- "total":1,
- "limit":5
- }`
+ ],
+ "total":1
+ }`
assertJSONSnapshot(t, got, want)
}
@@ -5505,18 +5496,11 @@ func TestContract_HostedTenantEntitlementsFallbackToDefaultBillingState(t *testi
if !sliceContainsString(payload.Capabilities, pkglicensing.FeatureRelay) {
t.Fatalf("expected hosted tenant payload to include %q from default hosted billing state", pkglicensing.FeatureRelay)
}
- foundMonitoredSystemLimit := false
for _, limit := range payload.Limits {
if limit.Key == pkglicensing.MaxMonitoredSystemsLicenseGateKey {
- foundMonitoredSystemLimit = true
- if limit.Limit != 50 {
- t.Fatalf("max_monitored_systems limit=%d, want 50", limit.Limit)
- }
+ t.Fatalf("expected retired max_monitored_systems limit to be omitted, got %+v", payload.Limits)
}
}
- if !foundMonitoredSystemLimit {
- t.Fatalf("expected max_monitored_systems limit in payload, got %+v", payload.Limits)
- }
}
func TestContract_HostedTenantEntitlementRefreshFallsBackToDefaultBillingState(t *testing.T) {
@@ -5650,10 +5634,9 @@ func TestContract_HostedEntitlementVerifierBridgeUsesCompatibilityEnvAlias(t *te
func TestContract_EntitlementPayloadMonitoredSystemUsageJSONSnapshot(t *testing.T) {
payload := buildEntitlementPayloadWithUsage(&licenseStatus{
- Valid: true,
- Tier: pkglicensing.TierPro,
- Features: append([]string(nil), pkglicensing.TierFeatures[pkglicensing.TierPro]...),
- MaxMonitoredSystems: 0,
+ Valid: true,
+ Tier: pkglicensing.TierPro,
+ Features: append([]string(nil), pkglicensing.TierFeatures[pkglicensing.TierPro]...),
}, string(pkglicensing.SubStateActive), entitlementUsageSnapshot{
MonitoredSystems: 7,
MonitoredSystemsAvailable: true,
@@ -5681,10 +5664,9 @@ func TestContract_EntitlementPayloadMonitoredSystemUsageJSONSnapshot(t *testing.
"days_remaining":0,
"trial_eligible":false,
"max_history_days":90,
- "legacy_connections":{"proxmox_nodes":2,"docker_hosts":1,"kubernetes_clusters":1},
- "has_migration_gap":false,
- "monitored_system_capacity":{"mode":"unlimited","urgency":"ok","current":7,"limit":0,"current_available":true,"available_slots":0,"overage":0,"blocks_new_systems":false,"existing_monitoring_continues":true}
- }`
+ "legacy_connections":{"proxmox_nodes":2,"docker_hosts":1,"kubernetes_clusters":1},
+ "has_migration_gap":false
+ }`
assertJSONSnapshot(t, got, want)
}
@@ -5737,10 +5719,9 @@ func TestContract_SelfHostedCommunityEntitlementsJSONSnapshot(t *testing.T) {
"trial_eligible":false,
"max_history_days":7,
"overflow_days_remaining":14,
- "legacy_connections":{"proxmox_nodes":0,"docker_hosts":0,"kubernetes_clusters":0},
- "has_migration_gap":false,
- "monitored_system_capacity":{"mode":"usage_unavailable","urgency":"ok","current":0,"limit":0,"current_available":false,"available_slots":0,"overage":0,"blocks_new_systems":false,"existing_monitoring_continues":false}
- }`
+ "legacy_connections":{"proxmox_nodes":0,"docker_hosts":0,"kubernetes_clusters":0},
+ "has_migration_gap":false
+ }`
assertJSONSnapshot(t, got, want)
}
@@ -5776,27 +5757,20 @@ func TestContract_SelfHostedCommunityRuntimeCapabilitiesJSONSnapshot(t *testing.
const want = `{
"capabilities":["update_alerts","sso","advanced_sso","ai_patrol"],
- "limits":[],
- "hosted_mode":false,
- "max_history_days":7,
- "monitored_system_capacity":{"mode":"usage_unavailable","urgency":"ok","current":0,"limit":0,"current_available":false,"available_slots":0,"overage":0,"blocks_new_systems":false,"existing_monitoring_continues":false}
- }`
+ "limits":[],
+ "hosted_mode":false,
+ "max_history_days":7
+ }`
assertJSONSnapshot(t, got, want)
}
func TestContract_EntitlementPayloadMonitoredSystemUsageUnavailableJSONSnapshot(t *testing.T) {
payload := buildEntitlementPayloadWithUsage(&licenseStatus{
- Valid: true,
- Tier: pkglicensing.TierCloud,
- PlanVersion: "cloud_starter",
- Features: append([]string(nil), pkglicensing.TierFeatures[pkglicensing.TierPro]...),
- MaxMonitoredSystems: 15,
- MonitoredSystemContinuity: &pkglicensing.MonitoredSystemContinuityStatus{
- PlanLimit: 15,
- EffectiveLimit: 15,
- CapturePending: true,
- },
+ Valid: true,
+ Tier: pkglicensing.TierCloud,
+ PlanVersion: "cloud_starter",
+ Features: append([]string(nil), pkglicensing.TierFeatures[pkglicensing.TierPro]...),
}, string(pkglicensing.SubStateActive), entitlementUsageSnapshot{
MonitoredSystemsUnavailableReason: "supplemental_inventory_unsettled",
}, nil)
@@ -5808,7 +5782,7 @@ func TestContract_EntitlementPayloadMonitoredSystemUsageUnavailableJSONSnapshot(
const want = `{
"capabilities":["update_alerts","sso","advanced_sso","ai_patrol","relay","mobile_app","push_notifications","long_term_metrics","ai_alerts","ai_autofix","kubernetes_ai","agent_profiles","rbac","audit_logging","advanced_reporting"],
- "limits":[{"key":"max_monitored_systems","limit":15,"current":0,"current_available":false,"current_unavailable_reason":"supplemental_inventory_unsettled","state":"ok"}],
+ "limits":[],
"subscription_state":"active",
"upgrade_reasons":[],
"plan_version":"cloud_starter",
@@ -5820,9 +5794,7 @@ func TestContract_EntitlementPayloadMonitoredSystemUsageUnavailableJSONSnapshot(
"trial_eligible":false,
"max_history_days":90,
"legacy_connections":{"proxmox_nodes":0,"docker_hosts":0,"kubernetes_clusters":0},
- "has_migration_gap":false,
- "monitored_system_continuity":{"plan_limit":15,"effective_limit":15,"capture_pending":true},
- "monitored_system_capacity":{"mode":"usage_unavailable","urgency":"ok","current":0,"limit":15,"current_available":false,"current_unavailable_reason":"supplemental_inventory_unsettled","available_slots":0,"overage":0,"blocks_new_systems":false,"existing_monitoring_continues":false}
+ "has_migration_gap":false
}`
assertJSONSnapshot(t, got, want)
@@ -5830,13 +5802,12 @@ func TestContract_EntitlementPayloadMonitoredSystemUsageUnavailableJSONSnapshot(
func TestContract_EntitlementPayloadLifetimeJSONSnapshot(t *testing.T) {
payload := buildEntitlementPayloadWithUsage(&licenseStatus{
- Valid: true,
- Tier: pkglicensing.TierLifetime,
- PlanVersion: "v5_lifetime_grandfathered",
- IsLifetime: true,
- Features: append([]string(nil), pkglicensing.TierFeatures[pkglicensing.TierLifetime]...),
- MaxMonitoredSystems: 0,
- MaxGuests: 0,
+ Valid: true,
+ Tier: pkglicensing.TierLifetime,
+ PlanVersion: "v5_lifetime_grandfathered",
+ IsLifetime: true,
+ Features: append([]string(nil), pkglicensing.TierFeatures[pkglicensing.TierLifetime]...),
+ MaxGuests: 0,
}, string(pkglicensing.SubStateActive), entitlementUsageSnapshot{
MonitoredSystems: 15,
MonitoredSystemsAvailable: true,
@@ -5864,10 +5835,9 @@ func TestContract_EntitlementPayloadLifetimeJSONSnapshot(t *testing.T) {
"days_remaining":0,
"trial_eligible":false,
"max_history_days":90,
- "legacy_connections":{"proxmox_nodes":1,"docker_hosts":1,"kubernetes_clusters":0},
- "has_migration_gap":false,
- "monitored_system_capacity":{"mode":"unlimited","urgency":"ok","current":15,"limit":0,"current_available":true,"available_slots":0,"overage":0,"blocks_new_systems":false,"existing_monitoring_continues":true}
- }`
+ "legacy_connections":{"proxmox_nodes":1,"docker_hosts":1,"kubernetes_clusters":0},
+ "has_migration_gap":false
+ }`
assertJSONSnapshot(t, got, want)
}
@@ -5906,15 +5876,14 @@ func TestContract_LegacyMigrationFallbackStaysUncappedJSONSnapshot(t *testing.T)
const expectedClientVersion = "6.0.0-rc.1"
grantJWT, grantPublicKey, err := licensetestsupport.GenerateGrantJWTForTesting(pkglicensing.GrantClaims{
- LicenseID: "lic_contract_floor",
- Tier: "pro",
- PlanKey: "legacy_migration_fallback",
- State: "active",
- Features: []string{"relay"},
- MaxMonitoredSystems: 10,
- IssuedAt: time.Now().Unix(),
- ExpiresAt: time.Now().Add(72 * time.Hour).Unix(),
- Email: "contract-floor@example.com",
+ LicenseID: "lic_contract_floor",
+ Tier: "pro",
+ PlanKey: "legacy_migration_fallback",
+ State: "active",
+ Features: []string{"relay"},
+ IssuedAt: time.Now().Unix(),
+ ExpiresAt: time.Now().Add(72 * time.Hour).Unix(),
+ Email: "contract-floor@example.com",
})
if err != nil {
t.Fatalf("generate grant jwt: %v", err)
@@ -5936,11 +5905,10 @@ func TestContract_LegacyMigrationFallbackStaysUncappedJSONSnapshot(t *testing.T)
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(pkglicensing.ActivateInstallationResponse{
License: pkglicensing.ActivateResponseLicense{
- LicenseID: "lic_contract_floor",
- State: "active",
- Tier: "pro",
- Features: []string{"relay"},
- MaxMonitoredSystems: 10,
+ LicenseID: "lic_contract_floor",
+ State: "active",
+ Tier: "pro",
+ Features: []string{"relay"},
},
Installation: pkglicensing.ActivateResponseInstallation{
InstallationID: "inst_contract_floor",
@@ -6004,67 +5972,38 @@ func TestContract_LegacyMigrationFallbackStaysUncappedJSONSnapshot(t *testing.T)
if err := json.Unmarshal(entRec.Body.Bytes(), &payload); err != nil {
t.Fatalf("decode entitlements: %v", err)
}
- statusContinuity := status.MonitoredSystemContinuity
- if statusContinuity != nil {
- copied := *statusContinuity
- if copied.CapturedAt > 0 {
- copied.CapturedAt = 123
- }
- statusContinuity = &copied
- }
- payloadContinuity := payload.MonitoredSystemContinuity
- if payloadContinuity != nil {
- copied := *payloadContinuity
- if copied.CapturedAt > 0 {
- copied.CapturedAt = 123
- }
- payloadContinuity = &copied
- }
-
got, err := json.Marshal(struct {
Status struct {
- Tier pkglicensing.Tier `json:"tier"`
- PlanVersion string `json:"plan_version"`
- MaxMonitoredSystems int `json:"max_monitored_systems"`
- Valid bool `json:"valid"`
- MonitoredSystemContinuity *pkglicensing.MonitoredSystemContinuityStatus `json:"monitored_system_continuity,omitempty"`
+ Tier pkglicensing.Tier `json:"tier"`
+ PlanVersion string `json:"plan_version"`
+ Valid bool `json:"valid"`
} `json:"status"`
Entitlements struct {
- Tier string `json:"tier"`
- PlanVersion string `json:"plan_version"`
- SubscriptionState string `json:"subscription_state"`
- Limits []pkglicensing.LimitStatus `json:"limits"`
- MonitoredSystemContinuity *pkglicensing.MonitoredSystemContinuityStatus `json:"monitored_system_continuity,omitempty"`
- MonitoredSystemCapacity *pkglicensing.MonitoredSystemCapacityStatus `json:"monitored_system_capacity,omitempty"`
+ Tier string `json:"tier"`
+ PlanVersion string `json:"plan_version"`
+ SubscriptionState string `json:"subscription_state"`
+ Limits []pkglicensing.LimitStatus `json:"limits"`
} `json:"entitlements"`
}{
Status: struct {
- Tier pkglicensing.Tier `json:"tier"`
- PlanVersion string `json:"plan_version"`
- MaxMonitoredSystems int `json:"max_monitored_systems"`
- Valid bool `json:"valid"`
- MonitoredSystemContinuity *pkglicensing.MonitoredSystemContinuityStatus `json:"monitored_system_continuity,omitempty"`
+ Tier pkglicensing.Tier `json:"tier"`
+ PlanVersion string `json:"plan_version"`
+ Valid bool `json:"valid"`
}{
- Tier: status.Tier,
- PlanVersion: status.PlanVersion,
- MaxMonitoredSystems: status.MaxMonitoredSystems,
- Valid: status.Valid,
- MonitoredSystemContinuity: statusContinuity,
+ Tier: status.Tier,
+ PlanVersion: status.PlanVersion,
+ Valid: status.Valid,
},
Entitlements: struct {
- Tier string `json:"tier"`
- PlanVersion string `json:"plan_version"`
- SubscriptionState string `json:"subscription_state"`
- Limits []pkglicensing.LimitStatus `json:"limits"`
- MonitoredSystemContinuity *pkglicensing.MonitoredSystemContinuityStatus `json:"monitored_system_continuity,omitempty"`
- MonitoredSystemCapacity *pkglicensing.MonitoredSystemCapacityStatus `json:"monitored_system_capacity,omitempty"`
+ Tier string `json:"tier"`
+ PlanVersion string `json:"plan_version"`
+ SubscriptionState string `json:"subscription_state"`
+ Limits []pkglicensing.LimitStatus `json:"limits"`
}{
- Tier: payload.Tier,
- PlanVersion: payload.PlanVersion,
- SubscriptionState: payload.SubscriptionState,
- Limits: payload.Limits,
- MonitoredSystemContinuity: payloadContinuity,
- MonitoredSystemCapacity: payload.MonitoredSystemCapacity,
+ Tier: payload.Tier,
+ PlanVersion: payload.PlanVersion,
+ SubscriptionState: payload.SubscriptionState,
+ Limits: payload.Limits,
},
})
if err != nil {
@@ -6075,15 +6014,13 @@ func TestContract_LegacyMigrationFallbackStaysUncappedJSONSnapshot(t *testing.T)
"status":{
"tier":"pro",
"plan_version":"legacy_migration_fallback",
- "max_monitored_systems":0,
"valid":true
},
"entitlements":{
"tier":"pro",
"plan_version":"legacy_migration_fallback",
"subscription_state":"active",
- "limits":[],
- "monitored_system_capacity":{"mode":"unlimited","urgency":"ok","current":23,"limit":0,"current_available":true,"available_slots":0,"overage":0,"blocks_new_systems":false,"existing_monitoring_continues":true}
+ "limits":[]
}
}`
@@ -6118,7 +6055,7 @@ func TestContract_HostedBillingStateFallbackJSONSnapshot(t *testing.T) {
const want = `{
"capabilities":["relay","rbac"],
- "limits":{"max_monitored_systems":50},
+ "limits":{},
"meters_enabled":[],
"plan_version":"msp_starter",
"subscription_state":"active",
@@ -6218,72 +6155,13 @@ func TestContract_HostReportAdmissionPreservesRestartContinuityAtLimit(t *testin
},
Timestamp: report.Timestamp.Add(time.Minute),
}
- blockedRec := postReport(t, restartedHandler, newHostReport)
- if blockedRec.Code != http.StatusPaymentRequired {
- t.Fatalf("new host should remain blocked at limit, got %d: %s", blockedRec.Code, blockedRec.Body.String())
- }
-
- payload := decodeMonitoredSystemLimitBlockedPayload(t, blockedRec.Body.Bytes())
- if payload.Feature != maxMonitoredSystemsLicenseGateKey {
- t.Fatalf("feature=%q, want %q", payload.Feature, maxMonitoredSystemsLicenseGateKey)
- }
- if !payload.MonitoredSystemPreview.WouldExceedLimit {
- t.Fatalf("expected monitored_system_preview.would_exceed_limit=true, got %+v", payload.MonitoredSystemPreview)
+ newHostRec := postReport(t, restartedHandler, newHostReport)
+ if newHostRec.Code != http.StatusOK {
+ t.Fatalf("new host should be admitted because monitored-system caps are retired, got %d: %s", newHostRec.Code, newHostRec.Body.String())
}
}
-func TestContract_ResolveMonitoredSystemAdmissionPolicyHookUsesCanonicalInput(t *testing.T) {
- SetResolveMonitoredSystemAdmissionPolicy(nil)
- t.Cleanup(func() { SetResolveMonitoredSystemAdmissionPolicy(nil) })
-
- if hook := getResolveMonitoredSystemAdmissionPolicy(); hook != nil {
- t.Fatalf("expected no monitored-system admission hook by default, got %v", hook)
- }
-
- SetResolveMonitoredSystemAdmissionPolicy(func(_ context.Context, input extensions.MonitoredSystemAdmissionInput) extensions.MonitoredSystemAdmissionDecision {
- return extensions.MonitoredSystemAdmissionDecision{
- Current: input.Current,
- Additional: input.Additional,
- Limit: input.Limit,
- UsageAvailable: input.UsageAvailable,
- UsageUnavailableReason: input.UsageUnavailableReason,
- Exceeded: input.CandidateCountsTowardCap &&
- input.UsageAvailable &&
- input.Additional > 0 &&
- input.Limit > 0 &&
- input.Current+input.Additional > input.Limit,
- }
- })
-
- hook := getResolveMonitoredSystemAdmissionPolicy()
- if hook == nil {
- t.Fatal("expected monitored-system admission hook to round-trip through the shared API boundary")
- }
-
- decision := hook(context.Background(), extensions.MonitoredSystemAdmissionInput{
- Current: 5,
- Additional: 1,
- Limit: 5,
- UsageAvailable: true,
- CandidateCountsTowardCap: true,
- })
- if !decision.Exceeded {
- t.Fatalf("expected canonical counted-system input to preserve the exceeded verdict, got %+v", decision)
- }
-
- decision = hook(context.Background(), extensions.MonitoredSystemAdmissionInput{
- Current: 5,
- Additional: 1,
- Limit: 5,
- UsageAvailable: true,
- CandidateCountsTowardCap: false,
- })
- if decision.Exceeded {
- t.Fatalf("expected non-counted candidate to stay outside the exceeded verdict, got %+v", decision)
- }
-}
-
-func TestContract_PlatformConnectionWritesFailClosedWhenUsageUnavailable(t *testing.T) {
+func TestContract_PlatformConnectionWritesIgnoreUsageUnavailableWithCapsRetired(t *testing.T) {
t.Run("truenas add", func(t *testing.T) {
setTrueNASFeatureForTest(t, true)
setMockModeForTrueNASTest(t, false)
@@ -6306,11 +6184,9 @@ func TestContract_PlatformConnectionWritesFailClosedWhenUsageUnavailable(t *test
rec := httptest.NewRecorder()
handler.HandleAdd(rec, req)
- assertMonitoredSystemUsageUnavailableReason(
- t,
- rec,
- monitoring.MonitoredSystemUsageUnavailableSupplementalInventoryUnsettled,
- )
+ if rec.Code != http.StatusCreated {
+ t.Fatalf("expected 201 with monitored-system caps retired, got %d: %s", rec.Code, rec.Body.String())
+ }
})
t.Run("vmware add", func(t *testing.T) {
@@ -6346,13 +6222,11 @@ func TestContract_PlatformConnectionWritesFailClosedWhenUsageUnavailable(t *test
rec := httptest.NewRecorder()
handler.HandleAdd(rec, req)
- assertMonitoredSystemUsageUnavailableReason(
- t,
- rec,
- monitoring.MonitoredSystemUsageUnavailableSupplementalInventoryUnsettled,
- )
+ if rec.Code != http.StatusCreated {
+ t.Fatalf("expected 201 with monitored-system caps retired, got %d: %s", rec.Code, rec.Body.String())
+ }
if previewRecordsCalled {
- t.Fatal("expected VMware write admission to fail before external inventory preview")
+ t.Fatal("expected VMware write not to preview external inventory when monitored-system caps are retired")
}
})
}
@@ -6634,143 +6508,6 @@ func TestContract_PlatformConnectionPreviewPreservesCanonicalEnabledDefaults(t *
})
}
-func TestContract_ConfiguredNodeReplacementsUseCanonicalSelectors(t *testing.T) {
- testCases := []struct {
- name string
- current any
- updated any
- source unifiedresources.DataSource
- currentRoot unifiedresources.Resource
- replacement func(any) unifiedresources.MonitoredSystemReplacement
- candidate func(any) unifiedresources.MonitoredSystemCandidate
- }{
- {
- name: "proxmox",
- current: config.PVEInstance{
- Name: " pve-a ",
- Host: " https://pve-a.lab.local:8006 ",
- },
- updated: config.PVEInstance{
- Name: "pve-b",
- Host: "https://pve-b.lab.local:8006",
- },
- source: unifiedresources.SourceProxmox,
- currentRoot: unifiedresources.Resource{
- ID: "pve-a",
- Type: unifiedresources.ResourceTypeAgent,
- Name: "pve-a",
- Status: unifiedresources.StatusOnline,
- Proxmox: &unifiedresources.ProxmoxData{
- Instance: "pve-a",
- NodeName: "pve-a-node",
- HostURL: "https://pve-a.lab.local:8006",
- },
- },
- replacement: func(value any) unifiedresources.MonitoredSystemReplacement {
- return proxmoxMonitoredSystemReplacement(value.(config.PVEInstance))
- },
- candidate: func(value any) unifiedresources.MonitoredSystemCandidate {
- return proxmoxMonitoredSystemCandidate(value.(config.PVEInstance))
- },
- },
- {
- name: "pbs",
- current: config.PBSInstance{
- Name: " backup-a ",
- Host: " https://backup-a.lab.local:8007 ",
- },
- updated: config.PBSInstance{
- Name: "backup-b",
- Host: "https://backup-b.lab.local:8007",
- },
- source: unifiedresources.SourcePBS,
- currentRoot: unifiedresources.Resource{
- ID: "pbs-a",
- Type: unifiedresources.ResourceTypePBS,
- Name: "backup-a",
- Status: unifiedresources.StatusOnline,
- PBS: &unifiedresources.PBSData{
- InstanceID: "backup-a",
- Hostname: "backup-a.lab.local",
- HostURL: "https://backup-a.lab.local:8007",
- },
- },
- replacement: func(value any) unifiedresources.MonitoredSystemReplacement {
- return pbsMonitoredSystemReplacement(value.(config.PBSInstance))
- },
- candidate: func(value any) unifiedresources.MonitoredSystemCandidate {
- return pbsMonitoredSystemCandidate(value.(config.PBSInstance))
- },
- },
- {
- name: "pmg",
- current: config.PMGInstance{
- Name: " mail-a ",
- Host: " https://mail-a.lab.local:8006 ",
- },
- updated: config.PMGInstance{
- Name: "mail-b",
- Host: "https://mail-b.lab.local:8006",
- },
- source: unifiedresources.SourcePMG,
- currentRoot: unifiedresources.Resource{
- ID: "pmg-a",
- Type: unifiedresources.ResourceTypePMG,
- Name: "mail-a",
- Status: unifiedresources.StatusOnline,
- PMG: &unifiedresources.PMGData{
- InstanceID: "mail-a",
- Hostname: "mail-a.lab.local",
- },
- },
- replacement: func(value any) unifiedresources.MonitoredSystemReplacement {
- return pmgMonitoredSystemReplacement(value.(config.PMGInstance))
- },
- candidate: func(value any) unifiedresources.MonitoredSystemCandidate {
- return pmgMonitoredSystemCandidate(value.(config.PMGInstance))
- },
- },
- }
-
- for _, tc := range testCases {
- t.Run(tc.name, func(t *testing.T) {
- replacement := tc.replacement(tc.current)
- if replacement.Source != tc.source {
- t.Fatalf("replacement source = %q, want %q", replacement.Source, tc.source)
- }
- if replacement.Matches != nil {
- t.Fatal("configured node replacement must use the canonical selector contract, not a handler-local matcher")
- }
- if !replacement.MatchesResource(tc.currentRoot) {
- t.Fatalf("replacement selector did not match current %s root", tc.name)
- }
-
- registry := unifiedresources.NewRegistry(nil)
- registry.IngestRecords(tc.source, []unifiedresources.IngestRecord{
- {
- SourceID: tc.currentRoot.ID,
- Resource: tc.currentRoot,
- },
- })
-
- projection := unifiedresources.ProjectMonitoredSystemCandidateReplacement(
- registry,
- replacement,
- tc.candidate(tc.updated),
- )
- if projection.CurrentCount != 1 {
- t.Fatalf("CurrentCount = %d, want 1", projection.CurrentCount)
- }
- if projection.ProjectedCount != 1 {
- t.Fatalf("ProjectedCount = %d, want 1", projection.ProjectedCount)
- }
- if projection.AdditionalCount != 0 {
- t.Fatalf("AdditionalCount = %d, want 0", projection.AdditionalCount)
- }
- })
- }
-}
-
func TestContract_DemoModeCommercialSurfacePolicy(t *testing.T) {
t.Run("hidden routes return not found", func(t *testing.T) {
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -12845,18 +12582,18 @@ func TestContract_BootstrapTokenValidationRateLimitsPerClient(t *testing.T) {
}
}
-func TestContract_DeployCapacityDenialUsesWorkspaceCapacityCopy(t *testing.T) {
+func TestContract_DeployHandlersDoNotSurfaceLicenseSlotCapacityCopy(t *testing.T) {
source, err := os.ReadFile("deploy_handlers.go")
if err != nil {
t.Fatalf("read deploy handlers: %v", err)
}
text := string(source)
- if !strings.Contains(text, `"No workspace capacity available for retry"`) {
- t.Fatal("deploy retry capacity denial must use workspace-capacity copy")
- }
if strings.Contains(text, "No license slots available") {
t.Fatal("deploy retry capacity denial must not surface legacy license-slot copy")
}
+ if strings.Contains(text, "reservedLicenseSlots") {
+ t.Fatal("deploy response must not expose retired reservedLicenseSlots field")
+ }
}
func mustStreamEvent(t *testing.T, eventType string, data interface{}) chat.StreamEvent {
diff --git a/internal/api/demo_mode_commercial.go b/internal/api/demo_mode_commercial.go
index 1be969ab5..3df06bc90 100644
--- a/internal/api/demo_mode_commercial.go
+++ b/internal/api/demo_mode_commercial.go
@@ -124,7 +124,6 @@ func sanitizeRuntimeCapabilitiesPayloadForPublicDemo(
) RuntimeCapabilitiesPayload {
sanitized := payload
sanitized.Limits = sanitizeLimitStatusesForPublicDemo(payload.Limits)
- sanitized.MonitoredSystemCapacity = nil
if sanitized.Capabilities == nil {
sanitized.Capabilities = []string{}
}
diff --git a/internal/api/demo_mode_commercial_test.go b/internal/api/demo_mode_commercial_test.go
index d8099dc15..80c8e9d13 100644
--- a/internal/api/demo_mode_commercial_test.go
+++ b/internal/api/demo_mode_commercial_test.go
@@ -5,8 +5,6 @@ import (
"net/http/httptest"
"strings"
"testing"
-
- pkglicensing "github.com/rcourtman/pulse-go-rewrite/pkg/licensing"
)
func TestPublicDemoCommercialRouteInventoryCoverage(t *testing.T) {
@@ -45,7 +43,7 @@ func TestSanitizeRuntimeCapabilitiesPayloadForPublicDemo(t *testing.T) {
Capabilities: []string{"relay", "ai_patrol"},
Limits: []LimitStatus{
{
- Key: maxMonitoredSystemsLicenseGateKey,
+ Key: "max_guests",
Limit: 5,
Current: 16,
State: "enforced",
@@ -53,17 +51,6 @@ func TestSanitizeRuntimeCapabilitiesPayloadForPublicDemo(t *testing.T) {
},
HostedMode: true,
MaxHistoryDays: 90,
- MonitoredSystemCapacity: &pkglicensing.MonitoredSystemCapacityStatus{
- Mode: "over_limit_frozen",
- Urgency: "enforced",
- Current: 16,
- Limit: 5,
- CurrentAvailable: true,
- AvailableSlots: 0,
- Overage: 11,
- BlocksNewSystems: true,
- ExistingMonitoringContinues: true,
- },
})
if len(sanitized.Capabilities) != 2 {
@@ -78,9 +65,6 @@ func TestSanitizeRuntimeCapabilitiesPayloadForPublicDemo(t *testing.T) {
if sanitized.MaxHistoryDays != 90 || !sanitized.HostedMode {
t.Fatalf("non-commercial runtime capability fields should be preserved, got max_history_days=%d hosted_mode=%v", sanitized.MaxHistoryDays, sanitized.HostedMode)
}
- if sanitized.MonitoredSystemCapacity != nil {
- t.Fatalf("monitored_system_capacity=%+v, want nil after public demo sanitization", sanitized.MonitoredSystemCapacity)
- }
}
func TestPublicDemoCommercialPolicyForRequestHidesUsagePreviewRoutes(t *testing.T) {
diff --git a/internal/api/deploy_handlers.go b/internal/api/deploy_handlers.go
index 4485336fd..e7f639eab 100644
--- a/internal/api/deploy_handlers.go
+++ b/internal/api/deploy_handlers.go
@@ -23,10 +23,9 @@ import (
// DeployHandlers provides HTTP handlers for cluster agent deployment.
type DeployHandlers struct {
- store *deploy.Store
- monitor *monitoring.Monitor
- execServer *agentexec.Server
- reservation *deploy.ReservationManager
+ store *deploy.Store
+ monitor *monitoring.Monitor
+ execServer *agentexec.Server
// resolvePublicURL derives the Pulse URL for agent reachability checks.
resolvePublicURL func(req *http.Request) string
@@ -51,7 +50,6 @@ func NewDeployHandlers(
store *deploy.Store,
monitor *monitoring.Monitor,
execServer *agentexec.Server,
- reservation *deploy.ReservationManager,
resolvePublicURL func(req *http.Request) string,
cfg *config.Config,
persistence *config.ConfigPersistence,
@@ -60,7 +58,6 @@ func NewDeployHandlers(
store: store,
monitor: monitor,
execServer: execServer,
- reservation: reservation,
resolvePublicURL: resolvePublicURL,
config: cfg,
persistence: persistence,
@@ -968,11 +965,10 @@ type createJobSkip struct {
}
type createJobResponse struct {
- JobID string `json:"jobId"`
- AcceptedTargets []string `json:"acceptedTargets"`
- SkippedTargets []createJobSkip `json:"skippedTargets"`
- ReservedLicenseSlots int `json:"reservedLicenseSlots"`
- EventsURL string `json:"eventsUrl"`
+ JobID string `json:"jobId"`
+ AcceptedTargets []string `json:"acceptedTargets"`
+ SkippedTargets []createJobSkip `json:"skippedTargets"`
+ EventsURL string `json:"eventsUrl"`
}
// HandleCreateJob creates a deploy install job from preflight results.
@@ -1094,30 +1090,6 @@ func (h *DeployHandlers) HandleCreateJob(w http.ResponseWriter, r *http.Request)
acceptedPfTargets = append(acceptedPfTargets, pfTgt)
}
- // Workspace capacity check.
- maxLimit := maxMonitoredSystemsLimitForContext(ctx)
- if maxLimit > 0 {
- decision := monitoredSystemLimitDecisionForAdditionalSlots(ctx, h.monitor, 0)
- if !decision.usageAvailable {
- writeMonitoredSystemUsageUnavailable(w, decision.usageUnavailableReason)
- return
- }
- available := decision.limit - decision.current
- if available < 0 {
- available = 0
- }
- if available < len(acceptedPfTargets) {
- // Accept only what fits; skip the rest.
- for i := available; i < len(acceptedPfTargets); i++ {
- skipped = append(skipped, createJobSkip{
- NodeID: acceptedPfTargets[i].NodeID,
- Reason: "skipped_license",
- })
- }
- acceptedPfTargets = acceptedPfTargets[:available]
- }
- }
-
if len(acceptedPfTargets) == 0 {
writeErrorResponse(w, http.StatusConflict, "no_eligible_targets",
"No targets are eligible for deployment", nil)
@@ -1226,12 +1198,6 @@ func (h *DeployHandlers) HandleCreateJob(w http.ResponseWriter, r *http.Request)
return
}
- // Reserve workspace capacity based on actual dispatched target count.
- if err := h.reservation.Reserve(jobID, orgID, len(installTargets), 1*time.Hour); err != nil {
- log.Error().Err(err).Str("job_id", jobID).Msg("Failed to reserve workspace capacity")
- // Non-fatal — continue. The reservation is for proactive capacity tracking.
- }
-
// Transition to running.
_ = h.store.UpdateJobStatus(ctx, jobID, deploy.JobRunning)
@@ -1261,7 +1227,6 @@ func (h *DeployHandlers) HandleCreateJob(w http.ResponseWriter, r *http.Request)
if err := h.execServer.SendDeployInstall(ctx, req.SourceAgentID, payload); err != nil {
h.execServer.UnsubscribeDeployProgress(req.SourceAgentID, jobID)
_ = h.store.UpdateJobStatus(ctx, jobID, deploy.JobFailed)
- h.reservation.Release(jobID)
// Mark pending targets as failed so they're eligible for retry.
for _, it := range installTargets {
_ = h.store.UpdateTargetStatus(ctx, it.TargetID, deploy.TargetFailedRetryable, "dispatch failed")
@@ -1276,11 +1241,10 @@ func (h *DeployHandlers) HandleCreateJob(w http.ResponseWriter, r *http.Request)
go h.processInstallProgress(jobID, req.SourceAgentID, job.RetryMax, progressCh)
resp := createJobResponse{
- JobID: jobID,
- AcceptedTargets: acceptedNodeIDs,
- SkippedTargets: skipped,
- ReservedLicenseSlots: len(installTargets),
- EventsURL: fmt.Sprintf("/api/agent-deploy/jobs/%s/events", jobID),
+ JobID: jobID,
+ AcceptedTargets: acceptedNodeIDs,
+ SkippedTargets: skipped,
+ EventsURL: fmt.Sprintf("/api/agent-deploy/jobs/%s/events", jobID),
}
w.Header().Set("Content-Type", "application/json")
@@ -1594,28 +1558,6 @@ func (h *DeployHandlers) HandleRetryJob(w http.ResponseWriter, r *http.Request)
return
}
- // Workspace capacity re-check.
- maxLimit := maxMonitoredSystemsLimitForContext(ctx)
- if maxLimit > 0 {
- decision := monitoredSystemLimitDecisionForAdditionalSlots(ctx, h.monitor, 0)
- if !decision.usageAvailable {
- writeMonitoredSystemUsageUnavailable(w, decision.usageUnavailableReason)
- return
- }
- available := decision.limit - decision.current
- if available < 0 {
- available = 0
- }
- if available < len(retryTargets) {
- retryTargets = retryTargets[:available]
- }
- if len(retryTargets) == 0 {
- writeErrorResponse(w, http.StatusConflict, "license_limit",
- "No workspace capacity available for retry", nil)
- return
- }
- }
-
// Reset targets to pending.
retryIDs := make([]string, len(retryTargets))
for i, t := range retryTargets {
@@ -1644,7 +1586,6 @@ func (h *DeployHandlers) HandleRetryJob(w http.ResponseWriter, r *http.Request)
for _, id := range retryIDs {
_ = h.store.UpdateTargetStatus(ctx, id, deploy.TargetFailedRetryable, "dispatch failed: no Pulse URL")
}
- h.reservation.Release(jobID + "-retry")
writeErrorResponse(w, http.StatusInternalServerError, "no_pulse_url",
"Cannot determine Pulse URL for agent installation", nil)
return
@@ -1689,11 +1630,6 @@ func (h *DeployHandlers) HandleRetryJob(w http.ResponseWriter, r *http.Request)
return
}
- // Reserve workspace capacity based on actual dispatch count (after token minting).
- if err := h.reservation.Reserve(jobID+"-retry", orgID, len(installTargets), 1*time.Hour); err != nil {
- log.Warn().Err(err).Str("job_id", jobID).Msg("Failed to reserve workspace capacity for retry")
- }
-
// Append retry event.
_ = h.store.AppendEvent(ctx, &deploy.Event{
ID: generateID("evt"),
@@ -1718,7 +1654,6 @@ func (h *DeployHandlers) HandleRetryJob(w http.ResponseWriter, r *http.Request)
if err := h.execServer.SendDeployInstall(ctx, job.SourceAgentID, payload); err != nil {
h.execServer.UnsubscribeDeployProgress(job.SourceAgentID, jobID)
_ = h.store.UpdateJobStatus(ctx, jobID, deploy.JobFailed)
- h.reservation.Release(jobID + "-retry")
// Mark retried targets back to failed so they can be retried again.
for _, it := range installTargets {
_ = h.store.UpdateTargetStatus(ctx, it.TargetID, deploy.TargetFailedRetryable, "dispatch failed")
@@ -1796,10 +1731,6 @@ func (h *DeployHandlers) processInstallProgress(jobID, agentID string, retryMax
h.broadcastSSE(jobID, finalEvt)
}
- // Release license reservation.
- h.reservation.Release(jobID)
- h.reservation.Release(jobID + "-retry") // in case of retry
-
// Close SSE channels.
h.closeSSESub(jobID)
return
@@ -1808,8 +1739,6 @@ func (h *DeployHandlers) processInstallProgress(jobID, agentID string, retryMax
// Channel closed without final — agent disconnected.
_ = h.store.UpdateJobStatus(ctx, jobID, deploy.JobFailed)
- h.reservation.Release(jobID)
- h.reservation.Release(jobID + "-retry")
finalEvt := &deploy.Event{
ID: generateID("evt"),
@@ -1999,7 +1928,7 @@ func deriveInstallJobStatus(targets []deploy.Target) deploy.JobStatus {
// enrolling = install completed, enrollment is async and expected to succeed
succeeded++
case deploy.TargetFailedPermanent, deploy.TargetFailedRetryable,
- deploy.TargetSkippedAgent, deploy.TargetSkippedLicense, deploy.TargetCanceled:
+ deploy.TargetSkippedAgent, deploy.TargetCanceled:
failed++
// pending/installing — shouldn't happen at Final but treat as incomplete
default:
diff --git a/internal/api/deploy_handlers_test.go b/internal/api/deploy_handlers_test.go
index c08d23c26..658717c41 100644
--- a/internal/api/deploy_handlers_test.go
+++ b/internal/api/deploy_handlers_test.go
@@ -41,13 +41,12 @@ func newTestDeployHandlers(t *testing.T, nodes []models.Node, hosts []models.Hos
}
execServer := agentexec.NewServer(func(string, string, string) bool { return true })
- reservation := deploy.NewReservationManager()
cfg := &config.Config{
DataPath: t.TempDir(),
}
- return NewDeployHandlers(store, monitor, execServer, reservation, func(_ *http.Request) string {
+ return NewDeployHandlers(store, monitor, execServer, func(_ *http.Request) string {
return "http://10.0.0.1:7655"
}, cfg, nil)
}
@@ -1055,9 +1054,6 @@ func TestHandleCreateJob_Success(t *testing.T) {
if len(resp.SkippedTargets) != 0 {
t.Fatalf("expected 0 skipped targets, got %d", len(resp.SkippedTargets))
}
- if resp.ReservedLicenseSlots != 2 {
- t.Fatalf("expected 2 reserved workspace capacity slots, got %d", resp.ReservedLicenseSlots)
- }
if resp.EventsURL == "" {
t.Fatal("expected non-empty eventsUrl")
}
@@ -1179,7 +1175,7 @@ func TestHandleCreateJob_TargetsNotReady(t *testing.T) {
}
}
-func TestHandleCreateJob_TruncatesTargetsToAvailableLicenseSlots(t *testing.T) {
+func TestHandleCreateJob_AcceptsAllTargetsWithMonitoredSystemCapsRetired(t *testing.T) {
setMaxMonitoredSystemsLicenseForTests(t, 4)
nodes := []models.Node{
@@ -1238,20 +1234,14 @@ func TestHandleCreateJob_TruncatesTargetsToAvailableLicenseSlots(t *testing.T) {
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("decode: %v", err)
}
- if len(resp.AcceptedTargets) != 1 {
- t.Fatalf("expected 1 accepted target, got %d", len(resp.AcceptedTargets))
+ if len(resp.AcceptedTargets) != 2 {
+ t.Fatalf("expected 2 accepted targets with monitored-system caps retired, got %d", len(resp.AcceptedTargets))
}
- if resp.AcceptedTargets[0] != "node_pve-b" {
- t.Fatalf("expected deterministic accepted node_pve-b, got %q", resp.AcceptedTargets[0])
+ if resp.AcceptedTargets[0] != "node_pve-b" || resp.AcceptedTargets[1] != "node_pve-c" {
+ t.Fatalf("unexpected accepted targets: %+v", resp.AcceptedTargets)
}
- if resp.ReservedLicenseSlots != 1 {
- t.Fatalf("expected 1 reserved workspace capacity slot, got %d", resp.ReservedLicenseSlots)
- }
- if len(resp.SkippedTargets) != 1 {
- t.Fatalf("expected 1 skipped target, got %d", len(resp.SkippedTargets))
- }
- if resp.SkippedTargets[0].NodeID != "node_pve-c" || resp.SkippedTargets[0].Reason != "skipped_license" {
- t.Fatalf("unexpected skipped target: %+v", resp.SkippedTargets[0])
+ if len(resp.SkippedTargets) != 0 {
+ t.Fatalf("expected no skipped targets with monitored-system caps retired, got %+v", resp.SkippedTargets)
}
}
@@ -1424,7 +1414,7 @@ func TestHandleRetryJob_Success(t *testing.T) {
}
}
-func TestHandleRetryJob_BlocksWhenNoLicenseSlotsAvailable(t *testing.T) {
+func TestHandleRetryJob_AllowsRetryWithMonitoredSystemCapsRetired(t *testing.T) {
setMaxMonitoredSystemsLicenseForTests(t, 1)
h := newTestDeployHandlers(t, nil, []models.Host{{ID: "host-existing", Hostname: "existing"}})
@@ -1455,19 +1445,16 @@ func TestHandleRetryJob_BlocksWhenNoLicenseSlotsAvailable(t *testing.T) {
rec := httptest.NewRecorder()
h.HandleRetryJob(rec, req)
- if rec.Code != http.StatusConflict {
- t.Fatalf("expected 409, got %d: %s", rec.Code, rec.Body.String())
- }
- if !strings.Contains(rec.Body.String(), "license_limit") {
- t.Fatalf("expected license_limit response, got %s", rec.Body.String())
+ if rec.Code != http.StatusAccepted {
+ t.Fatalf("expected 202 with monitored-system caps retired, got %d: %s", rec.Code, rec.Body.String())
}
targets, err := h.store.GetTargetsForJob(ctx, "dep_retry_license")
if err != nil {
t.Fatalf("get targets: %v", err)
}
- if len(targets) != 1 || targets[0].Status != deploy.TargetFailedRetryable {
- t.Fatalf("expected retry target to remain failed_retryable, got %+v", targets)
+ if len(targets) != 1 || targets[0].Status != deploy.TargetPending {
+ t.Fatalf("expected retry target to be queued, got %+v", targets)
}
}
@@ -1630,9 +1617,6 @@ func TestProcessInstallProgress_AgentDisconnect(t *testing.T) {
t.Fatalf("create job: %v", err)
}
- // Reserve so we can verify release.
- _ = h.reservation.Reserve("dep_disc", "default", 1, 1*time.Hour)
-
// Close the channel immediately to simulate disconnect.
ch := make(chan agentexec.DeployProgressPayload, 1)
close(ch)
diff --git a/internal/api/docker_agents.go b/internal/api/docker_agents.go
index f371c8c4d..29146516a 100644
--- a/internal/api/docker_agents.go
+++ b/internal/api/docker_agents.go
@@ -106,16 +106,6 @@ func (h *DockerAgentHandlers) HandleReport(w http.ResponseWriter, r *http.Reques
}
tokenRecord := getAPITokenRecordFromRequest(r)
- if enforceMonitoredSystemLimitForDockerReport(
- w,
- r.Context(),
- h.getMonitor(r.Context()),
- report,
- tokenRecord,
- ) {
- return
- }
-
host, err := h.getMonitor(r.Context()).ApplyDockerReport(report, tokenRecord)
if err != nil {
writeErrorResponse(w, http.StatusBadRequest, "invalid_report", err.Error(), nil)
diff --git a/internal/api/docker_agents_additional_test.go b/internal/api/docker_agents_additional_test.go
index 2b515c7d5..5905244f9 100644
--- a/internal/api/docker_agents_additional_test.go
+++ b/internal/api/docker_agents_additional_test.go
@@ -109,9 +109,9 @@ func TestDockerAgentHandlers_HandleReport(t *testing.T) {
}
}
-// TestDockerAgentHandlers_HandleReport_BlocksNewMonitoredSystemAtLimit verifies
-// that a new Docker host counts as a monitored system.
-func TestDockerAgentHandlers_HandleReport_BlocksNewMonitoredSystemAtLimit(t *testing.T) {
+// TestDockerAgentHandlers_HandleReport_AllowsNewMonitoredSystemWithCapsRetired
+// verifies that a new Docker host is accepted without monitored-system caps.
+func TestDockerAgentHandlers_HandleReport_AllowsNewMonitoredSystemWithCapsRetired(t *testing.T) {
setMaxMonitoredSystemsLicenseForTests(t, 1)
handler, monitor := newDockerAgentHandlers(t, nil)
@@ -120,7 +120,7 @@ func TestDockerAgentHandlers_HandleReport_BlocksNewMonitoredSystemAtLimit(t *tes
t.Fatalf("expected seeded host ID")
}
- // New Docker host should be blocked because it is a distinct monitored system.
+ // New Docker host should be accepted because monitored-system caps are retired.
newReport := agentsdocker.Report{
Agent: agentsdocker.AgentInfo{
ID: "agent-2",
@@ -142,8 +142,8 @@ func TestDockerAgentHandlers_HandleReport_BlocksNewMonitoredSystemAtLimit(t *tes
newReq := httptest.NewRequest(http.MethodPost, "/api/agents/docker/report", bytes.NewReader(newBody))
newRec := httptest.NewRecorder()
handler.HandleReport(newRec, newReq)
- if newRec.Code != http.StatusPaymentRequired {
- t.Fatalf("status = %d, want 402: %s", newRec.Code, newRec.Body.String())
+ if newRec.Code != http.StatusOK {
+ t.Fatalf("status = %d, want 200: %s", newRec.Code, newRec.Body.String())
}
}
diff --git a/internal/api/enterprise_extension_monitored_system_admission.go b/internal/api/enterprise_extension_monitored_system_admission.go
deleted file mode 100644
index 74408c7b5..000000000
--- a/internal/api/enterprise_extension_monitored_system_admission.go
+++ /dev/null
@@ -1,26 +0,0 @@
-package api
-
-import (
- "sync"
-
- "github.com/rcourtman/pulse-go-rewrite/pkg/extensions"
-)
-
-var (
- monitoredSystemAdmissionPolicyMu sync.RWMutex
- resolveMonitoredSystemPolicyFunc extensions.ResolveMonitoredSystemAdmissionPolicyFunc
-)
-
-// SetResolveMonitoredSystemAdmissionPolicy registers a private commercial
-// policy hook for monitored-system admission decisions.
-func SetResolveMonitoredSystemAdmissionPolicy(fn extensions.ResolveMonitoredSystemAdmissionPolicyFunc) {
- monitoredSystemAdmissionPolicyMu.Lock()
- defer monitoredSystemAdmissionPolicyMu.Unlock()
- resolveMonitoredSystemPolicyFunc = fn
-}
-
-func getResolveMonitoredSystemAdmissionPolicy() extensions.ResolveMonitoredSystemAdmissionPolicyFunc {
- monitoredSystemAdmissionPolicyMu.RLock()
- defer monitoredSystemAdmissionPolicyMu.RUnlock()
- return resolveMonitoredSystemPolicyFunc
-}
diff --git a/internal/api/enterprise_extension_monitored_system_admission_test.go b/internal/api/enterprise_extension_monitored_system_admission_test.go
deleted file mode 100644
index efdebad0d..000000000
--- a/internal/api/enterprise_extension_monitored_system_admission_test.go
+++ /dev/null
@@ -1,52 +0,0 @@
-package api
-
-import (
- "context"
- "testing"
-
- "github.com/rcourtman/pulse-go-rewrite/pkg/extensions"
-)
-
-func TestGetResolveMonitoredSystemAdmissionPolicy_DefaultNil(t *testing.T) {
- SetResolveMonitoredSystemAdmissionPolicy(nil)
- t.Cleanup(func() { SetResolveMonitoredSystemAdmissionPolicy(nil) })
-
- if getResolveMonitoredSystemAdmissionPolicy() != nil {
- t.Fatal("expected nil when no monitored-system admission policy hook is registered")
- }
-}
-
-func TestGetResolveMonitoredSystemAdmissionPolicy_RoundTripsHook(t *testing.T) {
- SetResolveMonitoredSystemAdmissionPolicy(nil)
- t.Cleanup(func() { SetResolveMonitoredSystemAdmissionPolicy(nil) })
-
- SetResolveMonitoredSystemAdmissionPolicy(func(_ context.Context, input extensions.MonitoredSystemAdmissionInput) extensions.MonitoredSystemAdmissionDecision {
- return extensions.MonitoredSystemAdmissionDecision{
- Current: input.Current,
- Additional: input.Additional,
- Limit: input.Limit,
- UsageAvailable: input.UsageAvailable,
- UsageUnavailableReason: input.UsageUnavailableReason,
- Exceeded: input.UsageAvailable && input.Additional > 0 && input.Limit > 0 && input.Current+input.Additional > input.Limit,
- }
- })
-
- hook := getResolveMonitoredSystemAdmissionPolicy()
- if hook == nil {
- t.Fatal("expected monitored-system admission policy hook to be returned")
- }
-
- decision := hook(context.Background(), extensions.MonitoredSystemAdmissionInput{
- Current: 5,
- Additional: 1,
- Limit: 5,
- UsageAvailable: true,
- UsageUnavailableReason: "",
- })
- if !decision.Exceeded {
- t.Fatalf("expected hook result to preserve exceeded state, got %+v", decision)
- }
- if decision.Current != 5 || decision.Additional != 1 || decision.Limit != 5 {
- t.Fatalf("expected hook result to preserve admission values, got %+v", decision)
- }
-}
diff --git a/internal/api/entitlement_handlers_test.go b/internal/api/entitlement_handlers_test.go
index 8ee5bff8c..cec175da7 100644
--- a/internal/api/entitlement_handlers_test.go
+++ b/internal/api/entitlement_handlers_test.go
@@ -28,10 +28,9 @@ func containsCapability(values []string, key string) bool {
func TestBuildEntitlementPayload_ActiveLicense(t *testing.T) {
status := &license.LicenseStatus{
- Valid: true,
- Tier: license.TierPro,
- Features: append([]string(nil), license.TierFeatures[license.TierPro]...),
- MaxMonitoredSystems: 0,
+ Valid: true,
+ Tier: license.TierPro,
+ Features: append([]string(nil), license.TierFeatures[license.TierPro]...),
}
payload := buildEntitlementPayload(status, "")
@@ -46,12 +45,6 @@ func TestBuildEntitlementPayload_ActiveLicense(t *testing.T) {
if len(payload.Limits) != 0 {
t.Fatalf("expected no max_monitored_systems limit in payload, got %+v", payload.Limits)
}
- if payload.MonitoredSystemCapacity == nil {
- t.Fatal("expected monitored_system_capacity in payload")
- }
- if payload.MonitoredSystemCapacity.Mode != "usage_unavailable" {
- t.Fatalf("expected usage_unavailable monitored-system capacity before inventory settles, got %+v", payload.MonitoredSystemCapacity)
- }
if len(payload.UpgradeReasons) != 0 {
t.Fatalf("expected no upgrade reasons for pro tier, got %d", len(payload.UpgradeReasons))
}
@@ -81,11 +74,10 @@ func TestBuildEntitlementPayload_FreeTier(t *testing.T) {
func TestBuildEntitlementPayloadWithUsage_CurrentValues(t *testing.T) {
status := &license.LicenseStatus{
- Valid: true,
- Tier: license.TierPro,
- Features: append([]string(nil), license.TierFeatures[license.TierPro]...),
- MaxMonitoredSystems: 0,
- MaxGuests: 100,
+ Valid: true,
+ Tier: license.TierPro,
+ Features: append([]string(nil), license.TierFeatures[license.TierPro]...),
+ MaxGuests: 100,
}
payload := buildEntitlementPayloadWithUsage(status, "", entitlementUsageSnapshot{
@@ -116,12 +108,6 @@ func TestBuildEntitlementPayloadWithUsage_CurrentValues(t *testing.T) {
if agentLimit != nil {
t.Fatalf("expected no max_monitored_systems limit for self-hosted pro, got %+v", agentLimit)
}
- if payload.MonitoredSystemCapacity == nil {
- t.Fatal("expected monitored_system_capacity")
- }
- if payload.MonitoredSystemCapacity.Mode != "unlimited" || payload.MonitoredSystemCapacity.Current != 12 {
- t.Fatalf("expected unlimited monitored-system capacity with current=12, got %+v", payload.MonitoredSystemCapacity)
- }
if payload.LegacyConnections.ProxmoxNodes != 2 {
t.Fatalf("expected proxmox_nodes 2, got %d", payload.LegacyConnections.ProxmoxNodes)
}
@@ -132,22 +118,15 @@ func TestBuildEntitlementPayloadWithUsage_CurrentValues(t *testing.T) {
func TestBuildEntitlementPayloadWithUsage_MonitoredSystemUsageUnavailable(t *testing.T) {
status := &license.LicenseStatus{
- Valid: true,
- Tier: license.TierPro,
- Features: append([]string(nil), license.TierFeatures[license.TierPro]...),
- MaxMonitoredSystems: 0,
+ Valid: true,
+ Tier: license.TierPro,
+ Features: append([]string(nil), license.TierFeatures[license.TierPro]...),
}
payload := buildEntitlementPayloadWithUsage(status, "", entitlementUsageSnapshot{}, nil)
if len(payload.Limits) != 0 {
t.Fatalf("expected no monitored-system limit, got %d", len(payload.Limits))
}
- if payload.MonitoredSystemCapacity == nil {
- t.Fatal("expected monitored_system_capacity")
- }
- if payload.MonitoredSystemCapacity.Mode != "usage_unavailable" {
- t.Fatalf("expected usage_unavailable monitored-system capacity, got %+v", payload.MonitoredSystemCapacity)
- }
}
func TestBuildEntitlementPayload_Expired(t *testing.T) {
@@ -190,10 +169,9 @@ func TestBuildEntitlementPayload_NilCapabilities(t *testing.T) {
func TestBuildCommercialPosturePayloadWithUsage_CurrentValues(t *testing.T) {
status := &license.LicenseStatus{
- Valid: true,
- Tier: license.TierFree,
- Features: append([]string(nil), license.TierFeatures[license.TierFree]...),
- MaxMonitoredSystems: 0,
+ Valid: true,
+ Tier: license.TierFree,
+ Features: append([]string(nil), license.TierFeatures[license.TierFree]...),
}
payload := buildCommercialPosturePayloadWithUsage(status, "", entitlementUsageSnapshot{
@@ -214,9 +192,6 @@ func TestBuildCommercialPosturePayloadWithUsage_CurrentValues(t *testing.T) {
if len(payload.UpgradeReasons) == 0 {
t.Fatal("expected upgrade reasons for free-tier commercial posture")
}
- if payload.MonitoredSystemCapacity == nil || payload.MonitoredSystemCapacity.Mode != "unlimited" {
- t.Fatalf("expected unlimited monitored-system capacity in commercial posture, got %+v", payload.MonitoredSystemCapacity)
- }
if payload.LegacyConnections.ProxmoxNodes != 2 || payload.LegacyConnections.DockerHosts != 1 {
t.Fatalf("expected legacy counts to be preserved, got %+v", payload.LegacyConnections)
}
@@ -415,19 +390,11 @@ func TestEntitlementHandler_HostedEvaluatorKeepsCloudLimitsWhenNoLicense(t *test
t.Fatalf("expected capabilities to include %q, got %v", license.FeatureAIPatrol, payload.Capabilities)
}
- var maxMonitoredSystems *LimitStatus
for i := range payload.Limits {
if payload.Limits[i].Key == "max_monitored_systems" {
- maxMonitoredSystems = &payload.Limits[i]
- break
+ t.Fatalf("expected retired monitored-system limit to be omitted, got %v", payload.Limits)
}
}
- if maxMonitoredSystems == nil {
- t.Fatalf("expected max_monitored_systems limit in payload, got %v", payload.Limits)
- }
- if maxMonitoredSystems.Limit != 10 {
- t.Fatalf("max_monitored_systems.limit=%d, want %d", maxMonitoredSystems.Limit, 10)
- }
// Parity: every advertised capability must be enforced by HasFeature.
ctx := context.WithValue(context.Background(), OrgIDContextKey, orgID)
@@ -589,12 +556,6 @@ func TestHandleRuntimeCapabilities_HostedCommunityEvaluatorStateStripsLegacyComm
t.Fatalf("expected runtime capabilities to omit stale commercial caps, got %+v", payload.Limits)
}
}
- if payload.MonitoredSystemCapacity == nil {
- t.Fatal("expected monitored_system_capacity in runtime capabilities payload")
- }
- if payload.MonitoredSystemCapacity.Limit != 0 {
- t.Fatalf("monitored_system_capacity.limit=%d, want %d", payload.MonitoredSystemCapacity.Limit, 0)
- }
}
func TestEntitlementHandler_SelfHostedTrialEligibilityRetiredForFreshOrg(t *testing.T) {
@@ -629,15 +590,6 @@ func TestEntitlementHandler_SelfHostedTrialEligibilityRetiredForFreshOrg(t *test
t.Fatalf("expected no max_monitored_systems limit in payload, got %+v", payload.Limits)
}
}
- if payload.MonitoredSystemCapacity == nil {
- t.Fatal("expected monitored_system_capacity in payload")
- }
- if payload.MonitoredSystemCapacity.Limit != 0 || payload.MonitoredSystemCapacity.BlocksNewSystems {
- t.Fatalf(
- "expected uncapped monitored_system_capacity for fresh community org, got %+v",
- payload.MonitoredSystemCapacity,
- )
- }
}
func TestEntitlementHandler_OverflowOnlyBillingStateReportsActiveCommunity(t *testing.T) {
@@ -674,9 +626,6 @@ func TestEntitlementHandler_OverflowOnlyBillingStateReportsActiveCommunity(t *te
if payload.TrialEligible {
t.Fatalf("trial_eligible=%v, want false", payload.TrialEligible)
}
- if payload.MonitoredSystemCapacity == nil || payload.MonitoredSystemCapacity.Limit != 0 || payload.MonitoredSystemCapacity.BlocksNewSystems {
- t.Fatalf("expected uncapped monitored_system_capacity, got %+v", payload.MonitoredSystemCapacity)
- }
}
func TestEntitlementHandler_DevModeDoesNotExposeSyntheticFeatureGateCapabilities(t *testing.T) {
diff --git a/internal/api/kubernetes_agents.go b/internal/api/kubernetes_agents.go
index c33477069..738b50008 100644
--- a/internal/api/kubernetes_agents.go
+++ b/internal/api/kubernetes_agents.go
@@ -63,16 +63,6 @@ func (h *KubernetesAgentHandlers) HandleReport(w http.ResponseWriter, r *http.Re
}
tokenRecord := getAPITokenRecordFromRequest(r)
- if enforceMonitoredSystemLimitForKubernetesReport(
- w,
- r.Context(),
- h.getMonitor(r.Context()),
- report,
- tokenRecord,
- ) {
- return
- }
-
cluster, err := h.getMonitor(r.Context()).ApplyKubernetesReport(report, tokenRecord)
if err != nil {
writeErrorResponse(w, http.StatusBadRequest, "invalid_report", err.Error(), nil)
diff --git a/internal/api/kubernetes_agents_additional_test.go b/internal/api/kubernetes_agents_additional_test.go
index 924a8ffbc..1c3bb4eb7 100644
--- a/internal/api/kubernetes_agents_additional_test.go
+++ b/internal/api/kubernetes_agents_additional_test.go
@@ -116,9 +116,9 @@ func TestKubernetesAgentHandlers_HandleReport(t *testing.T) {
}
}
-// TestKubernetesAgentHandlers_HandleReport_BlocksNewMonitoredSystemAtLimit
-// verifies that a new Kubernetes cluster counts as a monitored system.
-func TestKubernetesAgentHandlers_HandleReport_BlocksNewMonitoredSystemAtLimit(t *testing.T) {
+// TestKubernetesAgentHandlers_HandleReport_AllowsNewMonitoredSystemWithCapsRetired
+// verifies that a new Kubernetes cluster is accepted without monitored-system caps.
+func TestKubernetesAgentHandlers_HandleReport_AllowsNewMonitoredSystemWithCapsRetired(t *testing.T) {
setMaxMonitoredSystemsLicenseForTests(t, 1)
handler, monitor := newKubernetesAgentHandlers(t, nil)
@@ -127,7 +127,7 @@ func TestKubernetesAgentHandlers_HandleReport_BlocksNewMonitoredSystemAtLimit(t
t.Fatalf("expected seeded cluster ID")
}
- // New cluster should be blocked because it is a distinct monitored system.
+ // New cluster should be accepted because monitored-system caps are retired.
newReport := agentsk8s.Report{
Agent: agentsk8s.AgentInfo{
ID: "agent-2",
@@ -145,8 +145,8 @@ func TestKubernetesAgentHandlers_HandleReport_BlocksNewMonitoredSystemAtLimit(t
newReq := httptest.NewRequest(http.MethodPost, "/api/agents/kubernetes/report", bytes.NewReader(newBody))
newRec := httptest.NewRecorder()
handler.HandleReport(newRec, newReq)
- if newRec.Code != http.StatusPaymentRequired {
- t.Fatalf("status = %d, want 402: %s", newRec.Code, newRec.Body.String())
+ if newRec.Code != http.StatusOK {
+ t.Fatalf("status = %d, want 200: %s", newRec.Code, newRec.Body.String())
}
}
diff --git a/internal/api/legacy_grandfather_reconcile.go b/internal/api/legacy_grandfather_reconcile.go
deleted file mode 100644
index be5983b4a..000000000
--- a/internal/api/legacy_grandfather_reconcile.go
+++ /dev/null
@@ -1,234 +0,0 @@
-package api
-
-import (
- "context"
- "sync"
- "time"
-
- "github.com/rs/zerolog/log"
-)
-
-const legacyGrandfatherReconcileInterval = 5 * time.Second
-
-type legacyGrandfatherReconcileLoop struct {
- mu sync.Mutex
- cancel context.CancelFunc
- wg sync.WaitGroup
- running bool
-}
-
-func (l *legacyGrandfatherReconcileLoop) isRunning() bool {
- if l == nil {
- return false
- }
- l.mu.Lock()
- defer l.mu.Unlock()
- return l.running
-}
-
-func (h *LicenseHandlers) legacyGrandfatherReconcileLoop(orgID string) *legacyGrandfatherReconcileLoop {
- if h == nil {
- return nil
- }
- if loop, ok := h.legacyGrandfatherReconcile.Load(orgID); ok {
- if typed, ok := loop.(*legacyGrandfatherReconcileLoop); ok {
- return typed
- }
- }
- loop := &legacyGrandfatherReconcileLoop{}
- actual, _ := h.legacyGrandfatherReconcile.LoadOrStore(orgID, loop)
- if typed, ok := actual.(*legacyGrandfatherReconcileLoop); ok {
- return typed
- }
- return loop
-}
-
-func (h *LicenseHandlers) bindLegacyGrandfatherReconcileOwnership(orgID string, service *licenseService) {
- if h == nil || service == nil {
- return
- }
-
- orgID = normalizeHostedEntitlementOrgID(orgID)
- service.SetActivationStateChangeCallback(func(*activationStateModel) {
- h.syncLegacyGrandfatherReconcileOwnership(orgID, service)
- })
-}
-
-func (h *LicenseHandlers) syncLegacyGrandfatherReconcileOwnership(orgID string, service *licenseService) {
- if h == nil {
- return
- }
-
- orgID = normalizeHostedEntitlementOrgID(orgID)
- if service == nil || !service.NeedsLegacyMonitoredSystemCapture() {
- h.requestLegacyGrandfatherReconcileLoopStop(orgID)
- return
- }
-
- h.ensureLegacyGrandfatherReconcileLoop(orgID, service)
-}
-
-func (h *LicenseHandlers) ensureLegacyGrandfatherReconcileLoop(orgID string, service *licenseService) {
- if h == nil || service == nil {
- return
- }
- orgID = normalizeHostedEntitlementOrgID(orgID)
- if !service.NeedsLegacyMonitoredSystemCapture() {
- h.requestLegacyGrandfatherReconcileLoopStop(orgID)
- return
- }
-
- loop := h.legacyGrandfatherReconcileLoop(orgID)
- if loop == nil {
- return
- }
- if loop.isRunning() {
- return
- }
-
- loop.mu.Lock()
- defer loop.mu.Unlock()
- if loop.running {
- return
- }
-
- ctx, cancel := context.WithCancel(context.Background())
- loop.cancel = cancel
- loop.running = true
- loop.wg.Add(1)
- go func() {
- defer func() {
- loop.mu.Lock()
- loop.running = false
- loop.mu.Unlock()
- h.legacyGrandfatherReconcile.Delete(orgID)
- loop.wg.Done()
- }()
- h.runLegacyGrandfatherReconcileLoop(ctx, orgID, service)
- }()
-}
-
-func (h *LicenseHandlers) stopLegacyGrandfatherReconcileLoop(orgID string) {
- if h == nil {
- return
- }
- orgID = normalizeHostedEntitlementOrgID(orgID)
- value, ok := h.legacyGrandfatherReconcile.Load(orgID)
- if !ok {
- return
- }
- loop, ok := value.(*legacyGrandfatherReconcileLoop)
- if !ok || loop == nil {
- h.legacyGrandfatherReconcile.Delete(orgID)
- return
- }
-
- loop.mu.Lock()
- if !loop.running {
- loop.mu.Unlock()
- h.legacyGrandfatherReconcile.Delete(orgID)
- return
- }
- cancel := loop.cancel
- loop.running = false
- loop.mu.Unlock()
-
- if cancel != nil {
- cancel()
- }
- loop.wg.Wait()
- h.legacyGrandfatherReconcile.Delete(orgID)
-}
-
-func (h *LicenseHandlers) requestLegacyGrandfatherReconcileLoopStop(orgID string) {
- if h == nil {
- return
- }
- orgID = normalizeHostedEntitlementOrgID(orgID)
- value, ok := h.legacyGrandfatherReconcile.Load(orgID)
- if !ok {
- return
- }
- loop, ok := value.(*legacyGrandfatherReconcileLoop)
- if !ok || loop == nil {
- h.legacyGrandfatherReconcile.Delete(orgID)
- return
- }
-
- loop.mu.Lock()
- if !loop.running {
- loop.mu.Unlock()
- h.legacyGrandfatherReconcile.Delete(orgID)
- return
- }
- cancel := loop.cancel
- loop.mu.Unlock()
-
- if cancel != nil {
- cancel()
- }
-}
-
-func (h *LicenseHandlers) runLegacyGrandfatherReconcileLoop(
- ctx context.Context,
- orgID string,
- service *licenseService,
-) {
- ticker := time.NewTicker(legacyGrandfatherReconcileInterval)
- defer ticker.Stop()
-
- for {
- select {
- case <-ctx.Done():
- return
- case <-ticker.C:
- reconcileCtx := context.WithValue(context.Background(), OrgIDContextKey, orgID)
- h.reconcileLegacyMigrationGrandfatherFloor(reconcileCtx, orgID, service)
- if !service.NeedsLegacyMonitoredSystemCapture() {
- return
- }
- }
- }
-}
-
-func (h *LicenseHandlers) reconcileLegacyMigrationGrandfatherFloor(
- ctx context.Context,
- orgID string,
- service *licenseService,
-) {
- if h == nil || service == nil {
- return
- }
-
- orgID = normalizeHostedEntitlementOrgID(orgID)
- if !service.NeedsLegacyMonitoredSystemCapture() {
- return
- }
-
- if ctx == nil {
- ctx = context.Background()
- }
- if resolved := GetOrgID(ctx); resolved == "" {
- ctx = context.WithValue(ctx, OrgIDContextKey, orgID)
- }
-
- count, ok := h.canonicalMonitoredSystemGrandfatherFloor(ctx)
- if !ok {
- h.ensureLegacyGrandfatherReconcileLoop(orgID, service)
- return
- }
-
- if err := service.CaptureLegacyMonitoredSystemGrandfatherFloor(count); err != nil {
- log.Warn().
- Str("org_id", orgID).
- Int("monitored_systems", count).
- Err(err).
- Msg("Failed to persist migrated monitored-system grandfather floor")
- h.ensureLegacyGrandfatherReconcileLoop(orgID, service)
- return
- }
-
- if service.NeedsLegacyMonitoredSystemCapture() {
- h.ensureLegacyGrandfatherReconcileLoop(orgID, service)
- }
-}
diff --git a/internal/api/license_handlers_test.go b/internal/api/license_handlers_test.go
index 0648e92ad..7f1c6550f 100644
--- a/internal/api/license_handlers_test.go
+++ b/internal/api/license_handlers_test.go
@@ -66,14 +66,13 @@ func issueCheckoutActivationGrant(t *testing.T) string {
t.Helper()
grantJWT, grantPublicKey, err := licensetestsupport.GenerateGrantJWTForTesting(pkglicensing.GrantClaims{
- LicenseID: "lic_checkout_success",
- Tier: "pro_plus",
- State: "active",
- Features: []string{"relay", "ai_alerts"},
- MaxMonitoredSystems: 50,
- IssuedAt: time.Now().Unix(),
- ExpiresAt: time.Now().Add(72 * time.Hour).Unix(),
- Email: "buyer@example.com",
+ LicenseID: "lic_checkout_success",
+ Tier: "pro_plus",
+ State: "active",
+ Features: []string{"relay", "ai_alerts"},
+ IssuedAt: time.Now().Unix(),
+ ExpiresAt: time.Now().Add(72 * time.Hour).Unix(),
+ Email: "buyer@example.com",
})
if err != nil {
t.Fatalf("generate grant jwt: %v", err)
@@ -318,9 +317,6 @@ func TestHandleLicenseStatus_ExpiredBillingBackedTrialFallsBackToFreeDisplay(t *
if resp.Tier != license.TierFree {
t.Fatalf("expected tier %q, got %q", license.TierFree, resp.Tier)
}
- if resp.MaxMonitoredSystems != license.TierMonitoredSystemLimits[license.TierFree] {
- t.Fatalf("expected max_monitored_systems %d, got %d", license.TierMonitoredSystemLimits[license.TierFree], resp.MaxMonitoredSystems)
- }
if resp.MaxGuests != 0 {
t.Fatalf("expected max_guests 0, got %d", resp.MaxGuests)
}
@@ -405,15 +401,14 @@ func TestHandleActivateLicense_ExchangesLegacyJWTInStrictV6(t *testing.T) {
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
grantJWT, grantPublicKey, err := licensetestsupport.GenerateGrantJWTForTesting(pkglicensing.GrantClaims{
- LicenseID: "lic_exchanged",
- Tier: "pro",
- PlanKey: tc.planKey,
- State: "active",
- Features: []string{"relay"},
- MaxMonitoredSystems: 10,
- IssuedAt: time.Now().Unix(),
- ExpiresAt: time.Now().Add(72 * time.Hour).Unix(),
- Email: "legacy-jwt@example.com",
+ LicenseID: "lic_exchanged",
+ Tier: "pro",
+ PlanKey: tc.planKey,
+ State: "active",
+ Features: []string{"relay"},
+ IssuedAt: time.Now().Unix(),
+ ExpiresAt: time.Now().Add(72 * time.Hour).Unix(),
+ Email: "legacy-jwt@example.com",
})
if err != nil {
t.Fatalf("generate grant jwt: %v", err)
@@ -504,14 +499,13 @@ func TestHandleActivateLicense_ClearsCommercialMigrationStateOnNativeActivation(
t.Setenv("PULSE_LICENSE_DEV_MODE", "false")
grantJWT, grantPublicKey, err := licensetestsupport.GenerateGrantJWTForTesting(pkglicensing.GrantClaims{
- LicenseID: "lic_v6_native",
- Tier: "pro",
- State: "active",
- Features: []string{"relay"},
- MaxMonitoredSystems: 10,
- IssuedAt: time.Now().Unix(),
- ExpiresAt: time.Now().Add(72 * time.Hour).Unix(),
- Email: "native-v6@example.com",
+ LicenseID: "lic_v6_native",
+ Tier: "pro",
+ State: "active",
+ Features: []string{"relay"},
+ IssuedAt: time.Now().Unix(),
+ ExpiresAt: time.Now().Add(72 * time.Hour).Unix(),
+ Email: "native-v6@example.com",
})
if err != nil {
t.Fatalf("generate grant jwt: %v", err)
@@ -590,14 +584,13 @@ func TestHandleActivateLicense_ActivationKeyClearsStaleLegacyPersistence(t *test
t.Setenv("PULSE_LICENSE_DEV_MODE", "false")
grantJWT, grantPublicKey, err := licensetestsupport.GenerateGrantJWTForTesting(pkglicensing.GrantClaims{
- LicenseID: "lic_v6_native",
- Tier: "pro",
- State: "active",
- Features: []string{"relay"},
- MaxMonitoredSystems: 10,
- IssuedAt: time.Now().Unix(),
- ExpiresAt: time.Now().Add(72 * time.Hour).Unix(),
- Email: "native-v6@example.com",
+ LicenseID: "lic_v6_native",
+ Tier: "pro",
+ State: "active",
+ Features: []string{"relay"},
+ IssuedAt: time.Now().Unix(),
+ ExpiresAt: time.Now().Add(72 * time.Hour).Unix(),
+ Email: "native-v6@example.com",
})
if err != nil {
t.Fatalf("generate grant jwt: %v", err)
diff --git a/internal/api/license_limits_test_helpers_test.go b/internal/api/license_limits_test_helpers_test.go
index 8af415fe5..aac997855 100644
--- a/internal/api/license_limits_test_helpers_test.go
+++ b/internal/api/license_limits_test_helpers_test.go
@@ -22,6 +22,7 @@ func (p *staticLicenseProvider) Service(context.Context) *pkglicensing.Service {
func setMaxMonitoredSystemsLicenseForTests(t *testing.T, maxMonitoredSystems int) {
t.Helper()
+ _ = maxMonitoredSystems
testLicenseProviderMu.Lock()
@@ -33,11 +34,9 @@ func setMaxMonitoredSystemsLicenseForTests(t *testing.T, maxMonitoredSystems int
t.Fatalf("failed to generate test license: %v", err)
}
- lic, err := service.Activate(licenseKey)
- if err != nil {
+ if _, err := service.Activate(licenseKey); err != nil {
t.Fatalf("failed to activate test license: %v", err)
}
- lic.Claims.MaxMonitoredSystems = maxMonitoredSystems
SetLicenseServiceProvider(&staticLicenseProvider{service: service})
t.Cleanup(func() {
diff --git a/internal/api/licensing_bridge.go b/internal/api/licensing_bridge.go
index 4328ba8c6..58cfae748 100644
--- a/internal/api/licensing_bridge.go
+++ b/internal/api/licensing_bridge.go
@@ -5,10 +5,6 @@ import (
"net/http"
"time"
- "github.com/rcourtman/pulse-go-rewrite/internal/models"
- agentsdocker "github.com/rcourtman/pulse-go-rewrite/pkg/agents/docker"
- agentshost "github.com/rcourtman/pulse-go-rewrite/pkg/agents/host"
- agentsk8s "github.com/rcourtman/pulse-go-rewrite/pkg/agents/kubernetes"
pkglicensing "github.com/rcourtman/pulse-go-rewrite/pkg/licensing"
)
@@ -52,23 +48,22 @@ type checkoutPortalHandoffRequestModel = pkglicensing.CheckoutPortalHandoffReque
type checkoutPortalHandoffResponseModel = pkglicensing.CheckoutPortalHandoffResponse
const (
- featureMultiTenantKey = pkglicensing.FeatureMultiTenant
- featureAgentProfilesValue = pkglicensing.FeatureAgentProfiles
- featureAIPatrolValue = pkglicensing.FeatureAIPatrol
- featureAIAutoFixValue = pkglicensing.FeatureAIAutoFix
- featureAuditLoggingValue = pkglicensing.FeatureAuditLogging
- featureRBACValue = pkglicensing.FeatureRBAC
- featureAdvancedReportingValue = pkglicensing.FeatureAdvancedReporting
- featureLongTermMetricsValue = pkglicensing.FeatureLongTermMetrics
- featureDemoFixturesValue = pkglicensing.FeatureDemoFixtures
- maxMonitoredSystemsLicenseGateKey = pkglicensing.MaxMonitoredSystemsLicenseGateKey
- maxUsersLicenseGateKey = pkglicensing.MaxUsersLicenseGateKey
- subscriptionStateActiveValue = pkglicensing.SubStateActive
- subscriptionStateExpiredValue = pkglicensing.SubStateExpired
- subscriptionStateGraceValue = pkglicensing.SubStateGrace
- subscriptionStateCanceledValue = pkglicensing.SubStateCanceled
- subscriptionStateTrialValue = pkglicensing.SubStateTrial
- activationKeyPrefixValue = pkglicensing.ActivationKeyPrefix
+ featureMultiTenantKey = pkglicensing.FeatureMultiTenant
+ featureAgentProfilesValue = pkglicensing.FeatureAgentProfiles
+ featureAIPatrolValue = pkglicensing.FeatureAIPatrol
+ featureAIAutoFixValue = pkglicensing.FeatureAIAutoFix
+ featureAuditLoggingValue = pkglicensing.FeatureAuditLogging
+ featureRBACValue = pkglicensing.FeatureRBAC
+ featureAdvancedReportingValue = pkglicensing.FeatureAdvancedReporting
+ featureLongTermMetricsValue = pkglicensing.FeatureLongTermMetrics
+ featureDemoFixturesValue = pkglicensing.FeatureDemoFixtures
+ maxUsersLicenseGateKey = pkglicensing.MaxUsersLicenseGateKey
+ subscriptionStateActiveValue = pkglicensing.SubStateActive
+ subscriptionStateExpiredValue = pkglicensing.SubStateExpired
+ subscriptionStateGraceValue = pkglicensing.SubStateGrace
+ subscriptionStateCanceledValue = pkglicensing.SubStateCanceled
+ subscriptionStateTrialValue = pkglicensing.SubStateTrial
+ activationKeyPrefixValue = pkglicensing.ActivationKeyPrefix
)
func newLicenseService() *licenseService {
@@ -153,30 +148,6 @@ func userLimitExceededMessageFromLicensing(current, limit int) string {
return pkglicensing.UserLimitExceededMessage(current, limit)
}
-func monitoredSystemLimitExceededMessageFromLicensing(current, limit int) string {
- return pkglicensing.MonitoredSystemLimitExceededMessage(current, limit)
-}
-
-func hostReportTargetsExistingHostFromLicensing(snapshot models.StateSnapshot, report agentshost.Report, tokenID string) bool {
- return pkglicensing.HostReportTargetsExistingHost(snapshot, report, tokenID)
-}
-
-func dockerReportTargetsExistingHostFromLicensing(snapshot models.StateSnapshot, report agentsdocker.Report, tokenID string) bool {
- return pkglicensing.DockerReportTargetsExistingHost(snapshot, report, tokenID)
-}
-
-func kubernetesReportTargetsExistingClusterFromLicensing(snapshot models.StateSnapshot, report agentsk8s.Report, tokenID string) bool {
- return pkglicensing.KubernetesReportTargetsExistingCluster(snapshot, report, tokenID)
-}
-
-func kubernetesReportIdentifierFromLicensing(report agentsk8s.Report) string {
- return pkglicensing.KubernetesReportIdentifier(report)
-}
-
-func hostReportTargetsExistingHostsFromLicensing(hosts []models.Host, report agentshost.Report, tokenID string) bool {
- return pkglicensing.HostReportTargetsExistingHosts(hosts, report, tokenID)
-}
-
func mapStripeSubscriptionStatusToStateFromLicensing(status string) subscriptionState {
return pkglicensing.MapStripeSubscriptionStatusToState(status)
}
@@ -189,10 +160,6 @@ func deriveStripePlanVersionFromLicensing(metadata map[string]string, priceID st
return pkglicensing.DeriveStripePlanVersion(metadata, priceID)
}
-func limitsForCloudPlanFromLicensing(planVersion string) (map[string]int64, bool) {
- return pkglicensing.LimitsForCloudPlan(planVersion)
-}
-
func buildEntitlementPayloadFromLicensing(status *licenseStatus, subscriptionState string) entitlementPayloadModel {
return pkglicensing.BuildEntitlementPayload(status, subscriptionState)
}
diff --git a/internal/api/licensing_handlers.go b/internal/api/licensing_handlers.go
index 37981e44f..91ae8b4bf 100644
--- a/internal/api/licensing_handlers.go
+++ b/internal/api/licensing_handlers.go
@@ -54,16 +54,15 @@ func wantsMockFixturesFromEnv() bool {
// LicenseHandlers handles license management API endpoints.
type LicenseHandlers struct {
- mtPersistence *config.MultiTenantPersistence
- hostedMode bool
- cfg *config.Config
- services sync.Map // map[string]*licenseService
- purchaseReturnRedemptions *purchaseReturnRedemptionStore
- monitor *monitoring.Monitor
- mtMonitor *monitoring.MultiTenantMonitor
- hostedLeaseRefresh sync.Map // map[string]*hostedEntitlementRefreshLoop
- legacyGrandfatherReconcile sync.Map // map[string]*legacyGrandfatherReconcileLoop
- runtimeVersion string
+ mtPersistence *config.MultiTenantPersistence
+ hostedMode bool
+ cfg *config.Config
+ services sync.Map // map[string]*licenseService
+ purchaseReturnRedemptions *purchaseReturnRedemptionStore
+ monitor *monitoring.Monitor
+ mtMonitor *monitoring.MultiTenantMonitor
+ hostedLeaseRefresh sync.Map // map[string]*hostedEntitlementRefreshLoop
+ runtimeVersion string
}
// NewLicenseHandlers creates a new license handlers instance.
@@ -122,12 +121,6 @@ func (h *LicenseHandlers) StopAllBackgroundLoops() {
}
return true
})
- h.legacyGrandfatherReconcile.Range(func(key, value any) bool {
- if orgID, ok := key.(string); ok {
- h.stopLegacyGrandfatherReconcileLoop(orgID)
- }
- return true
- })
h.services.Range(func(_, value any) bool {
if svc, ok := value.(*licenseService); ok {
svc.StopGrantRefresh()
@@ -524,7 +517,6 @@ func (h *LicenseHandlers) getTenantComponents(ctx context.Context) (*licenseServ
service := newLicenseService()
service.SetClientVersion(h.runtimeVersion)
- h.bindLegacyGrandfatherReconcileOwnership(orgID, service)
// Wire license server client and persistence so activation / refresh can use them.
lsClient := newLicenseServerClientFromLicensing("")
@@ -546,7 +538,6 @@ func (h *LicenseHandlers) getTenantComponents(ctx context.Context) (*licenseServ
if err := service.RestoreActivation(activationState); err != nil {
log.Warn().Str("org_id", orgID).Err(err).Msg("Failed to restore activation")
} else {
- h.reconcileLegacyMigrationGrandfatherFloor(ctx, orgID, service)
if clearErr := h.setCommercialMigrationState(orgID, nil); clearErr != nil {
log.Warn().Str("org_id", orgID).Err(clearErr).Msg("Failed to clear commercial migration state after activation restore")
}
@@ -576,7 +567,6 @@ func (h *LicenseHandlers) getTenantComponents(ctx context.Context) (*licenseServ
}
log.Warn().Str("org_id", orgID).Err(err).Msg("Failed to auto-exchange persisted legacy license")
} else if service.IsActivated() {
- h.reconcileLegacyMigrationGrandfatherFloor(ctx, orgID, service)
if clearErr := h.setCommercialMigrationState(orgID, nil); clearErr != nil {
log.Warn().Str("org_id", orgID).Err(clearErr).Msg("Failed to clear commercial migration state after successful auto-exchange")
}
@@ -601,17 +591,12 @@ func (h *LicenseHandlers) getTenantComponents(ctx context.Context) (*licenseServ
service.StopGrantRefresh() // stop our orphaned refresh loop if started
service.StopRevocationPoll() // stop our orphaned revocation poller if started
svc := actual.(*licenseService)
- // Re-home legacy continuity ownership onto the canonical service when
- // concurrent first-request initialization races create an orphan.
- h.stopLegacyGrandfatherReconcileLoop(orgID)
- h.syncLegacyGrandfatherReconcileOwnership(orgID, svc)
h.ensureHostedEntitlementRefreshForOrg(orgID, svc)
p, pErr := h.getPersistenceForOrg(orgID)
h.syncReleaseDemoFixtureRuntime(orgID, svc)
return svc, p, pErr
}
- h.syncLegacyGrandfatherReconcileOwnership(orgID, service)
h.ensureHostedEntitlementRefreshForOrg(orgID, service)
h.syncReleaseDemoFixtureRuntime(orgID, service)
@@ -955,9 +940,6 @@ func (h *LicenseHandlers) activateLicenseKey(ctx context.Context, licenseKey str
}
if service.IsActivated() {
- if migratedLegacyKey {
- h.reconcileLegacyMigrationGrandfatherFloor(ctx, orgID, service)
- }
h.stopHostedEntitlementRefreshLoop(orgID)
if clearErr := h.setCommercialMigrationState(orgID, nil); clearErr != nil {
log.Warn().Err(clearErr).Str("org_id", orgID).Msg("Failed to clear commercial migration state after activation")
@@ -1432,7 +1414,6 @@ func (h *LicenseHandlers) HandleClearLicense(w http.ResponseWriter, r *http.Requ
// Preserve trial_started_at and free-tier bookkeeping so the effective trial
// ends immediately but trial reuse remains blocked.
if h != nil && h.mtPersistence != nil {
- h.stopLegacyGrandfatherReconcileLoop(orgID)
h.stopHostedEntitlementRefreshLoop(orgID)
billingStore := config.NewFileBillingStore(h.mtPersistence.BaseDataDir())
existing, err := billingStore.GetBillingState(orgID)
diff --git a/internal/api/licensing_handlers_auto_migrate_test.go b/internal/api/licensing_handlers_auto_migrate_test.go
index 41e2b879e..ea70912c7 100644
--- a/internal/api/licensing_handlers_auto_migrate_test.go
+++ b/internal/api/licensing_handlers_auto_migrate_test.go
@@ -48,14 +48,13 @@ func TestGetTenantComponents_AutoExchangesPersistedLegacyJWT(t *testing.T) {
t.Setenv("PULSE_LICENSE_DEV_MODE", "false")
grantJWT, grantPublicKey, err := licensetestsupport.GenerateGrantJWTForTesting(pkglicensing.GrantClaims{
- LicenseID: "lic_migrated",
- Tier: "pro",
- State: "active",
- Features: []string{"relay"},
- MaxMonitoredSystems: 10,
- IssuedAt: time.Now().Unix(),
- ExpiresAt: time.Now().Add(72 * time.Hour).Unix(),
- Email: "user@example.com",
+ LicenseID: "lic_migrated",
+ Tier: "pro",
+ State: "active",
+ Features: []string{"relay"},
+ IssuedAt: time.Now().Unix(),
+ ExpiresAt: time.Now().Add(72 * time.Hour).Unix(),
+ Email: "user@example.com",
})
if err != nil {
t.Fatalf("generate grant jwt: %v", err)
@@ -81,11 +80,10 @@ func TestGetTenantComponents_AutoExchangesPersistedLegacyJWT(t *testing.T) {
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(pkglicensing.ActivateInstallationResponse{
License: pkglicensing.ActivateResponseLicense{
- LicenseID: "lic_migrated",
- State: "active",
- Tier: "pro",
- Features: []string{"relay"},
- MaxMonitoredSystems: 10,
+ LicenseID: "lic_migrated",
+ State: "active",
+ Tier: "pro",
+ Features: []string{"relay"},
},
Installation: pkglicensing.ActivateResponseInstallation{
InstallationID: "inst_migrated",
@@ -327,15 +325,14 @@ func TestGetTenantComponents_AutoExchangeLeavesSelfHostedMonitoringUncapped(t *t
t.Setenv("PULSE_LICENSE_DEV_MODE", "false")
grantJWT, grantPublicKey, err := licensetestsupport.GenerateGrantJWTForTesting(pkglicensing.GrantClaims{
- LicenseID: "lic_floor_auto",
- Tier: "pro",
- PlanKey: "legacy_migration_fallback",
- State: "active",
- Features: []string{"relay"},
- MaxMonitoredSystems: 10,
- IssuedAt: time.Now().Unix(),
- ExpiresAt: time.Now().Add(72 * time.Hour).Unix(),
- Email: "floor-auto@example.com",
+ LicenseID: "lic_floor_auto",
+ Tier: "pro",
+ PlanKey: "legacy_migration_fallback",
+ State: "active",
+ Features: []string{"relay"},
+ IssuedAt: time.Now().Unix(),
+ ExpiresAt: time.Now().Add(72 * time.Hour).Unix(),
+ Email: "floor-auto@example.com",
})
if err != nil {
t.Fatalf("generate grant jwt: %v", err)
@@ -350,11 +347,10 @@ func TestGetTenantComponents_AutoExchangeLeavesSelfHostedMonitoringUncapped(t *t
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(pkglicensing.ActivateInstallationResponse{
License: pkglicensing.ActivateResponseLicense{
- LicenseID: "lic_floor_auto",
- State: "active",
- Tier: "pro",
- Features: []string{"relay"},
- MaxMonitoredSystems: 10,
+ LicenseID: "lic_floor_auto",
+ State: "active",
+ Tier: "pro",
+ Features: []string{"relay"},
},
Installation: pkglicensing.ActivateResponseInstallation{
InstallationID: "inst_floor_auto",
@@ -400,9 +396,6 @@ func TestGetTenantComponents_AutoExchangeLeavesSelfHostedMonitoringUncapped(t *t
if !svc.IsActivated() {
t.Fatal("expected persisted legacy JWT to auto-exchange into activation state")
}
- if got := svc.Status().MaxMonitoredSystems; got != 0 {
- t.Fatalf("status.MaxMonitoredSystems=%d, want 0 for uncapped self-hosted continuity", got)
- }
entReq := httptest.NewRequest(http.MethodGet, "/api/license/entitlements", nil).WithContext(ctx)
entRec := httptest.NewRecorder()
@@ -446,14 +439,10 @@ func TestGetTenantComponents_AutoExchangeLeavesSelfHostedMonitoringUncapped(t *t
t.Fatalf("migrated v5 Pro entitlements missing capability %q in %v", capability, payload.Capabilities)
}
}
- if payload.MonitoredSystemCapacity == nil {
- t.Fatal("expected monitored-system capacity payload")
- }
- if payload.MonitoredSystemCapacity.Mode != "unlimited" || payload.MonitoredSystemCapacity.Limit != 0 {
- t.Fatalf(
- "expected uncapped monitored-system capacity for migrated v5 Pro, got %+v",
- payload.MonitoredSystemCapacity,
- )
+ for _, limit := range payload.Limits {
+ if limit.Key == pkglicensing.MaxMonitoredSystemsLicenseGateKey {
+ t.Fatalf("migrated v5 Pro entitlements still expose retired monitored-system limit: %+v", payload.Limits)
+ }
}
if activationState, err := persistence.LoadActivationState(); err != nil {
t.Fatalf("load activation state: %v", err)
@@ -463,12 +452,6 @@ func TestGetTenantComponents_AutoExchangeLeavesSelfHostedMonitoringUncapped(t *t
if !activationState.Continuity.LegacyMigration {
t.Fatal("expected legacy migration continuity flag")
}
- if activationState.Continuity.GrandfatheredMaxMonitoredSystems != 0 {
- t.Fatalf("GrandfatheredMaxMonitoredSystems=%d, want 0 for uncapped self-hosted migration", activationState.Continuity.GrandfatheredMaxMonitoredSystems)
- }
- if activationState.Continuity.GrandfatheredMonitoredSystemsCapturedAt != 0 {
- t.Fatalf("GrandfatheredMonitoredSystemsCapturedAt=%d, want 0 for uncapped self-hosted migration", activationState.Continuity.GrandfatheredMonitoredSystemsCapturedAt)
- }
}
handlers.StopAllBackgroundLoops()
@@ -478,15 +461,14 @@ func TestGetTenantComponents_DoesNotBackfillGrandfatherFloorForUncappedSelfHoste
t.Setenv("PULSE_LICENSE_DEV_MODE", "false")
grantJWT, grantPublicKey, err := licensetestsupport.GenerateGrantJWTForTesting(pkglicensing.GrantClaims{
- LicenseID: "lic_floor_restore",
- Tier: "pro",
- PlanKey: "legacy_migration_fallback",
- State: "active",
- Features: []string{"relay"},
- MaxMonitoredSystems: 10,
- IssuedAt: time.Now().Unix(),
- ExpiresAt: time.Now().Add(72 * time.Hour).Unix(),
- Email: "floor-restore@example.com",
+ LicenseID: "lic_floor_restore",
+ Tier: "pro",
+ PlanKey: "legacy_migration_fallback",
+ State: "active",
+ Features: []string{"relay"},
+ IssuedAt: time.Now().Unix(),
+ ExpiresAt: time.Now().Add(72 * time.Hour).Unix(),
+ Email: "floor-restore@example.com",
})
if err != nil {
t.Fatalf("generate grant jwt: %v", err)
@@ -528,9 +510,6 @@ func TestGetTenantComponents_DoesNotBackfillGrandfatherFloorForUncappedSelfHoste
if svc == nil {
t.Fatal("expected non-nil service")
}
- if got := svc.Status().MaxMonitoredSystems; got != 0 {
- t.Fatalf("initial status.MaxMonitoredSystems=%d, want 0 before floor capture for uncapped self-hosted continuity", got)
- }
handlers.SetMonitors(buildGrandfatherFloorMonitor(23), nil)
time.Sleep(100 * time.Millisecond)
@@ -542,120 +521,25 @@ func TestGetTenantComponents_DoesNotBackfillGrandfatherFloorForUncappedSelfHoste
if loaded == nil {
t.Fatal("expected activation state")
}
- if loaded.Continuity.GrandfatheredMaxMonitoredSystems != 0 {
- t.Fatalf("GrandfatheredMaxMonitoredSystems=%d, want 0 for uncapped self-hosted restore", loaded.Continuity.GrandfatheredMaxMonitoredSystems)
- }
- if loaded.Continuity.GrandfatheredMonitoredSystemsCapturedAt != 0 {
- t.Fatalf("GrandfatheredMonitoredSystemsCapturedAt=%d, want 0 for uncapped self-hosted restore", loaded.Continuity.GrandfatheredMonitoredSystemsCapturedAt)
- }
- if got := svc.Status().MaxMonitoredSystems; got != 0 {
- t.Fatalf("status.MaxMonitoredSystems=%d, want 0 for uncapped self-hosted restore", got)
- }
- if svc.Status().MonitoredSystemContinuity != nil {
- t.Fatalf("expected no monitored-system continuity for uncapped self-hosted restore, got %+v", svc.Status().MonitoredSystemContinuity)
+ if !loaded.Continuity.LegacyMigration {
+ t.Fatal("expected legacy migration continuity flag to survive restore")
}
handlers.StopAllBackgroundLoops()
}
-func TestBillingReads_DoNotRestartLegacyGrandfatherReconcileLoop(t *testing.T) {
- t.Setenv("PULSE_LICENSE_DEV_MODE", "false")
-
- grantJWT, grantPublicKey, err := licensetestsupport.GenerateGrantJWTForTesting(pkglicensing.GrantClaims{
- LicenseID: "lic_floor_read_only",
- Tier: "pro",
- PlanKey: "legacy_migration_fallback",
- State: "active",
- Features: []string{"relay"},
- MaxMonitoredSystems: 10,
- IssuedAt: time.Now().Unix(),
- ExpiresAt: time.Now().Add(72 * time.Hour).Unix(),
- Email: "floor-read-only@example.com",
- })
- if err != nil {
- t.Fatalf("generate grant jwt: %v", err)
- }
- pkglicensing.SetPublicKey(grantPublicKey)
- t.Cleanup(func() { pkglicensing.SetPublicKey(nil) })
-
- baseDir := t.TempDir()
- mtp := config.NewMultiTenantPersistence(baseDir)
- cp, err := mtp.GetPersistence("default")
- if err != nil {
- t.Fatalf("init default persistence: %v", err)
- }
- persistence, err := pkglicensing.NewPersistence(cp.GetConfigDir())
- if err != nil {
- t.Fatalf("new persistence: %v", err)
- }
- if err := persistence.SaveActivationState(&pkglicensing.ActivationState{
- InstallationID: "inst_floor_read_only",
- InstallationToken: "pit_live_floor_read_only",
- LicenseID: "lic_floor_read_only",
- GrantJWT: grantJWT,
- GrantJTI: "grant_floor_read_only",
- GrantExpiresAt: time.Now().Add(72 * time.Hour).Unix(),
- InstanceFingerprint: "fp-floor-read-only",
- LicenseServerURL: "https://license.pulserelay.pro",
- ActivatedAt: time.Now().Add(-time.Hour).Unix(),
- LastRefreshedAt: time.Now().Add(-time.Hour).Unix(),
- Continuity: pkglicensing.ActivationContinuity{
- LegacyMigration: true,
- },
- }); err != nil {
- t.Fatalf("save activation state: %v", err)
- }
-
- handlers := NewLicenseHandlers(mtp, false)
- t.Cleanup(handlers.StopAllBackgroundLoops)
-
- ctx := context.WithValue(context.Background(), OrgIDContextKey, "default")
- svc := handlers.Service(ctx)
- if svc == nil {
- t.Fatal("expected non-nil service")
- }
-
- time.Sleep(100 * time.Millisecond)
- if _, ok := handlers.legacyGrandfatherReconcile.Load("default"); ok {
- t.Fatal("uncapped self-hosted restore started grandfather reconcile loop")
- }
-
- req := httptest.NewRequest(http.MethodGet, "/api/license/status", nil).WithContext(ctx)
- rec := httptest.NewRecorder()
- handlers.HandleLicenseStatus(rec, req)
- if rec.Code != http.StatusOK {
- t.Fatalf("license status=%d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String())
- }
- time.Sleep(100 * time.Millisecond)
- if _, ok := handlers.legacyGrandfatherReconcile.Load("default"); ok {
- t.Fatal("license status read restarted grandfather reconcile loop")
- }
-
- req = httptest.NewRequest(http.MethodGet, "/api/license/entitlements", nil).WithContext(ctx)
- rec = httptest.NewRecorder()
- handlers.HandleEntitlements(rec, req)
- if rec.Code != http.StatusOK {
- t.Fatalf("license entitlements=%d, want %d: %s", rec.Code, http.StatusOK, rec.Body.String())
- }
- time.Sleep(100 * time.Millisecond)
- if _, ok := handlers.legacyGrandfatherReconcile.Load("default"); ok {
- t.Fatal("license entitlements read restarted grandfather reconcile loop")
- }
-}
-
func TestActivateLicenseKey_KeepsLegacySelfHostedMigrationUncapped(t *testing.T) {
t.Setenv("PULSE_LICENSE_DEV_MODE", "false")
grantJWT, grantPublicKey, err := licensetestsupport.GenerateGrantJWTForTesting(pkglicensing.GrantClaims{
- LicenseID: "lic_floor_manual",
- Tier: "pro",
- PlanKey: "legacy_migration_fallback",
- State: "active",
- Features: []string{"relay"},
- MaxMonitoredSystems: 10,
- IssuedAt: time.Now().Unix(),
- ExpiresAt: time.Now().Add(72 * time.Hour).Unix(),
- Email: "floor-manual@example.com",
+ LicenseID: "lic_floor_manual",
+ Tier: "pro",
+ PlanKey: "legacy_migration_fallback",
+ State: "active",
+ Features: []string{"relay"},
+ IssuedAt: time.Now().Unix(),
+ ExpiresAt: time.Now().Add(72 * time.Hour).Unix(),
+ Email: "floor-manual@example.com",
})
if err != nil {
t.Fatalf("generate grant jwt: %v", err)
@@ -670,11 +554,10 @@ func TestActivateLicenseKey_KeepsLegacySelfHostedMigrationUncapped(t *testing.T)
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(pkglicensing.ActivateInstallationResponse{
License: pkglicensing.ActivateResponseLicense{
- LicenseID: "lic_floor_manual",
- State: "active",
- Tier: "pro",
- Features: []string{"relay"},
- MaxMonitoredSystems: 10,
+ LicenseID: "lic_floor_manual",
+ State: "active",
+ Tier: "pro",
+ Features: []string{"relay"},
},
Installation: pkglicensing.ActivateResponseInstallation{
InstallationID: "inst_floor_manual",
@@ -713,24 +596,20 @@ func TestActivateLicenseKey_KeepsLegacySelfHostedMigrationUncapped(t *testing.T)
if svc == nil {
t.Fatal("expected non-nil service")
}
- if got := svc.Status().MaxMonitoredSystems; got != 0 {
- t.Fatalf("status.MaxMonitoredSystems=%d, want 0 for uncapped self-hosted continuity", got)
- }
}
func TestGetTenantComponents_DoesNotCaptureGrandfatherFloorWhenSupplementalInventorySettlesForUncappedSelfHosted(t *testing.T) {
t.Setenv("PULSE_LICENSE_DEV_MODE", "false")
grantJWT, grantPublicKey, err := licensetestsupport.GenerateGrantJWTForTesting(pkglicensing.GrantClaims{
- LicenseID: "lic_floor_supplemental",
- Tier: "pro",
- PlanKey: "legacy_migration_fallback",
- State: "active",
- Features: []string{"relay"},
- MaxMonitoredSystems: 10,
- IssuedAt: time.Now().Unix(),
- ExpiresAt: time.Now().Add(72 * time.Hour).Unix(),
- Email: "floor-supplemental@example.com",
+ LicenseID: "lic_floor_supplemental",
+ Tier: "pro",
+ PlanKey: "legacy_migration_fallback",
+ State: "active",
+ Features: []string{"relay"},
+ IssuedAt: time.Now().Unix(),
+ ExpiresAt: time.Now().Add(72 * time.Hour).Unix(),
+ Email: "floor-supplemental@example.com",
})
if err != nil {
t.Fatalf("generate grant jwt: %v", err)
@@ -745,11 +624,10 @@ func TestGetTenantComponents_DoesNotCaptureGrandfatherFloorWhenSupplementalInven
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(pkglicensing.ActivateInstallationResponse{
License: pkglicensing.ActivateResponseLicense{
- LicenseID: "lic_floor_supplemental",
- State: "active",
- Tier: "pro",
- Features: []string{"relay"},
- MaxMonitoredSystems: 10,
+ LicenseID: "lic_floor_supplemental",
+ State: "active",
+ Tier: "pro",
+ Features: []string{"relay"},
},
Installation: pkglicensing.ActivateResponseInstallation{
InstallationID: "inst_floor_supplemental",
@@ -821,35 +699,14 @@ func TestGetTenantComponents_DoesNotCaptureGrandfatherFloorWhenSupplementalInven
}
status := readStatus()
- if got := status.MaxMonitoredSystems; got != 0 {
- t.Fatalf("initial status.MaxMonitoredSystems=%d, want 0 while supplemental inventory is unsettled for uncapped self-hosted continuity", got)
- }
- if status.MonitoredSystemContinuity != nil {
- t.Fatalf("expected no continuity in status payload for uncapped self-hosted migration, got %+v", status.MonitoredSystemContinuity)
+ if !status.Valid {
+ t.Fatal("expected migrated self-hosted license status to remain valid")
}
payload := readEntitlements()
- if payload.MonitoredSystemContinuity != nil {
- t.Fatalf("expected no continuity in entitlements payload for uncapped self-hosted migration, got %+v", payload.MonitoredSystemContinuity)
- }
if len(payload.Limits) != 0 {
t.Fatalf("expected no enforced monitored-system limit for uncapped continuity, got %+v", payload.Limits)
}
- if payload.MonitoredSystemCapacity == nil {
- t.Fatal("expected monitored-system capacity payload")
- }
- if payload.MonitoredSystemCapacity.Mode != "usage_unavailable" {
- t.Fatalf("expected usage_unavailable monitored-system capacity while supplemental inventory is unsettled, got %+v", payload.MonitoredSystemCapacity)
- }
- if payload.MonitoredSystemCapacity.Limit != 0 {
- t.Fatalf("monitored_system_capacity.limit=%d, want 0 for uncapped continuity", payload.MonitoredSystemCapacity.Limit)
- }
- if payload.MonitoredSystemCapacity.CurrentAvailable {
- t.Fatalf("expected unavailable monitored-system usage while supplemental inventory is unsettled, got %+v", payload.MonitoredSystemCapacity)
- }
- if payload.MonitoredSystemCapacity.CurrentUnavailableReason != "supplemental_inventory_unsettled" {
- t.Fatalf("CurrentUnavailableReason=%q, want %q", payload.MonitoredSystemCapacity.CurrentUnavailableReason, "supplemental_inventory_unsettled")
- }
activationState, err := persistence.LoadActivationState()
if err != nil {
@@ -858,20 +715,20 @@ func TestGetTenantComponents_DoesNotCaptureGrandfatherFloorWhenSupplementalInven
if activationState == nil {
t.Fatal("expected activation state after legacy exchange")
}
- if activationState.Continuity.GrandfatheredMonitoredSystemsCapturedAt != 0 {
- t.Fatalf("GrandfatheredMonitoredSystemsCapturedAt=%d, want 0 for uncapped self-hosted migration", activationState.Continuity.GrandfatheredMonitoredSystemsCapturedAt)
+ if !activationState.Continuity.LegacyMigration {
+ t.Fatal("expected legacy migration continuity flag")
}
provider.settle(23)
status = readStatus()
- if got := status.MaxMonitoredSystems; got != 0 {
- t.Fatalf("stale supplemental store should keep uncapped self-hosted status, got %d", got)
+ if !status.Valid {
+ t.Fatal("stale supplemental store should keep migrated self-hosted status valid")
}
monitor.SetSupplementalRecordsProvider(unifiedresources.SourceTrueNAS, provider)
status = readStatus()
- if got := status.MaxMonitoredSystems; got != 0 {
- t.Fatalf("status read should not reintroduce a monitored-system cap directly after canonical store rebuild, got %d", got)
+ if !status.Valid {
+ t.Fatal("status read should keep migrated self-hosted status valid after canonical store rebuild")
}
time.Sleep(100 * time.Millisecond)
@@ -882,34 +739,19 @@ func TestGetTenantComponents_DoesNotCaptureGrandfatherFloorWhenSupplementalInven
if activationState == nil {
t.Fatal("expected activation state after canonical store rebuild")
}
- if activationState.Continuity.GrandfatheredMaxMonitoredSystems != 0 {
- t.Fatalf("GrandfatheredMaxMonitoredSystems=%d, want 0 for uncapped self-hosted migration", activationState.Continuity.GrandfatheredMaxMonitoredSystems)
- }
- if activationState.Continuity.GrandfatheredMonitoredSystemsCapturedAt != 0 {
- t.Fatalf("GrandfatheredMonitoredSystemsCapturedAt=%d, want 0 for uncapped self-hosted migration", activationState.Continuity.GrandfatheredMonitoredSystemsCapturedAt)
+ if !activationState.Continuity.LegacyMigration {
+ t.Fatal("expected legacy migration continuity flag after canonical store rebuild")
}
status = readStatus()
- if got := status.MaxMonitoredSystems; got != 0 {
- t.Fatalf("status.MaxMonitoredSystems=%d, want 0 after canonical store rebuild for uncapped self-hosted continuity", got)
- }
- if status.MonitoredSystemContinuity != nil {
- t.Fatalf("expected no continuity in status payload after canonical store rebuild, got %+v", status.MonitoredSystemContinuity)
+ if !status.Valid {
+ t.Fatal("expected migrated self-hosted license status to remain valid after canonical store rebuild")
}
payload = readEntitlements()
- if payload.MonitoredSystemContinuity != nil {
- t.Fatalf("expected no continuity in entitlements payload after canonical store rebuild, got %+v", payload.MonitoredSystemContinuity)
- }
if len(payload.Limits) != 0 {
t.Fatalf("expected no enforced monitored-system limit in entitlements payload after async capture, got %+v", payload.Limits)
}
- if payload.MonitoredSystemCapacity == nil {
- t.Fatal("expected monitored-system capacity payload after async capture")
- }
- if payload.MonitoredSystemCapacity.Mode != "unlimited" || payload.MonitoredSystemCapacity.Current != 23 {
- t.Fatalf("expected unlimited monitored-system capacity with current=23 after canonical store rebuild, got %+v", payload.MonitoredSystemCapacity)
- }
}
func buildGrandfatherFloorMonitor(count int) *monitoring.Monitor {
diff --git a/internal/api/monitored_system_ledger.go b/internal/api/monitored_system_ledger.go
index 2bb6e4aec..341b6182a 100644
--- a/internal/api/monitored_system_ledger.go
+++ b/internal/api/monitored_system_ledger.go
@@ -1,7 +1,6 @@
package api
import (
- "context"
"encoding/json"
"io"
"net/http"
@@ -67,7 +66,6 @@ type MonitoredSystemLedgerExplanationSurface struct {
type MonitoredSystemLedgerResponse struct {
Systems []MonitoredSystemLedgerEntry `json:"systems"`
Total int `json:"total"`
- Limit int `json:"limit"` // 0 = unlimited
}
// MonitoredSystemLedgerExplainRequest describes an optional monitored-system
@@ -119,8 +117,6 @@ type MonitoredSystemLedgerPreviewResponse struct {
CurrentCount int `json:"current_count"`
ProjectedCount int `json:"projected_count"`
AdditionalCount int `json:"additional_count"`
- Limit int `json:"limit"`
- WouldExceedLimit bool `json:"would_exceed_limit"`
Effect string `json:"effect"`
CurrentSystems []MonitoredSystemLedgerEntry `json:"current_systems"`
ProjectedSystems []MonitoredSystemLedgerEntry `json:"projected_systems"`
@@ -177,7 +173,7 @@ func (r *Router) handleMonitoredSystemLedger(w http.ResponseWriter, req *http.Re
return
}
- resp := monitoredSystemLedgerResponseFromReadState(req.Context(), usage.readState)
+ resp := monitoredSystemLedgerResponseFromReadState(usage.readState)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp.NormalizeCollections())
@@ -221,7 +217,7 @@ func (r *Router) handleMonitoredSystemLedgerPreview(w http.ResponseWriter, req *
return
}
- resp := monitoredSystemLedgerPreviewResponse(req.Context(), previewReq.Replacement != nil, preview)
+ resp := monitoredSystemLedgerPreviewResponse(previewReq.Replacement != nil, preview)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp.NormalizeCollections())
@@ -246,7 +242,7 @@ func (r *Router) handleMonitoredSystemLedgerExplain(w http.ResponseWriter, req *
}
resp := MonitoredSystemLedgerExplainResponse{
- Ledger: monitoredSystemLedgerResponseFromReadState(req.Context(), usage.readState),
+ Ledger: monitoredSystemLedgerResponseFromReadState(usage.readState),
}
if explainReq.Candidate != nil {
@@ -278,7 +274,6 @@ func (r *Router) handleMonitoredSystemLedgerExplain(w http.ResponseWriter, req *
}
previewResp := monitoredSystemLedgerPreviewResponse(
- req.Context(),
explainReq.Replacement != nil,
preview,
).NormalizeCollections()
@@ -349,7 +344,6 @@ func monitoredSystemLedgerEntries(
}
func monitoredSystemLedgerResponseFromReadState(
- ctx context.Context,
rs unifiedresources.ReadState,
) MonitoredSystemLedgerResponse {
systems := unifiedresources.MonitoredSystems(rs)
@@ -357,17 +351,13 @@ func monitoredSystemLedgerResponseFromReadState(
return MonitoredSystemLedgerResponse{
Systems: entries,
Total: len(entries),
- Limit: maxMonitoredSystemsLimitForContext(ctx),
}
}
func monitoredSystemLedgerPreviewResponse(
- ctx context.Context,
hasReplacement bool,
preview unifiedresources.MonitoredSystemProjectionPreview,
) MonitoredSystemLedgerPreviewResponse {
- limit := maxMonitoredSystemsLimitForContext(ctx)
- decision := monitoredSystemLimitDecisionFromAdditional(ctx, limit, preview.CurrentCount, preview.AdditionalCount)
currentSystems := monitoredSystemLedgerEntries(preview.CurrentSystems)
projectedSystems := monitoredSystemLedgerEntries(preview.ProjectedSystems)
@@ -375,8 +365,6 @@ func monitoredSystemLedgerPreviewResponse(
CurrentCount: preview.CurrentCount,
ProjectedCount: preview.ProjectedCount,
AdditionalCount: preview.AdditionalCount,
- Limit: limit,
- WouldExceedLimit: decision.exceeded,
Effect: monitoredSystemLedgerPreviewEffect(hasReplacement, preview),
CurrentSystems: currentSystems,
ProjectedSystems: projectedSystems,
diff --git a/internal/api/monitored_system_ledger_test.go b/internal/api/monitored_system_ledger_test.go
index f8a14c289..05a6b0c1a 100644
--- a/internal/api/monitored_system_ledger_test.go
+++ b/internal/api/monitored_system_ledger_test.go
@@ -214,15 +214,13 @@ func TestMonitoredSystemLedgerResponseEmptyState(t *testing.T) {
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("unmarshal: %v", err)
}
- if decoded.Total != 0 || decoded.Limit != 0 || len(decoded.Systems) != 0 {
+ if decoded.Total != 0 || len(decoded.Systems) != 0 {
t.Errorf("unexpected response: %+v", decoded)
}
}
func TestMonitoredSystemLedgerNilSystemsBecomesEmptyArray(t *testing.T) {
- resp := MonitoredSystemLedgerResponse{
- Limit: 5,
- }.NormalizeCollections()
+ resp := MonitoredSystemLedgerResponse{}.NormalizeCollections()
data, err := json.Marshal(resp)
if err != nil {
@@ -294,7 +292,6 @@ func TestHandleMonitoredSystemLedgerHTTP(t *testing.T) {
},
},
Total: 1,
- Limit: 5,
}
rec.Header().Set("Content-Type", "application/json")
@@ -310,7 +307,7 @@ func TestHandleMonitoredSystemLedgerHTTP(t *testing.T) {
if err := json.Unmarshal(rec.Body.Bytes(), &decoded); err != nil {
t.Fatalf("unmarshal: %v", err)
}
- if decoded.Total != 1 || decoded.Limit != 5 {
+ if decoded.Total != 1 {
t.Errorf("unexpected response: %+v", decoded)
}
if decoded.Systems[0].Name != "test-host" || decoded.Systems[0].Type != "host" {
@@ -372,12 +369,6 @@ func TestHandleMonitoredSystemLedgerPreviewHTTP(t *testing.T) {
if decoded.CurrentCount != 1 || decoded.ProjectedCount != 1 || decoded.AdditionalCount != 0 {
t.Fatalf("unexpected counts: %+v", decoded)
}
- if decoded.Limit != 1 {
- t.Fatalf("Limit = %d, want 1", decoded.Limit)
- }
- if decoded.WouldExceedLimit {
- t.Fatalf("expected attach preview to stay within limit, got %+v", decoded)
- }
if decoded.Effect != "attaches_existing" {
t.Fatalf("Effect = %q, want attaches_existing", decoded.Effect)
}
@@ -552,9 +543,6 @@ func TestHandleMonitoredSystemLedgerExplainCurrentAndPreview(t *testing.T) {
if decoded.Ledger.Total != 1 {
t.Fatalf("Ledger.Total = %d, want 1", decoded.Ledger.Total)
}
- if decoded.Ledger.Limit != 1 {
- t.Fatalf("Ledger.Limit = %d, want 1", decoded.Ledger.Limit)
- }
if len(decoded.Ledger.Systems) != 1 {
t.Fatalf("len(Ledger.Systems) = %d, want 1", len(decoded.Ledger.Systems))
}
@@ -603,7 +591,7 @@ func TestHandleMonitoredSystemLedgerExplainCurrentOnly(t *testing.T) {
if decoded.Preview != nil {
t.Fatalf("expected nil preview, got %+v", decoded.Preview)
}
- if decoded.Ledger.Total != 1 || decoded.Ledger.Limit != 2 {
+ if decoded.Ledger.Total != 1 {
t.Fatalf("unexpected ledger payload: %+v", decoded.Ledger)
}
}
diff --git a/internal/api/monitored_system_limit_enforcement.go b/internal/api/monitored_system_limit_enforcement.go
deleted file mode 100644
index e25c47f0c..000000000
--- a/internal/api/monitored_system_limit_enforcement.go
+++ /dev/null
@@ -1,535 +0,0 @@
-package api
-
-import (
- "context"
- "net/http"
- "strings"
- "sync"
- "time"
-
- "github.com/rcourtman/pulse-go-rewrite/internal/config"
- "github.com/rcourtman/pulse-go-rewrite/internal/models"
- "github.com/rcourtman/pulse-go-rewrite/internal/monitoring"
- "github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
- agentsdocker "github.com/rcourtman/pulse-go-rewrite/pkg/agents/docker"
- agentshost "github.com/rcourtman/pulse-go-rewrite/pkg/agents/host"
- agentsk8s "github.com/rcourtman/pulse-go-rewrite/pkg/agents/kubernetes"
-)
-
-// overflowBaseDataDir is set during router initialization to allow the
-// enforcement path to read billing-state overflow fields without requiring
-// a handler reference.
-var (
- overflowBaseDataDirMu sync.RWMutex
- overflowBaseDataDir string
-
- deployReservationCounterMu sync.RWMutex
- deployReservationCounter func(ctx context.Context) int
-)
-
-// SetDeployReservationCounter wires a callback that returns the number of
-// monitored-system slots reserved by in-flight cluster deployments for the org
-// in ctx.
-func SetDeployReservationCounter(fn func(ctx context.Context) int) {
- deployReservationCounterMu.Lock()
- defer deployReservationCounterMu.Unlock()
- deployReservationCounter = fn
-}
-
-func deployReservedCount(ctx context.Context) int {
- deployReservationCounterMu.RLock()
- fn := deployReservationCounter
- deployReservationCounterMu.RUnlock()
- if fn == nil {
- return 0
- }
- return fn(ctx)
-}
-
-// SetOverflowBaseDataDir configures the base data directory used by the
-// enforcement path to read OverflowGrantedAt from billing state.
-func SetOverflowBaseDataDir(dir string) {
- overflowBaseDataDirMu.Lock()
- defer overflowBaseDataDirMu.Unlock()
- overflowBaseDataDir = dir
-}
-
-func maxMonitoredSystemsLimitForContext(ctx context.Context) int {
- service := getLicenseServiceForContext(ctx)
- if service == nil {
- return 0
- }
- status := service.Status()
- if status == nil {
- return 0
- }
-
- limit := status.MaxMonitoredSystems
-
- // 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).
- if eval := service.Evaluator(); eval != nil {
- overflowGrantedAt = eval.OverflowGrantedAt()
- }
-
- // Self-hosted fallback: read from billing state on disk.
- if overflowGrantedAt == nil {
- overflowBaseDataDirMu.RLock()
- baseDir := overflowBaseDataDir
- overflowBaseDataDirMu.RUnlock()
-
- if baseDir != "" {
- orgID := GetOrgID(ctx)
- if orgID == "" {
- orgID = "default"
- }
- store := config.NewFileBillingStore(baseDir)
- if bs, err := store.GetBillingState(orgID); err == nil && bs != nil {
- overflowGrantedAt = bs.OverflowGrantedAt
- }
- }
- }
-
- limit += overflowBonusFromLicensing(status.Tier, overflowGrantedAt, time.Now())
- }
-
- return limit
-}
-
-// monitoredSystemCount returns the canonical number of counted top-level
-// monitored systems. Agent-backed and API-backed views share the same cap.
-func monitoredSystemCount(monitor *monitoring.Monitor) int {
- return monitoredSystemUsage(monitor).count
-}
-
-func monitoredSystemCandidateStateFromEnabled(
- enabled bool,
-) unifiedresources.MonitoredSystemCandidateState {
- if enabled {
- return unifiedresources.MonitoredSystemCandidateStateActive
- }
- return unifiedresources.MonitoredSystemCandidateStateInactive
-}
-
-type monitoredSystemUsageSnapshot struct {
- count int
- readState unifiedresources.ReadState
- available bool
- unavailableReason string
-}
-
-func monitoredSystemUsage(monitor *monitoring.Monitor) monitoredSystemUsageSnapshot {
- usage := monitor.MonitoredSystemUsage()
- return monitoredSystemUsageSnapshot{
- count: usage.Count,
- readState: usage.ReadState,
- available: usage.Available,
- unavailableReason: usage.UnavailableReason,
- }
-}
-
-type monitoredSystemLimitDecision struct {
- current int
- limit int
- additional int
- usageAvailable bool
- usageUnavailableReason string
- exceeded bool
- preview *MonitoredSystemLedgerPreviewResponse
-}
-
-func legacyConnectionCounts(monitor *monitoring.Monitor) legacyConnectionCountsModel {
- return legacyConnectionCountsModel{}
-}
-
-func legacyConnectionCountsFromReadState(rs unifiedresources.ReadState) legacyConnectionCountsModel {
- return legacyConnectionCountsModel{}
-}
-
-func monitoredSystemLimitExceededPayload(decision monitoredSystemLimitDecision) map[string]interface{} {
- payload := map[string]interface{}{
- "error": "license_required",
- "message": monitoredSystemLimitExceededMessageFromLicensing(decision.current, decision.limit),
- "feature": maxMonitoredSystemsLicenseGateKey,
- "upgrade_url": upgradeURLForFeatureFromLicensing(maxMonitoredSystemsLicenseGateKey),
- }
- if decision.preview != nil {
- payload["monitored_system_preview"] = decision.preview.NormalizeCollections()
- }
- return payload
-}
-
-func writeMaxMonitoredSystemsLimitExceeded(w http.ResponseWriter, decision monitoredSystemLimitDecision) {
- writePaymentRequired(w, monitoredSystemLimitExceededPayload(decision))
-}
-
-func writeMonitoredSystemUsageUnavailable(w http.ResponseWriter, reason string) {
- details := map[string]string{}
- if trimmed := strings.TrimSpace(reason); trimmed != "" {
- details["reason"] = trimmed
- }
- writeErrorResponse(
- w,
- http.StatusServiceUnavailable,
- "monitored_system_usage_unavailable",
- "Unable to verify monitored-system capacity right now",
- details,
- )
-}
-
-func monitoredSystemLimitDecisionFromAdditional(
- ctx context.Context,
- limit int,
- current int,
- additional int,
-) monitoredSystemLimitDecision {
- if limit <= 0 {
- return monitoredSystemLimitDecision{
- limit: limit,
- additional: additional,
- usageAvailable: true,
- }
- }
-
- effectiveCurrent := current + deployReservedCount(ctx)
- return monitoredSystemLimitDecision{
- current: effectiveCurrent,
- limit: limit,
- additional: additional,
- usageAvailable: true,
- exceeded: additional > 0 && effectiveCurrent+additional > limit,
- }
-}
-
-func monitoredSystemLimitDecisionFromPreview(
- ctx context.Context,
- limit int,
- hasReplacement bool,
- preview unifiedresources.MonitoredSystemProjectionPreview,
-) monitoredSystemLimitDecision {
- decision := monitoredSystemLimitDecisionFromAdditional(
- ctx,
- limit,
- preview.CurrentCount,
- preview.AdditionalCount,
- )
- resp := monitoredSystemLedgerPreviewResponse(ctx, hasReplacement, preview).NormalizeCollections()
- decision.preview = &resp
- return decision
-}
-
-func monitoredSystemLimitDecisionForCandidate(
- ctx context.Context,
- monitor *monitoring.Monitor,
- candidate unifiedresources.MonitoredSystemCandidate,
-) monitoredSystemLimitDecision {
- limit := maxMonitoredSystemsLimitForContext(ctx)
- if !candidate.CountsTowardMonitoredSystems() {
- return monitoredSystemLimitDecision{
- limit: limit,
- usageAvailable: true,
- }
- }
- if limit <= 0 {
- return monitoredSystemLimitDecision{
- limit: limit,
- usageAvailable: true,
- }
- }
-
- usage := monitoredSystemUsage(monitor)
- if !usage.available {
- return monitoredSystemLimitDecision{
- limit: limit,
- usageUnavailableReason: usage.unavailableReason,
- }
- }
-
- projection := unifiedresources.PreviewMonitoredSystemCandidate(usage.readState, candidate)
- return monitoredSystemLimitDecisionFromPreview(ctx, limit, false, projection)
-}
-
-func monitoredSystemLimitDecisionForCandidateReplacement(
- ctx context.Context,
- monitor *monitoring.Monitor,
- replacement unifiedresources.MonitoredSystemReplacement,
- candidate unifiedresources.MonitoredSystemCandidate,
-) monitoredSystemLimitDecision {
- limit := maxMonitoredSystemsLimitForContext(ctx)
- if !candidate.CountsTowardMonitoredSystems() {
- return monitoredSystemLimitDecision{
- limit: limit,
- usageAvailable: true,
- }
- }
- if limit <= 0 {
- return monitoredSystemLimitDecision{
- limit: limit,
- usageAvailable: true,
- }
- }
-
- usage := monitoredSystemUsage(monitor)
- if !usage.available {
- return monitoredSystemLimitDecision{
- limit: limit,
- usageUnavailableReason: usage.unavailableReason,
- }
- }
-
- projection := unifiedresources.PreviewMonitoredSystemCandidateReplacement(
- usage.readState,
- replacement,
- candidate,
- )
- return monitoredSystemLimitDecisionFromPreview(ctx, limit, true, projection)
-}
-
-func monitoredSystemLimitDecisionForRecordsFromUsage(
- ctx context.Context,
- limit int,
- usage monitoredSystemUsageSnapshot,
- recordsBySource map[unifiedresources.DataSource][]unifiedresources.IngestRecord,
-) monitoredSystemLimitDecision {
- if limit <= 0 {
- return monitoredSystemLimitDecision{
- limit: limit,
- usageAvailable: true,
- }
- }
-
- if !usage.available {
- return monitoredSystemLimitDecision{
- limit: limit,
- usageUnavailableReason: usage.unavailableReason,
- }
- }
-
- projection := unifiedresources.PreviewMonitoredSystemRecords(usage.readState, recordsBySource)
- return monitoredSystemLimitDecisionFromPreview(ctx, limit, false, projection)
-}
-
-func monitoredSystemLimitDecisionForRecordsReplacementFromUsage(
- ctx context.Context,
- limit int,
- usage monitoredSystemUsageSnapshot,
- replacement unifiedresources.MonitoredSystemReplacement,
- recordsBySource map[unifiedresources.DataSource][]unifiedresources.IngestRecord,
-) monitoredSystemLimitDecision {
- if limit <= 0 {
- return monitoredSystemLimitDecision{
- limit: limit,
- usageAvailable: true,
- }
- }
-
- if !usage.available {
- return monitoredSystemLimitDecision{
- limit: limit,
- usageUnavailableReason: usage.unavailableReason,
- }
- }
-
- projection := unifiedresources.PreviewMonitoredSystemRecordsReplacement(
- usage.readState,
- replacement,
- recordsBySource,
- )
- return monitoredSystemLimitDecisionFromPreview(ctx, limit, true, projection)
-}
-
-func monitoredSystemLimitDecisionForAdditionalSlots(
- ctx context.Context,
- monitor *monitoring.Monitor,
- additional int,
-) monitoredSystemLimitDecision {
- limit := maxMonitoredSystemsLimitForContext(ctx)
- if limit <= 0 {
- return monitoredSystemLimitDecision{
- limit: limit,
- additional: additional,
- usageAvailable: true,
- }
- }
-
- usage := monitoredSystemUsage(monitor)
- if !usage.available {
- return monitoredSystemLimitDecision{
- limit: limit,
- usageUnavailableReason: usage.unavailableReason,
- }
- }
-
- return monitoredSystemLimitDecisionFromAdditional(ctx, limit, usage.count, additional)
-}
-
-func enforceMonitoredSystemLimitForConfigRegistration(
- w http.ResponseWriter,
- ctx context.Context,
- _ *config.Config,
- monitor *monitoring.Monitor,
- candidate unifiedresources.MonitoredSystemCandidate,
-) bool {
- decision := monitoredSystemLimitDecisionForCandidate(ctx, monitor, candidate)
- if !decision.usageAvailable {
- writeMonitoredSystemUsageUnavailable(w, decision.usageUnavailableReason)
- return true
- }
- if !decision.exceeded {
- return false
- }
-
- writeMaxMonitoredSystemsLimitExceeded(w, decision)
- return true
-}
-
-func enforceMonitoredSystemLimitForConfigReplacement(
- w http.ResponseWriter,
- ctx context.Context,
- monitor *monitoring.Monitor,
- replacement unifiedresources.MonitoredSystemReplacement,
- candidate unifiedresources.MonitoredSystemCandidate,
-) bool {
- decision := monitoredSystemLimitDecisionForCandidateReplacement(ctx, monitor, replacement, candidate)
- if !decision.usageAvailable {
- writeMonitoredSystemUsageUnavailable(w, decision.usageUnavailableReason)
- return true
- }
- if !decision.exceeded {
- return false
- }
-
- writeMaxMonitoredSystemsLimitExceeded(w, decision)
- return true
-}
-
-// enforceMonitoredSystemLimitForHostReport checks whether a new host report
-// would create a new counted monitored system.
-func enforceMonitoredSystemLimitForHostReport(
- w http.ResponseWriter,
- ctx context.Context,
- monitor *monitoring.Monitor,
- report agentshost.Report,
- tokenRecord *config.APITokenRecord,
-) bool {
- if monitor != nil && monitor.HostReportMatchesKnownIdentity(report, tokenRecord) {
- return false
- }
-
- candidate := unifiedresources.MonitoredSystemCandidate{
- Source: unifiedresources.SourceAgent,
- Type: unifiedresources.ResourceTypeAgent,
- Name: report.Host.DisplayName,
- Hostname: report.Host.Hostname,
- HostURL: report.Host.ReportIP,
- AgentID: report.Agent.ID,
- MachineID: report.Host.MachineID,
- ResourceID: report.Host.ID,
- }
- decision := monitoredSystemLimitDecisionForCandidate(ctx, monitor, candidate)
- if !decision.usageAvailable {
- writeMonitoredSystemUsageUnavailable(w, decision.usageUnavailableReason)
- return true
- }
- if !decision.exceeded {
- return false
- }
-
- writeMaxMonitoredSystemsLimitExceeded(w, decision)
- return true
-}
-
-// enforceMonitoredSystemLimitForDockerReport checks whether a docker report
-// would create a new counted top-level monitored system.
-func enforceMonitoredSystemLimitForDockerReport(
- w http.ResponseWriter,
- ctx context.Context,
- monitor *monitoring.Monitor,
- report agentsdocker.Report,
- tokenRecord *config.APITokenRecord,
-) bool {
- tokenID := ""
- if tokenRecord != nil {
- tokenID = tokenRecord.ID
- }
- if monitor != nil && dockerReportTargetsExistingHostFromLicensing(monitor.ReadSnapshot(), report, tokenID) {
- return false
- }
-
- candidate := unifiedresources.MonitoredSystemCandidate{
- Source: unifiedresources.SourceDocker,
- Type: unifiedresources.ResourceTypeAgent,
- Name: report.Host.Name,
- Hostname: report.Host.Hostname,
- AgentID: report.Agent.ID,
- MachineID: report.Host.MachineID,
- ResourceID: report.AgentKey(),
- }
- decision := monitoredSystemLimitDecisionForCandidate(ctx, monitor, candidate)
- if !decision.usageAvailable {
- writeMonitoredSystemUsageUnavailable(w, decision.usageUnavailableReason)
- return true
- }
- if !decision.exceeded {
- return false
- }
-
- writeMaxMonitoredSystemsLimitExceeded(w, decision)
- return true
-}
-
-// enforceMonitoredSystemLimitForKubernetesReport checks whether a Kubernetes
-// report would create a new counted cluster.
-func enforceMonitoredSystemLimitForKubernetesReport(
- w http.ResponseWriter,
- ctx context.Context,
- monitor *monitoring.Monitor,
- report agentsk8s.Report,
- tokenRecord *config.APITokenRecord,
-) bool {
- tokenID := ""
- if tokenRecord != nil {
- tokenID = tokenRecord.ID
- }
- if monitor != nil && kubernetesReportTargetsExistingClusterFromLicensing(monitor.ReadSnapshot(), report, tokenID) {
- return false
- }
-
- candidate := unifiedresources.MonitoredSystemCandidate{
- Source: unifiedresources.SourceK8s,
- Type: unifiedresources.ResourceTypeK8sCluster,
- Name: report.Cluster.Name,
- Hostname: report.Cluster.Name,
- HostURL: report.Cluster.Server,
- AgentID: report.Agent.ID,
- ResourceID: kubernetesReportIdentifierFromLicensing(report),
- }
- decision := monitoredSystemLimitDecisionForCandidate(ctx, monitor, candidate)
- if !decision.usageAvailable {
- writeMonitoredSystemUsageUnavailable(w, decision.usageUnavailableReason)
- return true
- }
- if !decision.exceeded {
- return false
- }
-
- writeMaxMonitoredSystemsLimitExceeded(w, decision)
- return true
-}
-
-func hostReportTargetsExistingHost(
- hosts []models.Host,
- report agentshost.Report,
- tokenRecord *config.APITokenRecord,
-) bool {
- tokenID := ""
- if tokenRecord != nil {
- tokenID = tokenRecord.ID
- }
- return hostReportTargetsExistingHostsFromLicensing(hosts, report, tokenID)
-}
diff --git a/internal/api/monitored_system_limit_enforcement_test.go b/internal/api/monitored_system_limit_enforcement_test.go
deleted file mode 100644
index cbc6d5c35..000000000
--- a/internal/api/monitored_system_limit_enforcement_test.go
+++ /dev/null
@@ -1,743 +0,0 @@
-package api
-
-import (
- "context"
- "encoding/json"
- "os"
- "path/filepath"
- "runtime"
- "strings"
- "testing"
-
- "github.com/rcourtman/pulse-go-rewrite/internal/config"
- "github.com/rcourtman/pulse-go-rewrite/internal/models"
- "github.com/rcourtman/pulse-go-rewrite/internal/monitoring"
- "github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
- agentshost "github.com/rcourtman/pulse-go-rewrite/pkg/agents/host"
-)
-
-type monitoredSystemLimitBlockedPayload struct {
- Error string `json:"error"`
- Feature string `json:"feature"`
- Message string `json:"message"`
- MonitoredSystemPreview MonitoredSystemLedgerPreviewResponse `json:"monitored_system_preview"`
-}
-
-func decodeMonitoredSystemLimitBlockedPayload(
- t *testing.T,
- body []byte,
-) monitoredSystemLimitBlockedPayload {
- t.Helper()
-
- var payload monitoredSystemLimitBlockedPayload
- if err := json.Unmarshal(body, &payload); err != nil {
- t.Fatalf("decode monitored-system limit blocked payload: %v", err)
- }
- return payload
-}
-
-func readAPIPackageFile(t *testing.T, name string) string {
- t.Helper()
-
- _, filename, _, ok := runtime.Caller(0)
- if !ok {
- t.Fatal("resolve API package test file")
- }
- content, err := os.ReadFile(filepath.Join(filepath.Dir(filename), name))
- if err != nil {
- t.Fatalf("read %s: %v", name, err)
- }
- return string(content)
-}
-
-func requireContainsSnippet(t *testing.T, source string, snippet string) {
- t.Helper()
-
- if !strings.Contains(source, snippet) {
- t.Fatalf("missing snippet %q", snippet)
- }
-}
-
-func requireSnippetCountAtLeast(t *testing.T, source string, snippet string, min int) {
- t.Helper()
-
- if got := strings.Count(source, snippet); got < min {
- t.Fatalf("snippet %q count = %d, want at least %d", snippet, got, min)
- }
-}
-
-func requireSnippetBefore(t *testing.T, source string, earlier string, later string) {
- t.Helper()
-
- earlierIndex := strings.Index(source, earlier)
- if earlierIndex < 0 {
- t.Fatalf("missing earlier snippet %q", earlier)
- }
- laterIndex := strings.Index(source, later)
- if laterIndex < 0 {
- t.Fatalf("missing later snippet %q", later)
- }
- if earlierIndex > laterIndex {
- t.Fatalf("snippet %q must appear before %q", earlier, later)
- }
-}
-
-func requireSourceSegment(t *testing.T, source string, start string, end string) string {
- t.Helper()
-
- startIndex := strings.Index(source, start)
- if startIndex < 0 {
- t.Fatalf("missing segment start %q", start)
- }
- remainder := source[startIndex:]
- endIndex := strings.Index(remainder[len(start):], end)
- if endIndex < 0 {
- t.Fatalf("missing segment end %q after %q", end, start)
- }
- return remainder[:len(start)+endIndex]
-}
-
-func TestMonitoredSystemLimitDecisionOnlyBlocksNetNewSystems(t *testing.T) {
- ctx := context.Background()
-
- atLimitExisting := monitoredSystemLimitDecisionFromAdditional(ctx, 5, 5, 0)
- if atLimitExisting.exceeded {
- t.Fatalf("existing monitored systems must continue reporting at the limit: %+v", atLimitExisting)
- }
-
- atLimitNew := monitoredSystemLimitDecisionFromAdditional(ctx, 5, 5, 1)
- if !atLimitNew.exceeded {
- t.Fatalf("net-new monitored systems must be blocked when the cap is full: %+v", atLimitNew)
- }
-}
-
-func TestMonitoredSystemLimitDecisionForInactiveCandidateBypassesUsageAvailability(t *testing.T) {
- ctx := context.Background()
- setMaxMonitoredSystemsLicenseForTests(t, 1)
-
- decision := monitoredSystemLimitDecisionForCandidate(ctx, nil, unifiedresources.MonitoredSystemCandidate{
- Source: unifiedresources.SourceTrueNAS,
- Type: unifiedresources.ResourceTypeAgent,
- Name: "tower",
- Hostname: "tower.local",
- HostURL: "https://tower.local",
- State: unifiedresources.MonitoredSystemCandidateStateInactive,
- })
- if !decision.usageAvailable {
- t.Fatalf("inactive candidate should not require usage availability: %+v", decision)
- }
- if decision.exceeded {
- t.Fatalf("inactive candidate must not exceed the limit: %+v", decision)
- }
-
- replacementDecision := monitoredSystemLimitDecisionForCandidateReplacement(
- ctx,
- nil,
- unifiedresources.MonitoredSystemReplacement{
- Source: unifiedresources.SourceTrueNAS,
- Selector: unifiedresources.MonitoredSystemReplacementSelector{
- Hostname: "tower.local",
- },
- },
- unifiedresources.MonitoredSystemCandidate{
- Source: unifiedresources.SourceTrueNAS,
- Type: unifiedresources.ResourceTypeAgent,
- Name: "tower",
- Hostname: "tower.local",
- HostURL: "https://tower.local",
- State: unifiedresources.MonitoredSystemCandidateStateInactive,
- },
- )
- if !replacementDecision.usageAvailable {
- t.Fatalf("inactive replacement candidate should not require usage availability: %+v", replacementDecision)
- }
- if replacementDecision.exceeded {
- t.Fatalf("inactive replacement candidate must not exceed the limit: %+v", replacementDecision)
- }
-}
-
-func TestMonitoredSystemAdmissionSurfacesStayBehindSharedLimitGate(t *testing.T) {
- router := readAPIPackageFile(t, "router_routes_registration.go")
-
- type sourceContract struct {
- file string
- requiredSnippets []string
- requiredCounts map[string]int
- requiredOrderings [][2]string
- }
- type proofContract struct {
- file string
- snippets []string
- }
- type admissionSurface struct {
- name string
- routerSnippets []string
- sources []sourceContract
- proofs []proofContract
- }
-
- surfaces := []admissionSurface{
- {
- name: "unified agent report",
- routerSnippets: []string{
- `"/api/agents/agent/report"`,
- `"/api/agents/host/report"`,
- "r.unifiedAgentHandlers.HandleReport",
- },
- sources: []sourceContract{{
- file: "agent_ingest.go",
- requiredSnippets: []string{
- "enforceMonitoredSystemLimitForHostReport(",
- "ApplyHostReport(report, tokenRecord)",
- },
- requiredOrderings: [][2]string{
- {"enforceMonitoredSystemLimitForHostReport(", "ApplyHostReport(report, tokenRecord)"},
- },
- }},
- proofs: []proofContract{{
- file: "unified_agent_handlers_test.go",
- snippets: []string{
- "TestUnifiedAgentHandlers_HandleReport_EnforcesMaxMonitoredSystemsForNewHostsOnly",
- "Existing host should continue to report at the limit.",
- "New host should be blocked.",
- "http.StatusPaymentRequired",
- },
- }},
- },
- {
- name: "docker agent report",
- routerSnippets: []string{
- `"/api/agents/docker/report"`,
- "r.dockerAgentHandlers.HandleReport",
- },
- sources: []sourceContract{{
- file: "docker_agents.go",
- requiredSnippets: []string{
- "enforceMonitoredSystemLimitForDockerReport(",
- "ApplyDockerReport(report, tokenRecord)",
- },
- requiredOrderings: [][2]string{
- {"enforceMonitoredSystemLimitForDockerReport(", "ApplyDockerReport(report, tokenRecord)"},
- },
- }},
- proofs: []proofContract{{
- file: "docker_agents_additional_test.go",
- snippets: []string{
- "TestDockerAgentHandlers_HandleReport_BlocksNewMonitoredSystemAtLimit",
- "http.StatusPaymentRequired",
- },
- }},
- },
- {
- name: "kubernetes agent report",
- routerSnippets: []string{
- `"/api/agents/kubernetes/report"`,
- "r.kubernetesAgentHandlers.HandleReport",
- },
- sources: []sourceContract{{
- file: "kubernetes_agents.go",
- requiredSnippets: []string{
- "enforceMonitoredSystemLimitForKubernetesReport(",
- "ApplyKubernetesReport(report, tokenRecord)",
- },
- requiredOrderings: [][2]string{
- {"enforceMonitoredSystemLimitForKubernetesReport(", "ApplyKubernetesReport(report, tokenRecord)"},
- },
- }},
- proofs: []proofContract{{
- file: "kubernetes_agents_additional_test.go",
- snippets: []string{
- "TestKubernetesAgentHandlers_HandleReport_BlocksNewMonitoredSystemAtLimit",
- "http.StatusPaymentRequired",
- },
- }},
- },
- {
- name: "proxmox family config add and update",
- routerSnippets: []string{
- `"/api/config/nodes"`,
- `"/api/config/nodes/"`,
- "r.configHandlers.HandleAddNode",
- "r.configHandlers.HandleUpdateNode",
- },
- sources: []sourceContract{{
- file: "config_node_handlers.go",
- requiredSnippets: []string{
- "enforceMonitoredSystemLimitForConfigRegistration(",
- "enforceMonitoredSystemLimitForConfigReplacement(",
- "SaveNodesConfig(",
- },
- requiredCounts: map[string]int{
- "enforceMonitoredSystemLimitForConfigRegistration(": 3,
- "enforceMonitoredSystemLimitForConfigReplacement(": 3,
- },
- requiredOrderings: [][2]string{
- {"enforceMonitoredSystemLimitForConfigRegistration(", "PVEInstances = append"},
- {"enforceMonitoredSystemLimitForConfigReplacement(", "*pve = updated"},
- },
- }},
- proofs: []proofContract{
- {
- file: "config_handlers_add_test.go",
- snippets: []string{
- "TestHandleAddNode_BlocksNewCountedSystemAtLimit",
- "http.StatusPaymentRequired",
- },
- },
- {
- file: "config_handlers_update_test.go",
- snippets: []string{
- "TestHandleUpdateNode_AllowsProjectedNetNewSystemWithoutPaidLimit",
- "without paid limit enforcement",
- "http.StatusOK",
- },
- },
- },
- },
- {
- name: "auto registration",
- routerSnippets: []string{
- `"/api/auto-register"`,
- "r.configHandlers.HandleAutoRegister",
- },
- sources: []sourceContract{{
- file: "config_setup_handlers.go",
- requiredSnippets: []string{
- "enforceMonitoredSystemLimitForConfigRegistration(",
- "SaveNodesConfig(",
- },
- requiredCounts: map[string]int{
- "enforceMonitoredSystemLimitForConfigRegistration(": 2,
- },
- }},
- proofs: []proofContract{{
- file: "config_handlers_auto_register_test.go",
- snippets: []string{
- "TestHandleAutoRegister_BlocksNewCountedSystemAtLimit",
- "http.StatusPaymentRequired",
- },
- }},
- },
- {
- name: "truenas connection add and update",
- routerSnippets: []string{
- `"/api/truenas/connections"`,
- `"/api/truenas/connections/"`,
- "r.trueNASHandlers.HandleAdd",
- "r.trueNASHandlers.HandleUpdate",
- },
- sources: []sourceContract{{
- file: "truenas_handlers.go",
- requiredSnippets: []string{
- "monitoredSystemLimitDecisionForCandidate(",
- "monitoredSystemLimitDecisionForCandidateReplacement(",
- "SaveTrueNASConfig(",
- },
- requiredOrderings: [][2]string{
- {"h.enforceMonitoredSystemLimit(w, r, instance)", "existing = append(existing, instance)"},
- {"h.enforceMonitoredSystemLimitReplacement(w, r, instances[index], instance)", "instances[index] = instance"},
- },
- }},
- proofs: []proofContract{{
- file: "truenas_handlers_test.go",
- snippets: []string{
- "TestTrueNASHandlers_HandleAdd_BlocksNewCountedSystemAtLimit",
- "TestTrueNASHandlers_HandleUpdate_BlocksProjectedNetNewSystemAtLimit",
- "http.StatusPaymentRequired",
- "license_required",
- },
- }},
- },
- {
- name: "vmware connection add and update",
- routerSnippets: []string{
- `"/api/vmware/connections"`,
- `"/api/vmware/connections/"`,
- "r.vmwareHandlers.HandleAdd",
- "r.vmwareHandlers.HandleUpdate",
- },
- sources: []sourceContract{{
- file: "vmware_handlers.go",
- requiredSnippets: []string{
- "monitoredSystemLimitDecisionForRecordsFromUsage(",
- "monitoredSystemLimitDecisionForRecordsReplacementFromUsage(",
- "SaveVMwareConfig(",
- },
- requiredOrderings: [][2]string{
- {"h.enforceMonitoredSystemLimit(w, r, instance)", "instances = append(instances, instance)"},
- {"h.enforceMonitoredSystemLimitReplacement(w, r, instances[index], instance)", "instances[index] = instance"},
- },
- }},
- proofs: []proofContract{{
- file: "vmware_handlers_test.go",
- snippets: []string{
- "TestVMwareHandlers_HandleAdd_BlocksProjectedNetNewSystemsAtLimit",
- "TestVMwareHandlers_HandleUpdate_BlocksProjectedNetNewSystemsAtLimit",
- "http.StatusPaymentRequired",
- "license_required",
- },
- }},
- },
- {
- name: "agent deployment jobs",
- routerSnippets: []string{
- `"/api/clusters/"`,
- `"/api/agent-deploy/jobs/"`,
- "r.deployHandlers.HandleCreateJob",
- "r.deployHandlers.HandleRetryJob",
- },
- sources: []sourceContract{{
- file: "deploy_handlers.go",
- requiredSnippets: []string{
- "monitoredSystemLimitDecisionForAdditionalSlots(ctx, h.monitor, 0)",
- `Reason: "skipped_license"`,
- `"license_limit"`,
- },
- requiredCounts: map[string]int{
- "monitoredSystemLimitDecisionForAdditionalSlots(ctx, h.monitor, 0)": 2,
- },
- requiredOrderings: [][2]string{
- {"monitoredSystemLimitDecisionForAdditionalSlots(ctx, h.monitor, 0)", `Reason: "skipped_license"`},
- {"monitoredSystemLimitDecisionForAdditionalSlots(ctx, h.monitor, 0)", `"license_limit"`},
- },
- }},
- proofs: []proofContract{{
- file: "deploy_handlers_test.go",
- snippets: []string{
- "TestHandleCreateJob_TruncatesTargetsToAvailableLicenseSlots",
- "TestHandleRetryJob_BlocksWhenNoLicenseSlotsAvailable",
- "skipped_license",
- "license_limit",
- },
- }},
- },
- }
-
- for _, surface := range surfaces {
- t.Run(surface.name, func(t *testing.T) {
- for _, snippet := range surface.routerSnippets {
- requireContainsSnippet(t, router, snippet)
- }
- for _, contract := range surface.sources {
- source := readAPIPackageFile(t, contract.file)
- for _, snippet := range contract.requiredSnippets {
- requireContainsSnippet(t, source, snippet)
- }
- for snippet, min := range contract.requiredCounts {
- requireSnippetCountAtLeast(t, source, snippet, min)
- }
- for _, ordering := range contract.requiredOrderings {
- requireSnippetBefore(t, source, ordering[0], ordering[1])
- }
- }
- for _, proof := range surface.proofs {
- source := readAPIPackageFile(t, proof.file)
- for _, snippet := range proof.snippets {
- requireContainsSnippet(t, source, snippet)
- }
- }
- })
- }
-}
-
-func TestVMwareAdmissionEnforcementChecksUsageBeforeExternalInventory(t *testing.T) {
- source := readAPIPackageFile(t, "vmware_handlers.go")
-
- addEnforcement := requireSourceSegment(
- t,
- source,
- "func (h *VMwareHandlers) enforceMonitoredSystemLimit(",
- "func (h *VMwareHandlers) enforceMonitoredSystemLimitReplacement(",
- )
- requireSnippetBefore(
- t,
- addEnforcement,
- "usage := monitoredSystemUsage(monitor)",
- "records, invalidConfig, err := h.previewMonitoredSystemRecords(r.Context(), instance)",
- )
- requireSnippetBefore(
- t,
- addEnforcement,
- "if !usage.available",
- "records, invalidConfig, err := h.previewMonitoredSystemRecords(r.Context(), instance)",
- )
-
- replacementEnforcement := requireSourceSegment(
- t,
- source,
- "func (h *VMwareHandlers) enforceMonitoredSystemLimitReplacement(",
- "func (h *VMwareHandlers) previewMonitoredSystemRecords(",
- )
- requireSnippetBefore(
- t,
- replacementEnforcement,
- "candidate := vmwareMonitoredSystemCandidate(next)",
- "usage := monitoredSystemUsage(monitor)",
- )
- requireSnippetBefore(
- t,
- replacementEnforcement,
- "if !candidate.CountsTowardMonitoredSystems()",
- "usage := monitoredSystemUsage(monitor)",
- )
- requireSnippetBefore(
- t,
- replacementEnforcement,
- "usage := monitoredSystemUsage(monitor)",
- "records, invalidConfig, err := h.previewMonitoredSystemRecords(r.Context(), next)",
- )
- requireSnippetBefore(
- t,
- replacementEnforcement,
- "if !usage.available",
- "records, invalidConfig, err := h.previewMonitoredSystemRecords(r.Context(), next)",
- )
-}
-
-func TestVMwareAdmissionEnforcementSkipsUsageAndInventoryForDisabledConnections(t *testing.T) {
- source := readAPIPackageFile(t, "vmware_handlers.go")
-
- addEnforcement := requireSourceSegment(
- t,
- source,
- "func (h *VMwareHandlers) enforceMonitoredSystemLimit(",
- "func (h *VMwareHandlers) enforceMonitoredSystemLimitReplacement(",
- )
- requireSnippetBefore(
- t,
- addEnforcement,
- "candidate := vmwareMonitoredSystemCandidate(instance)",
- "usage := monitoredSystemUsage(monitor)",
- )
- requireSnippetBefore(
- t,
- addEnforcement,
- "if !candidate.CountsTowardMonitoredSystems()",
- "usage := monitoredSystemUsage(monitor)",
- )
- requireSnippetBefore(
- t,
- addEnforcement,
- "if !candidate.CountsTowardMonitoredSystems()",
- "records, invalidConfig, err := h.previewMonitoredSystemRecords(r.Context(), instance)",
- )
-
- replacementEnforcement := requireSourceSegment(
- t,
- source,
- "func (h *VMwareHandlers) enforceMonitoredSystemLimitReplacement(",
- "func (h *VMwareHandlers) previewMonitoredSystemRecords(",
- )
- requireSnippetBefore(
- t,
- replacementEnforcement,
- "candidate := vmwareMonitoredSystemCandidate(next)",
- "usage := monitoredSystemUsage(monitor)",
- )
- requireSnippetBefore(
- t,
- replacementEnforcement,
- "if !candidate.CountsTowardMonitoredSystems()",
- "usage := monitoredSystemUsage(monitor)",
- )
- requireSnippetBefore(
- t,
- replacementEnforcement,
- "if !candidate.CountsTowardMonitoredSystems()",
- "records, invalidConfig, err := h.previewMonitoredSystemRecords(r.Context(), next)",
- )
-}
-
-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 {
- t.Fatalf("expected 0 for nil monitor, got %d", got)
- }
-}
-
-func TestLegacyConnectionCountsFromReadState(t *testing.T) {
- registry := unifiedresources.NewRegistry(nil)
- registry.IngestRecords(unifiedresources.SourceProxmox, []unifiedresources.IngestRecord{
- {
- SourceID: "pve-1",
- Resource: unifiedresources.Resource{
- ID: "pve-1",
- Name: "pve-1",
- Type: unifiedresources.ResourceTypeAgent,
- Status: unifiedresources.StatusOnline,
- Proxmox: &unifiedresources.ProxmoxData{},
- },
- },
- {
- SourceID: "pve-2",
- Resource: unifiedresources.Resource{
- ID: "pve-2",
- Name: "pve-2",
- Type: unifiedresources.ResourceTypeAgent,
- Status: unifiedresources.StatusOnline,
- Proxmox: &unifiedresources.ProxmoxData{},
- Agent: &unifiedresources.AgentData{},
- },
- },
- })
- registry.IngestRecords(unifiedresources.SourceDocker, []unifiedresources.IngestRecord{
- {
- SourceID: "docker-1",
- Resource: unifiedresources.Resource{
- ID: "docker-1",
- Name: "docker-1",
- Type: unifiedresources.ResourceTypeAgent,
- Status: unifiedresources.StatusOnline,
- Docker: &unifiedresources.DockerData{},
- },
- },
- {
- SourceID: "docker-2",
- Resource: unifiedresources.Resource{
- ID: "docker-2",
- Name: "docker-2",
- Type: unifiedresources.ResourceTypeAgent,
- Status: unifiedresources.StatusOnline,
- Docker: &unifiedresources.DockerData{},
- Agent: &unifiedresources.AgentData{},
- },
- },
- })
- registry.IngestRecords(unifiedresources.SourceK8s, []unifiedresources.IngestRecord{
- {
- SourceID: "k8s-1",
- Resource: unifiedresources.Resource{
- ID: "k8s-1",
- Name: "prod",
- Type: unifiedresources.ResourceTypeK8sCluster,
- Status: unifiedresources.StatusOnline,
- Kubernetes: &unifiedresources.K8sData{AgentID: "legacy-k8s-1"},
- },
- },
- })
-
- counts := legacyConnectionCountsFromReadState(unifiedresources.NewMonitorAdapter(registry))
- if counts.KubernetesClusters != 0 {
- t.Fatalf("expected kubernetes_clusters=0, got %d", counts.KubernetesClusters)
- }
- if counts.ProxmoxNodes != 0 || counts.DockerHosts != 0 {
- t.Fatalf("expected legacy connection counts to stay zero under monitored-system counting, got %+v", counts)
- }
-}
-
-func TestLegacyConnectionCountsUsesSnapshotFallback(t *testing.T) {
- registry := unifiedresources.NewRegistry(nil)
- registry.IngestRecords(unifiedresources.SourceProxmox, []unifiedresources.IngestRecord{
- {
- SourceID: "pve-1",
- Resource: unifiedresources.Resource{
- ID: "pve-1",
- Name: "pve-1",
- Type: unifiedresources.ResourceTypeAgent,
- Status: unifiedresources.StatusOnline,
- Proxmox: &unifiedresources.ProxmoxData{},
- },
- },
- })
- registry.IngestRecords(unifiedresources.SourceK8s, []unifiedresources.IngestRecord{
- {
- SourceID: "k8s-1",
- Resource: unifiedresources.Resource{
- ID: "k8s-1",
- Name: "prod",
- Type: unifiedresources.ResourceTypeK8sCluster,
- Status: unifiedresources.StatusOnline,
- Kubernetes: &unifiedresources.K8sData{AgentID: "legacy-k8s-1"},
- },
- },
- })
-
- monitor := &monitoring.Monitor{}
- monitor.SetResourceStore(unifiedresources.NewMonitorAdapter(registry))
-
- counts := legacyConnectionCounts(monitor)
- if counts.ProxmoxNodes != 0 || counts.DockerHosts != 0 || counts.KubernetesClusters != 0 {
- t.Fatalf("expected legacy connection counts to stay zero, got %+v", counts)
- }
-}
-
-func TestHostReportTargetsExistingHostBridge(t *testing.T) {
- snapshot := models.StateSnapshot{
- Hosts: []models.Host{{ID: "host-1"}},
- }
-
- t.Run("matches_existing", func(t *testing.T) {
- report := agentshost.Report{
- Host: agentshost.HostInfo{ID: "host-1"},
- }
- if !hostReportTargetsExistingHost(snapshot.Hosts, report, nil) {
- t.Fatal("expected match by host ID")
- }
- })
-
- t.Run("no_match_for_new_host", func(t *testing.T) {
- report := agentshost.Report{
- Host: agentshost.HostInfo{ID: "host-new", Hostname: "new-server"},
- }
- if hostReportTargetsExistingHost(snapshot.Hosts, report, nil) {
- t.Fatal("expected no match for unknown host")
- }
- })
-
- t.Run("token_record_forwarded", func(t *testing.T) {
- snapshot := models.StateSnapshot{
- Hosts: []models.Host{{Hostname: "srv-1", TokenID: "token-a"}},
- }
- report := agentshost.Report{
- Host: agentshost.HostInfo{Hostname: "srv-1"},
- }
- token := &config.APITokenRecord{ID: "token-a"}
- if !hostReportTargetsExistingHost(snapshot.Hosts, report, token) {
- t.Fatal("expected match with matching token")
- }
- wrongToken := &config.APITokenRecord{ID: "token-b"}
- if hostReportTargetsExistingHost(snapshot.Hosts, report, wrongToken) {
- t.Fatal("expected no match with different token")
- }
- })
-}
-
-func TestDeployReservedCount(t *testing.T) {
- // Nil counter returns 0.
- SetDeployReservationCounter(nil)
- if got := deployReservedCount(context.Background()); got != 0 {
- t.Fatalf("expected 0 with nil counter, got %d", got)
- }
-
- // Wired counter returns value.
- SetDeployReservationCounter(func(_ context.Context) int { return 5 })
- t.Cleanup(func() { SetDeployReservationCounter(nil) })
-
- if got := deployReservedCount(context.Background()); got != 5 {
- t.Fatalf("expected 5, got %d", got)
- }
-}
diff --git a/internal/api/monitored_system_usage.go b/internal/api/monitored_system_usage.go
new file mode 100644
index 000000000..5763b5d87
--- /dev/null
+++ b/internal/api/monitored_system_usage.go
@@ -0,0 +1,57 @@
+package api
+
+import (
+ "net/http"
+ "strings"
+
+ "github.com/rcourtman/pulse-go-rewrite/internal/monitoring"
+ "github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
+)
+
+func monitoredSystemCandidateStateFromEnabled(
+ enabled bool,
+) unifiedresources.MonitoredSystemCandidateState {
+ if enabled {
+ return unifiedresources.MonitoredSystemCandidateStateActive
+ }
+ return unifiedresources.MonitoredSystemCandidateStateInactive
+}
+
+type monitoredSystemUsageSnapshot struct {
+ count int
+ readState unifiedresources.ReadState
+ available bool
+ unavailableReason string
+}
+
+func monitoredSystemUsage(monitor *monitoring.Monitor) monitoredSystemUsageSnapshot {
+ usage := monitor.MonitoredSystemUsage()
+ return monitoredSystemUsageSnapshot{
+ count: usage.Count,
+ readState: usage.ReadState,
+ available: usage.Available,
+ unavailableReason: usage.UnavailableReason,
+ }
+}
+
+func writeMonitoredSystemUsageUnavailable(w http.ResponseWriter, reason string) {
+ details := map[string]string{}
+ if trimmed := strings.TrimSpace(reason); trimmed != "" {
+ details["reason"] = trimmed
+ }
+ writeErrorResponse(
+ w,
+ http.StatusServiceUnavailable,
+ "monitored_system_usage_unavailable",
+ "Unable to verify monitored-system inventory right now",
+ details,
+ )
+}
+
+func legacyConnectionCounts(monitor *monitoring.Monitor) legacyConnectionCountsModel {
+ return legacyConnectionCountsModel{}
+}
+
+func legacyConnectionCountsFromReadState(rs unifiedresources.ReadState) legacyConnectionCountsModel {
+ return legacyConnectionCountsModel{}
+}
diff --git a/internal/api/payments_webhook_handlers.go b/internal/api/payments_webhook_handlers.go
index 4dfdc7640..1e3f39884 100644
--- a/internal/api/payments_webhook_handlers.go
+++ b/internal/api/payments_webhook_handlers.go
@@ -293,11 +293,10 @@ func (h *StripeWebhookHandlers) handleCheckoutSessionCompleted(ctx context.Conte
}
planVersion := derivePlanVersion(session.Metadata, "")
- limits, _ := limitsForCloudPlanFromLicensing(planVersion)
state := &billingState{
Capabilities: cloudCapabilitiesFromLicensing(),
- Limits: limits,
+ Limits: map[string]int64{},
MetersEnabled: []string{},
PlanVersion: planVersion,
SubscriptionState: subscriptionStateActiveValue,
@@ -389,8 +388,7 @@ func (h *StripeWebhookHandlers) handleSubscriptionUpdated(ctx context.Context, s
if shouldGrantPaidCapabilities(subState) {
state.Capabilities = cloudCapabilitiesFromLicensing()
- limits, _ := limitsForCloudPlanFromLicensing(state.PlanVersion)
- state.Limits = limits
+ state.Limits = map[string]int64{}
} else {
state.Capabilities = []string{}
state.Limits = map[string]int64{}
diff --git a/internal/api/router.go b/internal/api/router.go
index 5ad3dde23..dd4774b5b 100644
--- a/internal/api/router.go
+++ b/internal/api/router.go
@@ -433,8 +433,6 @@ func (r *Router) setupRoutes() {
})
// Wire license service provider so middleware can access per-tenant license services
SetLicenseServiceProvider(r.licenseHandlers)
- // Wire base data dir for overflow enforcement (reads OverflowGrantedAt from billing state).
- SetOverflowBaseDataDir(r.config.DataPath)
r.reportingHandlers = NewReportingHandlers(r.mtMonitor, recoveryManager)
r.logHandlers = NewLogHandlers(r.config, r.persistence)
rbacHandlers := NewRBACHandlers(r.config, rbacProvider)
@@ -531,30 +529,7 @@ func (r *Router) setupRoutes() {
if deployStore != nil {
r.deployStore = deployStore
if r.monitor != nil {
- reservation := deploy.NewReservationManager()
- r.deployHandlers = NewDeployHandlers(deployStore, r.monitor, r.agentExecServer, reservation, r.resolvePublicURL, r.config, r.persistence)
-
- SetDeployReservationCounter(func(ctx context.Context) int {
- orgID := GetOrgID(ctx)
- if orgID == "" {
- orgID = "default"
- }
- return reservation.ReservedForOrg(orgID)
- })
-
- // Periodic cleanup of expired reservations.
- go func() {
- ticker := time.NewTicker(5 * time.Minute)
- defer ticker.Stop()
- for {
- select {
- case <-r.lifecycleCtx.Done():
- return
- case <-ticker.C:
- reservation.CleanExpired()
- }
- }
- }()
+ r.deployHandlers = NewDeployHandlers(deployStore, r.monitor, r.agentExecServer, r.resolvePublicURL, r.config, r.persistence)
}
}
diff --git a/internal/api/stripe_webhook_handlers_additional_test.go b/internal/api/stripe_webhook_handlers_additional_test.go
index e9df00f5a..79959b3eb 100644
--- a/internal/api/stripe_webhook_handlers_additional_test.go
+++ b/internal/api/stripe_webhook_handlers_additional_test.go
@@ -109,8 +109,8 @@ func TestStripeWebhook_SubscriptionUpdated_BackfillsIndexAndAppliesState(t *test
if state.PlanVersion != "stripe_price:price_gold" {
t.Fatalf("plan_version=%q, want %q", state.PlanVersion, "stripe_price:price_gold")
}
- if got := state.Limits["max_monitored_systems"]; got != 10 {
- t.Fatalf("limits[max_monitored_systems]=%d, want %d", got, 10)
+ if _, ok := state.Limits["max_monitored_systems"]; ok {
+ t.Fatalf("expected retired max_monitored_systems limit to be omitted, got %+v", state.Limits)
}
if state.StripeSubscriptionID != "sub_new" {
t.Fatalf("stripe_subscription_id=%q, want %q", state.StripeSubscriptionID, "sub_new")
diff --git a/internal/api/stripe_webhook_handlers_test.go b/internal/api/stripe_webhook_handlers_test.go
index 973c12ece..305a47017 100644
--- a/internal/api/stripe_webhook_handlers_test.go
+++ b/internal/api/stripe_webhook_handlers_test.go
@@ -266,8 +266,8 @@ func TestStripeWebhook_CheckoutCompleted_IdempotentProvisioning(t *testing.T) {
if state.PlanVersion != "cloud_starter" {
t.Fatalf("plan_version=%q, want %q", state.PlanVersion, "cloud_starter")
}
- if got := state.Limits["max_monitored_systems"]; got != 10 {
- t.Fatalf("limits[max_monitored_systems]=%d, want %d", got, 10)
+ if _, ok := state.Limits["max_monitored_systems"]; ok {
+ t.Fatalf("expected retired max_monitored_systems limit to be omitted, got %+v", state.Limits)
}
if state.StripeCustomerID != "cus_abc" {
t.Fatalf("stripe_customer_id=%q, want %q", state.StripeCustomerID, "cus_abc")
diff --git a/internal/api/subscription_entitlements.go b/internal/api/subscription_entitlements.go
index 9c7b4de5b..9fc3ea76e 100644
--- a/internal/api/subscription_entitlements.go
+++ b/internal/api/subscription_entitlements.go
@@ -40,16 +40,8 @@ func (h *LicenseHandlers) buildCommercialEntitlementPayload(
usage := h.entitlementUsageSnapshot(ctx)
trialEndsAtUnix := trialEndsAtUnixFromService(svc)
- // 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 status.MaxMonitoredSystems > 0 {
- if bonus := overflowBonusFromLicensing(status.Tier, overflowGrantedAt, now); bonus > 0 {
- status.MaxMonitoredSystems += bonus
- }
- }
payload := buildEntitlementPayloadWithUsage(
status,
@@ -129,14 +121,6 @@ func (h *LicenseHandlers) HandleRuntimeCapabilities(w http.ResponseWriter, r *ht
status := svc.Status()
usage := h.entitlementUsageSnapshot(r.Context())
- overflowGrantedAt := h.ensureOnboardingOverflow(r.Context(), status.Tier)
- now := time.Now()
- if status.MaxMonitoredSystems > 0 {
- if bonus := overflowBonusFromLicensing(status.Tier, overflowGrantedAt, now); bonus > 0 {
- status.MaxMonitoredSystems += bonus
- }
- }
-
payload := buildRuntimeCapabilitiesPayloadWithUsage(
status,
h.payloadSubscriptionStateForService(svc),
@@ -177,9 +161,8 @@ func trialEndsAtUnixFromService(svc *licenseService) *int64 {
type entitlementUsageSnapshot = entitlementUsageSnapshotModel
-// entitlementUsageSnapshot returns best-effort runtime usage counts for limits.
-// The monitored-system cap applies to canonical top-level monitored systems
-// regardless of collection path.
+// entitlementUsageSnapshot returns best-effort runtime usage counts for
+// entitlement payloads and billing/support context.
func (h *LicenseHandlers) entitlementUsageSnapshot(ctx context.Context) entitlementUsageSnapshot {
usage := entitlementUsageSnapshot{}
if h == nil {
diff --git a/internal/api/truenas_handlers.go b/internal/api/truenas_handlers.go
index cef3950c9..4e3761c90 100644
--- a/internal/api/truenas_handlers.go
+++ b/internal/api/truenas_handlers.go
@@ -73,10 +73,6 @@ func (h *TrueNASHandlers) HandleAdd(w http.ResponseWriter, r *http.Request) {
return
}
- if h.enforceMonitoredSystemLimit(w, r, instance) {
- return
- }
-
existing, err := persistence.LoadTrueNASConfig()
if err != nil {
writeErrorResponse(w, http.StatusInternalServerError, "truenas_load_failed", "Failed to load TrueNAS configuration", map[string]string{"error": err.Error()})
@@ -241,10 +237,6 @@ func (h *TrueNASHandlers) HandleUpdate(w http.ResponseWriter, r *http.Request) {
writeErrorResponse(w, http.StatusBadRequest, "validation_error", err.Error(), nil)
return
}
- if h.enforceMonitoredSystemLimitReplacement(w, r, instances[index], instance) {
- return
- }
-
instances[index] = instance
if err := persistence.SaveTrueNASConfig(instances); err != nil {
writeErrorResponse(w, http.StatusInternalServerError, "truenas_save_failed", "Failed to save TrueNAS configuration", map[string]string{"error": err.Error()})
@@ -286,7 +278,7 @@ func (h *TrueNASHandlers) HandlePreviewConnection(w http.ResponseWriter, r *http
return
}
- writeJSON(w, http.StatusOK, monitoredSystemLedgerPreviewResponse(r.Context(), false, preview).NormalizeCollections())
+ writeJSON(w, http.StatusOK, monitoredSystemLedgerPreviewResponse(false, preview).NormalizeCollections())
}
// HandlePreviewSavedConnection projects the monitored-system impact of an edit
@@ -360,7 +352,7 @@ func (h *TrueNASHandlers) HandlePreviewSavedConnection(w http.ResponseWriter, r
return
}
- writeJSON(w, http.StatusOK, monitoredSystemLedgerPreviewResponse(r.Context(), true, preview).NormalizeCollections())
+ writeJSON(w, http.StatusOK, monitoredSystemLedgerPreviewResponse(true, preview).NormalizeCollections())
return
}
@@ -592,59 +584,6 @@ func (h *TrueNASHandlers) persistenceForRequest(w http.ResponseWriter, ctx conte
return persistence
}
-func (h *TrueNASHandlers) enforceMonitoredSystemLimit(
- w http.ResponseWriter,
- r *http.Request,
- instance config.TrueNASInstance,
-) bool {
- var monitor *monitoring.Monitor
- if h != nil && h.getMonitor != nil {
- monitor = h.getMonitor(r.Context())
- }
-
- decision := monitoredSystemLimitDecisionForCandidate(r.Context(), monitor, trueNASMonitoredSystemCandidate(instance))
- if !decision.usageAvailable {
- writeMonitoredSystemUsageUnavailable(w, decision.usageUnavailableReason)
- return true
- }
- if !decision.exceeded {
- return false
- }
-
- writeMaxMonitoredSystemsLimitExceeded(w, decision)
- return true
-}
-
-func (h *TrueNASHandlers) enforceMonitoredSystemLimitReplacement(
- w http.ResponseWriter,
- r *http.Request,
- current config.TrueNASInstance,
- next config.TrueNASInstance,
-) bool {
- var monitor *monitoring.Monitor
- if h != nil && h.getMonitor != nil {
- monitor = h.getMonitor(r.Context())
- }
-
- replacementHost := pulseTokenHostCandidate(current.Host)
- decision := monitoredSystemLimitDecisionForCandidateReplacement(r.Context(), monitor, unifiedresources.MonitoredSystemReplacement{
- Source: unifiedresources.SourceTrueNAS,
- Selector: unifiedresources.MonitoredSystemReplacementSelector{
- Hostname: replacementHost,
- },
- }, trueNASMonitoredSystemCandidate(next))
- if !decision.usageAvailable {
- writeMonitoredSystemUsageUnavailable(w, decision.usageUnavailableReason)
- return true
- }
- if !decision.exceeded {
- return false
- }
-
- writeMaxMonitoredSystemsLimitExceeded(w, decision)
- return true
-}
-
func trueNASMonitoredSystemCandidate(instance config.TrueNASInstance) unifiedresources.MonitoredSystemCandidate {
return unifiedresources.MonitoredSystemCandidate{
Source: unifiedresources.SourceTrueNAS,
diff --git a/internal/api/truenas_handlers_test.go b/internal/api/truenas_handlers_test.go
index ec37b4586..b225c84db 100644
--- a/internal/api/truenas_handlers_test.go
+++ b/internal/api/truenas_handlers_test.go
@@ -159,35 +159,16 @@ func TestTrueNASHandlers_HandleAdd_BlocksNewCountedSystemAtLimit(t *testing.T) {
rec := httptest.NewRecorder()
handler.HandleAdd(rec, req)
- if rec.Code != http.StatusPaymentRequired {
- t.Fatalf("expected 402 once monitored-system cap is full, got %d: %s", rec.Code, rec.Body.String())
- }
- payload := decodeMonitoredSystemLimitBlockedPayload(t, rec.Body.Bytes())
- if payload.Error != "license_required" {
- t.Fatalf("error=%q, want license_required", payload.Error)
- }
- if payload.Feature != maxMonitoredSystemsLicenseGateKey {
- t.Fatalf("feature=%q, want %q", payload.Feature, maxMonitoredSystemsLicenseGateKey)
- }
- if !payload.MonitoredSystemPreview.WouldExceedLimit {
- t.Fatalf("expected monitored_system_preview.would_exceed_limit=true, got %+v", payload.MonitoredSystemPreview)
- }
- if payload.MonitoredSystemPreview.Effect != "creates_new" {
- t.Fatalf("effect=%q, want creates_new", payload.MonitoredSystemPreview.Effect)
- }
- if payload.MonitoredSystemPreview.AdditionalCount != 1 {
- t.Fatalf("additional_count=%d, want 1", payload.MonitoredSystemPreview.AdditionalCount)
- }
- if len(payload.MonitoredSystemPreview.ProjectedSystems) != 1 {
- t.Fatalf("len(projected_systems)=%d, want 1", len(payload.MonitoredSystemPreview.ProjectedSystems))
+ if rec.Code != http.StatusCreated {
+ t.Fatalf("expected 201 with monitored-system caps retired, got %d: %s", rec.Code, rec.Body.String())
}
stored, err := persistence.LoadTrueNASConfig()
if err != nil {
t.Fatalf("load persisted config: %v", err)
}
- if len(stored) != 1 {
- t.Fatalf("expected blocked TrueNAS add not to persist, got %d instances", len(stored))
+ if len(stored) != 2 {
+ t.Fatalf("expected TrueNAS add to persist with monitored-system caps retired, got %d instances", len(stored))
}
}
@@ -222,14 +203,16 @@ func TestTrueNASHandlers_HandleAdd_ReturnsUnavailableWhenSupplementalInventoryNo
rec := httptest.NewRecorder()
handler.HandleAdd(rec, req)
- assertMonitoredSystemUsageUnavailableReason(t, rec, tc.reason)
+ if rec.Code != http.StatusCreated {
+ t.Fatalf("expected 201 with monitored-system caps retired, got %d: %s", rec.Code, rec.Body.String())
+ }
stored, err := persistence.LoadTrueNASConfig()
if err != nil {
t.Fatalf("load truenas config: %v", err)
}
- if len(stored) != 0 {
- t.Fatalf("expected unavailable monitored-system usage not to persist add, got %d connections", len(stored))
+ if len(stored) != 1 {
+ t.Fatalf("expected add to persist with monitored-system caps retired, got %d connections", len(stored))
}
})
}
@@ -716,32 +699,16 @@ func TestTrueNASHandlers_HandleUpdate_BlocksProjectedNetNewSystemAtLimit(t *test
rec := httptest.NewRecorder()
handler.HandleUpdate(rec, req)
- if rec.Code != http.StatusPaymentRequired {
- t.Fatalf("expected 402 when update would add a new monitored system, got %d: %s", rec.Code, rec.Body.String())
- }
- payload := decodeMonitoredSystemLimitBlockedPayload(t, rec.Body.Bytes())
- if !payload.MonitoredSystemPreview.WouldExceedLimit {
- t.Fatalf("expected monitored_system_preview.would_exceed_limit=true, got %+v", payload.MonitoredSystemPreview)
- }
- if payload.MonitoredSystemPreview.Effect != "splits_existing" {
- t.Fatalf("effect=%q, want splits_existing", payload.MonitoredSystemPreview.Effect)
- }
- if payload.MonitoredSystemPreview.AdditionalCount != 1 {
- t.Fatalf("additional_count=%d, want 1", payload.MonitoredSystemPreview.AdditionalCount)
- }
- if len(payload.MonitoredSystemPreview.CurrentSystems) != 1 {
- t.Fatalf("len(current_systems)=%d, want 1", len(payload.MonitoredSystemPreview.CurrentSystems))
- }
- if len(payload.MonitoredSystemPreview.ProjectedSystems) != 1 {
- t.Fatalf("len(projected_systems)=%d, want 1", len(payload.MonitoredSystemPreview.ProjectedSystems))
+ if rec.Code != http.StatusOK {
+ t.Fatalf("expected 200 with monitored-system caps retired, got %d: %s", rec.Code, rec.Body.String())
}
stored, err := persistence.LoadTrueNASConfig()
if err != nil {
t.Fatalf("load persisted config: %v", err)
}
- if stored[0].Host != "archive.local" {
- t.Fatalf("expected blocked update to preserve original host, got %+v", stored[0])
+ if stored[0].Host != "backup.local" {
+ t.Fatalf("expected update to persist with monitored-system caps retired, got %+v", stored[0])
}
}
@@ -790,14 +757,16 @@ func TestTrueNASHandlers_HandleUpdate_ReturnsUnavailableWhenSupplementalInventor
rec := httptest.NewRecorder()
handler.HandleUpdate(rec, req)
- assertMonitoredSystemUsageUnavailableReason(t, rec, tc.reason)
+ if rec.Code != http.StatusOK {
+ t.Fatalf("expected 200 with monitored-system caps retired, got %d: %s", rec.Code, rec.Body.String())
+ }
stored, err := persistence.LoadTrueNASConfig()
if err != nil {
t.Fatalf("load truenas config: %v", err)
}
- if len(stored) != 1 || stored[0].Host != "archive.local" {
- t.Fatalf("expected unavailable monitored-system usage to preserve stored connection, got %+v", stored)
+ if len(stored) != 1 || stored[0].Host != "backup.local" {
+ t.Fatalf("expected update to persist with monitored-system caps retired, got %+v", stored)
}
})
}
diff --git a/internal/api/unified_agent_handlers_test.go b/internal/api/unified_agent_handlers_test.go
index 908af47df..90265a02d 100644
--- a/internal/api/unified_agent_handlers_test.go
+++ b/internal/api/unified_agent_handlers_test.go
@@ -98,7 +98,7 @@ func TestUnifiedAgentHandlers_HandleReport(t *testing.T) {
}
}
-func TestUnifiedAgentHandlers_HandleReport_EnforcesMaxMonitoredSystemsForNewHostsOnly(t *testing.T) {
+func TestUnifiedAgentHandlers_HandleReport_AllowsNewHostsWithCapsRetired(t *testing.T) {
setMaxMonitoredSystemsLicenseForTests(t, 1)
handler, monitor := newUnifiedAgentHandlers(t, nil)
@@ -107,7 +107,7 @@ func TestUnifiedAgentHandlers_HandleReport_EnforcesMaxMonitoredSystemsForNewHost
t.Fatalf("expected seeded host ID")
}
- // Existing host should continue to report at the limit.
+ // Existing host should continue to report with monitored-system caps retired.
existingReport := agentshost.Report{
Agent: agentshost.AgentInfo{
ID: "agent-1",
@@ -128,7 +128,7 @@ func TestUnifiedAgentHandlers_HandleReport_EnforcesMaxMonitoredSystemsForNewHost
t.Fatalf("existing host report should pass at limit, got %d: %s", existingRec.Code, existingRec.Body.String())
}
- // New host should be blocked.
+ // New host should also be accepted because monitored-system caps are retired.
newReport := agentshost.Report{
Agent: agentshost.AgentInfo{
ID: "agent-2",
@@ -145,12 +145,12 @@ func TestUnifiedAgentHandlers_HandleReport_EnforcesMaxMonitoredSystemsForNewHost
newReq := httptest.NewRequest(http.MethodPost, "/api/agents/agent/report", bytes.NewReader(newBody))
newRec := httptest.NewRecorder()
handler.HandleReport(newRec, newReq)
- if newRec.Code != http.StatusPaymentRequired {
- t.Fatalf("new host should be blocked at limit, got %d: %s", newRec.Code, newRec.Body.String())
+ if newRec.Code != http.StatusOK {
+ t.Fatalf("new host should be accepted with monitored-system caps retired, got %d: %s", newRec.Code, newRec.Body.String())
}
}
-func TestUnifiedAgentHandlers_HandleReport_PreservesExistingHostLimitContinuityAcrossRestart(t *testing.T) {
+func TestUnifiedAgentHandlers_HandleReport_AcceptsNewHostAcrossRestartWithCapsRetired(t *testing.T) {
setMaxMonitoredSystemsLicenseForTests(t, 1)
cfg := &config.Config{DataPath: t.TempDir()}
@@ -191,7 +191,7 @@ func TestUnifiedAgentHandlers_HandleReport_PreservesExistingHostLimitContinuityA
restartReport.Timestamp = report.Timestamp.Add(30 * time.Second)
restartRec := postReport(t, restartedHandler, restartReport)
if restartRec.Code != http.StatusOK {
- t.Fatalf("existing host should still report after restart at limit, got %d: %s", restartRec.Code, restartRec.Body.String())
+ t.Fatalf("existing host should still report after restart with monitored-system caps retired, got %d: %s", restartRec.Code, restartRec.Body.String())
}
newReport := agentshost.Report{
@@ -208,8 +208,8 @@ func TestUnifiedAgentHandlers_HandleReport_PreservesExistingHostLimitContinuityA
Timestamp: time.Now().UTC(),
}
newRec := postReport(t, restartedHandler, newReport)
- if newRec.Code != http.StatusPaymentRequired {
- t.Fatalf("new host should still be blocked at limit after restart, got %d: %s", newRec.Code, newRec.Body.String())
+ if newRec.Code != http.StatusOK {
+ t.Fatalf("new host should still be accepted after restart with monitored-system caps retired, got %d: %s", newRec.Code, newRec.Body.String())
}
}
diff --git a/internal/api/vmware_handlers.go b/internal/api/vmware_handlers.go
index f27bd0345..db4f20823 100644
--- a/internal/api/vmware_handlers.go
+++ b/internal/api/vmware_handlers.go
@@ -81,10 +81,6 @@ func (h *VMwareHandlers) HandleAdd(w http.ResponseWriter, r *http.Request) {
if persistence == nil {
return
}
- if h.enforceMonitoredSystemLimit(w, r, instance) {
- return
- }
-
instances, err := persistence.LoadVMwareConfig()
if err != nil {
writeErrorResponse(w, http.StatusInternalServerError, "vmware_load_failed", "Failed to load VMware configuration", map[string]string{"error": err.Error()})
@@ -255,10 +251,6 @@ func (h *VMwareHandlers) HandleUpdate(w http.ResponseWriter, r *http.Request) {
writeErrorResponse(w, http.StatusBadRequest, "validation_error", err.Error(), nil)
return
}
- if h.enforceMonitoredSystemLimitReplacement(w, r, instances[index], instance) {
- return
- }
-
instances[index] = instance
if err := persistence.SaveVMwareConfig(instances); err != nil {
writeErrorResponse(w, http.StatusInternalServerError, "vmware_save_failed", "Failed to save VMware configuration", map[string]string{"error": err.Error()})
@@ -296,7 +288,7 @@ func (h *VMwareHandlers) HandlePreviewConnection(w http.ResponseWriter, r *http.
candidate := vmwareMonitoredSystemCandidate(instance)
if !candidate.CountsTowardMonitoredSystems() {
preview := unifiedresources.PreviewMonitoredSystemCandidate(usage.readState, candidate)
- writeJSON(w, http.StatusOK, monitoredSystemLedgerPreviewResponse(r.Context(), false, preview).NormalizeCollections())
+ writeJSON(w, http.StatusOK, monitoredSystemLedgerPreviewResponse(false, preview).NormalizeCollections())
return
}
@@ -314,7 +306,7 @@ func (h *VMwareHandlers) HandlePreviewConnection(w http.ResponseWriter, r *http.
return
}
- writeJSON(w, http.StatusOK, monitoredSystemLedgerPreviewResponse(r.Context(), false, preview).NormalizeCollections())
+ writeJSON(w, http.StatusOK, monitoredSystemLedgerPreviewResponse(false, preview).NormalizeCollections())
}
// HandlePreviewSavedConnection projects the monitored-system impact of editing
@@ -384,7 +376,7 @@ func (h *VMwareHandlers) HandlePreviewSavedConnection(w http.ResponseWriter, r *
replacement,
candidate,
)
- writeJSON(w, http.StatusOK, monitoredSystemLedgerPreviewResponse(r.Context(), true, preview).NormalizeCollections())
+ writeJSON(w, http.StatusOK, monitoredSystemLedgerPreviewResponse(true, preview).NormalizeCollections())
return
}
@@ -406,7 +398,7 @@ func (h *VMwareHandlers) HandlePreviewSavedConnection(w http.ResponseWriter, r *
return
}
- writeJSON(w, http.StatusOK, monitoredSystemLedgerPreviewResponse(r.Context(), true, preview).NormalizeCollections())
+ writeJSON(w, http.StatusOK, monitoredSystemLedgerPreviewResponse(true, preview).NormalizeCollections())
return
}
@@ -670,99 +662,6 @@ func (h *VMwareHandlers) monitorForRequest(ctx context.Context) *monitoring.Moni
return h.getMonitor(ctx)
}
-func (h *VMwareHandlers) enforceMonitoredSystemLimit(
- w http.ResponseWriter,
- r *http.Request,
- instance config.VMwareVCenterInstance,
-) bool {
- candidate := vmwareMonitoredSystemCandidate(instance)
- if !candidate.CountsTowardMonitoredSystems() {
- return false
- }
-
- limit := maxMonitoredSystemsLimitForContext(r.Context())
- if limit <= 0 {
- return false
- }
-
- monitor := h.monitorForRequest(r.Context())
- usage := monitoredSystemUsage(monitor)
- if !usage.available {
- writeMonitoredSystemUsageUnavailable(w, usage.unavailableReason)
- return true
- }
-
- records, invalidConfig, err := h.previewMonitoredSystemRecords(r.Context(), instance)
- if err != nil {
- h.writeConnectionFailure(w, invalidConfig, err)
- return true
- }
-
- decision := monitoredSystemLimitDecisionForRecordsFromUsage(r.Context(), limit, usage, map[unifiedresources.DataSource][]unifiedresources.IngestRecord{
- unifiedresources.SourceVMware: records,
- })
- if !decision.usageAvailable {
- writeMonitoredSystemUsageUnavailable(w, decision.usageUnavailableReason)
- return true
- }
- if !decision.exceeded {
- return false
- }
-
- writeMaxMonitoredSystemsLimitExceeded(w, decision)
- return true
-}
-
-func (h *VMwareHandlers) enforceMonitoredSystemLimitReplacement(
- w http.ResponseWriter,
- r *http.Request,
- current config.VMwareVCenterInstance,
- next config.VMwareVCenterInstance,
-) bool {
- candidate := vmwareMonitoredSystemCandidate(next)
- if !candidate.CountsTowardMonitoredSystems() {
- return false
- }
-
- limit := maxMonitoredSystemsLimitForContext(r.Context())
- if limit <= 0 {
- return false
- }
-
- monitor := h.monitorForRequest(r.Context())
- usage := monitoredSystemUsage(monitor)
- if !usage.available {
- writeMonitoredSystemUsageUnavailable(w, usage.unavailableReason)
- return true
- }
-
- records, invalidConfig, err := h.previewMonitoredSystemRecords(r.Context(), next)
- if err != nil {
- h.writeConnectionFailure(w, invalidConfig, err)
- return true
- }
-
- replacementID := strings.TrimSpace(current.ID)
- decision := monitoredSystemLimitDecisionForRecordsReplacementFromUsage(r.Context(), limit, usage, unifiedresources.MonitoredSystemReplacement{
- Source: unifiedresources.SourceVMware,
- Selector: unifiedresources.MonitoredSystemReplacementSelector{
- ResourceID: replacementID,
- },
- }, map[unifiedresources.DataSource][]unifiedresources.IngestRecord{
- unifiedresources.SourceVMware: records,
- })
- if !decision.usageAvailable {
- writeMonitoredSystemUsageUnavailable(w, decision.usageUnavailableReason)
- return true
- }
- if !decision.exceeded {
- return false
- }
-
- writeMaxMonitoredSystemsLimitExceeded(w, decision)
- return true
-}
-
func (h *VMwareHandlers) previewMonitoredSystemRecords(
ctx context.Context,
instance config.VMwareVCenterInstance,
diff --git a/internal/api/vmware_handlers_test.go b/internal/api/vmware_handlers_test.go
index f1eaf3310..d8c1e00aa 100644
--- a/internal/api/vmware_handlers_test.go
+++ b/internal/api/vmware_handlers_test.go
@@ -187,35 +187,16 @@ func TestVMwareHandlers_HandleAdd_BlocksProjectedNetNewSystemsAtLimit(t *testing
rec := httptest.NewRecorder()
handler.HandleAdd(rec, req)
- if rec.Code != http.StatusPaymentRequired {
- t.Fatalf("expected 402 once projected VMware inventory exceeds the cap, got %d: %s", rec.Code, rec.Body.String())
- }
- payload := decodeMonitoredSystemLimitBlockedPayload(t, rec.Body.Bytes())
- if payload.Error != "license_required" {
- t.Fatalf("error=%q, want license_required", payload.Error)
- }
- if payload.Feature != maxMonitoredSystemsLicenseGateKey {
- t.Fatalf("feature=%q, want %q", payload.Feature, maxMonitoredSystemsLicenseGateKey)
- }
- if !payload.MonitoredSystemPreview.WouldExceedLimit {
- t.Fatalf("expected monitored_system_preview.would_exceed_limit=true, got %+v", payload.MonitoredSystemPreview)
- }
- if payload.MonitoredSystemPreview.Effect != "creates_new" {
- t.Fatalf("effect=%q, want creates_new", payload.MonitoredSystemPreview.Effect)
- }
- if payload.MonitoredSystemPreview.AdditionalCount != 1 {
- t.Fatalf("additional_count=%d, want 1", payload.MonitoredSystemPreview.AdditionalCount)
- }
- if len(payload.MonitoredSystemPreview.ProjectedSystems) != 1 {
- t.Fatalf("len(projected_systems)=%d, want 1", len(payload.MonitoredSystemPreview.ProjectedSystems))
+ if rec.Code != http.StatusCreated {
+ t.Fatalf("expected 201 with monitored-system caps retired, got %d: %s", rec.Code, rec.Body.String())
}
stored, err := persistence.LoadVMwareConfig()
if err != nil {
t.Fatalf("load vmware config: %v", err)
}
- if len(stored) != 0 {
- t.Fatalf("expected blocked VMware add not to persist, got %d connections", len(stored))
+ if len(stored) != 1 {
+ t.Fatalf("expected VMware add to persist with monitored-system caps retired, got %d connections", len(stored))
}
}
@@ -261,17 +242,19 @@ func TestVMwareHandlers_HandleAdd_ReturnsUnavailableBeforePreviewingInventory(t
rec := httptest.NewRecorder()
handler.HandleAdd(rec, req)
- assertMonitoredSystemUsageUnavailableReason(t, rec, tc.reason)
+ if rec.Code != http.StatusCreated {
+ t.Fatalf("expected 201 with monitored-system caps retired, got %d: %s", rec.Code, rec.Body.String())
+ }
if previewRecordsCalled {
- t.Fatal("expected VMware add not to preview external inventory while monitored-system usage is unavailable")
+ t.Fatal("expected VMware add not to preview external inventory when monitored-system caps are retired")
}
stored, err := persistence.LoadVMwareConfig()
if err != nil {
t.Fatalf("load vmware config: %v", err)
}
- if len(stored) != 0 {
- t.Fatalf("expected unavailable monitored-system usage not to persist add, got %d connections", len(stored))
+ if len(stored) != 1 {
+ t.Fatalf("expected add to persist with monitored-system caps retired, got %d connections", len(stored))
}
})
}
@@ -838,32 +821,16 @@ func TestVMwareHandlers_HandleUpdate_BlocksProjectedNetNewSystemsAtLimit(t *test
rec := httptest.NewRecorder()
handler.HandleUpdate(rec, req)
- if rec.Code != http.StatusPaymentRequired {
- t.Fatalf("expected 402 when update would add a new monitored system, got %d: %s", rec.Code, rec.Body.String())
- }
- payload := decodeMonitoredSystemLimitBlockedPayload(t, rec.Body.Bytes())
- if !payload.MonitoredSystemPreview.WouldExceedLimit {
- t.Fatalf("expected monitored_system_preview.would_exceed_limit=true, got %+v", payload.MonitoredSystemPreview)
- }
- if payload.MonitoredSystemPreview.Effect != "splits_existing" {
- t.Fatalf("effect=%q, want splits_existing", payload.MonitoredSystemPreview.Effect)
- }
- if payload.MonitoredSystemPreview.AdditionalCount != 1 {
- t.Fatalf("additional_count=%d, want 1", payload.MonitoredSystemPreview.AdditionalCount)
- }
- if len(payload.MonitoredSystemPreview.CurrentSystems) != 1 {
- t.Fatalf("len(current_systems)=%d, want 1", len(payload.MonitoredSystemPreview.CurrentSystems))
- }
- if len(payload.MonitoredSystemPreview.ProjectedSystems) != 1 {
- t.Fatalf("len(projected_systems)=%d, want 1", len(payload.MonitoredSystemPreview.ProjectedSystems))
+ if rec.Code != http.StatusOK {
+ t.Fatalf("expected 200 with monitored-system caps retired, got %d: %s", rec.Code, rec.Body.String())
}
stored, err := persistence.LoadVMwareConfig()
if err != nil {
t.Fatalf("load persisted config: %v", err)
}
- if stored[0].Host != "vc-a.lab.local" {
- t.Fatalf("expected blocked update to preserve original host, got %+v", stored[0])
+ if stored[0].Host != "vc-b.lab.local" {
+ t.Fatalf("expected update to persist with monitored-system caps retired, got %+v", stored[0])
}
}
@@ -922,17 +889,19 @@ func TestVMwareHandlers_HandleUpdate_ReturnsUnavailableBeforePreviewingInventory
rec := httptest.NewRecorder()
handler.HandleUpdate(rec, req)
- assertMonitoredSystemUsageUnavailableReason(t, rec, tc.reason)
+ if rec.Code != http.StatusOK {
+ t.Fatalf("expected 200 with monitored-system caps retired, got %d: %s", rec.Code, rec.Body.String())
+ }
if previewRecordsCalled {
- t.Fatal("expected VMware update not to preview external inventory while monitored-system usage is unavailable")
+ t.Fatal("expected VMware update not to preview external inventory when monitored-system caps are retired")
}
stored, err := persistence.LoadVMwareConfig()
if err != nil {
t.Fatalf("load vmware config: %v", err)
}
- if len(stored) != 1 || stored[0].Host != "vc-a.lab.local" {
- t.Fatalf("expected unavailable monitored-system usage to preserve stored connection, got %+v", stored)
+ if len(stored) != 1 || stored[0].Host != "vc-b.lab.local" {
+ t.Fatalf("expected update to persist with monitored-system caps retired, got %+v", stored)
}
})
}
@@ -1202,9 +1171,6 @@ func TestVMwareHandlers_HandlePreviewConnection_ReturnsCanonicalMultiSystemImpac
if preview.CurrentCount != 1 || preview.ProjectedCount != 2 || preview.AdditionalCount != 1 {
t.Fatalf("unexpected preview counts: %+v", preview)
}
- if !preview.WouldExceedLimit {
- t.Fatalf("expected preview to report limit overrun, got %+v", preview)
- }
if len(preview.CurrentSystems) != 1 {
t.Fatalf("len(CurrentSystems) = %d, want 1", len(preview.CurrentSystems))
}
diff --git a/internal/cloudcp/entitlements/service.go b/internal/cloudcp/entitlements/service.go
index b14d65b7d..20950347d 100644
--- a/internal/cloudcp/entitlements/service.go
+++ b/internal/cloudcp/entitlements/service.go
@@ -12,7 +12,6 @@ import (
"github.com/golang-jwt/jwt/v5"
"github.com/rcourtman/pulse-go-rewrite/internal/cloudcp/registry"
pkglicensing "github.com/rcourtman/pulse-go-rewrite/pkg/licensing"
- "github.com/rs/zerolog/log"
)
const defaultCloudPlanVersion = "cloud_starter"
@@ -344,15 +343,6 @@ func tenantSubscriptionState(tenant *registry.Tenant) pkglicensing.SubscriptionS
}
func buildPaidEntitlementLeaseClaims(ctx *tenantLeaseContext, instanceHost string, now time.Time) pkglicensing.EntitlementLeaseClaims {
- limits, known := pkglicensing.LimitsForCloudPlan(ctx.planVersion)
- if !known && ctx.tenant != nil {
- log.Warn().
- Str("tenant_id", ctx.tenant.ID).
- Str("plan_version", ctx.planVersion).
- Int64("default_max_monitored_systems", limits["max_monitored_systems"]).
- Msg("Unknown plan version during hosted entitlement lease build; applying safe default monitored-system limit")
- }
-
var capabilities []string
if pkglicensing.ShouldGrantPaidCapabilities(ctx.subscriptionState) {
capabilities = pkglicensing.DeriveCapabilitiesFromTier(pkglicensing.TierCloud, nil)
@@ -364,7 +354,7 @@ func buildPaidEntitlementLeaseClaims(ctx *tenantLeaseContext, instanceHost strin
PlanVersion: strings.TrimSpace(ctx.planVersion),
SubscriptionState: ctx.subscriptionState,
Capabilities: capabilities,
- Limits: limits,
+ Limits: map[string]int64{},
MetersEnabled: []string{},
RegisteredClaims: jwt.RegisteredClaims{
IssuedAt: jwt.NewNumericDate(now.UTC()),
@@ -383,7 +373,7 @@ func buildTrialLeaseClaims(ctx trialEntitlementContext, now time.Time) pkglicens
trialCapabilities, trialLimits := pkglicensing.DeriveEntitlements(
pkglicensing.TierPro,
nil,
- pkglicensing.TierMonitoredSystemLimits[pkglicensing.TierPro],
+ 0,
0,
)
trialState := pkglicensing.BuildTrialBillingState(ctx.trialStartedAt.UTC(), trialCapabilities)
diff --git a/internal/cloudcp/entitlements/service_test.go b/internal/cloudcp/entitlements/service_test.go
index b1ab4ac13..c76ac9d1f 100644
--- a/internal/cloudcp/entitlements/service_test.go
+++ b/internal/cloudcp/entitlements/service_test.go
@@ -133,8 +133,8 @@ func TestIssueTenantBillingStateDefaultsIndividualCloudPlanToStarter(t *testing.
if claims.PlanVersion != "cloud_starter" {
t.Fatalf("claims.PlanVersion=%q, want %q", claims.PlanVersion, "cloud_starter")
}
- if got := claims.Limits["max_monitored_systems"]; got != 10 {
- t.Fatalf("claims.Limits[max_monitored_systems]=%d, want 10", got)
+ if _, ok := claims.Limits["max_monitored_systems"]; ok {
+ t.Fatalf("claims retained retired max_monitored_systems: %v", claims.Limits)
}
}
@@ -188,8 +188,8 @@ func TestIssueTenantBillingStateCanonicalizesLegacyStoredPlanVersion(t *testing.
if claims.PlanVersion != "cloud_starter" {
t.Fatalf("claims.PlanVersion=%q, want %q", claims.PlanVersion, "cloud_starter")
}
- if got := claims.Limits["max_monitored_systems"]; got != 10 {
- t.Fatalf("claims.Limits[max_monitored_systems]=%d, want 10", got)
+ if _, ok := claims.Limits["max_monitored_systems"]; ok {
+ t.Fatalf("claims retained retired max_monitored_systems: %v", claims.Limits)
}
}
@@ -321,13 +321,8 @@ func TestRefreshLegacyTrialEntitlementReturnsLease(t *testing.T) {
if claims.SubscriptionState != pkglicensing.SubStateTrial {
t.Fatalf("claims.SubscriptionState=%q, want %q", claims.SubscriptionState, pkglicensing.SubStateTrial)
}
- if got := claims.Limits[pkglicensing.MaxMonitoredSystemsLicenseGateKey]; got != int64(pkglicensing.TierMonitoredSystemLimits[pkglicensing.TierPro]) {
- t.Fatalf(
- "claims.Limits[%s]=%d, want %d",
- pkglicensing.MaxMonitoredSystemsLicenseGateKey,
- got,
- pkglicensing.TierMonitoredSystemLimits[pkglicensing.TierPro],
- )
+ if _, ok := claims.Limits[pkglicensing.MaxMonitoredSystemsLicenseGateKey]; ok {
+ t.Fatalf("claims retained retired %s: %v", pkglicensing.MaxMonitoredSystemsLicenseGateKey, claims.Limits)
}
}
diff --git a/internal/cloudcp/stripe/cloud_lifecycle_integration_test.go b/internal/cloudcp/stripe/cloud_lifecycle_integration_test.go
index 5a2418537..bd14cab97 100644
--- a/internal/cloudcp/stripe/cloud_lifecycle_integration_test.go
+++ b/internal/cloudcp/stripe/cloud_lifecycle_integration_test.go
@@ -18,53 +18,48 @@ import (
pkglicensing "github.com/rcourtman/pulse-go-rewrite/pkg/licensing"
)
-// TestCloudLifecycle_CheckoutToBillingToMonitoredSystemLimits exercises the full Cloud
+// TestCloudLifecycle_CheckoutToBillingWithoutMonitoredSystemVolumeKeys exercises the full Cloud
// individual tier lifecycle:
//
// checkout.session.completed with plan_version metadata →
// tenant provisioning with correct plan version →
-// billing state written with correct monitored-system limits →
-// subscription update propagates new tier limits →
+// billing state written without retired monitored-system volume keys →
+// subscription update preserves the uncapped runtime contract →
// subscription cancellation revokes capabilities.
//
// This is an integration test that wires together the registry, provisioner,
// and entitlements service to verify that Cloud tier assignment is end-to-end
-// correct for Starter (10 monitored systems), Power (30), and Max (75).
-func TestCloudLifecycle_CheckoutToBillingToMonitoredSystemLimits(t *testing.T) {
+// correct for Starter, Power, Max, and Founding plans.
+func TestCloudLifecycle_CheckoutToBillingWithoutMonitoredSystemLimits(t *testing.T) {
tests := []struct {
- name string
- planVersion string
- wantMonitoredSystems int64
- wantCaps []string // subset of expected capabilities
- wantSubState pkglicensing.SubscriptionState
+ name string
+ planVersion string
+ wantCaps []string // subset of expected capabilities
+ wantSubState pkglicensing.SubscriptionState
}{
{
- name: "cloud_starter_via_metadata",
- planVersion: "cloud_starter",
- wantMonitoredSystems: 10,
- wantCaps: []string{"ai_autofix", "relay", "mobile_app", "rbac"},
- wantSubState: pkglicensing.SubStateActive,
+ name: "cloud_starter_via_metadata",
+ planVersion: "cloud_starter",
+ wantCaps: []string{"ai_autofix", "relay", "mobile_app", "rbac"},
+ wantSubState: pkglicensing.SubStateActive,
},
{
- name: "cloud_power_via_metadata",
- planVersion: "cloud_power",
- wantMonitoredSystems: 30,
- wantCaps: []string{"ai_autofix", "relay", "mobile_app", "rbac"},
- wantSubState: pkglicensing.SubStateActive,
+ name: "cloud_power_via_metadata",
+ planVersion: "cloud_power",
+ wantCaps: []string{"ai_autofix", "relay", "mobile_app", "rbac"},
+ wantSubState: pkglicensing.SubStateActive,
},
{
- name: "cloud_max_via_metadata",
- planVersion: "cloud_max",
- wantMonitoredSystems: 75,
- wantCaps: []string{"ai_autofix", "relay", "mobile_app", "rbac"},
- wantSubState: pkglicensing.SubStateActive,
+ name: "cloud_max_via_metadata",
+ planVersion: "cloud_max",
+ wantCaps: []string{"ai_autofix", "relay", "mobile_app", "rbac"},
+ wantSubState: pkglicensing.SubStateActive,
},
{
- name: "cloud_founding_via_metadata",
- planVersion: "cloud_founding",
- wantMonitoredSystems: 10, // Founding rate = Starter limits
- wantCaps: []string{"ai_autofix", "relay"},
- wantSubState: pkglicensing.SubStateActive,
+ name: "cloud_founding_via_metadata",
+ planVersion: "cloud_founding",
+ wantCaps: []string{"ai_autofix", "relay"},
+ wantSubState: pkglicensing.SubStateActive,
},
}
@@ -174,7 +169,7 @@ func TestCloudLifecycle_CheckoutToBillingToMonitoredSystemLimits(t *testing.T) {
t.Fatalf("StripeAccount.StripeSubscriptionID = %q, want %q", sa.StripeSubscriptionID, session.Subscription)
}
- // ── Verify billing state and monitored-system limits ────────
+ // ── Verify billing state and retired monitored-system volume keys ─
store := config.NewFileBillingStore(provisioner.tenantDataDir(tenant.ID))
bs, err := store.GetBillingState("default")
if err != nil {
@@ -187,16 +182,7 @@ func TestCloudLifecycle_CheckoutToBillingToMonitoredSystemLimits(t *testing.T) {
t.Fatalf("billing.SubscriptionState = %q, want %q", bs.SubscriptionState, tc.wantSubState)
}
- // This is the critical assertion the runtime reads to enforce
- // monitored-system caps for Cloud tenants.
- if bs.Limits[pkglicensing.MaxMonitoredSystemsLicenseGateKey] != tc.wantMonitoredSystems {
- t.Fatalf(
- "billing.Limits[%s] = %d, want %d",
- pkglicensing.MaxMonitoredSystemsLicenseGateKey,
- bs.Limits[pkglicensing.MaxMonitoredSystemsLicenseGateKey],
- tc.wantMonitoredSystems,
- )
- }
+ assertNoRetiredMonitoredSystemLimit(t, bs.Limits)
// Verify capabilities include Pro-level features.
capSet := make(map[string]struct{}, len(bs.Capabilities))
@@ -285,11 +271,11 @@ func TestCloudLifecycle_AdmissionFailureBlocksCheckoutBeforeTenantOrAccountMutat
}
}
-// TestCloudLifecycle_SubscriptionUpdateChangesLimits verifies that when a
+// TestCloudLifecycle_SubscriptionUpdateChangesPlans verifies that when a
// Cloud tenant upgrades (e.g., Starter → Power), the subscription.updated
// webhook correctly updates both the tenant record and billing state with
-// the new plan's monitored-system limits.
-func TestCloudLifecycle_SubscriptionUpdateChangesLimits(t *testing.T) {
+// the new plan while keeping retired monitored-system volume keys scrubbed.
+func TestCloudLifecycle_SubscriptionUpdateChangesPlans(t *testing.T) {
reg := newStripeTestRegistry(t)
tenantsDir := t.TempDir()
provisioner := newTestProvisioner(t, reg, tenantsDir, nil, true)
@@ -310,17 +296,15 @@ func TestCloudLifecycle_SubscriptionUpdateChangesLimits(t *testing.T) {
t.Fatalf("lookup tenant: %v (tenant=%v)", err, tenant)
}
- // Verify initial state: Starter = 10 monitored systems.
+ // Verify initial state: Starter has no monitored-system cap.
store := config.NewFileBillingStore(provisioner.tenantDataDir(tenant.ID))
bs, err := store.GetBillingState("default")
if err != nil || bs == nil {
t.Fatalf("initial GetBillingState: %v", err)
}
- if bs.Limits[pkglicensing.MaxMonitoredSystemsLicenseGateKey] != 10 {
- t.Fatalf("initial %s = %d, want 10", pkglicensing.MaxMonitoredSystemsLicenseGateKey, bs.Limits[pkglicensing.MaxMonitoredSystemsLicenseGateKey])
- }
+ assertNoRetiredMonitoredSystemLimit(t, bs.Limits)
- // Phase 2: Simulate subscription.updated → upgrade to Cloud Power (30 monitored systems).
+ // Phase 2: Simulate subscription.updated → upgrade to Cloud Power.
sub := Subscription{
ID: "sub_upgrade_test",
Customer: "cus_upgrade_test",
@@ -343,19 +327,17 @@ func TestCloudLifecycle_SubscriptionUpdateChangesLimits(t *testing.T) {
t.Fatalf("tenant.State after upgrade = %q, want %q", tenant.State, registry.TenantStateActive)
}
- // Verify billing state has new limits.
+ // Verify billing state keeps monitored-system caps retired.
bs, err = store.GetBillingState("default")
if err != nil || bs == nil {
t.Fatalf("GetBillingState after upgrade: %v", err)
}
- if bs.Limits[pkglicensing.MaxMonitoredSystemsLicenseGateKey] != 30 {
- t.Fatalf("max_monitored_systems after upgrade = %d, want 30", bs.Limits[pkglicensing.MaxMonitoredSystemsLicenseGateKey])
- }
+ assertNoRetiredMonitoredSystemLimit(t, bs.Limits)
if bs.SubscriptionState != pkglicensing.SubStateActive {
t.Fatalf("SubscriptionState after upgrade = %q, want %q", bs.SubscriptionState, pkglicensing.SubStateActive)
}
- // Phase 3: Upgrade again to Cloud Max (75 monitored systems).
+ // Phase 3: Upgrade again to Cloud Max.
sub.Metadata = map[string]string{"plan_version": "cloud_max"}
if err := provisioner.HandleSubscriptionUpdated(context.Background(), sub); err != nil {
t.Fatalf("HandleSubscriptionUpdated (upgrade to max): %v", err)
@@ -365,9 +347,7 @@ func TestCloudLifecycle_SubscriptionUpdateChangesLimits(t *testing.T) {
if err != nil || bs == nil {
t.Fatalf("GetBillingState after max upgrade: %v", err)
}
- if bs.Limits[pkglicensing.MaxMonitoredSystemsLicenseGateKey] != 75 {
- t.Fatalf("max_monitored_systems after max upgrade = %d, want 75", bs.Limits[pkglicensing.MaxMonitoredSystemsLicenseGateKey])
- }
+ assertNoRetiredMonitoredSystemLimit(t, bs.Limits)
}
// TestCloudLifecycle_CancellationRevokesCapabilities proves that
@@ -403,9 +383,7 @@ func TestCloudLifecycle_CancellationRevokesCapabilities(t *testing.T) {
if len(bs.Capabilities) == 0 {
t.Fatal("expected capabilities before cancellation, got empty")
}
- if bs.Limits[pkglicensing.MaxMonitoredSystemsLicenseGateKey] != 10 {
- t.Fatalf("max_monitored_systems before cancel = %d, want 10", bs.Limits[pkglicensing.MaxMonitoredSystemsLicenseGateKey])
- }
+ assertNoRetiredMonitoredSystemLimit(t, bs.Limits)
// Simulate subscription.deleted.
delSub := Subscription{
@@ -518,9 +496,7 @@ func TestCloudLifecycle_GracePeriodPreservesAccess(t *testing.T) {
if len(bs.Capabilities) == 0 {
t.Fatal("expected capabilities during grace, got empty")
}
- if bs.Limits[pkglicensing.MaxMonitoredSystemsLicenseGateKey] != 30 {
- t.Fatalf("max_monitored_systems during grace = %d, want 30 (Cloud Power)", bs.Limits[pkglicensing.MaxMonitoredSystemsLicenseGateKey])
- }
+ assertNoRetiredMonitoredSystemLimit(t, bs.Limits)
// Stripe account should have grace window started.
sa, err := reg.GetStripeAccountByCustomerID("cus_grace_test")
@@ -542,18 +518,17 @@ func TestCloudLifecycle_GracePeriodPreservesAccess(t *testing.T) {
// outside the control plane (e.g., directly in Stripe Dashboard).
func TestCloudLifecycle_PriceIDResolution(t *testing.T) {
tests := []struct {
- name string
- priceID string
- wantPlan string
- wantMonitoredSystems int64
+ name string
+ priceID string
+ wantPlan string
}{
- {"starter_monthly", "price_1T5kflBrHBocJIGHUqPv1dzV", "cloud_starter", 10},
- {"starter_annual", "price_1T5kfmBrHBocJIGHTS3ymKxM", "cloud_starter", 10},
- {"founding_monthly", "price_1T5kfnBrHBocJIGHATQJr79D", "cloud_founding", 10},
- {"power_monthly", "price_1T5kg2BrHBocJIGHmkoF0zXY", "cloud_power", 30},
- {"power_annual", "price_1T5kg3BrHBocJIGH2EtzKofV", "cloud_power", 30},
- {"max_monthly", "price_1T5kg4BrHBocJIGHHa8Ecqho", "cloud_max", 75},
- {"max_annual", "price_1T5kg5BrHBocJIGH5AIJ4nVc", "cloud_max", 75},
+ {"starter_monthly", "price_1T5kflBrHBocJIGHUqPv1dzV", "cloud_starter"},
+ {"starter_annual", "price_1T5kfmBrHBocJIGHTS3ymKxM", "cloud_starter"},
+ {"founding_monthly", "price_1T5kfnBrHBocJIGHATQJr79D", "cloud_founding"},
+ {"power_monthly", "price_1T5kg2BrHBocJIGHmkoF0zXY", "cloud_power"},
+ {"power_annual", "price_1T5kg3BrHBocJIGH2EtzKofV", "cloud_power"},
+ {"max_monthly", "price_1T5kg4BrHBocJIGHHa8Ecqho", "cloud_max"},
+ {"max_annual", "price_1T5kg5BrHBocJIGH5AIJ4nVc", "cloud_max"},
}
for _, tc := range tests {
@@ -615,13 +590,7 @@ func TestCloudLifecycle_PriceIDResolution(t *testing.T) {
if err != nil || bs == nil {
t.Fatalf("GetBillingState: %v", err)
}
- if bs.Limits[pkglicensing.MaxMonitoredSystemsLicenseGateKey] != tc.wantMonitoredSystems {
- t.Fatalf(
- "max_monitored_systems = %d, want %d",
- bs.Limits[pkglicensing.MaxMonitoredSystemsLicenseGateKey],
- tc.wantMonitoredSystems,
- )
- }
+ assertNoRetiredMonitoredSystemLimit(t, bs.Limits)
})
}
}
@@ -705,12 +674,13 @@ func TestCloudLifecycle_SubscriptionUpdateCanonicalizesStoredFallbackPlan(t *tes
if bs.PlanVersion != "cloud_starter" {
t.Fatalf("billing.PlanVersion = %q, want %q", bs.PlanVersion, "cloud_starter")
}
- if bs.Limits[pkglicensing.MaxMonitoredSystemsLicenseGateKey] != 10 {
- t.Fatalf(
- "billing.Limits[%s] = %d, want 10",
- pkglicensing.MaxMonitoredSystemsLicenseGateKey,
- bs.Limits[pkglicensing.MaxMonitoredSystemsLicenseGateKey],
- )
+ assertNoRetiredMonitoredSystemLimit(t, bs.Limits)
+}
+
+func assertNoRetiredMonitoredSystemLimit(t *testing.T, limits map[string]int64) {
+ t.Helper()
+ if _, ok := limits[pkglicensing.MaxMonitoredSystemsLicenseGateKey]; ok {
+ t.Fatalf("expected retired %s limit to be omitted, got %+v", pkglicensing.MaxMonitoredSystemsLicenseGateKey, limits)
}
}
diff --git a/internal/cloudcp/stripe/msp_lifecycle_integration_test.go b/internal/cloudcp/stripe/msp_lifecycle_integration_test.go
index 4dbf29c4f..640179449 100644
--- a/internal/cloudcp/stripe/msp_lifecycle_integration_test.go
+++ b/internal/cloudcp/stripe/msp_lifecycle_integration_test.go
@@ -741,22 +741,21 @@ func TestMSPLifecycle_TenantIsolation(t *testing.T) {
// TestMSPLifecycle_PlanVersionFromAccount verifies that ProvisionWorkspace
// uses the account's actual Stripe plan version instead of a hardcoded default.
-// MSP Growth accounts should get msp_growth limits (150 monitored systems), not msp_hosted_v1 (50).
+// MSP Growth accounts should keep the msp_growth plan version while monitored-system caps stay retired.
func TestMSPLifecycle_PlanVersionFromAccount(t *testing.T) {
reg := newStripeTestRegistry(t)
tenantsDir := t.TempDir()
provisioner := newTestProvisioner(t, reg, tenantsDir, nil, true)
tests := []struct {
- name string
- planVersion string
- wantMonitoredSystems int64
+ name string
+ planVersion string
}{
- {"msp_starter", "msp_starter", 50},
- {"msp_growth", "msp_growth", 150},
- {"msp_scale", "msp_scale", 400},
- {"legacy_msp_hosted_v1", "msp_hosted_v1", 50},
- {"canonicalized_cloud_alias", "cloud_v1", 10},
+ {"msp_starter", "msp_starter"},
+ {"msp_growth", "msp_growth"},
+ {"msp_scale", "msp_scale"},
+ {"legacy_msp_hosted_v1", "msp_hosted_v1"},
+ {"canonicalized_cloud_alias", "cloud_v1"},
}
for _, tc := range tests {
@@ -800,14 +799,7 @@ func TestMSPLifecycle_PlanVersionFromAccount(t *testing.T) {
if bs.PlanVersion != wantPlanVersion {
t.Fatalf("billing.PlanVersion = %q, want %q", bs.PlanVersion, wantPlanVersion)
}
- if bs.Limits[pkglicensing.MaxMonitoredSystemsLicenseGateKey] != tc.wantMonitoredSystems {
- t.Fatalf(
- "billing.Limits[%s] = %d, want %d",
- pkglicensing.MaxMonitoredSystemsLicenseGateKey,
- bs.Limits[pkglicensing.MaxMonitoredSystemsLicenseGateKey],
- tc.wantMonitoredSystems,
- )
- }
+ assertNoRetiredMonitoredSystemLimit(t, bs.Limits)
})
}
}
@@ -851,13 +843,7 @@ func TestMSPLifecycle_PlanVersionFallback(t *testing.T) {
if bs.PlanVersion != "msp_starter" {
t.Fatalf("billing.PlanVersion = %q, want %q", bs.PlanVersion, "msp_starter")
}
- if bs.Limits[pkglicensing.MaxMonitoredSystemsLicenseGateKey] != 50 {
- t.Fatalf(
- "billing.Limits[%s] = %d, want 50 (msp_starter default)",
- pkglicensing.MaxMonitoredSystemsLicenseGateKey,
- bs.Limits[pkglicensing.MaxMonitoredSystemsLicenseGateKey],
- )
- }
+ assertNoRetiredMonitoredSystemLimit(t, bs.Limits)
}
// ─── Test helpers ──────────────────────────────────────────────────────────
diff --git a/internal/cloudcp/stripe/provisioner.go b/internal/cloudcp/stripe/provisioner.go
index cd9953993..0da96976c 100644
--- a/internal/cloudcp/stripe/provisioner.go
+++ b/internal/cloudcp/stripe/provisioner.go
@@ -1038,8 +1038,8 @@ func (p *Provisioner) HandleSubscriptionUpdated(ctx context.Context, sub Subscri
planVersion := DerivePlanVersion(sub.Metadata, priceID)
// Preserve existing plan version only when the price hasn't changed
// (same subscription metadata refresh). If the price changed to an
- // unknown ID, keep the opaque fallback so LimitsForCloudPlan applies
- // fail-closed defaults rather than inheriting stale higher-tier limits.
+ // unknown ID, keep the opaque fallback so workspace-policy reconciliation
+ // does not inherit stale higher-tier metadata.
if (planVersion == "" || planVersion == "stripe" || strings.HasPrefix(planVersion, "stripe_price:")) &&
strings.TrimSpace(tenant.PlanVersion) != "" &&
(priceID == "" || priceID == strings.TrimSpace(tenant.StripePriceID)) {
diff --git a/internal/deploy/reservation.go b/internal/deploy/reservation.go
deleted file mode 100644
index eea1c7963..000000000
--- a/internal/deploy/reservation.go
+++ /dev/null
@@ -1,92 +0,0 @@
-package deploy
-
-import (
- "fmt"
- "sync"
- "time"
-)
-
-// ReservationManager tracks monitored-system capacity reservations for in-flight deployments.
-type ReservationManager struct {
- mu sync.Mutex
- reservations map[string]*Reservation
-}
-
-// Reservation represents capacity reserved for a deployment job.
-type Reservation struct {
- JobID string
- OrgID string
- Slots int
- CreatedAt time.Time
- ExpiresAt time.Time
-}
-
-// NewReservationManager creates a new reservation manager.
-func NewReservationManager() *ReservationManager {
- return &ReservationManager{
- reservations: make(map[string]*Reservation),
- }
-}
-
-// Reserve allocates monitored-system capacity for a deployment job. Returns an error if
-// the job already has a reservation.
-func (rm *ReservationManager) Reserve(jobID, orgID string, slots int, ttl time.Duration) error {
- rm.mu.Lock()
- defer rm.mu.Unlock()
-
- if _, exists := rm.reservations[jobID]; exists {
- return fmt.Errorf("reservation already exists for job %q", jobID)
- }
- if slots <= 0 {
- return fmt.Errorf("slots must be positive, got %d", slots)
- }
- if ttl <= 0 {
- return fmt.Errorf("ttl must be positive, got %v", ttl)
- }
-
- now := time.Now().UTC()
- rm.reservations[jobID] = &Reservation{
- JobID: jobID,
- OrgID: orgID,
- Slots: slots,
- CreatedAt: now,
- ExpiresAt: now.Add(ttl),
- }
- return nil
-}
-
-// Release removes the reservation for a job.
-func (rm *ReservationManager) Release(jobID string) {
- rm.mu.Lock()
- defer rm.mu.Unlock()
- delete(rm.reservations, jobID)
-}
-
-// ReservedForOrg returns the total number of reserved slots for a given org,
-// excluding expired reservations.
-func (rm *ReservationManager) ReservedForOrg(orgID string) int {
- rm.mu.Lock()
- defer rm.mu.Unlock()
-
- now := time.Now().UTC()
- total := 0
- for _, r := range rm.reservations {
- if r.OrgID == orgID && now.Before(r.ExpiresAt) {
- total += r.Slots
- }
- }
- return total
-}
-
-// CleanExpired removes all expired reservations.
-func (rm *ReservationManager) CleanExpired() {
- rm.mu.Lock()
- defer rm.mu.Unlock()
-
- now := time.Now().UTC()
- for id, r := range rm.reservations {
- if !now.Before(r.ExpiresAt) {
- delete(rm.reservations, id)
- }
- }
-}
diff --git a/internal/deploy/reservation_test.go b/internal/deploy/reservation_test.go
deleted file mode 100644
index 4779d43f1..000000000
--- a/internal/deploy/reservation_test.go
+++ /dev/null
@@ -1,133 +0,0 @@
-package deploy
-
-import (
- "testing"
- "time"
-)
-
-func TestReserveAndRelease(t *testing.T) {
- rm := NewReservationManager()
-
- if err := rm.Reserve("j1", "org-1", 3, 30*time.Minute); err != nil {
- t.Fatalf("Reserve: %v", err)
- }
- if got := rm.ReservedForOrg("org-1"); got != 3 {
- t.Errorf("expected 3 reserved, got %d", got)
- }
-
- rm.Release("j1")
- if got := rm.ReservedForOrg("org-1"); got != 0 {
- t.Errorf("expected 0 after release, got %d", got)
- }
-}
-
-func TestReserveDuplicate(t *testing.T) {
- rm := NewReservationManager()
- _ = rm.Reserve("j1", "org-1", 2, 30*time.Minute)
-
- if err := rm.Reserve("j1", "org-1", 1, 30*time.Minute); err == nil {
- t.Fatal("expected error for duplicate reservation")
- }
-}
-
-func TestReserveInvalidSlots(t *testing.T) {
- rm := NewReservationManager()
- if err := rm.Reserve("j1", "org-1", 0, 30*time.Minute); err == nil {
- t.Fatal("expected error for zero slots")
- }
- if err := rm.Reserve("j2", "org-1", -1, 30*time.Minute); err == nil {
- t.Fatal("expected error for negative slots")
- }
-}
-
-func TestMultiOrgIsolation(t *testing.T) {
- rm := NewReservationManager()
- _ = rm.Reserve("j1", "org-1", 3, 30*time.Minute)
- _ = rm.Reserve("j2", "org-2", 5, 30*time.Minute)
-
- if got := rm.ReservedForOrg("org-1"); got != 3 {
- t.Errorf("expected 3 for org-1, got %d", got)
- }
- if got := rm.ReservedForOrg("org-2"); got != 5 {
- t.Errorf("expected 5 for org-2, got %d", got)
- }
- if got := rm.ReservedForOrg("org-3"); got != 0 {
- t.Errorf("expected 0 for unknown org, got %d", got)
- }
-}
-
-func TestMultipleReservationsSameOrg(t *testing.T) {
- rm := NewReservationManager()
- _ = rm.Reserve("j1", "org-1", 2, 30*time.Minute)
- _ = rm.Reserve("j2", "org-1", 3, 30*time.Minute)
-
- if got := rm.ReservedForOrg("org-1"); got != 5 {
- t.Errorf("expected 5 total, got %d", got)
- }
-
- rm.Release("j1")
- if got := rm.ReservedForOrg("org-1"); got != 3 {
- t.Errorf("expected 3 after partial release, got %d", got)
- }
-}
-
-func TestReserveInvalidTTL(t *testing.T) {
- rm := NewReservationManager()
- if err := rm.Reserve("j1", "org-1", 3, 0); err == nil {
- t.Fatal("expected error for zero TTL")
- }
- if err := rm.Reserve("j2", "org-1", 3, -1*time.Second); err == nil {
- t.Fatal("expected error for negative TTL")
- }
-}
-
-func TestTTLExpiry(t *testing.T) {
- rm := NewReservationManager()
-
- // Directly inject an already-expired reservation to test expiry counting.
- rm.mu.Lock()
- rm.reservations["j1"] = &Reservation{
- JobID: "j1", OrgID: "org-1", Slots: 3,
- CreatedAt: time.Now().UTC().Add(-2 * time.Minute),
- ExpiresAt: time.Now().UTC().Add(-1 * time.Minute),
- }
- rm.mu.Unlock()
-
- // Expired reservations should not be counted.
- if got := rm.ReservedForOrg("org-1"); got != 0 {
- t.Errorf("expected 0 for expired reservation, got %d", got)
- }
-}
-
-func TestCleanExpired(t *testing.T) {
- rm := NewReservationManager()
-
- // Directly inject an already-expired reservation.
- rm.mu.Lock()
- rm.reservations["j1"] = &Reservation{
- JobID: "j1", OrgID: "org-1", Slots: 2,
- CreatedAt: time.Now().UTC().Add(-2 * time.Minute),
- ExpiresAt: time.Now().UTC().Add(-1 * time.Minute),
- }
- rm.mu.Unlock()
-
- _ = rm.Reserve("j2", "org-1", 3, 30*time.Minute) // still valid
-
- rm.CleanExpired()
-
- // j1 should be cleaned up, only j2 remains.
- if got := rm.ReservedForOrg("org-1"); got != 3 {
- t.Errorf("expected 3 after cleanup, got %d", got)
- }
-
- // Verify j1 was actually removed (can re-reserve that ID).
- if err := rm.Reserve("j1", "org-1", 1, 30*time.Minute); err != nil {
- t.Errorf("expected to re-reserve cleaned-up job, got: %v", err)
- }
-}
-
-func TestReleaseNonexistent(t *testing.T) {
- rm := NewReservationManager()
- // Should not panic.
- rm.Release("nonexistent")
-}
diff --git a/internal/deploy/transitions.go b/internal/deploy/transitions.go
index fcfea88d7..2194114c0 100644
--- a/internal/deploy/transitions.go
+++ b/internal/deploy/transitions.go
@@ -15,7 +15,7 @@ var validJobTransitions = map[JobStatus][]JobStatus{
// validTargetTransitions defines allowed state transitions for a Target.
var validTargetTransitions = map[TargetStatus][]TargetStatus{
- TargetPending: {TargetPreflighting, TargetSkippedAgent, TargetSkippedLicense, TargetCanceled},
+ TargetPending: {TargetPreflighting, TargetSkippedAgent, TargetCanceled},
TargetPreflighting: {TargetReady, TargetFailedRetryable, TargetFailedPermanent, TargetCanceled},
TargetReady: {TargetInstalling, TargetCanceled},
TargetInstalling: {TargetEnrolling, TargetFailedRetryable, TargetFailedPermanent, TargetCanceled},
@@ -72,7 +72,7 @@ func (j *Job) DeriveStatus(targets []Target) JobStatus {
case TargetSucceeded:
succeeded++
terminal++
- case TargetFailedPermanent, TargetSkippedAgent, TargetSkippedLicense, TargetCanceled:
+ case TargetFailedPermanent, TargetSkippedAgent, TargetCanceled:
terminal++
case TargetPending, TargetPreflighting, TargetReady, TargetInstalling,
TargetEnrolling, TargetVerifying, TargetFailedRetryable:
diff --git a/internal/deploy/transitions_test.go b/internal/deploy/transitions_test.go
index 569339bbe..e0339bfe1 100644
--- a/internal/deploy/transitions_test.go
+++ b/internal/deploy/transitions_test.go
@@ -88,7 +88,6 @@ func TestTargetTransitionTo_Valid(t *testing.T) {
}{
{TargetPending, TargetPreflighting},
{TargetPending, TargetSkippedAgent},
- {TargetPending, TargetSkippedLicense},
{TargetPending, TargetCanceled},
{TargetPreflighting, TargetReady},
{TargetPreflighting, TargetFailedRetryable},
@@ -131,7 +130,6 @@ func TestTargetTransitionTo_Invalid(t *testing.T) {
{TargetSucceeded, TargetPending}, // terminal
{TargetFailedPermanent, TargetPending}, // terminal
{TargetSkippedAgent, TargetPending}, // terminal
- {TargetSkippedLicense, TargetPending}, // terminal
{TargetCanceled, TargetPending}, // terminal
{TargetPending, TargetInstalling}, // skip preflighting
{TargetPending, TargetSucceeded}, // skip to end
@@ -181,7 +179,7 @@ func TestDeriveStatus(t *testing.T) {
},
{
name: "all skipped",
- targets: targets(TargetSkippedAgent, TargetSkippedLicense),
+ targets: targets(TargetSkippedAgent, TargetSkippedAgent),
expected: JobFailed,
},
{
diff --git a/internal/deploy/types.go b/internal/deploy/types.go
index 8cc34e90a..2dde6dcb5 100644
--- a/internal/deploy/types.go
+++ b/internal/deploy/types.go
@@ -30,7 +30,6 @@ const (
TargetFailedRetryable TargetStatus = "failed_retryable"
TargetFailedPermanent TargetStatus = "failed_permanent"
TargetSkippedAgent TargetStatus = "skipped_already_agent"
- TargetSkippedLicense TargetStatus = "skipped_license"
TargetCanceled TargetStatus = "canceled"
)
diff --git a/internal/license/coverage_additional_test.go b/internal/license/coverage_additional_test.go
index a9f996e9e..2de347f09 100644
--- a/internal/license/coverage_additional_test.go
+++ b/internal/license/coverage_additional_test.go
@@ -121,9 +121,6 @@ func TestStatusHostedEvaluatorClampsNegativeLimits(t *testing.T) {
if !status.Valid {
t.Fatalf("Status().Valid = %v, want true", status.Valid)
}
- if status.MaxMonitoredSystems != 0 {
- t.Fatalf("Status().MaxMonitoredSystems = %d, want 0", status.MaxMonitoredSystems)
- }
if status.MaxGuests != 0 {
t.Fatalf("Status().MaxGuests = %d, want 0", status.MaxGuests)
}
diff --git a/internal/license/entitlements/database_source_test.go b/internal/license/entitlements/database_source_test.go
index 3ace77a65..e5ebfa360 100644
--- a/internal/license/entitlements/database_source_test.go
+++ b/internal/license/entitlements/database_source_test.go
@@ -53,7 +53,7 @@ func TestDatabaseSourceHappyPath(t *testing.T) {
store := &mockBillingStore{
state: &BillingState{
Capabilities: []string{"rbac", "relay"},
- Limits: map[string]int64{"max_monitored_systems": 50},
+ Limits: map[string]int64{"max_monitored_systems": 50, "max_guests": 75},
MetersEnabled: []string{"active_agents"},
PlanVersion: "pro-v2",
SubscriptionState: SubStateActive,
@@ -65,8 +65,8 @@ func TestDatabaseSourceHappyPath(t *testing.T) {
if got := source.Capabilities(); !reflect.DeepEqual(got, []string{"rbac", "relay"}) {
t.Fatalf("expected capabilities %v, got %v", []string{"rbac", "relay"}, got)
}
- if got := source.Limits(); !reflect.DeepEqual(got, map[string]int64{"max_monitored_systems": 50}) {
- t.Fatalf("expected limits %v, got %v", map[string]int64{"max_monitored_systems": 50}, got)
+ if got := source.Limits(); !reflect.DeepEqual(got, map[string]int64{"max_guests": 75}) {
+ t.Fatalf("expected limits %v, got %v", map[string]int64{"max_guests": 75}, got)
}
if got := source.MetersEnabled(); !reflect.DeepEqual(got, []string{"active_agents"}) {
t.Fatalf("expected meters %v, got %v", []string{"active_agents"}, got)
diff --git a/internal/license/entitlements/evaluator_test.go b/internal/license/entitlements/evaluator_test.go
index 20c283065..b155ab493 100644
--- a/internal/license/entitlements/evaluator_test.go
+++ b/internal/license/entitlements/evaluator_test.go
@@ -279,8 +279,10 @@ func TestEvaluatorMeterEnabled(t *testing.T) {
func TestTokenSourceLegacyDerivation(t *testing.T) {
t.Run("legacy self-hosted claims keep capabilities but scrub monitored-system caps", func(t *testing.T) {
claims := &license.Claims{
- Tier: license.TierPro,
- MaxMonitoredSystems: 25,
+ Tier: license.TierPro,
+ Limits: map[string]int64{
+ "max_monitored_systems": 25,
+ },
}
source := NewTokenSource(claims)
diff --git a/internal/license/features.go b/internal/license/features.go
index 927ea79a1..0ea92f9ac 100644
--- a/internal/license/features.go
+++ b/internal/license/features.go
@@ -46,9 +46,6 @@ const (
// TierFeatures maps each tier to its included features.
var TierFeatures = licensing.TierFeatures
-// TierMonitoredSystemLimits defines the maximum agent count per tier.
-var TierMonitoredSystemLimits = licensing.TierMonitoredSystemLimits
-
// TierHistoryDays defines the maximum metrics history retention per tier.
var TierHistoryDays = licensing.TierHistoryDays
diff --git a/internal/license/license_test.go b/internal/license/license_test.go
index c2f5f39fc..f3823c773 100644
--- a/internal/license/license_test.go
+++ b/internal/license/license_test.go
@@ -123,8 +123,7 @@ func TestServiceStatus_UsesEffectiveClaimsCapabilitiesAndScrubsSelfHostedCommerc
"max_monitored_systems": 99,
"max_guests": 7,
},
- MaxMonitoredSystems: 1,
- MaxGuests: 2,
+ MaxGuests: 2,
},
})
@@ -139,9 +138,6 @@ func TestServiceStatus_UsesEffectiveClaimsCapabilitiesAndScrubsSelfHostedCommerc
if !reflect.DeepEqual(status.Features, []string{FeatureAIAutoFix}) {
t.Fatalf("Status().Features=%v, want %v", status.Features, []string{FeatureAIAutoFix})
}
- if status.MaxMonitoredSystems != 0 {
- t.Fatalf("Status().MaxMonitoredSystems=%d, want 0 for uncapped self-hosted Pro", status.MaxMonitoredSystems)
- }
if status.MaxGuests != 0 {
t.Fatalf("Status().MaxGuests=%d, want 0 for uncapped self-hosted Pro", status.MaxGuests)
}
@@ -976,14 +972,14 @@ func TestClaimsEffectiveCapabilities(t *testing.T) {
func TestClaimsEffectiveLimits(t *testing.T) {
t.Run("explicit limits", func(t *testing.T) {
claims := Claims{
- MaxMonitoredSystems: 25,
Limits: map[string]int64{
"max_monitored_systems": 50,
+ "max_guests": 75,
},
}
got := claims.EffectiveLimits()
- want := map[string]int64{"max_monitored_systems": 50}
+ want := map[string]int64{"max_guests": 75}
if !reflect.DeepEqual(got, want) {
t.Fatalf("EffectiveLimits() = %v, want %v", got, want)
}
@@ -991,14 +987,12 @@ func TestClaimsEffectiveLimits(t *testing.T) {
t.Run("derived from fields", func(t *testing.T) {
claims := Claims{
- MaxMonitoredSystems: 25,
- MaxGuests: 100,
+ MaxGuests: 100,
}
got := claims.EffectiveLimits()
want := map[string]int64{
- "max_monitored_systems": 25,
- "max_guests": 100,
+ "max_guests": 100,
}
if !reflect.DeepEqual(got, want) {
t.Fatalf("EffectiveLimits() = %v, want %v", got, want)
@@ -1007,8 +1001,7 @@ func TestClaimsEffectiveLimits(t *testing.T) {
t.Run("zero fields omitted", func(t *testing.T) {
claims := Claims{
- MaxMonitoredSystems: 0,
- Limits: nil,
+ Limits: nil,
}
got := claims.EffectiveLimits()
@@ -1021,15 +1014,14 @@ func TestClaimsEffectiveLimits(t *testing.T) {
func TestClaimsJSONRoundtrip(t *testing.T) {
t.Run("new fields set", func(t *testing.T) {
original := Claims{
- LicenseID: "license_roundtrip",
- Email: "roundtrip@example.com",
- Tier: TierPro,
- IssuedAt: 1700000000,
- ExpiresAt: 1800000000,
- Features: []string{"legacy_feature"},
- MaxMonitoredSystems: 10,
- MaxGuests: 20,
- Capabilities: []string{"cap_a", "cap_b"},
+ LicenseID: "license_roundtrip",
+ Email: "roundtrip@example.com",
+ Tier: TierPro,
+ IssuedAt: 1700000000,
+ ExpiresAt: 1800000000,
+ Features: []string{"legacy_feature"},
+ MaxGuests: 20,
+ Capabilities: []string{"cap_a", "cap_b"},
Limits: map[string]int64{
"max_monitored_systems": 50,
"max_guests": 100,
@@ -1124,8 +1116,7 @@ func TestDeriveEntitlements(t *testing.T) {
sort.Strings(wantCapabilities)
wantLimits := map[string]int64{
- "max_monitored_systems": 25,
- "max_guests": 100,
+ "max_guests": 100,
}
if !reflect.DeepEqual(capabilities, wantCapabilities) {
@@ -1413,9 +1404,6 @@ func TestEvaluatorMatrix(t *testing.T) {
}
// Hosted path unions free-tier baseline capabilities with evaluator-provided capabilities.
assertFeatureSetEq(t, status.Features, []string{FeatureUpdateAlerts, FeatureSSO, FeatureAdvancedSSO, FeatureAIPatrol, FeatureAIAutoFix})
- if status.MaxMonitoredSystems != 42 {
- t.Fatalf("Status().MaxMonitoredSystems=%d, want %d", status.MaxMonitoredSystems, 42)
- }
if status.MaxGuests != 13 {
t.Fatalf("Status().MaxGuests=%d, want %d", status.MaxGuests, 13)
}
@@ -1490,8 +1478,8 @@ func TestEvaluatorMatrix(t *testing.T) {
if !reflect.DeepEqual(status.Features, TierFeatures[TierFree]) {
t.Fatalf("Status().Features=%v, want %v", status.Features, TierFeatures[TierFree])
}
- if status.MaxMonitoredSystems != TierMonitoredSystemLimits[TierFree] || status.MaxGuests != 0 {
- t.Fatalf("expected free-tier fallback limits for expired subscription, got MaxMonitoredSystems=%d MaxGuests=%d", status.MaxMonitoredSystems, status.MaxGuests)
+ if status.MaxGuests != 0 {
+ t.Fatalf("expected free-tier fallback limits for expired subscription, got MaxGuests=%d", status.MaxGuests)
}
})
diff --git a/internal/monitoring/canonical_guardrails_test.go b/internal/monitoring/canonical_guardrails_test.go
index 32c1288e8..0e364dbb1 100644
--- a/internal/monitoring/canonical_guardrails_test.go
+++ b/internal/monitoring/canonical_guardrails_test.go
@@ -190,6 +190,26 @@ func TestMonitoredSystemUsageReadinessGuardrailsRemainCanonical(t *testing.T) {
}
}
+func TestMonitoredSystemUsageStaysInventoryOnly(t *testing.T) {
+ data, err := os.ReadFile("monitored_system_usage.go")
+ if err != nil {
+ t.Fatalf("failed to read monitored_system_usage.go: %v", err)
+ }
+ source := string(data)
+
+ for _, forbidden := range []string{
+ "max_monitored_systems",
+ "license_limit",
+ "would_exceed_limit",
+ "monitored_system_capacity",
+ "admission",
+ } {
+ if strings.Contains(source, forbidden) {
+ t.Fatalf("monitored_system_usage.go must not contain retired cap/admission token %q", forbidden)
+ }
+ }
+}
+
func TestDockerHostIdentityUsesCanonicalHostnameEquivalence(t *testing.T) {
data, err := os.ReadFile("docker_host_identity.go")
if err != nil {
diff --git a/internal/monitoring/monitor.go b/internal/monitoring/monitor.go
index ed22c6dd6..4520ade19 100644
--- a/internal/monitoring/monitor.go
+++ b/internal/monitoring/monitor.go
@@ -150,7 +150,7 @@ type MonitorSupplementalChangesProvider interface {
// MonitorSupplementalInventoryReadinessProvider optionally reports when a
// supplemental provider's current org-scoped inventory is settled enough to be
-// consumed by billing and monitored-system admission boundaries.
+// consumed by monitored-system grouping and support boundaries.
//
// Providers that suppress snapshot-owned sources must implement this contract
// so the monitor can fail closed until the canonical store has been rebuilt
diff --git a/internal/monitoring/monitored_system_usage.go b/internal/monitoring/monitored_system_usage.go
index 81fafae77..3a099aa0d 100644
--- a/internal/monitoring/monitored_system_usage.go
+++ b/internal/monitoring/monitored_system_usage.go
@@ -8,8 +8,8 @@ import (
)
// MonitoredSystemUsageSnapshot describes whether the monitor can currently
-// supply a canonical monitored-system count that is safe for billing and
-// admission enforcement to consume.
+// supply a canonical monitored-system count that is safe for settings and
+// support context to consume.
type MonitoredSystemUsageSnapshot struct {
Count int
ReadState unifiedresources.ReadState
@@ -24,7 +24,7 @@ const (
)
// MonitoredSystemUsage returns the canonical monitored-system count only when
-// the current unified view is settled enough for billing boundaries. When
+// the current unified view is settled enough for settings/support context. When
// supplemental provider-owned sources are still settling, the result fails
// closed with Available=false.
func (m *Monitor) MonitoredSystemUsage() MonitoredSystemUsageSnapshot {
diff --git a/internal/monitoring/truenas_poller.go b/internal/monitoring/truenas_poller.go
index 4974beb06..f2d708cfd 100644
--- a/internal/monitoring/truenas_poller.go
+++ b/internal/monitoring/truenas_poller.go
@@ -1021,7 +1021,7 @@ func (p *TrueNASPoller) SnapshotOwnedSourcesForOrg(string) []unifiedresources.Da
// SupplementalInventoryReadyAt reports when the current org-scoped TrueNAS
// inventory has reached a settled initial baseline that has to be reflected in
-// the canonical monitor store before billing can consume monitored-system
+// the canonical monitor store before support surfaces consume monitored-system
// counts.
func (p *TrueNASPoller) SupplementalInventoryReadyAt(_ *Monitor, orgID string) (time.Time, bool) {
if p == nil {
diff --git a/internal/monitoring/truenas_poller_test.go b/internal/monitoring/truenas_poller_test.go
index ca33e39e9..435b7f4ab 100644
--- a/internal/monitoring/truenas_poller_test.go
+++ b/internal/monitoring/truenas_poller_test.go
@@ -8,6 +8,7 @@ import (
"net/http"
"net/http/httptest"
"net/url"
+ "os"
"strconv"
"strings"
"sync/atomic"
@@ -86,6 +87,28 @@ func TestTrueNASPollerFeatureFlagGate(t *testing.T) {
poller.Stop()
}
+func TestTrueNASPollerStaysMonitoringOnly(t *testing.T) {
+ source, err := os.ReadFile("truenas_poller.go")
+ if err != nil {
+ t.Fatalf("ReadFile() error = %v", err)
+ }
+
+ for _, retired := range []string{
+ "max_monitored_systems",
+ "plan_limit",
+ "would_exceed_limit",
+ "grandfather",
+ "admission",
+ "billing",
+ "capacity",
+ "limit",
+ } {
+ if strings.Contains(string(source), retired) {
+ t.Fatalf("TrueNAS poller must stay inventory-only and must not reference retired monitor-count cap token %q", retired)
+ }
+ }
+}
+
func TestTrueNASPollerEnableDisableCycle(t *testing.T) {
previous := truenas.IsFeatureEnabled()
truenas.SetFeatureEnabled(true)
diff --git a/internal/monitoring/vmware_poller.go b/internal/monitoring/vmware_poller.go
index adad3e17c..5fd4edab5 100644
--- a/internal/monitoring/vmware_poller.go
+++ b/internal/monitoring/vmware_poller.go
@@ -583,7 +583,7 @@ func (p *VMwarePoller) SnapshotOwnedSourcesForOrg(string) []unifiedresources.Dat
// SupplementalInventoryReadyAt reports when the current org-scoped VMware
// inventory has reached a settled initial baseline that has to be reflected in
-// the canonical monitor store before billing can consume monitored-system
+// the canonical monitor store before support surfaces consume monitored-system
// counts.
func (p *VMwarePoller) SupplementalInventoryReadyAt(_ *Monitor, orgID string) (time.Time, bool) {
if p == nil {
diff --git a/pkg/extensions/monitored_system_admission.go b/pkg/extensions/monitored_system_admission.go
deleted file mode 100644
index b6a8c998d..000000000
--- a/pkg/extensions/monitored_system_admission.go
+++ /dev/null
@@ -1,33 +0,0 @@
-package extensions
-
-import "context"
-
-// MonitoredSystemAdmissionInput captures the public counted-system admission
-// state computed by the OSS runtime before any private commercial policy hook
-// is consulted.
-type MonitoredSystemAdmissionInput struct {
- Current int
- Additional int
- Limit int
- UsageAvailable bool
- UsageUnavailableReason string
- CandidateCountsTowardCap bool
-}
-
-// MonitoredSystemAdmissionDecision captures the commercial admission outcome
-// that a private build may return. This intentionally stays scoped to backend
-// admission semantics rather than customer-facing billing/posture messaging.
-type MonitoredSystemAdmissionDecision struct {
- Current int
- Additional int
- Limit int
- UsageAvailable bool
- UsageUnavailableReason string
- Exceeded bool
-}
-
-// ResolveMonitoredSystemAdmissionPolicyFunc allows private builds to own the
-// commercial monitored-system admission decision without importing internal API
-// packages. The public runtime remains the source of truth for counted-system
-// projection and may call this hook in a later migration slice.
-type ResolveMonitoredSystemAdmissionPolicyFunc func(context.Context, MonitoredSystemAdmissionInput) MonitoredSystemAdmissionDecision
diff --git a/pkg/licensing/activation_types.go b/pkg/licensing/activation_types.go
index 6fd42a6a1..2810dbfcb 100644
--- a/pkg/licensing/activation_types.go
+++ b/pkg/licensing/activation_types.go
@@ -37,64 +37,14 @@ type ActivationContinuity struct {
// LegacyMigration marks installations activated from a supported v5 license
// exchange rather than a native v6 activation key.
LegacyMigration bool `json:"legacy_migration,omitempty"`
-
- // GrandfatheredMaxMonitoredSystems stores the deduped monitored-system floor
- // captured for a migrated v5 installation when its existing estate exceeds
- // the exchanged plan limit.
- GrandfatheredMaxMonitoredSystems int `json:"grandfathered_max_monitored_systems,omitempty"`
-
- // GrandfatheredMonitoredSystemsCapturedAt marks that the migration floor was
- // resolved exactly once from canonical runtime usage, even when no override
- // was needed because the observed estate was already within the exchanged
- // plan limit.
- GrandfatheredMonitoredSystemsCapturedAt int64 `json:"grandfathered_monitored_systems_captured_at,omitempty"`
}
func normalizeActivationContinuity(continuity ActivationContinuity) ActivationContinuity {
- if continuity.GrandfatheredMaxMonitoredSystems < 0 {
- continuity.GrandfatheredMaxMonitoredSystems = 0
- }
- if continuity.GrandfatheredMonitoredSystemsCapturedAt < 0 {
- continuity.GrandfatheredMonitoredSystemsCapturedAt = 0
- }
return continuity
}
func (c ActivationContinuity) needsLegacyMonitoredSystemCapture() bool {
- c = normalizeActivationContinuity(c)
- return c.LegacyMigration && c.GrandfatheredMonitoredSystemsCapturedAt == 0
-}
-
-func applyActivationContinuityToClaims(claims *Claims, continuity ActivationContinuity) {
- if claims == nil {
- return
- }
-
- continuity = normalizeActivationContinuity(continuity)
- if continuity.GrandfatheredMaxMonitoredSystems <= 0 {
- return
- }
- if claims.shouldScrubLegacyCommercialCaps() {
- return
- }
-
- currentLimit := int64(0)
- if existing, ok := claims.EffectiveLimits()[MaxMonitoredSystemsLicenseGateKey]; ok {
- currentLimit = existing
- }
- if currentLimit >= int64(continuity.GrandfatheredMaxMonitoredSystems) {
- return
- }
-
- if claims.Limits == nil {
- claims.Limits = map[string]int64{}
- } else {
- claims.Limits = NormalizeMonitoredSystemLimits(claims.Limits)
- }
- claims.Limits[MaxMonitoredSystemsLicenseGateKey] = int64(continuity.GrandfatheredMaxMonitoredSystems)
- if claims.MaxMonitoredSystems < continuity.GrandfatheredMaxMonitoredSystems {
- claims.MaxMonitoredSystems = continuity.GrandfatheredMaxMonitoredSystems
- }
+ return false
}
func grantClaimsUseUncappedCoreMonitoring(gc *GrantClaims) bool {
@@ -108,22 +58,21 @@ func grantClaimsUseUncappedCoreMonitoring(gc *GrantClaims) bool {
// GrantClaims are the claims parsed from a relay grant JWT payload.
// The grant is a short-lived JWT (72h TTL) issued by the license server.
type GrantClaims struct {
- Issuer string `json:"iss"`
- Audience string `json:"aud"`
- LicenseID string `json:"lid"`
- InstallationID string `json:"iid"`
- LicenseVersion int64 `json:"lv"`
- State string `json:"st"` // active|past_due|grace
- Tier string `json:"tier"` // matches Tier constants: "relay", "pro", "pro_plus", etc.
- PlanKey string `json:"plan"`
- Features []string `json:"feat"`
- MaxMonitoredSystems int `json:"max_monitored_systems"`
- MaxGuests int `json:"max_guests"`
- IssuedAt int64 `json:"iat"`
- ExpiresAt int64 `json:"exp"`
- GraceUntil int64 `json:"grace_until"`
- JTI string `json:"jti"` // unique grant ID
- Email string `json:"email"` // license owner email
+ Issuer string `json:"iss"`
+ Audience string `json:"aud"`
+ LicenseID string `json:"lid"`
+ InstallationID string `json:"iid"`
+ LicenseVersion int64 `json:"lv"`
+ State string `json:"st"` // active|past_due|grace
+ Tier string `json:"tier"` // matches Tier constants: "relay", "pro", "pro_plus", etc.
+ PlanKey string `json:"plan"`
+ Features []string `json:"feat"`
+ MaxGuests int `json:"max_guests"`
+ IssuedAt int64 `json:"iat"`
+ ExpiresAt int64 `json:"exp"`
+ GraceUntil int64 `json:"grace_until"`
+ JTI string `json:"jti"` // unique grant ID
+ Email string `json:"email"` // license owner email
}
func (g *GrantClaims) UnmarshalJSON(data []byte) error {
@@ -132,19 +81,16 @@ func (g *GrantClaims) UnmarshalJSON(data []byte) error {
return err
}
- if legacy, ok, err := decodeLegacyV5MonitoredSystemLimitFromJSON(data); err == nil && ok {
- g.MaxMonitoredSystems = legacy
- }
return nil
}
-// grantClaimsToClaims maps grant claims to the existing Claims struct
-// so that all feature gating and monitored-system limits work unchanged.
+// grantClaimsToClaims maps grant claims to the existing Claims struct so that
+// feature gating works unchanged.
func grantClaimsToClaims(gc *GrantClaims) Claims {
return grantClaimsToClaimsWithContinuity(gc, ActivationContinuity{})
}
-func grantClaimsToClaimsWithContinuity(gc *GrantClaims, continuity ActivationContinuity) Claims {
+func grantClaimsToClaimsWithContinuity(gc *GrantClaims, _ ActivationContinuity) Claims {
c := Claims{
LicenseID: gc.LicenseID,
Email: gc.Email,
@@ -152,7 +98,6 @@ func grantClaimsToClaimsWithContinuity(gc *GrantClaims, continuity ActivationCon
IssuedAt: gc.IssuedAt,
ExpiresAt: gc.ExpiresAt,
Features: gc.Features,
- MaxMonitoredSystems: gc.MaxMonitoredSystems,
MaxGuests: gc.MaxGuests,
PlanVersion: gc.PlanKey,
CoreMonitoringUncapped: grantClaimsUseUncappedCoreMonitoring(gc),
@@ -171,8 +116,6 @@ func grantClaimsToClaimsWithContinuity(gc *GrantClaims, continuity ActivationCon
c.SubState = SubStateSuspended
}
- applyActivationContinuityToClaims(&c, continuity)
-
return c
}
@@ -271,13 +214,12 @@ type ActivateInstallationResponse struct {
// ActivateResponseLicense is the license portion of the activation response.
type ActivateResponseLicense struct {
- LicenseID string `json:"license_id"`
- State string `json:"state"`
- Tier string `json:"tier"`
- MaxMonitoredSystems int `json:"max_monitored_systems"`
- MaxGuests int `json:"max_guests"`
- Features []string `json:"features"`
- LicenseVersion int64 `json:"license_version"`
+ LicenseID string `json:"license_id"`
+ State string `json:"state"`
+ Tier string `json:"tier"`
+ MaxGuests int `json:"max_guests"`
+ Features []string `json:"features"`
+ LicenseVersion int64 `json:"license_version"`
}
func (l *ActivateResponseLicense) UnmarshalJSON(data []byte) error {
@@ -286,9 +228,6 @@ func (l *ActivateResponseLicense) UnmarshalJSON(data []byte) error {
return err
}
- if legacy, ok, err := decodeLegacyV5MonitoredSystemLimitFromJSON(data); err == nil && ok {
- l.MaxMonitoredSystems = legacy
- }
return nil
}
diff --git a/pkg/licensing/activation_types_test.go b/pkg/licensing/activation_types_test.go
index b14fe3199..526089724 100644
--- a/pkg/licensing/activation_types_test.go
+++ b/pkg/licensing/activation_types_test.go
@@ -8,37 +8,34 @@ import (
func TestGrantClaimsToClaims(t *testing.T) {
tests := []struct {
- name string
- gc *GrantClaims
- wantTier Tier
- wantSubState SubscriptionState
- wantLicenseID string
- wantEmail string
- wantFeatures []string
- wantMaxMonitoredSystems int
- wantMaxGuests int
+ name string
+ gc *GrantClaims
+ wantTier Tier
+ wantSubState SubscriptionState
+ wantLicenseID string
+ wantEmail string
+ wantFeatures []string
+ wantMaxGuests int
}{
{
name: "active state with email",
gc: &GrantClaims{
- LicenseID: "lic_123",
- InstallationID: "inst_abc",
- State: "active",
- Tier: "pro",
- Email: "user@example.com",
- Features: []string{"ai_patrol", "relay"},
- MaxMonitoredSystems: 10,
- MaxGuests: 5,
- IssuedAt: time.Now().Unix(),
- ExpiresAt: time.Now().Add(72 * time.Hour).Unix(),
+ LicenseID: "lic_123",
+ InstallationID: "inst_abc",
+ State: "active",
+ Tier: "pro",
+ Email: "user@example.com",
+ Features: []string{"ai_patrol", "relay"},
+ MaxGuests: 5,
+ IssuedAt: time.Now().Unix(),
+ ExpiresAt: time.Now().Add(72 * time.Hour).Unix(),
},
- wantTier: TierPro,
- wantSubState: SubStateActive,
- wantLicenseID: "lic_123",
- wantEmail: "user@example.com",
- wantFeatures: []string{"ai_patrol", "relay"},
- wantMaxMonitoredSystems: 10,
- wantMaxGuests: 5,
+ wantTier: TierPro,
+ wantSubState: SubStateActive,
+ wantLicenseID: "lic_123",
+ wantEmail: "user@example.com",
+ wantFeatures: []string{"ai_patrol", "relay"},
+ wantMaxGuests: 5,
},
{
name: "past_due maps to grace",
@@ -118,9 +115,6 @@ func TestGrantClaimsToClaims(t *testing.T) {
}
}
}
- if c.MaxMonitoredSystems != tt.wantMaxMonitoredSystems {
- t.Errorf("MaxMonitoredSystems = %d, want %d", c.MaxMonitoredSystems, tt.wantMaxMonitoredSystems)
- }
if c.MaxGuests != tt.wantMaxGuests {
t.Errorf("MaxGuests = %d, want %d", c.MaxGuests, tt.wantMaxGuests)
}
@@ -131,13 +125,12 @@ func TestGrantClaimsToClaims(t *testing.T) {
func TestGrantClaimsToLicense(t *testing.T) {
t.Run("basic license from grant", func(t *testing.T) {
gc := &GrantClaims{
- LicenseID: "lic_test",
- State: "active",
- Tier: "pro",
- Features: []string{"relay", "ai_patrol"},
- MaxMonitoredSystems: 25,
- IssuedAt: time.Now().Unix(),
- ExpiresAt: time.Now().Add(72 * time.Hour).Unix(),
+ LicenseID: "lic_test",
+ State: "active",
+ Tier: "pro",
+ Features: []string{"relay", "ai_patrol"},
+ IssuedAt: time.Now().Unix(),
+ ExpiresAt: time.Now().Add(72 * time.Hour).Unix(),
}
lic := grantClaimsToLicense(gc, "fake.jwt.token")
@@ -150,9 +143,6 @@ func TestGrantClaimsToLicense(t *testing.T) {
if lic.Claims.Tier != TierPro {
t.Errorf("Tier = %q, want %q", lic.Claims.Tier, TierPro)
}
- if lic.Claims.MaxMonitoredSystems != 25 {
- t.Errorf("MaxMonitoredSystems = %d, want 25", lic.Claims.MaxMonitoredSystems)
- }
if lic.GracePeriodEnd != nil {
t.Error("GracePeriodEnd should be nil when no grace_until")
}
@@ -179,28 +169,22 @@ func TestGrantClaimsToLicense(t *testing.T) {
})
}
-func TestGrantClaimsToClaimsWithContinuityDoesNotWriteRawFloorForUncappedSelfHostedPro(t *testing.T) {
+func TestGrantClaimsToClaimsWithContinuityDoesNotSurfaceRetiredMonitoredSystemLimit(t *testing.T) {
gc := &GrantClaims{
- LicenseID: "lic_floor",
- State: "active",
- Tier: "pro",
- MaxMonitoredSystems: 0,
- IssuedAt: time.Now().Unix(),
- ExpiresAt: time.Now().Add(72 * time.Hour).Unix(),
+ LicenseID: "lic_floor",
+ State: "active",
+ Tier: "pro",
+ IssuedAt: time.Now().Unix(),
+ ExpiresAt: time.Now().Add(72 * time.Hour).Unix(),
}
claims := grantClaimsToClaimsWithContinuity(gc, ActivationContinuity{
- LegacyMigration: true,
- GrandfatheredMaxMonitoredSystems: 1,
- GrandfatheredMonitoredSystemsCapturedAt: time.Now().Unix(),
+ LegacyMigration: true,
})
if !claims.CoreMonitoringUncapped {
t.Fatal("expected grant-backed self-hosted continuity claims to carry the uncapped core monitoring marker")
}
- if claims.MaxMonitoredSystems != 0 {
- t.Fatalf("MaxMonitoredSystems = %d, want 0 for uncapped self-hosted Pro", claims.MaxMonitoredSystems)
- }
if _, ok := claims.Limits[MaxMonitoredSystemsLicenseGateKey]; ok {
t.Fatalf("Limits[%q] present, want absent for uncapped self-hosted Pro", MaxMonitoredSystemsLicenseGateKey)
}
@@ -209,81 +193,21 @@ func TestGrantClaimsToClaimsWithContinuityDoesNotWriteRawFloorForUncappedSelfHos
}
}
-func TestGrantClaimsToClaimsWithContinuityAppliesGrandfatherFloorToCappedGrant(t *testing.T) {
- gc := &GrantClaims{
- LicenseID: "lic_floor_capped",
- State: "active",
- Tier: "legacy_capped",
- MaxMonitoredSystems: 10,
- IssuedAt: time.Now().Unix(),
- ExpiresAt: time.Now().Add(72 * time.Hour).Unix(),
- }
-
- claims := grantClaimsToClaimsWithContinuity(gc, ActivationContinuity{
- LegacyMigration: true,
- GrandfatheredMaxMonitoredSystems: 23,
- GrandfatheredMonitoredSystemsCapturedAt: time.Now().Unix(),
- })
-
- if claims.CoreMonitoringUncapped {
- t.Fatal("did not expect capped continuity claims to carry the uncapped core monitoring marker")
- }
- if claims.MaxMonitoredSystems != 23 {
- t.Fatalf("MaxMonitoredSystems = %d, want 23", claims.MaxMonitoredSystems)
- }
- if got := claims.EffectiveLimits()[MaxMonitoredSystemsLicenseGateKey]; got != 23 {
- t.Fatalf("EffectiveLimits()[max_monitored_systems] = %d, want 23", got)
- }
-}
-
-func TestGrantClaimsToClaimsWithContinuityDoesNotLowerGrantLimit(t *testing.T) {
- gc := &GrantClaims{
- LicenseID: "lic_noop",
- State: "active",
- Tier: "pro",
- MaxMonitoredSystems: 15,
- IssuedAt: time.Now().Unix(),
- ExpiresAt: time.Now().Add(72 * time.Hour).Unix(),
- }
-
- claims := grantClaimsToClaimsWithContinuity(gc, ActivationContinuity{
- LegacyMigration: true,
- GrandfatheredMaxMonitoredSystems: 10,
- GrandfatheredMonitoredSystemsCapturedAt: time.Now().Unix(),
- })
-
- // Floor of 10 is below grant's 15, so the raw field is left at 15. Either
- // way, self-hosted Pro is uncapped so EffectiveLimits does not surface a cap.
- if !claims.CoreMonitoringUncapped {
- t.Fatal("expected grant-backed self-hosted continuity claims to carry the uncapped core monitoring marker")
- }
- if claims.MaxMonitoredSystems != 15 {
- t.Fatalf("MaxMonitoredSystems = %d, want 15", claims.MaxMonitoredSystems)
- }
- if got, ok := claims.EffectiveLimits()[MaxMonitoredSystemsLicenseGateKey]; ok {
- t.Fatalf("EffectiveLimits()[max_monitored_systems] = %d present, want absent (uncapped self-hosted)", got)
- }
-}
-
func TestGrantClaimsToClaimsCanonicalizesCloudPlanAtEntitlementBoundary(t *testing.T) {
gc := &GrantClaims{
- LicenseID: "lic_cloud",
- State: "active",
- Tier: string(TierCloud),
- PlanKey: "cloud_v1",
- MaxMonitoredSystems: 999,
- IssuedAt: time.Now().Unix(),
- ExpiresAt: time.Now().Add(72 * time.Hour).Unix(),
- GraceUntil: 0,
+ LicenseID: "lic_cloud",
+ State: "active",
+ Tier: string(TierCloud),
+ PlanKey: "cloud_v1",
+ IssuedAt: time.Now().Unix(),
+ ExpiresAt: time.Now().Add(72 * time.Hour).Unix(),
+ GraceUntil: 0,
}
claims := grantClaimsToClaims(gc)
if got := claims.EntitlementPlanVersion(); got != "cloud_starter" {
t.Fatalf("EntitlementPlanVersion()=%q, want %q", got, "cloud_starter")
}
- if got := claims.EffectiveLimits()["max_monitored_systems"]; got != 10 {
- t.Fatalf("EffectiveLimits()[max_monitored_systems]=%d, want %d", got, 10)
- }
if got := claims.EntitlementSubscriptionState(); got != SubStateActive {
t.Fatalf("EntitlementSubscriptionState()=%q, want %q", got, SubStateActive)
}
@@ -387,14 +311,13 @@ func TestParseGrantJWTUnsafe(t *testing.T) {
{
name: "valid grant",
jwt: makeUnsignedTestGrantJWT(t, &GrantClaims{
- LicenseID: "lic_test",
- InstallationID: "inst_abc",
- State: "active",
- Tier: "pro",
- Features: []string{"relay"},
- MaxMonitoredSystems: 10,
- IssuedAt: 1000,
- ExpiresAt: 2000,
+ LicenseID: "lic_test",
+ InstallationID: "inst_abc",
+ State: "active",
+ Tier: "pro",
+ Features: []string{"relay"},
+ IssuedAt: 1000,
+ ExpiresAt: 2000,
}),
check: func(t *testing.T, gc *GrantClaims) {
if gc.LicenseID != "lic_test" {
@@ -403,9 +326,6 @@ func TestParseGrantJWTUnsafe(t *testing.T) {
if gc.Tier != "pro" {
t.Errorf("Tier = %q, want %q", gc.Tier, "pro")
}
- if gc.MaxMonitoredSystems != 10 {
- t.Errorf("MaxMonitoredSystems = %d, want 10", gc.MaxMonitoredSystems)
- }
if gc.State != "active" {
t.Errorf("State = %q, want %q", gc.State, "active")
}
diff --git a/pkg/licensing/billing_state_normalization.go b/pkg/licensing/billing_state_normalization.go
index 9c0aef4ef..70d2806cc 100644
--- a/pkg/licensing/billing_state_normalization.go
+++ b/pkg/licensing/billing_state_normalization.go
@@ -48,9 +48,6 @@ func NormalizeBillingState(state *BillingState) *BillingState {
normalized.CommercialMigration = NormalizeCommercialMigrationStatus(normalized.CommercialMigration)
normalized.Limits = NormalizeMonitoredSystemLimits(normalized.Limits)
- if IsSelfHostedCoreMonitoringUncappedPlanVersion(normalized.PlanVersion) {
- stripLegacyCommercialCaps(normalized.Limits)
- }
// Ensure slices/maps are never nil (JSON marshals as [] / {} instead of null).
if normalized.Capabilities == nil {
@@ -65,32 +62,17 @@ func NormalizeBillingState(state *BillingState) *BillingState {
// Preserve absence when the stored hosted billing record has no plan label.
// Canonical defaults still come from DefaultBillingState()/call-site defaults.
normalized.PlanVersion = CanonicalizePlanVersion(normalized.PlanVersion)
+ stripSelfHostedCommercialVolumeCaps(normalized.Limits, normalized.PlanVersion, "", false)
switch normalized.SubscriptionState {
case SubStateExpired, SubStateSuspended, SubStateCanceled:
normalized.Capabilities = []string{}
normalized.Limits = map[string]int64{}
normalized.MetersEnabled = []string{}
- default:
- if limit, ok := billingStateStoredMonitoredSystemLimit(normalized.PlanVersion); ok {
- normalized.Limits[MaxMonitoredSystemsLicenseGateKey] = int64(limit)
- }
}
return normalized
}
-func billingStateStoredMonitoredSystemLimit(planVersion string) (int, bool) {
- planVersion = CanonicalizePlanVersion(planVersion)
- if IsSelfHostedCoreMonitoringUncappedPlanVersion(planVersion) {
- return 0, false
- }
- limit, known := CloudPlanMonitoredSystemLimits[planVersion]
- if !known || limit <= 0 {
- return 0, false
- }
- return limit, true
-}
-
func IsValidBillingSubscriptionState(state SubscriptionState) bool {
switch state {
case SubStateTrial,
diff --git a/pkg/licensing/billing_state_normalization_test.go b/pkg/licensing/billing_state_normalization_test.go
index 727558631..d786f7ff9 100644
--- a/pkg/licensing/billing_state_normalization_test.go
+++ b/pkg/licensing/billing_state_normalization_test.go
@@ -29,7 +29,7 @@ func TestNormalizeBillingState(t *testing.T) {
trialEnds := int64(200)
input := &BillingState{
Capabilities: []string{" a ", "b"},
- Limits: map[string]int64{"max_monitored_systems": 10},
+ Limits: map[string]int64{"max_monitored_systems": 10, "max_guests": 20},
MetersEnabled: []string{"meter_a"},
PlanVersion: " ",
SubscriptionState: SubscriptionState(" ACTIVE "),
@@ -48,20 +48,24 @@ func TestNormalizeBillingState(t *testing.T) {
if normalized.StripeCustomerID != "cus_123" {
t.Fatalf("stripe_customer_id=%q, want %q", normalized.StripeCustomerID, "cus_123")
}
- if normalized.Limits["max_monitored_systems"] != 10 {
- t.Fatalf("limits[max_nodes]=%d, want 10", normalized.Limits["max_monitored_systems"])
+ if _, ok := normalized.Limits["max_monitored_systems"]; ok {
+ t.Fatalf("expected retired max_monitored_systems to be scrubbed, got %v", normalized.Limits)
+ }
+ if normalized.Limits["max_guests"] != 20 {
+ t.Fatalf("limits[max_guests]=%d, want 20", normalized.Limits["max_guests"])
}
input.Capabilities[0] = "changed"
input.MetersEnabled[0] = "changed"
input.Limits["max_monitored_systems"] = 99
+ input.Limits["max_guests"] = 99
if normalized.Capabilities[0] != " a " {
t.Fatalf("expected capabilities to be copied")
}
if normalized.MetersEnabled[0] != "meter_a" {
t.Fatalf("expected meters_enabled to be copied")
}
- if normalized.Limits["max_monitored_systems"] != 10 {
+ if normalized.Limits["max_guests"] != 20 {
t.Fatalf("expected limits map to be copied")
}
}
@@ -80,7 +84,7 @@ func TestNormalizeBillingState_PreservesAllFields(t *testing.T) {
input := &BillingState{
Capabilities: []string{"relay", "ai"},
- Limits: map[string]int64{"max_monitored_systems": 50},
+ Limits: map[string]int64{"max_monitored_systems": 50, "max_guests": 100},
MetersEnabled: []string{"active_agents"},
PlanVersion: "pro-v2",
SubscriptionState: SubStateActive,
@@ -99,8 +103,11 @@ func TestNormalizeBillingState_PreservesAllFields(t *testing.T) {
if len(normalized.Capabilities) != 2 {
t.Fatalf("capabilities: got %v", normalized.Capabilities)
}
- if normalized.Limits["max_monitored_systems"] != 50 {
- t.Fatalf("limits[max_nodes]: got %d", normalized.Limits["max_monitored_systems"])
+ if _, ok := normalized.Limits["max_monitored_systems"]; ok {
+ t.Fatalf("expected retired max_monitored_systems to be scrubbed, got %v", normalized.Limits)
+ }
+ if normalized.Limits["max_guests"] != 100 {
+ t.Fatalf("limits[max_guests]: got %d", normalized.Limits["max_guests"])
}
if normalized.MetersEnabled[0] != "active_agents" {
t.Fatalf("meters_enabled: got %v", normalized.MetersEnabled)
@@ -247,40 +254,40 @@ func TestIsValidBillingSubscriptionState(t *testing.T) {
}
}
-func TestNormalizeBillingState_MaxNodesToMaxMonitoredSystemsMigration(t *testing.T) {
- t.Run("legacy_key_migrated", func(t *testing.T) {
+func TestNormalizeBillingState_ScrubsRetiredMonitoredSystemLimitAliases(t *testing.T) {
+ t.Run("legacy_key_removed", func(t *testing.T) {
state := &BillingState{
- Limits: map[string]int64{"max_nodes": 10},
+ Limits: map[string]int64{"max_nodes": 10, "max_reports": 7},
}
normalized := NormalizeBillingState(state)
- if normalized.Limits["max_monitored_systems"] != 10 {
- t.Fatalf("expected max_monitored_systems=10, got %d", normalized.Limits["max_monitored_systems"])
- }
if _, hasOld := normalized.Limits["max_nodes"]; hasOld {
- t.Fatal("expected max_nodes to be deleted after migration")
+ t.Fatal("expected max_nodes to be deleted")
+ }
+ if got := normalized.Limits["max_reports"]; got != 7 {
+ t.Fatalf("limits[max_reports]=%d, want 7", got)
}
})
- t.Run("new_key_preserved_legacy_deleted", func(t *testing.T) {
+ t.Run("new_key_and_legacy_deleted", func(t *testing.T) {
state := &BillingState{
Limits: map[string]int64{"max_monitored_systems": 15, "max_nodes": 5},
}
normalized := NormalizeBillingState(state)
- if normalized.Limits["max_monitored_systems"] != 15 {
- t.Fatalf("expected max_monitored_systems=15, got %d", normalized.Limits["max_monitored_systems"])
+ if _, ok := normalized.Limits["max_monitored_systems"]; ok {
+ t.Fatalf("expected max_monitored_systems to be deleted, got %v", normalized.Limits)
}
if _, hasOld := normalized.Limits["max_nodes"]; hasOld {
- t.Fatal("expected max_nodes to be deleted when max_monitored_systems exists")
+ t.Fatal("expected max_nodes to be deleted")
}
})
- t.Run("no_legacy_key_no_change", func(t *testing.T) {
+ t.Run("retired_new_key_deleted", func(t *testing.T) {
state := &BillingState{
Limits: map[string]int64{"max_monitored_systems": 20},
}
normalized := NormalizeBillingState(state)
- if normalized.Limits["max_monitored_systems"] != 20 {
- t.Fatalf("expected max_monitored_systems=20, got %d", normalized.Limits["max_monitored_systems"])
+ if _, ok := normalized.Limits["max_monitored_systems"]; ok {
+ t.Fatalf("expected max_monitored_systems to be deleted, got %v", normalized.Limits)
}
})
}
@@ -291,6 +298,7 @@ func TestNormalizeBillingState_CanonicalizesCloudPlanVersionAndLimits(t *testing
Limits: map[string]int64{
"max_monitored_systems": 999,
"max_nodes": 5,
+ "max_guests": 7,
},
}
@@ -298,12 +306,15 @@ func TestNormalizeBillingState_CanonicalizesCloudPlanVersionAndLimits(t *testing
if normalized.PlanVersion != "cloud_starter" {
t.Fatalf("plan_version=%q, want %q", normalized.PlanVersion, "cloud_starter")
}
- if got := normalized.Limits["max_monitored_systems"]; got != 10 {
- t.Fatalf("limits[max_monitored_systems]=%d, want %d", got, 10)
+ if _, ok := normalized.Limits["max_monitored_systems"]; ok {
+ t.Fatalf("expected max_monitored_systems to be scrubbed, got %v", normalized.Limits)
}
if _, hasOld := normalized.Limits["max_nodes"]; hasOld {
t.Fatal("expected max_nodes to be deleted during normalization")
}
+ if got := normalized.Limits["max_guests"]; got != 7 {
+ t.Fatalf("limits[max_guests]=%d, want 7", got)
+ }
}
func TestNormalizeBillingState_GrandfatheredRecurringPlanStripsLegacyCommercialCaps(t *testing.T) {
@@ -337,6 +348,7 @@ func TestNormalizeBillingState_PreservesUnknownNonCloudPlanLimits(t *testing.T)
PlanVersion: "pro-v2",
Limits: map[string]int64{
"max_monitored_systems": 50,
+ "max_guests": 7,
},
}
@@ -344,8 +356,11 @@ func TestNormalizeBillingState_PreservesUnknownNonCloudPlanLimits(t *testing.T)
if normalized.PlanVersion != "pro-v2" {
t.Fatalf("plan_version=%q, want %q", normalized.PlanVersion, "pro-v2")
}
- if got := normalized.Limits["max_monitored_systems"]; got != 50 {
- t.Fatalf("limits[max_monitored_systems]=%d, want %d", got, 50)
+ if _, ok := normalized.Limits["max_monitored_systems"]; ok {
+ t.Fatalf("expected max_monitored_systems to be scrubbed, got %v", normalized.Limits)
+ }
+ if got := normalized.Limits["max_guests"]; got != 7 {
+ t.Fatalf("limits[max_guests]=%d, want 7", got)
}
}
diff --git a/pkg/licensing/cloud_paid_guardrails_test.go b/pkg/licensing/cloud_paid_guardrails_test.go
index 85ebc4e97..90093bec6 100644
--- a/pkg/licensing/cloud_paid_guardrails_test.go
+++ b/pkg/licensing/cloud_paid_guardrails_test.go
@@ -2,10 +2,10 @@ package licensing
import "testing"
-func TestNormalizeBillingStatePreservesMissingPlanVersion(t *testing.T) {
+func TestNormalizeBillingStatePreservesMissingPlanVersionAndScrubsRetiredMonitoringLimit(t *testing.T) {
state := &BillingState{
PlanVersion: " ",
- Limits: map[string]int64{"max_monitored_systems": 42},
+ Limits: map[string]int64{"max_monitored_systems": 42, "max_guests": 7},
SubscriptionState: SubscriptionState(" ACTIVE "),
}
@@ -16,39 +16,49 @@ func TestNormalizeBillingStatePreservesMissingPlanVersion(t *testing.T) {
if normalized.SubscriptionState != SubStateActive {
t.Fatalf("subscription_state=%q, want %q", normalized.SubscriptionState, SubStateActive)
}
- if got := normalized.Limits["max_monitored_systems"]; got != 42 {
- t.Fatalf("limits[max_monitored_systems]=%d, want %d", got, 42)
+ if _, ok := normalized.Limits["max_monitored_systems"]; ok {
+ t.Fatalf("limits retained retired max_monitored_systems: %v", normalized.Limits)
+ }
+ if got := normalized.Limits["max_guests"]; got != 7 {
+ t.Fatalf("limits[max_guests]=%d, want %d", got, 7)
}
}
-func TestNormalizeEntitlementLeaseClaimsPreservesMissingPlanVersion(t *testing.T) {
+func TestNormalizeEntitlementLeaseClaimsPreservesMissingPlanVersionAndScrubsRetiredMonitoringLimit(t *testing.T) {
claims := &EntitlementLeaseClaims{
PlanVersion: " ",
SubscriptionState: SubStateActive,
- Limits: map[string]int64{"max_monitored_systems": 42},
+ Limits: map[string]int64{"max_monitored_systems": 42, "max_guests": 7},
}
normalizeEntitlementLeaseClaims(claims)
if claims.PlanVersion != "" {
t.Fatalf("plan_version=%q, want empty", claims.PlanVersion)
}
- if got := claims.Limits["max_monitored_systems"]; got != 42 {
- t.Fatalf("limits[max_monitored_systems]=%d, want %d", got, 42)
+ if _, ok := claims.Limits["max_monitored_systems"]; ok {
+ t.Fatalf("limits retained retired max_monitored_systems: %v", claims.Limits)
+ }
+ if got := claims.Limits["max_guests"]; got != 7 {
+ t.Fatalf("limits[max_guests]=%d, want %d", got, 7)
}
}
-func TestClaimsPreserveMissingPlanVersion(t *testing.T) {
+func TestClaimsPreserveMissingPlanVersionAndScrubRetiredMonitoringLimit(t *testing.T) {
claims := &Claims{
Tier: TierCloud,
PlanVersion: " ",
- Limits: map[string]int64{"max_monitored_systems": 42},
+ Limits: map[string]int64{"max_monitored_systems": 42, "max_guests": 7},
}
if got := claims.EntitlementPlanVersion(); got != "" {
t.Fatalf("EntitlementPlanVersion()=%q, want empty", got)
}
- if got := claims.EffectiveLimits()["max_monitored_systems"]; got != 42 {
- t.Fatalf("EffectiveLimits()[max_monitored_systems]=%d, want %d", got, 42)
+ limits := claims.EffectiveLimits()
+ if _, ok := limits["max_monitored_systems"]; ok {
+ t.Fatalf("EffectiveLimits retained retired max_monitored_systems: %v", limits)
+ }
+ if got := limits["max_guests"]; got != 7 {
+ t.Fatalf("EffectiveLimits()[max_guests]=%d, want %d", got, 7)
}
}
@@ -56,7 +66,7 @@ func TestTokenSourcePreservesMissingPlanVersionContract(t *testing.T) {
source := NewTokenSource(stubTokenClaims{
planVersion: "",
subscriptionState: SubStateActive,
- limits: map[string]int64{"max_monitored_systems": 42},
+ limits: map[string]int64{"max_guests": 7},
})
if got := source.PlanVersion(); got != "" {
@@ -65,12 +75,12 @@ func TestTokenSourcePreservesMissingPlanVersionContract(t *testing.T) {
if got := source.SubscriptionState(); got != SubStateActive {
t.Fatalf("SubscriptionState()=%q, want %q", got, SubStateActive)
}
- if got := source.Limits()["max_monitored_systems"]; got != 42 {
- t.Fatalf("Limits()[max_monitored_systems]=%d, want %d", got, 42)
+ if got := source.Limits()["max_guests"]; got != 7 {
+ t.Fatalf("Limits()[max_guests]=%d, want %d", got, 7)
}
}
-func TestCloudClaimsMissingPlanVersionFailClosedOnMonitoredSystemLimit(t *testing.T) {
+func TestCloudClaimsMissingPlanVersionDoesNotReintroduceMonitoringLimit(t *testing.T) {
claims := &Claims{
Tier: TierCloud,
PlanVersion: " ",
@@ -79,7 +89,7 @@ func TestCloudClaimsMissingPlanVersionFailClosedOnMonitoredSystemLimit(t *testin
if got := claims.EntitlementPlanVersion(); got != "" {
t.Fatalf("EntitlementPlanVersion()=%q, want empty", got)
}
- if got := claims.EffectiveLimits()["max_monitored_systems"]; got != int64(UnknownPlanDefaultMonitoredSystemLimit) {
- t.Fatalf("EffectiveLimits()[max_monitored_systems]=%d, want %d", got, UnknownPlanDefaultMonitoredSystemLimit)
+ if _, ok := claims.EffectiveLimits()["max_monitored_systems"]; ok {
+ t.Fatalf("EffectiveLimits retained retired max_monitored_systems: %v", claims.EffectiveLimits())
}
}
diff --git a/pkg/licensing/database_source.go b/pkg/licensing/database_source.go
index fcfeee0c1..5aef4e932 100644
--- a/pkg/licensing/database_source.go
+++ b/pkg/licensing/database_source.go
@@ -243,22 +243,13 @@ func normalizeDatabaseSourceState(state BillingState) BillingState {
normalized.CommercialMigration = NormalizeCommercialMigrationStatus(normalized.CommercialMigration)
normalized.Limits = NormalizeMonitoredSystemLimits(normalized.Limits)
- if IsSelfHostedCoreMonitoringUncappedPlanVersion(normalized.PlanVersion) {
- stripLegacyCommercialCaps(normalized.Limits)
- }
+ stripSelfHostedCommercialVolumeCaps(normalized.Limits, normalized.PlanVersion, "", false)
switch normalized.SubscriptionState {
case SubStateExpired, SubStateSuspended, SubStateCanceled:
normalized.Capabilities = nil
normalized.Limits = nil
normalized.MetersEnabled = nil
- default:
- if limit, known := CloudPlanMonitoredSystemLimits[normalized.PlanVersion]; known && limit > 0 {
- if normalized.Limits == nil {
- normalized.Limits = map[string]int64{}
- }
- normalized.Limits[MaxMonitoredSystemsLicenseGateKey] = int64(limit)
- }
}
return normalized
diff --git a/pkg/licensing/database_source_test.go b/pkg/licensing/database_source_test.go
index c889d272e..19ffe6e73 100644
--- a/pkg/licensing/database_source_test.go
+++ b/pkg/licensing/database_source_test.go
@@ -56,7 +56,7 @@ func TestDatabaseSourceHappyPath(t *testing.T) {
store := &mockBillingStore{
state: &BillingState{
Capabilities: []string{"rbac", "relay"},
- Limits: map[string]int64{"max_monitored_systems": 50},
+ Limits: map[string]int64{"max_monitored_systems": 50, "max_guests": 75},
MetersEnabled: []string{"active_agents"},
PlanVersion: "pro-v2",
SubscriptionState: SubStateActive,
@@ -68,8 +68,8 @@ func TestDatabaseSourceHappyPath(t *testing.T) {
if got := source.Capabilities(); !reflect.DeepEqual(got, []string{"rbac", "relay"}) {
t.Fatalf("expected capabilities %v, got %v", []string{"rbac", "relay"}, got)
}
- if got := source.Limits(); !reflect.DeepEqual(got, map[string]int64{"max_monitored_systems": 50}) {
- t.Fatalf("expected limits %v, got %v", map[string]int64{"max_monitored_systems": 50}, got)
+ if got := source.Limits(); !reflect.DeepEqual(got, map[string]int64{"max_guests": 75}) {
+ t.Fatalf("expected limits %v, got %v", map[string]int64{"max_guests": 75}, got)
}
if got := source.MetersEnabled(); !reflect.DeepEqual(got, []string{"active_agents"}) {
t.Fatalf("expected meters %v, got %v", []string{"active_agents"}, got)
@@ -86,20 +86,23 @@ func TestDatabaseSourceHappyPath(t *testing.T) {
}
}
-func TestDatabaseSourceLimits_MaxNodesMigration(t *testing.T) {
+func TestDatabaseSourceLimits_ScrubsRetiredMonitoredSystemAliases(t *testing.T) {
store := &mockBillingStore{
state: &BillingState{
- Limits: map[string]int64{"max_nodes": 25},
+ Limits: map[string]int64{"max_nodes": 25, "max_reports": 7},
SubscriptionState: SubStateActive,
},
}
source := NewDatabaseSource(store, "org-1", time.Hour)
got := source.Limits()
- if got["max_monitored_systems"] != 25 {
- t.Fatalf("expected max_monitored_systems=25, got %d", got["max_monitored_systems"])
+ if _, ok := got["max_monitored_systems"]; ok {
+ t.Fatalf("expected max_monitored_systems to be absent, got %v", got)
}
if _, hasOld := got["max_nodes"]; hasOld {
- t.Fatal("expected max_nodes to be absent after migration")
+ t.Fatal("expected max_nodes to be absent")
+ }
+ if got["max_reports"] != 7 {
+ t.Fatalf("expected max_reports=7, got %d", got["max_reports"])
}
}
@@ -107,7 +110,7 @@ func TestDatabaseSourceCanonicalizesCloudPlanVersionAndLimits(t *testing.T) {
store := &mockBillingStore{
state: &BillingState{
PlanVersion: "cloud_v1",
- Limits: map[string]int64{"max_monitored_systems": 999},
+ Limits: map[string]int64{"max_monitored_systems": 999, "max_guests": 7},
SubscriptionState: SubStateActive,
},
}
@@ -117,8 +120,12 @@ func TestDatabaseSourceCanonicalizesCloudPlanVersionAndLimits(t *testing.T) {
if got := source.PlanVersion(); got != "cloud_starter" {
t.Fatalf("expected plan_version %q, got %q", "cloud_starter", got)
}
- if got := source.Limits()["max_monitored_systems"]; got != 10 {
- t.Fatalf("expected max_monitored_systems=%d, got %d", 10, got)
+ limits := source.Limits()
+ if _, ok := limits["max_monitored_systems"]; ok {
+ t.Fatalf("expected max_monitored_systems to be absent, got %v", limits)
+ }
+ if got := limits["max_guests"]; got != 7 {
+ t.Fatalf("expected max_guests=%d, got %d", 7, got)
}
}
@@ -182,7 +189,7 @@ func TestDatabaseSourcePreservesMissingPlanVersion(t *testing.T) {
store := &mockBillingStore{
state: &BillingState{
PlanVersion: " ",
- Limits: map[string]int64{"max_monitored_systems": 42},
+ Limits: map[string]int64{"max_monitored_systems": 42, "max_guests": 7},
SubscriptionState: SubscriptionState(" ACTIVE "),
},
}
@@ -195,8 +202,12 @@ func TestDatabaseSourcePreservesMissingPlanVersion(t *testing.T) {
if got := source.SubscriptionState(); got != SubStateActive {
t.Fatalf("expected subscription_state %q, got %q", SubStateActive, got)
}
- if got := source.Limits()["max_monitored_systems"]; got != 42 {
- t.Fatalf("expected max_monitored_systems=%d, got %d", 42, got)
+ limits := source.Limits()
+ if _, ok := limits["max_monitored_systems"]; ok {
+ t.Fatalf("expected max_monitored_systems to be absent, got %v", limits)
+ }
+ if got := limits["max_guests"]; got != 7 {
+ t.Fatalf("expected max_guests=%d, got %d", 7, got)
}
}
@@ -384,7 +395,7 @@ func TestDatabaseSourceLeaseOnlyStateResolvesTrialEntitlement(t *testing.T) {
PlanVersion: trialState.PlanVersion,
SubscriptionState: trialState.SubscriptionState,
Capabilities: append([]string(nil), trialState.Capabilities...),
- Limits: map[string]int64{"max_monitored_systems": 25},
+ Limits: map[string]int64{"max_monitored_systems": 25, "max_guests": 7},
TrialStartedAt: trialState.TrialStartedAt,
TrialEndsAt: trialState.TrialEndsAt,
RegisteredClaims: jwt.RegisteredClaims{
@@ -410,8 +421,8 @@ func TestDatabaseSourceLeaseOnlyStateResolvesTrialEntitlement(t *testing.T) {
if got := source.Capabilities(); !reflect.DeepEqual(got, []string{"ai_autofix"}) {
t.Fatalf("expected capabilities %v, got %v", []string{"ai_autofix"}, got)
}
- if got := source.Limits(); !reflect.DeepEqual(got, map[string]int64{"max_monitored_systems": 25}) {
- t.Fatalf("expected limits %v, got %v", map[string]int64{"max_monitored_systems": 25}, got)
+ if got := source.Limits(); !reflect.DeepEqual(got, map[string]int64{"max_guests": 7}) {
+ t.Fatalf("expected limits %v, got %v", map[string]int64{"max_guests": 7}, got)
}
if got := source.TrialStartedAt(); got == nil || *got != *trialState.TrialStartedAt {
t.Fatalf("expected trial_started_at %v, got %v", trialState.TrialStartedAt, got)
@@ -434,7 +445,7 @@ func TestDatabaseSourceLeaseOnlyStatePreservesMissingPlanVersion(t *testing.T) {
InstanceHost: "pulse.example.com",
PlanVersion: " ",
SubscriptionState: SubStateActive,
- Limits: map[string]int64{"max_monitored_systems": 42},
+ Limits: map[string]int64{"max_monitored_systems": 42, "max_guests": 7},
RegisteredClaims: jwt.RegisteredClaims{
IssuedAt: jwt.NewNumericDate(now),
ExpiresAt: jwt.NewNumericDate(now.Add(time.Hour)),
@@ -457,8 +468,8 @@ func TestDatabaseSourceLeaseOnlyStatePreservesMissingPlanVersion(t *testing.T) {
if got := source.SubscriptionState(); got != SubStateActive {
t.Fatalf("expected subscription_state %q, got %q", SubStateActive, got)
}
- if got := source.Limits(); !reflect.DeepEqual(got, map[string]int64{"max_monitored_systems": 42}) {
- t.Fatalf("expected limits %v, got %v", map[string]int64{"max_monitored_systems": 42}, got)
+ if got := source.Limits(); !reflect.DeepEqual(got, map[string]int64{"max_guests": 7}) {
+ t.Fatalf("expected limits %v, got %v", map[string]int64{"max_guests": 7}, got)
}
}
diff --git a/pkg/licensing/entitlement_lease.go b/pkg/licensing/entitlement_lease.go
index 3a3451c42..5ba61c96d 100644
--- a/pkg/licensing/entitlement_lease.go
+++ b/pkg/licensing/entitlement_lease.go
@@ -56,15 +56,10 @@ func normalizeEntitlementLeaseClaims(claims *EntitlementLeaseClaims) {
claims.PlanVersion = CanonicalizePlanVersion(strings.TrimSpace(claims.PlanVersion))
claims.Capabilities = cloneStringSlice(claims.Capabilities)
claims.Limits = NormalizeMonitoredSystemLimits(claims.Limits)
+ stripSelfHostedCommercialVolumeCaps(claims.Limits, claims.PlanVersion, "", false)
claims.MetersEnabled = cloneStringSlice(claims.MetersEnabled)
claims.TrialStartedAt = cloneInt64Ptr(claims.TrialStartedAt)
claims.TrialEndsAt = cloneInt64Ptr(claims.TrialEndsAt)
- if limit, known := CloudPlanMonitoredSystemLimits[claims.PlanVersion]; known {
- if claims.Limits == nil {
- claims.Limits = map[string]int64{}
- }
- claims.Limits[MaxMonitoredSystemsLicenseGateKey] = int64(limit)
- }
}
// SignEntitlementLeaseToken signs a hosted entitlement lease JWT.
diff --git a/pkg/licensing/entitlement_lease_test.go b/pkg/licensing/entitlement_lease_test.go
index e63fe672e..4c13b8a54 100644
--- a/pkg/licensing/entitlement_lease_test.go
+++ b/pkg/licensing/entitlement_lease_test.go
@@ -24,7 +24,7 @@ func TestSignAndVerifyEntitlementLeaseToken(t *testing.T) {
PlanVersion: string(SubStateTrial),
SubscriptionState: SubStateTrial,
Capabilities: []string{"ai_autofix"},
- Limits: map[string]int64{"max_monitored_systems": 25},
+ Limits: map[string]int64{"max_monitored_systems": 25, "max_guests": 7},
MetersEnabled: []string{"agents"},
TrialStartedAt: &startedAt,
TrialEndsAt: &endsAt,
@@ -51,8 +51,11 @@ func TestSignAndVerifyEntitlementLeaseToken(t *testing.T) {
if claims.SubscriptionState != SubStateTrial {
t.Fatalf("claims.SubscriptionState=%q, want %q", claims.SubscriptionState, SubStateTrial)
}
- if got := claims.Limits["max_monitored_systems"]; got != 25 {
- t.Fatalf("claims.Limits[max_monitored_systems]=%d, want %d", got, 25)
+ if _, ok := claims.Limits["max_monitored_systems"]; ok {
+ t.Fatalf("claims retained retired max_monitored_systems: %v", claims.Limits)
+ }
+ if got := claims.Limits["max_guests"]; got != 7 {
+ t.Fatalf("claims.Limits[max_guests]=%d, want %d", got, 7)
}
}
@@ -132,7 +135,7 @@ func TestEntitlementLeaseCanonicalizesCloudPlanVersionAndLimits(t *testing.T) {
InstanceHost: "pulse.example.com",
PlanVersion: " cloud_v1 ",
SubscriptionState: SubStateActive,
- Limits: map[string]int64{"max_monitored_systems": 999},
+ Limits: map[string]int64{"max_monitored_systems": 999, "max_guests": 7},
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(5 * time.Minute)),
},
@@ -148,8 +151,11 @@ func TestEntitlementLeaseCanonicalizesCloudPlanVersionAndLimits(t *testing.T) {
if claims.PlanVersion != "cloud_starter" {
t.Fatalf("claims.PlanVersion=%q, want %q", claims.PlanVersion, "cloud_starter")
}
- if got := claims.Limits["max_monitored_systems"]; got != 10 {
- t.Fatalf("claims.Limits[max_monitored_systems]=%d, want %d", got, 10)
+ if _, ok := claims.Limits["max_monitored_systems"]; ok {
+ t.Fatalf("claims retained retired max_monitored_systems: %v", claims.Limits)
+ }
+ if got := claims.Limits["max_guests"]; got != 7 {
+ t.Fatalf("claims.Limits[max_guests]=%d, want %d", got, 7)
}
}
@@ -164,7 +170,7 @@ func TestEntitlementLeasePreservesNonCloudLimits(t *testing.T) {
InstanceHost: "pulse.example.com",
PlanVersion: "pro-v2",
SubscriptionState: SubStateActive,
- Limits: map[string]int64{"max_monitored_systems": 42},
+ Limits: map[string]int64{"max_monitored_systems": 42, "max_guests": 7},
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(5 * time.Minute)),
},
@@ -180,8 +186,11 @@ func TestEntitlementLeasePreservesNonCloudLimits(t *testing.T) {
if claims.PlanVersion != "pro-v2" {
t.Fatalf("claims.PlanVersion=%q, want %q", claims.PlanVersion, "pro-v2")
}
- if got := claims.Limits["max_monitored_systems"]; got != 42 {
- t.Fatalf("claims.Limits[max_monitored_systems]=%d, want %d", got, 42)
+ if _, ok := claims.Limits["max_monitored_systems"]; ok {
+ t.Fatalf("claims retained retired max_monitored_systems: %v", claims.Limits)
+ }
+ if got := claims.Limits["max_guests"]; got != 7 {
+ t.Fatalf("claims.Limits[max_guests]=%d, want %d", got, 7)
}
}
@@ -196,7 +205,7 @@ func TestEntitlementLeasePreservesMissingPlanVersion(t *testing.T) {
InstanceHost: "pulse.example.com",
PlanVersion: " ",
SubscriptionState: SubStateActive,
- Limits: map[string]int64{"max_monitored_systems": 42},
+ Limits: map[string]int64{"max_monitored_systems": 42, "max_guests": 7},
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(5 * time.Minute)),
},
@@ -215,8 +224,11 @@ func TestEntitlementLeasePreservesMissingPlanVersion(t *testing.T) {
if claims.SubscriptionState != SubStateActive {
t.Fatalf("claims.SubscriptionState=%q, want %q", claims.SubscriptionState, SubStateActive)
}
- if got := claims.Limits["max_monitored_systems"]; got != 42 {
- t.Fatalf("claims.Limits[max_monitored_systems]=%d, want %d", got, 42)
+ if _, ok := claims.Limits["max_monitored_systems"]; ok {
+ t.Fatalf("claims retained retired max_monitored_systems: %v", claims.Limits)
+ }
+ if got := claims.Limits["max_guests"]; got != 7 {
+ t.Fatalf("claims.Limits[max_guests]=%d, want %d", got, 7)
}
}
@@ -232,7 +244,7 @@ func TestResolveEntitlementLeaseBillingStatePreservesMissingPlanVersion(t *testi
InstanceHost: "pulse.example.com",
PlanVersion: " ",
SubscriptionState: SubStateActive,
- Limits: map[string]int64{"max_monitored_systems": 42},
+ Limits: map[string]int64{"max_monitored_systems": 42, "max_guests": 7},
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(5 * time.Minute)),
},
@@ -250,7 +262,10 @@ func TestResolveEntitlementLeaseBillingStatePreservesMissingPlanVersion(t *testi
if resolved.SubscriptionState != SubStateActive {
t.Fatalf("resolved.SubscriptionState=%q, want %q", resolved.SubscriptionState, SubStateActive)
}
- if got := resolved.Limits["max_monitored_systems"]; got != 42 {
- t.Fatalf("resolved.Limits[max_monitored_systems]=%d, want %d", got, 42)
+ if _, ok := resolved.Limits["max_monitored_systems"]; ok {
+ t.Fatalf("resolved retained retired max_monitored_systems: %v", resolved.Limits)
+ }
+ if got := resolved.Limits["max_guests"]; got != 7 {
+ t.Fatalf("resolved.Limits[max_guests]=%d, want %d", got, 7)
}
}
diff --git a/pkg/licensing/entitlement_payload.go b/pkg/licensing/entitlement_payload.go
index 80f6439d9..d987ac2bf 100644
--- a/pkg/licensing/entitlement_payload.go
+++ b/pkg/licensing/entitlement_payload.go
@@ -72,32 +72,21 @@ type EntitlementPayload struct {
// Indicates the number of days remaining in the 14-day overflow window.
OverflowDaysRemaining *int `json:"overflow_days_remaining,omitempty"`
- // LegacyConnections is retained for response compatibility. Monitored-system
- // enforcement now counts API-backed and agent-backed top-level systems
- // together, so this field is informational only.
+ // LegacyConnections is retained for response compatibility and informational
+ // estate review only.
LegacyConnections LegacyConnectionCounts `json:"legacy_connections"`
- // HasMigrationGap is retained for response compatibility. API-backed systems
- // now count toward the same monitored-system cap as agent-backed systems.
+ // HasMigrationGap is retained for response compatibility. It no longer
+ // describes a commercial volume limit.
HasMigrationGap bool `json:"has_migration_gap"`
// CommercialMigration reports unresolved paid-license migration work entering
// from v5-era commercial state.
CommercialMigration *CommercialMigrationStatus `json:"commercial_migration,omitempty"`
-
- // MonitoredSystemContinuity exposes migrated monitored-system continuity
- // state for billing and support-grade plan-limit presentation.
- MonitoredSystemContinuity *MonitoredSystemContinuityStatus `json:"monitored_system_continuity,omitempty"`
-
- // MonitoredSystemCapacity exposes the canonical monitored-system
- // admission posture so the frontend can distinguish between a hard cap,
- // an admission freeze, and uncapped continuity without inferring that
- // behavior from raw current/limit math.
- MonitoredSystemCapacity *MonitoredSystemCapacityStatus `json:"monitored_system_capacity,omitempty"`
}
// CommercialPosturePayload is the canonical non-billing commercial contract
-// for upgrade messaging and monitored-system migration copy.
+// for upgrade messaging and paid migration copy.
// It intentionally excludes billing identity, grandfathered plan terms, and
// other full-entitlement details that belong only to billing surfaces.
type CommercialPosturePayload struct {
@@ -125,22 +114,17 @@ type CommercialPosturePayload struct {
// OverflowDaysRemaining is set when the onboarding overflow (+1 host) is active.
OverflowDaysRemaining *int `json:"overflow_days_remaining,omitempty"`
- // LegacyConnections is retained for response compatibility. Monitored-system
- // enforcement now counts API-backed and agent-backed top-level systems
- // together, so this field is informational only.
+ // LegacyConnections is retained for response compatibility and informational
+ // estate review only.
LegacyConnections LegacyConnectionCounts `json:"legacy_connections"`
- // HasMigrationGap is retained for response compatibility. API-backed systems
- // now count toward the same monitored-system cap as agent-backed systems.
+ // HasMigrationGap is retained for response compatibility. It no longer
+ // describes a commercial volume limit.
HasMigrationGap bool `json:"has_migration_gap"`
// CommercialMigration reports unresolved paid-license migration work entering
// from v5-era commercial state.
CommercialMigration *CommercialMigrationStatus `json:"commercial_migration,omitempty"`
-
- // MonitoredSystemCapacity exposes the canonical monitored-system admission
- // posture without exposing billing identity or plan-term internals.
- MonitoredSystemCapacity *MonitoredSystemCapacityStatus `json:"monitored_system_capacity,omitempty"`
}
// RuntimeCapabilitiesPayload is the canonical non-commercial license contract
@@ -157,15 +141,11 @@ type RuntimeCapabilitiesPayload struct {
// MaxHistoryDays is the maximum metrics history retention in days for the current tier.
MaxHistoryDays int `json:"max_history_days"`
-
- // MonitoredSystemCapacity exposes the canonical monitored-system runtime
- // posture for warning banners and admission-freeze UX.
- MonitoredSystemCapacity *MonitoredSystemCapacityStatus `json:"monitored_system_capacity,omitempty"`
}
// LimitStatus represents a quantitative limit with current usage state.
type LimitStatus struct {
- // Key is the limit identifier (e.g., "max_monitored_systems").
+ // Key is the limit identifier (e.g., "max_guests").
Key string `json:"key"`
// Limit is the maximum allowed value (0 = unlimited).
@@ -174,69 +154,11 @@ type LimitStatus struct {
// Current is the observed current usage.
Current int64 `json:"current"`
- // CurrentAvailable reports whether Current reflects a resolved runtime
- // usage value rather than an unavailable best-effort fallback.
- CurrentAvailable *bool `json:"current_available,omitempty"`
-
- // CurrentUnavailableReason explains why Current is unavailable when
- // CurrentAvailable is false.
- CurrentUnavailableReason string `json:"current_unavailable_reason,omitempty"`
-
// State describes the over-limit UX state.
// Values: "ok", "warning", "enforced"
State string `json:"state"`
}
-// MonitoredSystemCapacityStatus describes the canonical monitored-system
-// admission posture. It makes explicit that Pulse blocks net-new monitored
-// systems at or above the plan limit while keeping already-counted systems
-// visible and reporting.
-type MonitoredSystemCapacityStatus struct {
- // Mode is the canonical monitored-system capacity posture.
- // Values: "usage_unavailable", "unlimited", "within_limit",
- // "at_limit_blocking_new", "over_limit_frozen"
- Mode string `json:"mode"`
-
- // Urgency mirrors the user-facing severity of the current posture.
- // Values: "ok", "warning", "enforced"
- Urgency string `json:"urgency"`
-
- // Current is the observed current monitored-system usage.
- Current int64 `json:"current"`
-
- // Limit is the plan limit for monitored systems (0 = unlimited).
- Limit int64 `json:"limit"`
-
- // CurrentAvailable reports whether Current reflects a resolved runtime
- // usage value rather than an unavailable best-effort fallback.
- CurrentAvailable bool `json:"current_available"`
-
- // CurrentUnavailableReason explains why Current is unavailable when
- // CurrentAvailable is false.
- CurrentUnavailableReason string `json:"current_unavailable_reason,omitempty"`
-
- // AvailableSlots reports how many net-new monitored systems can be added
- // before the plan blocks additional admissions.
- AvailableSlots int64 `json:"available_slots"`
-
- // Overage reports how far above the current plan limit this installation
- // is while existing monitoring continues.
- Overage int64 `json:"overage"`
-
- // Reason explains why the current monitored-system posture is legitimate.
- // Values: "limit_reached", "preexisting_usage",
- // "legacy_migration_capture_pending"
- Reason string `json:"reason,omitempty"`
-
- // BlocksNewSystems indicates that Pulse will reject net-new monitored
- // systems until capacity is freed or the plan changes.
- BlocksNewSystems bool `json:"blocks_new_systems"`
-
- // ExistingMonitoringContinues indicates that already-counted monitored
- // systems remain visible and reporting under the current posture.
- ExistingMonitoringContinues bool `json:"existing_monitoring_continues"`
-}
-
// UpgradeReason provides context for why a user should upgrade.
type UpgradeReason struct {
// Key is the capability or limit this reason relates to.
@@ -284,21 +206,6 @@ type EntitlementUsageSnapshot struct {
LegacyConnections LegacyConnectionCounts
}
-func (s EntitlementUsageSnapshot) monitoredSystemCount() int64 {
- if !s.MonitoredSystemsAvailable || s.MonitoredSystems < 0 {
- return 0
- }
- return s.MonitoredSystems
-}
-
-func (s EntitlementUsageSnapshot) monitoredSystemCountAvailable() bool {
- return s.MonitoredSystemsAvailable
-}
-
-func (s EntitlementUsageSnapshot) monitoredSystemCountUnavailableReason() string {
- return s.MonitoredSystemsUnavailableReason
-}
-
// BuildEntitlementPayload constructs the normalized payload from LicenseStatus.
func BuildEntitlementPayload(status *LicenseStatus, subscriptionState string) EntitlementPayload {
return BuildEntitlementPayloadWithUsage(status, subscriptionState, EntitlementUsageSnapshot{}, nil)
@@ -344,9 +251,6 @@ func BuildRuntimeCapabilitiesPayloadWithUsage(
Limits: cloneLimitStatuses(entitlementPayload.Limits),
HostedMode: entitlementPayload.HostedMode,
MaxHistoryDays: entitlementPayload.MaxHistoryDays,
- MonitoredSystemCapacity: cloneMonitoredSystemCapacityStatus(
- entitlementPayload.MonitoredSystemCapacity,
- ),
}
}
@@ -383,27 +287,12 @@ func CommercialPosturePayloadFromEntitlementPayload(
if payload.CommercialMigration != nil {
sanitized.CommercialMigration = CloneCommercialMigrationStatus(payload.CommercialMigration)
}
- if payload.MonitoredSystemCapacity != nil {
- sanitized.MonitoredSystemCapacity = cloneMonitoredSystemCapacityStatus(
- payload.MonitoredSystemCapacity,
- )
- }
if sanitized.UpgradeReasons == nil {
sanitized.UpgradeReasons = []UpgradeReason{}
}
return sanitized
}
-func licenseStatusUsesUncappedCoreMonitoring(status *LicenseStatus) bool {
- if status == nil {
- return false
- }
- if status.PlanVersion != "" {
- return IsSelfHostedCoreMonitoringUncappedPlanVersion(status.PlanVersion)
- }
- return IsSelfHostedCoreMonitoringUncappedTier(status.Tier)
-}
-
// BuildEntitlementPayloadWithUsage constructs the normalized payload from LicenseStatus and observed usage.
func BuildEntitlementPayloadWithUsage(
status *LicenseStatus,
@@ -444,24 +333,6 @@ func BuildEntitlementPayloadWithUsage(
LegacyConnections: usage.LegacyConnections,
HasMigrationGap: false,
}
- uncappedCoreMonitoring := licenseStatusUsesUncappedCoreMonitoring(status)
- if status.MonitoredSystemContinuity != nil {
- continuity := *status.MonitoredSystemContinuity
- payload.MonitoredSystemContinuity = &continuity
- }
- monitoredSystemLimit := int64(status.MaxMonitoredSystems)
- capacityContinuity := status.MonitoredSystemContinuity
- if uncappedCoreMonitoring {
- monitoredSystemLimit = 0
- payload.MonitoredSystemContinuity = nil
- capacityContinuity = nil
- }
- payload.MonitoredSystemCapacity = buildMonitoredSystemCapacityStatus(
- monitoredSystemLimit,
- usage,
- capacityContinuity,
- )
-
if payload.Capabilities == nil {
payload.Capabilities = []string{}
}
@@ -487,22 +358,8 @@ func BuildEntitlementPayloadWithUsage(
payload.MaxHistoryDays = TierHistoryDays[TierFree]
}
- // Build limits.
- if status.MaxMonitoredSystems > 0 && !uncappedCoreMonitoring {
- currentSystems := usage.monitoredSystemCount()
- limit := LimitStatus{
- Key: MaxMonitoredSystemsLicenseGateKey,
- Limit: int64(status.MaxMonitoredSystems),
- Current: currentSystems,
- CurrentAvailable: boolPointer(usage.monitoredSystemCountAvailable()),
- State: LimitState(currentSystems, int64(status.MaxMonitoredSystems)),
- }
- if !usage.monitoredSystemCountAvailable() {
- limit.CurrentUnavailableReason = usage.monitoredSystemCountUnavailableReason()
- }
- payload.Limits = append(payload.Limits, limit)
- }
- if status.MaxGuests > 0 && !uncappedCoreMonitoring {
+ // Build hosted-only runtime limits. Self-hosted commercial volume caps are retired.
+ if status.Tier == TierCloud && status.MaxGuests > 0 {
payload.Limits = append(payload.Limits, LimitStatus{
Key: "max_guests",
Limit: int64(status.MaxGuests),
@@ -557,92 +414,6 @@ func remainingTrialDays(expiresAtUnix, nowUnix int64) int {
return daysRemaining
}
-func boolPointer(value bool) *bool {
- v := value
- return &v
-}
-
-func buildMonitoredSystemCapacityStatus(
- limit int64,
- usage EntitlementUsageSnapshot,
- continuity *MonitoredSystemContinuityStatus,
-) *MonitoredSystemCapacityStatus {
- currentAvailable := usage.monitoredSystemCountAvailable()
- if !currentAvailable {
- return &MonitoredSystemCapacityStatus{
- Mode: "usage_unavailable",
- Urgency: "ok",
- Current: 0,
- Limit: limit,
- CurrentAvailable: false,
- CurrentUnavailableReason: usage.monitoredSystemCountUnavailableReason(),
- AvailableSlots: 0,
- Overage: 0,
- BlocksNewSystems: false,
- ExistingMonitoringContinues: false,
- }
- }
-
- current := usage.monitoredSystemCount()
- if limit <= 0 {
- return &MonitoredSystemCapacityStatus{
- Mode: "unlimited",
- Urgency: "ok",
- Current: current,
- Limit: 0,
- CurrentAvailable: true,
- AvailableSlots: 0,
- Overage: 0,
- BlocksNewSystems: false,
- ExistingMonitoringContinues: true,
- }
- }
-
- status := &MonitoredSystemCapacityStatus{
- Current: current,
- Limit: limit,
- CurrentAvailable: true,
- AvailableSlots: 0,
- Overage: 0,
- BlocksNewSystems: false,
- ExistingMonitoringContinues: true,
- Urgency: LimitState(current, limit),
- }
-
- switch {
- case current < limit:
- status.Mode = "within_limit"
- status.AvailableSlots = limit - current
- case current == limit:
- status.Mode = "at_limit_blocking_new"
- status.Reason = "limit_reached"
- status.BlocksNewSystems = true
- case current > limit:
- status.Mode = "over_limit_frozen"
- status.BlocksNewSystems = true
- status.Overage = current - limit
- if continuity != nil && continuity.CapturePending {
- status.Reason = "legacy_migration_capture_pending"
- } else {
- status.Reason = "preexisting_usage"
- }
- default:
- status.Mode = "within_limit"
- }
-
- return status
-}
-
-func cloneMonitoredSystemCapacityStatus(
- status *MonitoredSystemCapacityStatus,
-) *MonitoredSystemCapacityStatus {
- if status == nil {
- return nil
- }
- cloned := *status
- return &cloned
-}
-
// LimitState returns the over-limit UX state string.
func LimitState(current, limit int64) string {
if limit <= 0 {
diff --git a/pkg/licensing/entitlement_payload_test.go b/pkg/licensing/entitlement_payload_test.go
index d0d054834..7ab43b746 100644
--- a/pkg/licensing/entitlement_payload_test.go
+++ b/pkg/licensing/entitlement_payload_test.go
@@ -17,13 +17,21 @@ func containsString(values []string, want string) bool {
return false
}
-func TestBuildEntitlementPayload_ActiveLicense(t *testing.T) {
+func hasLimit(payload EntitlementPayload, key string) bool {
+ for _, limit := range payload.Limits {
+ if limit.Key == key {
+ return true
+ }
+ }
+ return false
+}
+
+func TestBuildEntitlementPayload_ActiveLicenseOmitsRetiredMonitoredSystemLimit(t *testing.T) {
status := &LicenseStatus{
- Valid: true,
- Tier: TierCloud,
- PlanVersion: "cloud_starter",
- Features: append([]string(nil), TierFeatures[TierPro]...),
- MaxMonitoredSystems: 50,
+ Valid: true,
+ Tier: TierCloud,
+ PlanVersion: "cloud_starter",
+ Features: append([]string(nil), TierFeatures[TierPro]...),
}
payload := BuildEntitlementPayload(status, "")
@@ -34,19 +42,8 @@ func TestBuildEntitlementPayload_ActiveLicense(t *testing.T) {
if !reflect.DeepEqual(payload.Capabilities, status.Features) {
t.Fatalf("expected capabilities to match status features")
}
-
- var agentLimit *LimitStatus
- for i := range payload.Limits {
- if payload.Limits[i].Key == MaxMonitoredSystemsLicenseGateKey {
- agentLimit = &payload.Limits[i]
- break
- }
- }
- if agentLimit == nil {
- t.Fatalf("expected max_monitored_systems limit in payload")
- }
- if agentLimit.Limit != 50 {
- t.Fatalf("expected max_monitored_systems limit 50, got %d", agentLimit.Limit)
+ if hasLimit(payload, MaxMonitoredSystemsLicenseGateKey) {
+ t.Fatalf("payload exposed retired monitored-system limit: %+v", payload.Limits)
}
if len(payload.UpgradeReasons) != 0 {
t.Fatalf("expected no upgrade reasons for pro tier, got %d", len(payload.UpgradeReasons))
@@ -102,12 +99,11 @@ func TestBuildEntitlementPayload_FreeTier(t *testing.T) {
func TestBuildEntitlementPayloadWithUsage_CurrentValues(t *testing.T) {
status := &LicenseStatus{
- Valid: true,
- Tier: TierCloud,
- PlanVersion: "cloud_starter",
- Features: append([]string(nil), TierFeatures[TierPro]...),
- MaxMonitoredSystems: 50,
- MaxGuests: 100,
+ Valid: true,
+ Tier: TierCloud,
+ PlanVersion: "cloud_starter",
+ Features: append([]string(nil), TierFeatures[TierPro]...),
+ MaxGuests: 100,
}
payload := BuildEntitlementPayloadWithUsage(status, "", EntitlementUsageSnapshot{
@@ -121,29 +117,18 @@ func TestBuildEntitlementPayloadWithUsage_CurrentValues(t *testing.T) {
},
}, nil)
- var agentLimit *LimitStatus
+ if hasLimit(payload, MaxMonitoredSystemsLicenseGateKey) {
+ t.Fatalf("payload exposed retired monitored-system limit: %+v", payload.Limits)
+ }
var guestLimit *LimitStatus
for i := range payload.Limits {
- if payload.Limits[i].Key == MaxMonitoredSystemsLicenseGateKey {
- agentLimit = &payload.Limits[i]
- }
if payload.Limits[i].Key == "max_guests" {
guestLimit = &payload.Limits[i]
}
}
-
- if agentLimit == nil {
- t.Fatalf("expected max_monitored_systems limit")
- }
if guestLimit == nil {
t.Fatalf("expected max_guests limit")
}
- if agentLimit.Current != 12 {
- t.Fatalf("expected agent current 12, got %d", agentLimit.Current)
- }
- if agentLimit.CurrentAvailable == nil || !*agentLimit.CurrentAvailable {
- t.Fatalf("expected agent current availability to be true, got %+v", agentLimit.CurrentAvailable)
- }
if guestLimit.Current != 44 {
t.Fatalf("expected guest current 44, got %d", guestLimit.Current)
}
@@ -160,16 +145,7 @@ func TestBuildEntitlementPayloadWithUsage_CurrentValues(t *testing.T) {
)
}
if payload.HasMigrationGap {
- t.Fatal("expected has_migration_gap=false under monitored-system counting")
- }
- if payload.MonitoredSystemCapacity == nil {
- t.Fatal("expected monitored-system capacity posture")
- }
- if payload.MonitoredSystemCapacity.Mode != "within_limit" {
- t.Fatalf("Mode=%q, want within_limit", payload.MonitoredSystemCapacity.Mode)
- }
- if payload.MonitoredSystemCapacity.AvailableSlots != 38 {
- t.Fatalf("AvailableSlots=%d, want 38", payload.MonitoredSystemCapacity.AvailableSlots)
+ t.Fatal("expected has_migration_gap=false")
}
}
@@ -198,120 +174,6 @@ func TestBuildEntitlementPayload_TrialState(t *testing.T) {
}
}
-func TestBuildEntitlementPayloadWithUsage_RequiresCanonicalMonitoredSystemAvailability(t *testing.T) {
- status := &LicenseStatus{
- Valid: true,
- Tier: TierCloud,
- PlanVersion: "cloud_starter",
- Features: append([]string(nil), TierFeatures[TierPro]...),
- MaxMonitoredSystems: 50,
- }
-
- payload := BuildEntitlementPayloadWithUsage(status, "", EntitlementUsageSnapshot{
- MonitoredSystems: 12,
- Nodes: 99,
- MonitoredSystemsUnavailableReason: "canonical_usage_unavailable",
- }, nil)
-
- if len(payload.Limits) != 1 {
- t.Fatalf("expected one limit, got %d", len(payload.Limits))
- }
- if payload.Limits[0].Current != 0 {
- t.Fatalf("expected unavailable canonical usage to remain 0, got %d", payload.Limits[0].Current)
- }
- if payload.Limits[0].CurrentAvailable == nil || *payload.Limits[0].CurrentAvailable {
- t.Fatalf("expected canonical usage availability to be false, got %+v", payload.Limits[0].CurrentAvailable)
- }
- if payload.Limits[0].CurrentUnavailableReason != "canonical_usage_unavailable" {
- t.Fatalf("CurrentUnavailableReason=%q, want %q", payload.Limits[0].CurrentUnavailableReason, "canonical_usage_unavailable")
- }
-}
-
-func TestBuildEntitlementPayloadWithUsage_MonitoredSystemUsageUnavailable(t *testing.T) {
- status := &LicenseStatus{
- Valid: true,
- Tier: TierCloud,
- PlanVersion: "cloud_starter",
- Features: append([]string(nil), TierFeatures[TierPro]...),
- MaxMonitoredSystems: 50,
- }
-
- payload := BuildEntitlementPayloadWithUsage(status, "", EntitlementUsageSnapshot{
- MonitoredSystemsUnavailableReason: "supplemental_inventory_unsettled",
- }, nil)
- if len(payload.Limits) != 1 {
- t.Fatalf("expected one limit, got %d", len(payload.Limits))
- }
- if payload.Limits[0].Current != 0 {
- t.Fatalf("expected unresolved current to fall back to 0, got %d", payload.Limits[0].Current)
- }
- if payload.Limits[0].CurrentAvailable == nil || *payload.Limits[0].CurrentAvailable {
- t.Fatalf("expected unresolved current availability to be false, got %+v", payload.Limits[0].CurrentAvailable)
- }
- if payload.Limits[0].CurrentUnavailableReason != "supplemental_inventory_unsettled" {
- t.Fatalf("CurrentUnavailableReason=%q, want %q", payload.Limits[0].CurrentUnavailableReason, "supplemental_inventory_unsettled")
- }
- if payload.MonitoredSystemCapacity == nil {
- t.Fatal("expected monitored-system capacity posture")
- }
- if payload.MonitoredSystemCapacity.Mode != "usage_unavailable" {
- t.Fatalf("Mode=%q, want usage_unavailable", payload.MonitoredSystemCapacity.Mode)
- }
- if payload.MonitoredSystemCapacity.CurrentAvailable {
- t.Fatalf("CurrentAvailable=%v, want false", payload.MonitoredSystemCapacity.CurrentAvailable)
- }
-}
-
-func TestBuildEntitlementPayloadWithUsage_CopiesMonitoredSystemContinuity(t *testing.T) {
- status := &LicenseStatus{
- Valid: true,
- Tier: TierCloud,
- PlanVersion: "cloud_starter",
- Features: append([]string(nil), TierFeatures[TierPro]...),
- MaxMonitoredSystems: 23,
- MonitoredSystemContinuity: &MonitoredSystemContinuityStatus{
- PlanLimit: 10,
- GrandfatheredFloor: 23,
- EffectiveLimit: 23,
- CapturePending: false,
- CapturedAt: 123,
- },
- }
-
- payload := BuildEntitlementPayloadWithUsage(status, "", EntitlementUsageSnapshot{
- MonitoredSystems: 23,
- MonitoredSystemsAvailable: true,
- }, nil)
-
- if payload.MonitoredSystemContinuity == nil {
- t.Fatal("expected monitored-system continuity to be copied")
- }
- if payload.MonitoredSystemContinuity.PlanLimit != 10 {
- t.Fatalf("PlanLimit=%d, want %d", payload.MonitoredSystemContinuity.PlanLimit, 10)
- }
- if payload.MonitoredSystemContinuity.EffectiveLimit != 23 {
- t.Fatalf("EffectiveLimit=%d, want %d", payload.MonitoredSystemContinuity.EffectiveLimit, 23)
- }
- if payload.MonitoredSystemContinuity.GrandfatheredFloor != 23 {
- t.Fatalf("GrandfatheredFloor=%d, want %d", payload.MonitoredSystemContinuity.GrandfatheredFloor, 23)
- }
- if payload.MonitoredSystemContinuity.CapturePending {
- t.Fatal("expected continuity capture to be settled")
- }
- if payload.MonitoredSystemCapacity == nil {
- t.Fatal("expected monitored-system capacity posture")
- }
- if payload.MonitoredSystemCapacity.Mode != "at_limit_blocking_new" {
- t.Fatalf("Mode=%q, want at_limit_blocking_new", payload.MonitoredSystemCapacity.Mode)
- }
- if payload.MonitoredSystemCapacity.Reason != "limit_reached" {
- t.Fatalf("Reason=%q, want limit_reached", payload.MonitoredSystemCapacity.Reason)
- }
- if !payload.MonitoredSystemCapacity.BlocksNewSystems {
- t.Fatal("expected at-limit posture to block new monitored systems")
- }
-}
-
func TestBuildEntitlementPayload_CopiesStatusDisplayFields(t *testing.T) {
tests := []struct {
name string
@@ -368,13 +230,12 @@ func TestBuildEntitlementPayload_CopiesStatusDisplayFields(t *testing.T) {
func TestBuildEntitlementPayload_LifetimeOmitsCommercialCaps(t *testing.T) {
payload := BuildEntitlementPayloadWithUsage(&LicenseStatus{
- Valid: true,
- Tier: TierLifetime,
- PlanVersion: "v5_lifetime_grandfathered",
- IsLifetime: true,
- Features: append([]string(nil), TierFeatures[TierLifetime]...),
- MaxMonitoredSystems: 0,
- MaxGuests: 0,
+ Valid: true,
+ Tier: TierLifetime,
+ PlanVersion: "v5_lifetime_grandfathered",
+ IsLifetime: true,
+ Features: append([]string(nil), TierFeatures[TierLifetime]...),
+ MaxGuests: 0,
}, string(SubStateActive), EntitlementUsageSnapshot{
MonitoredSystems: 15,
MonitoredSystemsAvailable: true,
@@ -384,12 +245,6 @@ func TestBuildEntitlementPayload_LifetimeOmitsCommercialCaps(t *testing.T) {
if len(payload.Limits) != 0 {
t.Fatalf("expected lifetime entitlements to omit commercial caps, got %+v", payload.Limits)
}
- if payload.MonitoredSystemCapacity == nil {
- t.Fatal("expected lifetime payload to include monitored-system capacity posture")
- }
- if payload.MonitoredSystemCapacity.Mode != "unlimited" {
- t.Fatalf("Mode=%q, want unlimited", payload.MonitoredSystemCapacity.Mode)
- }
}
func TestBuildEntitlementPayload_MaxHistoryDays(t *testing.T) {
@@ -437,11 +292,10 @@ func TestBuildEntitlementPayload_NilStatus_MaxHistoryDays(t *testing.T) {
func TestBuildCommercialPosturePayloadWithUsage_CurrentValues(t *testing.T) {
status := &LicenseStatus{
- Valid: true,
- Tier: TierCloud,
- PlanVersion: "cloud_starter",
- Features: append([]string(nil), TierFeatures[TierPro]...),
- MaxMonitoredSystems: 5,
+ Valid: true,
+ Tier: TierCloud,
+ PlanVersion: "cloud_starter",
+ Features: append([]string(nil), TierFeatures[TierPro]...),
}
payload := BuildCommercialPosturePayloadWithUsage(status, "", EntitlementUsageSnapshot{
@@ -466,65 +320,21 @@ func TestBuildCommercialPosturePayloadWithUsage_CurrentValues(t *testing.T) {
t.Fatalf("expected legacy connection counts to be preserved, got %+v", payload.LegacyConnections)
}
if payload.HasMigrationGap {
- t.Fatal("expected has_migration_gap=false under canonical monitored-system counting")
- }
- if payload.MonitoredSystemCapacity == nil {
- t.Fatal("expected commercial posture to preserve monitored-system capacity")
- }
- if payload.MonitoredSystemCapacity.Mode != "over_limit_frozen" {
- t.Fatalf("Mode=%q, want over_limit_frozen", payload.MonitoredSystemCapacity.Mode)
- }
- if payload.MonitoredSystemCapacity.Overage != 2 {
- t.Fatalf("Overage=%d, want 2", payload.MonitoredSystemCapacity.Overage)
- }
- if payload.MonitoredSystemCapacity.Reason != "preexisting_usage" {
- t.Fatalf("Reason=%q, want preexisting_usage", payload.MonitoredSystemCapacity.Reason)
- }
-}
-
-func TestBuildEntitlementPayloadWithUsage_MarksLegacyContinuityCapturePendingOverage(t *testing.T) {
- payload := BuildEntitlementPayloadWithUsage(&LicenseStatus{
- Valid: true,
- Tier: TierCloud,
- PlanVersion: "cloud_starter",
- Features: append([]string(nil), TierFeatures[TierPro]...),
- MaxMonitoredSystems: 10,
- MonitoredSystemContinuity: &MonitoredSystemContinuityStatus{
- PlanLimit: 10,
- EffectiveLimit: 10,
- CapturePending: true,
- },
- }, string(SubStateActive), EntitlementUsageSnapshot{
- MonitoredSystems: 23,
- MonitoredSystemsAvailable: true,
- }, nil)
-
- if payload.MonitoredSystemCapacity == nil {
- t.Fatal("expected monitored-system capacity posture")
- }
- if payload.MonitoredSystemCapacity.Mode != "over_limit_frozen" {
- t.Fatalf("Mode=%q, want over_limit_frozen", payload.MonitoredSystemCapacity.Mode)
- }
- if payload.MonitoredSystemCapacity.Reason != "legacy_migration_capture_pending" {
- t.Fatalf(
- "Reason=%q, want legacy_migration_capture_pending",
- payload.MonitoredSystemCapacity.Reason,
- )
+ t.Fatal("expected has_migration_gap=false")
}
}
func TestCommercialPosturePayloadFromEntitlementPayload_StripsBillingIdentityFields(t *testing.T) {
expiresAt := time.Now().Add(48 * time.Hour).UTC().Format(time.RFC3339)
payload := BuildEntitlementPayloadWithUsage(&LicenseStatus{
- Valid: true,
- Tier: TierCloud,
- PlanVersion: "cloud_starter",
- Email: "owner@example.com",
- ExpiresAt: &expiresAt,
- IsLifetime: false,
- DaysRemaining: 2,
- Features: append([]string(nil), TierFeatures[TierPro]...),
- MaxMonitoredSystems: 50,
+ Valid: true,
+ Tier: TierCloud,
+ PlanVersion: "cloud_starter",
+ Email: "owner@example.com",
+ ExpiresAt: &expiresAt,
+ IsLifetime: false,
+ DaysRemaining: 2,
+ Features: append([]string(nil), TierFeatures[TierPro]...),
}, string(SubStateActive), EntitlementUsageSnapshot{
MonitoredSystems: 12,
MonitoredSystemsAvailable: true,
@@ -544,12 +354,6 @@ func TestCommercialPosturePayloadFromEntitlementPayload_StripsBillingIdentityFie
if posture.LegacyConnections.ProxmoxNodes != 1 {
t.Fatalf("expected proxmox_nodes=1, got %+v", posture.LegacyConnections)
}
- if posture.MonitoredSystemCapacity == nil {
- t.Fatal("expected commercial posture to preserve monitored-system capacity posture")
- }
- if posture.MonitoredSystemCapacity.Mode != "within_limit" {
- t.Fatalf("Mode=%q, want within_limit", posture.MonitoredSystemCapacity.Mode)
- }
body, err := json.Marshal(posture)
if err != nil {
@@ -573,16 +377,15 @@ func TestCommercialPosturePayloadFromEntitlementPayload_StripsBillingIdentityFie
func TestBuildRuntimeCapabilitiesPayloadWithUsage_CurrentValues(t *testing.T) {
status := &LicenseStatus{
- Valid: true,
- Tier: TierCloud,
- PlanVersion: "cloud_starter",
- Features: append([]string(nil), TierFeatures[TierRelay]...),
- MaxMonitoredSystems: 12,
+ Valid: true,
+ Tier: TierCloud,
+ PlanVersion: "cloud_starter",
+ Features: append([]string(nil), TierFeatures[TierRelay]...),
+ MaxGuests: 12,
}
payload := BuildRuntimeCapabilitiesPayloadWithUsage(status, "", EntitlementUsageSnapshot{
- MonitoredSystems: 5,
- MonitoredSystemsAvailable: true,
+ Guests: 5,
})
if !reflect.DeepEqual(payload.Capabilities, status.Features) {
@@ -594,21 +397,12 @@ func TestBuildRuntimeCapabilitiesPayloadWithUsage_CurrentValues(t *testing.T) {
if len(payload.Limits) != 1 {
t.Fatalf("expected one runtime limit, got %d", len(payload.Limits))
}
- if payload.Limits[0].Key != MaxMonitoredSystemsLicenseGateKey {
- t.Fatalf("expected runtime limit key %q, got %q", MaxMonitoredSystemsLicenseGateKey, payload.Limits[0].Key)
+ if payload.Limits[0].Key != "max_guests" {
+ t.Fatalf("expected runtime limit key max_guests, got %q", payload.Limits[0].Key)
}
if payload.Limits[0].Current != 5 {
t.Fatalf("expected runtime current 5, got %d", payload.Limits[0].Current)
}
- if payload.Limits[0].CurrentAvailable == nil || !*payload.Limits[0].CurrentAvailable {
- t.Fatalf("expected runtime current availability true, got %+v", payload.Limits[0].CurrentAvailable)
- }
- if payload.MonitoredSystemCapacity == nil {
- t.Fatal("expected runtime capabilities to include monitored-system capacity posture")
- }
- if payload.MonitoredSystemCapacity.Mode != "within_limit" {
- t.Fatalf("Mode=%q, want within_limit", payload.MonitoredSystemCapacity.Mode)
- }
}
func TestBuildRuntimeCapabilitiesPayloadWithUsage_StripsInternalOnlyCapabilities(t *testing.T) {
@@ -619,7 +413,6 @@ func TestBuildRuntimeCapabilitiesPayloadWithUsage_StripsInternalOnlyCapabilities
FeatureRelay,
FeatureDemoFixtures,
},
- MaxMonitoredSystems: 12,
}
payload := BuildRuntimeCapabilitiesPayloadWithUsage(status, "", EntitlementUsageSnapshot{})
@@ -679,7 +472,6 @@ func TestLimitState(t *testing.T) {
{name: "enforced_at_limit", current: 100, limit: 100, want: "enforced"},
{name: "enforced_above_limit", current: 110, limit: 100, want: "enforced"},
{name: "ok_unlimited", current: 50, limit: 0, want: "ok"},
- // Small-limit behavior: warn at N-1
{name: "small_limit_ok", current: 3, limit: 5, want: "ok"},
{name: "small_limit_warning_at_n_minus_1", current: 4, limit: 5, want: "warning"},
{name: "small_limit_enforced_at_limit", current: 5, limit: 5, want: "enforced"},
@@ -692,7 +484,6 @@ func TestLimitState(t *testing.T) {
}
for _, tc := range tests {
- tc := tc
t.Run(tc.name, func(t *testing.T) {
got := LimitState(tc.current, tc.limit)
if got != tc.want {
diff --git a/pkg/licensing/features.go b/pkg/licensing/features.go
index 2905fbeda..4cabb8cd0 100644
--- a/pkg/licensing/features.go
+++ b/pkg/licensing/features.go
@@ -38,7 +38,7 @@ const (
FeatureMultiUser = "multi_user" // Multi-user (likely merged with RBAC)
FeatureWhiteLabel = "white_label" // Custom branding - NOT IMPLEMENTED YET
FeatureMultiTenant = "multi_tenant" // Multi-tenant organizations
- FeatureUnlimited = "unlimited" // Hosted capacity policy marker for MSP/enterprise deals
+ FeatureUnlimited = "unlimited" // Compatibility capability marker for MSP/enterprise deals
// Internal-only runtime capabilities. These must never be added to public
// tier defaults or public pricing contracts.
@@ -60,48 +60,6 @@ const (
TierEnterprise Tier = "enterprise"
)
-// TierMonitoredSystemLimits defines the maximum monitored-system count per tier.
-// A value of 0 means unlimited.
-var TierMonitoredSystemLimits = map[Tier]int{
- TierFree: 0, // Self-hosted Community no longer caps core monitoring
- TierRelay: 0, // Self-hosted Relay adds convenience features, not more room
- TierPro: 0, // Self-hosted Pro monetizes operations features, not monitoring volume
- TierProPlus: 0, // Legacy/self-hosted compatibility tier; no monitored-system cap
- TierProAnnual: 0, // Legacy: same as Pro
- TierLifetime: 0, // Grandfathered lifetime entitlements remain uncapped
- TierCloud: 0, // Cloud tiers have per-plan limits set in license claims
- TierMSP: 0, // MSP tiers have per-plan pool limits set in license claims
- TierEnterprise: 0, // Custom
-}
-
-// CloudPlanMonitoredSystemLimits maps hosted and continuity plan version strings to
-// per-plan monitored-system limits. When a tenant is provisioned or its subscription
-// changes, the provisioner uses this map to populate
-// BillingState.Limits[MaxMonitoredSystemsLicenseGateKey].
-//
-// This intentionally includes the grandfathered recurring v5/v1 continuity
-// plans that still renew through Stripe. Those subscriptions are not "unknown"
-// just because they are no longer sold; they remain canonical paid states that
-// must preserve their plan identity during webhook-driven billing updates.
-// A value of 0 means uncapped continuity for active recurring v5 customers.
-var CloudPlanMonitoredSystemLimits = map[string]int{
- // Individual Cloud tiers
- "cloud_starter": 10,
- "cloud_power": 30,
- "cloud_max": 75,
- "cloud_founding": 10, // Founding rate = Starter limits
-
- // Grandfathered recurring Pulse Pro continuity plans remain uncapped while
- // the recurring subscription stays active.
- "v5_pro_monthly_grandfathered": 0,
- "v5_pro_annual_grandfathered": 0,
-
- // MSP tiers — host pool limits from pricing spec
- "msp_starter": 50, // MSP Starter: 10 clients, 50 host pool
- "msp_growth": 150, // MSP Growth: 25 clients, 150 host pool
- "msp_scale": 400, // MSP Scale: 50 clients, 400 host pool
-}
-
// PriceIDToPlanVersion maps Stripe price IDs to canonical plan version strings.
// This is the authoritative reverse lookup: given a price ID from a checkout
// session, subscription, or webhook event, callers can resolve the plan version
@@ -205,10 +163,16 @@ func stripLegacyCommercialCaps(limits map[string]int64) {
delete(limits, "max_guests")
}
-// UnknownPlanDefaultMonitoredSystemLimit is the safe-default monitored-system limit applied when a
-// plan version is not recognized. Fail-closed: unknown plans get the smallest
-// tier limit rather than unlimited access.
-const UnknownPlanDefaultMonitoredSystemLimit = 10
+func stripSelfHostedCommercialVolumeCaps(limits map[string]int64, planVersion string, tier Tier, uncapped bool) {
+ if limits == nil {
+ return
+ }
+ if uncapped ||
+ IsSelfHostedCoreMonitoringUncappedPlanVersion(planVersion) ||
+ (planVersion == "" && IsSelfHostedCoreMonitoringUncappedTier(tier)) {
+ stripLegacyCommercialCaps(limits)
+ }
+}
// CloudPlanWorkspaceLimits maps cloud plan version strings to the maximum
// number of active workspaces (tenants) the account may create. Individual
@@ -243,20 +207,6 @@ func WorkspaceLimitForPlan(planVersion string) (limit int, known bool) {
return UnknownPlanDefaultWorkspaceLimit, false
}
-// LimitsForCloudPlan returns the monitored-system limit map for a given cloud plan
-// version and whether the plan was recognized. If the plan is recognized, the
-// map contains MaxMonitoredSystemsLicenseGateKey with the per-plan limit.
-// If unrecognized, returns
-// a safe default limit (fail-closed) and known=false so callers can decide
-// whether to reject, quarantine, or proceed with restricted access.
-func LimitsForCloudPlan(planVersion string) (limits map[string]int64, known bool) {
- planVersion = CanonicalizePlanVersion(planVersion)
- if limit, ok := CloudPlanMonitoredSystemLimits[planVersion]; ok {
- return map[string]int64{MaxMonitoredSystemsLicenseGateKey: int64(limit)}, true
- }
- return map[string]int64{MaxMonitoredSystemsLicenseGateKey: int64(UnknownPlanDefaultMonitoredSystemLimit)}, false
-}
-
// TierHistoryDays defines the maximum metrics history retention per tier.
var TierHistoryDays = map[Tier]int{
TierFree: 7,
@@ -297,7 +247,7 @@ var proFeatures = appendFeatures(relayFeatures,
FeatureAdvancedReporting,
)
-// mspFeatures adds multi-tenant and hosted capacity policy on top of pro.
+// mspFeatures adds multi-tenant policy on top of pro.
var mspFeatures = appendFeatures(proFeatures,
FeatureUnlimited,
FeatureMultiTenant,
@@ -347,14 +297,13 @@ func DeriveCapabilitiesFromTier(tier Tier, explicitFeatures []string) []string {
return capabilities
}
-// DeriveEntitlements derives capabilities and limits from tier and canonical monitored-system fields.
-func DeriveEntitlements(tier Tier, features []string, maxMonitoredSystems int, maxGuests int) (capabilities []string, limits map[string]int64) {
+// DeriveEntitlements derives capabilities and non-monitoring quantitative limits.
+// The monitored-system parameter is retained for old callers but ignored:
+// monitored-system volume is no longer a license entitlement.
+func DeriveEntitlements(tier Tier, features []string, _ int, maxGuests int) (capabilities []string, limits map[string]int64) {
capabilities = DeriveCapabilitiesFromTier(tier, features)
limits = make(map[string]int64)
- if maxMonitoredSystems > 0 {
- limits["max_monitored_systems"] = int64(maxMonitoredSystems)
- }
if maxGuests > 0 {
limits["max_guests"] = int64(maxGuests)
}
diff --git a/pkg/licensing/features_test.go b/pkg/licensing/features_test.go
index 1bd811cb8..403262ab3 100644
--- a/pkg/licensing/features_test.go
+++ b/pkg/licensing/features_test.go
@@ -132,15 +132,13 @@ func TestSelfHostedPaidFeatureClaimMatrix(t *testing.T) {
name string
tier Tier
wantHistoryDays int
- wantCoreMonitoringLimit int
wantIncludedCapabilities []string
wantExcludedCapabilities []string
}{
{
- name: "community keeps core monitoring free and does not claim paid extras",
- tier: TierFree,
- wantHistoryDays: 7,
- wantCoreMonitoringLimit: 0,
+ name: "community keeps core monitoring free and does not claim paid extras",
+ tier: TierFree,
+ wantHistoryDays: 7,
wantIncludedCapabilities: []string{
FeatureUpdateAlerts,
FeatureSSO,
@@ -159,10 +157,9 @@ func TestSelfHostedPaidFeatureClaimMatrix(t *testing.T) {
},
},
{
- name: "relay sells remote access mobile handoff push and fourteen day history",
- tier: TierRelay,
- wantHistoryDays: 14,
- wantCoreMonitoringLimit: 0,
+ name: "relay sells remote access mobile handoff push and fourteen day history",
+ tier: TierRelay,
+ wantHistoryDays: 14,
wantIncludedCapabilities: []string{
FeatureUpdateAlerts,
FeatureSSO,
@@ -182,10 +179,9 @@ func TestSelfHostedPaidFeatureClaimMatrix(t *testing.T) {
},
},
{
- name: "pro sells operator extras and preserves relay capabilities",
- tier: TierPro,
- wantHistoryDays: 90,
- wantCoreMonitoringLimit: 0,
+ name: "pro sells operator extras and preserves relay capabilities",
+ tier: TierPro,
+ wantHistoryDays: 90,
wantIncludedCapabilities: []string{
FeatureRelay,
FeatureMobileApp,
@@ -221,19 +217,6 @@ func TestSelfHostedPaidFeatureClaimMatrix(t *testing.T) {
)
}
- gotMonitoringLimit, ok := TierMonitoredSystemLimits[tt.tier]
- if !ok {
- t.Fatalf("TierMonitoredSystemLimits missing entry for tier %q", tt.tier)
- }
- if gotMonitoringLimit != tt.wantCoreMonitoringLimit {
- t.Fatalf(
- "TierMonitoredSystemLimits[%q] = %d, want %d",
- tt.tier,
- gotMonitoringLimit,
- tt.wantCoreMonitoringLimit,
- )
- }
-
capabilities := make(map[string]struct{})
for _, capability := range TierFeatures[tt.tier] {
capabilities[capability] = struct{}{}
@@ -259,8 +242,8 @@ func TestDeriveEntitlements(t *testing.T) {
t.Error("DeriveEntitlements() returned no capabilities")
}
- if limits["max_monitored_systems"] != 50 {
- t.Errorf("max_monitored_systems limit = %d, want 50", limits["max_monitored_systems"])
+ if _, ok := limits["max_monitored_systems"]; ok {
+ t.Errorf("DeriveEntitlements() exposed retired max_monitored_systems limit: %v", limits)
}
if limits["max_guests"] != 100 {
t.Errorf("max_guests limit = %d, want 100", limits["max_guests"])
@@ -474,33 +457,6 @@ func sortedFeatureSet(features []string) []string {
return out
}
-func TestTierMonitoredSystemLimits(t *testing.T) {
- tests := []struct {
- tier Tier
- want int
- }{
- {TierFree, 0},
- {TierRelay, 0},
- {TierPro, 0},
- {TierProPlus, 0},
- {TierProAnnual, 0},
- {TierLifetime, 0},
- }
-
- for _, tt := range tests {
- tt := tt
- t.Run(string(tt.tier), func(t *testing.T) {
- got, ok := TierMonitoredSystemLimits[tt.tier]
- if !ok {
- t.Fatalf("TierMonitoredSystemLimits missing entry for tier %q", tt.tier)
- }
- if got != tt.want {
- t.Errorf("TierMonitoredSystemLimits[%q] = %d, want %d", tt.tier, got, tt.want)
- }
- })
- }
-}
-
func TestTierHistoryDays(t *testing.T) {
tests := []struct {
tier Tier
@@ -609,85 +565,10 @@ func TestFilterPublicCapabilitiesStripsInternalOnlyFeatures(t *testing.T) {
}
}
-func TestLimitsForCloudPlan_KnownPlans(t *testing.T) {
- tests := []struct {
- plan string
- wantLimit int64
- }{
- {"cloud_starter", 10},
- {"cloud_power", 30},
- {"cloud_max", 75},
- {"cloud_founding", 10},
- {"msp_starter", 50},
- {"msp_hosted_v1", 50},
- {"msp_growth", 150},
- {"msp_scale", 400},
- {"v5_pro_monthly_grandfathered", 0},
- {"v5_pro_annual_grandfathered", 0},
- }
-
- for _, tt := range tests {
- t.Run(tt.plan, func(t *testing.T) {
- limits, known := LimitsForCloudPlan(tt.plan)
- if !known {
- t.Errorf("LimitsForCloudPlan(%q): known = false, want true", tt.plan)
- }
- got, ok := limits["max_monitored_systems"]
- if !ok {
- t.Fatalf("LimitsForCloudPlan(%q): missing max_monitored_systems key", tt.plan)
- }
- if got != tt.wantLimit {
- t.Errorf("LimitsForCloudPlan(%q): max_monitored_systems = %d, want %d", tt.plan, got, tt.wantLimit)
- }
- })
- }
-}
-
-func TestLimitsForCloudPlan_UnknownPlanFailsClosed(t *testing.T) {
- unknownPlans := []string{
- "stripe",
- "stripe_price:price_123",
- "",
- "unknown_plan",
- "cloud_unknown",
- }
-
- for _, plan := range unknownPlans {
- t.Run(plan, func(t *testing.T) {
- limits, known := LimitsForCloudPlan(plan)
- if known {
- t.Errorf("LimitsForCloudPlan(%q): known = true, want false", plan)
- }
- got, ok := limits["max_monitored_systems"]
- if !ok {
- t.Fatalf("LimitsForCloudPlan(%q): missing max_monitored_systems key (fail-open!)", plan)
- }
- if got != int64(UnknownPlanDefaultMonitoredSystemLimit) {
- t.Errorf("LimitsForCloudPlan(%q): max_monitored_systems = %d, want default %d", plan, got, UnknownPlanDefaultMonitoredSystemLimit)
- }
- })
- }
-}
-
-func TestLimitsForCloudPlan_NeverReturnsEmptyMap(t *testing.T) {
- // This test ensures the fail-closed invariant: LimitsForCloudPlan must
- // ALWAYS return a map with "max_monitored_systems" set, regardless of input.
- inputs := []string{"cloud_starter", "stripe", "", "garbage", "msp_starter", "msp_hosted_v1"}
- for _, plan := range inputs {
- limits, _ := LimitsForCloudPlan(plan)
- if _, ok := limits["max_monitored_systems"]; !ok {
- t.Errorf("LimitsForCloudPlan(%q) returned map without max_monitored_systems — fail-open vulnerability", plan)
- }
- }
-}
-
-// TestAllTiersHaveHostLimitsAndHistoryDays ensures every tier in TierFeatures
-// also has entries in TierMonitoredSystemLimits and TierHistoryDays.
-func TestAllTiersHaveHostLimitsAndHistoryDays(t *testing.T) {
+// TestAllTiersHaveHistoryDays ensures every tier in TierFeatures also has a
+// metrics-retention entry. Core monitoring volume limits are retired.
+func TestAllTiersHaveHistoryDays(t *testing.T) {
for tier := range TierFeatures {
- if _, ok := TierMonitoredSystemLimits[tier]; !ok {
- t.Errorf("TierMonitoredSystemLimits missing entry for tier %q", tier)
- }
if _, ok := TierHistoryDays[tier]; !ok {
t.Errorf("TierHistoryDays missing entry for tier %q", tier)
}
@@ -738,15 +619,12 @@ func TestWorkspaceLimitForPlan_UnknownPlanFailsClosed(t *testing.T) {
}
}
-// TestCloudWorkspacePlansHaveWorkspaceLimits ensures only hosted plans that
-// actually support workspaces also carry a workspace limit entry.
+// TestCloudWorkspacePlansHaveWorkspaceLimits ensures hosted workspace plans
+// still carry a workspace limit entry after monitored-system caps were retired.
func TestCloudWorkspacePlansHaveWorkspaceLimits(t *testing.T) {
- for plan := range CloudPlanMonitoredSystemLimits {
+ for plan := range CloudPlanWorkspaceLimits {
if !strings.HasPrefix(plan, "cloud_") && !strings.HasPrefix(plan, "msp_") {
- continue
- }
- if _, ok := CloudPlanWorkspaceLimits[plan]; !ok {
- t.Errorf("CloudPlanWorkspaceLimits missing entry for plan %q (present in CloudPlanMonitoredSystemLimits)", plan)
+ t.Errorf("CloudPlanWorkspaceLimits includes non-hosted plan %q", plan)
}
}
}
@@ -915,14 +793,16 @@ func TestIsSelfHostedCoreMonitoringUncappedTier(t *testing.T) {
}
}
-// TestPriceIDToPlanVersion_AllMapToKnownPlans ensures every plan version in the
-// price→plan map is recognized by LimitsForCloudPlan (fail-closed safety net).
+// TestPriceIDToPlanVersion_HostedPlansHaveWorkspaceLimits ensures hosted plans
+// in the price map retain workspace policy after monitored-system caps retire.
func TestPriceIDToPlanVersion_AllMapToKnownPlans(t *testing.T) {
for priceID, plan := range PriceIDToPlanVersion {
t.Run(priceID, func(t *testing.T) {
- _, known := LimitsForCloudPlan(plan)
- if !known {
- t.Errorf("PriceIDToPlanVersion[%q] = %q, but LimitsForCloudPlan does not recognize it", priceID, plan)
+ if !strings.HasPrefix(plan, "cloud_") && !strings.HasPrefix(plan, "msp_") {
+ return
+ }
+ if _, known := WorkspaceLimitForPlan(plan); !known {
+ t.Errorf("PriceIDToPlanVersion[%q] = %q, but WorkspaceLimitForPlan does not recognize it", priceID, plan)
}
})
}
diff --git a/pkg/licensing/grant_claims_contract_test.go b/pkg/licensing/grant_claims_contract_test.go
index 447a48803..acf01da4f 100644
--- a/pkg/licensing/grant_claims_contract_test.go
+++ b/pkg/licensing/grant_claims_contract_test.go
@@ -23,14 +23,19 @@ func TestCloudClaimPlanVersionNormalizationContract(t *testing.T) {
PlanVersion: "cloud_v1",
Limits: map[string]int64{
"max_monitored_systems": 999,
+ "max_guests": 7,
},
}
if got := claims.EntitlementPlanVersion(); got != "cloud_starter" {
t.Fatalf("EntitlementPlanVersion() = %q, want %q", got, "cloud_starter")
}
- if got := claims.EffectiveLimits()["max_monitored_systems"]; got != 10 {
- t.Fatalf("EffectiveLimits()[max_monitored_systems] = %d, want %d", got, 10)
+ limits := claims.EffectiveLimits()
+ if _, ok := limits["max_monitored_systems"]; ok {
+ t.Fatalf("EffectiveLimits retained retired max_monitored_systems: %v", limits)
+ }
+ if got := limits["max_guests"]; got != 7 {
+ t.Fatalf("EffectiveLimits()[max_guests] = %d, want %d", got, 7)
}
}
@@ -38,9 +43,6 @@ func TestMSPPlanAliasCanonicalizationContract(t *testing.T) {
if got := CanonicalizePlanVersion("msp_hosted_v1"); got != "msp_starter" {
t.Fatalf("CanonicalizePlanVersion(msp_hosted_v1) = %q, want %q", got, "msp_starter")
}
- if limits, known := LimitsForCloudPlan("msp_hosted_v1"); !known || limits["max_monitored_systems"] != 50 {
- t.Fatalf("LimitsForCloudPlan(msp_hosted_v1) = (%v, %v), want max_monitored_systems=50 and known=true", limits, known)
- }
if limit, known := WorkspaceLimitForPlan("msp_hosted_v1"); !known || limit != 10 {
t.Fatalf("WorkspaceLimitForPlan(msp_hosted_v1) = (%d, %v), want (10, true)", limit, known)
}
@@ -51,7 +53,7 @@ func TestDatabaseSourceCanonicalBoundaryContract(t *testing.T) {
store := &mockBillingStore{
state: &BillingState{
PlanVersion: "cloud_v1",
- Limits: map[string]int64{"max_nodes": 999},
+ Limits: map[string]int64{"max_nodes": 999, "max_guests": 7},
SubscriptionState: SubscriptionState(" ACTIVE "),
},
}
@@ -64,19 +66,23 @@ func TestDatabaseSourceCanonicalBoundaryContract(t *testing.T) {
if got := source.SubscriptionState(); got != SubStateActive {
t.Fatalf("SubscriptionState() = %q, want %q", got, SubStateActive)
}
- if got := source.Limits()["max_monitored_systems"]; got != 10 {
- t.Fatalf("Limits()[max_monitored_systems] = %d, want %d", got, 10)
+ limits := source.Limits()
+ if _, ok := limits["max_monitored_systems"]; ok {
+ t.Fatalf("Limits() retained retired max_monitored_systems: %v", limits)
}
- if _, hasLegacy := source.Limits()["max_nodes"]; hasLegacy {
+ if _, hasLegacy := limits["max_nodes"]; hasLegacy {
t.Fatal("Limits() preserved legacy max_nodes key")
}
+ if got := limits["max_guests"]; got != 7 {
+ t.Fatalf("Limits()[max_guests] = %d, want %d", got, 7)
+ }
})
t.Run("preserves_missing_plan_version", func(t *testing.T) {
store := &mockBillingStore{
state: &BillingState{
PlanVersion: " ",
- Limits: map[string]int64{"max_monitored_systems": 42},
+ Limits: map[string]int64{"max_monitored_systems": 42, "max_guests": 7},
SubscriptionState: SubscriptionState(" ACTIVE "),
},
}
@@ -86,8 +92,12 @@ func TestDatabaseSourceCanonicalBoundaryContract(t *testing.T) {
if got := source.PlanVersion(); got != "" {
t.Fatalf("PlanVersion() = %q, want empty", got)
}
- if got := source.Limits()["max_monitored_systems"]; got != 42 {
- t.Fatalf("Limits()[max_monitored_systems] = %d, want %d", got, 42)
+ limits := source.Limits()
+ if _, ok := limits["max_monitored_systems"]; ok {
+ t.Fatalf("Limits() retained retired max_monitored_systems: %v", limits)
+ }
+ if got := limits["max_guests"]; got != 7 {
+ t.Fatalf("Limits()[max_guests] = %d, want %d", got, 7)
}
})
@@ -142,9 +152,7 @@ func TestDatabaseSourceCanonicalBoundaryContract(t *testing.T) {
//
// The license server issues grants with these fields, the pulse client parses
// them, and the relay server validates them. Any JSON tag drift means the
-// receiving side silently drops the field value — a real bug (for example, a
-// legacy alias instead of max_monitored_systems would break monitored-system
-// enforcement on the relay).
+// receiving side silently drops the field value.
//
// We compare by JSON tag (the wire contract) rather than Go field name because
// the two repos may use different Go naming conventions (e.g. JTI vs JWTID)
@@ -164,7 +172,6 @@ var grantContractJSONTags = []string{
"tier",
"plan",
"feat",
- "max_monitored_systems",
"max_guests",
"grace_until",
"email",
diff --git a/pkg/licensing/grant_refresh_test.go b/pkg/licensing/grant_refresh_test.go
index ce3335d3a..fc3dffe37 100644
--- a/pkg/licensing/grant_refresh_test.go
+++ b/pkg/licensing/grant_refresh_test.go
@@ -465,17 +465,16 @@ func TestRefreshGrantOnce_PersistsState(t *testing.T) {
}
}
-func TestRefreshGrantOnce_PreservesLegacyGrandfatherFloor(t *testing.T) {
+func TestRefreshGrantOnce_PreservesLegacyMigrationContinuity(t *testing.T) {
setupTestPublicKey(t)
newGrantJWT := makeTestGrantJWT(t, &GrantClaims{
- LicenseID: "lic_refreshed_floor",
- Tier: "pro",
- PlanKey: "legacy_migration_fallback",
- State: "active",
- MaxMonitoredSystems: 10,
- IssuedAt: time.Now().Unix(),
- ExpiresAt: time.Now().Add(72 * time.Hour).Unix(),
+ LicenseID: "lic_refreshed_floor",
+ Tier: "pro",
+ PlanKey: "legacy_migration_fallback",
+ State: "active",
+ IssuedAt: time.Now().Unix(),
+ ExpiresAt: time.Now().Add(72 * time.Hour).Unix(),
})
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -501,13 +500,12 @@ func TestRefreshGrantOnce_PreservesLegacyGrandfatherFloor(t *testing.T) {
}
initialGrantJWT := makeTestGrantJWT(t, &GrantClaims{
- LicenseID: "lic_initial_floor",
- Tier: "pro",
- PlanKey: "legacy_migration_fallback",
- State: "active",
- MaxMonitoredSystems: 10,
- IssuedAt: time.Now().Unix(),
- ExpiresAt: time.Now().Add(72 * time.Hour).Unix(),
+ LicenseID: "lic_initial_floor",
+ Tier: "pro",
+ PlanKey: "legacy_migration_fallback",
+ State: "active",
+ IssuedAt: time.Now().Unix(),
+ ExpiresAt: time.Now().Add(72 * time.Hour).Unix(),
})
svc := NewService()
@@ -522,9 +520,7 @@ func TestRefreshGrantOnce_PreservesLegacyGrandfatherFloor(t *testing.T) {
GrantJTI: "grant_old_floor",
InstanceFingerprint: "fp-refresh-floor",
Continuity: ActivationContinuity{
- LegacyMigration: true,
- GrandfatheredMaxMonitoredSystems: 23,
- GrandfatheredMonitoredSystemsCapturedAt: time.Now().Unix(),
+ LegacyMigration: true,
},
}
if err := svc.RestoreActivation(state); err != nil {
@@ -536,10 +532,8 @@ func TestRefreshGrantOnce_PreservesLegacyGrandfatherFloor(t *testing.T) {
}
status := svc.Status()
- // Self-hosted Pro is uncapped, so status reports 0 regardless of the
- // captured continuity floor. The floor is still persisted for audit.
- if status.MaxMonitoredSystems != 0 {
- t.Fatalf("status.MaxMonitoredSystems=%d, want 0 (uncapped self-hosted)", status.MaxMonitoredSystems)
+ if status == nil || !status.Valid {
+ t.Fatalf("expected valid status after refresh, got %+v", status)
}
loaded, err := p.LoadActivationState()
@@ -549,8 +543,8 @@ func TestRefreshGrantOnce_PreservesLegacyGrandfatherFloor(t *testing.T) {
if loaded == nil {
t.Fatal("expected persisted activation state")
}
- if loaded.Continuity.GrandfatheredMaxMonitoredSystems != 23 {
- t.Fatalf("GrandfatheredMaxMonitoredSystems=%d, want 23", loaded.Continuity.GrandfatheredMaxMonitoredSystems)
+ if !loaded.Continuity.LegacyMigration {
+ t.Fatalf("LegacyMigration=%v, want true", loaded.Continuity.LegacyMigration)
}
}
@@ -612,17 +606,14 @@ func TestRefreshGrantOnce_MissingCloudPlanFailsClosed(t *testing.T) {
if got := lic.Claims.EntitlementPlanVersion(); got != "" {
t.Fatalf("EntitlementPlanVersion()=%q, want empty", got)
}
- if got := lic.Claims.EffectiveLimits()["max_monitored_systems"]; got != int64(UnknownPlanDefaultMonitoredSystemLimit) {
- t.Fatalf("EffectiveLimits()[max_monitored_systems]=%d, want %d", got, UnknownPlanDefaultMonitoredSystemLimit)
+ if _, ok := lic.Claims.EffectiveLimits()["max_monitored_systems"]; ok {
+ t.Fatalf("EffectiveLimits retained retired max_monitored_systems: %v", lic.Claims.EffectiveLimits())
}
status := svc.Status()
if status.PlanVersion != "" {
t.Fatalf("status.PlanVersion=%q, want empty", status.PlanVersion)
}
- if status.MaxMonitoredSystems != UnknownPlanDefaultMonitoredSystemLimit {
- t.Fatalf("status.MaxMonitoredSystems=%d, want %d", status.MaxMonitoredSystems, UnknownPlanDefaultMonitoredSystemLimit)
- }
}
func TestRefreshGrantOnce_CallsLicenseChangeCallback(t *testing.T) {
diff --git a/pkg/licensing/grant_verification_test.go b/pkg/licensing/grant_verification_test.go
index bde88111e..3680d6540 100644
--- a/pkg/licensing/grant_verification_test.go
+++ b/pkg/licensing/grant_verification_test.go
@@ -15,14 +15,13 @@ func TestVerifyAndParseGrantJWT_ValidSignature(t *testing.T) {
setupTestPublicKey(t)
jwt := makeTestGrantJWT(t, &GrantClaims{
- LicenseID: "lic_test",
- InstallationID: "inst_abc",
- State: "active",
- Tier: "pro",
- Features: []string{"relay"},
- MaxMonitoredSystems: 10,
- IssuedAt: time.Now().Unix(),
- ExpiresAt: time.Now().Add(72 * time.Hour).Unix(),
+ LicenseID: "lic_test",
+ InstallationID: "inst_abc",
+ State: "active",
+ Tier: "pro",
+ Features: []string{"relay"},
+ IssuedAt: time.Now().Unix(),
+ ExpiresAt: time.Now().Add(72 * time.Hour).Unix(),
})
gc, err := verifyAndParseGrantJWT(jwt)
@@ -35,9 +34,6 @@ func TestVerifyAndParseGrantJWT_ValidSignature(t *testing.T) {
if gc.Tier != "pro" {
t.Errorf("Tier = %q, want %q", gc.Tier, "pro")
}
- if gc.MaxMonitoredSystems != 10 {
- t.Errorf("MaxMonitoredSystems = %d, want 10", gc.MaxMonitoredSystems)
- }
}
func TestVerifyAndParseGrantJWT_TamperedPayload(t *testing.T) {
diff --git a/pkg/licensing/host_lifecycle.go b/pkg/licensing/host_lifecycle.go
index d3f6bcbc5..b0cbf7bdc 100644
--- a/pkg/licensing/host_lifecycle.go
+++ b/pkg/licensing/host_lifecycle.go
@@ -1,256 +1 @@
-// Package licensing — host_lifecycle.go
-//
-// HostLifecycleTracker implements the v6 host counting rules:
-//
-// - H2: A host is only counted toward monitored-system capacity after 10+
-// minutes of stable heartbeat (prevents transient connections from consuming capacity).
-// - H3: A host slot is released after 72 hours of inactivity (no heartbeat).
-//
-// Wiring instructions:
-//
-// - node_limit.go should call tracker.StableActiveHosts() to filter which
-// hosts count toward monitored-system capacity, instead of counting all hosts
-// unconditionally.
-// - The monitor's update loop (where host heartbeats arrive) should call
-// tracker.RecordHeartbeat(hostID) on every heartbeat.
-// - The host ledger (host_ledger.go) can use tracker.FirstSeen(hostID) for
-// the first_seen field in ledger entries.
-// - Call tracker.Prune() periodically (e.g. every hour) to release stale
-// entries and free memory.
-// - On startup, call LoadState(path) to restore persisted lifecycle state.
-// On shutdown (or periodically), call SaveState(path) to persist it.
package licensing
-
-import (
- "encoding/json"
- "os"
- "path/filepath"
- "sync"
- "time"
-)
-
-const (
- // HostStabilizationPeriod is the minimum duration of heartbeats before a
- // host is considered "stable" and counted toward monitored-system capacity.
- HostStabilizationPeriod = 10 * time.Minute
-
- // HostInactivityTimeout is the duration after which a host with no
- // heartbeat is considered inactive and its slot is released.
- HostInactivityTimeout = 72 * time.Hour
-)
-
-// hostEntry tracks lifecycle timestamps for a single host.
-type hostEntry struct {
- FirstSeen time.Time `json:"first_seen"`
- LastSeen time.Time `json:"last_seen"`
- StableAt *time.Time `json:"stable_at,omitempty"`
-}
-
-// HostLifecycleTracker tracks heartbeat stability and inactivity per host.
-// All methods are safe for concurrent use.
-type HostLifecycleTracker struct {
- mu sync.RWMutex
- hosts map[string]*hostEntry
-
- // nowFunc is used for time; overridden in tests.
- nowFunc func() time.Time
-}
-
-// NewHostLifecycleTracker creates a new tracker with an empty host map.
-func NewHostLifecycleTracker() *HostLifecycleTracker {
- return &HostLifecycleTracker{
- hosts: make(map[string]*hostEntry),
- nowFunc: time.Now,
- }
-}
-
-// RecordHeartbeat records a heartbeat for the given host. If the host is new,
-// its firstSeen is set. Once HostStabilizationPeriod has elapsed since
-// firstSeen, stableAt is set (and never cleared). Empty hostID is ignored.
-func (t *HostLifecycleTracker) RecordHeartbeat(hostID string) {
- if hostID == "" {
- return
- }
-
- t.mu.Lock()
- defer t.mu.Unlock()
-
- now := t.nowFunc()
- e, ok := t.hosts[hostID]
- if !ok {
- e = &hostEntry{
- FirstSeen: now,
- }
- t.hosts[hostID] = e
- }
- e.LastSeen = now
-
- if e.StableAt == nil && now.Sub(e.FirstSeen) >= HostStabilizationPeriod {
- ts := now
- e.StableAt = &ts
- }
-}
-
-// IsStable reports whether the host has achieved stability (10+ minutes of
-// heartbeats since first seen).
-func (t *HostLifecycleTracker) IsStable(hostID string) bool {
- t.mu.RLock()
- defer t.mu.RUnlock()
-
- e, ok := t.hosts[hostID]
- if !ok {
- return false
- }
- return e.StableAt != nil
-}
-
-// IsActive reports whether the host has sent a heartbeat within the last 72
-// hours.
-func (t *HostLifecycleTracker) IsActive(hostID string) bool {
- t.mu.RLock()
- defer t.mu.RUnlock()
-
- e, ok := t.hosts[hostID]
- if !ok {
- return false
- }
- return t.nowFunc().Sub(e.LastSeen) <= HostInactivityTimeout
-}
-
-// FirstSeen returns the time the host was first seen. Returns the zero value
-// if the host is unknown.
-func (t *HostLifecycleTracker) FirstSeen(hostID string) time.Time {
- t.mu.RLock()
- defer t.mu.RUnlock()
-
- e, ok := t.hosts[hostID]
- if !ok {
- return time.Time{}
- }
- return e.FirstSeen
-}
-
-// StableActiveHosts returns the IDs of all hosts that are both stable (10+
-// minutes of heartbeats) and active (heartbeat within the last 72 hours).
-// The returned slice is in no particular order.
-func (t *HostLifecycleTracker) StableActiveHosts() []string {
- t.mu.RLock()
- defer t.mu.RUnlock()
-
- now := t.nowFunc()
- var result []string
- for id, e := range t.hosts {
- if e.StableAt != nil && now.Sub(e.LastSeen) <= HostInactivityTimeout {
- result = append(result, id)
- }
- }
- return result
-}
-
-// Prune removes hosts that have been inactive for longer than
-// HostInactivityTimeout. Call periodically (e.g. hourly) to free memory.
-func (t *HostLifecycleTracker) Prune() {
- t.mu.Lock()
- defer t.mu.Unlock()
-
- now := t.nowFunc()
- for id, e := range t.hosts {
- if now.Sub(e.LastSeen) > HostInactivityTimeout {
- delete(t.hosts, id)
- }
- }
-}
-
-// HostCount returns the total number of tracked hosts (regardless of state).
-func (t *HostLifecycleTracker) HostCount() int {
- t.mu.RLock()
- defer t.mu.RUnlock()
- return len(t.hosts)
-}
-
-// ---------------------------------------------------------------------------
-// Persistence
-// ---------------------------------------------------------------------------
-
-// persistedState is the JSON-serializable representation of the tracker state.
-type persistedState struct {
- Hosts map[string]*hostEntry `json:"hosts"`
-}
-
-// SaveState persists the current tracker state to a JSON file at the given
-// path. The file is written atomically (temp file + rename).
-func (t *HostLifecycleTracker) SaveState(path string) error {
- t.mu.RLock()
- state := persistedState{Hosts: t.hosts}
- data, err := json.MarshalIndent(state, "", " ")
- t.mu.RUnlock()
- if err != nil {
- return err
- }
-
- dir := filepath.Dir(path)
- if err := os.MkdirAll(dir, 0700); err != nil {
- return err
- }
-
- f, err := os.CreateTemp(dir, filepath.Base(path)+".*.tmp")
- if err != nil {
- return err
- }
- tmp := f.Name()
-
- if _, err := f.Write(data); err != nil {
- f.Close()
- os.Remove(tmp)
- return err
- }
- if err := f.Close(); err != nil {
- os.Remove(tmp)
- return err
- }
- if err := os.Rename(tmp, path); err != nil {
- os.Remove(tmp)
- return err
- }
- return nil
-}
-
-// LoadState restores tracker state from a JSON file previously written by
-// SaveState. If the file does not exist, the tracker is left empty (no error).
-func (t *HostLifecycleTracker) LoadState(path string) error {
- data, err := os.ReadFile(path)
- if err != nil {
- if os.IsNotExist(err) {
- return nil
- }
- return err
- }
- if len(data) == 0 {
- t.mu.Lock()
- t.hosts = make(map[string]*hostEntry)
- t.mu.Unlock()
- return nil
- }
-
- var state persistedState
- if err := json.Unmarshal(data, &state); err != nil {
- return err
- }
-
- t.mu.Lock()
- defer t.mu.Unlock()
-
- if state.Hosts == nil {
- t.hosts = make(map[string]*hostEntry)
- return nil
- }
-
- // Filter out nil entries that could cause panics on read paths.
- for id, e := range state.Hosts {
- if e == nil {
- delete(state.Hosts, id)
- }
- }
- t.hosts = state.Hosts
- return nil
-}
diff --git a/pkg/licensing/host_lifecycle_test.go b/pkg/licensing/host_lifecycle_test.go
index e2ac7317b..687c1e96c 100644
--- a/pkg/licensing/host_lifecycle_test.go
+++ b/pkg/licensing/host_lifecycle_test.go
@@ -2,444 +2,26 @@ package licensing
import (
"os"
- "path/filepath"
- "sync"
+ "strings"
"testing"
- "time"
)
-// testClock is a controllable clock for deterministic tests.
-type testClock struct {
- mu sync.Mutex
- now time.Time
-}
-
-func newTestClock(t time.Time) *testClock {
- return &testClock{now: t}
-}
-
-func (c *testClock) Now() time.Time {
- c.mu.Lock()
- defer c.mu.Unlock()
- return c.now
-}
-
-func (c *testClock) Advance(d time.Duration) {
- c.mu.Lock()
- defer c.mu.Unlock()
- c.now = c.now.Add(d)
-}
-
-func newTestTracker(clock *testClock) *HostLifecycleTracker {
- t := NewHostLifecycleTracker()
- t.nowFunc = clock.Now
- return t
-}
-
-// ---------------------------------------------------------------------------
-// H2: Stabilization
-// ---------------------------------------------------------------------------
-
-func TestNewHostIsNotStable(t *testing.T) {
- clock := newTestClock(time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC))
- tracker := newTestTracker(clock)
-
- tracker.RecordHeartbeat("host-1")
-
- if tracker.IsStable("host-1") {
- t.Fatal("new host should not be stable immediately")
+func TestHostLifecycleCapacityTrackerStaysRetired(t *testing.T) {
+ source, err := os.ReadFile("host_lifecycle.go")
+ if err != nil {
+ t.Fatalf("ReadFile() error = %v", err)
}
- // Should not appear in stable-active list.
- if got := tracker.StableActiveHosts(); len(got) != 0 {
- t.Fatalf("expected 0 stable-active hosts, got %d", len(got))
- }
-}
-
-func TestHostBecomesCapacityEligibleAfter10Min(t *testing.T) {
- clock := newTestClock(time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC))
- tracker := newTestTracker(clock)
-
- tracker.RecordHeartbeat("host-1")
-
- // 9 minutes: still not stable.
- clock.Advance(9 * time.Minute)
- tracker.RecordHeartbeat("host-1")
- if tracker.IsStable("host-1") {
- t.Fatal("host should not be stable before 10 minutes")
- }
-
- // Exactly 10 minutes: becomes stable on heartbeat.
- clock.Advance(1 * time.Minute)
- tracker.RecordHeartbeat("host-1")
- if !tracker.IsStable("host-1") {
- t.Fatal("host should be stable after 10 minutes of heartbeats")
- }
-
- hosts := tracker.StableActiveHosts()
- if len(hosts) != 1 || hosts[0] != "host-1" {
- t.Fatalf("expected [host-1], got %v", hosts)
- }
-}
-
-func TestStabilityIsNotLostOnInactivity(t *testing.T) {
- clock := newTestClock(time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC))
- tracker := newTestTracker(clock)
-
- tracker.RecordHeartbeat("host-1")
- clock.Advance(15 * time.Minute)
- tracker.RecordHeartbeat("host-1")
- if !tracker.IsStable("host-1") {
- t.Fatal("host should be stable")
- }
-
- // After 50h (still within 72h), host is still stable.
- clock.Advance(50 * time.Hour)
- if !tracker.IsStable("host-1") {
- t.Fatal("stability should not be cleared by passage of time alone")
- }
-}
-
-// ---------------------------------------------------------------------------
-// H3: Inactivity
-// ---------------------------------------------------------------------------
-
-func TestHostBecomesInactiveAfter72h(t *testing.T) {
- clock := newTestClock(time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC))
- tracker := newTestTracker(clock)
-
- tracker.RecordHeartbeat("host-1")
- clock.Advance(15 * time.Minute)
- tracker.RecordHeartbeat("host-1") // now stable
-
- if !tracker.IsActive("host-1") {
- t.Fatal("host should be active right after heartbeat")
- }
-
- // Just within the window.
- clock.Advance(72 * time.Hour)
- if !tracker.IsActive("host-1") {
- t.Fatal("host should still be active at exactly 72h")
- }
-
- // Past the window.
- clock.Advance(1 * time.Second)
- if tracker.IsActive("host-1") {
- t.Fatal("host should be inactive after 72h + 1s")
- }
-
- // Stable but inactive — should NOT be in stable-active list.
- if got := tracker.StableActiveHosts(); len(got) != 0 {
- t.Fatalf("expected 0 stable-active hosts after inactivity, got %d", len(got))
- }
-}
-
-func TestHeartbeatResetsInactivityTimer(t *testing.T) {
- clock := newTestClock(time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC))
- tracker := newTestTracker(clock)
-
- tracker.RecordHeartbeat("host-1")
- clock.Advance(15 * time.Minute)
- tracker.RecordHeartbeat("host-1")
-
- // Advance close to 72h, then send another heartbeat.
- clock.Advance(71 * time.Hour)
- tracker.RecordHeartbeat("host-1")
-
- // Another 71h — total 142h since first seen, but only 71h since last heartbeat.
- clock.Advance(71 * time.Hour)
- if !tracker.IsActive("host-1") {
- t.Fatal("host should still be active — heartbeat reset the timer")
- }
-}
-
-// ---------------------------------------------------------------------------
-// Prune
-// ---------------------------------------------------------------------------
-
-func TestPruneRemovesInactiveHosts(t *testing.T) {
- clock := newTestClock(time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC))
- tracker := newTestTracker(clock)
-
- tracker.RecordHeartbeat("keep")
- tracker.RecordHeartbeat("remove")
-
- // Make "remove" inactive.
- clock.Advance(73 * time.Hour)
- tracker.RecordHeartbeat("keep") // keep stays alive
-
- tracker.Prune()
-
- if tracker.HostCount() != 1 {
- t.Fatalf("expected 1 host after prune, got %d", tracker.HostCount())
- }
- if tracker.IsActive("remove") {
- t.Fatal("pruned host should not exist")
- }
- if !tracker.IsActive("keep") {
- t.Fatal("kept host should still be active")
- }
-}
-
-func TestPruneNoOpWhenAllActive(t *testing.T) {
- clock := newTestClock(time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC))
- tracker := newTestTracker(clock)
-
- tracker.RecordHeartbeat("a")
- tracker.RecordHeartbeat("b")
- tracker.Prune()
-
- if tracker.HostCount() != 2 {
- t.Fatalf("expected 2 hosts, got %d", tracker.HostCount())
- }
-}
-
-// ---------------------------------------------------------------------------
-// FirstSeen
-// ---------------------------------------------------------------------------
-
-func TestFirstSeen(t *testing.T) {
- start := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC)
- clock := newTestClock(start)
- tracker := newTestTracker(clock)
-
- tracker.RecordHeartbeat("host-1")
- clock.Advance(5 * time.Minute)
- tracker.RecordHeartbeat("host-1") // should not change firstSeen
-
- got := tracker.FirstSeen("host-1")
- if !got.Equal(start) {
- t.Fatalf("FirstSeen should be %v, got %v", start, got)
- }
-
- // Unknown host returns zero.
- if fs := tracker.FirstSeen("unknown"); !fs.IsZero() {
- t.Fatalf("FirstSeen for unknown host should be zero, got %v", fs)
- }
-}
-
-// ---------------------------------------------------------------------------
-// Unknown host defaults
-// ---------------------------------------------------------------------------
-
-func TestUnknownHostDefaults(t *testing.T) {
- tracker := NewHostLifecycleTracker()
-
- if tracker.IsStable("nope") {
- t.Fatal("unknown host should not be stable")
- }
- if tracker.IsActive("nope") {
- t.Fatal("unknown host should not be active")
- }
-}
-
-// ---------------------------------------------------------------------------
-// Multiple hosts
-// ---------------------------------------------------------------------------
-
-func TestMultipleHostsIndependent(t *testing.T) {
- clock := newTestClock(time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC))
- tracker := newTestTracker(clock)
-
- tracker.RecordHeartbeat("a")
- clock.Advance(5 * time.Minute)
- tracker.RecordHeartbeat("b")
- clock.Advance(5 * time.Minute)
- // a: 10min elapsed, b: 5min elapsed
- tracker.RecordHeartbeat("a")
- tracker.RecordHeartbeat("b")
-
- if !tracker.IsStable("a") {
- t.Fatal("a should be stable (10min)")
- }
- if tracker.IsStable("b") {
- t.Fatal("b should not be stable yet (5min)")
- }
-
- clock.Advance(5 * time.Minute)
- tracker.RecordHeartbeat("b")
- if !tracker.IsStable("b") {
- t.Fatal("b should be stable now (10min)")
- }
-
- hosts := tracker.StableActiveHosts()
- if len(hosts) != 2 {
- t.Fatalf("expected 2 stable-active hosts, got %d", len(hosts))
- }
-}
-
-// ---------------------------------------------------------------------------
-// Persistence
-// ---------------------------------------------------------------------------
-
-func TestSaveAndLoadState(t *testing.T) {
- dir := t.TempDir()
- path := filepath.Join(dir, "lifecycle.json")
-
- clock := newTestClock(time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC))
- tracker := newTestTracker(clock)
-
- tracker.RecordHeartbeat("host-1")
- clock.Advance(15 * time.Minute)
- tracker.RecordHeartbeat("host-1") // stable
-
- tracker.RecordHeartbeat("host-2") // not stable yet
-
- if err := tracker.SaveState(path); err != nil {
- t.Fatalf("SaveState: %v", err)
- }
-
- // Load into a fresh tracker.
- tracker2 := newTestTracker(clock)
- if err := tracker2.LoadState(path); err != nil {
- t.Fatalf("LoadState: %v", err)
- }
-
- if !tracker2.IsStable("host-1") {
- t.Fatal("host-1 should be stable after load")
- }
- if tracker2.IsStable("host-2") {
- t.Fatal("host-2 should not be stable after load")
- }
- if tracker2.HostCount() != 2 {
- t.Fatalf("expected 2 hosts after load, got %d", tracker2.HostCount())
- }
-
- // FirstSeen preserved.
- expected := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
- if fs := tracker2.FirstSeen("host-1"); !fs.Equal(expected) {
- t.Fatalf("FirstSeen not preserved: want %v, got %v", expected, fs)
- }
-}
-
-func TestLoadStateMissingFile(t *testing.T) {
- tracker := NewHostLifecycleTracker()
- if err := tracker.LoadState("/nonexistent/path/lifecycle.json"); err != nil {
- t.Fatalf("LoadState on missing file should not error, got: %v", err)
- }
- if tracker.HostCount() != 0 {
- t.Fatal("tracker should be empty after loading missing file")
- }
-}
-
-func TestLoadStateEmptyFile(t *testing.T) {
- dir := t.TempDir()
- path := filepath.Join(dir, "lifecycle.json")
- if err := os.WriteFile(path, []byte{}, 0600); err != nil {
- t.Fatal(err)
- }
-
- // Pre-populate to verify empty file replaces state.
- tracker := NewHostLifecycleTracker()
- tracker.RecordHeartbeat("old")
-
- if err := tracker.LoadState(path); err != nil {
- t.Fatalf("LoadState on empty file should not error, got: %v", err)
- }
- if tracker.HostCount() != 0 {
- t.Fatalf("expected 0 hosts after loading empty file, got %d", tracker.HostCount())
- }
-}
-
-func TestLoadStateNullHostEntry(t *testing.T) {
- dir := t.TempDir()
- path := filepath.Join(dir, "lifecycle.json")
- // Simulate a corrupted file with a null entry.
- if err := os.WriteFile(path, []byte(`{"hosts":{"h1":null,"h2":{"first_seen":"2026-01-01T00:00:00Z","last_seen":"2026-01-01T00:15:00Z","stable_at":"2026-01-01T00:10:00Z"}}}`), 0600); err != nil {
- t.Fatal(err)
- }
-
- tracker := NewHostLifecycleTracker()
- if err := tracker.LoadState(path); err != nil {
- t.Fatalf("LoadState should handle null entries, got: %v", err)
- }
- // Null entry filtered out; valid entry kept.
- if tracker.HostCount() != 1 {
- t.Fatalf("expected 1 host (null filtered), got %d", tracker.HostCount())
- }
- if !tracker.IsStable("h2") {
- t.Fatal("h2 should be stable")
- }
-}
-
-func TestLoadStateNullHostsMap(t *testing.T) {
- dir := t.TempDir()
- path := filepath.Join(dir, "lifecycle.json")
- if err := os.WriteFile(path, []byte(`{"hosts":null}`), 0600); err != nil {
- t.Fatal(err)
- }
-
- // Pre-populate tracker to verify LoadState replaces state.
- tracker := NewHostLifecycleTracker()
- tracker.RecordHeartbeat("old-host")
-
- if err := tracker.LoadState(path); err != nil {
- t.Fatalf("LoadState: %v", err)
- }
- if tracker.HostCount() != 0 {
- t.Fatalf("expected 0 hosts after loading null map, got %d", tracker.HostCount())
- }
-}
-
-func TestLoadStateReplacesExistingState(t *testing.T) {
- dir := t.TempDir()
- path := filepath.Join(dir, "lifecycle.json")
- if err := os.WriteFile(path, []byte(`{}`), 0600); err != nil {
- t.Fatal(err)
- }
-
- tracker := NewHostLifecycleTracker()
- tracker.RecordHeartbeat("stale")
-
- if err := tracker.LoadState(path); err != nil {
- t.Fatalf("LoadState: %v", err)
- }
- // Loading an empty object should replace existing state.
- if tracker.HostCount() != 0 {
- t.Fatalf("expected 0 hosts after loading empty state, got %d", tracker.HostCount())
- }
-}
-
-func TestEmptyHostIDIgnored(t *testing.T) {
- tracker := NewHostLifecycleTracker()
- tracker.RecordHeartbeat("")
- if tracker.HostCount() != 0 {
- t.Fatalf("empty hostID should be ignored, got %d hosts", tracker.HostCount())
- }
-}
-
-// ---------------------------------------------------------------------------
-// Thread safety
-// ---------------------------------------------------------------------------
-
-func TestConcurrentAccess(t *testing.T) {
- tracker := NewHostLifecycleTracker()
-
- var wg sync.WaitGroup
- const goroutines = 50
- const iterations = 100
-
- wg.Add(goroutines)
- for i := 0; i < goroutines; i++ {
- go func(id int) {
- defer wg.Done()
- hostID := "host-" + string(rune('A'+id%26))
- for j := 0; j < iterations; j++ {
- tracker.RecordHeartbeat(hostID)
- tracker.IsStable(hostID)
- tracker.IsActive(hostID)
- tracker.FirstSeen(hostID)
- tracker.StableActiveHosts()
- if j%50 == 0 {
- tracker.Prune()
- }
- }
- }(i)
- }
-
- wg.Wait()
-
- // No panics or races = pass. Just verify basic sanity.
- if tracker.HostCount() == 0 {
- t.Fatal("expected some hosts after concurrent access")
+
+ for _, retired := range []string{
+ "HostLifecycleTracker",
+ "HostStabilizationPeriod",
+ "HostInactivityTimeout",
+ "StableActiveHosts",
+ "RecordHeartbeat",
+ "monitored-system capacity",
+ } {
+ if strings.Contains(string(source), retired) {
+ t.Fatalf("host lifecycle capacity tracker must stay retired; found %q", retired)
+ }
}
}
diff --git a/pkg/licensing/legacy_v5_monitored_system_limit_aliases.go b/pkg/licensing/legacy_v5_monitored_system_limit_aliases.go
index 025a0fce7..4b9803295 100644
--- a/pkg/licensing/legacy_v5_monitored_system_limit_aliases.go
+++ b/pkg/licensing/legacy_v5_monitored_system_limit_aliases.go
@@ -1,9 +1,6 @@
package licensing
-import (
- "encoding/json"
- "strings"
-)
+import "strings"
const legacyV5AgentLimitKey = "max_agents"
const legacyV5NodeLimitKey = "max_nodes"
@@ -21,25 +18,3 @@ func canonicalizeLegacyV5MonitoredSystemLimitKey(key string) (string, bool) {
return "", false
}
}
-
-func decodeLegacyV5MonitoredSystemLimitFromJSON(data []byte) (int, bool, error) {
- var raw map[string]json.RawMessage
- if err := json.Unmarshal(data, &raw); err != nil {
- return 0, false, err
- }
- if _, hasCanonical := raw[MaxMonitoredSystemsLicenseGateKey]; hasCanonical {
- return 0, false, nil
- }
- for _, key := range legacyV5MonitoredSystemLimitAliasKeys {
- value, ok := raw[key]
- if !ok {
- continue
- }
- var parsed int
- if err := json.Unmarshal(value, &parsed); err != nil {
- return 0, false, err
- }
- return parsed, true, nil
- }
- return 0, false, nil
-}
diff --git a/pkg/licensing/models.go b/pkg/licensing/models.go
index ee7f1c03c..364d0a11a 100644
--- a/pkg/licensing/models.go
+++ b/pkg/licensing/models.go
@@ -1,7 +1,6 @@
package licensing
import (
- "encoding/json"
"sort"
"time"
)
@@ -26,9 +25,6 @@ type Claims struct {
// Features explicitly granted (optional, tier implies features)
Features []string `json:"features,omitempty"`
- // Max agents (0 = unlimited)
- MaxMonitoredSystems int `json:"max_monitored_systems,omitempty"`
-
// Max guests (0 = unlimited)
MaxGuests int `json:"max_guests,omitempty"`
@@ -47,25 +43,6 @@ type Claims struct {
CoreMonitoringUncapped bool `json:"-"`
}
-// UnmarshalJSON implements custom JSON unmarshaling for Claims to handle the
-// migration from legacy monitored-system aliases to "max_monitored_systems".
-// Existing JWTs and billing.json files may still contain older keys; this shim
-// reads them all and prefers the canonical monitored-system field when present.
-func (c *Claims) UnmarshalJSON(data []byte) error {
- // Unmarshal into the base type (avoids infinite recursion).
- type Alias Claims
- if err := json.Unmarshal(data, (*Alias)(c)); err != nil {
- return err
- }
-
- // Migration shim: if the canonical field was absent, adopt the legacy v5
- // monitored-system alias that may still exist in older JWTs or billing.json.
- if legacy, ok, err := decodeLegacyV5MonitoredSystemLimitFromJSON(data); err == nil && ok {
- c.MaxMonitoredSystems = legacy
- }
- return nil
-}
-
// EffectiveCapabilities returns explicit capabilities when present; otherwise tier-derived capabilities.
func (c Claims) EffectiveCapabilities() []string {
if c.Capabilities != nil && len(c.Capabilities) > 0 {
@@ -74,45 +51,16 @@ func (c Claims) EffectiveCapabilities() []string {
return DeriveCapabilitiesFromTier(c.Tier, c.Features)
}
-func (c Claims) shouldScrubLegacyCommercialCaps() bool {
- if c.CoreMonitoringUncapped {
- return true
- }
- if IsSelfHostedCoreMonitoringUncappedTier(c.Tier) {
- return true
- }
- if IsSelfHostedCoreMonitoringUncappedPlanVersion(c.PlanVersion) {
- return true
- }
- return false
-}
-
// EffectiveLimits returns explicit limits when present; otherwise limits derived from legacy fields.
func (c Claims) EffectiveLimits() map[string]int64 {
limits := NormalizeMonitoredSystemLimits(c.Limits)
+ stripSelfHostedCommercialVolumeCaps(limits, c.EntitlementPlanVersion(), c.Tier, c.CoreMonitoringUncapped)
if len(limits) == 0 {
limits = make(map[string]int64)
- if c.MaxMonitoredSystems > 0 {
- limits[MaxMonitoredSystemsLicenseGateKey] = int64(c.MaxMonitoredSystems)
- }
if c.MaxGuests > 0 {
limits["max_guests"] = int64(c.MaxGuests)
}
- }
- if c.shouldScrubLegacyCommercialCaps() {
- // Local activation migrations and grandfathered continuity may still
- // carry legacy monitored-system caps in stale claim fields. Scrub those
- // caps only when the canonical contract says core monitoring is uncapped.
- delete(limits, MaxMonitoredSystemsLicenseGateKey)
- delete(limits, "max_guests")
- }
- if c.Tier == TierCloud || c.Tier == TierMSP {
- if limit, known := CloudPlanMonitoredSystemLimits[CanonicalizePlanVersion(c.PlanVersion)]; known {
- limits[MaxMonitoredSystemsLicenseGateKey] = int64(limit)
- }
- if _, hasSystems := limits[MaxMonitoredSystemsLicenseGateKey]; !hasSystems {
- limits[MaxMonitoredSystemsLicenseGateKey] = int64(UnknownPlanDefaultMonitoredSystemLimit)
- }
+ stripSelfHostedCommercialVolumeCaps(limits, c.EntitlementPlanVersion(), c.Tier, c.CoreMonitoringUncapped)
}
return limits
}
@@ -220,27 +168,15 @@ const (
// LicenseStatus is the JSON response for license status API.
type LicenseStatus struct {
- Valid bool `json:"valid"`
- Tier Tier `json:"tier"`
- PlanVersion string `json:"plan_version,omitempty"`
- Email string `json:"email,omitempty"`
- ExpiresAt *string `json:"expires_at,omitempty"`
- IsLifetime bool `json:"is_lifetime"`
- DaysRemaining int `json:"days_remaining"`
- Features []string `json:"features"`
- MaxMonitoredSystems int `json:"max_monitored_systems,omitempty"`
- MaxGuests int `json:"max_guests,omitempty"`
- InGracePeriod bool `json:"in_grace_period,omitempty"`
- GracePeriodEnd *string `json:"grace_period_end,omitempty"`
- MonitoredSystemContinuity *MonitoredSystemContinuityStatus `json:"monitored_system_continuity,omitempty"`
-}
-
-// MonitoredSystemContinuityStatus describes the effective monitored-system
-// limit continuity applied to a migrated legacy installation.
-type MonitoredSystemContinuityStatus struct {
- PlanLimit int `json:"plan_limit"`
- GrandfatheredFloor int `json:"grandfathered_floor,omitempty"`
- EffectiveLimit int `json:"effective_limit"`
- CapturePending bool `json:"capture_pending"`
- CapturedAt int64 `json:"captured_at,omitempty"`
+ Valid bool `json:"valid"`
+ Tier Tier `json:"tier"`
+ PlanVersion string `json:"plan_version,omitempty"`
+ Email string `json:"email,omitempty"`
+ ExpiresAt *string `json:"expires_at,omitempty"`
+ IsLifetime bool `json:"is_lifetime"`
+ DaysRemaining int `json:"days_remaining"`
+ Features []string `json:"features"`
+ MaxGuests int `json:"max_guests,omitempty"`
+ InGracePeriod bool `json:"in_grace_period,omitempty"`
+ GracePeriodEnd *string `json:"grace_period_end,omitempty"`
}
diff --git a/pkg/licensing/models_test.go b/pkg/licensing/models_test.go
index a3a335e81..dca87607d 100644
--- a/pkg/licensing/models_test.go
+++ b/pkg/licensing/models_test.go
@@ -65,98 +65,89 @@ func TestClaims_EffectiveLimits(t *testing.T) {
expected map[string]int64
}{
{
- name: "explicit_limits_returns_them",
+ name: "explicit_limits_scrub_retired_monitored_system_cap",
claims: Claims{
- Limits: map[string]int64{"max_monitored_systems": 100, "max_guests": 500},
- MaxMonitoredSystems: 50,
- MaxGuests: 200,
+ Limits: map[string]int64{"max_monitored_systems": 100, "max_guests": 500},
+ MaxGuests: 200,
},
- expected: map[string]int64{"max_monitored_systems": 100, "max_guests": 500},
+ expected: map[string]int64{"max_guests": 500},
},
{
- name: "nil_limits_derives_from_legacy_fields",
+ name: "nil_limits_derives_from_guest_field",
claims: Claims{
- Limits: nil,
- MaxMonitoredSystems: 25,
- MaxGuests: 100,
- },
- expected: map[string]int64{"max_monitored_systems": 25, "max_guests": 100},
- },
- {
- name: "zero_max_monitored_systems_ignored",
- claims: Claims{
- Limits: nil,
- MaxMonitoredSystems: 0,
- MaxGuests: 100,
+ Limits: nil,
+ MaxGuests: 100,
},
expected: map[string]int64{"max_guests": 100},
},
+ {
+ name: "zero_max_guests_ignored",
+ claims: Claims{
+ Limits: nil,
+ MaxGuests: 0,
+ },
+ expected: map[string]int64{},
+ },
{
name: "no_limits_returns_empty",
claims: Claims{},
expected: map[string]int64{},
},
{
- name: "lifetime_strips_legacy_caps",
+ name: "lifetime_scrubs_retired_monitored_system_cap",
claims: Claims{
- Tier: TierLifetime,
- Limits: map[string]int64{"max_monitored_systems": 15, "max_guests": 5},
- MaxMonitoredSystems: 15,
- MaxGuests: 5,
+ Tier: TierLifetime,
+ Limits: map[string]int64{"max_monitored_systems": 15, "max_guests": 5},
+ MaxGuests: 5,
},
expected: map[string]int64{},
},
{
- name: "grandfathered_recurring_v5_strips_new_v6_caps",
+ name: "grandfathered_recurring_v5_scrubs_retired_monitored_system_cap",
claims: Claims{
- Tier: TierPro,
- PlanVersion: "v5_pro_annual_grandfathered",
- Limits: map[string]int64{"max_monitored_systems": 15, "max_guests": 5},
- MaxMonitoredSystems: 15,
- MaxGuests: 5,
+ Tier: TierPro,
+ PlanVersion: "v5_pro_annual_grandfathered",
+ Limits: map[string]int64{"max_monitored_systems": 15, "max_guests": 5},
+ MaxGuests: 5,
},
expected: map[string]int64{},
},
{
- name: "grandfathered_monthly_v5_strips_new_v6_caps",
+ name: "grandfathered_monthly_v5_scrubs_retired_monitored_system_cap",
claims: Claims{
- Tier: TierPro,
- PlanVersion: "v5_pro_monthly_grandfathered",
- Limits: map[string]int64{"max_monitored_systems": 15, "max_guests": 5},
- MaxMonitoredSystems: 15,
- MaxGuests: 5,
+ Tier: TierPro,
+ PlanVersion: "v5_pro_monthly_grandfathered",
+ Limits: map[string]int64{"max_monitored_systems": 15, "max_guests": 5},
+ MaxGuests: 5,
},
expected: map[string]int64{},
},
{
- name: "community_plan_strips_legacy_v5_cap_after_upgrade",
+ name: "community_plan_scrubs_retired_monitored_system_cap_after_upgrade",
claims: Claims{
- Tier: TierFree,
- PlanVersion: "community",
- Limits: map[string]int64{"max_monitored_systems": 1, "max_guests": 5},
- MaxMonitoredSystems: 1,
- MaxGuests: 5,
+ Tier: TierFree,
+ PlanVersion: "community",
+ Limits: map[string]int64{"max_monitored_systems": 1, "max_guests": 5},
+ MaxGuests: 5,
},
expected: map[string]int64{},
},
{
- name: "self_hosted_pro_claims_strip_legacy_volume_caps",
+ name: "self_hosted_pro_claims_scrub_retired_monitored_system_cap",
claims: Claims{
- Tier: TierPro,
- Limits: map[string]int64{"max_monitored_systems": 15, "max_guests": 100},
- MaxMonitoredSystems: 15,
- MaxGuests: 100,
+ Tier: TierPro,
+ Limits: map[string]int64{"max_monitored_systems": 15, "max_guests": 100},
+ MaxGuests: 100,
},
expected: map[string]int64{},
},
{
- name: "generic_non_self_hosted_claims_preserve_explicit_limits_without_uncapped_marker",
+ name: "generic_non_self_hosted_claims_scrub_retired_monitored_system_cap",
claims: Claims{
- Limits: map[string]int64{"max_monitored_systems": 15, "max_guests": 100},
- MaxMonitoredSystems: 15,
- MaxGuests: 100,
+ Limits: map[string]int64{"max_monitored_systems": 15, "max_guests": 100},
+ MaxGuests: 100,
},
- expected: map[string]int64{"max_monitored_systems": 15, "max_guests": 100},
+ expected: map[string]int64{"max_guests": 100},
},
{
name: "grant_backed_self_hosted_uncapped_marker_strips_legacy_cap",
@@ -164,7 +155,6 @@ func TestClaims_EffectiveLimits(t *testing.T) {
Tier: TierPro,
CoreMonitoringUncapped: true,
Limits: map[string]int64{"max_monitored_systems": 15, "max_guests": 100},
- MaxMonitoredSystems: 15,
MaxGuests: 100,
},
expected: map[string]int64{},
@@ -270,7 +260,7 @@ func TestClaims_EntitlementPlanVersion(t *testing.T) {
}
}
-func TestClaims_EffectiveLimitsCanonicalizesCloudPlanLimits(t *testing.T) {
+func TestClaims_EffectiveLimitsScrubsRetiredCloudPlanMonitoringLimit(t *testing.T) {
claims := Claims{
Tier: TierCloud,
PlanVersion: "cloud-v1",
@@ -281,40 +271,36 @@ func TestClaims_EffectiveLimitsCanonicalizesCloudPlanLimits(t *testing.T) {
}
limits := claims.EffectiveLimits()
- if got := limits["max_monitored_systems"]; got != 10 {
- t.Fatalf("EffectiveLimits()[max_monitored_systems]=%d, want %d", got, 10)
+ if _, ok := limits["max_monitored_systems"]; ok {
+ t.Fatalf("EffectiveLimits retained retired max_monitored_systems: %v", limits)
}
if got := limits["max_guests"]; got != 5 {
t.Fatalf("EffectiveLimits()[max_guests]=%d, want %d", got, 5)
}
}
-func TestClaims_EffectiveLimitsPreservesNonCloudPlanLimits(t *testing.T) {
+func TestClaims_EffectiveLimitsScrubsRetiredNonCloudMonitoringLimit(t *testing.T) {
claims := Claims{
PlanVersion: "pro-v2",
Limits: map[string]int64{
"max_monitored_systems": 42,
+ "max_guests": 7,
},
}
limits := claims.EffectiveLimits()
- if got := limits["max_monitored_systems"]; got != 42 {
- t.Fatalf("EffectiveLimits()[max_monitored_systems]=%d, want %d", got, 42)
+ if _, ok := limits["max_monitored_systems"]; ok {
+ t.Fatalf("EffectiveLimits retained retired max_monitored_systems: %v", limits)
+ }
+ if got := limits["max_guests"]; got != 7 {
+ t.Fatalf("EffectiveLimits()[max_guests]=%d, want %d", got, 7)
}
}
-func TestLicenseStatusJSON_EncodesMonitoredSystemContinuity(t *testing.T) {
+func TestLicenseStatusJSON_OmitsRetiredMonitoredSystemFields(t *testing.T) {
status := LicenseStatus{
- Valid: true,
- Tier: TierPro,
- MaxMonitoredSystems: 23,
- MonitoredSystemContinuity: &MonitoredSystemContinuityStatus{
- PlanLimit: 10,
- GrandfatheredFloor: 23,
- EffectiveLimit: 23,
- CapturePending: false,
- CapturedAt: 123,
- },
+ Valid: true,
+ Tier: TierPro,
}
data, err := json.Marshal(status)
@@ -327,21 +313,10 @@ func TestLicenseStatusJSON_EncodesMonitoredSystemContinuity(t *testing.T) {
t.Fatalf("decode status json: %v", err)
}
- continuity, ok := decoded["monitored_system_continuity"].(map[string]any)
- if !ok {
- t.Fatalf("expected monitored_system_continuity object, got %#v", decoded["monitored_system_continuity"])
- }
- if got := continuity["plan_limit"]; got != float64(10) {
- t.Fatalf("plan_limit=%v, want %v", got, float64(10))
- }
- if got := continuity["effective_limit"]; got != float64(23) {
- t.Fatalf("effective_limit=%v, want %v", got, float64(23))
- }
- if got := continuity["grandfathered_floor"]; got != float64(23) {
- t.Fatalf("grandfathered_floor=%v, want %v", got, float64(23))
- }
- if got := continuity["capture_pending"]; got != false {
- t.Fatalf("capture_pending=%v, want false", got)
+ for _, key := range []string{"max_monitored_systems", "monitored_system_continuity"} {
+ if _, ok := decoded[key]; ok {
+ t.Fatalf("status JSON retained retired %q field: %v", key, decoded)
+ }
}
}
@@ -352,23 +327,27 @@ func TestClaims_EffectiveLimitsMissingCloudPlanFailsClosed(t *testing.T) {
}
limits := claims.EffectiveLimits()
- if got := limits["max_monitored_systems"]; got != int64(UnknownPlanDefaultMonitoredSystemLimit) {
- t.Fatalf("EffectiveLimits()[max_monitored_systems]=%d, want %d", got, UnknownPlanDefaultMonitoredSystemLimit)
+ if _, ok := limits["max_monitored_systems"]; ok {
+ t.Fatalf("EffectiveLimits retained retired max_monitored_systems: %v", limits)
}
}
-func TestClaims_EffectiveLimitsPreservesExplicitCustomCloudLimit(t *testing.T) {
+func TestClaims_EffectiveLimitsScrubsExplicitCustomCloudMonitoringLimit(t *testing.T) {
claims := Claims{
Tier: TierCloud,
PlanVersion: "custom_plan",
Limits: map[string]int64{
"max_monitored_systems": 42,
+ "max_guests": 7,
},
}
limits := claims.EffectiveLimits()
- if got := limits["max_monitored_systems"]; got != 42 {
- t.Fatalf("EffectiveLimits()[max_monitored_systems]=%d, want %d", got, 42)
+ if _, ok := limits["max_monitored_systems"]; ok {
+ t.Fatalf("EffectiveLimits retained retired max_monitored_systems: %v", limits)
+ }
+ if got := limits["max_guests"]; got != 7 {
+ t.Fatalf("EffectiveLimits()[max_guests]=%d, want %d", got, 7)
}
}
@@ -655,34 +634,28 @@ func TestLicense_AllFeatures(t *testing.T) {
func TestClaims_UnmarshalJSON_Migration(t *testing.T) {
tests := []struct {
- name string
- json string
- wantMax int
+ name string
+ json string
}{
{
- name: "new_key_only",
- json: `{"lid":"x","email":"a@b","tier":"pro","iat":1,"max_monitored_systems":10}`,
- wantMax: 10,
+ name: "new_key_only",
+ json: `{"lid":"x","email":"a@b","tier":"pro","iat":1,"max_monitored_systems":10}`,
},
{
- name: "legacy_key_only",
- json: `{"lid":"x","email":"a@b","tier":"pro","iat":1,"max_nodes":5}`,
- wantMax: 5,
+ name: "legacy_key_only",
+ json: `{"lid":"x","email":"a@b","tier":"pro","iat":1,"max_nodes":5}`,
},
{
- name: "both_keys_prefer_new",
- json: `{"lid":"x","email":"a@b","tier":"pro","iat":1,"max_monitored_systems":15,"max_nodes":5}`,
- wantMax: 15,
+ name: "both_keys_ignored",
+ json: `{"lid":"x","email":"a@b","tier":"pro","iat":1,"max_monitored_systems":15,"max_nodes":5}`,
},
{
- name: "new_key_zero_ignores_legacy",
- json: `{"lid":"x","email":"a@b","tier":"pro","iat":1,"max_monitored_systems":0,"max_nodes":5}`,
- wantMax: 0,
+ name: "new_key_zero_ignored",
+ json: `{"lid":"x","email":"a@b","tier":"pro","iat":1,"max_monitored_systems":0,"max_nodes":5}`,
},
{
- name: "neither_key",
- json: `{"lid":"x","email":"a@b","tier":"pro","iat":1}`,
- wantMax: 0,
+ name: "neither_key",
+ json: `{"lid":"x","email":"a@b","tier":"pro","iat":1}`,
},
}
for _, tt := range tests {
@@ -691,8 +664,8 @@ func TestClaims_UnmarshalJSON_Migration(t *testing.T) {
if err := json.Unmarshal([]byte(tt.json), &c); err != nil {
t.Fatalf("unmarshal: %v", err)
}
- if c.MaxMonitoredSystems != tt.wantMax {
- t.Fatalf("MaxMonitoredSystems = %d, want %d", c.MaxMonitoredSystems, tt.wantMax)
+ if _, ok := c.EffectiveLimits()["max_monitored_systems"]; ok {
+ t.Fatalf("EffectiveLimits retained retired max_monitored_systems: %v", c.EffectiveLimits())
}
})
}
diff --git a/pkg/licensing/monitored_system_limit.go b/pkg/licensing/monitored_system_limit.go
index 1ed6a117d..6f61eed7e 100644
--- a/pkg/licensing/monitored_system_limit.go
+++ b/pkg/licensing/monitored_system_limit.go
@@ -3,7 +3,6 @@ package licensing
import (
"crypto/sha256"
"encoding/hex"
- "fmt"
"strings"
"github.com/rcourtman/pulse-go-rewrite/internal/models"
@@ -12,23 +11,13 @@ import (
agentsk8s "github.com/rcourtman/pulse-go-rewrite/pkg/agents/kubernetes"
)
+// MaxMonitoredSystemsLicenseGateKey is retained only to identify and scrub
+// retired v5/v6 monitored-system volume-limit metadata from old licenses,
+// billing records, purchase-return state, and entitlement leases.
const MaxMonitoredSystemsLicenseGateKey = "max_monitored_systems"
-func ExceedsMonitoredSystemLimit(current, additions, limit int) bool {
- if limit <= 0 || additions <= 0 {
- return false
- }
- return current+additions > limit
-}
-
-func MonitoredSystemLimitExceededMessage(current, limit int) string {
- return fmt.Sprintf("Monitored-system capacity reached (%d/%d). Remove a monitored system before adding more.", current, limit)
-}
-
// InstalledUnifiedAgentCount returns the number of installed Pulse Unified
-// Agents. This is inventory-only metadata and is not the commercial counted
-// unit; monitored-system enforcement is derived from canonical unified
-// resources instead.
+// Agents. This is inventory-only metadata and is not a commercial counted unit.
func InstalledUnifiedAgentCount(state models.StateSnapshot) int {
return len(state.Hosts)
}
@@ -50,32 +39,13 @@ func NormalizeMonitoredSystemLimits(limits map[string]int64) map[string]int64 {
return nil
}
normalized := cloneInt64Map(limits)
- if value, ok := MonitoredSystemLimitValue(normalized); ok {
- normalized[MaxMonitoredSystemsLicenseGateKey] = value
- }
+ delete(normalized, MaxMonitoredSystemsLicenseGateKey)
for _, key := range legacyV5MonitoredSystemLimitAliasKeys {
delete(normalized, key)
}
return normalized
}
-func MonitoredSystemLimitValue(limits map[string]int64) (int64, bool) {
- if limits == nil {
- return 0, false
- }
- for _, key := range []string{MaxMonitoredSystemsLicenseGateKey} {
- if value, ok := limits[key]; ok {
- return value, true
- }
- }
- for _, key := range legacyV5MonitoredSystemLimitAliasKeys {
- if value, ok := limits[key]; ok {
- return value, true
- }
- }
- return 0, false
-}
-
func HostReportTargetsExistingHost(
snapshot models.StateSnapshot,
report agentshost.Report,
diff --git a/pkg/licensing/monitored_system_limit_test.go b/pkg/licensing/monitored_system_limit_test.go
index e924c0314..80035b18b 100644
--- a/pkg/licensing/monitored_system_limit_test.go
+++ b/pkg/licensing/monitored_system_limit_test.go
@@ -9,38 +9,6 @@ import (
agentsk8s "github.com/rcourtman/pulse-go-rewrite/pkg/agents/kubernetes"
)
-func TestExceedsMonitoredSystemLimit(t *testing.T) {
- tests := []struct {
- name string
- current int
- additions int
- limit int
- want bool
- }{
- {name: "unlimited", current: 10, additions: 1, limit: 0, want: false},
- {name: "no additions", current: 5, additions: 0, limit: 5, want: false},
- {name: "within limit", current: 4, additions: 1, limit: 5, want: false},
- {name: "at limit", current: 5, additions: 1, limit: 5, want: true},
- {name: "over limit", current: 6, additions: 1, limit: 5, want: true},
- }
-
- for _, tt := range tests {
- t.Run(tt.name, func(t *testing.T) {
- if got := ExceedsMonitoredSystemLimit(tt.current, tt.additions, tt.limit); got != tt.want {
- t.Fatalf("expected %t, got %t", tt.want, got)
- }
- })
- }
-}
-
-func TestMonitoredSystemLimitExceededMessage(t *testing.T) {
- got := MonitoredSystemLimitExceededMessage(6, 5)
- want := "Monitored-system capacity reached (6/5). Remove a monitored system before adding more."
- if got != want {
- t.Fatalf("expected %q, got %q", want, got)
- }
-}
-
func TestInstalledUnifiedAgentCount(t *testing.T) {
snapshot := models.StateSnapshot{
Hosts: []models.Host{{ID: "h1"}, {ID: "h2"}, {ID: "h3"}},
diff --git a/pkg/licensing/service.go b/pkg/licensing/service.go
index c183396f8..18da53c50 100644
--- a/pkg/licensing/service.go
+++ b/pkg/licensing/service.go
@@ -445,134 +445,6 @@ func (s *Service) RestoreActivation(state *ActivationState) error {
return nil
}
-// CaptureLegacyMonitoredSystemGrandfatherFloor resolves the one-time
-// monitored-system floor for a migrated legacy activation using the canonical
-// deduped monitored-system count observed at runtime.
-func (s *Service) CaptureLegacyMonitoredSystemGrandfatherFloor(count int) error {
- if count < 0 {
- count = 0
- }
-
- s.mu.Lock()
- if s.activationState == nil {
- s.mu.Unlock()
- return nil
- }
-
- continuity := normalizeActivationContinuity(s.activationState.Continuity)
- if !continuity.needsLegacyMonitoredSystemCapture() {
- s.mu.Unlock()
- return nil
- }
- if s.legacyMigrationUsesUncappedCoreMonitoringLocked() {
- s.mu.Unlock()
- return nil
- }
-
- currentLimit := 0
- if s.license != nil {
- currentLimit = monitoredSystemLimitFromClaims(s.license.Claims)
- }
- continuity.GrandfatheredMonitoredSystemsCapturedAt = time.Now().Unix()
- if count > currentLimit {
- continuity.GrandfatheredMaxMonitoredSystems = count
- }
- s.activationState.Continuity = continuity
-
- shouldNotify := false
- if s.license != nil {
- claims := cloneClaims(s.license.Claims)
- applyActivationContinuityToClaims(&claims, continuity)
- updatedLimit := monitoredSystemLimitFromClaims(claims)
- shouldNotify = updatedLimit != currentLimit
- s.license.Claims = claims
- source := NewTokenSource(&s.license.Claims)
- s.evaluator = NewEvaluator(source)
- }
-
- stateCopy := *s.activationState
- persistence := s.persistence
- cb := s.onLicenseChange
- activationCB := s.onActivationStateChange
- snapshot := cloneLicense(s.license)
- stateSnapshot := cloneActivationState(s.activationState)
- s.mu.Unlock()
-
- if persistence != nil {
- if err := persistence.SaveActivationState(&stateCopy); err != nil {
- return fmt.Errorf("persist activation continuity: %w", err)
- }
- }
-
- if shouldNotify && cb != nil {
- cb(snapshot)
- }
- if activationCB != nil {
- activationCB(stateSnapshot)
- }
-
- return nil
-}
-
-func (s *Service) needsLegacyMonitoredSystemCaptureLocked() bool {
- if s == nil || s.activationState == nil {
- return false
- }
- if s.legacyMigrationUsesUncappedCoreMonitoringLocked() {
- return false
- }
- return normalizeActivationContinuity(s.activationState.Continuity).needsLegacyMonitoredSystemCapture()
-}
-
-// NeedsLegacyMonitoredSystemCapture reports whether a migrated activation is
-// still waiting for its one-time monitored-system continuity capture.
-func (s *Service) NeedsLegacyMonitoredSystemCapture() bool {
- if s == nil {
- return false
- }
- s.mu.Lock()
- defer s.mu.Unlock()
- return s.needsLegacyMonitoredSystemCaptureLocked()
-}
-
-func (s *Service) monitoredSystemContinuityStatusLocked() *MonitoredSystemContinuityStatus {
- if s == nil || s.activationState == nil {
- return nil
- }
-
- continuity := normalizeActivationContinuity(s.activationState.Continuity)
- if !continuity.LegacyMigration {
- return nil
- }
- if s.legacyMigrationUsesUncappedCoreMonitoringLocked() {
- return nil
- }
-
- planLimit := 0
- if gc, err := verifyAndParseGrantJWT(s.activationState.GrantJWT); err == nil && gc != nil {
- planLimit = gc.MaxMonitoredSystems
- }
-
- effectiveLimit := planLimit
- if s.license != nil {
- // When a license is present, trust its claims. Self-hosted uncapped
- // tiers return 0 here as a first-class "unlimited" signal, not a
- // missing-data sentinel — do not fall back to the grant's plan limit.
- effectiveLimit = monitoredSystemLimitFromClaims(s.license.Claims)
- }
-
- status := &MonitoredSystemContinuityStatus{
- PlanLimit: planLimit,
- EffectiveLimit: effectiveLimit,
- CapturePending: continuity.needsLegacyMonitoredSystemCapture(),
- CapturedAt: continuity.GrandfatheredMonitoredSystemsCapturedAt,
- }
- if continuity.GrandfatheredMaxMonitoredSystems > 0 {
- status.GrandfatheredFloor = continuity.GrandfatheredMaxMonitoredSystems
- }
- return status
-}
-
// Clear removes the current license.
// If an activation-key license is present, it also stops the refresh loop and clears the state.
func (s *Service) Clear() {
@@ -787,9 +659,6 @@ func (s *Service) Status() *LicenseStatus {
}
status.Features = unionFeatures(TierFeatures[TierFree], evaluatorFeatures(s.evaluator))
- if maxSystems, ok := s.evaluator.GetLimit(MaxMonitoredSystemsLicenseGateKey); ok {
- status.MaxMonitoredSystems = safeIntFromInt64(maxSystems)
- }
if maxGuests, ok := s.evaluator.GetLimit("max_guests"); ok {
status.MaxGuests = safeIntFromInt64(maxGuests)
}
@@ -798,18 +667,11 @@ func (s *Service) Status() *LicenseStatus {
status.Valid = false
// Keep effective capabilities free-tier only when subscription is not entitled.
status.Features = append([]string(nil), TierFeatures[TierFree]...)
- if defaultSystems := TierMonitoredSystemLimits[TierFree]; defaultSystems > 0 {
- status.MaxMonitoredSystems = defaultSystems
- }
status.MaxGuests = 0
}
} else {
- // No license, no evaluator — apply the free-tier monitored-system limit.
- if defaultSystems := TierMonitoredSystemLimits[TierFree]; defaultSystems > 0 {
- status.MaxMonitoredSystems = defaultSystems
- }
+ // No license, no evaluator: Community remains active for core monitoring.
}
- status.MonitoredSystemContinuity = s.monitoredSystemContinuityStatusLocked()
return status
}
@@ -820,24 +682,9 @@ func (s *Service) Status() *LicenseStatus {
status.DaysRemaining = s.license.DaysRemaining()
status.Features = s.license.AllFeatures()
- if maxSystems, ok := s.license.Claims.EffectiveLimits()[MaxMonitoredSystemsLicenseGateKey]; ok {
- status.MaxMonitoredSystems = safeIntFromInt64(maxSystems)
- }
if maxGuests, ok := s.license.Claims.EffectiveLimits()["max_guests"]; ok {
status.MaxGuests = safeIntFromInt64(maxGuests)
}
- status.MonitoredSystemContinuity = s.monitoredSystemContinuityStatusLocked()
-
- // Apply the tier default monitored-system limit when claims don't specify one.
- // For recognized tiers, use their defined limit (0 = unlimited for Cloud/MSP/Enterprise).
- // For unrecognized tiers, fall back to free tier limit to prevent unlimited access.
- if status.MaxMonitoredSystems == 0 && !IsGrandfatheredRecurringV5PlanVersion(status.PlanVersion) {
- if defaultSystems, ok := TierMonitoredSystemLimits[status.Tier]; ok {
- status.MaxMonitoredSystems = defaultSystems
- } else {
- status.MaxMonitoredSystems = TierMonitoredSystemLimits[TierFree]
- }
- }
if s.license.ExpiresAt() != nil {
exp := s.license.ExpiresAt().Format(time.RFC3339)
@@ -857,12 +704,6 @@ func (s *Service) Status() *LicenseStatus {
default:
status.Valid = false
status.Features = append([]string(nil), TierFeatures[TierFree]...)
- // Downgrade limits to the free tier when subscription is not entitled.
- if defaultSystems := TierMonitoredSystemLimits[TierFree]; defaultSystems > 0 {
- status.MaxMonitoredSystems = defaultSystems
- } else {
- status.MaxMonitoredSystems = 0
- }
status.MaxGuests = 0
}
@@ -967,28 +808,6 @@ func safeIntFromInt64(v int64) int {
return int(v)
}
-func monitoredSystemLimitFromClaims(claims Claims) int {
- if limit, ok := claims.EffectiveLimits()[MaxMonitoredSystemsLicenseGateKey]; ok {
- return safeIntFromInt64(limit)
- }
- return 0
-}
-
-func (s *Service) legacyMigrationUsesUncappedCoreMonitoringLocked() bool {
- if s == nil || s.activationState == nil {
- return false
- }
- if s.license != nil && s.license.Claims.shouldScrubLegacyCommercialCaps() {
- return true
- }
- gc, err := verifyAndParseGrantJWT(s.activationState.GrantJWT)
- if err != nil || gc == nil {
- return false
- }
- return IsSelfHostedCoreMonitoringUncappedTier(Tier(gc.Tier)) ||
- IsSelfHostedCoreMonitoringUncappedPlanVersion(gc.PlanKey)
-}
-
func remainingDaysCeil(expiresAtUnix, nowUnix int64) int {
deltaSeconds := expiresAtUnix - nowUnix
if deltaSeconds <= 0 {
diff --git a/pkg/licensing/service_activate_test.go b/pkg/licensing/service_activate_test.go
index 7c68e5546..bfbc7499c 100644
--- a/pkg/licensing/service_activate_test.go
+++ b/pkg/licensing/service_activate_test.go
@@ -216,13 +216,12 @@ func TestServiceActivate_ExchangedLegacyJWTMarksLegacyMigrationContinuity(t *tes
}
grantJWT := makeTestGrantJWT(t, &GrantClaims{
- LicenseID: "lic_continuity",
- Tier: "pro",
- PlanKey: "v5_pro_monthly_grandfathered",
- State: "active",
- MaxMonitoredSystems: 10,
- IssuedAt: time.Now().Unix(),
- ExpiresAt: time.Now().Add(72 * time.Hour).Unix(),
+ LicenseID: "lic_continuity",
+ Tier: "pro",
+ PlanKey: "v5_pro_monthly_grandfathered",
+ State: "active",
+ IssuedAt: time.Now().Unix(),
+ ExpiresAt: time.Now().Add(72 * time.Hour).Unix(),
})
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -232,10 +231,9 @@ func TestServiceActivate_ExchangedLegacyJWTMarksLegacyMigrationContinuity(t *tes
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(ActivateInstallationResponse{
License: ActivateResponseLicense{
- LicenseID: "lic_continuity",
- State: "active",
- Tier: "pro",
- MaxMonitoredSystems: 10,
+ LicenseID: "lic_continuity",
+ State: "active",
+ Tier: "pro",
},
Installation: ActivateResponseInstallation{
InstallationID: "inst_continuity",
@@ -264,9 +262,6 @@ func TestServiceActivate_ExchangedLegacyJWTMarksLegacyMigrationContinuity(t *tes
if !state.Continuity.LegacyMigration {
t.Fatal("expected legacy exchange to mark legacy migration continuity")
}
- if state.Continuity.GrandfatheredMonitoredSystemsCapturedAt != 0 {
- t.Fatalf("GrandfatheredMonitoredSystemsCapturedAt=%d, want 0 before capture", state.Continuity.GrandfatheredMonitoredSystemsCapturedAt)
- }
}
func TestServiceActivate_CallsActivationStateChangeCallback(t *testing.T) {
@@ -280,13 +275,12 @@ func TestServiceActivate_CallsActivationStateChangeCallback(t *testing.T) {
}
grantJWT := makeTestGrantJWT(t, &GrantClaims{
- LicenseID: "lic_activation_callback",
- Tier: "pro",
- PlanKey: "v5_pro_monthly_grandfathered",
- State: "active",
- MaxMonitoredSystems: 10,
- IssuedAt: time.Now().Unix(),
- ExpiresAt: time.Now().Add(72 * time.Hour).Unix(),
+ LicenseID: "lic_activation_callback",
+ Tier: "pro",
+ PlanKey: "v5_pro_monthly_grandfathered",
+ State: "active",
+ IssuedAt: time.Now().Unix(),
+ ExpiresAt: time.Now().Add(72 * time.Hour).Unix(),
})
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -296,10 +290,9 @@ func TestServiceActivate_CallsActivationStateChangeCallback(t *testing.T) {
w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(ActivateInstallationResponse{
License: ActivateResponseLicense{
- LicenseID: "lic_activation_callback",
- State: "active",
- Tier: "pro",
- MaxMonitoredSystems: 10,
+ LicenseID: "lic_activation_callback",
+ State: "active",
+ Tier: "pro",
},
Installation: ActivateResponseInstallation{
InstallationID: "inst_activation_callback",
@@ -340,13 +333,12 @@ func TestServiceRestoreActivation_CallsActivationStateChangeCallback(t *testing.
setupTestPublicKey(t)
grantJWT := makeTestGrantJWT(t, &GrantClaims{
- LicenseID: "lic_restore_callback",
- Tier: "pro",
- PlanKey: "v5_pro_monthly_grandfathered",
- State: "active",
- MaxMonitoredSystems: 10,
- IssuedAt: time.Now().Unix(),
- ExpiresAt: time.Now().Add(72 * time.Hour).Unix(),
+ LicenseID: "lic_restore_callback",
+ Tier: "pro",
+ PlanKey: "v5_pro_monthly_grandfathered",
+ State: "active",
+ IssuedAt: time.Now().Unix(),
+ ExpiresAt: time.Now().Add(72 * time.Hour).Unix(),
})
svc := NewService()
@@ -383,109 +375,27 @@ func TestServiceRestoreActivation_CallsActivationStateChangeCallback(t *testing.
}
}
-func TestServiceCaptureLegacyMonitoredSystemGrandfatherFloorSkipsUncappedSelfHosted(t *testing.T) {
- setupTestPublicKey(t)
-
- tmpDir, err := os.MkdirTemp("", "pulse-service-floor-*")
- if err != nil {
- t.Fatalf("create temp dir: %v", err)
- }
- defer os.RemoveAll(tmpDir)
-
- p, err := NewPersistence(tmpDir)
- if err != nil {
- t.Fatalf("create persistence: %v", err)
- }
-
- initialGrantJWT := makeTestGrantJWT(t, &GrantClaims{
- LicenseID: "lic_floor",
- Tier: "pro",
- PlanKey: "legacy_migration_fallback",
- State: "active",
- MaxMonitoredSystems: 10,
- IssuedAt: time.Now().Unix(),
- ExpiresAt: time.Now().Add(72 * time.Hour).Unix(),
- })
-
- svc := NewService()
- svc.SetPersistence(p)
- var callbackState *ActivationState
- svc.SetActivationStateChangeCallback(func(state *ActivationState) {
- callbackState = state
- })
- state := &ActivationState{
- InstallationID: "inst_floor",
- InstallationToken: "pit_live_floor",
- LicenseID: "lic_floor",
- GrantJWT: initialGrantJWT,
- GrantJTI: "grant_floor",
- InstanceFingerprint: "fp-floor",
- Continuity: ActivationContinuity{
- LegacyMigration: true,
- },
- }
- if err := svc.RestoreActivation(state); err != nil {
- t.Fatalf("RestoreActivation: %v", err)
- }
-
- callbackState = nil
- if err := svc.CaptureLegacyMonitoredSystemGrandfatherFloor(23); err != nil {
- t.Fatalf("CaptureLegacyMonitoredSystemGrandfatherFloor: %v", err)
- }
- if callbackState != nil {
- t.Fatalf("expected no activation-state callback for uncapped self-hosted capture, got %+v", callbackState)
- }
-
- status := svc.Status()
- if status.MaxMonitoredSystems != 0 {
- t.Fatalf("status.MaxMonitoredSystems=%d, want 0 (uncapped self-hosted)", status.MaxMonitoredSystems)
- }
- if status.MonitoredSystemContinuity != nil {
- t.Fatalf("expected no monitored-system continuity banner for uncapped self-hosted migration, got %+v", status.MonitoredSystemContinuity)
- }
-
- current := svc.Current()
- if current == nil {
- t.Fatal("expected current license")
- }
- if got, ok := current.Claims.EffectiveLimits()[MaxMonitoredSystemsLicenseGateKey]; ok {
- t.Fatalf("EffectiveLimits()[max_monitored_systems]=%d present, want absent (uncapped self-hosted)", got)
- }
-}
-
-func TestServiceStatus_HidesMonitoredSystemContinuityForSelfHostedFallbackMigrations(t *testing.T) {
+func TestServiceStatus_RetiredMonitoredSystemContinuityDoesNotSurfaceForSelfHostedFallbackMigrations(t *testing.T) {
setupTestPublicKey(t)
grantJWT := makeTestGrantJWT(t, &GrantClaims{
- LicenseID: "lic_continuity_status",
- Tier: "pro",
- PlanKey: "legacy_migration_fallback",
- State: "active",
- MaxMonitoredSystems: 10,
- IssuedAt: time.Now().Unix(),
- ExpiresAt: time.Now().Add(72 * time.Hour).Unix(),
+ LicenseID: "lic_continuity_status",
+ Tier: "pro",
+ PlanKey: "legacy_migration_fallback",
+ State: "active",
+ IssuedAt: time.Now().Unix(),
+ ExpiresAt: time.Now().Add(72 * time.Hour).Unix(),
})
tests := []struct {
- name string
- continuity ActivationContinuity
- wantMaxSystems int
+ name string
+ continuity ActivationContinuity
}{
{
name: "pending capture",
continuity: ActivationContinuity{
LegacyMigration: true,
},
- wantMaxSystems: 0,
- },
- {
- name: "captured floor",
- continuity: ActivationContinuity{
- LegacyMigration: true,
- GrandfatheredMaxMonitoredSystems: 23,
- GrandfatheredMonitoredSystemsCapturedAt: 123,
- },
- wantMaxSystems: 0,
},
}
@@ -505,11 +415,13 @@ func TestServiceStatus_HidesMonitoredSystemContinuityForSelfHostedFallbackMigrat
}
status := svc.Status()
- if status.MaxMonitoredSystems != tt.wantMaxSystems {
- t.Fatalf("status.MaxMonitoredSystems=%d, want %d", status.MaxMonitoredSystems, tt.wantMaxSystems)
+ if status == nil || !status.Valid {
+ t.Fatalf("expected valid status, got %+v", status)
}
- if status.MonitoredSystemContinuity != nil {
- t.Fatalf("expected no monitored-system continuity banner for uncapped self-hosted migration, got %+v", status.MonitoredSystemContinuity)
+ if current := svc.Current(); current == nil {
+ t.Fatal("expected current license")
+ } else if _, ok := current.Claims.EffectiveLimits()[MaxMonitoredSystemsLicenseGateKey]; ok {
+ t.Fatalf("EffectiveLimits retained retired max_monitored_systems: %v", current.Claims.EffectiveLimits())
}
})
}
@@ -519,14 +431,12 @@ func TestServiceStatus_GrandfatheredRecurringV5IsUncapped(t *testing.T) {
setupTestPublicKey(t)
grantJWT := makeTestGrantJWT(t, &GrantClaims{
- LicenseID: "lic_recurring_grandfathered",
- Tier: "pro",
- PlanKey: "v5_pro_monthly_grandfathered",
- State: "active",
- MaxMonitoredSystems: 10,
- MaxGuests: 5,
- IssuedAt: time.Now().Unix(),
- ExpiresAt: time.Now().Add(72 * time.Hour).Unix(),
+ LicenseID: "lic_recurring_grandfathered",
+ Tier: "pro",
+ PlanKey: "v5_pro_monthly_grandfathered",
+ State: "active",
+ IssuedAt: time.Now().Unix(),
+ ExpiresAt: time.Now().Add(72 * time.Hour).Unix(),
})
svc := NewService()
@@ -545,11 +455,8 @@ func TestServiceStatus_GrandfatheredRecurringV5IsUncapped(t *testing.T) {
}
status := svc.Status()
- if status.MaxMonitoredSystems != 0 {
- t.Fatalf("status.MaxMonitoredSystems=%d, want 0 for uncapped grandfathered recurring plan", status.MaxMonitoredSystems)
- }
- if status.MonitoredSystemContinuity != nil {
- t.Fatalf("expected no monitored-system continuity banner for uncapped recurring v5 migration, got %+v", status.MonitoredSystemContinuity)
+ if status == nil || !status.Valid {
+ t.Fatalf("expected valid grandfathered status, got %+v", status)
}
current := svc.Current()
@@ -574,7 +481,7 @@ func TestServiceActivate_RejectsMalformedLegacyKeyOutsideDevMode(t *testing.T) {
}
}
-func TestServiceStatusCanonicalizesJWTCloudPlanVersionAndLimits(t *testing.T) {
+func TestServiceStatusCanonicalizesJWTCloudPlanVersionAndScrubsRetiredMonitoringLimit(t *testing.T) {
svc := NewService()
svc.license = &License{
Claims: Claims{
@@ -591,9 +498,6 @@ func TestServiceStatusCanonicalizesJWTCloudPlanVersionAndLimits(t *testing.T) {
if status.PlanVersion != "cloud_starter" {
t.Fatalf("status.PlanVersion=%q, want %q", status.PlanVersion, "cloud_starter")
}
- if status.MaxMonitoredSystems != 10 {
- t.Fatalf("status.MaxMonitoredSystems=%d, want %d", status.MaxMonitoredSystems, 10)
- }
}
func TestServiceStatusMissingJWTCloudPlanFailsClosed(t *testing.T) {
@@ -610,9 +514,6 @@ func TestServiceStatusMissingJWTCloudPlanFailsClosed(t *testing.T) {
if status.PlanVersion != "" {
t.Fatalf("status.PlanVersion=%q, want empty", status.PlanVersion)
}
- if status.MaxMonitoredSystems != UnknownPlanDefaultMonitoredSystemLimit {
- t.Fatalf("status.MaxMonitoredSystems=%d, want %d", status.MaxMonitoredSystems, UnknownPlanDefaultMonitoredSystemLimit)
- }
}
func TestServiceStatus_DevModeKeepsCustomerFacingStatusCommunityWithoutLicense(t *testing.T) {
diff --git a/pkg/licensing/token_source.go b/pkg/licensing/token_source.go
index ba5234241..7e9d8a27b 100644
--- a/pkg/licensing/token_source.go
+++ b/pkg/licensing/token_source.go
@@ -28,7 +28,7 @@ func (t *TokenSource) Capabilities() []string {
return t.claims.EffectiveCapabilities()
}
-// Limits returns effective limits (explicit or derived from MaxMonitoredSystems/MaxGuests).
+// Limits returns effective limits from explicit claims and derived guest policy.
func (t *TokenSource) Limits() map[string]int64 {
if t == nil || t.claims == nil {
return nil
diff --git a/pkg/server/server.go b/pkg/server/server.go
index 144e13363..871332454 100644
--- a/pkg/server/server.go
+++ b/pkg/server/server.go
@@ -97,11 +97,6 @@ type BusinessHooks struct {
// CreateAlertAnalyzer creates the premium alert-triggered analyzer.
// Returns nil in OSS. Enterprise provides a concrete implementation.
CreateAlertAnalyzer func(deps aicontracts.AlertAnalyzerDeps) aicontracts.AlertAnalyzer
-
- // ResolveMonitoredSystemAdmissionPolicy allows private builds to own the
- // commercial monitored-system admission decision without importing internal
- // API packages. Public runtime still owns counted-system projection.
- ResolveMonitoredSystemAdmissionPolicy extensions.ResolveMonitoredSystemAdmissionPolicyFunc
}
var (
@@ -272,7 +267,6 @@ func Run(ctx context.Context, version string) error {
createInvestigationStore := globalHooks.CreateInvestigationStore
createInvestigationOrchestrator := globalHooks.CreateInvestigationOrchestrator
createAlertAnalyzer := globalHooks.CreateAlertAnalyzer
- resolveMonitoredSystemAdmissionPolicy := globalHooks.ResolveMonitoredSystemAdmissionPolicy
globalHooksMu.Unlock()
api.SetAIInvestigationEnabled(aiInvestigationEnabled)
@@ -280,7 +274,6 @@ func Run(ctx context.Context, version string) error {
api.SetCreateInvestigationStore(createInvestigationStore)
api.SetCreateInvestigationOrchestrator(createInvestigationOrchestrator)
api.SetCreateAlertAnalyzer(createAlertAnalyzer)
- api.SetResolveMonitoredSystemAdmissionPolicy(resolveMonitoredSystemAdmissionPolicy)
api.SetRBACAdminEndpointsBinder(bindRBACAdminEndpoints)
api.SetAuditAdminEndpointsBinder(bindAuditAdminEndpoints)
api.SetSSOAdminEndpointsBinder(bindSSOAdminEndpoints)
diff --git a/pkg/server/server_test.go b/pkg/server/server_test.go
index fa078b50a..c0b154796 100644
--- a/pkg/server/server_test.go
+++ b/pkg/server/server_test.go
@@ -9,30 +9,17 @@ import (
"time"
"github.com/rcourtman/pulse-go-rewrite/internal/config"
- "github.com/rcourtman/pulse-go-rewrite/pkg/extensions"
"github.com/rcourtman/pulse-go-rewrite/pkg/metrics"
)
func TestBusinessHooks(t *testing.T) {
called := false
- resolveCalled := false
hook := func(store *metrics.Store) {
called = true
}
SetBusinessHooks(BusinessHooks{
OnMetricsStoreReady: hook,
- ResolveMonitoredSystemAdmissionPolicy: func(_ context.Context, input extensions.MonitoredSystemAdmissionInput) extensions.MonitoredSystemAdmissionDecision {
- resolveCalled = true
- return extensions.MonitoredSystemAdmissionDecision{
- Current: input.Current,
- Additional: input.Additional,
- Limit: input.Limit,
- UsageAvailable: input.UsageAvailable,
- UsageUnavailableReason: input.UsageUnavailableReason,
- Exceeded: input.UsageAvailable && input.Additional > 0 && input.Limit > 0 && input.Current+input.Additional > input.Limit,
- }
- },
})
globalHooksMu.Lock()
@@ -48,22 +35,6 @@ func TestBusinessHooks(t *testing.T) {
t.Error("expected hook to be called")
}
- if globalHooks.ResolveMonitoredSystemAdmissionPolicy == nil {
- t.Error("expected ResolveMonitoredSystemAdmissionPolicy to be set")
- }
-
- decision := globalHooks.ResolveMonitoredSystemAdmissionPolicy(context.Background(), extensions.MonitoredSystemAdmissionInput{
- Current: 5,
- Additional: 1,
- Limit: 5,
- UsageAvailable: true,
- })
- if !resolveCalled {
- t.Error("expected monitored-system admission policy hook to be called")
- }
- if !decision.Exceeded {
- t.Fatalf("expected exceeded decision from hook, got %+v", decision)
- }
}
func TestPerformAutoImport_Success(t *testing.T) {