diff --git a/frontend-modern/src/features/kubernetes/KubernetesPodsTable.tsx b/frontend-modern/src/features/kubernetes/KubernetesPodsTable.tsx
index 9b6776772b..213196196d 100644
--- a/frontend-modern/src/features/kubernetes/KubernetesPodsTable.tsx
+++ b/frontend-modern/src/features/kubernetes/KubernetesPodsTable.tsx
@@ -53,9 +53,6 @@ const podScope = (resource: Resource): string => {
return cluster || 'Cluster';
};
-const podPhase = (resource: Resource): string =>
- textValue(resource.kubernetes?.podPhase || resource.kubernetes?.phase);
-
const podContainers = (resource: Resource): ResourceKubernetesPodContainerStatus[] =>
resource.kubernetes?.podContainers ?? [];
@@ -171,7 +168,7 @@ export const KubernetesPodsTable: Component<{
Node
- Phase
+ Status
podName(resource);
const scope = () => podScope(resource);
const node = () => textValue(resource.kubernetes?.nodeName);
- const phase = () => podPhase(resource);
const owner = () => ownerValue(resource);
const image = () => imageValue(resource);
const age = () => ageValue(resource);
@@ -255,7 +251,12 @@ export const KubernetesPodsTable: Component<{
- {phase()}
+ {/* The mapped label carries the container failure reason
+ (CrashLoopBackOff, ImagePullBackOff, ...) where the raw
+ phase would still read "Running". */}
+
+ {indicator().label}
+
{
expect(screen.getByText('Pod')).toBeInTheDocument();
expect(screen.getByText('Scope')).toBeInTheDocument();
expect(screen.getByText('Node')).toBeInTheDocument();
- expect(screen.getByText('Phase')).toBeInTheDocument();
+ expect(screen.getByText('Status')).toBeInTheDocument();
expect(screen.getByText('Ready')).toBeInTheDocument();
expect(screen.getByText('Restarts')).toBeInTheDocument();
expect(screen.getByText('Owner')).toBeInTheDocument();
@@ -83,7 +83,9 @@ describe('KubernetesPodsTable', () => {
expect(screen.getByText('checkout-api-6c746d5bcf-c7z2p')).toBeInTheDocument();
expect(screen.getByText('prod-euw1/services')).toBeInTheDocument();
expect(screen.getByText('prod-euw1-k8s-02')).toBeInTheDocument();
- expect(screen.getByText('Running')).toBeInTheDocument();
+ // Raw phase is Running, but the metrics-sidecar container is not ready;
+ // the status column shows the mapped label, not the phase.
+ expect(screen.getByText('Not ready')).toBeInTheDocument();
expect(screen.getByText('1/2')).toBeInTheDocument();
expect(screen.getByText('3')).toBeInTheDocument();
expect(screen.getByText('Deployment/checkout-api')).toBeInTheDocument();
@@ -136,8 +138,17 @@ describe('KubernetesPodsTable', () => {
(row) => row.getAttribute('data-kubernetes-pod-row'),
);
expect(rows).toEqual(['crashing-pod', 'not-ready-pod', 'happy-pod']);
- expect(screen.getByTitle('CrashLoopBackOff')).toHaveClass('bg-red-500');
- expect(screen.getByTitle('Not ready')).toHaveClass('bg-amber-500');
- expect(screen.getByTitle('Running')).toHaveClass('bg-emerald-500');
+ // The failure reason is visible cell text, not just a dot tooltip.
+ expect(screen.getByText('CrashLoopBackOff')).toBeInTheDocument();
+ expect(screen.getByText('Not ready')).toBeInTheDocument();
+ expect(
+ screen.getAllByTitle('CrashLoopBackOff').some((el) => el.classList.contains('bg-red-500')),
+ ).toBe(true);
+ expect(
+ screen.getAllByTitle('Not ready').some((el) => el.classList.contains('bg-amber-500')),
+ ).toBe(true);
+ expect(
+ screen.getAllByTitle('Running').some((el) => el.classList.contains('bg-emerald-500')),
+ ).toBe(true);
});
});
diff --git a/frontend-modern/src/features/kubernetes/__tests__/kubernetesPageModel.test.ts b/frontend-modern/src/features/kubernetes/__tests__/kubernetesPageModel.test.ts
index dc508f675d..ddced6bb87 100644
--- a/frontend-modern/src/features/kubernetes/__tests__/kubernetesPageModel.test.ts
+++ b/frontend-modern/src/features/kubernetes/__tests__/kubernetesPageModel.test.ts
@@ -365,6 +365,32 @@ describe('kubernetesPageModel', () => {
).toEqual({ variant: 'success', label: 'Ready' });
});
+ it('returns warning for a cordoned node even though it reports ready', () => {
+ expect(
+ mapKubernetesNodeStatus(
+ makeResource({
+ id: 'cordoned',
+ type: 'k8s-node',
+ status: 'warning',
+ kubernetes: { ready: true, unschedulable: true },
+ }),
+ ),
+ ).toEqual({ variant: 'warning', label: 'Unschedulable' });
+ });
+
+ it('keeps NotReady as the dominant state when a cordoned node is also down', () => {
+ expect(
+ mapKubernetesNodeStatus(
+ makeResource({
+ id: 'cordoned-down',
+ type: 'k8s-node',
+ status: 'offline',
+ kubernetes: { ready: false, unschedulable: true },
+ }),
+ ),
+ ).toEqual({ variant: 'danger', label: 'NotReady' });
+ });
+
it('falls back to resource.status when ready is undefined', () => {
expect(
mapKubernetesNodeStatus(makeResource({ id: 'fallback-ok', type: 'k8s-node', status: 'online' })),
@@ -758,6 +784,27 @@ describe('kubernetesPageModel', () => {
]);
expect(filterKubernetesResources(rows, 'v1.30', 'online')).toEqual([]);
});
+
+ it('buckets the backend `warning` status into the degraded filter', () => {
+ // The Kubernetes adapter emits `warning` (never `degraded`) for
+ // unhealthy-but-alive rows; the Degraded chip must match them.
+ const warningRows: Resource[] = [
+ makeResource({
+ id: 'pod-crashloop',
+ type: 'pod',
+ status: 'warning',
+ kubernetes: { namespace: 'payments', podName: 'checkout-api-abc' },
+ }),
+ makeResource({ id: 'pod-healthy', type: 'pod', status: 'online' }),
+ ];
+ expect(filterKubernetesResources(warningRows, '', 'degraded').map((r) => r.id)).toEqual([
+ 'pod-crashloop',
+ ]);
+ expect(filterKubernetesResources(warningRows, '', 'online').map((r) => r.id)).toEqual([
+ 'pod-healthy',
+ ]);
+ expect(filterKubernetesResources(warningRows, '', 'offline')).toEqual([]);
+ });
});
describe('mapKubernetesIncidentSeverity', () => {
diff --git a/frontend-modern/src/features/kubernetes/kubernetesPageModel.ts b/frontend-modern/src/features/kubernetes/kubernetesPageModel.ts
index e259521912..c76f27dbf2 100644
--- a/frontend-modern/src/features/kubernetes/kubernetesPageModel.ts
+++ b/frontend-modern/src/features/kubernetes/kubernetesPageModel.ts
@@ -132,6 +132,11 @@ export function mapKubernetesPodStatus(resource: Resource): StatusIndicator {
export function mapKubernetesNodeStatus(resource: Resource): StatusIndicator {
const ready = resource.kubernetes?.ready;
if (ready === false) return { variant: 'danger', label: 'NotReady' };
+ // A cordoned node still reports ready=true; check unschedulable before
+ // declaring the row healthy or the cordon is invisible on the page.
+ if (resource.kubernetes?.unschedulable === true) {
+ return { variant: 'warning', label: 'Unschedulable' };
+ }
if (ready === true) return { variant: 'success', label: 'Ready' };
const status = normalizeKubernetesToken(resource.status);
@@ -438,7 +443,11 @@ export function filterKubernetesIncidents(
export type KubernetesResourceStatusFilter = 'all' | 'online' | 'degraded' | 'offline';
const ONLINE_RESOURCE_STATUSES = new Set(['online', 'running']);
-const DEGRADED_RESOURCE_STATUSES = new Set(['degraded', 'paused']);
+// The canonical Kubernetes adapter reports every unhealthy-but-alive state
+// (cordoned node, not-ready container, pending pod, partially available
+// controller) as `warning`, so the degraded bucket must accept it or those
+// rows match none of the Healthy / Degraded / Offline filter chips.
+const DEGRADED_RESOURCE_STATUSES = new Set(['degraded', 'paused', 'warning']);
const OFFLINE_RESOURCE_STATUSES = new Set(['offline', 'stopped']);
const mapResourceStatusToTriad = (
diff --git a/frontend-modern/src/hooks/__tests__/useResources.test.ts b/frontend-modern/src/hooks/__tests__/useResources.test.ts
index c91386007e..bbf84ea332 100644
--- a/frontend-modern/src/hooks/__tests__/useResources.test.ts
+++ b/frontend-modern/src/hooks/__tests__/useResources.test.ts
@@ -135,6 +135,7 @@ describe('useResources - Resource Filtering Logic', () => {
running: 0,
stopped: 0,
degraded: 0,
+ warning: 0,
paused: 0,
unknown: 0,
};
diff --git a/frontend-modern/src/hooks/useResources.ts b/frontend-modern/src/hooks/useResources.ts
index 922a3875af..6e3bfb7219 100644
--- a/frontend-modern/src/hooks/useResources.ts
+++ b/frontend-modern/src/hooks/useResources.ts
@@ -143,6 +143,7 @@ export function useResources(storeOverride?: ResourceStoreLike): UseResourcesRet
running: 0,
stopped: 0,
degraded: 0,
+ warning: 0,
paused: 0,
unknown: 0,
};
diff --git a/frontend-modern/src/types/resource.ts b/frontend-modern/src/types/resource.ts
index c340d2e799..bcc091082d 100644
--- a/frontend-modern/src/types/resource.ts
+++ b/frontend-modern/src/types/resource.ts
@@ -95,13 +95,16 @@ export type SourceType =
| 'agent' // Data pushed from agent
| 'hybrid'; // Both sources, agent preferred
-// Resource status - operational state
+// Resource status - operational state. `warning` is what the canonical
+// Kubernetes adapter emits for unhealthy-but-alive rows (cordoned nodes,
+// not-ready containers, pending pods, partially available controllers).
export type ResourceStatus =
| 'online'
| 'offline'
| 'running'
| 'stopped'
| 'degraded'
+ | 'warning'
| 'paused'
| 'unknown';
@@ -1108,6 +1111,7 @@ export interface ResourceKubernetesMeta {
kernelVersion?: string;
roles?: string[];
ready?: boolean;
+ unschedulable?: boolean;
capacityCpuCores?: number;
allocatableCpuCores?: number;
capacityMemoryBytes?: number;