From 00b98cf08e2aac8ca158ca7e4dedfe71809d2451 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Tue, 19 May 2026 15:33:08 +0100 Subject: [PATCH] Canonicalize runtime badge tones --- .../subsystems/frontend-primitives.md | 2 +- .../subsystems/performance-and-scalability.md | 5 ++ .../internal/subsystems/unified-resources.md | 5 ++ .../Infrastructure/resourceBadges.ts | 1 + .../src/components/Workloads/GuestRow.tsx | 13 ++--- .../Workloads/__tests__/GuestRow.test.tsx | 8 ++- .../components/Workloads/useGuestRowState.ts | 12 ++-- .../resourceBadgePresentation.test.ts | 17 ++++++ .../src/utils/resourceBadgePresentation.ts | 55 ++++++++++++++----- 9 files changed, 84 insertions(+), 34 deletions(-) diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index 8b932c1d7..b685a24fa 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -716,7 +716,7 @@ AI runtime. so client-side family grouping behaves identically against mock fixtures and live backends that leave `platformType` empty on a subset of canonical resource types. -8. Keep shared source/platform vocabulary on the governed manifest boundary. `frontend-modern/src/utils/platformSupportManifest.generated.ts` must be the tracked frontend projection of `docs/release-control/v6/internal/PLATFORM_SUPPORT_MANIFEST.json`, `frontend-modern/src/utils/platformSupportManifest.ts`, `frontend-modern/src/utils/sourcePlatforms.ts`, and `frontend-modern/src/utils/sourcePlatformOptions.ts` must consume that generated projection instead of embedding divergent future-label lists, setup/onboarding path allowlists, host-profile labels, or presentation-only guesses, and `frontend-modern/scripts/canonical-platform-audit.mjs` must fail when the generated projection drifts from the governed manifest. The generic `docker` source-platform label is "Docker / Podman" in shared selectors, badges, and filter options so v5 Docker users can find the runtime surface while Podman-backed rows are not mislabeled as Docker-only; "Container runtime" remains the governed platform family, not the primary customer-facing label. Agent host-profile entries, including Unraid, stay in the generated `agentHostProfiles` projection and shared wrapper helpers; frontend primitives may render those labels for Pulse Agent install/identity copy but must not add them to the first-class platform union. +8. Keep shared source/platform vocabulary on the governed manifest boundary. `frontend-modern/src/utils/platformSupportManifest.generated.ts` must be the tracked frontend projection of `docs/release-control/v6/internal/PLATFORM_SUPPORT_MANIFEST.json`, `frontend-modern/src/utils/platformSupportManifest.ts`, `frontend-modern/src/utils/sourcePlatforms.ts`, and `frontend-modern/src/utils/sourcePlatformOptions.ts` must consume that generated projection instead of embedding divergent future-label lists, setup/onboarding path allowlists, host-profile labels, or presentation-only guesses, and `frontend-modern/scripts/canonical-platform-audit.mjs` must fail when the generated projection drifts from the governed manifest. The generic `docker` source-platform label is "Docker / Podman" in shared selectors, badges, and filter options so v5 Docker users can find the runtime surface while Podman-backed rows are not mislabeled as Docker-only; "Container runtime" remains the governed platform family, not the primary customer-facing label. Identity colour is semantic, not page-local decoration: shared source/platform badges, host identity badges, and container runtime badges must use the shared presentation helpers so Docker remains on the Docker/Podman blue runtime tone, Podman uses its distinct runtime tone, Proxmox PVE remains orange, and those meanings do not drift across table rows, filters, drawers, or platform pages. Agent host-profile entries, including Unraid, stay in the generated `agentHostProfiles` projection and shared wrapper helpers; frontend primitives may render those labels for Pulse Agent install/identity copy but must not add them to the first-class platform union. The generated host-profile projection also carries runtime platform fallback metadata for shared explanation and parity with backend normalization, but frontend primitives must still render host-profile labels through diff --git a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md index 1173e7713..75b54b99f 100644 --- a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md +++ b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md @@ -258,6 +258,11 @@ regression protection. 8. Render workload row identity directly from the shared canonical workload helper so row selection, hover, and fallback metadata lookup stay aligned with the same workload contract 9. Format infrastructure sensor labels through the shared `frontend-modern/src/utils/textPresentation.ts` presentation helper instead of maintaining a local title-casing implementation in `frontend-modern/src/components/Infrastructure/resourceDetailMappers.ts` 10. Extend workload row contract and per-row hot-path derivations through `frontend-modern/src/components/Workloads/guestRowModel.tsx` and `frontend-modern/src/components/Workloads/useGuestRowState.ts`, and extend tooltip-backed row cell presentation through `frontend-modern/src/components/Workloads/GuestRowCells.tsx`, rather than rebuilding column metadata, row identity, cell tooltips, or anomaly correlation inside `frontend-modern/src/components/Workloads/GuestRow.tsx` + Workload runtime badges must consume the shared resource badge + presentation helper rather than carrying local neutral chip classes, so + Docker, Podman, and future container runtimes keep the same identity tones + across Workloads, Docker, and infrastructure surfaces without adding + per-row styling branches to the hot path. 11. Extend workload drawer derivations and runtime wiring through `frontend-modern/src/components/Workloads/guestDrawerModel.ts` and `frontend-modern/src/components/Workloads/useGuestDrawerState.ts`, and extend drawer overview rendering through `frontend-modern/src/components/Workloads/GuestDrawerOverview.tsx`, rather than rebuilding canonical guest identity, discovery routing, or drawer-local normalization inside `frontend-modern/src/components/Workloads/GuestDrawer.tsx` Drawer history charts belong to `frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx`. History cards must let the plot area stretch to the card height instead of diff --git a/docs/release-control/v6/internal/subsystems/unified-resources.md b/docs/release-control/v6/internal/subsystems/unified-resources.md index d842cdd9e..4b31b7766 100644 --- a/docs/release-control/v6/internal/subsystems/unified-resources.md +++ b/docs/release-control/v6/internal/subsystems/unified-resources.md @@ -969,6 +969,11 @@ platform or host-profile identity, such as PVE `ResourceProxmoxMeta.pveVersion` or `platformData.proxmox.pveVersion`, PBS `version`, or an agent OS report that resolves to Unraid or Proxmox VE. They must not attach a collector OS version to a different API-backed platform identity. +Container runtime badges follow that same shared identity boundary: +`frontend-modern/src/utils/resourceBadgePresentation.ts` owns the runtime badge +label and tone mapping for Docker, Podman, and unknown runtimes, and consumers +must render those badges from the shared helper instead of rebuilding local +runtime chips or page-specific colour classes. Agent-backed storage resources follow the same distinction: `StorageMeta.platform` may carry appliance presentation context such as `unraid` so the operator can see what system owns the array, but realtime `platformType` and source filters must diff --git a/frontend-modern/src/components/Infrastructure/resourceBadges.ts b/frontend-modern/src/components/Infrastructure/resourceBadges.ts index 32ebdc153..42dbc5a4f 100644 --- a/frontend-modern/src/components/Infrastructure/resourceBadges.ts +++ b/frontend-modern/src/components/Infrastructure/resourceBadges.ts @@ -1,6 +1,7 @@ export type { ResourceBadge } from '@/utils/resourceBadgePresentation'; export { getContainerRuntimeBadge, + getContainerRuntimeBadgeForRuntime, getInfrastructureSystemIdentityBadges, getInfrastructureSystemIdentitySortLabel, getInfrastructurePlatformBadges, diff --git a/frontend-modern/src/components/Workloads/GuestRow.tsx b/frontend-modern/src/components/Workloads/GuestRow.tsx index 430ff4514..7b9a0685b 100644 --- a/frontend-modern/src/components/Workloads/GuestRow.tsx +++ b/frontend-modern/src/components/Workloads/GuestRow.tsx @@ -42,7 +42,7 @@ import { getGuestColumnStyle } from './guestRowModel'; export function GuestRow(props: GuestRowProps) { const { agentVersion, - appContainerRuntimeLabel, + appContainerRuntimeBadge, clusterName, contextLabel, cpuAnomaly, @@ -238,19 +238,16 @@ export function GuestRow(props: GuestRowProps) {
diff --git a/frontend-modern/src/components/Workloads/__tests__/GuestRow.test.tsx b/frontend-modern/src/components/Workloads/__tests__/GuestRow.test.tsx index 491f7ed64..d44252fc3 100644 --- a/frontend-modern/src/components/Workloads/__tests__/GuestRow.test.tsx +++ b/frontend-modern/src/components/Workloads/__tests__/GuestRow.test.tsx @@ -638,7 +638,9 @@ describe('GuestRow', () => { }), visibleColumnIds: ['name', 'runtime', 'image'], }); - expect(screen.getByText('Docker')).toBeTruthy(); + const runtimeBadge = screen.getByText('Docker'); + expect(runtimeBadge).toBeTruthy(); + expect(runtimeBadge.className).toContain('bg-sky-100'); }); it('renders Podman runtime chip in the runtime column for Podman-managed app-containers', () => { @@ -651,7 +653,9 @@ describe('GuestRow', () => { }), visibleColumnIds: ['name', 'runtime', 'image'], }); - expect(screen.getByText('Podman')).toBeTruthy(); + const runtimeBadge = screen.getByText('Podman'); + expect(runtimeBadge).toBeTruthy(); + expect(runtimeBadge.className).toContain('bg-violet-100'); }); it('does not render a runtime chip when runtime and platform are both unknown', () => { diff --git a/frontend-modern/src/components/Workloads/useGuestRowState.ts b/frontend-modern/src/components/Workloads/useGuestRowState.ts index 94337a7e1..5fb7c7ee9 100644 --- a/frontend-modern/src/components/Workloads/useGuestRowState.ts +++ b/frontend-modern/src/components/Workloads/useGuestRowState.ts @@ -8,6 +8,7 @@ import type { DisplayMetricType } from '@/utils/metricThresholds'; import { buildMetricKey } from '@/utils/metricsKeys'; import { getGuestPowerIndicator, isGuestRunning } from '@/utils/status'; import { getShortImageName, formatBytes } from '@/utils/format'; +import { getContainerRuntimeBadgeForRuntime } from '@/utils/resourceBadgePresentation'; import { getCanonicalWorkloadId, getWorkloadMetricsKind, @@ -114,14 +115,9 @@ export function useGuestRowState(props: GuestRowProps) { return type === 'vm' || type === 'system-container'; }); - const appContainerRuntimeLabel = createMemo(() => { + const appContainerRuntimeBadge = createMemo(() => { if (workloadType() !== 'app-container') return null; - const runtime = (props.guest.containerRuntime || '').trim(); - const normalized = runtime.toLowerCase(); - if (normalized === 'docker') return 'Docker'; - if (normalized === 'podman') return 'Podman'; - if (runtime) return runtime; - return null; + return getContainerRuntimeBadgeForRuntime(props.guest.containerRuntime); }); const isOCIContainer = createMemo(() => { @@ -272,7 +268,7 @@ export function useGuestRowState(props: GuestRowProps) { ); return { - appContainerRuntimeLabel, + appContainerRuntimeBadge, cpuAnomaly, cpuThresholds, customUrl, diff --git a/frontend-modern/src/utils/__tests__/resourceBadgePresentation.test.ts b/frontend-modern/src/utils/__tests__/resourceBadgePresentation.test.ts index 24d30c4fc..13a313119 100644 --- a/frontend-modern/src/utils/__tests__/resourceBadgePresentation.test.ts +++ b/frontend-modern/src/utils/__tests__/resourceBadgePresentation.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest'; import { dedupeResourceBadges, + getContainerRuntimeBadgeForRuntime, getInfrastructurePlatformBadges, getInfrastructureSystemIdentityBadges, getInfrastructureSystemIdentitySortLabel, @@ -33,6 +34,22 @@ describe('resourceBadgePresentation', () => { expect(getPlatformBadge('availability')?.label).toBe('Availability'); }); + it('returns canonical runtime badge tones for Docker and Podman identities', () => { + const dockerBadge = getContainerRuntimeBadgeForRuntime('docker'); + const podmanBadge = getContainerRuntimeBadgeForRuntime('podman'); + + expect(dockerBadge).toMatchObject({ + label: 'Docker', + title: 'Runtime: Docker', + }); + expect(dockerBadge?.classes).toContain('bg-sky-100'); + expect(podmanBadge).toMatchObject({ + label: 'Podman', + title: 'Runtime: Podman', + }); + expect(podmanBadge?.classes).toContain('bg-violet-100'); + }); + it('returns source badges for infrastructure source types', () => { expect(getSourceBadge('agent')).toMatchObject({ label: 'Agent', title: 'agent' }); expect(getSourceBadge('hybrid')).toMatchObject({ label: 'Hybrid', title: 'hybrid' }); diff --git a/frontend-modern/src/utils/resourceBadgePresentation.ts b/frontend-modern/src/utils/resourceBadgePresentation.ts index c726a1b04..34cf56f4c 100644 --- a/frontend-modern/src/utils/resourceBadgePresentation.ts +++ b/frontend-modern/src/utils/resourceBadgePresentation.ts @@ -9,7 +9,11 @@ import { getAgentHostProfileManifestEntry, getSourcePlatformManifestEntry, } from '@/utils/platformSupportManifest'; -import { normalizeSourcePlatformKey, type KnownSourcePlatform } from '@/utils/sourcePlatforms'; +import { + getSourcePlatformPresentation, + normalizeSourcePlatformKey, + type KnownSourcePlatform, +} from '@/utils/sourcePlatforms'; import { getSourceTypePresentation } from '@/utils/sourceTypePresentation'; import { canonicalResourceTypeForDisplay, @@ -28,6 +32,9 @@ const baseBadge = const typeClasses = 'bg-surface-alt text-base-content'; const availabilityBadgeClasses = 'bg-sky-100 text-sky-700 dark:bg-sky-900 dark:text-sky-300'; +const dockerRuntimeBadgeClasses = 'bg-sky-100 text-sky-700 dark:bg-sky-900 dark:text-sky-400'; +const podmanRuntimeBadgeClasses = + 'bg-violet-100 text-violet-700 dark:bg-violet-900 dark:text-violet-300'; const PRIMARY_SYSTEM_SOURCE_PRIORITY: KnownSourcePlatform[] = [ 'proxmox-pve', @@ -581,9 +588,10 @@ const proxmoxLxcDockerBadge = (resource: Resource): ResourceBadge | null => { return { label: 'LXC', classes: `${baseBadge} ${getWorkloadTypePresentation('system-container').className}`, - title: vmid !== null - ? `Docker running inside Proxmox LXC ${vmid}` - : 'Docker running inside a Proxmox LXC', + title: + vmid !== null + ? `Docker running inside Proxmox LXC ${vmid}` + : 'Docker running inside a Proxmox LXC', }; }; @@ -689,6 +697,33 @@ export function getInfrastructureSystemTitleBadges( ]); } +const getContainerRuntimeLabel = (runtime?: string | null): string => { + const raw = (runtime || '').trim(); + const normalized = raw.toLowerCase(); + if (normalized === 'docker') return 'Docker'; + if (normalized === 'podman') return 'Podman'; + return raw; +}; + +const getContainerRuntimeTone = (label: string): string => { + const normalized = label.trim().toLowerCase(); + if (normalized === 'docker') { + return getSourcePlatformPresentation('docker')?.tone ?? dockerRuntimeBadgeClasses; + } + if (normalized === 'podman') return podmanRuntimeBadgeClasses; + return typeClasses; +}; + +export function getContainerRuntimeBadgeForRuntime(runtime?: string | null): ResourceBadge | null { + const label = getContainerRuntimeLabel(runtime); + if (!label) return null; + return { + label, + classes: `${baseBadge} ${getContainerRuntimeTone(label)}`, + title: `Runtime: ${label}`, + }; +} + export function getContainerRuntimeBadge( platformType?: PlatformType, platformData?: Record | null, @@ -696,15 +731,5 @@ export function getContainerRuntimeBadge( if (platformType !== 'docker' || !platformData) return null; const docker = (platformData as { docker?: { runtime?: string } } | undefined)?.docker; - const raw = (docker?.runtime || '').trim(); - if (!raw) return null; - - const normalized = raw.toLowerCase(); - const label = normalized === 'podman' ? 'Podman' : normalized === 'docker' ? 'Docker' : raw; - - return { - label, - classes: `${baseBadge} ${typeClasses}`, - title: `Runtime: ${label}`, - }; + return getContainerRuntimeBadgeForRuntime(docker?.runtime); }