import { useEffect, useState } from 'react' import { Cloud, Sun, AlertTriangle, AlertCircle, Info, CheckCircle, Activity, Wind, Flame, Mountain, Droplets, Car, Satellite, } from 'lucide-react' import { fetchEnvStatus, fetchEnvActive, fetchSWPC, fetchDucting, fetchFires, fetchAvalanche, fetchStreams, fetchTraffic, fetchRoads, fetchHotspots, type EnvStatus, type EnvEvent, type SWPCStatus, type DuctingStatus, type FireEvent, type AvalancheResponse, type StreamGaugeEvent, type TrafficEvent, type RoadEvent, type HotspotEvent, } from '@/lib/api' function FeedStatusCard({ feed }: { feed: { source: string; is_loaded: boolean; last_error: string | null; consecutive_errors: number; event_count: number; last_fetch: number } }) { const getStatusColor = () => { if (!feed.is_loaded) return 'bg-red-500' if (feed.consecutive_errors > 0) return 'bg-amber-500' return 'bg-green-500' } const getStatusText = () => { if (!feed.is_loaded) return 'Not loaded' if (feed.consecutive_errors > 0) return `${feed.consecutive_errors} errors` return 'Healthy' } const formatLastFetch = (ts: number) => { if (!ts) return 'Never' const date = new Date(ts * 1000) return date.toLocaleTimeString() } return (
{feed.source}
{getStatusText()}
Events: {feed.event_count}
Last fetch: {formatLastFetch(feed.last_fetch)}
{feed.last_error && (
{feed.last_error}
)}
) } function AlertEventCard({ event }: { event: EnvEvent }) { const getSeverityStyles = (severity: string) => { switch (severity.toLowerCase()) { case 'extreme': case 'severe': return { bg: 'bg-red-500/10', border: 'border-red-500', icon: AlertCircle, iconColor: 'text-red-500', } case 'moderate': case 'warning': return { bg: 'bg-amber-500/10', border: 'border-amber-500', icon: AlertTriangle, iconColor: 'text-amber-500', } case 'minor': return { bg: 'bg-yellow-500/10', border: 'border-yellow-500', icon: Info, iconColor: 'text-yellow-500', } default: return { bg: 'bg-slate-500/10', border: 'border-slate-500', icon: Info, iconColor: 'text-slate-400', } } } const styles = getSeverityStyles(event.severity) const Icon = styles.icon const formatExpires = (ts?: number) => { if (!ts) return null const date = new Date(ts * 1000) return date.toLocaleString() } return (
{event.event_type} {event.severity}
{event.headline}
{event.description && (
{event.description}
)}
{event.source} {event.expires && ( Expires: {formatExpires(event.expires)} )}
) } function SolarIndicesPanel({ swpc }: { swpc: SWPCStatus | null }) { if (!swpc || !swpc.enabled) { return (

Solar/Geomagnetic Indices

Data not available
) } const getKpColor = (kp?: number) => { if (kp === undefined) return 'text-slate-400' if (kp <= 2) return 'text-green-500' if (kp <= 4) return 'text-amber-500' if (kp <= 6) return 'text-orange-500' return 'text-red-500' } const getScaleColor = (scale?: number) => { if (scale === undefined || scale === 0) return 'text-green-500' if (scale <= 2) return 'text-amber-500' if (scale <= 3) return 'text-orange-500' return 'text-red-500' } return (

Solar/Geomagnetic Indices

{/* SFI */}
Solar Flux Index
{swpc.sfi?.toFixed(0) ?? '—'}
SFI (10.7 cm)
{/* Kp */}
Planetary K-Index
{swpc.kp_current?.toFixed(1) ?? '—'}
Kp
{/* NOAA Scales */}
NOAA Space Weather Scales
R: {swpc.r_scale ?? 0}
S: {swpc.s_scale ?? 0}
G: {swpc.g_scale ?? 0}
Radio Blackout / Solar Radiation / Geomagnetic Storm
{/* Active Warnings */} {swpc.active_warnings && swpc.active_warnings.length > 0 && (
Active Warnings
{swpc.active_warnings.slice(0, 3).map((warning, i) => (
{warning}
))}
)}
) } function DuctingPanel({ ducting }: { ducting: DuctingStatus | null }) { if (!ducting || !ducting.enabled) { return (

Tropospheric Ducting

Data not available
) } const getConditionColor = (condition?: string) => { 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 formatCondition = (condition?: string) => { if (!condition) return 'Unknown' return condition.replace('_', ' ').replace(/\b\w/g, l => l.toUpperCase()) } return (

Tropospheric Ducting

{/* Condition */}
Condition
{formatCondition(ducting.condition)}
{/* Refractivity Gradient */}
Min Gradient
{ducting.min_gradient ?? '—'}
M-units/km
{ducting.duct_thickness_m && (
Duct Thickness
{ducting.duct_thickness_m}
meters
)} {ducting.duct_base_m && (
Duct Base
{ducting.duct_base_m}
meters AGL
)}
{/* Reference */}
dM/dz reference:
>79: Normal propagation
0–79: Super-refraction
<0: Ducting (trapping layer)
{ducting.last_update && (
Last update: {ducting.last_update}
)}
) } export default function Environment() { const [envStatus, setEnvStatus] = useState(null) const [events, setEvents] = useState([]) const [swpc, setSWPC] = useState(null) const [ducting, setDucting] = useState(null) const [fires, setFires] = useState([]) const [avalanche, setAvalanche] = useState(null) const [streams, setStreams] = useState([]) const [traffic, setTraffic] = useState([]) const [roads, setRoads] = useState([]) const [hotspots, setHotspots] = useState([]) const [newIgnitions, setNewIgnitions] = useState(0) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) useEffect(() => { document.title = 'Environment — MeshAI' Promise.all([ fetchEnvStatus().catch(() => null), fetchEnvActive().catch(() => []), fetchSWPC().catch(() => null), fetchDucting().catch(() => null), fetchFires().catch(() => []), fetchAvalanche().catch(() => null), fetchStreams().catch(() => []), fetchTraffic().catch(() => []), fetchRoads().catch(() => []), fetchHotspots().catch(() => ({ hotspots: [], new_ignitions: 0 })), ]) .then(([status, active, swpcData, ductingData, firesData, avyData, streamsData, trafficData, roadsData, hotspotsData]) => { setEnvStatus(status) setEvents(active) setSWPC(swpcData) setDucting(ductingData) setFires(firesData) setAvalanche(avyData) setStreams(streamsData || []) setTraffic(trafficData || []) setRoads(roadsData || []) setHotspots(hotspotsData?.hotspots || []) setNewIgnitions(hotspotsData?.new_ignitions || 0) setLoading(false) }) .catch((err) => { setError(err.message) setLoading(false) }) }, []) if (loading) { return (
Loading environmental data...
) } if (error) { return (
Error: {error}
) } if (!envStatus?.enabled) { return (

Environmental Feeds Disabled

Enable environmental feeds in config.yaml to see weather alerts, space weather indices, and tropospheric ducting data.

) } return (
{/* Header */}

Environment

{events.length} active event{events.length !== 1 ? 's' : ''}
{/* Feed Status */}

Feed Status

{envStatus.feeds.map((feed) => ( ))}
{/* Main content grid */}
{/* Solar Indices */} {/* Tropospheric Ducting */}
{/* Fires and Avalanche */}
{/* Wildfires */}

Active Wildfires ({fires.length})

{fires.length > 0 ? (
{fires.map((fire) => (
{fire.name} {fire.severity}
{fire.acres.toLocaleString()} acres, {fire.pct_contained}% contained
{fire.distance_km && fire.nearest_anchor && (
{Math.round(fire.distance_km)} km from {fire.nearest_anchor}
)}
))}
) : (
No active wildfires in the area
)}
{/* Avalanche */}

Avalanche Advisories

{avalanche?.off_season ? (

Off season - check back in December

) : avalanche && avalanche.advisories.length > 0 ? (
{avalanche.advisories.map((avy) => (
= 4 ? 'bg-red-500/10 border-l-2 border-red-500' : avy.danger_level >= 3 ? 'bg-amber-500/10 border-l-2 border-amber-500' : avy.danger_level >= 2 ? 'bg-yellow-500/10 border-l-2 border-yellow-500' : 'bg-green-500/10 border-l-2 border-green-500' }`} >
{avy.zone_name} = 4 ? 'bg-red-500/20 text-red-400' : avy.danger_level >= 3 ? 'bg-amber-500/20 text-amber-400' : avy.danger_level >= 2 ? 'bg-yellow-500/20 text-yellow-400' : 'bg-green-500/20 text-green-400' }`}> {avy.danger_name}
{avy.center}
{avy.travel_advice && (
{avy.travel_advice}
)}
))} {avalanche.advisories[0]?.center_link && ( View full forecast )}
) : (
No avalanche advisories
)}
{/* Stream Gauges */} {streams.length > 0 && (

Stream Gauges ({streams.length})

{streams.map((stream) => (
{stream.properties?.site_name || 'Unknown Site'} {stream.properties?.value?.toLocaleString()} {stream.properties?.unit}
{stream.properties?.parameter}
))}
)} {/* Road Conditions */} {(traffic.length > 0 || roads.length > 0) && (

Road Conditions

{traffic.length > 0 && (
Traffic Flow
{traffic.map((t) => (
{t.properties?.corridor || 'Unknown'} {t.properties?.roadClosure ? 'CLOSED' : `${Math.round(t.properties?.currentSpeed || 0)}mph`}
{!t.properties?.roadClosure && (
{Math.round((t.properties?.speedRatio || 1) * 100)}% of free flow ({Math.round(t.properties?.freeFlowSpeed || 0)}mph)
)}
))}
)} {roads.length > 0 && (
Road Events
{roads.map((r) => (
{r.properties?.is_closure && ( CLOSURE )} {r.headline}
{r.event_type}
))}
)}
)} {/* Satellite Hotspots */} {hotspots.length > 0 && (

Satellite Hotspots ({hotspots.length}) {newIgnitions > 0 && ( {newIgnitions} NEW )}

{hotspots.map((h) => (
{h.properties?.new_ignition && ( NEW )} {h.headline}
{h.properties?.frp && ( {Math.round(h.properties.frp)} MW )}
Conf: {h.properties?.confidence || 'N/A'} {h.properties?.acq_time && @{h.properties.acq_time}Z} {h.properties?.near_fire && ( Near: {h.properties.near_fire} )}
))}
)} {/* Active Events */}

Active Events ({events.length})

{events.length > 0 ? (
{events.map((event) => ( ))}
) : (
No active environmental events
)}
) }