Canonicalize self-hosted pricing presentation

This commit is contained in:
rcourtman
2026-03-23 16:21:08 +00:00
parent 8adbdbc058
commit 9ec8f85af2
11 changed files with 505 additions and 251 deletions
@@ -52,19 +52,23 @@ agreement, and cloud-specific enforcement rules.
30. `frontend-modern/src/components/Settings/BillingAdminOrganizationsTable.tsx`
31. `frontend-modern/src/components/Settings/OrganizationBillingPanel.tsx`
32. `frontend-modern/src/components/Settings/OrganizationBillingLoadingState.tsx`
33. `frontend-modern/src/components/Settings/ProLicensePanel.tsx`
34. `frontend-modern/src/components/Settings/ProLicensePlanSection.tsx`
35. `frontend-modern/src/components/Settings/CommercialBillingSections.tsx`
36. `frontend-modern/src/components/Settings/SelfHostedCommercialActivationSection.tsx`
37. `frontend-modern/src/components/Settings/RelaySettingsPanel.tsx`
38. `frontend-modern/src/components/Settings/RelayPairingSection.tsx`
39. `frontend-modern/src/components/Settings/useBillingAdminPanelState.ts`
40. `frontend-modern/src/components/Settings/useOrganizationBillingPanelState.ts`
41. `frontend-modern/src/components/Settings/useProLicensePanelState.ts`
42. `frontend-modern/src/components/Settings/useRelaySettingsPanelState.ts`
43. `frontend-modern/src/pages/CloudPricing.tsx`
44. `frontend-modern/src/utils/apiClient.ts`
45. `frontend-modern/src/utils/commercialBillingModel.ts`
33. `frontend-modern/src/components/Settings/MonitoredSystemLedgerPanel.tsx`
34. `frontend-modern/src/components/Settings/ProLicensePanel.tsx`
35. `frontend-modern/src/components/Settings/ProLicensePlanSection.tsx`
36. `frontend-modern/src/components/Settings/CommercialBillingSections.tsx`
37. `frontend-modern/src/components/Settings/SelfHostedCommercialActivationSection.tsx`
38. `frontend-modern/src/components/Settings/RelaySettingsPanel.tsx`
39. `frontend-modern/src/components/Settings/RelayPairingSection.tsx`
40. `frontend-modern/src/components/Settings/useBillingAdminPanelState.ts`
41. `frontend-modern/src/components/Settings/useOrganizationBillingPanelState.ts`
42. `frontend-modern/src/components/Settings/useProLicensePanelState.ts`
43. `frontend-modern/src/components/Settings/useRelaySettingsPanelState.ts`
44. `frontend-modern/src/pages/CloudPricing.tsx`
45. `frontend-modern/src/pages/PricingV6.tsx`
46. `frontend-modern/src/utils/apiClient.ts`
47. `frontend-modern/src/utils/cloudPlans.ts`
48. `frontend-modern/src/utils/commercialBillingModel.ts`
49. `frontend-modern/src/utils/selfHostedPlans.ts`
## Shared Boundaries
@@ -299,6 +303,14 @@ the monitored-system model as well. `ProLicensePanel.tsx`,
retail capacity as monitored systems rather than agents for Community, Relay,
Pro, and Pro+, while leaving Cloud/MSP pricing semantics unchanged and
preserving grandfathered v5 continuity copy as an explicit boundary policy.
That same pricing boundary now also owns the shared frontend plan-definition
models. `frontend-modern/src/utils/cloudPlans.ts` and
`frontend-modern/src/utils/selfHostedPlans.ts` are the canonical frontend
sources for cloud and self-hosted plan copy, limits, and comparison metadata,
while `frontend-modern/src/pages/CloudPricing.tsx`,
`frontend-modern/src/pages/HostedSignup.tsx`, `frontend-modern/src/pages/PricingV6.tsx`,
and the self-hosted billing settings surfaces must consume those shared owners
instead of redefining retail plan facts or counted-unit policy locally.
That contract applies to both plan summary labels and upgrade/paywall copy:
current v6 self-hosted pricing may not drift back to the older `$49/yr Relay`,
`$99/yr Pro`, or monitored-system-count marketing drift that contradicts the
@@ -1283,6 +1283,7 @@
"frontend-modern/src/components/Dashboard/useRelayOnboardingCardState.ts",
"frontend-modern/src/components/Settings/BillingAdminOrganizationsTable.tsx",
"frontend-modern/src/components/Settings/BillingAdminPanel.tsx",
"frontend-modern/src/components/Settings/MonitoredSystemLedgerPanel.tsx",
"frontend-modern/src/components/Settings/OrganizationBillingLoadingState.tsx",
"frontend-modern/src/components/Settings/OrganizationBillingPanel.tsx",
"frontend-modern/src/components/Settings/ProLicensePanel.tsx",
@@ -1295,8 +1296,12 @@
"frontend-modern/src/components/Settings/useRelaySettingsPanelState.ts",
"frontend-modern/src/pages/CloudPricing.tsx",
"frontend-modern/src/pages/HostedSignup.tsx",
"frontend-modern/src/pages/PricingV6.tsx",
"frontend-modern/src/useAppRuntimeState.ts",
"frontend-modern/src/utils/apiClient.ts",
"frontend-modern/src/utils/cloudPlans.ts",
"frontend-modern/src/utils/commercialBillingModel.ts",
"frontend-modern/src/utils/selfHostedPlans.ts",
"internal/api/public_signup_handlers.go",
"internal/cloudcp/account/tenant_handlers.go",
"internal/cloudcp/entitlements/service.go",
@@ -1712,6 +1717,7 @@
"label": "pro license surface proof",
"match_prefixes": [],
"match_files": [
"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/useProLicensePanelState.ts"
@@ -1719,9 +1725,30 @@
"allow_same_subsystem_tests": false,
"test_prefixes": [],
"exact_files": [
"frontend-modern/src/components/Settings/__tests__/MonitoredSystemLedgerPanel.test.tsx",
"frontend-modern/src/components/Settings/__tests__/ProLicensePanel.test.tsx"
]
},
{
"id": "commercial-plan-models",
"label": "commercial plan model proof",
"match_prefixes": [],
"match_files": [
"frontend-modern/src/utils/cloudPlans.ts",
"frontend-modern/src/utils/commercialBillingModel.ts",
"frontend-modern/src/utils/selfHostedPlans.ts"
],
"allow_same_subsystem_tests": false,
"test_prefixes": [],
"exact_files": [
"frontend-modern/src/components/Settings/__tests__/OrganizationBillingPanel.test.tsx",
"frontend-modern/src/components/Settings/__tests__/ProLicensePanel.test.tsx",
"frontend-modern/src/pages/__tests__/CloudPricing.test.tsx",
"frontend-modern/src/pages/__tests__/HostedSignup.test.tsx",
"frontend-modern/src/pages/__tests__/PricingV6.test.tsx",
"frontend-modern/src/utils/__tests__/selfHostedPlans.test.ts"
]
},
{
"id": "relay-frontend-surfaces",
"label": "relay frontend surface proof",
@@ -1781,14 +1808,16 @@
"match_prefixes": [],
"match_files": [
"frontend-modern/src/pages/CloudPricing.tsx",
"frontend-modern/src/pages/HostedSignup.tsx"
"frontend-modern/src/pages/HostedSignup.tsx",
"frontend-modern/src/pages/PricingV6.tsx"
],
"allow_same_subsystem_tests": false,
"test_prefixes": [],
"exact_files": [
"frontend-modern/src/api/__tests__/hostedSignup.test.ts",
"frontend-modern/src/pages/__tests__/CloudPricing.test.tsx",
"frontend-modern/src/pages/__tests__/HostedSignup.test.tsx"
"frontend-modern/src/pages/__tests__/HostedSignup.test.tsx",
"frontend-modern/src/pages/__tests__/PricingV6.test.tsx"
]
}
]
@@ -18,6 +18,10 @@ import {
getMonitoredSystemLedgerLoadingState,
} from '@/utils/unifiedAgentInventoryPresentation';
import { PulseLogoIcon } from '@/components/icons/PulseLogoIcon';
import {
SELF_HOSTED_MONITORED_SYSTEM_LEDGER_DESCRIPTION,
SELF_HOSTED_MONITORED_SYSTEMS_DESCRIPTION,
} from '@/utils/selfHostedPlans';
interface MonitoredSystemLedgerPanelProps {
embedded?: boolean;
@@ -58,8 +62,7 @@ export function MonitoredSystemLedgerPanel(props: MonitoredSystemLedgerPanelProp
</Show>
</div>
<p class="text-xs text-muted">
Pulse sells monitored systems, not everything underneath them. VMs, containers, pods,
disks, backups, and other child resources do not count separately.
{SELF_HOSTED_MONITORED_SYSTEMS_DESCRIPTION}
</p>
</div>
@@ -152,7 +155,7 @@ export function MonitoredSystemLedgerPanel(props: MonitoredSystemLedgerPanelProp
return (
<SettingsPanel
title="Monitored System Ledger"
description="Review the monitored systems currently counting toward your Pulse Pro allocation."
description={SELF_HOSTED_MONITORED_SYSTEM_LEDGER_DESCRIPTION}
icon={<PulseLogoIcon class="w-5 h-5" />}
bodyClass="space-y-4"
>
@@ -6,6 +6,7 @@ import { CommercialBillingShell, CommercialSection } from './CommercialBillingSe
import { ProLicensePlanSection } from './ProLicensePlanSection';
import { SelfHostedCommercialActivationSection } from './SelfHostedCommercialActivationSection';
import { useProLicensePanelState } from './useProLicensePanelState';
import { SELF_HOSTED_MONITORED_SYSTEMS_DESCRIPTION } from '@/utils/selfHostedPlans';
export const ProLicensePanel: Component = () => {
const state = useProLicensePanelState();
@@ -51,7 +52,7 @@ export const ProLicensePanel: Component = () => {
<CommercialSection
title="Usage"
description="Self-hosted plans are sold by monitored systems. Child resources like VMs, containers, pods, disks, and backups do not count separately."
description={SELF_HOSTED_MONITORED_SYSTEMS_DESCRIPTION}
>
<MonitoredSystemLedgerPanel embedded />
</CommercialSection>
@@ -147,6 +147,9 @@ describe('MonitoredSystemLedgerPanel', () => {
'Review the monitored systems currently counting toward your Pulse Pro allocation.',
),
).toBeInTheDocument();
expect(
screen.getByText(/a monitored system is a top-level machine or cluster pulse actively monitors/i),
).toBeInTheDocument();
expect(screen.getByText('server-b')).toBeInTheDocument();
expect(screen.getByText('2 / 10')).toBeInTheDocument();
expect(screen.queryByText('Failed to load monitored system ledger.')).not.toBeInTheDocument();
@@ -148,15 +148,15 @@ describe('ProLicensePanel', () => {
expect(screen.getByText('Plan Terms')).toBeInTheDocument();
});
expect(screen.getByText('5 / 12')).toBeInTheDocument();
expect(screen.getByText('7')).toBeInTheDocument();
expect(screen.getAllByText('Active').length).toBeGreaterThan(0);
expect(screen.getByText('V5 Lifetime Grandfathered')).toBeInTheDocument();
expect(screen.getByText('Included Monitored Systems')).toBeInTheDocument();
expect(
screen.getByText(/self-hosted plans are sold by monitored systems/i),
).toBeInTheDocument();
});
expect(screen.getByText('5 / 12')).toBeInTheDocument();
expect(screen.getByText('7')).toBeInTheDocument();
expect(screen.getAllByText('Active').length).toBeGreaterThan(0);
expect(screen.getByText('V5 Lifetime Grandfathered')).toBeInTheDocument();
expect(screen.getByText('Included Monitored Systems')).toBeInTheDocument();
expect(
screen.getAllByText(/a monitored system is a top-level machine or cluster/i).length,
).toBeGreaterThan(0);
});
it('shows recurring grandfathered pricing continuity for migrated v5 Pro plans', async () => {
const tests = [
+19 -222
View File
@@ -18,220 +18,13 @@ import {
} from '@/stores/license';
import { showToast } from '@/utils/toast';
import { logger } from '@/utils/logger';
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
type TierColumn = 'community' | 'relay' | 'pro' | 'proPlus';
type FeatureRow = {
key: string;
name: string;
community: boolean | string;
relay: boolean | string;
pro: boolean | string;
proPlus: boolean | string;
};
// ---------------------------------------------------------------------------
// Tier pricing data
// ---------------------------------------------------------------------------
const TIERS = {
community: {
name: 'Community',
price: 'Free forever',
subline: 'Monitor up to 5 systems for free',
highlights: [
'Real-time monitoring',
'7-day metric history',
'AI Patrol (BYOK + 25 quickstart credits)',
'Update alerts',
'Basic SSO (OIDC)',
'Community support',
],
},
relay: {
name: 'Relay',
price: '$4.99/month',
subline: 'or $39/year',
highlights: [
'Everything in Community',
'Remote access via Relay',
'Mobile app access',
'Push notifications',
'Custom URL (yourlab.pulserelay.pro)',
'8 monitored systems · 14-day history',
],
},
pro: {
name: 'Pro',
price: '$8.99/month',
subline: 'or $79/year',
highlights: [
'Everything in Relay',
'AI Auto-Fix & investigation',
'AI alert analysis',
'Kubernetes AI analysis',
'90-day metric history',
'RBAC, audit logging, SAML SSO',
'Agent profiles · PDF/CSV reports',
'15 monitored systems',
],
},
proPlus: {
name: 'Pro+',
price: '$14.99/month',
subline: 'or $129/year',
highlights: ['Everything in Pro', '50 monitored systems'],
},
} as const;
// ---------------------------------------------------------------------------
// Feature comparison matrix for self-hosted tiers (Community → Pro+).
// Based on ENTITLEMENT_MATRIX.md with display-only rows (hosts, history) added
// and enterprise-only rows (multi_user, white_label, multi_tenant, unlimited) omitted.
// ---------------------------------------------------------------------------
const FEATURE_ROWS: FeatureRow[] = [
// -- Monitoring & basics --
{
key: 'monitoring',
name: 'Real-time Monitoring',
community: true,
relay: true,
pro: true,
proPlus: true,
},
{
key: 'hosts',
name: 'Monitored Systems',
community: '5',
relay: '8',
pro: '15',
proPlus: '50',
},
{
key: 'history',
name: 'Metric History',
community: '7 days',
relay: '14 days',
pro: '90 days',
proPlus: '90 days',
},
{
key: 'update_alerts',
name: 'Update Alerts (Container/Package)',
community: true,
relay: true,
pro: true,
proPlus: true,
},
{ key: 'sso', name: 'Basic SSO (OIDC)', community: true, relay: true, pro: true, proPlus: true },
// -- Relay features --
{
key: 'relay',
name: 'Remote Access (Relay)',
community: false,
relay: true,
pro: true,
proPlus: true,
},
{
key: 'mobile_app',
name: 'Mobile App Access',
community: false,
relay: true,
pro: true,
proPlus: true,
},
{
key: 'push_notifications',
name: 'Push Notifications',
community: false,
relay: true,
pro: true,
proPlus: true,
},
// -- AI features --
{
key: 'ai_patrol',
name: 'AI Patrol (Background Health Checks)',
community: true,
relay: true,
pro: true,
proPlus: true,
},
{
key: 'ai_autofix',
name: 'AI Patrol Auto-Fix',
community: false,
relay: false,
pro: true,
proPlus: true,
},
{
key: 'ai_alerts',
name: 'AI Alert Analysis',
community: false,
relay: false,
pro: true,
proPlus: true,
},
{
key: 'kubernetes_ai',
name: 'Kubernetes AI Analysis',
community: false,
relay: false,
pro: true,
proPlus: true,
},
// -- Team & compliance --
{
key: 'rbac',
name: 'Role-Based Access Control (RBAC)',
community: false,
relay: false,
pro: true,
proPlus: true,
},
{
key: 'audit_logging',
name: 'Audit Logging',
community: false,
relay: false,
pro: true,
proPlus: true,
},
{
key: 'advanced_sso',
name: 'Advanced SSO (SAML/Multi-Provider)',
community: false,
relay: false,
pro: true,
proPlus: true,
},
{
key: 'agent_profiles',
name: 'Centralized Agent Profiles',
community: false,
relay: false,
pro: true,
proPlus: true,
},
{
key: 'advanced_reporting',
name: 'PDF/CSV Reporting',
community: false,
relay: false,
pro: true,
proPlus: true,
},
];
import {
SELF_HOSTED_FEATURE_ROWS,
SELF_HOSTED_PLAN_BY_TIER,
type SelfHostedFeatureRow,
type SelfHostedPlanDefinition,
type SelfHostedTierKey,
} from '@/utils/selfHostedPlans';
// ---------------------------------------------------------------------------
// Shared sub-components
@@ -263,7 +56,11 @@ function TierCtaLabel(props: { children: string }) {
);
}
function CheckCell(props: { value: boolean | string; tier: TierColumn; featureKey: string }) {
function CheckCell(props: {
value: boolean | string;
tier: SelfHostedTierKey;
featureKey: string;
}) {
if (typeof props.value === 'string') {
const label = `${props.tier} tier: ${props.value}`;
return (
@@ -314,7 +111,7 @@ function BulletList(props: { items: readonly string[] }) {
// ---------------------------------------------------------------------------
function TierCard(props: {
tier: (typeof TIERS)[keyof typeof TIERS];
tier: SelfHostedPlanDefinition;
cta: () => import('solid-js').JSX.Element;
highlighted?: boolean;
badge?: string;
@@ -530,16 +327,16 @@ export default function PricingV6() {
{/* Tier cards — 4 columns on desktop, 2 on tablet, 1 on mobile */}
<div class="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4">
<TierCard tier={TIERS.community} cta={communityCta} />
<TierCard tier={TIERS.relay} cta={relayCta} />
<TierCard tier={SELF_HOSTED_PLAN_BY_TIER.community} cta={communityCta} />
<TierCard tier={SELF_HOSTED_PLAN_BY_TIER.relay} cta={relayCta} />
<TierCard
tier={TIERS.pro}
tier={SELF_HOSTED_PLAN_BY_TIER.pro}
cta={proTrialOrUpgrade}
highlighted
badge="Most Popular"
message={trialMessage()}
/>
<TierCard tier={TIERS.proPlus} cta={proPlusCta} />
<TierCard tier={SELF_HOSTED_PLAN_BY_TIER.proPlus} cta={proPlusCta} />
</div>
{/* Upsell links */}
@@ -595,8 +392,8 @@ export default function PricingV6() {
</TableRow>
</TableHeader>
<TableBody class="bg-surface divide-y divide-border-subtle">
<For each={FEATURE_ROWS}>
{(row) => (
<For each={SELF_HOSTED_FEATURE_ROWS}>
{(row: SelfHostedFeatureRow) => (
<TableRow>
<TableCell class="px-4 py-2">
<div class="text-sm font-medium text-base-content">{row.name}</div>
@@ -0,0 +1,75 @@
import { describe, expect, it, vi } from 'vitest';
import { render, screen } from '@solidjs/testing-library';
import type { JSX } from 'solid-js';
import PricingV6 from '../PricingV6';
import pricingV6Source from '../PricingV6.tsx?raw';
vi.mock('@/components/shared/Card', () => ({
Card: (props: { children?: JSX.Element }) => <div>{props.children}</div>,
}));
vi.mock('@/components/shared/Table', () => ({
Table: (props: { children?: JSX.Element }) => <table>{props.children}</table>,
TableHeader: (props: { children?: JSX.Element }) => <thead>{props.children}</thead>,
TableBody: (props: { children?: JSX.Element }) => <tbody>{props.children}</tbody>,
TableRow: (props: { children?: JSX.Element }) => <tr>{props.children}</tr>,
TableHead: (props: { children?: JSX.Element }) => <th>{props.children}</th>,
TableCell: (props: { children?: JSX.Element }) => <td>{props.children}</td>,
}));
vi.mock('@/components/shared/PageHeader', () => ({
PageHeader: (props: { title: string; description: string }) => (
<header>
<h1>{props.title}</h1>
<p>{props.description}</p>
</header>
),
}));
vi.mock('@/stores/license', () => ({
entitlements: () => ({
subscription_state: 'expired',
tier: 'free',
trial_eligible: false,
}),
getUpgradeActionUrlOrFallback: (tier: string) => `/${tier}`,
loadLicenseStatus: vi.fn().mockResolvedValue(undefined),
startProTrial: vi.fn(),
}));
vi.mock('@/utils/upgradeMetrics', () => ({
trackPaywallViewed: vi.fn(),
}));
vi.mock('@/utils/toast', () => ({
showToast: vi.fn(),
}));
vi.mock('@/utils/logger', () => ({
logger: {
debug: vi.fn(),
warn: vi.fn(),
},
}));
describe('PricingV6', () => {
it('renders self-hosted plan tiers from the shared pricing model', () => {
render(() => <PricingV6 />);
expect(screen.getAllByText('Community').length).toBeGreaterThan(0);
expect(screen.getAllByText('Relay').length).toBeGreaterThan(0);
expect(screen.getAllByText('Pro').length).toBeGreaterThan(0);
expect(screen.getAllByText('Pro+').length).toBeGreaterThan(0);
expect(screen.getByText('8 monitored systems · 14-day history')).toBeInTheDocument();
expect(screen.getByText('15 monitored systems')).toBeInTheDocument();
expect(screen.getByText('50 monitored systems')).toBeInTheDocument();
expect(screen.getByText('Feature Comparison')).toBeInTheDocument();
});
it('imports the shared self-hosted pricing model instead of redefining it locally', () => {
expect(pricingV6Source).toContain("@/utils/selfHostedPlans");
expect(pricingV6Source).not.toContain('const TIERS =');
expect(pricingV6Source).not.toContain('const FEATURE_ROWS');
});
});
@@ -0,0 +1,43 @@
import { describe, expect, it } from 'vitest';
import {
SELF_HOSTED_FEATURE_ROWS,
SELF_HOSTED_MONITORED_SYSTEMS_DESCRIPTION,
SELF_HOSTED_PLAN_BY_TIER,
SELF_HOSTED_PLAN_DEFINITIONS,
} from '../selfHostedPlans';
describe('selfHostedPlans', () => {
it('keeps the monitored-system definition explicit and child-resource inclusive', () => {
expect(SELF_HOSTED_MONITORED_SYSTEMS_DESCRIPTION).toContain(
'top-level machine or cluster',
);
expect(SELF_HOSTED_MONITORED_SYSTEMS_DESCRIPTION).toContain('counts once');
expect(SELF_HOSTED_MONITORED_SYSTEMS_DESCRIPTION).toContain('VMs');
expect(SELF_HOSTED_MONITORED_SYSTEMS_DESCRIPTION).toContain('services');
});
it('keeps self-hosted plan limits aligned across tier cards and comparison rows', () => {
expect(SELF_HOSTED_PLAN_DEFINITIONS.map((tier) => tier.name)).toEqual([
'Community',
'Relay',
'Pro',
'Pro+',
]);
expect(SELF_HOSTED_PLAN_BY_TIER.community.monitoredSystems).toBe(5);
expect(SELF_HOSTED_PLAN_BY_TIER.relay.monitoredSystems).toBe(8);
expect(SELF_HOSTED_PLAN_BY_TIER.pro.monitoredSystems).toBe(15);
expect(SELF_HOSTED_PLAN_BY_TIER.proPlus.monitoredSystems).toBe(50);
const hostsRow = SELF_HOSTED_FEATURE_ROWS.find((row) => row.key === 'hosts');
expect(hostsRow).toEqual({
key: 'hosts',
name: 'Monitored Systems',
community: '5',
relay: '8',
pro: '15',
proPlus: '50',
});
});
});
@@ -0,0 +1,234 @@
export type SelfHostedTierKey = 'community' | 'relay' | 'pro' | 'proPlus';
export interface SelfHostedPlanDefinition {
tier: SelfHostedTierKey;
name: string;
price: string;
subline: string;
monitoredSystems: number;
metricHistoryDays: number;
highlights: readonly string[];
}
export interface SelfHostedFeatureRow {
key: string;
name: string;
community: boolean | string;
relay: boolean | string;
pro: boolean | string;
proPlus: boolean | string;
}
export const SELF_HOSTED_MONITORED_SYSTEMS_DESCRIPTION =
'A monitored system is a top-level machine or cluster Pulse actively monitors. Each system counts once no matter how Pulse collects it. Child resources like VMs, containers, pods, disks, backups, and services are included.';
export const SELF_HOSTED_MONITORED_SYSTEM_LEDGER_DESCRIPTION =
'Review the monitored systems currently counting toward your Pulse Pro allocation.';
export const SELF_HOSTED_PLAN_DEFINITIONS: readonly SelfHostedPlanDefinition[] = [
{
tier: 'community',
name: 'Community',
price: 'Free forever',
subline: 'Monitor up to 5 systems for free',
monitoredSystems: 5,
metricHistoryDays: 7,
highlights: [
'Real-time monitoring',
'7-day metric history',
'AI Patrol (BYOK + 25 quickstart credits)',
'Update alerts',
'Basic SSO (OIDC)',
'Community support',
],
},
{
tier: 'relay',
name: 'Relay',
price: '$4.99/month',
subline: 'or $39/year',
monitoredSystems: 8,
metricHistoryDays: 14,
highlights: [
'Everything in Community',
'Remote access via Relay',
'Mobile app access',
'Push notifications',
'Custom URL (yourlab.pulserelay.pro)',
'8 monitored systems · 14-day history',
],
},
{
tier: 'pro',
name: 'Pro',
price: '$8.99/month',
subline: 'or $79/year',
monitoredSystems: 15,
metricHistoryDays: 90,
highlights: [
'Everything in Relay',
'AI Auto-Fix & investigation',
'AI alert analysis',
'Kubernetes AI analysis',
'90-day metric history',
'RBAC, audit logging, SAML SSO',
'Agent profiles · PDF/CSV reports',
'15 monitored systems',
],
},
{
tier: 'proPlus',
name: 'Pro+',
price: '$14.99/month',
subline: 'or $129/year',
monitoredSystems: 50,
metricHistoryDays: 90,
highlights: ['Everything in Pro', '50 monitored systems'],
},
] as const;
export const SELF_HOSTED_PLAN_BY_TIER: Record<SelfHostedTierKey, SelfHostedPlanDefinition> = {
community: SELF_HOSTED_PLAN_DEFINITIONS[0],
relay: SELF_HOSTED_PLAN_DEFINITIONS[1],
pro: SELF_HOSTED_PLAN_DEFINITIONS[2],
proPlus: SELF_HOSTED_PLAN_DEFINITIONS[3],
};
export const SELF_HOSTED_FEATURE_ROWS: readonly SelfHostedFeatureRow[] = [
{
key: 'monitoring',
name: 'Real-time Monitoring',
community: true,
relay: true,
pro: true,
proPlus: true,
},
{
key: 'hosts',
name: 'Monitored Systems',
community: String(SELF_HOSTED_PLAN_BY_TIER.community.monitoredSystems),
relay: String(SELF_HOSTED_PLAN_BY_TIER.relay.monitoredSystems),
pro: String(SELF_HOSTED_PLAN_BY_TIER.pro.monitoredSystems),
proPlus: String(SELF_HOSTED_PLAN_BY_TIER.proPlus.monitoredSystems),
},
{
key: 'history',
name: 'Metric History',
community: `${SELF_HOSTED_PLAN_BY_TIER.community.metricHistoryDays} days`,
relay: `${SELF_HOSTED_PLAN_BY_TIER.relay.metricHistoryDays} days`,
pro: `${SELF_HOSTED_PLAN_BY_TIER.pro.metricHistoryDays} days`,
proPlus: `${SELF_HOSTED_PLAN_BY_TIER.proPlus.metricHistoryDays} days`,
},
{
key: 'update_alerts',
name: 'Update Alerts (Container/Package)',
community: true,
relay: true,
pro: true,
proPlus: true,
},
{
key: 'sso',
name: 'Basic SSO (OIDC)',
community: true,
relay: true,
pro: true,
proPlus: true,
},
{
key: 'relay',
name: 'Remote Access (Relay)',
community: false,
relay: true,
pro: true,
proPlus: true,
},
{
key: 'mobile_app',
name: 'Mobile App Access',
community: false,
relay: true,
pro: true,
proPlus: true,
},
{
key: 'push_notifications',
name: 'Push Notifications',
community: false,
relay: true,
pro: true,
proPlus: true,
},
{
key: 'ai_patrol',
name: 'AI Patrol (Background Health Checks)',
community: true,
relay: true,
pro: true,
proPlus: true,
},
{
key: 'ai_autofix',
name: 'AI Patrol Auto-Fix',
community: false,
relay: false,
pro: true,
proPlus: true,
},
{
key: 'ai_alerts',
name: 'AI Alert Analysis',
community: false,
relay: false,
pro: true,
proPlus: true,
},
{
key: 'kubernetes_ai',
name: 'Kubernetes AI Analysis',
community: false,
relay: false,
pro: true,
proPlus: true,
},
{
key: 'rbac',
name: 'Role-Based Access Control (RBAC)',
community: false,
relay: false,
pro: true,
proPlus: true,
},
{
key: 'audit_logging',
name: 'Audit Logging',
community: false,
relay: false,
pro: true,
proPlus: true,
},
{
key: 'advanced_sso',
name: 'Advanced SSO (SAML/Multi-Provider)',
community: false,
relay: false,
pro: true,
proPlus: true,
},
{
key: 'agent_profiles',
name: 'Centralized Agent Profiles',
community: false,
relay: false,
pro: true,
proPlus: true,
},
{
key: 'advanced_reporting',
name: 'PDF/CSV Reporting',
community: false,
relay: false,
pro: true,
proPlus: true,
},
] as const;
@@ -483,7 +483,10 @@ class SubsystemLookupTest(unittest.TestCase):
)
self.assertEqual(
match["verification_requirement"]["exact_files"],
["frontend-modern/src/components/Settings/__tests__/ProLicensePanel.test.tsx"],
[
"frontend-modern/src/components/Settings/__tests__/MonitoredSystemLedgerPanel.test.tsx",
"frontend-modern/src/components/Settings/__tests__/ProLicensePanel.test.tsx",
],
)
def test_lookup_paths_assigns_pro_license_state_owner_to_cloud_paid(self) -> None:
@@ -507,6 +510,60 @@ class SubsystemLookupTest(unittest.TestCase):
"pro-license-surface",
)
def test_lookup_paths_assigns_monitored_system_ledger_panel_to_cloud_paid(self) -> None:
result = lookup_paths(["frontend-modern/src/components/Settings/MonitoredSystemLedgerPanel.tsx"])
self.assertEqual(result["unowned_runtime_files"], [])
self.assertEqual(
{item["subsystem"] for item in result["impacted_subsystems"]},
{"cloud-paid"},
)
file_entry = result["files"][0]
self.assertEqual(file_entry["classification"], "runtime")
self.assertEqual(
{match["subsystem"] for match in file_entry["matches"]},
{"cloud-paid"},
)
match = file_entry["matches"][0]
self.assertEqual(match["contract"], "docs/release-control/v6/internal/subsystems/cloud-paid.md")
self.assertEqual(match["lane_context"]["lane_id"], "L3")
self.assertEqual(match["verification_requirement"]["id"], "pro-license-surface")
def test_lookup_paths_assigns_pricing_v6_to_cloud_paid(self) -> None:
result = lookup_paths(["frontend-modern/src/pages/PricingV6.tsx"])
self.assertEqual(result["unowned_runtime_files"], [])
self.assertEqual(
{item["subsystem"] for item in result["impacted_subsystems"]},
{"cloud-paid"},
)
file_entry = result["files"][0]
self.assertEqual(file_entry["classification"], "runtime")
self.assertEqual(
{match["subsystem"] for match in file_entry["matches"]},
{"cloud-paid"},
)
match = file_entry["matches"][0]
self.assertEqual(match["contract"], "docs/release-control/v6/internal/subsystems/cloud-paid.md")
self.assertEqual(match["lane_context"]["lane_id"], "L3")
self.assertEqual(match["verification_requirement"]["id"], "pricing-pages")
def test_lookup_paths_assigns_self_hosted_plan_model_to_cloud_paid(self) -> None:
result = lookup_paths(["frontend-modern/src/utils/selfHostedPlans.ts"])
self.assertEqual(result["unowned_runtime_files"], [])
self.assertEqual(
{item["subsystem"] for item in result["impacted_subsystems"]},
{"cloud-paid"},
)
file_entry = result["files"][0]
self.assertEqual(file_entry["classification"], "runtime")
self.assertEqual(
{match["subsystem"] for match in file_entry["matches"]},
{"cloud-paid"},
)
match = file_entry["matches"][0]
self.assertEqual(match["contract"], "docs/release-control/v6/internal/subsystems/cloud-paid.md")
self.assertEqual(match["lane_context"]["lane_id"], "L3")
self.assertEqual(match["verification_requirement"]["id"], "commercial-plan-models")
def test_lookup_paths_assigns_relay_settings_state_owner_to_cloud_paid(self) -> None:
result = lookup_paths(["frontend-modern/src/components/Settings/useRelaySettingsPanelState.ts"])
self.assertEqual(result["unowned_runtime_files"], [])