diff --git a/frontend-modern/src/features/kubernetes/KubernetesAutoscalingTable.tsx b/frontend-modern/src/features/kubernetes/KubernetesAutoscalingTable.tsx
index ddc6a0691b..91256b45aa 100644
--- a/frontend-modern/src/features/kubernetes/KubernetesAutoscalingTable.tsx
+++ b/frontend-modern/src/features/kubernetes/KubernetesAutoscalingTable.tsx
@@ -32,6 +32,7 @@ import {
import type { Resource } from '@/types/resource';
import {
filterKubernetesResources,
+ kubernetesScopeLabel,
type KubernetesResourceStatusFilter,
} from './kubernetesPageModel';
@@ -40,15 +41,6 @@ const textValue = (value: string | undefined): string => asTrimmedString(value)
const autoscalerName = (resource: Resource): string =>
asTrimmedString(resource.displayName) || asTrimmedString(resource.name) || resource.id;
-const autoscalerScope = (resource: Resource): string => {
- const cluster =
- asTrimmedString(resource.kubernetes?.clusterId) ||
- asTrimmedString(resource.kubernetes?.clusterName);
- const namespace = asTrimmedString(resource.kubernetes?.namespace);
- if (namespace) return cluster ? `${cluster}/${namespace}` : namespace;
- return cluster || 'Cluster';
-};
-
const targetRef = (resource: Resource): string =>
textValue(
[resource.kubernetes?.targetKind, resource.kubernetes?.targetName].filter(Boolean).join('/'),
@@ -184,7 +176,7 @@ export const KubernetesAutoscalingTable: Component<{
{(resource) => {
const indicator = () => getSimpleStatusIndicator(resource.status);
const name = () => autoscalerName(resource);
- const scope = () => autoscalerScope(resource);
+ const scope = () => kubernetesScopeLabel(resource);
const target = () => targetRef(resource);
const metrics = () => metricSources(resource);
const labels = () => labelSummary(resource);
diff --git a/frontend-modern/src/features/kubernetes/KubernetesConfigTable.tsx b/frontend-modern/src/features/kubernetes/KubernetesConfigTable.tsx
index c14ecfde57..c928319b5b 100644
--- a/frontend-modern/src/features/kubernetes/KubernetesConfigTable.tsx
+++ b/frontend-modern/src/features/kubernetes/KubernetesConfigTable.tsx
@@ -32,6 +32,7 @@ import {
import type { Resource } from '@/types/resource';
import {
filterKubernetesResources,
+ kubernetesScopeLabel,
type KubernetesResourceStatusFilter,
} from './kubernetesPageModel';
@@ -56,15 +57,6 @@ const configKind = (resource: Resource): string => {
return resource.kubernetes?.resourceKind || resource.type;
};
-const configScope = (resource: Resource): string => {
- const cluster =
- asTrimmedString(resource.kubernetes?.clusterId) ||
- asTrimmedString(resource.kubernetes?.clusterName);
- const namespace = asTrimmedString(resource.kubernetes?.namespace);
- if (namespace) return cluster ? `${cluster}/${namespace}` : namespace;
- return cluster || 'Cluster';
-};
-
const plural = (count: number, singular: string, pluralLabel = `${singular}s`): string =>
`${count} ${count === 1 ? singular : pluralLabel}`;
@@ -284,7 +276,7 @@ export const KubernetesConfigTable: Component<{
const indicator = () => getSimpleStatusIndicator(resource.status);
const name = () => configName(resource);
const kind = () => configKind(resource);
- const scope = () => configScope(resource);
+ const scope = () => kubernetesScopeLabel(resource);
const state = () => lifecycleOrTrust(resource);
const data = () => dataShape(resource);
const refs = () => serviceAccountRefs(resource);
diff --git a/frontend-modern/src/features/kubernetes/KubernetesControllersTable.tsx b/frontend-modern/src/features/kubernetes/KubernetesControllersTable.tsx
index 05f89acd89..1cf52d4f22 100644
--- a/frontend-modern/src/features/kubernetes/KubernetesControllersTable.tsx
+++ b/frontend-modern/src/features/kubernetes/KubernetesControllersTable.tsx
@@ -33,6 +33,7 @@ import type { Resource } from '@/types/resource';
import {
compareKubernetesControllers,
filterKubernetesResources,
+ kubernetesScopeLabel,
mapKubernetesControllerStatus,
type KubernetesResourceStatusFilter,
} from './kubernetesPageModel';
@@ -48,15 +49,6 @@ const controllerName = (resource: Resource): string =>
const controllerKind = (resource: Resource): string =>
resource.kubernetes?.resourceKind || getResourceTypeLabel(resource.type) || resource.type;
-const controllerScope = (resource: Resource): string => {
- const cluster =
- asTrimmedString(resource.kubernetes?.clusterId) ||
- asTrimmedString(resource.kubernetes?.clusterName);
- const namespace = asTrimmedString(resource.kubernetes?.namespace);
- if (namespace) return cluster ? `${cluster}/${namespace}` : namespace;
- return cluster || 'Cluster';
-};
-
const targetValue = (resource: Resource): string => {
switch (resource.type) {
case 'k8s-replicaset':
@@ -278,7 +270,7 @@ export const KubernetesControllersTable: Component<{
const indicator = () => mapKubernetesControllerStatus(resource);
const name = () => controllerName(resource);
const kind = () => controllerKind(resource);
- const scope = () => controllerScope(resource);
+ const scope = () => kubernetesScopeLabel(resource);
const target = () => targetValue(resource);
const exceptions = () => exceptionSummary(resource);
const detail = () => apiDetail(resource);
diff --git a/frontend-modern/src/features/kubernetes/KubernetesDeploymentsTable.tsx b/frontend-modern/src/features/kubernetes/KubernetesDeploymentsTable.tsx
index 4c99b8c666..151b3d7420 100644
--- a/frontend-modern/src/features/kubernetes/KubernetesDeploymentsTable.tsx
+++ b/frontend-modern/src/features/kubernetes/KubernetesDeploymentsTable.tsx
@@ -33,6 +33,7 @@ import type { Resource } from '@/types/resource';
import {
compareKubernetesDeployments,
filterKubernetesResources,
+ kubernetesClusterLabel,
mapKubernetesDeploymentStatus,
type KubernetesResourceStatusFilter,
} from './kubernetesPageModel';
@@ -43,8 +44,9 @@ import {
// render as dashes. This deployment-native table reuses canonical shared
// primitives (Card, Table, SearchInput, FilterButtonGroup, StatusDot) but
// surfaces deployment-meaningful columns only: namespace, cluster,
-// desired / updated / ready / available replicas, observed generation, and
-// metadata age.
+// desired / updated / ready / available replicas, and metadata age.
+// observedGeneration is deliberately not a column: without the spec
+// generation beside it the raw number is unactionable.
const replicaCount = (value: number | undefined): JSX.Element => (
{value ?? 0}
@@ -121,19 +123,19 @@ export const KubernetesDeploymentsTable: Component<{
Desktop widths: Deployment, Namespace, and Cluster take
the biggest shares because their content can be long.
The integer-count columns (Desired / Updated / Ready /
- Available / Observed) trim to what their headers plus
- 1-2 digit values need. Mobile widths are unchanged.
+ Available) trim to what their headers plus 1-2 digit
+ values need. Mobile widths are unchanged.
*/}
-
+
Deployment
Namespace
Cluster
@@ -157,11 +159,6 @@ export const KubernetesDeploymentsTable: Component<{
>
Available
-
- Observed
-
@@ -174,10 +171,7 @@ export const KubernetesDeploymentsTable: Component<{
{(deployment) => {
const name = () => asTrimmedString(deployment.name) || deployment.id;
const ns = () => asTrimmedString(deployment.kubernetes?.namespace) || '—';
- const cluster = () =>
- asTrimmedString(deployment.kubernetes?.clusterName) ||
- asTrimmedString(deployment.kubernetes?.clusterId) ||
- '—';
+ const cluster = () => kubernetesClusterLabel(deployment) || '—';
const indicator = () => mapKubernetesDeploymentStatus(deployment);
const age = () => formatAge(deployment.kubernetes?.createdAt);
const detailRowId = () => drawer.detailRowId(deployment);
@@ -236,11 +230,6 @@ export const KubernetesDeploymentsTable: Component<{
>
{replicaCount(deployment.kubernetes?.availableReplicas)}
-
- {replicaCount(deployment.kubernetes?.observedGeneration)}
-
@@ -253,7 +242,7 @@ export const KubernetesDeploymentsTable: Component<{
resource={deployment}
open={isExpanded()}
detailRowId={detailRowId()}
- colSpan={9}
+ colSpan={8}
resolveResourceLabel={resolveResourceLabel}
onClose={() => drawer.close(deployment)}
/>
diff --git a/frontend-modern/src/features/kubernetes/KubernetesEventsTable.tsx b/frontend-modern/src/features/kubernetes/KubernetesEventsTable.tsx
index eaa6f62c1f..50845d9806 100644
--- a/frontend-modern/src/features/kubernetes/KubernetesEventsTable.tsx
+++ b/frontend-modern/src/features/kubernetes/KubernetesEventsTable.tsx
@@ -11,6 +11,7 @@ import {
TableRow,
} from '@/components/shared/Table';
import { asTrimmedString } from '@/utils/stringUtils';
+import { formatRelativeTime } from '@/utils/format';
import {
PLATFORM_HEALTH_FILTER_OPTIONS,
PLATFORM_TABLE_BODY_CLASS,
@@ -32,6 +33,7 @@ import type { Resource } from '@/types/resource';
import {
compareKubernetesEvents,
filterKubernetesResources,
+ kubernetesScopeLabel,
mapKubernetesEventSeverity,
type KubernetesResourceStatusFilter,
} from './kubernetesPageModel';
@@ -41,15 +43,6 @@ const textValue = (value: string | undefined): string => asTrimmedString(value)
const eventName = (resource: Resource): string =>
asTrimmedString(resource.displayName) || asTrimmedString(resource.name) || resource.id;
-const eventScope = (resource: Resource): string => {
- const cluster =
- asTrimmedString(resource.kubernetes?.clusterId) ||
- asTrimmedString(resource.kubernetes?.clusterName);
- const namespace = asTrimmedString(resource.kubernetes?.namespace);
- if (namespace) return cluster ? `${cluster}/${namespace}` : namespace;
- return cluster || 'Cluster';
-};
-
const involvedObject = (resource: Resource): string =>
textValue(
[resource.kubernetes?.involvedKind, resource.kubernetes?.involvedName]
@@ -57,12 +50,16 @@ const involvedObject = (resource: Resource): string =>
.join('/'),
);
-const observedTime = (resource: Resource): string =>
- textValue(
+const observedTimestamp = (resource: Resource): string =>
+ asTrimmedString(
resource.kubernetes?.eventTime ||
resource.kubernetes?.firstSeen ||
resource.kubernetes?.createdAt,
- );
+ ) || '';
+
+// Humanized for the cell ("2h ago"); the raw timestamp stays on the title.
+const observedTime = (resource: Resource): string =>
+ formatRelativeTime(observedTimestamp(resource), { compact: true, emptyText: '—' }) || '—';
const numberValue = (value: number | undefined): JSX.Element =>
typeof value === 'number' ? {value} : —;
@@ -163,7 +160,7 @@ export const KubernetesEventsTable: Component<{
const indicator = () =>
mapKubernetesEventSeverity(resource.kubernetes?.eventType);
const name = () => eventName(resource);
- const scope = () => eventScope(resource);
+ const scope = () => kubernetesScopeLabel(resource);
const observed = () => observedTime(resource);
const message = () => textValue(resource.kubernetes?.message);
const detailRowId = () => drawer.detailRowId(resource);
@@ -233,7 +230,10 @@ export const KubernetesEventsTable: Component<{
-
+
{observed()}
diff --git a/frontend-modern/src/features/kubernetes/KubernetesNetworkingTable.tsx b/frontend-modern/src/features/kubernetes/KubernetesNetworkingTable.tsx
index 5a2a5a6a3e..532aadcaa3 100644
--- a/frontend-modern/src/features/kubernetes/KubernetesNetworkingTable.tsx
+++ b/frontend-modern/src/features/kubernetes/KubernetesNetworkingTable.tsx
@@ -32,6 +32,7 @@ import {
import type { Resource } from '@/types/resource';
import {
filterKubernetesResources,
+ kubernetesScopeLabel,
type KubernetesResourceStatusFilter,
} from './kubernetesPageModel';
@@ -46,15 +47,6 @@ const networkKind = (resource: Resource): string => {
return resource.kubernetes?.resourceKind || resource.type;
};
-const scopeLabel = (resource: Resource): string => {
- const cluster =
- asTrimmedString(resource.kubernetes?.clusterId) ||
- asTrimmedString(resource.kubernetes?.clusterName);
- const namespace = asTrimmedString(resource.kubernetes?.namespace);
- if (namespace) return cluster ? `${cluster}/${namespace}` : namespace;
- return cluster || 'Cluster';
-};
-
const summarizeValues = (
values: readonly (string | undefined)[] | undefined,
visible = 2,
@@ -226,7 +218,7 @@ export const KubernetesNetworkingTable: Component<{
{(resource) => {
const indicator = () => getSimpleStatusIndicator(resource.status);
const name = () => resourceName(resource);
- const scope = () => scopeLabel(resource);
+ const scope = () => kubernetesScopeLabel(resource);
const address = () => addressOrHosts(resource);
const ports = () => portLabel(resource);
const targets = () => targetSummary(resource);
diff --git a/frontend-modern/src/features/kubernetes/KubernetesNodesTable.tsx b/frontend-modern/src/features/kubernetes/KubernetesNodesTable.tsx
index 3778488cff..2c767278bb 100644
--- a/frontend-modern/src/features/kubernetes/KubernetesNodesTable.tsx
+++ b/frontend-modern/src/features/kubernetes/KubernetesNodesTable.tsx
@@ -35,6 +35,7 @@ import type { Resource } from '@/types/resource';
import {
compareKubernetesNodes,
filterKubernetesResources,
+ kubernetesClusterLabel,
mapKubernetesNodeStatus,
type KubernetesResourceStatusFilter,
} from './kubernetesPageModel';
@@ -203,10 +204,7 @@ export const KubernetesNodesTable: Component<{
{(node) => {
const meta = () => node.kubernetes;
const name = () => asTrimmedString(node.name) || node.id;
- const cluster = () =>
- asTrimmedString(meta()?.clusterName) ||
- asTrimmedString(meta()?.clusterId) ||
- '—';
+ const cluster = () => kubernetesClusterLabel(node) || '—';
const kubelet = () => asTrimmedString(meta()?.kubeletVersion) || '—';
const runtime = () => asTrimmedString(meta()?.containerRuntimeVersion) || '—';
const capacityLabel = () => {
diff --git a/frontend-modern/src/features/kubernetes/KubernetesPodsTable.tsx b/frontend-modern/src/features/kubernetes/KubernetesPodsTable.tsx
index 213196196d..e804b247a5 100644
--- a/frontend-modern/src/features/kubernetes/KubernetesPodsTable.tsx
+++ b/frontend-modern/src/features/kubernetes/KubernetesPodsTable.tsx
@@ -32,6 +32,7 @@ import type { Resource, ResourceKubernetesPodContainerStatus } from '@/types/res
import {
compareKubernetesPods,
filterKubernetesResources,
+ kubernetesScopeLabel,
mapKubernetesPodStatus,
type KubernetesResourceStatusFilter,
} from './kubernetesPageModel';
@@ -44,15 +45,6 @@ const podName = (resource: Resource): string =>
asTrimmedString(resource.name) ||
resource.id;
-const podScope = (resource: Resource): string => {
- const cluster =
- asTrimmedString(resource.kubernetes?.clusterId) ||
- asTrimmedString(resource.kubernetes?.clusterName);
- const namespace = asTrimmedString(resource.kubernetes?.namespace);
- if (namespace) return cluster ? `${cluster}/${namespace}` : namespace;
- return cluster || 'Cluster';
-};
-
const podContainers = (resource: Resource): ResourceKubernetesPodContainerStatus[] =>
resource.kubernetes?.podContainers ?? [];
@@ -202,7 +194,7 @@ export const KubernetesPodsTable: Component<{
{(resource) => {
const indicator = () => mapKubernetesPodStatus(resource);
const name = () => podName(resource);
- const scope = () => podScope(resource);
+ const scope = () => kubernetesScopeLabel(resource);
const node = () => textValue(resource.kubernetes?.nodeName);
const owner = () => ownerValue(resource);
const image = () => imageValue(resource);
diff --git a/frontend-modern/src/features/kubernetes/KubernetesPolicyTable.tsx b/frontend-modern/src/features/kubernetes/KubernetesPolicyTable.tsx
index 7be5ae93a6..17f6223148 100644
--- a/frontend-modern/src/features/kubernetes/KubernetesPolicyTable.tsx
+++ b/frontend-modern/src/features/kubernetes/KubernetesPolicyTable.tsx
@@ -32,6 +32,7 @@ import {
import type { Resource } from '@/types/resource';
import {
filterKubernetesResources,
+ kubernetesScopeLabel,
type KubernetesResourceStatusFilter,
} from './kubernetesPageModel';
@@ -52,15 +53,6 @@ const policyKind = (resource: Resource): string => {
return resource.kubernetes?.resourceKind || resource.type;
};
-const policyScope = (resource: Resource): string => {
- const cluster =
- asTrimmedString(resource.kubernetes?.clusterId) ||
- asTrimmedString(resource.kubernetes?.clusterName);
- const namespace = asTrimmedString(resource.kubernetes?.namespace);
- if (namespace) return cluster ? `${cluster}/${namespace}` : namespace;
- return cluster || 'Cluster';
-};
-
const summarizeValues = (
values: readonly (string | undefined)[] | undefined,
visible = 2,
@@ -307,7 +299,7 @@ export const KubernetesPolicyTable: Component<{
{(resource) => {
const indicator = () => getSimpleStatusIndicator(resource.status);
const name = () => policyName(resource);
- const scope = () => policyScope(resource);
+ const scope = () => kubernetesScopeLabel(resource);
const shape = () => policyShape(resource);
const spec = () => policySpec(resource);
const state = () => policyState(resource);
diff --git a/frontend-modern/src/features/kubernetes/KubernetesServicesTable.tsx b/frontend-modern/src/features/kubernetes/KubernetesServicesTable.tsx
index a3be7cf582..cb658e9ebc 100644
--- a/frontend-modern/src/features/kubernetes/KubernetesServicesTable.tsx
+++ b/frontend-modern/src/features/kubernetes/KubernetesServicesTable.tsx
@@ -32,6 +32,7 @@ import {
import type { Resource } from '@/types/resource';
import {
filterKubernetesResources,
+ kubernetesScopeLabel,
type KubernetesResourceStatusFilter,
} from './kubernetesPageModel';
@@ -44,15 +45,6 @@ const textValue = (value: string | undefined): string => asTrimmedString(value)
const serviceName = (resource: Resource): string =>
asTrimmedString(resource.displayName) || asTrimmedString(resource.name) || resource.id;
-const serviceScope = (resource: Resource): string => {
- const cluster =
- asTrimmedString(resource.kubernetes?.clusterId) ||
- asTrimmedString(resource.kubernetes?.clusterName);
- const namespace = asTrimmedString(resource.kubernetes?.namespace);
- if (namespace) return cluster ? `${cluster}/${namespace}` : namespace;
- return cluster || 'Cluster';
-};
-
const summarizeValues = (
values: readonly (string | undefined)[] | undefined,
visible = 2,
@@ -185,7 +177,7 @@ export const KubernetesServicesTable: Component<{
{(resource) => {
const indicator = () => getSimpleStatusIndicator(resource.status);
const name = () => serviceName(resource);
- const scope = () => serviceScope(resource);
+ const scope = () => kubernetesScopeLabel(resource);
const serviceType = () => textValue(resource.kubernetes?.serviceType);
const clusterIp = () => textValue(resource.kubernetes?.clusterIp);
const externalIps = () => externalIpSummary(resource);
diff --git a/frontend-modern/src/features/kubernetes/KubernetesStorageTable.tsx b/frontend-modern/src/features/kubernetes/KubernetesStorageTable.tsx
index 411127608f..5d920a62ab 100644
--- a/frontend-modern/src/features/kubernetes/KubernetesStorageTable.tsx
+++ b/frontend-modern/src/features/kubernetes/KubernetesStorageTable.tsx
@@ -33,6 +33,7 @@ import {
import type { Resource } from '@/types/resource';
import {
filterKubernetesResources,
+ kubernetesScopeLabel,
type KubernetesResourceStatusFilter,
} from './kubernetesPageModel';
@@ -62,17 +63,6 @@ const storageKind = (resource: Resource): string => {
return resource.kubernetes?.resourceKind || resource.type;
};
-const storageScope = (resource: Resource): string => {
- const clusterScope =
- asTrimmedString(resource.kubernetes?.clusterId) ||
- asTrimmedString(resource.kubernetes?.clusterName);
- const namespace = asTrimmedString(resource.kubernetes?.namespace);
- if (namespace) {
- return clusterScope ? `${clusterScope}/${namespace}` : namespace;
- }
- return clusterScope || 'Cluster';
-};
-
const bindingOrPhase = (resource: Resource): string => {
if (resource.type === 'k8s-storage-class') {
return textValue(resource.kubernetes?.volumeBindingMode || 'Immediate');
@@ -259,7 +249,7 @@ export const KubernetesStorageTable: Component<{
const indicator = () => getSimpleStatusIndicator(resource.status);
const name = () => storageName(resource);
const kind = () => storageKind(resource);
- const scope = () => storageScope(resource);
+ const scope = () => kubernetesScopeLabel(resource);
const policy = () => policyLabel(resource);
const target = () => bindingTarget(resource);
const detailRowId = () => drawer.detailRowId(resource);
diff --git a/frontend-modern/src/features/kubernetes/__tests__/KubernetesDeploymentsTable.test.tsx b/frontend-modern/src/features/kubernetes/__tests__/KubernetesDeploymentsTable.test.tsx
index 11fc2b9857..ee844d536b 100644
--- a/frontend-modern/src/features/kubernetes/__tests__/KubernetesDeploymentsTable.test.tsx
+++ b/frontend-modern/src/features/kubernetes/__tests__/KubernetesDeploymentsTable.test.tsx
@@ -66,7 +66,9 @@ describe('KubernetesDeploymentsTable', () => {
expect(screen.getByText('Updated')).toBeInTheDocument();
expect(screen.getByText('Ready')).toBeInTheDocument();
expect(screen.getByText('Available')).toBeInTheDocument();
- expect(screen.getByText('Observed')).toBeInTheDocument();
+ // observedGeneration is deliberately not a column: the raw number is
+ // unactionable without the spec generation beside it.
+ expect(screen.queryByText('Observed')).toBeNull();
expect(screen.getByText('Age')).toBeInTheDocument();
expect(screen.getByText('checkout-api')).toBeInTheDocument();
@@ -75,7 +77,7 @@ describe('KubernetesDeploymentsTable', () => {
expect(screen.getByText('4')).toBeInTheDocument();
expect(screen.getByText('3')).toBeInTheDocument();
expect(screen.getAllByText('2')).toHaveLength(2);
- expect(screen.getByText('12')).toBeInTheDocument();
+ expect(screen.queryByText('12')).toBeNull();
expect(screen.getByText('2h ago')).toBeInTheDocument();
expect(
document.querySelector('[data-kubernetes-deployment-row="checkout-api"]'),
diff --git a/frontend-modern/src/features/kubernetes/__tests__/KubernetesEventsTable.test.tsx b/frontend-modern/src/features/kubernetes/__tests__/KubernetesEventsTable.test.tsx
index 42feb49e78..accb322e0a 100644
--- a/frontend-modern/src/features/kubernetes/__tests__/KubernetesEventsTable.test.tsx
+++ b/frontend-modern/src/features/kubernetes/__tests__/KubernetesEventsTable.test.tsx
@@ -1,5 +1,5 @@
import { cleanup, render, screen } from '@solidjs/testing-library';
-import { afterEach, describe, expect, it } from 'vitest';
+import { afterEach, describe, expect, it, vi } from 'vitest';
import type { Resource } from '@/types/resource';
import { KubernetesEventsTable } from '../KubernetesEventsTable';
@@ -24,10 +24,14 @@ const makeResource = ({
afterEach(() => {
cleanup();
+ vi.useRealTimers();
});
describe('KubernetesEventsTable', () => {
it('renders Kubernetes Event fields with severity, newest-first ordering, and message context', () => {
+ vi.useFakeTimers();
+ vi.setSystemTime(new Date('2026-05-24T15:00:00Z'));
+
render(() => (
{
expect(screen.getByText('FailedScheduling')).toBeInTheDocument();
expect(screen.getByText('Pod/checkout-api-123')).toBeInTheDocument();
expect(screen.getByText('3')).toBeInTheDocument();
- expect(screen.getByText('2026-05-24T13:00:00Z')).toBeInTheDocument();
+ // The cell humanizes the observed time; the raw timestamp stays on the
+ // hover title.
+ expect(screen.getByText('2h ago')).toBeInTheDocument();
+ expect(screen.getByTitle('2026-05-24T13:00:00Z')).toBeInTheDocument();
expect(screen.getByText('0/3 nodes are available')).toBeInTheDocument();
expect(
diff --git a/frontend-modern/src/features/kubernetes/__tests__/kubernetesPageModel.test.ts b/frontend-modern/src/features/kubernetes/__tests__/kubernetesPageModel.test.ts
index ddced6bb87..811c887294 100644
--- a/frontend-modern/src/features/kubernetes/__tests__/kubernetesPageModel.test.ts
+++ b/frontend-modern/src/features/kubernetes/__tests__/kubernetesPageModel.test.ts
@@ -13,6 +13,8 @@ import {
compareKubernetesNodes,
compareKubernetesPods,
getKubernetesPageTabSpecs,
+ kubernetesClusterLabel,
+ kubernetesScopeLabel,
mapKubernetesControllerStatus,
mapKubernetesCronJobStatus,
mapKubernetesDaemonSetStatus,
@@ -807,6 +809,42 @@ describe('kubernetesPageModel', () => {
});
});
+ describe('kubernetesScopeLabel', () => {
+ it('prefers the human cluster name over the internal cluster id', () => {
+ expect(
+ kubernetesScopeLabel(
+ makeResource({
+ id: 'pod-1',
+ type: 'pod',
+ kubernetes: {
+ clusterId: 'k8s-production-1',
+ clusterName: 'Production EU',
+ namespace: 'services',
+ },
+ }),
+ ),
+ ).toBe('Production EU/services');
+ expect(
+ kubernetesClusterLabel(
+ makeResource({
+ id: 'pod-2',
+ type: 'pod',
+ kubernetes: { clusterId: 'k8s-production-1' },
+ }),
+ ),
+ ).toBe('k8s-production-1');
+ });
+
+ it('falls back through namespace-only and empty scopes', () => {
+ expect(
+ kubernetesScopeLabel(
+ makeResource({ id: 'ns-only', type: 'pod', kubernetes: { namespace: 'apps' } }),
+ ),
+ ).toBe('apps');
+ expect(kubernetesScopeLabel(makeResource({ id: 'bare', type: 'pod' }))).toBe('Cluster');
+ });
+ });
+
describe('mapKubernetesIncidentSeverity', () => {
it('buckets severity strings into critical / warning / info', () => {
expect(mapKubernetesIncidentSeverity('critical')).toBe('critical');
diff --git a/frontend-modern/src/features/kubernetes/kubernetesPageModel.ts b/frontend-modern/src/features/kubernetes/kubernetesPageModel.ts
index c76f27dbf2..a28e578b22 100644
--- a/frontend-modern/src/features/kubernetes/kubernetesPageModel.ts
+++ b/frontend-modern/src/features/kubernetes/kubernetesPageModel.ts
@@ -94,6 +94,21 @@ const displayName = (resource: Resource): string =>
asTrimmedString(resource.kubernetes?.podName) ||
resource.id;
+// Scope labels prefer the human-facing cluster name; the internal cluster id
+// is only a fallback for rows whose adapter carries no name. Every Kubernetes
+// page table shares this so the same cluster never reads as "Production EU"
+// in one column and "k8s-production-1" in another.
+export const kubernetesClusterLabel = (resource: Resource): string =>
+ asTrimmedString(resource.kubernetes?.clusterName) ||
+ asTrimmedString(resource.kubernetes?.clusterId);
+
+export const kubernetesScopeLabel = (resource: Resource): string => {
+ const cluster = kubernetesClusterLabel(resource);
+ const namespace = asTrimmedString(resource.kubernetes?.namespace);
+ if (namespace) return cluster ? `${cluster}/${namespace}` : namespace;
+ return cluster || 'Cluster';
+};
+
const containerHasFatalReason = (container: ResourceKubernetesPodContainerStatus): boolean => {
const reason = normalizeKubernetesToken(container.reason);
if (reason && POD_CONTAINER_FATAL_REASONS.has(reason)) return true;