diff --git a/meshview/lang/en.json b/meshview/lang/en.json index 18df79d..b0e77a9 100644 --- a/meshview/lang/en.json +++ b/meshview/lang/en.json @@ -22,47 +22,69 @@ } }, "chat": { + "chat_title": "Chats:", "replying_to": "Replying to:", "view_packet_details": "View packet details" }, "nodelist": { - "search_placeholder": "Search by name or ID...", - "all_roles": "All Roles", - "all_channels": "All Channels", - "all_hw_models": "All HW Models", - "all_firmware": "All Firmware", - "export_csv": "Export CSV", - "clear_filters": "Clear Filters", - "showing": "Showing", - "nodes": "nodes", - "short": "Short", - "long_name": "Long Name", - "hw_model": "HW Model", - "firmware": "Firmware", - "role": "Role", - "last_lat": "Last Latitude", - "last_long": "Last Longitude", - "channel": "Channel", - "last_update": "Last Update", - "loading_nodes": "Loading nodes...", - "no_nodes": "No nodes found", - "error_nodes": "Error loading nodes" + "search_placeholder": "Search by name or ID...", + "all_roles": "All Roles", + "all_channels": "All Channels", + "all_hw": "All HW Models", + "all_firmware": "All Firmware", + + "show_favorites": "⭐ Show Favorites", + "show_all": "⭐ Show All", + "export_csv": "Export CSV", + "clear_filters": "Clear Filters", + + "showing_nodes": "Showing", + "nodes_suffix": "nodes", + + "loading_nodes": "Loading nodes...", + "error_loading_nodes": "Error loading nodes", + "no_nodes_found": "No nodes found", + + "short_name": "Short", + "long_name": "Long Name", + "hw_model": "HW Model", + "firmware": "Firmware", + "role": "Role", + "last_lat": "Last Latitude", + "last_long": "Last Longitude", + "channel": "Channel", + "last_seen": "Last Seen", + "favorite": "Favorite", + + "time_just_now": "just now", + "time_min_ago": "min ago", + "time_hr_ago": "hr ago", + "time_day_ago": "day ago", + "time_days_ago": "days ago" }, + "net": { "number_of_checkins": "Number of Check-ins:", "view_packet_details": "View packet details", "view_all_packets_from_node": "View all packets from this node", "no_packets_found": "No packets found." }, + "map": { - "channel": "Channel:", - "model": "Model:", - "role": "Role:", + "show_routers_only": "Show Routers Only", + "share_view": "Share This View", + "reset_filters": "Reset Filters To Defaults", + "channel_label": "Channel:", + "model_label": "Model:", + "role_label": "Role:", "last_seen": "Last seen:", "firmware": "Firmware:", - "show_routers_only": "Show Routers Only", - "share_view": "Share This View" + "link_copied": "Link Copied!", + "legend_traceroute": "Ruta de traceroute (con flechas)", + "legend_neighbor": "Enlace de vecino" + }, + "stats": { "mesh_stats_summary": "Mesh Statistics - Summary (all available in Database)", @@ -82,21 +104,20 @@ "all_channels": "All Channels", "node_id": "Node ID" }, - "top": - { - "top_traffic_nodes": "Top Traffic Nodes (last 24 hours)", - "chart_description_1": "This chart shows a bell curve (normal distribution) based on the total \"Times Seen\" values for all nodes. It helps visualize how frequently nodes are heard, relative to the average.", - "chart_description_2": "This \"Times Seen\" value is the closest that we can get to Mesh utilization by node.", - "mean_label": "Mean:", - "stddev_label": "Standard Deviation:", + "top": { + "top_traffic_nodes": "Top Nodes Traffic", + "channel": "Channel", + "search": "Search", + "search_placeholder": "Search nodes...", "long_name": "Long Name", "short_name": "Short Name", - "channel": "Channel", - "packets_sent": "Packets Sent", - "times_seen": "Times Seen", - "seen_percent": "Seen % of Mean", - "no_nodes": "No top traffic nodes available." + "packets_sent": "Sent (24h)", + "times_seen": "Seen (24h)", + "avg_gateways": "Avg Gateways", + "showing_nodes": "Showing", + "nodes_suffix": "nodes" }, + "nodegraph": { "channel_label": "Channel:", @@ -131,11 +152,59 @@ "telemetry": "Telemetry", "trace_route": "Trace Route", "neighbor_info": "Neighbor Info", - "direct_to_mqtt": "direct to MQTT", "all": "All", "map": "Map", "graph": "Graph" - } + }, + "node": { + "specifications": "Specifications:", + "node_id": "Node ID:", + "long_name": "Long Name:", + "short_name": "Short Name:", + "hw_model": "Hardware Model:", + "firmware": "Firmware:", + "role": "Role:", + "channel": "Channel:", + "latitude": "Latitude:", + "longitude": "Longitude:", + "last_update": "Last Update:", + "battery_voltage": "Battery & Voltage", + "air_channel": "Air & Channel Utilization", + "environment": "Environment Metrics", + "neighbors_chart": "Neighbors (Signal-to-Noise)", + "expand": "Expand", + "export_csv": "Export CSV", + "time": "Time", + "packet_id": "Packet ID", + "from": "From", + "to": "To", + "port": "Port", + "direct_to_mqtt": "Direct to MQTT", + "all_broadcast": "All" + }, + "packet": { + "loading": "Loading packet information...", + "packet_id_label": "Packet ID", + "from_node": "From Node", + "to_node": "To Node", + "channel": "Channel", + "port": "Port", + "raw_payload": "Raw Payload", + "decoded_telemetry": "Decoded Telemetry", + "location": "Location", + "seen_by": "Seen By", + "gateway": "Gateway", + "rssi": "RSSI", + "snr": "SNR", + "hops": "Hop", + "time": "Time", + "packet_source": "Packet Source", + "distance": "Distance", + "node_id_short": "Node ID", + "all_broadcast": "All", + "direct_to_mqtt": "Direct to MQTT" + } + } \ No newline at end of file diff --git a/meshview/lang/es.json b/meshview/lang/es.json index e430b12..83980fb 100644 --- a/meshview/lang/es.json +++ b/meshview/lang/es.json @@ -1,16 +1,16 @@ { "base": { - "conversations": "Conversaciones", + "chat": "Conversaciones", "nodes": "Nodos", - "everything": "Mostrar Todo", - "graph": "Gráficos de la Malla", + "everything": "Mostrar todo", + "graphs": "Gráficos de la Malla", "net": "Red Semanal", "map": "Mapa en Vivo", "stats": "Estadísticas", "top": "Nodos con Mayor Tráfico", "footer": "Visita Meshview en Github.", - "node id": "ID de Nodo", - "go to node": "Ir al nodo", + "node_id": "ID de Nodo", + "go_to_node": "Ir al nodo", "all": "Todos", "portnum_options": { "1": "Mensaje de Texto", @@ -21,48 +21,65 @@ "71": "Información de Vecinos" } }, + "chat": { - "replying_to": "Respondiendo a:", - "view_packet_details": "Ver detalles del paquete" + "chat_title": "Conversaciones:", + "replying_to": "Respondiendo a:", + "view_packet_details": "Ver detalles del paquete" }, + "nodelist": { "search_placeholder": "Buscar por nombre o ID...", - "all_roles": "Todos los Roles", - "all_channels": "Todos los Canales", - "all_hw_models": "Todos los Modelos", - "all_firmware": "Todo el Firmware", + "all_roles": "Todos los roles", + "all_channels": "Todos los canales", + "all_hw": "Todos los modelos", + "all_firmware": "Todo el firmware", + "show_favorites": "⭐ Mostrar favoritos", + "show_all": "⭐ Mostrar todos", "export_csv": "Exportar CSV", - "clear_filters": "Limpiar Filtros", - "showing": "Mostrando", - "nodes": "nodos", - "short": "Corto", - "long_name": "Largo", - "hw_model": "Modelo", + "clear_filters": "Limpiar filtros", + "showing_nodes": "Mostrando", + "nodes_suffix": "nodos", + "loading_nodes": "Cargando nodos...", + "error_loading_nodes": "Error al cargar nodos", + "no_nodes_found": "No se encontraron nodos", + "short_name": "Corto", + "long_name": "Nombre largo", + "hw_model": "Modelo HW", "firmware": "Firmware", "role": "Rol", - "last_lat": "Última Latitud", - "last_long": "Última Longitud", + "last_lat": "Última latitud", + "last_long": "Última longitud", "channel": "Canal", - "last_update": "Última Actualización", - "loading_nodes": "Cargando nodos...", - "no_nodes": "No se encontraron nodos", - "error_nodes": "Error al cargar nodos" + "last_seen": "Última vez visto", + "favorite": "Favorito", + "time_just_now": "justo ahora", + "time_min_ago": "min atrás", + "time_hr_ago": "h atrás", + "time_day_ago": "día atrás", + "time_days_ago": "días atrás" }, + "net": { - "number_of_checkins": "Número de registros:", - "view_packet_details": "Ver detalles del paquete", - "view_all_packets_from_node": "Ver todos los paquetes de este nodo", - "no_packets_found": "No se encontraron paquetes." + "net_title": "Red Semanal:", + "total_messages": "Número de mensajes:", + "view_packet_details": "Más Detalles" }, + "map": { - "channel": "Canal:", - "model": "Modelo:", - "role": "Rol:", + "filter_routers_only": "Mostrar solo enrutadores", + "share_view": "Compartir esta vista", + "reset_filters": "Restablecer filtros", + "channel_label": "Canal:", + "model_label": "Modelo:", + "role_label": "Rol:", "last_seen": "Visto por última vez:", "firmware": "Firmware:", - "show_routers_only": "Mostrar solo enrutadores", - "share_view": "Compartir esta vista" + "link_copied": "¡Enlace copiado!", + "legend_traceroute": "Ruta de traceroute (flechas de dirección)", + "legend_neighbor": "Vínculo de vecinos" }, + "stats": { "mesh_stats_summary": "Estadísticas de la Malla - Resumen (completas en la base de datos)", "total_nodes": "Nodos Totales", @@ -80,22 +97,22 @@ "export_csv": "Exportar CSV", "all_channels": "Todos los Canales" }, + "top": { - "top_traffic_nodes": "Tráfico (últimas 24 horas)", - "chart_description_1": "Este gráfico muestra una curva normal (distribución normal) basada en el valor total de \"Veces Visto\" para todos los nodos. Ayuda a visualizar con qué frecuencia se detectan los nodos en relación con el promedio.", - "chart_description_2": "Este valor de \"Veces Visto\" es lo más aproximado que tenemos al nivel de uso de la malla por nodo.", - "mean_label": "Media:", - "stddev_label": "Desviación Estándar:", + "top_traffic_nodes": "Tráfico de Nodos (24h)", + "channel": "Canal", + "search": "Buscar", + "search_placeholder": "Buscar nodos...", "long_name": "Nombre Largo", "short_name": "Nombre Corto", - "channel": "Canal", - "packets_sent": "Paquetes Enviados", - "times_seen": "Veces Visto", - "seen_percent": "% Visto respecto a la Media", - "no_nodes": "No hay nodos con mayor tráfico disponibles." + "packets_sent": "Enviados (24h)", + "times_seen": "Visto (24h)", + "avg_gateways": "Promedio de Gateways", + "showing_nodes": "Mostrando", + "nodes_suffix": "nodos" }, - "nodegraph": - { + + "nodegraph": { "channel_label": "Canal:", "search_placeholder": "Buscar nodo...", "search_button": "Buscar", @@ -109,34 +126,68 @@ "unknown": "Desconocido", "node_not_found": "¡Nodo no encontrado en el canal actual!" }, - "firehose": - { - "live_feed": "📡 Flujo en Vivo", - "pause": "Pausar", - "resume": "Continuar", - "time": "Hora", - "packet_id": "ID del Paquete", - "from": "De", - "to": "Para", - "port": "Puerto", - "links": "Enlaces", - "unknown_app": "APLICACIÓN DESCONOCIDA", - "text_message": "Mensaje de Texto", - "position": "Posición", - "node_info": "Información del Nodo", - "routing": "Enrutamiento", - "administration": "Administración", - "waypoint": "Punto de Ruta", - "store_forward": "Almacenar y Reenviar", - "telemetry": "Telemetría", - "trace_route": "Rastreo de Ruta", - "neighbor_info": "Información de Vecinos", + "firehose": { + "live_feed": "📡 Flujo en vivo", + "pause": "Pausar", + "resume": "Reanudar", + "time": "Hora", + "packet_id": "ID de paquete", + "from": "De", + "to": "A", + "port": "Puerto", + "direct_to_mqtt": "Directo a MQTT", + "all_broadcast": "Todos" + }, - "direct_to_mqtt": "Directo a MQTT", - "all": "Todos", - "map": "Mapa", - "graph": "Gráfico" - } + "node": { + "specifications": "Especificaciones:", + "node_id": "ID de Nodo:", + "long_name": "Nombre Largo:", + "short_name": "Nombre Corto:", + "hw_model": "Modelo de Hardware:", + "firmware": "Firmware:", + "role": "Rol:", + "channel": "Canal:", + "latitude": "Latitud:", + "longitude": "Longitud:", + "last_update": "Última Actualización:", + "battery_voltage": "Batería y voltaje", + "air_channel": "Utilización del aire y del canal", + "environment": "Métricas Ambientales", + "neighbors_chart": "Vecinos (Relación Señal/Ruido)", + "expand": "Ampliar", + "export_csv": "Exportar CSV", + "time": "Hora", + "packet_id": "ID del Paquete", + "from": "De", + "to": "A", + "port": "Puerto", + "direct_to_mqtt": "Directo a MQTT", + "all_broadcast": "Todos" + }, + "packet": { + "loading": "Cargando información del paquete...", + "packet_id_label": "ID del Paquete", + "from_node": "De", + "to_node": "A", + "channel": "Canal", + "port": "Puerto", + "raw_payload": "Payload sin procesar", + "decoded_telemetry": "Telemetría Decodificada", + "location": "Ubicación", + "seen_by": "Visto por", + "gateway": "Gateway", + "rssi": "RSSI", + "snr": "SNR", + "hops": "Saltos", + "time": "Hora", + "packet_source": "Origen del Paquete", + "distance": "Distancia", + "node_id_short": "ID de Nodo", + "all_broadcast": "Todos", + "direct_to_mqtt": "Directo a MQTT", + "signal": "Señal" + } } diff --git a/meshview/store.py b/meshview/store.py index b50d7a7..db85028 100644 --- a/meshview/store.py +++ b/meshview/store.py @@ -1,6 +1,5 @@ from datetime import datetime, timedelta - -from sqlalchemy import and_, func, or_, select, text +from sqlalchemy import select, and_, or_, func, cast, Text from sqlalchemy.orm import lazyload from meshview import database, models @@ -27,18 +26,12 @@ async def get_fuzzy_nodes(query): async def get_packets( from_node_id=None, to_node_id=None, - node_id=None, # legacy: match either from OR to + node_id=None, # legacy portnum=None, after=None, - contains=None, # NEW: SQL-level substring match + contains=None, # substring search limit=50, ): - """ - SQLAlchemy 2.0 async ORM version. - Supports strict from/to/node filtering, substring payload search, - portnum, since, and limit. - """ - async with database.async_session() as session: stmt = select(models.Packet) conditions = [] @@ -51,36 +44,40 @@ async def get_packets( if to_node_id is not None: conditions.append(models.Packet.to_node_id == to_node_id) - # Legacy node ID filter: match either direction + # Legacy node_id (either direction) if node_id is not None: conditions.append( - or_(models.Packet.from_node_id == node_id, models.Packet.to_node_id == node_id) + or_( + models.Packet.from_node_id == node_id, + models.Packet.to_node_id == node_id, + ) ) # Port filter if portnum is not None: conditions.append(models.Packet.portnum == portnum) - # Timestamp filter + # Timestamp filter using microseconds if after is not None: conditions.append(models.Packet.import_time_us > after) - # Case-insensitive substring search on UTF-8 payload (stored as BLOB) + # Case-insensitive substring search on payload (BLOB → TEXT) if contains: - contains_lower = contains.lower() - conditions.append(func.lower(models.Packet.payload).like(f"%{contains_lower}%")) + contains_lower = f"%{contains.lower()}%" + payload_text = cast(models.Packet.payload, Text) + conditions.append(func.lower(payload_text).like(contains_lower)) - # Apply all conditions + # Apply WHERE conditions if conditions: stmt = stmt.where(and_(*conditions)) - # Order newest → oldest + # Order by newest first stmt = stmt.order_by(models.Packet.import_time_us.desc()) - # Apply limit + # Limit stmt = stmt.limit(limit) - # Execute query + # Run query result = await session.execute(stmt) return result.scalars().all() diff --git a/meshview/templates/chat.html b/meshview/templates/chat.html index bfba95b..bd719fb 100644 --- a/meshview/templates/chat.html +++ b/meshview/templates/chat.html @@ -53,8 +53,9 @@
-

- 💬 Chat +

+ 💬 +

@@ -71,24 +72,45 @@ document.addEventListener("DOMContentLoaded", async () => { const packetMap = new Map(); let chatLang = {}; - function applyTranslations(dict, root = document) { + /* ========================================================== + TRANSLATIONS FOR CHAT PAGE + ========================================================== */ + function applyTranslations(dict, root=document) { root.querySelectorAll("[data-translate]").forEach(el => { const key = el.dataset.translate; const val = dict[key]; if (!val) return; if (el.placeholder) el.placeholder = val; - else if (el.tagName === "INPUT" && el.value) el.value = val; - else if (key === "footer") el.innerHTML = val; else el.textContent = val; }); } + async function loadChatLang() { + try { + const cfg = await window._siteConfigPromise; + const langCode = cfg?.site?.language || "en"; + const res = await fetch(`/api/lang?lang=${langCode}§ion=chat`); + chatLang = await res.json(); + + // Apply to existing DOM + applyTranslations(chatLang); + } catch (err) { + console.error("Chat translation load failed:", err); + } + } + + /* ========================================================== + SAFE HTML + ========================================================== */ function escapeHtml(text) { const div = document.createElement("div"); div.textContent = text ?? ""; return div.innerHTML; } + /* ========================================================== + RENDERING PACKETS + ========================================================== */ function renderPacket(packet, highlight = false) { if (renderedPacketIds.has(packet.id)) return; renderedPacketIds.add(packet.id); @@ -139,20 +161,31 @@ document.addEventListener("DOMContentLoaded", async () => { const div = document.createElement("div"); div.className = "row chat-packet" + (highlight ? " flash" : ""); div.dataset.packetId = packet.id; + div.innerHTML = ` - ${formattedTimestamp} + + ${formattedTimestamp} + + 🔎 - ${escapeHtml(packet.channel || "")} + ${escapeHtml(packet.channel || "")} + ${escapeHtml((packet.long_name || "").trim() || `Node ${packet.from_node_id}`)} - ${escapeHtml(packet.payload)}${replyHtml} + + + ${escapeHtml(packet.payload)}${replyHtml} + `; + chatContainer.prepend(div); + + // Translate newly added DOM applyTranslations(chatLang, div); if (highlight) setTimeout(() => div.classList.remove("flash"), 2500); @@ -161,26 +194,27 @@ document.addEventListener("DOMContentLoaded", async () => { function renderPacketsEnsureDescending(packets, highlight=false) { if (!Array.isArray(packets) || packets.length===0) return; const sortedDesc = packets.slice().sort((a,b)=>{ - const aTime = - (a.import_time_us && a.import_time_us > 0) - ? a.import_time_us - : (a.import_time ? new Date(a.import_time).getTime() * 1000 : 0); - const bTime = - (b.import_time_us && b.import_time_us > 0) - ? b.import_time_us - : (b.import_time ? new Date(b.import_time).getTime() * 1000 : 0); + const aTime = a.import_time_us || (new Date(a.import_time).getTime() * 1000); + const bTime = b.import_time_us || (new Date(b.import_time).getTime() * 1000); return bTime - aTime; }); - for (let i=sortedDesc.length-1; i>=0; i--) renderPacket(sortedDesc[i], highlight); + for (let i=sortedDesc.length-1; i>=0; i--) { + renderPacket(sortedDesc[i], highlight); + } } + /* ========================================================== + FETCHING PACKETS + ========================================================== */ async function fetchInitial() { try { const resp = await fetch("/api/packets?portnum=1&limit=100"); const data = await resp.json(); if (data?.packets?.length) renderPacketsEnsureDescending(data.packets); lastTime = data?.latest_import_time || lastTime; - } catch(err){ console.error("Initial fetch error:", err); } + } catch(err){ + console.error("Initial fetch error:", err); + } } async function fetchUpdates() { @@ -192,21 +226,19 @@ document.addEventListener("DOMContentLoaded", async () => { const data = await resp.json(); if (data?.packets?.length) renderPacketsEnsureDescending(data.packets, true); lastTime = data?.latest_import_time || lastTime; - } catch(err){ console.error("Fetch updates error:", err); } + } catch(err){ + console.error("Fetch updates error:", err); + } } - async function loadChatLang() { - try { - const cfg = await window._siteConfigPromise; - const langCode = cfg?.site?.language || "en"; - const res = await fetch(`/api/lang?lang=${langCode}§ion=chat`); - chatLang = await res.json(); - applyTranslations(chatLang); - } catch(err){ console.error("Chat translation load failed:", err); } - } + /* ========================================================== + INIT + ========================================================== */ + await loadChatLang(); // load translations FIRST + await fetchInitial(); // then fetch initial packets - await Promise.all([loadChatLang(), fetchInitial()]); setInterval(fetchUpdates, 5000); }); + {% endblock %} diff --git a/meshview/templates/firehose.html b/meshview/templates/firehose.html index d40a1e7..a2d5948 100644 --- a/meshview/templates/firehose.html +++ b/meshview/templates/firehose.html @@ -12,6 +12,16 @@ border-radius: 6px; } +.port-tag { + display: inline-block; + padding: 2px 6px; + border-radius: 6px; + font-size: 0.75rem; + font-weight: 500; + color: #fff; +} + +/* Packet table */ .packet-table { width: 100%; border-collapse: collapse; @@ -31,6 +41,7 @@ .packet-table tr:nth-of-type(odd) { background-color: #272b2f; } .packet-table tr:nth-of-type(even) { background-color: #212529; } +/* Port tag */ .port-tag { display: inline-block; padding: 1px 6px; @@ -39,29 +50,22 @@ font-weight: 500; color: #fff; } -.port-0 { background-color: #6c757d; } -.port-1 { background-color: #007bff; } -.port-3 { background-color: #28a745; } -.port-4 { background-color: #ffc107; } -.port-5 { background-color: #dc3545; } -.port-6 { background-color: #20c997; } -.port-65 { background-color: #ff66b3; } -.port-67 { background-color: #17a2b8; } -.port-70 { background-color: #6f42c1; } -.port-71 { background-color: #fd7e14; } .to-mqtt { font-style: italic; color: #aaa; } +/* Payload rows */ .payload-row { display: none; background-color: #1b1e22; } .payload-cell { padding: 8px 12px; font-family: monospace; white-space: pre-wrap; color: #b0bec5; - border-top: none; } -.packet-table tr.expanded + .payload-row { display: table-row; } +.packet-table tr.expanded + .payload-row { + display: table-row; +} +/* Toggle arrow */ .toggle-btn { cursor: pointer; color: #aaa; @@ -70,7 +74,7 @@ } .toggle-btn:hover { color: #fff; } -/* Link next to port tag */ +/* Inline link next to port tag */ .inline-link { margin-left: 6px; font-weight: bold; @@ -84,9 +88,16 @@ {% block body %}
+
-
📡 Live Feed
- +

📡 Live Feed

+ +
@@ -101,34 +112,71 @@
+
+ {% endblock %} diff --git a/meshview/templates/map.html b/meshview/templates/map.html index 1c7d3d3..d2627c1 100644 --- a/meshview/templates/map.html +++ b/meshview/templates/map.html @@ -1,9 +1,7 @@ {% extends "base.html" %} {% block css %} - + {% endblock %} {% block body %}
- - - - - + - - - + + + + + + + + + + + + +
- Showing 0 nodes + Showing + 0 + nodes
- - - - - - - - - - + + + + + + + + + + - + + +
Short Long Name HW Model Firmware Role Last Latitude Last Longitude Channel Last Seen Short Long Name HW Model Firmware Role Last Latitude Last Longitude Channel Last Seen
Loading nodes...
+ Loading nodes... +
+ {% endblock %} diff --git a/meshview/templates/top.html b/meshview/templates/top.html index 9531503..49918bb 100644 --- a/meshview/templates/top.html +++ b/meshview/templates/top.html @@ -15,7 +15,7 @@ display: flex; flex-wrap: wrap; align-items: center; - gap: 10px; + gap: 20px; margin-bottom: 15px; } @@ -38,14 +38,6 @@ color: #ddd; } - .filter-bar { - display: flex; - flex-direction: row; - align-items: center; - gap: 20px; - } - - table th { background-color: #333; } table tbody tr:nth-child(odd) { background-color: #272b2f; } table tbody tr:nth-child(even) { background-color: #212529; } @@ -65,37 +57,42 @@ {% block body %} -

Top Nodes Traffic

+

Top Nodes Traffic

-
-
- - +
+
+ + +
+ +
+ + +
-
- - + +
+ Showing + 0 + nodes
-
- - -
- - - - - - + + + + + + @@ -105,74 +102,105 @@ {% endblock %} diff --git a/meshview/web_api/api.py b/meshview/web_api/api.py index 8f5fb5d..667df38 100644 --- a/meshview/web_api/api.py +++ b/meshview/web_api/api.py @@ -421,45 +421,63 @@ async def api_stats_count(request): async def api_edges(request): since = datetime.datetime.now() - datetime.timedelta(hours=48) filter_type = request.query.get("type") - edges = {} + traceroute_count = 0 + neighbor_packet_count = 0 + edges_added_tr = 0 + edges_added_neighbor = 0 - # Only build traceroute edges if requested + # --- Traceroute edges --- if filter_type in (None, "traceroute"): + async for tr in store.get_traceroutes(since): + traceroute_count += 1 + try: route = decode_payload.decode_payload(PortNum.TRACEROUTE_APP, tr.route) except Exception as e: - logger.error(f"Error decoding Traceroute {tr.id}: {e}") + print(f" ERROR decoding traceroute {tr.id}: {e}") continue + # Build full path path = [tr.packet.from_node_id] + list(route.route) path.append(tr.packet.to_node_id if tr.done else tr.gateway_node_id) for a, b in zip(path, path[1:], strict=False): - edges[(a, b)] = "traceroute" + if (a, b) not in edges: + edges[(a, b)] = "traceroute" + edges_added_tr += 1 - # Only build neighbor edges if requested + # --- Neighbor edges --- if filter_type in (None, "neighbor"): - packets = await store.get_packets(portnum=PortNum.NEIGHBORINFO_APP, after=since) + packets = await store.get_packets(portnum=71) + neighbor_packet_count = len(packets) + for packet in packets: + packet_id = getattr(packet, "id", "?") try: _, neighbor_info = decode_payload.decode(packet) - for node in neighbor_info.neighbors: - edges.setdefault((node.node_id, packet.from_node_id), "neighbor") except Exception as e: - logger.error( - f"Error decoding NeighborInfo packet {getattr(packet, 'id', '?')}: {e}" - ) + print(f" ERROR decoding NeighborInfo packet {packet_id}: {e}") + continue - # Convert edges dict to list format for JSON response + for node in neighbor_info.neighbors: + edge = (node.node_id, packet.from_node_id) + + if edge not in edges: + edges[edge] = "neighbor" + edges_added_neighbor += 1 + + # Convert to list edges_list = [ - {"from": frm, "to": to, "type": edge_type} for (frm, to), edge_type in edges.items() + {"from": frm, "to": to, "type": edge_type} + for (frm, to), edge_type in edges.items() ] return web.json_response({"edges": edges_list}) + @routes.get("/api/config") async def api_config(request): try:
Long NameShort NameChannelSent (24h)Seen (24h)Avg GatewaysLong NameShort NameChannelSent (24h)Seen (24h)Avg Gateways