diff --git a/docs/release-control/v6/internal/subsystems/alerts.md b/docs/release-control/v6/internal/subsystems/alerts.md index 3cbba5e92..10a7260f6 100644 --- a/docs/release-control/v6/internal/subsystems/alerts.md +++ b/docs/release-control/v6/internal/subsystems/alerts.md @@ -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 diff --git a/docs/release-control/v6/internal/subsystems/cloud-paid.md b/docs/release-control/v6/internal/subsystems/cloud-paid.md index b9b82e083..85c14256f 100644 --- a/docs/release-control/v6/internal/subsystems/cloud-paid.md +++ b/docs/release-control/v6/internal/subsystems/cloud-paid.md @@ -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 diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index e470ccec5..cda0945c5 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -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 diff --git a/frontend-modern/src/components/Settings/RelayPairingSection.tsx b/frontend-modern/src/components/Settings/RelayPairingSection.tsx index edb74ddd2..3ec1084cd 100644 --- a/frontend-modern/src/components/Settings/RelayPairingSection.tsx +++ b/frontend-modern/src/components/Settings/RelayPairingSection.tsx @@ -66,8 +66,8 @@ export const RelayPairingSection: Component = (props)

- 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}{' '} { 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(); diff --git a/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts b/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts index a3848b426..536a866fc 100644 --- a/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts +++ b/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts @@ -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', ); 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 899e13461..20b267fda 100644 --- a/frontend-modern/src/components/Settings/__tests__/settingsHeaderMeta.branchcov0713.test.ts +++ b/frontend-modern/src/components/Settings/__tests__/settingsHeaderMeta.branchcov0713.test.ts @@ -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'), + ); }); }); diff --git a/frontend-modern/src/components/Settings/__tests__/settingsLocalization.test.ts b/frontend-modern/src/components/Settings/__tests__/settingsLocalization.test.ts index 1699fdb2c..b9f917b15 100644 --- a/frontend-modern/src/components/Settings/__tests__/settingsLocalization.test.ts +++ b/frontend-modern/src/components/Settings/__tests__/settingsLocalization.test.ts @@ -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({ diff --git a/frontend-modern/src/components/Settings/__tests__/settingsNavVisibility.branchcov2.test.ts b/frontend-modern/src/components/Settings/__tests__/settingsNavVisibility.branchcov2.test.ts index e1d724dd0..cdeb06b36 100644 --- a/frontend-modern/src/components/Settings/__tests__/settingsNavVisibility.branchcov2.test.ts +++ b/frontend-modern/src/components/Settings/__tests__/settingsNavVisibility.branchcov2.test.ts @@ -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, + ); } }); }); 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 511976923..675a0702c 100644 --- a/frontend-modern/src/components/Settings/__tests__/settingsNavigation.integration.test.tsx +++ b/frontend-modern/src/components/Settings/__tests__/settingsNavigation.integration.test.tsx @@ -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', diff --git a/frontend-modern/src/components/Settings/settingsNavCatalog.ts b/frontend-modern/src/components/Settings/settingsNavCatalog.ts index 32a22f2f6..a944ac00b 100644 --- a/frontend-modern/src/components/Settings/settingsNavCatalog.ts +++ b/frontend-modern/src/components/Settings/settingsNavCatalog.ts @@ -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', }, ], diff --git a/frontend-modern/src/features/alerts/AlertPushDestinationsSection.test.tsx b/frontend-modern/src/features/alerts/AlertPushDestinationsSection.test.tsx new file mode 100644 index 000000000..248e274b6 --- /dev/null +++ b/frontend-modern/src/features/alerts/AlertPushDestinationsSection.test.tsx @@ -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(() => ( + + + + )); +} + +describe('AlertPushDestinationsSection', () => { + afterEach(() => { + cleanup(); + }); + + it('points licensed installs at Remote Access settings', () => { + renderWithRouter(() => ( + + )); + + 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(() => ( + + )); + + 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.queryByRole('link')).not.toBeInTheDocument(); + }); +}); diff --git a/frontend-modern/src/features/alerts/AlertPushDestinationsSection.tsx b/frontend-modern/src/features/alerts/AlertPushDestinationsSection.tsx new file mode 100644 index 000000000..567054ca5 --- /dev/null +++ b/frontend-modern/src/features/alerts/AlertPushDestinationsSection.tsx @@ -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 ( + + } + title={ALERT_DESTINATIONS_PUSH_GATE_TITLE} + body={ALERT_DESTINATIONS_PUSH_GATE_MESSAGE} + upgradeDestination={props.upgradeDestination} + showUpgradePrompts={props.showUpgradePrompts} + /> + } + > +

+

{ALERT_DESTINATIONS_PUSH_READY_MESSAGE}

+
+ {ALERT_DESTINATIONS_PUSH_SETUP_LINK_LABEL} → + +
+ + + ); +} diff --git a/frontend-modern/src/features/alerts/tabs/DestinationsTab.tsx b/frontend-modern/src/features/alerts/tabs/DestinationsTab.tsx index 97b53e9ec..54ffac80a 100644 --- a/frontend-modern/src/features/alerts/tabs/DestinationsTab.tsx +++ b/frontend-modern/src/features/alerts/tabs/DestinationsTab.tsx @@ -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 (
- } - > + }> + +
); diff --git a/frontend-modern/src/i18n/messages.de.ts b/frontend-modern/src/i18n/messages.de.ts index dab9b515b..bb45d5524 100644 --- a/frontend-modern/src/i18n/messages.de.ts +++ b/frontend-modern/src/i18n/messages.de.ts @@ -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.', diff --git a/frontend-modern/src/i18n/messages.es.ts b/frontend-modern/src/i18n/messages.es.ts index 5edd54d54..c7ca68010 100644 --- a/frontend-modern/src/i18n/messages.es.ts +++ b/frontend-modern/src/i18n/messages.es.ts @@ -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.', diff --git a/frontend-modern/src/i18n/messages.ts b/frontend-modern/src/i18n/messages.ts index 2373526a5..a530322f4 100644 --- a/frontend-modern/src/i18n/messages.ts +++ b/frontend-modern/src/i18n/messages.ts @@ -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.', diff --git a/frontend-modern/src/utils/__tests__/alertDestinationsPresentation.test.ts b/frontend-modern/src/utils/__tests__/alertDestinationsPresentation.test.ts index a6415de96..1e1df3a8a 100644 --- a/frontend-modern/src/utils/__tests__/alertDestinationsPresentation.test.ts +++ b/frontend-modern/src/utils/__tests__/alertDestinationsPresentation.test.ts @@ -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'); + }); }); diff --git a/frontend-modern/src/utils/__tests__/alertOverviewPresentation.test.ts b/frontend-modern/src/utils/__tests__/alertOverviewPresentation.test.ts index ba1c430d9..5868f7418 100644 --- a/frontend-modern/src/utils/__tests__/alertOverviewPresentation.test.ts +++ b/frontend-modern/src/utils/__tests__/alertOverviewPresentation.test.ts @@ -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', diff --git a/frontend-modern/src/utils/__tests__/relayPresentation.test.ts b/frontend-modern/src/utils/__tests__/relayPresentation.test.ts index a8e3e78ca..a9809f2a0 100644 --- a/frontend-modern/src/utils/__tests__/relayPresentation.test.ts +++ b/frontend-modern/src/utils/__tests__/relayPresentation.test.ts @@ -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'); diff --git a/frontend-modern/src/utils/alertDestinationsPresentation.ts b/frontend-modern/src/utils/alertDestinationsPresentation.ts index 3924bf210..588b2d49e 100644 --- a/frontend-modern/src/utils/alertDestinationsPresentation.ts +++ b/frontend-modern/src/utils/alertDestinationsPresentation.ts @@ -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; diff --git a/frontend-modern/src/utils/alertOverviewPresentation.ts b/frontend-modern/src/utils/alertOverviewPresentation.ts index 4644f2ba4..067147256 100644 --- a/frontend-modern/src/utils/alertOverviewPresentation.ts +++ b/frontend-modern/src/utils/alertOverviewPresentation.ts @@ -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 = diff --git a/frontend-modern/src/utils/relayPresentation.ts b/frontend-modern/src/utils/relayPresentation.ts index cb13ba1ef..502f98c22 100644 --- a/frontend-modern/src/utils/relayPresentation.ts +++ b/frontend-modern/src/utils/relayPresentation.ts @@ -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';