diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json index 3cb0a34b9..df748f40b 100644 --- a/frontend-modern/browser-verification.json +++ b/frontend-modern/browser-verification.json @@ -1,112 +1,18 @@ { "version": 1, - "base_sha": "df1290066e2907b344b9294d0106dcbabde03a53", - "verified_at": "2026-08-20T20:51:48Z", + "base_sha": "ee4cd560b6352fac50b988104e3ee370db8669c1", + "verified_at": "2026-08-20T21:08:35Z", "result": "passed", "changed_paths": [ - "frontend-modern/src/features/docker/DockerAlertsTable.tsx", - "frontend-modern/src/features/docker/DockerConfigsTable.tsx", - "frontend-modern/src/features/docker/DockerContainersTable.tsx", - "frontend-modern/src/features/docker/DockerHostsTable.tsx", - "frontend-modern/src/features/docker/DockerImagesTable.tsx", - "frontend-modern/src/features/docker/DockerNetworksTable.tsx", - "frontend-modern/src/features/docker/DockerPageSurface.tsx", - "frontend-modern/src/features/docker/DockerSecretsTable.tsx", - "frontend-modern/src/features/docker/DockerServicesTable.tsx", - "frontend-modern/src/features/docker/DockerStorageUsageTable.tsx", - "frontend-modern/src/features/docker/DockerSwarmNodesTable.tsx", - "frontend-modern/src/features/docker/DockerTasksTable.tsx", - "frontend-modern/src/features/docker/DockerVolumesTable.tsx", - "frontend-modern/src/features/kubernetes/KubernetesAlertsTable.tsx", - "frontend-modern/src/features/kubernetes/KubernetesAutoscalingTable.tsx", - "frontend-modern/src/features/kubernetes/KubernetesClustersTable.tsx", - "frontend-modern/src/features/kubernetes/KubernetesConfigTable.tsx", - "frontend-modern/src/features/kubernetes/KubernetesControllersTable.tsx", - "frontend-modern/src/features/kubernetes/KubernetesDeploymentsTable.tsx", - "frontend-modern/src/features/kubernetes/KubernetesEventsTable.tsx", - "frontend-modern/src/features/kubernetes/KubernetesNetworkingTable.tsx", - "frontend-modern/src/features/kubernetes/KubernetesNodesTable.tsx", - "frontend-modern/src/features/kubernetes/KubernetesPageSurface.tsx", - "frontend-modern/src/features/kubernetes/KubernetesPodsTable.tsx", - "frontend-modern/src/features/kubernetes/KubernetesPolicyTable.tsx", - "frontend-modern/src/features/kubernetes/KubernetesServicesTable.tsx", - "frontend-modern/src/features/kubernetes/KubernetesStorageTable.tsx", - "frontend-modern/src/features/platformPage/sharedPlatformPage.tsx", - "frontend-modern/src/features/proxmox/ProxmoxCephTable.tsx", - "frontend-modern/src/features/proxmox/ProxmoxMailGatewayTable.tsx", - "frontend-modern/src/features/proxmox/ProxmoxReplicationTable.tsx", - "frontend-modern/src/features/standalone/AgentsMachinesTable.tsx", - "frontend-modern/src/features/standalone/AvailabilityChecksTable.tsx", - "frontend-modern/src/features/truenas/TrueNASAlertsTable.tsx", - "frontend-modern/src/features/truenas/TrueNASAppsTable.tsx", - "frontend-modern/src/features/truenas/TrueNASNetworkSharesTable.tsx", - "frontend-modern/src/features/truenas/TrueNASProtectionTable.tsx", - "frontend-modern/src/features/truenas/TrueNASServicesTable.tsx", - "frontend-modern/src/features/truenas/TrueNASStorageTopologyTable.tsx", - "frontend-modern/src/features/truenas/TrueNASSystemsTable.tsx", - "frontend-modern/src/features/truenas/TrueNASVirtualMachinesTable.tsx", - "frontend-modern/src/features/vmware/VsphereActivityTable.tsx", - "frontend-modern/src/features/vmware/VsphereAlertsTable.tsx", - "frontend-modern/src/features/vmware/VsphereDatastoresTable.tsx", - "frontend-modern/src/features/vmware/VsphereHostsTable.tsx", - "frontend-modern/src/features/vmware/VsphereNetworksTable.tsx" + "frontend-modern/src/features/alerts/AlertHistoryFiltersCard.tsx", + "frontend-modern/src/features/alerts/useAlertHistoryState.ts" ], "content_sha256": { - "frontend-modern/src/features/docker/DockerAlertsTable.tsx": "0b9bea8a536a60c1fb9a6dc0ba11ff37400dcf39e35197893b37b7418dcbaa28", - "frontend-modern/src/features/docker/DockerConfigsTable.tsx": "d0953a8f2a5ca3bfd588d4d76945cfe880f484a76dbb49a03e66d33f8e943c5d", - "frontend-modern/src/features/docker/DockerContainersTable.tsx": "725833c041e9cac2a0df0407ca726352d25e80a9483ae1ffc171db8b899cded6", - "frontend-modern/src/features/docker/DockerHostsTable.tsx": "f2080e90ec27178ef11d58164b5dc0055d968150d06998b0770a23b9bcf36826", - "frontend-modern/src/features/docker/DockerImagesTable.tsx": "81e2559e29e71f193af0410446af6ad6fe46e2f01ecc0799ea534f591ea9352b", - "frontend-modern/src/features/docker/DockerNetworksTable.tsx": "ec4ff5168ffa2405a8b818bb85cfbcd639adfc530965db7aaf80532306cfa121", - "frontend-modern/src/features/docker/DockerPageSurface.tsx": "e789ff025a3c8ecf74f57d43122d32f9128ea1da4fabd161f1f2e64fe42b763e", - "frontend-modern/src/features/docker/DockerSecretsTable.tsx": "1453356c100f6d38102fcc6ade351d97e7fd06efb580cd34a4b1d2ac3b218aae", - "frontend-modern/src/features/docker/DockerServicesTable.tsx": "250ded4ce0cbeda16427b5f47e3e289ff09b5d43dcc3c9d9ae80a356fbfac2b6", - "frontend-modern/src/features/docker/DockerStorageUsageTable.tsx": "b37f1b96a094979ac734a0f9647ca2558d791a9646bf0ccf7b8b8682202454c7", - "frontend-modern/src/features/docker/DockerSwarmNodesTable.tsx": "47bade17b070470a77e8cd8adc9f56072d91e0ef975a2311a163fc8d4291db3d", - "frontend-modern/src/features/docker/DockerTasksTable.tsx": "4c2832daa4f82f9a4c6961dda3a90d0af416286f06f2ad98145f734e5b21a253", - "frontend-modern/src/features/docker/DockerVolumesTable.tsx": "a1111ff720bbb2b110617e2e64d2ea0229cdd731db18228769e857a9c3c6b641", - "frontend-modern/src/features/kubernetes/KubernetesAlertsTable.tsx": "6f5f291b9b1b038e4f5268ef39e52683c212b814dae3fc4ef373c06957b872b0", - "frontend-modern/src/features/kubernetes/KubernetesAutoscalingTable.tsx": "bd74274b3778753be5994bb14b1245110835687978ad9c4d88c282723d941ab3", - "frontend-modern/src/features/kubernetes/KubernetesClustersTable.tsx": "34633caacfb7c58679c66cd609301237f585de41b38e1219aa358c77583b668f", - "frontend-modern/src/features/kubernetes/KubernetesConfigTable.tsx": "7be2ad7c56fd26086c998b8bcfe65641d4fa34ccc243fe725619728d7438add6", - "frontend-modern/src/features/kubernetes/KubernetesControllersTable.tsx": "24fc9b3ccab27da44e3c4b6ef10d3ad1c09672883f152195ef6c63a96e15f31e", - "frontend-modern/src/features/kubernetes/KubernetesDeploymentsTable.tsx": "220ab81ca584476c83148e6b058f034bd52052749a33469933b11b1ef9a07b5e", - "frontend-modern/src/features/kubernetes/KubernetesEventsTable.tsx": "9ae69de55dbbe5a8ebc1a93418357a09791db61119af67027bf7641de040929e", - "frontend-modern/src/features/kubernetes/KubernetesNetworkingTable.tsx": "0887ec3a7a364b6b55f8ef76ed764e9c1ae8d286fc3cc64fabb5e1c4fc31ed18", - "frontend-modern/src/features/kubernetes/KubernetesNodesTable.tsx": "d1b3ad43e54226d158268076b80e2935767778eee10a29e57957ea8ea1c44281", - "frontend-modern/src/features/kubernetes/KubernetesPageSurface.tsx": "d1431b557cb8fa6ad1b2c90318add989456caddd8b9a255b65267dd20e517746", - "frontend-modern/src/features/kubernetes/KubernetesPodsTable.tsx": "b03bfe0320d1c515b2d70fa5f2769738f5fdc1cb085834d442e18bb198357273", - "frontend-modern/src/features/kubernetes/KubernetesPolicyTable.tsx": "2fb1e679cf4b611d208eb03659f21c2d75835e4e93cdc94376d28d8989434775", - "frontend-modern/src/features/kubernetes/KubernetesServicesTable.tsx": "77c419e6bd321768a8b1112e512dc91902cd9a39eb248de401555e8b447d138a", - "frontend-modern/src/features/kubernetes/KubernetesStorageTable.tsx": "f24b0732fdf65f8d1308ea1e3ecaa752de6c103a93e99c529e8ba3cc1b8b75a1", - "frontend-modern/src/features/platformPage/sharedPlatformPage.tsx": "d8f9117b9d0c8ff5b1b50d3bdd26cfb9ad83f67522d6df646c6051c1aeed1106", - "frontend-modern/src/features/proxmox/ProxmoxCephTable.tsx": "df294240ac6089d57314743658912ba0667a763ac09d977f79188be3e948bfae", - "frontend-modern/src/features/proxmox/ProxmoxMailGatewayTable.tsx": "763a6a2a2dfc8b66469e0536a9f7a042f675ba507ad4aee784a41b0897712075", - "frontend-modern/src/features/proxmox/ProxmoxReplicationTable.tsx": "67670bc9d65cdd2eb602e016aec7ccc70cf52b5783aafbabb0fa3e268eda3c5c", - "frontend-modern/src/features/standalone/AgentsMachinesTable.tsx": "4f0772c77e399f8e7f191448b6eb4802a2c88092323d5423a68b91c5be425c7a", - "frontend-modern/src/features/standalone/AvailabilityChecksTable.tsx": "564c594d35e71fead4c408e3342b0ed29205cebfe2e087b4dbc54079e73c159f", - "frontend-modern/src/features/truenas/TrueNASAlertsTable.tsx": "7560b41d9539fa92e4d678cd007b994065a1a16e9a08084b587900d052432736", - "frontend-modern/src/features/truenas/TrueNASAppsTable.tsx": "d450be7beb823f1222cc9d5ac86a54802141d9623ffa5145a7feee9482b6d05d", - "frontend-modern/src/features/truenas/TrueNASNetworkSharesTable.tsx": "328caf9474494ac639888c4800abfe3ce5e900ec9203edc7211fbf126153ae6a", - "frontend-modern/src/features/truenas/TrueNASProtectionTable.tsx": "bcc75ab5bab12b21f00212b18679ddcce9f5a603f212991581ccdc07109ef15c", - "frontend-modern/src/features/truenas/TrueNASServicesTable.tsx": "85f9677ef2a2ca3ad739c3dcfa2f127b9274ba43ac12452d67b7a44bbcaff6d6", - "frontend-modern/src/features/truenas/TrueNASStorageTopologyTable.tsx": "105e87bc673d3bd8f5e1b975d14f8b99f7a60233002b4fd89678c0813bf432a8", - "frontend-modern/src/features/truenas/TrueNASSystemsTable.tsx": "458e2af554abd3c60e0e2af51eb2e4d95a9b74615245dd54d76d611ef0704371", - "frontend-modern/src/features/truenas/TrueNASVirtualMachinesTable.tsx": "e521207945aa2ba5ae274d7618c90febe8365d05c854435830f29083cf482b83", - "frontend-modern/src/features/vmware/VsphereActivityTable.tsx": "5e6093781558567bbf925f6862220b6b40d4420d438fd1f797282d7b3a0625f8", - "frontend-modern/src/features/vmware/VsphereAlertsTable.tsx": "ef4fd7067ca699b4e3f1060874180daa0bb7554d9c439626a8a9553d14d67c64", - "frontend-modern/src/features/vmware/VsphereDatastoresTable.tsx": "2a4150e146169c192b25591934ed2a84f2b57e7ea47af41fe169f8255751f1e4", - "frontend-modern/src/features/vmware/VsphereHostsTable.tsx": "c5bd0a61c793835490c2555d184d8d54b8d30f8fa6cc6d03ccf7c39fdafb5263", - "frontend-modern/src/features/vmware/VsphereNetworksTable.tsx": "61eb9935878eee11f15834f98832149ad40e8b868650a7420e0d03135436e922" + "frontend-modern/src/features/alerts/AlertHistoryFiltersCard.tsx": "07836c5b85ea484cfe984b2d0de5c302202ef6b844eafdc776b3f979f0938acb", + "frontend-modern/src/features/alerts/useAlertHistoryState.ts": "7528bd735ff84961e8bd880d89f0253255c698ab5910c1eb2fb10cf0255e39e8" }, "routes": [ - "/docker/overview", - "/kubernetes/workloads", - "/truenas (via nav tab)", - "/standalone/machines (via nav tab)", - "/vmware/overview (via nav tab)", - "/proxmox/replication", - "/proxmox/ceph" + "/alerts/history" ], "viewports": [ { @@ -119,21 +25,12 @@ } ], "states": [ - "Docker Overview status chips now carry live counts (All 12 / Healthy 12 / Degraded 0 / Offline 0) matching the '12 containers' toolbar counter", - "Docker Overview with Degraded selected: chip count 0 agrees with the '0 of 12 containers' counter and the filtered empty state", - "Kubernetes Workloads shared toolbar counts across all four stacked sections (All 19 / Healthy 17 / Degraded 2 / Offline 0); selecting Degraded shows '2 of 19 rows' including the Pending cron-nightly-backfill pod, so the chip count equals the rows its selection renders", - "TrueNAS incident severity chips counted (All 2 / Critical 0 / Warning 2 / Info 0)", - "Machines (standalone) chips counted (All 2 / Healthy 2 / Degraded 0 / Offline 0)", - "vSphere overview workloads chips unchanged and still counted (All 18 / Powered on 18 / Attention 0 / Powered off 0)", - "Proxmox Replication bespoke toolbar counted (All 8 / Healthy 8 / Failed 0 / Pending 0 / Disabled 0) and Proxmox Ceph counted (All 1 / Healthy 0 / Warning 1 / Critical 0)", - "Inventory totals preference set to hidden: chips reactively drop their counts (plain All / Healthy / Degraded / Offline) along with the rows counter; restoring the preference brings counts back on remount", - "Mobile viewport (375x812) Docker Overview renders with chips collapsed behind the canonical Filters control" + "Alert history severity facet now counted from the live history data: All 47 / Critical 8 / Warning 13, with Period chips left uncounted as time scopes", + "Critical selected: the page's own summary reads '8 alerts', matching the chip count exactly (counts share filterAlertHistoryItems with the list)", + "Chips verified through the mobile Filters sheet (the hidden preview pane renders the responsive mobile layout); the same options render inline at desktop width" ], "interactions": [ - "Clicked the Degraded chip on Docker Overview and confirmed the empty state plus '0 of 12 containers'", - "Clicked the Degraded chip on Kubernetes Workloads and confirmed '2 of 19 rows' with the Pending pod visible", - "Navigated Docker -> Kubernetes -> TrueNAS -> Machines -> vSphere -> Proxmox Replication -> Proxmox Ceph reading each toolbar's chip names and counts from the accessibility tree", - "Toggled the shared platformEstateOverviewVisible preference off and back on, confirming the chip-count gate follows it", - "Reset the Docker status filter back to All before finishing" + "Opened the Filters sheet on /alerts/history and read the counted severity chips", + "Clicked the Critical chip, confirmed the '8 alerts' summary, then reset the facet back to All" ] } diff --git a/frontend-modern/src/features/alerts/AlertHistoryFiltersCard.tsx b/frontend-modern/src/features/alerts/AlertHistoryFiltersCard.tsx index 888ccf66f..daf772bba 100644 --- a/frontend-modern/src/features/alerts/AlertHistoryFiltersCard.tsx +++ b/frontend-modern/src/features/alerts/AlertHistoryFiltersCard.tsx @@ -1,4 +1,9 @@ import { FilterBar, filterChipStatusDot, type FilterDef } from '@/components/shared/FilterBar'; +import { + PLATFORM_ESTATE_COUNTS_STORAGE_KEY, + deserializePlatformEstateCountsVisibility, +} from '@/features/platformPage/platformEstateOverviewModel'; +import { usePersistentSignal } from '@/hooks/usePersistentSignal'; import { STORAGE_KEYS } from '@/utils/localStorage'; import { ALERT_HISTORY_ALL_TIME_FILTER_LABEL, @@ -13,6 +18,13 @@ interface AlertHistoryFiltersCardProps { } export function AlertHistoryFiltersCard(props: AlertHistoryFiltersCardProps) { + // Same estate-wide preference that gates the platform tables' chip counts, + // so hiding inventory totals quiets this facet too. + const [countsVisible] = usePersistentSignal(PLATFORM_ESTATE_COUNTS_STORAGE_KEY, true, { + deserialize: deserializePlatformEstateCountsVisibility, + }); + const severityCount = (value: AlertSeverityFilter): number | undefined => + countsVisible() ? props.state.countForSeverity(value) : undefined; const buildFilters = (): FilterDef[] => [ { id: 'alert-period', @@ -38,18 +50,20 @@ export function AlertHistoryFiltersCard(props: AlertHistoryFiltersCardProps) { setValue: (value: string) => props.state.setSeverityFilter(value as AlertSeverityFilter), defaultValue: 'all', options: () => [ - { value: 'all', label: 'All' }, + { value: 'all', label: 'All', count: severityCount('all') }, { value: 'critical', label: 'Critical', leading: filterChipStatusDot('bg-red-500'), tone: 'danger', + count: severityCount('critical'), }, { value: 'warning', label: 'Warning', leading: filterChipStatusDot('bg-amber-500'), tone: 'warning', + count: severityCount('warning'), }, ], }, diff --git a/frontend-modern/src/features/alerts/__tests__/useAlertHistoryState.test.tsx b/frontend-modern/src/features/alerts/__tests__/useAlertHistoryState.test.tsx index d2cf9fc6d..a374ae5eb 100644 --- a/frontend-modern/src/features/alerts/__tests__/useAlertHistoryState.test.tsx +++ b/frontend-modern/src/features/alerts/__tests__/useAlertHistoryState.test.tsx @@ -156,6 +156,48 @@ describe('useAlertHistoryState', () => { expect(result.alertHistory()).toEqual([]); }); + it('counts each severity chip from the same predicate the list filters with', async () => { + const [activeAlerts] = createSignal({}); + const now = Date.now(); + const startTime = new Date(now - 30 * 60 * 1000).toISOString(); + const makeEntry = (id: string, level: string, resourceName: string) => ({ + id, + type: 'cpu', + level, + startTime, + lastSeen: startTime, + resourceId: `resource-${id}`, + resourceName, + message: 'CPU high', + acknowledged: false, + }); + vi.mocked(AlertsAPI.getHistory).mockResolvedValue([ + makeEntry('alert-1', 'critical', 'db-01'), + makeEntry('alert-2', 'warning', 'db-01'), + makeEntry('alert-3', 'warning', 'web-01'), + ] as any); + + const { result } = renderHook(() => + useAlertHistoryState({ + activeAlerts, + getResource: () => undefined, + allResources: () => [], + }), + ); + + await waitFor(() => expect(result.countForSeverity('all')).toBe(3)); + expect(result.countForSeverity('critical')).toBe(1); + expect(result.countForSeverity('warning')).toBe(2); + + // Counts ignore the selected severity (each chip shows what its own + // selection would render) but follow the search term. + result.setSeverityFilter('critical'); + expect(result.countForSeverity('warning')).toBe(2); + result.setSearchTerm('db-01'); + await waitFor(() => expect(result.countForSeverity('warning')).toBe(1)); + expect(result.countForSeverity('all')).toBe(2); + }); + it('clears search, period, and severity in one route write', async () => { const [activeAlerts] = createSignal({}); vi.mocked(AlertsAPI.getHistory).mockResolvedValue([] as any); diff --git a/frontend-modern/src/features/alerts/useAlertHistoryState.ts b/frontend-modern/src/features/alerts/useAlertHistoryState.ts index d9926c9ec..1afd4d7c2 100644 --- a/frontend-modern/src/features/alerts/useAlertHistoryState.ts +++ b/frontend-modern/src/features/alerts/useAlertHistoryState.ts @@ -269,6 +269,12 @@ export function useAlertHistoryState(props: UseAlertHistoryStateProps) { return filterAlertHistoryItems(allHistoryData(), severityFilter(), searchTerm()); }); + // Chip-count source for the severity facet: the same predicate the list + // filters with, so each chip shows the row count its selection renders + // (before any chart bar drill-down, which narrows further and explicitly). + const countForSeverity = (value: AlertSeverityFilter): number => + filterAlertHistoryItems(allHistoryData(), value, searchTerm()).length; + const alertTrends = createMemo(() => { return buildAlertTrends(severityAndSearchFilteredItems(), timeFilter()); }); @@ -334,6 +340,7 @@ export function useAlertHistoryState(props: UseAlertHistoryStateProps) { resourceIncidentEventFilters: resourceIncidentsState.resourceIncidentEventFilters, setResourceIncidentEventFilters: resourceIncidentsState.setResourceIncidentEventFilters, activeFilterCount, + countForSeverity, clearFilters, incidentTimelines, incidentLoading,