fix: 6 reporter bugs — missing methods, undefined var, indentation, key types

1. Added _find_node() — delegates to health_engine.get_node()
2. Added _find_region() — fuzzy match with config aliases
3. Fixed undefined unified var in _node_recommendations
4. Fixed env recommendation indentation (was inside MQTT uplink check)
5. Fixed 6 string-vs-int key mismatches on health.nodes lookups
6. Fixed estimated_position_interval — compute inline from packets_by_type

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
K7ZVX 2026-05-05 20:58:41 +00:00
commit 29a57b459a

View file

@ -177,6 +177,52 @@ class MeshReporter:
return local or desc return local or desc
def _find_node(self, identifier: str) -> "UnifiedNode | None":
"""Find a node by any identifier (shortname, longname, node_num, hex ID).
Delegates to health_engine.get_node() which searches all formats.
"""
return self.health_engine.get_node(identifier)
def _find_region(self, region_name: str) -> "RegionHealth | None":
"""Find a region by name (fuzzy match).
Tries exact match, then substring, then alias from config.
"""
health = self.health_engine.mesh_health
if not health:
return None
name_lower = region_name.lower().strip()
# Exact match
for region in health.regions:
if region.name.lower() == name_lower:
return region
# Substring match (longest region name first to avoid partial matches)
for region in sorted(health.regions, key=lambda r: len(r.name), reverse=True):
if name_lower in region.name.lower() or region.name.lower() in name_lower:
return region
# Check config aliases
for region_cfg_name, cfg in self._region_configs.items():
aliases = getattr(cfg, 'aliases', []) or []
cities = getattr(cfg, 'cities', []) or []
local_name = getattr(cfg, 'local_name', '') or ''
all_matches = [a.lower() for a in aliases] + [c.lower() for c in cities]
if local_name:
all_matches.append(local_name.lower())
if name_lower in all_matches or any(name_lower in m for m in all_matches):
# Found config match, now find the region
for region in health.regions:
if region.name == region_cfg_name:
return region
return None
def _build_source_health_section(self) -> list[str]: def _build_source_health_section(self) -> list[str]:
"""Build source health section for Tier 1.""" """Build source health section for Tier 1."""
lines = [] lines = []
@ -312,7 +358,7 @@ class MeshReporter:
single_client_nodes = [n for n in single_gw_nodes if not n.is_infrastructure] single_client_nodes = [n for n in single_gw_nodes if not n.is_infrastructure]
lines.append(f" Single-gw clients ({single_clients}):") lines.append(f" Single-gw clients ({single_clients}):")
for scn in single_client_nodes[:10]: for scn in single_client_nodes[:10]:
scn_name = _node_display_name(scn.long_name, scn.short_name, str(scn.node~um)) scn_name = _node_display_name(scn.long_name, scn.short_name, str(scn.node_num))
src_info = f" via {scn.sources[0]}" if len(scn.sources) == 1 else "" src_info = f" via {scn.sources[0]}" if len(scn.sources) == 1 else ""
lines.append(f" {scn_name}{src_info}") lines.append(f" {scn_name}{src_info}")
if len(single_client_nodes) > 10: if len(single_client_nodes) > 10:
@ -592,17 +638,25 @@ class MeshReporter:
# Infrastructure issues (offline nodes) # Infrastructure issues (offline nodes)
for region in health.regions: for region in health.regions:
offline_infra = [] offline_infra = []
for nid in region.node_ids: for nid_str in region.node_ids:
try:
nid = int(nid_str)
except (ValueError, TypeError):
continue
node = health.nodes.get(nid) node = health.nodes.get(nid)
if node and node.is_infrastructure and not node.is_online: if node and node.is_infrastructure and not node.is_online:
name = _node_display_name(node.long_name, node.short_name, nid) name = _node_display_name(node.long_name, node.short_name, str(nid))
offline_infra.append(name) offline_infra.append(name)
if offline_infra: if offline_infra:
total_infra = sum( total_infra = 0
1 for nid_str in region.node_ids:
for nid in region.node_ids try:
if health.nodes.get(nid) and health.nodes[nid].is_infrastructure nid = int(nid_str)
) except (ValueError, TypeError):
continue
node = health.nodes.get(nid)
if node and node.is_infrastructure:
total_infra += 1
issues.append( issues.append(
f"{region.name}: {len(offline_infra)}/{total_infra} infrastructure offline " f"{region.name}: {len(offline_infra)}/{total_infra} infrastructure offline "
f"({', '.join(offline_infra[:3])})" f"({', '.join(offline_infra[:3])})"
@ -717,11 +771,15 @@ class MeshReporter:
lines.append("Channel Utilization: No data available") lines.append("Channel Utilization: No data available")
# MQTT uplink stats for region # MQTT uplink stats for region
uplink_nodes = [ uplink_nodes = []
health.nodes.get(nid) for nid_str in region.node_ids:
for nid in region.node_ids try:
if health.nodes.get(nid) and health.nodes[nid].uplink_enabled nid = int(nid_str)
] except (ValueError, TypeError):
continue
node = health.nodes.get(nid)
if node and node.uplink_enabled:
uplink_nodes.append(node)
lines.append("") lines.append("")
lines.append(f"MQTT Uplinks: {len(uplink_nodes)} nodes") lines.append(f"MQTT Uplinks: {len(uplink_nodes)} nodes")
@ -1085,7 +1143,7 @@ class MeshReporter:
# Check if trending up # Check if trending up
trend_note = "" trend_note = ""
if unified: if node:
avg_7d = node.packets_sent_7d / 7 if node.packets_sent_7d else 0 avg_7d = node.packets_sent_7d / 7 if node.packets_sent_7d else 0
if avg_7d > 0 and node.packets_sent_24h > avg_7d * 1.5: if avg_7d > 0 and node.packets_sent_24h > avg_7d * 1.5:
trend_note = " (trending up vs 7d avg)" trend_note = " (trending up vs 7d avg)"
@ -1245,11 +1303,12 @@ class MeshReporter:
for nid_str in region.node_ids: for nid_str in region.node_ids:
try: try:
nid = int(nid_str) nid = int(nid_str)
except ValueError: except (ValueError, TypeError):
continue continue
node = health.nodes.get(nid) node = health.nodes.get(nid)
if node: if node:
est = node.estimated_position_interval pos_count = node.packets_by_type.get("POSITION_APP", 0)
est = 86400 / pos_count if pos_count > 0 else None
if est is not None and est < 300: if est is not None and est < 300:
aggressive_interval_nodes.append(node) aggressive_interval_nodes.append(node)
if aggressive_interval_nodes: if aggressive_interval_nodes:
@ -1262,11 +1321,15 @@ class MeshReporter:
) )
# Check MQTT/uplink coverage in region # Check MQTT/uplink coverage in region
infra_nodes = [ infra_nodes = []
health.nodes.get(nid) for nid_str in region.node_ids:
for nid in region.node_ids try:
if health.nodes.get(nid) and health.nodes[nid].is_infrastructure nid = int(nid_str)
] except (ValueError, TypeError):
continue
node = health.nodes.get(nid)
if node and node.is_infrastructure:
infra_nodes.append(node)
uplink_count = sum(1 for n in infra_nodes if n and n.uplink_enabled) uplink_count = sum(1 for n in infra_nodes if n and n.uplink_enabled)
if infra_nodes and uplink_count == 0: if infra_nodes and uplink_count == 0:
recs.append( recs.append(