feat(dashboard): bring ipaws adapter + emergency family to full GUI parity (#158)

The IPAWS civil-alert adapter shipped backend-complete but frontend-partial:
it rendered only in the generic adapter-config page and the Advanced (raw)
Data Feeds tab, and the `emergency` family was absent from the curated Data
Feeds panel, the family-settings toggles, and the MeshCore routing matrix.

Adapter (ipaws), benchmarked against firms/nws:
- Environment.tsx: add `ipaws` to AdapterKey union, EnvConfig interface,
  META (native-only, keyless), a new `emergency` FAMILIES group, PANEL_META_KEY
  (LLM toggle), and a hand-written renderSettings panel exposing base_url,
  user_agent, tick_seconds, state_fips, same_codes, exclude_weather,
  status_actual_only — with coverage-scope handling like the other adapters.
- Environment.tsx: IPAWS_DEFAULT backfill so pre-ipaws GET payloads don't crash.
- Dashboard.tsx: SOURCE_ICONS entry (Siren/IPAWS) so ipaws events aren't a slug.
- ActivityLog.tsx: TABLE_LABELS + CATEGORIES + text-hint so ipaws_alerts rows
  show labeled "Emergency" and honor the category filter.
- dispatcher.py: _SOURCE_TO_TABLE fallback ipaws -> ipaws_alerts so region-routed
  emergency sends land labeled in the audit feed (not NULL).

Family (emergency), benchmarked against fire:
- Notifications.tsx: add `emergency` to TOGGLE_FAMILY_META (Siren icon). This
  cascades to Family Settings, the Meshtastic delivery matrix, and the MeshCore
  routing matrix (the last was hardcoded to the static list and previously
  omitted emergency entirely). Backend VALID_TOGGLES/gating/categories were
  already complete — no backend family change needed.

Tests: update the _SOURCE_TO_TABLE exact-match guard and add an ipaws audit-row
parity test. Full suite 2407 passed / 6 pre-existing unrelated failures.

Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
malice 2026-07-17 11:36:13 -06:00 committed by GitHub
commit e244405230
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 207 additions and 82 deletions

View file

@ -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"
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):
"""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
expected = {
"nws": "nws_alerts",
@ -563,6 +605,7 @@ def test_source_to_table_fallback_all_native_sources(db_path):
"wzdx": "traffic_events",
"traffic": "traffic_events",
"511": "traffic_events",
"ipaws": "ipaws_alerts",
}
cfg = _build_config()
factory, _ = _mk_channel_factory()