diff --git a/docs/features/editor.mdx b/docs/features/editor.mdx index 28c7bdf4..a975023f 100644 --- a/docs/features/editor.mdx +++ b/docs/features/editor.mdx @@ -58,7 +58,7 @@ Below the action bar, the **CONTAINERS** section lists every container the stack | **Status badge** | `✓` (green, healthy or running), `✗` (red, exited or unhealthy), `…` (yellow, starting). | | **Name** | The Docker container name; falls back to the first 12 characters of the container ID. | | **Uptime / state** | `up 2h 15m` for running containers, the raw state for everything else. | -| **Healthcheck label** | `healthcheck passing`, `healthcheck failing`, or `healthcheck starting`, only when a healthcheck is defined. | +| **Healthcheck label** | `healthy`, `unhealthy`, or `starting`, only when a healthcheck is defined. | | **Port mapping** | The first detected web-UI port, formatted `host → container/proto`. The mapping itself is a link that opens the service in a new tab, with a **Copy URL** button beside it. The address uses the active node's host and switches to `https` for port 443. | | **Action buttons** | **Image source links**, **View logs**, **Open bash shell** (admin only), **Service actions**. Per-container image references and registry links live here, not in the header. | | **Live stats** | CPU, memory, and net I/O with rolling sparklines. Only rendered while the container is running. Stats refresh on the same 1500 ms cadence as the dashboard. | diff --git a/docs/features/stack-management.mdx b/docs/features/stack-management.mdx index 08cdf81d..61c22619 100644 --- a/docs/features/stack-management.mdx +++ b/docs/features/stack-management.mdx @@ -185,8 +185,8 @@ The header answers two questions at a glance: - **What is this?** A breadcrumb (`LOCAL · STACKS · NAME`) and the stack name as the title. - **Is it healthy?** A state pill to the right of the title reports the live state: - - `running · healthy` (green) when at least one container reports a passing healthcheck. - - `running · unhealthy` (red) when any container reports a failing healthcheck. + - `running · healthy` (green) when at least one container reports Docker health status `healthy`. + - `running · unhealthy` (red) when any container reports Docker health status `unhealthy`. - `running · starting` (amber) during the Docker healthcheck start period. - `running` (green) when no healthcheck is defined. - `exited` (red) when no containers are up. @@ -215,9 +215,9 @@ Below the header, each container in the stack gets a single row that answers "is Each row includes: -- **Health badge.** A colored glyph reports the Docker healthcheck state: `✓` green for passing, `✗` red for failing, `…` amber while the healthcheck start period is in flight. Containers without a `healthcheck:` block show a neutral `✓`. +- **Health badge.** A colored glyph reports the Docker healthcheck state: `✓` green for `healthy`, `✗` red for `unhealthy`, `…` amber while the healthcheck start period is in flight. Containers without a `healthcheck:` block show a neutral `✓`. - **Container name** in mono. -- **Meta line.** Uptime (`up 12 hours`) and the primary port mapping (`8989 → 8989/tcp`). +- **Meta line.** Uptime (`up 12 hours`), the Docker health status when defined (`healthy`, `unhealthy`, or `starting`), and the primary port mapping (`8989 → 8989/tcp`). - **Open link.** When the container publishes a port, the mapping itself is a link (`8989 → 8989/tcp ↗`) that opens the service in a new tab, with a **Copy URL** button beside it. The address uses the active node's host and switches to `https` for port 443. Recognised multi-port apps open their web path automatically (for example, Plex opens `/web`). - **Live stat tiles.** Three tiles show CPU, memory, and network I/O with a rolling sparkline. The sparkline uses the cyan data color and refreshes roughly every 1.5 seconds. - **Action icons.** The image source links button (see above), plus shortcuts to **View logs**, open a bash shell, and (on single-service stacks) the per-container Start / Stop / Restart kebab. diff --git a/frontend/src/components/EditorLayout/__tests__/ContainersHealth.test.tsx b/frontend/src/components/EditorLayout/__tests__/ContainersHealth.test.tsx index 083e9283..74b9d690 100644 --- a/frontend/src/components/EditorLayout/__tests__/ContainersHealth.test.tsx +++ b/frontend/src/components/EditorLayout/__tests__/ContainersHealth.test.tsx @@ -549,3 +549,58 @@ describe('containers load states', () => { expect(screen.queryByRole('button', { name: /Monitor / })).toBeNull(); }); }); + +describe('ContainersHealth Docker health status labels', () => { + const OLD_PHRASES = ['healthcheck passing', 'healthcheck failing', 'healthcheck starting'] as const; + const HEALTH_TOKENS = ['healthy', 'unhealthy', 'starting'] as const; + + function metaLine(): HTMLElement { + const parent = screen.getByText('up 2 hours').parentElement; + if (!parent) throw new Error('expected meta line parent'); + return parent; + } + + function metaSpanTexts(meta: HTMLElement = metaLine()): Array { + return Array.from(meta.querySelectorAll('span')).map((el) => el.textContent); + } + + function renderWithHealth(healthStatus?: ContainerInfo['healthStatus']) { + const base = container([{ PrivatePort: 80, PublicPort: 8080 }]); + return renderHealth( + healthStatus === undefined ? base : ({ ...base, healthStatus } as ContainerInfo), + ); + } + + function expectNoLegacyPhrases(meta: HTMLElement) { + for (const phrase of OLD_PHRASES) { + expect(meta.textContent).not.toContain(phrase); + } + } + + it.each(HEALTH_TOKENS)('renders exact meta-line token for healthStatus %s', (token) => { + renderWithHealth(token); + const meta = metaLine(); + const labels = metaSpanTexts(meta); + expect(labels[0]).toBe('up 2 hours'); + expect(labels).toContain(token); + expect(labels.filter((t) => t === token)).toHaveLength(1); + expectNoLegacyPhrases(meta); + expect(screen.getByRole('link', { name: /8080/ })).toBeInTheDocument(); + }); + + it.each([ + ['none', 'none'], + ['omitted', undefined], + ] as const)('omits a health token when healthStatus is %s', (_case, healthStatus) => { + renderWithHealth(healthStatus); + const meta = metaLine(); + const labels = metaSpanTexts(meta); + expect(labels).toContain('up 2 hours'); + for (const token of HEALTH_TOKENS) { + expect(labels).not.toContain(token); + } + expect(labels.filter((t) => t === '·')).toHaveLength(1); + expectNoLegacyPhrases(meta); + expect(screen.getByRole('link', { name: /8080/ })).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/components/EditorLayout/editor-view-blocks.tsx b/frontend/src/components/EditorLayout/editor-view-blocks.tsx index 09735b60..e43b5dbf 100644 --- a/frontend/src/components/EditorLayout/editor-view-blocks.tsx +++ b/frontend/src/components/EditorLayout/editor-view-blocks.tsx @@ -64,9 +64,7 @@ const healthcheckLabel = ( health?: 'healthy' | 'unhealthy' | 'starting' | 'none', ): string | null => { if (!health || health === 'none') return null; - if (health === 'healthy') return 'healthcheck passing'; - if (health === 'unhealthy') return 'healthcheck failing'; - return 'healthcheck starting'; + return health; }; type StackPill = {