From 7f95e40259e4ea6ec9cf537e7ffe02233ebbf2c6 Mon Sep 17 00:00:00 2001
From: "pulse-triage[bot]"
<249995291+pulse-triage[bot]@users.noreply.github.com>
Date: Mon, 5 Oct 2026 00:58:56 +0100
Subject: [PATCH 1/2] Keep fractional container CPU labels truthful
Distinguish small positive CPU and History percentages from actual zero, and explain the existing host-capacity scale in container tables and drawers. Preserve raw values, sorting, collection and alert thresholds; cover desktop and phone rendering with a parent-bound browser receipt and connected tests.
Change-source: pulse-maintainer
---
.../subsystems/frontend-primitives.md | 15 ++
.../internal/subsystems/unified-resources.md | 14 ++
.../1695cea4cd6443e2.json | 49 ++++++
.../Infrastructure/ResourceDetailDrawer.tsx | 9 ++
.../shared/__tests__/HistoryChart.test.tsx | 11 ++
.../ResponsiveMetricCell.cpu.test.tsx | 67 +++++++++
.../components/shared/historyChartModel.ts | 1 +
.../responsive/ResponsiveMetricCell.tsx | 9 +-
.../features/docker/DockerContainersTable.tsx | 9 ++
...rContainersTable.cpu-presentation.test.tsx | 142 ++++++++++++++++++
.../__tests__/DockerNativeTables.test.tsx | 6 +
.../features/docker/dockerCpuPresentation.ts | 2 +
.../src/utils/__tests__/format.test.ts | 23 +++
frontend-modern/src/utils/format.ts | 8 +
14 files changed, 362 insertions(+), 3 deletions(-)
create mode 100644 frontend-modern/browser-verification/1695cea4cd6443e2.json
create mode 100644 frontend-modern/src/components/shared/__tests__/ResponsiveMetricCell.cpu.test.tsx
create mode 100644 frontend-modern/src/features/docker/__tests__/DockerContainersTable.cpu-presentation.test.tsx
create mode 100644 frontend-modern/src/features/docker/dockerCpuPresentation.ts
diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md
index a4ad773973..80a6bd9f36 100644
--- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md
+++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md
@@ -20,6 +20,21 @@
## Purpose
+### Observed CPU labels preserve small positives
+
+Shared responsive CPU cells distinguish actual zero from small positive readings:
+positive values below 0.1% display `<0.1%`, other values below 10% retain one
+decimal without a trailing zero, and larger values keep whole-percent labels.
+Both compact text and animated bars use the same formatter. Explicit labels,
+non-CPU table formatting, unavailable/offline fallbacks, raw progress widths and
+threshold colouring retain their existing contracts.
+
+Shared percentage History legends, inspection and accessible descriptions never
+round positive values below 0.1% to zero. Actual zero remains `0.0%`; all other
+History units and precision stay unchanged. This changes displayed text only,
+not readings, coordinates, stored points, selection, requests or alert policy.
+Mounted responsive-cell and container row/drawer tests exercise these boundaries.
+
### Guest drawers consume selected memory provenance
Existing guest drawers consume optional server-owned `memory.observation` and
diff --git a/docs/release-control/v6/internal/subsystems/unified-resources.md b/docs/release-control/v6/internal/subsystems/unified-resources.md
index ae04260b9a..8353a0031c 100644
--- a/docs/release-control/v6/internal/subsystems/unified-resources.md
+++ b/docs/release-control/v6/internal/subsystems/unified-resources.md
@@ -23,6 +23,20 @@ and sort the complete canonical change table while startup and ingestion wait.
## Purpose
+### Container CPU presentation explains its existing scale
+
+The existing Docker/Podman container table and drawer visibly explain that CPU
+is a percentage of total host capacity: 100% means all host CPUs. Runtime stats
+may use a per-core scale; the client never guesses core count or renormalises
+readings to match a screenshot. The explanation remains visible in Overview and
+History, including narrow screens, without relying on hover-only help.
+
+Small positive CPU labels stay distinct from true zero in the real responsive
+row and stored History inspection. Numeric sorting, metric identity, requests,
+collector/ingest units, stored values and alert thresholds remain unchanged.
+Connected container row/drawer controls establish presentation, not accurate
+native collection, every-container continuity, alert delivery or reporter relief.
+
### Guest drawers consume selected memory provenance
Existing guest drawers consume optional server-owned `memory.observation` and
diff --git a/frontend-modern/browser-verification/1695cea4cd6443e2.json b/frontend-modern/browser-verification/1695cea4cd6443e2.json
new file mode 100644
index 0000000000..7c8aea1d23
--- /dev/null
+++ b/frontend-modern/browser-verification/1695cea4cd6443e2.json
@@ -0,0 +1,49 @@
+{
+ "version": 1,
+ "result": "passed",
+ "base_sha": "b01dad904c3436e70bc01588ea1546fffaa05dc6",
+ "changed_paths": [
+ "frontend-modern/src/components/Infrastructure/ResourceDetailDrawer.tsx",
+ "frontend-modern/src/components/shared/historyChartModel.ts",
+ "frontend-modern/src/components/shared/responsive/ResponsiveMetricCell.tsx",
+ "frontend-modern/src/features/docker/DockerContainersTable.tsx",
+ "frontend-modern/src/features/docker/dockerCpuPresentation.ts",
+ "frontend-modern/src/utils/format.ts"
+ ],
+ "content_sha256": {
+ "frontend-modern/src/components/Infrastructure/ResourceDetailDrawer.tsx": "53c9e59281392d26cbdbba2d1153d321bebe8b3a49ed06c7ba3b7a48504f3977",
+ "frontend-modern/src/components/shared/historyChartModel.ts": "3229e5c4d3aa1e8645a70a1a29c018e1b90361c2453b66336fc0d9321ceb7bd2",
+ "frontend-modern/src/components/shared/responsive/ResponsiveMetricCell.tsx": "4c3eea14a621354fa5ece0c05451db2426b37af272c984f829ba8833dfbabe96",
+ "frontend-modern/src/features/docker/DockerContainersTable.tsx": "2a9793922a95864e96950a76754bfb6acf8a7f152b4085d39d567f358d5bf26c",
+ "frontend-modern/src/features/docker/dockerCpuPresentation.ts": "d3eee94c693339e3318c1b70c6324a221d1c6d1162c4cda47ae10495eeef8393",
+ "frontend-modern/src/utils/format.ts": "0e012dae598fdc14f22bfed07af0e7e43d41568fe4ce7e1025f2a42e3c44d175"
+ },
+ "routes": [
+ "/docker: existing production container table and inline ResourceDetailDrawer; isolated offline fixture, synthetic API readings"
+ ],
+ "states": [
+ "Docker/Podman mixed runtime; true zero, tiny positive, other fractional CPU, unavailable and stopped readings",
+ "Overview and stored History; actual zero versus tiny positive and 9.4%; light/dark"
+ ],
+ "interactions": [
+ "Numeric CPU sorting, keyboard drawer open/collapse, History tab selection",
+ "Native Home/ArrowRight and pointer/touch stored-point inspection without extra requests",
+ "Positive-to-zero live replacement; raw progress widths, visible capacity units and narrow label fit",
+ "All 18 desktop and phone screenshots visually inspected"
+ ],
+ "viewports": [
+ {
+ "width": 1280,
+ "height": 900
+ },
+ {
+ "width": 390,
+ "height": 844
+ },
+ {
+ "width": 320,
+ "height": 740
+ }
+ ],
+ "verified_at": "2026-10-04T23:56:44.161Z"
+}
diff --git a/frontend-modern/src/components/Infrastructure/ResourceDetailDrawer.tsx b/frontend-modern/src/components/Infrastructure/ResourceDetailDrawer.tsx
index fafa64a341..4175655c0e 100644
--- a/frontend-modern/src/components/Infrastructure/ResourceDetailDrawer.tsx
+++ b/frontend-modern/src/components/Infrastructure/ResourceDetailDrawer.tsx
@@ -30,6 +30,7 @@ import { isDockerContainerLifecycleResource } from '@/features/docker/dockerCont
import { ResourceOperatorStateSection } from './ResourceOperatorStateSection';
import { MaintenanceVerificationSection } from './MaintenanceVerificationSection';
import { ResourceActionHistory } from './ResourceActionHistory';
+import { CONTAINER_CPU_CAPACITY_DESCRIPTION } from '@/features/docker/dockerCpuPresentation';
interface ResourceDetailDrawerProps {
resource: Resource;
@@ -150,6 +151,14 @@ const DrawerContent: Component = (props) => {
)}
+
+
+ {CONTAINER_CPU_CAPACITY_DESCRIPTION}
+
+
+
{
+ expect(formatHistoryChartTooltipValue(0, '%')).toBe('0.0%');
+ expect(formatHistoryChartTooltipValue(Number.MIN_VALUE, '%')).toBe('<0.1%');
+ expect(formatHistoryChartTooltipValue(0.026, '%')).toBe('<0.1%');
+ expect(formatHistoryChartTooltipValue(0.09999, '%')).toBe('<0.1%');
+ expect(formatHistoryChartTooltipValue(0.1, '%')).toBe('0.1%');
+ expect(formatHistoryChartTooltipValue(9.4, '%')).toBe('9.4%');
+ expect(formatHistoryChartTooltipValue(0.026, 'C')).toBe('0°C');
+});
+
function touchPointer(canvas: Element, type: string, fields: Partial = {}) {
const event = Object.assign(new Event(type, { bubbles: true, cancelable: true }), {
pointerId: 1,
diff --git a/frontend-modern/src/components/shared/__tests__/ResponsiveMetricCell.cpu.test.tsx b/frontend-modern/src/components/shared/__tests__/ResponsiveMetricCell.cpu.test.tsx
new file mode 100644
index 0000000000..c625dd6edf
--- /dev/null
+++ b/frontend-modern/src/components/shared/__tests__/ResponsiveMetricCell.cpu.test.tsx
@@ -0,0 +1,67 @@
+import { cleanup, render, screen, waitFor } from '@solidjs/testing-library';
+import { createSignal } from 'solid-js';
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+import { ResponsiveMetricCell } from '../responsive/ResponsiveMetricCell';
+
+beforeEach(() => {
+ vi.stubGlobal(
+ 'ResizeObserver',
+ class {
+ observe() {}
+ disconnect() {}
+ },
+ );
+});
+
+afterEach(() => {
+ cleanup();
+ vi.unstubAllGlobals();
+});
+
+describe('ResponsiveMetricCell observed CPU labels', () => {
+ it.each([false, true])('keeps fractions in desktop/mobile mode %s', (showMobile) => {
+ const { container } = render(() => (
+
+ ));
+ const numbers = container.querySelectorAll('[data-animated-number]');
+ expect(numbers.length).toBeGreaterThan(0);
+ for (const label of numbers) {
+ expect(label).toHaveTextContent('<0.1%');
+ expect(label).toHaveAttribute('aria-label', '<0.1%');
+ }
+ expect(container.querySelector('[data-progress-fill]')).toHaveAttribute('width', '0.026');
+ });
+
+ it('updates the formatted target while retaining raw width and severity', async () => {
+ const [cpu, setCpu] = createSignal(0);
+ const { container } = render(() => (
+
+ ));
+ expect(screen.getByText('0%')).toBeInTheDocument();
+ setCpu(0.17);
+ await waitFor(() => expect(screen.getByLabelText('0.2%')).toHaveTextContent('0.2%'));
+ const fill = container.querySelector('[data-progress-fill]');
+ expect(fill).toHaveAttribute('width', '0.17');
+ expect(fill?.firstElementChild?.className).not.toMatch(/orange|red/);
+ setCpu(0);
+ await waitFor(() => expect(screen.getByLabelText('0%')).toHaveTextContent('0%'));
+ });
+
+ it('preserves custom labels, non-CPU formatting and offline fallback', () => {
+ const { container } = render(() => (
+ <>
+
+
+
+ >
+ ));
+ expect(screen.getByText('Unavailable')).toBeInTheDocument();
+ expect(screen.getByLabelText('0%')).toBeInTheDocument();
+ expect(screen.getByText('—')).toBeInTheDocument();
+ expect(container.querySelectorAll('[data-animated-number]')).toHaveLength(1);
+ });
+});
diff --git a/frontend-modern/src/components/shared/historyChartModel.ts b/frontend-modern/src/components/shared/historyChartModel.ts
index bd7436171d..fbf3e3f578 100644
--- a/frontend-modern/src/components/shared/historyChartModel.ts
+++ b/frontend-modern/src/components/shared/historyChartModel.ts
@@ -66,6 +66,7 @@ export interface HistoryChartAccessibleDescriptionInput {
}
export function formatHistoryChartTooltipValue(value: number, unit?: string): string {
+ if (unit === '%' && value > 0 && value < 0.1) return '<0.1%';
if (unit === '%') return `${value.toFixed(1)}%`;
if (unit === 'B/s') return `${formatBytes(value)}/s`;
if (unit === 'C') return `${Math.round(value)}°C`;
diff --git a/frontend-modern/src/components/shared/responsive/ResponsiveMetricCell.tsx b/frontend-modern/src/components/shared/responsive/ResponsiveMetricCell.tsx
index 6002dbd55c..0834407c68 100644
--- a/frontend-modern/src/components/shared/responsive/ResponsiveMetricCell.tsx
+++ b/frontend-modern/src/components/shared/responsive/ResponsiveMetricCell.tsx
@@ -2,7 +2,7 @@ import { Component, Show, createMemo, JSX } from 'solid-js';
import { AnimatedNumber } from '@/components/shared/AnimatedNumber';
import { MetricBar } from '@/components/Workloads/MetricBar';
import { useBreakpoint } from '@/hooks/useBreakpoint';
-import { formatPercent } from '@/utils/format';
+import { formatCpuPercent, formatPercent } from '@/utils/format';
import { getMetricSeverity } from '@/utils/metricThresholds';
import type { MetricDisplayThresholds, MetricSeverity } from '@/utils/metricThresholds';
@@ -98,7 +98,9 @@ function compactCapacityLabel(sublabel?: string): string | undefined {
*/
export const ResponsiveMetricCell: Component = (props) => {
const { isAtLeast, isBelow } = useBreakpoint();
- const displayLabel = createMemo(() => props.label ?? formatPercent(props.value));
+ const formatValue = (value: number) =>
+ props.type === 'cpu' ? formatCpuPercent(value) : formatPercent(value);
+ const displayLabel = createMemo(() => props.label ?? formatValue(props.value));
const colorClass = createMemo(() => metricTextClass(props.value, props.type, props.thresholds));
const isRunning = () => props.isRunning !== false; // Default to true if not specified
@@ -133,7 +135,7 @@ export const ResponsiveMetricCell: Component = (props
class={`md:hidden text-xs text-center ${colorClass()} whitespace-nowrap overflow-hidden text-ellipsis`}
>
-
+
@@ -144,6 +146,7 @@ export const ResponsiveMetricCell: Component = (props
value={props.value}
label={displayLabel()}
animatedLabelValue={props.label ? undefined : props.value}
+ animatedLabelFormat={formatValue}
sublabel={resolvedSublabel()}
showLabel={showLabel()}
type={props.type}
diff --git a/frontend-modern/src/features/docker/DockerContainersTable.tsx b/frontend-modern/src/features/docker/DockerContainersTable.tsx
index 85e9c3c0bd..4109b5784f 100644
--- a/frontend-modern/src/features/docker/DockerContainersTable.tsx
+++ b/frontend-modern/src/features/docker/DockerContainersTable.tsx
@@ -80,6 +80,7 @@ import {
import type { DockerContainerUpdateStatus } from '@/types/api';
import type { Resource } from '@/types/resource';
import { DockerContainerLifecycleControls } from './DockerContainerLifecycleControls';
+import { CONTAINER_CPU_CAPACITY_DESCRIPTION } from './dockerCpuPresentation';
type DockerNetwork = NonNullable['networks']>[number];
type DockerMount = NonNullable['mounts']>[number];
@@ -783,6 +784,14 @@ export const DockerContainersTable: Component = (pro
>
+ {CONTAINER_CPU_CAPACITY_DESCRIPTION}
+
+ }
tableClass={`${getDockerContainerTableMinWidthClass()} table-fixed text-xs`}
colgroup={
diff --git a/frontend-modern/src/features/docker/__tests__/DockerContainersTable.cpu-presentation.test.tsx b/frontend-modern/src/features/docker/__tests__/DockerContainersTable.cpu-presentation.test.tsx
new file mode 100644
index 0000000000..40df70eff9
--- /dev/null
+++ b/frontend-modern/src/features/docker/__tests__/DockerContainersTable.cpu-presentation.test.tsx
@@ -0,0 +1,142 @@
+import { cleanup, fireEvent, render, screen, waitFor } from '@solidjs/testing-library';
+import { Route, Router } from '@solidjs/router';
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
+import { ChartsAPI } from '@/api/charts';
+import { resetCreateNonSuspendingQueryCacheForTest } from '@/hooks/createNonSuspendingQuery';
+import type { Resource } from '@/types/resource';
+import { DockerContainersTable } from '../DockerContainersTable';
+import { CONTAINER_CPU_CAPACITY_DESCRIPTION } from '../dockerCpuPresentation';
+
+vi.mock('@/stores/license', async (importOriginal) => ({
+ ...(await importOriginal()),
+ loadRuntimeCapabilities: vi.fn(async () => undefined),
+ maxHistoryDays: () => 7,
+ isRangeLocked: () => false,
+}));
+
+const containerResource = (id: string, cpu: number | undefined, running = true): Resource => ({
+ id,
+ name: id,
+ platformId: 'docker',
+ platformType: 'docker',
+ sourceType: 'agent',
+ sources: ['docker'],
+ type: 'app-container',
+ status: running ? 'running' : 'stopped',
+ lastSeen: 1_700_000_000_000,
+ cpu: cpu === undefined ? undefined : { current: cpu },
+ memory: { current: 23.9 },
+ metricsTarget: { resourceType: 'app-container', resourceId: id },
+ docker: {
+ hostname: 'lab-host',
+ containerId: id,
+ containerState: running ? 'running' : 'exited',
+ runtime: 'podman',
+ },
+});
+
+beforeEach(() => {
+ vi.stubGlobal(
+ 'ResizeObserver',
+ class {
+ observe() {}
+ disconnect() {}
+ },
+ );
+});
+
+afterEach(() => {
+ cleanup();
+ window.history.pushState({}, '', '/');
+ window.localStorage.clear();
+ resetCreateNonSuspendingQueryCacheForTest();
+ vi.restoreAllMocks();
+ vi.unstubAllGlobals();
+});
+
+describe('Docker container CPU presentation through the real row and drawer', () => {
+ it('separates zero, fractional, unavailable and stopped readings without changing sort values', async () => {
+ const { container } = render(() => (
+
+ (
+ }
+ emptyTitle="No containers"
+ emptyDescription="No containers"
+ showToolbar={false}
+ />
+ )}
+ />
+
+ ));
+ const heads = screen.getAllByRole('columnheader');
+ const cpuIndex = heads.findIndex((head) => head.textContent?.trim() === 'CPU');
+ expect(cpuIndex).toBeGreaterThan(0);
+ const cell = (id: string) =>
+ container.querySelector(`[data-docker-container-row="${id}"]`)?.children[cpuIndex];
+ for (const [id, label] of [
+ ['zero', '0%'],
+ ['tiny', '<0.1%'],
+ ['small', '0.2%'],
+ ['rounded', '0.5%'],
+ ['unavailable', '—'],
+ ['stopped', '—'],
+ ]) {
+ expect(cell(id)?.textContent?.trim()).toBe(label);
+ }
+ expect(screen.getByTestId('container-table-cpu-scale')).toHaveTextContent(
+ CONTAINER_CPU_CAPACITY_DESCRIPTION,
+ );
+ fireEvent.click(heads[cpuIndex]);
+ const order = [...container.querySelectorAll('[data-docker-container-row]')].map((row) =>
+ row.getAttribute('data-docker-container-row'),
+ );
+ // The stopped container still has its raw reading. Presentation never
+ // edits that datum, so the existing numeric sort continues to see it.
+ expect(order.slice(0, 5)).toEqual(['stopped', 'rounded', 'small', 'tiny', 'zero']);
+ expect(order[5]).toBe('unavailable');
+
+ const time = 1_700_000_000_000;
+ const fetch = vi.spyOn(ChartsAPI, 'getMetricsHistory').mockResolvedValue({
+ resourceType: 'app-container',
+ resourceId: 'tiny',
+ range: '24h',
+ start: time,
+ end: time + 120_000,
+ source: 'store',
+ metrics: {
+ cpu: [0, 0.026, 9.4].map((value, index) => ({
+ timestamp: time + index * 60_000,
+ value,
+ min: value,
+ max: value,
+ })),
+ },
+ });
+ fireEvent.click(screen.getByRole('button', { name: 'Expand details for tiny' }));
+ expect(screen.getByTestId('container-drawer-cpu-scale')).toHaveTextContent(
+ CONTAINER_CPU_CAPACITY_DESCRIPTION,
+ );
+ fireEvent.click(screen.getByRole('tab', { name: 'History' }));
+ const slider = await screen.findByRole('slider', { name: 'Inspect Utilization history' });
+ await waitFor(() => expect(slider).toHaveAttribute('max', '2'));
+ fireEvent.input(slider, { target: { value: '1' } });
+ expect(slider).toHaveAttribute('aria-valuetext', expect.stringContaining('CPU <0.1%'));
+ fireEvent.input(slider, { target: { value: '0' } });
+ expect(slider).toHaveAttribute('aria-valuetext', expect.stringContaining('CPU 0.0%'));
+ expect(fetch).toHaveBeenCalledTimes(1);
+ expect(fetch).toHaveBeenCalledWith(
+ expect.objectContaining({ resourceType: 'app-container', resourceId: 'tiny' }),
+ );
+ });
+});
diff --git a/frontend-modern/src/features/docker/__tests__/DockerNativeTables.test.tsx b/frontend-modern/src/features/docker/__tests__/DockerNativeTables.test.tsx
index 15bb7087d2..5dcf308a46 100644
--- a/frontend-modern/src/features/docker/__tests__/DockerNativeTables.test.tsx
+++ b/frontend-modern/src/features/docker/__tests__/DockerNativeTables.test.tsx
@@ -275,6 +275,12 @@ describe('Docker native tables', () => {
// Both rows get a CPU cell; only the row with memory data gets a bar.
expect(screen.getAllByTestId('responsive-cpu-metric').length).toBe(2);
+ expect(screen.getByTestId('container-table-cpu-scale')).toHaveTextContent(
+ 'CPU is a percentage of total host capacity: 100% means all host CPUs.',
+ );
+ expect(screen.getByTestId('container-table-cpu-scale')).toHaveTextContent(
+ 'Docker and Podman stats may use a per-core scale.',
+ );
expect(screen.getByTestId('stacked-memory-bar')).toHaveAttribute(
'data-used',
String(512 * 1024 * 1024),
diff --git a/frontend-modern/src/features/docker/dockerCpuPresentation.ts b/frontend-modern/src/features/docker/dockerCpuPresentation.ts
new file mode 100644
index 0000000000..cd2bf0b453
--- /dev/null
+++ b/frontend-modern/src/features/docker/dockerCpuPresentation.ts
@@ -0,0 +1,2 @@
+export const CONTAINER_CPU_CAPACITY_DESCRIPTION =
+ 'CPU is a percentage of total host capacity: 100% means all host CPUs. Docker and Podman stats may use a per-core scale.';
diff --git a/frontend-modern/src/utils/__tests__/format.test.ts b/frontend-modern/src/utils/__tests__/format.test.ts
index 30522ac44b..417c38b56e 100644
--- a/frontend-modern/src/utils/__tests__/format.test.ts
+++ b/frontend-modern/src/utils/__tests__/format.test.ts
@@ -7,6 +7,7 @@ import {
formatSpeed,
formatObservedSpeed,
formatPercent,
+ formatCpuPercent,
formatNumber,
formatUptime,
formatAbsoluteTime,
@@ -68,6 +69,28 @@ describe('formatSpeed', () => {
});
});
+describe('formatCpuPercent', () => {
+ it.each([
+ [0, '0%'],
+ [Number.MIN_VALUE, '<0.1%'],
+ [0.026, '<0.1%'],
+ [0.09999, '<0.1%'],
+ [0.1, '0.1%'],
+ [0.17, '0.2%'],
+ [0.49, '0.5%'],
+ [1.04, '1%'],
+ [3.04, '3%'],
+ [9.4, '9.4%'],
+ [10, '10%'],
+ [50.5, '51%'],
+ [100, '100%'],
+ [NaN, '—'],
+ [Infinity, '—'],
+ ])('formats %s as %s without inventing a zero', (value, label) => {
+ expect(formatCpuPercent(value)).toBe(label);
+ });
+});
+
describe('formatPercent', () => {
it('formats percentages correctly', () => {
expect(formatPercent(0)).toBe('0%');
diff --git a/frontend-modern/src/utils/format.ts b/frontend-modern/src/utils/format.ts
index 49c35bbe2f..c8d180b13e 100644
--- a/frontend-modern/src/utils/format.ts
+++ b/frontend-modern/src/utils/format.ts
@@ -85,6 +85,14 @@ export function formatPercent(value: number): string {
return `${Math.round(value)}%`;
}
+/** Keep small observed CPU readings distinct from an actual zero. */
+export function formatCpuPercent(value: number): string {
+ if (!Number.isFinite(value)) return '—';
+ if (value > 0 && value < 0.1) return '<0.1%';
+ if (value > 0 && value < 10) return `${value.toFixed(1).replace(/\.0$/, '')}%`;
+ return formatPercent(value);
+}
+
export function formatNumber(value: number): string {
if (!Number.isFinite(value)) return '0';
return value.toLocaleString();
From 902bba393f39c164230ea9843ee888139aa1e789 Mon Sep 17 00:00:00 2001
From: "pulse-triage[bot]"
<249995291+pulse-triage[bot]@users.noreply.github.com>
Date: Mon, 5 Oct 2026 01:14:25 +0100
Subject: [PATCH 2/2] Complete the fractional CPU test resource identity
Supply the required displayName in the new connected row/drawer fixture after exact-source TypeScript validation caught the omission. This changes only test data; production content and its parent-bound browser receipt remain unchanged.
Change-source: pulse-maintainer
---
.../__tests__/DockerContainersTable.cpu-presentation.test.tsx | 1 +
1 file changed, 1 insertion(+)
diff --git a/frontend-modern/src/features/docker/__tests__/DockerContainersTable.cpu-presentation.test.tsx b/frontend-modern/src/features/docker/__tests__/DockerContainersTable.cpu-presentation.test.tsx
index 40df70eff9..3c3f31f1b5 100644
--- a/frontend-modern/src/features/docker/__tests__/DockerContainersTable.cpu-presentation.test.tsx
+++ b/frontend-modern/src/features/docker/__tests__/DockerContainersTable.cpu-presentation.test.tsx
@@ -17,6 +17,7 @@ vi.mock('@/stores/license', async (importOriginal) => ({
const containerResource = (id: string, cpu: number | undefined, running = true): Resource => ({
id,
name: id,
+ displayName: id,
platformId: 'docker',
platformType: 'docker',
sourceType: 'agent',