Delays 1.5-2.5s (was 3-5s, only for broadcasts now).
DMs: send → ACK → next immediately. No ACK → retry once → abort.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Alert conditions across all 5 pillars:
Infrastructure: offline, recovery, new router
Power: battery 50/25/10%, 7-day trend, USB→battery, solar not charging
Utilization: sustained >20% for 6h, packet flood >500/24h
Coverage: infra single gateway, feeder offline, region blackout
Scores: mesh <70, region <60
Scaling cooldown: immediate → 12h → 24h → 48h → stop
Recovery notifications when conditions resolve
Per-condition on/off toggles in TUI
Battery trend queries SQLite node_snapshots for 7-day history
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- build_lora_compact returns list[str] instead of str
- Each line is a separate LoRa message (no chunking needed)
- main.py handles list responses from commands
- _try_compute_distance supports partial names (TVM Pearl → TVM Pearl Relay)
- Ambiguous names detected (TVM → asks which node)
- Max message size: 54 bytes (well under 228 byte limit)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
!health: 🔵 perfect, 🟢 healthy, 🟠 warning, 🔴 critical — no /100 scores.
Each line ends with period for separate LoRa messages.
Uses long_name to avoid emoji shortnames (📡 → TVM Tablerock Relay).
Distance from AIDA shown on every infra node in Tier 1.
Router detects how far questions and injects computed distance.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Commands now chunk output same as LLM responses
- split_sentences splits on newlines first for !health output
- chunk_response uses byte counting instead of character counting
- Emojis and UTF-8 properly counted for 228-byte LoRa limit
- !health 274 bytes now splits into 2 messages (195 + 74 bytes)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Replace build_lora_compact with personality version using emojis
- Add _region_lora_compact helper for region-specific display
- Skip empty regions in 3 loops (build_tier1_summary, utilization, list_regions_compact)
- Update AIDA identity: she IS a physical node (!27780c47 AIDA-N2)
- AIDA now knows she has real GPS coordinates and radio connections
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add build_distance() method to MeshReporter
- Uses _haversine_km() for GPS distance calculation
- Formats output with both km and miles
- Handles missing nodes or GPS positions gracefully
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add max_response_tokens config (8192) to LLMConfig
- Use config value in router.py instead of hardcoded 500
- Update base.py default from 300 to 8192
- Lets LLM generate full responses; chunker handles size limits
Fixes truncated responses like Here are three nodes in the freq
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
These methods were called by commands/health.py and main.py but missing
from mesh_reporter.py, causing crashes on !health and !region commands.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Added hops_away to Connectivity section in node detail
- Added nearest infra distance after Position in node detail
- Added distance from reference infra to single-gw client listings
- Added _haversine_km and _format_distance helper functions
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Single-gateway nodes now display "via {source_name}" to indicate
which data source they depend on. This helps identify coverage gaps
and understand node visibility.
Adds source info to:
- Tier 1 region summary (infra and client nodes)
- Node distribution section (infrastructure nodes)
- Region detail view (single gateway list)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Coverage:
- Single-gateway infra nodes named as critical risks per region
- Client single-gw nodes counted but not individually named
- Mesh-wide single-gw infra summary
Monitoring rules by node type:
- Infrastructure: full detail - battery, offline, coverage, neighbors, hardware
- Clients causing problems: named - high util, top senders
- Clients otherwise: counted per region, not individually tracked
- POWER breakdown now infra-only
Commands:
- Removed hardcoded command list from config.py system_prompt
- Dynamic command list in router.py from dispatcher (only enabled commands)
- MeshMonitor commands no longer listed as MeshAI commands
- !help overhaul: grouped by category, per-command detailed help
- LLM explicitly told to only mention listed commands
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Tier 1 now includes:
- Every infrastructure node BY NAME per region with status/battery/util/gateways
- Problem nodes section: offline infra, critical battery, high util, coverage risks
- Per-region coverage with gateway counts and single-gw counts
- Environmental data per region
- All 5 pillars with weights
- Expanded recommendations with specifics (10 max, up from 5)
- LLM prompt simplified: data speaks for itself
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Added CRITICAL instruction to keep sentences under 150 chars
- Chunker now splits long sentences at word boundaries instead of truncating
- No words lost when splitting
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Line 763: REGION_CONTEXT.get() → self._region_context() (same method used elsewhere)
- Deleted _CITY_TO_REGION hardcoded dict
- Scope detection now uses config aliases/cities from RegionAnchor
- Fixed Sun Valley/Ketchum geography (was Central ID, should be South Central ID)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Extended RegionAnchor with local_name, description, aliases, cities
- Moved region geographic context from hardcoded Python to config.yaml
- Added 7-day stale node purge in _do_refresh (556 → 267 nodes)
- Fixed coverage lookup: str(node_num) → node_num (int key)
- Added bidirectional neighbor lookup for better region assignment
- Dynamic geography building in router from config
- Reporter reads region context from config instead of hardcoded dict
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Magic Valley, Treasure Valley, Panhandle, Dixie — local names in all output
- Southern Idaho correctly maps to Magic Valley, not lumped with Boise/IF
- Unlocated nodes excluded from coverage gap recommendations
- LLM response rules override brevity for mesh questions
- Node detail shows region with local context
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
NodeHealth is gone. MeshHealth.nodes is now dict[int, UnifiedNode].
Reporter reads all fields from UnifiedNode: coverage, environment,
neighbors, hw_model — everything available without cross-referencing.
This eliminates the entire category of field missing on NodeHealth bugs.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Update HealthScore with coverage pillar (20% weight)
- Adjust weights: Infra 30%, Util 25%, Coverage 20%, Behavior 15%, Power 10%
- Add coverage metrics: avg_gateways, single_gw_count, full_count
- Add health score fields to UnifiedNode for direct sync
- compute() now syncs scores back to UnifiedNode objects
- Coverage scoring penalizes single-gateway nodes
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Health engine stores node IDs as decimal strings, but UnifiedNode.node_num
is an integer. The lookup health.nodes.get(n.node_num) was failing, causing
all nodes to show as Unlocated. Fixed by converting to string first.
- Show per-region coverage stats in tier1 summary
- List single-gateway nodes in region detail
- Add coverage status to node detail view
- Add coverage gap recommendations
Replaces broken per-packet gateway sampling with node-level source counting.
Each Meshview/MeshMonitor source represents a gateway view of the mesh.
If a node is seen by N sources, its packets are reaching N gateways.
- Removed _sample_gateway_coverage() (required non-existent API)
- Rewrote _enrich_deliverability() to use node.sources count
- Per-node: avg_gateways, max_gateways, source_reach, deliverability_score
- Mesh-wide: avg 4.16 gateways/node with 7 sources
- Fixed edge.timestamp -> edge.last_seen in get_all_edges()
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add hasattr check for fetch_recent_packets in gateway sampling
- Add get_all_nodes(), get_all_telemetry(), get_all_packets(), get_all_edges() methods
- These methods return data in dict format expected by mesh_health.py
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Create subscriptions.py with SubscriptionManager class for SQLite storage
- Add subscribe.py commands: !sub, !unsub, !mysubs with aliases
- Update dispatcher.py to register subscription commands
- Modify main.py with scheduler tick (60s) and _check_scheduled_subs()
- Add build_node_compact() and build_region_compact() to mesh_reporter.py
- Support daily, weekly, and alerts subscription types
- Support mesh, region, and node scope filtering
- 5-minute matching window for schedule tolerance
- Dedup via last_sent tracking
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Removed SNR-required filter from !neighbors command
- Show all infra neighbors, add signal quality when available
- Enrich edges with SNR from traceroute snrTowards/snrBack data
- Fallback: use node-level SNR for edges without traceroute data
- Sort by SNR when available, alphabetically otherwise
Co-Authored-By: Claude <noreply@anthropic.com>
Root cause: health engine keyed nodes by database row IDs instead of
Meshtastic node numbers. Packets and telemetry could never match.
Fixed:
- Store _node_num on all normalized nodes (mesh_sources.py)
- Key health engine node dict by _node_num (mesh_health.py)
- Fix packet field names: from_node not from/fromId
- Fix telemetry parsing: handle telemetryType/value structure
- Increase packet/telemetry fetch limits for 24h coverage
- Fix utilization formula to compute actual airtime percentage
- router.py: Fixed region scope detection to match longest region name first
- router.py: Added region abbreviations (SCID, SWID, etc.) for quick matching
- router.py: Added city name mapping (Boise -> South Western ID, etc.)
- router.py: Fixed node longname matching (case-insensitive substring)
- router.py: Added follow-up message context tracking (_user_mesh_context)
- router.py: Added more mesh keywords (noisy, traffic, packets, etc.)
- mesh_reporter.py: Added disambiguation for duplicate shortnames in region detail
- mesh_health.py: Added util_data_available flag to track packet data presence
- mesh_health.py: Passes has_packet_data through score computation
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Add _normalize_node() function to mesh_sources.py that standardizes:
- Role field: Map integer enums to string names using correct Meshtastic
protobuf values (0=CLIENT, 1=CLIENT_MUTE, 2=ROUTER, 3=ROUTER_CLIENT,
11=ROUTER_LATE, etc.). Now detects 18 infrastructure nodes.
- GPS fields: Check latitude/longitude, then last_lat/last_long (Meshview
scaled integers), then lat/lon. Filter out invalid 0,0 coordinates.
Now 238 nodes with GPS (was 201).
- Timestamps: Normalize to last_heard as epoch seconds. Handle
microseconds (last_seen_us), milliseconds, and seconds formats.
Now 527 nodes with timestamps (was 0).
- Hardware model: Prefer string hw_model over integer hwModel.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
New files:
- mesh_reporter.py: MeshReporter class for prompt injection
- build_tier1_summary(): ~500-800 token mesh health summary
- build_region_detail(): Detailed region breakdown
- build_node_detail(): Single node info with recommendations
- build_recommendations(): Optimization suggestions
- build_lora_compact(): Short format for LoRa messages
- list_regions_compact(): Region list with scores
- commands/health.py: !health and !region commands
- !health: Quick mesh summary (no LLM)
- !region [name]: Region info or list all regions
Modified files:
- router.py: Mesh question detection and prompt injection
- _is_mesh_question(): Keyword/phrase matching
- _detect_mesh_scope(): Node/region/mesh scope detection
- Inject Tier 1/2 data for mesh questions
- Add mesh awareness instructions to LLM
- main.py: Create MeshReporter, pass to dispatcher/router
- commands/dispatcher.py: Register health/region commands
- mesh_health.py: Fix role type (int -> str)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>