diff --git a/frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx b/frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx
index d771d56e1..d6f0cdae7 100644
--- a/frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx
+++ b/frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx
@@ -12,6 +12,7 @@ import { CountdownTimer } from '@/components/patrol';
import { presentationPolicyHidesUpgradePrompts } from '@/stores/sessionPresentationPolicy';
import { formatRelativeTime } from '@/utils/format';
import { groupModelsByProvider } from '@/utils/patrolFormat';
+import { getPatrolPageHeaderMeta } from '@/utils/patrolPagePresentation';
import {
getAIQuickstartCreditsPresentation,
isPatrolQuickstartExhaustedReason,
@@ -23,6 +24,7 @@ import type { PatrolIntelligenceState } from './usePatrolIntelligenceState';
export function PatrolIntelligenceHeader(props: { state: PatrolIntelligenceState }) {
const state = props.state;
+ const headerMeta = getPatrolPageHeaderMeta();
const quickstartPresentation = createMemo(() =>
getAIQuickstartCreditsPresentation(
state.patrolStatus()?.quickstart_credits_remaining ?? 0,
@@ -49,7 +51,9 @@ export function PatrolIntelligenceHeader(props: { state: PatrolIntelligenceState
const patrolStatus = state.patrolStatus();
if (!patrolStatus) return false;
if (patrolStatus.using_quickstart) return true;
- return state.runtimeState() === 'blocked' && isPatrolQuickstartExhaustedReason(state.blockedReason());
+ return (
+ state.runtimeState() === 'blocked' && isPatrolQuickstartExhaustedReason(state.blockedReason())
+ );
});
const patrolModelStale = createMemo(() => {
const model = state.patrolModel();
@@ -62,13 +66,14 @@ export function PatrolIntelligenceHeader(props: { state: PatrolIntelligenceState
- Patrol
+ {headerMeta.title}
}
class="mb-3"
diff --git a/frontend-modern/src/pages/Alerts.tsx b/frontend-modern/src/pages/Alerts.tsx
index a4f684793..274f86999 100644
--- a/frontend-modern/src/pages/Alerts.tsx
+++ b/frontend-modern/src/pages/Alerts.tsx
@@ -11,7 +11,7 @@ import {
import { useLocation, useNavigate } from '@solidjs/router';
import { logger } from '@/utils/logger';
import { Card } from '@/components/shared/Card';
-import { SectionHeader } from '@/components/shared/SectionHeader';
+import { PageHeader } from '@/components/shared/PageHeader';
import { notificationStore } from '@/stores/notifications';
import Calendar from 'lucide-solid/icons/calendar';
@@ -34,12 +34,8 @@ import {
getAlertsTabTitle,
isAlertsConfigurationTab,
} from '@/utils/alertTabsPresentation';
-import {
- getAlertsPageHeaderMeta,
-} from '@/utils/alertOverviewPresentation';
-import {
- getAlertConfigLeaveConfirmation,
-} from '@/utils/alertConfigPresentation';
+import { getAlertsPageHeaderMeta } from '@/utils/alertOverviewPresentation';
+import { getAlertConfigLeaveConfirmation } from '@/utils/alertConfigPresentation';
import { useAlertsActivation } from '@/stores/alertsActivation';
import LayoutDashboard from 'lucide-solid/icons/layout-dashboard';
import History from 'lucide-solid/icons/history';
@@ -49,12 +45,7 @@ import { presentationPolicyIsReadOnly } from '@/stores/sessionPresentationPolicy
import { AlertsConfigurationSurface } from '@/features/alerts/AlertsConfigurationSurface';
import { OverviewTab } from '@/features/alerts/OverviewTab';
import { HistoryTab } from '@/features/alerts/tabs/HistoryTab';
-import {
- pathForTab,
- tabFromPath,
- type AlertTab,
- type Override,
-} from '@/features/alerts/types';
+import { pathForTab, tabFromPath, type AlertTab, type Override } from '@/features/alerts/types';
export function Alerts() {
const { activeAlerts, updateAlert, removeAlerts } = useWebSocket();
@@ -66,9 +57,7 @@ export function Alerts() {
const readOnlySession = createMemo(() => presentationPolicyIsReadOnly());
const isAlertsActive = createMemo(() => alertsActivation.activationState() === 'active');
const areAlertsDisabled = createMemo(() => !isAlertsActive());
- const alertsConfigurationLocked = createMemo(
- () => !readOnlySession() && areAlertsDisabled(),
- );
+ const alertsConfigurationLocked = createMemo(() => !readOnlySession() && areAlertsDisabled());
const alertActivationPresentation = createMemo(() =>
getAlertActivationPresentation({
isActive: isAlertsActive(),
@@ -127,8 +116,7 @@ export function Alerts() {
const [overviewOverrides, setOverviewOverrides] = createSignal([]);
const alertsPageHeaderMeta = getAlertsPageHeaderMeta();
- const headerMeta = () =>
- alertsPageHeaderMeta[activeTab()] ?? alertsPageHeaderMeta.default;
+ const headerMeta = () => alertsPageHeaderMeta[activeTab()] ?? alertsPageHeaderMeta.default;
createEffect(() => {
const currentPath = location.pathname;
@@ -175,8 +163,12 @@ export function Alerts() {
localStorage.getItem('hideAlertsQuickTip') !== 'true',
);
- const runtimeCapabilitiesLoading = createMemo(() => !runtimeCapabilitiesLoaded() || entitlementsLoading());
- const hasAIAlertsFeature = createMemo(() => !runtimeCapabilitiesLoaded() || hasFeature('ai_alerts'));
+ const runtimeCapabilitiesLoading = createMemo(
+ () => !runtimeCapabilitiesLoaded() || entitlementsLoading(),
+ );
+ const hasAIAlertsFeature = createMemo(
+ () => !runtimeCapabilitiesLoaded() || hasFeature('ai_alerts'),
+ );
createEffect((wasPaywallVisible) => {
const isPaywallVisible =
@@ -253,11 +245,11 @@ export function Alerts() {
const [sidebarCollapsed, setSidebarCollapsed] = createSignal(false);
return (
-
- {/* Header with better styling */}
-
-
-
+
+
@@ -284,180 +276,172 @@ export function Alerts() {
-
-
+ }
+ />
-
-
+
+
-
-
-
-
Alerts
-
-
-
-
+
+
+
Alerts
-
-
-
-
-
-
-
0}>
-
-
-
-
- {(tab) => (
-
- )}
-
-
-
-
- {/* Tab Content */}
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
-
-
+
+
+
+
0}>
+
+
+
+
+ {(tab) => (
+
+ )}
+
+
+
+
+
+
+
+
+
);
}
diff --git a/frontend-modern/src/pages/__tests__/AIIntelligence.test.tsx b/frontend-modern/src/pages/__tests__/AIIntelligence.test.tsx
index cfe675588..594287ddf 100644
--- a/frontend-modern/src/pages/__tests__/AIIntelligence.test.tsx
+++ b/frontend-modern/src/pages/__tests__/AIIntelligence.test.tsx
@@ -256,9 +256,10 @@ vi.mock('@/hooks/usePatrolStream', () => ({
}));
vi.mock('@/components/shared/PageHeader', () => ({
- PageHeader: (props: { title?: string; actions?: unknown }) => (
+ PageHeader: (props: { title?: string; description?: string; actions?: unknown }) => (
{props.title}
+
{props.description}
{props.actions as any}
),
@@ -387,8 +388,8 @@ describe('AIIntelligence entitlement gating', () => {
href: getPublicPricingUrl(feature),
external: true,
}));
- getUpgradeActionUrlOrFallbackMock.mockImplementation(
- (feature?: string) => getPublicPricingUrl(feature),
+ getUpgradeActionUrlOrFallbackMock.mockImplementation((feature?: string) =>
+ getPublicPricingUrl(feature),
);
getCorrelationsMock.mockResolvedValue({
correlations: [],
@@ -659,7 +660,9 @@ describe('AIIntelligence entitlement gating', () => {
expect(screen.getByText(/Health A · 91\/100/)).toBeInTheDocument();
expect(screen.queryByText('Supporting context')).not.toBeInTheDocument();
- expect(screen.queryByText('1 recent change · 4 policy-covered resources')).not.toBeInTheDocument();
+ expect(
+ screen.queryByText('1 recent change · 4 policy-covered resources'),
+ ).not.toBeInTheDocument();
expect(screen.queryByText('Policy posture')).not.toBeInTheDocument();
});
diff --git a/frontend-modern/src/pages/__tests__/Alerts.helpers.test.ts b/frontend-modern/src/pages/__tests__/Alerts.helpers.test.ts
index 18eb50bdc..61c410bea 100644
--- a/frontend-modern/src/pages/__tests__/Alerts.helpers.test.ts
+++ b/frontend-modern/src/pages/__tests__/Alerts.helpers.test.ts
@@ -217,6 +217,11 @@ describe('tab path helpers', () => {
});
it('keeps alerts configuration owned by a feature surface instead of the page shell', () => {
+ expect(alertsPageSource).toContain(
+ "import { PageHeader } from '@/components/shared/PageHeader';",
+ );
+ expect(alertsPageSource).toContain(' {
expect(alertDestinationsModelSource).toContain('export function buildAppriseConfigPayload');
expect(alertDestinationsModelSource).toContain('formatAppriseTargets');
expect(alertDestinationsModelSource).toContain('parseAppriseTargets');
- expect(alertDestinationsTabStateSource).toContain('export function useAlertDestinationsTabState');
+ expect(alertDestinationsTabStateSource).toContain(
+ 'export function useAlertDestinationsTabState',
+ );
expect(alertDestinationsTabStateSource).toContain('NotificationsAPI.testNotification');
expect(alertDestinationsTabStateSource).toContain('useAlertWebhookDestinationsState');
expect(alertDestinationsTabStateSource).not.toContain('NotificationsAPI.getWebhooks');
@@ -359,21 +366,21 @@ describe('tab path helpers', () => {
expect(alertHistoryTabSource).not.toContain('const loadIncidentTimeline = async');
expect(alertHistoryTabSource).not.toContain('const saveIncidentNote = async');
expect(alertHistoryTabSource).not.toContain('usePersistentSignal(');
- expect(alertHistoryTabSource).not.toContain("const [searchTerm, setSearchTerm] = createSignal");
+ expect(alertHistoryTabSource).not.toContain('const [searchTerm, setSearchTerm] = createSignal');
expect(alertHistoryFrequencyCardSource).toContain('export function AlertHistoryFrequencyCard');
expect(alertHistoryFrequencyCardSource).toContain('getAlertFrequencySelectionPresentation');
expect(alertHistoryFrequencyCardSource).toContain('getAlertBucketCountLabel');
expect(alertHistoryFiltersCardSource).toContain('export function AlertHistoryFiltersCard');
expect(alertHistoryFiltersCardSource).toContain('getAlertHistorySearchPlaceholder');
- expect(alertResourceIncidentsPanelSource).toContain('export function AlertResourceIncidentsPanel');
+ expect(alertResourceIncidentsPanelSource).toContain(
+ 'export function AlertResourceIncidentsPanel',
+ );
expect(alertResourceIncidentsPanelSource).toContain('IncidentEventFilters');
expect(alertResourceIncidentsPanelSource).toContain('IncidentTimelineEventCard');
expect(alertResourceIncidentsPanelSource).toContain('buildResolvedResourceSurfaceLinks');
expect(alertResourceIncidentsPanelSource).toContain('allowInfrastructureFallback: true');
expect(alertResourceIncidentsPanelSource).not.toContain('buildInfrastructureResourceLink');
- expect(alertResourceIncidentsPanelSource).not.toContain(
- 'buildResourceSurfaceLinksForResource',
- );
+ expect(alertResourceIncidentsPanelSource).not.toContain('buildResourceSurfaceLinksForResource');
expect(alertResourceIncidentsPanelSource).toContain('{link.compactLabel}');
expect(alertHistoryTableSectionSource).toContain('export function AlertHistoryTableSection');
expect(alertHistoryTableSectionSource).toContain('AlertHistoryTableGroupRow');
@@ -441,7 +448,9 @@ describe('tab path helpers', () => {
expect(alertOverviewAlertCardSource).toContain('getAlertOverviewStartedAtClass');
expect(alertOverviewAlertCardSource).toContain('getAlertOverviewPrimaryActionClass');
expect(alertOverviewAlertCardSource).toContain('getAlertOverviewSecondaryActionClass');
- expect(alertAcknowledgementStateSource).toContain('export function useAlertAcknowledgementState');
+ expect(alertAcknowledgementStateSource).toContain(
+ 'export function useAlertAcknowledgementState',
+ );
expect(alertAcknowledgementStateSource).toContain('AlertsAPI.bulkAcknowledge');
expect(alertAcknowledgementStateSource).toContain('AlertsAPI.acknowledge');
expect(alertAcknowledgementStateSource).toContain('AlertsAPI.unacknowledge');
@@ -483,28 +492,46 @@ describe('tab path helpers', () => {
expect(thresholdsTableSource).toContain(
"import { useThresholdsTableState } from '@/features/alerts/thresholds/hooks/useThresholdsTableState';",
);
- expect(thresholdsTableSource).toContain("import { ThresholdsTableProxmoxTab } from './ThresholdsTableProxmoxTab';");
- expect(thresholdsTableSource).toContain("import { ThresholdsTablePMGTab } from './ThresholdsTablePMGTab';");
- expect(thresholdsTableSource).toContain("import { ThresholdsTableAgentsTab } from './ThresholdsTableAgentsTab';");
- expect(thresholdsTableSource).toContain("import { ThresholdsTableDockerTab } from './ThresholdsTableDockerTab';");
+ expect(thresholdsTableSource).toContain(
+ "import { ThresholdsTableProxmoxTab } from './ThresholdsTableProxmoxTab';",
+ );
+ expect(thresholdsTableSource).toContain(
+ "import { ThresholdsTablePMGTab } from './ThresholdsTablePMGTab';",
+ );
+ expect(thresholdsTableSource).toContain(
+ "import { ThresholdsTableAgentsTab } from './ThresholdsTableAgentsTab';",
+ );
+ expect(thresholdsTableSource).toContain(
+ "import { ThresholdsTableDockerTab } from './ThresholdsTableDockerTab';",
+ );
expect(thresholdsTableSource).not.toContain('const [searchTerm, setSearchTerm] = createSignal');
expect(thresholdsTableSource).not.toContain('const handleTabClick =');
- expect(thresholdsTableSource).not.toContain("groupedResources={state.guestsGroupedByNode()}");
+ expect(thresholdsTableSource).not.toContain('groupedResources={state.guestsGroupedByNode()}');
expect(thresholdsTableSource).not.toContain('dockerIgnoredPrefixesPresentation.title');
expect(thresholdsTableProxmoxTabSource).toContain('export function ThresholdsTableProxmoxTab');
expect(thresholdsTableProxmoxTabSource).toContain('ThresholdsTableProxmoxNodesSection');
expect(thresholdsTableProxmoxTabSource).toContain('ThresholdsTableProxmoxPBSSection');
expect(thresholdsTableProxmoxTabSource).toContain('ThresholdsTableProxmoxGuestsSection');
- expect(thresholdsTableProxmoxTabSource).toContain('ThresholdsTableProxmoxGuestFilteringSection');
+ expect(thresholdsTableProxmoxTabSource).toContain(
+ 'ThresholdsTableProxmoxGuestFilteringSection',
+ );
expect(thresholdsTableProxmoxTabSource).toContain('ThresholdsTableProxmoxBackupsSection');
expect(thresholdsTableProxmoxTabSource).toContain('ThresholdsTableProxmoxSnapshotsSection');
expect(thresholdsTableProxmoxTabSource).toContain('ThresholdsTableProxmoxStorageSection');
expect(thresholdsTableProxmoxTabSource).not.toContain('backupOrphanedPresentation');
- expect(thresholdsTableProxmoxTabSource).not.toContain("sectionTitles.guestFiltering");
- expect(thresholdsTableSectionPropsSource).toContain('export interface ThresholdsTableSectionProps');
- expect(thresholdsTableProxmoxNodesSectionSource).toContain('export function ThresholdsTableProxmoxNodesSection');
- expect(thresholdsTableProxmoxPBSSectionSource).toContain('export function ThresholdsTableProxmoxPBSSection');
- expect(thresholdsTableProxmoxGuestsSectionSource).toContain('export function ThresholdsTableProxmoxGuestsSection');
+ expect(thresholdsTableProxmoxTabSource).not.toContain('sectionTitles.guestFiltering');
+ expect(thresholdsTableSectionPropsSource).toContain(
+ 'export interface ThresholdsTableSectionProps',
+ );
+ expect(thresholdsTableProxmoxNodesSectionSource).toContain(
+ 'export function ThresholdsTableProxmoxNodesSection',
+ );
+ expect(thresholdsTableProxmoxPBSSectionSource).toContain(
+ 'export function ThresholdsTableProxmoxPBSSection',
+ );
+ expect(thresholdsTableProxmoxGuestsSectionSource).toContain(
+ 'export function ThresholdsTableProxmoxGuestsSection',
+ );
expect(thresholdsTableProxmoxGuestFilteringSectionSource).toContain(
'export function ThresholdsTableProxmoxGuestFilteringSection',
);
@@ -560,7 +587,9 @@ describe('tab path helpers', () => {
expect(thresholdsDataHookSource).toContain('useThresholdsGuestData(inputs)');
expect(thresholdsDataHookSource).toContain('useThresholdsInfrastructureData(inputs)');
expect(thresholdsDataHookSource).not.toContain('const hostOverrideIdCandidates =');
- expect(thresholdsDataHookSource).not.toContain('const dockerContainersGroupedByHost = createMemo');
+ expect(thresholdsDataHookSource).not.toContain(
+ 'const dockerContainersGroupedByHost = createMemo',
+ );
expect(thresholdsHostDataHookSource).toContain('export function useThresholdsHostData');
expect(thresholdsHostDataHookSource).toContain('hostOverrideIdCandidates(agentResource)');
expect(thresholdsDockerDataHookSource).toContain('export function useThresholdsDockerData');
@@ -573,11 +602,15 @@ describe('tab path helpers', () => {
expect(thresholdsResourceModelSource).toContain('export function buildNodeHeaderMeta');
expect(thresholdsResourceModelSource).toContain('export const normalizeStorageStatus');
expect(thresholdsTableStateHookSource).toContain('export function useThresholdsTableState');
- expect(thresholdsTableStateHookSource).toContain('useThresholdsData(props, editingId, searchTerm)');
+ expect(thresholdsTableStateHookSource).toContain(
+ 'useThresholdsData(props, editingId, searchTerm)',
+ );
expect(thresholdsTableStateHookSource).toContain('useThresholdsRecoveryDefaultsState(props)');
expect(thresholdsTableStateHookSource).toContain('useThresholdsOverrideMutations');
expect(thresholdsTableStateHookSource).toContain('useThresholdsAvailabilityMutations');
- expect(thresholdsTableStateHookSource).not.toContain('const saveEdit = (resourceId: string) => {');
+ expect(thresholdsTableStateHookSource).not.toContain(
+ 'const saveEdit = (resourceId: string) => {',
+ );
expect(thresholdsTableStateHookSource).not.toContain(
'const toggleNodeConnectivity = (resourceId: string, forceState?: boolean) => {',
);
@@ -591,7 +624,9 @@ describe('tab path helpers', () => {
expect(thresholdsOverrideMutationsHookSource).toContain(
'export function useThresholdsOverrideMutations',
);
- expect(thresholdsOverrideMutationsHookSource).toContain('const saveEdit = (resourceId: string) => {');
+ expect(thresholdsOverrideMutationsHookSource).toContain(
+ 'const saveEdit = (resourceId: string) => {',
+ );
expect(thresholdsOverrideMutationsHookSource).toContain(
'const handleSaveBulkEdit = (thresholds: Record) => {',
);
@@ -610,9 +645,7 @@ describe('tab path helpers', () => {
'const setOfflineState = (resourceId: string, state: OfflineState) => {',
);
expect(thresholdsOverrideMutationModelSource).toContain('export const upsertOverride =');
- expect(thresholdsOverrideMutationModelSource).toContain(
- 'export const withThresholdEntries =',
- );
+ expect(thresholdsOverrideMutationModelSource).toContain('export const withThresholdEntries =');
expect(thresholdsOverrideMutationModelSource).toContain('export const stripStateKeys =');
});
@@ -738,16 +771,12 @@ describe('incident timeline presentation helpers', () => {
);
expect(getAlertIncidentTimelineHeadingClass()).toBe('font-medium text-base-content');
expect(getAlertIncidentTimelineDetailClass()).toBe('mt-1 text-xs text-base-content');
- expect(getAlertIncidentTimelineCommandClass()).toBe(
- 'mt-1 font-mono text-xs text-base-content',
- );
+ expect(getAlertIncidentTimelineCommandClass()).toBe('mt-1 font-mono text-xs text-base-content');
expect(getAlertIncidentTimelineOutputClass()).toBe('mt-1 text-xs text-muted');
});
it('returns the resource incident panel presentation', () => {
- expect(getAlertResourceIncidentCardClass()).toBe(
- 'rounded border border-border bg-surface p-3',
- );
+ expect(getAlertResourceIncidentCardClass()).toBe('rounded border border-border bg-surface p-3');
expect(getAlertResourceIncidentSummaryRowClass()).toBe(
'mt-2 flex flex-wrap items-center justify-between gap-2 text-xs text-muted',
);
diff --git a/frontend-modern/src/utils/__tests__/alertOverviewPresentation.test.ts b/frontend-modern/src/utils/__tests__/alertOverviewPresentation.test.ts
index 52b47d80b..295a2e95a 100644
--- a/frontend-modern/src/utils/__tests__/alertOverviewPresentation.test.ts
+++ b/frontend-modern/src/utils/__tests__/alertOverviewPresentation.test.ts
@@ -7,10 +7,16 @@ import {
ALERT_HISTORY_SEARCH_PLACEHOLDER,
ALERTS_EMPTY_STATE,
ALERTS_PAGE_DEFAULT_TITLE,
+ ALERTS_PAGE_DEFAULT_DESCRIPTION,
+ ALERTS_PAGE_DESTINATIONS_DESCRIPTION,
ALERTS_PAGE_DESTINATIONS_TITLE,
+ ALERTS_PAGE_HISTORY_DESCRIPTION,
ALERTS_PAGE_HISTORY_TITLE,
+ ALERTS_PAGE_OVERVIEW_DESCRIPTION,
ALERTS_PAGE_OVERVIEW_TITLE,
+ ALERTS_PAGE_SCHEDULE_DESCRIPTION,
ALERTS_PAGE_SCHEDULE_TITLE,
+ ALERTS_PAGE_THRESHOLDS_DESCRIPTION,
ALERTS_PAGE_THRESHOLDS_TITLE,
ALERT_TIMELINE_EMPTY_STATE,
ALERT_TIMELINE_FAILURE_STATE,
@@ -60,9 +66,7 @@ describe('alertOverviewPresentation', () => {
expect(ALERT_HISTORY_SEARCH_PLACEHOLDER).toBe('Search alerts...');
expect(getAlertHistorySearchPlaceholder()).toBe('Search alerts...');
expect(ALERT_HISTORY_EMPTY_STATE).toBe('No alerts found');
- expect(ALERT_HISTORY_EMPTY_DESCRIPTION).toBe(
- 'Try adjusting your filters or check back later',
- );
+ expect(ALERT_HISTORY_EMPTY_DESCRIPTION).toBe('Try adjusting your filters or check back later');
expect(getAlertHistoryEmptyState()).toEqual({
title: 'No alerts found',
description: 'Try adjusting your filters or check back later',
@@ -80,13 +84,54 @@ describe('alertOverviewPresentation', () => {
expect(ALERTS_PAGE_DESTINATIONS_TITLE).toBe('Notification Destinations');
expect(ALERTS_PAGE_SCHEDULE_TITLE).toBe('Maintenance Schedule');
expect(ALERTS_PAGE_HISTORY_TITLE).toBe('Alert History');
+ expect(ALERTS_PAGE_DEFAULT_DESCRIPTION).toBe(
+ 'Review active incidents, inspect alert history, and manage thresholds, notifications, and schedules.',
+ );
+ expect(ALERTS_PAGE_OVERVIEW_DESCRIPTION).toBe(
+ 'Review active incidents, confirm alert coverage, and control whether alerts are actively monitoring this install.',
+ );
+ expect(ALERTS_PAGE_THRESHOLDS_DESCRIPTION).toBe(
+ '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.',
+ );
+ expect(ALERTS_PAGE_SCHEDULE_DESCRIPTION).toBe(
+ 'Define quiet hours, grouping, cooldowns, recovery, and escalation behavior for alert delivery.',
+ );
+ expect(ALERTS_PAGE_HISTORY_DESCRIPTION).toBe(
+ 'Search prior alerts, review incident timelines, and inspect alert frequency over time.',
+ );
expect(getAlertsPageHeaderMeta()).toEqual({
- overview: { title: 'Alerts Overview' },
- thresholds: { title: 'Alert Thresholds' },
- destinations: { title: 'Notification Destinations' },
- schedule: { title: 'Maintenance Schedule' },
- history: { title: 'Alert History' },
- default: { title: 'Alerts' },
+ overview: {
+ title: 'Alerts Overview',
+ description:
+ 'Review active incidents, confirm alert coverage, and control whether alerts are actively monitoring this install.',
+ },
+ thresholds: {
+ title: 'Alert Thresholds',
+ description:
+ 'Tune thresholds and scoped overrides for infrastructure, workloads, and integrations.',
+ },
+ destinations: {
+ title: 'Notification Destinations',
+ description: 'Route alert notifications to email, Apprise, and webhook destinations.',
+ },
+ schedule: {
+ title: 'Maintenance Schedule',
+ description:
+ 'Define quiet hours, grouping, cooldowns, recovery, and escalation behavior for alert delivery.',
+ },
+ history: {
+ title: 'Alert History',
+ description:
+ 'Search prior alerts, review incident timelines, and inspect alert frequency over time.',
+ },
+ default: {
+ title: 'Alerts',
+ description:
+ 'Review active incidents, inspect alert history, and manage thresholds, notifications, and schedules.',
+ },
});
});
diff --git a/frontend-modern/src/utils/__tests__/patrolPagePresentation.test.ts b/frontend-modern/src/utils/__tests__/patrolPagePresentation.test.ts
new file mode 100644
index 000000000..ace1d7c49
--- /dev/null
+++ b/frontend-modern/src/utils/__tests__/patrolPagePresentation.test.ts
@@ -0,0 +1,20 @@
+import { describe, expect, it } from 'vitest';
+import {
+ PATROL_PAGE_DESCRIPTION,
+ PATROL_PAGE_TITLE,
+ getPatrolPageHeaderMeta,
+} from '@/utils/patrolPagePresentation';
+
+describe('patrolPagePresentation', () => {
+ it('returns canonical Patrol page header metadata', () => {
+ expect(PATROL_PAGE_TITLE).toBe('Patrol');
+ expect(PATROL_PAGE_DESCRIPTION).toBe(
+ 'Continuously verify infrastructure health, review findings, and control Patrol runtime behavior.',
+ );
+ expect(getPatrolPageHeaderMeta()).toEqual({
+ title: 'Patrol',
+ description:
+ 'Continuously verify infrastructure health, review findings, and control Patrol runtime behavior.',
+ });
+ });
+});
diff --git a/frontend-modern/src/utils/alertOverviewPresentation.ts b/frontend-modern/src/utils/alertOverviewPresentation.ts
index fac12ef1e..4c7fead3c 100644
--- a/frontend-modern/src/utils/alertOverviewPresentation.ts
+++ b/frontend-modern/src/utils/alertOverviewPresentation.ts
@@ -1,8 +1,7 @@
export const ALERTS_EMPTY_STATE = 'No active alerts';
export const ALERTS_THRESHOLD_HINT = 'Alerts will appear here when thresholds are exceeded';
export const ALERT_TIMELINE_LOADING_STATE = 'Loading timeline...';
-export const ALERT_TIMELINE_FILTER_EMPTY_STATE =
- 'No timeline events match the selected filters.';
+export const ALERT_TIMELINE_FILTER_EMPTY_STATE = 'No timeline events match the selected filters.';
export const ALERT_TIMELINE_EMPTY_STATE = 'No timeline events yet.';
export const ALERT_TIMELINE_UNAVAILABLE_STATE = 'No incident timeline available.';
export const ALERT_TIMELINE_FAILURE_STATE = 'Failed to load timeline.';
@@ -18,6 +17,18 @@ export const ALERTS_PAGE_THRESHOLDS_TITLE = 'Alert Thresholds';
export const ALERTS_PAGE_DESTINATIONS_TITLE = 'Notification Destinations';
export const ALERTS_PAGE_SCHEDULE_TITLE = 'Maintenance Schedule';
export const ALERTS_PAGE_HISTORY_TITLE = 'Alert History';
+export const ALERTS_PAGE_DEFAULT_DESCRIPTION =
+ 'Review active incidents, inspect alert history, and manage thresholds, notifications, and schedules.';
+export const ALERTS_PAGE_OVERVIEW_DESCRIPTION =
+ 'Review active incidents, confirm alert coverage, and control whether alerts are actively monitoring this install.';
+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.';
+export const ALERTS_PAGE_SCHEDULE_DESCRIPTION =
+ 'Define quiet hours, grouping, cooldowns, recovery, and escalation behavior for alert delivery.';
+export const ALERTS_PAGE_HISTORY_DESCRIPTION =
+ 'Search prior alerts, review incident timelines, and inspect alert frequency over time.';
export type DashboardAlertTone = 'default' | 'warning' | 'danger';
@@ -34,12 +45,30 @@ export interface AlertOverviewCardPresentation {
export function getAlertsPageHeaderMeta() {
return {
- overview: { title: ALERTS_PAGE_OVERVIEW_TITLE },
- thresholds: { title: ALERTS_PAGE_THRESHOLDS_TITLE },
- destinations: { title: ALERTS_PAGE_DESTINATIONS_TITLE },
- schedule: { title: ALERTS_PAGE_SCHEDULE_TITLE },
- history: { title: ALERTS_PAGE_HISTORY_TITLE },
- default: { title: ALERTS_PAGE_DEFAULT_TITLE },
+ overview: {
+ title: ALERTS_PAGE_OVERVIEW_TITLE,
+ description: ALERTS_PAGE_OVERVIEW_DESCRIPTION,
+ },
+ thresholds: {
+ title: ALERTS_PAGE_THRESHOLDS_TITLE,
+ description: ALERTS_PAGE_THRESHOLDS_DESCRIPTION,
+ },
+ destinations: {
+ title: ALERTS_PAGE_DESTINATIONS_TITLE,
+ description: ALERTS_PAGE_DESTINATIONS_DESCRIPTION,
+ },
+ schedule: {
+ title: ALERTS_PAGE_SCHEDULE_TITLE,
+ description: ALERTS_PAGE_SCHEDULE_DESCRIPTION,
+ },
+ history: {
+ title: ALERTS_PAGE_HISTORY_TITLE,
+ description: ALERTS_PAGE_HISTORY_DESCRIPTION,
+ },
+ default: {
+ title: ALERTS_PAGE_DEFAULT_TITLE,
+ description: ALERTS_PAGE_DEFAULT_DESCRIPTION,
+ },
} as const;
}
diff --git a/frontend-modern/src/utils/patrolPagePresentation.ts b/frontend-modern/src/utils/patrolPagePresentation.ts
new file mode 100644
index 000000000..adf8d5bb6
--- /dev/null
+++ b/frontend-modern/src/utils/patrolPagePresentation.ts
@@ -0,0 +1,10 @@
+export const PATROL_PAGE_TITLE = 'Patrol';
+export const PATROL_PAGE_DESCRIPTION =
+ 'Continuously verify infrastructure health, review findings, and control Patrol runtime behavior.';
+
+export function getPatrolPageHeaderMeta() {
+ return {
+ title: PATROL_PAGE_TITLE,
+ description: PATROL_PAGE_DESCRIPTION,
+ } as const;
+}
diff --git a/tests/integration/tests/60-page-header-consistency.spec.ts b/tests/integration/tests/60-page-header-consistency.spec.ts
new file mode 100644
index 000000000..61c26e713
--- /dev/null
+++ b/tests/integration/tests/60-page-header-consistency.spec.ts
@@ -0,0 +1,86 @@
+import { test, expect } from "@playwright/test";
+import { readRuntimeState } from "./runtime-defaults";
+
+const PAGE_HEADER_ROUTES = [
+ {
+ slug: "operations",
+ route: "/operations",
+ title: "Operations",
+ description:
+ "Run diagnostics, review generated reports, and inspect system logs without leaving the app.",
+ },
+ {
+ slug: "alerts",
+ route: "/alerts/overview",
+ title: "Alerts Overview",
+ description:
+ "Review active incidents, confirm alert coverage, and control whether alerts are actively monitoring this install.",
+ },
+ {
+ slug: "patrol",
+ route: "/patrol",
+ title: "Patrol",
+ description:
+ "Continuously verify infrastructure health, review findings, and control Patrol runtime behavior.",
+ },
+] as const;
+
+const PRIMARY_API_TOKEN =
+ process.env.PULSE_E2E_PRIMARY_API_TOKEN?.trim() ||
+ (typeof readRuntimeState()?.primaryAPIToken === "string"
+ ? readRuntimeState()!.primaryAPIToken!.trim()
+ : "");
+
+test.skip(
+ PRIMARY_API_TOKEN === "",
+ "Top-level header browser proof requires a runtime API token.",
+);
+
+test.describe("Top-level page header consistency", () => {
+ test.setTimeout(180_000);
+
+ test.beforeEach(async ({ page }) => {
+ await page.addInitScript((token: string) => {
+ sessionStorage.setItem(
+ "pulse_auth",
+ JSON.stringify({
+ type: "token",
+ value: token,
+ }),
+ );
+ sessionStorage.setItem("pulse_auth_user", "admin");
+ localStorage.setItem("pulse_whats_new_v2_shown", "true");
+ }, PRIMARY_API_TOKEN);
+ });
+
+ for (const surface of PAGE_HEADER_ROUTES) {
+ test(`renders the canonical header framing on ${surface.route}`, async ({
+ page,
+ }, testInfo) => {
+ await page.goto(surface.route, { waitUntil: "domcontentloaded" });
+
+ const pageHeading = page.getByRole("heading", {
+ level: 1,
+ name: surface.title,
+ });
+ await expect(
+ pageHeading,
+ `${surface.route} should render a single top-level heading`,
+ ).toBeVisible();
+ await expect(
+ page.getByText(surface.description, { exact: true }).first(),
+ `${surface.route} should render the canonical subheader copy`,
+ ).toBeVisible();
+ await expect(
+ page.locator("h1"),
+ `${surface.route} should not render duplicate page headings`,
+ ).toHaveCount(1);
+
+ const screenshotPath = testInfo.outputPath(`${surface.slug}.png`);
+ await page.screenshot({ path: screenshotPath });
+ console.log(
+ `[page-header-consistency] screenshot ${surface.slug}: ${screenshotPath}`,
+ );
+ });
+ }
+});