Promote relay public rollout posture

This commit is contained in:
rcourtman
2026-03-28 19:07:37 +00:00
parent ebfc4fd3c0
commit d76e8fc58c
17 changed files with 103 additions and 55 deletions
+1 -1
View File
@@ -61,7 +61,7 @@ Power-user shortcuts:
- **Secure by Design**: Credentials encrypted at rest, strict API scoping, agent commands disabled by default
- **One-Click Updates**: Easy upgrades for supported deployments
- **OIDC/SSO/SAML**: Single sign-on with multi-provider support
- **Mobile Remote Access (Coming Soon)**: Relay protocol with end-to-end encryption is available now; public mobile app launch is in staged rollout (Relay and above)
- **Mobile Remote Access**: Relay protocol with end-to-end encryption for supported Pulse Mobile clients (Relay and above)
- **Privacy Focused**: Anonymous telemetry is enabled by default and [fully documented](docs/PRIVACY.md) — no hostnames, credentials, or personal data is ever sent. Disable any time in Settings or via `PULSE_TELEMETRY=false`.
## ⚡ Quick Start
+1 -1
View File
@@ -1113,7 +1113,7 @@ TrueNAS resources (pools, datasets, disks, ZFS snapshots, replication tasks, ale
End-to-end encrypted relay protocol for mobile connectivity.
> Mobile app status: public rollout is coming soon; current pairing endpoints are primarily used for staged beta onboarding.
> Relay pairing endpoints generate the QR code and deep link used by supported Pulse Mobile clients.
### Relay Configuration (Admin, Relay and Above)
- `GET /api/settings/relay` (admin, `settings:read`, Relay+) — Get current relay configuration.
+2 -2
View File
@@ -6,7 +6,7 @@ Pulse Cloud is the hosted version of Pulse — a fully managed monitoring instan
1. **Sign up** at the Pulse Cloud portal.
2. **Connect your agents** — install the Pulse agent on your infrastructure pointing to your cloud URL.
3. **Monitor** — access your dashboard from any browser; mobile app rollout is coming soon.
3. **Monitor** — access your dashboard from any browser or supported Pulse Mobile client.
Each Cloud account gets a dedicated, isolated Pulse instance with its own subdomain (e.g., `yourname.cloud.pulserelay.pro`).
@@ -21,7 +21,7 @@ Pulse Cloud includes everything in the **Pro** plan, plus:
| **Automatic backups** | Daily encrypted backups with 7-day retention |
| **Dedicated instance** | Your data runs in an isolated container — not shared with other tenants |
| **Wildcard TLS** | HTTPS with auto-renewing certificates |
| **Mobile ready** | Relay is pre-configured now; mobile app rollout is coming soon |
| **Mobile ready** | Relay is pre-configured for secure Pulse Mobile remote access |
### Cloud Enterprise (Add-On)
+2 -2
View File
@@ -494,13 +494,13 @@ TrueNAS connections are stored encrypted in `truenas.enc`.
The relay protocol provides end-to-end encrypted remote access foundations for Pulse mobile connectivity.
> Mobile app availability: staged rollout (coming soon). Relay configuration is available now for early-access/beta onboarding and future readiness.
> Supported Pulse Mobile clients pair here using the generated QR code or deep link once relay is enabled for this instance.
### Configuration
1. Go to **Settings → Relay**.
2. Toggle relay **On**.
3. Use the **QR Code** or **Deep Link** when your mobile beta access is enabled.
3. Use the **QR Code** or **Deep Link** to pair a supported Pulse Mobile client.
### Environment Overrides
+1 -1
View File
@@ -70,7 +70,7 @@ All other documents are supporting references unless explicitly required for evi
- **[Unified Resource Model](UNIFIED_RESOURCES.md)** How all platforms merge into one model with task-based navigation.
- **[Unified Navigation Migration](MIGRATION_UNIFIED_NAV.md)** Upgrading from platform-specific tabs to v6 navigation.
- **[TrueNAS Integration](TRUENAS.md)** First-class TrueNAS SCALE/CORE monitoring (pools, datasets, disks, snapshots, replication).
- **[Relay / Mobile Remote Access](RELAY.md)** End-to-end encrypted relay (mobile app public rollout is coming soon; Relay and above).
- **[Relay / Mobile Remote Access](RELAY.md)** End-to-end encrypted relay for supported Pulse Mobile clients (Relay and above).
- **[Recovery Central](RECOVERY.md)** Unified backup, snapshot, and replication view across all providers.
- **[Pulse Cloud (Hosted)](CLOUD.md)** Fully managed hosting with automatic updates and backups.
- **[Pulse AI](AI.md)** Chat assistant, patrol findings, alert analysis, intelligence, and forecasts.
+2 -2
View File
@@ -2,7 +2,7 @@
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.
> Mobile app status: public rollout is coming soon. Relay remains available now for early-access/beta onboarding.
> Supported Pulse Mobile clients pair from **Settings → Relay** using a QR code or deep link and connect through Pulse Relay over end-to-end encrypted remote access.
## How It Works
@@ -22,7 +22,7 @@ Pulse Relay provides **end-to-end encrypted remote access** foundations for Puls
1. Go to **Settings → Relay**.
2. Toggle relay **On**.
3. Use the **QR Code** or **Deep Link** when mobile beta access is enabled.
3. Use the **QR Code** or **Deep Link** to pair a supported Pulse Mobile client.
4. Your paired mobile client connects through relay.
## Requirements
+1 -18
View File
@@ -1982,15 +1982,11 @@
"status": "partial",
"completion": {
"state": "bounded-residual",
"summary": "Relay infrastructure is at the RC floor; the remaining residual is explicitly limited to the backend-owned mobile relay capability boundary and the later public rollout posture required for GA promotion.",
"summary": "Relay infrastructure is at the RC floor; the remaining residual is limited to the backend-owned mobile relay capability boundary.",
"tracking": [
{
"kind": "lane-followup",
"id": "relay-mobile-capability-boundary"
},
{
"kind": "lane-followup",
"id": "relay-public-rollout-posture"
}
]
},
@@ -3155,19 +3151,6 @@
"subsystem_ids": [
"relay-runtime"
]
},
{
"id": "relay-public-rollout-posture",
"summary": "Replace the current early-access and coming-soon relay public posture with an explicit public rollout decision only after mobile availability, onboarding language, and support expectations are intentionally promoted for normal use.",
"owner": "project-owner",
"status": "planned",
"recorded_at": "2026-03-28",
"lane_ids": [
"L7"
],
"subsystem_ids": [
"relay-runtime"
]
}
],
"coverage_gaps": [
@@ -803,12 +803,17 @@ provisioning must complete under an explicit background timeout instead of
depending on the inbound Stripe request context surviving long enough for first
boot and health polling.
That same paid relay onboarding boundary now also owns QR-token lifecycle on
the staged mobile rollout surface. `RelaySettingsPanel.tsx`,
the supported Pulse Mobile pairing surface. `RelaySettingsPanel.tsx`,
`RelayPairingSection.tsx`, and `useRelaySettingsPanelState.ts` may revoke a
displayed pairing token only when canonical token metadata still shows no
`lastUsedAt`; once a token has been used by a paired device, refreshing or
hiding the QR must preserve that credential instead of treating it as
disposable UI state.
That same paid relay onboarding boundary also owns the operator-facing rollout
posture on the licensed settings surface: relay paywall and pairing copy must
describe supported Pulse Mobile pairing as a normal-use Relay capability, and
must not fall back to staged-beta or coming-soon messaging once the owned
pairing/runtime path is live.
The customer-account surface is now also an explicit cloud-paid ownership
boundary. Pulse already has a real hosted control-plane portal in
`internal/cloudcp/portal/`, account/workspace mutation APIs in
@@ -206,6 +206,11 @@ work extends shared components instead of creating new local variants.
the governed dashboard empty state route first-time operators into
Infrastructure Install, instead of preserving a separate root-only jump to
`/infrastructure` that drifts from the rest of the onboarding contract.
12. Keep relay settings shell copy on the shared presentation owner in
`frontend-modern/src/utils/relayPresentation.ts`. The route metadata in
`settingsHeaderMeta.ts` and the leading `SettingsPanel` in
`RelaySettingsPanel.tsx` must reuse the same description and availability
copy instead of drifting into separate rollout or pairing wording.
## Current State
@@ -1412,6 +1417,12 @@ That shared shell boundary now also covers version-matched docs-link framing:
customer-facing privacy disclosures in shared settings surfaces must route
through `frontend-modern/src/utils/docsLinks.ts` rather than panel-local
external URLs.
The same shell boundary now also owns shared relay route framing copy:
`frontend-modern/src/utils/relayPresentation.ts` is the canonical owner for
the top-level relay settings description and availability copy used by both
`settingsHeaderMeta.ts` and `RelaySettingsPanel.tsx`, so the route shell and
its first `SettingsPanel` cannot drift into separate rollout or pairing
descriptions.
Single-surface settings pages that only render one canonical `SettingsPanel`
must stay rooted directly at that panel instead of wrapping it in an extra
+1 -1
View File
@@ -70,7 +70,7 @@ All other documents are supporting references unless explicitly required for evi
- **[Unified Resource Model](UNIFIED_RESOURCES.md)** How all platforms merge into one model with task-based navigation.
- **[Unified Navigation Migration](MIGRATION_UNIFIED_NAV.md)** Upgrading from platform-specific tabs to v6 navigation.
- **[TrueNAS Integration](TRUENAS.md)** First-class TrueNAS SCALE/CORE monitoring (pools, datasets, disks, snapshots, replication).
- **[Relay / Mobile Remote Access](RELAY.md)** End-to-end encrypted relay (mobile app public rollout is coming soon; Relay and above).
- **[Relay / Mobile Remote Access](RELAY.md)** End-to-end encrypted relay for supported Pulse Mobile clients (Relay and above).
- **[Recovery Central](RECOVERY.md)** Unified backup, snapshot, and replication view across all providers.
- **[Pulse Cloud (Hosted)](CLOUD.md)** Fully managed hosting with automatic updates and backups.
- **[Pulse AI](AI.md)** Chat assistant, patrol findings, alert analysis, intelligence, and forecasts.
@@ -8,13 +8,18 @@ import { formField, labelClass, controlClass, formHelpText } from '@/components/
import { trackUpgradeClicked } from '@/utils/upgradeMetrics';
import { getSettingsConfigurationLoadingState } from '@/utils/settingsShellPresentation';
import {
RELAY_BETA_MESSAGE_CLASS,
RELAY_BETA_TITLE_CLASS,
RELAY_ENABLE_HELP_TEXT,
RELAY_INFO_MESSAGE_CLASS,
RELAY_INFO_TITLE_CLASS,
RELAY_INLINE_ACTION_CLASS,
RELAY_LAST_ERROR_CLASS,
RELAY_LICENSE_REQUIRED_MESSAGE,
RELAY_PAIRING_AVAILABILITY_MESSAGE,
RELAY_PAIRING_AVAILABILITY_TITLE,
RELAY_PRIMARY_BUTTON_CLASS,
RELAY_PRIMARY_LINK_CLASS,
RELAY_READONLY_NOTICE_CLASS,
RELAY_SETTINGS_DESCRIPTION,
} from '@/utils/relayPresentation';
import { UPGRADE_TRIAL_LABEL } from '@/utils/upgradePresentation';
import { RelayPairingSection } from './RelayPairingSection';
@@ -31,7 +36,7 @@ export const RelaySettingsPanel: Component<RelaySettingsPanelProps> = (props) =>
return (
<SettingsPanel
title="Remote Access"
description="Configure Pulse relay connectivity for secure remote access (mobile rollout coming soon)."
description={RELAY_SETTINGS_DESCRIPTION}
icon={<RadioTower size={20} strokeWidth={2} />}
>
<Show when={!state.loading()} fallback={<div class="text-sm ">Loading...</div>}>
@@ -41,10 +46,7 @@ export const RelaySettingsPanel: Component<RelaySettingsPanelProps> = (props) =>
<RadioTower size={20} class="text-blue-500 mt-0.5 flex-shrink-0" strokeWidth={2} />
<div>
<p class="text-sm font-medium text-base-content">Remote Access (Relay)</p>
<p class="text-sm text-muted mt-1">
Remote access via Pulse Relay requires a Relay license or above. Mobile app
public rollout is coming soon.
</p>
<p class="text-sm text-muted mt-1">{RELAY_LICENSE_REQUIRED_MESSAGE}</p>
</div>
</div>
<div class="flex flex-col sm:flex-row items-center gap-2">
@@ -78,7 +80,7 @@ export const RelaySettingsPanel: Component<RelaySettingsPanelProps> = (props) =>
return (
<SettingsPanel
title="Remote Access"
description="Configure Pulse relay connectivity for secure remote access (mobile rollout coming soon)."
description={RELAY_SETTINGS_DESCRIPTION}
icon={<RadioTower size={20} strokeWidth={2} />}
>
<Show
@@ -96,11 +98,8 @@ export const RelaySettingsPanel: Component<RelaySettingsPanelProps> = (props) =>
</Show>
<Card tone="info" padding="md">
<p class={RELAY_BETA_TITLE_CLASS}>Pulse Mobile rollout is coming soon</p>
<p class={RELAY_BETA_MESSAGE_CLASS}>
Relay infrastructure is available now. Pairing and remote sessions are currently
intended for staged beta access.
</p>
<p class={RELAY_INFO_TITLE_CLASS}>{RELAY_PAIRING_AVAILABILITY_TITLE}</p>
<p class={RELAY_INFO_MESSAGE_CLASS}>{RELAY_PAIRING_AVAILABILITY_MESSAGE}</p>
</Card>
{/* Connection Status */}
@@ -136,10 +135,7 @@ export const RelaySettingsPanel: Component<RelaySettingsPanelProps> = (props) =>
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div>
<label class={labelClass()}>Enable Remote Access</label>
<p class={formHelpText}>
Connect this Pulse instance to the relay server for secure remote access and mobile
beta readiness.
</p>
<p class={formHelpText}>{RELAY_ENABLE_HELP_TEXT}</p>
</div>
<Toggle
checked={state.config()?.enabled ?? false}
@@ -146,6 +146,23 @@ describe('RelaySettingsPanel runtime', () => {
cleanup();
});
it('shows supported Pulse Mobile pairing copy on the relay paywall', async () => {
hasFeatureMock.mockReturnValue(false);
render(() => <RelaySettingsPanel canManage />);
await waitFor(() => {
expect(screen.getByText('Remote Access (Relay)')).toBeInTheDocument();
});
expect(
screen.getByText(
'Remote access via Pulse Relay requires a Relay license or above. Pair supported Pulse Mobile clients with this instance using a QR code or deep link.',
),
).toBeInTheDocument();
expect(screen.getByRole('link', { name: 'Upgrade' })).toHaveAttribute('href', '/upgrade');
});
it('loads connected relay state and generates a pairing QR payload', async () => {
render(() => <RelaySettingsPanel canManage />);
@@ -153,6 +170,12 @@ 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(
'Supported Pulse Mobile clients connect to this Pulse instance with a QR code or deep link over end-to-end encrypted relay connectivity.',
),
).toBeInTheDocument();
expect(screen.getByText('Connected')).toBeInTheDocument();
expect(screen.getByText('Instance: instance-local')).toBeInTheDocument();
expect(screen.getByText('1 active channel')).toBeInTheDocument();
@@ -1,5 +1,6 @@
import { describe, expect, it } from 'vitest';
import { SELF_HOSTED_PRO_BILLING_PRESENTATION } from '../selfHostedBillingPresentation';
import { RELAY_SETTINGS_DESCRIPTION } from '@/utils/relayPresentation';
import settingsSource from '../Settings.tsx?raw';
import settingsDialogsSource from '../SettingsDialogs.tsx?raw';
import settingsShellSource from '../SettingsPageShell.tsx?raw';
@@ -1318,6 +1319,13 @@ describe('Settings architecture guardrails', () => {
expect(SETTINGS_HEADER_META['organization-billing'].description).toContain('plan limits');
});
it('keeps relay shell copy on the shared relay presentation owner', () => {
expect(settingsHeaderMetaSource).toContain('RELAY_SETTINGS_DESCRIPTION');
expect(relaySettingsPanelSource).toContain('description={RELAY_SETTINGS_DESCRIPTION}');
expect(SETTINGS_HEADER_META['system-relay'].description).toBe(RELAY_SETTINGS_DESCRIPTION);
expect(SETTINGS_HEADER_META['system-relay'].description).toContain('Pulse Mobile pairing');
});
it('keeps self-hosted billing shell copy in a settings-owned presentation module', () => {
expect(selfHostedBillingPresentationSource).toContain(
'export const SELF_HOSTED_PRO_BILLING_PRESENTATION',
@@ -1,5 +1,6 @@
import type { SettingsHeaderMetaMap } from './settingsNavigationModel';
import { SELF_HOSTED_PRO_BILLING_PRESENTATION } from './selfHostedBillingPresentation';
import { RELAY_SETTINGS_DESCRIPTION } from '@/utils/relayPresentation';
export const SETTINGS_HEADER_META: SettingsHeaderMetaMap = {
proxmox: {
@@ -34,8 +35,7 @@ export const SETTINGS_HEADER_META: SettingsHeaderMetaMap = {
},
'system-relay': {
title: 'Remote Access',
description:
'Configure Pulse relay connectivity for secure remote access (mobile rollout coming soon).',
description: RELAY_SETTINGS_DESCRIPTION,
},
'system-billing': {
title: SELF_HOSTED_PRO_BILLING_PRESENTATION.shellTitle,
@@ -2,11 +2,13 @@ import { describe, expect, it } from 'vitest';
import {
getRelayConnectionPresentation,
getRelayDiagnosticClass,
RELAY_BETA_MESSAGE_CLASS,
RELAY_BETA_TITLE_CLASS,
RELAY_CODE_BLOCK_CLASS,
RELAY_ENABLE_HELP_TEXT,
RELAY_INFO_MESSAGE_CLASS,
RELAY_INFO_TITLE_CLASS,
RELAY_INLINE_ACTION_CLASS,
RELAY_LAST_ERROR_CLASS,
RELAY_LICENSE_REQUIRED_MESSAGE,
RELAY_ONBOARDING_DESCRIPTION,
RELAY_ONBOARDING_DISCONNECTED_LABEL,
RELAY_ONBOARDING_SETUP_LABEL,
@@ -16,11 +18,14 @@ import {
RELAY_ONBOARDING_SETUP_WIZARD_TRIAL_LABEL,
RELAY_ONBOARDING_TRIAL_HINT,
RELAY_ONBOARDING_UPGRADE_LABEL,
RELAY_PAIRING_AVAILABILITY_MESSAGE,
RELAY_PAIRING_AVAILABILITY_TITLE,
RELAY_PRIMARY_BUTTON_CLASS,
RELAY_PRIMARY_LINK_CLASS,
RELAY_QR_IMAGE_CLASS,
RELAY_READONLY_NOTICE_CLASS,
RELAY_SECONDARY_BUTTON_CLASS,
RELAY_SETTINGS_DESCRIPTION,
} from '@/utils/relayPresentation';
describe('relayPresentation', () => {
@@ -58,8 +63,8 @@ describe('relayPresentation', () => {
expect(RELAY_PRIMARY_LINK_CLASS).toContain('text-center');
expect(RELAY_SECONDARY_BUTTON_CLASS).toContain('bg-surface-hover');
expect(RELAY_INLINE_ACTION_CLASS).toContain('hover:underline');
expect(RELAY_BETA_TITLE_CLASS).toContain('font-medium');
expect(RELAY_BETA_MESSAGE_CLASS).toContain('text-muted');
expect(RELAY_INFO_TITLE_CLASS).toContain('font-medium');
expect(RELAY_INFO_MESSAGE_CLASS).toContain('text-muted');
expect(RELAY_LAST_ERROR_CLASS).toContain('text-red-600');
expect(RELAY_CODE_BLOCK_CLASS).toContain('font-mono');
expect(RELAY_QR_IMAGE_CLASS).toContain('border-border');
@@ -67,6 +72,14 @@ describe('relayPresentation', () => {
expect(getRelayDiagnosticClass('warning')).toContain('bg-amber-50');
});
it('centralizes relay availability copy', () => {
expect(RELAY_SETTINGS_DESCRIPTION).toContain('Pulse Mobile pairing');
expect(RELAY_LICENSE_REQUIRED_MESSAGE).toContain('supported Pulse Mobile clients');
expect(RELAY_PAIRING_AVAILABILITY_TITLE).toBe('Pair Pulse Mobile through Relay');
expect(RELAY_PAIRING_AVAILABILITY_MESSAGE).toContain('QR code or deep link');
expect(RELAY_ENABLE_HELP_TEXT).toContain('Pulse Mobile pairing');
});
it('centralizes relay onboarding copy', () => {
expect(RELAY_ONBOARDING_TITLE).toBe('Pair Your Mobile Device');
expect(RELAY_ONBOARDING_DESCRIPTION).toContain('remote monitoring');
+11 -2
View File
@@ -17,8 +17,8 @@ export const RELAY_SECONDARY_BUTTON_CLASS =
'min-h-10 sm:min-h-10 px-3 py-2 text-sm font-medium text-base-content bg-surface-hover hover:bg-surface-hover rounded-md disabled:opacity-50';
export const RELAY_INLINE_ACTION_CLASS =
'text-sm text-indigo-500 hover:underline disabled:opacity-50';
export const RELAY_BETA_TITLE_CLASS = 'text-sm font-medium text-base-content';
export const RELAY_BETA_MESSAGE_CLASS = 'text-xs text-muted mt-1';
export const RELAY_INFO_TITLE_CLASS = 'text-sm font-medium text-base-content';
export const RELAY_INFO_MESSAGE_CLASS = 'text-xs text-muted mt-1';
export const RELAY_LAST_ERROR_CLASS =
'mt-2 text-xs text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-900 rounded px-2 py-1';
export const RELAY_CODE_BLOCK_CLASS =
@@ -26,6 +26,15 @@ export const RELAY_CODE_BLOCK_CLASS =
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 =
'Configure Pulse relay connectivity for secure remote access and Pulse Mobile pairing.';
export const RELAY_LICENSE_REQUIRED_MESSAGE =
'Remote access via Pulse Relay requires a Relay license or above. Pair supported Pulse Mobile clients with this instance using a QR code or deep link.';
export const RELAY_PAIRING_AVAILABILITY_TITLE = 'Pair Pulse Mobile through Relay';
export const RELAY_PAIRING_AVAILABILITY_MESSAGE =
'Supported Pulse Mobile clients connect to this Pulse instance with a QR code or deep link over end-to-end encrypted relay connectivity.';
export const RELAY_ENABLE_HELP_TEXT =
'Connect this Pulse instance to the relay server for secure remote access and Pulse Mobile pairing.';
export const RELAY_ONBOARDING_TITLE = 'Pair Your Mobile Device';
export const RELAY_ONBOARDING_DESCRIPTION =
'Pulse Relay lets your phone securely connect to this Pulse instance for remote monitoring.';
@@ -29,7 +29,7 @@ const SETTINGS_SHELL_ROUTES = [
{
route: '/settings/system-relay',
title: 'Remote Access',
description: 'Configure Pulse relay connectivity for secure remote access (mobile rollout coming soon).',
description: 'Configure Pulse relay connectivity for secure remote access and Pulse Mobile pairing.',
},
{
route: '/settings/security-auth',