mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-05-21 15:14:45 +02:00
feat: !health colored dots no numbers, distance from AIDA, router distance detection
!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>
This commit is contained in:
parent
5be1d20b24
commit
5839fdeb18
2 changed files with 110 additions and 34 deletions
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue