Files
giglez/static/js/stats.js
T
Trilltechnician 48fcb00241 Phase 3 Complete: Web Interface MVP
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>
2026-01-12 18:21:11 -08:00

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²';
}