diff --git a/docs/release-control/v6/internal/subsystems/cloud-paid.md b/docs/release-control/v6/internal/subsystems/cloud-paid.md
index 4a47d5c11..7f24d6ea4 100644
--- a/docs/release-control/v6/internal/subsystems/cloud-paid.md
+++ b/docs/release-control/v6/internal/subsystems/cloud-paid.md
@@ -650,6 +650,12 @@ as `portal_handoff_id`, feature, handoff lifecycle state, resolve timestamp,
and expiry; it must not disclose the bound `checkout_intent_id`, and browser
checkout creation must post only `portal_handoff_id` so Pulse Account
resolves the private checkout intent server-side before contacting Stripe.
+For self-hosted upgrades, that browser-facing feature metadata is now
+canonically `self_hosted_plan`. Pulse Account may continue normalizing the
+legacy `max_monitored_systems` alias so older handoff links do not break, but
+the portal proxy contract, checked-in embedded bundle, and rendered upgrade
+copy must treat self-hosted commerce as plan selection and paid extras rather
+than monitored-system-cap expansion.
If Pulse cannot create or resolve that portal handoff locally, the Pulse-owned
start route must still return the operator to the owned billing plan surface
with an explicit `purchase=unavailable` arrival instead of leaving the browser
diff --git a/internal/cloudcp/portal/dist/build_manifest.json b/internal/cloudcp/portal/dist/build_manifest.json
index 2683636a0..e8fa70810 100644
--- a/internal/cloudcp/portal/dist/build_manifest.json
+++ b/internal/cloudcp/portal/dist/build_manifest.json
@@ -1,5 +1,5 @@
{
- "source_hash": "6c9c1e7fc2f927d3c1862c8fee6c226a01fbbe0593e11742a9bca698963f418b",
+ "source_hash": "e8950e21d522eef56000c6801e20706305189e18e3e3ccc36ed54a35e672fa4e",
"build_inputs": [
"package.json",
"tsconfig.json",
diff --git a/internal/cloudcp/portal/dist/portal_app.js b/internal/cloudcp/portal/dist/portal_app.js
index abacdee25..48c71572d 100644
--- a/internal/cloudcp/portal/dist/portal_app.js
+++ b/internal/cloudcp/portal/dist/portal_app.js
@@ -1471,7 +1471,7 @@
if (explainer) {
summaryItems.push('
' + explainer + "
");
}
- root.innerHTML = '' + summaryItems.join("") + renderUpgradePlansHTML(billingState) + (pricingState.status === "ready" && pricingState.data && pricingState.data.description ? '
' + escapeText(pricingState.data.description) + "
" : "") + '
' + (featureKey === "max_monitored_systems" ? "Pulse Account compares self-hosted tiers by top-level monitored systems, keeps child resources underneath those roots included, 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.") + "
";
+ root.innerHTML = '' + summaryItems.join("") + renderUpgradePlansHTML(billingState) + (pricingState.status === "ready" && pricingState.data && pricingState.data.description ? '
' + escapeText(pricingState.data.description) + "
" : "") + '
' + (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.") + "
";
}
function renderButton(id, disabled, label) {
if (!id || !label) return;
@@ -2240,14 +2240,18 @@
function normalizeUpgradeFeatureKey(featureKey) {
return String(featureKey || "").trim();
}
+ function isSelfHostedPlanUpgrade(featureKey) {
+ var normalized = normalizeUpgradeFeatureKey(featureKey);
+ return normalized === "self_hosted_plan" || normalized === "max_monitored_systems";
+ }
function selfHostedUpgradeActionTitle(featureKey) {
- return normalizeUpgradeFeatureKey(featureKey) === "max_monitored_systems" ? "Upgrade monitored-system cap" : "Upgrade self-hosted plan";
+ return isSelfHostedPlanUpgrade(featureKey) ? "Compare self-hosted plans" : "Upgrade self-hosted plan";
}
function selfHostedUpgradeActionDescription(featureKey) {
- return normalizeUpgradeFeatureKey(featureKey) === "max_monitored_systems" ? "Compare self-hosted plans by top-level monitored systems such as Docker hosts, Kubernetes clusters, Proxmox nodes, standalone hosts, and TrueNAS systems." : "Compare self-hosted plans and continue into the commercial checkout path.";
+ 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.";
}
function selfHostedUpgradeActionHighlights(featureKey) {
- return normalizeUpgradeFeatureKey(featureKey) === "max_monitored_systems" ? ["Top-level monitored systems", "Plan comparison"] : ["Plan comparison", "Checkout handoff"];
+ return isSelfHostedPlanUpgrade(featureKey) ? ["Plan comparison", "Pulse Pro checkout"] : ["Plan comparison", "Checkout handoff"];
}
function renderSelfHostedUpgradeActionRow(context) {
var featureKey = normalizeUpgradeFeatureKey(context.billingState.upgradeFeatureKey);
@@ -2263,7 +2267,7 @@
}
function renderSelfHostedUpgradeBillingPanel(context) {
var featureKey = normalizeUpgradeFeatureKey(context.billingState.upgradeFeatureKey);
- var helperCopy = featureKey === "max_monitored_systems" ? "Choose the self-hosted tier that fits the top-level monitored systems you run. Child resources like VMs, containers, pods, disks, backups, and services underneath those roots are included. 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 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.";
return renderBillingTaskPanel(
selfHostedUpgradeActionTitle(featureKey),
"Pulse Account owns self-hosted plan selection and checkout for Pulse Pro upgrades.",
diff --git a/internal/cloudcp/portal/handlers_test.go b/internal/cloudcp/portal/handlers_test.go
index 6e9c29d6d..8e12f1746 100644
--- a/internal/cloudcp/portal/handlers_test.go
+++ b/internal/cloudcp/portal/handlers_test.go
@@ -1304,7 +1304,7 @@ func TestCommercialProxy_AllowsPortalHandoffGETRequests(t *testing.T) {
t.Fatalf("portal_handoff_id = %q, want cph_upgrade", got)
}
w.Header().Set("Content-Type", "application/json")
- _, _ = w.Write([]byte(`{"portal_handoff_id":"cph_upgrade","feature":"max_monitored_systems"}`))
+ _, _ = w.Write([]byte(`{"portal_handoff_id":"cph_upgrade","feature":"self_hosted_plan"}`))
}))
defer upstream.Close()
@@ -1317,7 +1317,7 @@ func TestCommercialProxy_AllowsPortalHandoffGETRequests(t *testing.T) {
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want %d (body=%q)", rec.Code, http.StatusOK, rec.Body.String())
}
- if got := strings.TrimSpace(rec.Body.String()); got != `{"portal_handoff_id":"cph_upgrade","feature":"max_monitored_systems"}` {
+ if got := strings.TrimSpace(rec.Body.String()); got != `{"portal_handoff_id":"cph_upgrade","feature":"self_hosted_plan"}` {
t.Fatalf("body = %q", got)
}
}