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>
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
// 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²';
|
||||
}
|
||||
Reference in New Issue
Block a user