mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-06-11 09:24:44 +02:00
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 <noreply@anthropic.com>
This commit is contained in:
parent
fa8d89c9cd
commit
31c464c0ee
1 changed files with 72 additions and 6 deletions
|
|
@ -54,6 +54,11 @@ interface Roads511Config {
|
||||||
}
|
}
|
||||||
|
|
||||||
// TomTom adapter config shape
|
// TomTom adapter config shape
|
||||||
|
interface NwsConfig {
|
||||||
|
broadcast_severities: string[]
|
||||||
|
duplicate_allowed_after_seconds: number
|
||||||
|
}
|
||||||
|
|
||||||
interface TomtomConfig {
|
interface TomtomConfig {
|
||||||
min_magnitude: number
|
min_magnitude: number
|
||||||
drop_non_present: boolean
|
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"],
|
enabled_sub_types: ["accident", "road_closed", "closure", "lane_closed", "vehicle_on_fire", "flooding", "debris"],
|
||||||
})
|
})
|
||||||
const [roads511Original, setRoads511Original] = useState<string>("")
|
const [roads511Original, setRoads511Original] = useState<string>("")
|
||||||
|
const [nwsConfig, setNwsConfig] = useState<NwsConfig>({
|
||||||
|
broadcast_severities: ["Extreme", "Severe"],
|
||||||
|
duplicate_allowed_after_seconds: 3600,
|
||||||
|
})
|
||||||
|
const [nwsOriginal, setNwsOriginal] = useState<string>("")
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -327,6 +337,20 @@ export default function Environment() {
|
||||||
}
|
}
|
||||||
} catch { /* adapter-config optional */ }
|
} 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) {
|
} catch (e) {
|
||||||
setError(e instanceof Error ? e.message : 'Failed to load config')
|
setError(e instanceof Error ? e.message : 'Failed to load config')
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -352,7 +376,8 @@ export default function Environment() {
|
||||||
const hasFiresChanges = JSON.stringify(firesConfig) !== firesOriginal
|
const hasFiresChanges = JSON.stringify(firesConfig) !== firesOriginal
|
||||||
const hasTomtomChanges = JSON.stringify(tomtomConfig) !== tomtomOriginal
|
const hasTomtomChanges = JSON.stringify(tomtomConfig) !== tomtomOriginal
|
||||||
const hasRoads511Changes = JSON.stringify(roads511Config) !== roads511Original
|
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) => {
|
const saveAdapterConfig = async (adapterName: string, key: string, value: unknown) => {
|
||||||
|
|
@ -450,6 +475,18 @@ const save = async () => {
|
||||||
setRoads511Original(JSON.stringify(roads511Config))
|
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')
|
setSuccess('Config saved')
|
||||||
setTimeout(() => setSuccess(null), 3000)
|
setTimeout(() => setSuccess(null), 3000)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -465,6 +502,7 @@ const save = async () => {
|
||||||
setFiresConfig(JSON.parse(firesOriginal || JSON.stringify(firesConfig)))
|
setFiresConfig(JSON.parse(firesOriginal || JSON.stringify(firesConfig)))
|
||||||
setTomtomConfig(JSON.parse(tomtomOriginal || JSON.stringify(tomtomConfig)))
|
setTomtomConfig(JSON.parse(tomtomOriginal || JSON.stringify(tomtomConfig)))
|
||||||
setRoads511Config(JSON.parse(roads511Original || JSON.stringify(roads511Config)))
|
setRoads511Config(JSON.parse(roads511Original || JSON.stringify(roads511Config)))
|
||||||
|
setNwsConfig(JSON.parse(nwsOriginal || JSON.stringify(nwsConfig)))
|
||||||
}
|
}
|
||||||
const restart = async () => {
|
const restart = async () => {
|
||||||
try { await fetch('/api/restart', { method: 'POST' }); setRestartRequired(false); setSuccess('Restart initiated') }
|
try { await fetch('/api/restart', { method: 'POST' }); setRestartRequired(false); setSuccess('Restart initiated') }
|
||||||
|
|
@ -490,11 +528,39 @@ const save = async () => {
|
||||||
switch (key) {
|
switch (key) {
|
||||||
case 'nws': return (<>
|
case 'nws': return (<>
|
||||||
<ListInput label="NWS Zones" value={env.nws_zones} onChange={(v) => up({ nws_zones: v })} helper="Zone IDs like IDZ016, IDZ030" infoLink="https://www.weather.gov/pimar/PubZone" />
|
<ListInput label="NWS Zones" value={env.nws_zones} onChange={(v) => up({ nws_zones: v })} helper="Zone IDs like IDZ016, IDZ030" infoLink="https://www.weather.gov/pimar/PubZone" />
|
||||||
<TextInput label="User Agent" value={env.nws.user_agent} onChange={(v) => up({ nws: { ...env.nws, user_agent: v } })} placeholder="(MeshAI, you@email.com)" helper="Format: (app_name, contact_email)" />
|
{env.nws.feed_source !== 'central' && (
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<>
|
||||||
<NumberInput label="Tick Seconds" value={env.nws.tick_seconds} onChange={(v) => up({ nws: { ...env.nws, tick_seconds: v } })} min={30} />
|
<TextInput label="User Agent" value={env.nws.user_agent} onChange={(v) => up({ nws: { ...env.nws, user_agent: v } })} placeholder="(MeshAI, you@email.com)" helper="Format: (app_name, contact_email)" />
|
||||||
<SelectInput label="Min Severity" value={env.nws.severity_min} onChange={(v) => up({ nws: { ...env.nws, severity_min: v } })} options={[{ value: 'minor', label: 'Minor' }, { value: 'moderate', label: 'Moderate' }, { value: 'severe', label: 'Severe' }, { value: 'extreme', label: 'Extreme' }]} />
|
<div className="grid grid-cols-2 gap-4">
|
||||||
</div>
|
<NumberInput label="Tick Seconds" value={env.nws.tick_seconds} onChange={(v) => up({ nws: { ...env.nws, tick_seconds: v } })} min={30} />
|
||||||
|
<SelectInput label="Min Severity" value={env.nws.severity_min} onChange={(v) => up({ nws: { ...env.nws, severity_min: v } })} options={[{ value: 'minor', label: 'Minor' }, { value: 'moderate', label: 'Moderate' }, { value: 'severe', label: 'Severe' }, { value: 'extreme', label: 'Extreme' }]} />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{env.nws.feed_source === 'central' && (
|
||||||
|
<div className="border-t border-slate-700/50 pt-4 mt-4">
|
||||||
|
<div className="text-xs font-medium text-slate-400 uppercase tracking-wider mb-3">Broadcast Filters</div>
|
||||||
|
<div className="mb-3">
|
||||||
|
<div className="text-xs text-slate-400 mb-2">Severities to broadcast</div>
|
||||||
|
<div className="flex gap-6">
|
||||||
|
{['Extreme', 'Severe', 'Moderate', 'Minor'].map((sev) => (
|
||||||
|
<label key={sev} className="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="checkbox" checked={nwsConfig.broadcast_severities.includes(sev)}
|
||||||
|
onChange={(e) => {
|
||||||
|
const cur = nwsConfig.broadcast_severities
|
||||||
|
setNwsConfig({ ...nwsConfig, broadcast_severities: e.target.checked ? [...cur, sev] : cur.filter(s => s !== sev) })
|
||||||
|
}}
|
||||||
|
className="w-4 h-4 rounded accent-blue-500" />
|
||||||
|
<span className="text-sm text-slate-300">{sev}</span>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<NumberInput label="Re-broadcast Cooldown (seconds)" value={nwsConfig.duplicate_allowed_after_seconds}
|
||||||
|
onChange={(v) => setNwsConfig({ ...nwsConfig, duplicate_allowed_after_seconds: v })}
|
||||||
|
min={0} helper="Minimum seconds before the same alert ID can be re-broadcast" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</>)
|
</>)
|
||||||
case 'swpc': return <div className="text-xs text-slate-500">No additional settings.</div>
|
case 'swpc': return <div className="text-xs text-slate-500">No additional settings.</div>
|
||||||
case 'ducting': return (
|
case 'ducting': return (
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue