feat: add manual photo proof capture, consensus voting, and anti-sabotage camera verification engine

This commit is contained in:
2026-09-12 09:06:15 -07:00
parent b000c7dda6
commit ad6b99260e
4 changed files with 1126 additions and 22 deletions
+214 -8
View File
@@ -8,6 +8,8 @@
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 = {
@@ -19,16 +21,21 @@
vehicle: 'Sedan (Silver)',
soundEnabled: true,
alertsMuted: false,
reportsCount: 0
reportsCount: 0,
photosCount: 0
};
// State
let currentUser = null;
let communityReports = [];
let cameraMetadata = {};
let userVotes = { confirmed: {}, contested: {} };
function init() {
loadUser();
loadReports();
loadMetadata();
loadVotes();
}
function loadUser() {
@@ -53,17 +60,53 @@
}
}
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: 250,
points: (currentUser ? currentUser.points : 0) + 100,
vehicle: vehicle || 'Standard Vehicle',
soundEnabled: soundEnabled !== false,
alertsMuted: false,
reportsCount: communityReports.length
reportsCount: communityReports.length,
photosCount: currentUser ? (currentUser.photosCount || 0) : 0
};
saveUser();
return currentUser;
@@ -97,30 +140,186 @@
}
function addReport(report) {
const reportId = 'rpt-' + Date.now();
const newReport = {
id: 'rpt-' + Date.now(),
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.username : 'Anonymous Scout',
reportedBy: currentUser ? (currentUser.callsign || currentUser.username) : 'Anonymous Scout',
timestamp: new Date().toISOString(),
confirmations: 1
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) + 50;
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
cameraMetadata[camId] = {
confirmations: 3, // Baseline community confirmations for directory portals
hasPhoto: false,
photoUrl: null,
photoDate: null,
photoAuthor: null,
isContested: false,
contests: []
};
}
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;
@@ -139,7 +338,14 @@
logout,
saveUser,
getReports,
addReport
addReport,
getCameraMeta,
confirmCamera,
addPhotoProof,
contestCamera,
hasUserConfirmed,
hasUserContested,
compressImage
};
init();
+406 -14
View File
@@ -95,6 +95,12 @@
let radarRangeMode = 'city'; // 'city' (1,000 ft) or 'highway' (2,500 ft)
let lastCautionSpokenCamId = null;
// Photo Evidence & Consensus State
let activeModalCamera = null;
let photoUploadTarget = 'detail'; // 'detail' | 'report' | 'contest'
let reportPhotoPendingDataUrl = null;
let contestPhotoPendingDataUrl = null;
// --- DOM REFERENCES ---
const el = {
radarView: document.getElementById('radar-view'),
@@ -152,7 +158,47 @@
reportForm: document.getElementById('report-form'),
loginCloseBtn: document.getElementById('login-close-btn'),
reportCloseBtn: document.getElementById('report-close-btn'),
loginGuestBtn: document.getElementById('login-guest-btn')
loginGuestBtn: document.getElementById('login-guest-btn'),
// Hardware Proof & Verification Modals
cameraDetailModal: document.getElementById('camera-detail-modal'),
camDetailTitle: document.getElementById('cam-detail-title'),
camDetailVerifyBadge: document.getElementById('cam-detail-verify-badge'),
camDetailVerifyText: document.getElementById('cam-detail-verify-text'),
camDetailMeta: document.getElementById('cam-detail-meta'),
camProofContainer: document.getElementById('cam-proof-container'),
specMountType: document.getElementById('spec-mount-type'),
specAgency: document.getElementById('spec-agency'),
specCaptures: document.getElementById('spec-captures'),
specCoords: document.getElementById('spec-coords'),
btnVoteConfirmCam: document.getElementById('btn-vote-confirm-cam'),
btnVoteConfirmText: document.getElementById('btn-vote-confirm-text'),
btnProofAddPhoto: document.getElementById('btn-proof-add-photo'),
btnProofPhotoText: document.getElementById('btn-proof-photo-text'),
btnProofContest: document.getElementById('btn-proof-contest'),
btnProofNavigate: document.getElementById('btn-proof-navigate'),
btnProofOpenPortal: document.getElementById('btn-proof-open-portal'),
camDetailCloseBtn: document.getElementById('cam-detail-close-btn'),
// Contest Modal
contestModal: document.getElementById('contest-modal'),
contestCloseBtn: document.getElementById('contest-close-btn'),
contestForm: document.getElementById('contest-form'),
btnContestAddPhoto: document.getElementById('btn-contest-add-photo'),
contestPhotoBtnText: document.getElementById('contest-photo-btn-text'),
contestPhotoPreviewBox: document.getElementById('contest-photo-preview-box'),
contestPhotoPreviewImg: document.getElementById('contest-photo-preview-img'),
btnContestRemovePhoto: document.getElementById('btn-contest-remove-photo'),
// Report Form Photo Proof
btnReportAddPhoto: document.getElementById('btn-report-add-photo'),
reportPhotoBtnText: document.getElementById('report-photo-btn-text'),
reportPhotoPreviewBox: document.getElementById('report-photo-preview-box'),
reportPhotoPreviewImg: document.getElementById('report-photo-preview-img'),
btnReportRemovePhoto: document.getElementById('btn-report-remove-photo'),
// Native Camera / File Input
cameraPhotoFileInput: document.getElementById('camera-photo-file-input')
};
// --- INITIALIZATION ---
@@ -294,20 +340,23 @@
cameraCircles = [];
allCameras.forEach(cam => {
const meta = window.FlockAuth ? window.FlockAuth.getCameraMeta(cam.id) : { confirmations: 1, hasPhoto: false, isContested: false };
const hasPhoto = meta.hasPhoto && !!meta.photoUrl;
// 1. Red Radar Recording Zone Circle (Translucent buffer)
const radarCircle = L.circle([cam.lat, cam.lng], {
radius: cam.radius,
color: '#ef4444',
color: meta.isContested ? '#f59e0b' : '#ef4444',
weight: 1.5,
opacity: 0.8,
fillColor: '#ef4444',
fillOpacity: 0.18,
fillColor: meta.isContested ? '#f59e0b' : '#ef4444',
fillOpacity: meta.isContested ? 0.12 : 0.18,
className: 'flock-radar-circle'
}).addTo(map);
// 2. Camera Center Node Marker
const iconHtml = `
<div class="camera-map-pin ${cam.isCommunity ? 'community-pin' : ''}">
<div class="camera-map-pin ${cam.isCommunity ? 'community-pin' : ''} ${hasPhoto ? 'has-photo-pin' : ''} ${meta.isContested ? 'is-contested-pin' : ''}">
<div class="cam-pulse-wave"></div>
<div class="cam-dot-center">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="#ffffff" stroke-width="2.5">
@@ -315,6 +364,7 @@
<circle cx="12" cy="13" r="4"></circle>
</svg>
</div>
${hasPhoto ? '<div class="pin-photo-dot" title="Photo Evidence Verified">📸</div>' : ''}
</div>
`;
@@ -328,26 +378,40 @@
const marker = L.marker([cam.lat, cam.lng], { icon: customIcon }).addTo(map);
// Popup details
const badgeText = meta.isContested
? '🟠 CONTESTED SIGHTING'
: (hasPhoto ? '🟢 PHOTO VERIFIED HARDWARE' : (cam.isCommunity ? 'COMMUNITY SIGHTING' : 'FLOCK RECORDED ZONE'));
const popupHtml = `
<div class="radar-popup-card">
<div class="popup-badge">${cam.isCommunity ? 'COMMUNITY REPORT' : 'FLOCK RECORDED ZONE'}</div>
<div class="popup-badge ${hasPhoto ? 'badge-verified-photo' : (meta.isContested ? 'badge-contested-tag' : '')}">${badgeText}</div>
<h4 class="popup-title">${escapeHtml(cam.name)}</h4>
<div class="popup-meta">
<span><strong>Agency:</strong> ${escapeHtml(cam.agency)}</span><br>
<span><strong>Cameras in Cluster:</strong> ${cam.cameras || 1}</span><br>
<span><strong>30d Activity:</strong> ${cam.captures30d || 'Active'}</span>
<span><strong>Consensus:</strong> 👍 ${meta.confirmations || 1} Confirmations</span><br>
<span><strong>Photo Proof:</strong> ${hasPhoto ? '🟢 Verified on Pole' : '⚠️ No Photo Yet (Needs Driver Sighting)'}</span>
${cam.notes ? `<br><span><em>${escapeHtml(cam.notes)}</em></span>` : ''}
</div>
${cam.slug ? `
<button class="popup-inspect-btn" onclick="window.inspectPortal('${escapeHtml(cam.slug)}')">
Inspect Agency Records & Sharing
<div style="display: flex; flex-direction: column; gap: 6px; margin-top: 10px;">
<button class="popup-inspect-btn" style="background: linear-gradient(135deg, #06b6d4, #0284c7); color: #fff; font-weight: 600;" onclick="window.openCameraHardwareProof('${escapeHtml(cam.id)}')">
📸 View Hardware Proof & Vote
</button>
` : ''}
${cam.slug ? `
<button class="popup-inspect-btn" onclick="window.inspectPortal('${escapeHtml(cam.slug)}')">
Inspect Agency Records & Sharing ↗
</button>
` : ''}
</div>
</div>
`;
marker.bindPopup(popupHtml);
// Clicking marker directly opens the full hardware proof modal
marker.on('click', () => {
// Leaflet will open popup, but user can also click proof button directly
});
cameraCircles.push(radarCircle);
cameraCircles.push(marker);
});
@@ -1261,6 +1325,218 @@
}
}
// --- SURVEILLANCE ZONE HARDWARE PROOF MODAL ---
function openCameraDetailModal(cam) {
if (!cam) return;
activeModalCamera = cam;
const meta = window.FlockAuth ? window.FlockAuth.getCameraMeta(cam.id) : { confirmations: 1, hasPhoto: false, isContested: false };
const hasPhoto = meta.hasPhoto && !!meta.photoUrl;
// Title & Agency
if (el.camDetailTitle) el.camDetailTitle.textContent = cam.name || 'Flock ALPR Surveillance Installation';
if (el.camDetailMeta) el.camDetailMeta.textContent = `${cam.agency || 'Law Enforcement'} • Monitored Highway Grid`;
// Verification Badge
if (el.camDetailVerifyBadge && el.camDetailVerifyText) {
if (meta.isContested) {
el.camDetailVerifyBadge.className = 'cam-verification-badge badge-contested';
el.camDetailVerifyText.textContent = `🟠 CONTESTED SIGHTING (${meta.contests ? meta.contests.length : 1} REPORTS)`;
} else if (hasPhoto) {
el.camDetailVerifyBadge.className = 'cam-verification-badge badge-verified';
el.camDetailVerifyText.textContent = `🟢 VERIFIED PHOTO PROOF • ${meta.confirmations || 1} CONFIRMATIONS`;
} else {
el.camDetailVerifyBadge.className = 'cam-verification-badge badge-unverified';
el.camDetailVerifyText.textContent = `🟡 UNVERIFIED PIN • NEEDS PHOTO PROOF (${meta.confirmations || 1} CONFIRMED)`;
}
}
// Photo Container
if (el.camProofContainer) {
if (hasPhoto) {
el.camProofContainer.innerHTML = `
<div class="proof-photo-box">
<img src="${meta.photoUrl}" class="proof-photo-img" alt="Flock camera mounted on pole" onclick="window.open('${meta.photoUrl}', '_blank')" title="Click to view full size">
<div class="proof-photo-overlay">
<div>
<span class="proof-photo-author">📸 Scout: ${escapeHtml(meta.photoAuthor || 'Active Driver')}</span><br>
<span class="proof-photo-date">Logged: ${new Date(meta.photoDate || Date.now()).toLocaleDateString([], { month: 'short', day: 'numeric', year: 'numeric', hour: '2-digit', minute: '2-digit' })}</span>
</div>
<span style="background: rgba(16,185,129,0.25); border: 1px solid #10b981; padding: 3px 8px; border-radius: 12px; font-size: 0.7rem; font-weight: 700; color: #34d399;">VERIFIED HARDWARE</span>
</div>
</div>
`;
if (el.btnProofPhotoText) el.btnProofPhotoText.textContent = 'Update Photo';
} else {
el.camProofContainer.innerHTML = `
<div class="proof-empty-box">
<div class="proof-empty-icon">📸</div>
<div class="proof-empty-title">No Physical Hardware Photo Yet</div>
<div class="proof-empty-desc">
Flock cameras stay permanently bolted to utility poles and traffic signals with small solar panels. Snap a photo of the pole to lock this camera into the permanent verified registry.
</div>
<button type="button" class="btn-proof-snap-cta" onclick="window.triggerPhotoCaptureForActiveCam()">
<span>📸</span>
<span>Snap Camera on Pole (+100 pts)</span>
</button>
</div>
`;
if (el.btnProofPhotoText) el.btnProofPhotoText.textContent = 'Add Photo Proof';
}
}
// Specs
if (el.specMountType) el.specMountType.textContent = cam.type || 'Solar Pole Mount (Falcon)';
if (el.specAgency) el.specAgency.textContent = cam.agency || 'Local Law Enforcement';
if (el.specCaptures) el.specCaptures.textContent = cam.captures30d ? `${cam.captures30d} Reads` : 'Active Corridor';
if (el.specCoords) el.specCoords.textContent = `${cam.lat.toFixed(4)}, ${cam.lng.toFixed(4)}`;
// Confirm button state
const userConfirmed = window.FlockAuth ? window.FlockAuth.hasUserConfirmed(cam.id) : false;
if (el.btnVoteConfirmCam && el.btnVoteConfirmText) {
if (userConfirmed) {
el.btnVoteConfirmCam.classList.add('voted');
el.btnVoteConfirmText.textContent = `👍 Confirmed (${meta.confirmations || 1})`;
} else {
el.btnVoteConfirmCam.classList.remove('voted');
el.btnVoteConfirmText.textContent = `Confirm Active (${meta.confirmations || 1})`;
}
}
// Navigate link
if (el.btnProofNavigate) {
el.btnProofNavigate.href = `https://www.google.com/maps/dir/?api=1&destination=${cam.lat},${cam.lng}`;
}
// Transparency Portal Link
const portalBox = document.getElementById('proof-portal-link-box');
if (portalBox) {
if (cam.slug) {
portalBox.style.display = 'block';
if (el.btnProofOpenPortal) {
el.btnProofOpenPortal.onclick = () => {
closeCameraDetailModal();
window.inspectPortal(cam.slug);
};
}
} else {
portalBox.style.display = 'none';
}
}
// Show modal
if (el.cameraDetailModal) {
if (typeof el.cameraDetailModal.showModal === 'function') {
el.cameraDetailModal.showModal();
} else {
el.cameraDetailModal.setAttribute('open', '');
}
}
}
function closeCameraDetailModal() {
if (el.cameraDetailModal) {
if (typeof el.cameraDetailModal.close === 'function') {
el.cameraDetailModal.close();
} else {
el.cameraDetailModal.removeAttribute('open');
}
}
}
// --- CONTEST CAMERA MODAL ---
function openContestModal(cam) {
activeModalCamera = cam;
const reasonEl = document.getElementById('contest-reason');
if (reasonEl) reasonEl.selectedIndex = 0;
const notesEl = document.getElementById('contest-notes');
if (notesEl) notesEl.value = '';
contestPhotoPendingDataUrl = null;
if (el.contestPhotoPreviewBox) el.contestPhotoPreviewBox.classList.add('hidden');
if (el.contestPhotoBtnText) el.contestPhotoBtnText.textContent = 'Snap Empty Pole';
if (el.contestModal) {
if (typeof el.contestModal.showModal === 'function') {
el.contestModal.showModal();
} else {
el.contestModal.setAttribute('open', '');
}
}
}
function closeContestModal() {
if (el.contestModal) {
if (typeof el.contestModal.close === 'function') {
el.contestModal.close();
} else {
el.contestModal.removeAttribute('open');
}
}
}
// --- PHOTO CAPTURE TRIGGERS ---
window.triggerPhotoCaptureForActiveCam = function() {
photoUploadTarget = 'detail';
if (el.cameraPhotoFileInput) {
el.cameraPhotoFileInput.value = '';
el.cameraPhotoFileInput.click();
}
};
function triggerPhotoCaptureForReport() {
photoUploadTarget = 'report';
if (el.cameraPhotoFileInput) {
el.cameraPhotoFileInput.value = '';
el.cameraPhotoFileInput.click();
}
}
function triggerPhotoCaptureForContest() {
photoUploadTarget = 'contest';
if (el.cameraPhotoFileInput) {
el.cameraPhotoFileInput.value = '';
el.cameraPhotoFileInput.click();
}
}
async function handlePhotoFileSelected(e) {
const file = e.target.files && e.target.files[0];
if (!file) return;
try {
showToast("📸 Processing & compressing photo proof...");
const compressedDataUrl = await window.FlockAuth.compressImage(file, 1000, 0.72);
if (photoUploadTarget === 'detail' && activeModalCamera) {
window.FlockAuth.addPhotoProof(activeModalCamera.id, compressedDataUrl, 'Physical pole photo uploaded by scout');
openCameraDetailModal(activeModalCamera);
renderCameraZones();
playLogConfirmationChirp();
speakVoiceAlert("Photo proof verified and locked into radar database. +100 Scout Points awarded!");
showToast("📸 Photo proof verified & locked! +100 Scout Points!");
} else if (photoUploadTarget === 'report') {
reportPhotoPendingDataUrl = compressedDataUrl;
if (el.reportPhotoPreviewImg) el.reportPhotoPreviewImg.src = compressedDataUrl;
if (el.reportPhotoPreviewBox) el.reportPhotoPreviewBox.classList.remove('hidden');
if (el.reportPhotoBtnText) el.reportPhotoBtnText.textContent = 'Change Photo Proof';
showToast("📸 Photo proof attached to camera report!");
} else if (photoUploadTarget === 'contest') {
contestPhotoPendingDataUrl = compressedDataUrl;
if (el.contestPhotoPreviewImg) el.contestPhotoPreviewImg.src = compressedDataUrl;
if (el.contestPhotoPreviewBox) el.contestPhotoPreviewBox.classList.remove('hidden');
if (el.contestPhotoBtnText) el.contestPhotoBtnText.textContent = 'Change Empty Pole Photo';
showToast("📸 Empty pole photo attached to contest report!");
}
} catch (err) {
console.error('Photo processing error:', err);
alert('Could not process photo: ' + (err.message || 'Unknown error'));
}
}
window.openCameraHardwareProof = function(camId) {
const cam = allCameras.find(c => c.id === camId);
if (cam) openCameraDetailModal(cam);
};
// --- EVENT BINDINGS ---
function bindEvents() {
// Live GPS Continuous Tracking
@@ -1324,6 +1600,108 @@
if (el.btnSensCity) el.btnSensCity.addEventListener('click', () => setRadarSensitivity('city'));
if (el.btnSensHwy) el.btnSensHwy.addEventListener('click', () => setRadarSensitivity('highway'));
// Camera Hardware Proof & Voting Events
if (el.camDetailCloseBtn) el.camDetailCloseBtn.addEventListener('click', closeCameraDetailModal);
if (el.btnVoteConfirmCam) {
el.btnVoteConfirmCam.addEventListener('click', () => {
if (!activeModalCamera) return;
const res = window.FlockAuth.confirmCamera(activeModalCamera.id);
if (res.success) {
playLogConfirmationChirp();
speakVoiceAlert("Camera confirmed active on pole. +25 Scout Points!");
showToast(`👍 Confirmed active on pole! Total: ${res.count} confirmations.`);
openCameraDetailModal(activeModalCamera);
renderCameraZones();
updateProfileDisplay();
} else {
showToast(res.message || "Already confirmed!");
}
});
}
if (el.btnProofAddPhoto) {
el.btnProofAddPhoto.addEventListener('click', () => {
window.triggerPhotoCaptureForActiveCam();
});
}
if (el.btnProofContest) {
el.btnProofContest.addEventListener('click', () => {
if (!activeModalCamera) return;
closeCameraDetailModal();
openContestModal(activeModalCamera);
});
}
// Contest Modal Events
if (el.contestCloseBtn) el.contestCloseBtn.addEventListener('click', closeContestModal);
if (el.btnContestAddPhoto) el.btnContestAddPhoto.addEventListener('click', triggerPhotoCaptureForContest);
if (el.btnContestRemovePhoto) {
el.btnContestRemovePhoto.addEventListener('click', () => {
contestPhotoPendingDataUrl = null;
if (el.contestPhotoPreviewBox) el.contestPhotoPreviewBox.classList.add('hidden');
if (el.contestPhotoBtnText) el.contestPhotoBtnText.textContent = 'Snap Empty Pole';
});
}
if (el.contestForm) {
el.contestForm.addEventListener('submit', e => {
e.preventDefault();
if (!activeModalCamera) return;
const reason = document.getElementById('contest-reason').value;
const notes = document.getElementById('contest-notes').value.trim();
const res = window.FlockAuth.contestCamera(activeModalCamera.id, reason, notes);
closeContestModal();
if (res.success) {
if (res.isContested) {
speakVoiceAlert("Camera flagged as contested by community consensus.");
showToast("⚠️ Location marked Contested by community consensus.");
} else {
showToast(`⚠️ Contest submitted (${res.contestCount}/3 flags for consensus review).`);
}
renderCameraZones();
} else {
showToast(res.message || "Already reported!");
}
});
}
// Report Form Photo Proof Events
if (el.btnReportAddPhoto) el.btnReportAddPhoto.addEventListener('click', triggerPhotoCaptureForReport);
if (el.btnReportRemovePhoto) {
el.btnReportRemovePhoto.addEventListener('click', () => {
reportPhotoPendingDataUrl = null;
if (el.reportPhotoPreviewBox) el.reportPhotoPreviewBox.classList.add('hidden');
if (el.reportPhotoBtnText) el.reportPhotoBtnText.textContent = 'Snap Camera on Pole (+100 pts)';
});
}
// Native Camera / File Input Change Event
if (el.cameraPhotoFileInput) {
el.cameraPhotoFileInput.addEventListener('change', handlePhotoFileSelected);
}
// Nearest Camera HUD tap opens details
const hudLocationBar = document.querySelector('.hud-location-bar');
if (hudLocationBar) {
hudLocationBar.style.cursor = 'pointer';
hudLocationBar.title = 'Click to inspect nearest camera hardware proof & vote';
hudLocationBar.addEventListener('click', () => {
if (allCameras.length > 0) {
let closest = null;
let minDist = Infinity;
allCameras.forEach(cam => {
const d = getHaversineDistanceMeters(currentPosition.lat, currentPosition.lng, cam.lat, cam.lng);
if (d < minDist) { minDist = d; closest = cam; }
});
if (closest) openCameraDetailModal(closest);
}
});
}
// Mute toggle
el.btnMuteSound.addEventListener('click', () => {
const user = window.FlockAuth ? window.FlockAuth.getUser() : null;
@@ -1374,11 +1752,25 @@
const lng = parseFloat(document.getElementById('report-lng').value);
if (window.FlockAuth) {
const newRpt = window.FlockAuth.addReport({ lat, lng, type, intersection, notes });
const newRpt = window.FlockAuth.addReport({
lat,
lng,
type,
intersection,
notes,
photoUrl: reportPhotoPendingDataUrl
});
reportPhotoPendingDataUrl = null;
if (el.reportPhotoPreviewBox) el.reportPhotoPreviewBox.classList.add('hidden');
if (el.reportPhotoBtnText) el.reportPhotoBtnText.textContent = 'Snap Camera on Pole (+100 pts)';
loadAllCameraData();
renderCameraZones();
updateProfileDisplay();
alert(`Camera report added! +50 Scout Points. Zone is now active on radar.`);
playLogConfirmationChirp();
speakVoiceAlert("Camera sighting broadcast to radar network!");
showToast(`Camera report added! +${newRpt.photoUrl ? '150' : '50'} Scout Points.`);
}
closeReportModal();