'use client'; import { useEffect, useState } from 'react'; interface Service { name: string; state: string; hasWeb: boolean; } interface Trace { service: string; operation: string; duration: number; spans: number; hasError: boolean; } interface DashboardData { services: Service[]; traces: Trace[]; node: { cpu: number | null; ram: number | null; disk: number | null; load1: number | null; load5: number | null; load15: number | null; netIn: number | null; netOut: number | null; }; rps: number[]; latency: number[]; errors: number[]; traceVolume: number[]; links: { url: string; label: string }[]; } function safeDur(us: number): string { if (us < 1000) return `${us}µs`; if (us < 1_000_000) return `${(us / 1000).toFixed(1)}ms`; return `${(us / 1_000_000).toFixed(2)}s`; } function Donut({ running, degraded }: { running: number; degraded: number }) { const total = running + degraded; if (!total) return ; const cx = 100, cy = 90, R = 60, circ = 2 * Math.PI * R; const segs = [ { n: running, c: '#3fb950', l: 'Running' }, { n: degraded, c: '#d29922', l: 'Degraded' }, ]; let off = 0; return ( {segs.map((s) => { if (!s.n) return null; const frac = s.n / total; const ln = frac * circ; const el = ( ); off += ln; return el; })} {total} total {segs.filter(s => s.n).map((s, i) => ( {s.l}: {s.n} ))} ); } function Sparkline({ data, color }: { data: number[]; color: string }) { const w = 300, h = 160, pl = 45, pt = 20, pr = 10, pb = 25; const vw = w - pl - pr, vh = h - pt - pb; if (!data.length) return ; const maxV = Math.max(...data.map(Math.abs), 1); const pts = data.map((v, i) => `${(pl + vw * i / (data.length - 1)).toFixed(1)},${(pt + vh * (1 - v / maxV)).toFixed(1)}` ).join(' '); const area = `M${pl},${pt + vh} L${pts} L${pl + vw},${pt + vh} Z`; const lv = data[data.length - 1]; const ly = pt + vh * (1 - lv / maxV); return ( {[0, 1, 2, 3, 4].map(i => { const y = pt + vh * i / 4; return ( {(maxV * (1 - i / 4)).toFixed(0)} ); })} {lv.toFixed(1)} {data.length > 5 && [...Array(5)].map((_, i) => { const idx = Math.floor((i + 1) * (data.length - 1) / 5); const x = pl + vw * idx / (data.length - 1); return {idx + 1}; })} ); } function Gauge({ pct, color, label, unit }: { pct: number | null; color: string; label: string; unit: string }) { if (pct === null || pct <= 0) return ; const w = 220, h = 100, bw = 200, bh = 14; const bx = (w - bw) / 2, by = 30; const fw = bw * Math.min(pct / 100, 1); return ( {fw > 0 && } {label} {pct.toFixed(1)}{unit} ); } function NoData() { return ( No data ); } function Pill({ state }: { state: string }) { const dot: Record = { running: '#3fb950', jaeger: '#58a6ff', exited: '#f85149', restarting: '#d29922' }; return {state} ; } export default function Dashboard() { const [data, setData] = useState(null); const [time, setTime] = useState(''); useEffect(() => { const fetchData = async () => { try { const res = await fetch('/api/dashboard'); if (res.ok) setData(await res.json()); } catch { /* ignore */ } }; fetchData(); const id = setInterval(fetchData, 15000); return () => clearInterval(id); }, []); useEffect(() => { const tick = () => setTime(new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })); tick(); const id = setInterval(tick, 10000); return () => clearInterval(id); }, []); const running = data?.services.filter(s => s.state === 'running').length ?? 0; const degraded = (data?.services.length ?? 0) - running; const hasNode = data?.node.cpu !== null || data?.node.ram !== null; const hasTraffik = (data?.rps?.length ?? 0) > 0 || (data?.latency?.length ?? 0) > 0; const hasOTel = false; const node = data?.node; const gaugeColor = (v: number | null) => { if (v === null) return '#3fb950'; if (v > 80) return '#f85149'; if (v > 60) return '#d29922'; return '#3fb950'; }; return (
H

Hub Dashboard

{degraded ? `${degraded} degraded` : 'All Systems Operational'} {time}
{/* Services */}

Services

{data?.services.length ?? 0}
{data?.services.map(s => ( {s.name} )) || No services detected}
{/* Overview */}

Overview

{data?.services.length ?? '-'}
Total
{running}
Healthy
{data?.traces.length ?? 0}
Traces
0
Errors
{/* Health */}

Health

{/* Links */}

Links

{data?.links.map(l => ( {l.label} ))}
{/* Node Resources */} {hasNode && (

System Resources

{node?.load1?.toFixed(2)} {node?.load5?.toFixed(2)} {node?.load15?.toFixed(2)}
)} {/* Request Rate */} {hasTraffik && (

Request Rate

{data?.rps?.length ? `${data.rps[data.rps.length - 1].toFixed(1)}/s` : '-'}
)} {/* Latency */} {hasTraffik && (

Latency

{data?.latency?.length ? `${data.latency[data.latency.length - 1].toFixed(0)}ms` : '-'}
)} {/* Error Rate */} {hasTraffik && (

Error Rate

{data?.errors?.length ? `${data.errors[data.errors.length - 1].toFixed(1)}/s` : '-'}
)} {/* Trace Volume */}

Trace Volume

{data?.traces.length ?? 0} traces
{/* Traces */}

Recent Traces

{data?.traces.length ?? 0}
{data?.traces.length ? (
    {data.traces.map((t, i) => (
  • {t.service}
    {t.operation}
    {safeDur(t.duration)} {t.spans} {t.hasError && err}
  • ))}
) :
No traces — data appears once services send OTel telemetry
}
); }