diff --git a/docs/release-control/v6/internal/subsystems/alerts.md b/docs/release-control/v6/internal/subsystems/alerts.md index d441026bb5..0a133cf8fe 100644 --- a/docs/release-control/v6/internal/subsystems/alerts.md +++ b/docs/release-control/v6/internal/subsystems/alerts.md @@ -2901,6 +2901,30 @@ severity and the triggered-in-24h total; threshold override counts are configuration and do not appear on the overview. None of this changes alert lifecycle, acknowledgement, snooze, or delivery evaluation. +### Thresholds lead with each group's default limits + +The Thresholds page answers "what will alert me, and at what limits" before +any group is opened. Every collapsed threshold group whose table edits global +defaults states them in its header through +`getAlertThresholdsDefaultsSummary` in +`frontend-modern/src/utils/alertThresholdsSectionPresentation.ts`. The helper +reads the same column list and defaults object the group's resource table +edits (each section hoists its columns into one module constant), and drops +every metric `isAlertResourceMetricOff` reports, so the header can never show +a limit the engine is not evaluating. Rules whose zero means a fallback rather +than Off are stated as whole rules with their effective values: Docker's +restart loop (count within its window) and memory-limit warn/critical, which +`NormalizeDockerDefaults` always resolves to positive values. Backup age is +summarized by its warning and critical days only, because fresh and stale +hours colour the dashboard and never alert. A group switched off as a whole +shows its Disabled badge and no defaults line. The CPU averaging line counts +every configured profile override, including ones only the API sets. CPU averaging is tuning, not the page's +answer. It is one collapsed disclosure stating the effective window and the +number of platform overrides, with the same per-profile selects inside. Docker +and Machines list their resource groups before Docker-only rules and the +SMART card. Threshold values, inheritance, metric evaluation windows and +persistence are unchanged. + ### External watchdog is Pulse-availability evidence, not notification delivery The Alerts destinations surface owns the operator contract for an external diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index ef1326e310..a8113e0120 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -8357,3 +8357,11 @@ regressions preserve their original missing-value and fallback obligations. drawer with synthetic APIs in Chromium/WebKit desktop and 390/320-pixel light and dark views. This is presentation acceptance, not native backup/QGA safety, provider outage attribution, installed recovery or release availability. + +### Threshold group headers carry a defaults line + +`CollapsibleSection` takes an optional `defaultsSummary` rendered under the +title as a muted line (`data-thresholds-defaults-summary`) that wraps only +between items on phones and truncates from `sm` up, and is hidden while +`isGloballyDisabled` is set, where it would promise alerts that cannot fire. Callers pass the shared summary helper's output rather than +composing per-section strings. diff --git a/frontend-modern/browser-verification/368f58089b70d7bf.json b/frontend-modern/browser-verification/368f58089b70d7bf.json new file mode 100644 index 0000000000..064551caa0 --- /dev/null +++ b/frontend-modern/browser-verification/368f58089b70d7bf.json @@ -0,0 +1,81 @@ +{ + "version": 1, + "base_sha": "1d4c3aeb6e529af3b63992ef22152e6b8b8a33d8", + "verified_at": "2026-10-05T01:34:30Z", + "result": "passed", + "changed_paths": [ + "frontend-modern/src/components/Alerts/Thresholds/sections/CollapsibleSection.tsx", + "frontend-modern/src/components/Alerts/ThresholdsTableAgentDisksSection.tsx", + "frontend-modern/src/components/Alerts/ThresholdsTableAgentsResourcesSection.tsx", + "frontend-modern/src/components/Alerts/ThresholdsTableAgentsTab.tsx", + "frontend-modern/src/components/Alerts/ThresholdsTableDockerContainersSection.tsx", + "frontend-modern/src/components/Alerts/ThresholdsTableDockerTab.tsx", + "frontend-modern/src/components/Alerts/ThresholdsTableKubernetesTab.tsx", + "frontend-modern/src/components/Alerts/ThresholdsTableProxmoxBackupsSection.tsx", + "frontend-modern/src/components/Alerts/ThresholdsTableProxmoxGuestDisksSection.tsx", + "frontend-modern/src/components/Alerts/ThresholdsTableProxmoxGuestsSection.tsx", + "frontend-modern/src/components/Alerts/ThresholdsTableProxmoxNodesSection.tsx", + "frontend-modern/src/components/Alerts/ThresholdsTableProxmoxPBSSection.tsx", + "frontend-modern/src/components/Alerts/ThresholdsTableProxmoxPMGSection.tsx", + "frontend-modern/src/components/Alerts/ThresholdsTableProxmoxSnapshotsSection.tsx", + "frontend-modern/src/components/Alerts/ThresholdsTableProxmoxStorageSection.tsx", + "frontend-modern/src/components/Alerts/ThresholdsTableTrueNASTab.tsx", + "frontend-modern/src/components/Alerts/ThresholdsTableVMwareTab.tsx", + "frontend-modern/src/features/alerts/tabs/ThresholdsTab.tsx", + "frontend-modern/src/utils/alertThresholdsSectionPresentation.ts" + ], + "content_sha256": { + "frontend-modern/src/components/Alerts/Thresholds/sections/CollapsibleSection.tsx": "0a993dded673dbd2eb0b3919af2586a0fe4579ce10465e495710f526cf7ca1bd", + "frontend-modern/src/components/Alerts/ThresholdsTableAgentDisksSection.tsx": "d54ba2278c35ca5748cc0a310bb738dd9128454b1283d547bed7938568f92221", + "frontend-modern/src/components/Alerts/ThresholdsTableAgentsResourcesSection.tsx": "3b5cb92fb9b684d5655c6453b7e2738fb776d4c91bd566c319cc6d135301e095", + "frontend-modern/src/components/Alerts/ThresholdsTableAgentsTab.tsx": "463bba443bc9dfc52e9cfcb83505640d5f8e91f5c731a8ec8cab1542f43e81a4", + "frontend-modern/src/components/Alerts/ThresholdsTableDockerContainersSection.tsx": "3ee73f2b61ed5da9532dce15637e9803c4465f7a70241edf16e1692439c55fa5", + "frontend-modern/src/components/Alerts/ThresholdsTableDockerTab.tsx": "6226defd1e8adba0d28bae0998abd832993fc1e6b5b63f80024798c92684ecbd", + "frontend-modern/src/components/Alerts/ThresholdsTableKubernetesTab.tsx": "7e793356d6488169f8307ada64db35d9c15c9eacf1bddd30144e193f53345051", + "frontend-modern/src/components/Alerts/ThresholdsTableProxmoxBackupsSection.tsx": "8b89d2d4e3ffcbfe1dceb6d4a5d309c1d424d1d58059110e51b9e73a93fcd670", + "frontend-modern/src/components/Alerts/ThresholdsTableProxmoxGuestDisksSection.tsx": "7849b5ed2d8c2674915aa98bcbd4bf348881efc155f00dbcbe9febeabcc38458", + "frontend-modern/src/components/Alerts/ThresholdsTableProxmoxGuestsSection.tsx": "6f1694090e0022d1fff0653b06d1cf136f5fcebd59c88c41a4f703125d555ec2", + "frontend-modern/src/components/Alerts/ThresholdsTableProxmoxNodesSection.tsx": "c48d40917928e0ca8564d4eb90cb0e6987c8c9313494a444fa98209a3eee5dc1", + "frontend-modern/src/components/Alerts/ThresholdsTableProxmoxPBSSection.tsx": "d6801951e3d0ec6eaa7b267ff68e0f13ba3838be9c697b58822b94c57d719f06", + "frontend-modern/src/components/Alerts/ThresholdsTableProxmoxPMGSection.tsx": "2606881fdc7a68b747c34793330756b41513c89f07cbebcc5a3f69c6039d6610", + "frontend-modern/src/components/Alerts/ThresholdsTableProxmoxSnapshotsSection.tsx": "3a1bfafe2b44c724a5b351d37d721bf5992db31d66e761a0b782ad6f0a57cc7c", + "frontend-modern/src/components/Alerts/ThresholdsTableProxmoxStorageSection.tsx": "c4da879c91150d4c8d32c2068a4a0988ac3934f9d7297bff45d63b2ee1f4f6e2", + "frontend-modern/src/components/Alerts/ThresholdsTableTrueNASTab.tsx": "2d78009148e366e1546d52127ea1b0d0ee372037ca9b627a0ff3605854a1ca53", + "frontend-modern/src/components/Alerts/ThresholdsTableVMwareTab.tsx": "a628c3a17f16cb3ba74ca15bf53c0f8afa6d10390386b5b42f932c60105e167e", + "frontend-modern/src/features/alerts/tabs/ThresholdsTab.tsx": "ea46c8920f008f1904fe6e51dbfc4c5fa755e5af92341fe223fe75428efdf077", + "frontend-modern/src/utils/alertThresholdsSectionPresentation.ts": "75c2c8d6e0728b0ecec30b02c92185af63a32f03c43cede6a7068cba29943112" + }, + "routes": [ + "/alerts/thresholds/proxmox", + "/alerts/thresholds/docker", + "/alerts/thresholds/kubernetes", + "/alerts/thresholds/truenas", + "/alerts/thresholds/vmware", + "/alerts/thresholds/systems" + ], + "viewports": [ + { + "width": 1440, + "height": 900 + }, + { + "width": 390, + "height": 844 + } + ], + "states": [ + "Proxmox tab: every collapsed group states its default limits (Virtualization Hosts CPU 80% \u00b7 Memory 85% \u00b7 Disk 90% \u00b7 Temp 80\u00b0C, VMs & Containers, Guest Filesystems, Storage Devices, PBS Servers, Mail Gateway with +12 more)", + "Recovery and Snapshot Age groups disabled: Disabled badge and no defaults line", + "CPU averaging collapsed to one line: 5-minute average for every resource", + "Docker tab: Container Runtimes and Containers lead (Restart loop 3 in 5 min \u00b7 Memory limit 90% warn, 95% critical), Docker-only rules follow", + "Kubernetes, TrueNAS, vSphere tabs: every group with defaults states them", + "Machines tab: Machines and Machine Disks lead with defaults, SMART alert rules card follows", + "Phone 390: defaults lines and CPU averaging summary wrap between items without breaking a value", + "No console errors" + ], + "interactions": [ + "Opened CPU averaging disclosure: ten profile selects shown", + "Changed Virtual machines CPU averaging to 15-minute: summary updated to 1 platform override and the unsaved-changes banner appeared (not saved)", + "Expanded VMs & Containers to compare header defaults with the Global Defaults row" + ] +} diff --git a/frontend-modern/src/components/Alerts/Thresholds/sections/CollapsibleSection.tsx b/frontend-modern/src/components/Alerts/Thresholds/sections/CollapsibleSection.tsx index d57c44a562..3723f82490 100644 --- a/frontend-modern/src/components/Alerts/Thresholds/sections/CollapsibleSection.tsx +++ b/frontend-modern/src/components/Alerts/Thresholds/sections/CollapsibleSection.tsx @@ -5,7 +5,7 @@ * Used to organize resource groups in the thresholds page. */ -import { Component, Show, createSignal, createEffect, JSX } from 'solid-js'; +import { Component, For, Show, createSignal, createEffect, JSX } from 'solid-js'; import ChevronRight from 'lucide-solid/icons/chevron-right'; import ChevronDown from 'lucide-solid/icons/chevron-down'; import { StatusDot } from '@/components/shared/StatusDot'; @@ -34,6 +34,12 @@ export interface CollapsibleSectionProps { icon?: JSX.Element; /** Subtitle or description */ subtitle?: string; + /** + * What the group alerts on by default, shown under the title so the page + * answers "what will alert me" before the group is opened. Hidden while the + * group is disabled, where it would promise alerts that cannot fire. + */ + defaultsSummary?: string; /** Message to show when section is empty */ emptyMessage?: string; /** Whether to show a visual indicator for global disable state */ @@ -133,6 +139,23 @@ export const CollapsibleSection: Component = (props) => {props.subtitle} + + + {/* Break between items only, never inside "Disk 90%". */} + + {(item, index) => ( + <> + {index() > 0 ? ' · ' : ''} + {item} + + )} + + + diff --git a/frontend-modern/src/components/Alerts/Thresholds/sections/__tests__/CollapsibleSection.test.tsx b/frontend-modern/src/components/Alerts/Thresholds/sections/__tests__/CollapsibleSection.test.tsx index 40f9cab7fa..710f053f07 100644 --- a/frontend-modern/src/components/Alerts/Thresholds/sections/__tests__/CollapsibleSection.test.tsx +++ b/frontend-modern/src/components/Alerts/Thresholds/sections/__tests__/CollapsibleSection.test.tsx @@ -26,6 +26,29 @@ describe('CollapsibleSection', () => { expect(screen.getByText('Child content')).toBeInTheDocument(); }); + it('states the default limits in the collapsed header, except while disabled', () => { + const [disabled, setDisabled] = createSignal(false); + const { container } = render(() => ( + +

Rows

+
+ )); + + const summary = () => container.querySelector('[data-thresholds-defaults-summary]'); + expect(summary()?.textContent).toBe('Defaults: CPU 80% · Memory 85% · Disk 90%'); + expect(summary()?.closest('button')?.getAttribute('aria-expanded')).toBe('false'); + + setDisabled(true); + expect(summary()).toBeNull(); + expect(screen.getByText(ALERT_THRESHOLDS_SECTION_DISABLED_LABEL)).toBeInTheDocument(); + }); + it('uses default data-testid from id prop', () => { const { container } = render(() => ( diff --git a/frontend-modern/src/components/Alerts/ThresholdsTableAgentDisksSection.tsx b/frontend-modern/src/components/Alerts/ThresholdsTableAgentDisksSection.tsx index 80eef07708..f737de1003 100644 --- a/frontend-modern/src/components/Alerts/ThresholdsTableAgentDisksSection.tsx +++ b/frontend-modern/src/components/Alerts/ThresholdsTableAgentDisksSection.tsx @@ -5,6 +5,9 @@ import { ResourceTable } from './ResourceTable'; import { CollapsibleSection } from './Thresholds/sections/CollapsibleSection'; import { formatMetricValue } from '@/features/alerts/thresholds/helpers'; import type { ThresholdsTableSectionProps } from '@/features/alerts/thresholds/thresholdsTableSectionProps'; +import { getAlertThresholdsDefaultsSummary } from '@/utils/alertThresholdsSectionPresentation'; + +const AGENT_DISK_THRESHOLD_COLUMNS = ['Disk %']; export function ThresholdsTableAgentDisksSection(props: ThresholdsTableSectionProps) { const { state, tableProps } = props; @@ -14,6 +17,9 @@ export function ThresholdsTableAgentDisksSection(props: ThresholdsTableSectionPr state.toggleSection('agentDisks')} @@ -27,7 +33,7 @@ export function ThresholdsTableAgentDisksSection(props: ThresholdsTableSectionPr onConfigureResourceIntent={tableProps.onConfigureResourceIntent} groupedResources={state.agentDisksGroupedByAgent()} groupHeaderMeta={state.agentGroupHeaderMeta()} - columns={['Disk %']} + columns={AGENT_DISK_THRESHOLD_COLUMNS} activeAlerts={tableProps.activeAlerts} emptyMessage={state.AGENT_DISKS_FILTER_EMPTY_STATE} onEdit={state.startEditing} diff --git a/frontend-modern/src/components/Alerts/ThresholdsTableAgentsResourcesSection.tsx b/frontend-modern/src/components/Alerts/ThresholdsTableAgentsResourcesSection.tsx index b4d77dbf87..5429e1e806 100644 --- a/frontend-modern/src/components/Alerts/ThresholdsTableAgentsResourcesSection.tsx +++ b/frontend-modern/src/components/Alerts/ThresholdsTableAgentsResourcesSection.tsx @@ -6,6 +6,9 @@ import { ResourceTable } from './ResourceTable'; import { CollapsibleSection } from './Thresholds/sections/CollapsibleSection'; import { formatMetricValue } from '@/features/alerts/thresholds/helpers'; import type { ThresholdsTableSectionProps } from '@/features/alerts/thresholds/thresholdsTableSectionProps'; +import { getAlertThresholdsDefaultsSummary } from '@/utils/alertThresholdsSectionPresentation'; + +const AGENT_THRESHOLD_COLUMNS = ['CPU %', 'Memory %', 'Disk %', 'Disk Temp °C']; const DISK_TEMP_TYPE_FIELDS: readonly { key: string; label: string }[] = [ { key: 'nvme', label: 'NVMe' }, @@ -21,6 +24,10 @@ export function ThresholdsTableAgentsResourcesSection(props: ThresholdsTableSect state.toggleSection('agents')} @@ -33,7 +40,7 @@ export function ThresholdsTableAgentsResourcesSection(props: ThresholdsTableSect title="" onConfigureResourceIntent={tableProps.onConfigureResourceIntent} resources={state.agentsWithOverrides()} - columns={['CPU %', 'Memory %', 'Disk %', 'Disk Temp °C']} + columns={AGENT_THRESHOLD_COLUMNS} activeAlerts={tableProps.activeAlerts} emptyMessage={state.AGENT_THRESHOLDS_FILTER_EMPTY_STATE} onEdit={state.startEditing} diff --git a/frontend-modern/src/components/Alerts/ThresholdsTableAgentsTab.tsx b/frontend-modern/src/components/Alerts/ThresholdsTableAgentsTab.tsx index 55eb3c0167..b860b18953 100644 --- a/frontend-modern/src/components/Alerts/ThresholdsTableAgentsTab.tsx +++ b/frontend-modern/src/components/Alerts/ThresholdsTableAgentsTab.tsx @@ -6,9 +6,10 @@ import type { ThresholdsTableSectionProps } from '@/features/alerts/thresholds/t export function ThresholdsTableAgentsTab(props: ThresholdsTableSectionProps) { return ( <> - + {/* The groups and their default limits lead; disk health rules follow. */} + ); } diff --git a/frontend-modern/src/components/Alerts/ThresholdsTableDockerContainersSection.tsx b/frontend-modern/src/components/Alerts/ThresholdsTableDockerContainersSection.tsx index 5078cf0441..a96305a6d5 100644 --- a/frontend-modern/src/components/Alerts/ThresholdsTableDockerContainersSection.tsx +++ b/frontend-modern/src/components/Alerts/ThresholdsTableDockerContainersSection.tsx @@ -5,15 +5,46 @@ import { ResourceTable } from './ResourceTable'; import { CollapsibleSection } from './Thresholds/sections/CollapsibleSection'; import { formatMetricValue } from '@/features/alerts/thresholds/helpers'; import type { ThresholdsTableSectionProps } from '@/features/alerts/thresholds/thresholdsTableSectionProps'; +import { + getAlertThresholdsDefaultsSummary, + getDockerContainerRuleSummaryItems, +} from '@/utils/alertThresholdsSectionPresentation'; + +const DOCKER_CONTAINER_THRESHOLD_COLUMNS = [ + 'CPU %', + 'Memory %', + 'Disk %', + 'Restart Count', + 'Restart Window (s)', + 'Memory Warn %', + 'Memory Critical %', +]; + +// Restart and memory-limit columns are summarized as whole rules instead. +const DOCKER_CONTAINER_METRIC_SUMMARY_COLUMNS = ['CPU %', 'Memory %', 'Disk %']; export function ThresholdsTableDockerContainersSection(props: ThresholdsTableSectionProps) { const { state, tableProps } = props; + const containerDefaults = () => ({ + cpu: tableProps.dockerDefaults.cpu, + memory: tableProps.dockerDefaults.memory, + disk: tableProps.dockerDefaults.disk, + restartCount: tableProps.dockerDefaults.restartCount, + restartWindow: tableProps.dockerDefaults.restartWindow, + memoryWarnPct: tableProps.dockerDefaults.memoryWarnPct, + memoryCriticalPct: tableProps.dockerDefaults.memoryCriticalPct, + }); return ( state.toggleSection('dockerContainers')} @@ -27,15 +58,7 @@ export function ThresholdsTableDockerContainersSection(props: ThresholdsTableSec onConfigureResourceIntent={tableProps.onConfigureResourceIntent} groupedResources={state.dockerContainersGroupedByHost()} groupHeaderMeta={state.dockerHostGroupMeta()} - columns={[ - 'CPU %', - 'Memory %', - 'Disk %', - 'Restart Count', - 'Restart Window (s)', - 'Memory Warn %', - 'Memory Critical %', - ]} + columns={DOCKER_CONTAINER_THRESHOLD_COLUMNS} activeAlerts={tableProps.activeAlerts} emptyMessage={state.CONTAINERS_FILTER_EMPTY_STATE} onEdit={state.startEditing} @@ -54,15 +77,7 @@ export function ThresholdsTableDockerContainersSection(props: ThresholdsTableSec } formatMetricValue={formatMetricValue} hasActiveAlert={state.hasActiveAlert} - globalDefaults={{ - cpu: tableProps.dockerDefaults.cpu, - memory: tableProps.dockerDefaults.memory, - disk: tableProps.dockerDefaults.disk, - restartCount: tableProps.dockerDefaults.restartCount, - restartWindow: tableProps.dockerDefaults.restartWindow, - memoryWarnPct: tableProps.dockerDefaults.memoryWarnPct, - memoryCriticalPct: tableProps.dockerDefaults.memoryCriticalPct, - }} + globalDefaults={containerDefaults()} setGlobalDefaults={(value) => { const current = { cpu: tableProps.dockerDefaults.cpu, diff --git a/frontend-modern/src/components/Alerts/ThresholdsTableDockerTab.tsx b/frontend-modern/src/components/Alerts/ThresholdsTableDockerTab.tsx index 9bd6aad526..60ba345a55 100644 --- a/frontend-modern/src/components/Alerts/ThresholdsTableDockerTab.tsx +++ b/frontend-modern/src/components/Alerts/ThresholdsTableDockerTab.tsx @@ -10,13 +10,14 @@ import type { ThresholdsTableSectionProps } from '@/features/alerts/thresholds/t export function ThresholdsTableDockerTab(props: ThresholdsTableSectionProps) { return ( <> + {/* The groups and their default limits lead; Docker-only rules follow. */} + + - - ); } diff --git a/frontend-modern/src/components/Alerts/ThresholdsTableKubernetesTab.tsx b/frontend-modern/src/components/Alerts/ThresholdsTableKubernetesTab.tsx index 34876a7331..d2c2310b61 100644 --- a/frontend-modern/src/components/Alerts/ThresholdsTableKubernetesTab.tsx +++ b/frontend-modern/src/components/Alerts/ThresholdsTableKubernetesTab.tsx @@ -10,6 +10,7 @@ import { CollapsibleSection } from './Thresholds/sections/CollapsibleSection'; import { formatMetricValue } from '@/features/alerts/thresholds/helpers'; import type { ThresholdsTableSectionProps } from '@/features/alerts/thresholds/thresholdsTableSectionProps'; import type { Resource } from '@/features/alerts/thresholds/tableTypes'; +import { getAlertThresholdsDefaultsSummary } from '@/utils/alertThresholdsSectionPresentation'; const KUBERNETES_WORKLOAD_COLUMNS = [ 'CPU %', @@ -40,6 +41,7 @@ function KubernetesResourceSection( state.toggleSection(props.id)} diff --git a/frontend-modern/src/components/Alerts/ThresholdsTableProxmoxBackupsSection.tsx b/frontend-modern/src/components/Alerts/ThresholdsTableProxmoxBackupsSection.tsx index e5461c875f..c4a547d7b3 100644 --- a/frontend-modern/src/components/Alerts/ThresholdsTableProxmoxBackupsSection.tsx +++ b/frontend-modern/src/components/Alerts/ThresholdsTableProxmoxBackupsSection.tsx @@ -15,6 +15,11 @@ import { reconcileWarningCriticalEdit, } from '@/features/alerts/thresholds/helpers'; import type { ThresholdsTableSectionProps } from '@/features/alerts/thresholds/thresholdsTableSectionProps'; +import { getAlertThresholdsDefaultsSummary } from '@/utils/alertThresholdsSectionPresentation'; + +const BACKUP_THRESHOLD_COLUMNS = ['Fresh Hours', 'Stale Hours', 'Warning Days', 'Critical Days']; +// Fresh and stale hours only colour the dashboard; the alert fires on days. +const BACKUP_ALERT_SUMMARY_COLUMNS = ['Warning Days', 'Critical Days']; export function ThresholdsTableProxmoxBackupsSection(props: ThresholdsTableSectionProps) { const { state, tableProps } = props; @@ -24,6 +29,10 @@ export function ThresholdsTableProxmoxBackupsSection(props: ThresholdsTableSecti state.toggleSection('backups')} icon={} @@ -33,7 +42,7 @@ export function ThresholdsTableProxmoxBackupsSection(props: ThresholdsTableSecti
state.toggleSection('guestDisks')} @@ -26,7 +32,7 @@ export function ThresholdsTableProxmoxGuestDisksSection(props: ThresholdsTableSe title="" onConfigureResourceIntent={tableProps.onConfigureResourceIntent} groupedResources={state.guestDisksGroupedByGuest()} - columns={['Disk %']} + columns={GUEST_DISK_THRESHOLD_COLUMNS} activeAlerts={tableProps.activeAlerts} emptyMessage={state.GUEST_DISKS_FILTER_EMPTY_STATE} onEdit={state.startEditing} diff --git a/frontend-modern/src/components/Alerts/ThresholdsTableProxmoxGuestsSection.tsx b/frontend-modern/src/components/Alerts/ThresholdsTableProxmoxGuestsSection.tsx index 71c161d74e..8e2cd38eea 100644 --- a/frontend-modern/src/components/Alerts/ThresholdsTableProxmoxGuestsSection.tsx +++ b/frontend-modern/src/components/Alerts/ThresholdsTableProxmoxGuestsSection.tsx @@ -5,6 +5,19 @@ import { ResourceTable } from './ResourceTable'; import { CollapsibleSection } from './Thresholds/sections/CollapsibleSection'; import { formatMetricValue } from '@/features/alerts/thresholds/helpers'; import type { ThresholdsTableSectionProps } from '@/features/alerts/thresholds/thresholdsTableSectionProps'; +import { getAlertThresholdsDefaultsSummary } from '@/utils/alertThresholdsSectionPresentation'; + +const GUEST_THRESHOLD_COLUMNS = [ + 'CPU %', + 'Memory %', + 'Disk %', + 'Backup', + 'Snapshot', + 'Disk R MB/s', + 'Disk W MB/s', + 'Net In MB/s', + 'Net Out MB/s', +]; export function ThresholdsTableProxmoxGuestsSection(props: ThresholdsTableSectionProps) { const { state, tableProps } = props; @@ -14,6 +27,10 @@ export function ThresholdsTableProxmoxGuestsSection(props: ThresholdsTableSectio state.toggleSection('guests')} @@ -27,17 +44,7 @@ export function ThresholdsTableProxmoxGuestsSection(props: ThresholdsTableSectio onConfigureResourceIntent={tableProps.onConfigureResourceIntent} groupedResources={state.guestsGroupedByNode()} groupHeaderMeta={state.guestGroupHeaderMeta()} - columns={[ - 'CPU %', - 'Memory %', - 'Disk %', - 'Backup', - 'Snapshot', - 'Disk R MB/s', - 'Disk W MB/s', - 'Net In MB/s', - 'Net Out MB/s', - ]} + columns={GUEST_THRESHOLD_COLUMNS} activeAlerts={tableProps.activeAlerts} emptyMessage={state.GUEST_THRESHOLDS_FILTER_EMPTY_STATE} onEdit={state.startEditing} diff --git a/frontend-modern/src/components/Alerts/ThresholdsTableProxmoxNodesSection.tsx b/frontend-modern/src/components/Alerts/ThresholdsTableProxmoxNodesSection.tsx index e73984b29f..d56496e4a8 100644 --- a/frontend-modern/src/components/Alerts/ThresholdsTableProxmoxNodesSection.tsx +++ b/frontend-modern/src/components/Alerts/ThresholdsTableProxmoxNodesSection.tsx @@ -5,6 +5,9 @@ import { ResourceTable } from './ResourceTable'; import { CollapsibleSection } from './Thresholds/sections/CollapsibleSection'; import { formatMetricValue } from '@/features/alerts/thresholds/helpers'; import type { ThresholdsTableSectionProps } from '@/features/alerts/thresholds/thresholdsTableSectionProps'; +import { getAlertThresholdsDefaultsSummary } from '@/utils/alertThresholdsSectionPresentation'; + +const NODE_THRESHOLD_COLUMNS = ['CPU %', 'Memory %', 'Disk %', 'Temp °C']; export function ThresholdsTableProxmoxNodesSection(props: ThresholdsTableSectionProps) { const { state, tableProps } = props; @@ -14,6 +17,10 @@ export function ThresholdsTableProxmoxNodesSection(props: ThresholdsTableSection state.toggleSection('nodes')} @@ -26,7 +33,7 @@ export function ThresholdsTableProxmoxNodesSection(props: ThresholdsTableSection title="" onConfigureResourceIntent={tableProps.onConfigureResourceIntent} resources={state.nodesWithOverrides()} - columns={['CPU %', 'Memory %', 'Disk %', 'Temp °C']} + columns={NODE_THRESHOLD_COLUMNS} activeAlerts={tableProps.activeAlerts} emptyMessage={state.NODE_THRESHOLDS_FILTER_EMPTY_STATE} onEdit={state.startEditing} diff --git a/frontend-modern/src/components/Alerts/ThresholdsTableProxmoxPBSSection.tsx b/frontend-modern/src/components/Alerts/ThresholdsTableProxmoxPBSSection.tsx index 3a3f5d5bb2..927a484a28 100644 --- a/frontend-modern/src/components/Alerts/ThresholdsTableProxmoxPBSSection.tsx +++ b/frontend-modern/src/components/Alerts/ThresholdsTableProxmoxPBSSection.tsx @@ -5,6 +5,9 @@ import { ResourceTable } from './ResourceTable'; import { CollapsibleSection } from './Thresholds/sections/CollapsibleSection'; import { formatMetricValue } from '@/features/alerts/thresholds/helpers'; import type { ThresholdsTableSectionProps } from '@/features/alerts/thresholds/thresholdsTableSectionProps'; +import { getAlertThresholdsDefaultsSummary } from '@/utils/alertThresholdsSectionPresentation'; + +const PBS_THRESHOLD_COLUMNS = ['CPU %', 'Memory %']; export function ThresholdsTableProxmoxPBSSection(props: ThresholdsTableSectionProps) { const { state, tableProps } = props; @@ -14,6 +17,10 @@ export function ThresholdsTableProxmoxPBSSection(props: ThresholdsTableSectionPr state.toggleSection('pbs')} @@ -26,7 +33,7 @@ export function ThresholdsTableProxmoxPBSSection(props: ThresholdsTableSectionPr title="" onConfigureResourceIntent={tableProps.onConfigureResourceIntent} resources={state.pbsServersWithOverrides()} - columns={['CPU %', 'Memory %']} + columns={PBS_THRESHOLD_COLUMNS} activeAlerts={tableProps.activeAlerts} emptyMessage={state.PBS_THRESHOLDS_FILTER_EMPTY_STATE} onEdit={state.startEditing} diff --git a/frontend-modern/src/components/Alerts/ThresholdsTableProxmoxPMGSection.tsx b/frontend-modern/src/components/Alerts/ThresholdsTableProxmoxPMGSection.tsx index 6bda08afce..a658b8d4ab 100644 --- a/frontend-modern/src/components/Alerts/ThresholdsTableProxmoxPMGSection.tsx +++ b/frontend-modern/src/components/Alerts/ThresholdsTableProxmoxPMGSection.tsx @@ -5,6 +5,26 @@ import { ResourceTable } from './ResourceTable'; import { CollapsibleSection } from './Thresholds/sections/CollapsibleSection'; import { formatMetricValue } from '@/features/alerts/thresholds/helpers'; import type { ThresholdsTableSectionProps } from '@/features/alerts/thresholds/thresholdsTableSectionProps'; +import { getAlertThresholdsDefaultsSummary } from '@/utils/alertThresholdsSectionPresentation'; + +const PMG_THRESHOLD_TABLE_COLUMNS = [ + 'Queue Warn', + 'Queue Crit', + 'Deferred Warn', + 'Deferred Crit', + 'Hold Warn', + 'Hold Crit', + 'Oldest Warn (min)', + 'Oldest Crit (min)', + 'Spam Warn', + 'Spam Crit', + 'Virus Warn', + 'Virus Crit', + 'Growth Warn %', + 'Growth Warn Min', + 'Growth Crit %', + 'Growth Crit Min', +]; export function ThresholdsTableProxmoxPMGSection(props: ThresholdsTableSectionProps) { const { state, tableProps } = props; @@ -14,6 +34,10 @@ export function ThresholdsTableProxmoxPMGSection(props: ThresholdsTableSectionPr state.toggleSection('pmg')} @@ -26,24 +50,7 @@ export function ThresholdsTableProxmoxPMGSection(props: ThresholdsTableSectionPr title="" onConfigureResourceIntent={tableProps.onConfigureResourceIntent} resources={state.pmgServersWithOverrides()} - columns={[ - 'Queue Warn', - 'Queue Crit', - 'Deferred Warn', - 'Deferred Crit', - 'Hold Warn', - 'Hold Crit', - 'Oldest Warn (min)', - 'Oldest Crit (min)', - 'Spam Warn', - 'Spam Crit', - 'Virus Warn', - 'Virus Crit', - 'Growth Warn %', - 'Growth Warn Min', - 'Growth Crit %', - 'Growth Crit Min', - ]} + columns={PMG_THRESHOLD_TABLE_COLUMNS} activeAlerts={tableProps.activeAlerts} emptyMessage={state.PMG_THRESHOLDS_FILTER_EMPTY_STATE} onEdit={state.startEditing} diff --git a/frontend-modern/src/components/Alerts/ThresholdsTableProxmoxSnapshotsSection.tsx b/frontend-modern/src/components/Alerts/ThresholdsTableProxmoxSnapshotsSection.tsx index 7337186da4..ce49f035c3 100644 --- a/frontend-modern/src/components/Alerts/ThresholdsTableProxmoxSnapshotsSection.tsx +++ b/frontend-modern/src/components/Alerts/ThresholdsTableProxmoxSnapshotsSection.tsx @@ -8,6 +8,14 @@ import { reconcileWarningCriticalEdit, } from '@/features/alerts/thresholds/helpers'; import type { ThresholdsTableSectionProps } from '@/features/alerts/thresholds/thresholdsTableSectionProps'; +import { getAlertThresholdsDefaultsSummary } from '@/utils/alertThresholdsSectionPresentation'; + +const SNAPSHOT_THRESHOLD_COLUMNS = [ + 'Warning Days', + 'Critical Days', + 'Warning Size (GiB)', + 'Critical Size (GiB)', +]; export function ThresholdsTableProxmoxSnapshotsSection(props: ThresholdsTableSectionProps) { const { state, tableProps } = props; @@ -17,6 +25,10 @@ export function ThresholdsTableProxmoxSnapshotsSection(props: ThresholdsTableSec state.toggleSection('snapshots')} icon={} @@ -26,7 +38,7 @@ export function ThresholdsTableProxmoxSnapshotsSection(props: ThresholdsTableSec
state.toggleSection('storage')} @@ -27,7 +33,7 @@ export function ThresholdsTableProxmoxStorageSection(props: ThresholdsTableSecti onConfigureResourceIntent={tableProps.onConfigureResourceIntent} groupedResources={state.storageGroupedByNode()} groupHeaderMeta={state.guestGroupHeaderMeta()} - columns={['Usage %']} + columns={STORAGE_THRESHOLD_COLUMNS} activeAlerts={tableProps.activeAlerts} emptyMessage={state.STORAGE_THRESHOLDS_FILTER_EMPTY_STATE} onEdit={state.startEditing} diff --git a/frontend-modern/src/components/Alerts/ThresholdsTableTrueNASTab.tsx b/frontend-modern/src/components/Alerts/ThresholdsTableTrueNASTab.tsx index 72a5e48ea4..5b70598a3f 100644 --- a/frontend-modern/src/components/Alerts/ThresholdsTableTrueNASTab.tsx +++ b/frontend-modern/src/components/Alerts/ThresholdsTableTrueNASTab.tsx @@ -8,6 +8,7 @@ import { CollapsibleSection } from './Thresholds/sections/CollapsibleSection'; import { formatMetricValue } from '@/features/alerts/thresholds/helpers'; import type { ThresholdsTableSectionProps } from '@/features/alerts/thresholds/thresholdsTableSectionProps'; import type { Resource } from '@/features/alerts/thresholds/tableTypes'; +import { getAlertThresholdsDefaultsSummary } from '@/utils/alertThresholdsSectionPresentation'; const TRUENAS_SYSTEM_COLUMNS = [ 'CPU %', @@ -47,6 +48,7 @@ function TrueNASResourceSection( state.toggleSection(props.id)} diff --git a/frontend-modern/src/components/Alerts/ThresholdsTableVMwareTab.tsx b/frontend-modern/src/components/Alerts/ThresholdsTableVMwareTab.tsx index 46968dfd50..f65d1fa72c 100644 --- a/frontend-modern/src/components/Alerts/ThresholdsTableVMwareTab.tsx +++ b/frontend-modern/src/components/Alerts/ThresholdsTableVMwareTab.tsx @@ -9,6 +9,7 @@ import { CollapsibleSection } from './Thresholds/sections/CollapsibleSection'; import { formatMetricValue } from '@/features/alerts/thresholds/helpers'; import type { ThresholdsTableSectionProps } from '@/features/alerts/thresholds/thresholdsTableSectionProps'; import type { Resource } from '@/features/alerts/thresholds/tableTypes'; +import { getAlertThresholdsDefaultsSummary } from '@/utils/alertThresholdsSectionPresentation'; const VMWARE_HOST_COLUMNS = [ 'CPU %', @@ -54,6 +55,7 @@ function VMwareResourceSection( state.toggleSection(props.id)} diff --git a/frontend-modern/src/features/alerts/__tests__/ThresholdsTab.test.tsx b/frontend-modern/src/features/alerts/__tests__/ThresholdsTab.test.tsx index 05a3847c84..f73939890e 100644 --- a/frontend-modern/src/features/alerts/__tests__/ThresholdsTab.test.tsx +++ b/frontend-modern/src/features/alerts/__tests__/ThresholdsTab.test.tsx @@ -342,6 +342,18 @@ describe('ThresholdsTab', () => { }); }); + it('counts API-only CPU averaging overrides in the collapsed summary', () => { + const props = buildProps(); + props.metricEvaluationWindows = () => ({ all: { cpu: 300 }, pod: { cpu: 60 } }); + props.setMetricEvaluationWindows = vi.fn(); + + render(() => ); + + expect(document.querySelector('[data-cpu-averaging-summary]')?.textContent).toBe( + '5-minute average, 1 platform override', + ); + }); + it('configures the canonical workload fallback and shows effective inheritance', () => { const props = buildProps(); const setMetricEvaluationWindows = vi.fn(); @@ -356,12 +368,16 @@ describe('ThresholdsTab', () => { render(() => ); const workloadWindow = screen.getAllByLabelText( - 'All workloads CPU evaluation window', + 'All workloads CPU averaging', )[0] as HTMLSelectElement; const virtualMachineWindow = screen.getAllByLabelText( - 'Virtual machines CPU evaluation window', + 'Virtual machines CPU averaging', )[0] as HTMLSelectElement; expect(workloadWindow.value).toBe('900'); + expect(document.querySelector('[data-cpu-averaging]')?.hasAttribute('open')).toBe(false); + expect(document.querySelector('[data-cpu-averaging-summary]')?.textContent).toBe( + '5-minute average, 1 platform override', + ); expect(virtualMachineWindow.options[0].textContent).toBe('Inherit (15 minutes)'); fireEvent.input(workloadWindow, { target: { value: '60' } }); diff --git a/frontend-modern/src/features/alerts/tabs/ThresholdsTab.tsx b/frontend-modern/src/features/alerts/tabs/ThresholdsTab.tsx index f04436c909..8f7b81bdb9 100644 --- a/frontend-modern/src/features/alerts/tabs/ThresholdsTab.tsx +++ b/frontend-modern/src/features/alerts/tabs/ThresholdsTab.tsx @@ -1,5 +1,5 @@ import { createSignal, For, Show } from 'solid-js'; -import { Card } from '@/components/shared/Card'; +import ChevronRight from 'lucide-solid/icons/chevron-right'; import { ThresholdsTable } from '@/components/Alerts/ThresholdsTable'; import { AlertIntentPolicyPanel, @@ -76,16 +76,31 @@ export function ThresholdsTab(props: ThresholdsTabProps) { }); props.setHasUnsavedChanges(true); }; + const averageLabel = (seconds: number) => { + if (seconds === 0) return 'Current value, no averaging'; + if (seconds < 60) return `${seconds}-second average`; + return `${seconds / 60}-minute average`; + }; + const cpuWindowSummary = () => { + const windows = props.metricEvaluationWindows?.() ?? {}; + const lead = averageLabel(windows.all?.cpu ?? 300); + // Count every configured profile, including ones only the API sets (such + // as pod), so the line never claims one window for every resource. + const overrides = Object.entries(windows).filter( + ([key, profile]) => key !== 'all' && profile?.cpu !== undefined, + ).length; + return overrides === 0 + ? `${lead} for every resource` + : `${lead}, ${overrides} platform override${overrides === 1 ? '' : 's'}`; + }; const evaluationProfileControl = (profile: (typeof evaluationProfiles)[number]) => { const configured = () => props.metricEvaluationWindows?.()[profile.key]?.cpu; return ( -