feat(env): NWS weather alerts, NOAA space weather, tropospheric ducting

- Environmental feed system with tick-based adapters
- NWS Active Alerts: polls api.weather.gov, zone-based filtering
- NOAA SWPC: Kp, SFI, R/S/G scales, band assessment, alert detection
- Tropospheric ducting: Open-Meteo GFS refractivity profile, duct classification
- !alerts command for active weather warnings
- !solar / !hf commands for RF propagation (HF + UHF ducting)
- Alert engine integration: severe weather, R3+ blackout, ducting events
- LLM context injection for weather/propagation queries
- Dashboard RF Propagation card with HF + UHF ducting display
- EnvironmentalConfig with per-feed toggles in config.yaml
This commit is contained in:
K7ZVX 2026-05-12 17:21:43 +00:00
commit 549ae4bdfb
20 changed files with 4142 additions and 2652 deletions

View file

@ -1,4 +1,4 @@
"""Environmental data API routes (Phase 1 placeholder)."""
"""Environmental data API routes."""
from fastapi import APIRouter, Request
@ -8,37 +8,70 @@ router = APIRouter(tags=["environment"])
@router.get("/env/status")
async def get_env_status(request: Request):
"""Get environmental feeds status."""
env_store = request.app.state.env_store
env_store = getattr(request.app.state, "env_store", None)
if not env_store:
return {"enabled": False, "feeds": []}
# Will be populated in Phase 1 when env_store exists
return {
"enabled": True,
"feeds": [],
"feeds": env_store.get_source_health(),
}
@router.get("/env/active")
async def get_active_env(request: Request):
"""Get active environmental conditions."""
env_store = request.app.state.env_store
"""Get active environmental events."""
env_store = getattr(request.app.state, "env_store", None)
if not env_store:
return []
# Will be populated in Phase 1
return []
return env_store.get_active()
@router.get("/env/swpc")
async def get_swpc_data(request: Request):
"""Get SWPC space weather data."""
env_store = request.app.state.env_store
env_store = getattr(request.app.state, "env_store", None)
if not env_store:
return {"enabled": False}
# Will be populated in Phase 1
return {"enabled": False}
status = env_store.get_swpc_status()
if not status:
return {"enabled": False}
return {
"enabled": True,
**status,
}
@router.get("/env/propagation")
async def get_rf_propagation(request: Request):
"""Get combined HF + UHF propagation data for dashboard."""
env_store = getattr(request.app.state, "env_store", None)
if not env_store:
return {"hf": {}, "uhf_ducting": {}}
return env_store.get_rf_propagation()
@router.get("/env/ducting")
async def get_ducting_data(request: Request):
"""Get tropospheric ducting assessment."""
env_store = getattr(request.app.state, "env_store", None)
if not env_store:
return {"enabled": False}
status = env_store.get_ducting_status()
if not status:
return {"enabled": False}
return {
"enabled": True,
**status,
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -8,8 +8,8 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
<script type="module" crossorigin src="/assets/index-DnO02g6m.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DdqEB3wX.css">
<script type="module" crossorigin src="/assets/index-CELmCk_K.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DKYlTqQ1.css">
</head>
<body>
<div id="root"></div>