Extract alerts configuration model owner

This commit is contained in:
rcourtman
2026-03-22 16:41:57 +00:00
parent 9549decc6e
commit 111d725c53
7 changed files with 1111 additions and 632 deletions
@@ -243,6 +243,9 @@ surface is now a shell that composes the destinations, schedule, and thresholds
tabs. The canonical alert-policy runtime owner is now
`frontend-modern/src/features/alerts/useAlertsConfigurationState.ts` for
config transport, defaults, and save/load orchestration,
`frontend-modern/src/features/alerts/alertsConfigurationModel.ts` for backend
config normalization, factory defaults, docker-gap validation, and save-payload
serialization,
`frontend-modern/src/features/alerts/useAlertOverridesState.ts` for raw
override normalization plus resource-backed override projection, and
`frontend-modern/src/features/alerts/useAlertDestinationsState.ts` for
@@ -251,9 +254,10 @@ notification destination reload and persistence.
webhook load/mutate/test flow plus destination test actions, while
`frontend-modern/src/features/alerts/tabs/DestinationsTab.tsx` stays the
destinations render shell. Future config cleanup should extend the config
transport hook, the override-projection hook, or the destinations runtime hook
based on which subsystem actually owns the behavior instead of letting the
broader configuration hook absorb all three concerns again.
transport hook, the config model, the override-projection hook, or the
destinations runtime hook based on which subsystem actually owns the behavior
instead of letting the broader configuration hook absorb all three concerns
again.
Alert filter metadata and grouped header consumers must also preserve the
canonical `agent` and `node` header boundary when reusing shared filter
@@ -444,16 +444,19 @@ configuration orchestration. `frontend-modern/src/pages/Alerts.tsx` is the page
shell, while `frontend-modern/src/features/alerts/AlertsConfigurationSurface.tsx`
is the feature shell. The canonical runtime owner is now
`frontend-modern/src/features/alerts/useAlertsConfigurationState.ts` for alert
config transport, `frontend-modern/src/features/alerts/useAlertOverridesState.ts`
config transport,
`frontend-modern/src/features/alerts/alertsConfigurationModel.ts` for config
normalization, factory defaults, docker-gap validation, and payload
serialization, `frontend-modern/src/features/alerts/useAlertOverridesState.ts`
for override projection and thresholds-facing resource selectors, and
`frontend-modern/src/features/alerts/useAlertDestinationsState.ts` for
notification destination reload and persistence.
`frontend-modern/src/features/alerts/useAlertDestinationsTabState.ts` now owns
webhook runtime and destination test actions while
`frontend-modern/src/features/alerts/tabs/DestinationsTab.tsx` stays the
render shell. Future cleanup should extend the transport hook, override hook,
or destinations runtime hook based on the true owner, not move config control
flow back into the top-level page shell.
render shell. Future cleanup should extend the transport hook, config model,
override hook, or destinations runtime hook based on the true owner, not move
config control flow back into the top-level page shell.
The same rule now also covers cross-tab incident timelines: the shared runtime
owner is `frontend-modern/src/features/alerts/useAlertIncidentTimelineState.ts`,
while `frontend-modern/src/features/alerts/OverviewTab.tsx` and
@@ -0,0 +1,164 @@
import { describe, expect, it } from 'vitest';
import type { AlertConfig } from '@/types/alerts';
import {
ALERT_DOCKER_GAP_VALIDATION_ERROR,
buildAlertsConfigurationPayload,
createDefaultAlertsConfigurationSnapshot,
readAlertsConfigurationSnapshot,
} from '../alertsConfigurationModel';
describe('alertsConfigurationModel', () => {
it('creates the canonical default snapshot', () => {
const snapshot = createDefaultAlertsConfigurationSnapshot();
expect(snapshot.guestDefaults.cpu).toBe(80);
expect(snapshot.dockerDefaults.serviceWarnGapPercent).toBe(10);
expect(snapshot.timeThresholds.guest).toBe(5);
expect(snapshot.scheduleCooldown.enabled).toBe(true);
expect(snapshot.backupDefaults.ignoreVMIDs).toEqual([]);
});
it('normalizes alert config into the runtime snapshot', () => {
const config = {
enabled: true,
guestDefaults: {
cpu: { trigger: 91, clear: 86 },
disableConnectivity: true,
poweredOffSeverity: 'critical',
},
nodeDefaults: {},
agentDefaults: {},
storageDefault: { trigger: 88, clear: 83 },
overrides: {},
dockerDefaults: {
serviceWarnGapPercent: 40,
serviceCriticalGapPercent: 10,
statePoweredOffSeverity: 'critical',
},
backupDefaults: {
enabled: true,
warningDays: 30,
criticalDays: 10,
ignoreVMIDs: [' 101 ', '101', ''],
},
snapshotDefaults: {
enabled: true,
warningDays: 25,
criticalDays: 5,
},
schedule: {
quietHours: {
enabled: true,
start: '21:00',
end: '06:00',
days: [1, 3, 5],
},
cooldown: 45,
maxAlertsHour: 7,
},
} as AlertConfig;
const snapshot = readAlertsConfigurationSnapshot(config);
expect(snapshot.guestDefaults.cpu).toBe(91);
expect(snapshot.guestDisableConnectivity).toBe(true);
expect(snapshot.guestPoweredOffSeverity).toBe('critical');
expect(snapshot.dockerDefaults.serviceWarnGapPercent).toBe(40);
expect(snapshot.dockerDefaults.serviceCriticalGapPercent).toBe(40);
expect(snapshot.backupDefaults.warningDays).toBe(10);
expect(snapshot.backupDefaults.criticalDays).toBe(10);
expect(snapshot.backupDefaults.ignoreVMIDs).toEqual(['101']);
expect(snapshot.snapshotDefaults.warningDays).toBe(5);
expect(snapshot.snapshotDefaults.criticalDays).toBe(5);
expect(snapshot.scheduleQuietHours.days).toEqual({
sunday: false,
monday: true,
tuesday: false,
wednesday: true,
thursday: false,
friday: true,
saturday: false,
});
expect(snapshot.scheduleCooldown.maxAlerts).toBe(7);
});
it('builds the canonical save payload from the runtime snapshot', () => {
const snapshot = createDefaultAlertsConfigurationSnapshot();
snapshot.guestDefaults.cpu = 92;
snapshot.dockerIgnoredPrefixes = [' web ', ''];
snapshot.guestTagWhitelist = [' prod ', ''];
snapshot.metricTimeThresholds = {
Guest: { CPU: 17 },
} as Record<string, Record<string, number>>;
snapshot.scheduleCooldown = {
enabled: true,
minutes: 47,
maxAlerts: 99,
};
snapshot.scheduleGrouping = {
enabled: true,
window: 2,
byNode: true,
byGuest: false,
};
snapshot.backupDefaults = {
enabled: true,
warningDays: 30,
criticalDays: 10,
freshHours: 12,
staleHours: 48,
alertOrphaned: true,
ignoreVMIDs: [' 101 ', ''],
};
const result = buildAlertsConfigurationPayload({
snapshot,
rawOverridesConfig: { 'guest-1': { disabled: true } },
alertsActivationState: 'active',
alertsActivationConfig: {
enabled: true,
activationTime: '2026-03-22T10:00:00Z',
observationWindowHours: 24,
},
});
expect(result.dockerValidationError).toBeUndefined();
expect(result.alertConfig).toBeDefined();
expect(result.alertConfig?.guestDefaults.cpu).toEqual({ trigger: 92, clear: 87 });
expect(result.alertConfig?.dockerIgnoredContainerPrefixes).toEqual(['web']);
expect(result.alertConfig?.guestTagWhitelist).toEqual(['prod']);
expect(result.alertConfig?.metricTimeThresholds).toEqual({ guest: { cpu: 17 } });
expect(result.alertConfig?.schedule?.cooldown).toBe(47);
expect(result.alertConfig?.schedule?.maxAlertsHour).toBe(10);
expect(result.alertConfig?.schedule?.grouping).toEqual({
enabled: true,
window: 120,
byNode: true,
byGuest: false,
});
expect(result.alertConfig?.backupDefaults).toMatchObject({
warningDays: 10,
criticalDays: 10,
ignoreVMIDs: ['101'],
});
expect(result.alertConfig?.overrides).toEqual({ 'guest-1': { disabled: true } });
});
it('returns the canonical docker-gap validation error before save', () => {
const snapshot = createDefaultAlertsConfigurationSnapshot();
snapshot.dockerDefaults.serviceWarnGapPercent = 60;
snapshot.dockerDefaults.serviceCriticalGapPercent = 40;
const result = buildAlertsConfigurationPayload({
snapshot,
rawOverridesConfig: {},
alertsActivationState: null,
alertsActivationConfig: null,
});
expect(result.alertConfig).toBeUndefined();
expect(result.dockerValidationError).toBe(ALERT_DOCKER_GAP_VALIDATION_ERROR);
});
});
@@ -0,0 +1,680 @@
import type { AlertConfig, ActivationState, BackupAlertConfig, SnapshotAlertConfig } from '@/types/alerts';
import {
clampMaxAlertsPerHour,
createDefaultCooldown,
createDefaultEscalation,
createDefaultGrouping,
createDefaultQuietHours,
createDefaultResolveNotifications,
DEFAULT_DELAY_SECONDS,
fallbackMaxAlertsPerHour,
getTriggerValue,
normalizeMetricDelayMap,
} from './helpers';
import type {
CooldownConfig,
EscalationConfig,
EscalationNotifyTarget,
GroupingConfig,
QuietHoursConfig,
} from './types';
import { GROUPING_WINDOW_DEFAULT_SECONDS, clampCooldownMinutes } from './types';
export const FACTORY_GUEST_DEFAULTS = {
cpu: 80,
memory: 85,
disk: 90,
diskRead: -1,
diskWrite: -1,
networkIn: -1,
networkOut: -1,
};
export const FACTORY_NODE_DEFAULTS = {
cpu: 80,
memory: 85,
disk: 90,
temperature: 80,
};
export const FACTORY_PBS_DEFAULTS = {
cpu: 80,
memory: 85,
};
export const FACTORY_AGENT_DEFAULTS = {
cpu: 80,
memory: 85,
disk: 90,
diskTemperature: 55,
};
export const FACTORY_DOCKER_DEFAULTS = {
cpu: 80,
memory: 85,
disk: 85,
restartCount: 3,
restartWindow: 300,
memoryWarnPct: 90,
memoryCriticalPct: 95,
serviceWarnGapPercent: 10,
serviceCriticalGapPercent: 50,
};
export const FACTORY_DOCKER_STATE_DISABLE_CONNECTIVITY = false;
export const FACTORY_DOCKER_STATE_SEVERITY: 'warning' | 'critical' = 'warning';
export const FACTORY_STORAGE_DEFAULT = 85;
export const FACTORY_SNAPSHOT_DEFAULTS: SnapshotAlertConfig = {
enabled: false,
warningDays: 30,
criticalDays: 45,
};
export const FACTORY_BACKUP_DEFAULTS: BackupAlertConfig = {
enabled: false,
warningDays: 7,
criticalDays: 14,
freshHours: 24,
staleHours: 72,
alertOrphaned: true,
ignoreVMIDs: [],
};
export interface AlertsConfigurationSnapshot {
scheduleQuietHours: QuietHoursConfig;
scheduleCooldown: CooldownConfig;
scheduleGrouping: GroupingConfig;
scheduleEscalation: EscalationConfig;
notifyOnResolve: boolean;
guestDefaults: Record<string, number | undefined>;
guestDisableConnectivity: boolean;
guestPoweredOffSeverity: 'warning' | 'critical';
nodeDefaults: Record<string, number | undefined>;
pbsDefaults: Record<string, number | undefined>;
agentDefaults: Record<string, number | undefined>;
dockerDefaults: typeof FACTORY_DOCKER_DEFAULTS;
dockerDisableConnectivity: boolean;
dockerPoweredOffSeverity: 'warning' | 'critical';
dockerIgnoredPrefixes: string[];
ignoredGuestPrefixes: string[];
guestTagWhitelist: string[];
guestTagBlacklist: string[];
storageDefault: number;
backupDefaults: BackupAlertConfig;
timeThresholds: {
guest: number;
node: number;
storage: number;
pbs: number;
agent: number;
};
metricTimeThresholds: Record<string, Record<string, number>>;
snapshotDefaults: SnapshotAlertConfig;
pmgThresholds: {
queueTotalWarning: number;
queueTotalCritical: number;
oldestMessageWarnMins: number;
oldestMessageCritMins: number;
deferredQueueWarn: number;
deferredQueueCritical: number;
holdQueueWarn: number;
holdQueueCritical: number;
quarantineSpamWarn: number;
quarantineSpamCritical: number;
quarantineVirusWarn: number;
quarantineVirusCritical: number;
quarantineGrowthWarnPct: number;
quarantineGrowthWarnMin: number;
quarantineGrowthCritPct: number;
quarantineGrowthCritMin: number;
};
disableAllNodes: boolean;
disableAllGuests: boolean;
disableAllAgents: boolean;
disableAllStorage: boolean;
disableAllPBS: boolean;
disableAllPMG: boolean;
disableAllDockerHosts: boolean;
disableAllDockerServices: boolean;
disableAllDockerContainers: boolean;
disableAllNodesOffline: boolean;
disableAllGuestsOffline: boolean;
disableAllAgentsOffline: boolean;
disableAllPBSOffline: boolean;
disableAllPMGOffline: boolean;
disableAllDockerHostsOffline: boolean;
}
export interface BuildAlertsConfigurationPayloadArgs {
snapshot: AlertsConfigurationSnapshot;
rawOverridesConfig: AlertConfig['overrides'];
alertsActivationState: ActivationState | null;
alertsActivationConfig: {
enabled?: boolean;
activationTime?: string | null;
observationWindowHours?: number | null;
} | null;
}
export const ALERT_DOCKER_GAP_VALIDATION_ERROR =
'Swarm service critical gap must be greater than or equal to the warning gap when enabled.';
const cloneDays = (days: QuietHoursConfig['days']): QuietHoursConfig['days'] => ({ ...days });
const cloneBackupDefaults = (backupDefaults: BackupAlertConfig): BackupAlertConfig => ({
...backupDefaults,
ignoreVMIDs: [...(backupDefaults.ignoreVMIDs ?? [])],
});
const createHysteresisThreshold = (trigger: number | undefined, clearMargin = 5) => {
const normalized = typeof trigger === 'number' ? trigger : 0;
return {
trigger: normalized,
clear: Math.max(0, normalized - clearMargin),
};
};
const normalizeGap = (value: unknown, fallback: number) => {
const numeric = Number(value);
if (!Number.isFinite(numeric)) {
return fallback;
}
return Math.max(0, Math.min(100, numeric));
};
const normalizeWarningCriticalPair = (
warning: number | undefined,
critical: number | undefined,
): { warning: number; critical: number } => {
const normalizedWarning = Math.max(0, warning ?? 0);
const safeCritical = Math.max(0, critical ?? 0);
const finalWarning =
safeCritical > 0 && normalizedWarning > safeCritical ? safeCritical : normalizedWarning;
const finalCritical = safeCritical > 0 ? Math.max(safeCritical, finalWarning) : finalWarning;
return {
warning: finalWarning,
critical: finalCritical,
};
};
const normalizeStringList = (values: string[] | undefined): string[] =>
(values ?? []).map((value) => value.trim()).filter((value) => value.length > 0);
export function createDefaultAlertsConfigurationSnapshot(): AlertsConfigurationSnapshot {
return {
scheduleQuietHours: createDefaultQuietHours(),
scheduleCooldown: createDefaultCooldown(),
scheduleGrouping: createDefaultGrouping(),
scheduleEscalation: createDefaultEscalation(),
notifyOnResolve: createDefaultResolveNotifications(),
guestDefaults: { ...FACTORY_GUEST_DEFAULTS },
guestDisableConnectivity: false,
guestPoweredOffSeverity: 'warning',
nodeDefaults: { ...FACTORY_NODE_DEFAULTS },
pbsDefaults: { ...FACTORY_PBS_DEFAULTS },
agentDefaults: { ...FACTORY_AGENT_DEFAULTS },
dockerDefaults: { ...FACTORY_DOCKER_DEFAULTS },
dockerDisableConnectivity: FACTORY_DOCKER_STATE_DISABLE_CONNECTIVITY,
dockerPoweredOffSeverity: FACTORY_DOCKER_STATE_SEVERITY,
dockerIgnoredPrefixes: [],
ignoredGuestPrefixes: [],
guestTagWhitelist: [],
guestTagBlacklist: [],
storageDefault: FACTORY_STORAGE_DEFAULT,
backupDefaults: cloneBackupDefaults(FACTORY_BACKUP_DEFAULTS),
timeThresholds: {
guest: DEFAULT_DELAY_SECONDS,
node: DEFAULT_DELAY_SECONDS,
storage: DEFAULT_DELAY_SECONDS,
pbs: DEFAULT_DELAY_SECONDS,
agent: DEFAULT_DELAY_SECONDS,
},
metricTimeThresholds: {},
snapshotDefaults: { ...FACTORY_SNAPSHOT_DEFAULTS },
pmgThresholds: {
queueTotalWarning: 500,
queueTotalCritical: 1000,
oldestMessageWarnMins: 30,
oldestMessageCritMins: 60,
deferredQueueWarn: 200,
deferredQueueCritical: 500,
holdQueueWarn: 100,
holdQueueCritical: 300,
quarantineSpamWarn: 2000,
quarantineSpamCritical: 5000,
quarantineVirusWarn: 2000,
quarantineVirusCritical: 5000,
quarantineGrowthWarnPct: 25,
quarantineGrowthWarnMin: 250,
quarantineGrowthCritPct: 50,
quarantineGrowthCritMin: 500,
},
disableAllNodes: false,
disableAllGuests: false,
disableAllAgents: false,
disableAllStorage: false,
disableAllPBS: false,
disableAllPMG: false,
disableAllDockerHosts: false,
disableAllDockerServices: false,
disableAllDockerContainers: false,
disableAllNodesOffline: false,
disableAllGuestsOffline: false,
disableAllAgentsOffline: false,
disableAllPBSOffline: false,
disableAllPMGOffline: false,
disableAllDockerHostsOffline: false,
};
}
export function readAlertsConfigurationSnapshot(config: AlertConfig): AlertsConfigurationSnapshot {
const snapshot = createDefaultAlertsConfigurationSnapshot();
if (config.guestDefaults) {
snapshot.guestDefaults = {
cpu: getTriggerValue(config.guestDefaults.cpu) ?? FACTORY_GUEST_DEFAULTS.cpu,
memory: getTriggerValue(config.guestDefaults.memory) ?? FACTORY_GUEST_DEFAULTS.memory,
disk: getTriggerValue(config.guestDefaults.disk) ?? FACTORY_GUEST_DEFAULTS.disk,
diskRead: getTriggerValue(config.guestDefaults.diskRead) ?? FACTORY_GUEST_DEFAULTS.diskRead,
diskWrite:
getTriggerValue(config.guestDefaults.diskWrite) ?? FACTORY_GUEST_DEFAULTS.diskWrite,
networkIn:
getTriggerValue(config.guestDefaults.networkIn) ?? FACTORY_GUEST_DEFAULTS.networkIn,
networkOut:
getTriggerValue(config.guestDefaults.networkOut) ?? FACTORY_GUEST_DEFAULTS.networkOut,
};
snapshot.guestDisableConnectivity = Boolean(config.guestDefaults.disableConnectivity);
snapshot.guestPoweredOffSeverity =
config.guestDefaults.poweredOffSeverity === 'critical' ? 'critical' : 'warning';
}
if (config.nodeDefaults) {
snapshot.nodeDefaults = {
cpu: getTriggerValue(config.nodeDefaults.cpu) ?? FACTORY_NODE_DEFAULTS.cpu,
memory: getTriggerValue(config.nodeDefaults.memory) ?? FACTORY_NODE_DEFAULTS.memory,
disk: getTriggerValue(config.nodeDefaults.disk) ?? FACTORY_NODE_DEFAULTS.disk,
temperature:
getTriggerValue(config.nodeDefaults.temperature) ?? FACTORY_NODE_DEFAULTS.temperature,
};
}
if (config.pbsDefaults) {
snapshot.pbsDefaults = {
cpu: getTriggerValue(config.pbsDefaults.cpu) ?? FACTORY_PBS_DEFAULTS.cpu,
memory: getTriggerValue(config.pbsDefaults.memory) ?? FACTORY_PBS_DEFAULTS.memory,
};
}
if (config.agentDefaults) {
snapshot.agentDefaults = {
cpu: getTriggerValue(config.agentDefaults.cpu) ?? FACTORY_AGENT_DEFAULTS.cpu,
memory: getTriggerValue(config.agentDefaults.memory) ?? FACTORY_AGENT_DEFAULTS.memory,
disk: getTriggerValue(config.agentDefaults.disk) ?? FACTORY_AGENT_DEFAULTS.disk,
diskTemperature:
getTriggerValue(config.agentDefaults.diskTemperature) ??
FACTORY_AGENT_DEFAULTS.diskTemperature,
};
}
if (config.dockerDefaults) {
const serviceWarnGap = normalizeGap(
config.dockerDefaults.serviceWarnGapPercent,
FACTORY_DOCKER_DEFAULTS.serviceWarnGapPercent,
);
let serviceCriticalGap = normalizeGap(
config.dockerDefaults.serviceCriticalGapPercent,
FACTORY_DOCKER_DEFAULTS.serviceCriticalGapPercent,
);
if (serviceCriticalGap > 0 && serviceWarnGap > serviceCriticalGap) {
serviceCriticalGap = serviceWarnGap;
}
snapshot.dockerDefaults = {
cpu: getTriggerValue(config.dockerDefaults.cpu) ?? FACTORY_DOCKER_DEFAULTS.cpu,
memory: getTriggerValue(config.dockerDefaults.memory) ?? FACTORY_DOCKER_DEFAULTS.memory,
disk: getTriggerValue(config.dockerDefaults.disk) ?? FACTORY_DOCKER_DEFAULTS.disk,
restartCount: config.dockerDefaults.restartCount ?? FACTORY_DOCKER_DEFAULTS.restartCount,
restartWindow: config.dockerDefaults.restartWindow ?? FACTORY_DOCKER_DEFAULTS.restartWindow,
memoryWarnPct: config.dockerDefaults.memoryWarnPct ?? FACTORY_DOCKER_DEFAULTS.memoryWarnPct,
memoryCriticalPct:
config.dockerDefaults.memoryCriticalPct ?? FACTORY_DOCKER_DEFAULTS.memoryCriticalPct,
serviceWarnGapPercent: serviceWarnGap,
serviceCriticalGapPercent: serviceCriticalGap,
};
snapshot.dockerDisableConnectivity = Boolean(config.dockerDefaults.stateDisableConnectivity);
snapshot.dockerPoweredOffSeverity =
config.dockerDefaults.statePoweredOffSeverity === 'critical' ? 'critical' : 'warning';
}
snapshot.dockerIgnoredPrefixes = [...(config.dockerIgnoredContainerPrefixes ?? [])];
snapshot.ignoredGuestPrefixes = [...(config.ignoredGuestPrefixes ?? [])];
snapshot.guestTagWhitelist = [...(config.guestTagWhitelist ?? [])];
snapshot.guestTagBlacklist = [...(config.guestTagBlacklist ?? [])];
if (config.storageDefault) {
snapshot.storageDefault = getTriggerValue(config.storageDefault) ?? FACTORY_STORAGE_DEFAULT;
}
if (config.timeThresholds) {
snapshot.timeThresholds = {
guest: config.timeThresholds.guest ?? DEFAULT_DELAY_SECONDS,
node: config.timeThresholds.node ?? DEFAULT_DELAY_SECONDS,
storage: config.timeThresholds.storage ?? DEFAULT_DELAY_SECONDS,
pbs: config.timeThresholds.pbs ?? DEFAULT_DELAY_SECONDS,
agent: config.timeThresholds.agent ?? DEFAULT_DELAY_SECONDS,
};
}
if (config.metricTimeThresholds) {
snapshot.metricTimeThresholds = normalizeMetricDelayMap(config.metricTimeThresholds);
}
if (config.backupDefaults) {
const normalizedPair = normalizeWarningCriticalPair(
config.backupDefaults.warningDays,
config.backupDefaults.criticalDays,
);
snapshot.backupDefaults = {
enabled: Boolean(config.backupDefaults.enabled),
warningDays: normalizedPair.warning,
criticalDays: normalizedPair.critical,
freshHours: config.backupDefaults.freshHours ?? FACTORY_BACKUP_DEFAULTS.freshHours,
staleHours: config.backupDefaults.staleHours ?? FACTORY_BACKUP_DEFAULTS.staleHours,
alertOrphaned:
config.backupDefaults.alertOrphaned ?? FACTORY_BACKUP_DEFAULTS.alertOrphaned ?? true,
ignoreVMIDs: Array.from(
new Set(
normalizeStringList(
config.backupDefaults.ignoreVMIDs ?? FACTORY_BACKUP_DEFAULTS.ignoreVMIDs,
),
),
),
};
}
if (config.snapshotDefaults) {
const normalizedPair = normalizeWarningCriticalPair(
config.snapshotDefaults.warningDays,
config.snapshotDefaults.criticalDays,
);
snapshot.snapshotDefaults = {
enabled: Boolean(config.snapshotDefaults.enabled),
warningDays: normalizedPair.warning,
criticalDays: normalizedPair.critical,
};
}
if (config.pmgDefaults) {
snapshot.pmgThresholds = {
queueTotalWarning: config.pmgDefaults.queueTotalWarning ?? 500,
queueTotalCritical: config.pmgDefaults.queueTotalCritical ?? 1000,
oldestMessageWarnMins: config.pmgDefaults.oldestMessageWarnMins ?? 30,
oldestMessageCritMins: config.pmgDefaults.oldestMessageCritMins ?? 60,
deferredQueueWarn: config.pmgDefaults.deferredQueueWarn ?? 200,
deferredQueueCritical: config.pmgDefaults.deferredQueueCritical ?? 500,
holdQueueWarn: config.pmgDefaults.holdQueueWarn ?? 100,
holdQueueCritical: config.pmgDefaults.holdQueueCritical ?? 300,
quarantineSpamWarn: config.pmgDefaults.quarantineSpamWarn ?? 2000,
quarantineSpamCritical: config.pmgDefaults.quarantineSpamCritical ?? 5000,
quarantineVirusWarn: config.pmgDefaults.quarantineVirusWarn ?? 2000,
quarantineVirusCritical: config.pmgDefaults.quarantineVirusCritical ?? 5000,
quarantineGrowthWarnPct: config.pmgDefaults.quarantineGrowthWarnPct ?? 25,
quarantineGrowthWarnMin: config.pmgDefaults.quarantineGrowthWarnMin ?? 250,
quarantineGrowthCritPct: config.pmgDefaults.quarantineGrowthCritPct ?? 50,
quarantineGrowthCritMin: config.pmgDefaults.quarantineGrowthCritMin ?? 500,
};
}
snapshot.disableAllNodes = config.disableAllNodes ?? false;
snapshot.disableAllGuests = config.disableAllGuests ?? false;
snapshot.disableAllAgents = config.disableAllAgents ?? false;
snapshot.disableAllStorage = config.disableAllStorage ?? false;
snapshot.disableAllPBS = config.disableAllPBS ?? false;
snapshot.disableAllPMG = config.disableAllPMG ?? false;
snapshot.disableAllDockerHosts = config.disableAllDockerHosts ?? false;
snapshot.disableAllDockerServices = config.disableAllDockerServices ?? false;
snapshot.disableAllDockerContainers = config.disableAllDockerContainers ?? false;
snapshot.disableAllNodesOffline = config.disableAllNodesOffline ?? false;
snapshot.disableAllGuestsOffline = config.disableAllGuestsOffline ?? false;
snapshot.disableAllAgentsOffline = config.disableAllAgentsOffline ?? false;
snapshot.disableAllPBSOffline = config.disableAllPBSOffline ?? false;
snapshot.disableAllPMGOffline = config.disableAllPMGOffline ?? false;
snapshot.disableAllDockerHostsOffline = config.disableAllDockerHostsOffline ?? false;
if (config.schedule) {
if (config.schedule.quietHours) {
const quietHours = config.schedule.quietHours;
const days = Array.isArray(quietHours.days)
? {
sunday: quietHours.days.includes(0),
monday: quietHours.days.includes(1),
tuesday: quietHours.days.includes(2),
wednesday: quietHours.days.includes(3),
thursday: quietHours.days.includes(4),
friday: quietHours.days.includes(5),
saturday: quietHours.days.includes(6),
}
: ((quietHours.days as Record<string, boolean>) || createDefaultQuietHours().days);
snapshot.scheduleQuietHours = {
enabled: quietHours.enabled || false,
start: quietHours.start || '22:00',
end: quietHours.end || '08:00',
timezone:
quietHours.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC',
days,
suppress: {
performance: quietHours.suppress?.performance ?? false,
storage: quietHours.suppress?.storage ?? false,
offline: quietHours.suppress?.offline ?? false,
},
};
}
if (config.schedule.cooldown !== undefined) {
const rawCooldown = config.schedule.cooldown;
const cooldownEnabled = rawCooldown > 0;
snapshot.scheduleCooldown = {
enabled: cooldownEnabled,
minutes: cooldownEnabled ? clampCooldownMinutes(rawCooldown) : 0,
maxAlerts: fallbackMaxAlertsPerHour(config.schedule.maxAlertsHour),
};
}
if (config.schedule.grouping) {
const groupingConfig = config.schedule.grouping;
const rawGroupingWindowSeconds =
typeof groupingConfig.window === 'number'
? groupingConfig.window
: GROUPING_WINDOW_DEFAULT_SECONDS;
const normalizedGroupingWindowSeconds = Math.max(0, rawGroupingWindowSeconds);
snapshot.scheduleGrouping = {
enabled:
groupingConfig.enabled !== undefined
? Boolean(groupingConfig.enabled)
: normalizedGroupingWindowSeconds > 0,
window: Math.round(normalizedGroupingWindowSeconds / 60),
byNode: groupingConfig.byNode !== undefined ? groupingConfig.byNode : true,
byGuest: groupingConfig.byGuest !== undefined ? groupingConfig.byGuest : false,
};
}
if (config.schedule.notifyOnResolve !== undefined) {
snapshot.notifyOnResolve = Boolean(config.schedule.notifyOnResolve);
}
if (config.schedule.escalation) {
snapshot.scheduleEscalation = {
enabled: Boolean(config.schedule.escalation.enabled),
levels: (config.schedule.escalation.levels || []).map((level) => ({
after: typeof level.after === 'number' ? level.after : 15,
notify: (level.notify as EscalationNotifyTarget) || 'all',
})),
};
}
}
return snapshot;
}
export function buildAlertsConfigurationPayload({
snapshot,
rawOverridesConfig,
alertsActivationState,
alertsActivationConfig,
}: BuildAlertsConfigurationPayloadArgs): {
alertConfig?: AlertConfig;
dockerValidationError?: string;
} {
if (
snapshot.dockerDefaults.serviceCriticalGapPercent > 0 &&
snapshot.dockerDefaults.serviceWarnGapPercent > snapshot.dockerDefaults.serviceCriticalGapPercent
) {
return {
dockerValidationError: ALERT_DOCKER_GAP_VALIDATION_ERROR,
};
}
const normalizedSnapshotPair = normalizeWarningCriticalPair(
snapshot.snapshotDefaults.warningDays,
snapshot.snapshotDefaults.criticalDays,
);
const normalizedBackupPair = normalizeWarningCriticalPair(
snapshot.backupDefaults.warningDays,
snapshot.backupDefaults.criticalDays,
);
const normalizedCooldownMinutes = snapshot.scheduleCooldown.enabled
? clampCooldownMinutes(snapshot.scheduleCooldown.minutes)
: 0;
const normalizedMaxAlertsHour = clampMaxAlertsPerHour(snapshot.scheduleCooldown.maxAlerts);
const groupingWindowSeconds =
snapshot.scheduleGrouping.enabled && snapshot.scheduleGrouping.window >= 0
? snapshot.scheduleGrouping.window * 60
: 0;
const groupingEnabled = snapshot.scheduleGrouping.enabled && groupingWindowSeconds > 0;
return {
alertConfig: {
enabled: alertsActivationConfig?.enabled ?? true,
activationState: alertsActivationState ?? undefined,
activationTime: alertsActivationConfig?.activationTime ?? undefined,
observationWindowHours: alertsActivationConfig?.observationWindowHours ?? undefined,
disableAllNodes: snapshot.disableAllNodes,
disableAllGuests: snapshot.disableAllGuests,
disableAllAgents: snapshot.disableAllAgents,
disableAllStorage: snapshot.disableAllStorage,
disableAllPBS: snapshot.disableAllPBS,
disableAllPMG: snapshot.disableAllPMG,
disableAllDockerHosts: snapshot.disableAllDockerHosts,
disableAllDockerContainers: snapshot.disableAllDockerContainers,
disableAllDockerServices: snapshot.disableAllDockerServices,
disableAllNodesOffline: snapshot.disableAllNodesOffline,
disableAllGuestsOffline: snapshot.disableAllGuestsOffline,
disableAllPBSOffline: snapshot.disableAllPBSOffline,
disableAllAgentsOffline: snapshot.disableAllAgentsOffline,
disableAllPMGOffline: snapshot.disableAllPMGOffline,
disableAllDockerHostsOffline: snapshot.disableAllDockerHostsOffline,
guestDefaults: {
cpu: createHysteresisThreshold(snapshot.guestDefaults.cpu),
memory: createHysteresisThreshold(snapshot.guestDefaults.memory),
disk: createHysteresisThreshold(snapshot.guestDefaults.disk),
diskRead: createHysteresisThreshold(snapshot.guestDefaults.diskRead),
diskWrite: createHysteresisThreshold(snapshot.guestDefaults.diskWrite),
networkIn: createHysteresisThreshold(snapshot.guestDefaults.networkIn),
networkOut: createHysteresisThreshold(snapshot.guestDefaults.networkOut),
disableConnectivity: snapshot.guestDisableConnectivity,
poweredOffSeverity: snapshot.guestPoweredOffSeverity,
},
nodeDefaults: {
cpu: createHysteresisThreshold(snapshot.nodeDefaults.cpu),
memory: createHysteresisThreshold(snapshot.nodeDefaults.memory),
disk: createHysteresisThreshold(snapshot.nodeDefaults.disk),
temperature: createHysteresisThreshold(snapshot.nodeDefaults.temperature),
},
agentDefaults: {
cpu: createHysteresisThreshold(snapshot.agentDefaults.cpu),
memory: createHysteresisThreshold(snapshot.agentDefaults.memory),
disk: createHysteresisThreshold(snapshot.agentDefaults.disk),
diskTemperature: createHysteresisThreshold(snapshot.agentDefaults.diskTemperature),
},
pbsDefaults: {
cpu: createHysteresisThreshold(snapshot.pbsDefaults.cpu),
memory: createHysteresisThreshold(snapshot.pbsDefaults.memory),
},
dockerDefaults: {
cpu: createHysteresisThreshold(snapshot.dockerDefaults.cpu),
memory: createHysteresisThreshold(snapshot.dockerDefaults.memory),
disk: createHysteresisThreshold(snapshot.dockerDefaults.disk),
restartCount: snapshot.dockerDefaults.restartCount,
restartWindow: snapshot.dockerDefaults.restartWindow,
memoryWarnPct: snapshot.dockerDefaults.memoryWarnPct,
memoryCriticalPct: snapshot.dockerDefaults.memoryCriticalPct,
serviceWarnGapPercent: snapshot.dockerDefaults.serviceWarnGapPercent,
serviceCriticalGapPercent: snapshot.dockerDefaults.serviceCriticalGapPercent,
stateDisableConnectivity: snapshot.dockerDisableConnectivity,
statePoweredOffSeverity: snapshot.dockerPoweredOffSeverity,
},
dockerIgnoredContainerPrefixes: normalizeStringList(snapshot.dockerIgnoredPrefixes),
ignoredGuestPrefixes: normalizeStringList(snapshot.ignoredGuestPrefixes),
guestTagWhitelist: normalizeStringList(snapshot.guestTagWhitelist),
guestTagBlacklist: normalizeStringList(snapshot.guestTagBlacklist),
storageDefault: createHysteresisThreshold(snapshot.storageDefault),
minimumDelta: 2.0,
suppressionWindow: 5,
hysteresisMargin: 5.0,
timeThresholds: { ...snapshot.timeThresholds },
metricTimeThresholds: normalizeMetricDelayMap(snapshot.metricTimeThresholds),
snapshotDefaults: {
enabled: snapshot.snapshotDefaults.enabled,
warningDays: normalizedSnapshotPair.warning,
criticalDays: normalizedSnapshotPair.critical,
},
backupDefaults: {
enabled: snapshot.backupDefaults.enabled,
warningDays: normalizedBackupPair.warning,
criticalDays: normalizedBackupPair.critical,
freshHours: snapshot.backupDefaults.freshHours ?? 24,
staleHours: snapshot.backupDefaults.staleHours ?? 72,
alertOrphaned: snapshot.backupDefaults.alertOrphaned ?? true,
ignoreVMIDs: normalizeStringList(snapshot.backupDefaults.ignoreVMIDs),
},
pmgDefaults: { ...snapshot.pmgThresholds },
overrides: rawOverridesConfig,
schedule: {
quietHours: {
...snapshot.scheduleQuietHours,
days: cloneDays(snapshot.scheduleQuietHours.days),
},
cooldown: normalizedCooldownMinutes,
notifyOnResolve: snapshot.notifyOnResolve,
maxAlertsHour: normalizedMaxAlertsHour,
escalation: {
enabled: snapshot.scheduleEscalation.enabled,
levels: snapshot.scheduleEscalation.levels.map((level) => ({ ...level })),
},
grouping: {
enabled: groupingEnabled,
window: groupingWindowSeconds,
byNode: snapshot.scheduleGrouping.byNode,
byGuest: snapshot.scheduleGrouping.byGuest,
},
},
aggregation: {
enabled: true,
timeWindow: 10,
countThreshold: 3,
similarityWindow: 5.0,
},
flapping: {
enabled: true,
threshold: 5,
window: 10,
suppressionTime: 30,
minStability: 0.8,
},
ioNormalization: {
enabled: true,
vmDiskMax: 500.0,
containerDiskMax: 300.0,
networkMax: 1000.0,
},
},
};
}
@@ -15,29 +15,32 @@ import {
import { logger } from '@/utils/logger';
import {
clampMaxAlertsPerHour,
createDefaultCooldown,
createDefaultEscalation,
createDefaultGrouping,
createDefaultQuietHours,
createDefaultResolveNotifications,
DEFAULT_DELAY_SECONDS,
fallbackMaxAlertsPerHour,
getTriggerValue,
normalizeMetricDelayMap,
} from './helpers';
ALERT_DOCKER_GAP_VALIDATION_ERROR,
createDefaultAlertsConfigurationSnapshot,
buildAlertsConfigurationPayload,
FACTORY_AGENT_DEFAULTS,
FACTORY_BACKUP_DEFAULTS,
FACTORY_DOCKER_DEFAULTS,
FACTORY_DOCKER_STATE_DISABLE_CONNECTIVITY,
FACTORY_DOCKER_STATE_SEVERITY,
FACTORY_GUEST_DEFAULTS,
FACTORY_NODE_DEFAULTS,
FACTORY_PBS_DEFAULTS,
FACTORY_SNAPSHOT_DEFAULTS,
FACTORY_STORAGE_DEFAULT,
readAlertsConfigurationSnapshot,
type AlertsConfigurationSnapshot,
} from './alertsConfigurationModel';
import { useAlertDestinationsState } from './useAlertDestinationsState';
import { useAlertOverridesState } from './useAlertOverridesState';
import type {
AlertTab,
CooldownConfig,
EscalationConfig,
EscalationNotifyTarget,
GroupingConfig,
Override,
QuietHoursConfig,
} from './types';
import { GROUPING_WINDOW_DEFAULT_SECONDS, clampCooldownMinutes } from './types';
export interface AlertsConfigurationSurfaceProps {
activeTab: Accessor<AlertTab>;
@@ -57,155 +60,104 @@ export interface AlertsConfigurationSurfaceProps {
} | null;
}
export const FACTORY_GUEST_DEFAULTS = {
cpu: 80,
memory: 85,
disk: 90,
diskRead: -1,
diskWrite: -1,
networkIn: -1,
networkOut: -1,
};
export const FACTORY_NODE_DEFAULTS = {
cpu: 80,
memory: 85,
disk: 90,
temperature: 80,
};
export const FACTORY_PBS_DEFAULTS = {
cpu: 80,
memory: 85,
};
export const FACTORY_AGENT_DEFAULTS = {
cpu: 80,
memory: 85,
disk: 90,
diskTemperature: 55,
};
export const FACTORY_DOCKER_DEFAULTS = {
cpu: 80,
memory: 85,
disk: 85,
restartCount: 3,
restartWindow: 300,
memoryWarnPct: 90,
memoryCriticalPct: 95,
serviceWarnGapPercent: 10,
serviceCriticalGapPercent: 50,
};
export const FACTORY_DOCKER_STATE_DISABLE_CONNECTIVITY = false;
export const FACTORY_DOCKER_STATE_SEVERITY: 'warning' | 'critical' = 'warning';
export const FACTORY_STORAGE_DEFAULT = 85;
export const FACTORY_SNAPSHOT_DEFAULTS: SnapshotAlertConfig = {
enabled: false,
warningDays: 30,
criticalDays: 45,
};
export const FACTORY_BACKUP_DEFAULTS: BackupAlertConfig = {
enabled: false,
warningDays: 7,
criticalDays: 14,
freshHours: 24,
staleHours: 72,
alertOrphaned: true,
ignoreVMIDs: [],
};
export function useAlertsConfigurationState(props: AlertsConfigurationSurfaceProps) {
const defaultSnapshot = createDefaultAlertsConfigurationSnapshot();
const [isReloadingConfig, setIsReloadingConfig] = createSignal(false);
const [scheduleQuietHours, setScheduleQuietHours] =
createSignal<QuietHoursConfig>(createDefaultQuietHours());
const [scheduleCooldown, setScheduleCooldown] =
createSignal<CooldownConfig>(createDefaultCooldown());
const [scheduleGrouping, setScheduleGrouping] =
createSignal<GroupingConfig>(createDefaultGrouping());
const [scheduleEscalation, setScheduleEscalation] =
createSignal<EscalationConfig>(createDefaultEscalation());
const [notifyOnResolve, setNotifyOnResolve] = createSignal<boolean>(
createDefaultResolveNotifications(),
const [scheduleQuietHours, setScheduleQuietHours] = createSignal<QuietHoursConfig>(
defaultSnapshot.scheduleQuietHours,
);
const [scheduleCooldown, setScheduleCooldown] = createSignal<CooldownConfig>(
defaultSnapshot.scheduleCooldown,
);
const [scheduleGrouping, setScheduleGrouping] = createSignal<GroupingConfig>(
defaultSnapshot.scheduleGrouping,
);
const [scheduleEscalation, setScheduleEscalation] = createSignal<EscalationConfig>(
defaultSnapshot.scheduleEscalation,
);
const [notifyOnResolve, setNotifyOnResolve] = createSignal<boolean>(defaultSnapshot.notifyOnResolve);
const [guestDefaults, setGuestDefaults] = createSignal<Record<string, number | undefined>>(
defaultSnapshot.guestDefaults,
);
const [guestDisableConnectivity, setGuestDisableConnectivity] = createSignal(
defaultSnapshot.guestDisableConnectivity,
);
const [guestDefaults, setGuestDefaults] = createSignal<Record<string, number | undefined>>({
...FACTORY_GUEST_DEFAULTS,
});
const [guestDisableConnectivity, setGuestDisableConnectivity] = createSignal(false);
const [guestPoweredOffSeverity, setGuestPoweredOffSeverity] = createSignal<
'warning' | 'critical'
>('warning');
const [nodeDefaults, setNodeDefaults] = createSignal<Record<string, number | undefined>>({
...FACTORY_NODE_DEFAULTS,
});
const [pbsDefaults, setPBSDefaults] = createSignal<Record<string, number | undefined>>({
...FACTORY_PBS_DEFAULTS,
});
const [agentDefaults, setAgentDefaults] = createSignal<Record<string, number | undefined>>({
...FACTORY_AGENT_DEFAULTS,
});
const [dockerDefaults, setDockerDefaults] = createSignal({ ...FACTORY_DOCKER_DEFAULTS });
>(defaultSnapshot.guestPoweredOffSeverity);
const [nodeDefaults, setNodeDefaults] = createSignal<Record<string, number | undefined>>(
defaultSnapshot.nodeDefaults,
);
const [pbsDefaults, setPBSDefaults] = createSignal<Record<string, number | undefined>>(
defaultSnapshot.pbsDefaults,
);
const [agentDefaults, setAgentDefaults] = createSignal<Record<string, number | undefined>>(
defaultSnapshot.agentDefaults,
);
const [dockerDefaults, setDockerDefaults] = createSignal(defaultSnapshot.dockerDefaults);
const [dockerDisableConnectivity, setDockerDisableConnectivity] = createSignal(
FACTORY_DOCKER_STATE_DISABLE_CONNECTIVITY,
defaultSnapshot.dockerDisableConnectivity,
);
const [dockerPoweredOffSeverity, setDockerPoweredOffSeverity] = createSignal<
'warning' | 'critical'
>(FACTORY_DOCKER_STATE_SEVERITY);
const [dockerIgnoredPrefixes, setDockerIgnoredPrefixes] = createSignal<string[]>([]);
const [ignoredGuestPrefixes, setIgnoredGuestPrefixes] = createSignal<string[]>([]);
const [guestTagWhitelist, setGuestTagWhitelist] = createSignal<string[]>([]);
const [guestTagBlacklist, setGuestTagBlacklist] = createSignal<string[]>([]);
const [storageDefault, setStorageDefault] = createSignal(FACTORY_STORAGE_DEFAULT);
const [backupDefaults, setBackupDefaults] = createSignal<BackupAlertConfig>({
...FACTORY_BACKUP_DEFAULTS,
});
const [timeThresholds, setTimeThresholds] = createSignal({
guest: DEFAULT_DELAY_SECONDS,
node: DEFAULT_DELAY_SECONDS,
storage: DEFAULT_DELAY_SECONDS,
pbs: DEFAULT_DELAY_SECONDS,
agent: DEFAULT_DELAY_SECONDS,
});
>(defaultSnapshot.dockerPoweredOffSeverity);
const [dockerIgnoredPrefixes, setDockerIgnoredPrefixes] = createSignal<string[]>(
defaultSnapshot.dockerIgnoredPrefixes,
);
const [ignoredGuestPrefixes, setIgnoredGuestPrefixes] = createSignal<string[]>(
defaultSnapshot.ignoredGuestPrefixes,
);
const [guestTagWhitelist, setGuestTagWhitelist] = createSignal<string[]>(
defaultSnapshot.guestTagWhitelist,
);
const [guestTagBlacklist, setGuestTagBlacklist] = createSignal<string[]>(
defaultSnapshot.guestTagBlacklist,
);
const [storageDefault, setStorageDefault] = createSignal(defaultSnapshot.storageDefault);
const [backupDefaults, setBackupDefaults] = createSignal<BackupAlertConfig>(
defaultSnapshot.backupDefaults,
);
const [timeThresholds, setTimeThresholds] = createSignal(defaultSnapshot.timeThresholds);
const [metricTimeThresholds, setMetricTimeThresholds] = createSignal<
Record<string, Record<string, number>>
>({});
const [snapshotDefaults, setSnapshotDefaults] = createSignal<SnapshotAlertConfig>({
...FACTORY_SNAPSHOT_DEFAULTS,
});
const [pmgThresholds, setPMGThresholds] = createSignal({
queueTotalWarning: 500,
queueTotalCritical: 1000,
oldestMessageWarnMins: 30,
oldestMessageCritMins: 60,
deferredQueueWarn: 200,
deferredQueueCritical: 500,
holdQueueWarn: 100,
holdQueueCritical: 300,
quarantineSpamWarn: 2000,
quarantineSpamCritical: 5000,
quarantineVirusWarn: 2000,
quarantineVirusCritical: 5000,
quarantineGrowthWarnPct: 25,
quarantineGrowthWarnMin: 250,
quarantineGrowthCritPct: 50,
quarantineGrowthCritMin: 500,
});
const [disableAllNodes, setDisableAllNodes] = createSignal(false);
const [disableAllGuests, setDisableAllGuests] = createSignal(false);
const [disableAllAgents, setDisableAllAgents] = createSignal(false);
const [disableAllStorage, setDisableAllStorage] = createSignal(false);
const [disableAllPBS, setDisableAllPBS] = createSignal(false);
const [disableAllPMG, setDisableAllPMG] = createSignal(false);
const [disableAllDockerHosts, setDisableAllDockerHosts] = createSignal(false);
const [disableAllDockerServices, setDisableAllDockerServices] = createSignal(false);
const [disableAllDockerContainers, setDisableAllDockerContainers] = createSignal(false);
const [disableAllNodesOffline, setDisableAllNodesOffline] = createSignal(false);
const [disableAllGuestsOffline, setDisableAllGuestsOffline] = createSignal(false);
const [disableAllAgentsOffline, setDisableAllAgentsOffline] = createSignal(false);
const [disableAllPBSOffline, setDisableAllPBSOffline] = createSignal(false);
const [disableAllPMGOffline, setDisableAllPMGOffline] = createSignal(false);
const [disableAllDockerHostsOffline, setDisableAllDockerHostsOffline] = createSignal(false);
>(defaultSnapshot.metricTimeThresholds);
const [snapshotDefaults, setSnapshotDefaults] = createSignal<SnapshotAlertConfig>(
defaultSnapshot.snapshotDefaults,
);
const [pmgThresholds, setPMGThresholds] = createSignal(defaultSnapshot.pmgThresholds);
const [disableAllNodes, setDisableAllNodes] = createSignal(defaultSnapshot.disableAllNodes);
const [disableAllGuests, setDisableAllGuests] = createSignal(defaultSnapshot.disableAllGuests);
const [disableAllAgents, setDisableAllAgents] = createSignal(defaultSnapshot.disableAllAgents);
const [disableAllStorage, setDisableAllStorage] = createSignal(defaultSnapshot.disableAllStorage);
const [disableAllPBS, setDisableAllPBS] = createSignal(defaultSnapshot.disableAllPBS);
const [disableAllPMG, setDisableAllPMG] = createSignal(defaultSnapshot.disableAllPMG);
const [disableAllDockerHosts, setDisableAllDockerHosts] = createSignal(
defaultSnapshot.disableAllDockerHosts,
);
const [disableAllDockerServices, setDisableAllDockerServices] = createSignal(
defaultSnapshot.disableAllDockerServices,
);
const [disableAllDockerContainers, setDisableAllDockerContainers] = createSignal(
defaultSnapshot.disableAllDockerContainers,
);
const [disableAllNodesOffline, setDisableAllNodesOffline] = createSignal(
defaultSnapshot.disableAllNodesOffline,
);
const [disableAllGuestsOffline, setDisableAllGuestsOffline] = createSignal(
defaultSnapshot.disableAllGuestsOffline,
);
const [disableAllAgentsOffline, setDisableAllAgentsOffline] = createSignal(
defaultSnapshot.disableAllAgentsOffline,
);
const [disableAllPBSOffline, setDisableAllPBSOffline] = createSignal(
defaultSnapshot.disableAllPBSOffline,
);
const [disableAllPMGOffline, setDisableAllPMGOffline] = createSignal(
defaultSnapshot.disableAllPMGOffline,
);
const [disableAllDockerHostsOffline, setDisableAllDockerHostsOffline] = createSignal(
defaultSnapshot.disableAllDockerHostsOffline,
);
const destinationsState = useAlertDestinationsState({ activeTab: props.activeTab });
const overridesState = useAlertOverridesState({
allResources: props.allResources,
@@ -215,6 +167,110 @@ export function useAlertsConfigurationState(props: AlertsConfigurationSurfacePro
setOverviewOverrides: props.setOverviewOverrides,
});
const applyConfigurationSnapshot = (snapshot: AlertsConfigurationSnapshot) => {
setScheduleQuietHours({
...snapshot.scheduleQuietHours,
days: { ...snapshot.scheduleQuietHours.days },
suppress: { ...snapshot.scheduleQuietHours.suppress },
});
setScheduleCooldown({ ...snapshot.scheduleCooldown });
setScheduleGrouping({ ...snapshot.scheduleGrouping });
setScheduleEscalation({
enabled: snapshot.scheduleEscalation.enabled,
levels: snapshot.scheduleEscalation.levels.map((level) => ({ ...level })),
});
setNotifyOnResolve(snapshot.notifyOnResolve);
setGuestDefaults({ ...snapshot.guestDefaults });
setGuestDisableConnectivity(snapshot.guestDisableConnectivity);
setGuestPoweredOffSeverity(snapshot.guestPoweredOffSeverity);
setNodeDefaults({ ...snapshot.nodeDefaults });
setPBSDefaults({ ...snapshot.pbsDefaults });
setAgentDefaults({ ...snapshot.agentDefaults });
setDockerDefaults({ ...snapshot.dockerDefaults });
setDockerDisableConnectivity(snapshot.dockerDisableConnectivity);
setDockerPoweredOffSeverity(snapshot.dockerPoweredOffSeverity);
setDockerIgnoredPrefixes([...snapshot.dockerIgnoredPrefixes]);
setIgnoredGuestPrefixes([...snapshot.ignoredGuestPrefixes]);
setGuestTagWhitelist([...snapshot.guestTagWhitelist]);
setGuestTagBlacklist([...snapshot.guestTagBlacklist]);
setStorageDefault(snapshot.storageDefault);
setBackupDefaults({
...snapshot.backupDefaults,
ignoreVMIDs: [...(snapshot.backupDefaults.ignoreVMIDs ?? [])],
});
setTimeThresholds({ ...snapshot.timeThresholds });
setMetricTimeThresholds(structuredClone(snapshot.metricTimeThresholds));
setSnapshotDefaults({ ...snapshot.snapshotDefaults });
setPMGThresholds({ ...snapshot.pmgThresholds });
setDisableAllNodes(snapshot.disableAllNodes);
setDisableAllGuests(snapshot.disableAllGuests);
setDisableAllAgents(snapshot.disableAllAgents);
setDisableAllStorage(snapshot.disableAllStorage);
setDisableAllPBS(snapshot.disableAllPBS);
setDisableAllPMG(snapshot.disableAllPMG);
setDisableAllDockerHosts(snapshot.disableAllDockerHosts);
setDisableAllDockerServices(snapshot.disableAllDockerServices);
setDisableAllDockerContainers(snapshot.disableAllDockerContainers);
setDisableAllNodesOffline(snapshot.disableAllNodesOffline);
setDisableAllGuestsOffline(snapshot.disableAllGuestsOffline);
setDisableAllAgentsOffline(snapshot.disableAllAgentsOffline);
setDisableAllPBSOffline(snapshot.disableAllPBSOffline);
setDisableAllPMGOffline(snapshot.disableAllPMGOffline);
setDisableAllDockerHostsOffline(snapshot.disableAllDockerHostsOffline);
};
const captureConfigurationSnapshot = (): AlertsConfigurationSnapshot => ({
scheduleQuietHours: {
...scheduleQuietHours(),
days: { ...scheduleQuietHours().days },
suppress: { ...scheduleQuietHours().suppress },
},
scheduleCooldown: { ...scheduleCooldown() },
scheduleGrouping: { ...scheduleGrouping() },
scheduleEscalation: {
enabled: scheduleEscalation().enabled,
levels: scheduleEscalation().levels.map((level) => ({ ...level })),
},
notifyOnResolve: notifyOnResolve(),
guestDefaults: { ...guestDefaults() },
guestDisableConnectivity: guestDisableConnectivity(),
guestPoweredOffSeverity: guestPoweredOffSeverity(),
nodeDefaults: { ...nodeDefaults() },
pbsDefaults: { ...pbsDefaults() },
agentDefaults: { ...agentDefaults() },
dockerDefaults: { ...dockerDefaults() },
dockerDisableConnectivity: dockerDisableConnectivity(),
dockerPoweredOffSeverity: dockerPoweredOffSeverity(),
dockerIgnoredPrefixes: [...dockerIgnoredPrefixes()],
ignoredGuestPrefixes: [...ignoredGuestPrefixes()],
guestTagWhitelist: [...guestTagWhitelist()],
guestTagBlacklist: [...guestTagBlacklist()],
storageDefault: storageDefault(),
backupDefaults: {
...backupDefaults(),
ignoreVMIDs: [...(backupDefaults().ignoreVMIDs ?? [])],
},
timeThresholds: { ...timeThresholds() },
metricTimeThresholds: structuredClone(metricTimeThresholds()),
snapshotDefaults: { ...snapshotDefaults() },
pmgThresholds: { ...pmgThresholds() },
disableAllNodes: disableAllNodes(),
disableAllGuests: disableAllGuests(),
disableAllAgents: disableAllAgents(),
disableAllStorage: disableAllStorage(),
disableAllPBS: disableAllPBS(),
disableAllPMG: disableAllPMG(),
disableAllDockerHosts: disableAllDockerHosts(),
disableAllDockerServices: disableAllDockerServices(),
disableAllDockerContainers: disableAllDockerContainers(),
disableAllNodesOffline: disableAllNodesOffline(),
disableAllGuestsOffline: disableAllGuestsOffline(),
disableAllAgentsOffline: disableAllAgentsOffline(),
disableAllPBSOffline: disableAllPBSOffline(),
disableAllPMGOffline: disableAllPMGOffline(),
disableAllDockerHostsOffline: disableAllDockerHostsOffline(),
});
const resetGuestDefaults = () => {
setGuestDefaults({ ...FACTORY_GUEST_DEFAULTS });
props.setHasUnsavedChanges(true);
@@ -258,313 +314,14 @@ export function useAlertsConfigurationState(props: AlertsConfigurationSurfacePro
setIsReloadingConfig(true);
props.setHasUnsavedChanges(false);
destinationsState.resetDestinations();
setGuestDefaults({ ...FACTORY_GUEST_DEFAULTS });
setGuestDisableConnectivity(false);
setGuestPoweredOffSeverity('warning');
setNodeDefaults({ ...FACTORY_NODE_DEFAULTS });
setPBSDefaults({ ...FACTORY_PBS_DEFAULTS });
setAgentDefaults({ ...FACTORY_AGENT_DEFAULTS });
setDockerDefaults({ ...FACTORY_DOCKER_DEFAULTS });
setDockerDisableConnectivity(FACTORY_DOCKER_STATE_DISABLE_CONNECTIVITY);
setDockerPoweredOffSeverity(FACTORY_DOCKER_STATE_SEVERITY);
setDockerIgnoredPrefixes([]);
setIgnoredGuestPrefixes([]);
setGuestTagWhitelist([]);
setGuestTagBlacklist([]);
setStorageDefault(FACTORY_STORAGE_DEFAULT);
setTimeThresholds({
guest: DEFAULT_DELAY_SECONDS,
node: DEFAULT_DELAY_SECONDS,
storage: DEFAULT_DELAY_SECONDS,
pbs: DEFAULT_DELAY_SECONDS,
agent: DEFAULT_DELAY_SECONDS,
});
setMetricTimeThresholds({});
setScheduleQuietHours(createDefaultQuietHours());
setScheduleCooldown(createDefaultCooldown());
setScheduleGrouping(createDefaultGrouping());
setScheduleEscalation(createDefaultEscalation());
setNotifyOnResolve(createDefaultResolveNotifications());
setBackupDefaults({ ...FACTORY_BACKUP_DEFAULTS });
setSnapshotDefaults({ ...FACTORY_SNAPSHOT_DEFAULTS });
applyConfigurationSnapshot(createDefaultAlertsConfigurationSnapshot());
try {
const config = await AlertsAPI.getConfig();
if (config.guestDefaults) {
setGuestDefaults({
cpu: getTriggerValue(config.guestDefaults.cpu) ?? FACTORY_GUEST_DEFAULTS.cpu,
memory: getTriggerValue(config.guestDefaults.memory) ?? FACTORY_GUEST_DEFAULTS.memory,
disk: getTriggerValue(config.guestDefaults.disk) ?? FACTORY_GUEST_DEFAULTS.disk,
diskRead:
getTriggerValue(config.guestDefaults.diskRead) ?? FACTORY_GUEST_DEFAULTS.diskRead,
diskWrite:
getTriggerValue(config.guestDefaults.diskWrite) ?? FACTORY_GUEST_DEFAULTS.diskWrite,
networkIn:
getTriggerValue(config.guestDefaults.networkIn) ?? FACTORY_GUEST_DEFAULTS.networkIn,
networkOut:
getTriggerValue(config.guestDefaults.networkOut) ?? FACTORY_GUEST_DEFAULTS.networkOut,
});
setGuestDisableConnectivity(Boolean(config.guestDefaults.disableConnectivity));
setGuestPoweredOffSeverity(
config.guestDefaults.poweredOffSeverity === 'critical' ? 'critical' : 'warning',
);
}
if (config.nodeDefaults) {
setNodeDefaults({
cpu: getTriggerValue(config.nodeDefaults.cpu) ?? FACTORY_NODE_DEFAULTS.cpu,
memory: getTriggerValue(config.nodeDefaults.memory) ?? FACTORY_NODE_DEFAULTS.memory,
disk: getTriggerValue(config.nodeDefaults.disk) ?? FACTORY_NODE_DEFAULTS.disk,
temperature:
getTriggerValue(config.nodeDefaults.temperature) ?? FACTORY_NODE_DEFAULTS.temperature,
});
}
if (config.pbsDefaults) {
setPBSDefaults({
cpu: getTriggerValue(config.pbsDefaults.cpu) ?? FACTORY_PBS_DEFAULTS.cpu,
memory: getTriggerValue(config.pbsDefaults.memory) ?? FACTORY_PBS_DEFAULTS.memory,
});
}
if (config.agentDefaults) {
setAgentDefaults({
cpu: getTriggerValue(config.agentDefaults.cpu) ?? FACTORY_AGENT_DEFAULTS.cpu,
memory: getTriggerValue(config.agentDefaults.memory) ?? FACTORY_AGENT_DEFAULTS.memory,
disk: getTriggerValue(config.agentDefaults.disk) ?? FACTORY_AGENT_DEFAULTS.disk,
diskTemperature:
getTriggerValue(config.agentDefaults.diskTemperature) ??
FACTORY_AGENT_DEFAULTS.diskTemperature,
});
}
if (config.dockerDefaults) {
const normalizeGap = (value: unknown, fallback: number) => {
const numeric = Number(value);
if (!Number.isFinite(numeric)) {
return fallback;
}
return Math.max(0, Math.min(100, numeric));
};
const serviceWarnGap = normalizeGap(
config.dockerDefaults.serviceWarnGapPercent,
FACTORY_DOCKER_DEFAULTS.serviceWarnGapPercent,
);
let serviceCriticalGap = normalizeGap(
config.dockerDefaults.serviceCriticalGapPercent,
FACTORY_DOCKER_DEFAULTS.serviceCriticalGapPercent,
);
if (serviceCriticalGap > 0 && serviceWarnGap > serviceCriticalGap) {
serviceCriticalGap = serviceWarnGap;
}
setDockerDefaults({
cpu: getTriggerValue(config.dockerDefaults.cpu) ?? FACTORY_DOCKER_DEFAULTS.cpu,
memory: getTriggerValue(config.dockerDefaults.memory) ?? FACTORY_DOCKER_DEFAULTS.memory,
disk: getTriggerValue(config.dockerDefaults.disk) ?? FACTORY_DOCKER_DEFAULTS.disk,
restartCount:
config.dockerDefaults.restartCount ?? FACTORY_DOCKER_DEFAULTS.restartCount,
restartWindow:
config.dockerDefaults.restartWindow ?? FACTORY_DOCKER_DEFAULTS.restartWindow,
memoryWarnPct:
config.dockerDefaults.memoryWarnPct ?? FACTORY_DOCKER_DEFAULTS.memoryWarnPct,
memoryCriticalPct:
config.dockerDefaults.memoryCriticalPct ??
FACTORY_DOCKER_DEFAULTS.memoryCriticalPct,
serviceWarnGapPercent: serviceWarnGap,
serviceCriticalGapPercent: serviceCriticalGap,
});
setDockerDisableConnectivity(Boolean(config.dockerDefaults.stateDisableConnectivity));
setDockerPoweredOffSeverity(
config.dockerDefaults.statePoweredOffSeverity === 'critical' ? 'critical' : 'warning',
);
}
setDockerIgnoredPrefixes(config.dockerIgnoredContainerPrefixes ?? []);
setIgnoredGuestPrefixes(config.ignoredGuestPrefixes ?? []);
setGuestTagWhitelist(config.guestTagWhitelist ?? []);
setGuestTagBlacklist(config.guestTagBlacklist ?? []);
if (config.storageDefault) {
setStorageDefault(getTriggerValue(config.storageDefault) ?? FACTORY_STORAGE_DEFAULT);
}
if (config.timeThresholds) {
setTimeThresholds({
guest: config.timeThresholds.guest ?? DEFAULT_DELAY_SECONDS,
node: config.timeThresholds.node ?? DEFAULT_DELAY_SECONDS,
storage: config.timeThresholds.storage ?? DEFAULT_DELAY_SECONDS,
pbs: config.timeThresholds.pbs ?? DEFAULT_DELAY_SECONDS,
agent: config.timeThresholds.agent ?? DEFAULT_DELAY_SECONDS,
});
}
if (config.metricTimeThresholds) {
setMetricTimeThresholds(normalizeMetricDelayMap(config.metricTimeThresholds));
}
if (config.backupDefaults) {
const enabled = Boolean(config.backupDefaults.enabled);
const rawWarning =
config.backupDefaults.warningDays ?? FACTORY_BACKUP_DEFAULTS.warningDays;
const rawCritical =
config.backupDefaults.criticalDays ?? FACTORY_BACKUP_DEFAULTS.criticalDays;
const safeCritical = Math.max(0, rawCritical);
const normalizedWarning = Math.max(0, rawWarning);
const warningDays =
safeCritical > 0 && normalizedWarning > safeCritical ? safeCritical : normalizedWarning;
const criticalDays = Math.max(safeCritical, warningDays);
const freshHours =
config.backupDefaults.freshHours ?? FACTORY_BACKUP_DEFAULTS.freshHours;
const staleHours =
config.backupDefaults.staleHours ?? FACTORY_BACKUP_DEFAULTS.staleHours;
const alertOrphaned =
config.backupDefaults.alertOrphaned ?? FACTORY_BACKUP_DEFAULTS.alertOrphaned ?? true;
const ignoreVMIDs = Array.from(
new Set(
(config.backupDefaults.ignoreVMIDs ?? FACTORY_BACKUP_DEFAULTS.ignoreVMIDs ?? [])
.map((value) => value.trim())
.filter((value) => value.length > 0),
),
);
setBackupDefaults({
enabled,
warningDays,
criticalDays,
freshHours,
staleHours,
alertOrphaned,
ignoreVMIDs,
});
}
if (config.snapshotDefaults) {
const enabled = Boolean(config.snapshotDefaults.enabled);
const rawWarning =
config.snapshotDefaults.warningDays ?? FACTORY_SNAPSHOT_DEFAULTS.warningDays;
const rawCritical =
config.snapshotDefaults.criticalDays ?? FACTORY_SNAPSHOT_DEFAULTS.criticalDays;
const safeCritical = Math.max(0, rawCritical);
const normalizedWarning = Math.max(0, rawWarning);
const warningDays =
safeCritical > 0 && normalizedWarning > safeCritical ? safeCritical : normalizedWarning;
const criticalDays = Math.max(safeCritical, warningDays);
setSnapshotDefaults({ enabled, warningDays, criticalDays });
}
if (config.pmgDefaults) {
setPMGThresholds({
queueTotalWarning: config.pmgDefaults.queueTotalWarning ?? 500,
queueTotalCritical: config.pmgDefaults.queueTotalCritical ?? 1000,
oldestMessageWarnMins: config.pmgDefaults.oldestMessageWarnMins ?? 30,
oldestMessageCritMins: config.pmgDefaults.oldestMessageCritMins ?? 60,
deferredQueueWarn: config.pmgDefaults.deferredQueueWarn ?? 200,
deferredQueueCritical: config.pmgDefaults.deferredQueueCritical ?? 500,
holdQueueWarn: config.pmgDefaults.holdQueueWarn ?? 100,
holdQueueCritical: config.pmgDefaults.holdQueueCritical ?? 300,
quarantineSpamWarn: config.pmgDefaults.quarantineSpamWarn ?? 2000,
quarantineSpamCritical: config.pmgDefaults.quarantineSpamCritical ?? 5000,
quarantineVirusWarn: config.pmgDefaults.quarantineVirusWarn ?? 2000,
quarantineVirusCritical: config.pmgDefaults.quarantineVirusCritical ?? 5000,
quarantineGrowthWarnPct: config.pmgDefaults.quarantineGrowthWarnPct ?? 25,
quarantineGrowthWarnMin: config.pmgDefaults.quarantineGrowthWarnMin ?? 250,
quarantineGrowthCritPct: config.pmgDefaults.quarantineGrowthCritPct ?? 50,
quarantineGrowthCritMin: config.pmgDefaults.quarantineGrowthCritMin ?? 500,
});
}
setDisableAllNodes(config.disableAllNodes ?? false);
setDisableAllGuests(config.disableAllGuests ?? false);
setDisableAllAgents(config.disableAllAgents ?? false);
setDisableAllStorage(config.disableAllStorage ?? false);
setDisableAllPBS(config.disableAllPBS ?? false);
setDisableAllPMG(config.disableAllPMG ?? false);
setDisableAllDockerHosts(config.disableAllDockerHosts ?? false);
setDisableAllDockerServices(config.disableAllDockerServices ?? false);
setDisableAllDockerContainers(config.disableAllDockerContainers ?? false);
setDisableAllNodesOffline(config.disableAllNodesOffline ?? false);
setDisableAllGuestsOffline(config.disableAllGuestsOffline ?? false);
setDisableAllAgentsOffline(config.disableAllAgentsOffline ?? false);
setDisableAllPBSOffline(config.disableAllPBSOffline ?? false);
setDisableAllPMGOffline(config.disableAllPMGOffline ?? false);
setDisableAllDockerHostsOffline(config.disableAllDockerHostsOffline ?? false);
applyConfigurationSnapshot(readAlertsConfigurationSnapshot(config));
overridesState.replaceRawOverridesConfig(config.overrides || {});
if (config.schedule) {
if (config.schedule.quietHours) {
const quietHours = config.schedule.quietHours;
const days = Array.isArray(quietHours.days)
? {
sunday: quietHours.days.includes(0),
monday: quietHours.days.includes(1),
tuesday: quietHours.days.includes(2),
wednesday: quietHours.days.includes(3),
thursday: quietHours.days.includes(4),
friday: quietHours.days.includes(5),
saturday: quietHours.days.includes(6),
}
: ((quietHours.days as Record<string, boolean>) || createDefaultQuietHours().days);
setScheduleQuietHours({
enabled: quietHours.enabled || false,
start: quietHours.start || '22:00',
end: quietHours.end || '08:00',
timezone:
quietHours.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC',
days,
suppress: {
performance: quietHours.suppress?.performance ?? false,
storage: quietHours.suppress?.storage ?? false,
offline: quietHours.suppress?.offline ?? false,
},
});
}
if (config.schedule.cooldown !== undefined) {
const rawCooldown = config.schedule.cooldown;
const cooldownEnabled = rawCooldown > 0;
setScheduleCooldown({
enabled: cooldownEnabled,
minutes: cooldownEnabled ? clampCooldownMinutes(rawCooldown) : 0,
maxAlerts: fallbackMaxAlertsPerHour(config.schedule.maxAlertsHour),
});
}
if (config.schedule.grouping) {
const groupingConfig = config.schedule.grouping;
const rawGroupingWindowSeconds =
typeof groupingConfig.window === 'number'
? groupingConfig.window
: GROUPING_WINDOW_DEFAULT_SECONDS;
const normalizedGroupingWindowSeconds = Math.max(0, rawGroupingWindowSeconds);
setScheduleGrouping({
enabled:
groupingConfig.enabled !== undefined
? Boolean(groupingConfig.enabled)
: normalizedGroupingWindowSeconds > 0,
window: Math.round(normalizedGroupingWindowSeconds / 60),
byNode: groupingConfig.byNode !== undefined ? groupingConfig.byNode : true,
byGuest: groupingConfig.byGuest !== undefined ? groupingConfig.byGuest : false,
});
}
if (config.schedule.notifyOnResolve !== undefined) {
setNotifyOnResolve(Boolean(config.schedule.notifyOnResolve));
}
if (config.schedule.escalation) {
const levels = (config.schedule.escalation.levels || []).map((level) => ({
after: typeof level.after === 'number' ? level.after : 15,
notify: (level.notify as EscalationNotifyTarget) || 'all',
}));
setScheduleEscalation({
enabled: Boolean(config.schedule.escalation.enabled),
levels,
});
}
}
await destinationsState.loadDestinations();
if (options.notify) {
@@ -581,182 +338,18 @@ export function useAlertsConfigurationState(props: AlertsConfigurationSurfacePro
};
const saveAlertConfiguration = async () => {
const createHysteresisThreshold = (trigger: number | undefined, clearMargin = 5) => {
const normalized = typeof trigger === 'number' ? trigger : 0;
return {
trigger: normalized,
clear: Math.max(0, normalized - clearMargin),
};
};
const snapshotConfig = snapshotDefaults();
const normalizedSnapshotWarning = Math.max(0, snapshotConfig.warningDays ?? 0);
const normalizedSnapshotCritical = Math.max(0, snapshotConfig.criticalDays ?? 0);
const finalSnapshotCritical =
normalizedSnapshotCritical > 0
? Math.max(normalizedSnapshotCritical, normalizedSnapshotWarning)
: normalizedSnapshotWarning;
const backupConfig = backupDefaults();
const normalizedBackupWarning = Math.max(0, backupConfig.warningDays ?? 0);
const normalizedBackupCritical = Math.max(0, backupConfig.criticalDays ?? 0);
const finalBackupCritical =
normalizedBackupCritical > 0
? Math.max(normalizedBackupCritical, normalizedBackupWarning)
: normalizedBackupWarning;
const dockerDefaultsValue = dockerDefaults();
if (
dockerDefaultsValue.serviceCriticalGapPercent > 0 &&
dockerDefaultsValue.serviceWarnGapPercent > dockerDefaultsValue.serviceCriticalGapPercent
) {
notificationStore.error(
'Swarm service critical gap must be greater than or equal to the warning gap when enabled.',
);
const result = buildAlertsConfigurationPayload({
snapshot: captureConfigurationSnapshot(),
rawOverridesConfig: overridesState.rawOverridesConfig(),
alertsActivationState: props.alertsActivationState(),
alertsActivationConfig: props.alertsActivationConfig(),
});
if (result.dockerValidationError) {
notificationStore.error(ALERT_DOCKER_GAP_VALIDATION_ERROR);
return;
}
const normalizedCooldownMinutes = scheduleCooldown().enabled
? clampCooldownMinutes(scheduleCooldown().minutes)
: 0;
const normalizedMaxAlertsHour = clampMaxAlertsPerHour(scheduleCooldown().maxAlerts);
const groupingState = scheduleGrouping();
const groupingWindowSeconds =
groupingState.enabled && groupingState.window >= 0 ? groupingState.window * 60 : 0;
const groupingEnabled = groupingState.enabled && groupingWindowSeconds > 0;
const activationConfig = props.alertsActivationConfig();
const alertConfig = {
enabled: activationConfig?.enabled ?? true,
activationState: props.alertsActivationState() ?? undefined,
activationTime: activationConfig?.activationTime ?? undefined,
observationWindowHours: activationConfig?.observationWindowHours ?? undefined,
disableAllNodes: disableAllNodes(),
disableAllGuests: disableAllGuests(),
disableAllAgents: disableAllAgents(),
disableAllStorage: disableAllStorage(),
disableAllPBS: disableAllPBS(),
disableAllPMG: disableAllPMG(),
disableAllDockerHosts: disableAllDockerHosts(),
disableAllDockerContainers: disableAllDockerContainers(),
disableAllDockerServices: disableAllDockerServices(),
disableAllNodesOffline: disableAllNodesOffline(),
disableAllGuestsOffline: disableAllGuestsOffline(),
disableAllPBSOffline: disableAllPBSOffline(),
disableAllAgentsOffline: disableAllAgentsOffline(),
disableAllPMGOffline: disableAllPMGOffline(),
disableAllDockerHostsOffline: disableAllDockerHostsOffline(),
guestDefaults: {
cpu: createHysteresisThreshold(guestDefaults().cpu),
memory: createHysteresisThreshold(guestDefaults().memory),
disk: createHysteresisThreshold(guestDefaults().disk),
diskRead: createHysteresisThreshold(guestDefaults().diskRead),
diskWrite: createHysteresisThreshold(guestDefaults().diskWrite),
networkIn: createHysteresisThreshold(guestDefaults().networkIn),
networkOut: createHysteresisThreshold(guestDefaults().networkOut),
disableConnectivity: guestDisableConnectivity(),
poweredOffSeverity: guestPoweredOffSeverity(),
},
nodeDefaults: {
cpu: createHysteresisThreshold(nodeDefaults().cpu),
memory: createHysteresisThreshold(nodeDefaults().memory),
disk: createHysteresisThreshold(nodeDefaults().disk),
temperature: createHysteresisThreshold(nodeDefaults().temperature),
},
agentDefaults: {
cpu: createHysteresisThreshold(agentDefaults().cpu),
memory: createHysteresisThreshold(agentDefaults().memory),
disk: createHysteresisThreshold(agentDefaults().disk),
diskTemperature: createHysteresisThreshold(agentDefaults().diskTemperature),
},
pbsDefaults: {
cpu: createHysteresisThreshold(pbsDefaults().cpu),
memory: createHysteresisThreshold(pbsDefaults().memory),
},
dockerDefaults: {
cpu: createHysteresisThreshold(dockerDefaultsValue.cpu),
memory: createHysteresisThreshold(dockerDefaultsValue.memory),
disk: createHysteresisThreshold(dockerDefaultsValue.disk),
restartCount: dockerDefaultsValue.restartCount,
restartWindow: dockerDefaultsValue.restartWindow,
memoryWarnPct: dockerDefaultsValue.memoryWarnPct,
memoryCriticalPct: dockerDefaultsValue.memoryCriticalPct,
serviceWarnGapPercent: dockerDefaultsValue.serviceWarnGapPercent,
serviceCriticalGapPercent: dockerDefaultsValue.serviceCriticalGapPercent,
stateDisableConnectivity: dockerDisableConnectivity(),
statePoweredOffSeverity: dockerPoweredOffSeverity(),
},
dockerIgnoredContainerPrefixes: dockerIgnoredPrefixes()
.map((prefix) => prefix.trim())
.filter((prefix) => prefix.length > 0),
ignoredGuestPrefixes: ignoredGuestPrefixes()
.map((prefix) => prefix.trim())
.filter((prefix) => prefix.length > 0),
guestTagWhitelist: guestTagWhitelist()
.map((tag) => tag.trim())
.filter((tag) => tag.length > 0),
guestTagBlacklist: guestTagBlacklist()
.map((tag) => tag.trim())
.filter((tag) => tag.length > 0),
storageDefault: createHysteresisThreshold(storageDefault()),
minimumDelta: 2.0,
suppressionWindow: 5,
hysteresisMargin: 5.0,
timeThresholds: timeThresholds(),
metricTimeThresholds: normalizeMetricDelayMap(metricTimeThresholds()),
snapshotDefaults: {
enabled: snapshotConfig.enabled,
warningDays: normalizedSnapshotWarning,
criticalDays: finalSnapshotCritical,
},
backupDefaults: {
enabled: backupConfig.enabled,
warningDays: normalizedBackupWarning,
criticalDays: finalBackupCritical,
freshHours: backupConfig.freshHours ?? 24,
staleHours: backupConfig.staleHours ?? 72,
alertOrphaned: backupConfig.alertOrphaned ?? true,
ignoreVMIDs: (backupConfig.ignoreVMIDs ?? [])
.map((value) => value.trim())
.filter((value) => value.length > 0),
},
pmgDefaults: pmgThresholds(),
overrides: overridesState.rawOverridesConfig(),
schedule: {
quietHours: scheduleQuietHours(),
cooldown: normalizedCooldownMinutes,
notifyOnResolve: notifyOnResolve(),
maxAlertsHour: normalizedMaxAlertsHour,
escalation: scheduleEscalation(),
grouping: {
enabled: groupingEnabled,
window: groupingWindowSeconds,
byNode: groupingState.byNode,
byGuest: groupingState.byGuest,
},
},
aggregation: {
enabled: true,
timeWindow: 10,
countThreshold: 3,
similarityWindow: 5.0,
},
flapping: {
enabled: true,
threshold: 5,
window: 10,
suppressionTime: 30,
minStability: 0.8,
},
ioNormalization: {
enabled: true,
vmDiskMax: 500.0,
containerDiskMax: 300.0,
networkMax: 1000.0,
},
};
await AlertsAPI.updateConfig(alertConfig);
await AlertsAPI.updateConfig(result.alertConfig!);
await destinationsState.saveDestinations();
props.setHasUnsavedChanges(false);
@@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest';
import alertsPageSource from '@/pages/Alerts.tsx?raw';
import alertsConfigurationSurfaceSource from '@/features/alerts/AlertsConfigurationSurface.tsx?raw';
import alertsConfigurationStateSource from '@/features/alerts/useAlertsConfigurationState.ts?raw';
import alertsConfigurationModelSource from '@/features/alerts/alertsConfigurationModel.ts?raw';
import alertOverridesStateSource from '@/features/alerts/useAlertOverridesState.ts?raw';
import alertDestinationsStateSource from '@/features/alerts/useAlertDestinationsState.ts?raw';
import alertDestinationsTabStateSource from '@/features/alerts/useAlertDestinationsTabState.ts?raw';
@@ -196,16 +197,32 @@ describe('tab path helpers', () => {
expect(alertsConfigurationSurfaceSource).not.toContain("eventBus.on('org_switched'");
expect(alertsConfigurationStateSource).toContain('export function useAlertsConfigurationState');
expect(alertsConfigurationStateSource).toContain('AlertsAPI.getConfig');
expect(alertsConfigurationStateSource).toContain('createDefaultAlertsConfigurationSnapshot');
expect(alertsConfigurationStateSource).toContain('readAlertsConfigurationSnapshot');
expect(alertsConfigurationStateSource).toContain('buildAlertsConfigurationPayload');
expect(alertsConfigurationStateSource).toContain('useAlertDestinationsState');
expect(alertsConfigurationStateSource).toContain('useAlertOverridesState');
expect(alertsConfigurationStateSource).not.toContain('NotificationsAPI.getEmailConfig');
expect(alertsConfigurationStateSource).not.toContain('NotificationsAPI.updateEmailConfig');
expect(alertsConfigurationStateSource).toContain("eventBus.on('org_switched'");
expect(alertsConfigurationStateSource).not.toContain('const createHysteresisThreshold =');
expect(alertsConfigurationStateSource).not.toContain('const normalizeGap =');
expect(alertsConfigurationStateSource).not.toContain(
'const [rawOverridesConfig, setRawOverridesConfig] = createSignal',
);
expect(alertsConfigurationStateSource).not.toContain('const hostOverrideIdCandidates =');
expect(alertsConfigurationStateSource).not.toContain('const allGuests = createMemo');
expect(alertsConfigurationModelSource).toContain(
'export function createDefaultAlertsConfigurationSnapshot',
);
expect(alertsConfigurationModelSource).toContain(
'export function readAlertsConfigurationSnapshot',
);
expect(alertsConfigurationModelSource).toContain(
'export function buildAlertsConfigurationPayload',
);
expect(alertsConfigurationModelSource).toContain('const createHysteresisThreshold =');
expect(alertsConfigurationModelSource).toContain('const normalizeGap =');
expect(alertOverridesStateSource).toContain('export function useAlertOverridesState');
expect(alertOverridesStateSource).toContain('getActionableAgentIdFromResource');
expect(alertOverridesStateSource).toContain('pbsInstanceFromResource');
@@ -326,6 +326,7 @@ import alertWebhookPresentationSource from '@/utils/alertWebhookPresentation.ts?
import alertOverviewTabSource from '@/features/alerts/OverviewTab.tsx?raw';
import alertsConfigurationSurfaceSource from '@/features/alerts/AlertsConfigurationSurface.tsx?raw';
import alertsConfigurationStateSource from '@/features/alerts/useAlertsConfigurationState.ts?raw';
import alertsConfigurationModelSource from '@/features/alerts/alertsConfigurationModel.ts?raw';
import alertOverridesStateSource from '@/features/alerts/useAlertOverridesState.ts?raw';
import alertDestinationsStateSource from '@/features/alerts/useAlertDestinationsState.ts?raw';
import alertDestinationsTabStateSource from '@/features/alerts/useAlertDestinationsTabState.ts?raw';
@@ -2774,16 +2775,32 @@ describe('frontend resource type boundaries', () => {
expect(alertsConfigurationSurfaceSource).toContain('useAlertsConfigurationState');
expect(alertsConfigurationSurfaceSource).not.toContain('AlertsAPI.getConfig');
expect(alertsConfigurationStateSource).toContain('AlertsAPI.getConfig');
expect(alertsConfigurationStateSource).toContain('createDefaultAlertsConfigurationSnapshot');
expect(alertsConfigurationStateSource).toContain('readAlertsConfigurationSnapshot');
expect(alertsConfigurationStateSource).toContain('buildAlertsConfigurationPayload');
expect(alertsConfigurationStateSource).toContain('useAlertDestinationsState');
expect(alertsConfigurationStateSource).toContain('useAlertOverridesState');
expect(alertsConfigurationStateSource).not.toContain('NotificationsAPI.getEmailConfig');
expect(alertsConfigurationStateSource).not.toContain('NotificationsAPI.updateEmailConfig');
expect(alertsConfigurationStateSource).toContain("eventBus.on('org_switched'");
expect(alertsConfigurationStateSource).not.toContain('const createHysteresisThreshold =');
expect(alertsConfigurationStateSource).not.toContain('const normalizeGap =');
expect(alertsConfigurationStateSource).not.toContain(
'const [rawOverridesConfig, setRawOverridesConfig] = createSignal',
);
expect(alertsConfigurationStateSource).not.toContain('const hostOverrideIdCandidates =');
expect(alertsConfigurationStateSource).not.toContain('const allGuests = createMemo');
expect(alertsConfigurationModelSource).toContain(
'export function createDefaultAlertsConfigurationSnapshot',
);
expect(alertsConfigurationModelSource).toContain(
'export function readAlertsConfigurationSnapshot',
);
expect(alertsConfigurationModelSource).toContain(
'export function buildAlertsConfigurationPayload',
);
expect(alertsConfigurationModelSource).toContain('const createHysteresisThreshold =');
expect(alertsConfigurationModelSource).toContain('const normalizeGap =');
expect(alertOverridesStateSource).toContain('export function useAlertOverridesState');
expect(alertOverridesStateSource).toContain('getActionableAgentIdFromResource');
expect(alertOverridesStateSource).toContain('pbsInstanceFromResource');
@@ -3858,6 +3875,7 @@ describe('frontend resource type boundaries', () => {
"import { ScheduleTab } from './tabs/ScheduleTab';",
);
expect(alertsConfigurationSurfaceSource).toContain('useAlertsConfigurationState');
expect(alertsConfigurationStateSource).toContain('buildAlertsConfigurationPayload');
expect(alertsPageSource).not.toContain('function DestinationsTab(');
expect(alertsPageSource).not.toContain('function ScheduleTab(');
expect(alertDestinationsTabSource).toContain('useAlertDestinationsTabState');