Reframe self-hosted paid value around operator outcomes

This commit is contained in:
rcourtman 2026-04-20 17:30:52 +01:00
parent ed58890471
commit 61430703b2
17 changed files with 118 additions and 67 deletions

View file

@ -220,6 +220,15 @@ Community limit enforcement.
`max_monitored_systems` entitlement row, and
`monitored_system_capacity.mode = unlimited` once runtime usage is
available.
That same self-hosted commercial boundary also owns the operator-value
narrative across the in-app billing shell, Pulse Account handoff, public
pricing contract, and owned upgrade reasons. Customer-facing self-hosted
copy must keep the ladder explicit as `Community = monitor`, `Relay = reach
Pulse from anywhere`, and `Pro = investigate root cause, apply safe
remediation, and retain longer incident memory`. Team/admin extras such as
RBAC, SSO, audit logging, reporting, and agent profiles may remain present,
but they are secondary included value and must not displace that operator
outcome framing on owned commercial surfaces.
24. Keep public-demo dashboard bootstrap route-owned on the adjacent
commercial/runtime boundary. `frontend-modern/src/useAppRuntimeState.ts`
may prewarm shared infrastructure summary caches for non-dashboard routes,

View file

@ -208,7 +208,7 @@ describe('ProLicensePanel', () => {
});
expect(screen.getAllByText('Compare self-hosted plans').length).toBeGreaterThan(0);
expect(screen.getByText(/Community keeps core monitoring free/i)).toBeInTheDocument();
expect(screen.getByText(/Community keeps monitoring free/i)).toBeInTheDocument();
const compareLinks = screen.getAllByRole('link', { name: 'Compare plans' });
expect(
compareLinks.some(
@ -321,7 +321,7 @@ 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.',
'Review your active plan, expiry, and whether this instance only monitors, reaches anywhere, or also helps investigate and fix issues.',
),
).toBeInTheDocument();
expect(screen.queryByText('Included Monitored Systems')).not.toBeInTheDocument();
@ -839,7 +839,7 @@ describe('ProLicensePanel', () => {
renderPanel();
expect(screen.getAllByText('Compare self-hosted plans').length).toBeGreaterThan(0);
expect(screen.getByText(/Community keeps core monitoring free/i)).toBeInTheDocument();
expect(screen.getByText(/Community keeps monitoring free/i)).toBeInTheDocument();
const compareLinks = screen.getAllByRole('link', { name: 'Compare plans' });
expect(
compareLinks.some(
@ -1008,7 +1008,7 @@ describe('ProLicensePanel', () => {
);
expect(proLicensePlanSectionSource).not.toContain('Your Pro trial has ended');
expect(proLicensePlanSectionSource).not.toContain(
'Unlock Pulse Patrol, alert analysis, auto-fix, and more.',
'Turn alert noise into root-cause answers, safe fixes, and 90-day incident history.',
);
expect(selfHostedCommercialRecoverySectionSource).toContain(
'SELF_HOSTED_RECOVERY_PRESENTATION',

View file

@ -35,17 +35,17 @@ export interface SelfHostedProBillingPresentation {
export const SELF_HOSTED_PRO_BILLING_PRESENTATION: SelfHostedProBillingPresentation = {
shellTitle: 'Plans & Billing',
shellDescription:
'Manage self-hosted billing, plan features, and activation status.',
infrastructureRouteReferral: 'Billing and self-hosted plan features live in Plans & Billing.',
'Manage self-hosted plans, activation, and how Pulse helps when something breaks.',
infrastructureRouteReferral: 'Billing and self-hosted plan changes live in Plans & Billing.',
infrastructureWorkspaceReferral:
'Billing, self-hosted plan features, and activation status live in Plans & Billing, not here.',
'Billing, self-hosted plan changes, and activation status live in Plans & Billing, not here.',
sectionSelectorAriaLabel: 'Self-hosted billing section',
refreshLabel: 'Refresh',
planTabLabel: 'Plan',
usageTabLabel: 'Usage',
planSectionTitle: 'Plan',
planSectionDescription:
'Review your active plan, expiry, and the paid capabilities that come with it.',
'Review your active plan, expiry, and whether this instance only monitors, reaches anywhere, or also helps investigate and fix issues.',
usageSectionTitle: 'Usage',
hiddenShellTitle: 'Demo mode',
hiddenShellDescription: 'Commercial settings are hidden for this session.',
@ -57,7 +57,7 @@ export const SELF_HOSTED_PRO_BILLING_PRESENTATION: SelfHostedProBillingPresentat
'Pulse waits for the session presentation policy before showing license, billing, or usage details.',
planSelectionPromptTitle: 'Compare self-hosted plans',
planSelectionPromptBody:
'Community keeps core monitoring free. Compare Relay and Pro in Pulse Account, then return here with the new plan activated automatically.',
'Community keeps monitoring free. Compare Relay when you want secure access from anywhere, or Pro when you want root-cause answers, safe remediation, and 90-day incident history.',
planSelectionPromptActionLabel: 'Compare plans',
purchaseActivatedPlanActionLabel: 'Review plan',
purchaseCancelledActionLabel: 'Compare plans',
@ -65,7 +65,8 @@ export const SELF_HOSTED_PRO_BILLING_PRESENTATION: SelfHostedProBillingPresentat
purchaseFailedActionLabel: 'Open recovery',
purchaseUnavailableActionLabel: 'Try again',
trialStartTitle: 'Try Pro for free',
trialStartBody: 'Start a 14-day Pro trial for this organization.',
trialStartBody:
'Start a 14-day Pro trial for root-cause analysis, safe auto-fix, and 90-day incident history on this organization.',
trialStartIdleActionLabel: 'Start 14-day Pro Trial',
trialStartPendingActionLabel: 'Starting...',
recoverySectionTitle: 'Recovery',

View file

@ -44,13 +44,13 @@ describe('licensePresentation', () => {
expect(getTrialEndedProLicenseNotice()).toEqual({
tone: 'border-red-200 dark:border-red-900 bg-red-50 dark:bg-red-900 text-red-900 dark:text-red-100',
title: 'Your Pro trial has ended',
body: 'Upgrade to keep Pro features.',
body: 'Upgrade to keep root-cause analysis, safe auto-fix, and 90-day incident history.',
actionLabel: 'View Pro plans',
});
expect(getInactiveProUpsellNotice()).toEqual({
tone: 'border-amber-200 dark:border-amber-800 bg-amber-50 dark:bg-amber-900 text-amber-800 dark:text-amber-200',
title: 'Upgrade to Pro',
body: 'Unlock Pulse Patrol, alert analysis, auto-fix, and more.',
body: 'Turn alert noise into root-cause answers, safe fixes, and 90-day incident history.',
actionLabel: 'View Pro plans',
});
expect(SELF_HOSTED_RECOVERY_PRESENTATION).toMatchObject({
@ -65,17 +65,17 @@ describe('licensePresentation', () => {
expect(SELF_HOSTED_PRO_BILLING_PRESENTATION).toEqual({
shellTitle: 'Plans & Billing',
shellDescription:
'Manage self-hosted billing, plan features, and activation status.',
infrastructureRouteReferral: 'Billing and self-hosted plan features live in Plans & Billing.',
'Manage self-hosted plans, activation, and how Pulse helps when something breaks.',
infrastructureRouteReferral: 'Billing and self-hosted plan changes live in Plans & Billing.',
infrastructureWorkspaceReferral:
'Billing, self-hosted plan features, and activation status live in Plans & Billing, not here.',
'Billing, self-hosted plan changes, and activation status live in Plans & Billing, not here.',
sectionSelectorAriaLabel: 'Self-hosted billing section',
refreshLabel: 'Refresh',
planTabLabel: 'Plan',
usageTabLabel: 'Usage',
planSectionTitle: 'Plan',
planSectionDescription:
'Review your active plan, expiry, and the paid capabilities that come with it.',
'Review your active plan, expiry, and whether this instance only monitors, reaches anywhere, or also helps investigate and fix issues.',
usageSectionTitle: 'Usage',
hiddenShellTitle: 'Demo mode',
hiddenShellDescription: 'Commercial settings are hidden for this session.',
@ -87,7 +87,7 @@ describe('licensePresentation', () => {
'Pulse waits for the session presentation policy before showing license, billing, or usage details.',
planSelectionPromptTitle: 'Compare self-hosted plans',
planSelectionPromptBody:
'Community keeps core monitoring free. Compare Relay and Pro in Pulse Account, then return here with the new plan activated automatically.',
'Community keeps monitoring free. Compare Relay when you want secure access from anywhere, or Pro when you want root-cause answers, safe remediation, and 90-day incident history.',
planSelectionPromptActionLabel: 'Compare plans',
purchaseActivatedPlanActionLabel: 'Review plan',
purchaseCancelledActionLabel: 'Compare plans',
@ -95,7 +95,8 @@ describe('licensePresentation', () => {
purchaseFailedActionLabel: 'Open recovery',
purchaseUnavailableActionLabel: 'Try again',
trialStartTitle: 'Try Pro for free',
trialStartBody: 'Start a 14-day Pro trial for this organization.',
trialStartBody:
'Start a 14-day Pro trial for root-cause analysis, safe auto-fix, and 90-day incident history on this organization.',
trialStartIdleActionLabel: 'Start 14-day Pro Trial',
trialStartPendingActionLabel: 'Starting...',
recoverySectionTitle: 'Recovery',

View file

@ -434,14 +434,14 @@ export const getNoActiveProLicenseState = (): LicenseLoadingStateCopy => ({
export const getTrialEndedProLicenseNotice = (): LicenseActionNotice => ({
tone: 'border-red-200 dark:border-red-900 bg-red-50 dark:bg-red-900 text-red-900 dark:text-red-100',
title: 'Your Pro trial has ended',
body: 'Upgrade to keep Pro features.',
body: 'Upgrade to keep root-cause analysis, safe auto-fix, and 90-day incident history.',
actionLabel: 'View Pro plans',
});
export const getInactiveProUpsellNotice = (): LicenseActionNotice => ({
tone: 'border-amber-200 dark:border-amber-800 bg-amber-50 dark:bg-amber-900 text-amber-800 dark:text-amber-200',
title: 'Upgrade to Pro',
body: 'Unlock Pulse Patrol, alert analysis, auto-fix, and more.',
body: 'Turn alert noise into root-cause answers, safe fixes, and 90-day incident history.',
actionLabel: 'View Pro plans',
});

View file

@ -1,5 +1,5 @@
{
"source_hash": "d2bcdf7f2c1910ca61deb35e6a85ea869d524c63c1bc8d0faba74e66ba62c4c1",
"source_hash": "de861ffaf5a9cea5ea57c6314ce7615a991b5c302d717438758b702668c01828",
"build_inputs": [
"package.json",
"tsconfig.json",

View file

@ -1468,7 +1468,7 @@
if (explainer) {
summaryItems.push('<div class="helper-text">' + explainer + "</div>");
}
root.innerHTML = '<div class="billing-upgrade-root">' + summaryItems.join("") + renderUpgradePlansHTML(billingState) + (pricingState.status === "ready" && pricingState.data && pricingState.data.description ? '<div class="helper-text">' + escapeText(pricingState.data.description) + "</div>" : "") + '<div class="helper-text">' + (featureKey === "self_hosted_plan" || featureKey === "max_monitored_systems" ? "Pulse Account compares self-hosted tiers by convenience and advanced capabilities, keeps core monitoring available across plans, and sends completed checkout straight back to Pulse Pro billing." : "Pulse Account compares self-hosted tiers and sends completed checkout straight back to Pulse Pro billing.") + "</div></div>";
root.innerHTML = '<div class="billing-upgrade-root">' + summaryItems.join("") + renderUpgradePlansHTML(billingState) + (pricingState.status === "ready" && pricingState.data && pricingState.data.description ? '<div class="helper-text">' + escapeText(pricingState.data.description) + "</div>" : "") + '<div class="helper-text">' + (featureKey === "self_hosted_plan" || featureKey === "max_monitored_systems" ? "Pulse Account keeps checkout tied to the Pulse instance that opened it, so completed Relay or Pro purchases return to the right billing surface automatically." : "Pulse Account compares self-hosted tiers and sends completed checkout straight back to Pulse Pro billing.") + "</div></div>";
}
function renderButton(id, disabled, label) {
if (!id || !label) return;
@ -2254,7 +2254,7 @@
return isSelfHostedPlanUpgrade(featureKey) ? "Compare self-hosted plans" : "Upgrade self-hosted plan";
}
function selfHostedUpgradeActionDescription(featureKey) {
return isSelfHostedPlanUpgrade(featureKey) ? "Compare self-hosted plans by Relay and Pro capabilities rather than monitored-system volume." : "Compare self-hosted plans and continue into the commercial checkout path.";
return isSelfHostedPlanUpgrade(featureKey) ? "Compare self-hosted plans as monitor, reach, or operate instead of by monitored-system volume." : "Compare self-hosted plans and continue into the commercial checkout path.";
}
function selfHostedUpgradeActionHighlights(featureKey) {
return isSelfHostedPlanUpgrade(featureKey) ? ["Plan comparison", "Pulse Pro checkout"] : ["Plan comparison", "Checkout handoff"];
@ -2273,7 +2273,7 @@
}
function renderSelfHostedUpgradeBillingPanel(context) {
var featureKey = normalizeUpgradeFeatureKey(context.billingState.upgradeFeatureKey);
var helperCopy = isSelfHostedPlanUpgrade(featureKey) ? "Choose the self-hosted tier that fits the convenience and advanced capabilities you want. Core monitoring stays available across self-hosted plans. Pulse Account will send completed checkout directly back to Pulse Pro billing." : "Choose the self-hosted tier that fits this upgrade. Pulse Account will send completed checkout directly back to Pulse Pro billing.";
var helperCopy = isSelfHostedPlanUpgrade(featureKey) ? "Choose the self-hosted tier that fits how you run Pulse: Community monitors, Relay reaches anywhere, and Pro investigates and helps fix issues. Pulse Account will send completed checkout directly back to Pulse Pro billing." : "Choose the self-hosted tier that fits this upgrade. Pulse Account will send completed checkout directly back to Pulse Pro billing.";
return renderBillingTaskPanel(
selfHostedUpgradeActionTitle(featureKey),
"Pulse Account owns self-hosted plan selection and checkout for Pulse Pro upgrades.",

View file

@ -482,9 +482,9 @@ function routeCommercialAPI(request, response, url, scenario) {
if (request.method === 'GET' && route === '/v1/public/pricing-model') {
sendJSON(response, 200, {
title: 'Simple self-hosted pricing for Pulse',
description: 'Preview pricing contract',
description: 'Community keeps monitoring free. Relay lets you reach Pulse securely from anywhere. Pro explains what broke, helps fix it safely, and keeps 90 days of incident history.',
explainer:
'Community keeps core monitoring free. Relay and Pro sell convenience, history, AI operations, and advanced administration.',
'Community keeps monitoring free. Relay gets Pulse securely from anywhere. Pro adds root-cause analysis, safe remediation, and 90-day incident history.',
plans: [
{
badge: 'Recommended',
@ -493,10 +493,12 @@ function routeCommercialAPI(request, response, url, scenario) {
title: 'Relay',
price: '$4.99',
period: '$39/year available too',
blurb: 'Secure remote access and mobile access.',
blurb: 'The reach-it-anywhere tier for secure remote access, mobile access, and push notifications.',
features: [
{ tone: 'check', html: 'Unlimited <strong>self-hosted monitoring</strong>' },
{ tone: 'check', html: 'Remote access' },
{ tone: 'check', html: 'Secure remote access' },
{ tone: 'check', html: 'Mobile access' },
{ tone: 'check', html: 'Push notifications' },
],
buttons: [
{ kind: 'checkout', className: 'btn btn-secondary', tier: 'relay', planKey: 'price_relay_monthly', billingCycle: 'monthly', label: 'Buy Monthly' },
@ -508,10 +510,13 @@ function routeCommercialAPI(request, response, url, scenario) {
title: 'Pro',
price: '$8.99',
period: '$79/year available too',
blurb: 'Investigation, alert analysis, and auto-fix.',
blurb: 'The operator tier for root-cause answers, safe remediation, and longer incident memory.',
features: [
{ tone: 'check', html: 'Unlimited <strong>self-hosted monitoring</strong>' },
{ tone: 'check', html: 'Everything in Relay' },
{ tone: 'check', html: 'Alert-triggered root-cause analysis' },
{ tone: 'check', html: 'Patrol auto-fix with approval or autonomous mode' },
{ tone: 'check', html: '90-day incident history' },
],
buttons: [
{ kind: 'checkout', className: 'btn btn-secondary', tier: 'pro', planKey: 'price_pro_monthly', billingCycle: 'monthly', label: 'Buy Monthly' },

View file

@ -240,14 +240,14 @@ describe('services view', function() {
title: 'Pricing',
description: 'Canonical pricing model',
explainer:
'Community keeps core monitoring free. Relay and Pro sell convenience, history, AI operations, and advanced administration.',
'Community keeps monitoring free. Relay gets Pulse securely from anywhere. Pro adds root-cause analysis, safe remediation, and 90-day incident history.',
plans: [
{
tierKicker: 'Pro',
title: 'Pro',
price: '$8.99',
period: '$79/year available too',
blurb: 'Investigation, alert analysis, and auto-fix.',
blurb: 'The operator tier for root-cause answers, safe remediation, and longer incident memory.',
features: [{ tone: 'check', html: 'Unlimited <strong>self-hosted monitoring</strong>' }],
buttons: [
{
@ -270,10 +270,10 @@ describe('services view', function() {
'Pulse Account will return completed checkout directly to Pulse Pro billing.',
);
expect(document.getElementById('upgrade-billing-root')?.innerHTML).toContain(
'Community keeps core monitoring free. Relay and Pro sell convenience, history, AI operations, and advanced administration.',
'Pulse Account keeps checkout tied to the Pulse instance that opened it, so completed Relay or Pro purchases return to the right billing surface automatically.',
);
expect(document.getElementById('upgrade-billing-root')?.innerHTML).toContain(
'Pulse Account compares self-hosted tiers by convenience and advanced capabilities, keeps core monitoring available across plans, and sends completed checkout straight back to Pulse Pro billing.',
'Community keeps monitoring free. Relay gets Pulse securely from anywhere. Pro adds root-cause analysis, safe remediation, and 90-day incident history.',
);
expect(document.getElementById('upgrade-billing-root')?.innerHTML).not.toContain('Activate in Pulse Pro');
expect(document.getElementById('upgrade-billing-root')?.innerHTML).not.toContain('ppk_live_preview');

View file

@ -196,7 +196,7 @@ export function renderUpgradePanel(billingState: PortalBillingState, _bootstrap:
: '') +
'<div class="helper-text">' +
((featureKey === 'self_hosted_plan' || featureKey === 'max_monitored_systems')
? 'Pulse Account compares self-hosted tiers by convenience and advanced capabilities, keeps core monitoring available across plans, and sends completed checkout straight back to Pulse Pro billing.'
? 'Pulse Account keeps checkout tied to the Pulse instance that opened it, so completed Relay or Pro purchases return to the right billing surface automatically.'
: 'Pulse Account compares self-hosted tiers and sends completed checkout straight back to Pulse Pro billing.') +
'</div>' +
'</div>';

View file

@ -676,12 +676,12 @@ describe('shell view', function() {
expect(html).toContain('Pulse Account owns the commercial handoff for self-hosted upgrades from the app.');
expect(html).toContain('Compare self-hosted plans');
expect(html).toContain(
'Compare self-hosted plans by Relay and Pro capabilities rather than monitored-system volume.',
'Compare self-hosted plans as monitor, reach, or operate instead of by monitored-system volume.',
);
expect(html).toContain('Plan comparison');
expect(html).toContain('id="upgrade-billing-root"');
expect(html).toContain(
'Choose the self-hosted tier that fits the convenience and advanced capabilities you want. Core monitoring stays available across self-hosted plans. Pulse Account will send completed checkout directly back to Pulse Pro billing.',
'Choose the self-hosted tier that fits how you run Pulse: Community monitors, Relay reaches anywhere, and Pro investigates and helps fix issues. Pulse Account will send completed checkout directly back to Pulse Pro billing.',
);
expect(html).toContain('Pulse Account owns self-hosted plan selection and checkout for Pulse Pro upgrades.');
expect(html).not.toContain('id="open-manage-billing"');

View file

@ -151,7 +151,7 @@ function selfHostedUpgradeActionTitle(featureKey: string): string {
function selfHostedUpgradeActionDescription(featureKey: string): string {
return isSelfHostedPlanUpgrade(featureKey)
? 'Compare self-hosted plans by Relay and Pro capabilities rather than monitored-system volume.'
? 'Compare self-hosted plans as monitor, reach, or operate instead of by monitored-system volume.'
: 'Compare self-hosted plans and continue into the commercial checkout path.';
}
@ -177,7 +177,7 @@ function renderSelfHostedUpgradeActionRow(context: ShellViewContext): string {
function renderSelfHostedUpgradeBillingPanel(context: ShellViewContext): string {
var featureKey = normalizeUpgradeFeatureKey(context.billingState.upgradeFeatureKey);
var helperCopy = isSelfHostedPlanUpgrade(featureKey)
? 'Choose the self-hosted tier that fits the convenience and advanced capabilities you want. Core monitoring stays available across self-hosted plans. Pulse Account will send completed checkout directly back to Pulse Pro billing.'
? 'Choose the self-hosted tier that fits how you run Pulse: Community monitors, Relay reaches anywhere, and Pro investigates and helps fix issues. Pulse Account will send completed checkout directly back to Pulse Pro billing.'
: 'Choose the self-hosted tier that fits this upgrade. Pulse Account will send completed checkout directly back to Pulse Pro billing.';
return renderBillingTaskPanel(
selfHostedUpgradeActionTitle(featureKey),

View file

@ -91,6 +91,25 @@ func TestUpgradeURLForFeatureUnknownFallsBackToGenericPricing(t *testing.T) {
}
}
func TestUpgradeReasonMatrix_OperatorOutcomeCopyRemainsCanonical(t *testing.T) {
expected := map[string]string{
license.FeatureRelay: "Get Relay so Pulse stays reachable securely from anywhere instead of only on the local dashboard.",
license.FeatureLongTermMetrics: "Get Relay for 14 days of history, or Pro for 90 days, so you can see what changed before and after an incident.",
license.FeatureAIAlerts: "Upgrade to Pro so alerts arrive with root-cause analysis instead of a stack of symptoms.",
license.FeatureAIAutoFix: "Upgrade to Pro so Pulse can move from finding issues to applying safe remediation with your approval or in autonomous mode.",
}
for _, entry := range UpgradeReasonMatrix {
want, ok := expected[entry.Feature]
if !ok {
continue
}
if entry.Reason != want {
t.Fatalf("expected canonical reason %q for feature %q, got %q", want, entry.Feature, entry.Reason)
}
}
}
func expectedProOnlyFeatureSet() map[string]struct{} {
proFeatures := make(map[string]struct{}, len(license.TierFeatures[license.TierPro]))
for _, feature := range license.TierFeatures[license.TierPro] {

View file

@ -15,73 +15,73 @@ type ReasonEntry struct {
var UpgradeReasonMatrix = []ReasonEntry{
{
Feature: FeatureRelay,
Reason: "Get Relay for secure remote access to your dashboard from anywhere — plus mobile app and push notifications.",
Reason: "Get Relay so Pulse stays reachable securely from anywhere instead of only on the local dashboard.",
ActionURL: UpgradeURLForFeature(FeatureRelay),
Priority: 1,
},
{
Feature: FeatureMobileApp,
Reason: "Get Relay to monitor your infrastructure on the go with the Pulse mobile app.",
Reason: "Get Relay so you can check Pulse from your phone when you are away from the dashboard.",
ActionURL: UpgradeURLForFeature(FeatureMobileApp),
Priority: 1,
},
{
Feature: FeaturePushNotifications,
Reason: "Get Relay for instant push notifications when alerts fire — never miss a critical event.",
Reason: "Get Relay so important alerts reach you immediately on mobile instead of waiting for you to reopen Pulse.",
ActionURL: UpgradeURLForFeature(FeaturePushNotifications),
Priority: 1,
},
{
Feature: FeatureLongTermMetrics,
Reason: "Get Relay for 14 days of metrics history, or Pro for 90 days — spot trends, plan capacity, and investigate past incidents.",
Reason: "Get Relay for 14 days of history, or Pro for 90 days, so you can see what changed before and after an incident.",
ActionURL: UpgradeURLForFeature(FeatureLongTermMetrics),
Priority: 2,
},
{
Feature: FeatureAIAutoFix,
Reason: "Upgrade to Pro to enable automatic remediation — Pulse Patrol will fix issues it finds without manual intervention.",
Reason: "Upgrade to Pro so Pulse can move from finding issues to applying safe remediation with your approval or in autonomous mode.",
ActionURL: UpgradeURLForFeature(FeatureAIAutoFix),
Priority: 3,
},
{
Feature: FeatureAIAlerts,
Reason: "Upgrade to Pro for AI-powered alert analysis — get root cause insights when alerts fire.",
Reason: "Upgrade to Pro so alerts arrive with root-cause analysis instead of a stack of symptoms.",
ActionURL: UpgradeURLForFeature(FeatureAIAlerts),
Priority: 4,
},
{
Feature: FeatureKubernetesAI,
Reason: "Upgrade to Pro for AI-powered Kubernetes insights — analyze pod health, resource pressure, and cluster issues.",
Reason: "Upgrade to Pro so Pulse can explain cluster pressure, failing pods, and likely causes without manual Kubernetes triage.",
ActionURL: UpgradeURLForFeature(FeatureKubernetesAI),
Priority: 5,
},
{
Feature: FeatureRBAC,
Reason: "Upgrade to Pro to control who can view, manage, and modify your infrastructure with fine-grained roles.",
Reason: "Upgrade to Pro when more than one operator needs safe access boundaries around infrastructure changes.",
ActionURL: UpgradeURLForFeature(FeatureRBAC),
Priority: 6,
},
{
Feature: FeatureAgentProfiles,
Reason: "Upgrade to Pro to manage agent configurations centrally — deploy consistent settings across your fleet.",
Reason: "Upgrade to Pro to standardize agent behavior across systems without reconfiguring every install by hand.",
ActionURL: UpgradeURLForFeature(FeatureAgentProfiles),
Priority: 7,
},
{
Feature: FeatureAdvancedSSO,
Reason: "Upgrade to Pro for SAML SSO, multi-provider support, and automatic role mapping.",
Reason: "Upgrade to Pro to connect your identity provider and keep operator access aligned with your existing auth controls.",
ActionURL: UpgradeURLForFeature(FeatureAdvancedSSO),
Priority: 8,
},
{
Feature: FeatureAuditLogging,
Reason: "Upgrade to Pro for tamper-evident audit logs — track every action for compliance and incident response.",
Reason: "Upgrade to Pro to keep a trustworthy action trail for incident review, accountability, and compliance.",
ActionURL: UpgradeURLForFeature(FeatureAuditLogging),
Priority: 9,
},
{
Feature: FeatureAdvancedReporting,
Reason: "Upgrade to Pro for automated infrastructure reports — generate PDF/CSV summaries for stakeholders.",
Reason: "Upgrade to Pro to turn live infrastructure state into shareable reports without manual screenshot work.",
ActionURL: UpgradeURLForFeature(FeatureAdvancedReporting),
Priority: 10,
},

View file

@ -63,3 +63,22 @@ func TestResolvePulseAccountPortalURL_RejectsInvalidOverrides(t *testing.T) {
})
}
}
func TestOperatorOutcomeUpgradeReasonsRemainCanonical(t *testing.T) {
expected := map[string]string{
FeatureRelay: "Get Relay so Pulse stays reachable securely from anywhere instead of only on the local dashboard.",
FeatureLongTermMetrics: "Get Relay for 14 days of history, or Pro for 90 days, so you can see what changed before and after an incident.",
FeatureAIAlerts: "Upgrade to Pro so alerts arrive with root-cause analysis instead of a stack of symptoms.",
FeatureAIAutoFix: "Upgrade to Pro so Pulse can move from finding issues to applying safe remediation with your approval or in autonomous mode.",
}
for _, entry := range UpgradeReasonMatrix {
want, ok := expected[entry.Feature]
if !ok {
continue
}
if entry.Reason != want {
t.Fatalf("reason for %q = %q, want %q", entry.Feature, entry.Reason, want)
}
}
}

View file

@ -17,7 +17,8 @@ const CANCELLED_BILLING_URL = `${DEV_SERVER_URL}/settings/system/billing/plan?in
const EXPIRED_BILLING_URL = `${DEV_SERVER_URL}/settings/system/billing/plan?intent=self_hosted_plan&purchase=expired`;
const FAILED_BILLING_URL = `${DEV_SERVER_URL}/settings/system/billing/plan?intent=self_hosted_plan&purchase=failed`;
const UNAVAILABLE_BILLING_URL = `${DEV_SERVER_URL}/settings/system/billing/plan?intent=self_hosted_plan&purchase=unavailable`;
const FINAL_BILLING_URL = `${DEV_SERVER_URL}/settings/system/billing/plan?intent=self_hosted_plan`;
const FINAL_BILLING_URL = `${DEV_SERVER_URL}/settings/system/billing/plan`;
const FINAL_RESTARTABLE_BILLING_URL = `${DEV_SERVER_URL}/settings/system/billing/plan?intent=self_hosted_plan`;
const USAGE_BILLING_URL = `${DEV_SERVER_URL}/settings/system/billing/usage`;
const RECOVERY_BILLING_HREF =
"/settings/system/billing/plan?intent=self_hosted_plan&details=recovery";
@ -356,7 +357,7 @@ async function openMonitoredSystemUpgradeArrival(page: Page) {
);
await expect(
page.getByText(
"Community keeps core monitoring free. Compare Relay and Pro in Pulse Account, then return here with Pulse Pro activated automatically.",
"Community keeps monitoring free. Compare Relay when you want secure access from anywhere, or Pro when you want root-cause answers, safe remediation, and 90-day incident history.",
),
).toBeVisible();
await expect(page.getByRole("button", { name: "Hide counting rules" })).toHaveCount(0);
@ -415,16 +416,11 @@ test.describe("Self-hosted upgrade return flow", () => {
).toHaveCount(0);
await expect(page).toHaveURL(FINAL_BILLING_URL);
await expect(
page.getByText("Pulse Pro activated", { exact: true }),
page.getByText("Plan activated", { exact: true }),
).toBeVisible();
const reviewUsageLink = page.getByRole("link", { name: "Review usage" });
await expect(reviewUsageLink).toHaveAttribute("href", "/settings/system/billing/usage");
await reviewUsageLink.click();
await expect(page).toHaveURL(USAGE_BILLING_URL);
await expect(page.getByRole("tab", { name: "Usage" })).toHaveAttribute(
"aria-selected",
"true",
);
const reviewPlanLink = page.getByRole("link", { name: "Review plan" });
await expect(reviewPlanLink).toHaveAttribute("href", "/settings/system/billing/plan");
await expect(page.getByRole("link", { name: "Review usage" })).toHaveCount(0);
});
test("returns cancelled checkout directly to the owned billing plan route", async ({
@ -487,12 +483,13 @@ test.describe("Self-hosted upgrade return flow", () => {
await expect(page).toHaveURL(FINAL_BILLING_URL);
await expect(
page.getByText("Pulse Pro activated", { exact: true }),
page.getByText("Plan activated", { exact: true }),
).toBeVisible();
await expect(page.getByRole("link", { name: "Review usage" })).toHaveAttribute(
await expect(page.getByRole("link", { name: "Review plan" })).toHaveAttribute(
"href",
"/settings/system/billing/usage",
"/settings/system/billing/plan",
);
await expect(page.getByRole("link", { name: "Review usage" })).toHaveCount(0);
});
test("returns an expired or mismatched checkout state to restart the owned upgrade flow", async ({
@ -621,7 +618,7 @@ test.describe("Self-hosted upgrade return flow", () => {
? page
: popup;
await expect(billingSurface).toHaveURL(FINAL_BILLING_URL);
await expect(billingSurface).toHaveURL(FINAL_RESTARTABLE_BILLING_URL);
await expect(
billingSurface.getByText("Pulse Account unavailable", { exact: true }),
).toBeVisible();

View file

@ -80,12 +80,12 @@ test.describe("Pulse Account upgrade bootstrap", () => {
).toBeVisible();
await expect(
page.getByText(
"Community keeps core monitoring free. Relay and Pro sell convenience, history, AI operations, and advanced administration.",
"Community keeps monitoring free. Relay gets Pulse securely from anywhere. Pro adds root-cause analysis, safe remediation, and 90-day incident history.",
),
).toBeVisible();
await expect(
page.getByText(
"Pulse Account compares self-hosted tiers by convenience and advanced capabilities, keeps core monitoring available across plans, and sends completed checkout straight back to Pulse Pro billing.",
"Pulse Account keeps checkout tied to the Pulse instance that opened it, so completed Relay or Pro purchases return to the right billing surface automatically.",
),
).toBeVisible();
await expect(