diff --git a/docs/API.md b/docs/API.md index aa44a080f..c5a767796 100644 --- a/docs/API.md +++ b/docs/API.md @@ -1586,7 +1586,7 @@ TrueNAS resources (pools, datasets, disks, ZFS snapshots, replication tasks, ale --- -## 📱 Relay / Mobile Remote Access (Relay and Above) +## 📱 Relay / Pulse Mobile (retiring 31 March 2027) End-to-end encrypted relay protocol for mobile connectivity. diff --git a/docs/CLOUD.md b/docs/CLOUD.md index 3037738d1..c64b1e8ec 100644 --- a/docs/CLOUD.md +++ b/docs/CLOUD.md @@ -62,7 +62,7 @@ Add your Proxmox VE, PBS, PMG, or TrueNAS systems via **Settings → Infrastruct ### 4. Set Up Mobile Access -Relay is enabled by default on Cloud instances. Open **Settings → Remote Access** to prepare pairing and connect once mobile beta/public access is enabled. +Relay is enabled by default on Cloud instances. Open **Settings → Pulse Mobile** to pair a phone. Pulse Mobile is being retired on 31 March 2027. ## Data & Privacy diff --git a/docs/PRIVACY.md b/docs/PRIVACY.md index e3db0c458..cdebc02e3 100644 --- a/docs/PRIVACY.md +++ b/docs/PRIVACY.md @@ -420,5 +420,5 @@ The telemetry implementation is in [`internal/telemetry/telemetry.go`](../intern Pulse can make outbound connections when you enable specific features: - **AI providers**: when AI features are configured, Pulse sends only the context required for your request to the provider you chose. This can include active Patrol objective briefs and their optional operator context when they apply to a Patrol run. Retained objective text and observer artifacts are encrypted at rest in the local organization data directory; objective text is not included in Pulse usage telemetry. A provider may return a model-authored observer proposal for an uncovered objective; Pulse encrypts that artifact with the retained objective, excludes it from public objective reads and later prompt seeds, and does not include its content in usage telemetry or audit messages. Saving an objective by itself does not call a model. Local providers stay on your network; non-local hosted providers receive provider-bound context directly from your Pulse instance. AI prompts from self-managed installs do not transit Pulse infrastructure. Before non-local model requests leave the instance, governed resource details use the same resource-policy redaction shown in Data Handling: local-only resource details are omitted from detailed prompt sections or replaced with policy-safe summaries, and known restricted resource identifiers are redacted where they appear in provider-bound context. See `docs/AI.md`. -- **Relay / Remote Access**: when relay is enabled, Pulse connects to the configured relay endpoint to enable secure remote web access, Pulse Mobile pairing for handoff, and push notifications. See Settings → Remote Access. +- **Relay / Pulse Mobile**: when Pulse Mobile connections are on, Pulse connects to the configured relay endpoint so paired Pulse Mobile devices can reach this instance and receive push notifications. Relay does not provide remote access to the web UI. See Settings → Pulse Mobile. Pulse Mobile is being retired on 31 March 2027. - **Update checks**: Pulse can check for new releases/updates (for example via GitHub release metadata) depending on your deployment and configuration. diff --git a/docs/PULSE_PRO.md b/docs/PULSE_PRO.md index deb8481b4..f4b4bb2ef 100644 --- a/docs/PULSE_PRO.md +++ b/docs/PULSE_PRO.md @@ -137,7 +137,7 @@ This matrix reflects the entitlement keys enforced in code plus the runtime hist | Constant | Capability Key | Display Name | Community | Relay | Pro | Cloud | Primary Gating Mechanism / Notes | |---|---|---|:---:|:---:|:---:|:---:|---| | `FeatureAIPatrol` | `ai_patrol` | Pulse Patrol (Background Health Checks) | Y | Y | Y | Y | Patrol itself is available on Community with your own provider or local model. Higher-autonomy outcomes and fix execution are separately gated. | -| `FeatureRelay` | `relay` | Remote Access (Mobile Relay) | N | Y | Y | Y | API route gating via `RequireLicenseFeature(..., relay, ...)` for relay settings and onboarding endpoints. | +| `FeatureRelay` | `relay` | Pulse Relay (Mobile Connection) | N | Y | Y | Y | API route gating via `RequireLicenseFeature(..., relay, ...)` for relay settings and onboarding endpoints. | | `FeatureAIAlerts` | `ai_alerts` | Patrol Investigates Issues and Explains the Root Cause | N | N | Y | Y | API route gating via `RequireLicenseFeature(..., ai_alerts, ...)`. | | `FeatureAIAutoFix` | `ai_autofix` | Patrol Applies Safe Fixes and Verifies the Result | N | N | Y | Y | Required for governed fix execution and automatic Patrol actions. | | `FeatureKubernetesAI` | `kubernetes_ai` | Kubernetes AI Analysis (Compatibility) | N | N | Y | Y | Legacy compatibility gate for `/api/ai/kubernetes/analyze`; not a primary marketed v6 Pro plan pillar. | diff --git a/docs/RELAY.md b/docs/RELAY.md index bda6176f2..cb791d27f 100644 --- a/docs/RELAY.md +++ b/docs/RELAY.md @@ -1,8 +1,10 @@ -# Relay / Pulse Mobile Handoff (Relay and Above) +# Relay / Pulse Mobile -Pulse Relay provides **end-to-end encrypted remote access** foundations for Pulse instances. It allows secure remote connectivity without exposing your Pulse server to the public internet. +> **Pulse Mobile is being retired on 31 March 2027.** Paired phones keep working until then. Relay is no longer sold as a plan; existing Relay subscribers have Pro features at their current price. For alerts on your phone afterwards, add an ntfy, Gotify, or Pushover destination under Alerts, and reach the Pulse web UI away from home through your own VPN or tunnel. -> Supported Pulse Mobile clients pair from **Settings → Remote Access** using a QR code or deep link and connect through Pulse Relay over end-to-end encrypted remote access. +Pulse Relay is the end-to-end encrypted connection between a Pulse instance and paired Pulse Mobile devices. It lets the app reach your server without exposing it to the public internet. It does not provide remote access to the Pulse web UI. + +> Supported Pulse Mobile clients pair from **Settings → Pulse Mobile** using a QR code or deep link and connect through Pulse Relay over an end-to-end encrypted channel. ## How It Works @@ -20,7 +22,7 @@ Pulse Relay provides **end-to-end encrypted remote access** foundations for Puls ## Quick Start -1. Go to **Settings → Remote Access**. +1. Go to **Settings → Pulse Mobile**. 2. Toggle relay **On**. 3. Use the **QR Code** or **Deep Link** to pair a supported Pulse Mobile client. 4. Your paired mobile client connects through relay. @@ -49,7 +51,7 @@ Relay was designed with a zero-trust model: ### UI -**Settings → Remote Access** — toggle on/off, view QR code, and manage relay pairing sessions. +**Settings → Pulse Mobile** — toggle on/off, view QR code, and manage relay pairing sessions. ### Environment Variables @@ -84,7 +86,7 @@ Relay configuration is stored encrypted in `relay.enc` in the Pulse data directo 1. Pulse Mobile is in early access. Relay and Pro customers get install links from the authenticated [download page](https://pulserelay.pro/download.html). 2. Open Pulse Mobile and tap **Connect to Server**. -3. Scan the QR code from **Settings → Remote Access** in your Pulse web UI. +3. Scan the QR code from **Settings → Pulse Mobile** in your Pulse web UI. 4. The app connects via the relay for push notifications and secure Open Pulse handoff. ### Multiple Servers @@ -109,14 +111,14 @@ Pulse Mobile can pair with multiple Pulse instances. Each pairing has its own en ### Pulse Mobile can't connect -1. Verify relay is enabled in **Settings → Remote Access**. +1. Verify relay is enabled in **Settings → Pulse Mobile**. 2. Confirm your mobile account has beta access. 3. Re-scan the QR code — sessions can expire. 4. Ensure your mobile device has internet access. ### Open Pulse handoff not loading -1. Check the relay connection status in **Settings → Remote Access**. +1. Check the relay connection status in **Settings → Pulse Mobile**. 2. Look for WebSocket reconnection messages in Pulse logs. 3. Restart Pulse Mobile. diff --git a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md index 4a8ce936a..2a5a1dd76 100644 --- a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md +++ b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md @@ -8321,3 +8321,9 @@ Doctor's light-tone guidance uses opaque text colors because the current are translucent. The 49-agent synthetic browser pass follows the deep link, inspects the guidance and status rows at phone and desktop widths, and keeps update commands and credential authority unchanged. + +### Pulse Mobile pairing copy only + +`internal/api/onboarding_handlers.go` changed only the human-readable pairing +readiness messages so they name Settings > Pulse Mobile. No agent +registration, lifecycle or install path changed. diff --git a/docs/release-control/v6/internal/subsystems/alerts.md b/docs/release-control/v6/internal/subsystems/alerts.md index 9df72c342..44d040e55 100644 --- a/docs/release-control/v6/internal/subsystems/alerts.md +++ b/docs/release-control/v6/internal/subsystems/alerts.md @@ -3012,3 +3012,12 @@ or sending provider notifications. Component proof is not installed delivery. Formatting-only follow-up retains this warning-level contract. The production-component browser matrix was rerun after formatting at desktop and narrow widths, including all/critical/warning save/reload, cancel, and warning webhook creation. + +### Pulse Mobile push destination without an upsell + +The Alerts destinations page renders the mobile push panel only when the +instance has the `relay` feature. Community installs see no panel and no +upgrade prompt, and never load or save relay push settings. Licensed instances +keep the minimum-severity control and the link to Pulse Mobile settings, with +the 31 March 2027 retirement date in the copy. Webhook, email, Apprise and +dead-man destinations are unaffected. diff --git a/docs/release-control/v6/internal/subsystems/api-contracts.md b/docs/release-control/v6/internal/subsystems/api-contracts.md index ee1a5418b..2a9ec4e1c 100644 --- a/docs/release-control/v6/internal/subsystems/api-contracts.md +++ b/docs/release-control/v6/internal/subsystems/api-contracts.md @@ -10981,3 +10981,11 @@ text-input normalization, literal quoted token bytes, root/sudo private-file permissions, cleanup and early exit when download or preflight fails. This client transport repair does not establish the reporter's native pfSense abort cause or successful offline installation. + +### Pulse Mobile pairing readiness wording + +The onboarding readiness diagnostics for a disabled or unconnected relay name +the renamed Settings > Pulse Mobile section instead of Remote Access. Codes, +severities and the response shape are unchanged. The API reference heading +for the relay protocol records that Pulse Mobile retires on 31 March 2027, and +the published copy stays identical to `docs/API.md`. diff --git a/docs/release-control/v6/internal/subsystems/cloud-paid.md b/docs/release-control/v6/internal/subsystems/cloud-paid.md index 4a22aee74..34c509408 100644 --- a/docs/release-control/v6/internal/subsystems/cloud-paid.md +++ b/docs/release-control/v6/internal/subsystems/cloud-paid.md @@ -3640,3 +3640,15 @@ bootstrap or upgrade state, and it adds no commercial prompt. Paid Patrol modes stay governed on the Patrol and Actions surfaces, which remain reachable by route. `App.architecture.test.ts` pins that the gate reads the same capability as the Assistant launcher. + +### Relay retired inside the app + +Relay left public checkout on 2026-09-29, and existing Relay subscribers carry +Pro entitlements through the pulse-pro license server. In-app commercial +presentation therefore never offers Relay: Community sees only the Pro +comparison card, a Relay-tier license sees none, `FEATURE_MIN_TIER_LABELS` +maps `relay`, `mobile_app` and `push_notifications` to Pro, and the Pulse +Mobile settings gate and Alerts push panel carry no upgrade prompt. The +`relay` feature is labelled "Pulse Relay (Mobile Connection)" because it never +provided remote access to the web UI. Pulse Mobile retires on 31 March 2027; +until then licensed instances keep pairing and push unchanged. diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index d43728358..517793eef 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -7471,3 +7471,11 @@ instead of saying Patrol runs in Watch only mode or pitching Pro Patrol modes for a feature that is not running (issue #905). With AI on, the Watch only, switch and upgrade branches are unchanged. `actionsWatchOnlyEmptyState.test.ts` pins both states and the page wiring. + +### Pulse Mobile settings section label + +The `system-relay` settings section is labelled Pulse Mobile in the nav +catalog, the header metadata and every locale catalog; the product name is not +translated. Its route id, feature gate and read capability are unchanged. +Relay never provided remote access to the web UI, so no settings chrome may +label it Remote Access. diff --git a/docs/release-control/v6/internal/subsystems/security-privacy.md b/docs/release-control/v6/internal/subsystems/security-privacy.md index 73a5bf5e5..66024394c 100644 --- a/docs/release-control/v6/internal/subsystems/security-privacy.md +++ b/docs/release-control/v6/internal/subsystems/security-privacy.md @@ -2914,3 +2914,11 @@ The System settings item formerly labelled "Pulse server updates" is now that still routes agent updates to Infrastructure. This is locale copy only: the `system-updates` route id, its `systemSettingsRead` capability gate and every update endpoint and authorization check are unchanged. + +### Relay privacy description corrected + +`PRIVACY.md` states that the relay connects paired Pulse Mobile devices and +delivers push notifications, and that it does not provide remote access to the +web UI. The earlier "secure remote web access" wording described a capability +that never existed. No data flow changed, and the published docs mirror stays +identical. diff --git a/docs/release-control/v6/internal/subsystems/storage-recovery.md b/docs/release-control/v6/internal/subsystems/storage-recovery.md index 0e49660db..293ae1f91 100644 --- a/docs/release-control/v6/internal/subsystems/storage-recovery.md +++ b/docs/release-control/v6/internal/subsystems/storage-recovery.md @@ -6174,3 +6174,9 @@ shortcuts and the shortcuts help so Patrol entry points hide while AI is off. The change is navigation presentation only. It adds no storage, backup, snapshot or recovery surface, and it changes no route, authority or recovery state. The Recovery settings item and its capability gate are untouched. + +### Pulse Mobile pairing copy only + +`internal/api/onboarding_handlers.go` changed only the human-readable pairing +readiness messages so they name Settings > Pulse Mobile. This opens no new +storage or recovery path. diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json index c94f0106e..01e886f36 100644 --- a/frontend-modern/browser-verification.json +++ b/frontend-modern/browser-verification.json @@ -1,73 +1,67 @@ { "version": 1, - "base_sha": "2606c9d36a0b91c40fbc234fecad2167b7979fec", - "verified_at": "2026-09-28T22:20:33Z", + "base_sha": "fcbb7e58ad514ac146f666f49075039d40d829e2", + "verified_at": "2026-09-29T12:15:52Z", "result": "passed", "changed_paths": [ - "frontend-modern/src/App.tsx", - "frontend-modern/src/AppLayout.tsx", + "frontend-modern/src/components/Settings/RelaySettingsPanel.tsx", "frontend-modern/src/components/Settings/settingsHeaderMeta.ts", "frontend-modern/src/components/Settings/settingsNavCatalog.ts", - "frontend-modern/src/components/shared/KeyboardShortcutsModal.tsx", - "frontend-modern/src/components/shared/commandPaletteModel.ts", - "frontend-modern/src/components/shared/useCommandPaletteState.ts", - "frontend-modern/src/features/actions/actionPresentation.ts", - "frontend-modern/src/hooks/useKeyboardShortcuts.ts", + "frontend-modern/src/components/Settings/useRelaySettingsPanelState.ts", + "frontend-modern/src/features/alerts/AlertPushDestinationsSection.tsx", + "frontend-modern/src/features/alerts/tabs/DestinationsTab.tsx", "frontend-modern/src/i18n/messages.de.ts", "frontend-modern/src/i18n/messages.es.ts", "frontend-modern/src/i18n/messages.ts", - "frontend-modern/src/pages/Actions.tsx", - "frontend-modern/src/types/config.ts", - "frontend-modern/src/utils/updatesPresentation.ts" + "frontend-modern/src/utils/alertDestinationsPresentation.ts", + "frontend-modern/src/utils/licensePresentation.ts", + "frontend-modern/src/utils/relayPresentation.ts", + "frontend-modern/src/utils/selfHostedFeatureCatalog.generated.ts", + "frontend-modern/src/utils/selfHostedPlans.ts" ], "content_sha256": { - "frontend-modern/src/App.tsx": "a6ca67087cce917dc28914701262e6f631eb03621c24c8a773d5d025beb41215", - "frontend-modern/src/AppLayout.tsx": "d1d8bd884c43a20c1dbbde2ddb9f3278eb885b02c21e4cc3fa3fa1089cd1d320", - "frontend-modern/src/components/Settings/settingsHeaderMeta.ts": "d024df468000f183b2452d6b3fcb60fdce1b050107cd2e9d4ac586c4f13f2ddd", - "frontend-modern/src/components/Settings/settingsNavCatalog.ts": "8d3bff1c977b9db02c807d03fd80d40dabfc18c6a59809358c52a938a9c97c88", - "frontend-modern/src/components/shared/KeyboardShortcutsModal.tsx": "a27ccd1bdccf31f1890779fc661ad4e896786594b99ff419160ce6c556fc7c96", - "frontend-modern/src/components/shared/commandPaletteModel.ts": "69a75c17579239c407ba18820e9f4d47d8b8b0e587cdcc3bb154a684b2a17047", - "frontend-modern/src/components/shared/useCommandPaletteState.ts": "95ff00c6e790f17fdd849e845b34e9047f655ec89ca279032ab764da283b4cd4", - "frontend-modern/src/features/actions/actionPresentation.ts": "8e67a99e744cf7dbbc1fc53da1d9697af78a90d95779df3553ee8867c3c51496", - "frontend-modern/src/hooks/useKeyboardShortcuts.ts": "f55d67c136d5d8e21ed220b33eac0512d5fae5b5aa835f60fa2570159c140d30", - "frontend-modern/src/i18n/messages.de.ts": "e55a115865e8b2c9cf4352eecac1a2f8996f4e0f51b9c14ddb7cfbe20e5b2f40", - "frontend-modern/src/i18n/messages.es.ts": "a348d63dd816e848cd75a0b5114b0f77c719d16117d9c86db719036fbe106af0", - "frontend-modern/src/i18n/messages.ts": "728aa1cf4b42698e14f16593825249c6a190c43d4ef28e2d4e28756c7c1990db", - "frontend-modern/src/pages/Actions.tsx": "88ab28e9333652e81770e570c140aa19f0b6d833be3b65fd41afd2e9614f60c5", - "frontend-modern/src/types/config.ts": "49ad539bcefc2f87380b744c1da387401151e3f4af9047db8a85b1ce0b70322e", - "frontend-modern/src/utils/updatesPresentation.ts": "4696f8426385aaf287ab610746ca278acc7e23bd377c9739fcfedcf635910634" + "frontend-modern/src/components/Settings/RelaySettingsPanel.tsx": "1def33d23464ed3e0a6127e75f16e7da814c9ca7d8fc6a680532fbef05e67755", + "frontend-modern/src/components/Settings/settingsHeaderMeta.ts": "4847df256a489833c5836e0babcd651ea85d640a204288979f965bda4ff7432b", + "frontend-modern/src/components/Settings/settingsNavCatalog.ts": "d4832713b84d88033fa15211342b6ff8a243e20c56732c5431aed8e10b22e9c2", + "frontend-modern/src/components/Settings/useRelaySettingsPanelState.ts": "56525ac9bd0f3634adea1cccbc53c557caed8faad223f48d788c074ab2fa8a86", + "frontend-modern/src/features/alerts/AlertPushDestinationsSection.tsx": "3a160748f1bd0f42ba412757ca919803a440ad8afb0a946d0d5807188c4bc5a0", + "frontend-modern/src/features/alerts/tabs/DestinationsTab.tsx": "1e07af69a4f4268601f8227500e79eb6a827c49f7b0d62b283129ff5fa2935f9", + "frontend-modern/src/i18n/messages.de.ts": "2daa6b5a6bb2a59bf7846fa2f93310a48065fe7bc3082777cbb51c2fd5f107e2", + "frontend-modern/src/i18n/messages.es.ts": "7052ac4596ef1d36b46935f1bf451f552aee87abe45e23840b3e9d570b215205", + "frontend-modern/src/i18n/messages.ts": "42c6798b32c5647c89a9f658b40e605773d85e5478a4b889bc0469ef3b2efe8e", + "frontend-modern/src/utils/alertDestinationsPresentation.ts": "82980cfc35fab1e91c5484b75e1bc3b84f6c2234e26ed5306b4583e89da3f880", + "frontend-modern/src/utils/licensePresentation.ts": "3573f465207e64e2984c5f18dc89f644b6a88de6e124779840714b01f572e3d6", + "frontend-modern/src/utils/relayPresentation.ts": "9a0d1000d91e307d31b7176be7d4529a150f7364fd913a4b379e2c96a3aa9e1c", + "frontend-modern/src/utils/selfHostedFeatureCatalog.generated.ts": "64baf62ae41b9832381ac2a91f7c9724131e765f2493cb894efabeed2ae16d0e", + "frontend-modern/src/utils/selfHostedPlans.ts": "5efab41402fa3409f307398d5feb3b8ccdbf16ac01d6ce0cdacc3024c640b970" }, "routes": [ - "/actions", - "/patrol", - "/settings/system-updates", - "/preview/setup-complete?scenario=empty", - "/preview/setup-complete?scenario=vmware-api-backed" + "/settings/system-relay", + "/alerts/notifications", + "/settings/pulse-intelligence/billing/plan", + "/settings/system-general" ], "viewports": [ { - "width": 1440, - "height": 900 + "width": 1024, + "height": 768 }, { - "width": 375, - "height": 812 + "width": 390, + "height": 844 } ], "states": [ - "AI off and AI on with mocked authenticated API responses: Actions empty Open tab, app-shell Patrol navigation, command palette and shortcuts at desktop and phone widths", - "AI off: plain Actions empty-state copy; no Patrol navigation, palette command, shortcut help row or g r navigation", - "AI on: Watch only guidance and Patrol entry points; g r reaches /patrol", - "Empty setup completion preview at desktop and VMware API-backed setup completion preview at phone width show first-source guidance and credential disclosure", - "System Updates settings route at desktop and phone widths labels the item Updates and retains the agent-update handoff to Infrastructure" + "Licensed instance (MSP test license from the offline issuer, relay capability present): Settings > Pulse Mobile shows 'Pair Pulse Mobile' with the 31 March 2027 retirement copy, the 'Allow Pulse Mobile connections' toggle, no Remote Access wording and no upgrade prompt", + "Licensed instance: Alerts > Notifications shows the Mobile push notifications panel with retirement copy, minimum-severity control and the 'Open Pulse Mobile settings' link", + "Licensed instance: Plans & Billing lists the relay feature as 'Pulse Relay (Mobile Connection)' with no Remote Access or remote web access wording", + "Community instance (no license): Alerts > Notifications has no Mobile push panel and no Relay upsell while webhook, email and Apprise destinations remain", + "Community instance: settings navigation has no Pulse Mobile or Remote Access item; the direct /settings/system-relay route shows the retirement message with no upgrade link", + "Community instance: Plans & Billing shows the Community plan with no Relay plan card or remote web access copy" ], "interactions": [ - "Loaded /actions, opened and closed command palette and keyboard shortcuts, then pressed g r in both AI states at both widths", - "Loaded both setup completion previews and checked connection choices, next-step copy and horizontal overflow", - "Loaded /settings/system-updates at both widths and checked heading and copy" - ], - "command": "pulse-worker-browser web-composed-proof.cjs from assigned workspace root; offline Vite preview and Chromium 141.0.7390.37", - "artifacts": "/var/lib/pulse-maintainer/worker-outputs/web-product-mc08uh4u/browser-composed-2606c9d36a", - "notes": "The browser used mocked API responses and no live backend, paired agent or installed release. Mocked WebSocket reconnect warnings do not establish runtime connectivity. Screenshots of Actions, command palette, shortcut help, setup and Updates at both widths were inspected.", - "verified_source_sha": "2606c9d36a0b91c40fbc234fecad2167b7979fec" + "Activated the offline issuer test license through /api/license/activate on the licensed stack and confirmed relay in /api/license/entitlements", + "Loaded /settings/system-relay and /alerts/notifications on the licensed stack at 1024px and 390px, checked the toggle and settings link stay inside the viewport with no horizontal overflow", + "Loaded /alerts/notifications, /settings/system-general, /settings/system-relay and /settings/system/billing/plan on the Community stack and checked nav items, panels and links" + ] } diff --git a/frontend-modern/public/docs/API.md b/frontend-modern/public/docs/API.md index aa44a080f..c5a767796 100644 --- a/frontend-modern/public/docs/API.md +++ b/frontend-modern/public/docs/API.md @@ -1586,7 +1586,7 @@ TrueNAS resources (pools, datasets, disks, ZFS snapshots, replication tasks, ale --- -## 📱 Relay / Mobile Remote Access (Relay and Above) +## 📱 Relay / Pulse Mobile (retiring 31 March 2027) End-to-end encrypted relay protocol for mobile connectivity. diff --git a/frontend-modern/public/docs/CLOUD.md b/frontend-modern/public/docs/CLOUD.md index 3037738d1..c64b1e8ec 100644 --- a/frontend-modern/public/docs/CLOUD.md +++ b/frontend-modern/public/docs/CLOUD.md @@ -62,7 +62,7 @@ Add your Proxmox VE, PBS, PMG, or TrueNAS systems via **Settings → Infrastruct ### 4. Set Up Mobile Access -Relay is enabled by default on Cloud instances. Open **Settings → Remote Access** to prepare pairing and connect once mobile beta/public access is enabled. +Relay is enabled by default on Cloud instances. Open **Settings → Pulse Mobile** to pair a phone. Pulse Mobile is being retired on 31 March 2027. ## Data & Privacy diff --git a/frontend-modern/public/docs/PRIVACY.md b/frontend-modern/public/docs/PRIVACY.md index e3db0c458..cdebc02e3 100644 --- a/frontend-modern/public/docs/PRIVACY.md +++ b/frontend-modern/public/docs/PRIVACY.md @@ -420,5 +420,5 @@ The telemetry implementation is in [`internal/telemetry/telemetry.go`](../intern Pulse can make outbound connections when you enable specific features: - **AI providers**: when AI features are configured, Pulse sends only the context required for your request to the provider you chose. This can include active Patrol objective briefs and their optional operator context when they apply to a Patrol run. Retained objective text and observer artifacts are encrypted at rest in the local organization data directory; objective text is not included in Pulse usage telemetry. A provider may return a model-authored observer proposal for an uncovered objective; Pulse encrypts that artifact with the retained objective, excludes it from public objective reads and later prompt seeds, and does not include its content in usage telemetry or audit messages. Saving an objective by itself does not call a model. Local providers stay on your network; non-local hosted providers receive provider-bound context directly from your Pulse instance. AI prompts from self-managed installs do not transit Pulse infrastructure. Before non-local model requests leave the instance, governed resource details use the same resource-policy redaction shown in Data Handling: local-only resource details are omitted from detailed prompt sections or replaced with policy-safe summaries, and known restricted resource identifiers are redacted where they appear in provider-bound context. See `docs/AI.md`. -- **Relay / Remote Access**: when relay is enabled, Pulse connects to the configured relay endpoint to enable secure remote web access, Pulse Mobile pairing for handoff, and push notifications. See Settings → Remote Access. +- **Relay / Pulse Mobile**: when Pulse Mobile connections are on, Pulse connects to the configured relay endpoint so paired Pulse Mobile devices can reach this instance and receive push notifications. Relay does not provide remote access to the web UI. See Settings → Pulse Mobile. Pulse Mobile is being retired on 31 March 2027. - **Update checks**: Pulse can check for new releases/updates (for example via GitHub release metadata) depending on your deployment and configuration. diff --git a/frontend-modern/public/docs/PULSE_PRO.md b/frontend-modern/public/docs/PULSE_PRO.md index deb8481b4..f4b4bb2ef 100644 --- a/frontend-modern/public/docs/PULSE_PRO.md +++ b/frontend-modern/public/docs/PULSE_PRO.md @@ -137,7 +137,7 @@ This matrix reflects the entitlement keys enforced in code plus the runtime hist | Constant | Capability Key | Display Name | Community | Relay | Pro | Cloud | Primary Gating Mechanism / Notes | |---|---|---|:---:|:---:|:---:|:---:|---| | `FeatureAIPatrol` | `ai_patrol` | Pulse Patrol (Background Health Checks) | Y | Y | Y | Y | Patrol itself is available on Community with your own provider or local model. Higher-autonomy outcomes and fix execution are separately gated. | -| `FeatureRelay` | `relay` | Remote Access (Mobile Relay) | N | Y | Y | Y | API route gating via `RequireLicenseFeature(..., relay, ...)` for relay settings and onboarding endpoints. | +| `FeatureRelay` | `relay` | Pulse Relay (Mobile Connection) | N | Y | Y | Y | API route gating via `RequireLicenseFeature(..., relay, ...)` for relay settings and onboarding endpoints. | | `FeatureAIAlerts` | `ai_alerts` | Patrol Investigates Issues and Explains the Root Cause | N | N | Y | Y | API route gating via `RequireLicenseFeature(..., ai_alerts, ...)`. | | `FeatureAIAutoFix` | `ai_autofix` | Patrol Applies Safe Fixes and Verifies the Result | N | N | Y | Y | Required for governed fix execution and automatic Patrol actions. | | `FeatureKubernetesAI` | `kubernetes_ai` | Kubernetes AI Analysis (Compatibility) | N | N | Y | Y | Legacy compatibility gate for `/api/ai/kubernetes/analyze`; not a primary marketed v6 Pro plan pillar. | diff --git a/frontend-modern/public/docs/RELAY.md b/frontend-modern/public/docs/RELAY.md index bda6176f2..cb791d27f 100644 --- a/frontend-modern/public/docs/RELAY.md +++ b/frontend-modern/public/docs/RELAY.md @@ -1,8 +1,10 @@ -# Relay / Pulse Mobile Handoff (Relay and Above) +# Relay / Pulse Mobile -Pulse Relay provides **end-to-end encrypted remote access** foundations for Pulse instances. It allows secure remote connectivity without exposing your Pulse server to the public internet. +> **Pulse Mobile is being retired on 31 March 2027.** Paired phones keep working until then. Relay is no longer sold as a plan; existing Relay subscribers have Pro features at their current price. For alerts on your phone afterwards, add an ntfy, Gotify, or Pushover destination under Alerts, and reach the Pulse web UI away from home through your own VPN or tunnel. -> Supported Pulse Mobile clients pair from **Settings → Remote Access** using a QR code or deep link and connect through Pulse Relay over end-to-end encrypted remote access. +Pulse Relay is the end-to-end encrypted connection between a Pulse instance and paired Pulse Mobile devices. It lets the app reach your server without exposing it to the public internet. It does not provide remote access to the Pulse web UI. + +> Supported Pulse Mobile clients pair from **Settings → Pulse Mobile** using a QR code or deep link and connect through Pulse Relay over an end-to-end encrypted channel. ## How It Works @@ -20,7 +22,7 @@ Pulse Relay provides **end-to-end encrypted remote access** foundations for Puls ## Quick Start -1. Go to **Settings → Remote Access**. +1. Go to **Settings → Pulse Mobile**. 2. Toggle relay **On**. 3. Use the **QR Code** or **Deep Link** to pair a supported Pulse Mobile client. 4. Your paired mobile client connects through relay. @@ -49,7 +51,7 @@ Relay was designed with a zero-trust model: ### UI -**Settings → Remote Access** — toggle on/off, view QR code, and manage relay pairing sessions. +**Settings → Pulse Mobile** — toggle on/off, view QR code, and manage relay pairing sessions. ### Environment Variables @@ -84,7 +86,7 @@ Relay configuration is stored encrypted in `relay.enc` in the Pulse data directo 1. Pulse Mobile is in early access. Relay and Pro customers get install links from the authenticated [download page](https://pulserelay.pro/download.html). 2. Open Pulse Mobile and tap **Connect to Server**. -3. Scan the QR code from **Settings → Remote Access** in your Pulse web UI. +3. Scan the QR code from **Settings → Pulse Mobile** in your Pulse web UI. 4. The app connects via the relay for push notifications and secure Open Pulse handoff. ### Multiple Servers @@ -109,14 +111,14 @@ Pulse Mobile can pair with multiple Pulse instances. Each pairing has its own en ### Pulse Mobile can't connect -1. Verify relay is enabled in **Settings → Remote Access**. +1. Verify relay is enabled in **Settings → Pulse Mobile**. 2. Confirm your mobile account has beta access. 3. Re-scan the QR code — sessions can expire. 4. Ensure your mobile device has internet access. ### Open Pulse handoff not loading -1. Check the relay connection status in **Settings → Remote Access**. +1. Check the relay connection status in **Settings → Pulse Mobile**. 2. Look for WebSocket reconnection messages in Pulse logs. 3. Restart Pulse Mobile. diff --git a/frontend-modern/src/components/Settings/RelaySettingsPanel.tsx b/frontend-modern/src/components/Settings/RelaySettingsPanel.tsx index 19bc511ec..390d8f97f 100644 --- a/frontend-modern/src/components/Settings/RelaySettingsPanel.tsx +++ b/frontend-modern/src/components/Settings/RelaySettingsPanel.tsx @@ -30,15 +30,15 @@ export const RelaySettingsPanel: Component = (props) => // Relay feature gate if (!state.relayEnabled()) { return ( - + Loading...}> } - title="Remote Access (Relay)" + title="Pulse Mobile" body={RELAY_LICENSE_REQUIRED_MESSAGE} upgradeDestination={state.upgradeDestination()} - showUpgradePrompts={state.showUpgradePrompts()} + showUpgradePrompts={false} /> @@ -47,14 +47,14 @@ export const RelaySettingsPanel: Component = (props) => } return ( - + {getSettingsConfigurationLoadingState().text}} > - Remote access settings are read-only for this account. + Pulse Mobile settings are read-only for this account. @@ -95,11 +95,11 @@ export const RelaySettingsPanel: Component = (props) =>
- Enable Remote Access + Allow Pulse Mobile connections

{RELAY_ENABLE_HELP_TEXT}

void state.handleToggleEnabled(e.currentTarget.checked)} disabled={!state.canManage() || state.saving()} diff --git a/frontend-modern/src/components/Settings/__tests__/ProLicensePanel.test.tsx b/frontend-modern/src/components/Settings/__tests__/ProLicensePanel.test.tsx index 49b985ca5..089710876 100644 --- a/frontend-modern/src/components/Settings/__tests__/ProLicensePanel.test.tsx +++ b/frontend-modern/src/components/Settings/__tests__/ProLicensePanel.test.tsx @@ -305,11 +305,10 @@ describe('ProLicensePanel', () => { expect(screen.getByText('Select a plan')).toBeInTheDocument(); expect(screen.getByText('Available plans')).toBeInTheDocument(); - expect(screen.getByText('Relay plan')).toBeInTheDocument(); + // Relay was retired on 2026-09-29: Community sees only the Pro card. + expect(screen.queryByText('Relay plan')).not.toBeInTheDocument(); expect(screen.getByText('Pulse Pro plan')).toBeInTheDocument(); - expect(screen.getByText('Remote web access via Relay')).toBeInTheDocument(); - expect(screen.getByText('Pulse Mobile pairing')).toBeInTheDocument(); - expect(screen.getByText('Push notifications')).toBeInTheDocument(); + expect(screen.queryByText(/remote web access/i)).not.toBeInTheDocument(); expect( screen.getByText('Patrol modes: Ask first, Safe auto-fix, or Autopilot'), ).toBeInTheDocument(); @@ -830,7 +829,7 @@ describe('ProLicensePanel', () => { expect(screen.getByText('Current plan: Pulse Pro')).toBeInTheDocument(); expect( screen.getByText( - 'Pulse Pro is active on this instance. It includes Relay connectivity, Pulse Mobile pairing, push notifications, Patrol modes (Ask first, Safe auto-fix, Autopilot), 90-day metric history, RBAC, audit logging, reporting, and agent profiles.', + 'Pulse Pro is active on this instance. It includes Patrol modes (Ask first, Safe auto-fix, Autopilot), 90-day metric history, RBAC, audit logging, reporting, agent profiles, and Pulse Mobile pairing until 31 March 2027.', ), ).toBeInTheDocument(); expect(screen.getByText('Primary capabilities')).toBeInTheDocument(); @@ -853,10 +852,10 @@ describe('ProLicensePanel', () => { 'Open this only when a Pro capability looks unavailable. Normal setup is choosing Patrol mode.', ), ).toBeInTheDocument(); - expect(screen.getByText('Remote access, pairing, and push')).toBeInTheDocument(); + expect(screen.getByText('Pulse Mobile pairing and push')).toBeInTheDocument(); expect( screen.getByText( - 'Relay, Pulse Mobile pairing, and push notifications are available on this instance.', + 'Pulse Mobile pairing and push notifications are available on this instance until Pulse Mobile is retired on 31 March 2027.', ), ).toBeInTheDocument(); expect(screen.getByText('Patrol investigation and remediation')).toBeInTheDocument(); @@ -900,7 +899,7 @@ describe('ProLicensePanel', () => { expect( screen.getByText( - 'Relay is active on this instance. It includes remote web access, Pulse Mobile pairing, push notifications, and 14-day metric history.', + 'Relay is active on this instance. It includes Pulse Mobile pairing and push notifications until 31 March 2027, and 14-day metric history.', ), ).toBeInTheDocument(); expect(screen.queryByText('Available plans')).not.toBeInTheDocument(); @@ -908,9 +907,9 @@ describe('ProLicensePanel', () => { expect(screen.queryByText('Pulse Pro plan')).not.toBeInTheDocument(); expect(screen.queryByRole('link', { name: 'View plans' })).not.toBeInTheDocument(); expect(screen.getByText('Relay status')).toBeInTheDocument(); - expect(screen.getByText('Remote access, pairing, and push')).toBeInTheDocument(); + expect(screen.getAllByText('Pulse Mobile pairing and push').length).toBeGreaterThan(0); expect(screen.getAllByText('14-day metric history').length).toBeGreaterThan(0); - expect(screen.getByText('Pulse Relay (Remote Access)')).toBeInTheDocument(); + expect(screen.getByText('Pulse Relay (Mobile Connection)')).toBeInTheDocument(); expect(screen.getByText('Pulse Mobile Pairing')).toBeInTheDocument(); expect(screen.getByText('Push Notifications')).toBeInTheDocument(); }); @@ -1098,7 +1097,7 @@ describe('ProLicensePanel', () => { expect(screen.getByText('Audit Logging')).toBeInTheDocument(); expect(screen.getByText('PDF/CSV Reporting')).toBeInTheDocument(); expect(screen.getByText('Centralized Agent Profiles')).toBeInTheDocument(); - expect(screen.getByText('Pulse Relay (Remote Access)')).toBeInTheDocument(); + expect(screen.getByText('Pulse Relay (Mobile Connection)')).toBeInTheDocument(); expect(screen.getByText('Pulse Mobile Pairing')).toBeInTheDocument(); expect(screen.getByText('Push Notifications')).toBeInTheDocument(); expect(screen.getByText('Extended Metric History')).toBeInTheDocument(); diff --git a/frontend-modern/src/components/Settings/__tests__/RelaySettingsPanel.runtime.test.tsx b/frontend-modern/src/components/Settings/__tests__/RelaySettingsPanel.runtime.test.tsx index 060931d81..42a00c8e0 100644 --- a/frontend-modern/src/components/Settings/__tests__/RelaySettingsPanel.runtime.test.tsx +++ b/frontend-modern/src/components/Settings/__tests__/RelaySettingsPanel.runtime.test.tsx @@ -97,6 +97,7 @@ vi.mock('qrcode', () => ({ })); import { RelaySettingsPanel } from '../RelaySettingsPanel'; +import { RELAY_LICENSE_REQUIRED_MESSAGE } from '@/utils/relayPresentation'; describe('RelaySettingsPanel runtime', () => { beforeEach(() => { @@ -178,24 +179,18 @@ describe('RelaySettingsPanel runtime', () => { expect(relaySettingsPanelStateSource).not.toContain('commercialPosture()?.trial_eligible'); }); - it('shows supported Pulse Mobile pairing copy on the relay paywall', async () => { + it('explains the Pulse Mobile retirement without an upsell when the plan lacks it', async () => { hasFeatureMock.mockReturnValue(false); render(() => ); await waitFor(() => { - expect(screen.getByText('Remote Access (Relay)')).toBeInTheDocument(); + expect(screen.getByText(RELAY_LICENSE_REQUIRED_MESSAGE)).toBeInTheDocument(); }); - expect( - screen.getByText( - 'See your infrastructure and get alert push notifications anywhere with the Pulse Mobile app — no port forwarding or VPN required. Available with Relay and Pro plans.', - ), - ).toBeInTheDocument(); - expect(screen.getByRole('link', { name: 'View plans' })).toHaveAttribute( - 'href', - 'https://example.com/upgrade', - ); + expect(RELAY_LICENSE_REQUIRED_MESSAGE).toContain('retired on 31 March 2027'); + expect(screen.queryByRole('link', { name: 'View plans' })).not.toBeInTheDocument(); + expect(screen.queryByText(/Remote Access/)).not.toBeInTheDocument(); }); it('loads connected relay state and generates a pairing QR payload', async () => { @@ -205,10 +200,10 @@ describe('RelaySettingsPanel runtime', () => { expect(screen.getByDisplayValue('wss://relay.example.test/ws/instance')).toBeInTheDocument(); }); - expect(screen.getByText('Pair Pulse Mobile through Relay')).toBeInTheDocument(); + expect(screen.getByText('Pair Pulse Mobile')).toBeInTheDocument(); expect( screen.getByText( - 'Install the Pulse Mobile app on your phone, then scan a QR code or open a deep link here to securely link it to this Pulse instance. Paired devices receive alert push notifications over an end-to-end encrypted connection.', + 'Install the Pulse Mobile app on your phone, then scan a QR code or open a deep link here to securely link it to this Pulse instance. Paired devices receive alert push notifications over an end-to-end encrypted connection until Pulse Mobile is retired on 31 March 2027.', ), ).toBeInTheDocument(); expect(screen.getByText('Connected')).toBeInTheDocument(); @@ -275,7 +270,7 @@ describe('RelaySettingsPanel runtime', () => { expect( screen.getByText( - 'Remote Access is enabled, but this instance does not have an active Relay token. Activate a Relay-capable plan or turn Remote Access off before pairing mobile clients.', + 'Pulse Mobile connections are on, but this instance does not have an active Relay token. Refresh your plan, or turn Pulse Mobile connections off before pairing phones.', ), ).toBeInTheDocument(); expect(screen.queryByText('register: no license token available')).not.toBeInTheDocument(); diff --git a/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts b/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts index 969074349..6619ee7a5 100644 --- a/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts +++ b/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts @@ -375,6 +375,10 @@ describe('settings architecture guardrails', () => { ); expect(systemRelayNavBlock?.[0]).toContain("features: ['relay']"); expect(systemRelayNavBlock?.[0]).toContain('hideWhenUnavailable: true'); + // Relay only ever connected Pulse Mobile; the section is named for the app, + // never as remote access to the web UI. + expect(systemRelayNavBlock?.[0]).toContain("label: 'Pulse Mobile'"); + expect(settingsNavCatalogSource).not.toContain("label: 'Remote Access'"); expect(settingsHeaderMetaSource).toContain( 'title: SELF_HOSTED_PRO_BILLING_PRESENTATION.shellTitle', ); diff --git a/frontend-modern/src/components/Settings/__tests__/settingsFeatureGates.branchcov0713.test.ts b/frontend-modern/src/components/Settings/__tests__/settingsFeatureGates.branchcov0713.test.ts index f424d9f91..95b80a84e 100644 --- a/frontend-modern/src/components/Settings/__tests__/settingsFeatureGates.branchcov0713.test.ts +++ b/frontend-modern/src/components/Settings/__tests__/settingsFeatureGates.branchcov0713.test.ts @@ -53,11 +53,11 @@ const trackingHas = // Hand-written snapshot of the exact reason string each gated tab must emit // when its required feature is missing. Derived from reading -// `getFeatureMinTierLabel` (relay -> 'Relay', multi_tenant -> 'MSP', +// `getFeatureMinTierLabel` (relay -> 'Pro', multi_tenant -> 'MSP', // audit_logging -> 'Pro' fallback); NOT computed by calling the helper, so a // change to either the tier map or the message template fails this loudly. const EXPECTED_LOCK_REASON: Partial> = { - 'system-relay': 'This settings section requires Relay.', + 'system-relay': 'This settings section requires Pro.', 'security-webhooks': 'This settings section requires Pro.', 'organization-overview': 'This settings section requires MSP.', 'organization-access': 'This settings section requires MSP.', diff --git a/frontend-modern/src/components/Settings/__tests__/settingsHeaderMeta.branchcov0713.test.ts b/frontend-modern/src/components/Settings/__tests__/settingsHeaderMeta.branchcov0713.test.ts index 6b48cbe8a..4b6b1ed92 100644 --- a/frontend-modern/src/components/Settings/__tests__/settingsHeaderMeta.branchcov0713.test.ts +++ b/frontend-modern/src/components/Settings/__tests__/settingsHeaderMeta.branchcov0713.test.ts @@ -165,9 +165,9 @@ describe('getSettingsHeaderMeta', () => { it('localizes system-relay to the English baseline', () => { expect(en['system-relay']).toEqual({ - title: 'Remote Access', + title: 'Pulse Mobile', description: - 'Check on your systems and get alert push notifications anywhere with the Pulse Mobile app — no port forwarding or VPN required.', + 'Connect paired Pulse Mobile devices to this instance. Pulse Mobile is being retired on 31 March 2027, and paired phones keep working until then.', }); }); @@ -440,9 +440,9 @@ describe('getSettingsHeaderMeta', () => { it('localizes system-relay', () => { expect(de['system-relay']).toEqual({ - title: 'Remote-Zugriff', + title: 'Pulse Mobile', description: - 'Behalten Sie Ihre Systeme von ueberall im Blick und erhalten Sie Alarm-Push-Benachrichtigungen ueber die Pulse-Mobile-App — ohne Portfreigaben oder VPN.', + 'Verbinden Sie gekoppelte Pulse-Mobile-Geraete mit dieser Instanz. Pulse Mobile wird am 31. Maerz 2027 eingestellt. Gekoppelte Telefone funktionieren bis dahin weiter.', }); }); @@ -639,9 +639,9 @@ describe('getSettingsHeaderMeta', () => { it('localizes system-relay', () => { expect(es['system-relay']).toEqual({ - title: 'Acceso remoto', + title: 'Pulse Mobile', description: - 'Consulta tus sistemas y recibe notificaciones push de alertas desde cualquier lugar con la aplicación Pulse Mobile — sin abrir puertos ni VPN.', + 'Conecta los dispositivos Pulse Mobile emparejados con esta instancia. Pulse Mobile se retirará el 31 de marzo de 2027 y los teléfonos emparejados seguirán funcionando hasta entonces.', }); }); diff --git a/frontend-modern/src/components/Settings/__tests__/settingsLocalization.test.ts b/frontend-modern/src/components/Settings/__tests__/settingsLocalization.test.ts index 886a986e9..b2b306719 100644 --- a/frontend-modern/src/components/Settings/__tests__/settingsLocalization.test.ts +++ b/frontend-modern/src/components/Settings/__tests__/settingsLocalization.test.ts @@ -56,21 +56,21 @@ describe('settings localization catalog', () => { expect(getSettingsShellCopy('es').mobileCloseLabel).toBe('Cerrar navegación de ajustes'); }); - it('localizes the value-first Remote Access header in every catalog locale', () => { + it('localizes the Pulse Mobile header in every catalog locale', () => { expect(SETTINGS_HEADER_META['system-relay']).toEqual({ - title: 'Remote Access', + title: 'Pulse Mobile', description: - 'Check on your systems and get alert push notifications anywhere with the Pulse Mobile app — no port forwarding or VPN required.', + 'Connect paired Pulse Mobile devices to this instance. Pulse Mobile is being retired on 31 March 2027, and paired phones keep working until then.', }); expect(getSettingsHeaderMeta('es')['system-relay']).toEqual({ - title: 'Acceso remoto', + title: 'Pulse Mobile', description: - 'Consulta tus sistemas y recibe notificaciones push de alertas desde cualquier lugar con la aplicación Pulse Mobile — sin abrir puertos ni VPN.', + 'Conecta los dispositivos Pulse Mobile emparejados con esta instancia. Pulse Mobile se retirará el 31 de marzo de 2027 y los teléfonos emparejados seguirán funcionando hasta entonces.', }); expect(getSettingsHeaderMeta('de')['system-relay']).toEqual({ - title: 'Remote-Zugriff', + title: 'Pulse Mobile', description: - 'Behalten Sie Ihre Systeme von ueberall im Blick und erhalten Sie Alarm-Push-Benachrichtigungen ueber die Pulse-Mobile-App — ohne Portfreigaben oder VPN.', + 'Verbinden Sie gekoppelte Pulse-Mobile-Geraete mit dieser Instanz. Pulse Mobile wird am 31. Maerz 2027 eingestellt. Gekoppelte Telefone funktionieren bis dahin weiter.', }); }); diff --git a/frontend-modern/src/components/Settings/__tests__/settingsNavigation.integration.test.tsx b/frontend-modern/src/components/Settings/__tests__/settingsNavigation.integration.test.tsx index 0ee91c25e..fcd812d15 100644 --- a/frontend-modern/src/components/Settings/__tests__/settingsNavigation.integration.test.tsx +++ b/frontend-modern/src/components/Settings/__tests__/settingsNavigation.integration.test.tsx @@ -151,7 +151,7 @@ describe('settingsNavigation integration scaffold', () => { ).toBe(false); }); - it('hides Remote Access navigation for free installs but keeps the direct route available', () => { + it('hides Pulse Mobile navigation for free installs but keeps the direct route available', () => { expect( shouldHideSettingsNavItem('system-relay', { hasFeature: hasFeatures([]), @@ -478,7 +478,7 @@ describe('settingsNavigation integration scaffold', () => { it('getTabLockReason returns tier-specific reason for locked tabs and null for unlocked', () => { const expectedTierLabels: Record = { - relay: 'Relay', + relay: 'Pro', multi_tenant: 'MSP', audit_logging: 'Pro', }; diff --git a/frontend-modern/src/components/Settings/__tests__/settingsReadOnlyPanels.test.tsx b/frontend-modern/src/components/Settings/__tests__/settingsReadOnlyPanels.test.tsx index 6a39a88e6..9ce03cf58 100644 --- a/frontend-modern/src/components/Settings/__tests__/settingsReadOnlyPanels.test.tsx +++ b/frontend-modern/src/components/Settings/__tests__/settingsReadOnlyPanels.test.tsx @@ -141,7 +141,7 @@ describe('settings read-only panel states', () => { render(() => ); await waitFor(() => { - expect(screen.getByText(/Remote access settings are read-only/i)).toBeInTheDocument(); + expect(screen.getByText(/Pulse Mobile settings are read-only/i)).toBeInTheDocument(); }); const serverUrlInput = screen.getByDisplayValue('wss://relay.example.test/ws/instance'); diff --git a/frontend-modern/src/components/Settings/settingsHeaderMeta.ts b/frontend-modern/src/components/Settings/settingsHeaderMeta.ts index 408a27f97..b4951cdab 100644 --- a/frontend-modern/src/components/Settings/settingsHeaderMeta.ts +++ b/frontend-modern/src/components/Settings/settingsHeaderMeta.ts @@ -52,7 +52,7 @@ export const SETTINGS_HEADER_META: SettingsHeaderMetaMap = { 'Configure the model-backed service context Assistant and Patrol use. Infrastructure discovery and onboarding stay under Infrastructure.', }, 'system-relay': { - title: 'Remote Access', + title: 'Pulse Mobile', description: RELAY_SETTINGS_DESCRIPTION, }, 'system-billing': { diff --git a/frontend-modern/src/components/Settings/settingsNavCatalog.ts b/frontend-modern/src/components/Settings/settingsNavCatalog.ts index be83485a9..ad3952278 100644 --- a/frontend-modern/src/components/Settings/settingsNavCatalog.ts +++ b/frontend-modern/src/components/Settings/settingsNavCatalog.ts @@ -313,7 +313,7 @@ export const SETTINGS_NAV_GROUPS: SettingsNavGroup[] = [ }, { id: 'system-relay', - label: 'Remote Access', + label: 'Pulse Mobile', icon: RadioTower, iconProps: { strokeWidth: 2 }, features: ['relay'], diff --git a/frontend-modern/src/components/Settings/useRelaySettingsPanelState.ts b/frontend-modern/src/components/Settings/useRelaySettingsPanelState.ts index 4407e0943..48a5646ab 100644 --- a/frontend-modern/src/components/Settings/useRelaySettingsPanelState.ts +++ b/frontend-modern/src/components/Settings/useRelaySettingsPanelState.ts @@ -162,13 +162,13 @@ export function useRelaySettingsPanelState(props: RelaySettingsPanelProps) { await loadConfig(); if (enabled) { startStatusPolling(); - showSuccess('Remote access enabled'); + showSuccess('Pulse Mobile connections on'); } else { stopStatusPolling(); setStatus(null); await deletePairingTokenIfUnused(pairingTokenId()); resetPairingState(); - showSuccess('Remote access disabled'); + showSuccess('Pulse Mobile connections off'); } } catch (error) { showError('Failed to update relay configuration'); diff --git a/frontend-modern/src/features/alerts/AlertPushDestinationsSection.test.tsx b/frontend-modern/src/features/alerts/AlertPushDestinationsSection.test.tsx index 213db7211..7bbd772da 100644 --- a/frontend-modern/src/features/alerts/AlertPushDestinationsSection.test.tsx +++ b/frontend-modern/src/features/alerts/AlertPushDestinationsSection.test.tsx @@ -4,17 +4,10 @@ import type { JSX } from 'solid-js'; import { afterEach, describe, expect, it, vi } from 'vitest'; import { AlertPushDestinationsSection } from './AlertPushDestinationsSection'; import { - ALERT_DESTINATIONS_PUSH_GATE_MESSAGE, ALERT_DESTINATIONS_PUSH_MINIMUM_SEVERITY_HELP, ALERT_DESTINATIONS_PUSH_READY_MESSAGE, ALERT_DESTINATIONS_PUSH_SETUP_LINK_LABEL, } from '@/utils/alertDestinationsPresentation'; -import type { UpgradeDestination } from '@/utils/upgradeNavigation'; - -const upgradeDestination: UpgradeDestination = { - href: '/settings/pulse-intelligence/billing', - external: false, -}; function renderWithRouter(ui: () => JSX.Element) { return render(() => ( @@ -29,13 +22,11 @@ describe('AlertPushDestinationsSection', () => { cleanup(); }); - it('points licensed installs at Remote Access settings', () => { + it('points licensed installs at Pulse Mobile settings', () => { const onMinimumSeverityChange = vi.fn(); renderWithRouter(() => ( @@ -57,29 +48,11 @@ describe('AlertPushDestinationsSection', () => { expect(screen.getByText(ALERT_DESTINATIONS_PUSH_MINIMUM_SEVERITY_HELP)).toBeInTheDocument(); }); - it('shows the Relay upgrade gate to unlicensed installs', () => { - renderWithRouter(() => ( - - )); + it('renders nothing for installs without Pulse Mobile, since it is no longer sold', () => { + renderWithRouter(() => ); - expect(screen.getByText(ALERT_DESTINATIONS_PUSH_GATE_MESSAGE)).toBeInTheDocument(); expect(screen.queryByText(ALERT_DESTINATIONS_PUSH_READY_MESSAGE)).not.toBeInTheDocument(); - }); - - it('hides the upgrade call-to-action when upgrade prompts are suppressed', () => { - renderWithRouter(() => ( - - )); - - expect(screen.getByText(ALERT_DESTINATIONS_PUSH_GATE_MESSAGE)).toBeInTheDocument(); + expect(screen.queryByText(/Relay/)).not.toBeInTheDocument(); expect(screen.queryByRole('link')).not.toBeInTheDocument(); }); }); diff --git a/frontend-modern/src/features/alerts/AlertPushDestinationsSection.tsx b/frontend-modern/src/features/alerts/AlertPushDestinationsSection.tsx index a8fec92e3..66ecbe8b1 100644 --- a/frontend-modern/src/features/alerts/AlertPushDestinationsSection.tsx +++ b/frontend-modern/src/features/alerts/AlertPushDestinationsSection.tsx @@ -1,11 +1,6 @@ import { Show } from 'solid-js'; -import Smartphone from 'lucide-solid/icons/smartphone'; import { SettingsPanel } from '@/components/shared/SettingsPanel'; -import { FeatureGateSection } from '@/components/shared/FeatureGateSection'; -import type { UpgradeDestination } from '@/utils/upgradeNavigation'; import { - ALERT_DESTINATIONS_PUSH_GATE_MESSAGE, - ALERT_DESTINATIONS_PUSH_GATE_TITLE, ALERT_DESTINATIONS_PUSH_MINIMUM_SEVERITY_HELP, ALERT_DESTINATIONS_PUSH_PANEL_DESCRIPTION, ALERT_DESTINATIONS_PUSH_PANEL_TITLE, @@ -16,31 +11,20 @@ import { DestinationSeveritySelect } from '@/components/Alerts/DestinationSeveri interface AlertPushDestinationsSectionProps { relayLicensed: boolean; - showUpgradePrompts: boolean; - upgradeDestination: UpgradeDestination; minimumSeverity?: 'all' | 'critical'; onMinimumSeverityChange?: (value: 'all' | 'critical') => void; } +// Pulse Mobile is being retired on 31 March 2027 and is no longer sold, so the +// panel only appears on instances that already have it; there is no upsell. export function AlertPushDestinationsSection(props: AlertPushDestinationsSectionProps) { return ( - - } - title={ALERT_DESTINATIONS_PUSH_GATE_TITLE} - body={ALERT_DESTINATIONS_PUSH_GATE_MESSAGE} - upgradeDestination={props.upgradeDestination} - showUpgradePrompts={props.showUpgradePrompts} - /> - } + +

{ALERT_DESTINATIONS_PUSH_READY_MESSAGE}

@@ -60,7 +44,7 @@ export function AlertPushDestinationsSection(props: AlertPushDestinationsSection {ALERT_DESTINATIONS_PUSH_SETUP_LINK_LABEL} →
-
-
+ + ); } diff --git a/frontend-modern/src/features/alerts/__tests__/useAlertDestinationsState.test.tsx b/frontend-modern/src/features/alerts/__tests__/useAlertDestinationsState.test.tsx index 5dceee863..77faffdf1 100644 --- a/frontend-modern/src/features/alerts/__tests__/useAlertDestinationsState.test.tsx +++ b/frontend-modern/src/features/alerts/__tests__/useAlertDestinationsState.test.tsx @@ -188,4 +188,29 @@ describe('useAlertDestinationsState', () => { expect(result.deadManPingUrl()).toBe(''); expect(result.pushMinimumSeverity()).toBe('all'); }); + + it('leaves Pulse Mobile push settings alone on instances without it', async () => { + // Pulse Mobile is being retired on 31 March 2027 and is no longer sold, so + // Community installs never load or write relay push settings. + const [activeTab] = createSignal<'overview' | 'destinations'>('destinations'); + vi.mocked(hasFeature).mockReturnValue(false); + vi.mocked(NotificationsAPI.getEmailConfig).mockResolvedValue({ enabled: false } as any); + vi.mocked(NotificationsAPI.getAppriseConfig).mockResolvedValue({ enabled: false } as any); + vi.mocked(NotificationsAPI.getWebhooks).mockResolvedValue([]); + vi.mocked(NotificationsAPI.updateEmailConfig).mockResolvedValue(undefined as any); + vi.mocked(NotificationsAPI.updateAppriseConfig).mockResolvedValue({ enabled: false } as any); + vi.mocked(AlertsAPI.getDeadManConfig).mockResolvedValue({ pingUrl: '', configured: false }); + vi.mocked(AlertsAPI.updateDeadManConfig).mockResolvedValue({ + success: true, + configured: false, + }); + + const { result } = renderHook(() => useAlertDestinationsState({ activeTab })); + + await result.loadDestinations(); + await result.saveDestinations(); + + expect(RelayAPI.getConfig).not.toHaveBeenCalled(); + expect(RelayAPI.updateConfig).not.toHaveBeenCalled(); + }); }); diff --git a/frontend-modern/src/features/alerts/tabs/DestinationsTab.tsx b/frontend-modern/src/features/alerts/tabs/DestinationsTab.tsx index 3bc361396..3f2e0f771 100644 --- a/frontend-modern/src/features/alerts/tabs/DestinationsTab.tsx +++ b/frontend-modern/src/features/alerts/tabs/DestinationsTab.tsx @@ -1,8 +1,6 @@ import { AlertQueueActionFeedback } from '../AlertQueueActionFeedback'; import { createMemo, createSignal, Show } from 'solid-js'; import { hasFeature } from '@/stores/license'; -import { getUpgradeActionDestination } from '@/stores/licenseCommercial'; -import { presentationPolicyHidesUpgradePrompts } from '@/stores/sessionPresentationPolicy'; import { useAlertsActivation } from '@/stores/alertsActivation'; import { logger } from '@/utils/logger'; import type { AlertDestinationsDeliveryPausedReason } from '@/utils/alertDestinationsPresentation'; @@ -147,8 +145,6 @@ export function DestinationsTab(props: DestinationsTabProps) { { props.setPushMinimumSeverity(minimumSeverity); diff --git a/frontend-modern/src/i18n/messages.de.ts b/frontend-modern/src/i18n/messages.de.ts index 8433dfe98..92aad8c3f 100644 --- a/frontend-modern/src/i18n/messages.de.ts +++ b/frontend-modern/src/i18n/messages.de.ts @@ -488,8 +488,8 @@ export const DE_MESSAGE_OVERRIDES = { 'Verwalten Sie Backup-/Snapshot-Abfragen sowie Export- und Importablaeufe der Konfiguration.', 'settings.header.systemRecovery.title': 'Wiederherstellung', 'settings.header.systemRelay.description': - 'Behalten Sie Ihre Systeme von ueberall im Blick und erhalten Sie Alarm-Push-Benachrichtigungen ueber die Pulse-Mobile-App — ohne Portfreigaben oder VPN.', - 'settings.header.systemRelay.title': 'Remote-Zugriff', + 'Verbinden Sie gekoppelte Pulse-Mobile-Geraete mit dieser Instanz. Pulse Mobile wird am 31. Maerz 2027 eingestellt. Gekoppelte Telefone funktionieren bis dahin weiter.', + 'settings.header.systemRelay.title': 'Pulse Mobile', 'settings.header.systemUpdates.description': 'Suchen Sie nach neuen Pulse-Versionen, waehlen Sie einen Update-Kanal und verwalten Sie automatische Updates. Agent-Updates bleiben unter Infrastruktur.', 'settings.header.systemUpdates.title': 'Updates', @@ -529,7 +529,7 @@ export const DE_MESSAGE_OVERRIDES = { 'settings.nav.item.plans': 'Plaene & Abrechnung', 'settings.nav.item.providerModels': 'Anbieter & Modelle', 'settings.nav.item.recovery': 'Wiederherstellung', - 'settings.nav.item.remoteAccess': 'Remote-Zugriff', + 'settings.nav.item.remoteAccess': 'Pulse Mobile', 'settings.nav.item.resourcePrivacy': 'Ressourcenschutz', 'settings.nav.item.roles': 'Rollen', 'settings.nav.item.securityOverview': 'Sicherheitsuebersicht', diff --git a/frontend-modern/src/i18n/messages.es.ts b/frontend-modern/src/i18n/messages.es.ts index e3ae87ed7..f8cb8c2f1 100644 --- a/frontend-modern/src/i18n/messages.es.ts +++ b/frontend-modern/src/i18n/messages.es.ts @@ -481,8 +481,8 @@ export const ES_MESSAGE_OVERRIDES = { 'Administra sondeos de copias/snapshots y flujos de exportación e importación de configuración.', 'settings.header.systemRecovery.title': 'Recuperación', 'settings.header.systemRelay.description': - 'Consulta tus sistemas y recibe notificaciones push de alertas desde cualquier lugar con la aplicación Pulse Mobile — sin abrir puertos ni VPN.', - 'settings.header.systemRelay.title': 'Acceso remoto', + 'Conecta los dispositivos Pulse Mobile emparejados con esta instancia. Pulse Mobile se retirará el 31 de marzo de 2027 y los teléfonos emparejados seguirán funcionando hasta entonces.', + 'settings.header.systemRelay.title': 'Pulse Mobile', 'settings.header.systemUpdates.description': 'Busca nuevas versiones de Pulse, elige un canal de actualización y administra las actualizaciones automáticas. Las actualizaciones de agentes permanecen en Infraestructura.', 'settings.header.systemUpdates.title': 'Actualizaciones', @@ -522,7 +522,7 @@ export const ES_MESSAGE_OVERRIDES = { 'settings.nav.item.plans': 'Planes y facturacion', 'settings.nav.item.providerModels': 'Proveedores y modelos', 'settings.nav.item.recovery': 'Recuperación', - 'settings.nav.item.remoteAccess': 'Acceso remoto', + 'settings.nav.item.remoteAccess': 'Pulse Mobile', 'settings.nav.item.resourcePrivacy': 'Privacidad de recursos', 'settings.nav.item.roles': 'Roles', 'settings.nav.item.securityOverview': 'Resumen de seguridad', diff --git a/frontend-modern/src/i18n/messages.ts b/frontend-modern/src/i18n/messages.ts index f6edd23d3..ab13ef5f4 100644 --- a/frontend-modern/src/i18n/messages.ts +++ b/frontend-modern/src/i18n/messages.ts @@ -471,8 +471,8 @@ export const EN_MESSAGES = { 'Manage backup/snapshot polling plus configuration export and import workflows.', 'settings.header.systemRecovery.title': 'Recovery', 'settings.header.systemRelay.description': - 'Check on your systems and get alert push notifications anywhere with the Pulse Mobile app — no port forwarding or VPN required.', - 'settings.header.systemRelay.title': 'Remote Access', + 'Connect paired Pulse Mobile devices to this instance. Pulse Mobile is being retired on 31 March 2027, and paired phones keep working until then.', + 'settings.header.systemRelay.title': 'Pulse Mobile', 'settings.header.systemUpdates.description': 'Check for new Pulse versions, choose an update channel, and manage automatic updates. Agent updates stay under Infrastructure.', 'settings.header.systemUpdates.title': 'Updates', @@ -512,7 +512,7 @@ export const EN_MESSAGES = { 'settings.nav.item.plans': 'Plans & Billing', 'settings.nav.item.providerModels': 'Provider & Models', 'settings.nav.item.recovery': 'Recovery', - 'settings.nav.item.remoteAccess': 'Remote Access', + 'settings.nav.item.remoteAccess': 'Pulse Mobile', 'settings.nav.item.resourcePrivacy': 'Resource Privacy', 'settings.nav.item.roles': 'Roles', 'settings.nav.item.securityOverview': 'Security Overview', diff --git a/frontend-modern/src/stores/__tests__/systemSettings.test.ts b/frontend-modern/src/stores/__tests__/systemSettings.test.ts index 13cdb09dd..92dd021eb 100644 --- a/frontend-modern/src/stores/__tests__/systemSettings.test.ts +++ b/frontend-modern/src/stores/__tests__/systemSettings.test.ts @@ -111,6 +111,8 @@ describe('systemSettings store', () => { 'utf8', ); + expect(privacyDoc).not.toContain('secure remote web access'); + expect(privacyDoc).toContain('Relay does not provide remote access to the web UI'); expect(privacyDoc).toContain('## Usage Data'); expect(privacyDoc).toContain('Pulse has one outbound usage-data scope'); expect(privacyDoc).toContain('Commercial activation and license-recovery runtime records'); diff --git a/frontend-modern/src/utils/__tests__/alertDestinationsPresentation.test.ts b/frontend-modern/src/utils/__tests__/alertDestinationsPresentation.test.ts index 441e4cf2e..839f7bc17 100644 --- a/frontend-modern/src/utils/__tests__/alertDestinationsPresentation.test.ts +++ b/frontend-modern/src/utils/__tests__/alertDestinationsPresentation.test.ts @@ -8,8 +8,6 @@ import { getAlertDestinationsDeliveryPausedDescription, getAlertDestinationsDeliveryPausedTitle, ALERT_DESTINATIONS_CONFIG_LOAD_ERROR, - ALERT_DESTINATIONS_PUSH_GATE_MESSAGE, - ALERT_DESTINATIONS_PUSH_GATE_TITLE, ALERT_DESTINATIONS_PUSH_PANEL_DESCRIPTION, ALERT_DESTINATIONS_PUSH_PANEL_TITLE, ALERT_DESTINATIONS_PUSH_READY_MESSAGE, @@ -146,16 +144,13 @@ describe('alertDestinationsPresentation', () => { 'Deliver alerts to your phone through the Pulse Mobile app.', ); expect(ALERT_DESTINATIONS_PUSH_READY_MESSAGE).toContain('Pulse Mobile devices paired'); - expect(ALERT_DESTINATIONS_PUSH_READY_MESSAGE).toContain('Remote Access settings'); + expect(ALERT_DESTINATIONS_PUSH_READY_MESSAGE).toContain('Pulse Mobile settings'); + expect(ALERT_DESTINATIONS_PUSH_READY_MESSAGE).toContain('retired on 31 March 2027'); expect(ALERT_DESTINATIONS_PUSH_MINIMUM_SEVERITY_HELP).toContain('Push copy stays private'); expect(ALERT_DESTINATIONS_PUSH_MINIMUM_SEVERITY_HELP).toContain( 'Open Pulse Mobile for current alert state', ); - expect(ALERT_DESTINATIONS_PUSH_SETUP_LINK_LABEL).toBe('Open Remote Access settings'); - expect(ALERT_DESTINATIONS_PUSH_GATE_TITLE).toBe('Get alerts on your phone'); - expect(ALERT_DESTINATIONS_PUSH_GATE_MESSAGE).toContain('Pulse Mobile app'); - expect(ALERT_DESTINATIONS_PUSH_GATE_MESSAGE).toContain('no port forwarding or VPN'); - expect(ALERT_DESTINATIONS_PUSH_GATE_MESSAGE).toContain('Available with Relay and Pro plans'); + expect(ALERT_DESTINATIONS_PUSH_SETUP_LINK_LABEL).toBe('Open Pulse Mobile settings'); }); it('distinguishes retained terminal failures from recoverable retry attempts', () => { diff --git a/frontend-modern/src/utils/__tests__/docsLinks.test.ts b/frontend-modern/src/utils/__tests__/docsLinks.test.ts index e7e4cded0..86f70f2d2 100644 --- a/frontend-modern/src/utils/__tests__/docsLinks.test.ts +++ b/frontend-modern/src/utils/__tests__/docsLinks.test.ts @@ -146,6 +146,18 @@ describe('docsLinks', () => { expect(apiReference).toContain('](#resource-maintenance-and-operator-state)'); }); + it('ships the Pulse Mobile retirement in the published API reference', () => { + const apiReference = readFileSync(path.join(repoRoot, 'docs', 'API.md'), 'utf8'); + const shippedAPIReference = readFileSync( + path.join(frontendRoot, 'public', 'docs', 'API.md'), + 'utf8', + ); + + expect(shippedAPIReference).toBe(apiReference); + expect(apiReference).toContain('## 📱 Relay / Pulse Mobile (retiring 31 March 2027)'); + expect(apiReference).not.toContain('Mobile Remote Access'); + }); + it('ships the per-alert snooze and resume API contract', () => { const apiReference = readFileSync(path.join(repoRoot, 'docs', 'API.md'), 'utf8'); const shippedAPIReference = readFileSync( diff --git a/frontend-modern/src/utils/__tests__/licensePresentation.branchcov0723pm.test.ts b/frontend-modern/src/utils/__tests__/licensePresentation.branchcov0723pm.test.ts index 9c3506e1b..d0c1989fd 100644 --- a/frontend-modern/src/utils/__tests__/licensePresentation.branchcov0723pm.test.ts +++ b/frontend-modern/src/utils/__tests__/licensePresentation.branchcov0723pm.test.ts @@ -75,7 +75,7 @@ describe('patrol-control counts - fractional completed count truncated to 0', () runtime: { build: 'pro', label: 'Pulse Pro runtime' }, }, displayableCapabilities: [ - 'Pulse Relay (Remote Access)', + 'Pulse Relay (Mobile Connection)', 'Patrol Applies Safe Fixes and Verifies the Result', ], patrolOperatorStatus: { @@ -105,7 +105,7 @@ describe('patrol-control counts - negative counts clamped to 0', () => { runtime: { build: 'pro', label: 'Pulse Pro runtime' }, }, displayableCapabilities: [ - 'Pulse Relay (Remote Access)', + 'Pulse Relay (Mobile Connection)', 'Patrol Applies Safe Fixes and Verifies the Result', ], patrolOperatorStatus: { @@ -139,7 +139,7 @@ describe('getFirstPartyPatrolControlCount - alias subtraction underflow', () => runtime: { build: 'pro', label: 'Pulse Pro runtime' }, }, displayableCapabilities: [ - 'Pulse Relay (Remote Access)', + 'Pulse Relay (Mobile Connection)', 'Patrol Applies Safe Fixes and Verifies the Result', ], patrolOperatorStatus: { @@ -175,7 +175,7 @@ describe('hasVerifiedPatrolOperatorOutcome - resolved count without explicit val runtime: { build: 'pro', label: 'Pulse Pro runtime' }, }, displayableCapabilities: [ - 'Pulse Relay (Remote Access)', + 'Pulse Relay (Mobile Connection)', 'Patrol Applies Safe Fixes and Verifies the Result', ], patrolOperatorStatus: { diff --git a/frontend-modern/src/utils/__tests__/licensePresentation.branchcov0725am.test.ts b/frontend-modern/src/utils/__tests__/licensePresentation.branchcov0725am.test.ts index c72478930..18d39a448 100644 --- a/frontend-modern/src/utils/__tests__/licensePresentation.branchcov0725am.test.ts +++ b/frontend-modern/src/utils/__tests__/licensePresentation.branchcov0725am.test.ts @@ -52,7 +52,7 @@ describe('getSelfHostedCurrentPlanPresentation - absent subscription_state hits runtime: { build: 'pro', label: 'Pulse Pro runtime' }, } as unknown as LicenseCommercialEntitlements, displayableCapabilities: [ - 'Pulse Relay (Remote Access)', + 'Pulse Relay (Mobile Connection)', 'Patrol Applies Safe Fixes and Verifies the Result', ], }); @@ -134,7 +134,7 @@ describe('getSelfHostedCurrentPlanPresentation - absent tier (line 663)', () => limits: [], upgrade_reasons: [], } as unknown as LicenseCommercialEntitlements, - displayableCapabilities: ['Pulse Relay (Remote Access)'], + displayableCapabilities: ['Pulse Relay (Mobile Connection)'], }); // (current.tier || '') → '' → getSelfHostedPlanLabel(undefined) === 'Unknown'. expect(result.title).toBe('Current plan: Unknown'); @@ -167,7 +167,7 @@ describe('getSelfHostedCurrentPlanPresentation - empty extras on a runtime-misma upgrade_reasons: [], runtime: { build: 'community', label: 'Pulse Community runtime' }, }, - displayableCapabilities: ['Pulse Relay (Remote Access)', 'Audit Logging'], + displayableCapabilities: ['Pulse Relay (Mobile Connection)', 'Audit Logging'], }); // includedExtras.length === 0 → the `: undefined` arm of the ternary. expect(result.includedExtrasLabel).toBeUndefined(); @@ -214,8 +214,8 @@ describe('getSelfHostedPlanStatusPresentation - absent capabilities (line 861)', } as unknown as LicenseCommercialEntitlements); expect(result).not.toBeNull(); // (entitlements.capabilities || []) takes the [] arm → empty capability Set - // → the remote-access item is fully 'missing'. - const remoteAccess = result?.items.find((i) => i.label.includes('Remote access')); + // → the Pulse Mobile item is fully 'missing'. + const remoteAccess = result?.items.find((i) => i.label.includes('Pulse Mobile')); expect(remoteAccess).toMatchObject({ state: 'missing', statusLabel: 'Needs attention', @@ -258,7 +258,7 @@ describe('getSelfHostedActivationSuccessPresentation - absent tier (line 966)', limits: [], upgrade_reasons: [], } as unknown as LicenseCommercialEntitlements, - displayableCapabilities: ['Pulse Relay (Remote Access)'], + displayableCapabilities: ['Pulse Relay (Mobile Connection)'], source: 'manual', }); expect(result).not.toBeNull(); diff --git a/frontend-modern/src/utils/__tests__/licensePresentation.coverage.test.ts b/frontend-modern/src/utils/__tests__/licensePresentation.coverage.test.ts index a9621a768..fee425424 100644 --- a/frontend-modern/src/utils/__tests__/licensePresentation.coverage.test.ts +++ b/frontend-modern/src/utils/__tests__/licensePresentation.coverage.test.ts @@ -99,9 +99,9 @@ describe('getFeatureMinTierLabel - edge cases', () => { expect(getFeatureMinTierLabel(undefined)).toBe('Pro'); }); - it('returns Relay for push_notifications and mobile_app', () => { - expect(getFeatureMinTierLabel('push_notifications')).toBe('Relay'); - expect(getFeatureMinTierLabel('mobile_app')).toBe('Relay'); + it('returns Pro for push_notifications and mobile_app now that Relay is retired', () => { + expect(getFeatureMinTierLabel('push_notifications')).toBe('Pro'); + expect(getFeatureMinTierLabel('mobile_app')).toBe('Pro'); }); }); @@ -430,14 +430,13 @@ describe('getBillingAdminTrialStatus - branch coverage', () => { * ------------------------------------------------------------------ */ describe('getSelfHostedPlanComparisonPresentation - edge cases', () => { - it('shows Relay and Pro cards for null / undefined entitlements', () => { + it('shows only the Pro card for null / undefined entitlements', () => { const result = getSelfHostedPlanComparisonPresentation({ entitlements: null }); - expect(result.cards).toHaveLength(2); - expect(result.cards[0].title).toBe('Relay plan'); - expect(result.cards[1].title).toBe('Pulse Pro plan'); + expect(result.cards).toHaveLength(1); + expect(result.cards[0].title).toBe('Pulse Pro plan'); }); - it('shows Relay and Pro cards for the community tier', () => { + it('shows only the Pro card for the community tier', () => { const result = getSelfHostedPlanComparisonPresentation({ entitlements: { tier: 'community', @@ -447,7 +446,7 @@ describe('getSelfHostedPlanComparisonPresentation - edge cases', () => { upgrade_reasons: [], }, }); - expect(result.cards.map((c) => c.title)).toEqual(['Relay plan', 'Pulse Pro plan']); + expect(result.cards.map((c) => c.title)).toEqual(['Pulse Pro plan']); }); it('returns no cards for an unrecognized tier', () => { @@ -523,7 +522,7 @@ describe('getSelfHostedCurrentPlanPresentation - trial branch', () => { upgrade_reasons: [], }, displayableCapabilities: [ - 'Pulse Relay (Remote Access)', + 'Pulse Relay (Mobile Connection)', 'Pulse Mobile Pairing', 'Push Notifications', ], @@ -562,7 +561,7 @@ describe('getSelfHostedCurrentPlanPresentation - is_lifetime branch', () => { runtime: { build: 'pro', label: 'Pulse Pro runtime' }, }, displayableCapabilities: [ - 'Pulse Relay (Remote Access)', + 'Pulse Relay (Mobile Connection)', 'Patrol Applies Safe Fixes and Verifies the Result', ], }); @@ -586,7 +585,7 @@ describe('getSelfHostedCurrentPlanPresentation - grace + grandfathered branch', runtime: { build: 'pro', label: 'Pulse Pro runtime' }, }, displayableCapabilities: [ - 'Pulse Relay (Remote Access)', + 'Pulse Relay (Mobile Connection)', 'Patrol Applies Safe Fixes and Verifies the Result', ], }); @@ -604,7 +603,7 @@ describe('getSelfHostedCurrentPlanPresentation - fallback branch', () => { limits: [], upgrade_reasons: [], }, - displayableCapabilities: ['Pulse Relay (Remote Access)'], + displayableCapabilities: ['Pulse Relay (Mobile Connection)'], }); expect(result.title).toBe('Current plan: Pulse Pro'); expect(result.body).toBe( @@ -626,7 +625,7 @@ describe('getSelfHostedCurrentPlanPresentation - getPatrolControlAction branches upgrade_reasons: [], runtime: { build: 'pro', label: 'Pulse Pro runtime' }, }, - displayableCapabilities: ['Pulse Relay (Remote Access)'], + displayableCapabilities: ['Pulse Relay (Mobile Connection)'], }); expect(result.patrolControlAction).toBeUndefined(); }); @@ -642,7 +641,7 @@ describe('getSelfHostedCurrentPlanPresentation - getPatrolControlAction branches runtime: { build: 'pro', label: 'Pulse Pro runtime' }, }, displayableCapabilities: [ - 'Pulse Relay (Remote Access)', + 'Pulse Relay (Mobile Connection)', 'Patrol Applies Safe Fixes and Verifies the Result', ], patrolOperatorStatus: { @@ -801,7 +800,7 @@ describe('getSelfHostedActivationSuccessPresentation - purchase + runtime mismat runtime: { build: 'community', label: 'Pulse Community runtime' }, }, displayableCapabilities: [ - 'Pulse Relay (Remote Access)', + 'Pulse Relay (Mobile Connection)', 'Patrol Applies Safe Fixes and Verifies the Result', ], source: 'purchase', @@ -828,7 +827,7 @@ describe('getSelfHostedActivationSuccessPresentation - purchase without patrol a upgrade_reasons: [], }, displayableCapabilities: [ - 'Pulse Relay (Remote Access)', + 'Pulse Relay (Mobile Connection)', 'Pulse Mobile Pairing', 'Push Notifications', ], @@ -854,7 +853,7 @@ describe('getSelfHostedActivationSuccessPresentation - manual + runtime mismatch limits: [], upgrade_reasons: [], }, - displayableCapabilities: ['Pulse Relay (Remote Access)', 'Audit Logging'], + displayableCapabilities: ['Pulse Relay (Mobile Connection)', 'Audit Logging'], source: 'manual', }); expect(result).not.toBeNull(); diff --git a/frontend-modern/src/utils/__tests__/licensePresentation.test.ts b/frontend-modern/src/utils/__tests__/licensePresentation.test.ts index 149359177..b8961ae8a 100644 --- a/frontend-modern/src/utils/__tests__/licensePresentation.test.ts +++ b/frontend-modern/src/utils/__tests__/licensePresentation.test.ts @@ -111,7 +111,7 @@ describe('licensePresentation', () => { expect(getLicenseFeatureLabel('ai_patrol')).toBe('Pulse Patrol'); expect(getLicenseFeatureLabel('mobile_app')).toBe('Pulse Mobile Pairing'); expect(getLicenseFeatureLabel('update_alerts')).toBe('Update Alerts'); - expect(getLicenseFeatureLabel('relay')).toBe('Pulse Relay (Remote Access)'); + expect(getLicenseFeatureLabel('relay')).toBe('Pulse Relay (Mobile Connection)'); expect(getLicenseFeatureLabel('custom_feature')).toBe('Custom Feature'); }); @@ -125,7 +125,7 @@ describe('licensePresentation', () => { }); it('returns minimum tier labels for gated features', () => { - expect(getFeatureMinTierLabel('relay')).toBe('Relay'); + expect(getFeatureMinTierLabel('relay')).toBe('Pro'); expect(getFeatureMinTierLabel('multi_tenant')).toBe('MSP'); expect(getFeatureMinTierLabel('unknown_feature')).toBe('Pro'); }); @@ -333,13 +333,13 @@ describe('licensePresentation', () => { runtime: { build: 'pro', label: 'Pulse Pro runtime' }, }, displayableCapabilities: [ - 'Pulse Relay (Remote Access)', + 'Pulse Relay (Mobile Connection)', 'Patrol Applies Safe Fixes and Verifies the Result', ], }), ).toMatchObject({ title: 'Current plan: Pulse Pro', - body: 'Pulse Pro is active on this instance. It includes Relay connectivity, Pulse Mobile pairing, push notifications, Patrol modes (Ask first, Safe auto-fix, Autopilot), 90-day metric history, RBAC, audit logging, reporting, and agent profiles.', + body: 'Pulse Pro is active on this instance. It includes Patrol modes (Ask first, Safe auto-fix, Autopilot), 90-day metric history, RBAC, audit logging, reporting, agent profiles, and Pulse Mobile pairing until 31 March 2027.', supplementalBadges: [], supplementalSummary: '', }); @@ -355,14 +355,14 @@ describe('licensePresentation', () => { runtime: { build: 'pro', label: 'Pulse Pro runtime' }, }, displayableCapabilities: [ - 'Pulse Relay (Remote Access)', + 'Pulse Relay (Mobile Connection)', 'Pulse Mobile Pairing', 'Patrol Applies Safe Fixes and Verifies the Result', ], }), ).toEqual({ title: 'Current plan: Pulse Pro', - body: 'Pulse Pro is active on this instance. It includes Relay connectivity, Pulse Mobile pairing, push notifications, Patrol modes (Ask first, Safe auto-fix, Autopilot), 90-day metric history, RBAC, audit logging, reporting, and agent profiles.', + body: 'Pulse Pro is active on this instance. It includes Patrol modes (Ask first, Safe auto-fix, Autopilot), 90-day metric history, RBAC, audit logging, reporting, agent profiles, and Pulse Mobile pairing until 31 March 2027.', unlockedFeaturesLabel: 'Primary capabilities', unlockedFeatures: [ 'Patrol Investigates Issues and Explains the Root Cause', @@ -397,7 +397,7 @@ describe('licensePresentation', () => { runtime: { build: 'pro', label: 'Pulse Pro runtime' }, }, displayableCapabilities: [ - 'Pulse Relay (Remote Access)', + 'Pulse Relay (Mobile Connection)', 'Pulse Mobile Pairing', 'Patrol Applies Safe Fixes and Verifies the Result', ], @@ -428,7 +428,7 @@ describe('licensePresentation', () => { runtime: { build: 'pro', label: 'Pulse Pro runtime' }, }, displayableCapabilities: [ - 'Pulse Relay (Remote Access)', + 'Pulse Relay (Mobile Connection)', 'Pulse Mobile Pairing', 'Patrol Applies Safe Fixes and Verifies the Result', ], @@ -457,7 +457,7 @@ describe('licensePresentation', () => { runtime: { build: 'pro', label: 'Pulse Pro runtime' }, }, displayableCapabilities: [ - 'Pulse Relay (Remote Access)', + 'Pulse Relay (Mobile Connection)', 'Pulse Mobile Pairing', 'Patrol Applies Safe Fixes and Verifies the Result', ], @@ -484,7 +484,7 @@ describe('licensePresentation', () => { runtime: { build: 'pro', label: 'Pulse Pro runtime' }, }, displayableCapabilities: [ - 'Pulse Relay (Remote Access)', + 'Pulse Relay (Mobile Connection)', 'Pulse Mobile Pairing', 'Patrol Applies Safe Fixes and Verifies the Result', ], @@ -511,7 +511,7 @@ describe('licensePresentation', () => { runtime: { build: 'pro', label: 'Pulse Pro runtime' }, }, displayableCapabilities: [ - 'Pulse Relay (Remote Access)', + 'Pulse Relay (Mobile Connection)', 'Pulse Mobile Pairing', 'Patrol Applies Safe Fixes and Verifies the Result', ], @@ -543,7 +543,7 @@ describe('licensePresentation', () => { runtime: { build: 'pro', label: 'Pulse Pro runtime' }, }, displayableCapabilities: [ - 'Pulse Relay (Remote Access)', + 'Pulse Relay (Mobile Connection)', 'Pulse Mobile Pairing', 'Patrol Applies Safe Fixes and Verifies the Result', ], @@ -572,7 +572,7 @@ describe('licensePresentation', () => { runtime: { build: 'pro', label: 'Pulse Pro runtime' }, }, displayableCapabilities: [ - 'Pulse Relay (Remote Access)', + 'Pulse Relay (Mobile Connection)', 'Pulse Mobile Pairing', 'Patrol Applies Safe Fixes and Verifies the Result', ], @@ -601,7 +601,7 @@ describe('licensePresentation', () => { runtime: { build: 'pro', label: 'Pulse Pro runtime' }, }, displayableCapabilities: [ - 'Pulse Relay (Remote Access)', + 'Pulse Relay (Mobile Connection)', 'Pulse Mobile Pairing', 'Patrol Applies Safe Fixes and Verifies the Result', ], @@ -631,13 +631,13 @@ describe('licensePresentation', () => { runtime: { build: 'pro', label: 'Pulse Pro runtime' }, }, displayableCapabilities: [ - 'Pulse Relay (Remote Access)', + 'Pulse Relay (Mobile Connection)', 'Patrol Applies Safe Fixes and Verifies the Result', ], }), ).toMatchObject({ title: 'Current plan: Legacy Pulse Pro+', - body: 'Legacy Pulse Pro+ is active on this instance. It includes Relay connectivity, Pulse Mobile pairing, push notifications, Patrol modes (Ask first, Safe auto-fix, Autopilot), 90-day metric history, RBAC, audit logging, reporting, and agent profiles.', + body: 'Legacy Pulse Pro+ is active on this instance. It includes Patrol modes (Ask first, Safe auto-fix, Autopilot), 90-day metric history, RBAC, audit logging, reporting, agent profiles, and Pulse Mobile pairing until 31 March 2027.', unlockedFeaturesLabel: 'Primary capabilities', unlockedFeatures: [ 'Patrol Investigates Issues and Explains the Root Cause', @@ -657,11 +657,11 @@ describe('licensePresentation', () => { upgrade_reasons: [], runtime: { build: 'pro', label: 'Pulse Pro runtime' }, }, - displayableCapabilities: ['Pulse Relay (Remote Access)'], + displayableCapabilities: ['Pulse Relay (Mobile Connection)'], }), ).toEqual({ title: 'Current plan: Pulse Pro', - body: 'Pulse Pro is active on this instance. It includes Relay connectivity, Pulse Mobile pairing, push notifications, Patrol modes (Ask first, Safe auto-fix, Autopilot), 90-day metric history, RBAC, audit logging, reporting, and agent profiles.', + body: 'Pulse Pro is active on this instance. It includes Patrol modes (Ask first, Safe auto-fix, Autopilot), 90-day metric history, RBAC, audit logging, reporting, agent profiles, and Pulse Mobile pairing until 31 March 2027.', unlockedFeaturesLabel: 'Primary capabilities', unlockedFeatures: [ 'Patrol Investigates Issues and Explains the Root Cause', @@ -700,7 +700,7 @@ describe('licensePresentation', () => { expect( getSelfHostedCurrentPlanPresentation({ entitlements, - displayableCapabilities: ['Pulse Relay (Remote Access)', 'Audit Logging'], + displayableCapabilities: ['Pulse Relay (Mobile Connection)', 'Audit Logging'], }), ).toMatchObject({ title: 'Current plan: Pulse Pro', @@ -720,7 +720,7 @@ describe('licensePresentation', () => { expect( getSelfHostedActivationSuccessPresentation({ entitlements, - displayableCapabilities: ['Pulse Relay (Remote Access)', 'Audit Logging'], + displayableCapabilities: ['Pulse Relay (Mobile Connection)', 'Audit Logging'], source: 'manual', }), ).toMatchObject({ @@ -747,7 +747,7 @@ describe('licensePresentation', () => { expect( getSelfHostedCurrentPlanPresentation({ entitlements, - displayableCapabilities: ['Pulse Relay (Remote Access)', 'Audit Logging'], + displayableCapabilities: ['Pulse Relay (Mobile Connection)', 'Audit Logging'], }), ).toMatchObject({ title: 'Current plan: Pulse Pro', @@ -767,7 +767,7 @@ describe('licensePresentation', () => { expect( getSelfHostedActivationSuccessPresentation({ entitlements, - displayableCapabilities: ['Pulse Relay (Remote Access)', 'Audit Logging'], + displayableCapabilities: ['Pulse Relay (Mobile Connection)', 'Audit Logging'], source: 'manual', }), ).toMatchObject({ @@ -791,7 +791,7 @@ describe('licensePresentation', () => { upgrade_reasons: [], }, displayableCapabilities: [ - 'Pulse Relay (Remote Access)', + 'Pulse Relay (Mobile Connection)', 'Pulse Mobile Pairing', 'Push Notifications', ], @@ -809,7 +809,7 @@ describe('licensePresentation', () => { runtime: { build: 'pro', label: 'Pulse Pro runtime' }, }, displayableCapabilities: [ - 'Pulse Relay (Remote Access)', + 'Pulse Relay (Mobile Connection)', 'Patrol Applies Safe Fixes and Verifies the Result', ], }).body, @@ -834,7 +834,7 @@ describe('licensePresentation', () => { runtime: { build: 'pro', label: 'Pulse Pro runtime' }, }, displayableCapabilities: [ - 'Pulse Relay (Remote Access)', + 'Pulse Relay (Mobile Connection)', 'Patrol Applies Safe Fixes and Verifies the Result', ], }).patrolControlAction, @@ -878,7 +878,7 @@ describe('licensePresentation', () => { upgrade_reasons: [], }, displayableCapabilities: [ - 'Pulse Relay (Remote Access)', + 'Pulse Relay (Mobile Connection)', 'Pulse Mobile Pairing', 'Push Notifications', 'Patrol Applies Safe Fixes and Verifies the Result', @@ -887,7 +887,7 @@ describe('licensePresentation', () => { ).toBeUndefined(); }); - it('builds restrained higher-tier comparison cards for Community and Relay only', () => { + it('offers Community only the Pro card and never offers the retired Relay plan', () => { expect( getSelfHostedPlanComparisonPresentation({ entitlements: { @@ -900,23 +900,10 @@ describe('licensePresentation', () => { }), ).toEqual({ cards: [ - { - title: 'Relay plan', - body: 'Remote web access, Pulse Mobile pairing, push notifications, and 14-day metric history.', - highlights: [ - 'Secure access when you are away from your network', - 'Remote web access via Relay', - 'Pulse Mobile pairing', - 'Push notifications', - 'No inbound ports required', - '14-day metric history', - ], - }, { title: 'Pulse Pro plan', - body: 'Patrol investigates issues, applies safe fixes, and verifies the result. Relay connectivity is included, plus 90-day metric history and team controls.', + body: 'Patrol investigates issues, applies safe fixes, and verifies the result, with 90-day metric history and team controls.', highlights: [ - 'Relay connectivity, Pulse Mobile pairing, and push notifications included', 'Patrol modes: Ask first, Safe auto-fix, or Autopilot', 'Patrol investigates issues and explains the root cause', 'Patrol applies safe fixes and verifies the result', @@ -937,22 +924,7 @@ describe('licensePresentation', () => { upgrade_reasons: [], }, }), - ).toEqual({ - cards: [ - { - title: 'Pulse Pro plan', - body: 'Patrol investigates issues, applies safe fixes, and verifies the result. Relay connectivity is included, plus 90-day metric history and team controls.', - highlights: [ - 'Relay connectivity, Pulse Mobile pairing, and push notifications included', - 'Patrol modes: Ask first, Safe auto-fix, or Autopilot', - 'Patrol investigates issues and explains the root cause', - 'Patrol applies safe fixes and verifies the result', - '90-day metric history', - 'Team controls: RBAC, audit logging, reporting, and agent profiles', - ], - }, - ], - }); + ).toEqual({ cards: [] }); expect( getSelfHostedPlanComparisonPresentation({ @@ -982,11 +954,11 @@ describe('licensePresentation', () => { body: 'These checks show the capabilities this instance can use right now, based on its entitlement and runtime payloads.', items: [ { - label: 'Remote access, pairing, and push', + label: 'Pulse Mobile pairing and push', statusLabel: 'Active', state: 'active', detail: - 'Relay, Pulse Mobile pairing, and push notifications are available on this instance.', + 'Pulse Mobile pairing and push notifications are available on this instance until Pulse Mobile is retired on 31 March 2027.', }, { label: '14-day metric history', @@ -1024,7 +996,7 @@ describe('licensePresentation', () => { body: 'Open this only when a Pro capability looks unavailable. Normal setup is choosing Patrol mode.', items: [ { label: 'Pulse Pro runtime', statusLabel: 'Active' }, - { label: 'Remote access, pairing, and push', statusLabel: 'Active' }, + { label: 'Pulse Mobile pairing and push', statusLabel: 'Active' }, { label: '90-day metric history', statusLabel: 'Active' }, { label: 'Patrol investigation and remediation', statusLabel: 'Active' }, { label: 'Team controls', statusLabel: 'Active' }, @@ -1042,7 +1014,7 @@ describe('licensePresentation', () => { }); expect(partialProStatus?.items.map((item) => [item.label, item.statusLabel])).toEqual([ ['Pulse Pro runtime', 'Active'], - ['Remote access, pairing, and push', 'Partial'], + ['Pulse Mobile pairing and push', 'Partial'], ['90-day metric history', 'Partial'], ['Patrol investigation and remediation', 'Partial'], ['Team controls', 'Needs attention'], @@ -1099,7 +1071,7 @@ describe('licensePresentation', () => { const expectedCapabilityItems = [ ['Pulse Pro runtime', 'Active'], - ['Remote access, pairing, and push', 'Active'], + ['Pulse Mobile pairing and push', 'Active'], ['90-day metric history', 'Active'], ['Patrol investigation and remediation', 'Active'], ['Team controls', 'Active'], @@ -1242,7 +1214,7 @@ describe('licensePresentation', () => { runtime: { build: 'pro', label: 'Pulse Pro runtime' }, }, displayableCapabilities: [ - 'Pulse Relay (Remote Access)', + 'Pulse Relay (Mobile Connection)', 'Pulse Mobile Pairing', 'Push Notifications', 'Patrol Applies Safe Fixes and Verifies the Result', @@ -1279,7 +1251,7 @@ describe('licensePresentation', () => { upgrade_reasons: [], }, displayableCapabilities: [ - 'Pulse Relay (Remote Access)', + 'Pulse Relay (Mobile Connection)', 'Pulse Mobile Pairing', 'Push Notifications', ], @@ -1291,7 +1263,7 @@ describe('licensePresentation', () => { body: 'The license key was accepted and this instance is now running Relay.', highlightsLabel: 'Available now on this instance', highlights: [ - 'Pulse Relay (Remote Access)', + 'Pulse Relay (Mobile Connection)', 'Pulse Mobile Pairing', 'Push Notifications', '14-day metric history', diff --git a/frontend-modern/src/utils/__tests__/relayPresentation.test.ts b/frontend-modern/src/utils/__tests__/relayPresentation.test.ts index 412518d52..e71e67d92 100644 --- a/frontend-modern/src/utils/__tests__/relayPresentation.test.ts +++ b/frontend-modern/src/utils/__tests__/relayPresentation.test.ts @@ -101,17 +101,14 @@ describe('relayPresentation', () => { }); it('centralizes relay availability copy', () => { - // Value-first copy: every surface states the user job (push notifications - // through the Pulse Mobile app, no port forwarding/VPN), not transport - // mechanics. - expect(RELAY_SETTINGS_DESCRIPTION).toContain('Pulse Mobile app'); - expect(RELAY_SETTINGS_DESCRIPTION).toContain('push notifications'); - expect(RELAY_SETTINGS_DESCRIPTION).toContain('no port forwarding or VPN'); - expect(RELAY_LICENSE_REQUIRED_MESSAGE).toContain('Pulse Mobile app'); - expect(RELAY_LICENSE_REQUIRED_MESSAGE).toContain('push notifications'); - expect(RELAY_LICENSE_REQUIRED_MESSAGE).toContain('Available with Relay and Pro plans'); - expect(RELAY_LICENSE_REQUIRED_MESSAGE).not.toContain('Relay or Pro'); - expect(RELAY_PAIRING_AVAILABILITY_TITLE).toBe('Pair Pulse Mobile through Relay'); + // Pulse Mobile retires on 31 March 2027 and Relay is no longer sold, so the + // copy states the end date and never offers Relay as a plan. + expect(RELAY_SETTINGS_DESCRIPTION).toContain('Pulse Mobile'); + expect(RELAY_SETTINGS_DESCRIPTION).toContain('retired on 31 March 2027'); + expect(RELAY_LICENSE_REQUIRED_MESSAGE).toContain('retired on 31 March 2027'); + expect(RELAY_LICENSE_REQUIRED_MESSAGE).toContain('ntfy, Gotify, or Pushover'); + expect(RELAY_LICENSE_REQUIRED_MESSAGE).not.toContain('Available with Relay'); + expect(RELAY_PAIRING_AVAILABILITY_TITLE).toBe('Pair Pulse Mobile'); expect(RELAY_PAIRING_AVAILABILITY_MESSAGE).toContain('QR code'); expect(RELAY_PAIRING_AVAILABILITY_MESSAGE).toContain('deep link'); expect(RELAY_PAIRING_AVAILABILITY_MESSAGE).toContain('push notifications'); @@ -119,7 +116,7 @@ describe('relayPresentation', () => { expect(RELAY_ENABLE_HELP_TEXT).toContain('No inbound ports'); expect(RELAY_ACTIVATION_REQUIRED_LABEL).toBe('Activation required'); expect(RELAY_ACTIVATION_REQUIRED_MESSAGE).toContain('active Relay token'); - expect(RELAY_ACTIVATION_REQUIRED_MESSAGE).toContain('Relay-capable plan'); + expect(RELAY_ACTIVATION_REQUIRED_MESSAGE).not.toContain('Relay-capable plan'); }); it('does not retain retired Relay price or trial-era onboarding copy', () => { diff --git a/frontend-modern/src/utils/__tests__/selfHostedPlans.test.ts b/frontend-modern/src/utils/__tests__/selfHostedPlans.test.ts index c54bcb00c..1007428f0 100644 --- a/frontend-modern/src/utils/__tests__/selfHostedPlans.test.ts +++ b/frontend-modern/src/utils/__tests__/selfHostedPlans.test.ts @@ -50,7 +50,7 @@ describe('selfHostedPlans', () => { }, { key: 'relay', - name: 'Pulse Relay (Remote Access)', + name: 'Pulse Relay (Mobile Connection)', community: false, relay: true, pro: true, @@ -104,23 +104,30 @@ describe('selfHostedPlans', () => { ]); expect(SELF_HOSTED_PLAN_BY_TIER.community.includedExtras).toEqual([]); expect(SELF_HOSTED_PLAN_BY_TIER.relay.billingExtrasSummary).toBe( - 'Remote web access, pairing, and push', + 'Pulse Mobile pairing and push', ); expect(SELF_HOSTED_PLAN_BY_TIER.relay.entitlementSummary).toContain( - 'remote web access, Pulse Mobile pairing, push notifications, and 14-day metric history.', + 'Pulse Mobile pairing and push notifications until 31 March 2027, and 14-day metric history.', ); expect(SELF_HOSTED_PLAN_BY_TIER.relay.comparisonSummary).toBe( - 'Remote web access, Pulse Mobile pairing, push notifications, and 14-day metric history.', + 'Retired plan: Pulse Mobile pairing and push notifications until 31 March 2027, and 14-day metric history.', ); + // Relay never gave remote web access; no plan copy may claim it. + for (const plan of Object.values(SELF_HOSTED_PLAN_BY_TIER)) { + expect( + `${plan.entitlementSummary} ${plan.comparisonSummary} ${plan.highlights.join(' ')}`, + ).not.toMatch(/remote web access/i); + } expect(SELF_HOSTED_PLAN_BY_TIER.relay.entitlementHighlights).toEqual([ - 'Pulse Relay (Remote Access)', + 'Pulse Relay (Mobile Connection)', 'Pulse Mobile Pairing', 'Push Notifications', '14-day metric history', ]); expect(SELF_HOSTED_PLAN_BY_TIER.relay.includedExtras).toEqual([]); - expect(SELF_HOSTED_PLAN_BY_TIER.relay.highlights).toContain('Pulse Mobile pairing'); - expect(SELF_HOSTED_PLAN_BY_TIER.relay.highlights).toContain('No inbound ports required'); + expect(SELF_HOSTED_PLAN_BY_TIER.relay.highlights).toContain( + 'Pulse Mobile pairing until 31 March 2027', + ); expect(SELF_HOSTED_PLAN_BY_TIER.relay.highlights.join('\n')).not.toMatch( /yourlab\.pulserelay\.pro|custom\s+(?:url|subdomain|domain)/i, ); @@ -128,10 +135,10 @@ describe('selfHostedPlans', () => { 'Patrol modes, history, and team controls', ); expect(SELF_HOSTED_PLAN_BY_TIER.pro.entitlementSummary).toContain( - 'Relay connectivity, Pulse Mobile pairing, push notifications, Patrol modes (Ask first, Safe auto-fix, Autopilot), 90-day metric history, RBAC, audit logging, reporting, and agent profiles.', + 'Patrol modes (Ask first, Safe auto-fix, Autopilot), 90-day metric history, RBAC, audit logging, reporting, agent profiles, and Pulse Mobile pairing until 31 March 2027.', ); expect(SELF_HOSTED_PLAN_BY_TIER.pro.comparisonSummary).toBe( - 'Patrol investigates issues, applies safe fixes, and verifies the result. Relay connectivity is included, plus 90-day metric history and team controls.', + 'Patrol investigates issues, applies safe fixes, and verifies the result, with 90-day metric history and team controls.', ); expect(SELF_HOSTED_PLAN_BY_TIER.pro.comparisonSummary).not.toContain('how much control'); expect(SELF_HOSTED_PLAN_BY_TIER.pro.entitlementHighlights).toEqual([ @@ -172,7 +179,7 @@ describe('selfHostedPlans', () => { getSelfHostedPlanEntitlementSummary('pro'), ); expect(getSelfHostedPlanEntitlementSummary('pro', 'Legacy Pulse Pro+')).toBe( - 'Legacy Pulse Pro+ is active on this instance. It includes Relay connectivity, Pulse Mobile pairing, push notifications, Patrol modes (Ask first, Safe auto-fix, Autopilot), 90-day metric history, RBAC, audit logging, reporting, and agent profiles.', + 'Legacy Pulse Pro+ is active on this instance. It includes Patrol modes (Ask first, Safe auto-fix, Autopilot), 90-day metric history, RBAC, audit logging, reporting, agent profiles, and Pulse Mobile pairing until 31 March 2027.', ); expect(SELF_HOSTED_PLAN_BY_TIER.relay.entitlementHighlights).toEqual( @@ -215,7 +222,7 @@ describe('selfHostedPlans', () => { { tier: 'relay', historyDays: 14, - extras: 'Remote web access, pairing, and push', + extras: 'Pulse Mobile pairing and push', }, { tier: 'pro', historyDays: 90, extras: 'Patrol modes, history, and team controls' }, ]); diff --git a/frontend-modern/src/utils/alertDestinationsPresentation.ts b/frontend-modern/src/utils/alertDestinationsPresentation.ts index 0498fe6c0..92ad797dc 100644 --- a/frontend-modern/src/utils/alertDestinationsPresentation.ts +++ b/frontend-modern/src/utils/alertDestinationsPresentation.ts @@ -76,13 +76,10 @@ export const ALERT_DESTINATIONS_PUSH_PANEL_TITLE = 'Mobile push notifications'; export const ALERT_DESTINATIONS_PUSH_PANEL_DESCRIPTION = 'Deliver alerts to your phone through the Pulse Mobile app.'; export const ALERT_DESTINATIONS_PUSH_READY_MESSAGE = - 'Alerts are pushed to Pulse Mobile devices paired with this instance. Manage pairing and connectivity in Remote Access settings.'; + 'Alerts are pushed to Pulse Mobile devices paired with this instance until Pulse Mobile is retired on 31 March 2027. Manage pairing in Pulse Mobile settings.'; export const ALERT_DESTINATIONS_PUSH_MINIMUM_SEVERITY_HELP = 'Choose whether phones receive warning and critical pushes or critical pushes only. Push copy stays private. Open Pulse Mobile for current alert state.'; -export const ALERT_DESTINATIONS_PUSH_SETUP_LINK_LABEL = 'Open Remote Access settings'; -export const ALERT_DESTINATIONS_PUSH_GATE_TITLE = 'Get alerts on your phone'; -export const ALERT_DESTINATIONS_PUSH_GATE_MESSAGE = - 'Pair the Pulse Mobile app to receive alert push notifications and check on your systems from anywhere — no port forwarding or VPN required. Available with Relay and Pro plans.'; +export const ALERT_DESTINATIONS_PUSH_SETUP_LINK_LABEL = 'Open Pulse Mobile settings'; export const ALERT_DESTINATIONS_APPRISE_TEST_SUCCESS = 'Test Apprise notification sent.'; export const ALERT_DESTINATIONS_APPRISE_TEST_FAILURE = 'Unable to send the test notification.'; export const ALERT_DESTINATIONS_DELIVERY_DEGRADED_TITLE = 'Notification delivery needs attention'; diff --git a/frontend-modern/src/utils/licensePresentation.ts b/frontend-modern/src/utils/licensePresentation.ts index f892631f5..12ab51e91 100644 --- a/frontend-modern/src/utils/licensePresentation.ts +++ b/frontend-modern/src/utils/licensePresentation.ts @@ -39,10 +39,12 @@ const LEGACY_PLAN_VERSION_LABELS: Record = { pro_plus: 'Legacy Pro Plus', }; +// Relay was retired on 29 September 2026; Pro is the lowest plan that still +// carries these capabilities until Pulse Mobile retires on 31 March 2027. const FEATURE_MIN_TIER_LABELS: Record = { - relay: 'Relay', - mobile_app: 'Relay', - push_notifications: 'Relay', + relay: 'Pro', + mobile_app: 'Pro', + push_notifications: 'Pro', multi_tenant: 'MSP', }; @@ -614,12 +616,10 @@ export const getSelfHostedPlanComparisonPresentation = ({ entitlements?: LicenseCommercialEntitlements | null; }): SelfHostedPlanComparisonPresentation => { const normalizedTier = (entitlements?.tier || '').trim().toLowerCase(); + // Relay was retired on 29 September 2026: it is never offered, and existing + // Relay subscribers already carry Pro entitlements, so they see no upsell. const comparisonTiers = - normalizedTier === 'relay' - ? ['pro'] - : normalizedTier === 'free' || normalizedTier === 'community' || !normalizedTier - ? ['relay', 'pro'] - : []; + normalizedTier === 'free' || normalizedTier === 'community' || !normalizedTier ? ['pro'] : []; return { cards: comparisonTiers @@ -876,14 +876,14 @@ export const getSelfHostedPlanStatusPresentation = ( items.push( buildCapabilityStatusItem({ capabilities, - label: 'Remote access, pairing, and push', + label: 'Pulse Mobile pairing and push', requiredCapabilities: ['relay', 'mobile_app', 'push_notifications'], activeDetail: - 'Relay, Pulse Mobile pairing, and push notifications are available on this instance.', + 'Pulse Mobile pairing and push notifications are available on this instance until Pulse Mobile is retired on 31 March 2027.', partialDetail: - 'Some remote-access capabilities are available. Refresh the plan or open recovery if remote access, Pulse Mobile pairing, or push stays unavailable.', + 'Some Pulse Mobile capabilities are available. Refresh the plan or open recovery if Pulse Mobile pairing or push stays unavailable.', missingDetail: - 'Remote access, Pulse Mobile pairing, or push notifications are not available yet. Refresh the plan or open recovery before relying on Relay.', + 'Pulse Mobile pairing or push notifications are not available yet. Refresh the plan or open recovery before relying on Pulse Mobile.', }), ); diff --git a/frontend-modern/src/utils/relayPresentation.ts b/frontend-modern/src/utils/relayPresentation.ts index 6af39f558..4131602f2 100644 --- a/frontend-modern/src/utils/relayPresentation.ts +++ b/frontend-modern/src/utils/relayPresentation.ts @@ -30,19 +30,19 @@ export const RELAY_QR_IMAGE_CLASS = 'rounded-md border border-border p-2'; export const RELAY_DIAGNOSTICS_WRAP_CLASS = 'space-y-2'; export const RELAY_DIAGNOSTICS_TITLE_CLASS = 'text-xs font-semibold text-base-content'; export const RELAY_SETTINGS_DESCRIPTION = - 'Check on your systems and get alert push notifications anywhere with the Pulse Mobile app — no port forwarding or VPN required.'; + 'Connect paired Pulse Mobile devices to this instance. Pulse Mobile is being retired on 31 March 2027, and paired phones keep working until then.'; export const RELAY_LICENSE_REQUIRED_MESSAGE = - 'See your infrastructure and get alert push notifications anywhere with the Pulse Mobile app — no port forwarding or VPN required. Available with Relay and Pro plans.'; -export const RELAY_PAIRING_AVAILABILITY_TITLE = 'Pair Pulse Mobile through Relay'; + 'Pulse Mobile is being retired on 31 March 2027 and is no longer part of new plans. For alerts on your phone, add an ntfy, Gotify, or Pushover destination under Alerts.'; +export const RELAY_PAIRING_AVAILABILITY_TITLE = 'Pair Pulse Mobile'; export const RELAY_PAIRING_AVAILABILITY_MESSAGE = - 'Install the Pulse Mobile app on your phone, then scan a QR code or open a deep link here to securely link it to this Pulse instance. Paired devices receive alert push notifications over an end-to-end encrypted connection.'; + 'Install the Pulse Mobile app on your phone, then scan a QR code or open a deep link here to securely link it to this Pulse instance. Paired devices receive alert push notifications over an end-to-end encrypted connection until Pulse Mobile is retired on 31 March 2027.'; export const RELAY_ENABLE_HELP_TEXT = 'Open a secure outbound connection so paired Pulse Mobile devices can reach this instance from anywhere. No inbound ports needed.'; export const RELAY_ACTIVATION_REQUIRED_LABEL = 'Activation required'; export const RELAY_ACTIVATION_REQUIRED_MESSAGE = - 'Remote Access is enabled, but this instance does not have an active Relay token. Activate a Relay-capable plan or turn Remote Access off before pairing mobile clients.'; + 'Pulse Mobile connections are on, but this instance does not have an active Relay token. Refresh your plan, or turn Pulse Mobile connections off before pairing phones.'; export const RELAY_PAIRING_APP_AVAILABILITY_TEXT = - 'Pulse Mobile is in early access for iOS and Android. To get the install links, enter your activation key on'; + 'Pulse Mobile stays available for iOS and Android until 31 March 2027. To get the install links, enter your activation key on'; export const RELAY_PAIRING_APP_DOWNLOAD_LINK_LABEL = 'your download page'; export function getRelayDiagnosticClass(severity: 'warning' | 'error'): string { diff --git a/frontend-modern/src/utils/selfHostedFeatureCatalog.generated.ts b/frontend-modern/src/utils/selfHostedFeatureCatalog.generated.ts index 14ebec60e..af39a245a 100644 --- a/frontend-modern/src/utils/selfHostedFeatureCatalog.generated.ts +++ b/frontend-modern/src/utils/selfHostedFeatureCatalog.generated.ts @@ -76,8 +76,8 @@ export const SELF_HOSTED_FEATURE_CATALOG: readonly GeneratedSelfHostedFeatureCat }, { key: 'relay', - displayName: 'Pulse Relay (Remote Access)', - comparisonName: 'Pulse Relay (Remote Access)', + displayName: 'Pulse Relay (Mobile Connection)', + comparisonName: 'Pulse Relay (Mobile Connection)', showInComparisonTable: true, displayableInSelfHostedPlan: true, roles: { diff --git a/frontend-modern/src/utils/selfHostedPlans.ts b/frontend-modern/src/utils/selfHostedPlans.ts index 71698fc20..4ff895c59 100644 --- a/frontend-modern/src/utils/selfHostedPlans.ts +++ b/frontend-modern/src/utils/selfHostedPlans.ts @@ -81,9 +81,9 @@ export function getSelfHostedPlanEntitlementSummary( case 'community': return `${planLabel} is active on this instance. It includes self-hosted monitoring, 7-day metric history, watch-only Patrol, update alerts, and SSO.`; case 'relay': - return `${planLabel} is active on this instance. It includes remote web access, Pulse Mobile pairing, push notifications, and 14-day metric history.`; + return `${planLabel} is active on this instance. It includes Pulse Mobile pairing and push notifications until 31 March 2027, and 14-day metric history.`; case 'pro': - return `${planLabel} is active on this instance. It includes Relay connectivity, Pulse Mobile pairing, push notifications, Patrol modes (Ask first, Safe auto-fix, Autopilot), 90-day metric history, RBAC, audit logging, reporting, and agent profiles.`; + return `${planLabel} is active on this instance. It includes Patrol modes (Ask first, Safe auto-fix, Autopilot), 90-day metric history, RBAC, audit logging, reporting, agent profiles, and Pulse Mobile pairing until 31 March 2027.`; } } @@ -129,18 +129,15 @@ export const SELF_HOSTED_PLAN_DEFINITIONS: readonly SelfHostedPlanDefinition[] = price: '$39/year', subline: 'or $4.99/month', metricHistoryDays: 14, - billingExtrasSummary: 'Remote web access, pairing, and push', + billingExtrasSummary: 'Pulse Mobile pairing and push', entitlementSummary: getSelfHostedPlanEntitlementSummary('relay'), entitlementHighlights: getTierEntitlementHighlights('relay', 14), includedExtras: [], comparisonSummary: - 'Remote web access, Pulse Mobile pairing, push notifications, and 14-day metric history.', + 'Retired plan: Pulse Mobile pairing and push notifications until 31 March 2027, and 14-day metric history.', highlights: [ - 'Secure access when you are away from your network', - 'Remote web access via Relay', - 'Pulse Mobile pairing', + 'Pulse Mobile pairing until 31 March 2027', 'Push notifications', - 'No inbound ports required', '14-day metric history', ], }, @@ -155,9 +152,8 @@ export const SELF_HOSTED_PLAN_DEFINITIONS: readonly SelfHostedPlanDefinition[] = entitlementHighlights: getTierEntitlementHighlights('pro', 90), includedExtras: getTierIncludedExtras('pro'), comparisonSummary: - 'Patrol investigates issues, applies safe fixes, and verifies the result. Relay connectivity is included, plus 90-day metric history and team controls.', + 'Patrol investigates issues, applies safe fixes, and verifies the result, with 90-day metric history and team controls.', highlights: [ - 'Relay connectivity, Pulse Mobile pairing, and push notifications included', 'Patrol modes: Ask first, Safe auto-fix, or Autopilot', 'Patrol investigates issues and explains the root cause', 'Patrol applies safe fixes and verifies the result', diff --git a/internal/api/contract_test.go b/internal/api/contract_test.go index 466f0d533..612a8fc8b 100644 --- a/internal/api/contract_test.go +++ b/internal/api/contract_test.go @@ -10008,7 +10008,7 @@ func TestContract_OnboardingNotReadyResponseJSONSnapshot(t *testing.T) { Code: "relay_registration_unavailable", Severity: "error", Field: "instance_id", - Message: "Remote Access is enabled, but this Pulse instance is not connected to the relay yet. Wait for the status to show Connected before generating a mobile pairing code.", + Message: "Pulse Mobile connections are on, but this Pulse instance is not connected to the relay yet. Wait for the status to show Connected before generating a pairing code.", }, }, } @@ -10022,7 +10022,7 @@ func TestContract_OnboardingNotReadyResponseJSONSnapshot(t *testing.T) { "code":"onboarding_not_ready", "error":"Pulse Mobile pairing is not ready yet.", "message":"Pulse Mobile pairing is not ready yet.", - "diagnostics":[{"code":"relay_registration_unavailable","severity":"error","message":"Remote Access is enabled, but this Pulse instance is not connected to the relay yet. Wait for the status to show Connected before generating a mobile pairing code.","field":"instance_id"}] + "diagnostics":[{"code":"relay_registration_unavailable","severity":"error","message":"Pulse Mobile connections are on, but this Pulse instance is not connected to the relay yet. Wait for the status to show Connected before generating a pairing code.","field":"instance_id"}] }` assertJSONSnapshot(t, got, want) diff --git a/internal/api/onboarding_handlers.go b/internal/api/onboarding_handlers.go index ab9125a53..f5ab6af45 100644 --- a/internal/api/onboarding_handlers.go +++ b/internal/api/onboarding_handlers.go @@ -307,7 +307,7 @@ func (r *Router) buildOnboardingPayload(req *http.Request, relayCfg *relay.Confi Code: "relay_disabled", Severity: "error", Field: "relay.enabled", - Message: "Remote Access is disabled. Turn on Remote Access and wait for the relay status to show Connected before pairing Pulse Mobile.", + Message: "Pulse Mobile connections are off. Turn them on in Settings > Pulse Mobile and wait for the status to show Connected before pairing a phone.", }) } if payload.InstanceID == "" { @@ -315,7 +315,7 @@ func (r *Router) buildOnboardingPayload(req *http.Request, relayCfg *relay.Confi Code: "relay_registration_unavailable", Severity: "error", Field: "instance_id", - Message: "Remote Access is enabled, but this Pulse instance is not connected to the relay yet. Wait for the status to show Connected before generating a mobile pairing code.", + Message: "Pulse Mobile connections are on, but this Pulse instance is not connected to the relay yet. Wait for the status to show Connected before generating a pairing code.", }) } if payload.AuthToken == "" { diff --git a/internal/license/coverage_additional_test.go b/internal/license/coverage_additional_test.go index 5c45773b1..8b0edffa7 100644 --- a/internal/license/coverage_additional_test.go +++ b/internal/license/coverage_additional_test.go @@ -91,7 +91,7 @@ func TestGetFeatureDisplayNameCoversKnownFeaturesAndFallback(t *testing.T) { {feature: FeatureAuditLogging, want: "Audit Logging"}, {feature: FeatureSSO, want: "Core SSO (OIDC/SAML)"}, {feature: FeatureAdvancedSSO, want: "Multi-Provider SSO"}, - {feature: FeatureRelay, want: "Pulse Relay (Remote Access)"}, + {feature: FeatureRelay, want: "Pulse Relay (Mobile Connection)"}, {feature: FeatureMobileApp, want: "Pulse Mobile Pairing"}, {feature: FeaturePushNotifications, want: "Push Notifications"}, {feature: FeatureAdvancedReporting, want: "PDF/CSV Reporting"}, diff --git a/pkg/licensing/features_test.go b/pkg/licensing/features_test.go index 6e56e19e2..0c8babcc4 100644 --- a/pkg/licensing/features_test.go +++ b/pkg/licensing/features_test.go @@ -384,7 +384,7 @@ func TestGetFeatureDisplayName(t *testing.T) { {FeatureAuditLogging, "Audit Logging"}, {FeatureSSO, "Core SSO (OIDC/SAML)"}, {FeatureAdvancedSSO, "Multi-Provider SSO"}, - {FeatureRelay, "Pulse Relay (Remote Access)"}, + {FeatureRelay, "Pulse Relay (Mobile Connection)"}, {FeatureMobileApp, "Pulse Mobile Pairing"}, {FeaturePushNotifications, "Push Notifications"}, {FeatureAdvancedReporting, "PDF/CSV Reporting"}, @@ -428,7 +428,7 @@ func TestSelfHostedFeatureMetadataKeepsCanonicalPlanLabelsAndVisibility(t *testi if !ok { t.Fatalf("expected metadata for %q", FeatureRelay) } - if relay.ComparisonName != "Pulse Relay (Remote Access)" { + if relay.ComparisonName != "Pulse Relay (Mobile Connection)" { t.Fatalf("ComparisonName = %q, want canonical Relay marketing label", relay.ComparisonName) } if GetSelfHostedFeatureRole(FeatureRelay, TierRelay) != SelfHostedFeatureRolePrimaryPillar { diff --git a/pkg/licensing/self_hosted_feature_catalog.go b/pkg/licensing/self_hosted_feature_catalog.go index caaf663d8..cf4014537 100644 --- a/pkg/licensing/self_hosted_feature_catalog.go +++ b/pkg/licensing/self_hosted_feature_catalog.go @@ -92,8 +92,8 @@ var featureMetadataCatalog = map[string]FeatureMetadata{ }, FeatureRelay: { Key: FeatureRelay, - DisplayName: "Pulse Relay (Remote Access)", - ComparisonName: "Pulse Relay (Remote Access)", + DisplayName: "Pulse Relay (Mobile Connection)", + ComparisonName: "Pulse Relay (Mobile Connection)", ShowInComparisonTable: true, DisplayableInPlanUI: true, SelfHostedRoles: SelfHostedFeatureRoles{