From 5839fdeb1845a0d12facd7ee3946a439ae0b6274 Mon Sep 17 00:00:00 2001 From: K7ZVX Date: Tue, 5 May 2026 23:47:48 +0000 Subject: [PATCH] feat: !health colored dots no numbers, distance from AIDA, router distance detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit !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 --- meshai/mesh_reporter.py | 89 +++++++++++++++++++++++++---------------- meshai/router.py | 55 +++++++++++++++++++++++++ 2 files changed, 110 insertions(+), 34 deletions(-) diff --git a/meshai/mesh_reporter.py b/meshai/mesh_reporter.py index a8f5a80..0400f06 100644 --- a/meshai/mesh_reporter.py +++ b/meshai/mesh_reporter.py @@ -282,6 +282,11 @@ class MeshReporter: score = health.score + # Get AIDA's position for distance calculations + aida_node = health.nodes.get(0x27780c47) # AIDA-N2 + aida_lat = aida_node.latitude if aida_node else None + aida_lon = aida_node.longitude if aida_node else None + lines = [ f"LIVE MESH HEALTH DATA (as of {age_str}):", "", @@ -344,6 +349,10 @@ class MeshReporter: if node.uplink_enabled: parts.append("MQTT") + if aida_lat and aida_lon and node.latitude and node.longitude and node.node_num != 0x27780c47: + km = _haversine_km(node.latitude, node.longitude, aida_lat, aida_lon) + parts.append(f"{_format_distance(km)} from AIDA") + lines.append(f" [{status}] {name}: {', '.join(parts)}") else: lines.append(f" Infrastructure: none in region") @@ -1511,25 +1520,33 @@ class MeshReporter: s = health.score - if s.composite >= 90: - header = f"📡 freq51 looking great — {s.composite:.0f}/100" + # Color dot — no numbers shown + if s.composite >= 100: + dot = "🔵" + mood = "perfect" elif s.composite >= 75: - header = f"📡 freq51 running solid — {s.composite:.0f}/100" - elif s.composite >= 60: - header = f"📡 freq51 needs attention — {s.composite:.0f}/100" + dot = "🟢" + mood = "healthy" + elif s.composite >= 50: + dot = "🟠" + mood = "needs attention" else: - header = f"🚨 freq51 struggling — {s.composite:.0f}/100" + dot = "🔴" + mood = "critical" - lines = [header, ""] + # Each line ends with period so chunker sends each as separate message + lines = [f"📡 freq51 {dot} {mood}."] offline_infra = [n for n in health.nodes.values() if n.is_infrastructure and not n.is_online] if offline_infra: - offline_names = ", ".join(n.short_name or str(n.node_num) for n in offline_infra[:3]) + offline_names = ", ".join( + (n.long_name or n.short_name or str(n.node_num)) for n in offline_infra[:3] + ) more = f" +{len(offline_infra)-3}" if len(offline_infra) > 3 else "" - lines.append(f"🏗️ {s.infra_online}/{s.infra_total} routers up") - lines.append(f" ❌ Down: {offline_names}{more}") + lines.append(f"🏗️ {s.infra_online}/{s.infra_total} routers up.") + lines.append(f"❌ Down: {offline_names}{more}.") else: - lines.append(f"🏗️ All {s.infra_total} routers up ✅") + lines.append(f"🏗️ All {s.infra_total} routers up ✅.") nodes_with_gw = [n for n in health.nodes.values() if n.avg_gateways is not None] if nodes_with_gw: @@ -1537,31 +1554,31 @@ class MeshReporter: full = sum(1 for n in nodes_with_gw if n.avg_gateways >= total_sources) single = sum(1 for n in nodes_with_gw if n.avg_gateways <= 1.0) if single > 0: - lines.append(f"📶 {full} nodes full coverage, {single} on thin ice with 1 gw") + lines.append(f"📶 {full} nodes full coverage, {single} on thin ice with 1 gw.") else: - lines.append(f"📶 {full} nodes full coverage ✅") + lines.append(f"📶 {full} nodes full coverage ✅.") high_util = [n for n in health.nodes.values() if n.channel_utilization is not None and n.channel_utilization > 15] if high_util: worst = max(high_util, key=lambda n: n.channel_utilization) - lines.append(f"🔥 {worst.short_name} running hot at {worst.channel_utilization:.0f}% util") + worst_name = worst.long_name or worst.short_name or str(worst.node_num) + lines.append(f"🔥 {worst_name} running hot at {worst.channel_utilization:.0f}% util.") infra_nodes = [n for n in health.nodes.values() if n.is_infrastructure] bat_low = [n for n in infra_nodes if n.battery_percent is not None and 0 < n.battery_percent < 20] if bat_low: - low_names = ", ".join(n.short_name for n in bat_low) - lines.append(f"🔋 ⚠️ {low_names} low battery") + low_names = ", ".join(n.long_name or n.short_name or str(n.node_num) for n in bat_low) + lines.append(f"🔋 ⚠️ {low_names} low battery.") else: - lines.append(f"🔋 All infra powered up ✅") + lines.append(f"🔋 All infra powered up ✅.") env_nodes = [n for n in health.nodes.values() if n.has_environment_sensor] if env_nodes: temps = [n.temperature for n in env_nodes if _is_valid_temperature(n.temperature)] if temps: - lines.append(f"🌡️ {min(temps):.0f}–{max(temps):.0f}°C across {len(env_nodes)} sensors") + lines.append(f"🌡️ {min(temps):.0f}–{max(temps):.0f}°C across {len(env_nodes)} sensors.") - lines.append("") region_parts = [] for region in health.regions: if not region.node_ids: @@ -1569,14 +1586,16 @@ class MeshReporter: rs = region.score context = self._region_context(region.name) name = context.split("(")[0].strip() if context else region.name - if rs.composite >= 90: - region_parts.append(f"{name} ✅") - elif rs.composite >= 70: - region_parts.append(f"{name} ⚠️") + if rs.composite >= 100: + region_parts.append(f"{name} 🔵") + elif rs.composite >= 75: + region_parts.append(f"{name} 🟢") + elif rs.composite >= 50: + region_parts.append(f"{name} 🟠") else: - region_parts.append(f"{name} ❌") + region_parts.append(f"{name} 🔴") if region_parts: - lines.append(" | ".join(region_parts)) + lines.append(" | ".join(region_parts) + ".") return "\n".join(lines) @@ -1586,15 +1605,17 @@ class MeshReporter: context = self._region_context(region.name) name = context.split("(")[0].strip() if context else region.name - if rs.composite >= 90: - header = f"📡 {name} looking good — {rs.composite:.0f}/100" - elif rs.composite >= 70: - header = f"📡 {name} needs attention — {rs.composite:.0f}/100" + if rs.composite >= 100: + dot = "🔵" + elif rs.composite >= 75: + dot = "🟢" + elif rs.composite >= 50: + dot = "🟠" else: - header = f"🚨 {name} in trouble — {rs.composite:.0f}/100" + dot = "🔴" - lines = [header] - lines.append(f"🏗️ {rs.infra_online}/{rs.infra_total} infra | 📡 {rs.util_percent:.0f}% util") + lines = [f"📡 {name} {dot}."] + lines.append(f"🏗️ {rs.infra_online}/{rs.infra_total} infra | 📡 {rs.util_percent:.0f}% util.") offline = [] for nid_str in region.node_ids: @@ -1604,9 +1625,9 @@ class MeshReporter: continue node = self.health_engine.mesh_health.nodes.get(nid) if node and node.is_infrastructure and not node.is_online: - offline.append(node.short_name or str(nid)) + offline.append(node.long_name or node.short_name or str(nid)) if offline: - lines.append(f" ❌ Down: {', '.join(offline)}") + lines.append(f"❌ Down: {', '.join(offline)}.") return "\n".join(lines) diff --git a/meshai/router.py b/meshai/router.py index c4ce5cb..9958768 100644 --- a/meshai/router.py +++ b/meshai/router.py @@ -465,6 +465,54 @@ class MessageRouter: ctx["last_was_mesh"] = False ctx["last_scope"] = ("mesh", None) + def _try_compute_distance(self, query: str) -> str: + """Extract two node names from a distance question and compute distance.""" + if not self.mesh_reporter: + return "" + + health = self.mesh_reporter.health_engine.mesh_health + if not health: + return "" + + query_lower = query.lower() + + # Build name -> node lookup + node_names = {} + for node in health.nodes.values(): + if node.short_name: + node_names[node.short_name.lower()] = node + if node.long_name: + node_names[node.long_name.lower()] = node + + # AIDA aliases + aida_node = health.nodes.get(0x27780c47) + if aida_node: + for alias in ["aida", "aida-n2", "me", "my node", "yourself", "your position", "you"]: + node_names[alias] = aida_node + + # Find mentioned nodes (longest names first) + found_nodes = [] + for name in sorted(node_names.keys(), key=len, reverse=True): + if name in query_lower and len(name) >= 2: + node = node_names[name] + if not any(n.node_num == node.node_num for n in found_nodes): + found_nodes.append(node) + if len(found_nodes) >= 2: + break + + if len(found_nodes) == 2: + return self.mesh_reporter.build_distance( + str(found_nodes[0].node_num), + str(found_nodes[1].node_num) + ) + elif len(found_nodes) == 1 and aida_node: + return self.mesh_reporter.build_distance( + str(found_nodes[0].node_num), + str(aida_node.node_num) + ) + + return "" + async def generate_llm_response(self, message: MeshMessage, query: str) -> str: """Generate LLM response for a message. @@ -642,6 +690,13 @@ class MessageRouter: # DEBUG: Log system prompt status logger.debug(f"System prompt length: {len(system_prompt)} chars") + # Detect distance questions and inject computed distance + distance_keywords = ["how far", "distance", "how close", "miles from", "km from", "away from"] + if any(kw in query.lower() for kw in distance_keywords): + distance_result = self._try_compute_distance(query) + if distance_result: + system_prompt += f"\n\nDISTANCE CALCULATION:\n{distance_result}\n" + try: response = await self.llm.generate( messages=history,