mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-08-19 22:13:31 +00:00
parent
3ab2d95d93
commit
5914a4127d
37 changed files with 341 additions and 356 deletions
47
README.md
47
README.md
|
|
@ -135,39 +135,44 @@ Community-maintained integrations and addons:
|
|||
|
||||
- **[Home Assistant Addons](https://github.com/Kosztyk/homeassistant-addons)** - Run Pulse Agent and Pulse Server as Home Assistant addons.
|
||||
|
||||
## 💳 Plans (Community / Relay / Pro / Pro+ / Cloud)
|
||||
## 💳 Plans (Community / Relay / Pro / Cloud)
|
||||
|
||||
Pulse is full-featured for core monitoring in every tier. Self-hosted pricing now sells monitored coverage by monitored system, not by installed agent. Cloud and MSP pricing are unchanged.
|
||||
Pulse is full-featured for core monitoring in every self-hosted tier. Self-hosted
|
||||
pricing no longer sells more room for monitoring volume; paid value comes from
|
||||
convenience, history, AI operations, and advanced administration. Cloud and MSP
|
||||
pricing are unchanged.
|
||||
|
||||
Self-hosted tiers:
|
||||
|
||||
| Plan | Price | Included monitored systems | Metric history | Key upgrade |
|
||||
|---|---:|---:|---:|---|
|
||||
| Community | Free | 5 | 7 days | Core monitoring for one real small lab |
|
||||
| Relay | $4.99/mo or $39/yr | 8 | 14 days | Remote access, mobile, and push notifications |
|
||||
| Pro | $8.99/mo or $79/yr | 15 | 90 days | AI investigation, auto-fix, and operations tooling |
|
||||
| Pro+ | $14.99/mo or $129/yr | 50 | 90 days | More room for larger self-hosted labs |
|
||||
| Plan | Price | Core monitoring | Metric history | Key upgrade |
|
||||
|---|---:|---|---:|---|
|
||||
| Community | Free | Unlimited | 7 days | Full self-hosted monitoring |
|
||||
| Relay | $4.99/mo or $39/yr | Unlimited | 14 days | Remote access, mobile, and push notifications |
|
||||
| Pro | $8.99/mo or $79/yr | Unlimited | 90 days | AI investigation, auto-fix, and operations tooling |
|
||||
|
||||
Pulse counts top-level monitored systems once no matter how they are collected. VMs, containers, pods, disks, backups, and other child resources under that system are included rather than counted separately.
|
||||
Pulse still counts top-level monitored systems once no matter how they are
|
||||
collected. VMs, containers, pods, disks, backups, and other child resources
|
||||
under that system are included rather than counted separately, but that count is
|
||||
no longer the self-hosted paid gate.
|
||||
|
||||
Community keeps Patrol available with BYOK. Activated or trial-backed installs can also use
|
||||
Patrol quickstart for first-run activation: 25 Patrol runs with no API key on a
|
||||
server-verified install. Without activation, self-hosted Patrol continues with your
|
||||
configured provider. Relay remains the convenience tier, and Pro/Pro+ are the paid
|
||||
operations tiers.
|
||||
configured provider. Relay remains the convenience tier, and Pro is the paid
|
||||
operations tier.
|
||||
|
||||
Runtime-aligned capability summary:
|
||||
|
||||
| Capability | Community | Relay | Pro | Pro+ | Cloud |
|
||||
|---|:---:|:---:|:---:|:---:|:---:|
|
||||
| Pulse Patrol (Background Health Checks) | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| Remote Access / Mobile / Push | — | ✅ | ✅ | ✅ | ✅ |
|
||||
| Alert Analysis | — | — | ✅ | ✅ | ✅ |
|
||||
| Pulse Patrol Auto-Fix | — | — | ✅ | ✅ | ✅ |
|
||||
| Kubernetes Analysis | — | — | ✅ | ✅ | ✅ |
|
||||
| Centralized Agent Profiles | — | — | ✅ | ✅ | ✅ |
|
||||
| Update Alerts (Container/Package Updates) | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| Basic SSO (OIDC) | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| Capability | Community | Relay | Pro | Cloud |
|
||||
|---|:---:|:---:|:---:|:---:|
|
||||
| Pulse Patrol (Background Health Checks) | ✅ | ✅ | ✅ | ✅ |
|
||||
| Remote Access / Mobile / Push | — | ✅ | ✅ | ✅ |
|
||||
| Alert Analysis | — | — | ✅ | ✅ |
|
||||
| Pulse Patrol Auto-Fix | — | — | ✅ | ✅ |
|
||||
| Kubernetes Analysis | — | — | ✅ | ✅ |
|
||||
| Centralized Agent Profiles | — | — | ✅ | ✅ |
|
||||
| Update Alerts (Container/Package Updates) | ✅ | ✅ | ✅ | ✅ |
|
||||
| Basic SSO (OIDC) | ✅ | ✅ | ✅ | ✅ |
|
||||
| Advanced SSO (SAML/Multi-Provider) | — | — | ✅ | ✅ | ✅ |
|
||||
| Role-Based Access Control (RBAC) | — | — | ✅ | ✅ | ✅ |
|
||||
| Enterprise Audit Logging | — | — | ✅ | ✅ | ✅ |
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ If a setting is disabled with an amber warning, it's being overridden by an envi
|
|||
## 🔍 Monitoring & Metrics
|
||||
|
||||
### What do Relay, Pro, and Cloud unlock?
|
||||
Relay adds remote access, mobile access, push notifications, and a little more monitored-system headroom. Pro, Pro+, and Cloud unlock **Auto-Fix and advanced AI analysis**. Pulse Patrol is available to everyone on Community with BYOK and provides scheduled, cross-system analysis that correlates real-time state, recent metrics history, and diagnostics to surface actionable findings.
|
||||
Relay adds remote access, mobile access, push notifications, and 14-day history. Pro and Cloud unlock **Auto-Fix and advanced AI analysis** along with the broader operations feature set. Existing Pro+ holders keep their current continuity, but self-hosted pricing no longer sells more monitoring volume. Pulse Patrol is available to everyone on Community with BYOK and provides scheduled, cross-system analysis that correlates real-time state, recent metrics history, and diagnostics to surface actionable findings.
|
||||
|
||||
Example output includes trend-based capacity warnings, backup regressions, Kubernetes AI cluster analysis, and correlated container failures that simple threshold alerts miss.
|
||||
See [Pulse AI](AI.md), [Plans and entitlements](PULSE_PRO.md), and <https://pulserelay.pro>.
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
# Pulse Plans and Entitlements (Community / Relay / Pro / Pro+ / Cloud)
|
||||
# Pulse Plans and Entitlements (Community / Relay / Pro / Cloud)
|
||||
|
||||
This document explains Pulse's user-facing plan structure, the locked self-hosted commercial model, and how those plans map to runtime feature gates.
|
||||
|
||||
|
|
@ -13,26 +13,26 @@ User-facing plans map to internal tiers as follows:
|
|||
- **Community**: `free`
|
||||
- **Relay**: `relay`
|
||||
- **Pro**: `pro`, `pro_annual`, `lifetime`
|
||||
- **Pro+**: `pro_plus`
|
||||
- **Cloud**: `msp` or `enterprise`
|
||||
|
||||
Notes:
|
||||
- `lifetime` keeps the same runtime feature set as Pro, but lifetime and grandfathered recurring legacy entitlements remain uncapped for monitored systems and guest access. Other migrated legacy paid installs can preserve monitored-system continuity without becoming uncapped.
|
||||
- `lifetime` keeps the same runtime feature set as Pro, but lifetime and grandfathered recurring legacy entitlements remain uncapped for monitored systems and guest access. Other migrated legacy paid installs can still carry cohort continuity metadata for support and audit, but self-hosted monitoring volume is no longer the paid gate.
|
||||
- Items marked **Cloud*** require the `enterprise` tier rather than the base `msp` tier.
|
||||
- If you are self-hosting, you can use capability keys and `GET /api/license/features` to discover exactly what is active in your instance.
|
||||
|
||||
## Self-Hosted Commercial Model
|
||||
|
||||
Pulse sells monitored coverage. The counted unit is a monitored system, not an installed agent.
|
||||
Pulse does not monetize self-hosted users on monitored-system volume. The counted
|
||||
unit remains a monitored system for product understanding, migrations, and
|
||||
inventory truth, but self-hosted core monitoring is not the paid gate.
|
||||
|
||||
Self-hosted pricing is locked to:
|
||||
Self-hosted pricing is:
|
||||
|
||||
| Plan | Price | Included monitored systems | Metric history | Purpose |
|
||||
|---|---:|---:|---:|---|
|
||||
| Community | Free | 5 | 7 days | One real small lab end to end |
|
||||
| Relay | $4.99/mo or $39/yr | 8 | 14 days | Cheap headroom plus remote access |
|
||||
| Pro | $8.99/mo or $79/yr | 15 | 90 days | Automation and operations tier |
|
||||
| Pro+ | $14.99/mo or $129/yr | 50 | 90 days | Larger self-hosted labs |
|
||||
| Plan | Price | Core monitoring | Metric history | Purpose |
|
||||
|---|---:|---|---:|---|
|
||||
| Community | Free | Unlimited | 7 days | Full self-hosted monitoring for normal homelab use |
|
||||
| Relay | $4.99/mo or $39/yr | Unlimited | 14 days | Remote access, mobile, push, and convenience |
|
||||
| Pro | $8.99/mo or $79/yr | Unlimited | 90 days | AI operations, automation, and advanced admin features |
|
||||
|
||||
Counted examples:
|
||||
- Proxmox PVE node
|
||||
|
|
@ -59,10 +59,8 @@ Runtime rules:
|
|||
|
||||
Migration policy:
|
||||
- Legacy recurring Pulse Pro subscriptions already active before the public v6 pricing cutover keep their grandfathered recurring price and uncapped monitored-system and guest capacity until cancellation.
|
||||
- Supported legacy paid v5 migrations outside that recurring grandfathered path can retain a monitored-system continuity floor for the migrated install where needed, but that continuity path is not the same as an uncapped entitlement.
|
||||
- Existing free users above the new Community cap are not hard-broken on rollout day.
|
||||
- During grace, existing monitoring keeps working.
|
||||
- During grace, only new counted-system additions are blocked until the user removes systems or upgrades.
|
||||
- Existing lifetime license holders remain valid and uncapped.
|
||||
- Supported legacy paid v5 migrations outside that recurring grandfathered path can still exchange into the v6 activation model without losing self-hosted monitoring access. Migration metadata can preserve the original cohort for support and audit, but monitored-system volume is no longer the paid gate.
|
||||
|
||||
### Paid Customer Continuity Matrix
|
||||
|
||||
|
|
@ -70,12 +68,12 @@ Migration policy:
|
|||
|---|---|---|
|
||||
| Legacy recurring subscriber from a v5 or earlier Pulse Pro monthly/annual plan, already active before the public v6 pricing cutover | The install can migrate into the v6 activation model without forcing a repurchase. | The existing recurring price stays in place and monitored-system plus guest capacity remain uncapped while the subscription remains continuously active. |
|
||||
| Existing lifetime license holder | The license remains valid through the v6 licensing transition. | Lifetime remains permanently valid and monitored-system plus guest capacity stay uncapped. |
|
||||
| Legacy paid v5 license migrated into v6 outside the recurring grandfathered path | The install can still exchange into the v6 activation model. If its existing monitored estate exceeds the exchanged plan limit, the migrated install can retain a monitored-system continuity floor for that install. | The continuity floor preserves the existing migrated estate where needed, but it is not an uncapped entitlement. |
|
||||
| Former recurring subscriber who already canceled or later lapses/cancels | A later return is treated as a new paid purchase, not as a grandfathered renewal. | The old grandfathered price and uncapped capacity do not resume automatically; current public v6 pricing and caps apply. |
|
||||
| New self-hosted v6 purchase | The purchase uses the current Relay / Pro / Pro+ self-hosted plans. | Current public v6 monitored-system and guest caps apply. |
|
||||
| Legacy paid v5 license migrated into v6 outside the recurring grandfathered path | The install can still exchange into the v6 activation model without forcing a repurchase. Migration records can still preserve the original cohort for support and audit. | Self-hosted monitoring stays available; monitored-system volume is no longer sold as a paid gate on current v6 self-hosted plans. |
|
||||
| Former recurring subscriber who already canceled or later lapses/cancels | A later return is treated as a new paid purchase, not as a grandfathered renewal. | The old grandfathered price and uncapped capacity do not resume automatically; current public v6 pricing applies. |
|
||||
| New self-hosted v6 purchase | The purchase uses the current Community / Relay / Pro self-hosted plans. | Core monitoring stays unlimited; paid value comes from convenience, AI, history, and advanced admin features. |
|
||||
|
||||
Support rule:
|
||||
- If a grandfathered recurring subscriber covered by the pre-cutover legacy policy, or a lifetime customer, sees a bounded monitored-system or guest cap in v6, treat it as a bug rather than as intended policy.
|
||||
- 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.
|
||||
|
||||
## Feature Matrix
|
||||
|
||||
|
|
@ -119,7 +117,7 @@ Patrol and the Assistant support tiered autonomy:
|
|||
## What You Get (By Plan)
|
||||
|
||||
### Community
|
||||
- Core monitoring for up to 5 monitored systems.
|
||||
- Unlimited self-hosted core monitoring.
|
||||
- 7-day history.
|
||||
- Pulse Patrol with BYOK.
|
||||
- Patrol quickstart after activation or trial: 25 Patrol runs with no API key on a server-verified install.
|
||||
|
|
@ -128,14 +126,12 @@ Patrol and the Assistant support tiered autonomy:
|
|||
|
||||
### Relay
|
||||
- Everything in Community, plus:
|
||||
- 8 monitored systems.
|
||||
- 14-day history.
|
||||
- Remote access via Relay.
|
||||
- Mobile app access and push notifications.
|
||||
|
||||
### Pro
|
||||
- Everything in Relay, plus:
|
||||
- 15 monitored systems.
|
||||
- AI alert analysis.
|
||||
- Auto-fix and higher autonomy.
|
||||
- Kubernetes AI analysis.
|
||||
|
|
@ -143,8 +139,8 @@ Patrol and the Assistant support tiered autonomy:
|
|||
- Advanced SSO, RBAC, audit logging, and advanced reporting.
|
||||
- 90-day history.
|
||||
|
||||
### Pro+
|
||||
- Everything in Pro, with 50 monitored systems for larger self-hosted labs.
|
||||
### Legacy Pro+
|
||||
- Existing Pro+ entitlements remain supported for current holders, but Pro+ is no longer presented as a public self-hosted plan because monitored-system volume is no longer the paid boundary.
|
||||
|
||||
### Cloud
|
||||
- Hosted Pulse with Pro-level capabilities and hosted lifecycle management.
|
||||
|
|
|
|||
|
|
@ -78,27 +78,26 @@ All other documents are supporting references unless explicitly required for evi
|
|||
- **[Mail Gateway](MAIL_GATEWAY.md)** – Proxmox Mail Gateway (PMG) monitoring.
|
||||
- **[Auto Updates](AUTO_UPDATE.md)** – One-click updates for supported deployments.
|
||||
- **[Multi-Tenant Organizations](MULTI_TENANT.md)** – Isolate infrastructure by organization (Enterprise, opt-in).
|
||||
- **[Entitlements Overhaul](PULSE_PRO.md)** – Capability-key-based feature gating across Community/Relay/Pro/Pro+/Cloud.
|
||||
- **[Entitlements Overhaul](PULSE_PRO.md)** – Capability-key-based feature gating across Community/Relay/Pro/Cloud, with legacy Pro+ continuity still supported.
|
||||
|
||||
## 💳 Plans (Community / Relay / Pro / Pro+ / Cloud)
|
||||
## 💳 Plans (Community / Relay / Pro / Cloud)
|
||||
|
||||
Pulse is available in four self-hosted tiers plus hosted Cloud:
|
||||
Pulse is available in three self-hosted tiers plus hosted Cloud:
|
||||
|
||||
- **Community**: Free self-hosted monitoring for up to 5 monitored systems with 7-day history.
|
||||
- **Relay**: Adds remote access, mobile, push notifications, 14-day history, and raises the monitored-system limit to 8.
|
||||
- **Pro**: Adds AI investigation, auto-fix, operations tooling, and raises the monitored-system limit to 15 with 90-day history.
|
||||
- **Pro+**: Everything in Pro with room for up to 50 monitored systems.
|
||||
- **Community**: Free self-hosted monitoring with unlimited core monitoring and 7-day history.
|
||||
- **Relay**: Adds remote access, mobile, push notifications, and 14-day history.
|
||||
- **Pro**: Adds AI investigation, auto-fix, operations tooling, governance features, and 90-day history.
|
||||
- **Cloud**: Hosted Pulse with Pro-level capabilities; hosted pricing is unchanged by the self-hosted model lock.
|
||||
|
||||
- **[Learn more at pulserelay.pro](https://pulserelay.pro)**
|
||||
- **[Plans and entitlements](PULSE_PRO.md)** (includes the Community/Relay/Pro/Pro+/Cloud matrix)
|
||||
- **[Plans and entitlements](PULSE_PRO.md)** (includes the Community/Relay/Pro/Cloud matrix)
|
||||
- **[AI deep dive](AI.md)**
|
||||
- **[Multi-Tenant Organizations (Enterprise)](MULTI_TENANT.md)** — Isolate infrastructure by organization for MSPs and multi-datacenter deployments.
|
||||
|
||||
## 📡 Monitoring & Agents
|
||||
|
||||
- **[Unified Agent](UNIFIED_AGENT.md)** – Single binary for host, Docker, and Kubernetes monitoring.
|
||||
- **[Centralized Agent Management (Pro/Pro+/Cloud)](CENTRALIZED_MANAGEMENT.md)** – Agent profiles and remote config.
|
||||
- **[Centralized Agent Management (Pro/Cloud)](CENTRALIZED_MANAGEMENT.md)** – Agent profiles and remote config.
|
||||
- **[Proxmox Backup Server](PBS.md)** – PBS integration, direct API vs PVE passthrough, token setup.
|
||||
- **[TrueNAS](TRUENAS.md)** – TrueNAS SCALE/CORE integration.
|
||||
- **[ZFS Monitoring](ZFS_MONITORING.md)** – Proxmox-native ZFS pool monitoring.
|
||||
|
|
|
|||
|
|
@ -115,12 +115,13 @@ Pulse v6 feature gating is driven by the entitlements endpoint:
|
|||
|
||||
- `GET /api/license/entitlements`
|
||||
|
||||
For self-hosted v6, Pulse now sells monitored coverage by monitored system rather than by installed agent. Community includes 5 monitored systems, Relay includes 8, Pro includes 15, and Pro+ includes 50. Relay also raises history to 14 days, while Pro and Pro+ raise it to 90 days.
|
||||
For self-hosted v6, Pulse no longer sells monitored-system volume. Core
|
||||
monitoring stays available across Community, Relay, and Pro, while Relay and
|
||||
Pro sell convenience, history, AI operations, and advanced administration.
|
||||
Relay raises history to 14 days, while Pro raises it to 90 days.
|
||||
|
||||
For self-hosted v6, `POST /api/license/trial/start` initiates hosted signup by returning `409 trial_signup_required` with a hosted `action_url` rather than minting a local trial directly. Duplicate start attempts stay on the hosted-signup retry burst until it is exhausted, then return canonical `429 trial_rate_limited` plus `Retry-After` backoff metadata. Pulse only reflects trial lifecycle entitlements after the hosted control plane returns a signed activation token to `/auth/trial-activate`.
|
||||
|
||||
If you are upgrading an existing free instance that already exceeds the new Community cap, Pulse should not hard-break monitoring on rollout day. During grace, existing monitoring continues and only newly added counted systems are blocked until you remove systems or upgrade.
|
||||
|
||||
#### v5 License Migration
|
||||
|
||||
Pulse v6 uses the activation/grant model for active licensing, but it can migrate valid Pulse v5 paid JWT-style licenses, including legacy Pro and Lifetime licenses.
|
||||
|
|
@ -131,8 +132,10 @@ Pulse v6 uses the activation/grant model for active licensing, but it can migrat
|
|||
- a Pulse v6 activation key, or
|
||||
- a valid Pulse v5 paid license key, which Pulse will try to exchange automatically into the v6 activation model
|
||||
- If the exchange service cannot complete the migration, retry from the v6 license panel or use the self-serve retrieval flow to fetch the current v6 activation key. Email is only a backup copy of that key.
|
||||
- The exchanged v6 entitlement depends on the original cohort. Lifetime, active pre-cutover recurring Pro, and other migrated legacy paid installs do not all land on the same monitored-system policy.
|
||||
- Legacy recurring Pulse Pro subscriptions already active before the public v6 pricing cutover keep their grandfathered recurring price and uncapped monitored-system and guest capacity until cancellation. If they cancel and later return, current v6 pricing and limits apply.
|
||||
- The exchanged v6 entitlement depends on the original cohort. Lifetime,
|
||||
active pre-cutover recurring Pro, and other migrated legacy paid installs do
|
||||
not all land on the same commercial continuity posture.
|
||||
- Legacy recurring Pulse Pro subscriptions already active before the public v6 pricing cutover keep their grandfathered recurring price and uncapped monitored-system and guest capacity until cancellation. If they cancel and later return, current v6 pricing applies.
|
||||
|
||||
#### Paid Upgrade Truth Table
|
||||
|
||||
|
|
@ -140,11 +143,11 @@ When an existing paid user asks what changes for them specifically, use this rul
|
|||
|
||||
- Legacy recurring Pulse Pro subscriptions from v5 or earlier that were already active before the public v6 pricing cutover keep their current recurring price and uncapped monitored-system plus guest capacity while the subscription remains continuously active.
|
||||
- Existing lifetime customers remain permanently valid and uncapped.
|
||||
- Legacy paid v5 licenses migrated into v6 outside the recurring grandfathered path can retain a monitored-system continuity floor for the migrated install where needed, but that continuity path is not the same thing as an uncapped entitlement.
|
||||
- Former recurring customers who already canceled, or who cancel and later return, do not resume the old grandfathered pricing or uncapped capacity automatically; they re-enter on current public v6 pricing and limits.
|
||||
- New self-hosted v6 purchases use the current Relay / Pro / Pro+ plan caps.
|
||||
- Legacy paid v5 licenses migrated into v6 outside the recurring grandfathered path can still exchange into the v6 activation model without repurchasing. Migration records can preserve the original cohort for support and audit, but self-hosted monitoring volume is no longer the paid gate.
|
||||
- Former recurring customers who already canceled, or who cancel and later return, do not resume the old grandfathered pricing or uncapped capacity automatically; they re-enter on current public v6 pricing.
|
||||
- New self-hosted v6 purchases use the current Community / Relay / Pro plan model with unlimited core monitoring.
|
||||
|
||||
If a grandfathered recurring subscriber covered by the pre-cutover legacy policy, or a lifetime customer, sees a new monitored-system or guest cap after moving to v6, treat that as a regression, not as expected upgrade behavior.
|
||||
If a self-hosted v6 install sees a new monitored-system cap after moving to v6, treat that as a regression, not as expected upgrade behavior.
|
||||
|
||||
Practical recommendation:
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
# Self-Hosted Commercial Model Lock
|
||||
# Self-Hosted Commercial Model Lock (Superseded)
|
||||
|
||||
Date: 2026-03-17
|
||||
Target: `v6-rc-stabilization`
|
||||
Lanes: `L2`, `L13`
|
||||
Status: Superseded on 2026-04-16 by `self-hosted-core-monitoring-free`; this record now preserves the capped RC1 self-hosted model as historical context only and is not the final GA pricing direction.
|
||||
|
||||
## Decision
|
||||
## Historical Decision
|
||||
|
||||
Pulse v6 self-hosted commercial packaging is locked to this model:
|
||||
On 2026-03-17, Pulse v6 self-hosted commercial packaging was locked to this capped RC1 model:
|
||||
|
||||
| Plan | Price | Included limit | History | Purpose |
|
||||
|---|---:|---:|---:|---|
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
# Self-Hosted Core Monitoring Free Direction
|
||||
|
||||
Date: 2026-04-16
|
||||
Target: `v6-rc-stabilization`
|
||||
Lanes: `L2`, `L3`, `L9`
|
||||
|
||||
## Decision
|
||||
|
||||
Pulse v6 self-hosted core monitoring is not a paid gate.
|
||||
|
||||
- Homelab users must not be monetized on monitored-system volume itself.
|
||||
- `monitored systems` remains the canonical counted unit for product understanding, migration truth, and any optional commercial UX that still needs usage context.
|
||||
- Paid self-hosted value should come from optional extras, hosted convenience, business workflow, support, or similar non-core surfaces rather than from gating core monitoring coverage.
|
||||
|
||||
## Why
|
||||
|
||||
- The capped RC1 self-hosted ladder puts commercial pressure on the same serious self-hosted users who are the natural Pulse adoption base.
|
||||
- The product goal is for self-hosted Pulse to feel useful and generous for normal and even fairly serious homelab use.
|
||||
- Open-source core monitoring should stay free, while paid value comes from extras that are meaningfully optional or business-oriented.
|
||||
|
||||
## Consequences
|
||||
|
||||
- The capped 2026-03-17 self-hosted model lock is superseded as the final GA direction and remains only as historical RC1 context.
|
||||
- Current RC-era self-hosted cap ladders, copy, and pricing records must not be treated as settled GA truth just because they already shipped in `v6.0.0-rc.1`.
|
||||
- Before GA, Pulse still needs one governed packaging decision for which self-hosted extras remain paid and how Relay, Pro, Pro+, or successor plans map to that non-cap model.
|
||||
- Existing lifetime and grandfathered recurring continuity remain unaffected: they stay valid and uncapped.
|
||||
|
|
@ -4238,8 +4238,8 @@
|
|||
]
|
||||
},
|
||||
{
|
||||
"id": "self-hosted-homelab-monetization-boundary",
|
||||
"summary": "Pulse v6 should not monetize homelab users on core monitoring itself; before GA, decide whether that principle lands as a materially more generous self-hosted cap ladder or as a model where core monitoring stays free and optional extras plus business-grade workflow, convenience, and support features carry the paid value.",
|
||||
"id": "self-hosted-paid-extras-packaging",
|
||||
"summary": "With self-hosted core monitoring no longer treated as a paid gate, decide before GA which optional extras, convenience features, AI or business-workflow surfaces, and support tiers remain the paid self-hosted packaging and how Relay, Pro, Pro+, or successor plans map onto that model.",
|
||||
"owner": "project-owner",
|
||||
"blocking_level": "release-ready",
|
||||
"status": "open",
|
||||
|
|
@ -4701,6 +4701,20 @@
|
|||
"L3",
|
||||
"L9"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "self-hosted-core-monitoring-free",
|
||||
"summary": "Pulse v6 self-hosted core monitoring is not a paid gate: homelab users must not be monetized on monitored-system volume itself, monitored systems remain the canonical counted unit for product understanding and migration truth, and paid self-hosted value must come from optional extras, hosted convenience, business workflow, support, or similar non-core surfaces.",
|
||||
"kind": "pricing",
|
||||
"decided_at": "2026-04-16",
|
||||
"subsystem_ids": [
|
||||
"cloud-paid"
|
||||
],
|
||||
"lane_ids": [
|
||||
"L2",
|
||||
"L3",
|
||||
"L9"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -161,7 +161,7 @@ the same active-at-snapshot rule as the live server before anyone sets
|
|||
`usage`) that survives direct links, compatibility redirects, and
|
||||
in-product CTA navigation. The current canonical arrivals are
|
||||
`/settings/system/billing/usage?details=counting-rules` for explanation and
|
||||
`/settings/system/billing/plan?intent=max_monitored_systems` for upgrade
|
||||
`/settings/system/billing/plan?intent=self_hosted_plan` for upgrade
|
||||
intent.
|
||||
24. Keep public-demo dashboard bootstrap route-owned on the adjacent
|
||||
commercial/runtime boundary. `frontend-modern/src/useAppRuntimeState.ts`
|
||||
|
|
@ -224,6 +224,11 @@ Insights`, rather than reviving generic `AI Patrol` or `AI ... analysis`
|
|||
`pkg/licensing/models.go`, and downstream runtime entitlement evaluation
|
||||
must strip that stored cap before enforcement so active recurring
|
||||
grandfathered continuity remains uncapped until cancellation.
|
||||
8. Before GA, treat self-hosted core monitoring as free for homelab use:
|
||||
monitored systems remain the canonical counted unit, but self-hosted paid
|
||||
value must come from optional extras, hosted convenience, business
|
||||
workflow, support, or similar non-core surfaces rather than using
|
||||
monitored-system volume itself as the primary paid gate.
|
||||
|
||||
## Current State
|
||||
|
||||
|
|
|
|||
|
|
@ -70,21 +70,22 @@ Use this cohort breakdown:
|
|||
remain permanently valid and uncapped.
|
||||
- Legacy paid v5 licenses migrated into v6 outside the recurring grandfathered
|
||||
path:
|
||||
can retain a monitored-system continuity floor for the migrated install where
|
||||
needed, but that continuity path is not the same as an uncapped entitlement.
|
||||
can still exchange into the v6 activation model without repurchasing.
|
||||
Migration records can preserve the original cohort for support and audit, but
|
||||
self-hosted monitoring volume is no longer the paid gate.
|
||||
- Former recurring subscribers who already canceled or later lapse:
|
||||
any later return uses current public v6 pricing and caps rather than
|
||||
any later return uses current public v6 pricing rather than
|
||||
resuming the old grandfathered terms.
|
||||
- New self-hosted v6 purchasers:
|
||||
take the current Relay / Pro / Pro+ caps.
|
||||
take the current Community / Relay / Pro model with unlimited core
|
||||
monitoring.
|
||||
|
||||
If a grandfathered recurring subscriber covered by the pre-cutover legacy
|
||||
policy, or a lifetime customer, sees a bounded monitored-system or guest cap
|
||||
in v6, treat that as a product bug, not as intended policy.
|
||||
If a self-hosted v6 install shows a bounded monitored-system cap after
|
||||
activation or migration, treat that as a product bug, not as intended policy.
|
||||
|
||||
Canonical short reply:
|
||||
|
||||
`Lifetime customers remain uncapped. Legacy recurring Pulse Pro subscribers who were already active before the public v6 pricing cutover remain uncapped while that subscription stays active. Other migrated legacy paid installs can preserve a monitored-system continuity floor for that install where needed, but that is not an uncapped entitlement. Former recurring subscribers who return later, and new/current public v6 purchases, use the current v6 plan caps.`
|
||||
`Lifetime customers remain uncapped. Legacy recurring Pulse Pro subscribers who were already active before the public v6 pricing cutover remain uncapped while that subscription stays active. Other migrated legacy paid installs can still exchange into the v6 activation model without repurchasing, but self-hosted monitoring volume is no longer the paid gate. Former recurring subscribers who return later re-enter on current v6 pricing, and current self-hosted v6 purchases keep core monitoring unlimited.`
|
||||
|
||||
### Will old bookmarks and familiar v5 pages still work?
|
||||
|
||||
|
|
|
|||
|
|
@ -78,27 +78,26 @@ All other documents are supporting references unless explicitly required for evi
|
|||
- **[Mail Gateway](MAIL_GATEWAY.md)** – Proxmox Mail Gateway (PMG) monitoring.
|
||||
- **[Auto Updates](AUTO_UPDATE.md)** – One-click updates for supported deployments.
|
||||
- **[Multi-Tenant Organizations](MULTI_TENANT.md)** – Isolate infrastructure by organization (Enterprise, opt-in).
|
||||
- **[Entitlements Overhaul](PULSE_PRO.md)** – Capability-key-based feature gating across Community/Relay/Pro/Pro+/Cloud.
|
||||
- **[Entitlements Overhaul](PULSE_PRO.md)** – Capability-key-based feature gating across Community/Relay/Pro/Cloud, with legacy Pro+ continuity still supported.
|
||||
|
||||
## 💳 Plans (Community / Relay / Pro / Pro+ / Cloud)
|
||||
## 💳 Plans (Community / Relay / Pro / Cloud)
|
||||
|
||||
Pulse is available in four self-hosted tiers plus hosted Cloud:
|
||||
Pulse is available in three self-hosted tiers plus hosted Cloud:
|
||||
|
||||
- **Community**: Free self-hosted monitoring for up to 5 monitored systems with 7-day history.
|
||||
- **Relay**: Adds remote access, mobile, push notifications, 14-day history, and raises the monitored-system limit to 8.
|
||||
- **Pro**: Adds AI investigation, auto-fix, operations tooling, and raises the monitored-system limit to 15 with 90-day history.
|
||||
- **Pro+**: Everything in Pro with room for up to 50 monitored systems.
|
||||
- **Community**: Free self-hosted monitoring with unlimited core monitoring and 7-day history.
|
||||
- **Relay**: Adds remote access, mobile, push notifications, and 14-day history.
|
||||
- **Pro**: Adds AI investigation, auto-fix, operations tooling, governance features, and 90-day history.
|
||||
- **Cloud**: Hosted Pulse with Pro-level capabilities; hosted pricing is unchanged by the self-hosted model lock.
|
||||
|
||||
- **[Learn more at pulserelay.pro](https://pulserelay.pro)**
|
||||
- **[Plans and entitlements](PULSE_PRO.md)** (includes the Community/Relay/Pro/Pro+/Cloud matrix)
|
||||
- **[Plans and entitlements](PULSE_PRO.md)** (includes the Community/Relay/Pro/Cloud matrix)
|
||||
- **[AI deep dive](AI.md)**
|
||||
- **[Multi-Tenant Organizations (Enterprise)](MULTI_TENANT.md)** — Isolate infrastructure by organization for MSPs and multi-datacenter deployments.
|
||||
|
||||
## 📡 Monitoring & Agents
|
||||
|
||||
- **[Unified Agent](UNIFIED_AGENT.md)** – Single binary for host, Docker, and Kubernetes monitoring.
|
||||
- **[Centralized Agent Management (Pro/Pro+/Cloud)](CENTRALIZED_MANAGEMENT.md)** – Agent profiles and remote config.
|
||||
- **[Centralized Agent Management (Pro/Cloud)](CENTRALIZED_MANAGEMENT.md)** – Agent profiles and remote config.
|
||||
- **[Proxmox Backup Server](PBS.md)** – PBS integration, direct API vs PVE passthrough, token setup.
|
||||
- **[TrueNAS](TRUENAS.md)** – TrueNAS SCALE/CORE integration.
|
||||
- **[ZFS Monitoring](ZFS_MONITORING.md)** – Proxmox-native ZFS pool monitoring.
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ export const ProLicensePlanSection: Component<ProLicensePlanSectionProps> = (pro
|
|||
const inactiveProUpsellNotice =
|
||||
!props.hasPaidFeatures && !props.trialEnded ? getInactiveProUpsellNotice() : null;
|
||||
const monitoredSystemUpgradeDestination = () =>
|
||||
resolveSelfHostedPurchaseStartDestination('max_monitored_systems');
|
||||
resolveSelfHostedPurchaseStartDestination('self_hosted_plan');
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import {
|
|||
getPublicPricingUrl,
|
||||
getSelfHostedPurchaseStartUrl,
|
||||
SELF_HOSTED_PRO_BILLING_PLAN_HREF,
|
||||
SELF_HOSTED_PRO_BILLING_MONITORED_SYSTEM_INTENT,
|
||||
SELF_HOSTED_PRO_BILLING_PURCHASE_ACTIVATED,
|
||||
SELF_HOSTED_PRO_BILLING_PURCHASE_CANCELLED,
|
||||
SELF_HOSTED_PRO_BILLING_PURCHASE_EXPIRED,
|
||||
|
|
@ -295,7 +296,7 @@ describe('ProLicensePanel', () => {
|
|||
});
|
||||
|
||||
expect(screen.getByText('V5 Lifetime Grandfathered')).toBeInTheDocument();
|
||||
expect(screen.getByText('Included Monitored Systems')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Included Monitored Systems')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('Max Guests')).toBeInTheDocument();
|
||||
expect(screen.getAllByText('Unlimited').length).toBeGreaterThan(0);
|
||||
expect(screen.queryByText('5 / 12')).not.toBeInTheDocument();
|
||||
|
|
@ -351,7 +352,7 @@ describe('ProLicensePanel', () => {
|
|||
'Unlimited',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText('Included Monitored Systems')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Included Monitored Systems')).not.toBeInTheDocument();
|
||||
expect(screen.getByText('Max Guests')).toBeInTheDocument();
|
||||
expect(screen.getAllByText('Unlimited').length).toBeGreaterThan(0);
|
||||
expect(screen.queryByText('Plan Monitored System Limit')).not.toBeInTheDocument();
|
||||
|
|
@ -694,7 +695,7 @@ describe('ProLicensePanel', () => {
|
|||
|
||||
it('shows a monitored-system activation success action and suppresses the compare-plans arrival callout', async () => {
|
||||
useLocationMock.mockReturnValue({
|
||||
search: `?purchase=${SELF_HOSTED_PRO_BILLING_PURCHASE_ACTIVATED}&intent=max_monitored_systems`,
|
||||
search: `?purchase=${SELF_HOSTED_PRO_BILLING_PURCHASE_ACTIVATED}&intent=${SELF_HOSTED_PRO_BILLING_MONITORED_SYSTEM_INTENT}`,
|
||||
pathname: '/settings/system/billing/plan',
|
||||
hash: '',
|
||||
});
|
||||
|
|
@ -706,7 +707,7 @@ describe('ProLicensePanel', () => {
|
|||
'href',
|
||||
SELF_HOSTED_PRO_BILLING_USAGE_HREF,
|
||||
);
|
||||
expect(screen.queryByText('Need a higher monitored-system cap?')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Compare self-hosted plans')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('opens recovery by default when the billing route requests the recovery detail', async () => {
|
||||
|
|
@ -742,17 +743,17 @@ describe('ProLicensePanel', () => {
|
|||
|
||||
it('shows a monitored-system upgrade arrival callout on the plan upgrade route', async () => {
|
||||
useLocationMock.mockReturnValue({
|
||||
search: '?intent=max_monitored_systems',
|
||||
search: `?intent=${SELF_HOSTED_PRO_BILLING_MONITORED_SYSTEM_INTENT}`,
|
||||
pathname: '/settings/system/billing/plan',
|
||||
hash: '',
|
||||
});
|
||||
|
||||
renderPanel();
|
||||
|
||||
expect(screen.getByText('Need a higher monitored-system cap?')).toBeInTheDocument();
|
||||
expect(screen.getByText('Compare self-hosted plans')).toBeInTheDocument();
|
||||
expect(
|
||||
screen.getByText(
|
||||
'Pulse counts top-level monitored systems such as Docker hosts, Kubernetes clusters, Proxmox nodes, standalone hosts, and TrueNAS systems. Compare self-hosted plans in Pulse Account and return here with Pulse Pro activated automatically.',
|
||||
'Community keeps core monitoring free. Compare Relay and Pro in Pulse Account, then return here with Pulse Pro activated automatically.',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Hide counting rules' })).toBeInTheDocument();
|
||||
|
|
@ -763,7 +764,7 @@ describe('ProLicensePanel', () => {
|
|||
).toBeInTheDocument();
|
||||
expect(screen.getByRole('link', { name: 'Compare plans' })).toHaveAttribute(
|
||||
'href',
|
||||
getSelfHostedPurchaseStartUrl('max_monitored_systems'),
|
||||
getSelfHostedPurchaseStartUrl('self_hosted_plan'),
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -982,7 +983,7 @@ describe('ProLicensePanel', () => {
|
|||
);
|
||||
expect(proLicensePlanSectionSource).toContain('monitoredSystemUpgradeArrivalTitle');
|
||||
expect(proLicensePlanSectionSource).toContain(
|
||||
"resolveSelfHostedPurchaseStartDestination('max_monitored_systems')",
|
||||
"resolveSelfHostedPurchaseStartDestination('self_hosted_plan')",
|
||||
);
|
||||
expect(proLicensePlanSectionSource).not.toContain('Your Pro trial has ended');
|
||||
expect(proLicensePlanSectionSource).not.toContain(
|
||||
|
|
|
|||
|
|
@ -1434,7 +1434,7 @@ describe('Settings architecture guardrails', () => {
|
|||
'SELF_HOSTED_PRO_BILLING_PRESENTATION.infrastructureRouteReferral',
|
||||
);
|
||||
expect(settingsHeaderMetaSource).not.toContain(
|
||||
'Billing and monitored-system limits live in Pulse Pro.',
|
||||
'Billing and self-hosted plan features live in Pulse Pro.',
|
||||
);
|
||||
expect(SETTINGS_HEADER_META['infrastructure-operations'].title).toBe(
|
||||
'Infrastructure Operations',
|
||||
|
|
@ -1447,9 +1447,9 @@ describe('Settings architecture guardrails', () => {
|
|||
);
|
||||
});
|
||||
|
||||
it('keeps billing-related shell framing on monitored-system commercial terms', () => {
|
||||
it('keeps billing-related shell framing on self-hosted commercial terms', () => {
|
||||
expect(SETTINGS_HEADER_META['infrastructure-operations'].description).toContain(
|
||||
'monitored-system limits',
|
||||
'self-hosted plan features',
|
||||
);
|
||||
expect(SETTINGS_HEADER_META['infrastructure-operations'].description).not.toContain(
|
||||
'installed-agent',
|
||||
|
|
|
|||
|
|
@ -35,10 +35,10 @@ export interface SelfHostedProBillingPresentation {
|
|||
export const SELF_HOSTED_PRO_BILLING_PRESENTATION: SelfHostedProBillingPresentation = {
|
||||
shellTitle: 'Pulse Pro',
|
||||
shellDescription:
|
||||
'Manage self-hosted billing, monitored-system limits, and Pulse Pro license status.',
|
||||
infrastructureRouteReferral: 'Billing and monitored-system limits live in Pulse Pro.',
|
||||
'Manage self-hosted billing, plan features, and Pulse Pro license status.',
|
||||
infrastructureRouteReferral: 'Billing and self-hosted plan features live in Pulse Pro.',
|
||||
infrastructureWorkspaceReferral:
|
||||
'Billing, monitored-system limits, and Pulse Pro license status live in Pulse Pro, not here.',
|
||||
'Billing, self-hosted plan features, and Pulse Pro license status live in Pulse Pro, not here.',
|
||||
sectionSelectorAriaLabel: 'Pulse Pro billing section',
|
||||
refreshLabel: 'Refresh',
|
||||
planTabLabel: 'Plan',
|
||||
|
|
@ -54,9 +54,9 @@ export const SELF_HOSTED_PRO_BILLING_PRESENTATION: SelfHostedProBillingPresentat
|
|||
policyLoadingTitle: 'Loading settings access',
|
||||
policyLoadingBody:
|
||||
'Pulse waits for the session presentation policy before showing license, billing, or usage details.',
|
||||
monitoredSystemUpgradeArrivalTitle: 'Need a higher monitored-system cap?',
|
||||
monitoredSystemUpgradeArrivalTitle: 'Compare self-hosted plans',
|
||||
monitoredSystemUpgradeArrivalBody:
|
||||
'Pulse counts top-level monitored systems such as Docker hosts, Kubernetes clusters, Proxmox nodes, standalone hosts, and TrueNAS systems. Compare self-hosted plans in Pulse Account and return here with Pulse Pro activated automatically.',
|
||||
'Community keeps core monitoring free. Compare Relay and Pro in Pulse Account, then return here with Pulse Pro activated automatically.',
|
||||
monitoredSystemUpgradeArrivalActionLabel: 'Compare plans',
|
||||
purchaseActivatedUsageActionLabel: 'Review usage',
|
||||
purchaseCancelledActionLabel: 'Compare plans',
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ describe('MonitoredSystemLimitWarningBanner', () => {
|
|||
external: false,
|
||||
});
|
||||
mockGetUpgradeActionUrlOrFallback.mockReturnValue(
|
||||
'/settings/system/billing/plan?intent=max_monitored_systems',
|
||||
'/settings/system/billing/plan?intent=self_hosted_plan',
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -211,7 +211,7 @@ describe('MonitoredSystemLimitWarningBanner', () => {
|
|||
expect(screen.getByText('Upgrade to add more')).toBeInTheDocument();
|
||||
expect(screen.getByText('Upgrade to add more')).toHaveAttribute(
|
||||
'href',
|
||||
'/settings/system/billing/plan?intent=max_monitored_systems',
|
||||
'/settings/system/billing/plan?intent=self_hosted_plan',
|
||||
);
|
||||
expect(screen.queryByText('Install v6 collectors')).not.toBeInTheDocument();
|
||||
});
|
||||
|
|
@ -268,7 +268,7 @@ describe('MonitoredSystemLimitWarningBanner', () => {
|
|||
);
|
||||
expect(screen.getByText('Upgrade to add more')).toHaveAttribute(
|
||||
'href',
|
||||
'/settings/system/billing/plan?intent=max_monitored_systems',
|
||||
'/settings/system/billing/plan?intent=self_hosted_plan',
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -543,7 +543,7 @@ describe('license stores', () => {
|
|||
});
|
||||
await loadCommercialPosture(true);
|
||||
expect(getUpgradeActionUrlOrFallback('max_monitored_systems')).toBe(
|
||||
'/settings/system/billing/plan?intent=max_monitored_systems',
|
||||
'/settings/system/billing/plan?intent=self_hosted_plan',
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -65,10 +65,10 @@ describe('licensePresentation', () => {
|
|||
expect(SELF_HOSTED_PRO_BILLING_PRESENTATION).toEqual({
|
||||
shellTitle: 'Pulse Pro',
|
||||
shellDescription:
|
||||
'Manage self-hosted billing, monitored-system limits, and Pulse Pro license status.',
|
||||
infrastructureRouteReferral: 'Billing and monitored-system limits live in Pulse Pro.',
|
||||
'Manage self-hosted billing, plan features, and Pulse Pro license status.',
|
||||
infrastructureRouteReferral: 'Billing and self-hosted plan features live in Pulse Pro.',
|
||||
infrastructureWorkspaceReferral:
|
||||
'Billing, monitored-system limits, and Pulse Pro license status live in Pulse Pro, not here.',
|
||||
'Billing, self-hosted plan features, and Pulse Pro license status live in Pulse Pro, not here.',
|
||||
sectionSelectorAriaLabel: 'Pulse Pro billing section',
|
||||
refreshLabel: 'Refresh',
|
||||
planTabLabel: 'Plan',
|
||||
|
|
@ -85,9 +85,9 @@ describe('licensePresentation', () => {
|
|||
policyLoadingTitle: 'Loading settings access',
|
||||
policyLoadingBody:
|
||||
'Pulse waits for the session presentation policy before showing license, billing, or usage details.',
|
||||
monitoredSystemUpgradeArrivalTitle: 'Need a higher monitored-system cap?',
|
||||
monitoredSystemUpgradeArrivalTitle: 'Compare self-hosted plans',
|
||||
monitoredSystemUpgradeArrivalBody:
|
||||
'Pulse counts top-level monitored systems such as Docker hosts, Kubernetes clusters, Proxmox nodes, standalone hosts, and TrueNAS systems. Compare self-hosted plans in Pulse Account and return here with Pulse Pro activated automatically.',
|
||||
'Community keeps core monitoring free. Compare Relay and Pro in Pulse Account, then return here with Pulse Pro activated automatically.',
|
||||
monitoredSystemUpgradeArrivalActionLabel: 'Compare plans',
|
||||
purchaseActivatedUsageActionLabel: 'Review usage',
|
||||
purchaseCancelledActionLabel: 'Compare plans',
|
||||
|
|
|
|||
|
|
@ -108,6 +108,9 @@ describe('pricingHandoff', () => {
|
|||
expect(
|
||||
getSelfHostedBillingPlanIntent('?intent=' + SELF_HOSTED_PRO_BILLING_MONITORED_SYSTEM_INTENT),
|
||||
).toBe(SELF_HOSTED_PRO_BILLING_MONITORED_SYSTEM_INTENT);
|
||||
expect(getSelfHostedBillingPlanIntent('?intent=max_monitored_systems')).toBe(
|
||||
SELF_HOSTED_PRO_BILLING_MONITORED_SYSTEM_INTENT,
|
||||
);
|
||||
expect(getSelfHostedBillingPlanDetail('?details=' + SELF_HOSTED_PRO_BILLING_RECOVERY_DETAIL)).toBe(
|
||||
SELF_HOSTED_PRO_BILLING_RECOVERY_DETAIL,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -13,22 +13,17 @@ describe('selfHostedPlans', () => {
|
|||
'Community',
|
||||
'Relay',
|
||||
'Pro',
|
||||
'Pro+',
|
||||
]);
|
||||
|
||||
expect(SELF_HOSTED_PLAN_BY_TIER.community.monitoredSystems).toBe(5);
|
||||
expect(SELF_HOSTED_PLAN_BY_TIER.relay.monitoredSystems).toBe(8);
|
||||
expect(SELF_HOSTED_PLAN_BY_TIER.pro.monitoredSystems).toBe(15);
|
||||
expect(SELF_HOSTED_PLAN_BY_TIER.proPlus.monitoredSystems).toBe(50);
|
||||
|
||||
const hostsRow = SELF_HOSTED_FEATURE_ROWS.find((row) => row.key === 'hosts');
|
||||
expect(hostsRow).toEqual({
|
||||
key: 'hosts',
|
||||
name: 'Monitored Systems',
|
||||
community: '5',
|
||||
relay: '8',
|
||||
pro: '15',
|
||||
proPlus: '50',
|
||||
const monitoringScopeRow = SELF_HOSTED_FEATURE_ROWS.find(
|
||||
(row) => row.key === 'core_monitoring_scope',
|
||||
);
|
||||
expect(monitoringScopeRow).toEqual({
|
||||
key: 'core_monitoring_scope',
|
||||
name: 'Core Monitoring Scope',
|
||||
community: 'Unlimited',
|
||||
relay: 'Unlimited',
|
||||
pro: 'Unlimited',
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -36,14 +31,13 @@ describe('selfHostedPlans', () => {
|
|||
expect(SELF_HOSTED_COMMERCIAL_PRESENTATION).toEqual({
|
||||
pageTitle: 'Pricing',
|
||||
pageDescription:
|
||||
'Core monitoring stays free. Pulse counts top-level monitored systems, while child resources underneath them are included. Relay adds remote access, and Pro unlocks Pulse Patrol, alert analysis, and auto-fix.',
|
||||
'Core monitoring stays free for self-hosted Pulse. Relay adds remote access and mobile convenience, while Pro unlocks AI operations, automation, governance, and longer history.',
|
||||
mostPopularBadge: 'Most Popular',
|
||||
currentPlanLabel: 'Current Plan',
|
||||
includedLabel: 'Included',
|
||||
freeLabel: 'Free',
|
||||
buyRelayLabel: 'Buy Relay',
|
||||
upgradeToProLabel: 'Upgrade to Pro',
|
||||
upgradeToProPlusLabel: 'Upgrade to Pro+',
|
||||
startTrialLabel: 'Start Free 14-day Trial',
|
||||
featureComparisonHeading: 'Feature Comparison',
|
||||
footerLinks: [
|
||||
|
|
@ -57,34 +51,21 @@ describe('selfHostedPlans', () => {
|
|||
label: 'See MSP plans',
|
||||
href: 'mailto:hello@pulserelay.pro?subject=Pulse%20MSP%20Inquiry',
|
||||
},
|
||||
{
|
||||
preface: 'Need more than 50 monitored systems?',
|
||||
label: 'Contact us',
|
||||
href: 'mailto:hello@pulserelay.pro?subject=Pulse%20Enterprise%20Inquiry',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps Community quickstart copy scoped to Patrol activation support', () => {
|
||||
expect(SELF_HOSTED_PLAN_BY_TIER.community.subline).toBe(
|
||||
'Up to 5 top-level monitored systems',
|
||||
);
|
||||
expect(SELF_HOSTED_PLAN_BY_TIER.community.subline).toBe('Unlimited self-hosted monitoring');
|
||||
expect(SELF_HOSTED_PLAN_BY_TIER.community.highlights).toEqual(
|
||||
expect.arrayContaining([
|
||||
'Unlimited self-hosted monitoring',
|
||||
'Pulse Patrol (BYOK)',
|
||||
'Patrol quickstart after activation or trial: 25 runs, no API key',
|
||||
]),
|
||||
);
|
||||
expect(SELF_HOSTED_PLAN_BY_TIER.relay.highlights).toContain(
|
||||
'8 top-level monitored systems · 14-day history',
|
||||
);
|
||||
expect(SELF_HOSTED_PLAN_BY_TIER.pro.highlights).toContain(
|
||||
'15 top-level monitored systems',
|
||||
);
|
||||
expect(SELF_HOSTED_PLAN_BY_TIER.proPlus.highlights).toContain(
|
||||
'50 top-level monitored systems',
|
||||
);
|
||||
expect(SELF_HOSTED_PLAN_BY_TIER.relay.highlights).toContain('14-day metric history');
|
||||
expect(SELF_HOSTED_PLAN_BY_TIER.pro.highlights).toContain('Unlimited self-hosted monitoring');
|
||||
expect(SELF_HOSTED_PLAN_BY_TIER.community.highlights).not.toContain(
|
||||
'Patrol quickstart: 25 runs, no API key',
|
||||
);
|
||||
|
|
|
|||
|
|
@ -54,6 +54,9 @@ export interface HostedCommercialModelInput {
|
|||
const asUnlimitedLimit = (value?: number) =>
|
||||
typeof value === 'number' && value > 0 ? value : undefined;
|
||||
|
||||
const hasFiniteSelfHostedLimit = (value: string | number) =>
|
||||
typeof value === 'number' ? value > 0 : value.trim().toLowerCase() !== 'unlimited';
|
||||
|
||||
export const buildSelfHostedCommercialPlanModel = (
|
||||
input: SelfHostedCommercialModelInput,
|
||||
): CommercialPlanViewModel => ({
|
||||
|
|
@ -128,12 +131,14 @@ export const buildSelfHostedCommercialPlanModel = (
|
|||
: input.continuityCapturedAt || 'Captured',
|
||||
},
|
||||
]
|
||||
: [
|
||||
{
|
||||
label: 'Included Monitored Systems',
|
||||
value: input.maxMonitoredSystems,
|
||||
},
|
||||
]),
|
||||
: hasFiniteSelfHostedLimit(input.maxMonitoredSystems)
|
||||
? [
|
||||
{
|
||||
label: 'Included Monitored Systems',
|
||||
value: input.maxMonitoredSystems,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
label: 'Max Guests',
|
||||
value: input.maxGuests,
|
||||
|
|
|
|||
|
|
@ -23,7 +23,8 @@ export const SELF_HOSTED_PRO_BILLING_PLAN_INTENT_QUERY_PARAM = 'intent';
|
|||
export const SELF_HOSTED_PRO_BILLING_PURCHASE_QUERY_PARAM = 'purchase';
|
||||
export const SELF_HOSTED_PRO_BILLING_COUNTING_RULES_DETAIL = 'counting-rules';
|
||||
export const SELF_HOSTED_PRO_BILLING_RECOVERY_DETAIL = 'recovery';
|
||||
export const SELF_HOSTED_PRO_BILLING_MONITORED_SYSTEM_INTENT = 'max_monitored_systems';
|
||||
export const SELF_HOSTED_PRO_BILLING_MONITORED_SYSTEM_INTENT = 'self_hosted_plan';
|
||||
const LEGACY_SELF_HOSTED_PRO_BILLING_MONITORED_SYSTEM_INTENT = 'max_monitored_systems';
|
||||
export const SELF_HOSTED_PRO_BILLING_PURCHASE_ACTIVATED = 'activated';
|
||||
export const SELF_HOSTED_PRO_BILLING_PURCHASE_CANCELLED = 'cancelled';
|
||||
export const SELF_HOSTED_PRO_BILLING_PURCHASE_EXPIRED = 'expired';
|
||||
|
|
@ -49,6 +50,7 @@ export const SELF_HOSTED_PRO_BILLING_USAGE_COUNTING_RULES_HREF = `${SELF_HOSTED_
|
|||
export const SELF_HOSTED_PRO_BILLING_PLAN_MONITORED_SYSTEM_UPGRADE_HREF = `${SELF_HOSTED_PRO_BILLING_PLAN_ROUTE}?${SELF_HOSTED_PRO_BILLING_PLAN_INTENT_QUERY_PARAM}=${SELF_HOSTED_PRO_BILLING_MONITORED_SYSTEM_INTENT}`;
|
||||
|
||||
const IN_PRODUCT_PRICING_DESTINATIONS: Record<string, string> = {
|
||||
self_hosted_plan: SELF_HOSTED_PRO_BILLING_PLAN_MONITORED_SYSTEM_UPGRADE_HREF,
|
||||
max_monitored_systems: SELF_HOSTED_PRO_BILLING_PLAN_MONITORED_SYSTEM_UPGRADE_HREF,
|
||||
cloud: '/cloud',
|
||||
};
|
||||
|
|
@ -162,7 +164,13 @@ export function getSelfHostedBillingPlanDetail(search: string): SelfHostedBillin
|
|||
|
||||
export function getSelfHostedBillingPlanIntent(search: string): SelfHostedBillingPlanIntent | null {
|
||||
const intent = billingSearch(search).get(SELF_HOSTED_PRO_BILLING_PLAN_INTENT_QUERY_PARAM)?.trim();
|
||||
return intent === SELF_HOSTED_PRO_BILLING_MONITORED_SYSTEM_INTENT ? intent : null;
|
||||
if (
|
||||
intent === SELF_HOSTED_PRO_BILLING_MONITORED_SYSTEM_INTENT ||
|
||||
intent === LEGACY_SELF_HOSTED_PRO_BILLING_MONITORED_SYSTEM_INTENT
|
||||
) {
|
||||
return SELF_HOSTED_PRO_BILLING_MONITORED_SYSTEM_INTENT;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getSelfHostedBillingPurchaseArrival(
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
export type SelfHostedTierKey = 'community' | 'relay' | 'pro' | 'proPlus';
|
||||
export type SelfHostedTierKey = 'community' | 'relay' | 'pro';
|
||||
|
||||
export interface SelfHostedPlanDefinition {
|
||||
tier: SelfHostedTierKey;
|
||||
name: string;
|
||||
price: string;
|
||||
subline: string;
|
||||
monitoredSystems: number;
|
||||
metricHistoryDays: number;
|
||||
highlights: readonly string[];
|
||||
}
|
||||
|
|
@ -16,7 +15,6 @@ export interface SelfHostedFeatureRow {
|
|||
community: boolean | string;
|
||||
relay: boolean | string;
|
||||
pro: boolean | string;
|
||||
proPlus: boolean | string;
|
||||
}
|
||||
|
||||
export interface SelfHostedLinkCTA {
|
||||
|
|
@ -34,7 +32,6 @@ export interface SelfHostedCommercialPresentation {
|
|||
freeLabel: string;
|
||||
buyRelayLabel: string;
|
||||
upgradeToProLabel: string;
|
||||
upgradeToProPlusLabel: string;
|
||||
startTrialLabel: string;
|
||||
featureComparisonHeading: string;
|
||||
footerLinks: readonly SelfHostedLinkCTA[];
|
||||
|
|
@ -45,11 +42,11 @@ export const SELF_HOSTED_PLAN_DEFINITIONS: readonly SelfHostedPlanDefinition[] =
|
|||
tier: 'community',
|
||||
name: 'Community',
|
||||
price: 'Free forever',
|
||||
subline: 'Up to 5 top-level monitored systems',
|
||||
monitoredSystems: 5,
|
||||
subline: 'Unlimited self-hosted monitoring',
|
||||
metricHistoryDays: 7,
|
||||
highlights: [
|
||||
'Real-time monitoring',
|
||||
'Unlimited self-hosted monitoring',
|
||||
'7-day metric history',
|
||||
'Pulse Patrol (BYOK)',
|
||||
'Patrol quickstart after activation or trial: 25 runs, no API key',
|
||||
|
|
@ -63,7 +60,6 @@ export const SELF_HOSTED_PLAN_DEFINITIONS: readonly SelfHostedPlanDefinition[] =
|
|||
name: 'Relay',
|
||||
price: '$4.99/month',
|
||||
subline: 'or $39/year',
|
||||
monitoredSystems: 8,
|
||||
metricHistoryDays: 14,
|
||||
highlights: [
|
||||
'Everything in Community',
|
||||
|
|
@ -71,7 +67,7 @@ export const SELF_HOSTED_PLAN_DEFINITIONS: readonly SelfHostedPlanDefinition[] =
|
|||
'Mobile app access',
|
||||
'Push notifications',
|
||||
'Custom URL (yourlab.pulserelay.pro)',
|
||||
'8 top-level monitored systems · 14-day history',
|
||||
'14-day metric history',
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
@ -79,7 +75,6 @@ export const SELF_HOSTED_PLAN_DEFINITIONS: readonly SelfHostedPlanDefinition[] =
|
|||
name: 'Pro',
|
||||
price: '$8.99/month',
|
||||
subline: 'or $79/year',
|
||||
monitoredSystems: 15,
|
||||
metricHistoryDays: 90,
|
||||
highlights: [
|
||||
'Everything in Relay',
|
||||
|
|
@ -89,38 +84,27 @@ export const SELF_HOSTED_PLAN_DEFINITIONS: readonly SelfHostedPlanDefinition[] =
|
|||
'90-day metric history',
|
||||
'RBAC, audit logging, SAML SSO',
|
||||
'Agent profiles · PDF/CSV reports',
|
||||
'15 top-level monitored systems',
|
||||
'Unlimited self-hosted monitoring',
|
||||
],
|
||||
},
|
||||
{
|
||||
tier: 'proPlus',
|
||||
name: 'Pro+',
|
||||
price: '$14.99/month',
|
||||
subline: 'or $129/year',
|
||||
monitoredSystems: 50,
|
||||
metricHistoryDays: 90,
|
||||
highlights: ['Everything in Pro', '50 top-level monitored systems'],
|
||||
},
|
||||
] as const;
|
||||
|
||||
export const SELF_HOSTED_PLAN_BY_TIER: Record<SelfHostedTierKey, SelfHostedPlanDefinition> = {
|
||||
community: SELF_HOSTED_PLAN_DEFINITIONS[0],
|
||||
relay: SELF_HOSTED_PLAN_DEFINITIONS[1],
|
||||
pro: SELF_HOSTED_PLAN_DEFINITIONS[2],
|
||||
proPlus: SELF_HOSTED_PLAN_DEFINITIONS[3],
|
||||
};
|
||||
|
||||
export const SELF_HOSTED_COMMERCIAL_PRESENTATION: SelfHostedCommercialPresentation = {
|
||||
pageTitle: 'Pricing',
|
||||
pageDescription:
|
||||
'Core monitoring stays free. Pulse counts top-level monitored systems, while child resources underneath them are included. Relay adds remote access, and Pro unlocks Pulse Patrol, alert analysis, and auto-fix.',
|
||||
'Core monitoring stays free for self-hosted Pulse. Relay adds remote access and mobile convenience, while Pro unlocks AI operations, automation, governance, and longer history.',
|
||||
mostPopularBadge: 'Most Popular',
|
||||
currentPlanLabel: 'Current Plan',
|
||||
includedLabel: 'Included',
|
||||
freeLabel: 'Free',
|
||||
buyRelayLabel: 'Buy Relay',
|
||||
upgradeToProLabel: 'Upgrade to Pro',
|
||||
upgradeToProPlusLabel: 'Upgrade to Pro+',
|
||||
startTrialLabel: 'Start Free 14-day Trial',
|
||||
featureComparisonHeading: 'Feature Comparison',
|
||||
footerLinks: [
|
||||
|
|
@ -134,11 +118,6 @@ export const SELF_HOSTED_COMMERCIAL_PRESENTATION: SelfHostedCommercialPresentati
|
|||
label: 'See MSP plans',
|
||||
href: 'mailto:hello@pulserelay.pro?subject=Pulse%20MSP%20Inquiry',
|
||||
},
|
||||
{
|
||||
preface: 'Need more than 50 monitored systems?',
|
||||
label: 'Contact us',
|
||||
href: 'mailto:hello@pulserelay.pro?subject=Pulse%20Enterprise%20Inquiry',
|
||||
},
|
||||
],
|
||||
} as const;
|
||||
|
||||
|
|
@ -149,15 +128,13 @@ export const SELF_HOSTED_FEATURE_ROWS: readonly SelfHostedFeatureRow[] = [
|
|||
community: true,
|
||||
relay: true,
|
||||
pro: true,
|
||||
proPlus: true,
|
||||
},
|
||||
{
|
||||
key: 'hosts',
|
||||
name: 'Monitored Systems',
|
||||
community: String(SELF_HOSTED_PLAN_BY_TIER.community.monitoredSystems),
|
||||
relay: String(SELF_HOSTED_PLAN_BY_TIER.relay.monitoredSystems),
|
||||
pro: String(SELF_HOSTED_PLAN_BY_TIER.pro.monitoredSystems),
|
||||
proPlus: String(SELF_HOSTED_PLAN_BY_TIER.proPlus.monitoredSystems),
|
||||
key: 'core_monitoring_scope',
|
||||
name: 'Core Monitoring Scope',
|
||||
community: 'Unlimited',
|
||||
relay: 'Unlimited',
|
||||
pro: 'Unlimited',
|
||||
},
|
||||
{
|
||||
key: 'history',
|
||||
|
|
@ -165,7 +142,6 @@ export const SELF_HOSTED_FEATURE_ROWS: readonly SelfHostedFeatureRow[] = [
|
|||
community: `${SELF_HOSTED_PLAN_BY_TIER.community.metricHistoryDays} days`,
|
||||
relay: `${SELF_HOSTED_PLAN_BY_TIER.relay.metricHistoryDays} days`,
|
||||
pro: `${SELF_HOSTED_PLAN_BY_TIER.pro.metricHistoryDays} days`,
|
||||
proPlus: `${SELF_HOSTED_PLAN_BY_TIER.proPlus.metricHistoryDays} days`,
|
||||
},
|
||||
{
|
||||
key: 'update_alerts',
|
||||
|
|
@ -173,7 +149,6 @@ export const SELF_HOSTED_FEATURE_ROWS: readonly SelfHostedFeatureRow[] = [
|
|||
community: true,
|
||||
relay: true,
|
||||
pro: true,
|
||||
proPlus: true,
|
||||
},
|
||||
{
|
||||
key: 'sso',
|
||||
|
|
@ -181,7 +156,6 @@ export const SELF_HOSTED_FEATURE_ROWS: readonly SelfHostedFeatureRow[] = [
|
|||
community: true,
|
||||
relay: true,
|
||||
pro: true,
|
||||
proPlus: true,
|
||||
},
|
||||
{
|
||||
key: 'relay',
|
||||
|
|
@ -189,7 +163,6 @@ export const SELF_HOSTED_FEATURE_ROWS: readonly SelfHostedFeatureRow[] = [
|
|||
community: false,
|
||||
relay: true,
|
||||
pro: true,
|
||||
proPlus: true,
|
||||
},
|
||||
{
|
||||
key: 'mobile_app',
|
||||
|
|
@ -197,7 +170,6 @@ export const SELF_HOSTED_FEATURE_ROWS: readonly SelfHostedFeatureRow[] = [
|
|||
community: false,
|
||||
relay: true,
|
||||
pro: true,
|
||||
proPlus: true,
|
||||
},
|
||||
{
|
||||
key: 'push_notifications',
|
||||
|
|
@ -205,7 +177,6 @@ export const SELF_HOSTED_FEATURE_ROWS: readonly SelfHostedFeatureRow[] = [
|
|||
community: false,
|
||||
relay: true,
|
||||
pro: true,
|
||||
proPlus: true,
|
||||
},
|
||||
{
|
||||
key: 'ai_patrol',
|
||||
|
|
@ -213,7 +184,6 @@ export const SELF_HOSTED_FEATURE_ROWS: readonly SelfHostedFeatureRow[] = [
|
|||
community: true,
|
||||
relay: true,
|
||||
pro: true,
|
||||
proPlus: true,
|
||||
},
|
||||
{
|
||||
key: 'ai_autofix',
|
||||
|
|
@ -221,7 +191,6 @@ export const SELF_HOSTED_FEATURE_ROWS: readonly SelfHostedFeatureRow[] = [
|
|||
community: false,
|
||||
relay: false,
|
||||
pro: true,
|
||||
proPlus: true,
|
||||
},
|
||||
{
|
||||
key: 'ai_alerts',
|
||||
|
|
@ -229,7 +198,6 @@ export const SELF_HOSTED_FEATURE_ROWS: readonly SelfHostedFeatureRow[] = [
|
|||
community: false,
|
||||
relay: false,
|
||||
pro: true,
|
||||
proPlus: true,
|
||||
},
|
||||
{
|
||||
key: 'kubernetes_ai',
|
||||
|
|
@ -237,7 +205,6 @@ export const SELF_HOSTED_FEATURE_ROWS: readonly SelfHostedFeatureRow[] = [
|
|||
community: false,
|
||||
relay: false,
|
||||
pro: true,
|
||||
proPlus: true,
|
||||
},
|
||||
{
|
||||
key: 'rbac',
|
||||
|
|
@ -245,7 +212,6 @@ export const SELF_HOSTED_FEATURE_ROWS: readonly SelfHostedFeatureRow[] = [
|
|||
community: false,
|
||||
relay: false,
|
||||
pro: true,
|
||||
proPlus: true,
|
||||
},
|
||||
{
|
||||
key: 'audit_logging',
|
||||
|
|
@ -253,7 +219,6 @@ export const SELF_HOSTED_FEATURE_ROWS: readonly SelfHostedFeatureRow[] = [
|
|||
community: false,
|
||||
relay: false,
|
||||
pro: true,
|
||||
proPlus: true,
|
||||
},
|
||||
{
|
||||
key: 'advanced_sso',
|
||||
|
|
@ -261,7 +226,6 @@ export const SELF_HOSTED_FEATURE_ROWS: readonly SelfHostedFeatureRow[] = [
|
|||
community: false,
|
||||
relay: false,
|
||||
pro: true,
|
||||
proPlus: true,
|
||||
},
|
||||
{
|
||||
key: 'agent_profiles',
|
||||
|
|
@ -269,7 +233,6 @@ export const SELF_HOSTED_FEATURE_ROWS: readonly SelfHostedFeatureRow[] = [
|
|||
community: false,
|
||||
relay: false,
|
||||
pro: true,
|
||||
proPlus: true,
|
||||
},
|
||||
{
|
||||
key: 'advanced_reporting',
|
||||
|
|
@ -277,6 +240,5 @@ export const SELF_HOSTED_FEATURE_ROWS: readonly SelfHostedFeatureRow[] = [
|
|||
community: false,
|
||||
relay: false,
|
||||
pro: true,
|
||||
proPlus: true,
|
||||
},
|
||||
] as const;
|
||||
|
|
|
|||
|
|
@ -5325,7 +5325,7 @@ func TestContract_EntitlementPayloadMonitoredSystemUsageJSONSnapshot(t *testing.
|
|||
Valid: true,
|
||||
Tier: pkglicensing.TierPro,
|
||||
Features: append([]string(nil), pkglicensing.TierFeatures[pkglicensing.TierPro]...),
|
||||
MaxMonitoredSystems: 15,
|
||||
MaxMonitoredSystems: 0,
|
||||
}, string(pkglicensing.SubStateActive), entitlementUsageSnapshot{
|
||||
MonitoredSystems: 7,
|
||||
MonitoredSystemsAvailable: true,
|
||||
|
|
@ -5343,7 +5343,7 @@ func TestContract_EntitlementPayloadMonitoredSystemUsageJSONSnapshot(t *testing.
|
|||
|
||||
const want = `{
|
||||
"capabilities":["update_alerts","sso","ai_patrol","relay","mobile_app","push_notifications","long_term_metrics","ai_alerts","ai_autofix","kubernetes_ai","agent_profiles","advanced_sso","rbac","audit_logging","advanced_reporting"],
|
||||
"limits":[{"key":"max_monitored_systems","limit":15,"current":7,"current_available":true,"state":"ok"}],
|
||||
"limits":[],
|
||||
"subscription_state":"active",
|
||||
"upgrade_reasons":[],
|
||||
"tier":"pro",
|
||||
|
|
@ -5355,7 +5355,7 @@ func TestContract_EntitlementPayloadMonitoredSystemUsageJSONSnapshot(t *testing.
|
|||
"max_history_days":90,
|
||||
"legacy_connections":{"proxmox_nodes":2,"docker_hosts":1,"kubernetes_clusters":1},
|
||||
"has_migration_gap":false,
|
||||
"monitored_system_capacity":{"mode":"within_limit","urgency":"ok","current":7,"limit":15,"current_available":true,"available_slots":8,"overage":0,"blocks_new_systems":false,"existing_monitoring_continues":true}
|
||||
"monitored_system_capacity":{"mode":"unlimited","urgency":"ok","current":7,"limit":0,"current_available":true,"available_slots":0,"overage":0,"blocks_new_systems":false,"existing_monitoring_continues":true}
|
||||
}`
|
||||
|
||||
assertJSONSnapshot(t, got, want)
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ func TestBuildEntitlementPayload_ActiveLicense(t *testing.T) {
|
|||
Valid: true,
|
||||
Tier: license.TierPro,
|
||||
Features: append([]string(nil), license.TierFeatures[license.TierPro]...),
|
||||
MaxMonitoredSystems: 50,
|
||||
MaxMonitoredSystems: 0,
|
||||
}
|
||||
|
||||
payload := buildEntitlementPayload(status, "")
|
||||
|
|
@ -42,18 +42,14 @@ func TestBuildEntitlementPayload_ActiveLicense(t *testing.T) {
|
|||
t.Fatalf("expected capabilities to match status features")
|
||||
}
|
||||
|
||||
var agentLimit *LimitStatus
|
||||
for i := range payload.Limits {
|
||||
if payload.Limits[i].Key == "max_monitored_systems" {
|
||||
agentLimit = &payload.Limits[i]
|
||||
break
|
||||
}
|
||||
if len(payload.Limits) != 0 {
|
||||
t.Fatalf("expected no max_monitored_systems limit in payload, got %+v", payload.Limits)
|
||||
}
|
||||
if agentLimit == nil {
|
||||
t.Fatalf("expected max_monitored_systems limit in payload")
|
||||
if payload.MonitoredSystemCapacity == nil {
|
||||
t.Fatal("expected monitored_system_capacity in payload")
|
||||
}
|
||||
if agentLimit.Limit != 50 {
|
||||
t.Fatalf("expected max_monitored_systems limit 50, got %d", agentLimit.Limit)
|
||||
if payload.MonitoredSystemCapacity.Mode != "usage_unavailable" {
|
||||
t.Fatalf("expected usage_unavailable monitored-system capacity before inventory settles, got %+v", payload.MonitoredSystemCapacity)
|
||||
}
|
||||
if len(payload.UpgradeReasons) != 0 {
|
||||
t.Fatalf("expected no upgrade reasons for pro tier, got %d", len(payload.UpgradeReasons))
|
||||
|
|
@ -86,7 +82,7 @@ func TestBuildEntitlementPayloadWithUsage_CurrentValues(t *testing.T) {
|
|||
Valid: true,
|
||||
Tier: license.TierPro,
|
||||
Features: append([]string(nil), license.TierFeatures[license.TierPro]...),
|
||||
MaxMonitoredSystems: 50,
|
||||
MaxMonitoredSystems: 0,
|
||||
MaxGuests: 100,
|
||||
}
|
||||
|
||||
|
|
@ -112,17 +108,17 @@ func TestBuildEntitlementPayloadWithUsage_CurrentValues(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
if agentLimit == nil {
|
||||
t.Fatalf("expected max_monitored_systems limit")
|
||||
}
|
||||
if guestLimit == nil {
|
||||
t.Fatalf("expected max_guests limit")
|
||||
}
|
||||
if agentLimit.Current != 12 {
|
||||
t.Fatalf("expected agent current 12, got %d", agentLimit.Current)
|
||||
if agentLimit != nil {
|
||||
t.Fatalf("expected no max_monitored_systems limit for self-hosted pro, got %+v", agentLimit)
|
||||
}
|
||||
if agentLimit.CurrentAvailable == nil || !*agentLimit.CurrentAvailable {
|
||||
t.Fatalf("expected agent current availability true, got %+v", agentLimit.CurrentAvailable)
|
||||
if payload.MonitoredSystemCapacity == nil {
|
||||
t.Fatal("expected monitored_system_capacity")
|
||||
}
|
||||
if payload.MonitoredSystemCapacity.Mode != "unlimited" || payload.MonitoredSystemCapacity.Current != 12 {
|
||||
t.Fatalf("expected unlimited monitored-system capacity with current=12, got %+v", payload.MonitoredSystemCapacity)
|
||||
}
|
||||
if guestLimit.Current != 44 {
|
||||
t.Fatalf("expected guest current 44, got %d", guestLimit.Current)
|
||||
|
|
@ -140,18 +136,18 @@ func TestBuildEntitlementPayloadWithUsage_MonitoredSystemUsageUnavailable(t *tes
|
|||
Valid: true,
|
||||
Tier: license.TierPro,
|
||||
Features: append([]string(nil), license.TierFeatures[license.TierPro]...),
|
||||
MaxMonitoredSystems: 50,
|
||||
MaxMonitoredSystems: 0,
|
||||
}
|
||||
|
||||
payload := buildEntitlementPayloadWithUsage(status, "", entitlementUsageSnapshot{}, nil)
|
||||
if len(payload.Limits) != 1 {
|
||||
t.Fatalf("expected one monitored-system limit, got %d", len(payload.Limits))
|
||||
if len(payload.Limits) != 0 {
|
||||
t.Fatalf("expected no monitored-system limit, got %d", len(payload.Limits))
|
||||
}
|
||||
if payload.Limits[0].Current != 0 {
|
||||
t.Fatalf("expected unresolved current to remain 0, got %d", payload.Limits[0].Current)
|
||||
if payload.MonitoredSystemCapacity == nil {
|
||||
t.Fatal("expected monitored_system_capacity")
|
||||
}
|
||||
if payload.Limits[0].CurrentAvailable == nil || *payload.Limits[0].CurrentAvailable {
|
||||
t.Fatalf("expected unresolved current availability false, got %+v", payload.Limits[0].CurrentAvailable)
|
||||
if payload.MonitoredSystemCapacity.Mode != "usage_unavailable" {
|
||||
t.Fatalf("expected usage_unavailable monitored-system capacity, got %+v", payload.MonitoredSystemCapacity)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -198,7 +194,7 @@ func TestBuildCommercialPosturePayloadWithUsage_CurrentValues(t *testing.T) {
|
|||
Valid: true,
|
||||
Tier: license.TierFree,
|
||||
Features: append([]string(nil), license.TierFeatures[license.TierFree]...),
|
||||
MaxMonitoredSystems: 5,
|
||||
MaxMonitoredSystems: 0,
|
||||
}
|
||||
|
||||
payload := buildCommercialPosturePayloadWithUsage(status, "", entitlementUsageSnapshot{
|
||||
|
|
@ -219,6 +215,9 @@ func TestBuildCommercialPosturePayloadWithUsage_CurrentValues(t *testing.T) {
|
|||
if len(payload.UpgradeReasons) == 0 {
|
||||
t.Fatal("expected upgrade reasons for free-tier commercial posture")
|
||||
}
|
||||
if payload.MonitoredSystemCapacity == nil || payload.MonitoredSystemCapacity.Mode != "unlimited" {
|
||||
t.Fatalf("expected unlimited monitored-system capacity in commercial posture, got %+v", payload.MonitoredSystemCapacity)
|
||||
}
|
||||
if payload.LegacyConnections.ProxmoxNodes != 2 || payload.LegacyConnections.DockerHosts != 1 {
|
||||
t.Fatalf("expected legacy counts to be preserved, got %+v", payload.LegacyConnections)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -484,7 +484,7 @@ function routeCommercialAPI(request, response, url, scenario) {
|
|||
title: 'Simple self-hosted pricing for Pulse',
|
||||
description: 'Preview pricing contract',
|
||||
explainer:
|
||||
'Pulse counts <strong>top-level monitored systems</strong>. Child resources underneath them are included.',
|
||||
'Community keeps core monitoring free. Relay and Pro sell convenience, history, AI operations, and advanced administration.',
|
||||
plans: [
|
||||
{
|
||||
badge: 'Recommended',
|
||||
|
|
@ -495,7 +495,7 @@ function routeCommercialAPI(request, response, url, scenario) {
|
|||
period: '$39/year available too',
|
||||
blurb: 'Secure remote access and mobile access.',
|
||||
features: [
|
||||
{ tone: 'check', html: 'Up to <strong>8 monitored systems</strong>' },
|
||||
{ tone: 'check', html: 'Unlimited <strong>self-hosted monitoring</strong>' },
|
||||
{ tone: 'check', html: 'Remote access' },
|
||||
],
|
||||
buttons: [
|
||||
|
|
@ -510,7 +510,7 @@ function routeCommercialAPI(request, response, url, scenario) {
|
|||
period: '$79/year available too',
|
||||
blurb: 'Investigation, alert analysis, and auto-fix.',
|
||||
features: [
|
||||
{ tone: 'check', html: 'Up to <strong>15 monitored systems</strong>' },
|
||||
{ tone: 'check', html: 'Unlimited <strong>self-hosted monitoring</strong>' },
|
||||
{ tone: 'check', html: 'Everything in Relay' },
|
||||
],
|
||||
buttons: [
|
||||
|
|
@ -518,21 +518,6 @@ function routeCommercialAPI(request, response, url, scenario) {
|
|||
{ kind: 'checkout', className: 'btn btn-primary', tier: 'pro', planKey: 'price_pro_annual', billingCycle: 'annual', label: 'Buy Annual' },
|
||||
],
|
||||
},
|
||||
{
|
||||
tierKicker: 'Pro+',
|
||||
title: 'Pro+',
|
||||
price: '$14.99',
|
||||
period: '$129/year available too',
|
||||
blurb: 'More room for larger self-hosted environments.',
|
||||
features: [
|
||||
{ tone: 'check', html: 'Up to <strong>50 monitored systems</strong>' },
|
||||
{ tone: 'check', html: 'Everything in Pro' },
|
||||
],
|
||||
buttons: [
|
||||
{ kind: 'checkout', className: 'btn btn-secondary', tier: 'pro_plus', planKey: 'price_pro_plus_monthly', billingCycle: 'monthly', label: 'Buy Monthly' },
|
||||
{ kind: 'checkout', className: 'btn btn-primary', tier: 'pro_plus', planKey: 'price_pro_plus_annual', billingCycle: 'annual', label: 'Buy Annual' },
|
||||
],
|
||||
},
|
||||
],
|
||||
});
|
||||
return;
|
||||
|
|
@ -549,7 +534,7 @@ function routeCommercialAPI(request, response, url, scenario) {
|
|||
}
|
||||
sendJSON(response, 200, {
|
||||
portal_handoff_id: portalHandoffID,
|
||||
feature: 'max_monitored_systems',
|
||||
feature: 'self_hosted_plan',
|
||||
status: portalHandoffID === 'cph_preview_completed' ? 'completed' : 'resolved',
|
||||
resolved_at: Math.floor(Date.now() / 1000) - 30,
|
||||
expires_at: Math.floor(Date.now() / 1000) + 3600,
|
||||
|
|
@ -561,10 +546,10 @@ function routeCommercialAPI(request, response, url, scenario) {
|
|||
sendJSON(response, 200, {
|
||||
status: 'fulfilled',
|
||||
owner_email: 'buyer@example.com',
|
||||
tier: 'pro_plus',
|
||||
plan_key: 'price_pro_plus_annual',
|
||||
tier: 'pro',
|
||||
plan_key: 'price_pro_annual',
|
||||
activation_key_prefix: 'ppk_live_preview',
|
||||
max_monitored_systems: 50,
|
||||
max_monitored_systems: 0,
|
||||
current_period_end: iso('2027-03-20T10:00:00Z'),
|
||||
});
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -227,12 +227,12 @@ describe('services view', function() {
|
|||
document.body.innerHTML = '<div id="upgrade-billing-root"></div>';
|
||||
|
||||
var billingState = createPortalBillingState();
|
||||
billingState.upgradeFeatureKey = 'max_monitored_systems';
|
||||
billingState.upgradeFeatureKey = 'self_hosted_plan';
|
||||
billingState.upgradePortalHandoffID = 'cph_signed';
|
||||
billingState.upgradePortalHandoff.status = 'ready';
|
||||
billingState.upgradePortalHandoff.data = {
|
||||
portal_handoff_id: 'cph_signed',
|
||||
feature: 'max_monitored_systems',
|
||||
feature: 'self_hosted_plan',
|
||||
status: 'resolved',
|
||||
};
|
||||
billingState.upgradePricing.status = 'ready';
|
||||
|
|
@ -240,21 +240,21 @@ describe('services view', function() {
|
|||
title: 'Pricing',
|
||||
description: 'Canonical pricing model',
|
||||
explainer:
|
||||
'Pulse counts <strong>top-level monitored systems</strong>. Child resources underneath them are included.',
|
||||
'Community keeps core monitoring free. Relay and Pro sell convenience, history, AI operations, and advanced administration.',
|
||||
plans: [
|
||||
{
|
||||
tierKicker: 'Pro+',
|
||||
title: 'Pro+',
|
||||
price: '$14.99',
|
||||
period: '$129/year available too',
|
||||
blurb: 'More room.',
|
||||
features: [{ tone: 'check', html: 'Up to <strong>50 monitored systems</strong>' }],
|
||||
tierKicker: 'Pro',
|
||||
title: 'Pro',
|
||||
price: '$8.99',
|
||||
period: '$79/year available too',
|
||||
blurb: 'Investigation, alert analysis, and auto-fix.',
|
||||
features: [{ tone: 'check', html: 'Unlimited <strong>self-hosted monitoring</strong>' }],
|
||||
buttons: [
|
||||
{
|
||||
kind: 'checkout',
|
||||
className: 'btn btn-primary',
|
||||
tier: 'pro_plus',
|
||||
planKey: 'price_pro_plus_annual',
|
||||
tier: 'pro',
|
||||
planKey: 'price_pro_annual',
|
||||
billingCycle: 'annual',
|
||||
label: 'Buy Annual',
|
||||
},
|
||||
|
|
@ -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(
|
||||
'Pulse counts <strong>top-level monitored systems</strong>. Child resources underneath them are included.',
|
||||
'Community keeps core monitoring free. Relay and Pro sell convenience, history, AI operations, and advanced administration.',
|
||||
);
|
||||
expect(document.getElementById('upgrade-billing-root')?.innerHTML).toContain(
|
||||
'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 by convenience and advanced capabilities, keeps core monitoring available across plans, and sends completed checkout straight back to Pulse Pro billing.',
|
||||
);
|
||||
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');
|
||||
|
|
@ -300,7 +300,7 @@ describe('services view', function() {
|
|||
price: '$4.99',
|
||||
period: '$39/year available too',
|
||||
blurb: 'Secure remote access and mobile access.',
|
||||
features: [{ tone: 'check', html: 'Up to <strong>8 monitored systems</strong>' }],
|
||||
features: [{ tone: 'check', html: 'Unlimited <strong>self-hosted monitoring</strong>' }],
|
||||
buttons: [
|
||||
{
|
||||
kind: 'checkout',
|
||||
|
|
@ -333,7 +333,7 @@ describe('services view', function() {
|
|||
billingState.upgradePortalHandoff.status = 'ready';
|
||||
billingState.upgradePortalHandoff.data = {
|
||||
portal_handoff_id: 'cph_completed',
|
||||
feature: 'max_monitored_systems',
|
||||
feature: 'self_hosted_plan',
|
||||
status: 'completed',
|
||||
};
|
||||
billingState.upgradePricing.status = 'ready';
|
||||
|
|
@ -342,18 +342,18 @@ describe('services view', function() {
|
|||
description: 'Canonical pricing model',
|
||||
plans: [
|
||||
{
|
||||
tierKicker: 'Pro+',
|
||||
title: 'Pro+',
|
||||
price: '$14.99',
|
||||
period: '$129/year available too',
|
||||
blurb: 'More room.',
|
||||
features: [{ tone: 'check', html: 'Up to <strong>50 monitored systems</strong>' }],
|
||||
tierKicker: 'Pro',
|
||||
title: 'Pro',
|
||||
price: '$8.99',
|
||||
period: '$79/year available too',
|
||||
blurb: 'Investigation, alert analysis, and auto-fix.',
|
||||
features: [{ tone: 'check', html: 'Unlimited <strong>self-hosted monitoring</strong>' }],
|
||||
buttons: [
|
||||
{
|
||||
kind: 'checkout',
|
||||
className: 'btn btn-primary',
|
||||
tier: 'pro_plus',
|
||||
planKey: 'price_pro_plus_annual',
|
||||
tier: 'pro',
|
||||
planKey: 'price_pro_annual',
|
||||
billingCycle: 'annual',
|
||||
label: 'Buy Annual',
|
||||
},
|
||||
|
|
@ -382,18 +382,18 @@ describe('services view', function() {
|
|||
description: 'Canonical pricing model',
|
||||
plans: [
|
||||
{
|
||||
tierKicker: 'Pro+',
|
||||
title: 'Pro+',
|
||||
price: '$14.99',
|
||||
period: '$129/year available too',
|
||||
blurb: 'More room.',
|
||||
features: [{ tone: 'check', html: 'Up to <strong>50 monitored systems</strong>' }],
|
||||
tierKicker: 'Pro',
|
||||
title: 'Pro',
|
||||
price: '$8.99',
|
||||
period: '$79/year available too',
|
||||
blurb: 'Investigation, alert analysis, and auto-fix.',
|
||||
features: [{ tone: 'check', html: 'Unlimited <strong>self-hosted monitoring</strong>' }],
|
||||
buttons: [
|
||||
{
|
||||
kind: 'checkout',
|
||||
className: 'btn btn-primary',
|
||||
tier: 'pro_plus',
|
||||
planKey: 'price_pro_plus_annual',
|
||||
tier: 'pro',
|
||||
planKey: 'price_pro_annual',
|
||||
billingCycle: 'annual',
|
||||
label: 'Buy Annual',
|
||||
},
|
||||
|
|
|
|||
|
|
@ -195,8 +195,8 @@ export function renderUpgradePanel(billingState: PortalBillingState, _bootstrap:
|
|||
? '<div class="helper-text">' + escapeText(pricingState.data.description) + '</div>'
|
||||
: '') +
|
||||
'<div class="helper-text">' +
|
||||
(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.'
|
||||
((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>';
|
||||
|
|
|
|||
|
|
@ -629,21 +629,21 @@ describe('shell view', function() {
|
|||
}),
|
||||
billingState: createBillingState({
|
||||
openBillingPanelID: 'upgrade-billing-panel',
|
||||
upgradeFeatureKey: 'max_monitored_systems',
|
||||
upgradeFeatureKey: 'self_hosted_plan',
|
||||
}),
|
||||
activeSection: 'billing',
|
||||
})
|
||||
);
|
||||
|
||||
expect(html).toContain('Pulse Account owns the commercial handoff for self-hosted upgrades from the app.');
|
||||
expect(html).toContain('Upgrade monitored-system cap');
|
||||
expect(html).toContain('Compare self-hosted plans');
|
||||
expect(html).toContain(
|
||||
'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 by Relay and Pro capabilities rather than monitored-system volume.',
|
||||
);
|
||||
expect(html).toContain('Top-level monitored systems');
|
||||
expect(html).toContain('Plan comparison');
|
||||
expect(html).toContain('id="upgrade-billing-root"');
|
||||
expect(html).toContain(
|
||||
'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 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.',
|
||||
);
|
||||
expect(html).toContain('Pulse Account owns self-hosted plan selection and checkout for Pulse Pro upgrades.');
|
||||
expect(html).not.toContain('id="open-manage-billing"');
|
||||
|
|
|
|||
|
|
@ -134,21 +134,26 @@ function normalizeUpgradeFeatureKey(featureKey: string): string {
|
|||
return String(featureKey || '').trim();
|
||||
}
|
||||
|
||||
function isSelfHostedPlanUpgrade(featureKey: string): boolean {
|
||||
var normalized = normalizeUpgradeFeatureKey(featureKey);
|
||||
return normalized === 'self_hosted_plan' || normalized === 'max_monitored_systems';
|
||||
}
|
||||
|
||||
function selfHostedUpgradeActionTitle(featureKey: string): string {
|
||||
return normalizeUpgradeFeatureKey(featureKey) === 'max_monitored_systems'
|
||||
? 'Upgrade monitored-system cap'
|
||||
return isSelfHostedPlanUpgrade(featureKey)
|
||||
? 'Compare self-hosted plans'
|
||||
: 'Upgrade self-hosted plan';
|
||||
}
|
||||
|
||||
function selfHostedUpgradeActionDescription(featureKey: string): string {
|
||||
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.'
|
||||
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: string): string[] {
|
||||
return normalizeUpgradeFeatureKey(featureKey) === 'max_monitored_systems'
|
||||
? ['Top-level monitored systems', 'Plan comparison']
|
||||
return isSelfHostedPlanUpgrade(featureKey)
|
||||
? ['Plan comparison', 'Pulse Pro checkout']
|
||||
: ['Plan comparison', 'Checkout handoff'];
|
||||
}
|
||||
|
||||
|
|
@ -167,8 +172,8 @@ function renderSelfHostedUpgradeActionRow(context: ShellViewContext): string {
|
|||
|
||||
function renderSelfHostedUpgradeBillingPanel(context: ShellViewContext): string {
|
||||
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.'
|
||||
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),
|
||||
|
|
|
|||
|
|
@ -60,17 +60,17 @@ const (
|
|||
)
|
||||
|
||||
// TierMonitoredSystemLimits defines the maximum monitored-system count per tier.
|
||||
// A value of 0 means unlimited (enforced at the MSP/Enterprise level).
|
||||
// A value of 0 means unlimited.
|
||||
var TierMonitoredSystemLimits = map[Tier]int{
|
||||
TierFree: 5,
|
||||
TierRelay: 8,
|
||||
TierPro: 15,
|
||||
TierProPlus: 50,
|
||||
TierProAnnual: 15, // Legacy: same as Pro
|
||||
TierLifetime: 0, // Grandfathered lifetime entitlements remain uncapped
|
||||
TierCloud: 0, // Cloud tiers have per-plan limits set in license claims
|
||||
TierMSP: 0, // MSP tiers have per-plan pool limits set in license claims
|
||||
TierEnterprise: 0, // Custom
|
||||
TierFree: 0, // Self-hosted Community no longer caps core monitoring
|
||||
TierRelay: 0, // Self-hosted Relay adds convenience features, not more room
|
||||
TierPro: 0, // Self-hosted Pro monetizes operations features, not monitoring volume
|
||||
TierProPlus: 0, // Legacy/self-hosted compatibility tier; no monitored-system cap
|
||||
TierProAnnual: 0, // Legacy: same as Pro
|
||||
TierLifetime: 0, // Grandfathered lifetime entitlements remain uncapped
|
||||
TierCloud: 0, // Cloud tiers have per-plan limits set in license claims
|
||||
TierMSP: 0, // MSP tiers have per-plan pool limits set in license claims
|
||||
TierEnterprise: 0, // Custom
|
||||
}
|
||||
|
||||
// CloudPlanMonitoredSystemLimits maps hosted and continuity plan version strings to
|
||||
|
|
|
|||
|
|
@ -260,11 +260,11 @@ func TestTierMonitoredSystemLimits(t *testing.T) {
|
|||
tier Tier
|
||||
want int
|
||||
}{
|
||||
{TierFree, 5},
|
||||
{TierRelay, 8},
|
||||
{TierPro, 15},
|
||||
{TierProPlus, 50},
|
||||
{TierProAnnual, 15},
|
||||
{TierFree, 0},
|
||||
{TierRelay, 0},
|
||||
{TierPro, 0},
|
||||
{TierProPlus, 0},
|
||||
{TierProAnnual, 0},
|
||||
{TierLifetime, 0},
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4408,7 +4408,7 @@ class SubsystemLookupTest(unittest.TestCase):
|
|||
self.assertEqual(lane_context["lane_id"], "L3")
|
||||
self.assertEqual(
|
||||
{decision["id"] for decision in lane_context["open_decisions"]},
|
||||
{"self-hosted-homelab-monetization-boundary"},
|
||||
{"self-hosted-paid-extras-packaging"},
|
||||
)
|
||||
self.assertEqual(
|
||||
{gate["id"] for gate in lane_context["release_gates"]},
|
||||
|
|
@ -4437,6 +4437,7 @@ class SubsystemLookupTest(unittest.TestCase):
|
|||
"cloud-msp-price-id-propagation",
|
||||
"cloud-msp-stripe-prices",
|
||||
"legacy-grandfathering-eligibility-cutoff",
|
||||
"self-hosted-core-monitoring-free",
|
||||
"stable-release-promotion-model",
|
||||
"stripe-mapping-contract-lock",
|
||||
"trial-authority-saas-controlled",
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ test.describe.serial('V6 license activation flow', () => {
|
|||
plan_key: 'pro_monthly',
|
||||
billing_model: 'manual',
|
||||
duration_days: 1,
|
||||
max_monitored_systems: 15,
|
||||
max_monitored_systems: 0,
|
||||
max_guests: 5,
|
||||
max_installations: 3,
|
||||
send_email: false,
|
||||
|
|
@ -188,7 +188,7 @@ test.describe.serial('V6 license activation flow', () => {
|
|||
await expect(page.getByRole('heading', { name: 'Plan' })).toBeVisible();
|
||||
await expect(page.getByText(/^Active$/).first()).toBeVisible({ timeout: 10_000 });
|
||||
await expect(page.getByText('No Pro license is active.')).toHaveCount(0);
|
||||
await expect(page.getByText('15').first()).toBeVisible();
|
||||
await expect(page.getByText('Unlimited').first()).toBeVisible();
|
||||
|
||||
// Verify entitlements API agrees.
|
||||
const entRes = await apiRequest(page, '/api/license/entitlements');
|
||||
|
|
@ -200,10 +200,9 @@ test.describe.serial('V6 license activation flow', () => {
|
|||
expect(ent.valid).toBe(true);
|
||||
expect(ent.licensed_email).toBe('e2e-playwright@test.local');
|
||||
|
||||
// Check max_monitored_systems limit.
|
||||
// Core monitoring is unlimited for self-hosted v6 plans.
|
||||
const agentLimit = ent.limits?.find((l) => l.key === 'max_monitored_systems');
|
||||
expect(agentLimit, 'max_monitored_systems limit not found in entitlements').toBeTruthy();
|
||||
expect(agentLimit!.limit).toBe(15);
|
||||
expect(agentLimit).toBeUndefined();
|
||||
|
||||
// Check max_guests limit.
|
||||
const guestLimit = ent.limits?.find((l) => l.key === 'max_guests');
|
||||
|
|
|
|||
|
|
@ -303,14 +303,14 @@ test("keeps capacity and upgrade arrivals distinct on the billing surface", asyn
|
|||
|
||||
await banner.getByRole("link", { name: "Upgrade to add more" }).click();
|
||||
await page.waitForURL(
|
||||
"**/settings/system/billing/plan?intent=max_monitored_systems",
|
||||
"**/settings/system/billing/plan?intent=self_hosted_plan",
|
||||
);
|
||||
await expect(page.getByRole("tab", { name: "Plan" })).toHaveAttribute(
|
||||
"aria-selected",
|
||||
"true",
|
||||
);
|
||||
await expect(
|
||||
page.getByText("Need a higher monitored-system cap?"),
|
||||
page.getByText("Compare self-hosted plans"),
|
||||
).toBeVisible();
|
||||
await expect(page.getByText("Redeem existing key")).toBeVisible();
|
||||
await expect(page.getByLabel("Pulse Pro Key")).toBeHidden();
|
||||
|
|
@ -318,7 +318,7 @@ test("keeps capacity and upgrade arrivals distinct on the billing surface", asyn
|
|||
page.getByRole("link", { name: "Compare plans" }),
|
||||
).toHaveAttribute(
|
||||
"href",
|
||||
"/auth/license-purchase-start?feature=max_monitored_systems",
|
||||
"/auth/license-purchase-start?feature=self_hosted_plan",
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -12,19 +12,19 @@ const PURCHASE_START_URL = `${DEV_SERVER_URL}${PURCHASE_START_PATH}`;
|
|||
const PULSE_ACCOUNT_PORTAL_URL = "https://cloud.pulserelay.pro/portal";
|
||||
const PURCHASE_RETURN_URL = `${DEV_SERVER_URL}/auth/license-purchase-activate`;
|
||||
const PORTAL_HANDOFF_ID = "cph_checkout_return";
|
||||
const ACTIVATED_BILLING_URL = `${DEV_SERVER_URL}/settings/system/billing/plan?intent=max_monitored_systems&purchase=activated`;
|
||||
const CANCELLED_BILLING_URL = `${DEV_SERVER_URL}/settings/system/billing/plan?intent=max_monitored_systems&purchase=cancelled`;
|
||||
const EXPIRED_BILLING_URL = `${DEV_SERVER_URL}/settings/system/billing/plan?intent=max_monitored_systems&purchase=expired`;
|
||||
const FAILED_BILLING_URL = `${DEV_SERVER_URL}/settings/system/billing/plan?intent=max_monitored_systems&purchase=failed`;
|
||||
const UNAVAILABLE_BILLING_URL = `${DEV_SERVER_URL}/settings/system/billing/plan?intent=max_monitored_systems&purchase=unavailable`;
|
||||
const FINAL_BILLING_URL = `${DEV_SERVER_URL}/settings/system/billing/plan?intent=max_monitored_systems`;
|
||||
const ACTIVATED_BILLING_URL = `${DEV_SERVER_URL}/settings/system/billing/plan?intent=self_hosted_plan&purchase=activated`;
|
||||
const CANCELLED_BILLING_URL = `${DEV_SERVER_URL}/settings/system/billing/plan?intent=self_hosted_plan&purchase=cancelled`;
|
||||
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 USAGE_BILLING_URL = `${DEV_SERVER_URL}/settings/system/billing/usage`;
|
||||
const RECOVERY_BILLING_HREF =
|
||||
"/settings/system/billing/plan?intent=max_monitored_systems&details=recovery";
|
||||
"/settings/system/billing/plan?intent=self_hosted_plan&details=recovery";
|
||||
const RECOVERY_BILLING_URL = `${DEV_SERVER_URL}${RECOVERY_BILLING_HREF}`;
|
||||
const PURCHASE_RETURN_TOKEN = "prt_signed_checkout_return";
|
||||
const CHECKOUT_SESSION_ID = "cs_upgrade_return";
|
||||
const MONITORED_SYSTEM_FEATURE = "max_monitored_systems";
|
||||
const MONITORED_SYSTEM_FEATURE = "self_hosted_plan";
|
||||
|
||||
const MONITORED_SYSTEM_ENTITLEMENTS = {
|
||||
capabilities: [],
|
||||
|
|
@ -353,29 +353,16 @@ async function configureBillingFixtures(context: BrowserContext, page: Page) {
|
|||
}
|
||||
|
||||
async function openMonitoredSystemUpgradeArrival(page: Page) {
|
||||
await page.goto(`${DEV_SERVER_URL}/settings/system-general`, {
|
||||
await page.goto(`${DEV_SERVER_URL}/settings/system/billing/plan?intent=self_hosted_plan`, {
|
||||
waitUntil: "domcontentloaded",
|
||||
});
|
||||
await expect(
|
||||
page
|
||||
.getByRole("status")
|
||||
.filter({ hasText: "16 monitored systems currently counted" }),
|
||||
).toBeVisible();
|
||||
await page
|
||||
.getByRole("status")
|
||||
.filter({ hasText: "16 monitored systems currently counted" })
|
||||
.getByRole("link", { name: "Upgrade to add more" })
|
||||
.click();
|
||||
await page.waitForURL(
|
||||
"**/settings/system/billing/plan?intent=max_monitored_systems",
|
||||
);
|
||||
await expect(page.getByRole("tab", { name: "Plan" })).toHaveAttribute(
|
||||
"aria-selected",
|
||||
"true",
|
||||
);
|
||||
await expect(
|
||||
page.getByText(
|
||||
"Pulse counts top-level monitored systems such as Docker hosts, Kubernetes clusters, Proxmox nodes, standalone hosts, and TrueNAS systems. Compare self-hosted plans in Pulse Account and return here with Pulse Pro activated automatically.",
|
||||
"Community keeps core monitoring free. Compare Relay and Pro in Pulse Account, then return here with Pulse Pro activated automatically.",
|
||||
),
|
||||
).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "Hide counting rules" })).toBeVisible();
|
||||
|
|
@ -416,14 +403,14 @@ test.describe("Self-hosted upgrade return flow", () => {
|
|||
const comparePlansLink = page.getByRole("link", { name: "Compare plans" });
|
||||
await expect(comparePlansLink).toHaveAttribute(
|
||||
"href",
|
||||
`${PURCHASE_START_PATH}?feature=max_monitored_systems`,
|
||||
`${PURCHASE_START_PATH}?feature=self_hosted_plan`,
|
||||
);
|
||||
|
||||
await expect(comparePlansLink).toHaveAttribute("target", "_blank");
|
||||
await comparePlansLink.click();
|
||||
await expect
|
||||
.poll(() => purchaseStartURL)
|
||||
.toBe(`${PURCHASE_START_URL}?feature=max_monitored_systems`);
|
||||
.toBe(`${PURCHASE_START_URL}?feature=self_hosted_plan`);
|
||||
|
||||
await page.goto(buildPortalHandoffUrl(), { waitUntil: "domcontentloaded" });
|
||||
await expect(
|
||||
|
|
@ -477,7 +464,7 @@ test.describe("Self-hosted upgrade return flow", () => {
|
|||
await expect(page.getByText("Checkout cancelled", { exact: true })).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: "Compare plans" })).toHaveAttribute(
|
||||
"href",
|
||||
`${PURCHASE_START_PATH}?feature=max_monitored_systems`,
|
||||
`${PURCHASE_START_PATH}?feature=self_hosted_plan`,
|
||||
);
|
||||
});
|
||||
|
||||
|
|
@ -550,7 +537,7 @@ test.describe("Self-hosted upgrade return flow", () => {
|
|||
const restartUpgradeLink = page.getByRole("link", { name: "Restart upgrade" });
|
||||
await expect(restartUpgradeLink).toHaveAttribute(
|
||||
"href",
|
||||
`${PURCHASE_START_PATH}?feature=max_monitored_systems`,
|
||||
`${PURCHASE_START_PATH}?feature=self_hosted_plan`,
|
||||
);
|
||||
await expect(restartUpgradeLink).toHaveAttribute("target", "_blank");
|
||||
});
|
||||
|
|
@ -649,7 +636,7 @@ test.describe("Self-hosted upgrade return flow", () => {
|
|||
).toBeVisible();
|
||||
await expect(billingSurface.getByRole("link", { name: "Try again" })).toHaveAttribute(
|
||||
"href",
|
||||
`${PURCHASE_START_PATH}?feature=max_monitored_systems`,
|
||||
`${PURCHASE_START_PATH}?feature=self_hosted_plan`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -80,12 +80,12 @@ test.describe("Pulse Account upgrade bootstrap", () => {
|
|||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByText(
|
||||
"Pulse counts top-level monitored systems. Child resources underneath them are included.",
|
||||
"Community keeps core monitoring free. Relay and Pro sell convenience, history, AI operations, and advanced administration.",
|
||||
),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
page.getByText(
|
||||
"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 by convenience and advanced capabilities, keeps core monitoring available across plans, and sends completed checkout straight back to Pulse Pro billing.",
|
||||
),
|
||||
).toBeVisible();
|
||||
await expect(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue