import { useCallback, useEffect, useState } from 'react' import { Activity, RefreshCw, Loader2, AlertTriangle, Clock, Database, TrendingUp, TrendingDown } from 'lucide-react' import { cn } from '../../lib/utils' type Point = { t: string; rows: number; delta: number } type DsMetric = { key: string; label: string; engine: string; color: string; table: string rows: number | null; delta: number | null; freshness_age_min: number | null columns: number | null; stalled_cycles: number; error: string | null; ts: string | null series: Point[] } type Alert = { id: string; dataset: string; type: string; severity: string; message: string; count: number; ts: string; last_ts: string } type Metrics = { ok: boolean; enabled: boolean; cycles: number; freshness_min: number alert_counts: { critical: number; warning: number; info: number; total: number } datasets: DsMetric[] } function Spark({ data, color }: { data: Point[]; color: string }) { const pts = data.slice(-40) if (pts.length < 2) return
const w = 240 const h = 40 const vals = pts.map((p) => p.rows) const max = Math.max(...vals) const min = Math.min(...vals) const range = max - min || 1 const step = w / (pts.length - 1) const line = pts.map((p, i) => `${i === 0 ? 'M' : 'L'}${(i * step).toFixed(1)},${(h - ((p.rows - min) / range) * (h - 6) - 3).toFixed(1)}`).join(' ') return ( ) } export function ObservabilityView() { const [metrics, setMetrics] = useState(null) const [alerts, setAlerts] = useState([]) const [loading, setLoading] = useState(true) const load = useCallback(async () => { setLoading(true) try { const [m, a] = await Promise.all([ fetch('/api/observability/metrics').then((r) => r.json()), fetch('/api/observability/alerts').then((r) => r.json()), ]) setMetrics(m) setAlerts(a.active || []) } catch { /* */ } finally { setLoading(false) } }, []) useEffect(() => { load() }, [load]) useEffect(() => { const t = setInterval(load, 8000); return () => clearInterval(t) }, [load]) const ac = metrics?.alert_counts const sevBorder = (s: string) => (s === 'critical' ? 'border-rose-500/40 bg-rose-500/5' : s === 'warning' ? 'border-amber-500/40 bg-amber-500/5' : 'border-sky-500/40 bg-sky-500/5') const sevText = (s: string) => (s === 'critical' ? 'text-rose-400' : s === 'warning' ? 'text-amber-400' : 'text-sky-400') return (
Tracking volume, freshness & schema drift across every business table
{/* active alerts */} {alerts.length > 0 && (
{alerts.map((a) => (
{a.dataset} {a.message} {a.type} {a.count > 1 && ×{a.count}}
))}
)} {/* per-dataset volume + freshness */}
{(metrics?.datasets || []).map((d) => { const stale = d.freshness_age_min != null && d.freshness_age_min > (metrics?.freshness_min ?? 30) return (
{d.label} {d.delta != null && d.delta !== 0 && ( 0 ? 'text-emerald-400' : 'text-rose-400')}> {d.delta > 0 ? : } {d.delta > 0 ? '+' : ''}{d.delta.toLocaleString()} )}
Rows: {d.rows?.toLocaleString() ?? '—'} {d.freshness_age_min != null ? `${d.freshness_age_min.toFixed(0)}m old` : 'n/a'} {d.columns != null && {d.columns} cols} {d.stalled_cycles > 0 && stalled ×{d.stalled_cycles}} {d.error && err}
) })}
) } function Kpi({ icon: Icon, label, value, accent }: { icon: typeof Activity; label: string; value: string; accent: string }) { return (

{label}

{value}

) }