mirror of
https://github.com/pablorevilla-meshtastic/meshview.git
synced 2026-03-04 23:27:46 +01:00
Finishing up all the pages for the 3.0 release.
Now all pages are functional.
This commit is contained in:
@@ -1,16 +0,0 @@
|
||||
<div id="buttons" class="btn-group" role="group">
|
||||
<a
|
||||
role="button"
|
||||
class="btn {{ 'btn-primary' if packet_event == 'packet' else 'btn-secondary'}}"
|
||||
href="/packet_list/{{node_id}}?{{query_string}}"
|
||||
>
|
||||
TX/RX
|
||||
</a>
|
||||
<a
|
||||
role="button"
|
||||
class="btn {{ 'btn-primary' if packet_event == 'uplinked' else 'btn-secondary'}}"
|
||||
href="/uplinked_list/{{node_id}}?{{query_string}}"
|
||||
>
|
||||
Uplinked
|
||||
</a>
|
||||
</div>
|
||||
@@ -1,7 +0,0 @@
|
||||
<datalist
|
||||
id="node_options"
|
||||
>
|
||||
{% for option in node_options %}
|
||||
<option value="{{option.id}}">{{option.id}} -- {{option.long_name}} ({{option.short_name}})</option>
|
||||
{% endfor %}
|
||||
</datalist>
|
||||
@@ -48,10 +48,10 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* --- Source Marker (bigger, no animation) --- */
|
||||
/* --- Source Marker: 2px bigger than gateway (26px vs 24px) --- */
|
||||
.source-marker {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
background: rgba(255,0,0,0.55);
|
||||
border: 3px solid #ff0000;
|
||||
border-radius: 50%;
|
||||
@@ -268,17 +268,17 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
|
||||
const allBounds = [];
|
||||
|
||||
/* -------------------------------------------------------
|
||||
PACKET SOURCE MARKER: bigger red circle, no animation
|
||||
-------------------------------------------------------- */
|
||||
/* --------------------------------------------------------------------
|
||||
SOURCE MARKER (26px vs 24px)
|
||||
--------------------------------------------------------------------- */
|
||||
if (lat && lon) {
|
||||
allBounds.push([lat, lon]);
|
||||
|
||||
const sourceIcon = L.divIcon({
|
||||
html: `<div class="source-marker"></div>`,
|
||||
className: "",
|
||||
iconSize: [36, 36],
|
||||
iconAnchor: [18, 18]
|
||||
iconSize: [26, 26],
|
||||
iconAnchor: [13, 13]
|
||||
});
|
||||
|
||||
const sourceMarker = L.marker([lat, lon], {
|
||||
@@ -303,14 +303,25 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
/* -------------------------
|
||||
Helpers
|
||||
-------------------------- */
|
||||
function hopColor(hop){
|
||||
const c=[
|
||||
"#ff3b30","#ff6b22","#ff9f0c",
|
||||
"#ffd60a","#87d957","#57d9c4","#3db2ff"
|
||||
|
||||
/* 0 = warmest → 7 = coldest */
|
||||
function hopColor(hopValue){
|
||||
const colors = [
|
||||
"#ff3b30", // 0 red
|
||||
"#ff6b22",
|
||||
"#ff9f0c",
|
||||
"#ffd60a",
|
||||
"#87d957",
|
||||
"#57d9c4",
|
||||
"#3db2ff",
|
||||
"#1e63ff" // 7 deep cold blue
|
||||
];
|
||||
if(!hop||hop<1)return"#aaa";
|
||||
if(hop>7)hop=7;
|
||||
return c[hop-1];
|
||||
|
||||
let h = Number(hopValue);
|
||||
if (isNaN(h)) return "#aaa";
|
||||
if (h < 0) h = 0;
|
||||
if (h > 7) h = 7;
|
||||
return colors[h];
|
||||
}
|
||||
|
||||
function haversine(lat1,lon1,lat2,lon2){
|
||||
@@ -360,9 +371,9 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
|
||||
const start = Number(s.hop_start ?? 0);
|
||||
const limit = Number(s.hop_limit ?? 0);
|
||||
const hopValue = limit - start;
|
||||
const hopValue = start - limit;
|
||||
|
||||
const color = hopColor(Number(s.hop_limit));
|
||||
const color = hopColor(hopValue);
|
||||
|
||||
const iconHtml = `
|
||||
<div style="
|
||||
@@ -404,8 +415,7 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
<b>Signal</b><br>
|
||||
RSSI: ${s.rx_rssi ?? "—"}<br>
|
||||
SNR: ${s.rx_snr ?? "—"}<br><br>
|
||||
<b>Hop Distance</b><br>
|
||||
hop_limit (${limit}) − hop_start (${start}) = <b>${hopValue}</b><br><br>
|
||||
<b>Hops</b>: ${hopValue}<br>
|
||||
<b>Distance</b><br>
|
||||
${
|
||||
distKm
|
||||
|
||||
@@ -1,234 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block css %}
|
||||
/* Styles for the node info card */
|
||||
#node_info {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Styles for the map */
|
||||
#map {
|
||||
height: 100%;
|
||||
min-height: 400px;
|
||||
}
|
||||
|
||||
/* Styles for packet details section */
|
||||
#packet_details {
|
||||
height: 95vh;
|
||||
overflow: scroll;
|
||||
top: 3em;
|
||||
}
|
||||
|
||||
/* Ensure inline display for details */
|
||||
div.tab-pane > dl {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* Set the maximum width of the page to 900px */
|
||||
.container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto; /* Center the content horizontally */
|
||||
}
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div id="node" class="container text-center">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col mb-3">
|
||||
<!-- Node Information Card -->
|
||||
<div class="card" id="node_info">
|
||||
{% if node %}
|
||||
<div class="card-header" id="node_color">
|
||||
<strong style="margin-right: 1em ; margin-left: 1em; font-size: x-large;">{{node.short_name}}</strong>
|
||||
<p style="margin-bottom: 0px; font-size: large; font-weight: bold;">{{node.long_name}}</p>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<dl >
|
||||
{% if trace %}
|
||||
<dd id="map"></dd>
|
||||
{% endif %}
|
||||
<dt>NodeID</dt>
|
||||
<dd>{{node.node_id|node_id_to_hex}}</dd>
|
||||
<dt>Channel</dt>
|
||||
<dd>{{node.channel}}</dd>
|
||||
<dt>HW Model</dt>
|
||||
<dd>{{node.hw_model}}</dd>
|
||||
<dt>Role</dt>
|
||||
<dd>{{node.role}}</dd>
|
||||
{% if node.firmware %}
|
||||
<dt>Firmware</dt>
|
||||
<dd>{{node.firmware}}</dd>
|
||||
{% endif %}
|
||||
</dl>
|
||||
<a href="/top?node_id={{node.node_id}}" >Get node traffic totals</a>
|
||||
{% include "node_graphs.html" %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="card-body">
|
||||
A NodeInfo has not been seen.
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<!-- Additional buttons can be included here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{% include 'packet_list.html' %}
|
||||
</div>
|
||||
<!-- <div class="col sticky-top" id="packet_details"></div> -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var node_color = document.getElementById('node_color');
|
||||
var node_id = '{{node.node_id | node_id_to_hex}}';
|
||||
var color = node_id.slice(-6);
|
||||
var bg_color = "#"+color;
|
||||
node_color.style.background = bg_color;
|
||||
var hex = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(bg_color);
|
||||
var text_color = [
|
||||
parseInt(hex[1], 16),
|
||||
parseInt(hex[2], 16),
|
||||
parseInt(hex[3], 16),
|
||||
];
|
||||
|
||||
const brightness = Math.round(((parseInt(text_color[0]) * 299) +
|
||||
(parseInt(text_color[1]) * 587) +
|
||||
(parseInt(text_color[2]) * 114)) / 1000);
|
||||
|
||||
if (brightness > 125) {
|
||||
var textColor = '#212529'
|
||||
node_color.style.color = textColor
|
||||
}
|
||||
</script>
|
||||
|
||||
{% if trace %}
|
||||
<script>
|
||||
var trace = {{ trace | tojson }}; // Load trace data into JavaScript
|
||||
var map = L.map('map').setView(trace[0], 13); // Initialize map centered at first trace point
|
||||
var markers = L.featureGroup(); // Create a feature group for markers
|
||||
markers.addTo(map);
|
||||
|
||||
// Add tile layer (OpenStreetMap)
|
||||
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 19,
|
||||
attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
||||
}).addTo(map);
|
||||
|
||||
// Draw a polyline along the trace path
|
||||
L.polyline(trace, { color: 'blue', weight: 1}).addTo(map);
|
||||
|
||||
// Add a red circle marker for the starting node with a tooltip
|
||||
var startMarker = L.circleMarker(trace[0], {
|
||||
radius: 8,
|
||||
color: 'red',
|
||||
weight: 1,
|
||||
fillColor: 'red',
|
||||
fillOpacity: 0.5
|
||||
}).addTo(markers) // Add to feature group
|
||||
.bindPopup(`
|
||||
<b>{{node.long_name}}</b><br/>
|
||||
<b>Short:</b> {{node.short_name}}<br/>
|
||||
<b>Channel:</b> {{node.channel}}<br/>
|
||||
<b>Hardware:</b> {{node.hw_model}}<br/>
|
||||
<b>Role:</b> {{node.role}}<br/>
|
||||
<b>Firmware:</b> {{node.firmware}}<br/>
|
||||
<b>Coordinates:</b> [{{node.last_lat}} , {{node.last_long}}]
|
||||
`, {permanent: false, direction: 'top', opacity: 0.9});
|
||||
|
||||
// Function to calculate distance and convert to miles
|
||||
function getDistanceInMiles(latlng1, latlng2) {
|
||||
var meters = latlng1.distanceTo(latlng2); // Get distance in meters
|
||||
return meters * 0.000621371; // Convert meters to miles
|
||||
}
|
||||
|
||||
{% for n in neighbors %}
|
||||
var neighborLatLng = L.latLng([{{n.location[0]}}, {{n.location[1]}}]);
|
||||
var startLatLng = L.latLng(trace[0]);
|
||||
|
||||
// Calculate distance in miles with 1 decimal place
|
||||
var distanceMiles = getDistanceInMiles(startLatLng, neighborLatLng).toFixed(1);
|
||||
|
||||
// Create a blue circle marker for each neighbor node
|
||||
var m = L.circleMarker(neighborLatLng, {
|
||||
radius: 6,
|
||||
color: 'blue',
|
||||
weight: 1,
|
||||
fillColor: 'blue',
|
||||
fillOpacity: 0.5
|
||||
}).addTo(markers) // Add to feature group
|
||||
.bindPopup(`
|
||||
<b><a href="/packet_list/{{n.node_id}}">{{n.long_name}}</a></b><br>
|
||||
<b>{{n.short_name}}</b> <br/>
|
||||
<b>SNR:</b> {{n.snr}} <br/>
|
||||
<b>Distance:</b> ${distanceMiles} miles <br/>
|
||||
`, {permanent: false, direction: 'top', opacity: 0.9});
|
||||
|
||||
// Draw a polyline from the first trace point to each neighbor node
|
||||
L.polyline([startLatLng, neighborLatLng], {
|
||||
color: 'grey',
|
||||
weight: 1
|
||||
}).addTo(map);
|
||||
|
||||
{% endfor %}
|
||||
|
||||
// Add a legend to the map
|
||||
var legend = L.control({ position: 'bottomleft' });
|
||||
|
||||
legend.onAdd = function(map) {
|
||||
var div = L.DomUtil.create('div', 'info legend');
|
||||
div.style.background = 'white';
|
||||
div.style.padding = '8px';
|
||||
div.style.border = '1px solid black';
|
||||
div.style.borderRadius = '5px';
|
||||
div.style.boxShadow = '0 0 5px rgba(0,0,0,0.3)';
|
||||
div.style.color = 'black'; // Ensure text is black
|
||||
div.style.textAlign = 'left'; /* Ensure left alignment */
|
||||
div.innerHTML = `
|
||||
<b>Legend</b><br>
|
||||
<svg width="16" height="16">
|
||||
<circle cx="8" cy="8" r="6" fill="blue" stroke="blue" stroke-width="1" fill-opacity="0.4"/>
|
||||
</svg> Neighbor Node<br>
|
||||
<svg width="20" height="20">
|
||||
<circle cx="10" cy="10" r="8" fill="red" stroke="red" stroke-width="1" fill-opacity="0.4"/>
|
||||
</svg> Home Node<br>
|
||||
<svg width="20" height="4">
|
||||
<line x1="0" y1="2" x2="20" y2="2" stroke="grey" stroke-width="2"/>
|
||||
</svg> Connection to Neighbors<br>
|
||||
<svg width="20" height="4">
|
||||
<line x1="0" y1="2" x2="20" y2="2" stroke="blue" stroke-width="2"/>
|
||||
</svg> Path taken by node
|
||||
`;
|
||||
return div;
|
||||
};
|
||||
|
||||
legend.addTo(map);
|
||||
|
||||
// Ensure the map adjusts to fit all markers and trace points
|
||||
setTimeout(() => {
|
||||
if (markers.getLayers().length > 0 || trace.length > 0) {
|
||||
var bounds = markers.getBounds(); // Get bounds from markers
|
||||
|
||||
// Ensure trace points are included in the bounds
|
||||
trace.forEach(point => {
|
||||
bounds.extend(point);
|
||||
});
|
||||
|
||||
map.fitBounds(bounds.pad(0.1), { maxZoom: 10 });
|
||||
}
|
||||
}, 200); // Slightly longer delay to ensure all elements are fully loaded
|
||||
|
||||
</script>
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
@@ -1,109 +0,0 @@
|
||||
{% 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">
|
||||
{% if traffic %}
|
||||
{{ traffic[0].long_name }} (last 24 hours)
|
||||
{% else %}
|
||||
No Traffic Data Available
|
||||
{% endif %}
|
||||
</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 == 8 %}
|
||||
WAYPOINT_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
|
||||
{% 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 %}
|
||||
@@ -1,132 +0,0 @@
|
||||
<div id="details_map"></div>
|
||||
|
||||
{% for seen in packets_seen %}
|
||||
<div class="card mt-2">
|
||||
<div class="card-header">
|
||||
{{seen.node.long_name}}(
|
||||
<a hx-target="#node" href="/node_search?q={{seen.node_id|node_id_to_hex}}">
|
||||
{{seen.node_id|node_id_to_hex}}
|
||||
</a>
|
||||
)
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="card-text text-start">
|
||||
<dl>
|
||||
<dt>Import Time</dt>
|
||||
<dd>{{seen.import_time.strftime('%-I:%M:%S %p - %m-%d-%Y')}}</dd>
|
||||
<dt>rx_time</dt>
|
||||
<dd>{{seen.rx_time|format_timestamp}}</dd>
|
||||
<dt>hop_limit</dt>
|
||||
<dd>{{seen.hop_limit}}</dd>
|
||||
<dt>hop_start</dt>
|
||||
<dd>{{seen.hop_start}}</dd>
|
||||
<dt>channel</dt>
|
||||
<dd>{{seen.channel}}</dd>
|
||||
<dt>rx_snr</dt>
|
||||
<dd>{{seen.rx_snr}}</dd>
|
||||
<dt>rx_rssi</dt>
|
||||
<dd>{{seen.rx_rssi}}</dd>
|
||||
<dt>topic</dt>
|
||||
<dd>{{seen.topic}}</dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
{% if map_center %}
|
||||
<script>
|
||||
var details_map = L.map('details_map').setView({{ map_center | tojson }}, 8);
|
||||
var markers = L.featureGroup();
|
||||
markers.addTo(details_map);
|
||||
|
||||
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
maxZoom: 15,
|
||||
attribution: '© <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
|
||||
}).addTo(details_map);
|
||||
|
||||
function getDistanceInMiles(latlng1, latlng2) {
|
||||
var meters = latlng1.distanceTo(latlng2);
|
||||
return meters * 0.000621371;
|
||||
}
|
||||
|
||||
{% if from_node_cord %}
|
||||
var fromNodeLatLng = L.latLng({{ from_node_cord | tojson }});
|
||||
var fromNode = L.circleMarker(fromNodeLatLng, {
|
||||
radius: 10,
|
||||
color: 'red',
|
||||
weight: 1,
|
||||
fillColor: 'red',
|
||||
fillOpacity: .4
|
||||
}).addTo(markers);
|
||||
|
||||
fromNode.bindPopup(`
|
||||
Sent by: <b>{{node.long_name}}</b><br/>
|
||||
<b>Short:</b> {{node.short_name}}<br/>
|
||||
<b>Channel:</b> {{node.channel}}<br/>
|
||||
<b>Hardware:</b> {{node.hw_model}}<br/>
|
||||
<b>Role:</b> {{node.role}}<br/>
|
||||
<b>Firmware:</b> {{node.firmware}}<br/>
|
||||
<b>Coordinates:</b> [{{node.last_lat}}, {{node.last_long}}]
|
||||
`, { permanent: false, direction: 'top', opacity: 0.9 });
|
||||
{% endif %}
|
||||
|
||||
{% for u in uplinked_nodes %}
|
||||
var uplinkNodeLatLng = L.latLng([{{ u.lat }}, {{ u.long }}]);
|
||||
|
||||
{% if from_node_cord %}
|
||||
var distanceMiles = getDistanceInMiles(fromNodeLatLng, uplinkNodeLatLng).toFixed(1);
|
||||
{% endif %}
|
||||
|
||||
var node = L.marker(uplinkNodeLatLng, {
|
||||
icon: L.divIcon({
|
||||
className: 'text-icon',
|
||||
html: `<div style="font-size: 12px; color: white; font-weight: bold; display: flex; justify-content: center; align-items: center; height: 16px; width: 16px; border-radius: 50%; background-color: blue; border: 1px solid blue;">{{u.hops}}</div>`,
|
||||
iconSize: [16, 16],
|
||||
iconAnchor: [8, 8]
|
||||
})
|
||||
}).addTo(markers);
|
||||
node.setZIndexOffset({{u.hops}}*-1);
|
||||
|
||||
node.bindPopup(`
|
||||
Heard by: <b>{{u.long_name}}</b><br>
|
||||
<b>{{ u.short_name }}</b><br/>
|
||||
<b>Hops:</b> {{ u.hops }}<br/>
|
||||
<b>SNR:</b> {{ u.snr }}<br/>
|
||||
<b>RSSI:</b> {{ u.rssi }}<br/>
|
||||
{% if from_node_cord %}
|
||||
<b>Distance:</b> ${distanceMiles} miles <br/>
|
||||
{% endif %}
|
||||
<b>Coordinates:</b> [{{u.lat}}, {{u.long}}]
|
||||
`, { permanent: false, direction: 'top', opacity: 0.9 });
|
||||
{% endfor %}
|
||||
|
||||
if (markers.getLayers().length > 0) {
|
||||
details_map.fitBounds(markers.getBounds().pad(0.1), { animate: true });
|
||||
}
|
||||
|
||||
var legend = L.control({ position: 'bottomleft' });
|
||||
legend.onAdd = function(map) {
|
||||
var div = L.DomUtil.create('div', 'info legend');
|
||||
div.style.background = 'white';
|
||||
div.style.padding = '8px';
|
||||
div.style.border = '1px solid black';
|
||||
div.style.borderRadius = '5px';
|
||||
div.style.boxShadow = '0 0 5px rgba(0,0,0,0.3)';
|
||||
div.style.color = 'black';
|
||||
div.style.textAlign = 'left';
|
||||
div.innerHTML = `
|
||||
<b>Legend</b><br>
|
||||
<svg width="20" height="20">
|
||||
<circle cx="8" cy="8" r="6" fill="blue" stroke="blue" stroke-width="1" fill-opacity="0.9"/>
|
||||
</svg> Receiving Node (Number is hop count)<br>
|
||||
<svg width="20" height="20">
|
||||
<circle cx="10" cy="10" r="8" fill="red" stroke="red" stroke-width="1" fill-opacity="0.4"/>
|
||||
</svg> Sending Node<br>
|
||||
`;
|
||||
return div;
|
||||
};
|
||||
legend.addTo(details_map);
|
||||
</script>
|
||||
|
||||
{% endif %}
|
||||
@@ -1,24 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% block css %}
|
||||
|
||||
/* Set the maximum width of the page to 900px */
|
||||
.container {
|
||||
max-width: 900px;
|
||||
margin: 0 auto; /* Center the content horizontally */
|
||||
}
|
||||
{% endblock %}
|
||||
{% block body %}
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div>
|
||||
{% include 'packet.html' %}
|
||||
</div>
|
||||
<div
|
||||
id="packet_details"
|
||||
hx-get="/packet_details/{{packet.id}}"
|
||||
hx-trigger="load"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -1,7 +0,0 @@
|
||||
<div class="col" id="packet_list">
|
||||
{% for packet in packets %}
|
||||
{% include 'packet.html' %}
|
||||
{% else %}
|
||||
No packets found.
|
||||
{% endfor %}
|
||||
</div>
|
||||
@@ -1,21 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
|
||||
{% block body %}
|
||||
|
||||
{% include "search_form.html" %}
|
||||
|
||||
<ul>
|
||||
{% for node in nodes %}
|
||||
<li>
|
||||
<a href="/packet_list/{{node.node_id}}?{{query_string}}">
|
||||
{{node.node_id | node_id_to_hex}}
|
||||
{% if node.long_name %}
|
||||
{{node.short_name}} — {{node.long_name}}
|
||||
{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
{% endblock %}
|
||||
@@ -1,44 +0,0 @@
|
||||
<form
|
||||
class="container p-2 sticky-top mx-auto"
|
||||
id="search_form"
|
||||
action="/node_search"
|
||||
>
|
||||
<div class="row">
|
||||
<input
|
||||
class="col m-2"
|
||||
id="q"
|
||||
type="text"
|
||||
name="q"
|
||||
placeholder="Node id"
|
||||
autocomplete="off"
|
||||
list="node_options"
|
||||
value="{{raw_node_id}}"
|
||||
hx-trigger="input delay:100ms"
|
||||
hx-get="/node_match"
|
||||
hx-target="#node_options"
|
||||
/>
|
||||
{% include "datalist.html" %}
|
||||
{% set options = {
|
||||
1: "Text Message",
|
||||
3: "Position",
|
||||
4: "Node Info",
|
||||
67: "Telemetry",
|
||||
70: "Traceroute",
|
||||
71: "Neighbor Info",
|
||||
}
|
||||
%}
|
||||
<select name="portnum" class="col-2 m-2">
|
||||
<option
|
||||
value = ""
|
||||
{% if portnum not in options %}selected{% endif %}
|
||||
>All</option>
|
||||
{% for value, name in options.items() %}
|
||||
<option
|
||||
value="{{value}}"
|
||||
{% if value == portnum %}selected{% endif %}
|
||||
>{{ name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<input type="submit" value="Go to Node" class="col-2 m-2" />
|
||||
</div>
|
||||
</form>
|
||||
@@ -93,26 +93,31 @@
|
||||
|
||||
{% block body %}
|
||||
<div class="main-container">
|
||||
<h2 class="main-header" data-translate-lang="mesh_stats_summary">Mesh Statistics - Summary (all available in Database)</h2>
|
||||
<h2 class="main-header" data-translate-lang="mesh_stats_summary">
|
||||
Mesh Statistics - Summary (all available in Database)
|
||||
</h2>
|
||||
|
||||
<!-- Summary cards now fully driven by API + JS -->
|
||||
<div class="summary-container" style="display:flex; justify-content:space-between; gap:10px; margin-bottom:20px;">
|
||||
<div class="summary-card" style="flex:1;">
|
||||
<p data-translate-lang="total_nodes">Total Nodes</p>
|
||||
<div class="summary-count">{{ "{:,}".format(total_nodes) }}</div>
|
||||
<div class="summary-count" id="summary_nodes">0</div>
|
||||
</div>
|
||||
<div class="summary-card" style="flex:1;">
|
||||
<p data-translate-lang="total_packets">Total Packets</p>
|
||||
<div class="summary-count">{{ "{:,}".format(total_packets) }}</div>
|
||||
<div class="summary-count" id="summary_packets">0</div>
|
||||
</div>
|
||||
<div class="summary-card" style="flex:1;">
|
||||
<p data-translate-lang="total_packets_seen">Total Packets Seen</p>
|
||||
<div class="summary-count">{{ "{:,}".format(total_packets_seen) }}</div>
|
||||
<div class="summary-count" id="summary_seen">0</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Daily Charts -->
|
||||
<div class="card-section">
|
||||
<p class="section-header" data-translate-lang="packets_per_day_all">Packets per Day - All Ports (Last 14 Days)</p>
|
||||
<p class="section-header" data-translate-lang="packets_per_day_all">
|
||||
Packets per Day - All Ports (Last 14 Days)
|
||||
</p>
|
||||
<div id="total_daily_all" class="total-count">Total: 0</div>
|
||||
<button class="expand-btn" data-chart="chart_daily_all" data-translate-lang="expand_chart">Expand Chart</button>
|
||||
<button class="export-btn" data-chart="chart_daily_all" data-translate-lang="export_csv">Export CSV</button>
|
||||
@@ -121,7 +126,9 @@
|
||||
|
||||
<!-- Packet Types Pie Chart with Channel Selector -->
|
||||
<div class="card-section">
|
||||
<p class="section-header" data-translate-lang="packet_types_last_24h">Packet Types - Last 24 Hours</p>
|
||||
<p class="section-header" data-translate-lang="packet_types_last_24h">
|
||||
Packet Types - Last 24 Hours
|
||||
</p>
|
||||
<select id="channelSelect">
|
||||
<option value="" data-translate-lang="all_channels">All Channels</option>
|
||||
</select>
|
||||
@@ -131,7 +138,9 @@
|
||||
</div>
|
||||
|
||||
<div class="card-section">
|
||||
<p class="section-header" data-translate-lang="packets_per_day_text">Packets per Day - Text Messages (Port 1, Last 14 Days)</p>
|
||||
<p class="section-header" data-translate-lang="packets_per_day_text">
|
||||
Packets per Day - Text Messages (Port 1, Last 14 Days)
|
||||
</p>
|
||||
<div id="total_daily_portnum_1" class="total-count">Total: 0</div>
|
||||
<button class="expand-btn" data-chart="chart_daily_portnum_1" data-translate-lang="expand_chart">Expand Chart</button>
|
||||
<button class="export-btn" data-chart="chart_daily_portnum_1" data-translate-lang="export_csv">Export CSV</button>
|
||||
@@ -140,7 +149,9 @@
|
||||
|
||||
<!-- Hourly Charts -->
|
||||
<div class="card-section">
|
||||
<p class="section-header" data-translate-lang="packets_per_hour_all">Packets per Hour - All Ports</p>
|
||||
<p class="section-header" data-translate-lang="packets_per_hour_all">
|
||||
Packets per Hour - All Ports
|
||||
</p>
|
||||
<div id="total_hourly_all" class="total-count">Total: 0</div>
|
||||
<button class="expand-btn" data-chart="chart_hourly_all" data-translate-lang="expand_chart">Expand Chart</button>
|
||||
<button class="export-btn" data-chart="chart_hourly_all" data-translate-lang="export_csv">Export CSV</button>
|
||||
@@ -148,7 +159,9 @@
|
||||
</div>
|
||||
|
||||
<div class="card-section">
|
||||
<p class="section-header" data-translate-lang="packets_per_hour_text">Packets per Hour - Text Messages (Port 1)</p>
|
||||
<p class="section-header" data-translate-lang="packets_per_hour_text">
|
||||
Packets per Hour - Text Messages (Port 1)
|
||||
</p>
|
||||
<div id="total_portnum_1" class="total-count">Total: 0</div>
|
||||
<button class="expand-btn" data-chart="chart_portnum_1" data-translate-lang="expand_chart">Expand Chart</button>
|
||||
<button class="export-btn" data-chart="chart_portnum_1" data-translate-lang="export_csv">Export CSV</button>
|
||||
@@ -214,17 +227,123 @@ async function fetchStats(period_type,length,portnum=null,channel=null){
|
||||
}catch{return [];}
|
||||
}
|
||||
|
||||
async function fetchNodes(){ try{ const res=await fetch("/api/nodes"); const json=await res.json(); return json.nodes||[];}catch{return [];} }
|
||||
async function fetchChannels(){ try{ const res = await fetch("/api/channels"); const json = await res.json(); return json.channels || [];}catch{return [];} }
|
||||
async function fetchNodes(){
|
||||
try{
|
||||
const res=await fetch("/api/nodes");
|
||||
const json=await res.json();
|
||||
return json.nodes||[];
|
||||
}catch{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function processCountField(nodes,field){ const counts={}; nodes.forEach(n=>{ const key=n[field]||"Unknown"; counts[key]=(counts[key]||0)+1; }); return Object.entries(counts).map(([name,value])=>({name,value})); }
|
||||
function updateTotalCount(domId,data){ const el=document.getElementById(domId); if(!el||!data.length) return; const total=data.reduce((acc,d)=>acc+(d.count??d.packet_count??0),0); el.textContent=`Total: ${total.toLocaleString()}`; }
|
||||
function prepareTopN(data,n=20){ data.sort((a,b)=>b.value-a.value); let top=data.slice(0,n); if(data.length>n){ const otherValue=data.slice(n).reduce((sum,item)=>sum+item.value,0); top.push({name:"Other", value:otherValue}); } return top; }
|
||||
async function fetchChannels(){
|
||||
try{
|
||||
const res = await fetch("/api/channels");
|
||||
const json = await res.json();
|
||||
return json.channels || [];
|
||||
}catch{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function processCountField(nodes,field){
|
||||
const counts={};
|
||||
nodes.forEach(n=>{
|
||||
const key=n[field]||"Unknown";
|
||||
counts[key]=(counts[key]||0)+1;
|
||||
});
|
||||
return Object.entries(counts).map(([name,value])=>({name,value}));
|
||||
}
|
||||
|
||||
function updateTotalCount(domId,data){
|
||||
const el=document.getElementById(domId);
|
||||
if(!el||!data.length) return;
|
||||
const total=data.reduce((acc,d)=>acc+(d.count??d.packet_count??0),0);
|
||||
el.textContent=`Total: ${total.toLocaleString()}`;
|
||||
}
|
||||
|
||||
function prepareTopN(data,n=20){
|
||||
data.sort((a,b)=>b.value-a.value);
|
||||
let top=data.slice(0,n);
|
||||
if(data.length>n){
|
||||
const otherValue=data.slice(n).reduce((sum,item)=>sum+item.value,0);
|
||||
top.push({name:"Other", value:otherValue});
|
||||
}
|
||||
return top;
|
||||
}
|
||||
|
||||
// --- Chart Rendering ---
|
||||
function renderChart(domId,data,type,color){ const el=document.getElementById(domId); if(!el) return; const chart=echarts.init(el); const periods=data.map(d=>(d.period??d.period===0)?d.period.toString():''); const counts=data.map(d=>d.count??d.packet_count??0); chart.setOption({backgroundColor:'#272b2f', tooltip:{trigger:'axis'}, grid:{left:'6%', right:'6%', bottom:'18%'}, xAxis:{type:'category', data:periods, axisLine:{lineStyle:{color:'#aaa'}}, axisLabel:{rotate:45,color:'#ccc'}}, yAxis:{type:'value', axisLine:{lineStyle:{color:'#aaa'}}, axisLabel:{color:'#ccc'}}, series:[{data:counts,type:type,smooth:type==='line',itemStyle:{color:color}, areaStyle:type==='line'?{}:undefined}]}); return chart; }
|
||||
function renderChart(domId,data,type,color){
|
||||
const el=document.getElementById(domId);
|
||||
if(!el) return;
|
||||
const chart=echarts.init(el);
|
||||
const periods=data.map(d=>(d.period??d.period===0)?d.period.toString():'');
|
||||
const counts=data.map(d=>d.count??d.packet_count??0);
|
||||
chart.setOption({
|
||||
backgroundColor:'#272b2f',
|
||||
tooltip:{trigger:'axis'},
|
||||
grid:{left:'6%', right:'6%', bottom:'18%'},
|
||||
xAxis:{
|
||||
type:'category',
|
||||
data:periods,
|
||||
axisLine:{lineStyle:{color:'#aaa'}},
|
||||
axisLabel:{rotate:45,color:'#ccc'}
|
||||
},
|
||||
yAxis:{
|
||||
type:'value',
|
||||
axisLine:{lineStyle:{color:'#aaa'}},
|
||||
axisLabel:{color:'#ccc'}
|
||||
},
|
||||
series:[{
|
||||
data:counts,
|
||||
type:type,
|
||||
smooth:type==='line',
|
||||
itemStyle:{color:color},
|
||||
areaStyle:type==='line'?{}:undefined
|
||||
}]
|
||||
});
|
||||
return chart;
|
||||
}
|
||||
|
||||
function renderPieChart(elId,data,name){ const el=document.getElementById(elId); if(!el) return; const chart=echarts.init(el); const top20=prepareTopN(data,20); chart.setOption({backgroundColor:"#272b2f", tooltip:{trigger:"item", formatter: params=>`${params.name}: ${Math.round(params.percent)}% (${params.value})`}, series:[{name:name, type:"pie", radius:["30%","70%"], center:["50%","50%"], avoidLabelOverlap:true, itemStyle:{borderRadius:6,borderColor:"#272b2f",borderWidth:2}, label:{show:true,formatter:"{b}\n{d}%", color:"#ccc", fontSize:10}, labelLine:{show:true,length:10,length2:6}, data:top20}]}); return chart; }
|
||||
function renderPieChart(elId,data,name){
|
||||
const el=document.getElementById(elId);
|
||||
if(!el) return;
|
||||
const chart=echarts.init(el);
|
||||
const top20=prepareTopN(data,20);
|
||||
chart.setOption({
|
||||
backgroundColor:"#272b2f",
|
||||
tooltip:{
|
||||
trigger:"item",
|
||||
formatter: params=>`${params.name}: ${Math.round(params.percent)}% (${params.value})`
|
||||
},
|
||||
series:[{
|
||||
name:name,
|
||||
type:"pie",
|
||||
radius:["30%","70%"],
|
||||
center:["50%","50%"],
|
||||
avoidLabelOverlap:true,
|
||||
itemStyle:{
|
||||
borderRadius:6,
|
||||
borderColor:"#272b2f",
|
||||
borderWidth:2
|
||||
},
|
||||
label:{
|
||||
show:true,
|
||||
formatter:"{b}\n{d}%",
|
||||
color:"#ccc",
|
||||
fontSize:10
|
||||
},
|
||||
labelLine:{
|
||||
show:true,
|
||||
length:10,
|
||||
length2:6
|
||||
},
|
||||
data:top20
|
||||
}]
|
||||
});
|
||||
return chart;
|
||||
}
|
||||
|
||||
// --- Packet Type Pie Chart ---
|
||||
async function fetchPacketTypeBreakdown(channel=null) {
|
||||
@@ -234,8 +353,10 @@ async function fetchPacketTypeBreakdown(channel=null) {
|
||||
const total = (data || []).reduce((sum,d)=>sum+(d.count??d.packet_count??0),0);
|
||||
return {portnum: pn, count: total};
|
||||
});
|
||||
|
||||
const allData = await fetchStats('hour',24,null,channel);
|
||||
const totalAll = allData.reduce((sum,d)=>sum+(d.count??d.packet_count??0),0);
|
||||
|
||||
const results = await Promise.all(requests);
|
||||
const trackedTotal = results.reduce((sum,d)=>sum+d.count,0);
|
||||
const other = Math.max(totalAll - trackedTotal,0);
|
||||
@@ -250,40 +371,102 @@ let chartHwModel, chartRole, chartChannel;
|
||||
let chartPacketTypes;
|
||||
|
||||
async function init(){
|
||||
// Channel selector
|
||||
const channels = await fetchChannels();
|
||||
const select = document.getElementById("channelSelect");
|
||||
channels.forEach(ch=>{ const opt = document.createElement("option"); opt.value = ch; opt.textContent = ch; select.appendChild(opt); });
|
||||
channels.forEach(ch=>{
|
||||
const opt = document.createElement("option");
|
||||
opt.value = ch;
|
||||
opt.textContent = ch;
|
||||
select.appendChild(opt);
|
||||
});
|
||||
|
||||
// Daily all ports
|
||||
const dailyAllData=await fetchStats('day',14);
|
||||
updateTotalCount('total_daily_all',dailyAllData);
|
||||
chartDailyAll=renderChart('chart_daily_all',dailyAllData,'line','#66bb6a');
|
||||
|
||||
// Daily port 1
|
||||
const dailyPort1Data=await fetchStats('day',14,1);
|
||||
updateTotalCount('total_daily_portnum_1',dailyPort1Data);
|
||||
chartDailyPortnum1=renderChart('chart_daily_portnum_1',dailyPort1Data,'bar','#ff5722');
|
||||
|
||||
// Hourly all ports
|
||||
const hourlyAllData=await fetchStats('hour',24);
|
||||
updateTotalCount('total_hourly_all',hourlyAllData);
|
||||
chartHourlyAll=renderChart('chart_hourly_all',hourlyAllData,'bar','#03dac6');
|
||||
|
||||
// Hourly per port
|
||||
const portnums=[1,3,4,67,70,71];
|
||||
const colors=['#ff5722','#2196f3','#9c27b0','#ffeb3b','#795548','#4caf50'];
|
||||
const domIds=['chart_portnum_1','chart_portnum_3','chart_portnum_4','chart_portnum_67','chart_portnum_70','chart_portnum_71'];
|
||||
const totalIds=['total_portnum_1','total_portnum_3','total_portnum_4','total_portnum_67','total_portnum_70','total_portnum_71'];
|
||||
const allData=await Promise.all(portnums.map(pn=>fetchStats('hour',24,pn)));
|
||||
for(let i=0;i<portnums.length;i++){ updateTotalCount(totalIds[i],allData[i]); window['chartPortnum'+portnums[i]]=renderChart(domIds[i],allData[i],'bar',colors[i]); }
|
||||
|
||||
const allData=await Promise.all(portnums.map(pn=>fetchStats('hour',24,pn)));
|
||||
for(let i=0;i<portnums.length;i++){
|
||||
updateTotalCount(totalIds[i],allData[i]);
|
||||
window['chartPortnum'+portnums[i]]=renderChart(domIds[i],allData[i],'bar',colors[i]);
|
||||
}
|
||||
|
||||
// Nodes for breakdown + summary node count
|
||||
const nodes=await fetchNodes();
|
||||
chartHwModel=renderPieChart("chart_hw_model",processCountField(nodes,"hw_model"),"Hardware");
|
||||
chartRole=renderPieChart("chart_role",processCountField(nodes,"role"),"Role");
|
||||
chartChannel=renderPieChart("chart_channel",processCountField(nodes,"channel"),"Channel");
|
||||
|
||||
const summaryNodesEl = document.getElementById("summary_nodes");
|
||||
if (summaryNodesEl) {
|
||||
summaryNodesEl.textContent = nodes.length.toLocaleString();
|
||||
}
|
||||
|
||||
// Packet types pie
|
||||
const packetTypesData = await fetchPacketTypeBreakdown();
|
||||
const formatted = packetTypesData.filter(d=>d.count>0).map(d=>({ name: d.portnum==="other" ? "Other" : (PORTNUM_LABELS[d.portnum]||`Port ${d.portnum}`), value: d.count }));
|
||||
const formatted = packetTypesData
|
||||
.filter(d=>d.count>0)
|
||||
.map(d=>({
|
||||
name: d.portnum==="other"
|
||||
? "Other"
|
||||
: (PORTNUM_LABELS[d.portnum]||`Port ${d.portnum}`),
|
||||
value: d.count
|
||||
}));
|
||||
chartPacketTypes = renderPieChart("chart_packet_types",formatted,"Packet Types (Last 24h)");
|
||||
|
||||
// Total packet + total seen from /api/stats/count
|
||||
try {
|
||||
const countsRes = await fetch("/api/stats/count");
|
||||
if (countsRes.ok) {
|
||||
const countsJson = await countsRes.json();
|
||||
const elPackets = document.getElementById("summary_packets");
|
||||
const elSeen = document.getElementById("summary_seen");
|
||||
if (elPackets) {
|
||||
elPackets.textContent = (countsJson.total_packets || 0).toLocaleString();
|
||||
}
|
||||
if (elSeen) {
|
||||
elSeen.textContent = (countsJson.total_seen || 0).toLocaleString();
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Failed to load /api/stats/count:", err);
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('resize',()=>{ [chartHourlyAll,chartPortnum1,chartPortnum3,chartPortnum4,chartPortnum67,chartPortnum70,chartPortnum71, chartDailyAll,chartDailyPortnum1,chartHwModel,chartRole,chartChannel,chartPacketTypes].forEach(c=>c?.resize()); });
|
||||
window.addEventListener('resize',()=>{
|
||||
[
|
||||
chartHourlyAll,
|
||||
chartPortnum1,
|
||||
chartPortnum3,
|
||||
chartPortnum4,
|
||||
chartPortnum67,
|
||||
chartPortnum70,
|
||||
chartPortnum71,
|
||||
chartDailyAll,
|
||||
chartDailyPortnum1,
|
||||
chartHwModel,
|
||||
chartRole,
|
||||
chartChannel,
|
||||
chartPacketTypes
|
||||
].forEach(c=>c?.resize());
|
||||
});
|
||||
|
||||
const modal=document.getElementById("chartModal");
|
||||
const modalChartEl=document.getElementById("modalChart");
|
||||
@@ -345,11 +528,19 @@ document.querySelectorAll(".export-btn").forEach(btn=>{
|
||||
document.getElementById("channelSelect").addEventListener("change", async (e)=>{
|
||||
const channel = e.target.value;
|
||||
const packetTypesData = await fetchPacketTypeBreakdown(channel);
|
||||
const formatted = packetTypesData.filter(d=>d.count>0).map(d=>({ name: d.portnum==="other" ? "Other" : (PORTNUM_LABELS[d.portnum]||`Port ${d.portnum}`), value: d.count }));
|
||||
const formatted = packetTypesData
|
||||
.filter(d=>d.count>0)
|
||||
.map(d=>({
|
||||
name: d.portnum==="other"
|
||||
? "Other"
|
||||
: (PORTNUM_LABELS[d.portnum]||`Port ${d.portnum}`),
|
||||
value: d.count
|
||||
}));
|
||||
chartPacketTypes?.dispose();
|
||||
chartPacketTypes = renderPieChart("chart_packet_types",formatted,"Packet Types (Last 24h)");
|
||||
});
|
||||
|
||||
// Kick everything off
|
||||
init();
|
||||
|
||||
// --- Load config and translations ---
|
||||
@@ -383,6 +574,5 @@ async function loadConfigAndTranslations() {
|
||||
|
||||
// Call after init
|
||||
loadConfigAndTranslations();
|
||||
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
{% block head %}
|
||||
<script src="https://cdn.jsdelivr.net/npm/echarts/dist/echarts.min.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div id="mynetwork" style="width: 100%; height: 800px;"></div>
|
||||
|
||||
<script type="text/javascript">
|
||||
const chart = echarts.init(document.getElementById('mynetwork'));
|
||||
|
||||
const rawNodes = {{ chart_data['nodes'] | tojson }};
|
||||
const rawEdges = {{ chart_data['edges'] | tojson }};
|
||||
|
||||
// Build DAG layout
|
||||
const layers = {};
|
||||
const nodeDepth = {};
|
||||
|
||||
// Organize nodes into layers by hop count
|
||||
for (const edge of rawEdges) {
|
||||
const { source, target } = edge;
|
||||
if (!(source in nodeDepth)) nodeDepth[source] = 0;
|
||||
const nextDepth = nodeDepth[source] + 1;
|
||||
nodeDepth[target] = Math.max(nodeDepth[target] || 0, nextDepth);
|
||||
}
|
||||
|
||||
for (const node of rawNodes) {
|
||||
const depth = nodeDepth[node.name] || 0;
|
||||
if (!(depth in layers)) layers[depth] = [];
|
||||
layers[depth].push(node);
|
||||
}
|
||||
|
||||
// Position nodes manually
|
||||
const chartNodes = [];
|
||||
const layerKeys = Object.keys(layers).sort((a, b) => +a - +b);
|
||||
const verticalSpacing = 100;
|
||||
const horizontalSpacing = 180;
|
||||
|
||||
layerKeys.forEach((depth, layerIndex) => {
|
||||
const layer = layers[depth];
|
||||
const y = layerIndex * verticalSpacing;
|
||||
const xStart = -(layer.length - 1) * horizontalSpacing / 2;
|
||||
layer.forEach((node, i) => {
|
||||
chartNodes.push({
|
||||
...node,
|
||||
x: xStart + i * horizontalSpacing,
|
||||
y: y,
|
||||
itemStyle: {
|
||||
color: '#dddddd',
|
||||
borderColor: '#222',
|
||||
borderWidth: 2,
|
||||
},
|
||||
label: {
|
||||
show: true,
|
||||
position: 'inside',
|
||||
color: '#000',
|
||||
fontSize: 12,
|
||||
formatter: node.short_name || node.name,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const chartEdges = rawEdges.map(edge => ({
|
||||
source: edge.source,
|
||||
target: edge.target,
|
||||
lineStyle: {
|
||||
color: edge.originalColor || '#ccc',
|
||||
width: 2,
|
||||
type: 'solid',
|
||||
},
|
||||
}));
|
||||
|
||||
const option = {
|
||||
backgroundColor: '#fff',
|
||||
tooltip: {},
|
||||
animation: false,
|
||||
series: [{
|
||||
type: 'graph',
|
||||
layout: 'none',
|
||||
coordinateSystem: null,
|
||||
data: chartNodes,
|
||||
links: chartEdges,
|
||||
roam: true,
|
||||
edgeSymbol: ['none', 'arrow'],
|
||||
edgeSymbolSize: [0, 10],
|
||||
lineStyle: {
|
||||
curveness: 0,
|
||||
},
|
||||
}],
|
||||
};
|
||||
|
||||
chart.setOption(option);
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -269,6 +269,14 @@ async def top(request):
|
||||
content_type="text/html",
|
||||
)
|
||||
|
||||
@routes.get("/stats")
|
||||
async def stats(request):
|
||||
template = env.get_template("stats.html")
|
||||
return web.Response(
|
||||
text=template.render(),
|
||||
content_type="text/html",
|
||||
)
|
||||
|
||||
|
||||
# Keep !!
|
||||
@routes.get("/graph/traceroute/{packet_id}")
|
||||
@@ -377,7 +385,7 @@ async def graph_traceroute(request):
|
||||
content_type="image/svg+xml",
|
||||
)
|
||||
|
||||
|
||||
'''
|
||||
@routes.get("/stats")
|
||||
async def stats(request):
|
||||
try:
|
||||
@@ -399,86 +407,7 @@ async def stats(request):
|
||||
status=500,
|
||||
content_type="text/plain",
|
||||
)
|
||||
|
||||
|
||||
'''
|
||||
@routes.get("/top")
|
||||
async def top(request):
|
||||
import time
|
||||
|
||||
try:
|
||||
# Check if performance metrics should be displayed
|
||||
show_perf = request.query.get("perf", "").lower() in ("true", "1", "yes")
|
||||
|
||||
# Start overall timing
|
||||
start_time = time.perf_counter()
|
||||
timing_data = None
|
||||
|
||||
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
|
||||
db_start = time.perf_counter()
|
||||
node_traffic = await store.get_node_traffic(int(node_id))
|
||||
db_time = time.perf_counter() - db_start
|
||||
|
||||
template = env.get_template("node_traffic.html")
|
||||
html_content = template.render(
|
||||
traffic=node_traffic, node_id=node_id, site_config=CONFIG
|
||||
)
|
||||
else:
|
||||
# Otherwise, fetch top traffic nodes as usual
|
||||
db_start = time.perf_counter()
|
||||
top_nodes = await store.get_top_traffic_nodes()
|
||||
db_time = time.perf_counter() - db_start
|
||||
|
||||
# Data processing timing
|
||||
process_start = time.perf_counter()
|
||||
|
||||
# Count records processed
|
||||
total_packets = sum(node.get('total_packets_sent', 0) for node in top_nodes)
|
||||
total_seen = sum(node.get('total_times_seen', 0) for node in top_nodes)
|
||||
|
||||
process_time = time.perf_counter() - process_start
|
||||
|
||||
# Calculate total time
|
||||
total_time = time.perf_counter() - start_time
|
||||
|
||||
# Only include timing_data if perf parameter is set
|
||||
if show_perf:
|
||||
timing_data = {
|
||||
'db_query_ms': f"{db_time * 1000:.2f}",
|
||||
'processing_ms': f"{process_time * 1000:.2f}",
|
||||
'total_ms': f"{total_time * 1000:.2f}",
|
||||
'node_count': len(top_nodes),
|
||||
'total_packets': total_packets,
|
||||
'total_seen': total_seen,
|
||||
}
|
||||
|
||||
template = env.get_template("top.html")
|
||||
html_content = template.render(
|
||||
nodes=top_nodes,
|
||||
timing_data=timing_data,
|
||||
site_config=CONFIG,
|
||||
SOFTWARE_RELEASE=SOFTWARE_RELEASE,
|
||||
)
|
||||
|
||||
return web.Response(
|
||||
text=html_content,
|
||||
content_type="text/html",
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Error in /top: {e}")
|
||||
template = env.get_template("error.html")
|
||||
rendered = template.render(
|
||||
error_message="An error occurred in /top",
|
||||
error_details=traceback.format_exc(),
|
||||
site_config=CONFIG,
|
||||
SOFTWARE_RELEASE=SOFTWARE_RELEASE,
|
||||
)
|
||||
return web.Response(text=rendered, status=500, content_type="text/html")
|
||||
'''
|
||||
|
||||
|
||||
async def run_server():
|
||||
# Wait for database migrations to complete before starting web server
|
||||
|
||||
Reference in New Issue
Block a user