From ab5b8a70122ecec4ce14ed50db0f27b74e5e361c Mon Sep 17 00:00:00 2001 From: Pablo Revilla Date: Thu, 6 Mar 2025 15:50:33 -0800 Subject: [PATCH] Added more analytics by reporting on the number and kinds of packets each node sends in a 24 hour period. --- meshview/store.py | 63 ++++++++++++++++ meshview/templates/base.html | 2 +- meshview/templates/node.html | 1 + meshview/templates/node_traffic.html | 105 +++++++++++++++++++++++++++ meshview/templates/packet.html | 3 +- meshview/templates/packet_list.html | 2 +- meshview/templates/top.html | 65 +++++++++++++++++ meshview/web.py | 30 +++++++- 8 files changed, 266 insertions(+), 5 deletions(-) create mode 100644 meshview/templates/node_traffic.html create mode 100644 meshview/templates/top.html diff --git a/meshview/store.py b/meshview/store.py index 87ef868..fd27e51 100644 --- a/meshview/store.py +++ b/meshview/store.py @@ -3,6 +3,7 @@ from sqlalchemy import select, func from sqlalchemy.orm import lazyload from meshview import database from meshview.models import Packet, PacketSeen, Node, Traceroute +from sqlalchemy import text async def get_node(node_id): async with database.async_session() as session: @@ -211,6 +212,68 @@ async def get_nodes_mediumslow(): return result.scalars() +async def get_top_traffic_nodes(): + async with database.async_session() as session: + result = await session.execute(text(""" + SELECT + n.node_id, + n.long_name, + n.role, + COUNT(p.id) AS packet_count + FROM + packet p + JOIN + node n + ON + p.from_node_id = n.node_id + WHERE + p.import_time >= DATETIME('now', '-1 day') + GROUP BY + n.long_name, n.role + ORDER BY + packet_count DESC + LIMIT 100; + """)) + + return result.fetchall() # Returns a list of tuples + + +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.future import select + + +async def get_node_traffic(node_id: int): + try: + async with database.async_session() as session: + result = await session.execute( + text(""" + SELECT + node.long_name, packet.portnum, + COUNT(*) AS packet_count + FROM packet + JOIN node ON packet.from_node_id = node.node_id OR packet.to_node_id = node.node_id + WHERE node.node_id = :node_id + AND packet.import_time >= DATETIME('now', '-1 day') + GROUP BY packet.portnum + ORDER BY packet_count DESC; + """), {"node_id": node_id} + ) + + # Map the result to include node.long_name and packet data + traffic_data = [{ + "long_name": row[0], # node.long_name + "portnum": row[1], # packet.portnum + "packet_count": row[2] # COUNT(*) as packet_count + } for row in result.all()] + + return traffic_data + + except Exception as e: + # Log the error or handle it as needed + print(f"Error fetching node traffic: {str(e)}") + return [] + + async def get_nodes(role=None, channel=None, hw_model=None): """ diff --git a/meshview/templates/base.html b/meshview/templates/base.html index 54a5ac1..436b396 100644 --- a/meshview/templates/base.html +++ b/meshview/templates/base.html @@ -38,7 +38,7 @@
Bay Area Mesh - http://bayme.sh
Quick Links:  Nodes - Conversations - See everything  - Mesh Graph LF - MS  - Stats -  - Weekly Net - Map

+  - Weekly Net - Map - Top Traffic
Loading...
diff --git a/meshview/templates/node.html b/meshview/templates/node.html index a348752..434bffe 100644 --- a/meshview/templates/node.html +++ b/meshview/templates/node.html @@ -53,6 +53,7 @@
Role
{{node.role}}
+ Get node traffic totals {% include "node_graphs.html" %} {% else %} diff --git a/meshview/templates/node_traffic.html b/meshview/templates/node_traffic.html new file mode 100644 index 0000000..a8aca5b --- /dev/null +++ b/meshview/templates/node_traffic.html @@ -0,0 +1,105 @@ +{% extends "base.html" %} + +{% block css %} +.table-title { + font-size: 2rem; + text-align: center; + margin-bottom: 20px; + } + + .traffic-table { + width: 50%; + border-collapse: collapse; + margin: 0 auto; + font-family: Arial, sans-serif; + } + + .traffic-table th, + .traffic-table td { + padding: 10px 15px; + text-align: left; + border: 1px solid #474b4e; + } + + .traffic-table th { + background-color: #272b2f; + color: white; + } + + .traffic:nth-of-type(odd) { + background-color: #272b2f; /* Lighter than #2a2a2a */ + } + + .traffic { + border: 1px solid #474b4e; + padding: 8px; + margin-bottom: 4px; + border-radius: 8px; + } + + .traffic:nth-of-type(even) { + background-color: #212529; /* Slightly lighter than the previous #181818 */ + } + + .footer { + text-align: center; + margin-top: 20px; + } + +{% endblock %} + +{% block body %} +
+

{{ traffic[0].long_name }} (last 24 hours)

+ + + + + + + + + {% for port in traffic %} + + + + + {% else %} + + + + {% endfor %} + +
Port NumberPacket Count
+ {% if port.portnum == 1 %} + TEXT_MESSAGE_APP + {% elif port.portnum == 3 %} + POSITION_APP + {% elif port.portnum == 4 %} + NODEINFO_APP + {% elif port.portnum == 5 %} + ROUTING_APP + {% elif port.portnum == 67 %} + TELEMETRY_APP + {% elif port.portnum == 70 %} + TRACEROUTE_APP + {% elif port.portnum == 71 %} + NEIGHBORINFO_APP + {% elif port.portnum == 73 %} + MAP_REPORT_APP + {% elif port.portnum == 0 %} + UNKNOWN_APP + {% elif port.portnum == 0 %} + UNKNOWN_APP + {% elif port.portnum == 0 %} + UNKNOWN_APP + {% else %} + {{ port.portnum }} + {% endif %} + {{ port.packet_count }}
No traffic data available for this node.
+
+ + +{% endblock %} diff --git a/meshview/templates/packet.html b/meshview/templates/packet.html index a6d7cc7..c832b97 100644 --- a/meshview/templates/packet.html +++ b/meshview/templates/packet.html @@ -13,7 +13,6 @@ {%- endif -%} ) - -> {{packet.to_node.long_name}}( {%- if not to_me -%} @@ -40,7 +39,7 @@
payload
{% if packet.pretty_payload %} -
{{packet.pretty_payload}}
+
{{packet.pretty_payload}}
{% endif %} {% if packet.raw_mesh_packet.decoded and packet.raw_mesh_packet.decoded.portnum == 70 %}
    diff --git a/meshview/templates/packet_list.html b/meshview/templates/packet_list.html index f90c58f..24f2f8c 100644 --- a/meshview/templates/packet_list.html +++ b/meshview/templates/packet_list.html @@ -1,4 +1,4 @@ -
    +
    {% for packet in packets %} {% include 'packet.html' %} {% else %} diff --git a/meshview/templates/top.html b/meshview/templates/top.html new file mode 100644 index 0000000..b875f77 --- /dev/null +++ b/meshview/templates/top.html @@ -0,0 +1,65 @@ +{% extends "base.html" %} +{% block css %} +.table-title { + font-size: 2rem; + text-align: center; + margin-bottom: 20px; + } + + .traffic-table { + width: 60%; + border-collapse: collapse; + margin: 0 auto; + font-family: Arial, sans-serif; + } + + .traffic-table th, + .traffic-table td { + padding: 10px 15px; + text-align: left; + border: 1px solid #474b4e; + } + + .traffic-table th { + background-color: #272b2f; + color: white; + } + + .traffic:nth-of-type(odd) { + background-color: #272b2f; /* Lighter than #2a2a2a */ + } + + .traffic { + border: 1px solid #474b4e; + padding: 8px; + margin-bottom: 4px; + border-radius: 8px; + } + + .traffic:nth-of-type(even) { + background-color: #212529; /* Slightly lighter than the previous #181818 */ + } +{% endblock %} + +{% block body %} +

    Top Traffic Nodes (last 24 hours)

    + + + + + + + + + + {% for node in nodes %} + + + + + + {% endfor %} + +
    Node NameRolePacket Count
    {{ node[1] }} {{ node[2] }}{{ node[3] }}
    + +{% endblock %} diff --git a/meshview/web.py b/meshview/web.py index 3a62550..39b5fbc 100644 --- a/meshview/web.py +++ b/meshview/web.py @@ -23,7 +23,6 @@ from meshview import decode_payload import gc import psutil - env = Environment(loader=PackageLoader("meshview"), autoescape=select_autoescape()) # Optimize garbage collection frequency @@ -1190,6 +1189,35 @@ async def stats(request): content_type="text/plain", ) +@routes.get("/top") +async def top(request): + try: + node_id = request.query.get("node_id") # Get node_id from the URL query parameters + + if node_id: + # If node_id is provided, fetch traffic data for the specific node + node_traffic = await store.get_node_traffic(int(node_id)) + print(node_traffic) + template = env.get_template("node_traffic.html") # Render a different template + html_content = template.render(traffic=node_traffic, node_id=node_id) + else: + # Otherwise, fetch top traffic nodes as usual + top_nodes = await store.get_top_traffic_nodes() + template = env.get_template("top.html") + html_content = template.render(nodes=top_nodes) + + return web.Response( + text=html_content, + content_type="text/html", + ) + except Exception as e: + return web.Response( + text=f"An error occurred: {str(e)}", + status=500, + content_type="text/plain", + ) + + @routes.get("/chat") async def chat(request):