From 0f66f5c136808e6f2ab1d7e8c8ee9b50bcfa35e2 Mon Sep 17 00:00:00 2001 From: rcourtman <8825017+rcourtman@users.noreply.github.com> Date: Mon, 5 Oct 2026 02:34:42 +0100 Subject: [PATCH] Lead alert thresholds with each group's default limits MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Thresholds page opened on a ten-select CPU evaluation grid, and every resource group started collapsed showing only a name and a count, so the limits that decide what alerts were invisible until a group was opened one at a time. Each collapsed group header now states its defaults ("CPU 80% · Memory 85% · Disk 90%"), built by one helper from the same columns and defaults the group's table edits. It drops metrics the engine treats as off and hides the line while the group is disabled. Rules whose zero means a fallback are stated as whole rules with their effective values: Docker's restart loop and memory-limit, and backup age by its alerting days rather than the dashboard-only fresh/stale hours. CPU averaging becomes one collapsed line stating the effective window and how many profile overrides exist, with the same selects inside. Docker and Machines list their groups before Docker-only rules and the SMART card. --- .../v6/internal/subsystems/alerts.md | 24 ++++ .../subsystems/frontend-primitives.md | 8 ++ .../368f58089b70d7bf.json | 81 +++++++++++ .../sections/CollapsibleSection.tsx | 25 +++- .../__tests__/CollapsibleSection.test.tsx | 23 ++++ .../ThresholdsTableAgentDisksSection.tsx | 8 +- .../ThresholdsTableAgentsResourcesSection.tsx | 9 +- .../Alerts/ThresholdsTableAgentsTab.tsx | 3 +- ...ThresholdsTableDockerContainersSection.tsx | 51 ++++--- .../Alerts/ThresholdsTableDockerTab.tsx | 5 +- .../Alerts/ThresholdsTableKubernetesTab.tsx | 2 + .../ThresholdsTableProxmoxBackupsSection.tsx | 11 +- ...hresholdsTableProxmoxGuestDisksSection.tsx | 8 +- .../ThresholdsTableProxmoxGuestsSection.tsx | 29 ++-- .../ThresholdsTableProxmoxNodesSection.tsx | 9 +- .../ThresholdsTableProxmoxPBSSection.tsx | 9 +- .../ThresholdsTableProxmoxPMGSection.tsx | 43 +++--- ...ThresholdsTableProxmoxSnapshotsSection.tsx | 14 +- .../ThresholdsTableProxmoxStorageSection.tsx | 8 +- .../Alerts/ThresholdsTableTrueNASTab.tsx | 2 + .../Alerts/ThresholdsTableVMwareTab.tsx | 2 + .../alerts/__tests__/ThresholdsTab.test.tsx | 20 ++- .../features/alerts/tabs/ThresholdsTab.tsx | 91 +++++++------ ...alertThresholdsSectionPresentation.test.ts | 96 +++++++++++++ .../alertThresholdsSectionPresentation.ts | 127 ++++++++++++++++++ 25 files changed, 606 insertions(+), 102 deletions(-) create mode 100644 frontend-modern/browser-verification/368f58089b70d7bf.json 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 ( -