import { useEffect, useState } from 'react' import { fetchHealth, fetchSources, fetchAlerts, fetchEnvStatus, fetchRFPropagation, type MeshHealth, type SourceHealth, type Alert, type EnvStatus, type RFPropagation, } from '@/lib/api' import { useWebSocket } from '@/hooks/useWebSocket' import { AlertTriangle, AlertCircle, Info, CheckCircle, Radio, Cpu, Activity, MapPin, Zap, } from 'lucide-react' function HealthGauge({ health }: { health: MeshHealth }) { const score = health.score const tier = health.tier // Color based on score const getColor = (s: number) => { if (s >= 80) return '#22c55e' if (s >= 60) return '#f59e0b' return '#ef4444' } const color = getColor(score) const circumference = 2 * Math.PI * 45 const progress = (score / 100) * circumference return (
{/* Background circle */} {/* Progress arc */} {/* Score text */} {score.toFixed(1)} {tier}
) } function PillarBar({ label, value, }: { label: string value: number }) { const getColor = (v: number) => { if (v >= 80) return 'bg-green-500' if (v >= 60) return 'bg-amber-500' return 'bg-red-500' } return (
{label}
{value.toFixed(1)}
) } function AlertItem({ alert }: { alert: Alert }) { const getSeverityStyles = (severity: string) => { switch (severity.toLowerCase()) { case 'critical': case 'emergency': return { bg: 'bg-red-500/10', border: 'border-red-500', icon: AlertCircle, iconColor: 'text-red-500', } case 'warning': return { bg: 'bg-amber-500/10', border: 'border-amber-500', icon: AlertTriangle, iconColor: 'text-amber-500', } default: return { bg: 'bg-green-500/10', border: 'border-green-500', icon: Info, iconColor: 'text-green-500', } } } const styles = getSeverityStyles(alert.severity) const Icon = styles.icon return (
{alert.message}
{alert.timestamp || 'Just now'}
) } function SourceCard({ source }: { source: SourceHealth }) { const getStatusColor = () => { if (!source.is_loaded) return 'bg-red-500' if (source.last_error) return 'bg-amber-500' return 'bg-green-500' } return (
{source.name}
{source.node_count} nodes * {source.type}
) } function StatCard({ icon: Icon, label, value, subvalue, }: { icon: typeof Radio label: string value: string | number subvalue?: string }) { return (
{label}
{value}
{subvalue && (
{subvalue}
)}
) } function RFPropagationCard({ propagation }: { propagation: RFPropagation | null }) { if (!propagation) { return (

RF Propagation

Loading propagation data...

) } const hf = propagation.hf const ducting = propagation.uhf_ducting const getDuctingColor = (condition?: string) => { if (!condition) return 'text-slate-400' switch (condition) { case 'normal': return 'text-green-500' case 'super_refraction': return 'text-amber-500' case 'surface_duct': case 'elevated_duct': return 'text-blue-400' default: return 'text-slate-400' } } const hasHF = hf && (hf.sfi || hf.kp_current !== undefined) const hasDucting = ducting && ducting.condition return (

RF Propagation

{/* Solar/Geomagnetic Indices */}
Solar/Geomagnetic
{hasHF ? (
SFI {hf.sfi?.toFixed(0) || '?'} / Kp {hf.kp_current?.toFixed(1) || '?'}
R{hf.r_scale ?? 0} / S{hf.s_scale ?? 0} / G{hf.g_scale ?? 0}
{hf.r_scale !== undefined && hf.r_scale > 0 && (
R{hf.r_scale} Radio Blackout
)}
) : (
No data
)}
{/* Tropospheric Ducting */}
Tropospheric
{hasDucting ? (
{ducting.condition === 'normal' ? 'Normal' : ducting.condition?.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase())}
dM/dz: {ducting.min_gradient ?? '?'} M-units/km
{ducting.duct_thickness_m && (
Duct: ~{ducting.duct_thickness_m}m thick
)}
) : (
No ducting data
)}
) } export default function Dashboard() { const [health, setHealth] = useState(null) const [sources, setSources] = useState([]) const [alerts, setAlerts] = useState([]) const [envStatus, setEnvStatus] = useState(null) const [rfProp, setRFProp] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const { lastHealth } = useWebSocket() useEffect(() => { Promise.all([ fetchHealth(), fetchSources(), fetchAlerts(), fetchEnvStatus(), fetchRFPropagation().catch(() => null), ]) .then(([h, src, a, e, rf]) => { setHealth(h) setSources(src) setAlerts(a) setEnvStatus(e) setRFProp(rf) setLoading(false) document.title = 'Dashboard — MeshAI' }) .catch((err) => { setError(err.message) setLoading(false) document.title = 'Dashboard — MeshAI' }) }, []) // Update health from WebSocket useEffect(() => { if (lastHealth) { setHealth(lastHealth) } }, [lastHealth]) if (loading) { return (
Loading...
) } if (error) { return (
Error: {error}
) } return (
{/* Mesh Health */}

Mesh Health

{health && ( <>
)}
{/* Alerts + Stats */}
{/* Active Alerts */}

Active Alerts

{alerts.length > 0 ? (
{alerts.map((alert, i) => ( ))}
) : (
No active alerts
)}
{/* Quick Stats */}
{/* Mesh Sources */}

Mesh Sources ({sources.length})

{sources.length > 0 ? (
{sources.map((source, i) => ( ))}
) : (
No sources configured
)}
{/* Environmental Feeds */}

Environmental Feeds

{envStatus?.enabled ? (
{envStatus.feeds.length} feeds active
) : (

Environmental feeds not enabled.

Enable in config.yaml

)}
{/* RF Propagation */}
) }