- Vite + React 18 + TypeScript + Tailwind CSS
- Dashboard overview with health gauge, pillar bars, alerts
- WebSocket hook for real-time updates
- Layout with sidebar navigation and live indicator
- Placeholder pages for Mesh, Environment, Config, Alerts
- Dark theme ops center aesthetic with JetBrains Mono
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- FastAPI runs in MeshAI asyncio loop (no separate process)
- REST API: /api/status, /api/health, /api/nodes, /api/edges,
/api/regions, /api/sources, /api/config, /api/alerts
- WebSocket at /ws/live pushes health updates and alerts
- Config CRUD: GET/PUT per section with validation and save
- DashboardConfig with port/host in config.yaml
Packet dedup: track seen packet IDs across all sources. Same packet from
Meshview + MeshMonitor counted once, not twice. Fixes inflated counts.
Portnum: numeric values (3, 4, 71) mapped to names (Position, NodeInfo, Neighbors).
LLM prompt: guidance on normal vs abnormal packet rates per type.
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>