From 2312345a02d49e503c5af2382e89db296feeda6e Mon Sep 17 00:00:00 2001 From: "courtmanr@gmail.com" Date: Sun, 23 Nov 2025 00:10:58 +0000 Subject: [PATCH] feat: add degraded status dot to Proxmox and Docker tabs (fixes #744) --- .../src/components/Dashboard/Dashboard.tsx | 85 ++++++----- .../components/Docker/DockerUnifiedTable.tsx | 132 +++++++++++------- 2 files changed, 131 insertions(+), 86 deletions(-) diff --git a/frontend-modern/src/components/Dashboard/Dashboard.tsx b/frontend-modern/src/components/Dashboard/Dashboard.tsx index 89c9ea0..f783b93 100644 --- a/frontend-modern/src/components/Dashboard/Dashboard.tsx +++ b/frontend-modern/src/components/Dashboard/Dashboard.tsx @@ -16,7 +16,7 @@ import { Card } from '@/components/shared/Card'; import { EmptyState } from '@/components/shared/EmptyState'; import { NodeGroupHeader } from '@/components/shared/NodeGroupHeader'; import { ProxmoxSectionNav } from '@/components/Proxmox/ProxmoxSectionNav'; -import { isNodeOnline } from '@/utils/status'; +import { isNodeOnline, OFFLINE_HEALTH_STATUSES, DEGRADED_HEALTH_STATUSES } from '@/utils/status'; import { getNodeDisplayName } from '@/utils/nodes'; import { logger } from '@/utils/logger'; import { usePersistentSignal } from '@/hooks/usePersistentSignal'; @@ -683,12 +683,22 @@ export function Dashboard(props: DashboardProps) { const totalStats = createMemo(() => { const guests = filteredGuests(); const running = guests.filter((g) => g.status === 'running').length; + const degraded = guests.filter((g) => { + const status = (g.status || '').toLowerCase(); + // Count as degraded if explicitly in degraded list, or if not running and not offline/stopped + return ( + DEGRADED_HEALTH_STATUSES.has(status) || + (status !== 'running' && !OFFLINE_HEALTH_STATUSES.has(status)) + ); + }).length; + const stopped = guests.length - running - degraded; const vms = guests.filter((g) => g.type === 'qemu').length; const containers = guests.filter((g) => g.type === 'lxc').length; return { total: guests.length, running, - stopped: guests.length - running, + degraded, + stopped, vms, containers, }; @@ -1026,38 +1036,38 @@ export function Dashboard(props: DashboardProps) { const node = nodeByInstance()[instanceId]; return ( - <> - - - - }> - {(guest) => { - // Match backend ID generation logic: stable format is "instance-vmid" - const guestId = - guest.id || `${guest.instance}-${guest.vmid}`; - const metadata = - guestMetadata()[guestId] || - guestMetadata()[`${guest.node}-${guest.vmid}`]; - const parentNode = node ?? resolveParentNode(guest); - const parentNodeOnline = parentNode ? isNodeOnline(parentNode) : true; - return ( - - - - ); - }} - - - ); + <> + + + + }> + {(guest) => { + // Match backend ID generation logic: stable format is "instance-vmid" + const guestId = + guest.id || `${guest.instance}-${guest.vmid}`; + const metadata = + guestMetadata()[guestId] || + guestMetadata()[`${guest.node}-${guest.vmid}`]; + const parentNode = node ?? resolveParentNode(guest); + const parentNodeOnline = parentNode ? isNodeOnline(parentNode) : true; + return ( + + + + ); + }} + + + ); }} @@ -1110,6 +1120,13 @@ export function Dashboard(props: DashboardProps) { {totalStats().running} running + 0}> + | + + + {totalStats().degraded} degraded + + | diff --git a/frontend-modern/src/components/Docker/DockerUnifiedTable.tsx b/frontend-modern/src/components/Docker/DockerUnifiedTable.tsx index f618694..72134eb 100644 --- a/frontend-modern/src/components/Docker/DockerUnifiedTable.tsx +++ b/frontend-modern/src/components/Docker/DockerUnifiedTable.tsx @@ -46,18 +46,18 @@ type SearchToken = { key?: string; value: string }; type DockerRow = | { - kind: 'container'; - id: string; - host: DockerHost; - container: DockerContainer; - } + kind: 'container'; + id: string; + host: DockerHost; + container: DockerContainer; + } | { - kind: 'service'; - id: string; - host: DockerHost; - service: DockerService; - tasks: DockerTask[]; - }; + kind: 'service'; + id: string; + host: DockerHost; + service: DockerService; + tasks: DockerTask[]; + }; interface DockerUnifiedTableProps { hosts: DockerHost[]; @@ -283,36 +283,36 @@ const PODMAN_METADATA_GROUPS: Array<{ prefixes?: string[]; keys?: string[]; }> = [ - { - title: 'Pod', - prefixes: ['io.podman.annotations.pod.', 'io.podman.pod.', 'net.containers.podman.pod.'], - }, - { - title: 'Compose', - prefixes: ['io.podman.compose.'], - }, - { - title: 'Auto Update', - prefixes: ['io.containers.autoupdate.'], - keys: ['io.containers.autoupdate'], - }, - { - title: 'User Namespace', - keys: ['io.podman.annotations.userns', 'io.containers.userns'], - }, - { - title: 'Capabilities', - keys: ['io.containers.capabilities', 'io.containers.selinux', 'io.containers.seccomp'], - }, - { - title: 'Podman Annotations', - prefixes: ['io.podman.annotations.'], - }, - { - title: 'Container Settings', - prefixes: ['io.containers.'], - }, -]; + { + title: 'Pod', + prefixes: ['io.podman.annotations.pod.', 'io.podman.pod.', 'net.containers.podman.pod.'], + }, + { + title: 'Compose', + prefixes: ['io.podman.compose.'], + }, + { + title: 'Auto Update', + prefixes: ['io.containers.autoupdate.'], + keys: ['io.containers.autoupdate'], + }, + { + title: 'User Namespace', + keys: ['io.podman.annotations.userns', 'io.containers.userns'], + }, + { + title: 'Capabilities', + keys: ['io.containers.capabilities', 'io.containers.selinux', 'io.containers.seccomp'], + }, + { + title: 'Podman Annotations', + prefixes: ['io.podman.annotations.'], + }, + { + title: 'Container Settings', + prefixes: ['io.containers.'], + }, + ]; const humanizePodmanKey = (raw: string): string => { if (!raw) return 'Value'; @@ -1049,9 +1049,8 @@ const DockerContainerRow: Component<{ return ( <> @@ -1539,11 +1538,10 @@ const DockerContainerRow: Component<{
{rw} @@ -1819,13 +1817,11 @@ const DockerServiceRow: Component<{ return ( <> @@ -2356,6 +2352,31 @@ const DockerUnifiedTable: Component = (props) => { }, 0), ); + const degradedContainers = createMemo(() => + groupedRows().reduce((acc, group) => { + return ( + acc + + group.rows + .filter((row): row is Extract => row.kind === 'container') + .filter((row) => { + const state = toLower(row.container.state); + const health = toLower(row.container.health); + + // Explicitly degraded/error states + if (ERROR_CONTAINER_STATES.has(state)) return true; + + // Running but unhealthy + if (state === 'running' && health === 'unhealthy') return true; + + // Any other state that is NOT running and NOT stopped + if (state !== 'running' && !STOPPED_CONTAINER_STATES.has(state)) return true; + + return false; + }).length + ); + }, 0), + ); + const renderRow = (row: DockerRow, grouped: boolean) => { const resourceId = row.kind === 'container' @@ -2579,6 +2600,13 @@ const DockerUnifiedTable: Component = (props) => { + 0}> + | + + + |