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