mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
Formalize v6 paid-surface classification
This commit is contained in:
@@ -75,6 +75,37 @@ Migration policy:
|
||||
Support rule:
|
||||
- If any self-hosted v6 install shows a bounded monitored-system cap after activation or migration, treat it as a bug rather than as intended policy. Guest limits still follow the active tier or continuity contract.
|
||||
|
||||
## V6 Product Classification
|
||||
|
||||
Pulse keeps some entitlement keys for compatibility, but not every Pro
|
||||
capability key is a primary v6 product pillar.
|
||||
|
||||
### Build On In v6
|
||||
|
||||
These are the current self-hosted Pro pillars that Pulse should keep
|
||||
investing in, surfacing, and marketing:
|
||||
- Alert-triggered root-cause analysis.
|
||||
- Safe remediation through Patrol auto-fix and higher-autonomy controls.
|
||||
- 90-day history.
|
||||
- Included team/admin extras: Advanced SSO (SAML), RBAC, audit logging,
|
||||
reporting, and agent profiles.
|
||||
|
||||
### Compatibility-Only In v6
|
||||
|
||||
These remain valid runtime gates for backwards compatibility, but should not
|
||||
be elevated into headline Pro marketing or generic upgrade prompts:
|
||||
- `FeatureKubernetesAI` / `kubernetes_ai`
|
||||
- Keeps the legacy `/api/ai/kubernetes/analyze` route gate intact.
|
||||
- Do not present it as a primary Pulse Pro pillar on current v6 surfaces.
|
||||
|
||||
### Legacy / Retired Claims
|
||||
|
||||
These should not appear as current v6 Pro promises unless they are rebuilt
|
||||
into first-class product surfaces:
|
||||
- `incident memory` as a standalone feature name
|
||||
- `scheduled remediations`
|
||||
- `execution audit trail`
|
||||
|
||||
## Feature Matrix
|
||||
|
||||
Legend:
|
||||
|
||||
+1
-1
@@ -58,7 +58,7 @@ The following should not be treated as primary marketed Pro pillars:
|
||||
capability bundle and confirms that Community / Relay / Pro monitored-system
|
||||
limits are all uncapped.
|
||||
2. `pulse-pro/license-server/public_pricing.go` already markets Pro as
|
||||
root-cause analysis, safe remediation, longer incident memory, and included
|
||||
root-cause analysis, safe remediation, 90-day history, and included
|
||||
team extras.
|
||||
3. `frontend-modern/src/components/Alerts/InvestigateAlertButton.tsx` and
|
||||
`frontend-modern/src/features/patrol/usePatrolIntelligenceState.ts` show
|
||||
|
||||
@@ -4334,21 +4334,7 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"work_claims": [
|
||||
{
|
||||
"id": "codex-lane-l1",
|
||||
"agent_id": "codex",
|
||||
"summary": "Guard forward release signing against accidental trust-root drift",
|
||||
"target_id": "v6-product-lane-expansion",
|
||||
"claimed_at": "2026-04-22T18:47:37Z",
|
||||
"heartbeat_at": "2026-04-22T18:47:37Z",
|
||||
"expires_at": "2026-04-22T20:47:37Z",
|
||||
"work_item": {
|
||||
"kind": "lane",
|
||||
"id": "L1"
|
||||
}
|
||||
}
|
||||
],
|
||||
"work_claims": [],
|
||||
"open_decisions": [],
|
||||
"source_of_truth_file": "docs/release-control/v6/internal/SOURCE_OF_TRUTH.md",
|
||||
"resolved_decisions": [
|
||||
@@ -4853,6 +4839,19 @@
|
||||
"L8",
|
||||
"L9"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "self-hosted-paid-surface-classification",
|
||||
"summary": "Current v6 self-hosted paid surfaces now classify commercial capabilities explicitly: only the primary Pro pillars (root-cause analysis, safe remediation, 90-day history, and included admin extras) may be marketed in customer-facing copy and upgrade prompts; compatibility-only gates such as `kubernetes_ai` remain valid runtime facts but non-marketed, and legacy claims such as `incident memory`, `scheduled remediations`, and `execution audit trail` stay retired unless rebuilt into first-class product surfaces.",
|
||||
"kind": "pricing",
|
||||
"decided_at": "2026-04-23",
|
||||
"subsystem_ids": [
|
||||
"cloud-paid"
|
||||
],
|
||||
"lane_ids": [
|
||||
"L3",
|
||||
"L9"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -261,7 +261,7 @@ Community limit enforcement.
|
||||
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
|
||||
remediation, and retain 90-day history`. 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.
|
||||
@@ -294,7 +294,10 @@ Community limit enforcement.
|
||||
SSO, RBAC, audit logging, reporting, and agent profiles may appear as
|
||||
secondary included extras, while platform-specific compatibility keys such
|
||||
as `kubernetes_ai` must not be elevated into a marquee marketed Pro line
|
||||
item on the self-hosted Plans surface.
|
||||
item on the self-hosted Plans surface. Legacy packaging nouns such as
|
||||
`incident memory`, `scheduled remediations`, and `execution audit trail`
|
||||
must likewise stay out of current v6 commercial copy unless Pulse ships a
|
||||
first-class product surface that makes those names truthful again.
|
||||
26. Keep hosted trial-activation verifier source selection compile-time owned.
|
||||
`pkg/licensing/trial_activation.go`,
|
||||
`pkg/licensing/trial_activation_public_key_override_dev.go`, and
|
||||
@@ -1334,7 +1337,8 @@ the no-cap monitored-system model as well. `ProLicensePanel.tsx`,
|
||||
self-hosted packages as unlimited core monitoring plus plan-specific extras:
|
||||
Community stays free for core monitoring, Relay adds remote access/mobile/push
|
||||
convenience and 14-day history, Pro adds Relay plus AI operations, automation,
|
||||
advanced administration, and 90-day history, while Pro+ remains legacy
|
||||
root-cause analysis, safe remediation, advanced administration, and 90-day
|
||||
history, while Pro+ remains legacy
|
||||
continuity only. Cloud/MSP pricing semantics stay separate, and grandfathered
|
||||
v5 continuity copy remains an explicit boundary policy.
|
||||
That same settings-owned presentation must distinguish between active
|
||||
@@ -1383,7 +1387,10 @@ That same `frontend-modern/src/utils/licensePresentation.ts` owner must treat
|
||||
compatibility-only capability keys such as `kubernetes_ai` as non-marketed
|
||||
technical facts: API/docs surfaces may still name the compatibility route, but
|
||||
customer-facing self-hosted current-plan summaries and unlocked-capability
|
||||
lists must not surface those keys as marquee Pro value.
|
||||
lists must not surface those keys as marquee Pro value. The same rule applies
|
||||
to legacy claims such as `incident memory`: current v6 upgrade notices and
|
||||
commercial copy must use the canonical 90-day history framing until a distinct
|
||||
incident-memory product exists.
|
||||
That same plan-section boundary must also defer notice resolution to component
|
||||
runtime. `frontend-modern/src/components/Settings/ProLicensePlanSection.tsx`
|
||||
may not compute trial-ended notices at module scope, because hosted settings
|
||||
|
||||
@@ -728,7 +728,7 @@ describe('ProLicensePanel', () => {
|
||||
expect(screen.getByText('Push Notifications')).toBeInTheDocument();
|
||||
expect(screen.getByText('Extended Metric History')).toBeInTheDocument();
|
||||
expect(screen.getByText('Multi-Tenant Mode')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Kubernetes Analysis')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Kubernetes AI Analysis (Compatibility)')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Multi-User Mode')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('White-Label Branding')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Unlimited Instances')).not.toBeInTheDocument();
|
||||
@@ -1155,7 +1155,7 @@ describe('ProLicensePanel', () => {
|
||||
);
|
||||
expect(proLicensePlanSectionSource).not.toContain('Your Pro trial has ended');
|
||||
expect(proLicensePlanSectionSource).not.toContain(
|
||||
'Turn alert noise into root-cause answers, safe fixes, and 90-day incident history.',
|
||||
'Turn alert noise into root-cause answers, safe fixes, and 90-day history.',
|
||||
);
|
||||
expect(selfHostedCommercialRecoverySectionSource).toContain(
|
||||
'SELF_HOSTED_RECOVERY_PRESENTATION',
|
||||
|
||||
@@ -63,7 +63,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 includes self-hosted monitoring. Look at Relay for secure access from anywhere, or Pulse Pro for root-cause answers, safe remediation, and 90-day incident history.',
|
||||
'Community includes self-hosted monitoring. Look at Relay for secure access from anywhere, or Pulse Pro for root-cause answers, safe remediation, and 90-day history.',
|
||||
planSelectionPromptActionLabel: 'Compare plans',
|
||||
purchaseActivatedPlanActionLabel: 'Review plan',
|
||||
purchaseCancelledActionLabel: 'Compare plans',
|
||||
@@ -72,7 +72,7 @@ export const SELF_HOSTED_PRO_BILLING_PRESENTATION: SelfHostedProBillingPresentat
|
||||
purchaseUnavailableActionLabel: 'Try again',
|
||||
trialStartTitle: 'Try Pro for free',
|
||||
trialStartBody:
|
||||
'Start a 14-day Pro trial for root-cause analysis, safe auto-fix, and 90-day incident history on this organization.',
|
||||
'Start a 14-day Pro trial for root-cause analysis, safe auto-fix, and 90-day history on this organization.',
|
||||
trialStartIdleActionLabel: 'Start 14-day Pro Trial',
|
||||
trialStartPendingActionLabel: 'Starting...',
|
||||
recoverySectionTitle: 'Activation & Recovery',
|
||||
|
||||
@@ -51,13 +51,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 root-cause analysis, safe auto-fix, and 90-day incident history.',
|
||||
body: 'Upgrade to keep root-cause analysis, safe auto-fix, and 90-day 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: 'Turn alert noise into root-cause answers, safe fixes, and 90-day incident history.',
|
||||
body: 'Turn alert noise into root-cause answers, safe fixes, and 90-day history.',
|
||||
actionLabel: 'View Pro plans',
|
||||
});
|
||||
expect(SELF_HOSTED_RECOVERY_PRESENTATION).toMatchObject({
|
||||
@@ -97,7 +97,7 @@ describe('licensePresentation', () => {
|
||||
'Pulse waits for the session presentation policy before showing license, billing, or usage details.',
|
||||
planSelectionPromptTitle: 'Compare self-hosted plans',
|
||||
planSelectionPromptBody:
|
||||
'Community includes self-hosted monitoring. Look at Relay for secure access from anywhere, or Pulse Pro for root-cause answers, safe remediation, and 90-day incident history.',
|
||||
'Community includes self-hosted monitoring. Look at Relay for secure access from anywhere, or Pulse Pro for root-cause answers, safe remediation, and 90-day history.',
|
||||
planSelectionPromptActionLabel: 'Compare plans',
|
||||
purchaseActivatedPlanActionLabel: 'Review plan',
|
||||
purchaseCancelledActionLabel: 'Compare plans',
|
||||
@@ -106,7 +106,7 @@ describe('licensePresentation', () => {
|
||||
purchaseUnavailableActionLabel: 'Try again',
|
||||
trialStartTitle: 'Try Pro for free',
|
||||
trialStartBody:
|
||||
'Start a 14-day Pro trial for root-cause analysis, safe auto-fix, and 90-day incident history on this organization.',
|
||||
'Start a 14-day Pro trial for root-cause analysis, safe auto-fix, and 90-day history on this organization.',
|
||||
trialStartIdleActionLabel: 'Start 14-day Pro Trial',
|
||||
trialStartPendingActionLabel: 'Starting...',
|
||||
recoverySectionTitle: 'Activation & Recovery',
|
||||
|
||||
@@ -37,7 +37,7 @@ const FEATURE_LABELS: Record<string, string> = {
|
||||
ai_patrol: 'Pulse Patrol',
|
||||
ai_alerts: 'Pulse Alert Analysis',
|
||||
ai_autofix: 'Patrol Auto-Fix',
|
||||
kubernetes_ai: 'Kubernetes Analysis',
|
||||
kubernetes_ai: 'Kubernetes AI Analysis (Compatibility)',
|
||||
update_alerts: 'Update Alerts',
|
||||
sso: 'Basic SSO (OIDC)',
|
||||
advanced_sso: 'Advanced SSO (SAML/Multi-Provider)',
|
||||
@@ -761,14 +761,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 root-cause analysis, safe auto-fix, and 90-day incident history.',
|
||||
body: 'Upgrade to keep root-cause analysis, safe auto-fix, and 90-day 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: 'Turn alert noise into root-cause answers, safe fixes, and 90-day incident history.',
|
||||
body: 'Turn alert noise into root-cause answers, safe fixes, and 90-day history.',
|
||||
actionLabel: 'View Pro plans',
|
||||
});
|
||||
|
||||
|
||||
@@ -5591,7 +5591,6 @@ func TestContract_SelfHostedCommunityEntitlementsJSONSnapshot(t *testing.T) {
|
||||
{"key":"long_term_metrics","reason":"Get Relay for 14 days of history, or Pro for 90 days, so you can see what changed before and after an incident.","action_url":"https://pulserelay.pro/pricing?utm_source=pulse\u0026utm_medium=app\u0026utm_campaign=upgrade\u0026feature=long_term_metrics"},
|
||||
{"key":"ai_autofix","reason":"Upgrade to Pro so Pulse can move from finding issues to applying safe remediation with your approval or in autonomous mode.","action_url":"https://pulserelay.pro/pricing?utm_source=pulse\u0026utm_medium=app\u0026utm_campaign=upgrade\u0026feature=ai_autofix"},
|
||||
{"key":"ai_alerts","reason":"Upgrade to Pro so alerts arrive with root-cause analysis instead of a stack of symptoms.","action_url":"https://pulserelay.pro/pricing?utm_source=pulse\u0026utm_medium=app\u0026utm_campaign=upgrade\u0026feature=ai_alerts"},
|
||||
{"key":"kubernetes_ai","reason":"Upgrade to Pro so Pulse can explain cluster pressure, failing pods, and likely causes without manual Kubernetes triage.","action_url":"https://pulserelay.pro/pricing?utm_source=pulse\u0026utm_medium=app\u0026utm_campaign=upgrade\u0026feature=kubernetes_ai"},
|
||||
{"key":"rbac","reason":"Upgrade to Pro when more than one operator needs safe access boundaries around infrastructure changes.","action_url":"https://pulserelay.pro/pricing?utm_source=pulse\u0026utm_medium=app\u0026utm_campaign=upgrade\u0026feature=rbac"},
|
||||
{"key":"agent_profiles","reason":"Upgrade to Pro to standardize agent behavior across systems without reconfiguring every install by hand.","action_url":"https://pulserelay.pro/pricing?utm_source=pulse\u0026utm_medium=app\u0026utm_campaign=upgrade\u0026feature=agent_profiles"},
|
||||
{"key":"advanced_sso","reason":"Upgrade to Pro to connect your identity provider and keep operator access aligned with your existing auth controls.","action_url":"https://pulserelay.pro/pricing?utm_source=pulse\u0026utm_medium=app\u0026utm_campaign=upgrade\u0026feature=advanced_sso"},
|
||||
|
||||
@@ -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: '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.',
|
||||
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 history.',
|
||||
explainer:
|
||||
'Community keeps monitoring free. Relay gets Pulse securely from anywhere. Pro adds root-cause analysis, safe remediation, and 90-day incident history.',
|
||||
'Community keeps monitoring free. Relay gets Pulse securely from anywhere. Pro adds root-cause analysis, safe remediation, and 90-day history.',
|
||||
plans: [
|
||||
{
|
||||
badge: 'Recommended',
|
||||
@@ -510,13 +510,13 @@ function routeCommercialAPI(request, response, url, scenario) {
|
||||
title: 'Pro',
|
||||
price: '$8.99',
|
||||
period: '$79/year available too',
|
||||
blurb: 'The operator tier for root-cause answers, safe remediation, and longer incident memory.',
|
||||
blurb: 'The operator tier for root-cause answers, safe remediation, and 90-day history.',
|
||||
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' },
|
||||
{ tone: 'check', html: '90-day 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 monitoring free. Relay gets Pulse securely from anywhere. Pro adds root-cause analysis, safe remediation, and 90-day incident history.',
|
||||
'Community keeps monitoring free. Relay gets Pulse securely from anywhere. Pro adds root-cause analysis, safe remediation, and 90-day history.',
|
||||
plans: [
|
||||
{
|
||||
tierKicker: 'Pro',
|
||||
title: 'Pro',
|
||||
price: '$8.99',
|
||||
period: '$79/year available too',
|
||||
blurb: 'The operator tier for root-cause answers, safe remediation, and longer incident memory.',
|
||||
blurb: 'The operator tier for root-cause answers, safe remediation, and 90-day history.',
|
||||
features: [{ tone: 'check', html: 'Unlimited <strong>self-hosted monitoring</strong>' }],
|
||||
buttons: [
|
||||
{
|
||||
@@ -273,7 +273,7 @@ describe('services view', function() {
|
||||
'Pulse Account keeps checkout tied to the Pulse instance that opened it, so completed Relay or Pro purchases return to the right Plans page automatically.',
|
||||
);
|
||||
expect(document.getElementById('upgrade-billing-root')?.innerHTML).toContain(
|
||||
'Community keeps monitoring free. Relay gets Pulse securely from anywhere. Pro adds root-cause analysis, safe remediation, and 90-day incident history.',
|
||||
'Community keeps monitoring free. Relay gets Pulse securely from anywhere. Pro adds root-cause analysis, safe remediation, and 90-day history.',
|
||||
);
|
||||
expect(document.getElementById('upgrade-billing-root')?.innerHTML).not.toContain('Continue to Plans');
|
||||
expect(document.getElementById('upgrade-billing-root')?.innerHTML).not.toContain('ppk_live_preview');
|
||||
|
||||
@@ -118,5 +118,10 @@ func expectedProOnlyFeatureSet() map[string]struct{} {
|
||||
for _, feature := range license.TierFeatures[license.TierFree] {
|
||||
delete(proFeatures, feature)
|
||||
}
|
||||
for feature := range proFeatures {
|
||||
if license.IsCompatibilityOnlyFeature(feature) {
|
||||
delete(proFeatures, feature)
|
||||
}
|
||||
}
|
||||
return proFeatures
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ func TestGetFeatureDisplayNameCoversKnownFeaturesAndFallback(t *testing.T) {
|
||||
{feature: FeatureAIPatrol, want: "Pulse Patrol (Background Health Checks)"},
|
||||
{feature: FeatureAIAlerts, want: "Alert Analysis"},
|
||||
{feature: FeatureAIAutoFix, want: "Pulse Patrol Auto-Fix"},
|
||||
{feature: FeatureKubernetesAI, want: "Kubernetes Analysis"},
|
||||
{feature: FeatureKubernetesAI, want: "Kubernetes AI Analysis (Compatibility)"},
|
||||
{feature: FeatureUpdateAlerts, want: "Update Alerts (Container/Package Updates)"},
|
||||
{feature: FeatureRBAC, want: "Role-Based Access Control (RBAC)"},
|
||||
{feature: FeatureMultiUser, want: "Multi-User Mode"},
|
||||
|
||||
@@ -76,3 +76,10 @@ func GetTierDisplayName(tier Tier) string {
|
||||
func GetFeatureDisplayName(feature string) string {
|
||||
return licensing.GetFeatureDisplayName(feature)
|
||||
}
|
||||
|
||||
// IsCompatibilityOnlyFeature reports capability keys that remain valid runtime
|
||||
// contracts for backwards compatibility but should not be marketed as current
|
||||
// v6 commercial pillars or generic upgrade prompts.
|
||||
func IsCompatibilityOnlyFeature(feature string) bool {
|
||||
return licensing.IsCompatibilityOnlyFeature(feature)
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ const (
|
||||
// Pro tier features (everything in Relay, plus:)
|
||||
FeatureAIAlerts = "ai_alerts" // AI analysis when alerts fire
|
||||
FeatureAIAutoFix = "ai_autofix" // Automatic remediation (one-click apply)
|
||||
FeatureKubernetesAI = "kubernetes_ai" // AI analysis of K8s (NOT basic monitoring)
|
||||
FeatureKubernetesAI = "kubernetes_ai" // Legacy Kubernetes analysis compatibility gate (NOT basic monitoring)
|
||||
FeatureAgentProfiles = "agent_profiles" // Centralized agent configuration profiles
|
||||
FeatureRBAC = "rbac" // Role-Based Access Control
|
||||
FeatureAuditLogging = "audit_logging" // Persistent audit logs with signing
|
||||
@@ -466,7 +466,7 @@ func GetFeatureDisplayName(feature string) string {
|
||||
case FeatureAIAutoFix:
|
||||
return "Pulse Patrol Auto-Fix"
|
||||
case FeatureKubernetesAI:
|
||||
return "Kubernetes Analysis"
|
||||
return "Kubernetes AI Analysis (Compatibility)"
|
||||
case FeatureUpdateAlerts:
|
||||
return "Update Alerts (Container/Package Updates)"
|
||||
case FeatureRBAC:
|
||||
@@ -503,3 +503,15 @@ func GetFeatureDisplayName(feature string) string {
|
||||
return feature
|
||||
}
|
||||
}
|
||||
|
||||
var compatibilityOnlyFeatures = map[string]struct{}{
|
||||
FeatureKubernetesAI: {},
|
||||
}
|
||||
|
||||
// IsCompatibilityOnlyFeature reports capability keys that remain valid runtime
|
||||
// contracts for backwards compatibility but should not be marketed as current
|
||||
// v6 commercial pillars or generic upgrade prompts.
|
||||
func IsCompatibilityOnlyFeature(feature string) bool {
|
||||
_, ok := compatibilityOnlyFeatures[feature]
|
||||
return ok
|
||||
}
|
||||
|
||||
@@ -225,7 +225,7 @@ func TestGetFeatureDisplayName(t *testing.T) {
|
||||
{FeatureAIPatrol, "Pulse Patrol (Background Health Checks)"},
|
||||
{FeatureAIAlerts, "Alert Analysis"},
|
||||
{FeatureAIAutoFix, "Pulse Patrol Auto-Fix"},
|
||||
{FeatureKubernetesAI, "Kubernetes Analysis"},
|
||||
{FeatureKubernetesAI, "Kubernetes AI Analysis (Compatibility)"},
|
||||
{FeatureUpdateAlerts, "Update Alerts (Container/Package Updates)"},
|
||||
{FeatureRBAC, "Role-Based Access Control (RBAC)"},
|
||||
{FeatureMultiUser, "Multi-User Mode"},
|
||||
@@ -255,6 +255,15 @@ func TestGetFeatureDisplayName(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsCompatibilityOnlyFeature(t *testing.T) {
|
||||
if !IsCompatibilityOnlyFeature(FeatureKubernetesAI) {
|
||||
t.Fatalf("expected %q to remain compatibility-only", FeatureKubernetesAI)
|
||||
}
|
||||
if IsCompatibilityOnlyFeature(FeatureAIAlerts) {
|
||||
t.Fatalf("did not expect %q to be compatibility-only", FeatureAIAlerts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTierMonitoredSystemLimits(t *testing.T) {
|
||||
tests := []struct {
|
||||
tier Tier
|
||||
|
||||
@@ -12,6 +12,8 @@ type ReasonEntry struct {
|
||||
|
||||
// UpgradeReasonMatrix is the canonical feature-to-upgrade-reason mapping.
|
||||
// Relay features use "Upgrade to Relay" messaging; Pro features use "Upgrade to Pro".
|
||||
// Compatibility-only capabilities must not appear here as generic marketed
|
||||
// upgrade reasons.
|
||||
var UpgradeReasonMatrix = []ReasonEntry{
|
||||
{
|
||||
Feature: FeatureRelay,
|
||||
@@ -49,41 +51,35 @@ var UpgradeReasonMatrix = []ReasonEntry{
|
||||
ActionURL: UpgradeURLForFeature(FeatureAIAlerts),
|
||||
Priority: 4,
|
||||
},
|
||||
{
|
||||
Feature: FeatureKubernetesAI,
|
||||
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 when more than one operator needs safe access boundaries around infrastructure changes.",
|
||||
ActionURL: UpgradeURLForFeature(FeatureRBAC),
|
||||
Priority: 6,
|
||||
Priority: 5,
|
||||
},
|
||||
{
|
||||
Feature: FeatureAgentProfiles,
|
||||
Reason: "Upgrade to Pro to standardize agent behavior across systems without reconfiguring every install by hand.",
|
||||
ActionURL: UpgradeURLForFeature(FeatureAgentProfiles),
|
||||
Priority: 7,
|
||||
Priority: 6,
|
||||
},
|
||||
{
|
||||
Feature: FeatureAdvancedSSO,
|
||||
Reason: "Upgrade to Pro to connect your identity provider and keep operator access aligned with your existing auth controls.",
|
||||
ActionURL: UpgradeURLForFeature(FeatureAdvancedSSO),
|
||||
Priority: 8,
|
||||
Priority: 7,
|
||||
},
|
||||
{
|
||||
Feature: FeatureAuditLogging,
|
||||
Reason: "Upgrade to Pro to keep a trustworthy action trail for incident review, accountability, and compliance.",
|
||||
ActionURL: UpgradeURLForFeature(FeatureAuditLogging),
|
||||
Priority: 9,
|
||||
Priority: 8,
|
||||
},
|
||||
{
|
||||
Feature: FeatureAdvancedReporting,
|
||||
Reason: "Upgrade to Pro to turn live infrastructure state into shareable reports without manual screenshot work.",
|
||||
ActionURL: UpgradeURLForFeature(FeatureAdvancedReporting),
|
||||
Priority: 10,
|
||||
Priority: 9,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -100,3 +100,12 @@ func TestOperatorOutcomeUpgradeReasonsRemainCanonical(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCompatibilityOnlyFeaturesStayOutOfGenericUpgradeReasons(t *testing.T) {
|
||||
reasons := GenerateUpgradeReasons(nil)
|
||||
for _, entry := range reasons {
|
||||
if entry.Feature == FeatureKubernetesAI {
|
||||
t.Fatalf("compatibility-only feature %q should not produce a generic upgrade reason", entry.Feature)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4557,6 +4557,7 @@ class SubsystemLookupTest(unittest.TestCase):
|
||||
"cloud-msp-stripe-prices",
|
||||
"legacy-grandfathering-eligibility-cutoff",
|
||||
"self-hosted-plans-surface-entitlement-first",
|
||||
"self-hosted-paid-surface-classification",
|
||||
"self-hosted-core-monitoring-free",
|
||||
"self-hosted-paid-extras-packaging",
|
||||
"stable-release-promotion-model",
|
||||
|
||||
@@ -421,7 +421,7 @@ async function openMonitoredSystemUpgradeArrival(page: Page) {
|
||||
);
|
||||
await expect(
|
||||
page.getByText(
|
||||
"Community includes self-hosted monitoring. Look at Relay for secure access from anywhere, or Pulse Pro for root-cause answers, safe remediation, and 90-day incident history.",
|
||||
"Community includes self-hosted monitoring. Look at Relay for secure access from anywhere, or Pulse Pro for root-cause answers, safe remediation, and 90-day history.",
|
||||
),
|
||||
).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Hide counting rules" })).toHaveCount(0);
|
||||
|
||||
@@ -80,7 +80,7 @@ test.describe("Pulse Account upgrade bootstrap", () => {
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByText(
|
||||
"Community keeps monitoring free. Relay gets Pulse securely from anywhere. Pro adds root-cause analysis, safe remediation, and 90-day incident history.",
|
||||
"Community keeps monitoring free. Relay gets Pulse securely from anywhere. Pro adds root-cause analysis, safe remediation, and 90-day history.",
|
||||
),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
|
||||
Reference in New Issue
Block a user