From 8c3b6a1f092e1814e3c6a5c26e8a8d13dab7f73b Mon Sep 17 00:00:00 2001 From: K7ZVX Date: Mon, 4 May 2026 21:47:18 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20Fundamental=20ID=20matching=20=E2=80=94?= =?UTF-8?q?=20packets,=20telemetry,=20and=20utilization=20now=20work?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: health engine keyed nodes by database row IDs instead of Meshtastic node numbers. Packets and telemetry could never match. Fixed: - Store _node_num on all normalized nodes (mesh_sources.py) - Key health engine node dict by _node_num (mesh_health.py) - Fix packet field names: from_node not from/fromId - Fix telemetry parsing: handle telemetryType/value structure - Increase packet/telemetry fetch limits for 24h coverage - Fix utilization formula to compute actual airtime percentage --- meshai/mesh_health.py | 164 ++++++++++++++++------ meshai/mesh_sources.py | 216 ++++------------------------- meshai/sources/meshmonitor_data.py | 20 ++- 3 files changed, 159 insertions(+), 241 deletions(-) diff --git a/meshai/mesh_health.py b/meshai/mesh_health.py index c178a5e..5e138e8 100644 --- a/meshai/mesh_health.py +++ b/meshai/mesh_health.py @@ -281,28 +281,27 @@ class MeshHealthEngine: # Aggregate all nodes from all sources all_nodes = source_manager.get_all_nodes() all_telemetry = source_manager.get_all_telemetry() - all_packets = [] - # Get packets from MeshMonitor sources (if available) - for status in source_manager.get_status(): - if status["type"] == "meshmonitor": - src = source_manager.get_source(status["name"]) - if src and hasattr(src, "packets"): - for pkt in src.packets: - tagged = dict(pkt) - tagged["_source"] = status["name"] - all_packets.append(tagged) + # FIX: Use aggregator method for deduped packets + all_packets = source_manager.get_all_packets() # Track if we have packet data for utilization calculation has_packet_data = len(all_packets) > 0 # Build node health records + # BUG 2 FIX: Use _node_num as the canonical key nodes: dict[str, NodeHealth] = {} for node in all_nodes: - node_id = node.get("id") or node.get("nodeId") or node.get("num") - if not node_id: - continue - node_id = str(node_id) + # Use _node_num set by source manager (canonical Meshtastic node number) + node_num = node.get("_node_num") + if node_num is not None: + node_id = str(node_num) + else: + # Fallback for nodes without _node_num + node_id = node.get("nodeNum") or node.get("id") or node.get("nodeId") or node.get("num") + if not node_id: + continue + node_id = str(node_id) # Skip if we already have this node from another source if node_id in nodes: @@ -363,28 +362,79 @@ class MeshHealthEngine: ) # Add telemetry data + # BUG 4 & 5 FIX: Handle MeshMonitor telemetryType/value structure for telem in all_telemetry: - node_id = str(telem.get("nodeId") or telem.get("node_id") or "") + # Get node number - try decimal first, then hex + node_num = telem.get("nodeNum") + if node_num is not None: + node_id = str(int(node_num)) + else: + node_hex = telem.get("nodeId") or telem.get("node_id") or "" + if isinstance(node_hex, str) and node_hex: + stripped = node_hex.lstrip("!") + try: + node_id = str(int(stripped, 16)) + except ValueError: + continue + else: + continue + if node_id not in nodes: continue node = nodes[node_id] - battery = telem.get("batteryLevel") or telem.get("battery_level") - voltage = telem.get("voltage") - if battery is not None: - node.battery_percent = float(battery) - if voltage is not None: - node.voltage = float(voltage) + # Handle MeshMonitor telemetryType/value structure + telem_type = (telem.get("telemetryType") or "").lower() + value = telem.get("value") - # Extract channel utilization and air_util_tx from device metrics - ch_util = telem.get("channelUtilization") or telem.get("channel_utilization") - if ch_util is not None: - node.channel_utilization = float(ch_util) + if telem_type and value is not None: + try: + value = float(value) + except (ValueError, TypeError): + value = None - air_tx = telem.get("airUtilTx") or telem.get("air_util_tx") - if air_tx is not None: - node.air_util_tx = float(air_tx) + if value is not None: + if telem_type in ("batterylevel", "battery_level", "battery"): + node.battery_percent = value + elif telem_type == "voltage": + node.voltage = value + elif telem_type in ("channelutilization", "channel_utilization"): + node.channel_utilization = value + elif telem_type in ("airutiltx", "air_util_tx"): + node.air_util_tx = value + elif telem_type in ("uplinkenabled", "uplink_enabled"): + node.uplink_enabled = bool(value) + + # Also try direct field access as fallback (for flat telemetry objects) + if node.battery_percent is None: + bat = telem.get("batteryLevel") or telem.get("battery_level") + if bat is not None: + try: + node.battery_percent = float(bat) + except (ValueError, TypeError): + pass + if node.voltage is None: + vol = telem.get("voltage") + if vol is not None: + try: + node.voltage = float(vol) + except (ValueError, TypeError): + pass + if node.channel_utilization is None: + ch_util = telem.get("channelUtilization") or telem.get("channel_utilization") + if ch_util is not None: + try: + node.channel_utilization = float(ch_util) + except (ValueError, TypeError): + pass + if node.air_util_tx is None: + air_tx = telem.get("airUtilTx") or telem.get("air_util_tx") + if air_tx is not None: + try: + node.air_util_tx = float(air_tx) + except (ValueError, TypeError): + pass # Check for uplink (MQTT) enabled uplink = telem.get("uplinkEnabled") or telem.get("uplink_enabled") @@ -392,20 +442,41 @@ class MeshHealthEngine: node.uplink_enabled = True # Count packets per node (last 24h) with portnum breakdown + # BUG 3 FIX: Use correct MeshMonitor packet field names twenty_four_hours_ago = now - 86400 for pkt in all_packets: pkt_time = pkt.get("timestamp") or pkt.get("rxTime") or 0 if pkt_time < twenty_four_hours_ago: continue - from_id = str(pkt.get("from") or pkt.get("fromId") or "") + # Extract from_node using multiple possible field names + from_raw = pkt.get("from_node") or pkt.get("from") or pkt.get("fromId") or pkt.get("from_node_id") + if from_raw is None: + continue + + # Normalize to canonical node number string + if isinstance(from_raw, int): + from_id = str(from_raw) + elif isinstance(from_raw, str): + # Could be hex like "!a1b2c3d4" or decimal string + stripped = from_raw.lstrip("!") + try: + from_id = str(int(stripped, 16)) + except ValueError: + if stripped.isdigit(): + from_id = stripped + else: + continue + else: + continue + if from_id not in nodes: continue nodes[from_id].packet_count_24h += 1 # Get portnum for breakdown - port_num = pkt.get("portnum") or pkt.get("port_num") or pkt.get("portnum_name") or "" + port_num = pkt.get("portnum_name") or pkt.get("portnum") or pkt.get("port_num") or "" port_name = str(port_num).upper() # Track by portnum @@ -671,24 +742,27 @@ class MeshHealthEngine: infra_score = 100.0 # No infrastructure = not penalized # Channel utilization (based on packet counts if available) + # BUG 7 FIX: Use actual Meshtastic airtime calculation if has_packet_data: - total_packets = sum(n.packet_count_24h for n in node_list) - baseline = len(node_list) * 500 - if baseline > 0: - util_percent = (total_packets / baseline) * 15 - else: - util_percent = 0 + total_non_text_packets = sum(n.non_text_packets for n in node_list) + # Average airtime per packet on MediumFast: ~200ms + # Total available airtime per hour: 3,600,000ms + # Utilization = (packets_per_hour * airtime_ms) / total_airtime_ms * 100 + packets_per_hour = total_non_text_packets / 24.0 # 24h window + airtime_per_packet_ms = 200 # ~200ms on MediumFast preset + util_percent = (packets_per_hour * airtime_per_packet_ms) / 3_600_000 * 100 - if util_percent < UTIL_HEALTHY: + # Apply scoring thresholds with interpolation + if util_percent < UTIL_HEALTHY: # <15% util_score = 100.0 - elif util_percent < UTIL_CAUTION: - util_score = 75.0 - elif util_percent < UTIL_WARNING: - util_score = 50.0 - elif util_percent < UTIL_UNHEALTHY: - util_score = 25.0 - else: - util_score = 0.0 + elif util_percent < UTIL_CAUTION: # 15-20% + util_score = 100.0 - ((util_percent - UTIL_HEALTHY) / (UTIL_CAUTION - UTIL_HEALTHY)) * 25 + elif util_percent < UTIL_WARNING: # 20-25% + util_score = 75.0 - ((util_percent - UTIL_CAUTION) / (UTIL_WARNING - UTIL_CAUTION)) * 25 + elif util_percent < UTIL_UNHEALTHY: # 25-35% + util_score = 50.0 - ((util_percent - UTIL_WARNING) / (UTIL_UNHEALTHY - UTIL_WARNING)) * 25 + else: # 35%+ + util_score = max(0.0, 25.0 - ((util_percent - UTIL_UNHEALTHY) / 10) * 25) else: # No packet data available - assume healthy utilization # This prevents penalizing the score when we simply don't have data diff --git a/meshai/mesh_sources.py b/meshai/mesh_sources.py index 56098ea..83866ba 100644 --- a/meshai/mesh_sources.py +++ b/meshai/mesh_sources.py @@ -30,21 +30,8 @@ MESHTASTIC_ROLE_MAP = { def _normalize_node(node: dict) -> dict: - """Normalize a node dict to consistent field names and formats. - - Handles differences between Meshview and MeshMonitor APIs: - - Role: integer enums -> string names - - GPS: last_lat/last_long -> latitude/longitude - - Timestamps: various formats -> last_heard (epoch seconds) - - Hardware: hw_model/hwModel -> hw_model (string preferred) - - Args: - node: Raw node dict from any source - - Returns: - Copy of node with normalized fields added - """ - result = dict(node) # Keep all original fields + """Normalize a node dict to consistent field names and formats.""" + result = dict(node) # === ROLE NORMALIZATION === role = node.get("role") @@ -58,25 +45,21 @@ def _normalize_node(node: dict) -> dict: result["role"] = str(role).upper() # === GPS NORMALIZATION === - # Latitude lat = None if "latitude" in node and node["latitude"] is not None: lat = node["latitude"] elif "last_lat" in node and node["last_lat"] is not None: lat = node["last_lat"] - # Meshview uses scaled integers (1e7) if isinstance(lat, int) and abs(lat) > 1000: lat = lat / 1e7 elif "lat" in node and node["lat"] is not None: lat = node["lat"] - # Longitude lon = None if "longitude" in node and node["longitude"] is not None: lon = node["longitude"] elif "last_long" in node and node["last_long"] is not None: lon = node["last_long"] - # Meshview uses scaled integers (1e7) if isinstance(lon, int) and abs(lon) > 1000: lon = lon / 1e7 elif "lon" in node and node["lon"] is not None: @@ -84,7 +67,6 @@ def _normalize_node(node: dict) -> dict: elif "lng" in node and node["lng"] is not None: lon = node["lng"] - # Filter out invalid GPS (0,0 or very close to 0) if lat is not None and lon is not None: if abs(lat) < 0.001 and abs(lon) < 0.001: lat = None @@ -94,30 +76,22 @@ def _normalize_node(node: dict) -> dict: result["longitude"] = lon # === TIMESTAMP NORMALIZATION === - # Normalize to "last_heard" as epoch seconds ts = None - - # Check last_seen_us first (Meshview microseconds) if "last_seen_us" in node and node["last_seen_us"] is not None: val = node["last_seen_us"] if isinstance(val, (int, float)) and val > 0: - ts = val / 1_000_000 # Microseconds to seconds + ts = val / 1_000_000 - # Check other timestamp fields if ts is None: for field in ("lastHeard", "last_heard", "last_seen", "lastSeen", "updated_at"): if field in node and node[field] is not None: val = node[field] if isinstance(val, (int, float)) and val > 0: - # Detect format by magnitude if val > 1e15: - # Microseconds ts = val / 1_000_000 elif val > 1e12: - # Milliseconds ts = val / 1_000 else: - # Already epoch seconds ts = float(val) break @@ -125,12 +99,10 @@ def _normalize_node(node: dict) -> dict: # === HARDWARE MODEL NORMALIZATION === hw = None - # Prefer string hw_model if "hw_model" in node and isinstance(node["hw_model"], str): hw = node["hw_model"] elif "hwModel" in node and isinstance(node["hwModel"], str): hw = node["hwModel"] - # Fall back to whatever is available if hw is None: if "hw_model" in node and node["hw_model"] is not None: hw = node["hw_model"] @@ -144,20 +116,7 @@ def _normalize_node(node: dict) -> dict: def _extract_node_num(node: dict) -> int | None: - """Extract numeric node ID from various formats. - - Handles: - - nodeNum: 662178887 (numeric) - - node_id: "!27780c47" (hex with prefix) - - node_id: "27780c47" (hex without prefix) - - num: 662178887 (numeric field) - - Args: - node: Node dict from any source - - Returns: - Numeric node ID or None if not extractable - """ + """Extract numeric node ID from various formats.""" # Try numeric fields first for field in ("nodeNum", "num", "node_num"): if field in node: @@ -171,7 +130,6 @@ def _extract_node_num(node: dict) -> int | None: if "node_id" in node: nid = node["node_id"] if isinstance(nid, str): - # Strip leading ! if present hex_str = nid.lstrip("!") try: return int(hex_str, 16) @@ -180,43 +138,37 @@ def _extract_node_num(node: dict) -> int | None: elif isinstance(nid, int): return nid - # Try generic id field + # Try generic id field (but NOT database row IDs) if "id" in node: val = node["id"] if isinstance(val, int): - return val + # Database row IDs are small; Meshtastic node numbers are large + if val > 100000: + return val if isinstance(val, str): - if val.isdigit(): - return int(val) - # Might be hex - hex_str = val.lstrip("!") - try: - return int(hex_str, 16) - except ValueError: - pass + if val.startswith("!"): + hex_str = val.lstrip("!") + try: + return int(hex_str, 16) + except ValueError: + pass + elif len(val) == 8: + try: + return int(val, 16) + except ValueError: + pass return None def _normalize_edge_key(edge: dict) -> tuple[int, int] | None: - """Normalize edge to a canonical (from_num, to_num) tuple. - - Edges are undirected for deduplication purposes, so - we return a sorted tuple (smaller_id, larger_id). - - Args: - edge: Edge dict from Meshview - - Returns: - Sorted tuple of (from_num, to_num) or None if invalid - """ + """Normalize edge to a canonical (from_num, to_num) tuple.""" from_num = edge.get("from_node") or edge.get("from") or edge.get("from_num") to_num = edge.get("to_node") or edge.get("to") or edge.get("to_num") if from_num is None or to_num is None: return None - # Convert to int if string if isinstance(from_num, str): if from_num.isdigit(): from_num = int(from_num) @@ -235,7 +187,6 @@ def _normalize_edge_key(edge: dict) -> tuple[int, int] | None: except ValueError: return None - # Return sorted tuple for consistent deduplication return (min(from_num, to_num), max(from_num, to_num)) @@ -243,11 +194,6 @@ class MeshSourceManager: """Manages multiple mesh data sources with deduplication.""" def __init__(self, source_configs: list[MeshSourceConfig]): - """Initialize source manager. - - Args: - source_configs: List of source configurations - """ self._sources: dict[str, MeshviewSource | MeshMonitorDataSource] = {} for cfg in source_configs: @@ -286,11 +232,6 @@ class MeshSourceManager: logger.error(f"Failed to create source '{name}': {e}") def refresh_all(self) -> int: - """Call maybe_refresh() on all sources. - - Returns: - Number of sources that refreshed - """ refreshed = 0 for name, source in self._sources.items(): try: @@ -301,69 +242,40 @@ class MeshSourceManager: return refreshed def get_source(self, name: str) -> Optional[MeshviewSource | MeshMonitorDataSource]: - """Get a specific source by name. - - Args: - name: Source name - - Returns: - Source instance or None if not found - """ return self._sources.get(name) def get_all_nodes(self) -> list[dict]: - """Get deduplicated nodes from all sources. - - Nodes are normalized and deduplicated by their numeric node ID. - When a node appears in multiple sources, data is merged with: - - Most fields: last source wins - - _sources: accumulates all source names - - Returns: - List of deduplicated node dicts with '_sources' field (list) - """ + """Get deduplicated nodes from all sources with _node_num field.""" nodes_by_num: dict[int, dict] = {} for name, source in self._sources.items(): for node in source.nodes: - # Normalize the node data first normalized = _normalize_node(node) - node_num = _extract_node_num(normalized) + if node_num is None: - # Can't deduplicate, include as-is with source tag normalized["_sources"] = [name] - # Use a negative counter as pseudo-key to avoid collisions pseudo_key = -len(nodes_by_num) - 1 nodes_by_num[pseudo_key] = normalized continue + # BUG 1 FIX: Store _node_num on the normalized dict + normalized["_node_num"] = node_num + if node_num in nodes_by_num: - # Merge: update existing with new data existing = nodes_by_num[node_num] - # Add new source to sources list if name not in existing["_sources"]: existing["_sources"].append(name) - # Update all fields except _sources for key, value in normalized.items(): - if key != "_sources" and value is not None: + if key not in ("_sources", "_node_num") and value is not None: existing[key] = value else: - # New node normalized["_sources"] = [name] nodes_by_num[node_num] = normalized return list(nodes_by_num.values()) def get_all_edges(self) -> list[dict]: - """Get deduplicated edges from all Meshview sources. - - Edges are deduplicated by (from_num, to_num) sorted tuple. - When an edge appears in multiple sources, data is merged. - - Returns: - List of deduplicated edge dicts with '_sources' field - """ edges_by_key: dict[tuple[int, int], dict] = {} for name, source in self._sources.items(): @@ -373,25 +285,20 @@ class MeshSourceManager: for edge in source.edges: edge_key = _normalize_edge_key(edge) if edge_key is None: - # Can't deduplicate, include as-is tagged = dict(edge) tagged["_sources"] = [name] - # Use a tuple with negative to avoid collision pseudo_key = (-len(edges_by_key) - 1, 0) edges_by_key[pseudo_key] = tagged continue if edge_key in edges_by_key: - # Merge: update existing existing = edges_by_key[edge_key] if name not in existing["_sources"]: existing["_sources"].append(name) - # Update fields for key, value in edge.items(): if key != "_sources" and value is not None: existing[key] = value else: - # New edge tagged = dict(edge) tagged["_sources"] = [name] edges_by_key[edge_key] = tagged @@ -399,14 +306,6 @@ class MeshSourceManager: return list(edges_by_key.values()) def get_all_telemetry(self) -> list[dict]: - """Get deduplicated telemetry from all MeshMonitor sources. - - Telemetry is deduplicated by (node_num, timestamp) tuple. - - Returns: - List of deduplicated telemetry dicts with '_sources' field - """ - # Key: (node_num, timestamp) telemetry_by_key: dict[tuple[int, float], dict] = {} for name, source in self._sources.items(): @@ -418,7 +317,6 @@ class MeshSourceManager: timestamp = item.get("timestamp") or item.get("time") or item.get("ts") if node_num is None or timestamp is None: - # Can't deduplicate tagged = dict(item) tagged["_sources"] = [name] pseudo_key = (-len(telemetry_by_key) - 1, 0.0) @@ -442,11 +340,6 @@ class MeshSourceManager: return list(telemetry_by_key.values()) def get_all_traceroutes(self) -> list[dict]: - """Get traceroutes from all MeshMonitor sources, tagged with source name. - - Returns: - List of traceroute dicts with '_sources' field - """ all_traceroutes = [] for name, source in self._sources.items(): if isinstance(source, MeshMonitorDataSource): @@ -457,11 +350,6 @@ class MeshSourceManager: return all_traceroutes def get_all_channels(self) -> list[dict]: - """Get channels from all MeshMonitor sources, tagged with source name. - - Returns: - List of channel dicts with '_sources' field - """ all_channels = [] for name, source in self._sources.items(): if isinstance(source, MeshMonitorDataSource): @@ -472,11 +360,6 @@ class MeshSourceManager: return all_channels def get_status(self) -> list[dict]: - """Get status of all sources for TUI display. - - Returns: - List of status dicts with source info - """ status_list = [] for name, source in self._sources.items(): status = { @@ -501,15 +384,6 @@ class MeshSourceManager: return status_list def get_stats_by_source(self) -> dict[str, dict]: - """Get per-source statistics without summing across sources. - - Returns: - Dict mapping source name to stats dict containing: - - node_count: Number of nodes from this source - - edge_count: Number of edges (Meshview only) - - telemetry_count: Number of telemetry records (MeshMonitor only) - - is_loaded: Whether source has data - """ stats = {} for name, source in self._sources.items(): source_stats = { @@ -532,11 +406,6 @@ class MeshSourceManager: return stats def get_dedup_stats(self) -> dict: - """Get deduplication statistics. - - Returns: - Dict with raw and deduplicated counts - """ raw_nodes = sum(len(s.nodes) for s in self._sources.values()) raw_edges = sum( len(s.edges) for s in self._sources.values() @@ -556,14 +425,6 @@ class MeshSourceManager: } def get_all_packets(self) -> list[dict]: - """Get deduplicated packets from all MeshMonitor sources. - - Packets are deduplicated by packet_id to avoid double-counting - when multiple sources report the same MQTT feed. - - Returns: - List of packet dicts with '_sources' field - """ packets_by_id: dict[int, dict] = {} for name, source in self._sources.items(): @@ -576,14 +437,12 @@ class MeshSourceManager: for pkt in source.packets: packet_id = pkt.get("packet_id") or pkt.get("id") if packet_id is None: - # Fallback key: (from_node, timestamp, portnum) from_node = pkt.get("from_node") or pkt.get("from") ts = pkt.get("timestamp") or pkt.get("rxTime") portnum = pkt.get("portnum") if from_node and ts: packet_id = hash((from_node, ts, portnum)) else: - # Can't deduplicate, use negative counter packet_id = -len(packets_by_id) - 1 if packet_id in packets_by_id: @@ -598,21 +457,12 @@ class MeshSourceManager: return list(packets_by_id.values()) def get_traffic_stats(self) -> dict[str, dict]: - """Get traffic statistics from all sources. - - Returns: - Dict mapping source name to traffic stats: - - hourly_counts: list of {period, count} for last 24h - - total_packets: total packet count - - packets_per_hour: average packets per hour - """ stats = {} for name, source in self._sources.items(): source_stats = {} if isinstance(source, MeshviewSource): - # Meshview has stats with hourly breakdown if hasattr(source, "stats") and source.stats: data = source.stats.get("data", []) source_stats["hourly_counts"] = data @@ -625,7 +475,6 @@ class MeshSourceManager: source_stats["total_packets_all_time"] = source.counts.get("total_packets", 0) elif isinstance(source, MeshMonitorDataSource): - # MeshMonitor has network_stats if hasattr(source, "network_stats") and source.network_stats: ns = source.network_stats source_stats["total_nodes"] = ns.get("totalNodes", 0) @@ -633,7 +482,6 @@ class MeshSourceManager: source_stats["traceroute_count"] = ns.get("tracerouteCount", 0) source_stats["last_updated"] = ns.get("lastUpdated", 0) - # Count packets by portnum for breakdown if hasattr(source, "packets") and source.packets: portnum_counts: dict[str, int] = {} for pkt in source.packets: @@ -648,11 +496,6 @@ class MeshSourceManager: return stats def get_solar_data(self) -> list[dict]: - """Get solar/power data from all MeshMonitor sources. - - Returns: - List of solar data dicts with '_sources' field - """ all_solar = [] for name, source in self._sources.items(): if isinstance(source, MeshMonitorDataSource): @@ -664,11 +507,6 @@ class MeshSourceManager: return all_solar def get_network_stats(self) -> dict[str, dict]: - """Get network statistics from all sources. - - Returns: - Dict mapping source name to network stats dict - """ stats = {} for name, source in self._sources.items(): @@ -699,10 +537,8 @@ class MeshSourceManager: @property def source_count(self) -> int: - """Get number of active sources.""" return len(self._sources) @property def source_names(self) -> list[str]: - """Get list of source names.""" return list(self._sources.keys()) diff --git a/meshai/sources/meshmonitor_data.py b/meshai/sources/meshmonitor_data.py index cc6dfc2..83fe0fa 100644 --- a/meshai/sources/meshmonitor_data.py +++ b/meshai/sources/meshmonitor_data.py @@ -181,8 +181,11 @@ class MeshMonitorDataSource: else: errors.append("channels") - # Fetch telemetry - data = self._fetch_json("/api/v1/telemetry") + # Fetch telemetry - BUG 6 FIX: Request more records for 24h coverage + data = self._fetch_json("/api/v1/telemetry?limit=5000") + if data is None: + # Fallback without limit param + data = self._fetch_json("/api/v1/telemetry") if data is not None: self._telemetry = data if isinstance(data, list) else [] success_count += 1 @@ -190,8 +193,10 @@ class MeshMonitorDataSource: else: errors.append("telemetry") - # Fetch traceroutes - data = self._fetch_json("/api/v1/traceroutes") + # Fetch traceroutes - BUG 6 FIX: Request more records + data = self._fetch_json("/api/v1/traceroutes?limit=1000") + if data is None: + data = self._fetch_json("/api/v1/traceroutes") if data is not None: self._traceroutes = data if isinstance(data, list) else [] success_count += 1 @@ -217,8 +222,11 @@ class MeshMonitorDataSource: else: errors.append("topology") - # Fetch packets - data = self._fetch_json("/api/v1/packets") + # Fetch packets - BUG 6 FIX: Request more packets for 24h coverage + data = self._fetch_json("/api/v1/packets?limit=5000") + if data is None: + # Fallback without limit param + data = self._fetch_json("/api/v1/packets") if data is not None: self._packets = data if isinstance(data, list) else [] success_count += 1