Adds TomTomTrafficAdapter.to_event(), wiring the traffic adapter into
the notification EventBus following the FIRMS pattern (Phase 2.6).
to_event() design:
- Category: fixed "traffic_congestion" (a road closure raises severity,
not category).
- Severity: passed through unchanged from the adapter's existing
_fetch_point logic (priority on closure / heavy congestion, else
routine). No threshold is re-derived or introduced in to_event.
- Summary enriched with current/free-flow speed, % free flow, closure,
and confidence.
- Defensive: missing lat/lon or missing corridor identity returns None;
the whole body is try/except-guarded (returns None on corruption).
Inhibit-key composition:
- A single stable per-corridor key, "traffic_{corridor}" (lowercased,
spaces->_), is used as BOTH group_key and the sole inhibit_key. This
matches the adapter's own event_id, so re-polls of a corridor coalesce.
- Severity tiering is delegated to the pipeline Inhibitor, which ranks
routine<priority<immediate per shared inhibit_key: a higher-severity
emission for a corridor suppresses lower-severity re-emissions of the
same corridor within the Inhibitor TTL window. No severity is encoded
into the key (mirrors FIRMS's spatial-key approach).
Store wiring: no change. EnvironmentalStore._ingest()'s generic "else"
branch already emits any adapter exposing to_event() (live since 2.6.5).
Rule 17: to_event introduces no new tunable. The api_key is injected via
the secrets channel ($TOMTOM_API_KEY in /data/secrets/.env, referenced
as ${TOMTOM_API_KEY} in env_feeds.yaml) -- the GUI-editable reference
stays in config while the secret never enters git. The only other knob
in play is the pipeline-level Inhibitor TTL (1800s, set in
build_pipeline), which is pipeline infrastructure, not traffic-owned;
left out of scope.
Tests: tests/test_adapter_traffic.py (15 tests) mirrors
test_adapter_firms.py -- category, severity pass-through,
group_key/inhibit_keys, field population, defensive cases. Full suite:
147 passed.
Smoke test (prod, Magic Valley corridors I-84 @ Jerome, US-93 Perrine
Bridge, US-30 Twin Falls): clean startup, 6 env adapters loaded, no
traceback. "TomTom traffic updated: 3 corridors" (no auth/DNS error),
then 3 Events emitted to the pipeline bus with traffic_congestion
category -- the full store->bus->pipeline path observed live. Emission
count stable at 3 (one per corridor, is_new-gated).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The meshai container could not resolve external HTTP hostnames (NWS
api.weather.gov, SWPC services.swpc.noaa.gov, and the meshview mesh
source), failing every poll with "[Errno -3] Temporary failure in name
resolution". Docker's embedded resolver (127.0.0.11) forwards to the
daemon default upstreams 1.1.1.1/8.8.8.8, which are unreachable from
this container's NAT egress (the same egress filter that blocks Docker
Hub). The radio link was unaffected because it is an IP, not a hostname.
Fix: pin the meshai service to dns: [100.100.100.100], the LXC host's
own working resolver (Tailscale MagicDNS). The LXC's /etc/resolv.conf
uses only 100.100.100.100 and resolves the public feeds fine, and it
forwards public queries upstream. A preflight `docker run --dns=
100.100.100.100 ... getent hosts api.weather.gov` resolved successfully
from the docker bridge, confirming the container can reach MagicDNS.
Chosen over network_mode: host (more invasive, needs port-binding
review) and a host-side daemon.json dns key (affects all containers,
lives outside git). This directive is in-repo, git-tracked, and survives
daemon reloads.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the dark store->bus path. The to_event() methods added in Phase
2.6 for NWS and FIRMS were exercised only by unit tests because main.py
never built the pipeline or passed an EventBus to EnvironmentalStore.
Insertion points (matching existing init/lifecycle conventions):
- _init_components(): inside the notifications.enabled block, after the
NotificationRouter init, build the v0.3 pipeline via build_pipeline()
and stash it on self.event_bus; then construct EnvironmentalStore with
event_bus=self.event_bus so newly-seen adapter events emit to the bus.
- start(): after _write_pid(), await start_pipeline() to launch the
digest scheduler now that the event loop is running; the scheduler is
stored on self._pipeline_scheduler.
- stop(): await stop_pipeline() during teardown.
- env/store._emit_event(): emission log promoted DEBUG->INFO for runtime
traceability of events crossing the bus.
When notifications are disabled, self.event_bus stays None and the store
receives None (emission no-ops), preserving prior behavior.
Tests: 132 passing, no regressions.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Second adapter wired to the new pipeline (after NWS). Reuses the
store-side emission logic added in the NWS commit.
- FIRMSAdapter.to_event() maps stored dict to pipeline Event.
- Category decision: new_ignition vs wildfire_proximity based on
properties.new_ignition (computed by FIRMS during ingest from
proximity to known fires).
- Severity passes through (FIRMS already pre-maps to our 3-level
system during _parse_csv).
- group_key and inhibit_keys use a spatial grid key
(firms:LAT:LON rounded to 0.01 degrees, ~1km) so repeated
satellite detections of the same hotspot are coalesced and
lower-severity re-detections are inhibited.
- Summary text enriched with FRP, confidence, and distance from
the nearest region anchor when present.
- 13 tests covering category decision, severity pass-through,
spatial grouping, and defensive handling of incomplete dicts.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Wires the NWS adapter to the new notification pipeline via EventBus:
- Added fine-grained weather categories: weather_watch, weather_advisory,
weather_statement (all routine severity) alongside existing weather_warning
- NWSAlertsAdapter._derive_category() maps NWS event type suffix to category:
"Warning" -> weather_warning, "Watch" -> weather_watch, etc.
- NWSAlertsAdapter.to_event() converts internal event dict to pipeline Event
with proper group_key (event_id) and inhibit_keys (Warning suppresses Watch)
- EnvironmentalStore accepts optional event_bus parameter
- EnvironmentalStore._ingest() emits new events to bus via _emit_event()
- 22 new tests in test_adapter_nws.py covering category derivation,
severity mapping, and Event field population
All 119 tests pass.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Adds dedicated renderer classes per channel type:
- MeshRenderer produces 1+ chunks <=200 chars with (k/N) counters
when the payload overflows. Reuses the toggle-label vocabulary
from the digest. Mesh channels skip re-chunking when the payload
already carries chunk_index metadata (digest path).
- EmailRenderer produces {subject, body} with structured context
lines. Plain text only; HTML body is a future polish.
- WebhookRenderer produces a JSON-serializable dict with stable
schema_version 1.0. Optional fields omitted (not nulled) for
compactness. Designed for reuse by Phase 2.6.5's MQTT event
publisher.
- All four channel implementations (MeshBroadcast, MeshDM, Email,
Webhook) now call their renderer in deliver() before transport.
- New renderer tests cover each renderer in isolation; new channel
integration tests confirm channels actually call their renderer.
Renderers are pure functions of the payload - no network, no
state, fully testable without mocking I/O. The future MQTT
publisher will instantiate WebhookRenderer directly.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Switch channels.py from dict-based to dataclass-based interfaces
- Add NotificationPayload dataclass and make_payload_from_event helper
- Update channel.deliver() to be async with (payload, rule) signature
- Add connector parameter to Dispatcher, DigestScheduler, and pipeline builders
- Update pipeline tee to use asyncio.create_task for async dispatch
- Add create_channel_from_dict for legacy router.py compatibility
- Update tests for new async interfaces
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
build_pipeline previously constructed its own LLMBackend from
config.llm, which:
- duplicated main.py's already-running backend instance
- failed to inherit env-loaded LLM_API_KEY when called from
short-lived scripts (eyeball checks, tests), forcing fallback
- prevented pipeline components from sharing the live backend
build_pipeline and build_pipeline_components now require an
llm_backend parameter. main.py passes the same instance it
constructed for its primary responder. Tests pass mocks. The
digest accumulator now uses the live, authenticated backend.
Added test_build_pipeline_uses_provided_backend to lock in the
injection contract.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove severity-based fork; tee pattern sends all events to both dispatcher and accumulator
- Add ToggleFilter before tee; drops events for disabled toggles
- Rework DigestAccumulator: event log instead of active/resolved tracking
- render_digest now async, calls LLM once per toggle with severity-ordered events
- Fallback to count-based summary when LLM unavailable
- Add TogglesConfig to config.py for master toggle settings
- Update scheduler to await async render_digest
- 75 tests passing
Co-Authored-By: Claude Opus 4.5 <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.
Forces all text files to use LF on commit regardless of the editor or
OS that wrote them. Prevents the line-ending churn that has been
inflating diffs (e.g. 1510 lines of churn on config.py in commit
493b43f for what was really a 12-line change). The next commit will
normalize all existing files; from there forward, diffs only show
real semantic changes.
Adds DigestScheduler class that fires digest at configured time (default 07:00)
and routes to rules with trigger_type=schedule and schedule_match=digest.
- DigestScheduler: asyncio task with start/stop lifecycle
- Config: DigestConfig dataclass with schedule and include fields
- Config: schedule_match field on NotificationRuleConfig
- Pipeline: start_pipeline/stop_pipeline async lifecycle functions
- Mesh channels get per-chunk delivery, email/webhook get full text
- 26 new tests covering schedule computation, fire behavior, lifecycle
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Adds DigestAccumulator tracking ACTIVE NOW and SINCE LAST DIGEST
state per toggle. Replaces StubDigestQueue in build_pipeline; the
stub class is kept for Phase 2.1 backward-compat tests.
- enqueue(): adds new events, updates in place by id, detects
resolutions (expires past, or title contains cleared/reopened/
ended/resolved/back online/recovered/lifted)
- tick(now): rolls expired actives into since_last
- render_digest(now): produces a Digest with mesh_compact (<=200
chars) and full multi-line forms; clears since_last after
- Toggle ordering and labels match the v0.3 design
- Phase 2.3b will add real scheduling on top of this
Adds inline pipeline stages between the bus and the severity router:
- Inhibitor: suppresses lower-or-equal severity events when a key
in event.inhibit_keys is already active. TTL configurable, default
30 minutes.
- Grouper: coalesces events sharing group_key within a time window
(default 60s). Most recent event wins. tick() and flush_all()
drive emission; no background timers in Phase 2.2.
- build_pipeline now wires: bus -> inhibitor -> grouper -> severity_router
Phase 2.1 dispatcher tests continue to pass unchanged.
The initial 2.1 dispatcher was a logging stub with manual backend
registration. The spec required integration with the existing
NotificationRuleConfig schema and channels.py create_channel factory.
- Dispatcher takes (config, channel_factory)
- _matching_rules iterates config.notifications.rules with severity ranking
- dispatch() builds alert dict and calls channel.deliver()
- build_pipeline(config) returns EventBus per spec
- build_pipeline_components(config) added for test introspection
Phase 1.3:
- events.py: Event dataclass with ID generation and serialization
- region_tagger.py: Coordinate/NWS zone region tagging
- categories.py: Toggle field mapping for all 31 alert categories
Phase 2.1 Pipeline Skeleton:
- pipeline/bus.py: EventBus with subscribe/emit pattern
- pipeline/severity_router.py: Routes immediate->dispatch, routine->digest
- pipeline/dispatcher.py: Delivers immediate events to configured channels
- pipeline/__init__.py: build_pipeline() factory and exports
All components tested and verified in container.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Adds toggle field to each ALERT_CATEGORIES entry:
- mesh_health: 18 categories (infra, power, utilization, coverage, health)
- weather: 3 categories (NWS warnings, stream flooding)
- fire: 3 categories (NIFC, FIRMS hotspots)
- rf_propagation: 3 categories (solar, geomag, ducting)
- roads: 2 categories (closures, congestion)
- avalanche: 2 categories (high danger, considerable)
Also adds helper functions:
- categories_for_toggle(toggle) -> list of category IDs
- get_toggle(category_name) -> toggle name or None
Note: seismic and tracking toggles defined but have no categories yet
(reserved for Phase 3 and Phase 7 respectively).
All toggle assignments are unambiguous - no categories defaulted to
mesh_health due to ambiguity.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Adds meshai/notifications/region_tagger.py with:
- haversine_distance() for great-circle distance calculation
- tag_by_coordinates() maps lat/lon to nearest region within radius
- tag_by_nws_zone() maps NWS zone codes to matching regions
Also adds nws_zones field to RegionAnchor in config.py to support
zone-based matching. Default is empty list for backward compatibility.
This is scaffolding for Phase 2 - not yet wired into any adapters.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Adds meshai/notifications/events.py with:
- Event dataclass with all fields for unified pipeline shape
- Stable ID generation via sha1 hash for deduplication
- make_event() factory with auto-timestamp and severity validation
- to_dict/from_dict for serialization round-trip
This is scaffolding for Phase 2 - not yet wired into any adapters.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Added deep_compare function for recursive dict comparison
- Replaced shallow key-list check with full Config dataclass comparison
- Uses dataclasses.asdict for consistent dict representation
- Reports full path of mismatches (e.g. connection.tcp_host)
The previous gate only checked inline sections and missed the
include-related bugs that caused the restart loop.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Changed orchestrator to use meshtastic: include meshtastic.yaml
- Added hoisting logic to extract connection/commands from wrapper
- Fixes restart loop caused by connection.type defaulting to serial
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Update .gitignore for v0.3 multi-file layout
- Add config/.env.example template for secrets
- Add config/local.yaml.example for operator values
- Wire main.py to use new config_loader
- Support both legacy and new layouts
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Backup original config before migration
- Split monolithic config into domain files
- Extract operator-identifying values to local.yaml
- Extract secrets to /data/secrets/.env
- Create orchestrator with !include directives
- Post-migration verification
- Safe to run multiple times (idempotent checks)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add config_loader.py with !include directive support
- Environment variable interpolation with default syntax
- local.yaml merging for operator-identifying values
- Secret loading from /data/secrets/.env
- save_section() for dashboard write-back
- Cycle detection for include directives
- Graceful degradation when files missing
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Replace 11 info fallbacks with routine in router.py + channels.py
- Replace 2 warning min_severity defaults with priority
- Update config.example.yaml rules to use routine/priority/immediate
- Annotate config.example.yaml notifications section as transitional pending v0.3 8-toggle rewrite Phase 1.2
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Fix leftover severity references (info→routine in filter dropdown)
- Fix node_id int handling in connector and channels (handle both int and string)
- Add LLM-generated reports for notifications (replace raw data dumps)
- Fix health.score.composite attribute path for RF reports
- Add deterministic HF band conditions from SFI/Kp values
- Remove max_tokens from LLM calls (character limits at delivery)
- Weather feed improvements: show event_type + area, local events first
- Fix is_online to use configured offline_threshold_hours in data store
- Update stale defaults: offline 24→2h, battery_warning 20→30%
- Add TODO comments for packet_threshold scale bug
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add offline_threshold_hours parameter to MeshDataStore.__init__
- Compute is_online in _normalize_node using configured threshold
- Pass config.mesh_intelligence.offline_threshold_hours from main.py
- Removes reliance on health engine for initial is_online computation
Verification:
- Unit test confirms 2h threshold marks 3h-old node offline
- Unit test confirms 4h threshold marks same node online
- Container starts healthy with no config errors
- Health engine reports 16/16 infra online
- 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
- Fix MeshHealth.score.composite path (was accessing wrong object)
- Add deterministic band condition calculator from SFI/Kp/time
- RF reports use structured band format, not LLM
- Fix LLM prompts for health/weather reports (max_tokens, format)
- Graceful handling when data sources not configured
- Status/report messages use LLM to generate operator-readable summaries
- RF reports interpret SFI/Kp into which bands are open
- Mesh reports highlight problems, not just numbers
- Remove meaningless [STATUS] prefix
- Alerts stay templated (no LLM, no latency)
- Reports respect 180-char limit for mesh delivery
- connector.send_message accepts int or string destination
- channels.py converts node_id to str before string operations
- Rule stats write to /data/ (Docker volume) not /opt/meshai/data/
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>