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>
276 lines
8.1 KiB
JavaScript
276 lines
8.1 KiB
JavaScript
// GigLez - Upload Functionality
|
|
|
|
let selectedFiles = [];
|
|
|
|
// Initialize upload functionality
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
setupDropZone();
|
|
setupFileInput();
|
|
});
|
|
|
|
function setupDropZone() {
|
|
const dropZone = document.getElementById('drop-zone');
|
|
|
|
dropZone.addEventListener('click', () => {
|
|
document.getElementById('file-input').click();
|
|
});
|
|
|
|
dropZone.addEventListener('dragover', (e) => {
|
|
e.preventDefault();
|
|
dropZone.classList.add('drag-over');
|
|
});
|
|
|
|
dropZone.addEventListener('dragleave', () => {
|
|
dropZone.classList.remove('drag-over');
|
|
});
|
|
|
|
dropZone.addEventListener('drop', (e) => {
|
|
e.preventDefault();
|
|
dropZone.classList.remove('drag-over');
|
|
|
|
const files = Array.from(e.dataTransfer.files).filter(f => f.name.endsWith('.sub'));
|
|
addFiles(files);
|
|
});
|
|
}
|
|
|
|
function setupFileInput() {
|
|
const fileInput = document.getElementById('file-input');
|
|
|
|
fileInput.addEventListener('change', (e) => {
|
|
const files = Array.from(e.target.files);
|
|
addFiles(files);
|
|
});
|
|
}
|
|
|
|
function addFiles(files) {
|
|
if (files.length === 0) {
|
|
alert('Please select .sub files');
|
|
return;
|
|
}
|
|
|
|
// Add new files
|
|
selectedFiles.push(...files);
|
|
|
|
// Show file list
|
|
document.getElementById('file-list').style.display = 'block';
|
|
|
|
// Render file list
|
|
renderFileList();
|
|
}
|
|
|
|
function renderFileList() {
|
|
const container = document.getElementById('files-container');
|
|
|
|
container.innerHTML = selectedFiles.map((file, index) => `
|
|
<div class="file-item" data-index="${index}">
|
|
<div class="file-info">
|
|
<div class="file-name">${file.name}</div>
|
|
<div class="file-meta">${formatFileSize(file.size)}</div>
|
|
</div>
|
|
<div class="file-actions">
|
|
<button type="button" class="btn btn-secondary" onclick="removeFile(${index})">Remove</button>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
function removeFile(index) {
|
|
selectedFiles.splice(index, 1);
|
|
|
|
if (selectedFiles.length === 0) {
|
|
document.getElementById('file-list').style.display = 'none';
|
|
} else {
|
|
renderFileList();
|
|
}
|
|
}
|
|
|
|
function clearFiles() {
|
|
selectedFiles = [];
|
|
document.getElementById('file-list').style.display = 'none';
|
|
document.getElementById('file-input').value = '';
|
|
}
|
|
|
|
async function uploadFiles() {
|
|
if (selectedFiles.length === 0) {
|
|
alert('Please select files to upload');
|
|
return;
|
|
}
|
|
|
|
// Validate GPS coordinates
|
|
const lat = parseFloat(document.getElementById('default-lat').value);
|
|
const lon = parseFloat(document.getElementById('default-lon').value);
|
|
|
|
if (isNaN(lat) || isNaN(lon)) {
|
|
alert('Please provide valid GPS coordinates');
|
|
return;
|
|
}
|
|
|
|
if (lat < -90 || lat > 90 || lon < -180 || lon > 180) {
|
|
alert('GPS coordinates out of range');
|
|
return;
|
|
}
|
|
|
|
// Hide file list, show progress
|
|
document.getElementById('file-list').style.display = 'none';
|
|
document.getElementById('upload-progress').style.display = 'block';
|
|
|
|
try {
|
|
// Build manifest
|
|
const sessionUuid = document.getElementById('session-uuid').value || generateUUID();
|
|
const accuracy = parseFloat(document.getElementById('default-accuracy').value) || 5.0;
|
|
const altitude = parseFloat(document.getElementById('default-altitude').value) || null;
|
|
|
|
const manifest = {
|
|
session_uuid: sessionUuid,
|
|
captures: selectedFiles.map(file => ({
|
|
filename: file.name,
|
|
latitude: lat,
|
|
longitude: lon,
|
|
accuracy: accuracy,
|
|
altitude: altitude,
|
|
timestamp: new Date().toISOString()
|
|
}))
|
|
};
|
|
|
|
// Build FormData
|
|
const formData = new FormData();
|
|
formData.append('manifest', JSON.stringify(manifest));
|
|
|
|
selectedFiles.forEach(file => {
|
|
formData.append('files', file);
|
|
});
|
|
|
|
// Upload
|
|
updateProgress(0, 'Uploading files...');
|
|
|
|
const response = await fetch('/api/v1/captures/upload', {
|
|
method: 'POST',
|
|
body: formData
|
|
});
|
|
|
|
updateProgress(100, 'Processing...');
|
|
|
|
if (!response.ok) {
|
|
const error = await response.json();
|
|
throw new Error(error.detail || 'Upload failed');
|
|
}
|
|
|
|
const result = await response.json();
|
|
|
|
// Show results
|
|
showUploadResults(result);
|
|
|
|
} catch (error) {
|
|
console.error('Upload error:', error);
|
|
alert(`Upload failed: ${error.message}`);
|
|
|
|
// Reset
|
|
document.getElementById('upload-progress').style.display = 'none';
|
|
document.getElementById('file-list').style.display = 'block';
|
|
}
|
|
}
|
|
|
|
function updateProgress(percent, text) {
|
|
document.getElementById('progress-fill').style.width = `${percent}%`;
|
|
document.getElementById('progress-text').textContent = text;
|
|
}
|
|
|
|
function showUploadResults(result) {
|
|
// Hide progress
|
|
document.getElementById('upload-progress').style.display = 'none';
|
|
|
|
// Show results
|
|
const resultsDiv = document.getElementById('upload-results');
|
|
const container = document.getElementById('results-container');
|
|
|
|
resultsDiv.style.display = 'block';
|
|
|
|
const successCount = result.successful?.length || 0;
|
|
const errorCount = result.failed?.length || 0;
|
|
|
|
let html = `
|
|
<div class="result-summary">
|
|
<p>Successfully uploaded: <strong>${successCount}</strong> files</p>
|
|
${errorCount > 0 ? `<p>Failed: <strong>${errorCount}</strong> files</p>` : ''}
|
|
</div>
|
|
`;
|
|
|
|
if (result.successful && result.successful.length > 0) {
|
|
html += '<h4>Successful Uploads:</h4>';
|
|
result.successful.forEach(item => {
|
|
html += `
|
|
<div class="result-item">
|
|
<div class="file-name">${item.filename}</div>
|
|
<div class="file-meta">
|
|
Frequency: ${(item.frequency / 1e6).toFixed(2)} MHz |
|
|
Protocol: ${item.protocol || 'RAW'}
|
|
</div>
|
|
</div>
|
|
`;
|
|
});
|
|
}
|
|
|
|
if (result.failed && result.failed.length > 0) {
|
|
html += '<h4>Failed Uploads:</h4>';
|
|
result.failed.forEach(item => {
|
|
html += `
|
|
<div class="result-item error">
|
|
<div class="file-name">${item.filename}</div>
|
|
<div class="file-meta">Error: ${item.error}</div>
|
|
</div>
|
|
`;
|
|
});
|
|
}
|
|
|
|
container.innerHTML = html;
|
|
}
|
|
|
|
function resetUpload() {
|
|
// Hide results
|
|
document.getElementById('upload-results').style.display = 'none';
|
|
|
|
// Clear files
|
|
clearFiles();
|
|
|
|
// Reset form
|
|
document.getElementById('default-lat').value = '';
|
|
document.getElementById('default-lon').value = '';
|
|
document.getElementById('session-uuid').value = '';
|
|
}
|
|
|
|
function useCurrentLocation() {
|
|
if (!navigator.geolocation) {
|
|
alert('Geolocation is not supported by your browser');
|
|
return;
|
|
}
|
|
|
|
navigator.geolocation.getCurrentPosition(
|
|
(position) => {
|
|
document.getElementById('default-lat').value = position.coords.latitude.toFixed(6);
|
|
document.getElementById('default-lon').value = position.coords.longitude.toFixed(6);
|
|
document.getElementById('default-accuracy').value = position.coords.accuracy.toFixed(1);
|
|
|
|
if (position.coords.altitude) {
|
|
document.getElementById('default-altitude').value = position.coords.altitude.toFixed(1);
|
|
}
|
|
},
|
|
(error) => {
|
|
alert(`Geolocation error: ${error.message}`);
|
|
}
|
|
);
|
|
}
|
|
|
|
function generateUUID() {
|
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
|
const r = Math.random() * 16 | 0;
|
|
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
|
return v.toString(16);
|
|
});
|
|
}
|
|
|
|
function formatFileSize(bytes) {
|
|
if (bytes < 1024) return bytes + ' B';
|
|
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
|
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
|
}
|