From 31c464c0eed8fc2c10a7e3fcd862a0c9a832b697 Mon Sep 17 00:00:00 2001 From: "Matt Johnson (via Claude)" Date: Mon, 8 Jun 2026 14:55:05 +0000 Subject: [PATCH] dashboard: add NWS broadcast filter controls to Environment page Add NwsConfig adapter config (broadcast_severities, duplicate_allowed_after_seconds) with load/save/discard/change-detection wiring. When feed_source=central, hide native-only fields (User Agent, Tick Seconds) and show Broadcast Filters section with severity checkboxes and re-broadcast cooldown input. Co-Authored-By: Claude Opus 4.6 --- dashboard-frontend/src/pages/Environment.tsx | 78 ++++++++++++++++++-- 1 file changed, 72 insertions(+), 6 deletions(-) diff --git a/dashboard-frontend/src/pages/Environment.tsx b/dashboard-frontend/src/pages/Environment.tsx index bd04c6c..df69366 100644 --- a/dashboard-frontend/src/pages/Environment.tsx +++ b/dashboard-frontend/src/pages/Environment.tsx @@ -54,6 +54,11 @@ interface Roads511Config { } // TomTom adapter config shape +interface NwsConfig { + broadcast_severities: string[] + duplicate_allowed_after_seconds: number +} + interface TomtomConfig { min_magnitude: number drop_non_present: boolean @@ -254,6 +259,11 @@ export default function Environment() { enabled_sub_types: ["accident", "road_closed", "closure", "lane_closed", "vehicle_on_fire", "flooding", "debris"], }) const [roads511Original, setRoads511Original] = useState("") + const [nwsConfig, setNwsConfig] = useState({ + broadcast_severities: ["Extreme", "Severe"], + duplicate_allowed_after_seconds: 3600, + }) + const [nwsOriginal, setNwsOriginal] = useState("") useEffect(() => { @@ -327,6 +337,20 @@ export default function Environment() { } } catch { /* adapter-config optional */ } + // Load adapter-config for nws + try { + const nwsRes = await fetch("/api/adapter-config/nws") + if (nwsRes.ok) { + const nwsData = await nwsRes.json() + const cfg: NwsConfig = { + broadcast_severities: nwsData.broadcast_severities?.value ?? ["Extreme", "Severe"], + duplicate_allowed_after_seconds: nwsData.duplicate_allowed_after_seconds?.value ?? 3600, + } + setNwsConfig(cfg) + setNwsOriginal(JSON.stringify(cfg)) + } + } catch { /* adapter-config optional */ } + } catch (e) { setError(e instanceof Error ? e.message : 'Failed to load config') } finally { @@ -352,7 +376,8 @@ export default function Environment() { const hasFiresChanges = JSON.stringify(firesConfig) !== firesOriginal const hasTomtomChanges = JSON.stringify(tomtomConfig) !== tomtomOriginal const hasRoads511Changes = JSON.stringify(roads511Config) !== roads511Original - const hasChanges = hasEnvChanges || hasWfigsChanges || hasFiresChanges || hasTomtomChanges || hasRoads511Changes + const hasNwsChanges = JSON.stringify(nwsConfig) !== nwsOriginal + const hasChanges = hasEnvChanges || hasWfigsChanges || hasFiresChanges || hasTomtomChanges || hasRoads511Changes || hasNwsChanges const saveAdapterConfig = async (adapterName: string, key: string, value: unknown) => { @@ -450,6 +475,18 @@ const save = async () => { setRoads511Original(JSON.stringify(roads511Config)) } + // Save nws adapter config changes + if (hasNwsChanges) { + const orig = JSON.parse(nwsOriginal) as NwsConfig + if (JSON.stringify(nwsConfig.broadcast_severities) !== JSON.stringify(orig.broadcast_severities)) { + await saveAdapterConfig("nws", "broadcast_severities", nwsConfig.broadcast_severities) + } + if (nwsConfig.duplicate_allowed_after_seconds !== orig.duplicate_allowed_after_seconds) { + await saveAdapterConfig("nws", "duplicate_allowed_after_seconds", nwsConfig.duplicate_allowed_after_seconds) + } + setNwsOriginal(JSON.stringify(nwsConfig)) + } + setSuccess('Config saved') setTimeout(() => setSuccess(null), 3000) } catch (e) { @@ -465,6 +502,7 @@ const save = async () => { setFiresConfig(JSON.parse(firesOriginal || JSON.stringify(firesConfig))) setTomtomConfig(JSON.parse(tomtomOriginal || JSON.stringify(tomtomConfig))) setRoads511Config(JSON.parse(roads511Original || JSON.stringify(roads511Config))) + setNwsConfig(JSON.parse(nwsOriginal || JSON.stringify(nwsConfig))) } const restart = async () => { try { await fetch('/api/restart', { method: 'POST' }); setRestartRequired(false); setSuccess('Restart initiated') } @@ -490,11 +528,39 @@ const save = async () => { 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' }]} /> -
+ {env.nws.feed_source !== 'central' && ( + <> + 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' }]} /> +
+ + )} + {env.nws.feed_source === 'central' && ( +
+
Broadcast Filters
+
+
Severities to broadcast
+
+ {['Extreme', 'Severe', 'Moderate', 'Minor'].map((sev) => ( + + ))} +
+
+ setNwsConfig({ ...nwsConfig, duplicate_allowed_after_seconds: v })} + min={0} helper="Minimum seconds before the same alert ID can be re-broadcast" /> +
+ )} ) case 'swpc': return
No additional settings.
case 'ducting': return (