diff --git a/docs/release-control/v6/internal/subsystems/alerts.md b/docs/release-control/v6/internal/subsystems/alerts.md
index 6059611185..2895c0491b 100644
--- a/docs/release-control/v6/internal/subsystems/alerts.md
+++ b/docs/release-control/v6/internal/subsystems/alerts.md
@@ -102,6 +102,14 @@ The alerts schedule surface now also routes quiet-hour suppress-category card
styling through `frontend-modern/src/utils/alertSchedulePresentation.ts`
instead of leaving that selectable-card presentation inline in
`frontend-modern/src/pages/Alerts.tsx`.
+That schedule surface now also follows the same shell/runtime split as the
+other feature tabs: `frontend-modern/src/features/alerts/tabs/ScheduleTab.tsx`
+stays the render shell, while
+`frontend-modern/src/features/alerts/useAlertScheduleState.ts` owns schedule
+reset behavior, quiet-hours day/category toggles, cooldown/grouping/escalation
+update policy, and the canonical defaults handoff. Future schedule control-flow
+work should extend that hook instead of rebuilding those mutations inline in
+the tab shell.
Incident-event filter chip and filter-action styling now routes through
`frontend-modern/src/utils/alertIncidentPresentation.ts` for both
diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md
index 07da76924e..99fac03da7 100644
--- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md
+++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md
@@ -226,6 +226,13 @@ must both compose
acknowledge/restore behavior rather than keeping duplicate API and notification
logic inline in `useAlertOverviewState.ts` or
`frontend-modern/src/components/Alerts/RecentAlertsPanel.tsx`.
+The same feature-owner rule now applies to the alert scheduling surface:
+`frontend-modern/src/features/alerts/tabs/ScheduleTab.tsx` must remain the
+schedule render shell, while
+`frontend-modern/src/features/alerts/useAlertScheduleState.ts` owns schedule
+reset/update policy and canonical default application. The tab should not
+re-accumulate quiet-hours, cooldown, grouping, or escalation mutation logic
+inline.
The updates settings surface now follows the same presentation-owner rule.
`frontend-modern/src/components/Settings/UpdatesSettingsPanel.tsx` stays the
diff --git a/frontend-modern/src/features/alerts/__tests__/useAlertScheduleState.test.tsx b/frontend-modern/src/features/alerts/__tests__/useAlertScheduleState.test.tsx
new file mode 100644
index 0000000000..2e40919902
--- /dev/null
+++ b/frontend-modern/src/features/alerts/__tests__/useAlertScheduleState.test.tsx
@@ -0,0 +1,82 @@
+import { renderHook } from '@solidjs/testing-library';
+import { createSignal } from 'solid-js';
+import { describe, expect, it } from 'vitest';
+
+import { useAlertScheduleState } from '../useAlertScheduleState';
+import {
+ createDefaultCooldown,
+ createDefaultEscalation,
+ createDefaultGrouping,
+ createDefaultQuietHours,
+ createDefaultResolveNotifications,
+} from '../helpers';
+
+describe('useAlertScheduleState', () => {
+ it('owns schedule reset and update control flow outside the tab shell', () => {
+ const [hasUnsavedChanges, setHasUnsavedChanges] = createSignal(false);
+ const [quietHours, setQuietHours] = createSignal(createDefaultQuietHours());
+ const [cooldown, setCooldown] = createSignal(createDefaultCooldown());
+ const [grouping, setGrouping] = createSignal(createDefaultGrouping());
+ const [notifyOnResolve, setNotifyOnResolve] = createSignal(false);
+ const [escalation, setEscalation] = createSignal(createDefaultEscalation());
+
+ const { result } = renderHook(() =>
+ useAlertScheduleState({
+ setHasUnsavedChanges,
+ quietHours,
+ setQuietHours,
+ cooldown,
+ setCooldown,
+ grouping,
+ setGrouping,
+ notifyOnResolve,
+ setNotifyOnResolve,
+ escalation,
+ setEscalation,
+ }),
+ );
+
+ result.setQuietHoursEnabled(true);
+ result.setQuietHoursStart('21:30');
+ result.toggleQuietDay('saturday');
+ result.setQuietSuppressCategory('storage', true);
+ result.setCooldownEnabled(true);
+ result.setCooldownMinutes('45');
+ result.setCooldownMaxAlerts('5');
+ result.setGroupingEnabled(true);
+ result.setGroupingWindow('8');
+ result.setGroupingByNode(true);
+ result.setNotifyOnResolveEnabled(true);
+ result.setEscalationEnabled(true);
+ result.addEscalationLevel();
+ result.setEscalationAfter(0, '30');
+ result.setEscalationNotify(0, 'webhook');
+
+ expect(hasUnsavedChanges()).toBe(true);
+ expect(quietHours()).toMatchObject({
+ enabled: true,
+ start: '21:30',
+ suppress: expect.objectContaining({ storage: true }),
+ days: expect.objectContaining({ saturday: true }),
+ });
+ expect(result.weekendsOnly()).toBe(false);
+ expect(cooldown()).toMatchObject({ enabled: true, minutes: 45, maxAlerts: 5 });
+ expect(grouping()).toMatchObject({ enabled: true, window: 8, byNode: true });
+ expect(notifyOnResolve()).toBe(true);
+ expect(escalation()).toMatchObject({
+ enabled: true,
+ levels: [expect.objectContaining({ after: 30, notify: 'webhook' })],
+ });
+
+ result.removeEscalationLevel(0);
+ expect(escalation().levels).toHaveLength(0);
+
+ result.resetToDefaults();
+
+ expect(quietHours()).toEqual(createDefaultQuietHours());
+ expect(cooldown()).toEqual(createDefaultCooldown());
+ expect(grouping()).toEqual(createDefaultGrouping());
+ expect(notifyOnResolve()).toBe(createDefaultResolveNotifications());
+ expect(escalation()).toEqual(createDefaultEscalation());
+ });
+});
diff --git a/frontend-modern/src/features/alerts/tabs/ScheduleTab.tsx b/frontend-modern/src/features/alerts/tabs/ScheduleTab.tsx
index 83b9dea732..4c6a2c1942 100644
--- a/frontend-modern/src/features/alerts/tabs/ScheduleTab.tsx
+++ b/frontend-modern/src/features/alerts/tabs/ScheduleTab.tsx
@@ -68,22 +68,17 @@ import {
} from '@/utils/alertSchedulePresentation';
import {
- createDefaultCooldown,
- createDefaultEscalation,
- createDefaultGrouping,
- createDefaultQuietHours,
- createDefaultResolveNotifications,
- fallbackMaxAlertsPerHour,
-} from '../helpers';
+ ALERT_SCHEDULE_DAYS,
+ ALERT_SCHEDULE_TIMEZONES,
+ useAlertScheduleState,
+} from '../useAlertScheduleState';
import type {
CooldownConfig,
EscalationConfig,
- EscalationLevel,
EscalationNotifyTarget,
GroupingConfig,
QuietHoursConfig,
} from '../types';
-import { fallbackCooldownMinutes } from '../types';
export interface ScheduleTabProps {
setHasUnsavedChanges: (value: boolean) => void;
@@ -99,99 +94,9 @@ export interface ScheduleTabProps {
setEscalation: (value: EscalationConfig) => void;
}
-const timezones = [
- 'UTC',
- 'Africa/Cairo',
- 'Africa/Johannesburg',
- 'Africa/Lagos',
- 'Africa/Nairobi',
- 'America/Anchorage',
- 'America/Argentina/Buenos_Aires',
- 'America/Bogota',
- 'America/Caracas',
- 'America/Chicago',
- 'America/Denver',
- 'America/Halifax',
- 'America/Lima',
- 'America/Los_Angeles',
- 'America/Mexico_City',
- 'America/New_York',
- 'America/Phoenix',
- 'America/Santiago',
- 'America/Sao_Paulo',
- 'America/St_Johns',
- 'America/Toronto',
- 'America/Vancouver',
- 'Asia/Bangkok',
- 'Asia/Dhaka',
- 'Asia/Dubai',
- 'Asia/Hong_Kong',
- 'Asia/Jakarta',
- 'Asia/Jerusalem',
- 'Asia/Karachi',
- 'Asia/Kolkata',
- 'Asia/Kuala_Lumpur',
- 'Asia/Manila',
- 'Asia/Riyadh',
- 'Asia/Seoul',
- 'Asia/Shanghai',
- 'Asia/Singapore',
- 'Asia/Taipei',
- 'Asia/Tehran',
- 'Asia/Tokyo',
- 'Australia/Adelaide',
- 'Australia/Brisbane',
- 'Australia/Melbourne',
- 'Australia/Perth',
- 'Australia/Sydney',
- 'Europe/Amsterdam',
- 'Europe/Athens',
- 'Europe/Berlin',
- 'Europe/Brussels',
- 'Europe/Budapest',
- 'Europe/Copenhagen',
- 'Europe/Dublin',
- 'Europe/Helsinki',
- 'Europe/Istanbul',
- 'Europe/Lisbon',
- 'Europe/London',
- 'Europe/Madrid',
- 'Europe/Moscow',
- 'Europe/Oslo',
- 'Europe/Paris',
- 'Europe/Prague',
- 'Europe/Rome',
- 'Europe/Stockholm',
- 'Europe/Vienna',
- 'Europe/Warsaw',
- 'Europe/Zurich',
- 'Pacific/Auckland',
- 'Pacific/Fiji',
- 'Pacific/Guam',
- 'Pacific/Honolulu',
-];
-
-const days = [
- { id: 'monday', label: 'M', fullLabel: 'Monday' },
- { id: 'tuesday', label: 'T', fullLabel: 'Tuesday' },
- { id: 'wednesday', label: 'W', fullLabel: 'Wednesday' },
- { id: 'thursday', label: 'T', fullLabel: 'Thursday' },
- { id: 'friday', label: 'F', fullLabel: 'Friday' },
- { id: 'saturday', label: 'S', fullLabel: 'Saturday' },
- { id: 'sunday', label: 'S', fullLabel: 'Sunday' },
-];
-
export function ScheduleTab(props: ScheduleTabProps) {
const quietHourSuppressOptions = getAlertConfigQuietHourSuppressOptions();
-
- const resetToDefaults = () => {
- props.setQuietHours(createDefaultQuietHours());
- props.setCooldown(createDefaultCooldown());
- props.setGrouping(createDefaultGrouping());
- props.setNotifyOnResolve(createDefaultResolveNotifications());
- props.setEscalation(createDefaultEscalation());
- props.setHasUnsavedChanges(true);
- };
+ const scheduleState = useAlertScheduleState(props);
return (
@@ -204,7 +109,7 @@ export function ScheduleTab(props: ScheduleTabProps) {