feat: add California SB 34 PRA plate audit generator, pre-trip commute route scanner, background audio driving guard, corridor density heatmap, and open data export (v26)

This commit is contained in:
2026-09-12 18:25:36 -07:00
parent 6783a0b3ba
commit 671c908c46
4 changed files with 1130 additions and 20 deletions
+613 -1
View File
@@ -105,6 +105,159 @@
let reportPhotoPendingDataUrl = null;
let contestPhotoPendingDataUrl = null;
// --- V26 CIVIC UTILITY FEATURES STATE & CONSTANTS ---
let isCorridorHeatmapActive = false;
let heatmapLayerGroup = null;
let isBackgroundAudioActive = false;
let bgAudioElement = null;
let activeDetourLayerGroup = null;
const SURVEILLANCE_CORRIDORS = {
'palmdale-blvd': {
name: 'Palmdale Blvd Corridor (SR-14 to 50th St E)',
exposure: 'high',
exposureLabel: 'EXPOSURE: HIGH',
cams: 10,
summary: 'Heavy Flock ALPR density every 0.4 miles. Maximum surveillance exposure with real-time LASD hot-list scanning at 5th W, 10th W, 20th E, 30th E, and 47th E.',
detourTitle: '🟢 RECOMMENDED BYPASS: AVENUE R',
detourDesc: 'Take Avenue R eastbound instead — 0 Flock cameras detected, zero delay, connects directly from SR-14 to 50th St E.',
center: [34.5828, -118.0875],
zoom: 13,
corridorPath: [
[34.5828, -118.1525],
[34.5828, -118.1484],
[34.5828, -118.1360],
[34.5828, -118.1150],
[34.5828, -118.0900],
[34.5828, -118.0350]
],
bypassPath: [
[34.5700, -118.1525],
[34.5700, -118.0350]
]
},
'10th-st-west': {
name: '10th St West (Ave S to Ave L)',
exposure: 'high',
exposureLabel: 'EXPOSURE: HIGH',
cams: 8,
summary: 'Primary commercial arterial connecting Palmdale and Lancaster shopping centers. Monitored at Ave S, Rancho Vista Blvd, Technology Dr, Palmdale Blvd, Ave M, and Ave L.',
detourTitle: '🟢 RECOMMENDED BYPASS: 15TH ST WEST',
detourDesc: 'Use 15th St West for residential bypass or SR-14 Freeway for high-speed transit between Palmdale and Lancaster.',
center: [34.6325, -118.1470],
zoom: 12,
corridorPath: [
[34.5680, -118.1470],
[34.6019, -118.1470],
[34.6380, -118.1470],
[34.6970, -118.1470]
],
bypassPath: [
[34.5680, -118.1580],
[34.6019, -118.1580],
[34.6380, -118.1580],
[34.6970, -118.1580]
]
},
'sierra-hwy': {
name: 'Sierra Highway (Palmdale to Lancaster)',
exposure: 'medium',
exposureLabel: 'EXPOSURE: MODERATE',
cams: 5,
summary: 'Historical arterial monitored along railway crossings, Palmdale Blvd, Ave P, and Ave J.',
detourTitle: '🟢 RECOMMENDED BYPASS: DIVISION ST / 10TH ST EAST',
detourDesc: 'Division Street parallel run has minimal commercial surveillance and zero Flock pole hardware.',
center: [34.6325, -118.1250],
zoom: 12,
corridorPath: [
[34.5680, -118.1250],
[34.6019, -118.1250],
[34.6400, -118.1250],
[34.6970, -118.1250]
],
bypassPath: [
[34.5680, -118.1150],
[34.6019, -118.1150],
[34.6400, -118.1150],
[34.6970, -118.1150]
]
},
'avenue-s': {
name: 'Avenue S (Cross-Town Arterial)',
exposure: 'medium',
exposureLabel: 'EXPOSURE: MODERATE',
cams: 4,
summary: 'South Palmdale arterial monitored at 10th St W, 25th St E, and 47th St E.',
detourTitle: '🟢 RECOMMENDED BYPASS: AVENUE T / FORT TEJON RD',
detourDesc: 'Avenue T provides a scenic, camera-free southern cross-town connector with zero ALPR hardware.',
center: [34.5535, -118.1000],
zoom: 13,
corridorPath: [
[34.5535, -118.1700],
[34.5535, -118.0350]
],
bypassPath: [
[34.5420, -118.1700],
[34.5420, -118.0350]
]
},
'avenue-r-clean': {
name: 'Avenue R (Surveillance-Free Bypass)',
exposure: 'clean',
exposureLabel: 'EXPOSURE: CLEAN (0 CAMS)',
cams: 0,
summary: '🟢 Verified 0 detected Flock cameras. Optimal community-vetted clean civic route between West Palmdale and East Palmdale.',
detourTitle: '🟢 OPTIMAL DIRECT ROUTE',
detourDesc: 'No detour needed! You are currently viewing the designated surveillance-free bypass corridor.',
center: [34.5700, -118.0900],
zoom: 13,
corridorPath: [
[34.5700, -118.1700],
[34.5700, -118.0350]
],
bypassPath: null
}
};
const AGENCY_PRA_CONTACTS = {
palmdale: {
name: "City of Palmdale & Los Angeles County Sheriff's Department",
recordsDept: "Records & Public Information Officer / City Clerk",
email: "cityclerk@cityofpalmdale.org",
address: "38300 Sierra Hwy, Palmdale, CA 93550"
},
lancaster: {
name: "City of Lancaster & LASD Lancaster Station",
recordsDept: "City Clerk / Public Records Division",
email: "cityclerk@cityoflancasterca.gov",
address: "44933 Fern Ave, Lancaster, CA 93534"
},
bakersfield: {
name: "Bakersfield Police Department",
recordsDept: "BPD Records Management Division",
email: "bpdrecords@bakersfieldcity.us",
address: "1601 Trujillo Ave, Bakersfield, CA 93301"
},
chp: {
name: "California Highway Patrol (Headquarters)",
recordsDept: "Office of Legal Affairs / CPRA Unit",
email: "pra@chp.ca.gov",
address: "P.O. Box 942898, Sacramento, CA 94298"
},
riverside: {
name: "Riverside County Sheriff's Office",
recordsDept: "Sheriff's Public Records Coordinator",
email: "sheriffrecords@riversidesheriff.org",
address: "4095 Lemon St, Riverside, CA 92501"
},
other: {
name: "Public Law Enforcement Agency",
recordsDept: "Public Records Act Officer",
email: "records@agency.gov",
address: "Municipal Police Department / Sheriff Office"
}
};
// --- DOM REFERENCES ---
const el = {
radarView: document.getElementById('radar-view'),
@@ -227,7 +380,33 @@
btnCloseTools: document.getElementById('btn-close-tools'),
toolsOverlay: document.getElementById('tools-overlay'),
btnDesktopToolsMenu: document.getElementById('btn-desktop-tools-menu'),
controlsToolsMenu: document.getElementById('controls-tools-menu')
controlsToolsMenu: document.getElementById('controls-tools-menu'),
// Civic Utility Features (v26)
btnCorridorHeatmap: document.getElementById('btn-corridor-heatmap'),
routeCorridorSelect: document.getElementById('route-corridor-select'),
routeScanResult: document.getElementById('route-scan-result'),
scanScoreBadge: document.getElementById('scan-score-badge'),
scanCameraCount: document.getElementById('scan-camera-count'),
scanSummaryText: document.getElementById('scan-summary-text'),
scanDetourBox: document.getElementById('scan-detour-box'),
scanDetourText: document.getElementById('scan-detour-text'),
btnApplyRouteBypass: document.getElementById('btn-apply-route-bypass'),
btnOpenPraGenerator: document.getElementById('btn-open-pra-generator'),
btnToggleBgAudio: document.getElementById('btn-toggle-bg-audio'),
bgAudioLabel: document.getElementById('bg-audio-label'),
btnExportGeoJson: document.getElementById('btn-export-geojson'),
btnExportCsv: document.getElementById('btn-export-csv'),
praModal: document.getElementById('pra-modal'),
praModalCloseBtn: document.getElementById('pra-modal-close-btn'),
praPlateInput: document.getElementById('pra-plate-input'),
praStateInput: document.getElementById('pra-state-input'),
praAgencySelect: document.getElementById('pra-agency-select'),
praRequesterName: document.getElementById('pra-requester-name'),
btnGeneratePra: document.getElementById('btn-generate-pra'),
btnCopyPraLetter: document.getElementById('btn-copy-pra-letter'),
btnEmailPraLetter: document.getElementById('btn-email-pra-letter'),
praLetterText: document.getElementById('pra-letter-text')
};
// --- INITIALIZATION ---
@@ -1871,6 +2050,369 @@
}
}
// ========================================================================
// V26 CIVIC UTILITY FEATURES: SB 34 PRA, COMMUTE SCANNER, AUDIO GUARD, EXPORT
// ========================================================================
// --- SB 34 PUBLIC RECORDS ACT (CPRA) AUDIT GENERATOR ---
function openPraModal() {
if (!el.praModal) return;
generatePraLetter();
if (typeof el.praModal.showModal === 'function') {
el.praModal.showModal();
} else {
el.praModal.setAttribute('open', '');
}
}
function closePraModal() {
if (!el.praModal) return;
if (typeof el.praModal.close === 'function') {
el.praModal.close();
} else {
el.praModal.removeAttribute('open');
}
}
function generatePraLetter() {
const plate = (el.praPlateInput ? el.praPlateInput.value.trim().toUpperCase() : '') || '[INSERT PLATE #]';
const state = (el.praStateInput ? el.praStateInput.value.trim().toUpperCase() : 'CA') || 'CA';
const requester = (el.praRequesterName ? el.praRequesterName.value.trim() : '') || 'Registered Vehicle Owner / Concerned Citizen';
const agencyKey = el.praAgencySelect ? el.praAgencySelect.value : 'palmdale';
const agency = AGENCY_PRA_CONTACTS[agencyKey] || AGENCY_PRA_CONTACTS.palmdale;
const today = new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' });
const letter = `Date: ${today}
TO:
${agency.recordsDept}
${agency.name}
${agency.address}
Email: ${agency.email}
FROM:
${requester}
Registered Vehicle Owner / Citizen Requester
SUBJECT: PUBLIC RECORDS ACT DEMAND & SB 34 ALPR AUDIT TRAIL DISCLOSURE
STATUTORY AUTHORITIES:
- California Public Records Act (Gov. Code § 7920.000 et seq.)
- California Automated License Plate Recognition Privacy Act (SB 34 / Civ. Code § 1798.90.5 et seq.)
- California Constitution, Article I, Section 3(b)
Dear Records Custodian,
Pursuant to the California Public Records Act (Cal. Gov. Code § 7920 et seq.) and California Civil Code § 1798.90.52, I hereby formally request inspection and copies of all public records and automated license plate reader (ALPR) data logs pertaining to the following vehicle:
VEHICLE LICENSE PLATE: ${plate}
REGISTRATION JURISDICTION: ${state}
TIME PERIOD: All records within the maximum statutory or contractual retention window (including but not limited to the past 30 to 365 days).
SPECIFIC RECORDS REQUESTED:
1. DETECTIONS & SCANS: All automated license plate reader (ALPR) records, timestamps, GPS coordinates, optical character recognition reads, and contextual camera photographs captured by any fixed pole-mounted or mobile camera operated, contracted, or leased by ${agency.name} (including all cameras manufactured by Flock Group Inc. / Flock Safety) containing the license plate ${plate}.
2. SYSTEM SEARCH & AUDIT LOGS: Under Cal. Civil Code § 1798.90.52, an ALPR operator MUST maintain a record of all system queries. Please produce the complete, unredacted audit trail and query log indicating:
(a) The date and time of every query or hot-list search performed for license plate ${plate};
(b) The name, agency, and badge/operator ID of each individual who initiated the search or query;
(c) The specific, articulated law enforcement purpose or case number entered by the operator for each search.
3. DATA SHARING & THIRD-PARTY TRANSFERS: Under California Senate Bill 34 (Civ. Code § 1798.90.55), public agencies are strictly prohibited from selling, sharing, or transferring ALPR data to unauthorized, out-of-state, or federal entities. Please provide all records showing whether reads of plate ${plate} have been shared with, accessed by, or transmitted to any external law enforcement agency, regional intelligence center, federal agency, or out-of-state entity.
4. ALPR USAGE & PRIVACY POLICY: The current, adopted ALPR usage and privacy policy required under Cal. Civil Code § 1798.90.51, including confirmation of public posting and date of last compliance review.
STATUTORY TIMELINE:
Under California Government Code § 7922.535, you have ten (10) calendar days from receipt of this request to notify me whether this request seeks copies of disclosable public records. If any portion of this request is denied, please cite the specific statutory exemption justifying the withholding and provide all non-exempt portions.
Please provide all responsive records in electronic format (CSV, PDF, or spreadsheet) to my contact email or mailing address.
Respectfully submitted,
${requester}
Vehicle Owner / CPRA Requester
Generated via FlockRadar Civic Transparency Network (flockradar.org)`;
if (el.praLetterText) {
el.praLetterText.value = letter;
}
if (el.btnEmailPraLetter) {
const mailSubject = encodeURIComponent(`CPRA Demand & SB 34 ALPR Audit Request: Plate ${plate}`);
const mailBody = encodeURIComponent(letter);
el.btnEmailPraLetter.href = `mailto:${agency.email}?subject=${mailSubject}&body=${mailBody}`;
}
return letter;
}
// --- PRE-TRIP ROUTE SCANNER LOGIC ---
function handleRouteCorridorChange() {
if (!el.routeCorridorSelect || !el.routeScanResult) return;
const corridorId = el.routeCorridorSelect.value;
if (!corridorId || !SURVEILLANCE_CORRIDORS[corridorId]) {
el.routeScanResult.classList.add('hidden');
if (activeDetourLayerGroup && map && map.hasLayer(activeDetourLayerGroup)) {
map.removeLayer(activeDetourLayerGroup);
}
return;
}
const c = SURVEILLANCE_CORRIDORS[corridorId];
el.routeScanResult.classList.remove('hidden');
if (el.scanScoreBadge) {
el.scanScoreBadge.className = `scan-score-badge exposure-${c.exposure}`;
el.scanScoreBadge.textContent = c.exposureLabel;
}
if (el.scanCameraCount) {
el.scanCameraCount.textContent = `${c.cams} Cameras Detected`;
}
if (el.scanSummaryText) {
el.scanSummaryText.textContent = c.summary;
}
if (el.scanDetourBox && el.scanDetourText) {
el.scanDetourBox.style.display = 'flex';
el.scanDetourText.textContent = c.detourDesc;
}
if (el.btnApplyRouteBypass) {
el.btnApplyRouteBypass.textContent = c.bypassPath ? 'Show Clean Bypass on Map' : 'Center Clean Corridor on Map';
}
}
function applyRouteBypassDetour() {
if (!el.routeCorridorSelect || !map) return;
const corridorId = el.routeCorridorSelect.value;
const c = SURVEILLANCE_CORRIDORS[corridorId];
if (!c) return;
if (activeDetourLayerGroup) {
map.removeLayer(activeDetourLayerGroup);
}
activeDetourLayerGroup = L.layerGroup();
// Red or amber surveillance corridor line
const color = c.exposure === 'high' ? '#ef4444' : (c.exposure === 'medium' ? '#f59e0b' : '#10b981');
const corridorLine = L.polyline(c.corridorPath, {
color: color,
weight: 7,
opacity: 0.9,
lineCap: 'round',
lineJoin: 'round',
className: c.exposure === 'high' ? 'leaflet-corridor-high' : ''
});
corridorLine.bindPopup(`<strong>${c.name}</strong><br>${c.exposureLabel}<br>${c.cams} Cameras Monitored`);
activeDetourLayerGroup.addLayer(corridorLine);
// Green clean bypass line
if (c.bypassPath) {
const bypassLine = L.polyline(c.bypassPath, {
color: '#10b981',
weight: 6,
opacity: 0.95,
lineCap: 'round',
lineJoin: 'round'
});
bypassLine.bindPopup(`<strong>${c.detourTitle}</strong><br>${c.detourDesc}`);
activeDetourLayerGroup.addLayer(bypassLine);
}
activeDetourLayerGroup.addTo(map);
map.flyTo(c.center, c.zoom, { duration: 1.2 });
// Close tools drawer on mobile
if (window.innerWidth <= 900 && el.mapTopControls) {
el.mapTopControls.classList.remove('open-drawer');
if (el.toolsOverlay) el.toolsOverlay.classList.remove('visible');
}
showToast(`🟢 Route Bypass Map updated for ${c.name}`);
}
// --- BACKGROUND AUDIO DRIVING GUARD ---
function toggleBackgroundAudio() {
if (!isBackgroundAudioActive) {
if (!bgAudioElement) {
bgAudioElement = new Audio();
// 1-second inaudible 8kHz silent WAV loop
bgAudioElement.src = 'data:audio/wav;base64,UklGRigAAABXQVZFZm10IBIAAAABAAEARKwAAIhYAQACABAAAABkYXRhAgAAAAEA';
bgAudioElement.loop = true;
}
bgAudioElement.play().then(() => {
isBackgroundAudioActive = true;
if (el.bgAudioLabel) el.bgAudioLabel.textContent = 'Background Audio Guard: ON';
if (el.btnToggleBgAudio) el.btnToggleBgAudio.classList.add('btn-bg-audio-active');
if ('mediaSession' in navigator) {
navigator.mediaSession.metadata = new MediaMetadata({
title: 'FlockRadar Driving Guard: Active',
artist: 'FlockRadar Civic Radar',
album: 'Continuous ALPR Audio Alerts Active'
});
navigator.mediaSession.playbackState = 'playing';
}
requestWakeLock();
speakVoiceAlert("Background driving audio guard engaged. Audible radar alerts active over navigation.");
showToast("🟢 Background Audio Guard ON: Radar alerts active over Apple/Google Maps");
}).catch(err => {
console.warn("Background audio start error:", err);
showToast("Audio permission required to start background guard.");
});
} else {
if (bgAudioElement) {
bgAudioElement.pause();
}
isBackgroundAudioActive = false;
if (el.bgAudioLabel) el.bgAudioLabel.textContent = 'Background Audio Guard: OFF';
if (el.btnToggleBgAudio) el.btnToggleBgAudio.classList.remove('btn-bg-audio-active');
if ('mediaSession' in navigator) {
navigator.mediaSession.playbackState = 'none';
}
showToast("Background Audio Guard disengaged");
}
}
// --- CORRIDOR SURVEILLANCE HEATMAP & DENSITY OVERLAY ---
function toggleCorridorHeatmap() {
if (!map) return;
isCorridorHeatmapActive = !isCorridorHeatmapActive;
if (isCorridorHeatmapActive) {
if (!heatmapLayerGroup) {
heatmapLayerGroup = L.layerGroup();
Object.keys(SURVEILLANCE_CORRIDORS).forEach(key => {
const c = SURVEILLANCE_CORRIDORS[key];
const color = c.exposure === 'high' ? '#ef4444' : (c.exposure === 'medium' ? '#f59e0b' : '#10b981');
const weight = c.exposure === 'high' ? 8 : (c.exposure === 'medium' ? 6 : 5);
const opacity = 0.85;
const polyline = L.polyline(c.corridorPath, {
color: color,
weight: weight,
opacity: opacity,
lineCap: 'round',
lineJoin: 'round',
className: c.exposure === 'high' ? 'leaflet-corridor-high' : ''
});
polyline.bindPopup(`
<div style="font-family: inherit; font-size: 0.82rem; color: #0f172a; padding: 4px;">
<strong style="color: ${color}; font-size: 0.9rem;">${c.name}</strong><br>
<div style="margin-top: 4px; font-weight: 600;">Status: ${c.exposureLabel}</div>
<div style="margin-top: 2px; color: #475569;">${c.cams} Flock Cameras Monitored</div>
<p style="margin: 6px 0 0; font-size: 0.76rem; color: #334155;">${c.summary}</p>
</div>
`);
heatmapLayerGroup.addLayer(polyline);
if (c.bypassPath) {
const bypassLine = L.polyline(c.bypassPath, {
color: '#10b981',
weight: 4,
opacity: 0.7,
dashArray: '5, 8'
});
bypassLine.bindPopup(`
<div style="font-family: inherit; font-size: 0.82rem; color: #0f172a; padding: 4px;">
<strong style="color: #10b981;">Clean Bypass Corridor</strong><br>
<div style="margin-top: 4px; color: #334155;">${c.detourDesc}</div>
</div>
`);
heatmapLayerGroup.addLayer(bypassLine);
}
});
}
heatmapLayerGroup.addTo(map);
if (el.btnCorridorHeatmap) el.btnCorridorHeatmap.classList.add('active');
showToast("🔥 Surveillance Corridor Density Heatmap: ON");
} else {
if (heatmapLayerGroup && map.hasLayer(heatmapLayerGroup)) {
map.removeLayer(heatmapLayerGroup);
}
if (el.btnCorridorHeatmap) el.btnCorridorHeatmap.classList.remove('active');
showToast("Corridor Density Heatmap: OFF");
}
}
// --- OPEN CIVIC DATA EXPORT (GEOJSON & CSV) ---
function exportCameraData(format) {
if (!allCameras || allCameras.length === 0) {
showToast("No camera data loaded to export!");
return;
}
if (format === 'geojson') {
const geojson = {
type: "FeatureCollection",
metadata: {
title: "FlockRadar Verified Camera Database",
generated: new Date().toISOString(),
jurisdiction: "Antelope Valley, California",
totalCameras: allCameras.length,
license: "Open Civic Data / Public Records Act"
},
features: allCameras.map(cam => ({
type: "Feature",
geometry: {
type: "Point",
coordinates: [cam.lng, cam.lat]
},
properties: {
id: cam.id,
name: cam.name || cam.crossStreets || "Flock Safety ALPR",
crossStreets: cam.crossStreets || "",
city: cam.city || "Palmdale",
state: cam.state || "CA",
agency: cam.agency || "City of Palmdale / LASD",
mountType: cam.mountType || "Pole Mounted Solar Falcon",
status: cam.isContested ? "Contested" : (cam.isCommunity ? "Community Reported" : "Verified Active"),
confirmations: cam.confirmedCount || 1,
captures30d: cam.captures30d || "N/A",
latitude: cam.lat,
longitude: cam.lng
}
}))
};
const blob = new Blob([JSON.stringify(geojson, null, 2)], { type: 'application/geo+json' });
downloadBlob(blob, `flockradar_cameras_${new Date().toISOString().slice(0, 10)}.geojson`);
showToast(`Exported ${allCameras.length} cameras as GeoJSON!`);
} else if (format === 'csv') {
const headers = ["Camera_ID", "Location_Name", "Agency", "City", "State", "Latitude", "Longitude", "Mount_Type", "Status", "Confirmations"];
const rows = allCameras.map(cam => [
`"${(cam.id || '').replace(/"/g, '""')}"`,
`"${(cam.name || cam.crossStreets || '').replace(/"/g, '""')}"`,
`"${(cam.agency || '').replace(/"/g, '""')}"`,
`"${(cam.city || 'Palmdale').replace(/"/g, '""')}"`,
`"${(cam.state || 'CA').replace(/"/g, '""')}"`,
cam.lat,
cam.lng,
`"${(cam.mountType || 'Pole Mounted').replace(/"/g, '""')}"`,
`"${(cam.isContested ? 'Contested' : 'Verified Active').replace(/"/g, '""')}"`,
cam.confirmedCount || 1
]);
const csvContent = [headers.join(','), ...rows.map(r => r.join(','))].join('\r\n');
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
downloadBlob(blob, `flockradar_cameras_${new Date().toISOString().slice(0, 10)}.csv`);
showToast(`Exported ${allCameras.length} cameras as CSV spreadsheet!`);
}
}
function downloadBlob(blob, filename) {
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = url;
link.download = filename;
document.body.appendChild(link);
link.click();
setTimeout(() => {
document.body.removeChild(link);
URL.revokeObjectURL(url);
}, 100);
}
// --- SURVEILLANCE ZONE HARDWARE PROOF MODAL ---
function openCameraDetailModal(cam) {
if (!cam) return;
@@ -2798,6 +3340,76 @@
openCameraHardwareProof(camId);
}, 150);
};
// --- V26 CIVIC UTILITY EVENT BINDINGS ---
// Corridor Heatmap Toggle
if (el.btnCorridorHeatmap) {
el.btnCorridorHeatmap.addEventListener('click', toggleCorridorHeatmap);
}
// Pre-Trip Route Scanner
if (el.routeCorridorSelect) {
el.routeCorridorSelect.addEventListener('change', handleRouteCorridorChange);
}
if (el.btnApplyRouteBypass) {
el.btnApplyRouteBypass.addEventListener('click', applyRouteBypassDetour);
}
// Background Audio Guard
if (el.btnToggleBgAudio) {
el.btnToggleBgAudio.addEventListener('click', toggleBackgroundAudio);
}
// California SB 34 PRA Request Generator
if (el.btnOpenPraGenerator) {
el.btnOpenPraGenerator.addEventListener('click', openPraModal);
}
if (el.praModalCloseBtn) {
el.praModalCloseBtn.addEventListener('click', closePraModal);
}
if (el.praModal) {
el.praModal.addEventListener('click', e => {
if (e.target === el.praModal) closePraModal();
});
el.praModal.addEventListener('cancel', (e) => {
e.preventDefault();
closePraModal();
});
}
if (el.btnGeneratePra) el.btnGeneratePra.addEventListener('click', generatePraLetter);
if (el.praPlateInput) el.praPlateInput.addEventListener('input', generatePraLetter);
if (el.praStateInput) el.praStateInput.addEventListener('input', generatePraLetter);
if (el.praAgencySelect) el.praAgencySelect.addEventListener('change', generatePraLetter);
if (el.praRequesterName) el.praRequesterName.addEventListener('input', generatePraLetter);
if (el.btnCopyPraLetter) {
el.btnCopyPraLetter.addEventListener('click', () => {
const text = el.praLetterText ? el.praLetterText.value : '';
if (text) {
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(text).then(() => {
playLogConfirmationChirp();
showToast("📋 Formal CPRA Demand Letter copied to clipboard!");
}).catch(() => {
showToast("Letter copied!");
});
} else if (el.praLetterText) {
el.praLetterText.select();
document.execCommand('copy');
showToast("📋 Letter copied to clipboard!");
}
}
});
}
// Open Civic Data Export (GeoJSON & CSV)
if (el.btnExportGeoJson) {
el.btnExportGeoJson.addEventListener('click', () => exportCameraData('geojson'));
}
if (el.btnExportCsv) {
el.btnExportCsv.addEventListener('click', () => exportCameraData('csv'));
}
}
function escapeHtml(str) {