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>
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>
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>
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>
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>
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>
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>
- Add 'Central' as first tab in FAMILIES array (before Weather)
- Import Server icon from lucide-react
- Remove Central Connection card from header area
- Render Central config in its own tab panel (no adapter sub-tabs)
The Central tab now shows URL, Durable, and Region fields with the
enabled toggle, matching the previous inline card behavior.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Rework the NIFC Fire Perimeters tab to show all fire config in one place:
- Native polling settings (tick_seconds, state) shown only when native
- Broadcast Settings section (always shown):
- Broadcast on acres increase toggle
- Broadcast on containment increase toggle
- Update cooldown (hours, converted from cooldown_seconds)
- Freshness window (hours, 0 = disabled)
- Daily Digest section (always shown):
- Digest enabled toggle
- Schedule times (HH:MM list)
Changes persist to adapter_config table via PUT /api/adapter-config/{adapter}/{key}.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
All three approved tiers in one commit. Reference.tsx is the deep docs
hub (8 new sections); the 10 other pages get short helper text +
tooltips that cross-reference back into Reference; 3 components get
operational-context tooltips. No new features land here -- this is the
copy that catches the GUI up to v0.6 + v0.7 system behavior.
Decisions applied per Matt's call:
- Keep both bang commands AND the LLM DM path (bangs are short on a
mesh-constrained interface; LLM is the anything-else path). Cross-
references between the two land in Reference -> Commands and
Reference -> LLM DM Queries.
- Rename "wire-string rendering" to "broadcast text" in user-facing
copy on TownAnchors.tsx, GaugeSites.tsx, and the Curation section of
Reference.tsx.
- Keep the "AND-model anti-pattern" tooltip as-is on Environment.tsx +
GaugeSites.tsx (specificity is the value for advanced users); the
OR-not-AND Reference section is its home definition that other
tooltips can link to.
Ham terminology preserved:
- Reference.tsx solar/Kp section retains "Quiet sun" / "Quiet HF
conditions" language (SFI/Kp vocabulary, not the deleted Quiet Hours
feature -- confirmed via direct grep before writing).
Tier 1: Reference.tsx (the depth doc) -- 8 new sections, ordered for
readability:
- "Fire Tracker (Fusion)": Phases 1-4 unified. Six fire-family alert
categories with example wire strings (wildfire_declared,
wildfire_growth, wildfire_halted, wildfire_spotting,
unattributed_hotspot_cluster, wildfire_incident). Attribution
mechanics (spread_radius_mi default, centroid as 24h median).
Movement mechanics (pass_id bucketing, per-pass centroid, 8-way
bearing, mi/h drift). Spotting mechanics (convex-hull perimeter +
vertex-distance approximation + per-fire cooldown). Daily LLM digest
(twice-daily summary broadcaster). The 10 fires.* adapter_config
knobs with defaults.
- "Broadcast Types": the three prefix categories -- New: (first sight),
Update: (material change), Active: (clock-driven reminder).
- "Reminder System": cadences per adapter (WFIGS 8h, SWPC 8h, ITD 511
per-zone). The tombstone (fires.tombstoned_at) termination. The
per-adapter reminder_enabled flag.
- "LLM DM (Natural-Language Queries)": all 7 env_reporter adapter
blocks (build_fires_detail / build_alerts_detail / build_quakes_detail
/ build_traffic_detail / build_gauges_detail / build_swpc_detail /
build_drop_audit) with example questions that hit each one. The
grounding clause behavior ("No active X right now" when an adapter
block is empty -- the v0.7-fire-tracker-4-final clamp). The
include_in_llm_context per-adapter toggle.
- "OR-not-AND Architecture": the per-adapter Central vs native
contract. Mutually exclusive. The AND-mode anti-pattern definition
(referenced by the Environment + GaugeSites tooltips). The Spokane
fix context.
- "Adapter Config & the CODE Rule": the GUI knob hub. The CONFIG-vs-
CODE split (thresholds in CONFIG, sentence templates / emoji /
translation maps in CODE). Restart-required vs live keys. The
include_in_llm_context toggle.
- "Curation: Gauges & Towns": Gauge Sites (NWS-AHPS thresholds, USGS
lookup, Action/Minor/Moderate/Major). Town Anchors (broadcast text
suffix lookup chain: Photon -> this table -> landclass -> county
-> coords). Example output "3 mi N of Almo".
- "Schema Migrations": light touch. v11-v16 schema additions tagged
with the phase they shipped under.
Tier 2: per-page tooltips and cross-references (10 pages):
- AdapterConfig.tsx: header paragraph extended with the CODE rule
pointer + LLM context toggle explanation.
- Alerts.tsx: !subscribe blurb extended with the three broadcast types
and links to Reference -> Broadcast Types + Reminder System.
- Config.tsx: environmental section description updated to point at
Environment.tsx for adapter knobs + Reference -> OR-not-AND for the
architecture.
- Dashboard.tsx: RF Propagation title carries SWPC R/S/G + Kp legend
tooltip; LOCAL badge defines what counts as local.
- Environment.tsx: Central region-token helper now references the
OR-not-AND section; tick_seconds defined inline as the native-mode
poll interval.
- GaugeSites.tsx: page description rewritten -- replaces "envelope
time" jargon with operational language, explains USGS lookup
mechanics, points at Reference -> OR-not-AND for the central-feed
disable.
- Mesh.tsx: Topology + Geographic buttons get tooltips defining the
rendering model.
- Notifications.tsx: band-conditions block extended with the daily
fire digest pointer + Reference -> Fire Tracker + Broadcast Types
cross-refs.
- TownAnchors.tsx: page description rewritten -- "wire-string
rendering" -> "broadcast text", chain fallback explained ("Photon
-> this table -> landclass -> county/state -> coords"), example
output included.
Tier 3: component tooltip polish (3 components):
- NodeTable.tsx: Battery + Last Heard column headers get title-bearing
spans with the voltage chart + offline-threshold legend.
- NodeDetail.tsx: SNR quality bands documented as a comment in the
neighbor render block (the legend lives next to where the colored
quality dots are computed).
- RestartBanner.tsx: banner copy extended with the restart-required
catalog (Config -> environmental, LLM backend swap, dispatcher
cold-start grace) so operators know what touched it.
Build verification:
- tsc + vite build green (one warning about chunk size > 500kB --
pre-existing).
- All 8 new TOPICS ids resolve in the served bundle:
adapter-config, broadcast-types, curation, fire-tracker,
llm-dm, or-not-and, reminders, schema.
- Distinctive new strings present in the bundle ("3 mi N of Almo",
"Photon nearest-town", "AND-mode anti-pattern", "R (Radio Blackouts").
- "Quiet sun" preserved (the ham SFI/Kp vocabulary in the Solar
section, not the deleted Quiet Hours feature).
- Container Up healthy, 0 tracebacks in 2 min post-rebuild.
Changelog: v0.7-docs-rewrite.md (per-page strip / rewrite / add table).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Gap 1 -- env_routes.lookup_usgs_site no longer creates a temporary
USGSStreamsAdapter to hit USGS.gov directly. When the env_store has no
native usgs adapter (because usgs.feed_source != native), the endpoint
returns HTTP 404 with a body that says "site lookup unavailable in
central-feed mode; values must be entered manually or sourced from
Central". This closes the AND-mode anti-pattern Central's v0.10.2
report flagged: meshai was in central-feed mode for usgs but the
lookup helper would still call USGS.gov directly the first time the
dashboard opened the Add-Gauge form.
Gap 2 -- config_routes.RESTART_REQUIRED_SECTIONS gains "environmental"
and the PUT handler now diffs the section before/after, returning
{saved, restart_required, changed_keys}. restart_required is true only
when there are actual changes AND the section is in the restart-required
set, so a no-op PUT to environmental never raises a false alarm.
Frontend wiring:
- New RestartBanner component (yellow top-of-main banner) listens to a
meshai:restart-required CustomEvent + cross-tab storage event,
persists across navigations via localStorage, shows changed_keys
preview + Restart-now button (POSTs /api/system/restart) + dismiss.
- Layout.tsx mounts <RestartBanner /> above {children} so it surfaces
on every page.
- Config.tsx saveSection() now calls notifyRestartRequired(changed_keys)
alongside its existing setRestartRequired(true) when the API flags
the section.
- GaugeSites.tsx probes /api/config/environmental at mount and shows a
"USGS lookup" button next to the site_id input. The button is
disabled with an explanatory tooltip when usgs.feed_source != native,
and gracefully renders the 404 detail when the API returns 404 in
central-feed mode -- enter-manually UX, no silent fallback.
Tests -- tests/test_or_arch_continuous.py (11 cases, all passing):
- USGS lookup 404 with no env_store / no native usgs adapter
- 502 on native-adapter exception
- 200 + payload on native-adapter happy path
- environmental in RESTART_REQUIRED_SECTIONS
- PUT environmental with changed feed_source -> restart_required:true
+ changed_keys list including foo.feed_source dotted path
- PUT bot (non-restart section) -> restart_required:false
- No-op PUT to bot / environmental -> restart_required:false, empty
changed_keys
- _diff_keys helper unit tests (nested dicts, list-element changes)
Why this matters: per the Spokane post-mortem and Central's v0.10.2
response, both sides need belt-and-suspenders against transient
AND-modes. meshai's static OR enforcement at env_store boot is the
runtime guard; this commit makes the GUI honor it continuously --
the lookup helper can't sneak past it any more, and the user is told
explicitly that an environmental config change does not take effect
until the container restarts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 2 (b948ed7) stripped Notifications.tsx + backend config + pipeline
checks but missed Reference.tsx, which has live documentation describing
Quiet Hours as if it still exists. Per feedback-quiet-hours-trash.md the
intent is removal, not refactor -- the Reference page tells users about
a feature that does not exist. Strip the SectionHeader Quiet Hours block
(lines 952-956) and the quiet-hours clause from the rules-evaluation
checklist (line 922). Rebuild dashboard bundle so the JS no longer
contains the strings.
Reference.tsx changes:
- Line 922 rules-evaluation checklist: drop the 'Are we in quiet hours?'
clause from the 'MeshAI checks your rules' bullet.
- Lines 952-956 deleted: the SectionHeader 'Quiet Hours' + the two
paragraphs explaining 10pm-6am suppression and the 'Override Quiet
Hours' rule flag.
Other 'Quiet' matches in Reference.tsx are SFI/Kp solar terminology
('Quiet sun', 'Quiet. Best HF conditions.') and remain untouched --
unrelated to quiet hours.
Dashboard bundle rebuilt via vite. New artefacts:
meshai/dashboard/static/assets/index-WV9oBF1j.js (2,145.58 kB)
meshai/dashboard/static/assets/index-j88L17ja.css (45.08 kB)
Verification:
grep -rln 'quiet hours|quiet_hours|quietHours|override_quiet|Override Quiet' dashboard-frontend/src
-> 0 hits
grep -ic 'quiet hours|override.quiet|quiet_hours|quietHours' index-WV9oBF1j.js
-> 0 hits
Side artefact (not in commit): independent verification of Central
v0.10.2 Spokane-class leak fix completed today; all four meshai-side
checks pass (config audit, JetStream subscribe, event_log breakdown,
mesh_broadcasts_out scan). Report at OneDrive:
v0.6-tail-2-spokane-verify.md. No meshai-side code change needed.
Closes Section A.5 (gauge_sites) and A.12 (town_anchors) of the audit
doc by lifting both Python-dict curation lists into editable SQLite
tables. Operators can add/edit/disable rows from the dashboard without
a deploy; runtime reads go through cached accessors that invalidate
when the REST API mutates state.
Schema:
v8.sql adds gauge_sites(site_id PK, gauge_name, lat, lon, action_ft,
flood_minor_ft, flood_moderate_ft, flood_major_ft, enabled, updated_at).
v9.sql adds town_anchors(anchor_id AUTOINC PK, name UNIQUE, lat, lon,
state, enabled, updated_at).
SCHEMA_VERSION 7 -> 9.
Seed (meshai/persistence/curation.py):
_GAUGE_SITES_SEED carries the original 9 Idaho rows from
IDAHO_CURATED_SITES verbatim.
_TOWN_ANCHORS_SEED carries the 29 Idaho-and-neighbor towns from
_TOWN_COORDS verbatim.
seed_gauge_sites() / seed_town_anchors() INSERT OR IGNORE -- safe to
re-run; never overwrites user edits.
Handler integration:
- meshai/central/idaho_gauge_sites.py: IDAHO_CURATED_SITES dict deleted.
lookup_site() now calls meshai.persistence.curation.lookup_gauge_site()
which reads the table. THRESHOLD_RANK, normalize_site_id, and
compute_threshold_state remain in this module (CODE per Matt s rule).
- meshai/central/nwis_handler.py drops IDAHO_CURATED_SITES from its
import list; the table-backed lookup_site() is API-compatible.
- meshai/central_normalizer.py: _TOWN_COORDS dict deleted.
_compute_distance_bearing() now calls
meshai.persistence.curation.lookup_town_anchor() with the same
lowercased-name semantics it always used.
REST API (meshai/dashboard/api/curation_routes.py):
/api/gauge-sites GET list, GET one, POST add, PUT update, DELETE
/api/town-anchors GET list, GET one, POST add, PUT update, DELETE
Every mutation calls invalidate_curation_cache() so handler reads see
the new state on the next call -- no container restart.
Dashboard (dashboard-frontend/src/pages/):
- GaugeSites.tsx: table view with Add row / Edit row inline / Delete
confirm + per-row enabled toggle. 8 columns mirror the schema.
- TownAnchors.tsx: same pattern, 5 columns. Name is lowercased on
save to match the lookup key.
- Left-nav entries "Gauge Sites" (Droplets icon) and "Town Anchors"
(MapPin icon) added to Layout.tsx; routes added to App.tsx.
Tests (tests/test_curation.py, 18 cases):
- v8/v9 tables exist
- Seed lands every row from both dicts
- Seed idempotent; never overwrites user edits
- lookup_gauge_site hits/miss, disabled rows are invisible
- lookup_town_anchor case-insensitive
- REST API: GET list, GET one, GET 404, POST add, PUT update, DELETE,
POST missing-field 400; both gauge_sites + town_anchors
- Accessor reflects API mutations after invalidate_curation_cache()
tests/test_nwis_handler.py back-compat: IDAHO_CURATED_SITES dict alias
points at _GAUGE_SITES_SEED so the existing assertion suite still passes.
tests/test_adapter_config_foundation.py schema_meta v7 -> v9 bump.
Test count: 797 -> 819 (+18 curation cases + 4 maintenance updates).
Closes the audit-doc Section A keystone (the GUI editor). Together with
v0.6-3a foundation, v0.6-3a.1 trim, and v0.6-3b handler wiring, every
Rule-17 CONFIG knob from the audit is now editable in the dashboard
without a container restart.
API (meshai/dashboard/api/adapter_config_routes.py):
GET /api/adapter-config -- {adapter: [{key, value, default,
type, description}]}
GET /api/adapter-config/<adapter> -- one adapter list
GET /api/adapter-config/<adapter>/<key> -- single row
PUT /api/adapter-config/<adapter>/<key> body {value} -- typed validation
int: int or whole-number float; rejects bool, fractional float, str
float: int or float; rejects bool
str: str only
bool: bool only
json: any JSON-serializable value
Every PUT calls invalidate_cache() so the next handler accessor
read sees the new value -- no container restart needed.
POST /api/adapter-config/<adapter>/<key>/reset -- value_json = default_json,
cache invalidated
GET /api/adapter-meta -- {adapter: {display_name,
include_in_llm_context, description}}
PUT /api/adapter-meta/<adapter> partial-update body, fields:
include_in_llm_context: bool, display_name: non-empty str
Dashboard (dashboard-frontend/src/pages/AdapterConfig.tsx):
- Per-adapter cards. Header row shows display_name, the include_in_llm_context
toggle, and an expand chevron. Adapters with zero config keys (e.g. itd_511)
still render so users can toggle their LLM-context inclusion.
- Expanded body lists each key with a type-aware widget:
bool -> checkbox, commit-on-change
int/float -> number input, commit-on-blur (or Enter)
str -> text input, commit-on-blur
json -> textarea, commit-on-blur (JSON.parse with inline error)
Each row shows the key name, type tag, description, "edited" badge when
value != default, a per-key Reset button, and a save badge (spinner,
check, error tooltip, or a small amber dot for unsaved local changes).
- Auto-save semantics: every blur/change/reset triggers PUT immediately;
no explicit Save button needed. Reset is one-click per key.
Wiring:
- meshai/dashboard/server.py registers the new router with prefix /api.
- dashboard-frontend/src/App.tsx adds the /adapter-config route.
- dashboard-frontend/src/components/Layout.tsx adds the left-nav entry
(Sliders icon, label "Adapter Config", after Reference).
- Vite build produces a fresh meshai/dashboard/static bundle. The
Dockerfile copies meshai/ so the new bundle ships with the container
image at next rebuild.
Tests (tests/test_adapter_config_api.py, 30 cases):
- GET grouped, per-adapter, single key
- GET per-adapter returns [] for adapters with zero keys (itd_511)
- PUT updates value, GET shows new value, accessor returns new value
(proves cache invalidation propagates to the in-process accessor)
- PUT type validation per (int, float, str, bool, json) incl. edge cases:
int rejects str + fractional float + bool but accepts whole-number float;
float accepts int + float, rejects bool; bool rejects int; str rejects
other types; json accepts list / dict / None
- PUT 404 on unknown key, 400 on missing value field
- POST reset restores default + invalidates cache
- GET /api/adapter-meta: include_in_llm_context defaults match registry
(central / geocoder false, rest true)
- PUT meta partial update: only provided fields change
- PUT meta rejects non-bool include_in_llm_context, empty display_name,
unknown adapter
Test count: 731 -> 761 (+30 API cases, 0 regressions).
Refs audit doc v0.6-phase1-audit.md Section A keystone + finding #4.
First clock-driven broadcaster in meshai, distinct from the v0.5.8b/v0.5.9/v0.5.10 event-driven adapters. The same persistence + dispatcher + cold-start patterns apply, but the trigger is the wall clock at 06:00 / 14:00 / 22:00 Mountain Time (default; GUI-configurable per Rule 17).
Components: (1) meshai/notifications/scheduled/band_conditions.py with BandConditionsScheduler (asyncio loop, mirrors the existing DigestScheduler shape), compute_band_ratings() with two-tier data sourcing -- (a) latest swpc_kindex + swpc_alerts F10.7 rows from persistence within the last 6h, (b) HamQSL.com solarxml.php fallback when SWPC is stale or incomplete, (c) silent skip when both fail, format_band_conditions_wire() multi-line MEDIUM output (~115-120B). (2) v3 schema migration adding band_conditions_broadcasts(broadcast_id PK AUTO, sent_at, scheduled_for UNIQUE, ratings_json, source). UNIQUE(scheduled_for) enforces per-slot dedup so a retry storm cannot double-broadcast. (3) Dispatcher.dispatch_scheduled_broadcast() bypasses the toggle / rules / freshness-gate pipeline but DOES honour the v0.5.8b cold-start grace -- first scheduled broadcast within the grace window after meshai starts is suppressed, mesh_broadcasts_out audit row only inserted on actual delivery. Channel selection routes through the rf_propagation toggle\'s broadcast_channel since band conditions IS RF-propagation info. (4) NotificationsConfig gains band_conditions_enabled (default true), band_conditions_schedule (list of HH:MM strings, default ["06:00","14:00","22:00"]), band_conditions_tz (default "America/Boise" so DST handles automatically). (5) Notifications.tsx grows a Band Conditions card between Cold-Start Grace and Master Toggles with the enable toggle + 3 TimeInput slots + a one-liner explaining the source priority. (6) build_pipeline + start_pipeline spawn the BandConditionsScheduler alongside the existing DigestScheduler -- best-effort, scheduler failures must NOT break notifications startup.
Wire format examples (multi-line, all under 130B target):
☀️ Day Propagation
📡 Band Conditions:
80-40m: 🟡 Fair
30-20m: 🟢 Good
17-15m: 🟢 Good
12-10m: 🟡 Fair
🌞 Day Propagation (14:00 slot when storm onset, Kp=6 SFI=110)
📡 Band Conditions:
80-40m: 🔴 Poor
30-20m: 🔴 Poor
17-15m: 🔴 Poor
12-10m: 🟡 Fair
🌙 Night Propagation (22:00 slot, recovery, Kp=4 SFI=120)
📡 Band Conditions:
80-40m: 🟡 Fair
30-20m: 🟡 Fair
17-15m: 🔴 Poor
12-10m: 🔴 Poor
Tests: was 686 (v0.5.10 baseline), now 704 (+18 net new -- quiet/storm condition ratings, HamQSL XML parse fallback, both-fail silent-skip path, is_day_slot per HH:MM, wire format for all 3 slot variants, byte-size guard, 6-line shape, fire_slot record row, dedup via UNIQUE constraint, silent-skip path, slot_epoch DST alignment summer + winter). Synthetic 24h probe verified the 3 expected slots fire correctly with quiet/storm/recovery scenarios + the 4th no-data scenario lands as source=\'skipped_no_data\' with no broadcast.
usgs_nwis deferred to v0.5.12 (threshold-curation work). Master OFF in prod.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three integrated pieces that ship together because they were designed as one safety story: (1) PERSISTENCE FOUNDATION -- new meshai/persistence/ module with SQLite db.py, schema migration framework (v1), 13 tables covering all adapter event shapes (traffic_events, fires, firms_pixels, quake_events, nws_alerts, gauge_readings, swpc_events) + mesh state (mesh_nodes, mesh_telemetry, mesh_positions, mesh_messages_in, mesh_broadcasts_out, mesh_health_events) + cross-cutting event_log + schema_meta. WAL mode for reader concurrency, single-writer pattern, MESHAI_DB_PATH env var, mounted at /data/meshai.sqlite via existing docker-compose meshai_data volume. .gitignore updated. (2) WFIGS HANDLER -- meshai/central/wfigs_handler.py implements the first per-adapter handler that uses the persistence layer. Format: MEDIUM style with town/landclass/county fallback chain, lat/lon at 3-decimal precision, New:/Update: prefix. 8h-rate-limited change-detection per IRWIN via fires.last_broadcast_at. Skips tombstones and perimeters silently (logged to event_log with handled=0). Acres fallback chain DailyAcres -> IncidentSize -> raw.DiscoveryAcres -> raw.FinalAcres -> N/A. Pass-through Initial Attack auto-numbered names (IA 1, IA 2). (3) UNIVERSAL COLD-START GRACE -- meshai/notifications/pipeline/dispatcher.py grows a configurable grace window (cold_start_grace_seconds, default 60s, GUI-editable per Rule 17). Anchored to first-event-seen (not container boot), so the grace activates the moment broadcasts could fire. Suppresses mesh delivery during the window; handler-side persistence (fires UPSERT, event_log) still happens normally. New _cold_start_dropped counter exposed in dispatch_stats(). Designed to protect against JetStream backlog spam at toggle-flip time, applies universally to ALL adapters. (4) WFIGS HANDLER CALLBACK REFACTOR -- New:/Update: prefix now keys on fires.last_broadcast_at IS NULL (not row-missing), and last_broadcast_* field updates moved to a post-broadcast commit callback that the dispatcher invokes ONLY on successful delivery. This means: cold-start-suppressed events leave fires.last_broadcast_at NULL, so when they eventually broadcast post-grace, they correctly render as New: (first ACTUAL delivery for that IRWIN), not Update:. event_log.handled and mesh_broadcasts_out audit row also gated on the same callback -- decoupling persistence rows from broadcast rows for an honest audit trail. New tests: 15 in test_wfigs_handler.py, 15 in test_persistence.py, additional cold-start grace tests in test_dispatcher.py (+4 WFIGS callback scenarios). Synthetic probes wfigs-cleaned-samples.md (initial) and wfigs-cleaned-samples-v2.md (cold-start verification) generated against isolated temp SQLite databases. CT108 /data/meshai.sqlite untouched during build. Master stays off. No live toggle flips. Test count: was 535 (v0.5.7 baseline) -> 566 (persistence) -> 581 (wfigs handler) -> 589 expected (cold-start grace).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Frontend-only polish to the Advanced Rules section in Notifications.tsx. Master Toggles (v0.5.0) and safe-mode are untouched.
FIX 1 -- Grouped Alert Categories. Replace the flat ~45-item checkbox list with a per-family grouped picker. Each family (mesh_health/weather/fire/rf_propagation/roads/avalanche/seismic/tracking) is a collapsible section with the lucide icon used by Master Toggles, a category count in the header, and per-family "All" / "Clear" bulk-toggle buttons. Families that already have selections expand by default. Categories whose toggle field does not match a known family fall into an "Other" group at the bottom. Uses the backend toggle field already provided by /api/notifications/categories.
FIX 2 -- region_scope multi-select. Adds a REGIONS block between WHEN and SEND VIA, wired to rule.region_scope (NotificationRuleConfig backend field has existed since v0.5.0). Fetches /api/regions alongside config/categories. Pill-style toggle buttons; empty selection means all regions (backward compat). region_scope added to createDefaultRule; addFromTemplate merges over createDefaultRule so future config fields do not need to be backfilled into every literal template.
FIX 3 -- truncated rule name hover tooltip. Adds title={rule.name} to the collapsed-header name span so long rule names stay readable on hover.
FIX 4 -- Smart activity badge. Replaces the unconditional "Never fired" badge with state-aware variants: gray "Disabled" when rule.enabled is false; green "Active" when fire_count > 0 and last_fired within 7 days; yellow "Idle (no recent activity)" when fire_count > 0 but last_fired > 7d ago; gray "No activity yet" when never fired (without implying breakage). Same badge Tailwind shape as before; last_fired surfaces via the title tooltip.
Backend untouched. npm run build (tsc strict) clean. PYTHONPATH=. pytest -q: 328 passed (unchanged from v0.5.5). Safe-mode preserved (master off, all toggles off, all adapters native, central disabled).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pre-v0.5.4 every Central subscription used a bare wildcard (central.wx.>,
central.fire.>, central.traffic.>, central.quake.>, central.hydro.>,
central.space.>), so a Magic Valley operator flipping nws -> central was
in fact subscribing to the all-US firehose and discarding 95% of events
locally. Central v0.9.20 (2026-05-28) added per-region subject suffixes
so the firehose can be filtered server-side. This wires meshai to use them.
Backend (meshai/central/consumer.py):
- New _subjects_for(adapter, region) replaces the static ADAPTER_SUBJECTS
dict. ADAPTER_SUBJECTS is retained as an alias to _SUBJECTS_BARE for any
legacy importers; the dispatcher path is unchanged.
- Per-adapter subject patterns (region='us.id' default):
nws -> central.wx.alert.us.id.> (region BEFORE wildcard)
usgs_quake -> central.quake.event.>.us.id (region AFTER wildcard)
firms -> central.fire.hotspot.>.us.id
fires -> central.fire.incident.id.> (state token at fixed depth)
central.fire.perimeter.id.>
traffic -> central.traffic.>.id (bare state, no us. prefix)
roads511 -> central.traffic.>.id (shared with traffic, sub-adapter routing)
usgs -> central.hydro.>.us.id
central.hydro.>.unknown (workaround until v0.9.20.1)
swpc -> central.space.> (planetary; region ignored)
- Empty/None region falls back to bare wildcards (pre-v0.9.20 behaviour).
- _subject_owned() pulls region from env.central.region and routes through
_subjects_for; v0.5.3 sub-adapter routing (owned-sources set) still
applies on shared subjects like central.traffic.>.id.
- start() logs the active region at connect-time for ops visibility.
Config (meshai/config.py):
- CentralConsumerConfig.region: str = "us.id". One region per consumer
applies to every central-flipped adapter; per-adapter overrides can
land in v0.6 when there is a real use case.
Frontend (dashboard-frontend/src/pages/Environment.tsx):
- Central Connection panel gets a Region text input next to URL/Durable.
- EnvConfig.central type extended with region: string.
- Static bundle rebuilt; index-DCFmSeOM.js -> index-B24tHcYj.js.
Tests:
- tests/test_central_region_routing.py (new, 9 cases): asserts the exact
v0.9.20 subject string for each adapter at region='us.id', the SWPC
global-stays-global rule, the USGS .unknown workaround, the empty-region
backward-compat fallback for all 8 adapters, and integration through
CentralConsumer._subject_owned() with the default region.
- tests/test_central_consumer.py + tests/test_central_sub_adapter_routing.py:
the two tests that asserted bare-wildcard subjects now set
env.central.region = "" explicitly to preserve their original concern
(no region semantics — backward-compat path only).
Why swpc stays global: space weather is planetary -- a CME is detected on
the sun, the geomagnetic response is hemispheric. There is no Idaho-only
solar event; subscribing per-region would only drop events we want.
Why hydro has the .unknown workaround: Central v0.9.20 leaves gauges
whose USGS state can't be inferred on central.hydro.>.unknown. Until
v0.9.20.1 backfills the state tag we subscribe to both filters to
avoid silently losing those rows. Idaho downstream-filtering on
data['_enriched']['usgs_site']['state'] is future v0.6 work.
Orthogonal to v0.5.2 dispatcher guards (staleness / cooldown / dedup)
and v0.5.3 sub-adapter routing: the region filter operates at the NATS
subscription layer (server-side), upstream of everything else.
Verified: pytest 327 passed (318 prior + 9 new region-routing tests);
py_compile clean; frontend build clean. Safe-mode preserved -- no toggle
enabled, no master enabled, no central enabled.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
C.2 (v0.4) marked roads511 and FIRMS native-only in the dashboard despite the
Central event bus shipping both stream families. This recovers the v0.5.1
work paused before the v0.5.2 spam fix and lands it cleanly on top of v0.5.2.
Backend (meshai/central/consumer.py):
- ADAPTER_SUBJECTS: roads511 now subscribes to central.traffic.> (shared
with the existing traffic adapter); firms widened from
central.fire.hotspot.> to central.fire.>.
- CENTRAL_ADAPTER_TO_SOURCE: three new sub-adapter remaps so the inner
envelope.adapter routes correctly inside a shared subject --
tomtom_incidents -> traffic, state_511_atis -> roads511, firms -> firms.
- New _subject_owned() -> dict[subject_filter, set[meshai_source]]:
builds the per-subject ownership set so a single central.traffic.>
subscription can be owned by {traffic, roads511} simultaneously.
- subjects() now derives from _subject_owned().keys().
- _make_cb(owned) binds the owned set per-subscription; _on_message
forwards it.
- _handle(subject, raw, owned=None) drops events whose remapped source
isn't in the owned set (silent debug log). Enabling roads511 alone
no longer accidentally consumes wzdx; enabling traffic alone no
longer consumes state_511_atis.
- start() subscribes per (subject, owned) tuple; per-subject log line
records the owned sources at startup.
- Removed roads511 from the "no Central mapping" warning loop now that
it has one.
Frontend (dashboard-frontend/src/pages/Environment.tsx):
- roads511 META entry: hasCentral false -> true, nativeOnly true -> false
(the FIRMS entry was already correct).
- Static bundle rebuilt via npm run build; old index-CfYlhn4e.js dropped,
new index-DCFmSeOM.js + index-DjhQa8Mv.css land under static/assets;
index.html updated to the new bundle hash.
Tests (tests/test_central_sub_adapter_routing.py, 8 new):
- roads511-only drops wzdx; emits state_511_atis as source=roads511.
- traffic+roads511 both central: wzdx -> traffic, state_511_atis -> roads511.
- firms-only drops wfigs_incidents; emits hotspots as source=firms.
- tomtom_incidents remaps to traffic.
- _subject_owned() shares central.traffic.> across {traffic, roads511}.
Orthogonal to v0.5.2's dispatcher guards: cooldown / dedup / staleness still
apply downstream of the consumer; the owned-sources filter operates one
layer up at message ingest. No changes to the dispatcher path.
Verified: pytest 318 passed (310 prior + 8 new routing tests); py_compile clean.
Safe-mode preserved -- no toggle enabled, no master enabled, no central enabled.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per-family notification policy (PagerDuty/Grafana-style): each family gets a
severity threshold + region scope + a severity->channel routing matrix, so an
operator opts in per family rather than hand-writing rules.
SECTION 1 -- BACKEND
- config.py: new NotificationToggle dataclass (enabled, min_severity, regions,
severity_channels{severity->[channel types]}, quiet_hours_override, + per-channel
delivery config: broadcast_channel/node_ids/smtp_*/recipients/webhook_*).
notifications.toggles is now a dict[family]->NotificationToggle with 8 family
defaults (mesh_health, weather, fire, rf_propagation, roads, avalanche, seismic,
tracking), all enabled=false (opt-in), min_severity=priority,
severity_channels={priority:[mesh_broadcast], immediate:[mesh_broadcast, mesh_dm]},
quiet_hours_override=true. (Old TogglesConfig.enabled was only read by
build_pipeline via getattr -> degrades to ToggleFilter no-op, so the pipeline
filter is unchanged; toggles now drive the Dispatcher instead.)
- region_scope:list added to NotificationRuleConfig; _matching_rules filters by
event.region/regions ([] = all).
- Dispatcher: _dispatch_toggles runs IN PARALLEL to rule matching -- looks up
get_toggle(event.category), checks enabled + region scope + severity threshold,
then for each channel in severity_channels[event.severity] builds a synthetic
rule (override_quiet set only for immediate when quiet_hours_override) and
delivers. 'digest' channel is skipped in live dispatch (handled by accumulator).
- categories.py: get_toggle() prefix fallback maps the live phases-2.7-2.14
categories (weather_warning, wildfire_incident, earthquake_event,
traffic_congestion, geomagnetic/rf_*, stream_*, ...) to their family, fixing the
v0.4 "category -> other" gap.
- config_loader.py: SECRET_FIELDS += notifications.toggles.*.smtp_password.
- _dataclass_to_dict now recurses dict-of-dataclasses, and the loader coerces the
toggles dict -> NotificationToggle on both the full-load and section-PUT paths
(so GUI save round-trips correctly).
- tests/test_notification_toggles.py (11): enabled/disabled, region filter
(empty+populated+regions-list), severity threshold, per-severity channel routing,
digest-skipped-live, quiet-hours-override immediate-only, category->family,
rules+toggles both fire. Full suite: 294 passed (283 + 11).
SECTION 2 -- FRONTEND
- Notifications.tsx: MasterToggles component above the rules section -- 8 family
cards (icon + enable toggle; collapsed summary 'OFF' or 'N regions, M channels at
<sev>+'; expanded: severity threshold, severity x channel checkbox matrix,
region list, quiet-hours-override toggle, per-channel config:
broadcast_channel/DM node IDs/recipients/SMTP host+port/webhook URL).
- Environment.tsx: CentralConnectionPanel above the family tabs (url, durable,
enabled) wired to environmental.central.
- npm run build clean (tsc strict); rebuilt static committed (index-CfYlhn4e.js).
SECTION 3 -- VERIFICATION
- py_compile + tsc strict clean; pytest 294 passed.
- Rebuilt prod: /notifications serves Master Toggles, /environment serves Central
Connection (strings confirmed in the served bundle); 8 adapters, pipeline
started, no tracebacks, healthy.
- GUI round-trip: enable weather toggle (min_severity=priority,
regions=[Magic Valley], severity_channels.priority=[mesh_broadcast]) -> PUT
{saved:true} -> notifications.yaml reflects it; env_feeds traffic.api_key stayed
${TOMTOM_API_KEY} (C.3.1 secret preservation holds). Restored to clean opt-in
baseline.
- Synthetic NWS weather_warning/priority/Magic Valley -> routes through the weather
toggle to mesh_broadcast; out-of-region and below-threshold events correctly
dropped.
DEFERRED (noted for a follow-up, not blocking Matt's morning config): Section 2B
rules-editor polish -- grouped-by-family category checkboxes, region_scope
multi-select in the rule editor (backend field + filtering ARE in), tooltips, and
the fire-count Active/No-activity badge -- were not built tonight to keep the build
shippable and verified; the Advanced Rules section is otherwise unchanged and
still functional.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Restructures the environmental config UI into the 7-family taxonomy on the
/environment page (Matt-approved Option C: unified families = config +
live status per adapter). The editable adapter config moves out of the
Config page's "Environmental" tab (now deprecated) onto /environment, where
each adapter sub-tab shows its AdapterPanel (on/off + feed_source + settings)
together with its live status (feed health + active events).
Frontend (dashboard-frontend/src):
- pages/Environment.tsx rewritten: 7 family tabs (Weather, Fire, RF
Propagation, Roads, Geohazards, Tracking, Mesh Health) -> per-family
adapter sub-tab strip -> AdapterPanel.
* AdapterPanel: header row = on/off Toggle + feed_source toggle
(native|central). When OFF, feed_source + all settings grey out
(disabled, not hidden).
* Native-only adapters (ducting, avalanche, roads511 -- no Central stream
per C.1's ADAPTER_SUBJECTS) show the feed_source toggle with 'central'
disabled + a 'Central not available for this adapter' tooltip/label.
* Missing-key adapters (firms, roads511) show an 'API key not configured
-- contact admin' notice; toggles still operate.
* Tracking = placeholder ('No adapters yet. ADS-B / AIS / satellite passes
are planned for v0.5.'). Mesh Health = no env adapters; a disabled
feed_source toggle with 'central' greyed for future migration.
* All existing per-adapter settings preserved verbatim (NWS zones/user_agent/
severity, ducting lat/lon, fires state, avalanche centers/season, USGS
sites, traffic corridors+key, roads511 base_url/key/endpoints/bbox, FIRMS
map_key/source/confidence/bbox). ALSO adds a usgs_quake panel
(tick/min_magnitude/region/bbox) -- usgs_quake (2.14) was never exposed in
the old GUI. feed_source field (C.1) now surfaced per adapter.
* Per-adapter live status: reuses /api/env/status feed health + /api/env/active
events (filtered by source; fires->nifc mapping). Refreshes every 30s.
- pages/Config.tsx: removed the now-duplicate 'Environmental' tab (SECTIONS
entry + render case + EnvironmentalSection function + unused Thermometer
import); exported the shared form primitives (Toggle, TextInput, NumberInput,
SelectInput, ListInput, NumberListInput, US_STATES) for reuse by Environment.tsx.
- Reuses the existing restart_required banner pattern.
- Rebuilt static: meshai/dashboard/static/{index.html, assets/index-9OZ6ZqzI.js,
index-B_J_Z7c8.css} (vite emptyOutDir replaced the old hashed bundle).
Rule 17 / no backend change: config is wired to the existing schema-driven
GET/PUT /api/config/environmental (the C.1 feed_source + central + usgs_quake
fields ride the generic dataclass coercion). No backend edited this phase.
Verification: (A) `npm run build` clean -- tsc strict + vite, only the
pre-existing >500kB single-chunk advisory (not introduced here). (B) static
committed; prod rebuilt picks it up. (C) GET / returns the new SPA shell
(index-9OZ6ZqzI.js); the bundle contains the new family strings (Geohazards,
RF Propagation, ADS-B, 'Central not available', 'API key not configured').
(D) GET /api/config/environmental returns all adapters with feed_source=native,
usgs_quake present, central{enabled:false} -- toggles bind to real data; all
native, nothing flipped. Rebuilt prod healthy.
*** BLOCKER FOUND (pre-existing, NOT introduced by C.2) -- flagged for C.2.1 ***
The save half of gate D fails: PUT /api/config/environmental returns
{"detail":"could not determine a constructor for the tag '!include' ..."}.
Root cause: the dashboard PUT handler (meshai/dashboard/api/config_routes.py)
calls meshai/config.py::save_config (monolithic; re-parses config.yaml with a
loader lacking the !include constructor) instead of the multi-file-aware
meshai/config_loader.py::save_section that exists for exactly the !include
layout. This breaks ALL GUI config saves in prod (every section, not just
environmental) and predates C.2 -- the old Config 'Environmental' tab had the
same broken save; C.1 did not touch save_config. The C.2 GET/render/toggle-bind
works; only persistence is blocked. Verified disk pristine (idempotent PUT
errored, wrote nothing; env_feeds.yaml md5 unchanged, restored from backup).
FIX (one line, but prod-wide blast radius -> wants its own phase + verification):
config_routes PUT should call config_loader.save_section(section, data, config_dir)
instead of config.save_config(...). Recommend C.2.1 backend fix before C.3.
C.3 (quake-to-central flip) should not proceed until C.2.1 unblocks GUI save,
since flipping feed_source from the GUI is the whole point.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
One-time renormalization pass under the .gitattributes added in the
previous commit. Every tracked text file now uses LF. No semantic
changes — verified via git diff --cached --ignore-all-space showing
zero real differences. Future diffs will only show real content
changes.
This commit will appear huge in git log --stat but represents zero
behavior change. Use git log --follow --ignore-all-space or
git blame -w when archaeologically tracing through this commit.
- Event feed shows event_type + area_desc instead of timestamp headline
- First sentence of description shown as hazard summary
- Local events (matching NWS zones) pinned to top with highlight
- Nearby events grouped below, slightly dimmed
- Dedup by event_id
Merged remote changes with local notification verification system:
- Kept local: channels.py, router.py, notification_routes.py, Notifications.tsx
(contains the new end-to-end verification system)
- Accepted remote: Config, Environment, Reference pages, new commands,
categories, summarizer, and other supporting files
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Channel connectivity test: SMTP, webhook, mesh with real errors
- Rule test shows live data from feeds, not canned examples
- Near-miss detection: shows events filtered by threshold
- Three send actions: current conditions, example alert, live alert
- Rule status indicators: last fired, data source health
- All errors show actual error messages
- Disabled feed detection with clear warnings
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Test always shows current data for the rule's feed categories
- RF rules show live SFI/Kp/R/S/G and ducting conditions
- Weather rules show active NWS alert count and headlines
- Fire rules show active fire/hotspot count
- Stream rules show current gauge readings
- Mesh rules show current health score and infra status
- Send Current Conditions delivers live snapshot through channel
- Send Test Alert delivers example through channel
- Send Live Alert available when real conditions match
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Tests check current conditions against rule categories/severity
- Shows actual alert messages that would fire right now
- Falls back to example messages from category registry if no matches
- Preview mode shows without sending, Send Test delivers with [TEST] prefix
- Mesh delivery applies real summarization so preview matches actual output
- Added test dialog UI showing conditions matched and preview messages
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Utilization pillar now reads firmware-reported channel_utilization
instead of estimating from packet counts with hardcoded 200ms/pkt
- Uses highest infra node value (busiest node = bottleneck)
- Falls back to packet count estimate only when telemetry unavailable
- Updated thresholds: 20/25/35/45% matching real Meshtastic behavior
- Per-region utilization from region nodes, not mesh-wide
- API response includes util_method, util_max_percent, util_node_count
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Reference page now explains HOW each health pillar is calculated:
- Infrastructure: router online ratio
- Utilization: airtime estimation from packet counts
- Coverage: gateway redundancy with single-gw penalties
- Behavior: flagged node thresholds
- Power: battery warning ratio
Includes actual formulas and special cases.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Replace fixed overlay with useRef-based click-outside detection
- Add X close button in top-right corner of popover
- Click ? to toggle (open if closed, close if open)
- Click anywhere outside popover to dismiss
- Remove fixed inset-0 overlay that was blocking page interaction
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add helper text and info (?) buttons to every field in Config.tsx
- Add section descriptions at the top of each config section
- Battery thresholds now show voltage equivalents (e.g., "30% ≈ 3.60V")
- NWS severity dropdown shows descriptions per option
- Alert rules grouped by category with full explanations
- Add InfoButton popover component for detailed field documentation
- Add info buttons to Environment.tsx RF propagation panels
- VOLTAGE_MAP and getVoltageApprox helper for Li-ion voltage lookup
Researched defaults and descriptions include:
- Li-ion voltage curve (4.20V=100%, 3.60V=30%, 3.50V=15%, 3.40V=7%)
- LoRa channel utilization (firmware throttles at 25%, issues at 50%)
- Packet flood detection (normal 1-5/min, suspicious >10/min)
- NWS severity levels with actionable descriptions
- Tropospheric ducting M-units/km refractivity gradients
- NOAA Space Weather R/S/G scales
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- SFI/Kp as prominent color-coded values with trend chart
- R/S/G scales as colored severity badges
- Tropospheric ducting condition with refractivity profile
- Environmental feeds replaced with scrolling live event timeline
- Unified activity log across all 9 feed adapters
- Source icons, severity badges, chronological order
- Real-time updates via WebSocket
- SWPC adapter stores Kp/SFI history for charting
- No wasted card space
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Self-contained rules replace abstract channels
- Inline delivery config (broadcast/DM/email/webhook or none)
- quiet_hours_enabled master toggle separate from start/end times
- delivery_type="" valid: rule matches but does not deliver
- Severity dropdown with plain-English descriptions
- Example messages per alert category
- Default baseline rules: Emergency Broadcast, Infrastructure Down, Fire Alert, Severe Weather
- Condition vs Schedule trigger types
- Test and preview buttons per rule
- stream_flood_warning renamed from flood_warning (distinct from packet_flood)
- Categories display with descriptions
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Each notification rule contains its own delivery config inline
- No more separate channels with abstract IDs to cross-reference
- Delivery type selector (Mesh Broadcast/DM/Email/Webhook) with
inline config fields per type
- Follows MeshMonitor trigger-action UX pattern
- Channel picker from radio for mesh broadcast
- Node picker for mesh DM
- Collapsed rule cards show readable one-line summary
- Trigger type: condition (alerts) or schedule (daily reports)
- Schedule triggers support daily, weekly, custom cron
- Message types: mesh health, RF propagation, alerts digest, custom
- Migrates old channels+rules config to new flat format on load
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add GET /api/channels endpoint for live radio channel data
- Create ChannelPicker component (single/multi-select from live channels)
- Create NodePicker component (searchable multi-select from mesh nodes)
- Replace manual inputs in Config with data-driven pickers
- Update Notifications to use pickers for mesh broadcast/DM
- Resolve node names in Alerts subscriptions display
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Create standalone Notifications.tsx page with full notification config UI
- Add /notifications route in App.tsx
- Add Notifications nav item in Layout.tsx sidebar (below Alerts, BellRing icon)
- Remove notifications section from Config.tsx (keep settings sections only)
- Channels, rules, quiet hours, and dedup all configurable on dedicated page
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Notifications tab in Config sidebar with Bell icon
- Channels section: add/edit/delete channels (mesh broadcast, DM, email, webhook)
- Test button sends test alert to channel
- Rules section: create rules with category checkboxes fetched from API
- Quiet hours configurable with start/end times
- Dedup window to prevent alert spam
- Full helper text and info buttons on every field
- Category list fetched from /api/notifications/categories, not hardcoded
- Added notifications and environmental to VALID_SECTIONS in config_routes.py
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add InfoButton component with click-to-toggle popover for field help
- Add SectionDescription component for section intro paragraphs
- Add AlertRuleToggle component with grouped threshold controls
- Add detailed info and helper text for every field in all sections
- Convert Commands section to toggleable command list with descriptions
- Add dropdowns for severity_min, fire state, connection type, LLM backend
- Add region management: Add/Delete buttons with confirmation
- Group alert rules by category: Infrastructure, Power, Utilization, Health
- Remove hardcoded placeholders and Idaho-specific text
- Fix config.py DashboardConfig dataclass decorator
- Fix main.py MessageRouter initialization
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Full Alerts page with active alerts, history table, subscriptions
- Active alert cards with severity styling and acknowledge button
- Alert history table with type/severity filtering and pagination
- Subscription viewer showing mesh subscriptions
- ToastProvider for app-wide toast notifications
- Toast notifications triggered on WebSocket alert_fired messages
- Auto-dismiss toasts after 8 seconds, click to navigate
- Page titles on all pages (Dashboard/Mesh/Environment/Config/Alerts)
- Improved alert_routes.py with proper pending alert handling
- Added AlertHistoryItem, Subscription types to api.ts
- Added fetchAlertHistory, fetchSubscriptions functions
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>