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 (
)
}
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 getAssessmentColor = (assessment?: string) => {
if (!assessment) return 'text-slate-400'
switch (assessment.toLowerCase()) {
case 'excellent':
return 'text-green-400'
case 'good':
return 'text-green-500'
case 'fair':
return 'text-amber-500'
case 'poor':
return 'text-red-500'
default:
return 'text-slate-400'
}
}
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.band_assessment || hf.sfi || hf.kp_current !== undefined)
const hasDucting = ducting && ducting.condition
return (
RF Propagation
{/* HF Section */}
HF Bands
{hasHF ? (
{hf.band_assessment || 'Unknown'}
SFI {hf.sfi?.toFixed(0) || '?'} / Kp {hf.kp_current?.toFixed(1) || '?'}
{hf.r_scale !== undefined && hf.r_scale > 0 && (
R{hf.r_scale} Radio Blackout
)}
) : (
No HF data
)}
{/* UHF Ducting Section */}
UHF 906 MHz
{hasDucting ? (
{ducting.condition === 'normal'
? 'Normal'
: ducting.condition?.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase())}
{ducting.condition !== 'normal' && ducting.min_gradient !== undefined && (
dM/dz: {ducting.min_gradient} M-units/km
)}
{ducting.condition !== 'normal' && (
Extended range likely
)}
) : (
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)
})
.catch((err) => {
setError(err.message)
setLoading(false)
})
}, [])
// Update health from WebSocket
useEffect(() => {
if (lastHealth) {
setHealth(lastHealth)
}
}, [lastHealth])
if (loading) {
return (
)
}
if (error) {
return (
)
}
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 */}
)
}