mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-06-11 01:14:45 +02:00
itd_511: add configurable work zone broadcast gate + dashboard controls
defaults.py: add work_zone_enabled (bool, default false), work_zone_min_severity (str, default Minor), work_zone_sub_types (json, default [road_works, lane_closed, road_closed]) to itd_511. incident_handler.py: replace hardcoded work_zone return None with adapter_config-driven gate. Resolve sub_type and event_sev before the work_zone check so severity and sub-type filters apply. Non-work-zone events keep the existing min_severity / enabled_categories / enabled_sub_types filters unchanged. Environment.tsx: add work_zone_enabled, work_zone_min_severity, work_zone_sub_types to Roads511Config. Load/save/discard wired. Work Zones section in roads511 panel with enable toggle, min severity dropdown, and sub-type checkboxes (visible only when enabled). Bundle: KLGUZQYL -> D045j2lq. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f75bf75378
commit
53decde03c
5 changed files with 206 additions and 121 deletions
|
|
@ -51,6 +51,9 @@ interface Roads511Config {
|
||||||
min_severity: string
|
min_severity: string
|
||||||
enabled_categories: string[]
|
enabled_categories: string[]
|
||||||
enabled_sub_types: string[]
|
enabled_sub_types: string[]
|
||||||
|
work_zone_enabled: boolean
|
||||||
|
work_zone_min_severity: string
|
||||||
|
work_zone_sub_types: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
// TomTom adapter config shape
|
// TomTom adapter config shape
|
||||||
|
|
@ -257,6 +260,9 @@ export default function Environment() {
|
||||||
min_severity: "None",
|
min_severity: "None",
|
||||||
enabled_categories: ["incident", "closure"],
|
enabled_categories: ["incident", "closure"],
|
||||||
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"],
|
||||||
|
work_zone_enabled: false,
|
||||||
|
work_zone_min_severity: "Minor",
|
||||||
|
work_zone_sub_types: ["road_works", "lane_closed", "road_closed"],
|
||||||
})
|
})
|
||||||
const [roads511Original, setRoads511Original] = useState<string>("")
|
const [roads511Original, setRoads511Original] = useState<string>("")
|
||||||
const [nwsConfig, setNwsConfig] = useState<NwsConfig>({
|
const [nwsConfig, setNwsConfig] = useState<NwsConfig>({
|
||||||
|
|
@ -331,6 +337,9 @@ export default function Environment() {
|
||||||
min_severity: r511Data.min_severity?.value ?? "None",
|
min_severity: r511Data.min_severity?.value ?? "None",
|
||||||
enabled_categories: r511Data.enabled_categories?.value ?? ["incident", "closure"],
|
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"],
|
enabled_sub_types: r511Data.enabled_sub_types?.value ?? ["accident", "road_closed", "closure", "lane_closed", "vehicle_on_fire", "flooding", "debris"],
|
||||||
|
work_zone_enabled: r511Data.work_zone_enabled?.value ?? false,
|
||||||
|
work_zone_min_severity: r511Data.work_zone_min_severity?.value ?? "Minor",
|
||||||
|
work_zone_sub_types: r511Data.work_zone_sub_types?.value ?? ["road_works", "lane_closed", "road_closed"],
|
||||||
}
|
}
|
||||||
setRoads511Config(cfg)
|
setRoads511Config(cfg)
|
||||||
setRoads511Original(JSON.stringify(cfg))
|
setRoads511Original(JSON.stringify(cfg))
|
||||||
|
|
@ -472,6 +481,15 @@ const save = async () => {
|
||||||
if (JSON.stringify(roads511Config.enabled_sub_types) !== JSON.stringify(orig.enabled_sub_types)) {
|
if (JSON.stringify(roads511Config.enabled_sub_types) !== JSON.stringify(orig.enabled_sub_types)) {
|
||||||
await saveAdapterConfig("itd_511", "enabled_sub_types", roads511Config.enabled_sub_types)
|
await saveAdapterConfig("itd_511", "enabled_sub_types", roads511Config.enabled_sub_types)
|
||||||
}
|
}
|
||||||
|
if (roads511Config.work_zone_enabled !== orig.work_zone_enabled) {
|
||||||
|
await saveAdapterConfig("itd_511", "work_zone_enabled", roads511Config.work_zone_enabled)
|
||||||
|
}
|
||||||
|
if (roads511Config.work_zone_min_severity !== orig.work_zone_min_severity) {
|
||||||
|
await saveAdapterConfig("itd_511", "work_zone_min_severity", roads511Config.work_zone_min_severity)
|
||||||
|
}
|
||||||
|
if (JSON.stringify(roads511Config.work_zone_sub_types) !== JSON.stringify(orig.work_zone_sub_types)) {
|
||||||
|
await saveAdapterConfig("itd_511", "work_zone_sub_types", roads511Config.work_zone_sub_types)
|
||||||
|
}
|
||||||
setRoads511Original(JSON.stringify(roads511Config))
|
setRoads511Original(JSON.stringify(roads511Config))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -726,6 +744,46 @@ const save = async () => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="border-t border-slate-700/50 pt-4 mt-4">
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<div className="text-xs font-medium text-slate-400 uppercase tracking-wider">Work Zones</div>
|
||||||
|
<label className="flex items-center gap-2 cursor-pointer">
|
||||||
|
<span className="text-sm text-slate-300">Enable</span>
|
||||||
|
<input type="checkbox" checked={roads511Config.work_zone_enabled}
|
||||||
|
onChange={(e) => setRoads511Config({...roads511Config, work_zone_enabled: e.target.checked})}
|
||||||
|
className="w-4 h-4 rounded accent-blue-500" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{roads511Config.work_zone_enabled && (
|
||||||
|
<div className="space-y-3">
|
||||||
|
<div>
|
||||||
|
<label className="text-xs text-slate-400 mb-1 block">Min Severity</label>
|
||||||
|
<select
|
||||||
|
value={roads511Config.work_zone_min_severity}
|
||||||
|
onChange={(e) => setRoads511Config({...roads511Config, work_zone_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>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div className="text-xs text-slate-400 mb-2">Sub-types</div>
|
||||||
|
<div className="flex gap-6">
|
||||||
|
{([['road_works', 'Road Works'], ['lane_closed', 'Lane Closure'], ['road_closed', 'Road Closed']] as const).map(([val, label]) => (
|
||||||
|
<label key={val} className="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input type="checkbox" checked={roads511Config.work_zone_sub_types.includes(val)}
|
||||||
|
onChange={(e) => { const cur = roads511Config.work_zone_sub_types; setRoads511Config({...roads511Config, work_zone_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>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</>)
|
</>)
|
||||||
case 'firms': return (<>
|
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/" />
|
<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,7 +196,7 @@ REGISTRY: dict[tuple[str, str], dict[str, Any]] = {
|
||||||
},
|
},
|
||||||
|
|
||||||
# =================================================================
|
# =================================================================
|
||||||
# ITD_511 -- 3 settings (severity gate, category filter, sub-type filter)
|
# ITD_511 -- 6 settings (severity gate, category filter, sub-type filter, work zone)
|
||||||
# =================================================================
|
# =================================================================
|
||||||
("itd_511", "min_severity"): {
|
("itd_511", "min_severity"): {
|
||||||
"default": "None",
|
"default": "None",
|
||||||
|
|
@ -213,6 +213,21 @@ REGISTRY: dict[tuple[str, str], dict[str, Any]] = {
|
||||||
"type": "json",
|
"type": "json",
|
||||||
"description": "Which sub_types to broadcast. Empty list = all.",
|
"description": "Which sub_types to broadcast. Empty list = all.",
|
||||||
},
|
},
|
||||||
|
("itd_511", "work_zone_enabled"): {
|
||||||
|
"default": False,
|
||||||
|
"type": "bool",
|
||||||
|
"description": "Broadcast work zone events (road construction, lane closures). Off by default.",
|
||||||
|
},
|
||||||
|
("itd_511", "work_zone_min_severity"): {
|
||||||
|
"default": "Minor",
|
||||||
|
"type": "str",
|
||||||
|
"description": "Minimum severity to broadcast work zones: None, Minor, Major.",
|
||||||
|
},
|
||||||
|
("itd_511", "work_zone_sub_types"): {
|
||||||
|
"default": ["road_works", "lane_closed", "road_closed"],
|
||||||
|
"type": "json",
|
||||||
|
"description": "Work zone sub-types to broadcast. Empty = all.",
|
||||||
|
},
|
||||||
|
|
||||||
# =================================================================
|
# =================================================================
|
||||||
# CENTRAL consumer -- 1 setting (severity-int bucket boundaries)
|
# CENTRAL consumer -- 1 setting (severity-int bucket boundaries)
|
||||||
|
|
|
||||||
|
|
@ -399,21 +399,9 @@ def _parse_itd_511_incident(envelope: dict, category_raw: str, now: int) -> Opti
|
||||||
elif category_raw.startswith("work_zone."): kind = "work_zone"
|
elif category_raw.startswith("work_zone."): kind = "work_zone"
|
||||||
else: return None
|
else: return None
|
||||||
|
|
||||||
# Drop work_zone envelopes -- silently suppressed
|
# Resolve severity + sub_type early (needed by work_zone gate below)
|
||||||
if kind == "work_zone":
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Severity filter
|
|
||||||
min_sev = str(adapter_config.itd_511.min_severity or "None")
|
|
||||||
sev_order = {"None": 0, "Minor": 1, "Major": 2}
|
sev_order = {"None": 0, "Minor": 1, "Major": 2}
|
||||||
event_sev = d.get("itd_severity") or "None"
|
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")
|
external_id = inner.get("id")
|
||||||
if not external_id:
|
if not external_id:
|
||||||
|
|
@ -431,6 +419,30 @@ def _parse_itd_511_incident(envelope: dict, category_raw: str, now: int) -> Opti
|
||||||
"special_event": "special_event",
|
"special_event": "special_event",
|
||||||
}.get((d.get("event_type_short") or "").lower(), "incident")
|
}.get((d.get("event_type_short") or "").lower(), "incident")
|
||||||
|
|
||||||
|
# Work zone gate -- configurable via adapter_config
|
||||||
|
if kind == "work_zone":
|
||||||
|
if not adapter_config.itd_511.work_zone_enabled:
|
||||||
|
return None
|
||||||
|
# Apply severity filter
|
||||||
|
wz_min_sev = str(adapter_config.itd_511.work_zone_min_severity or "Minor")
|
||||||
|
if sev_order.get(event_sev, 0) < sev_order.get(wz_min_sev, 0):
|
||||||
|
return None
|
||||||
|
# Apply sub-type filter
|
||||||
|
wz_subs = adapter_config.itd_511.work_zone_sub_types or []
|
||||||
|
if wz_subs and sub_type not in wz_subs:
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Severity filter (non-work-zone)
|
||||||
|
if kind != "work_zone":
|
||||||
|
min_sev = str(adapter_config.itd_511.min_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
|
||||||
|
|
||||||
# Sub-type filter (applied after sub_type is resolved)
|
# Sub-type filter (applied after sub_type is resolved)
|
||||||
enabled_subs = adapter_config.itd_511.enabled_sub_types or []
|
enabled_subs = adapter_config.itd_511.enabled_sub_types or []
|
||||||
if enabled_subs and sub_type not in enabled_subs:
|
if enabled_subs and sub_type not in enabled_subs:
|
||||||
|
|
|
||||||
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.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<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">
|
<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-KLGUZQYL.js"></script>
|
<script type="module" crossorigin src="/assets/index-D045j2lq.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-Dp9XCfH-.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-Dp9XCfH-.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue