mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-06-10 17:04:45 +02:00
feat(incident): add ITD 511 severity, category, and sub-type filters
Backend: - Add itd_511.min_severity (str, default None), enabled_categories (json, default [incident, closure]), enabled_sub_types (json, default 7 common types) to adapter_config registry. - Wire _parse_itd_511_incident to gate on all three: severity ordering (None < Minor < Major), category whitelist, sub-type whitelist (empty = all pass). Dashboard: - Add Roads511Config state + API load/save/discard for itd_511. - Add Broadcast Filters section to the 511 Road Conditions panel: severity dropdown, category checkboxes, sub-type checkboxes. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
1e6d22ecfe
commit
49de5f3a86
6 changed files with 667 additions and 546 deletions
|
|
@ -46,6 +46,13 @@ interface FiresConfig {
|
|||
digest_timezone: string
|
||||
}
|
||||
|
||||
// ITD 511 adapter config shape
|
||||
interface Roads511Config {
|
||||
min_severity: string
|
||||
enabled_categories: string[]
|
||||
enabled_sub_types: string[]
|
||||
}
|
||||
|
||||
// TomTom adapter config shape
|
||||
interface TomtomConfig {
|
||||
min_magnitude: number
|
||||
|
|
@ -241,6 +248,12 @@ export default function Environment() {
|
|||
drop_zero_magnitude: true,
|
||||
})
|
||||
const [tomtomOriginal, setTomtomOriginal] = useState<string>("")
|
||||
const [roads511Config, setRoads511Config] = useState<Roads511Config>({
|
||||
min_severity: "None",
|
||||
enabled_categories: ["incident", "closure"],
|
||||
enabled_sub_types: ["accident", "road_closed", "closure", "lane_closed", "vehicle_on_fire", "flooding", "debris"],
|
||||
})
|
||||
const [roads511Original, setRoads511Original] = useState<string>("")
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -299,6 +312,21 @@ export default function Environment() {
|
|||
}
|
||||
} catch { /* adapter-config optional */ }
|
||||
|
||||
// Load adapter-config for itd_511
|
||||
try {
|
||||
const r511Res = await fetch("/api/adapter-config/itd_511")
|
||||
if (r511Res.ok) {
|
||||
const r511Data = await r511Res.json()
|
||||
const cfg: Roads511Config = {
|
||||
min_severity: r511Data.min_severity?.value ?? "None",
|
||||
enabled_categories: r511Data.enabled_categories?.value ?? ["incident", "closure"],
|
||||
enabled_sub_types: r511Data.enabled_sub_types?.value ?? ["accident", "road_closed", "closure", "lane_closed", "vehicle_on_fire", "flooding", "debris"],
|
||||
}
|
||||
setRoads511Config(cfg)
|
||||
setRoads511Original(JSON.stringify(cfg))
|
||||
}
|
||||
} catch { /* adapter-config optional */ }
|
||||
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : 'Failed to load config')
|
||||
} finally {
|
||||
|
|
@ -323,7 +351,8 @@ export default function Environment() {
|
|||
const hasWfigsChanges = JSON.stringify(wfigsConfig) !== wfigsOriginal
|
||||
const hasFiresChanges = JSON.stringify(firesConfig) !== firesOriginal
|
||||
const hasTomtomChanges = JSON.stringify(tomtomConfig) !== tomtomOriginal
|
||||
const hasChanges = hasEnvChanges || hasWfigsChanges || hasFiresChanges || hasTomtomChanges
|
||||
const hasRoads511Changes = JSON.stringify(roads511Config) !== roads511Original
|
||||
const hasChanges = hasEnvChanges || hasWfigsChanges || hasFiresChanges || hasTomtomChanges || hasRoads511Changes
|
||||
|
||||
|
||||
const saveAdapterConfig = async (adapterName: string, key: string, value: unknown) => {
|
||||
|
|
@ -406,6 +435,21 @@ const save = async () => {
|
|||
setTomtomOriginal(JSON.stringify(tomtomConfig))
|
||||
}
|
||||
|
||||
// Save itd_511 adapter config changes
|
||||
if (hasRoads511Changes) {
|
||||
const orig = JSON.parse(roads511Original) as Roads511Config
|
||||
if (roads511Config.min_severity !== orig.min_severity) {
|
||||
await saveAdapterConfig("itd_511", "min_severity", roads511Config.min_severity)
|
||||
}
|
||||
if (JSON.stringify(roads511Config.enabled_categories) !== JSON.stringify(orig.enabled_categories)) {
|
||||
await saveAdapterConfig("itd_511", "enabled_categories", roads511Config.enabled_categories)
|
||||
}
|
||||
if (JSON.stringify(roads511Config.enabled_sub_types) !== JSON.stringify(orig.enabled_sub_types)) {
|
||||
await saveAdapterConfig("itd_511", "enabled_sub_types", roads511Config.enabled_sub_types)
|
||||
}
|
||||
setRoads511Original(JSON.stringify(roads511Config))
|
||||
}
|
||||
|
||||
setSuccess('Config saved')
|
||||
setTimeout(() => setSuccess(null), 3000)
|
||||
} catch (e) {
|
||||
|
|
@ -420,6 +464,7 @@ const save = async () => {
|
|||
setWfigsConfig(JSON.parse(wfigsOriginal || JSON.stringify(wfigsConfig)))
|
||||
setFiresConfig(JSON.parse(firesOriginal || JSON.stringify(firesConfig)))
|
||||
setTomtomConfig(JSON.parse(tomtomOriginal || JSON.stringify(tomtomConfig)))
|
||||
setRoads511Config(JSON.parse(roads511Original || JSON.stringify(roads511Config)))
|
||||
}
|
||||
const restart = async () => {
|
||||
try { await fetch('/api/restart', { method: 'POST' }); setRestartRequired(false); setSuccess('Restart initiated') }
|
||||
|
|
@ -571,6 +616,50 @@ const save = async () => {
|
|||
<NumberInput key={lbl} label={lbl} value={env.roads511.bbox?.[i] ?? 0} onChange={(v) => { const b = [...(env.roads511.bbox || [0, 0, 0, 0])]; b[i] = v; up({ roads511: { ...env.roads511, bbox: b } }) }} step={0.01} />
|
||||
))}
|
||||
</div>
|
||||
<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="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-xs text-slate-400 mb-1 block">Minimum Severity</label>
|
||||
<select
|
||||
value={roads511Config.min_severity}
|
||||
onChange={(e) => setRoads511Config({...roads511Config, min_severity: e.target.value})}
|
||||
className="w-full bg-slate-900 border border-slate-700 rounded px-3 py-2 text-sm"
|
||||
>
|
||||
<option value="None">None (all)</option>
|
||||
<option value="Minor">Minor+</option>
|
||||
<option value="Major">Major only</option>
|
||||
</select>
|
||||
<p className="text-xs text-slate-500 mt-1">Drop ITD 511 events below this severity</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<div className="text-xs text-slate-400 mb-2">Categories</div>
|
||||
<div className="flex gap-6">
|
||||
{([['incident', 'Incident'], ['closure', 'Closure'], ['special_event', 'Special Event']] as const).map(([val, label]) => (
|
||||
<label key={val} className="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" checked={roads511Config.enabled_categories.includes(val)}
|
||||
onChange={(e) => { const cur = roads511Config.enabled_categories; setRoads511Config({...roads511Config, enabled_categories: e.target.checked ? [...cur, val] : cur.filter(c => c !== val)}) }}
|
||||
className="w-4 h-4 rounded accent-blue-500" />
|
||||
<span className="text-sm text-slate-300">{label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<div className="text-xs text-slate-400 mb-2">Sub-types</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{([['accident', 'Crash'], ['road_closed', 'Road Closed'], ['lane_closed', 'Lane Closure'], ['vehicle_on_fire', 'Vehicle Fire'], ['flooding', 'Flooding'], ['debris', 'Debris'], ['road_works', 'Road Works'], ['disabled_vehicle', 'Disabled Vehicle']] as const).map(([val, label]) => (
|
||||
<label key={val} className="flex items-center gap-2 cursor-pointer">
|
||||
<input type="checkbox" checked={roads511Config.enabled_sub_types.includes(val)}
|
||||
onChange={(e) => { const cur = roads511Config.enabled_sub_types; setRoads511Config({...roads511Config, enabled_sub_types: e.target.checked ? [...cur, val] : cur.filter(s => s !== val)}) }}
|
||||
className="w-4 h-4 rounded accent-blue-500" />
|
||||
<span className="text-sm text-slate-300">{label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>)
|
||||
case 'firms': return (<>
|
||||
<TextInput label="MAP Key" value={env.firms.map_key} onChange={(v) => 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/" />
|
||||
|
|
|
|||
|
|
@ -196,8 +196,23 @@ REGISTRY: dict[tuple[str, str], dict[str, Any]] = {
|
|||
},
|
||||
|
||||
# =================================================================
|
||||
# ITD_511 -- 0 settings (its maps/emoji/phrases are CODE, not config)
|
||||
# ITD_511 -- 3 settings (severity gate, category filter, sub-type filter)
|
||||
# =================================================================
|
||||
("itd_511", "min_severity"): {
|
||||
"default": "None",
|
||||
"type": "str",
|
||||
"description": "Minimum itd_511 severity to broadcast. Options: None, Minor, Major. Events below this are dropped.",
|
||||
},
|
||||
("itd_511", "enabled_categories"): {
|
||||
"default": ["incident", "closure"],
|
||||
"type": "json",
|
||||
"description": "Which event categories to broadcast: incident, closure, special_event.",
|
||||
},
|
||||
("itd_511", "enabled_sub_types"): {
|
||||
"default": ["accident", "road_closed", "closure", "lane_closed", "vehicle_on_fire", "flooding", "debris"],
|
||||
"type": "json",
|
||||
"description": "Which sub_types to broadcast. Empty list = all.",
|
||||
},
|
||||
|
||||
# =================================================================
|
||||
# CENTRAL consumer -- 1 setting (severity-int bucket boundaries)
|
||||
|
|
|
|||
|
|
@ -398,6 +398,18 @@ def _parse_itd_511_incident(envelope: dict, category_raw: str, now: int) -> Opti
|
|||
elif category_raw.startswith("special_event."): kind = "special_event"
|
||||
else: return None
|
||||
|
||||
# Severity filter
|
||||
min_sev = str(adapter_config.itd_511.min_severity or "None")
|
||||
sev_order = {"None": 0, "Minor": 1, "Major": 2}
|
||||
event_sev = d.get("itd_severity") or "None"
|
||||
if sev_order.get(event_sev, 0) < sev_order.get(min_sev, 0):
|
||||
return None
|
||||
|
||||
# Category filter
|
||||
enabled_cats = adapter_config.itd_511.enabled_categories or []
|
||||
if enabled_cats and kind not in enabled_cats:
|
||||
return None
|
||||
|
||||
external_id = inner.get("id")
|
||||
if not external_id:
|
||||
return None
|
||||
|
|
@ -414,6 +426,11 @@ def _parse_itd_511_incident(envelope: dict, category_raw: str, now: int) -> Opti
|
|||
"special_event": "special_event",
|
||||
}.get((d.get("event_type_short") or "").lower(), "incident")
|
||||
|
||||
# Sub-type filter (applied after sub_type is resolved)
|
||||
enabled_subs = adapter_config.itd_511.enabled_sub_types or []
|
||||
if enabled_subs and sub_type not in enabled_subs:
|
||||
return None
|
||||
|
||||
ge = (d.get("_enriched") or {}).get("geocoder") or {}
|
||||
|
||||
return {
|
||||
|
|
|
|||
543
meshai/dashboard/static/assets/index-BERKejLl.js
Normal file
543
meshai/dashboard/static/assets/index-BERKejLl.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
|
@ -8,7 +8,7 @@
|
|||
<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-CAtLxxSd.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-BERKejLl.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Dp9XCfH-.css">
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue