feat(relay): make Relay discoverable where users configure alerts and say what it does

Production telemetry shows 17 weekly-active installs with Relay enabled out
of 6,225 — the weakest adoption of any feature flag. A funnel walk from a
fresh session showed why: the only Relay surface was the last item of the
last Settings section, hidden entirely from unlicensed installs, described
in transport jargon, and never mentioned on the alert Notifications page
where "get alerts on my phone" actually lives.

- Alerts → Notifications: new "Mobile push notifications" destinations
  section (AlertPushDestinationsSection) presenting push via Pulse Mobile
  alongside email/Apprise/webhooks. Licensed installs get a pointer to the
  canonical /settings/system-relay panel; unlicensed installs get the shared
  FeatureGateSection with the relay upgrade destination, suppressed when
  upgrade prompts are hidden. Page description now names mobile push.
- Settings nav: system-relay no longer sets hideWhenUnavailable — the
  Remote Access entry stays visible to free installs and the panel-owned
  upgrade gate communicates the paid boundary. relayRead capability hiding
  is unchanged.
- Remote Access copy rewritten around user value: push notifications and
  remote monitoring through Pulse Mobile with no port forwarding or VPN;
  pairing help drops "provisions a dedicated relay access credential"
  jargon, names iOS + Android early access, and states that download-page
  install links appear after activation-key entry. EN/ES/DE headers updated.
- Contracts updated in-commit: alerts.md (destinations catalog extension
  point), frontend-primitives.md (system-relay is not a deep-link-only
  flow), cloud-paid.md (value-first relay presentation obligation).

Contract-Neutral: i18n messages edit changes settings-header/alerts-destinations copy only; no security-privacy delta; alerts/cloud-paid/frontend-primitives contracts updated in-commit
This commit is contained in:
rcourtman
2026-07-17 23:35:12 +01:00
parent a0f75b1bb2
commit fa1cf95dcf
23 changed files with 324 additions and 77 deletions
@@ -164,6 +164,17 @@ inline SVG shells in alert-owned runtime components.
`frontend-modern/src/utils/alertTargetTypes.ts`. Supported target types
must share the unified evaluator, the canonical threshold/override identity
chain, and the standard notification delivery path.
10. Add or change the alert notification destinations catalog through
`frontend-modern/src/features/alerts/tabs/DestinationsTab.tsx` and
`frontend-modern/src/utils/alertDestinationsPresentation.ts`. The
destinations surface presents mobile push (Pulse Mobile paired through
Relay) alongside email, Apprise, and webhooks so phone delivery is
discoverable where alert routing is configured. It stays a pointer
surface: it routes setup to the canonical `/settings/system-relay` Remote
Access panel rather than duplicating relay pairing state or relay API
calls, and when the `relay` feature is absent it gates through the shared
`FeatureGateSection` and upgrade-navigation contract, rendering no upgrade
call-to-action when prompt suppression applies.
## Forbidden Paths
@@ -243,6 +243,12 @@ avoids a cloud-control-plane report data path across clients.
settings surface must preserve API-owned diagnostics and clean up the
temporary server-minted mobile token; it must not downgrade that response
into a generic paid-state prompt or create browser-owned relay credentials.
Relay presentation on the settings surface is value-first: Remote Access
panel and pairing copy must state the user job (alert push notifications
and remote monitoring through the Pulse Mobile app with no inbound ports or
VPN), name iOS and Android early access, and state that download-page
install links appear only after activation-key entry, rather than
describing relay transport mechanics alone.
2. `frontend-modern/src/components/Settings/MonitoredSystemImpactPreview.tsx` shared with `agent-lifecycle`: the monitored-system impact preview is both a platform-connections lifecycle surface and a canonical cloud-paid monitored-system presentation boundary.
3. `frontend-modern/src/useAppRuntimeState.ts` shared with `performance-and-scalability`: the authenticated app runtime bootstrap is both a hosted commercial org-context boundary and a protected app-shell performance boundary.
The app runtime may use `ssoSessionDisplayName` from security status for
@@ -192,6 +192,12 @@ must be reachable through the sidebar unless it is explicitly a hidden
deep-link flow. The self-hosted `Plans` page is not a deep-link-only flow:
`system-billing` stays visible whenever commercial presentation is allowed,
while demo or commercial-suppressed sessions may still hide it.
`system-relay` (Remote Access) is likewise not a deep-link-only flow: its nav
item stays visible without the `relay` feature so Relay remains discoverable
to free installs, and the panel-owned feature gate — not nav hiding —
communicates the paid boundary. Capability-based hiding (`relayRead`) still
applies; feature-based `hideWhenUnavailable` must not be reintroduced on that
item.
Candidate import-plan presentation inside the Infrastructure settings dialog is
a shared primitive composition boundary. `NodeCandidateImportPlan.tsx` may use
@@ -66,8 +66,8 @@ export const RelayPairingSection: Component<RelayPairingSectionProps> = (props)
</div>
<p class={formHelpText}>
Generate a QR code that provisions a dedicated Pulse Mobile relay access credential.{' '}
{RELAY_PAIRING_APP_AVAILABILITY_TEXT}{' '}
Generate a QR code to securely link the Pulse Mobile app on your phone to this Pulse
instance. {RELAY_PAIRING_APP_AVAILABILITY_TEXT}{' '}
<a
href={PULSE_PRO_DOWNLOAD_URL}
target="_blank"
@@ -189,7 +189,7 @@ describe('RelaySettingsPanel runtime', () => {
expect(
screen.getByText(
'Remote access is available with Relay and higher plans. Pair supported Pulse Mobile clients with this instance using a QR code or deep link.',
'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(
@@ -208,7 +208,7 @@ describe('RelaySettingsPanel runtime', () => {
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.',
'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.',
),
).toBeInTheDocument();
expect(screen.getByText('Connected')).toBeInTheDocument();
@@ -275,6 +275,14 @@ describe('settings architecture guardrails', () => {
expect(settingsNavCatalogSource).toContain("features: ['relay']");
expect(settingsNavCatalogSource).toContain("features: ['advanced_reporting']");
expect(settingsNavCatalogSource).toContain('hideWhenUnavailable: true');
// system-relay deliberately stays visible without the relay feature so
// Relay remains discoverable to free installs; the panel-owned upgrade
// gate communicates the paid boundary instead of nav hiding.
const systemRelayNavBlock = settingsNavCatalogSource.match(
/id: 'system-relay',[\s\S]*?requiredCapability: 'relayRead',\n\s*},/,
);
expect(systemRelayNavBlock?.[0]).toContain("features: ['relay']");
expect(systemRelayNavBlock?.[0]).not.toContain('hideWhenUnavailable');
expect(settingsHeaderMetaSource).toContain(
'title: SELF_HOSTED_PRO_BILLING_PRESENTATION.shellTitle',
);
@@ -98,8 +98,7 @@ describe('getSettingsHeaderMeta', () => {
it('localizes monitoring-availability to the English baseline', () => {
expect(en['monitoring-availability']).toEqual({
title: 'Availability checks',
description:
'Monitor endpoint-only devices and services with ping, TCP, and HTTP probes.',
description: 'Monitor endpoint-only devices and services with ping, TCP, and HTTP probes.',
});
});
@@ -113,8 +112,7 @@ describe('getSettingsHeaderMeta', () => {
it('localizes system-network to the English baseline (sibling only pinned es/de)', () => {
expect(en['system-network']).toEqual({
title: 'Network',
description:
'Configure the public URL, CORS, embedding, and webhook network boundaries.',
description: 'Configure the public URL, CORS, embedding, and webhook network boundaries.',
});
});
@@ -167,7 +165,7 @@ describe('getSettingsHeaderMeta', () => {
expect(en['system-relay']).toEqual({
title: 'Remote Access',
description:
'Configure Pulse relay connectivity for secure remote access and Pulse Mobile pairing.',
'Check on your systems and get alert push notifications anywhere with the Pulse Mobile app — no port forwarding or VPN required.',
});
});
@@ -442,7 +440,7 @@ describe('getSettingsHeaderMeta', () => {
expect(de['system-relay']).toEqual({
title: 'Remote-Zugriff',
description:
'Konfigurieren Sie Pulse-Relay-Konnektivitaet fuer sicheren Remote-Zugriff und Pulse-Mobile-Kopplung.',
'Behalten Sie Ihre Systeme von ueberall im Blick und erhalten Sie Alarm-Push-Benachrichtigungen ueber die Pulse-Mobile-App — ohne Portfreigaben oder VPN.',
});
});
@@ -535,8 +533,7 @@ describe('getSettingsHeaderMeta', () => {
it('localizes security-auth', () => {
expect(de['security-auth']).toEqual({
title: 'Authentifizierung',
description:
'Verwalten Sie passwortbasierte Authentifizierung und Zugangsdatenrotation.',
description: 'Verwalten Sie passwortbasierte Authentifizierung und Zugangsdatenrotation.',
});
});
@@ -642,7 +639,7 @@ describe('getSettingsHeaderMeta', () => {
expect(es['system-relay']).toEqual({
title: 'Acceso remoto',
description:
'Configura la conectividad de relay de Pulse para acceso remoto seguro y vinculación con Pulse Mobile.',
'Consulta tus sistemas y recibe notificaciones push de alertas desde cualquier lugar con la aplicación Pulse Mobile — sin abrir puertos ni VPN.',
});
});
@@ -757,8 +754,7 @@ describe('getSettingsHeaderMeta', () => {
it('localizes security-users', () => {
expect(es['security-users']).toEqual({
title: 'Acceso de usuarios',
description:
'Asigna roles a usuarios y consulta permisos efectivos en tu infraestructura.',
description: 'Asigna roles a usuarios y consulta permisos efectivos en tu infraestructura.',
});
});
@@ -818,15 +814,15 @@ describe('getInfrastructureReadOnlyHeaderDescription', () => {
});
it('normalizes an explicit null locale to DEFAULT_LOCALE', () => {
expect(
getInfrastructureReadOnlyHeaderDescription(null as unknown as SupportedLocale),
).toBe(getInfrastructureReadOnlyHeaderDescription('en'));
expect(getInfrastructureReadOnlyHeaderDescription(null as unknown as SupportedLocale)).toBe(
getInfrastructureReadOnlyHeaderDescription('en'),
);
});
it('normalizes an unsupported locale string to DEFAULT_LOCALE', () => {
// 'fr' is not yet a SupportedLocale, so normalizeLocale falls back to en.
expect(
getInfrastructureReadOnlyHeaderDescription('fr' as unknown as SupportedLocale),
).toBe(getInfrastructureReadOnlyHeaderDescription('en'));
expect(getInfrastructureReadOnlyHeaderDescription('fr' as unknown as SupportedLocale)).toBe(
getInfrastructureReadOnlyHeaderDescription('en'),
);
});
});
@@ -49,6 +49,24 @@ describe('settings localization catalog', () => {
expect(getSettingsNavItem('support-diagnostics', 'es')?.label).toBe('Diagnóstico y salud');
});
it('localizes the value-first Remote Access header in every catalog locale', () => {
expect(SETTINGS_HEADER_META['system-relay']).toEqual({
title: 'Remote Access',
description:
'Check on your systems and get alert push notifications anywhere with the Pulse Mobile app — no port forwarding or VPN required.',
});
expect(getSettingsHeaderMeta('es')['system-relay']).toEqual({
title: 'Acceso remoto',
description:
'Consulta tus sistemas y recibe notificaciones push de alertas desde cualquier lugar con la aplicación Pulse Mobile — sin abrir puertos ni VPN.',
});
expect(getSettingsHeaderMeta('de')['system-relay']).toEqual({
title: 'Remote-Zugriff',
description:
'Behalten Sie Ihre Systeme von ueberall im Blick und erhalten Sie Alarm-Push-Benachrichtigungen ueber die Pulse-Mobile-App — ohne Portfreigaben oder VPN.',
});
});
it('localizes settings header metadata and read-only infrastructure copy', () => {
const spanishMeta = getSettingsHeaderMeta('es');
expect(spanishMeta['system-network']).toEqual({
@@ -363,21 +363,18 @@ describe('shouldBlockSettingsRouteItem', () => {
'security-users',
'security-audit',
'security-webhooks',
] as const)(
'does NOT block panel-owned tab %s even when its features are missing',
(tab) => {
expect(
shouldBlockSettingsRouteItem(
tab,
createContext({
hasFeature: hasFeatures([]),
presentationPolicyResolved: true,
settingsCapabilitiesResolved: false,
}),
),
).toBe(false);
},
);
] as const)('does NOT block panel-owned tab %s even when its features are missing', (tab) => {
expect(
shouldBlockSettingsRouteItem(
tab,
createContext({
hasFeature: hasFeatures([]),
presentationPolicyResolved: true,
settingsCapabilitiesResolved: false,
}),
),
).toBe(false);
});
});
describe('non-panel-owned hideWhenUnavailable tabs are feature-gated for routing', () => {
@@ -456,8 +453,17 @@ describe('isSettingsNavItemLocked', () => {
expect(
isSettingsNavItemLocked('security-roles', createContext({ hasFeature: hasFeatures([]) })),
).toBe(false);
});
it('reports system-relay as locked for free installs (visible nav item, panel-owned gate)', () => {
expect(
isSettingsNavItemLocked('system-relay', createContext({ hasFeature: hasFeatures([]) })),
).toBe(true);
expect(
isSettingsNavItemLocked(
'system-relay',
createContext({ hasFeature: hasFeatures(['relay']) }),
),
).toBe(false);
});
@@ -476,11 +482,11 @@ describe('isSettingsNavItemLocked', () => {
).toBe(false);
});
it('never reports a real catalog tab as locked (every feature-gated tab also has hideWhenUnavailable)', () => {
// See GLM_REPORT: the `return isTabLocked(...)` -> true outcome is unreachable
// for real catalog items, so every real tab resolves to false here.
it('never reports other real catalog tabs as locked (their feature gates also carry hideWhenUnavailable)', () => {
// system-relay is the one deliberate exception: it stays visible without the
// relay feature and reports locked (see the dedicated test above). Every
// other feature-gated tab still hides instead of locking.
const tabs: SettingsTab[] = [
'system-relay',
'security-webhooks',
'organization-overview',
'organization-access',
@@ -492,9 +498,9 @@ describe('isSettingsNavItemLocked', () => {
'support-diagnostics',
];
for (const tab of tabs) {
expect(
isSettingsNavItemLocked(tab, createContext({ hasFeature: hasFeatures([]) })),
).toBe(false);
expect(isSettingsNavItemLocked(tab, createContext({ hasFeature: hasFeatures([]) }))).toBe(
false,
);
}
});
});
@@ -135,9 +135,32 @@ describe('settingsNavigation integration scaffold', () => {
).toBe(false);
});
it('keeps the Remote Access tab visible to free installs so Relay stays discoverable', () => {
// system-relay deliberately omits hideWhenUnavailable: the panel renders its
// own upgrade gate, so free installs see the entry instead of never learning
// the capability exists.
expect(
shouldHideSettingsNavItem('system-relay', {
hasFeature: hasFeatures([]),
runtimeCapabilitiesLoaded: () => true,
hostedModeEnabled: false,
settingsCapabilitiesResolved: true,
settingsCapabilities: { relayRead: true },
}),
).toBe(false);
expect(
shouldBlockSettingsRouteItem('system-relay', {
hasFeature: hasFeatures([]),
runtimeCapabilitiesLoaded: () => true,
hostedModeEnabled: false,
settingsCapabilitiesResolved: true,
settingsCapabilities: { relayRead: true },
}),
).toBe(false);
});
it('hides paid-only self-hosted tabs from free installs', () => {
for (const tab of [
'system-relay',
'security-roles',
'security-users',
'security-audit',
@@ -186,7 +209,6 @@ describe('settingsNavigation integration scaffold', () => {
it('keeps direct panel-owned feature gates routeable even when hidden from navigation', () => {
for (const tab of [
'system-relay',
'support-reporting',
'security-roles',
'security-users',
@@ -280,7 +280,9 @@ export const SETTINGS_NAV_GROUPS: SettingsNavGroup[] = [
icon: RadioTower,
iconProps: { strokeWidth: 2 },
features: ['relay'],
hideWhenUnavailable: true,
// Deliberately visible without the relay feature: the panel renders its
// own upgrade gate, and hiding the item made Relay undiscoverable for
// free installs.
requiredCapability: 'relayRead',
},
],
@@ -0,0 +1,72 @@
import { cleanup, render, screen } from '@solidjs/testing-library';
import { Route, Router } from '@solidjs/router';
import type { JSX } from 'solid-js';
import { afterEach, describe, expect, it } from 'vitest';
import { AlertPushDestinationsSection } from './AlertPushDestinationsSection';
import {
ALERT_DESTINATIONS_PUSH_GATE_MESSAGE,
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(() => (
<Router>
<Route path="/" component={ui} />
</Router>
));
}
describe('AlertPushDestinationsSection', () => {
afterEach(() => {
cleanup();
});
it('points licensed installs at Remote Access settings', () => {
renderWithRouter(() => (
<AlertPushDestinationsSection
relayLicensed={true}
showUpgradePrompts={true}
upgradeDestination={upgradeDestination}
/>
));
expect(screen.getByText(ALERT_DESTINATIONS_PUSH_READY_MESSAGE)).toBeInTheDocument();
const link = screen.getByRole('link', {
name: `${ALERT_DESTINATIONS_PUSH_SETUP_LINK_LABEL}`,
});
expect(link).toHaveAttribute('href', '/settings/system-relay');
});
it('shows the Relay upgrade gate to unlicensed installs', () => {
renderWithRouter(() => (
<AlertPushDestinationsSection
relayLicensed={false}
showUpgradePrompts={true}
upgradeDestination={upgradeDestination}
/>
));
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(() => (
<AlertPushDestinationsSection
relayLicensed={false}
showUpgradePrompts={false}
upgradeDestination={upgradeDestination}
/>
));
expect(screen.getByText(ALERT_DESTINATIONS_PUSH_GATE_MESSAGE)).toBeInTheDocument();
expect(screen.queryByRole('link')).not.toBeInTheDocument();
});
});
@@ -0,0 +1,53 @@
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_PANEL_DESCRIPTION,
ALERT_DESTINATIONS_PUSH_PANEL_TITLE,
ALERT_DESTINATIONS_PUSH_READY_MESSAGE,
ALERT_DESTINATIONS_PUSH_SETUP_LINK_LABEL,
} from '@/utils/alertDestinationsPresentation';
interface AlertPushDestinationsSectionProps {
relayLicensed: boolean;
showUpgradePrompts: boolean;
upgradeDestination: UpgradeDestination;
}
export function AlertPushDestinationsSection(props: AlertPushDestinationsSectionProps) {
return (
<SettingsPanel
title={ALERT_DESTINATIONS_PUSH_PANEL_TITLE}
description={ALERT_DESTINATIONS_PUSH_PANEL_DESCRIPTION}
class="min-w-0"
bodyClass=""
>
<Show
when={props.relayLicensed}
fallback={
<FeatureGateSection
icon={<Smartphone size={20} strokeWidth={2} />}
title={ALERT_DESTINATIONS_PUSH_GATE_TITLE}
body={ALERT_DESTINATIONS_PUSH_GATE_MESSAGE}
upgradeDestination={props.upgradeDestination}
showUpgradePrompts={props.showUpgradePrompts}
/>
}
>
<div class="flex flex-col gap-2">
<p class="text-sm text-muted">{ALERT_DESTINATIONS_PUSH_READY_MESSAGE}</p>
<a
href="/settings/system-relay"
class="text-sm font-medium text-blue-600 dark:text-blue-400 hover:underline"
>
{ALERT_DESTINATIONS_PUSH_SETUP_LINK_LABEL}
</a>
</div>
</Show>
</SettingsPanel>
);
}
@@ -1,11 +1,18 @@
import { Show } from 'solid-js';
import { hasFeature } from '@/stores/license';
import { getUpgradeActionDestination } from '@/stores/licenseCommercial';
import { presentationPolicyHidesUpgradePrompts } from '@/stores/sessionPresentationPolicy';
import { AlertAppriseDestinationsSection } from '../AlertAppriseDestinationsSection';
import { AlertDestinationsLoadErrorCard } from '../AlertDestinationsLoadErrorCard';
import { AlertDestinationsLoadingState } from '../AlertDestinationsLoadingState';
import { AlertEmailDestinationsSection } from '../AlertEmailDestinationsSection';
import { AlertPushDestinationsSection } from '../AlertPushDestinationsSection';
import { AlertWebhookDestinationsSection } from '../AlertWebhookDestinationsSection';
import { useAlertDestinationsTabState, type AlertDestinationsTabStateProps } from '../useAlertDestinationsTabState';
import {
useAlertDestinationsTabState,
type AlertDestinationsTabStateProps,
} from '../useAlertDestinationsTabState';
export interface DestinationsTabProps extends AlertDestinationsTabStateProps {
setHasUnsavedChanges: (value: boolean) => void;
@@ -17,10 +24,7 @@ export function DestinationsTab(props: DestinationsTabProps) {
return (
<div class="flex w-full max-w-full flex-col gap-6 md:gap-8">
<Show
when={!state.isLoading()}
fallback={<AlertDestinationsLoadingState />}
>
<Show when={!state.isLoading()} fallback={<AlertDestinationsLoadingState />}>
<Show when={state.hasLoadError()}>
<AlertDestinationsLoadErrorCard
error={props.configLoadError() || state.webhookLoadError() || ''}
@@ -53,6 +57,12 @@ export function DestinationsTab(props: DestinationsTabProps) {
testWebhook={state.testWebhook}
testingWebhook={state.testingWebhook()}
/>
<AlertPushDestinationsSection
relayLicensed={hasFeature('relay')}
showUpgradePrompts={!presentationPolicyHidesUpgradePrompts()}
upgradeDestination={getUpgradeActionDestination('relay')}
/>
</Show>
</div>
);
+2 -2
View File
@@ -97,7 +97,7 @@ export const DE_MESSAGE_OVERRIDES = {
'Pruefen Sie aktive Vorfaelle, Warnungsverlauf, Schwellwerte, Benachrichtigungen und Zeitplaene.',
'alerts.page.default.title': 'Warnmeldungen',
'alerts.page.destinations.description':
'Leiten Sie Warnmeldungen an E-Mail-, Apprise- und Webhook-Ziele weiter.',
'Leiten Sie Warnmeldungen an E-Mail-, Apprise-, Webhook- und Mobile-Push-Ziele weiter.',
'alerts.page.destinations.title': 'Benachrichtigungen',
'alerts.page.history.description':
'Suchen Sie fruehere Warnmeldungen, pruefen Sie Vorfall-Zeitleisten und beobachten Sie Warnungshaeufigkeit ueber Zeit.',
@@ -423,7 +423,7 @@ 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':
'Konfigurieren Sie Pulse-Relay-Konnektivitaet fuer sicheren Remote-Zugriff und Pulse-Mobile-Kopplung.',
'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',
'settings.header.systemUpdates.description':
'Verwalten Sie Versionspruefungen, Update-Kanaele und automatisches Update-Verhalten.',
+2 -2
View File
@@ -92,7 +92,7 @@ export const ES_MESSAGE_OVERRIDES = {
'Revisa incidentes activos, el historial de alertas, umbrales, notificaciones y programaciones.',
'alerts.page.default.title': 'Alertas',
'alerts.page.destinations.description':
'Enruta notificaciones de alerta a destinos de email, Apprise y webhook.',
'Enruta notificaciones de alerta a destinos de email, Apprise, webhook y push móvil.',
'alerts.page.destinations.title': 'Notificaciones',
'alerts.page.history.description':
'Busca alertas anteriores, revisa lineas de tiempo de incidentes e inspecciona la frecuencia de alertas.',
@@ -416,7 +416,7 @@ 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':
'Configura la conectividad de relay de Pulse para acceso remoto seguro y vinculación con Pulse Mobile.',
'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',
'settings.header.systemUpdates.description':
'Administra comprobaciones de versión, canales de actualización y comportamiento de actualización automática.',
+2 -2
View File
@@ -181,7 +181,7 @@ export const EN_MESSAGES = {
'Review active incidents, inspect alert history, and manage thresholds, notifications, and schedules.',
'alerts.page.default.title': 'Alerts',
'alerts.page.destinations.description':
'Route alert notifications to email, Apprise, and webhook destinations.',
'Route alert notifications to email, Apprise, webhook, and mobile push destinations.',
'alerts.page.destinations.title': 'Notifications',
'alerts.page.history.description':
'Search prior alerts, review incident timelines, and inspect alert frequency over time.',
@@ -408,7 +408,7 @@ export const EN_MESSAGES = {
'Manage backup/snapshot polling plus configuration export and import workflows.',
'settings.header.systemRecovery.title': 'Recovery',
'settings.header.systemRelay.description':
'Configure Pulse relay connectivity for secure remote access and Pulse Mobile pairing.',
'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',
'settings.header.systemUpdates.description':
'Manage version checks, update channels, and automatic update behavior.',
@@ -1,6 +1,12 @@
import { describe, expect, it } from 'vitest';
import {
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,
ALERT_DESTINATIONS_PUSH_SETUP_LINK_LABEL,
ALERT_DESTINATIONS_APPRISE_API_KEY_HEADER_HELP,
ALERT_DESTINATIONS_APPRISE_ENABLE_FOR_TEST_ERROR,
ALERT_DESTINATIONS_APPRISE_MISSING_TARGETS_ERROR,
@@ -107,4 +113,18 @@ describe('alertDestinationsPresentation', () => {
'Unable to send the test notification.',
);
});
it('returns canonical mobile push destination copy', () => {
expect(ALERT_DESTINATIONS_PUSH_PANEL_TITLE).toBe('Mobile push notifications');
expect(ALERT_DESTINATIONS_PUSH_PANEL_DESCRIPTION).toBe(
'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_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');
});
});
@@ -147,7 +147,7 @@ describe('alertOverviewPresentation', () => {
'Tune thresholds and scoped overrides for infrastructure, workloads, and integrations.',
);
expect(ALERTS_PAGE_DESTINATIONS_DESCRIPTION).toBe(
'Route alert notifications to email, Apprise, and webhook destinations.',
'Route alert notifications to email, Apprise, webhook, and mobile push destinations.',
);
expect(ALERTS_PAGE_SCHEDULE_DESCRIPTION).toBe(
'Define quiet hours, grouping, cooldowns, recovery, and escalation behavior for alert delivery.',
@@ -168,7 +168,8 @@ describe('alertOverviewPresentation', () => {
},
destinations: {
title: 'Notifications',
description: 'Route alert notifications to email, Apprise, and webhook destinations.',
description:
'Route alert notifications to email, Apprise, webhook, and mobile push destinations.',
},
schedule: {
title: 'Maintenance Schedule',
@@ -104,13 +104,22 @@ 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 and higher plans');
// 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');
expect(RELAY_PAIRING_AVAILABILITY_MESSAGE).toContain('QR code or deep link');
expect(RELAY_ENABLE_HELP_TEXT).toContain('Pulse Mobile pairing');
expect(RELAY_PAIRING_AVAILABILITY_MESSAGE).toContain('QR code');
expect(RELAY_PAIRING_AVAILABILITY_MESSAGE).toContain('deep link');
expect(RELAY_PAIRING_AVAILABILITY_MESSAGE).toContain('push notifications');
expect(RELAY_ENABLE_HELP_TEXT).toContain('Pulse Mobile devices');
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');
@@ -61,13 +61,19 @@ export const ALERT_DESTINATIONS_APPRISE_MISSING_TARGETS_ERROR =
'Add at least one Apprise target to test CLI delivery.';
export const ALERT_DESTINATIONS_APPRISE_MISSING_SERVER_URL_ERROR =
'Enter an Apprise API server URL to test API delivery.';
export const ALERT_DESTINATIONS_EMAIL_TEST_SUCCESS =
'Test email sent. Check your inbox.';
export const ALERT_DESTINATIONS_EMAIL_TEST_SUCCESS = 'Test email sent. Check your inbox.';
export const ALERT_DESTINATIONS_EMAIL_TEST_FAILURE = 'Unable to send the test email.';
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_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.';
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_APPRISE_TEST_SUCCESS = 'Test Apprise notification sent.';
export const ALERT_DESTINATIONS_APPRISE_TEST_FAILURE = 'Unable to send the test notification.';
export function getAlertDestinationsConfigLoadError() {
return ALERT_DESTINATIONS_CONFIG_LOAD_ERROR;
@@ -31,7 +31,7 @@ export const ALERTS_PAGE_OVERVIEW_DESCRIPTION =
export const ALERTS_PAGE_THRESHOLDS_DESCRIPTION =
'Tune thresholds and scoped overrides for infrastructure, workloads, and integrations.';
export const ALERTS_PAGE_DESTINATIONS_DESCRIPTION =
'Route alert notifications to email, Apprise, and webhook destinations.';
'Route alert notifications to email, Apprise, webhook, and mobile push destinations.';
export const ALERTS_PAGE_SCHEDULE_DESCRIPTION =
'Define quiet hours, grouping, cooldowns, recovery, and escalation behavior for alert delivery.';
export const ALERTS_PAGE_HISTORY_DESCRIPTION =
@@ -7,7 +7,8 @@ export interface RelayConnectionPresentation {
pulse: boolean;
}
const RELAY_MISSING_TOKEN_ERROR = /\b(?:no license token available|license token provider not configured)\b/i;
const RELAY_MISSING_TOKEN_ERROR =
/\b(?:no license token available|license token provider not configured)\b/i;
export const RELAY_READONLY_NOTICE_CLASS =
'border border-blue-200 text-xs text-blue-800 dark:border-blue-800 dark:text-blue-200';
@@ -29,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 =
'Configure Pulse relay connectivity for secure remote access and Pulse Mobile pairing.';
'Check on your systems and get alert push notifications anywhere with the Pulse Mobile app — no port forwarding or VPN required.';
export const RELAY_LICENSE_REQUIRED_MESSAGE =
'Remote access is available with Relay and higher plans. Pair supported Pulse Mobile clients with this instance using a QR code or deep link.';
'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';
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.';
'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.';
export const RELAY_ENABLE_HELP_TEXT =
'Connect this Pulse instance to the relay server for secure remote access and Pulse Mobile pairing.';
'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.';
export const RELAY_PAIRING_APP_AVAILABILITY_TEXT =
'Pulse Mobile is in early access; install links for Relay and Pro plans are on';
'Pulse Mobile is in early access for iOS and Android; to get the install links, enter your activation key on';
export const RELAY_PAIRING_APP_DOWNLOAD_LINK_LABEL = 'your download page';
export const RELAY_PAIRING_APP_DOWNLOAD_LINK_CLASS =
'font-medium text-blue-600 underline-offset-2 hover:underline dark:text-blue-400';