48fcb00241
Major Achievements: - ✅ Full web interface (1,520+ lines of frontend code) - ✅ Interactive Leaflet.js map with marker clustering - ✅ Drag-and-drop upload system with GPS input - ✅ Search & filter UI with multi-criteria - ✅ Statistics dashboard with Chart.js - ✅ Responsive mobile-friendly design Backend: - ✅ FastAPI static file serving - ✅ Simplified server mode (main_simple.py) - ✅ Improved startup script with port auto-selection - ✅ PostgreSQL schema ready (requires setup) Database: - ✅ SQLite populated with 85 Flipper Zero signatures - ✅ Device matching system operational - ✅ Frequency-based search working Documentation: - ✅ PHASE_3_COMPLETE.md - Technical summary - ✅ WEB_INTERFACE_README.md - User guide - ✅ WEBAPP_STARTUP_GUIDE.md - Troubleshooting - ✅ POSTGRESQL_SETUP_EXPLANATION.md - DB setup guide - ✅ DATABASE_POPULATION_SUCCESS.md - Import report - ✅ DEVICE_IDENTIFICATION_REPORT.md - Matching analysis Files Created: - templates/index.html (260 lines) - static/css/main.css (500 lines) - static/js/*.js (760 lines total) - src/api/main_simple.py (simplified server) - start_web.sh (auto port selection) Status: Production MVP Ready Next: Phase 4 - API & Integration 🛰️ Generated with Claude Code https://claude.com/claude-code Co-Authored-By: Claude <noreply@anthropic.com>
179 lines
5.0 KiB
JavaScript
179 lines
5.0 KiB
JavaScript
// GigLez - Statistics Dashboard
|
|
|
|
let frequencyChart;
|
|
let timelineChart;
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
// Load stats when stats section becomes active
|
|
const statsSection = document.getElementById('stats-section');
|
|
const observer = new MutationObserver(() => {
|
|
if (statsSection.classList.contains('active')) {
|
|
loadStatistics();
|
|
}
|
|
});
|
|
|
|
observer.observe(statsSection, { attributes: true });
|
|
});
|
|
|
|
async function loadStatistics() {
|
|
try {
|
|
const response = await fetch('/api/v1/stats/summary');
|
|
|
|
if (!response.ok) {
|
|
console.error('Failed to load statistics');
|
|
return;
|
|
}
|
|
|
|
const stats = await response.json();
|
|
displayStatistics(stats);
|
|
|
|
} catch (error) {
|
|
console.error('Error loading statistics:', error);
|
|
displayDefaultStats();
|
|
}
|
|
}
|
|
|
|
function displayStatistics(stats) {
|
|
// Update stat cards
|
|
document.getElementById('stat-captures').textContent = formatNumber(stats.total_captures || 0);
|
|
document.getElementById('stat-devices').textContent = formatNumber(stats.unique_devices || 0);
|
|
document.getElementById('stat-coverage').textContent = formatCoverage(stats.coverage_area_km2 || 0);
|
|
document.getElementById('stat-contributors').textContent = formatNumber(stats.total_contributors || 0);
|
|
|
|
// Update charts
|
|
updateFrequencyChart(stats.frequency_distribution || {});
|
|
updateTimelineChart(stats.captures_timeline || []);
|
|
}
|
|
|
|
function displayDefaultStats() {
|
|
// Display placeholder stats
|
|
document.getElementById('stat-captures').textContent = '-';
|
|
document.getElementById('stat-devices').textContent = '-';
|
|
document.getElementById('stat-coverage').textContent = '-';
|
|
document.getElementById('stat-contributors').textContent = '-';
|
|
}
|
|
|
|
function updateFrequencyChart(distribution) {
|
|
const ctx = document.getElementById('frequency-chart').getContext('2d');
|
|
|
|
// Prepare data
|
|
const labels = Object.keys(distribution).map(freq => `${(freq / 1e6).toFixed(0)} MHz`);
|
|
const data = Object.values(distribution);
|
|
|
|
// Destroy existing chart
|
|
if (frequencyChart) {
|
|
frequencyChart.destroy();
|
|
}
|
|
|
|
// Create chart
|
|
frequencyChart = new Chart(ctx, {
|
|
type: 'bar',
|
|
data: {
|
|
labels: labels,
|
|
datasets: [{
|
|
label: 'Number of Captures',
|
|
data: data,
|
|
backgroundColor: [
|
|
'rgba(16, 185, 129, 0.7)', // 315 MHz - Green
|
|
'rgba(59, 130, 246, 0.7)', // 433 MHz - Blue
|
|
'rgba(245, 158, 11, 0.7)', // 868 MHz - Orange
|
|
'rgba(239, 68, 68, 0.7)', // 915 MHz - Red
|
|
],
|
|
borderColor: [
|
|
'rgb(16, 185, 129)',
|
|
'rgb(59, 130, 246)',
|
|
'rgb(245, 158, 11)',
|
|
'rgb(239, 68, 68)',
|
|
],
|
|
borderWidth: 2
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: true,
|
|
plugins: {
|
|
legend: {
|
|
display: false
|
|
}
|
|
},
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
ticks: {
|
|
precision: 0
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function updateTimelineChart(timeline) {
|
|
const ctx = document.getElementById('timeline-chart').getContext('2d');
|
|
|
|
// Prepare data
|
|
const labels = timeline.map(item => new Date(item.date).toLocaleDateString());
|
|
const data = timeline.map(item => item.count);
|
|
|
|
// Destroy existing chart
|
|
if (timelineChart) {
|
|
timelineChart.destroy();
|
|
}
|
|
|
|
// Create chart
|
|
timelineChart = new Chart(ctx, {
|
|
type: 'line',
|
|
data: {
|
|
labels: labels,
|
|
datasets: [{
|
|
label: 'Captures per Day',
|
|
data: data,
|
|
borderColor: 'rgb(37, 99, 235)',
|
|
backgroundColor: 'rgba(37, 99, 235, 0.1)',
|
|
tension: 0.4,
|
|
fill: true
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: true,
|
|
plugins: {
|
|
legend: {
|
|
display: false
|
|
}
|
|
},
|
|
scales: {
|
|
y: {
|
|
beginAtZero: true,
|
|
ticks: {
|
|
precision: 0
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
function formatNumber(num) {
|
|
if (num >= 1000000) {
|
|
return (num / 1000000).toFixed(1) + 'M';
|
|
}
|
|
if (num >= 1000) {
|
|
return (num / 1000).toFixed(1) + 'K';
|
|
}
|
|
return num.toString();
|
|
}
|
|
|
|
function formatCoverage(km2) {
|
|
if (km2 === 0) {
|
|
return 'N/A';
|
|
}
|
|
if (km2 >= 10000) {
|
|
return (km2 / 10000).toFixed(1) + ' x 10K km²';
|
|
}
|
|
if (km2 >= 1000) {
|
|
return (km2 / 1000).toFixed(1) + 'K km²';
|
|
}
|
|
return km2.toFixed(0) + ' km²';
|
|
}
|