mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-08-26 17:31:34 +00:00
Compare commits
1 commit
main
...
feat/ipaws
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
47d7ef7e78 |
8 changed files with 207 additions and 82 deletions
|
|
@ -52,6 +52,7 @@ const TABLE_LABELS: Record<string, string> = {
|
||||||
swpc_events: 'Space Wx',
|
swpc_events: 'Space Wx',
|
||||||
gauge_readings: 'Hydro',
|
gauge_readings: 'Hydro',
|
||||||
event_log: 'Avalanche',
|
event_log: 'Avalanche',
|
||||||
|
ipaws_alerts: 'Emergency',
|
||||||
}
|
}
|
||||||
|
|
||||||
// Text-prefix / emoji heuristics for legacy NULL-source rows.
|
// Text-prefix / emoji heuristics for legacy NULL-source rows.
|
||||||
|
|
@ -66,6 +67,7 @@ const TABLE_LABELS: Record<string, string> = {
|
||||||
// incident_handler.py (legacy) → "⚠️ Road Incident …" | "🚫 Road Closed …"
|
// incident_handler.py (legacy) → "⚠️ Road Incident …" | "🚫 Road Closed …"
|
||||||
// NOTE: more-specific prefixes must appear before shorter ones that share a leading char.
|
// NOTE: more-specific prefixes must appear before shorter ones that share a leading char.
|
||||||
const TEXT_HINTS: Array<[string, string]> = [
|
const TEXT_HINTS: Array<[string, string]> = [
|
||||||
|
['🚨', 'Emergency'], // ipaws.py immediate civil alerts (evac/AMBER/HazMat)
|
||||||
['🔥', 'Fire'],
|
['🔥', 'Fire'],
|
||||||
['🚧', 'Traffic'],
|
['🚧', 'Traffic'],
|
||||||
['⚠️ Road Incident', 'Traffic'], // legacy road-incident rows (⚠️ = U+26A0+FE0F)
|
['⚠️ Road Incident', 'Traffic'], // legacy road-incident rows (⚠️ = U+26A0+FE0F)
|
||||||
|
|
@ -115,6 +117,7 @@ const CATEGORIES = [
|
||||||
{ value: 'satpass_events', label: 'Satellite' },
|
{ value: 'satpass_events', label: 'Satellite' },
|
||||||
{ value: 'band_conditions_broadcasts', label: 'Band' },
|
{ value: 'band_conditions_broadcasts', label: 'Band' },
|
||||||
{ value: 'traffic_events', label: 'Traffic' },
|
{ value: 'traffic_events', label: 'Traffic' },
|
||||||
|
{ value: 'ipaws_alerts', label: 'Emergency' },
|
||||||
]
|
]
|
||||||
|
|
||||||
const PAGE = 100
|
const PAGE = 100
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ import {
|
||||||
Construction,
|
Construction,
|
||||||
Satellite,
|
Satellite,
|
||||||
Sun,
|
Sun,
|
||||||
|
Siren,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -357,6 +358,7 @@ const SOURCE_ICONS: Record<string, { icon: typeof Cloud; color: string; label: s
|
||||||
usgs: { icon: Droplets, color: 'text-sky-400', label: 'USGS' },
|
usgs: { icon: Droplets, color: 'text-sky-400', label: 'USGS' },
|
||||||
traffic: { icon: Car, color: 'text-[#777]', label: 'Traffic' },
|
traffic: { icon: Car, color: 'text-[#777]', label: 'Traffic' },
|
||||||
roads: { icon: Construction, color: 'text-accent-dim', label: '511' },
|
roads: { icon: Construction, color: 'text-accent-dim', label: '511' },
|
||||||
|
ipaws: { icon: Siren, color: 'text-red-500', label: 'IPAWS' },
|
||||||
}
|
}
|
||||||
|
|
||||||
// Severity badge colors (3-level system + legacy support)
|
// Severity badge colors (3-level system + legacy support)
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { useEffect, useState, type ReactNode } from 'react'
|
||||||
import {
|
import {
|
||||||
Cloud, Flame, Radio, Car, Mountain, Satellite, Activity, Server,
|
Cloud, Flame, Radio, Car, Mountain, Satellite, Activity, Server,
|
||||||
Save, RotateCcw, RefreshCw, AlertCircle, AlertTriangle, Info, Bell,
|
Save, RotateCcw, RefreshCw, AlertCircle, AlertTriangle, Info, Bell,
|
||||||
Sliders, ChevronRight,
|
Sliders, ChevronRight, Siren,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import {
|
import {
|
||||||
Toggle, TextInput, NumberInput, SelectInput, ListInput, NumberListInput,
|
Toggle, TextInput, NumberInput, SelectInput, ListInput, NumberListInput,
|
||||||
|
|
@ -33,6 +33,10 @@ interface EnvConfig {
|
||||||
roads511: { enabled: boolean; tick_seconds: number; api_key: string; base_url: string; endpoints: string[]; bbox: number[]; feed_source?: FeedSource }
|
roads511: { enabled: boolean; tick_seconds: number; api_key: string; base_url: string; endpoints: string[]; bbox: number[]; feed_source?: FeedSource }
|
||||||
wzdx: { enabled: boolean; tick_seconds: number; api_key: string; base_url: string; endpoints: string[]; bbox: number[]; states: string[]; registry_url: string; registry_ttl?: number; feed_source?: FeedSource }
|
wzdx: { enabled: boolean; tick_seconds: number; api_key: string; base_url: string; endpoints: string[]; bbox: number[]; states: string[]; registry_url: string; registry_ttl?: number; feed_source?: FeedSource }
|
||||||
firms: { enabled: boolean; tick_seconds: number; map_key: string; source: string; bbox: number[]; day_range: number; confidence_min: string; proximity_km: number; feed_source?: FeedSource }
|
firms: { enabled: boolean; tick_seconds: number; map_key: string; source: string; bbox: number[]; day_range: number; confidence_min: string; proximity_km: number; feed_source?: FeedSource }
|
||||||
|
// FEMA IPAWS-OPEN civil-alert feed (native only). Two-stage Atom+CAP fetch;
|
||||||
|
// NON-weather emergencies (evacuation, AMBER, HazMat, 911 outage, …) → the
|
||||||
|
// `emergency` notification family.
|
||||||
|
ipaws: { enabled: boolean; tick_seconds: number; base_url: string; user_agent: string; state_fips: string[]; same_codes: string[]; exclude_weather: boolean; drop_senders: string[]; status_actual_only: boolean; feed_source?: FeedSource }
|
||||||
// Native satpass (SGP4) YAML layer — drives env/satpass.py + env/tle_fetch.py.
|
// Native satpass (SGP4) YAML layer — drives env/satpass.py + env/tle_fetch.py.
|
||||||
// Distinct from the Central adapter_config/satpass layer (see SatpassConfig
|
// Distinct from the Central adapter_config/satpass layer (see SatpassConfig
|
||||||
// interface + satpassConfig state below). `observers` seeds observer_locations.
|
// interface + satpassConfig state below). `observers` seeds observer_locations.
|
||||||
|
|
@ -65,6 +69,22 @@ const SATPASS_NATIVE_DEFAULT: EnvConfig['satpass'] = {
|
||||||
feed_source: 'central',
|
feed_source: 'central',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sane defaults for the ipaws block so a GET payload predating the IPAWS
|
||||||
|
// adapter (no `environmental.ipaws`) doesn't crash the editor. Mirrors
|
||||||
|
// config.py::IPAWSConfig defaults (Idaho + neighbouring-state FIPS).
|
||||||
|
const IPAWS_DEFAULT: EnvConfig['ipaws'] = {
|
||||||
|
enabled: false,
|
||||||
|
tick_seconds: 60,
|
||||||
|
base_url: 'https://apps.fema.gov/IPAWSOPEN_EAS_SERVICE/rest',
|
||||||
|
user_agent: '',
|
||||||
|
state_fips: ['16', '53', '41', '32', '49', '56', '30'],
|
||||||
|
same_codes: [],
|
||||||
|
exclude_weather: true,
|
||||||
|
drop_senders: ['noaa.gov', 'nws', 'weather.gov'],
|
||||||
|
status_actual_only: true,
|
||||||
|
feed_source: 'native',
|
||||||
|
}
|
||||||
|
|
||||||
// WFIGS adapter config shape
|
// WFIGS adapter config shape
|
||||||
interface WfigsConfig {
|
interface WfigsConfig {
|
||||||
allowed_incident_types: string[]
|
allowed_incident_types: string[]
|
||||||
|
|
@ -262,7 +282,7 @@ function AdapterPanel({ title, subtitle, enabled, onEnabled, feedSource, onFeedS
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------- families
|
// ---------------------------------------------------------------- families
|
||||||
type AdapterKey = 'nws' | 'fires' | 'firms' | 'swpc' | 'ducting' | 'traffic' | 'roads511' | 'wzdx' | 'usgs_quake' | 'usgs' | 'avalanche' | 'satpass'
|
type AdapterKey = 'nws' | 'fires' | 'firms' | 'swpc' | 'ducting' | 'traffic' | 'roads511' | 'wzdx' | 'usgs_quake' | 'usgs' | 'avalanche' | 'satpass' | 'ipaws'
|
||||||
|
|
||||||
interface AdapterMeta { label: string; subtitle: string; health: string; hasCentral: boolean; nativeOnly: boolean; hasKey: boolean }
|
interface AdapterMeta { label: string; subtitle: string; health: string; hasCentral: boolean; nativeOnly: boolean; hasKey: boolean }
|
||||||
|
|
||||||
|
|
@ -279,6 +299,7 @@ const META: Record<AdapterKey, AdapterMeta> = {
|
||||||
usgs: { label: 'USGS Stream Gauges', subtitle: 'River and stream water levels', health: 'usgs', 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 },
|
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 },
|
satpass: { label: 'Satellite Passes', subtitle: 'Observer pass alerts via Central', health: 'satpass', hasCentral: true, nativeOnly: false, hasKey: true },
|
||||||
|
ipaws: { label: 'FEMA IPAWS civil alerts', subtitle: 'Evacuations, AMBER, HazMat, 911 outages (non-weather)', health: 'ipaws', hasCentral: false, nativeOnly: true, hasKey: false },
|
||||||
}
|
}
|
||||||
|
|
||||||
// Keyed adapters → their secret env var (matches secrets_store.SECRET_LABELS).
|
// Keyed adapters → their secret env var (matches secrets_store.SECRET_LABELS).
|
||||||
|
|
@ -299,6 +320,7 @@ const FAMILIES: { key: string; label: string; icon: typeof Cloud; adapters: Adap
|
||||||
{ key: 'rf', label: 'RF Propagation', icon: Radio, adapters: ['swpc', 'ducting'] },
|
{ key: 'rf', label: 'RF Propagation', icon: Radio, adapters: ['swpc', 'ducting'] },
|
||||||
{ key: 'roads', label: 'Roads', icon: Car, adapters: ['traffic', 'roads511', 'wzdx'] },
|
{ key: 'roads', label: 'Roads', icon: Car, adapters: ['traffic', 'roads511', 'wzdx'] },
|
||||||
{ key: 'geohazards', label: 'Geohazards', icon: Mountain, adapters: ['usgs_quake', 'usgs', 'avalanche'] },
|
{ key: 'geohazards', label: 'Geohazards', icon: Mountain, adapters: ['usgs_quake', 'usgs', 'avalanche'] },
|
||||||
|
{ key: 'emergency', label: 'Emergency', icon: Siren, adapters: ['ipaws'] },
|
||||||
{ key: 'tracking', label: 'Tracking', icon: Satellite, adapters: ['satpass'] },
|
{ key: 'tracking', label: 'Tracking', icon: Satellite, adapters: ['satpass'] },
|
||||||
{ key: 'mesh', label: 'Mesh Health', icon: Activity, adapters: [] },
|
{ key: 'mesh', label: 'Mesh Health', icon: Activity, adapters: [] },
|
||||||
{ key: 'family_settings', label: 'Family Settings', icon: Bell, adapters: [] },
|
{ key: 'family_settings', label: 'Family Settings', icon: Bell, adapters: [] },
|
||||||
|
|
@ -411,6 +433,7 @@ export default function Environment() {
|
||||||
// round-trip PUT restores them rather than dropping them).
|
// round-trip PUT restores them rather than dropping them).
|
||||||
data.satpass = { ...SATPASS_NATIVE_DEFAULT, ...(data.satpass ?? {}) }
|
data.satpass = { ...SATPASS_NATIVE_DEFAULT, ...(data.satpass ?? {}) }
|
||||||
data.wzdx = { states: ['ID'], registry_url: '', ...(data.wzdx ?? {}) }
|
data.wzdx = { states: ['ID'], registry_url: '', ...(data.wzdx ?? {}) }
|
||||||
|
data.ipaws = { ...IPAWS_DEFAULT, ...(data.ipaws ?? {}) }
|
||||||
setEnv(data)
|
setEnv(data)
|
||||||
setOriginal(JSON.stringify(data))
|
setOriginal(JSON.stringify(data))
|
||||||
|
|
||||||
|
|
@ -855,6 +878,7 @@ const save = async () => {
|
||||||
usgs_quake: 'usgs_quake',
|
usgs_quake: 'usgs_quake',
|
||||||
avalanche: 'avalanche',
|
avalanche: 'avalanche',
|
||||||
satpass: 'satpass',
|
satpass: 'satpass',
|
||||||
|
ipaws: 'ipaws',
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Notification family gating helpers ────────────────────────────────────
|
// ── Notification family gating helpers ────────────────────────────────────
|
||||||
|
|
@ -1450,6 +1474,49 @@ const save = async () => {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>)
|
</>)
|
||||||
|
case 'ipaws': return (<>
|
||||||
|
<div className="text-[11px] text-[#666]">
|
||||||
|
Keyless FEMA IPAWS-OPEN EAS feed. Broadcasts NON-weather civil emergencies
|
||||||
|
(evacuation orders, AMBER, HazMat, 911 outages, law-enforcement/shelter-in-place).
|
||||||
|
Weather CAP is dropped so it never double-broadcasts the NWS adapter.
|
||||||
|
</div>
|
||||||
|
<TextInput label="Base URL" value={env.ipaws.base_url} onChange={(v) => up({ ipaws: { ...env.ipaws, base_url: v } })}
|
||||||
|
placeholder="https://apps.fema.gov/IPAWSOPEN_EAS_SERVICE/rest"
|
||||||
|
helper="IPAWS-OPEN EAS REST root — Atom index at /feed, per-alert CAP at /eas/<id>. Point at the Conduit proxy in prod." />
|
||||||
|
<TextInput label="User Agent" value={env.ipaws.user_agent} onChange={(v) => up({ ipaws: { ...env.ipaws, user_agent: v } })}
|
||||||
|
placeholder="meshai-ipaws/1.0 (you@email.com)" helper="Sent on every FEMA request. Blank uses the built-in default." />
|
||||||
|
<NumberInput label="Tick Seconds" value={env.ipaws.tick_seconds} onChange={(v) => up({ ipaws: { ...env.ipaws, tick_seconds: v } })} min={30} />
|
||||||
|
{scopedByCoverage('ipaws') ? (
|
||||||
|
<div className="text-xs text-[#666] bg-bg-hover px-3 py-2 border border-border/50">
|
||||||
|
Region scope (state FIPS / SAME codes) is set by the{' '}
|
||||||
|
<a href="/coverage" className="text-accent hover:underline">Coverage map</a>.
|
||||||
|
</div>
|
||||||
|
) : (<>
|
||||||
|
<ListInput label="State FIPS" value={env.ipaws.state_fips} onChange={(v) => up({ ipaws: { ...env.ipaws, state_fips: v } })}
|
||||||
|
helper="Coarse pre-fetch gate — 2-digit state FIPS to keep, e.g. 16 (ID), 41 (OR), 53 (WA)" />
|
||||||
|
<ListInput label="SAME Codes" value={env.ipaws.same_codes} onChange={(v) => up({ ipaws: { ...env.ipaws, same_codes: v } })}
|
||||||
|
helper="Optional fine gate — 6-digit SAME county codes, e.g. 016001. Empty = all counties in the FIPS states." />
|
||||||
|
</>)}
|
||||||
|
<div className="border-t border-border pt-4 mt-4">
|
||||||
|
<div className="text-[10px] font-sans font-medium uppercase tracking-widest text-[#666] mb-3">Broadcast Filters</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<label className="flex items-center justify-between">
|
||||||
|
<span className="text-sm font-sans text-[#e0e0e0]">Exclude weather-sourced alerts</span>
|
||||||
|
<input type="checkbox" checked={env.ipaws.exclude_weather}
|
||||||
|
onChange={(e) => up({ ipaws: { ...env.ipaws, exclude_weather: e.target.checked } })}
|
||||||
|
className="w-4 h-4 accent-[#f59e0b]" />
|
||||||
|
</label>
|
||||||
|
<p className="text-xs text-[#666]">Drop NWS/NOAA-originated CAP so weather stays on the NWS adapter (no double-broadcast).</p>
|
||||||
|
<label className="flex items-center justify-between pt-2">
|
||||||
|
<span className="text-sm font-sans text-[#e0e0e0]">Actual status only</span>
|
||||||
|
<input type="checkbox" checked={env.ipaws.status_actual_only}
|
||||||
|
onChange={(e) => up({ ipaws: { ...env.ipaws, status_actual_only: e.target.checked } })}
|
||||||
|
className="w-4 h-4 accent-[#f59e0b]" />
|
||||||
|
</label>
|
||||||
|
<p className="text-xs text-[#666]">Skip Test / Exercise / System messages — broadcast only status=Actual alerts.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>)
|
||||||
case 'satpass': {
|
case 'satpass': {
|
||||||
// Armed state keys off the NATIVE enable (environmental.satpass.enabled),
|
// Armed state keys off the NATIVE enable (environmental.satpass.enabled),
|
||||||
// which is what actually gates the native SGP4 broadcaster — consistent
|
// which is what actually gates the native SGP4 broadcaster — consistent
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { useState, useEffect, useCallback } from 'react'
|
||||||
import {
|
import {
|
||||||
Save, RotateCcw, RefreshCw, Check,
|
Save, RotateCcw, RefreshCw, Check,
|
||||||
Eye as EyeIcon, EyeOff, Plus, X, Radio,
|
Eye as EyeIcon, EyeOff, Plus, X, Radio,
|
||||||
Activity, Cloud, Flame, Car, Snowflake, Mountain, MapPin, Satellite, Layers,
|
Activity, Cloud, Flame, Car, Snowflake, Mountain, MapPin, Satellite, Layers, Siren,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { useDirty } from '@/context/DirtyContext'
|
import { useDirty } from '@/context/DirtyContext'
|
||||||
|
|
||||||
|
|
@ -395,6 +395,7 @@ export const TOGGLE_FAMILY_META: FamilyMeta[] = [
|
||||||
{ key: 'mesh_health', label: 'Mesh Health', Icon: Activity },
|
{ key: 'mesh_health', label: 'Mesh Health', Icon: Activity },
|
||||||
{ key: 'weather', label: 'Weather', Icon: Cloud },
|
{ key: 'weather', label: 'Weather', Icon: Cloud },
|
||||||
{ key: 'fire', label: 'Fire', Icon: Flame },
|
{ key: 'fire', label: 'Fire', Icon: Flame },
|
||||||
|
{ key: 'emergency', label: 'Emergency', Icon: Siren },
|
||||||
{ key: 'rf_propagation', label: 'RF Propagation', Icon: Radio },
|
{ key: 'rf_propagation', label: 'RF Propagation', Icon: Radio },
|
||||||
{ key: 'roads', label: 'Roads', Icon: Car },
|
{ key: 'roads', label: 'Roads', Icon: Car },
|
||||||
{ key: 'avalanche', label: 'Avalanche', Icon: Snowflake },
|
{ key: 'avalanche', label: 'Avalanche', Icon: Snowflake },
|
||||||
|
|
|
||||||
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-dwX3LKqm.js"></script>
|
<script type="module" crossorigin src="/assets/index-FgUiz9s3.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-CTVGSJxQ.css">
|
<link rel="stylesheet" crossorigin href="/assets/index-CTVGSJxQ.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
||||||
|
|
@ -1354,6 +1354,10 @@ class Dispatcher:
|
||||||
"wzdx": "traffic_events",
|
"wzdx": "traffic_events",
|
||||||
"traffic": "traffic_events",
|
"traffic": "traffic_events",
|
||||||
"511": "traffic_events",
|
"511": "traffic_events",
|
||||||
|
# IPAWS civil alerts (env/ipaws.py, source="ipaws"). Own dedup table
|
||||||
|
# ipaws_alerts — so region-routed emergency sends land in the audit
|
||||||
|
# feed labeled "Emergency" instead of NULL/unlabeled.
|
||||||
|
"ipaws": "ipaws_alerts",
|
||||||
}
|
}
|
||||||
|
|
||||||
def _post_broadcast_commit(self, event, payload, rule, ch_type: str,
|
def _post_broadcast_commit(self, event, payload, rule, ch_type: str,
|
||||||
|
|
|
||||||
|
|
@ -554,8 +554,50 @@ def test_source_to_table_fallback_stamps_audit_row(db_path):
|
||||||
assert row["source_event_pk"] is None, "pk should be NULL for fallback path"
|
assert row["source_event_pk"] is None, "pk should be NULL for fallback path"
|
||||||
|
|
||||||
|
|
||||||
|
def test_source_to_table_fallback_stamps_ipaws_audit_row(db_path):
|
||||||
|
"""Parity guard for the IPAWS civil-alert adapter: a native ipaws event
|
||||||
|
with no _broadcast_audit must stamp source_event_table='ipaws_alerts' so
|
||||||
|
emergency sends show labeled (not NULL/unlabeled) in the Activity Log.
|
||||||
|
"""
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
from meshai.notifications.events import make_event
|
||||||
|
from meshai.notifications.pipeline.dispatcher import Dispatcher
|
||||||
|
from meshai.persistence import get_db
|
||||||
|
|
||||||
|
cfg = _build_config(cold_start_grace=0)
|
||||||
|
factory, _ = _mk_channel_factory()
|
||||||
|
d = Dispatcher(cfg, factory)
|
||||||
|
|
||||||
|
ev = make_event(
|
||||||
|
source="ipaws",
|
||||||
|
category="emergency_evacuation",
|
||||||
|
severity="immediate",
|
||||||
|
region="US-ID",
|
||||||
|
title="🚨 Evacuation Immediate",
|
||||||
|
lat=43.6, lon=-116.2,
|
||||||
|
)
|
||||||
|
|
||||||
|
rule = MagicMock()
|
||||||
|
rule.broadcast_channel = 1
|
||||||
|
rule.delivery_types = ["mesh_broadcast"]
|
||||||
|
|
||||||
|
payload = MagicMock()
|
||||||
|
payload.message = "🚨 Evacuation Immediate — test"
|
||||||
|
|
||||||
|
d._post_broadcast_commit(ev, payload, rule, "mesh_broadcast", success=True)
|
||||||
|
|
||||||
|
conn = get_db()
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT source_event_table FROM mesh_broadcasts_out ORDER BY id DESC LIMIT 1"
|
||||||
|
).fetchone()
|
||||||
|
assert row is not None, "No audit row was written"
|
||||||
|
assert row["source_event_table"] == "ipaws_alerts", (
|
||||||
|
f"Expected 'ipaws_alerts', got {row['source_event_table']!r}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_source_to_table_fallback_all_native_sources(db_path):
|
def test_source_to_table_fallback_all_native_sources(db_path):
|
||||||
"""Verify _SOURCE_TO_TABLE covers all five native adapter sources."""
|
"""Verify _SOURCE_TO_TABLE covers all native adapter sources."""
|
||||||
from meshai.notifications.pipeline.dispatcher import Dispatcher
|
from meshai.notifications.pipeline.dispatcher import Dispatcher
|
||||||
expected = {
|
expected = {
|
||||||
"nws": "nws_alerts",
|
"nws": "nws_alerts",
|
||||||
|
|
@ -563,6 +605,7 @@ def test_source_to_table_fallback_all_native_sources(db_path):
|
||||||
"wzdx": "traffic_events",
|
"wzdx": "traffic_events",
|
||||||
"traffic": "traffic_events",
|
"traffic": "traffic_events",
|
||||||
"511": "traffic_events",
|
"511": "traffic_events",
|
||||||
|
"ipaws": "ipaws_alerts",
|
||||||
}
|
}
|
||||||
cfg = _build_config()
|
cfg = _build_config()
|
||||||
factory, _ = _mk_channel_factory()
|
factory, _ = _mk_channel_factory()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue