feat: Tick-based staggered polling for all sources

Both MeshviewSource and MeshMonitorDataSource now use tick-based
staggered polling instead of batch-every-5-minutes:

MeshviewSource (30s ticks):
- Packets: every tick (30s)
- Nodes: every 4 ticks (2 min)
- Stats/Edges: every 6 ticks (3 min)
- Traceroutes: every 10 ticks (5 min)

MeshMonitorDataSource (30s ticks):
- Packets: every 2 ticks (60s)
- Nodes/Telemetry: every 4 ticks (2 min)
- Traceroutes/Channels/Network/Topology: every 10 ticks (5 min)
- Solar: every 20 ticks (10 min)

Features:
- Source health status (avg_response_ms, tick_count, backed_off)
- Source coverage analysis (unique vs shared nodes)
- Tier 1 DATA SOURCES section shows all source health
- Node detail shows source visibility
- Incremental packets and telemetry with dedup
- Rate limit detection (429) with backoff
- Consecutive error exponential backoff
- polite_mode config option for shared instances

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
K7ZVX 2026-05-05 19:16:00 +00:00
commit b3c79f12da
5 changed files with 1137 additions and 473 deletions

View file

@ -36,8 +36,9 @@ PORTNUM_DISPLAY = {
"ATAK_FORWARDER": "ATAK",
}
def _clean_portnum(portnum: str) -> str:
def _clean_portnum(portnum) -> str:
"""Convert raw portnum to display name."""
if isinstance(portnum, int): portnum = str(portnum)
return PORTNUM_DISPLAY.get(portnum, portnum.replace("_APP", "").replace("_", " ").title())
@ -175,6 +176,34 @@ class MeshReporter:
return f"{local} ({desc})"
return local or desc
def _build_source_health_section(self) -> list[str]:
"""Build source health section for Tier 1."""
lines = []
lines.append("")
lines.append("DATA SOURCES:")
for name, source in self.data_store._sources.items():
if hasattr(source, 'health_status'):
status = source.health_status
err_str = f" - {status['last_error']}" if status.get('last_error') else ""
backed = " [BACKED OFF]" if status.get('backed_off') else ""
polite = " [POLITE]" if status.get('polite_mode') else ""
lines.append(
f" {name}: {status.get('cached_nodes', 0)} nodes, "
f"{status.get('cached_packets', 0)} pkts, "
f"avg {status.get('avg_response_ms', 0)}ms"
f"{polite}{backed}{err_str}"
)
else:
# Legacy source without health_status
node_count = len(source.nodes) if hasattr(source, 'nodes') else 0
loaded = "OK" if source.is_loaded else "ERR"
err = f" - {source.last_error}" if source.last_error else ""
lines.append(f" {name}: [{loaded}] {node_count} nodes{err}")
return lines
def build_tier1_summary(self) -> str:
"""Build comprehensive mesh health summary with full data for LLM context."""
health = self.health_engine.mesh_health
@ -428,6 +457,9 @@ class MeshReporter:
if pb["critical"]: parts.append(f"{pb['critical']} battery critical")
lines.append(f"POWER (infra): {', '.join(parts)}")
# Source health section
lines.extend(self._build_source_health_section())
lines.append("")
lines.append(f"TOTAL: {health.total_nodes} nodes across {health.total_regions} regions.")
@ -899,6 +931,10 @@ class MeshReporter:
status = "Single gateway - node goes dark if that gateway fails"
lines.append(f" Coverage: {node.avg_gateways:.0f}/{total_gw} gateways ({pct:.0f}%) - {status}")
# Source visibility
if node.sources:
lines.append(f" Seen by: {', '.join(node.sources)} ({len(node.sources)} sources)")
# Neighbors section
if node.neighbors:
lines.append("")