Files
pyMC_Repeater/repeater/templates/statistics.html
T

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>