Added more analytics by reporting on the number and kinds of packets each node sends in a 24 hour period.

This commit is contained in:
Pablo Revilla
2025-03-06 15:50:33 -08:00
parent d910fadf24
commit ab5b8a7012
8 changed files with 266 additions and 5 deletions

View File

@@ -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):
"""

View File

@@ -38,7 +38,7 @@
<br><div style="text-align:center"><strong>Bay Area Mesh - http://bayme.sh</strong></div>
<div style="text-align:center">Quick Links:&nbsp;&nbsp;<a href="/nodelist">Nodes</a>&nbsp;-&nbsp;<a href="/chat">Conversations</a>&nbsp;-&nbsp;<a href="/firehose">See <strong>everything</strong> </a>
&nbsp;-&nbsp;Mesh Graph <a href="/nodegraph/LongFast">LF</a>&nbsp;-&nbsp;<a href="/nodegraph/MediumSlow">MS </a>&nbsp;-&nbsp;<a href="/stats">Stats </a>
&nbsp;-&nbsp;<a href="/net">Weekly Net</a>&nbsp;-&nbsp;<a href="/map">Map</a></div><br>
&nbsp;-&nbsp;<a href="/net">Weekly Net</a>&nbsp;-&nbsp;<a href="/map">Map</a>&nbsp;-&nbsp;<a href="/top">Top Traffic</a></div><br>
<div id="spinner" class="spinner-border secondary-primary htmx-indicator position-absolute top-50 start-50" role="status">
<span class="visually-hidden">Loading...</span>
</div>

View File

@@ -53,6 +53,7 @@
<dt>Role</dt>
<dd>{{node.role}}</dd>
</dl>
<a href="/top?node_id={{node.node_id}}" >Get node traffic totals</a>
{% include "node_graphs.html" %}
</div>
{% else %}

View File

@@ -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 %}
<section>
<h2 class="table-title">{{ traffic[0].long_name }} (last 24 hours)</h2>
<table class="traffic-table">
<thead>
<tr>
<th>Port Number</th>
<th>Packet Count</th>
</tr>
</thead>
<tbody>
{% for port in traffic %}
<tr class="traffic">
<td>
{% 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 %}
</td>
<td>{{ port.packet_count }}</td>
</tr>
{% else %}
<tr>
<td colspan="2">No traffic data available for this node.</td>
</tr>
{% endfor %}
</tbody>
</table>
</section>
<footer class="footer">
<a href="/top">Back to Top Nodes</a>
</footer>
{% endblock %}

View File

@@ -13,7 +13,6 @@
{%- endif -%}
)
</span>
-&gt;
<span {% if to_me %} class="fw-bold" {% endif %}>
{{packet.to_node.long_name}}(
{%- if not to_me -%}
@@ -40,7 +39,7 @@
<dt>payload</dt>
<dd>
{% if packet.pretty_payload %}
<div>{{packet.pretty_payload}}<div>
<div>{{packet.pretty_payload}}</div>
{% endif %}
{% if packet.raw_mesh_packet.decoded and packet.raw_mesh_packet.decoded.portnum == 70 %}
<ul>

View File

@@ -1,4 +1,4 @@
<div class="col" id="packet_list" sse-swap="{{packet_event | default('packet')}}" hx-swap="afterbegin">
<div class="col" id="packet_list">
{% for packet in packets %}
{% include 'packet.html' %}
{% else %}

View File

@@ -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 %}
<h2 class="table-title">Top Traffic Nodes (last 24 hours)</h2>
<table class="traffic-table">
<thead>
<tr>
<th>Node Name</th>
<th>Role</th>
<th>Packet Count</th>
</tr>
</thead>
<tbody>
{% for node in nodes %}
<tr class="traffic">
<td><a href="/packet_list/{{ node[0] }}">{{ node[1] }}</a></td> <!-- long_name -->
<td>{{ node[2] }}</td>
<td><a href="/top?node_id={{ node[0] }}">{{ node[3] }}</a></td> <!-- packet_count -->
</tr>
{% endfor %}
</tbody>
</table>
{% endblock %}

View File

@@ -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):