/** * FLOCKRADAR - USER AUTHENTICATION & COMMUNITY STATE MANAGER * Waze-style driver profile, preferences, and crowdsourced reporting store. */ (function() { 'use strict'; const STORAGE_KEY_USER = 'flock_radar_user'; const STORAGE_KEY_REPORTS = 'flock_community_reports'; const STORAGE_KEY_METADATA = 'flock_camera_metadata'; const STORAGE_KEY_VOTES = 'flock_user_votes'; // Default Guest Profile const defaultUser = { isLoggedIn: false, username: 'Guest Driver', callsign: 'SCOUT-104', level: 1, points: 120, vehicle: 'Sedan (Silver)', soundEnabled: true, alertsMuted: false, reportsCount: 0, photosCount: 0 }; // State let currentUser = null; let communityReports = []; let cameraMetadata = {}; let userVotes = { confirmed: {}, contested: {} }; function init() { loadUser(); loadReports(); loadMetadata(); loadVotes(); } function loadUser() { try { const stored = localStorage.getItem(STORAGE_KEY_USER); if (stored) { currentUser = JSON.parse(stored); } else { currentUser = { ...defaultUser }; } } catch (e) { currentUser = { ...defaultUser }; } } function saveUser() { try { localStorage.setItem(STORAGE_KEY_USER, JSON.stringify(currentUser)); window.dispatchEvent(new CustomEvent('flock_user_updated', { detail: currentUser })); } catch (e) { console.error('Failed to save user session', e); } } function loadVotes() { try { const stored = localStorage.getItem(STORAGE_KEY_VOTES); userVotes = stored ? JSON.parse(stored) : { confirmed: {}, contested: {} }; } catch (e) { userVotes = { confirmed: {}, contested: {} }; } } function saveVotes() { try { localStorage.setItem(STORAGE_KEY_VOTES, JSON.stringify(userVotes)); } catch (e) { console.error('Failed to save votes', e); } } function loadMetadata() { try { const stored = localStorage.getItem(STORAGE_KEY_METADATA); cameraMetadata = stored ? JSON.parse(stored) : {}; } catch (e) { cameraMetadata = {}; } } function saveMetadata() { try { localStorage.setItem(STORAGE_KEY_METADATA, JSON.stringify(cameraMetadata)); window.dispatchEvent(new CustomEvent('flock_meta_updated', { detail: cameraMetadata })); } catch (e) { console.error('Failed to save camera metadata', e); } } function login(username, vehicle, soundEnabled) { currentUser = { isLoggedIn: true, username: username || 'Citizen Scout', callsign: 'RADAR-' + Math.floor(100 + Math.random() * 900), level: 2, points: (currentUser ? currentUser.points : 0) + 100, vehicle: vehicle || 'Standard Vehicle', soundEnabled: soundEnabled !== false, alertsMuted: false, reportsCount: communityReports.length, photosCount: currentUser ? (currentUser.photosCount || 0) : 0 }; saveUser(); return currentUser; } function logout() { currentUser = { ...defaultUser }; saveUser(); } function loadReports() { try { const stored = localStorage.getItem(STORAGE_KEY_REPORTS); if (stored) { communityReports = JSON.parse(stored); // Automatically prune any unverified test marks or pins without photo proof const beforeCount = communityReports.length; communityReports = communityReports.filter(r => r && r.photoUrl && !r.id.startsWith('usr-pin-') && r.notes !== '1-Tap Mobile Driver Pin' && r.type !== 'Fixed ALPR Camera' ); if (communityReports.length !== beforeCount) { localStorage.setItem(STORAGE_KEY_REPORTS, JSON.stringify(communityReports)); } } else { communityReports = []; } } catch (e) { communityReports = []; } } function clearUnverifiedReports() { try { communityReports = communityReports.filter(r => r && r.photoUrl && !r.id.startsWith('usr-pin-') && r.notes !== '1-Tap Mobile Driver Pin' && r.type !== 'Fixed ALPR Camera' ); localStorage.setItem(STORAGE_KEY_REPORTS, JSON.stringify(communityReports)); Object.keys(cameraMetadata).forEach(id => { if (id.startsWith('usr-pin-') || !cameraMetadata[id].photoUrl) { delete cameraMetadata[id]; } }); localStorage.setItem(STORAGE_KEY_METADATA, JSON.stringify(cameraMetadata)); window.dispatchEvent(new CustomEvent('flock_reports_updated', { detail: communityReports })); } catch (e) { console.warn('Could not clear unverified reports', e); } } function removeReport(reportId) { communityReports = communityReports.filter(r => r.id !== reportId); if (cameraMetadata[reportId]) delete cameraMetadata[reportId]; localStorage.setItem(STORAGE_KEY_REPORTS, JSON.stringify(communityReports)); localStorage.setItem(STORAGE_KEY_METADATA, JSON.stringify(cameraMetadata)); window.dispatchEvent(new CustomEvent('flock_reports_updated', { detail: communityReports })); return true; } function clearAllPlacedReports() { communityReports = []; localStorage.setItem(STORAGE_KEY_REPORTS, JSON.stringify([])); // Clean up metadata for custom placed pins Object.keys(cameraMetadata).forEach(id => { if (id.startsWith('rpt-') || id.startsWith('usr-pin-') || id.startsWith('test-') || id.startsWith('cam-test-')) { delete cameraMetadata[id]; } }); localStorage.setItem(STORAGE_KEY_METADATA, JSON.stringify(cameraMetadata)); window.dispatchEvent(new CustomEvent('flock_reports_updated', { detail: [] })); return true; } function saveReports() { try { localStorage.setItem(STORAGE_KEY_REPORTS, JSON.stringify(communityReports)); window.dispatchEvent(new CustomEvent('flock_reports_updated', { detail: communityReports })); } catch (e) { console.error('Failed to save community reports', e); } } function addReport(report) { const reportId = 'rpt-' + Date.now(); const newReport = { id: reportId, lat: report.lat, lng: report.lng, type: report.type || 'Solar Pole Mount', intersection: report.intersection || 'Reported Location', notes: report.notes || 'Spotted by community driver', reportedBy: currentUser ? (currentUser.callsign || currentUser.username) : 'Anonymous Scout', timestamp: new Date().toISOString(), confirmations: 1, photoUrl: report.photoUrl || null }; communityReports.push(newReport); saveReports(); // If report has photo, initialize metadata if (report.photoUrl) { cameraMetadata[reportId] = { confirmations: 1, hasPhoto: true, photoUrl: report.photoUrl, photoDate: new Date().toISOString(), photoAuthor: currentUser ? (currentUser.callsign || currentUser.username) : 'Scout', isContested: false, contests: [] }; saveMetadata(); } if (currentUser) { currentUser.points = (currentUser.points || 0) + (report.photoUrl ? 150 : 50); currentUser.reportsCount = (currentUser.reportsCount || 0) + 1; if (report.photoUrl) { currentUser.photosCount = (currentUser.photosCount || 0) + 1; } saveUser(); } return newReport; } // --- CAMERA METADATA, CONFIRMATION & PHOTO PROOF API --- function getCameraMeta(camId) { if (!cameraMetadata[camId]) { // Default baseline const isVerifiedPalmdale = (camId === 'cam-plm-1' || camId === 'cam-plm-9'); cameraMetadata[camId] = { confirmations: isVerifiedPalmdale ? 18 : 3, // Baseline community confirmations for directory portals hasPhoto: isVerifiedPalmdale, photoUrl: isVerifiedPalmdale ? 'assets/img/flock_pole_cam_palmdale.jpg' : null, photoDate: isVerifiedPalmdale ? '2026-09-12' : null, photoAuthor: isVerifiedPalmdale ? 'Driver Scout #814' : null, isContested: false, contests: [] }; } else if ((camId === 'cam-plm-1' || camId === 'cam-plm-9') && !cameraMetadata[camId].photoUrl) { cameraMetadata[camId].hasPhoto = true; cameraMetadata[camId].photoUrl = 'assets/img/flock_pole_cam_palmdale.jpg'; cameraMetadata[camId].photoDate = '2026-09-12'; cameraMetadata[camId].photoAuthor = 'Driver Scout #814'; } return cameraMetadata[camId]; } function confirmCamera(camId) { if (userVotes.confirmed[camId]) { return { success: false, message: 'You have already confirmed this camera is active!' }; } const meta = getCameraMeta(camId); meta.confirmations = (meta.confirmations || 0) + 1; userVotes.confirmed[camId] = new Date().toISOString(); saveMetadata(); saveVotes(); if (currentUser) { currentUser.points = (currentUser.points || 0) + 25; saveUser(); } return { success: true, count: meta.confirmations }; } function addPhotoProof(camId, photoDataUrl, notes) { const meta = getCameraMeta(camId); meta.hasPhoto = true; meta.photoUrl = photoDataUrl; meta.photoDate = new Date().toISOString(); meta.photoAuthor = currentUser ? (currentUser.callsign || currentUser.username) : 'Driver Scout'; meta.confirmations = (meta.confirmations || 0) + 2; // Adding photo boosts verification confidence saveMetadata(); if (currentUser) { currentUser.points = (currentUser.points || 0) + 100; currentUser.photosCount = (currentUser.photosCount || 0) + 1; saveUser(); } return { success: true, meta }; } function contestCamera(camId, reason, notes) { if (userVotes.contested[camId]) { return { success: false, message: 'You have already submitted a report for this location.' }; } const meta = getCameraMeta(camId); if (!meta.contests) meta.contests = []; meta.contests.push({ reason: reason || 'Camera reported missing', notes: notes || '', author: currentUser ? (currentUser.callsign || currentUser.username) : 'Scout', timestamp: new Date().toISOString() }); userVotes.contested[camId] = new Date().toISOString(); // Anti-Sabotage Rule: It requires at least 3 distinct driver contests to flag a camera if (meta.contests.length >= 3) { meta.isContested = true; } saveMetadata(); saveVotes(); return { success: true, contestCount: meta.contests.length, isContested: meta.isContested }; } function hasUserConfirmed(camId) { return !!(userVotes.confirmed && userVotes.confirmed[camId]); } function hasUserContested(camId) { return !!(userVotes.contested && userVotes.contested[camId]); } // --- CLIENT-SIDE FAST IMAGE COMPRESSION (CANVAS) --- // Compresses 4MB-10MB phone camera photos down to ~60-80KB WebP/JPEG in milliseconds function compressImage(file, maxWidth = 1000, quality = 0.72) { return new Promise((resolve, reject) => { if (!file || !file.type.startsWith('image/')) { return reject(new Error('Invalid image file.')); } const reader = new FileReader(); reader.onload = e => { const img = new Image(); img.onload = () => { try { const canvas = document.createElement('canvas'); let width = img.width; let height = img.height; if (width > maxWidth) { height = Math.round((height * maxWidth) / width); width = maxWidth; } canvas.width = width; canvas.height = height; const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0, width, height); // Export as WebP if supported, fallback to JPEG let dataUrl = canvas.toDataURL('image/webp', quality); if (!dataUrl || dataUrl.indexOf('data:image/webp') !== 0) { dataUrl = canvas.toDataURL('image/jpeg', quality); } resolve(dataUrl); } catch (err) { reject(err); } }; img.onerror = () => reject(new Error('Failed to decode image data.')); img.src = e.target.result; }; reader.onerror = () => reject(new Error('Failed to read file.')); reader.readAsDataURL(file); }); } function getUser() { if (!currentUser) loadUser(); return currentUser; } function getReports() { if (!communityReports) loadReports(); return communityReports; } // Export API to global scope window.FlockAuth = { init, getUser, login, logout, saveUser, getReports, addReport, removeReport, clearAllPlacedReports, clearUnverifiedReports, getCameraMeta, confirmCamera, addPhotoProof, contestCamera, hasUserConfirmed, hasUserContested, compressImage }; init(); })();