'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}
  • ))}
)}
)}
); }