fix(alerts): single global defaults editor for recovery and snapshots

The Recovery and Snapshot Age threshold sections rendered two Global
Defaults surfaces at once, the live editor row plus a synthetic
read-only resource row of the same values. Both looked editable but
the synthetic row's pencil editor saved through findThresholdResource,
which never finds the pseudo-resource, so its edits were dropped
silently. Reported in #1680 by FabienD74. The synthetic rows are gone;
the always-live editor row is the single surface in both table and
card layouts. The dead editScope field and its unreachable saveEdit
branches go with them.

The size columns move from Recovery to Snapshot Age where they belong.
BackupAlertConfig has no size fields, so on Recovery they always
rendered an inert Off. SnapshotAlertConfig sizes are live in the alert
engine (backup_snapshot.go) but were previously invisible and, worse,
unpersistable. The records now carry the normalized warningSizeGiB and
criticalSizeGiB metric keys the column editor reads and writes, and
applyAlertsConfigToSnapshot plus buildAlertsConfigurationPayload carry
the size fields through load and save instead of stripping them.
Verified end to end against the mock dev stack, PUT then GET returns
the saved sizes and a reload shows them.

Guests in the VMs & Containers section now sort by display name with
vmid as tiebreaker. The old vmid-first order looked random because the
rows do not render the vmid, which is what the sorting complaint in
#1680 was about.

Dropping the phantom columns and the per-row bulk-edit checkbox also
brings the Recovery table under the desktop width budget, so it
renders as a table at 1280px and up instead of falling back to cards.
This commit is contained in:
rcourtman
2026-08-06 10:15:46 +01:00
parent 711eed6d69
commit 80d4558b42
16 changed files with 115 additions and 183 deletions
@@ -459,6 +459,24 @@ templates, threshold ignored-prefix input, and incident timeline notes must
compose `FormTextarea` for label/id/help wiring and textarea chrome instead of
recreating raw labelled `<textarea>` shells locally. Alert resource row/mobile
note editors now follow the same primitive contract.
The Recovery and Snapshot Age threshold sections expose exactly one Global
Defaults surface: the always-live editor row (table layout) or card (card
layout) that the shared resource table renders when global defaults are
supplied. They must not add a synthetic read-only resource row mirroring the
same record; a second surface invites edits that no override mutation path
persists, which is how #1680's silently-dropped edits happened. Snapshot size
thresholds belong to the Snapshot Age section, not Recovery, because
`BackupAlertConfig` has no size dimension. The recovery-defaults records carry
the normalized `warningSizeGiB` and `criticalSizeGiB` metric keys that the
column editor resolves for the size columns, and the configuration snapshot
layer must round-trip those fields: `applyAlertsConfigToSnapshot` and
`buildAlertsConfigurationPayload` carry them between the UI snapshot and
`/api/alerts/config` rather than stripping them. Guests in the VMs &
Containers section order by display name (numeric-aware, case-insensitive)
with vmid as tiebreaker, because the rows render the name and an invisible
sort key reads as an unsorted list. Regression ownership is
`frontend-modern/src/utils/__tests__/metricThresholds.test.ts` and
`frontend-modern/src/features/alerts/thresholds/hooks/__tests__/useThresholdsRecoveryDefaultsState.test.tsx`.
Guest metric canonical state remains resource-backed and therefore node-scoped
for Proxmox guests, so node moves must not strand active alert state on the
previous resource ID. When a guest metric alert survives a node move, alerts
@@ -795,6 +795,13 @@ Alert configuration tables follow the same primitive boundary: the alerts
owner supplies platform-specific threshold groups and filter catalog values,
while the shared FilterBar owns the chip, reset, and "+ Filter" interaction
shape so thresholds do not reintroduce page-local search/tab chrome.
The shared alert resource table's global-defaults row and card are the single
editing surface for a section's defaults. A section that supplies
`globalDefaults` must not also inject a synthetic resource row that mirrors
the same record; the table renders defaults once, in both desktop table and
narrow card layouts, and per-section metric columns must resolve to metric
keys the shared column normalizer produces so the defaults editor reads and
writes the same record keys the section persists.
Platform sub-routes that add native provider inventory must stay on the shared
platform page and table primitives. The vSphere Networks surface routes through
`/vmware/networks`, the shared platform tab model, the command palette
+35 -64
View File
@@ -1,86 +1,57 @@
{
"version": 1,
"base_sha": "ff97e3943c866ef062d743e6f1da136c13109a26",
"verified_at": "2026-08-06T09:05:33Z",
"base_sha": "711eed6d695e9008d7c633fbdf985a86d1e6aea9",
"verified_at": "2026-08-06T09:12:17Z",
"result": "passed",
"changed_paths": [
"frontend-modern/src/components/Workloads/guestRowModel.tsx",
"frontend-modern/src/features/docker/dockerContainerTableModel.ts",
"frontend-modern/src/features/proxmox/proxmoxHostTableModel.ts"
"frontend-modern/src/components/Alerts/ThresholdsTableProxmoxBackupsSection.tsx",
"frontend-modern/src/components/Alerts/ThresholdsTableProxmoxSnapshotsSection.tsx",
"frontend-modern/src/features/alerts/alertsConfigurationModel.ts",
"frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsGuestData.ts",
"frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsOverrideMutations.ts",
"frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsRecoveryDefaultsState.ts",
"frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsTableState.ts",
"frontend-modern/src/features/alerts/thresholds/tableTypes.ts",
"frontend-modern/src/utils/alertThresholdDefaults.ts"
],
"content_sha256": {
"frontend-modern/src/components/Workloads/guestRowModel.tsx": "dc5b5c79a0ddeb71e38bf2f01fa386f1850dffcf7315d9f697b681ad729b64ca",
"frontend-modern/src/features/docker/dockerContainerTableModel.ts": "85b9cac933ab3f5a7aa08f54d5fbcfa1b6e15b24b461eb8c3bea8efbc4e5364f",
"frontend-modern/src/features/proxmox/proxmoxHostTableModel.ts": "63078dbcaa4ac8b86c94612a3deeb116a72db90b0cdb01a9c997044be2e1750b"
"frontend-modern/src/components/Alerts/ThresholdsTableProxmoxBackupsSection.tsx": "a5dfeb2bfcc356e23b9ff164ab88782fca74593d8b3e45efea7261d02153a109",
"frontend-modern/src/components/Alerts/ThresholdsTableProxmoxSnapshotsSection.tsx": "222d2d08ad618fadbd34d5089287c3923a79b172c582203eac7469295b50a97e",
"frontend-modern/src/features/alerts/alertsConfigurationModel.ts": "4db657570ac209a00c16652a4f07b3be442a406d245766371c07ca2d3d32a1cf",
"frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsGuestData.ts": "d4584732f18e15316427e7b3a206978e2ca35d097028aee0e6ab2b11c34ee6f1",
"frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsOverrideMutations.ts": "c285c93a1e3a12f8481e80a8e6778334722055dcd5b885291fb33bed7d4270b0",
"frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsRecoveryDefaultsState.ts": "4a2deeb37740fe79b627df17abf25bba57f3048fc82a4bc34c7010b3c5058241",
"frontend-modern/src/features/alerts/thresholds/hooks/useThresholdsTableState.ts": "0178351e2848c42ad085fdbc24e9c84e5810a81d073805070e436d680277cc03",
"frontend-modern/src/features/alerts/thresholds/tableTypes.ts": "90c1910781527b16bbdd509b25a946202956a2616cceac287edb0cafca895c54",
"frontend-modern/src/utils/alertThresholdDefaults.ts": "f59d80d26290b0a4d021c4ca093f879659635048042bb374eb0173c0b8fa2691"
},
"routes": [
"/proxmox/overview",
"/proxmox/storage",
"/proxmox/replication",
"/proxmox/backups",
"/proxmox/ceph",
"/proxmox/mail",
"/docker/overview",
"/docker/images",
"/docker/storage",
"/docker/networks",
"/docker/swarm",
"/kubernetes/overview",
"/kubernetes/nodes",
"/kubernetes/workloads",
"/kubernetes/services",
"/kubernetes/storage",
"/kubernetes/configuration",
"/kubernetes/events",
"/truenas/overview",
"/truenas/storage",
"/truenas/services",
"/truenas/apps",
"/truenas/vms",
"/truenas/shares",
"/truenas/protection",
"/vmware/overview",
"/vmware/storage",
"/vmware/networks",
"/vmware/health",
"/vmware/activity",
"/standalone/machines",
"/standalone/availability"
"/alerts/thresholds/proxmox"
],
"viewports": [
{
"width": 1280,
"height": 820
"width": 1600,
"height": 900
},
{
"width": 768,
"height": 820
"width": 1000,
"height": 900
},
{
"width": 390,
"height": 820
},
{
"width": 320,
"height": 820
"width": 375,
"height": 812
}
],
"states": [
"populated mock inventory across every platform workflow",
"phone workload layout with identity, availability, CPU, and memory",
"similar Proxmox workload names with and without backup posture cues",
"small-tablet workload layout with disk restored",
"desktop workload and native platform tables with their full responsive column sets",
"deep platform navigation tabs selected at the end of horizontally scrollable tab strips"
"Recovery section expanded, desktop table layout at 1600px, single Global Defaults editor row",
"Recovery and Snapshot Age sections expanded in card layout at 1000px and 375px, single Global Defaults card each",
"Snapshot Age section with Warning/Critical Size (GiB) columns visible, factory Off state",
"VMs & Containers section expanded, guest cards sorted by display name within node group"
],
"interactions": [
"navigated all 32 platform workflow routes and waited for their loaded tables",
"scrolled populated Proxmox workload groups at 320 and 390 pixels and visually compared similarly named rows",
"changed from phone to 768 pixel tablet width and confirmed the disk column returned without table overflow",
"verified Proxmox, Docker, and VMware tables after the final layout-mode change at 320, 390, 768, and 1280 pixels",
"measured document, table, and scroll-container widths and found no page-level or table-level horizontal overflow",
"confirmed Proxmox, Docker, Kubernetes, TrueNAS, VMware, and Machines share identical desktop content bounds",
"opened deep Mail Gateway, Swarm, Events, Protection, Activity, and Availability tabs and confirmed the active tab was fully visible",
"checked the affected desktop journeys after the final code change and found no browser console errors"
"Enabled snapshot Warning Size via Off overlay, sanitizer mirrored value to Critical Size, Save Changes persisted warningSizeGiB and criticalSizeGiB via PUT /api/alerts/config and GET returned them",
"Reloaded page and confirmed persisted snapshot size values load back into the editor",
"Toggled snapshot size metrics back Off and saved, GET confirmed sizes cleared",
"Expanded and collapsed Recovery, Snapshot Age, and VMs & Containers sections at all three viewports"
]
}
@@ -30,37 +30,17 @@ export function ThresholdsTableProxmoxBackupsSection(props: ThresholdsTableSecti
<div ref={state.registerSection('backups')} class="scroll-mt-24">
<ResourceTable
title=""
resources={[
{
id: 'backups-defaults',
name: 'Global Defaults',
thresholds: state.backupDefaultsRecord(),
defaults: state.backupDefaultsRecord(),
editable: true,
editScope: 'backup',
},
]}
columns={[
'Fresh Hours',
'Stale Hours',
'Warning Days',
'Critical Days',
'Warning Size (GiB)',
'Critical Size (GiB)',
]}
columns={['Fresh Hours', 'Stale Hours', 'Warning Days', 'Critical Days']}
activeAlerts={tableProps.activeAlerts}
emptyMessage=""
onEdit={state.startEditing}
onSaveEdit={state.saveEdit}
onCancelEdit={state.cancelEdit}
onRemoveOverride={state.removeOverride}
showOfflineAlertsColumn={true}
editingId={state.editingId}
editingThresholds={state.editingThresholds}
setEditingThresholds={state.setEditingThresholds}
editingNote={state.editingNote}
setEditingNote={state.setEditingNote}
onBulkEdit={(ids) => state.handleBulkEdit(ids, ['Usage %'])}
formatMetricValue={formatMetricValue}
hasActiveAlert={state.hasActiveAlert}
globalDefaults={state.backupDefaultsRecord()}
@@ -23,30 +23,17 @@ export function ThresholdsTableProxmoxSnapshotsSection(props: ThresholdsTableSec
<div ref={state.registerSection('snapshots')} class="scroll-mt-24">
<ResourceTable
title=""
resources={[
{
id: 'snapshots-defaults',
name: 'Global Defaults',
thresholds: state.snapshotDefaultsRecord(),
defaults: state.snapshotDefaultsRecord(),
editable: true,
editScope: 'snapshot',
},
]}
columns={['Warning Days', 'Critical Days']}
columns={['Warning Days', 'Critical Days', 'Warning Size (GiB)', 'Critical Size (GiB)']}
activeAlerts={tableProps.activeAlerts}
emptyMessage=""
onEdit={state.startEditing}
onSaveEdit={state.saveEdit}
onCancelEdit={state.cancelEdit}
onRemoveOverride={state.removeOverride}
showOfflineAlertsColumn={true}
editingId={state.editingId}
editingThresholds={state.editingThresholds}
setEditingThresholds={state.setEditingThresholds}
editingNote={state.editingNote}
setEditingNote={state.setEditingNote}
onBulkEdit={(ids) => state.handleBulkEdit(ids, ['Usage %', 'Temperature °C'])}
formatMetricValue={formatMetricValue}
hasActiveAlert={state.hasActiveAlert}
globalDefaults={state.snapshotDefaultsRecord()}
@@ -55,8 +42,8 @@ export function ThresholdsTableProxmoxSnapshotsSection(props: ThresholdsTableSec
const currentRecord = {
'warning days': prev.warningDays ?? 0,
'critical days': prev.criticalDays ?? 0,
'warning size (gib)': prev.warningSizeGiB ?? 0,
'critical size (gib)': prev.criticalSizeGiB ?? 0,
warningSizeGiB: prev.warningSizeGiB ?? 0,
criticalSizeGiB: prev.criticalSizeGiB ?? 0,
};
const nextRecord =
typeof value === 'function'
@@ -73,12 +60,12 @@ export function ThresholdsTableProxmoxSnapshotsSection(props: ThresholdsTableSec
? nextRecord['critical days']
: prev.criticalDays,
warningSizeGiB:
typeof nextRecord['warning size (gib)'] === 'number'
? nextRecord['warning size (gib)']
typeof nextRecord.warningSizeGiB === 'number'
? nextRecord.warningSizeGiB
: prev.warningSizeGiB,
criticalSizeGiB:
typeof nextRecord['critical size (gib)'] === 'number'
? nextRecord['critical size (gib)']
typeof nextRecord.criticalSizeGiB === 'number'
? nextRecord.criticalSizeGiB
: prev.criticalSizeGiB,
};
});
@@ -549,10 +549,16 @@ export function readAlertsConfigurationSnapshot(config: AlertConfig): AlertsConf
config.snapshotDefaults.warningDays,
config.snapshotDefaults.criticalDays,
);
const normalizedSizePair = normalizeWarningCriticalPair(
config.snapshotDefaults.warningSizeGiB,
config.snapshotDefaults.criticalSizeGiB,
);
snapshot.snapshotDefaults = {
enabled: Boolean(config.snapshotDefaults.enabled),
warningDays: normalizedPair.warning,
criticalDays: normalizedPair.critical,
warningSizeGiB: normalizedSizePair.warning,
criticalSizeGiB: normalizedSizePair.critical,
};
}
@@ -695,6 +701,10 @@ export function buildAlertsConfigurationPayload({
snapshot.snapshotDefaults.warningDays,
snapshot.snapshotDefaults.criticalDays,
);
const normalizedSnapshotSizePair = normalizeWarningCriticalPair(
snapshot.snapshotDefaults.warningSizeGiB,
snapshot.snapshotDefaults.criticalSizeGiB,
);
const normalizedBackupPair = normalizeWarningCriticalPair(
snapshot.backupDefaults.warningDays,
snapshot.backupDefaults.criticalDays,
@@ -830,6 +840,8 @@ export function buildAlertsConfigurationPayload({
enabled: snapshot.snapshotDefaults.enabled,
warningDays: normalizedSnapshotPair.warning,
criticalDays: normalizedSnapshotPair.critical,
warningSizeGiB: normalizedSnapshotSizePair.warning,
criticalSizeGiB: normalizedSnapshotSizePair.critical,
},
backupDefaults: {
enabled: snapshot.backupDefaults.enabled,
@@ -156,8 +156,6 @@ describe('TrueNAS threshold persistence identity', () => {
editingNote,
bulkEditIds,
cancelEdit,
updateBackupDefaults: vi.fn(),
updateSnapshotDefaults: vi.fn(),
}),
);
@@ -289,8 +287,6 @@ describe('TrueNAS threshold persistence identity', () => {
editingNote,
bulkEditIds,
cancelEdit,
updateBackupDefaults: vi.fn(),
updateSnapshotDefaults: vi.fn(),
}),
);
@@ -86,8 +86,6 @@ describe('useThresholdsOverrideMutations', () => {
editingNote,
bulkEditIds,
cancelEdit,
updateBackupDefaults: vi.fn(),
updateSnapshotDefaults: vi.fn(),
}),
);
@@ -150,8 +148,6 @@ describe('useThresholdsOverrideMutations', () => {
editingNote: () => '',
bulkEditIds: () => [filesystemResource.id],
cancelEdit: vi.fn(),
updateBackupDefaults: vi.fn(),
updateSnapshotDefaults: vi.fn(),
}),
);
@@ -106,9 +106,9 @@ describe('useThresholdsRecoveryDefaultsState', () => {
expect(captured).toBeDefined();
expect(captured!.snapshotDefaultsRecord()).toEqual({
'critical days': 10,
'critical size (gib)': 18,
criticalSizeGiB: 18,
'warning days': 6,
'warning size (gib)': 12,
warningSizeGiB: 12,
});
expect(captured!.backupFactoryDefaultsRecord()).toEqual({
'critical days': 7,
@@ -87,10 +87,16 @@ export function useThresholdsGuestData(inputs: ThresholdsDataInputs) {
grouped[groupKey].push(guest);
});
// Sort by the name the row actually displays; vmid is not rendered here,
// so a vmid-first order reads as unsorted (#1680). vmid only breaks ties.
Object.keys(grouped).forEach((node) => {
grouped[node].sort((a, b) => {
if (a.vmid && b.vmid) return a.vmid - b.vmid;
return a.name.localeCompare(b.name);
const byName = a.name.localeCompare(b.name, undefined, {
numeric: true,
sensitivity: 'base',
});
if (byName !== 0) return byName;
return (a.vmid ?? 0) - (b.vmid ?? 0);
});
});
@@ -1,14 +1,6 @@
import type { Accessor } from 'solid-js';
import type { RawOverrideConfig, BackupAlertConfig, SnapshotAlertConfig } from '@/types/alerts';
import {
DEFAULT_SNAPSHOT_WARNING,
DEFAULT_SNAPSHOT_CRITICAL,
DEFAULT_SNAPSHOT_WARNING_SIZE,
DEFAULT_SNAPSHOT_CRITICAL_SIZE,
DEFAULT_BACKUP_WARNING,
DEFAULT_BACKUP_CRITICAL,
} from '@/features/alerts/thresholds/constants';
import type { RawOverrideConfig } from '@/types/alerts';
import type {
Override,
OverrideType,
@@ -60,12 +52,6 @@ interface ThresholdsOverrideMutationProps {
editingNote: Accessor<string>;
bulkEditIds: Accessor<string[]>;
cancelEdit: () => void;
updateBackupDefaults: (
updater: BackupAlertConfig | ((prev: BackupAlertConfig) => BackupAlertConfig),
) => void;
updateSnapshotDefaults: (
updater: SnapshotAlertConfig | ((prev: SnapshotAlertConfig) => SnapshotAlertConfig),
) => void;
}
export function useThresholdsOverrideMutations({
@@ -75,8 +61,6 @@ export function useThresholdsOverrideMutations({
editingNote,
bulkEditIds,
cancelEdit,
updateBackupDefaults,
updateSnapshotDefaults,
}: ThresholdsOverrideMutationProps) {
const optionalResources = (accessor?: Accessor<TableResource[]>): TableResource[] =>
accessor?.() ?? [];
@@ -126,48 +110,6 @@ export function useThresholdsOverrideMutations({
const trimmedNote = editingNote().trim();
const noteForOverride = trimmedNote.length > 0 ? trimmedNote : undefined;
if (resource.editScope === 'backup') {
const currentBackupDefaults = props.backupDefaults();
updateBackupDefaults({
criticalDays:
editedThresholdMap['critical days'] ??
currentBackupDefaults.criticalDays ??
DEFAULT_BACKUP_CRITICAL,
enabled: currentBackupDefaults.enabled,
warningDays:
editedThresholdMap['warning days'] ??
currentBackupDefaults.warningDays ??
DEFAULT_BACKUP_WARNING,
});
cancelEdit();
return;
}
if (resource.editScope === 'snapshot') {
const currentSnapshotDefaults = props.snapshotDefaults();
updateSnapshotDefaults({
criticalDays:
editedThresholdMap['critical days'] ??
currentSnapshotDefaults.criticalDays ??
DEFAULT_SNAPSHOT_CRITICAL,
criticalSizeGiB:
editedThresholdMap['critical size (gib)'] ??
currentSnapshotDefaults.criticalSizeGiB ??
DEFAULT_SNAPSHOT_CRITICAL_SIZE,
enabled: currentSnapshotDefaults.enabled,
warningDays:
editedThresholdMap['warning days'] ??
currentSnapshotDefaults.warningDays ??
DEFAULT_SNAPSHOT_WARNING,
warningSizeGiB:
editedThresholdMap['warning size (gib)'] ??
currentSnapshotDefaults.warningSizeGiB ??
DEFAULT_SNAPSHOT_WARNING_SIZE,
});
cancelEdit();
return;
}
const defaultThresholds = (resource.defaults ?? {}) as Record<string, number | undefined>;
const overrideThresholds: Record<string, number> = {};
@@ -106,13 +106,16 @@ export function useThresholdsRecoveryDefaultsState(props: ThresholdsTableProps)
};
};
// Size keys use the normalized metric key ('warningSizeGiB'), matching what
// normalizeAlertResourceMetricKey resolves for the 'Warning Size (GiB)'
// column — the global-defaults editor reads and writes through that key.
const snapshotDefaultsRecord = createMemo(() => {
const current = props.snapshotDefaults();
return {
'warning days': current.warningDays ?? 0,
'critical days': current.criticalDays ?? 0,
'warning size (gib)': current.warningSizeGiB ?? 0,
'critical size (gib)': current.criticalSizeGiB ?? 0,
warningSizeGiB: current.warningSizeGiB ?? 0,
criticalSizeGiB: current.criticalSizeGiB ?? 0,
};
});
@@ -121,8 +124,8 @@ export function useThresholdsRecoveryDefaultsState(props: ThresholdsTableProps)
return {
'warning days': factory.warningDays ?? DEFAULT_SNAPSHOT_WARNING,
'critical days': factory.criticalDays ?? DEFAULT_SNAPSHOT_CRITICAL,
'warning size (gib)': factory.warningSizeGiB ?? DEFAULT_SNAPSHOT_WARNING_SIZE,
'critical size (gib)': factory.criticalSizeGiB ?? DEFAULT_SNAPSHOT_CRITICAL_SIZE,
warningSizeGiB: factory.warningSizeGiB ?? DEFAULT_SNAPSHOT_WARNING_SIZE,
criticalSizeGiB: factory.criticalSizeGiB ?? DEFAULT_SNAPSHOT_CRITICAL_SIZE,
};
});
@@ -738,8 +738,6 @@ export function useThresholdsTableState(props: ThresholdsTableProps) {
editingNote,
bulkEditIds,
cancelEdit,
updateBackupDefaults,
updateSnapshotDefaults,
});
const { setOfflineState, toggleDisabled, toggleNodeConnectivity } =
@@ -34,7 +34,6 @@ export interface Resource {
clusterName?: string;
isClusterMember?: boolean;
delaySeconds?: number;
editScope?: 'snapshot' | 'backup';
isEnabled?: boolean;
toggleEnabled?: () => void;
toggleTitleEnabled?: string;
@@ -14,6 +14,7 @@ import {
} from '@/utils/metricThresholds';
import {
FACTORY_KUBERNETES_DEFAULTS,
FACTORY_SNAPSHOT_DEFAULTS,
FACTORY_TRUENAS_DEFAULTS,
FACTORY_TRUENAS_DISK_DEFAULTS,
FACTORY_VMWARE_DEFAULTS,
@@ -408,6 +409,19 @@ describe('metricThresholds', () => {
});
});
it('keeps snapshot factory defaults carrying size thresholds disabled at zero', () => {
// The size pair must exist on the factory config so the thresholds
// editor and the save payload round-trip them; 0 means size-based
// snapshot alerts are off until an operator enables them.
expect(FACTORY_SNAPSHOT_DEFAULTS).toEqual({
enabled: false,
warningDays: 30,
criticalDays: 45,
warningSizeGiB: 0,
criticalSizeGiB: 0,
});
});
it('keeps Kubernetes, TrueNAS, and vSphere factory defaults explicit for alert configuration', () => {
expect(FACTORY_KUBERNETES_DEFAULTS).toEqual({
cpu: 80,
@@ -95,6 +95,9 @@ export const FACTORY_SNAPSHOT_DEFAULTS: SnapshotAlertConfig = {
enabled: false,
warningDays: 30,
criticalDays: 45,
// 0 disables size-based snapshot alerts; the engine only fires on sizes > 0.
warningSizeGiB: 0,
criticalSizeGiB: 0,
};
export const FACTORY_BACKUP_DEFAULTS: BackupAlertConfig = {