Remove stale cap framing from uncapped self-hosted plans

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