import { useEffect, useState } from 'react' import { Cloud, Sun, AlertTriangle, AlertCircle, Info, CheckCircle, Activity, Wind, Flame, Mountain, } from 'lucide-react' import { fetchEnvStatus, fetchEnvActive, fetchSWPC, fetchDucting, fetchFires, fetchAvalanche, type EnvStatus, type EnvEvent, type SWPCStatus, type DuctingStatus, type FireEvent, type AvalancheResponse, } 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 [loading, setLoading] = useState(true) const [error, setError] = useState(null) useEffect(() => { Promise.all([ fetchEnvStatus().catch(() => null), fetchEnvActive().catch(() => []), fetchSWPC().catch(() => null), fetchDucting().catch(() => null), fetchFires().catch(() => []), fetchAvalanche().catch(() => null), ]) .then(([status, active, swpcData, ductingData, firesData, avyData]) => { setEnvStatus(status) setEvents(active) setSWPC(swpcData) setDucting(ductingData) setFires(firesData) setAvalanche(avyData) 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
)}
{/* Active Events */}

Active Events ({events.length})

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