feat(satpass): add satellite pass GUI to Environment + Notifications pages

- Environment: add satpass adapter to Tracking tab with observers,
  min_elevation, and norad_ids controls via adapter_config API
- Notifications: add satpass family to master toggles
- Rebuild dashboard bundle

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Matt Johnson (via Claude) 2026-06-12 00:10:51 +00:00
commit 1f34f74e5a
7 changed files with 549 additions and 490 deletions

View file

@ -72,6 +72,13 @@ interface AvalancheConfig {
min_danger_level: number
}
// Satpass adapter config shape
interface SatpassConfig {
observers: string[]
min_elevation: number
norad_ids: string[]
}
// SWPC adapter config shape
interface SwpcConfig {
geomag_kp_floor: number
@ -210,7 +217,7 @@ function AdapterPanel({ title, subtitle, enabled, onEnabled, feedSource, onFeedS
}
// ---------------------------------------------------------------- families
type AdapterKey = 'nws' | 'fires' | 'firms' | 'swpc' | 'ducting' | 'traffic' | 'roads511' | 'wzdx' | 'usgs_quake' | 'usgs' | 'avalanche'
type AdapterKey = 'nws' | 'fires' | 'firms' | 'swpc' | 'ducting' | 'traffic' | 'roads511' | 'wzdx' | 'usgs_quake' | 'usgs' | 'avalanche' | 'satpass'
interface AdapterMeta { label: string; subtitle: string; health: string; hasCentral: boolean; nativeOnly: boolean; hasKey: boolean }
@ -226,6 +233,7 @@ const META: Record<AdapterKey, AdapterMeta> = {
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: true, nativeOnly: false, hasKey: true },
satpass: { label: 'Satellite Passes', subtitle: 'Observer pass alerts via Central', health: 'satpass', hasCentral: true, nativeOnly: false, hasKey: true },
}
const FAMILIES: { key: string; label: string; icon: typeof Cloud; adapters: AdapterKey[] }[] = [
@ -235,7 +243,7 @@ const FAMILIES: { key: string; label: string; icon: typeof Cloud; adapters: Adap
{ key: 'rf', label: 'RF Propagation', icon: Radio, adapters: ['swpc', 'ducting'] },
{ key: 'roads', label: 'Roads', icon: Car, adapters: ['traffic', 'roads511', 'wzdx'] },
{ key: 'geohazards', label: 'Geohazards', icon: Mountain, adapters: ['usgs_quake', 'usgs', 'avalanche'] },
{ key: 'tracking', label: 'Tracking', icon: Satellite, adapters: [] },
{ key: 'tracking', label: 'Tracking', icon: Satellite, adapters: ['satpass'] },
{ key: 'mesh', label: 'Mesh Health', icon: Activity, adapters: [] },
]
@ -301,6 +309,12 @@ export default function Environment() {
proton_pfu_floor: 10.0,
})
const [swpcOriginal, setSwpcOriginal] = useState<string>("")
const [satpassConfig, setSatpassConfig] = useState<SatpassConfig>({
observers: [],
min_elevation: 30,
norad_ids: [],
})
const [satpassOriginal, setSatpassOriginal] = useState<string>("")
useEffect(() => {
@ -431,6 +445,21 @@ export default function Environment() {
}
} catch { /* adapter-config optional */ }
// Load adapter-config for satpass
try {
const satpassRes = await fetch("/api/adapter-config/satpass")
if (satpassRes.ok) {
const satpassData = await satpassRes.json()
const cfg: SatpassConfig = {
observers: satpassData.observers?.value ?? [],
min_elevation: satpassData.min_elevation?.value ?? 30,
norad_ids: satpassData.norad_ids?.value ?? [],
}
setSatpassConfig(cfg)
setSatpassOriginal(JSON.stringify(cfg))
}
} catch { /* adapter-config optional */ }
} catch (e) {
setError(e instanceof Error ? e.message : 'Failed to load config')
} finally {
@ -460,7 +489,8 @@ export default function Environment() {
const hasNwsChanges = JSON.stringify(nwsConfig) !== nwsOriginal
const hasAvalancheChanges = JSON.stringify(avalancheConfig) !== avalancheOriginal
const hasSwpcChanges = JSON.stringify(swpcConfig) !== swpcOriginal
const hasChanges = hasEnvChanges || hasWfigsChanges || hasFiresChanges || hasTomtomChanges || hasRoads511Changes || hasWzdxChanges || hasNwsChanges || hasAvalancheChanges || hasSwpcChanges
const hasSatpassChanges = JSON.stringify(satpassConfig) !== satpassOriginal
const hasChanges = hasEnvChanges || hasWfigsChanges || hasFiresChanges || hasTomtomChanges || hasRoads511Changes || hasWzdxChanges || hasNwsChanges || hasAvalancheChanges || hasSwpcChanges || hasSatpassChanges
const saveAdapterConfig = async (adapterName: string, key: string, value: unknown) => {
@ -609,6 +639,21 @@ const save = async () => {
setSwpcOriginal(JSON.stringify(swpcConfig))
}
// Save satpass adapter config changes
if (hasSatpassChanges) {
const orig = JSON.parse(satpassOriginal) as SatpassConfig
if (JSON.stringify(satpassConfig.observers) !== JSON.stringify(orig.observers)) {
await saveAdapterConfig("satpass", "observers", satpassConfig.observers)
}
if (satpassConfig.min_elevation !== orig.min_elevation) {
await saveAdapterConfig("satpass", "min_elevation", satpassConfig.min_elevation)
}
if (JSON.stringify(satpassConfig.norad_ids) !== JSON.stringify(orig.norad_ids)) {
await saveAdapterConfig("satpass", "norad_ids", satpassConfig.norad_ids)
}
setSatpassOriginal(JSON.stringify(satpassConfig))
}
setSuccess('Config saved')
setTimeout(() => setSuccess(null), 3000)
} catch (e) {
@ -628,6 +673,7 @@ const save = async () => {
setNwsConfig(JSON.parse(nwsOriginal || JSON.stringify(nwsConfig)))
setAvalancheConfig(JSON.parse(avalancheOriginal || JSON.stringify(avalancheConfig)))
setSwpcConfig(JSON.parse(swpcOriginal || JSON.stringify(swpcConfig)))
setSatpassConfig(JSON.parse(satpassOriginal || JSON.stringify(satpassConfig)))
}
const restart = async () => {
try { await fetch('/api/restart', { method: 'POST' }); setRestartRequired(false); setSuccess('Restart initiated') }
@ -1034,6 +1080,26 @@ const save = async () => {
))}
</div>
</>)
case 'satpass': return (
<div className="space-y-6">
<div>
<div className="text-[10px] font-sans font-medium uppercase tracking-widest text-[#666] mb-3">
Pass Filters
</div>
<div className="grid grid-cols-2 gap-4">
<NumberInput label="Min Elevation (deg)" value={satpassConfig.min_elevation}
onChange={(v) => setSatpassConfig({ ...satpassConfig, min_elevation: v })}
min={0} max={90} helper="Minimum max elevation for a pass to be broadcast" />
</div>
</div>
<ListInput label="Observer Locations" value={satpassConfig.observers}
onChange={(v) => setSatpassConfig({ ...satpassConfig, observers: v })}
helper="Observer names to include (empty = all)" />
<ListInput label="NORAD IDs" value={satpassConfig.norad_ids}
onChange={(v) => setSatpassConfig({ ...satpassConfig, norad_ids: v })}
helper="NORAD catalog IDs to track (empty = all)" />
</div>
)
}
}
@ -1110,14 +1176,6 @@ const save = async () => {
)}
{/* Tracking placeholder */}
{family === 'tracking' && (
<div className="flex flex-col items-center justify-center h-[40vh] text-center">
<Satellite size={32} className="text-[#666] mb-4" />
<p className="text-[#666] max-w-md">No adapters yet. ADS-B / AIS / satellite passes are planned for v0.5.</p>
</div>
)}
{/* Mesh Health (no env adapters; central greyed for future migration) */}
{family === 'mesh' && (
<div className="border border-border p-4 space-y-3">

View file

@ -4,7 +4,7 @@ import {
Check, X, Eye as EyeIcon, EyeOff, Send, Clock, Zap,
Calendar, AlertTriangle, Copy, AlertCircle, Layers,
Wifi, WifiOff, Mail, Globe, Radio, MessageSquare,
Activity, Cloud, Flame, Car, Snowflake, Mountain, MapPin
Activity, Cloud, Flame, Car, Snowflake, Mountain, MapPin, Satellite
} from 'lucide-react'
import ChannelPicker from '@/components/ChannelPicker'
import NodePicker from '@/components/NodePicker'
@ -1422,6 +1422,7 @@ const TOGGLE_FAMILY_META: { key: string; label: string; Icon: typeof Activity }[
{ key: 'rf_propagation', label: 'RF Propagation', Icon: Radio },
{ key: 'roads', label: 'Roads', Icon: Car },
{ key: 'avalanche', label: 'Avalanche', Icon: Snowflake },
{ key: 'satpass', label: 'Satellite Passes', Icon: Satellite },
{ key: 'seismic', label: 'Seismic', Icon: Mountain },
{ key: 'tracking', label: 'Tracking', Icon: MapPin },
]

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

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-D5IfmtDv.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BC90GDxp.css">
<script type="module" crossorigin src="/assets/index-D7cLYkH6.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CB06j1ej.css">
</head>
<body>
<div id="root"></div>