import { useEffect, useState, type ReactNode } from 'react'
import {
Cloud, Flame, Radio, Car, Mountain, Satellite, Activity,
Save, RotateCcw, RefreshCw, AlertCircle, AlertTriangle, Info,
} from 'lucide-react'
import {
Toggle, TextInput, NumberInput, SelectInput, ListInput, NumberListInput,
US_STATES,
} from './Config'
import {
fetchEnvStatus, fetchEnvActive,
type EnvStatus, type EnvEvent,
} from '@/lib/api'
type FeedSource = 'native' | 'central'
interface EnvConfig {
enabled: boolean
nws_zones: string[]
nws: { enabled: boolean; user_agent: string; tick_seconds: number; severity_min: string; feed_source?: FeedSource }
swpc: { enabled: boolean; feed_source?: FeedSource }
ducting: { enabled: boolean; tick_seconds: number; latitude: number; longitude: number; feed_source?: FeedSource }
fires: { enabled: boolean; tick_seconds: number; state: string; feed_source?: FeedSource }
avalanche: { enabled: boolean; tick_seconds: number; center_ids: string[]; season_months: number[]; feed_source?: FeedSource }
usgs: { enabled: boolean; tick_seconds: number; sites: string[]; feed_source?: FeedSource }
usgs_quake: { enabled: boolean; tick_seconds: number; feed_url: string; min_magnitude: number; bbox: number[]; region: string; feed_source?: FeedSource }
traffic: { enabled: boolean; tick_seconds: number; api_key: string; corridors: { name: string; lat: number; lon: number }[]; feed_source?: FeedSource }
roads511: { enabled: boolean; tick_seconds: number; api_key: string; base_url: string; endpoints: string[]; bbox: number[]; feed_source?: FeedSource }
firms: { enabled: boolean; tick_seconds: number; map_key: string; source: string; bbox: number[]; day_range: number; confidence_min: string; proximity_km: number; feed_source?: FeedSource }
central?: { enabled: boolean; url: string; durable: string; region: string }
}
type FeedHealth = EnvStatus['feeds'][number]
// ---------------------------------------------------------------- status cards
function FeedStatusCard({ feed }: { feed: FeedHealth }) {
const color = !feed.is_loaded ? 'bg-red-500' : feed.consecutive_errors > 0 ? 'bg-amber-500' : 'bg-green-500'
const text = !feed.is_loaded ? 'Not loaded' : feed.consecutive_errors > 0 ? `${feed.consecutive_errors} errors` : 'Healthy'
const lastFetch = feed.last_fetch ? new Date(feed.last_fetch * 1000).toLocaleTimeString() : 'Never'
return (
Events: {feed.event_count}
Last fetch: {lastFetch}
{feed.last_error &&
{feed.last_error}
}
)
}
function EventCard({ event }: { event: EnvEvent }) {
const sev = event.severity.toLowerCase()
const styles = (sev === 'extreme' || sev === 'severe' || sev === 'immediate')
? { bg: 'bg-red-500/10', border: 'border-red-500', Icon: AlertCircle, color: 'text-red-500' }
: (sev === 'moderate' || sev === 'warning' || sev === 'priority')
? { bg: 'bg-amber-500/10', border: 'border-amber-500', Icon: AlertTriangle, color: 'text-amber-500' }
: { bg: 'bg-blue-500/10', border: 'border-blue-500', Icon: Info, color: 'text-blue-500' }
const Icon = styles.Icon
return (
{event.event_type}
{event.severity}
{event.headline}
)
}
// ---------------------------------------------------------------- feed_source toggle
function FeedSourceToggle({ value, onChange, disabled, centralDisabled }: {
value: FeedSource; onChange: (v: FeedSource) => void; disabled: boolean; centralDisabled: boolean
}) {
const base = 'px-2 py-1 text-xs transition-colors'
return (
)
}
// ---------------------------------------------------------------- adapter panel
function AdapterPanel({ title, subtitle, enabled, onEnabled, feedSource, onFeedSource, hasCentral, nativeOnly, hasKey, health, events, children }: {
title: string; subtitle?: string
enabled: boolean; onEnabled: (v: boolean) => void
feedSource: FeedSource; onFeedSource: (v: FeedSource) => void
hasCentral: boolean; nativeOnly: boolean; hasKey: boolean
health?: FeedHealth; events?: EnvEvent[]; children?: ReactNode
}) {
const centralDisabled = nativeOnly || !hasCentral
return (
{title}
{subtitle &&
{subtitle}
}
{!hasKey && (
API key not configured — contact admin
)}
{nativeOnly && (
Central not available for this adapter — native only
)}
{children}
{(health || (events && events.length > 0)) && (
Live status
{health ?
:
No status reported.
}
{events && events.length > 0 && (
{events.slice(0, 5).map((e, i) => )}
)}
)}
)
}
// ---------------------------------------------------------------- families
type AdapterKey = 'nws' | 'fires' | 'firms' | 'swpc' | 'ducting' | 'traffic' | 'roads511' | 'usgs_quake' | 'usgs' | 'avalanche'
interface AdapterMeta { label: string; subtitle: string; health: string; hasCentral: boolean; nativeOnly: boolean; hasKey: boolean }
const META: Record = {
nws: { label: 'NWS Weather Alerts', subtitle: 'National Weather Service alerts', health: 'nws', hasCentral: true, nativeOnly: false, hasKey: true },
fires: { label: 'NIFC Fire Perimeters', subtitle: 'Active wildfires (National Interagency Fire Center)', health: 'nifc', hasCentral: true, nativeOnly: false, hasKey: true },
firms: { label: 'NASA FIRMS Hotspots', subtitle: 'Satellite thermal-anomaly detections', health: 'firms', hasCentral: true, nativeOnly: false, hasKey: false },
swpc: { label: 'NOAA Space Weather (SWPC)', subtitle: 'Solar indices, geomagnetic storms', health: 'swpc', hasCentral: true, nativeOnly: false, hasKey: true },
ducting: { label: 'Tropospheric Ducting', subtitle: 'VHF/UHF extended-range conditions', health: 'ducting', hasCentral: false, nativeOnly: true, hasKey: true },
traffic: { label: 'TomTom Traffic', subtitle: 'Traffic flow on monitored corridors', health: 'traffic', hasCentral: true, nativeOnly: false, hasKey: true },
roads511: { label: '511 Road Conditions', subtitle: 'State DOT road events and closures', health: 'roads511', hasCentral: true, nativeOnly: false, hasKey: false },
usgs_quake: { label: 'USGS Earthquakes', subtitle: 'Seismic events from the USGS feed', health: 'usgs_quake', hasCentral: true, nativeOnly: false, hasKey: true },
usgs: { label: 'USGS Stream Gauges', subtitle: 'River and stream water levels', health: 'usgs', hasCentral: true, nativeOnly: false, hasKey: true },
avalanche: { label: 'Avalanche Advisories', subtitle: 'Backcountry avalanche danger ratings', health: 'avalanche', hasCentral: false, nativeOnly: true, hasKey: true },
}
const FAMILIES: { key: string; label: string; icon: typeof Cloud; adapters: AdapterKey[] }[] = [
{ key: 'weather', label: 'Weather', icon: Cloud, adapters: ['nws'] },
{ key: 'fire', label: 'Fire', icon: Flame, adapters: ['fires', 'firms'] },
{ key: 'rf', label: 'RF Propagation', icon: Radio, adapters: ['swpc', 'ducting'] },
{ key: 'roads', label: 'Roads', icon: Car, adapters: ['traffic', 'roads511'] },
{ key: 'geohazards', label: 'Geohazards', icon: Mountain, adapters: ['usgs_quake', 'usgs', 'avalanche'] },
{ key: 'tracking', label: 'Tracking', icon: Satellite, adapters: [] },
{ key: 'mesh', label: 'Mesh Health', icon: Activity, adapters: [] },
]
// ---------------------------------------------------------------- main page
export default function Environment() {
const [env, setEnv] = useState(null)
const [original, setOriginal] = useState('')
const [status, setStatus] = useState(null)
const [events, setEvents] = useState([])
const [loading, setLoading] = useState(true)
const [saving, setSaving] = useState(false)
const [error, setError] = useState(null)
const [success, setSuccess] = useState(null)
const [restartRequired, setRestartRequired] = useState(false)
const [family, setFamily] = useState('weather')
const [adapter, setAdapter] = useState('nws')
useEffect(() => {
document.title = 'Environment — MeshAI'
;(async () => {
try {
const res = await fetch('/api/config/environmental')
const data = await res.json()
setEnv(data)
setOriginal(JSON.stringify(data))
} catch (e) {
setError(e instanceof Error ? e.message : 'Failed to load config')
} finally {
setLoading(false)
}
})()
}, [])
useEffect(() => {
const load = async () => {
try {
setStatus(await fetchEnvStatus())
setEvents(await fetchEnvActive())
} catch { /* status is best-effort */ }
}
load()
const t = setInterval(load, 30000)
return () => clearInterval(t)
}, [])
const hasChanges = env !== null && JSON.stringify(env) !== original
const save = async () => {
if (!env) return
setSaving(true); setError(null); setSuccess(null)
try {
const res = await fetch('/api/config/environmental', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(env),
})
const result = await res.json()
if (!res.ok) throw new Error(result.detail || 'Save failed')
setOriginal(JSON.stringify(env))
setSuccess('Environmental config saved')
if (result.restart_required) setRestartRequired(true)
setTimeout(() => setSuccess(null), 3000)
} catch (e) {
setError(e instanceof Error ? e.message : 'Save failed')
} finally {
setSaving(false)
}
}
const discard = () => { if (env) setEnv(JSON.parse(original)) }
const restart = async () => {
try { await fetch('/api/restart', { method: 'POST' }); setRestartRequired(false); setSuccess('Restart initiated') }
catch { setError('Restart failed') }
}
const up = (patch: Partial) => env && setEnv({ ...env, ...patch })
if (loading) return Loading environmental config…
if (!env) return {error || 'No config'}
const healthFor = (key: AdapterKey): FeedHealth | undefined =>
status?.feeds.find((f) => f.source === META[key].health)
const eventsFor = (key: AdapterKey): EnvEvent[] =>
events.filter((e) => e.source === META[key].health)
const fam = FAMILIES.find((f) => f.key === family)!
const activeAdapter: AdapterKey | null =
fam.adapters.length === 0 ? null : (adapter && fam.adapters.includes(adapter) ? adapter : fam.adapters[0])
// -- per-adapter settings forms (preserve all existing settings) --
const renderSettings = (key: AdapterKey) => {
switch (key) {
case 'nws': return (<>
up({ nws_zones: v })} helper="Zone IDs like IDZ016, IDZ030" infoLink="https://www.weather.gov/pimar/PubZone" />
up({ nws: { ...env.nws, user_agent: v } })} placeholder="(MeshAI, you@email.com)" helper="Format: (app_name, contact_email)" />
up({ nws: { ...env.nws, tick_seconds: v } })} min={30} />
up({ nws: { ...env.nws, severity_min: v } })} options={[{ value: 'minor', label: 'Minor' }, { value: 'moderate', label: 'Moderate' }, { value: 'severe', label: 'Severe' }, { value: 'extreme', label: 'Extreme' }]} />
>)
case 'swpc': return No additional settings.
case 'ducting': return (
up({ ducting: { ...env.ducting, tick_seconds: v } })} min={60} />
up({ ducting: { ...env.ducting, latitude: v } })} step={0.01} />
up({ ducting: { ...env.ducting, longitude: v } })} step={0.01} />
)
case 'fires': return (
up({ fires: { ...env.fires, tick_seconds: v } })} min={60} />
up({ fires: { ...env.fires, state: v } })} options={US_STATES} />
)
case 'avalanche': return (<>
up({ avalanche: { ...env.avalanche, tick_seconds: v } })} min={60} />
up({ avalanche: { ...env.avalanche, center_ids: v } })} helper="e.g., SNFAC" infoLink="https://avalanche.org/avalanche-centers/" />
up({ avalanche: { ...env.avalanche, season_months: v } })} helper="e.g., 12, 1, 2, 3, 4" />
>)
case 'usgs': return (<>
up({ usgs: { ...env.usgs, tick_seconds: v } })} min={900} helper="Minimum 15 min (900s)" />
up({ usgs: { ...env.usgs, sites: v } })} helper="USGS gauge site numbers" infoLink="https://waterdata.usgs.gov/nwis" />
>)
case 'usgs_quake': return (<>
up({ usgs_quake: { ...env.usgs_quake, tick_seconds: v } })} min={60} />
up({ usgs_quake: { ...env.usgs_quake, min_magnitude: v } })} step={0.1} min={0} />
up({ usgs_quake: { ...env.usgs_quake, region: v } })} />
{(['West', 'South', 'East', 'North'] as const).map((lbl, i) => (
{ const b = [...(env.usgs_quake.bbox || [0, 0, 0, 0])]; b[i] = v; up({ usgs_quake: { ...env.usgs_quake, bbox: b } }) }} step={0.01} />
))}
Bounding box [W,S,E,N] geographic filter
>)
case 'traffic': return (<>
up({ traffic: { ...env.traffic, api_key: v } })} type="password" helper="developer.tomtom.com" />
up({ traffic: { ...env.traffic, tick_seconds: v } })} min={60} />
Corridors:
{(env.traffic.corridors || []).map((c, i) => (
{ const n = [...env.traffic.corridors]; n[i] = { ...c, name: v }; up({ traffic: { ...env.traffic, corridors: n } }) }} />
{ const n = [...env.traffic.corridors]; n[i] = { ...c, lat: v }; up({ traffic: { ...env.traffic, corridors: n } }) }} step={0.01} />
{ const n = [...env.traffic.corridors]; n[i] = { ...c, lon: v }; up({ traffic: { ...env.traffic, corridors: n } }) }} step={0.01} />
))}
>)
case 'roads511': return (<>
up({ roads511: { ...env.roads511, base_url: v } })} placeholder="https://511.yourstate.gov/api/v2" />
up({ roads511: { ...env.roads511, api_key: v } })} type="password" helper="Leave empty if not required" />
up({ roads511: { ...env.roads511, tick_seconds: v } })} min={60} />
up({ roads511: { ...env.roads511, endpoints: v } })} helper="e.g., /get/event" />
{(['West', 'South', 'East', 'North'] as const).map((lbl, i) => (
{ const b = [...(env.roads511.bbox || [0, 0, 0, 0])]; b[i] = v; up({ roads511: { ...env.roads511, bbox: b } }) }} step={0.01} />
))}
>)
case 'firms': return (<>
up({ firms: { ...env.firms, map_key: v } })} type="password" helper="firms.modaps.eosdis.nasa.gov/api/area/" infoLink="https://firms.modaps.eosdis.nasa.gov/api/area/" />
up({ firms: { ...env.firms, tick_seconds: v } })} min={300} />
up({ firms: { ...env.firms, source: v } })} options={[{ value: 'VIIRS_SNPP_NRT', label: 'VIIRS SNPP (NRT)' }, { value: 'VIIRS_NOAA20_NRT', label: 'VIIRS NOAA-20 (NRT)' }, { value: 'MODIS_NRT', label: 'MODIS (NRT)' }]} />
up({ firms: { ...env.firms, day_range: v } })} min={1} max={10} />
up({ firms: { ...env.firms, confidence_min: v } })} options={[{ value: 'low', label: 'Low' }, { value: 'nominal', label: 'Nominal' }, { value: 'high', label: 'High' }]} />
up({ firms: { ...env.firms, proximity_km: v } })} step={0.5} />
{(['West', 'South', 'East', 'North'] as const).map((lbl, i) => (
{ const b = [...(env.firms.bbox || [0, 0, 0, 0])]; b[i] = v; up({ firms: { ...env.firms, bbox: b } }) }} step={0.01} />
))}
>)
}
}
const a = env as unknown as Record
const setAdapterField = (key: AdapterKey, patch: { enabled?: boolean; feed_source?: FeedSource }) => {
const cur = (env as any)[key] || {}
up({ [key]: { ...cur, ...patch } } as unknown as Partial)
}
return (
{/* Header + master enable + save bar */}
Environment
up({ enabled: v })} />
{hasChanges && (
<>
>
)}
{error &&
{error}
}
{success &&
{success}
}
{restartRequired && (
A restart is required for some changes to take effect.
)}
{/* Central Connection (v0.5) -- NATS source for adapters set to central */}
{env.central && (
Central Connection
NATS JetStream source for any adapter set to "central"
up({ central: { ...env.central!, enabled: v } })} />
up({ central: { ...env.central!, url: v } })}
placeholder="nats://central.echo6.mesh:4222" />
up({ central: { ...env.central!, durable: v } })}
placeholder="meshai-v04" />
up({ central: { ...env.central!, region: v } })}
placeholder="us.id"
helper="Central v0.9.20 region token (dotted, e.g. 'us.id'). Empty = bare wildcards (all-US firehose)." />
)}
{/* Family tabs */}
{FAMILIES.map(({ key, label, icon: Icon }) => (
))}
{/* Tracking placeholder */}
{family === 'tracking' && (
No adapters yet. ADS-B / AIS / satellite passes are planned for v0.5.
)}
{/* Mesh Health (no env adapters; central greyed for future migration) */}
{family === 'mesh' && (
Mesh Health
Node/infra telemetry — sourced from the mesh, not an environmental feed.
source
{}} disabled={false} centralDisabled={true} />
Central not available — reserved for a future migration.
)}
{/* Adapter sub-tabs + panel */}
{fam.adapters.length > 0 && activeAdapter && (
<>
{fam.adapters.length > 1 && (
{fam.adapters.map((k) => (
))}
)}
setAdapterField(activeAdapter, { enabled: v })}
feedSource={(a[activeAdapter]?.feed_source ?? 'native')}
onFeedSource={(v) => setAdapterField(activeAdapter, { feed_source: v })}
hasCentral={META[activeAdapter].hasCentral}
nativeOnly={META[activeAdapter].nativeOnly}
hasKey={META[activeAdapter].hasKey}
health={healthFor(activeAdapter)}
events={eventsFor(activeAdapter)}
>
{renderSettings(activeAdapter)}
>
)}
)
}