Commit graph

313 commits

Author SHA1 Message Date
858439c87a merge: feature/mesh-intelligence → main (SWPC standardization, fire halt/tombstone/spotting, NWS truncation + Update detection) 2026-06-10 02:41:10 +00:00
d88c6273ec nws: prefix Update: when incoming alert supersedes a prior broadcast
Add _is_update() helper that checks CAP references field against
nws_alerts table. When any referenced CAP id has last_broadcast_at
set, the wire gets an Update: prefix via _render(prefix=). Applied
in both the new-alert and cold-start-race branches.

References field shape: list of dicts with identifier key containing
the superseded CAP id (urn:oid:...).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-10 02:40:07 +00:00
860d06c4a3 fix: nws area truncation — configurable word-boundary cap (default 80)
Replace hardcoded 50-char area cap with adapter_config key
nws.area_max_chars (default 80). Truncation now appends ellipsis when
cut, matching the locations_max_chars pattern from the prior commit.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-10 02:26:31 +00:00
f5c1724567 fix: nws locations truncation — configurable word-boundary cap (default 120)
Replace hardcoded 40-char locations cap with adapter_config key
nws.locations_max_chars (default 120). Truncation now uses word-boundary
split with ellipsis, matching the swpc _trunc pattern.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-10 02:23:22 +00:00
196e19e76e feat: enable spotting detector — cold-start suppression + pull stub
Stamp last_spotting_broadcast_at on all 23 active fires before enabling
the detector, preventing false positives on first pixel after deploy.
Remove the return None stub from _check_spotting so the convex-hull
perimeter + cooldown logic runs on every attributed FIRMS pixel.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-09 18:59:51 +00:00
f4930a388f feat: fire halt broadcast + tombstone all-clear (wildfire_halted / wildfire_closed)
Enable _maybe_emit_halt in firms_handler (remove return None stub) so
fires with no FIRMS hotspot activity beyond halt_minimum_seconds emit a
routine wildfire_halted broadcast.

Add tombstone all-clear in wfigs_handler: when a fire is tombstoned and
has last_broadcast_at set (i.e. previously made it to mesh), broadcast a
wildfire_closed message with acres, containment, and location. Fires that
were never broadcast are silently consumed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-09 17:45:41 +00:00
3ff819eed9 fix: swpc geomag dedup — suppress swpc_alerts/kindex double-broadcast within 10-min window per G-scale
In-memory dict keyed on scale_code (G1-G5) with 600s suppression window.
Only geomag events are deduplicated — flare and proton sub-types have no
cross-adapter overlap. Suppressed events still persist to swpc_events and
event_log (handled=0) for audit trail.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-09 14:48:35 +00:00
2947af0fe5 fix: swpc _render() — truncate line 2 message at 120-char word boundary
Adds _trunc() helper that cuts at last space before 120 chars and appends
ellipsis. Applied at extraction site and all four _render line2 branches.
Line 3 (SWPC · timestamp) is a formatted fixed string — left unchanged.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-09 14:39:39 +00:00
be9bcfded4 feat: swpc multi-line wire + GUI wired to real adapter_config keys
swpc_handler.py: rewrite _render() to multi-line format (emoji + New:
prefix + scale/type — detail line — SWPC · time tag). Extract message
and time fields from envelope for line 2/3.

Environment.tsx: replace empty SWPC panel with broadcast threshold
controls — geomag Kp floor (G1-G5), flare class floor (M1-X10),
proton pfu floor (S1-S4). Full adapter_config save/load/discard wiring.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-09 06:29:18 +00:00
2aa528ae12 docs: avy danger_level scale TODO + fix deploy instructions (no bind mount)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-09 06:23:29 +00:00
45ca536140 refactor: fire digest — tighter format, 220-byte budget, 7d freshness gate
Query now filters out 100% contained and >7d stale fires. Line format
uses county-only anchor (no Photon geocoder). Greedy 220-byte packing
ensures the digest fits in a single Meshtastic frame.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-09 05:55:22 +00:00
862d2dce42 feat: auto-cleanup stale fires — prune >7d unflagged + >30d tombstones hourly
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-09 05:52:42 +00:00
376b0dbb2d feat: add reminders_wfigs.enabled kill switch, default disabled
Adds (reminders_wfigs, enabled) to REGISTRY (default=False) and
gates _tick_adapter() on the flag — when false, wfigs fire reminders
are skipped entirely. Use the digest instead.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-09 05:45:25 +00:00
8e810d6e83 fix: enable central feed source toggle for avalanche adapter
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-09 05:32:53 +00:00
a9d4ede68e fix: nullsafe broadcast_pager_alerts in quake panel — prevent geohazards blank page
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-09 05:31:14 +00:00
5624a0bfdb feat: wire avalanche to CENTRAL_AVY — central handler + consumer routing
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-09 05:18:29 +00:00
bf5b346215 fix: avalanche wire format — use _meshai_precomposed bypass so multi-line survives composer
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-09 04:17:52 +00:00
ae884b9651 feat: avalanche multi-line wire format, danger-level re-emit, GUI panel
- store.py: add avalanche-specific elif block with danger_level rise
  detection; re-emit on level increase with _is_update flag
- avalanche.py: rewrite to_event() with multi-line wire format
  (ski emoji + New:/Update: prefix, zone, danger name/level,
  travel advice, center_id), min_danger_level floor from adapter_config
- defaults.py: add (avalanche, min_danger_level) to REGISTRY (default=3)
- Environment.tsx: structured avalanche panel with broadcast settings
  section, min danger level select (3-Considerable/4-High/5-Extreme)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-09 04:00:47 +00:00
fe6589e0e5 feat: quake multi-line wire format + GUI panel wired to real adapter_config keys
A) _render() now emits multi-line format matching Fire/Roads style:
   emoji prefix M{mag} — place / Depth · coords / TSUNAMI WARNING
B) Environment.tsx usgs_quake panel replaced — dead min_magnitude/bbox
   controls removed, wired to real adapter_config keys: global_mag_floor,
   regional_mag_floor, regional_radius_mi, escalate_mag_floor,
   broadcast_pager_alerts.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-09 03:38:46 +00:00
951fddf079 docs: park nwis_handler -- flow-only feed, no stage data on Idaho sites
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-09 03:29:53 +00:00
96f14afba8 refactor: promote WZDx to first-class adapter with own config namespace
Move work zone settings out of itd_511 into dedicated wzdx adapter:
- config.py: add WZDxConfig dataclass with feed settings
- defaults.py: migrate 3 work_zone keys to wzdx namespace (broadcast,
  min_severity, sub_types) + add ADAPTER_META entry
- incident_handler.py: work zone gate reads adapter_config.wzdx
- Environment.tsx: full WzdxConfig state/load/save/discard, native feed
  fields when feed_source!=central, broadcast settings panel

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-08 21:33:38 +00:00
dc64430394 dashboard: split WZDx Work Zones into dedicated Roads sub-tab
Add 'wzdx' adapter key with its own META entry and render block.
Move work zone controls (enable toggle, min severity, sub-types)
out of the roads511 panel into the new WZDx tab. Data still
loads/saves via /api/adapter-config/itd_511 using the existing
roads511Config state. The wzdx panel mirrors roads511 enabled and
feed_source since they share the same backend adapter.

Bundle: D045j2lq -> BiMKNe5L.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-08 21:01:36 +00:00
53decde03c 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>
2026-06-08 18:18:39 +00:00
f75bf75378 dashboard: rebuild frontend bundle (BERKejLl -> KLGUZQYL)
Includes NWS broadcast filter controls from 31c464c that were missing
from the previous bundle.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-08 17:57:14 +00:00
c6c15e03c2 central: silently drop work_zone envelopes from broadcast pipeline
consumer.py: return None immediately for work_zone/road_closure/road_incident
categories instead of routing through format_work_zone_mesh.

incident_handler.py: add work_zone kind to _parse_itd_511_incident and return
None immediately so itd_511 work_zone events never reach change-detection.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-08 17:21:01 +00:00
31c464c0ee 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>
2026-06-08 14:55:05 +00:00
fa8d89c9cd nws: replace NWSheadline with structured line 2 — Until {time} {tz} — {area}
Drop NWSheadline entirely. Build line 2 from expires_epoch (formatted
to local America/Boise time with timezone abbrev) and first areaDesc
segment (truncated to 50 chars at word boundary).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-08 14:23:18 +00:00
9de514c4bd nws: use raw NWSheadline with no casing manipulation
Remove all headline casing (title-case, sentence-case, TZ regex). Pass
the NWSheadline string through as-is from the CAP parameters, only
applying word-boundary truncation to 80 UTF-8 bytes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-08 07:18:40 +00:00
d312f10ff8 nws: sentence-case NWSheadline instead of title-case
Replace .title() with manual sentence-casing: capitalize first char,
lowercase the rest, then re-uppercase timezone abbreviations (MDT, MST,
PDT, PST, CDT, CST, EDT, EST, UTC) via regex.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-08 07:16:21 +00:00
e27d60ca49 nws: lowercase wind speed, strip trailing punctuation on locations, fix compass
Fix 1: wind.lower() so 60 MPH winds becomes 60 mph winds.
Fix 2: rstrip trailing period/comma/space from locations text.
Fix 3: bearing is direction storm moves TOWARD, not FROM — remove
the +180 flip and use (deg+22.5)/45 for correct compass bucketing.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-08 07:09:47 +00:00
ddf24e10a9 nws: truncate NWSheadline at word boundary, no ellipsis
Cut at last space before 80 UTF-8 bytes instead of hard-slicing at 77
chars with trailing dots.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-08 07:07:07 +00:00
a4ecd05c60 nws: rewrite _render() to 4-line format with SAME-branched hazard and motion
New wire format:
  L1: emoji + event type (no office name)
  L2: NWSheadline (title-cased, 80 chars) or "{event} for {area}" fallback
  L3: SAME-code-branched hazard + certainty/threat:
      TOR: on-ground/radar + damage threat
      SVR: wind/hail + radar confirmed/indicated
      FFW/FLW: hazard sentence + inferred flood cause
      Others: hazard sentence + certainty if Observed/Likely
  L4: motion (compass + mph from eventMotionDescription) + locations

Drop expires, area/county, and impact lines. Add _parse_motion() helper
for eventMotionDescription (knots -> mph conversion). Add "locations"
pattern to _parse_nws_description(). Update test to remove expires check.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-08 07:05:22 +00:00
7210de1ed9 nws: remove _SAME_INSTRUCTION and instruction line from wire format
Drop the instruction line (line 6) from NWS wire output entirely.
Remove the _SAME_INSTRUCTION dict that was added in 503c16d.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-08 06:53:47 +00:00
503c16db5e nws: add _SAME_INSTRUCTION map and tighten instruction to 40-byte limit
Short, actionable instructions keyed by SAME event code (TOR -> "Seek
shelter immediately.", SVR -> "Move indoors now.", etc.). Falls back to
the CAP instruction field when no SAME code matches. Truncates at 40
UTF-8 bytes to keep wire size compact for mesh broadcast.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-08 06:46:35 +00:00
b3105c65f5 nws: multi-line wire format with SAME emoji, NWS office, hazard/impact/instruction
Replace single-line _render() with structured 6-line format:
  L1: SAME emoji + event type + NWS office (from WMO identifier)
  L2: area (first areaDesc segment, max 60 chars)
  L3: hazard (from HAZARD.../TORNADO... or maxWindGust/maxHailSize params)
  L4: impact (from IMPACT... in description)
  L5: expires
  L6: instruction (max 80 chars)

Add module-level helpers: _SAME_EMOJI, _NWS_OFFICE_SHORT, _nws_office(),
_parse_nws_description(). Emoji prefers SAME event code, falls back to
_emoji_for_event() substring match. All _render() call sites pass d=d.

Update test to match new format (coordinates removed from wire).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-08 06:37:03 +00:00
15c414f255 fix(incident): correct mile_marker key from _enrichment to _enriched
The enrichment pipeline writes to d._enriched, not d._enrichment.
Fix both _parse_state_511_incident and _parse_itd_511_incident.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-08 05:43:49 +00:00
ff887b500c fix(incident): add lowercase direction variants to _DIRECTION_LONG
ITD 511 sometimes sends lowercase direction strings (e.g. "east"
instead of "East"). Add lowercase and abbreviated lowercase keys
so the renderer resolves them without falling through to raw echo.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-08 05:22:53 +00:00
49de5f3a86 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>
2026-06-08 05:03:23 +00:00
1e6d22ecfe feat(dashboard): add TomTom broadcast filter knobs to traffic panel
Add min_magnitude dropdown (1-4), drop_non_present and
drop_zero_magnitude toggles to the TomTom Traffic adapter card.
State loads from /api/adapter-config/tomtom_incidents on mount
and saves changed keys on save, following the same pattern as
the WFIGS and fires config panels.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-08 03:55:43 +00:00
6149900917 feat(incident): config-driven TomTom min_magnitude filter
Add tomtom_incidents.min_magnitude setting (default 4 = severe)
to adapter_config registry. Replace the hardcoded magnitude==0
drop check with a config-driven floor that silently drops any
TomTom event below the configured threshold.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-08 03:53:02 +00:00
b60ea5c5db fix(incident): remove TomTom road fallback to from field
Let road stay None when road_numbers is absent so the renderer
uses the from → to segment format instead of clobbering it with
the raw from string.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-08 03:49:12 +00:00
198827a1b0 feat(incident): render TomTom from/to segments and length in wire output
Line 2 now falls back to from_loc → to_loc when road is absent
(common for TomTom street-level incidents without road_numbers).

Line 3 renders length (meters from TomTom) as human-readable
distance (mi or m) alongside delay and lanes_affected.

Add length field to _parse_tomtom_incident return dict.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-08 03:41:30 +00:00
71fb5dcef7 feat(incident): remove render prefix, add comment field to wire output
Remove the prefix parameter from _render() and all callers — the
New:/Update: labels are no longer surfaced in the multi-line format.

Add comment field extraction to _parse_state_511_incident and
_parse_itd_511_incident return dicts. Render comment as line 3b
when it provides additional context beyond lanes_affected and is
<=140 chars, skipping duplicates.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-08 00:11:04 +00:00
bc6df56e0f feat(incident): multi-line render format with display names and direction expansion
Replace single-line _render() with structured multi-line output:
  Line 1: emoji + display name + city/state anchor
  Line 2: road + full direction (Eastbound) + mile marker
  Line 3: lanes affected + delay
  Line 4: cause (if non-default)

Add _SUB_TYPE_DISPLAY and _DIRECTION_LONG mappings. Extend
_parse_state_511_incident and _parse_itd_511_incident return dicts
with lanes_affected, cause, description, and mile_marker fields.
Add mile_marker: None to _parse_tomtom_incident for consistency.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-07 21:41:24 +00:00
d305ce65d7 feat(dispatcher): bypass cooldown for immediate-severity events
Events with severity=immediate skip the per-toggle cooldown check
entirely — they are already rate-controlled by source handler change
detection. Also set cooldown_seconds default to 0 (disabled).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-07 16:57:21 +00:00
fa5a869401 feat(dashboard): add debug endpoint to clear dispatcher cooldowns
POST /api/debug/clear-cooldowns clears both in-memory toggle
cooldown map and SQLite dispatcher_cooldowns table.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-07 16:50:45 +00:00
b859a3b86a feat(firms): disable standalone broadcasts, use WFIGS renderer for growth
Disable all standalone FIRMS broadcast paths (cluster, halt, spotting)
by inserting early return None. Growth events now use the shared WFIGS
_render() for consistent multi-line format with movement data, and set
_severity_override=immediate + _cooldown_suffix for per-fire cooldown.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-07 16:16:20 +00:00
34e2e3f040 feat(dispatcher): per-fire cooldown independence via _cooldown_suffix
Each WFIGS fire event now carries its irwin_id as _cooldown_suffix in
event.data. The dispatcher incorporates this suffix into the cooldown
key region field, giving each fire its own independent cooldown slot
instead of sharing one per (toggle, category, region).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-07 16:11:49 +00:00
f320333f5f refactor(fire_digest): replace LLM renderer with deterministic output
Remove _build_prompt, _llm_render, _terse_fallback and all LLM backend
references. render_digest() now queries the fires table directly and
builds a structured multi-line wire: header with count, up to 5 fires
with acres/containment/anchor, and a +N more overflow line.

FireDigestScheduler no longer accepts or uses llm_backend. Updated the
pipeline __init__.py call site accordingly.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-07 16:03:51 +00:00
dea86883db fix(wfigs): set all fire broadcasts to immediate severity
Fire events are already change-detected by the wfigs handler so the
grouper coalescing window adds no value and causes commit callbacks
to be lost when events are replaced. Setting severity to immediate
unconditionally bypasses the grouper entirely.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-06-07 08:20:22 +00:00