Align Relay copy with standalone tier

This commit is contained in:
rcourtman 2026-04-30 11:01:22 +01:00
parent 274bb4c60f
commit 7a48cd4afd
12 changed files with 73 additions and 15 deletions

View file

@ -554,14 +554,14 @@ curl -s http://localhost:7655/api/monitoring/scheduler/health | jq
Use the API endpoint above or export diagnostics from **Settings → Diagnostics** when troubleshooting.
### Relay Security (Pro)
### Relay Security (Relay and Above)
The relay protocol provides mobile remote access with end-to-end encryption:
- **ECDH key exchange**: Per-channel encryption keys are derived via Elliptic Curve Diffie-Hellman, meaning the relay server never sees plaintext data.
- **Per-channel authentication**: Each mobile session authenticates independently.
- **Back-pressure**: Data limiters prevent channel flooding.
- **License-gated**: Relay functionality requires a Pro or Cloud license.
- **License-gated**: Relay functionality requires a Relay, Pro, legacy Pro+, or Cloud license.
- **Configurable**: Enable/disable via **Settings → Relay** (admin only).
### Agent Command Security

View file

@ -32,4 +32,4 @@
## Mobile Responsive Design
![Mobile View](images/08-mobile.png)
*Fully responsive mobile interface with a dedicated bottom tab bar for touch navigation. Supports mobile remote access via the relay protocol (Pro feature) for secure monitoring on the go. Touch-optimized controls, adaptive layouts, and the command palette ensure full functionality on smartphones and tablets.*
*Fully responsive mobile interface with a dedicated bottom tab bar for touch navigation. Supports mobile remote access through Pulse Relay for secure monitoring on the go. Touch-optimized controls, adaptive layouts, and the command palette ensure full functionality on smartphones and tablets.*

View file

@ -276,6 +276,9 @@ runtime gating as separate unlinked claims.
into an operator-actionable activation-required state. Customer-facing
Relay settings must not render raw `register:` or license-token-provider
diagnostics from the relay client as the primary status message.
Relay settings and public commercial docs must also keep Remote Access
positioned as a Relay-and-higher capability, not a Pro-only feature, so the
Relay tier remains a tangible standalone paid product.
19. Add or change cloud plan presentation through `frontend-modern/src/pages/CloudPricing.tsx`
That same presentation boundary also owns truthful customer-entry copy for
hosted Cloud pricing and signup. Cloud CTA labels, setup steps, and
@ -1697,6 +1700,9 @@ The paid relay settings surface is part of that same ownership model. Changes
to `frontend-modern/src/components/Settings/RelaySettingsPanel.tsx` must carry
this contract and the dedicated relay frontend proof files instead of
remaining unowned consumers of relay licensing state.
Its paywall and activation copy must say Relay and higher plans rather than
Pro-only wording; Pro may include Relay, but Relay is still its own public
self-hosted tier.
That relay settings owner is intentionally split by role:
`frontend-modern/src/components/Settings/RelaySettingsPanel.tsx` is the
settings shell, `frontend-modern/src/components/Settings/useRelaySettingsPanelState.ts`

View file

@ -855,7 +855,9 @@ work extends shared components instead of creating new local variants.
`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.
copy instead of drifting into separate rollout or pairing wording. Relay
availability copy must describe the Relay tier boundary as Relay and higher
plans rather than collapsing Remote Access back into a Pro-only feature.
25. Keep shared settings-shell legal and docs referrals on
`frontend-modern/src/utils/docsLinks.ts`. Shared settings surfaces such as
`AIRuntimeControlsSection.tsx` must not hardcode GitHub `main` doc URLs for
@ -2640,7 +2642,8 @@ The same shell boundary now also owns shared relay route framing copy:
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.
descriptions or describe Relay as a Pro-only feature after Relay became its
own self-hosted paid tier.
Single-surface settings pages that only render one canonical `SettingsPanel`
must stay rooted directly at that panel instead of wrapping it in an extra

View file

@ -292,7 +292,9 @@ silently inheriting whatever older protocol floor the host runtime would allow.
That same rule also applies inside shipped security guidance itself:
`SECURITY.md` and the synced `frontend-modern/public/docs/SECURITY.md` copy may
not bounce the operator back to GitHub `main` for section references that the
running build already owns locally.
running build already owns locally. Their Relay security section must also use
the current Relay-and-higher entitlement boundary instead of stale Pro-only
license wording.
That same governed settings trust boundary now also includes
`frontend-modern/src/components/Settings/SecurityOverviewPanel.tsx`,
`frontend-modern/src/components/Settings/QuickSecuritySetup.tsx`,

View file

@ -554,14 +554,14 @@ curl -s http://localhost:7655/api/monitoring/scheduler/health | jq
Use the API endpoint above or export diagnostics from **Settings → Diagnostics** when troubleshooting.
### Relay Security (Pro)
### Relay Security (Relay and Above)
The relay protocol provides mobile remote access with end-to-end encryption:
- **ECDH key exchange**: Per-channel encryption keys are derived via Elliptic Curve Diffie-Hellman, meaning the relay server never sees plaintext data.
- **Per-channel authentication**: Each mobile session authenticates independently.
- **Back-pressure**: Data limiters prevent channel flooding.
- **License-gated**: Relay functionality requires a Pro or Cloud license.
- **License-gated**: Relay functionality requires a Relay, Pro, legacy Pro+, or Cloud license.
- **Configurable**: Enable/disable via **Settings → Relay** (admin only).
### Agent Command Security

View file

@ -30,7 +30,7 @@ import {
export const RelaySettingsPanel: Component<RelaySettingsPanelProps> = (props) => {
const state = useRelaySettingsPanelState(props);
// Pro feature gate
// Relay feature gate
if (!state.relayEnabled()) {
return (
<SettingsPanel title="Remote Access" description={RELAY_SETTINGS_DESCRIPTION}>

View file

@ -171,7 +171,7 @@ describe('RelaySettingsPanel runtime', () => {
expect(
screen.getByText(
'Remote access is available with Relay or Pro. Pair supported Pulse Mobile clients with this instance using a QR code or deep link.',
'Remote access is available with Relay and higher plans. Pair supported Pulse Mobile clients with this instance using a QR code or deep link.',
),
).toBeInTheDocument();
expect(screen.getByRole('link', { name: 'View plans' })).toHaveAttribute(
@ -257,7 +257,7 @@ describe('RelaySettingsPanel runtime', () => {
expect(
screen.getByText(
'Remote Access is enabled, but this instance does not have an active Relay token. Activate Relay or turn Remote Access off before pairing mobile clients.',
'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.',
),
).toBeInTheDocument();
expect(screen.queryByText('register: no license token available')).not.toBeInTheDocument();

View file

@ -88,6 +88,21 @@ describe('systemSettings store', () => {
expect(configurationDoc).toContain('PULSE_TELEMETRY');
});
it('keeps Relay security guidance aligned with the Relay tier boundary', () => {
const securityDoc = readFileSync(path.join(repoRoot, 'SECURITY.md'), 'utf8');
const publicSecurityDoc = readFileSync(
path.join(frontendRoot, 'public', 'docs', 'SECURITY.md'),
'utf8',
);
for (const copy of [securityDoc, publicSecurityDoc]) {
expect(copy).toContain('Relay Security (Relay and Above)');
expect(copy).toContain('Relay functionality requires a Relay, Pro, legacy Pro+, or Cloud license');
expect(copy).not.toContain('Relay Security (Pro)');
expect(copy).not.toContain('Relay functionality requires a Pro or Cloud license');
}
});
it('documents self-hosted AI provider transport and resource-policy redaction in the privacy doc', () => {
const privacyDoc = readFileSync(path.join(repoRoot, 'docs', 'PRIVACY.md'), 'utf8');

View file

@ -12,13 +12,25 @@ describe('quickstart copy contract', () => {
const pulsePro = readRepoFile('docs/PULSE_PRO.md');
const ai = readRepoFile('docs/AI.md');
const privacy = readRepoFile('docs/PRIVACY.md');
const security = readRepoFile('SECURITY.md');
const publicPrivacy = readRepoFile('frontend-modern/public/docs/PRIVACY.md');
const publicSecurity = readRepoFile('frontend-modern/public/docs/SECURITY.md');
const pricingSpec = readRepoFile('docs/architecture/v6-pricing-and-tiering.md');
const aiSettingsDialog = readRepoFile(
'frontend-modern/src/components/Settings/AISettingsDialogs.tsx',
);
for (const copy of [readme, pulsePro, ai, privacy, publicPrivacy, pricingSpec, aiSettingsDialog]) {
for (const copy of [
readme,
pulsePro,
ai,
privacy,
security,
publicPrivacy,
publicSecurity,
pricingSpec,
aiSettingsDialog,
]) {
expect(copy).not.toMatch(/quickstart/i);
expect(copy).not.toContain('quickstart:pulse-hosted');
expect(copy).not.toMatch(/hosted AI/i);
@ -28,4 +40,21 @@ describe('quickstart copy contract', () => {
expect(copy).not.toMatch(/Pulse Account[\s\S]{0,120}no API key/i);
}
});
it('keeps Relay public docs aligned with the Relay tier instead of Pro-only copy', () => {
const security = readRepoFile('SECURITY.md');
const publicSecurity = readRepoFile('frontend-modern/public/docs/SECURITY.md');
const screenshots = readRepoFile('docs/SCREENSHOTS.md');
for (const copy of [security, publicSecurity, screenshots]) {
expect(copy).not.toContain('Relay Security (Pro)');
expect(copy).not.toContain('Relay functionality requires a Pro or Cloud license');
expect(copy).not.toContain('relay protocol (Pro feature)');
}
expect(security).toContain('Relay Security (Relay and Above)');
expect(publicSecurity).toContain('Relay Security (Relay and Above)');
expect(security).toContain('Relay, Pro, legacy Pro+, or Cloud license');
expect(publicSecurity).toContain('Relay, Pro, legacy Pro+, or Cloud license');
});
});

View file

@ -106,12 +106,14 @@ describe('relayPresentation', () => {
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_LICENSE_REQUIRED_MESSAGE).toContain('available with Relay or Pro');
expect(RELAY_LICENSE_REQUIRED_MESSAGE).toContain('available with Relay and higher plans');
expect(RELAY_LICENSE_REQUIRED_MESSAGE).not.toContain('Relay or Pro');
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');
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');
});
it('does not retain retired Relay price or trial-era onboarding copy', () => {
@ -121,5 +123,6 @@ describe('relayPresentation', () => {
expect(relayPresentationSource).not.toContain('$49');
expect(relayPresentationSource).not.toContain('$99');
expect(relayPresentationSource).not.toContain('Start free trial');
expect(relayPresentationSource).not.toContain('Pro feature gate');
});
});

View file

@ -31,7 +31,7 @@ export const RELAY_DIAGNOSTICS_TITLE_CLASS = 'text-xs font-semibold text-base-co
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 is available with Relay or Pro. Pair supported Pulse Mobile clients with this instance using a QR code or deep link.';
'Remote access is available with Relay and higher plans. 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.';
@ -39,7 +39,7 @@ 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_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 Relay or turn Remote Access off before pairing mobile clients.';
'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.';
export function getRelayDiagnosticClass(severity: 'warning' | 'error'): string {
return severity === 'error'