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 (
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 (
)
}
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
)}
)
}