Files
meshview/meshview/templates/stats.html
T
2025-09-12 11:46:20 -07:00

331 lines
11 KiB
HTML

{% extends "base.html" %}
{% block css %}
#packet_details {
height: 95vh;
overflow: auto;
}
.main-container, .container {
max-width: 900px;
margin: 0 auto;
text-align: center;
}
.card-section {
background-color: #272b2f;
border: 1px solid #474b4e;
padding: 15px 20px;
margin-bottom: 20px;
border-radius: 10px;
transition: background-color 0.2s ease;
}
.card-section:hover {
background-color: #2f3338;
}
.section-header {
font-size: 16px;
margin: 0 0 6px 0;
font-weight: 500;
color: #fff;
}
.main-header {
font-size: 22px;
margin-bottom: 20px;
font-weight: 600;
}
.chart {
height: 400px;
margin-top: 10px;
}
.total-count {
color: #ccc;
font-size: 14px;
margin-bottom: 8px;
}
{% endblock %}
{% block head %}
<script src="https://cdn.jsdelivr.net/npm/echarts@5.5.0/dist/echarts.min.js"></script>
{% endblock %}
{% block body %}
<div class="main-container">
<h2 class="main-header">Mesh Statistics - Hourly Packet Counts (Last 24 Hours)</h2>
<!-- Daily packets chart moved to top -->
<div class="card-section">
<p class="section-header">Packets per Day - All Ports (Last 14 Days)</p>
<div id="total_daily_all" class="total-count">Total: 0</div>
<div id="chart_daily_all" class="chart"></div>
</div>
<!-- Daily packets - Text Messages -->
<div class="card-section">
<p class="section-header">Packets per Day - Text Messages (Port 1, Last 14 Days)</p>
<div id="total_daily_portnum_1" class="total-count">Total: 0</div>
<div id="chart_daily_portnum_1" class="chart"></div>
</div>
<!-- Overall hourly packets -->
<div class="card-section">
<p class="section-header">Packets per Hour - All Ports</p>
<div id="total_hourly_all" class="total-count">Total: 0</div>
<div id="chart_hourly_all" class="chart"></div>
</div>
<!-- Hourly charts by portnum -->
<div class="card-section">
<p class="section-header">Packets per Hour - Text Messages (Port 1)</p>
<div id="total_portnum_1" class="total-count">Total: 0</div>
<div id="chart_portnum_1" class="chart"></div>
</div>
<div class="card-section">
<p class="section-header">Packets per Hour - Position (Port 3)</p>
<div id="total_portnum_3" class="total-count">Total: 0</div>
<div id="chart_portnum_3" class="chart"></div>
</div>
<div class="card-section">
<p class="section-header">Packets per Hour - Node Info (Port 4)</p>
<div id="total_portnum_4" class="total-count">Total: 0</div>
<div id="chart_portnum_4" class="chart"></div>
</div>
<div class="card-section">
<p class="section-header">Packets per Hour - Telemetry (Port 67)</p>
<div id="total_portnum_67" class="total-count">Total: 0</div>
<div id="chart_portnum_67" class="chart"></div>
</div>
<div class="card-section">
<p class="section-header">Packets per Hour - Traceroute (Port 70)</p>
<div id="total_portnum_70" class="total-count">Total: 0</div>
<div id="chart_portnum_70" class="chart"></div>
</div>
<div class="card-section">
<p class="section-header">Packets per Hour - Neighbor Info (Port 71)</p>
<div id="total_portnum_71" class="total-count">Total: 0</div>
<div id="chart_portnum_71" class="chart"></div>
</div>
<!-- Hardware breakdown -->
<div class="card-section">
<p class="section-header">Hardware Breakdown</p>
<div id="chart_hw_model" class="chart"></div>
</div>
<!-- Role breakdown -->
<div class="card-section">
<p class="section-header">Role Breakdown</p>
<div id="chart_role" class="chart"></div>
</div>
<!-- Channel breakdown -->
<div class="card-section">
<p class="section-header">Channel Breakdown</p>
<div id="chart_channel" class="chart"></div>
</div>
</div>
<script>
async function fetchStats(period_type, length, portnum = null) {
try {
let url = `/api/stats?period_type=${period_type}&length=${length}`;
if (portnum !== null) {
url += `&portnum=${portnum}`;
}
const res = await fetch(url);
if (!res.ok) {
console.error(`Failed to fetch ${period_type} stats portnum ${portnum}:`, res.status, res.statusText);
return [];
}
const json = await res.json();
return json.data || [];
} catch (err) {
console.error('Error fetching stats:', err);
return [];
}
}
async function fetchNodes() {
try {
const res = await fetch("/api/nodes");
if (!res.ok) {
console.error("Failed to fetch nodes:", res.status, res.statusText);
return [];
}
const json = await res.json();
return json.nodes || [];
} catch (err) {
console.error("Error fetching nodes:", err);
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()}`;
}
// Chart variables
let chartHourlyAll, chartPortnum1, chartPortnum3, chartPortnum4, chartPortnum67, chartPortnum70, chartPortnum71;
let chartDailyAll, chartDailyPortnum1;
let chartHwModel, chartRole, chartChannel;
// Utility function for top N + "Other"
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;
}
function renderChart(domId, data, type, color, isHourly) {
const el = document.getElementById(domId);
if (!el) return;
const chart = echarts.init(el);
switch(domId) {
case 'chart_hourly_all': chartHourlyAll = chart; break;
case 'chart_portnum_1': chartPortnum1 = chart; break;
case 'chart_portnum_3': chartPortnum3 = chart; break;
case 'chart_portnum_4': chartPortnum4 = chart; break;
case 'chart_portnum_67': chartPortnum67 = chart; break;
case 'chart_portnum_70': chartPortnum70 = chart; break;
case 'chart_portnum_71': chartPortnum71 = chart; break;
case 'chart_daily_all': chartDailyAll = chart; break;
case 'chart_daily_portnum_1': chartDailyPortnum1 = chart; break;
}
const periods = data.map(d => {
const p = (d && (d.period || d.period === 0)) ? d.period.toString() : '';
if (isHourly) {
if (p.includes(' ')) return p.split(' ')[1];
return p.slice(-5) || p;
}
return p;
});
const counts = data.map(d => (d.count ?? d.packet_count ?? 0));
const option = {
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
}]
};
chart.setOption(option);
}
function renderPieChart(elId, data, name) {
const el = document.getElementById(elId);
if (!el) return;
const chart = echarts.init(el);
const top20 = prepareTopN(data, 20);
const option = {
backgroundColor: "#272b2f",
tooltip: { trigger: "item", formatter: "{b}: {d}% ({c})" },
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
}
]
};
chart.setOption(option);
return chart;
}
async function init() {
// Daily all ports
const dailyAllData = await fetchStats('day', 14);
updateTotalCount('total_daily_all', dailyAllData);
renderChart('chart_daily_all', dailyAllData, 'line', '#66bb6a', false);
// Daily text packets (port 1) as BAR chart
const dailyPort1Data = await fetchStats('day', 14, 1);
updateTotalCount('total_daily_portnum_1', dailyPort1Data);
renderChart('chart_daily_portnum_1', dailyPort1Data, 'bar', '#ff5722', false);
// Hourly all ports
const hourlyAllData = await fetchStats('hour', 24);
updateTotalCount('total_hourly_all', hourlyAllData);
renderChart('chart_hourly_all', hourlyAllData, 'bar', '#03dac6', true);
// Hourly by 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]);
renderChart(domIds[i], allData[i], 'bar', colors[i], true);
}
// Nodes breakdown charts
const nodes = await fetchNodes();
renderPieChart("chart_hw_model", processCountField(nodes, "hw_model"), "Hardware");
renderPieChart("chart_role", processCountField(nodes, "role"), "Role");
renderPieChart("chart_channel", processCountField(nodes, "channel"), "Channel");
}
window.addEventListener('resize', () => {
[chartHourlyAll, chartPortnum1, chartPortnum3, chartPortnum4, chartPortnum67, chartPortnum70, chartPortnum71, chartDailyAll, chartDailyPortnum1, chartHwModel, chartRole, chartChannel]
.forEach(c => c?.resize());
});
init();
</script>
{% endblock %}