diff --git a/frontend-modern/src/App.tsx b/frontend-modern/src/App.tsx
index 71862d4..15ae3e8 100644
--- a/frontend-modern/src/App.tsx
+++ b/frontend-modern/src/App.tsx
@@ -41,6 +41,7 @@ import BoxesIcon from 'lucide-solid/icons/boxes';
import MonitorIcon from 'lucide-solid/icons/monitor';
import BellIcon from 'lucide-solid/icons/bell';
import SettingsIcon from 'lucide-solid/icons/settings';
+import NetworkIcon from 'lucide-solid/icons/network';
import { TokenRevealDialog } from './components/TokenRevealDialog';
import { useAlertsActivation } from './stores/alertsActivation';
import { UpdateProgressModal } from './components/UpdateProgressModal';
@@ -65,6 +66,11 @@ const SettingsPage = lazy(() => import('./components/Settings/Settings'));
const DockerHosts = lazy(() =>
import('./components/Docker/DockerHosts').then((module) => ({ default: module.DockerHosts })),
);
+const KubernetesClusters = lazy(() =>
+ import('./components/Kubernetes/KubernetesClusters').then((module) => ({
+ default: module.KubernetesClusters,
+ })),
+);
const HostsOverview = lazy(() =>
import('./components/Hosts/HostsOverview').then((module) => ({
default: module.HostsOverview,
@@ -112,6 +118,14 @@ function HostsRoute() {
return
+ Cluster health from the unified agent Kubernetes module. +
+| Cluster | +Status | +Nodes | +Pods | +Deployments | +Version | +Last Seen | +No clusters match the current filters. | + }> + {(cluster) => { + const indicator = () => getStatusIndicator(cluster.status); + const nodes = () => summarizeNodes(cluster.nodes); + const pods = () => summarizePods(cluster.pods); + const deployments = () => summarizeDeployments(cluster.deployments); + + return ( +
|---|---|---|---|---|---|---|
|
+
+ {getClusterDisplayName(cluster)}
+
+
+ {cluster.server || '—'}
+
+ |
+
+
+
+ |
+
+ 0 ? 'text-amber-600 dark:text-amber-400' : ''}>{nodes().total - nodes().notReady}
+ /{nodes().total}
+ |
+
+ 0 ? 'text-amber-600 dark:text-amber-400' : ''}>{pods().total - pods().unhealthy}
+ /{pods().total}
+ |
+
+ 0 ? 'text-amber-600 dark:text-amber-400' : ''}>{deployments().total - deployments().unhealthy}
+ /{deployments().total}
+ |
+ + {cluster.version || '—'} + | ++ {cluster.lastSeen ? formatRelativeTime(cluster.lastSeen) : '—'} + | +
| Node | +Cluster | +Status | +Roles | +CPU | +Memory | +Pods | +Version | +No nodes match the current filters. | + }> + {({ cluster, node }) => { + const isHealthy = () => node.ready && !node.unschedulable; + const roles = () => (node.roles ?? []).join(', ') || 'worker'; + + return ( +
|---|---|---|---|---|---|---|---|
|
+ {node.name}
+
+ {node.osImage || '—'}
+
+ |
+ + {getClusterDisplayName(cluster)} + | +
+ |
+ + + {roles()} + + | ++ {node.allocatableCpuCores ?? node.capacityCpuCores ?? '—'} cores + | ++ {node.allocatableMemoryBytes ? formatBytes(node.allocatableMemoryBytes) : node.capacityMemoryBytes ? formatBytes(node.capacityMemoryBytes) : '—'} + | ++ {node.allocatablePods ?? node.capacityPods ?? '—'} + | ++ {node.kubeletVersion || '—'} + | +
| Pod | +Namespace | +Cluster | +Status | +Ready | +Restarts | +Image | +Age | +No pods match the current filters. | + }> + {({ cluster, pod }) => { + const statusBadge = () => getPodStatusBadge(pod); + const containers = () => pod.containers ?? []; + const readyContainers = () => containers().filter(c => c.ready).length; + + return ( +
|---|---|---|---|---|---|---|---|
|
+ {pod.name}
+
+ {pod.nodeName || 'unscheduled'}
+
+ |
+ + + {pod.namespace} + + | ++ {getClusterDisplayName(cluster)} + | ++ + {statusBadge().label} + + | ++ + {readyContainers()}/{containers().length} + + | +
+ |
+ + + {getPrimaryImage(pod)} + + | ++ {formatAge(pod.createdAt)} + | +
| Deployment | +Namespace | +Cluster | +Status | +Replicas | +Ready | +Up-to-date | +No deployments match the current filters. | + }> + {({ cluster, deployment }) => { + const healthy = () => isDeploymentHealthy(deployment); + const desired = () => deployment.desiredReplicas ?? 0; + const ready = () => deployment.readyReplicas ?? 0; + const updated = () => deployment.updatedReplicas ?? 0; + + return ( +
|---|---|---|---|---|---|---|
|
+ {deployment.name}
+ |
+ + + {deployment.namespace} + + | ++ {getClusterDisplayName(cluster)} + | +
+ |
+ + {desired()} + | ++ = desired() ? 'text-green-600 dark:text-green-400' : 'text-amber-600 dark:text-amber-400'}> + {ready()}/{desired()} + + | ++ = desired() ? 'text-green-600 dark:text-green-400' : 'text-amber-600 dark:text-amber-400'}> + {updated()}/{desired()} + + | +