mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-19 02:28:59 +00:00
Reframe self-hosted paid value around operator outcomes
This commit is contained in:
parent
ed58890471
commit
61430703b2
17 changed files with 118 additions and 67 deletions
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"source_hash": "d2bcdf7f2c1910ca61deb35e6a85ea869d524c63c1bc8d0faba74e66ba62c4c1",
|
||||
"source_hash": "de861ffaf5a9cea5ea57c6314ce7615a991b5c302d717438758b702668c01828",
|
||||
"build_inputs": [
|
||||
"package.json",
|
||||
"tsconfig.json",
|
||||
|
|
|
|||
6
internal/cloudcp/portal/dist/portal_app.js
vendored
6
internal/cloudcp/portal/dist/portal_app.js
vendored
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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>';
|
||||
|
|
|
|||
|
|
@ -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"');
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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] {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue