mirror of
https://github.com/pyMC-dev/pyMC_Repeater.git
synced 2026-07-02 16:02:21 +02:00
336 lines
13 KiB
HTML
336 lines
13 KiB
HTML
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<title>pyMC Repeater - Statistics</title>
|
|
<meta charset="utf-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.9.1"></script>
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
|
<link rel="stylesheet" href="/static/style.css">
|
|
</head>
|
|
<body>
|
|
<div class="layout">
|
|
<!-- Navigation Component -->
|
|
<!-- NAVIGATION_PLACEHOLDER -->
|
|
|
|
<!-- Main Content -->
|
|
<main class="content">
|
|
<header>
|
|
<h1>Statistics</h1>
|
|
<p>Detailed performance analytics and metrics</p>
|
|
</header>
|
|
|
|
<!-- Summary Stats -->
|
|
<h2>Summary</h2>
|
|
<div class="stats-grid">
|
|
<div class="stat-card success">
|
|
<div class="stat-label">Total RX</div>
|
|
<div class="stat-value" id="total-rx">0<span class="stat-unit">packets</span></div>
|
|
</div>
|
|
|
|
<div class="stat-card success">
|
|
<div class="stat-label">Total TX</div>
|
|
<div class="stat-value" id="total-tx">0<span class="stat-unit">packets</span></div>
|
|
</div>
|
|
|
|
<div class="stat-card">
|
|
<div class="stat-label">Repeats</div>
|
|
<div class="stat-value" id="repeat-count">0<span class="stat-unit">packets</span></div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Charts -->
|
|
<h2>Performance Charts</h2>
|
|
<div class="charts-grid">
|
|
<div class="chart-card">
|
|
<h3>RX vs TX Over Time</h3>
|
|
<div class="chart-container">
|
|
<canvas id="rxtxChart"></canvas>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="chart-card">
|
|
<h3>Packet Type Distribution</h3>
|
|
<div class="chart-container">
|
|
<canvas id="packetTypeChart"></canvas>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="chart-card">
|
|
<h3>Signal Metrics Over Time</h3>
|
|
<div class="chart-container">
|
|
<canvas id="signalMetricsChart"></canvas>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="chart-card">
|
|
<h3>Route Type Distribution</h3>
|
|
<div class="chart-container">
|
|
<canvas id="routeTypeChart"></canvas>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</main>
|
|
</div>
|
|
|
|
<script>
|
|
let rxtxChart = null;
|
|
let packetTypeChart = null;
|
|
let signalMetricsChart = null;
|
|
let routeTypeChart = null;
|
|
|
|
const chartOptions = {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
labels: {
|
|
color: '#d4d4d4'
|
|
}
|
|
},
|
|
filler: {
|
|
propagate: true
|
|
}
|
|
},
|
|
scales: {
|
|
x: {
|
|
ticks: { color: '#999' },
|
|
grid: { color: '#333' }
|
|
},
|
|
y: {
|
|
ticks: { color: '#999' },
|
|
grid: { color: '#333' }
|
|
}
|
|
}
|
|
};
|
|
|
|
function initCharts() {
|
|
// RX vs TX chart
|
|
let rxtxCtx = document.getElementById('rxtxChart').getContext('2d');
|
|
rxtxChart = new Chart(rxtxCtx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: ['00:00', '05:00', '10:00', '15:00', '20:00'],
|
|
datasets: [
|
|
{
|
|
label: 'RX',
|
|
data: [0, 0, 0, 0, 0],
|
|
borderColor: '#4ec9b0',
|
|
backgroundColor: 'rgba(78, 201, 176, 0.1)',
|
|
borderWidth: 2,
|
|
fill: true,
|
|
tension: 0.4
|
|
},
|
|
{
|
|
label: 'TX',
|
|
data: [0, 0, 0, 0, 0],
|
|
borderColor: '#6a9955',
|
|
backgroundColor: 'rgba(106, 153, 85, 0.1)',
|
|
borderWidth: 2,
|
|
fill: true,
|
|
tension: 0.4
|
|
}
|
|
]
|
|
},
|
|
options: chartOptions
|
|
});
|
|
|
|
// Packet type chart
|
|
let typeCtx = document.getElementById('packetTypeChart').getContext('2d');
|
|
packetTypeChart = new Chart(typeCtx, {
|
|
type: 'doughnut',
|
|
data: {
|
|
labels: ['REQ', 'RESPONSE', 'TXT', 'ACK', 'ADVERT', 'GRP_TXT', 'GRP_DATA', 'PATH', 'OTHER'],
|
|
datasets: [{
|
|
data: [0, 0, 0, 0, 0, 0, 0, 0, 0],
|
|
backgroundColor: ['#ce9178', '#f48771', '#dcdcaa', '#6a9955', '#4ec9b0', '#c586c0', '#9cdcfe', '#569cd6', '#808080']
|
|
}]
|
|
},
|
|
options: chartOptions
|
|
});
|
|
|
|
// Signal metrics chart (RSSI, SNR, Noise Floor)
|
|
let metricsCtx = document.getElementById('signalMetricsChart').getContext('2d');
|
|
signalMetricsChart = new Chart(metricsCtx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: ['00:00', '05:00', '10:00', '15:00', '20:00'],
|
|
datasets: [
|
|
{
|
|
label: 'RSSI (dBm)',
|
|
data: [0, 0, 0, 0, 0],
|
|
borderColor: '#ce9178',
|
|
backgroundColor: 'rgba(206, 145, 120, 0.1)',
|
|
borderWidth: 2,
|
|
fill: true,
|
|
tension: 0.4,
|
|
yAxisID: 'y1'
|
|
},
|
|
{
|
|
label: 'SNR (dB)',
|
|
data: [0, 0, 0, 0, 0],
|
|
borderColor: '#4ec9b0',
|
|
backgroundColor: 'rgba(78, 201, 176, 0.1)',
|
|
borderWidth: 2,
|
|
fill: false,
|
|
tension: 0.4,
|
|
yAxisID: 'y2'
|
|
},
|
|
{
|
|
label: 'Noise Floor (dBm)',
|
|
data: [0, 0, 0, 0, 0],
|
|
borderColor: '#f48771',
|
|
backgroundColor: 'rgba(244, 135, 113, 0.1)',
|
|
borderWidth: 2,
|
|
fill: false,
|
|
tension: 0.4,
|
|
borderDash: [5, 5],
|
|
yAxisID: 'y1'
|
|
}
|
|
]
|
|
},
|
|
options: {
|
|
...chartOptions,
|
|
scales: {
|
|
x: {
|
|
ticks: { color: '#999' },
|
|
grid: { color: '#333' }
|
|
},
|
|
y1: {
|
|
type: 'linear',
|
|
position: 'left',
|
|
ticks: { color: '#999' },
|
|
grid: { color: '#333' },
|
|
title: {
|
|
display: true,
|
|
text: 'RSSI / Noise (dBm)',
|
|
color: '#ce9178'
|
|
}
|
|
},
|
|
y2: {
|
|
type: 'linear',
|
|
position: 'right',
|
|
ticks: { color: '#999' },
|
|
grid: { drawOnChartArea: false }
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Route type chart
|
|
let routeCtx = document.getElementById('routeTypeChart').getContext('2d');
|
|
routeTypeChart = new Chart(routeCtx, {
|
|
type: 'doughnut',
|
|
data: {
|
|
labels: ['FLOOD', 'DIRECT'],
|
|
datasets: [{
|
|
data: [0, 0],
|
|
backgroundColor: ['#dcdcaa', '#6a9955']
|
|
}]
|
|
},
|
|
options: chartOptions
|
|
});
|
|
}
|
|
|
|
function updateStats() {
|
|
fetch('/api/stats')
|
|
.then(r => r.json())
|
|
.then(data => {
|
|
// Update summary
|
|
const rx = data.rx_count || 0;
|
|
const tx = data.forwarded_count || 0;
|
|
const repeats = tx - rx;
|
|
|
|
document.getElementById('total-rx').textContent = rx;
|
|
document.getElementById('total-tx').textContent = tx;
|
|
document.getElementById('repeat-count').textContent = repeats;
|
|
|
|
// Update charts with data trends
|
|
const packets = data.recent_packets || [];
|
|
|
|
// Calculate packet type distribution
|
|
// Types: 0x00=REQ, 0x01=RESPONSE, 0x02=TXT, 0x03=ACK, 0x04=ADVERT,
|
|
// 0x05=GRP_TXT, 0x06=GRP_DATA, 0x08=PATH, other
|
|
const types = { 0: 0, 1: 0, 2: 0, 3: 0, 4: 0, 5: 0, 6: 0, 8: 0, other: 0 };
|
|
const routes = { flood: 0, direct: 0 };
|
|
let rssiSum = 0, snrSum = 0, rssiMin = 0;
|
|
let packetCount = 0;
|
|
|
|
packets.forEach(p => {
|
|
// Count packet types
|
|
if (p.type === 0 || p.type === 1 || p.type === 2 || p.type === 3 ||
|
|
p.type === 4 || p.type === 5 || p.type === 6 || p.type === 8) {
|
|
types[p.type] = (types[p.type] || 0) + 1;
|
|
} else {
|
|
types.other++;
|
|
}
|
|
if (p.route === 1) routes.flood++; else routes.direct++;
|
|
rssiSum += p.rssi || 0;
|
|
snrSum += p.snr || 0;
|
|
if (rssiMin === 0 || p.rssi < rssiMin) rssiMin = p.rssi;
|
|
packetCount++;
|
|
});
|
|
|
|
// Update packet type chart
|
|
packetTypeChart.data.datasets[0].data = [
|
|
types[0] || 0, // REQ
|
|
types[1] || 0, // RESPONSE
|
|
types[2] || 0, // TXT
|
|
types[3] || 0, // ACK
|
|
types[4] || 0, // ADVERT
|
|
types[5] || 0, // GRP_TXT (channel messages)
|
|
types[6] || 0, // GRP_DATA
|
|
types[8] || 0, // PATH
|
|
types.other || 0 // OTHER
|
|
];
|
|
packetTypeChart.update();
|
|
|
|
// Update RX vs TX chart (add current counts to timeline)
|
|
const now = new Date();
|
|
const timeLabel = now.getHours().toString().padStart(2, '0') + ':' +
|
|
now.getMinutes().toString().padStart(2, '0');
|
|
|
|
rxtxChart.data.labels.push(timeLabel);
|
|
rxtxChart.data.datasets[0].data.push(rx); // RX count
|
|
rxtxChart.data.datasets[1].data.push(tx); // TX count
|
|
|
|
// Keep only last 20 data points
|
|
if (rxtxChart.data.labels.length > 20) {
|
|
rxtxChart.data.labels.shift();
|
|
rxtxChart.data.datasets[0].data.shift();
|
|
rxtxChart.data.datasets[1].data.shift();
|
|
}
|
|
rxtxChart.update();
|
|
|
|
// Update route type chart
|
|
routeTypeChart.data.datasets[0].data = [routes.flood, routes.direct];
|
|
routeTypeChart.update();
|
|
|
|
// Update signal metrics chart
|
|
const avgRssi = packetCount > 0 ? Math.round(rssiSum / packetCount) : 0;
|
|
const avgSnr = packetCount > 0 ? Math.round(snrSum / packetCount) : 0;
|
|
const noiseFloor = avgRssi - avgSnr; // Noise Floor = RSSI - SNR
|
|
|
|
signalMetricsChart.data.datasets[0].data.push(avgRssi);
|
|
signalMetricsChart.data.datasets[1].data.push(avgSnr);
|
|
signalMetricsChart.data.datasets[2].data.push(noiseFloor);
|
|
|
|
if (signalMetricsChart.data.datasets[0].data.length > 5) {
|
|
signalMetricsChart.data.datasets[0].data.shift();
|
|
signalMetricsChart.data.datasets[1].data.shift();
|
|
signalMetricsChart.data.datasets[2].data.shift();
|
|
}
|
|
signalMetricsChart.update();
|
|
})
|
|
.catch(e => console.error('Error fetching stats:', e));
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
initCharts();
|
|
updateStats();
|
|
setInterval(updateStats, 5000);
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|