"use client"; import { useEffect, useMemo, useState } from "react"; import { useI18n } from "../../components/I18nProvider"; type HetznerServer = { id: number; name: string; status: string; type?: string; datacenter?: string; publicIp?: string; privateIp?: string; created?: string; }; type MonitorResponse = { hetzner?: { ok: boolean; error?: string; missingToken?: boolean; servers?: HetznerServer[]; }; k8s?: { ok: boolean; error?: string; nodes?: { name: string; ready: boolean; status: string; roles: string[]; internalIp?: string; kubeletVersion?: string; osImage?: string; lastReadyTransition?: string | null; }[]; pods?: { name: string; namespace: string; phase: string; reason?: string | null; message?: string | null; readyCount: number; totalContainers: number; restarts: number; nodeName?: string; hostIP?: string | null; podIP?: string | null; startedAt?: string | null; containers: { name: string; ready: boolean; restartCount: number; state: string; lastState?: string | null; }[]; }[]; }; db?: { ok: boolean; error?: string; serverTime?: string; recovery?: boolean; databaseSizeBytes?: number; connections?: { state: string; count: number }[]; }; }; const REFRESH_MS = 30000; function formatBytes(bytes?: number) { if (!bytes || Number.isNaN(bytes)) return "—"; if (bytes < 1024) return `${bytes} B`; const kb = bytes / 1024; if (kb < 1024) return `${kb.toFixed(1)} KB`; const mb = kb / 1024; if (mb < 1024) return `${mb.toFixed(1)} MB`; const gb = mb / 1024; return `${gb.toFixed(2)} GB`; } function formatDurationFrom(dateStr?: string | null) { if (!dateStr) return "—"; const started = new Date(dateStr).getTime(); const diff = Date.now() - started; if (Number.isNaN(diff) || diff < 0) return "—"; const mins = Math.floor(diff / 60000); if (mins < 1) return "<1m"; if (mins < 60) return `${mins}m`; const hours = Math.floor(mins / 60); if (hours < 24) return `${hours}h`; const days = Math.floor(hours / 24); return `${days}d`; } function statusPill(label: string, ok: boolean) { return ( {label} ); } export default function MonitorPage() { const { t } = useI18n(); const [data, setData] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(null); const [lastUpdated, setLastUpdated] = useState(null); const hasData = useMemo( () => Boolean(data?.hetzner || data?.k8s || data?.db), [data], ); async function load() { setLoading(true); setError(null); try { const res = await fetch("/api/admin/monitor", { cache: "no-store" }); const json = (await res.json()) as MonitorResponse & { error?: string }; if (!res.ok) { setError(json.error || t("monitorLoadFailed")); setLoading(false); return; } setData(json); setLastUpdated(Date.now()); } catch (err) { setError(t("monitorLoadFailed")); } finally { setLoading(false); } } useEffect(() => { load(); const id = setInterval(load, REFRESH_MS); return () => clearInterval(id); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return (

{t("monitorTitle")}

{t("monitorLead")}

{t("monitorLastUpdated")}:{" "} {lastUpdated ? new Date(lastUpdated).toLocaleTimeString() : "—"}
{error ?

{error}

: null} {!hasData && !loading ? (

{t("monitorNoData")}

) : null}

{t("monitorHetznerTitle")}

{data?.hetzner?.ok ? statusPill(t("monitorHealthy"), true) : statusPill(t("monitorAttention"), false)}
{!data?.hetzner ? (

{t("monitorNoData")}

) : data.hetzner.missingToken ? (

{t("monitorHetznerMissingToken")}

) : data.hetzner.ok && (data.hetzner.servers?.length ?? 0) > 0 ? (
    {data.hetzner.servers!.map((s) => (
  • {s.name} — {s.type ?? "server"} ( {s.datacenter ?? "dc"})
    {statusPill(s.status, s.status?.toLowerCase() === "running")}
    Public IP: {s.publicIp ?? "—"} Private IP: {s.privateIp ?? "—"} {t("monitorCreated")}{" "} {s.created ? new Date(s.created).toLocaleString() : "—"}
  • ))}
) : (

{data.hetzner.error || t("monitorHetznerEmpty")}

)}

{t("monitorK8sTitle")}

{data?.k8s?.ok ? statusPill(t("monitorHealthy"), true) : statusPill(t("monitorAttention"), false)}
{!data?.k8s ? (

{t("monitorNoData")}

) : !data.k8s.ok ? (

{data.k8s.error ?? t("monitorLoadFailed")}

) : ( <>

{t("monitorNodesTitle")}

{(data.k8s.nodes ?? []).map((n) => (
{n.name}
{n.roles.length ? n.roles.join(", ") : "node"}
{statusPill(n.status, n.ready)}
IP: {n.internalIp ?? "—"}
{n.kubeletVersion ?? "kubelet"} · {n.osImage ?? ""}
{t("monitorLastReady")}:{" "} {n.lastReadyTransition ? new Date(n.lastReadyTransition).toLocaleString() : "—"}
))}

{t("monitorPodsTitle")}

{(data.k8s.pods ?? []).length === 0 ? (

{t("monitorNoPods")}

) : (
{(data.k8s.pods ?? []).map((p) => ( ))}
Namespace Pod Ready {t("monitorRestarts")} Phase {t("monitorAge")} Node
{p.namespace}
{p.name}
{p.containers .map( (c) => `${c.name} (${c.state}${c.lastState ? `, ${c.lastState}` : ""})`, ) .join("; ")}
{p.readyCount}/{p.totalContainers} {p.restarts} {p.phase} {p.reason ? ` (${p.reason})` : ""} {formatDurationFrom(p.startedAt)} {p.nodeName ?? "—"}
)}
)}

{t("monitorDbTitle")}

{data?.db?.ok ? statusPill(t("monitorHealthy"), true) : statusPill(t("monitorAttention"), false)}
{!data?.db ? (

{t("monitorNoData")}

) : !data.db.ok ? (

{data.db.error ?? t("monitorLoadFailed")}

) : ( <>
{t("monitorServerTime")}
{data.db.serverTime ? new Date(data.db.serverTime).toLocaleString() : "—"}
{t("monitorDbSize")}
{formatBytes(data.db.databaseSizeBytes)}
{t("monitorDbRecovery")}
{data.db.recovery ? t("yes") : t("no")}

{t("monitorConnections")}

{(data.db.connections ?? []).length === 0 ? (

{t("monitorNoData")}

) : (
    {data.db.connections!.map((c) => (
  • {c.count} {c.state}
  • ))}
)}
)}
); }