From 48f0dc9e8271a3c40cf9f75de279d2f661741a4c Mon Sep 17 00:00:00 2001 From: rcourtman <8825017+rcourtman@users.noreply.github.com> Date: Wed, 14 Oct 2026 14:38:24 +0100 Subject: [PATCH] Colour a container drawer's status dot by its health, as the row does A running container failing its health check showed a red dot and "Unhealthy" in its table row but a green dot in the expanded drawer header, which used the generic run-status mapper for every resource. The drawer header now uses the container row's mapper for app containers, so run state and health check decide the dot and its label in both places. Other resources keep the shared run-status dot. Sol reviewed the desktop and phone screenshots and found the dot plus the lifted "Health check: Failing" row enough, without another label. --- .../internal/subsystems/unified-resources.md | 5 +++ .../browser-tests/proxmox-docker-rows.cjs | 13 ++++++++ .../eb8c958def6282ab.json | 33 +++++++++++++++++++ ...urceDetailDrawer.docker-container.test.tsx | 5 +++ .../ResourceDetailDrawer.history.test.tsx | 23 +++++++++++++ .../useResourceDetailDrawerDerivedState.ts | 10 +++++- 6 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 frontend-modern/browser-verification/eb8c958def6282ab.json diff --git a/docs/release-control/v6/internal/subsystems/unified-resources.md b/docs/release-control/v6/internal/subsystems/unified-resources.md index 276c781253..002c667242 100644 --- a/docs/release-control/v6/internal/subsystems/unified-resources.md +++ b/docs/release-control/v6/internal/subsystems/unified-resources.md @@ -2390,6 +2390,11 @@ address from the resource itself, state used amounts only for an online or running resource whose memory observation (if any) is current, and lift an offline resource's state with its `lastSeen`. A row an open alert of the resource's own already states (`getAlertStatedRowLabels`) stays in Details. +The drawer header's status dot for an `app-container` uses the container +row's mapper (`mapDockerContainerStatus`), so its run state and health check +decide the colour and label in both places: a running container failing its +health check is red "Unhealthy" in the row and the header alike. Other +resources keep `getAgentStatusIndicator` on `status`. Those open alerts come from the websocket `activeAlerts` map through `getAlertsForUnifiedResource` in `frontend-modern/src/utils/alerts.ts`, which the Docker host drawer (`DockerHostDrawerOverview.tsx`) uses the same way. diff --git a/frontend-modern/browser-tests/proxmox-docker-rows.cjs b/frontend-modern/browser-tests/proxmox-docker-rows.cjs index a99573c803..a8d285cb42 100644 --- a/frontend-modern/browser-tests/proxmox-docker-rows.cjs +++ b/frontend-modern/browser-tests/proxmox-docker-rows.cjs @@ -67,6 +67,8 @@ const observations = (mode) => [ runtimeVersion: '27.5.1', image: 'nginx:latest', containerState: 'running', + // Running but failing its health check: row and drawer must both say so. + health: 'unhealthy', restartCount: 7, updateStatus: { updateAvailable: true, @@ -371,6 +373,17 @@ const observations = (mode) => [ const detail = page.getByTestId('resource-drawer-exceptions'); await detail.getByText('Restarts', { exact: true }).waitFor(); await detail.getByText('7', { exact: true }).waitFor(); + // The drawer header dot agrees with the row's: unhealthy is red, not green. + const headerDot = page.getByRole('img', { name: 'Unhealthy' }).first(); + await headerDot.waitFor(); + const rowDot = row.locator('[title="Unhealthy"]').first(); + const colours = await Promise.all( + [headerDot, rowDot].map((dot) => + dot.evaluate((el) => getComputedStyle(el).backgroundColor), + ), + ); + assert.equal(colours[0], colours[1], JSON.stringify(colours)); + assert.match(colours[0], /^rgb\((2[0-9]{2}), ([0-9]{1,2}|1[0-2][0-9]), /); }, ); await capture('container-detail'); diff --git a/frontend-modern/browser-verification/eb8c958def6282ab.json b/frontend-modern/browser-verification/eb8c958def6282ab.json new file mode 100644 index 0000000000..642ae1592f --- /dev/null +++ b/frontend-modern/browser-verification/eb8c958def6282ab.json @@ -0,0 +1,33 @@ +{ + "version": 1, + "base_sha": "e72df20395b0659aadd08deacd4a1549cda9b26b", + "verified_at": "2026-10-14T13:37:16Z", + "result": "passed", + "changed_paths": [ + "frontend-modern/src/components/Infrastructure/useResourceDetailDrawerDerivedState.ts" + ], + "content_sha256": { + "frontend-modern/src/components/Infrastructure/useResourceDetailDrawerDerivedState.ts": "095e0365fc6bf30c8a5b5d88ac03616576bd66a71496d21a158330817dbdff5d" + }, + "routes": [ + "browser-tests/proxmox-docker-rows.html (production DockerContainersTable row and ResourceDetailDrawer, Chromium via Playwright 1.56.1 on pulse-dev)" + ], + "viewports": [ + { + "width": 1440, + "height": 900 + }, + { + "width": 390, + "height": 844 + } + ], + "states": [ + "running container failing its health check: row dot and drawer header dot both red \"Unhealthy\" (old code: header green)", + "exited container row unchanged", + "reviewed by Sol from the desktop and phone screenshots" + ], + "interactions": [ + "row expand by pointer (desktop) and tap (phone)" + ] +} diff --git a/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.docker-container.test.tsx b/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.docker-container.test.tsx index 7e4e80e3d5..812769d11c 100644 --- a/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.docker-container.test.tsx +++ b/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.docker-container.test.tsx @@ -326,6 +326,9 @@ describe('ResourceDetailDrawer for Docker containers', () => { /> )); + // The header dot agrees with the row: running but unhealthy is red, not green. + const dot = screen.getByRole('img', { name: 'Unhealthy' }); + expect(dot).toHaveClass('bg-red-500'); const runtime = screen.getByTestId('resource-runtime-context-section'); expect(within(runtime).getByText('Running')).toBeInTheDocument(); // The failing check is lifted into Worth checking, in front of the charts. @@ -357,6 +360,8 @@ describe('ResourceDetailDrawer for Docker containers', () => { /> )); + // A failed exit is red in the header, and its stale health check says nothing. + expect(screen.getByRole('img', { name: 'Exited (1)' })).toHaveClass('bg-red-500'); const runtime = screen.getByTestId('resource-runtime-context-section'); const lifted = screen.getByTestId('resource-drawer-exceptions'); expect(within(lifted).getByText('Exited (1)').closest('td')).toHaveClass('text-rose-700'); diff --git a/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.history.test.tsx b/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.history.test.tsx index e63425c02a..da3472401d 100644 --- a/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.history.test.tsx +++ b/frontend-modern/src/components/Infrastructure/__tests__/ResourceDetailDrawer.history.test.tsx @@ -1773,3 +1773,26 @@ describe('ResourceDetailDrawer Overview alert sources', () => { expect(resourceDetailDrawerOverviewSource).not.toContain('message: alert.message,'); }); }); + +describe('ResourceDetailDrawer header status dot', () => { + it("reads a container's run state and health check, as its row does", () => { + const { unmount } = render(() => ( + + )); + // Running but failing its check is red with the row's label, not green. + expect(screen.getByRole('img', { name: 'Unhealthy' })).toHaveClass('bg-red-500'); + unmount(); + + render(() => ); + // Other resources keep the shared run-status dot. + expect(screen.queryByRole('img', { name: 'Unhealthy' })).toBeNull(); + }); +}); diff --git a/frontend-modern/src/components/Infrastructure/useResourceDetailDrawerDerivedState.ts b/frontend-modern/src/components/Infrastructure/useResourceDetailDrawerDerivedState.ts index f80753c422..91cb75ef5e 100644 --- a/frontend-modern/src/components/Infrastructure/useResourceDetailDrawerDerivedState.ts +++ b/frontend-modern/src/components/Infrastructure/useResourceDetailDrawerDerivedState.ts @@ -6,6 +6,7 @@ import type { ResourceVMwareMeta } from '@/types/resource'; import { formatAbsoluteTime, formatRelativeTime } from '@/utils/format'; import { useRelativeTimeNow } from '@/utils/relativeTimeClock'; import { getAgentStatusIndicator } from '@/utils/status'; +import { mapDockerContainerStatus } from '@/features/docker/dockerPageModel'; import { dedupeResourceBadges, getInfrastructureSystemIdentityBadges, @@ -113,7 +114,14 @@ export const useResourceDetailDrawerDerivedState = ( const kubernetesClusterName = createMemo(() => getPreferredResourceClusterName(resource()) ?? ''); const resolveResourceLabel = (resourceId: string): string => resolveResourceLabelInput?.(resourceId)?.trim() || resourceId; - const statusIndicator = createMemo(() => getAgentStatusIndicator({ status: resource().status })); + // A container's dot reads its run state and health check, the same mapper + // its table row uses, so a running but unhealthy container is not green here + // while red in the row above. + const statusIndicator = createMemo(() => + resource().type === 'app-container' + ? mapDockerContainerStatus(resource()) + : getAgentStatusIndicator({ status: resource().status }), + ); const healthIssue = createMemo(() => getResourceHealthIssuePresentation(resource())); // An open drawer keeps its rows while a resource's timestamps stay the same, // which is exactly when a silent resource's Last seen must keep aging, so