feat(v27): directional FOV approach filter, dynamic ETA countdown, windshield mirror HUD, stealth AMOLED night mode, 1-tap driving sighting stamp, audio boost and steering haptics

This commit is contained in:
2026-09-12 19:01:06 -07:00
parent 671c908c46
commit 4f8c67e6f6
4 changed files with 862 additions and 128 deletions
+266 -17
View File
@@ -489,15 +489,15 @@ body {
/* HUD Metrics */
.hud-metrics-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
grid-template-columns: repeat(3, 1fr);
gap: 8px;
}
.hud-metric-box {
background: rgba(7, 9, 14, 0.6);
border: 1px solid var(--border-subtle);
border-radius: var(--radius-md);
padding: 12px;
padding: 10px 4px;
text-align: center;
}
@@ -2948,7 +2948,7 @@ body {
pointer-events: auto;
}
/* Compact Floating Action Button on Mobile */
/* Compact Floating Action Buttons on Mobile */
.driver-fab-container {
position: absolute;
bottom: max(18px, env(safe-area-inset-bottom));
@@ -2956,22 +2956,37 @@ body {
z-index: 500;
margin: 0;
pointer-events: auto;
display: flex;
flex-direction: row;
align-items: center;
gap: 8px;
}
.btn-driver-fab {
padding: 10px 16px;
padding: 10px 14px;
border-radius: 9999px;
gap: 8px;
box-shadow: 0 4px 16px rgba(239, 68, 68, 0.45);
gap: 6px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
}
.btn-fab-stamp {
background: linear-gradient(135deg, #0284c7, #0369a1);
border: 1.5px solid rgba(56, 189, 248, 0.5);
box-shadow: 0 4px 16px rgba(2, 132, 199, 0.45);
}
.btn-fab-photo {
background: linear-gradient(135deg, #ef4444, #dc2626);
border: 1.5px solid rgba(255, 255, 255, 0.3);
box-shadow: 0 4px 16px rgba(239, 68, 68, 0.45);
}
.fab-icon-glow {
font-size: 1.25rem;
font-size: 1.15rem;
}
.fab-main-title {
font-size: 0.84rem;
font-size: 0.82rem;
font-weight: 700;
}
@@ -3277,37 +3292,56 @@ body {
box-shadow: 0 0 8px rgba(56, 189, 248, 0.3);
}
/* 3. Giant 1-Tap "Log Flock Camera" Driver FAB */
/* 3. Floating Action Buttons: 1-Tap Sighting Stamp & Photo Log */
.driver-fab-container {
position: absolute;
bottom: 25px;
right: 25px;
z-index: 1100;
display: flex;
flex-direction: column;
gap: 10px;
align-items: flex-end;
}
.btn-driver-fab {
display: flex;
align-items: center;
gap: 12px;
background: linear-gradient(135deg, #ef4444 0%, #b91c1c 100%);
color: #ffffff;
border: 2px solid rgba(255, 255, 255, 0.4);
border-radius: 50px;
padding: 14px 22px;
font-size: 0.95rem;
padding: 12px 20px;
font-size: 0.92rem;
font-weight: 700;
letter-spacing: 0.5px;
cursor: pointer;
box-shadow: 0 6px 25px rgba(239, 68, 68, 0.55), 0 0 15px rgba(239, 68, 68, 0.4);
transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
user-select: none;
touch-action: manipulation;
}
.btn-driver-fab:hover {
.btn-fab-stamp {
background: linear-gradient(135deg, #0284c7 0%, #0369a1 100%);
border: 2px solid rgba(56, 189, 248, 0.6);
box-shadow: 0 6px 25px rgba(2, 132, 199, 0.5), 0 0 15px rgba(56, 189, 248, 0.35);
}
.btn-fab-stamp:hover {
transform: translateY(-2px) scale(1.02);
border-color: #38bdf8;
box-shadow: 0 8px 30px rgba(2, 132, 199, 0.7), 0 0 22px rgba(56, 189, 248, 0.5);
}
.btn-fab-photo {
background: linear-gradient(135deg, #ef4444 0%, #b91c1c 100%);
border: 2px solid rgba(255, 255, 255, 0.4);
box-shadow: 0 6px 25px rgba(239, 68, 68, 0.55), 0 0 15px rgba(239, 68, 68, 0.4);
}
.btn-fab-photo:hover {
transform: translateY(-2px) scale(1.02);
box-shadow: 0 8px 30px rgba(239, 68, 68, 0.75), 0 0 25px rgba(239, 68, 68, 0.5);
border-color: #ffffff;
box-shadow: 0 8px 30px rgba(239, 68, 68, 0.75), 0 0 25px rgba(239, 68, 68, 0.5);
}
.btn-driver-fab:active {
@@ -4689,3 +4723,218 @@ body {
grid-template-columns: 1fr;
}
}
/* ==========================================================================
v27 DRIVER-CENTRIC POLISH REFINEMENTS
1. Directional Approach Angle & FOV Badges
2. Time-to-Zone Intercept Dynamic Countdown
3. Windshield Reflection Mirror Mode & Stealth AMOLED Mode
4. Instant 1-Tap Driving Sighting Stamp
5. Tools Drawer Cockpit HUD & Sound/Haptics Controls
========================================================================== */
/* Directional Approach Angle Badges */
.hud-approach-badge {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 0.66rem;
font-weight: 700;
padding: 3px 8px;
border-radius: 9999px;
letter-spacing: 0.04em;
text-transform: uppercase;
transition: all 0.25s ease;
white-space: nowrap;
}
.hud-approach-badge.approach-direct {
background: rgba(239, 68, 68, 0.22);
color: #f87171;
border: 1px solid rgba(239, 68, 68, 0.5);
box-shadow: 0 0 10px rgba(239, 68, 68, 0.35);
animation: pulse-approach 1.2s infinite alternate;
}
@keyframes pulse-approach {
0% { transform: scale(1); box-shadow: 0 0 6px rgba(239, 68, 68, 0.3); }
100% { transform: scale(1.04); box-shadow: 0 0 12px rgba(239, 68, 68, 0.6); }
}
.hud-approach-badge.approach-cross {
background: rgba(245, 158, 11, 0.2);
color: #fbbf24;
border: 1px solid rgba(245, 158, 11, 0.45);
}
.hud-approach-badge.approach-away {
background: rgba(148, 163, 184, 0.15);
color: #94a3b8;
border: 1px solid rgba(148, 163, 184, 0.3);
}
.hud-approach-badge.approach-none {
background: rgba(16, 185, 129, 0.14);
color: #34d399;
border: 1px solid rgba(16, 185, 129, 0.3);
}
/* Time-to-Zone Intercept Countdown */
#hud-eta-box {
transition: all 0.2s ease;
}
#hud-eta-box .hud-metric-number {
color: #38bdf8;
text-shadow: 0 0 8px rgba(56, 189, 248, 0.3);
}
/* Windshield Reflection Mirror Mode (Inverts horizontally for dashboard reflection) */
.mirror-hud {
transform: scaleX(-1) !important;
transform-origin: center center;
}
/* Stealth AMOLED Pitch Dark Mode (#000000 Zero Cockpit Glare) */
body.stealth-amoled {
background-color: #000000 !important;
color: #e2e8f0;
}
body.stealth-amoled .radar-view-container,
body.stealth-amoled .radar-map-canvas,
body.stealth-amoled #radar-map {
background-color: #000000 !important;
filter: brightness(0.4) contrast(1.5) saturate(0.2) !important;
}
body.stealth-amoled .driver-hud {
background: #000000 !important;
border-color: #1e293b !important;
box-shadow: 0 0 24px rgba(0, 0, 0, 0.95), 0 0 0 1px #111827 !important;
backdrop-filter: none !important;
-webkit-backdrop-filter: none !important;
}
body.stealth-amoled .hud-capsule-header {
background: #000000 !important;
border-color: #1e293b !important;
}
body.stealth-amoled .hud-metric-box {
background: #040608 !important;
border-color: #1e293b !important;
}
body.stealth-amoled .hud-metric-number {
color: #00f0ff !important;
text-shadow: 0 0 8px rgba(0, 240, 255, 0.45) !important;
}
body.stealth-amoled .map-top-controls {
background: rgba(0, 0, 0, 0.98) !important;
border-color: #1e293b !important;
}
body.stealth-amoled .app-header,
body.stealth-amoled .site-nav {
background: #000000 !important;
border-color: #1e293b !important;
}
/* Tools Drawer Toggle Grid */
.tools-toggle-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
@media (max-width: 420px) {
.tools-toggle-grid {
grid-template-columns: 1fr;
}
}
/* Saved Sighting Stamps in Tools Drawer */
.stamps-list-container {
display: flex;
flex-direction: column;
gap: 6px;
max-height: 140px;
overflow-y: auto;
padding: 4px;
}
.stamps-empty-hint {
font-size: 0.76rem;
color: var(--text-muted);
line-height: 1.4;
margin: 0;
font-style: italic;
}
.stamp-item-card {
display: flex;
align-items: center;
justify-content: space-between;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 6px;
padding: 6px 10px;
font-size: 0.74rem;
color: #cbd5e1;
transition: background 0.15s ease;
}
.stamp-item-card:hover {
background: rgba(56, 189, 248, 0.1);
border-color: rgba(56, 189, 248, 0.3);
}
.stamp-item-left {
display: flex;
align-items: center;
gap: 6px;
}
.stamp-badge-zap {
color: #f59e0b;
font-weight: bold;
}
.btn-mini-clear {
background: none;
border: none;
color: #94a3b8;
font-size: 0.72rem;
cursor: pointer;
padding: 2px 6px;
border-radius: 4px;
transition: all 0.15s ease;
}
.btn-mini-clear:hover {
background: rgba(239, 68, 68, 0.15);
color: #ef4444;
}
/* Temporary Stamp Marker Animation on Map */
.stamp-pulse-pin {
display: flex;
align-items: center;
justify-content: center;
background: #f59e0b;
color: #000;
font-weight: 800;
font-size: 14px;
border-radius: 50%;
border: 2px solid #fff;
box-shadow: 0 0 16px rgba(245, 158, 11, 0.85);
animation: stamp-drop 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
@keyframes stamp-drop {
0% { transform: scale(0.3) translateY(-20px); opacity: 0; }
70% { transform: scale(1.2) translateY(3px); opacity: 1; }
100% { transform: scale(1) translateY(0); opacity: 1; }
}
+77 -15
View File
@@ -54,7 +54,7 @@
<meta name="apple-mobile-web-app-title" content="FlockRadar">
<!-- Main Stylesheet -->
<link rel="stylesheet" href="css/styles.css?v=26">
<link rel="stylesheet" href="css/styles.css?v=27">
</head>
<body>
@@ -155,7 +155,7 @@
<span>ALL CLEAR</span>
</div>
<!-- Metrics Row: Speed + Nearest Distance -->
<!-- Metrics Row: Speed + Nearest Distance + Intercept ETA -->
<div class="hud-metrics-row">
<div class="hud-metric-box">
<div class="hud-val-group">
@@ -175,6 +175,14 @@
</div>
<span class="hud-metric-label">Nearest</span>
</div>
<div class="hud-metric-box" id="hud-eta-box">
<div class="hud-val-group">
<span class="hud-metric-number" id="hud-eta-val"></span>
<span class="hud-metric-unit" id="hud-eta-sub">SEC</span>
</div>
<span class="hud-metric-label">Intercept</span>
</div>
</div>
<!-- Header Quick Triggers (Audio Toggle, Tools, Search) -->
@@ -197,14 +205,17 @@
</div>
</div>
<!-- Current Agency / Street Monitoring -->
<!-- Current Agency / Street Monitoring & Approach Vector Badge -->
<div class="hud-location-bar">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="var(--accent-cyan)" stroke-width="2">
<div class="hud-loc-left" style="display:flex; align-items:center; gap:6px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; flex:1; min-width:0;">
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="var(--accent-cyan)" stroke-width="2" style="flex-shrink:0;">
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="8" x2="12.01" y2="8"></line>
<polyline points="11 12 12 12 12 16 13 16"></polyline>
</svg>
<span id="hud-cam-agency">Monitoring Roadway Grid</span>
<span id="hud-cam-agency" style="overflow:hidden; text-overflow:ellipsis;">Monitoring Roadway Grid</span>
</div>
<span id="hud-approach-badge" class="hud-approach-badge approach-none">ALL CLEAR</span>
</div>
<!-- High-Priority Pulsing Warning Banner -->
@@ -370,6 +381,47 @@
</button>
</div>
<!-- Cockpit Display & Windshield Reflection HUD -->
<div class="tools-group">
<div class="tools-group-label">COCKPIT & WINDSHIELD HUD</div>
<div class="tools-toggle-grid">
<button type="button" id="btn-mirror-hud" class="btn-hud-action btn-tool-item" title="Mirror HUD horizontally so phone placed flat on dashboard reflects right-side up on windshield glass">
<svg class="flock-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="7 8 3 12 7 16"/><polyline points="17 8 21 12 17 16"/><line x1="14" y1="4" x2="10" y2="20"/></svg>
<span id="mirror-hud-label">Windshield Mirror: OFF</span>
</button>
<button type="button" id="btn-stealth-amoled" class="btn-hud-action btn-tool-item" title="Switch to pitch-black #000000 AMOLED stealth mode for zero cockpit glare">
<svg class="flock-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
<span id="stealth-amoled-label">AMOLED Pitch Dark: OFF</span>
</button>
</div>
</div>
<!-- Cabin Sound Boost & Haptic Feedback -->
<div class="tools-group">
<div class="tools-group-label">CABIN SOUND & HAPTICS</div>
<div class="tools-toggle-grid">
<button type="button" id="btn-toggle-audio-boost" class="btn-hud-action btn-tool-item" title="Boost radar siren & voice chime volume by +6dB (160%) for loud road noise">
<svg class="flock-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/><path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"/><line x1="22" y1="2" x2="22" y2="8"/><line x1="19" y1="5" x2="25" y2="5"/></svg>
<span id="audio-boost-label">Audio Boost (+6dB): OFF</span>
</button>
<button type="button" id="btn-toggle-haptic-boost" class="btn-hud-action btn-tool-item active" title="Vibrate phone on zone entry like lane-keep steering wheel feedback">
<svg class="flock-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="5" y="2" width="14" height="20" rx="2" ry="2"/><line x1="2" y1="8" x2="2" y2="16"/><line x1="22" y1="8" x2="22" y2="16"/></svg>
<span id="haptic-boost-label">Steering Haptics: ON</span>
</button>
</div>
</div>
<!-- Instant Stamped Sightings -->
<div class="tools-group" id="group-quick-stamps">
<div class="tools-group-label" style="display: flex; justify-content: space-between; align-items: center;">
<span>SAVED SIGHTINGS (<span id="stamps-count">0</span>)</span>
<button type="button" id="btn-clear-stamps" class="btn-mini-clear" title="Clear all local quick stamps">Clear</button>
</div>
<div id="stamps-list-container" class="stamps-list-container">
<p class="stamps-empty-hint">No quick stamps yet. Tap ⚡ Stamp while driving to record GPS instantly.</p>
</div>
</div>
<!-- Driving Audio Alert Mode Selector -->
<div class="tools-group">
<div class="tools-group-label">AUDIO ALERT STYLE</div>
@@ -506,15 +558,25 @@
<span>Recenter on Vehicle</span>
</button>
<!-- Giant 1-Tap Floating Action Button: Snap & Log Flock Camera (Bottom-Right) -->
<!-- Floating Action Buttons: Instant GPS Sighting Stamp + Photo Evidence (Bottom-Right) -->
<div class="driver-fab-container">
<button id="btn-quick-log-cam" class="btn-driver-fab" title="Snap camera photo on pole & log at GPS location">
<button id="btn-quick-mark-dash" class="btn-driver-fab btn-fab-stamp" title="⚡ 1-Tap Instant Sighting Stamp (Safe while driving - records GPS location instantly without typing)">
<div class="fab-icon-glow">
<svg class="flock-svg flock-svg-lg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14.5 4h-5L7 7H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3l-2.5-3z"/><circle cx="12" cy="13" r="3"/></svg>
<svg class="flock-svg flock-svg-md" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>
</div>
<div class="fab-text-group">
<span class="fab-main-title">+ SNAP & LOG CAMERA</span>
<span class="fab-sub-title">Opens Camera for Photo Proof</span>
<span class="fab-main-title"> STAMP</span>
<span class="fab-sub-title">1-Tap GPS Mark</span>
</div>
</button>
<button id="btn-quick-log-cam" class="btn-driver-fab btn-fab-photo" title="Snap camera photo on pole & log at GPS location">
<div class="fab-icon-glow">
<svg class="flock-svg flock-svg-md" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14.5 4h-5L7 7H4a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V9a2 2 0 0 0-2-2h-3l-2.5-3z"/><circle cx="12" cy="13" r="3"/></svg>
</div>
<div class="fab-text-group">
<span class="fab-main-title">+ SNAP</span>
<span class="fab-sub-title">Photo Proof</span>
</div>
</button>
</div>
@@ -1509,11 +1571,11 @@
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
<!-- Data & Application Logic with Cache Busting -->
<script src="data/portals.js?v=26"></script>
<script src="js/icons.js?v=26"></script>
<script src="js/auth.js?v=26"></script>
<script src="js/radar.js?v=26"></script>
<script src="js/app.js?v=26"></script>
<script src="data/portals.js?v=27"></script>
<script src="js/icons.js?v=27"></script>
<script src="js/auth.js?v=27"></script>
<script src="js/radar.js?v=27"></script>
<script src="js/app.js?v=27"></script>
<!-- Service Worker Instant Cache Invalidation -->
<script>
+493 -70
View File
@@ -112,6 +112,15 @@
let bgAudioElement = null;
let activeDetourLayerGroup = null;
// --- V27 DRIVER-CENTRIC POLISH STATE ---
let isHudMirrored = localStorage.getItem('flock_hud_mirror') === 'true';
let isStealthAmoled = localStorage.getItem('flock_stealth_amoled') === 'true';
let isAudioBoostEnabled = localStorage.getItem('flock_radar_audio_boost') === 'true';
let isHapticBoostEnabled = localStorage.getItem('flock_radar_haptic_boost') !== 'false';
let masterGainNode = null;
let savedQuickStamps = [];
let stampMarkers = [];
const SURVEILLANCE_CORRIDORS = {
'palmdale-blvd': {
name: 'Palmdale Blvd Corridor (SR-14 to 50th St E)',
@@ -406,7 +415,25 @@
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')
praLetterText: document.getElementById('pra-letter-text'),
// Cockpit, FOV & Driver Polish (v27)
hudApproachBadge: document.getElementById('hud-approach-badge'),
hudEtaBox: document.getElementById('hud-eta-box'),
hudEtaVal: document.getElementById('hud-eta-val'),
hudEtaSub: document.getElementById('hud-eta-sub'),
btnQuickMarkDash: document.getElementById('btn-quick-mark-dash'),
btnMirrorHud: document.getElementById('btn-mirror-hud'),
mirrorHudLabel: document.getElementById('mirror-hud-label'),
btnStealthAmoled: document.getElementById('btn-stealth-amoled'),
stealthAmoledLabel: document.getElementById('stealth-amoled-label'),
btnToggleAudioBoost: document.getElementById('btn-toggle-audio-boost'),
audioBoostLabel: document.getElementById('audio-boost-label'),
btnToggleHapticBoost: document.getElementById('btn-toggle-haptic-boost'),
hapticBoostLabel: document.getElementById('haptic-boost-label'),
stampsCount: document.getElementById('stamps-count'),
stampsListContainer: document.getElementById('stamps-list-container'),
btnClearStamps: document.getElementById('btn-clear-stamps')
};
// --- INITIALIZATION ---
@@ -421,6 +448,13 @@
updateProfileDisplay();
checkSupporterReturn();
// Initialize v27 Driver Cockpit Preferences & Quick Stamps
applyHudMirror(isHudMirrored, false);
applyStealthAmoled(isStealthAmoled, false);
applyAudioBoost(isAudioBoostEnabled, false);
applyHapticBoost(isHapticBoostEnabled, false);
loadQuickStamps();
// Auto-detect real location immediately (Palmdale / GPS)
attemptAutoLocate();
@@ -780,6 +814,12 @@
function setupHud() {
updateHudSpeedAndHeading(0, null);
el.hudDistVal.textContent = '—';
if (el.hudEtaVal) el.hudEtaVal.textContent = '—';
if (el.hudEtaSub) el.hudEtaSub.textContent = 'IDLE';
if (el.hudApproachBadge) {
el.hudApproachBadge.className = 'hud-approach-badge approach-none';
el.hudApproachBadge.textContent = 'ALL CLEAR';
}
el.hudZoneBadge.className = 'hud-badge badge-clear';
el.hudZoneBadge.innerHTML = `<span class="badge-dot dot-clear"></span> ALL CLEAR`;
el.hudCamAgency.textContent = 'Monitoring Highway Network';
@@ -788,6 +828,38 @@
updateVehicleSupporterAura();
}
// --- DIRECTIONAL TRAVEL FILTER & APPROACH ANGLE CLASSIFIER (v27) ---
function getBearing(lat1, lng1, lat2, lng2) {
const toRad = deg => deg * Math.PI / 180;
const toDeg = rad => rad * 180 / Math.PI;
const dLng = toRad(lng2 - lng1);
const lat1Rad = toRad(lat1);
const lat2Rad = toRad(lat2);
const y = Math.sin(dLng) * Math.cos(lat2Rad);
const x = Math.cos(lat1Rad) * Math.sin(lat2Rad) -
Math.sin(lat1Rad) * Math.cos(lat2Rad) * Math.cos(dLng);
return (toDeg(Math.atan2(y, x)) + 360) % 360;
}
function classifyApproach(userLat, userLng, userHeading, camLat, camLng, speedMph) {
if (speedMph < 3 || userHeading === null || isNaN(userHeading)) {
return { type: 'proximity', label: 'RADAR PROXIMITY', angleDelta: 0, suppressAlert: false };
}
const camBearing = getBearing(userLat, userLng, camLat, camLng);
const angleDelta = Math.abs((userHeading - camBearing + 180) % 360 - 180);
if (angleDelta < 50) {
return { type: 'direct', label: 'DIRECT APPROACH', angleDelta, suppressAlert: false };
} else if (angleDelta <= 130) {
return { type: 'cross', label: 'CROSS-TRAFFIC', angleDelta, suppressAlert: false };
} else {
return { type: 'away', label: 'RECEDING / BEHIND', angleDelta, suppressAlert: true };
}
}
window.getBearing = getBearing;
window.classifyApproach = classifyApproach;
// --- PROXIMITY CALCULATION & ALERT ENGINE ---
function checkProximityToCameras() {
if (!allCameras.length) return;
@@ -809,7 +881,7 @@
// Check if inside zone radius
const insideZone = closestDistMeters <= (closestCam.radius || 300);
// Update HUD
// Update HUD distance display
if (distFt < 5280) {
el.hudDistVal.textContent = distFt > 1000 ? (distFt / 1000).toFixed(1) + 'k' : distFt;
el.hudDistSub.textContent = 'FT';
@@ -822,18 +894,55 @@
el.hudCamAgency.textContent = closestCam.name;
}
// Update Graduated Radar Signal Strength Meter (Valentine One Style)
updateRadarGauge(distFt, insideZone, closestCam);
const approach = closestCam
? classifyApproach(currentPosition.lat, currentPosition.lng, lastGpsHeading, closestCam.lat, closestCam.lng, currentSpeed)
: { type: 'proximity', label: 'ALL CLEAR', angleDelta: 0, suppressAlert: false };
const cautionThreshold = (radarRangeMode === 'highway') ? 2500 : 1200;
// Update Directional Approach Angle Badge
if (el.hudApproachBadge) {
if (distFt > cautionThreshold || !closestCam) {
el.hudApproachBadge.className = 'hud-approach-badge approach-none';
el.hudApproachBadge.textContent = 'ALL CLEAR';
} else {
el.hudApproachBadge.className = `hud-approach-badge approach-${approach.type}`;
el.hudApproachBadge.textContent = approach.label;
}
}
// Dynamic Time-to-Zone Intercept Countdown
if (el.hudEtaVal && el.hudEtaSub) {
if (currentSpeed >= 4 && !approach.suppressAlert && distFt <= (radarRangeMode === 'highway' ? 3500 : 2000)) {
const speedMps = currentSpeed * 0.44704;
const etaSec = Math.round(closestDistMeters / speedMps);
if (etaSec <= 60) {
el.hudEtaVal.textContent = `~${etaSec}`;
el.hudEtaSub.textContent = 'SEC';
} else {
el.hudEtaVal.textContent = `~${(etaSec / 60).toFixed(1)}`;
el.hudEtaSub.textContent = 'MIN';
}
} else if (approach.suppressAlert && distFt <= cautionThreshold) {
el.hudEtaVal.textContent = '—';
el.hudEtaSub.textContent = 'AWAY';
} else {
el.hudEtaVal.textContent = '—';
el.hudEtaSub.textContent = 'IDLE';
}
}
// Update Graduated Radar Signal Strength Meter
updateRadarGauge(distFt, insideZone, closestCam, approach);
// Audio Proximity Geiger-Counter Chirps
handleProximityChirps(distFt);
handleProximityChirps(distFt, approach);
// State Transitions
const cautionThreshold = (radarRangeMode === 'highway') ? 2500 : 1200;
if (insideZone) {
if (!isInZone) {
// Just entered zone! Trigger Alert!
triggerZoneAlert(closestCam, distFt);
triggerZoneAlert(closestCam, distFt, approach);
}
isInZone = true;
el.hudZoneBadge.className = 'hud-badge badge-danger';
@@ -843,14 +952,22 @@
<span class="banner-pulse-icon">${window.FlockIcons ? FlockIcons.alertTriangle('flock-svg-lg') : ''}</span>
<div>
<strong>FLOCK CAMERA ACTIVE ZONE</strong>
<p>${escapeHtml(closestCam.agency)}Vehicle Profile Logging</p>
<p>${escapeHtml(closestCam.agency)}${approach.label}</p>
</div>
`;
} else if (distFt <= cautionThreshold) {
// Approaching camera
isInZone = false;
if (approach.suppressAlert) {
el.hudZoneBadge.className = 'hud-badge badge-clear';
el.hudZoneBadge.innerHTML = `<span class="badge-dot dot-clear"></span> RECEDING CAMERA`;
} else if (approach.type === 'cross') {
el.hudZoneBadge.className = 'hud-badge badge-warning';
el.hudZoneBadge.innerHTML = `<span class="badge-dot dot-warning"></span> CROSS-TRAFFIC CAMERA`;
} else {
el.hudZoneBadge.className = 'hud-badge badge-warning';
el.hudZoneBadge.innerHTML = `<span class="badge-dot dot-warning"></span> CAMERA AHEAD`;
}
el.hudBannerAlert.classList.remove('visible');
} else {
// Clear
@@ -862,7 +979,7 @@
}
// --- 3-STAGE GRADUATED RADAR DETECTOR SIGNAL GAUGE ---
function updateRadarGauge(distFt, insideZone, closestCam) {
function updateRadarGauge(distFt, insideZone, closestCam, approach) {
if (!el.hudGaugeState || !el.hudSignalMeter) return;
const isHighway = (radarRangeMode === 'highway');
@@ -874,6 +991,14 @@
b.className = 'signal-bar';
});
// If moving away from camera past immediate zone, show subdued reassurance
if (approach && approach.suppressAlert && distFt > 350) {
el.hudGaugeState.className = 'hud-gauge-status status-green';
el.hudGaugeState.textContent = `RECEDING (${distFt} FT)`;
bars[0].classList.add('active-green');
return;
}
if (insideZone || distFt <= 450) {
// Level 5: Red Alert In Zone
el.hudGaugeState.className = 'hud-gauge-status status-red';
@@ -882,7 +1007,9 @@
} else if (distFt <= warnDist) {
// Level 4: Immediate Proximity
el.hudGaugeState.className = 'hud-gauge-status status-red';
el.hudGaugeState.textContent = `ALERT: ${distFt} FT`;
el.hudGaugeState.textContent = (approach && approach.type === 'cross')
? `CROSS-TRAFFIC: ${distFt} FT`
: `ALERT: ${distFt} FT`;
bars[0].classList.add('active-red');
bars[1].classList.add('active-red');
bars[2].classList.add('active-red');
@@ -890,16 +1017,21 @@
} else if (distFt <= cautionDist) {
// Level 3: Approaching (Yellow Caution)
el.hudGaugeState.className = 'hud-gauge-status status-yellow';
el.hudGaugeState.textContent = `APPROACHING (${Math.round(distFt / 100) * 100} FT)`;
el.hudGaugeState.textContent = (approach && approach.type === 'cross')
? `CROSS-STREET (${Math.round(distFt / 100) * 100} FT)`
: `APPROACHING (${Math.round(distFt / 100) * 100} FT)`;
bars[0].classList.add('active-yellow');
bars[1].classList.add('active-yellow');
bars[2].classList.add('active-yellow');
// Pre-alert announcement once per camera approach
// Pre-alert announcement once per camera approach (only if not moving away)
if (closestCam && lastCautionSpokenCamId !== closestCam.id) {
if (!approach || !approach.suppressAlert) {
lastCautionSpokenCamId = closestCam.id;
playCautionBeep();
speakVoiceAlert(`Caution. Approaching surveillance camera in ${Math.round(distFt / 100) * 100} feet.`);
const approachDesc = (approach && approach.type === 'cross') ? 'cross-traffic ' : '';
speakVoiceAlert(`Caution. Approaching ${approachDesc}surveillance camera in ${Math.round(distFt / 100) * 100} feet.`);
}
}
} else if (distFt <= (cautionDist * 1.6)) {
// Level 2: Grid Range
@@ -919,27 +1051,53 @@
}
}
// --- AUDIO SYNTHESIZERS: CHIRPS & CAUTION BEEPS ---
function playLogConfirmationChirp() {
try {
// --- AUDIO SYNTHESIZERS & MASTER GAIN BOOSTER (v27) ---
function getAudioContext() {
if (!audioContext) {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
}
if (audioContext.state === 'suspended') {
audioContext.resume();
}
const osc = audioContext.createOscillator();
const gain = audioContext.createGain();
if (!masterGainNode) {
masterGainNode = audioContext.createGain();
masterGainNode.gain.setValueAtTime(isAudioBoostEnabled ? 1.6 : 1.0, audioContext.currentTime);
masterGainNode.connect(audioContext.destination);
}
return audioContext;
}
function getAudioOutput() {
const ctx = getAudioContext();
return masterGainNode || ctx.destination;
}
function triggerHapticFeedback(pattern = [180, 70, 180, 70, 350]) {
if (!isHapticBoostEnabled) return;
if ('vibrate' in navigator) {
try {
navigator.vibrate(pattern);
} catch (e) {
// Ignored
}
}
}
function playLogConfirmationChirp() {
try {
const ctx = getAudioContext();
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = 'triangle';
osc.frequency.setValueAtTime(587.33, audioContext.currentTime); // D5
osc.frequency.setValueAtTime(880, audioContext.currentTime + 0.1); // A5
osc.frequency.setValueAtTime(1174.66, audioContext.currentTime + 0.2); // D6
gain.gain.setValueAtTime(0.35, audioContext.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime + 0.4);
osc.frequency.setValueAtTime(587.33, ctx.currentTime); // D5
osc.frequency.setValueAtTime(880, ctx.currentTime + 0.1); // A5
osc.frequency.setValueAtTime(1174.66, ctx.currentTime + 0.2); // D6
gain.gain.setValueAtTime(0.35, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.4);
osc.connect(gain);
gain.connect(audioContext.destination);
gain.connect(getAudioOutput());
osc.start();
osc.stop(audioContext.currentTime + 0.4);
osc.stop(ctx.currentTime + 0.4);
} catch (e) {
console.warn('Log chirp error:', e);
}
@@ -949,22 +1107,17 @@
const user = window.FlockAuth ? window.FlockAuth.getUser() : null;
if (user && !user.soundEnabled) return;
try {
if (!audioContext) {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
}
if (audioContext.state === 'suspended') {
audioContext.resume();
}
const osc = audioContext.createOscillator();
const gain = audioContext.createGain();
const ctx = getAudioContext();
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(520, audioContext.currentTime);
gain.gain.setValueAtTime(0.25, audioContext.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime + 0.22);
osc.frequency.setValueAtTime(520, ctx.currentTime);
gain.gain.setValueAtTime(0.25, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.22);
osc.connect(gain);
gain.connect(audioContext.destination);
gain.connect(getAudioOutput());
osc.start();
osc.stop(audioContext.currentTime + 0.22);
osc.stop(ctx.currentTime + 0.22);
} catch (e) {
console.warn('Caution beep error:', e);
}
@@ -1047,37 +1200,37 @@
if (user && !user.soundEnabled) return;
try {
if (!audioContext) {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
}
if (audioContext.state === 'suspended') {
audioContext.resume();
}
const osc = audioContext.createOscillator();
const gain = audioContext.createGain();
const ctx = getAudioContext();
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(freq, audioContext.currentTime);
osc.frequency.exponentialRampToValueAtTime(freq * 1.15, audioContext.currentTime + 0.045);
osc.frequency.setValueAtTime(freq, ctx.currentTime);
osc.frequency.exponentialRampToValueAtTime(freq * 1.15, ctx.currentTime + 0.045);
gain.gain.setValueAtTime(volume, audioContext.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime + 0.05);
gain.gain.setValueAtTime(volume, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.05);
osc.connect(gain);
gain.connect(audioContext.destination);
gain.connect(getAudioOutput());
osc.start();
osc.stop(audioContext.currentTime + 0.05);
osc.stop(ctx.currentTime + 0.05);
} catch (e) {
// AudioContext policy
}
}
function handleProximityChirps(distFt) {
function handleProximityChirps(distFt, approach) {
if (radarAudioMode === 'silent') return;
const user = window.FlockAuth ? window.FlockAuth.getUser() : null;
if (user && !user.soundEnabled) return;
// Suppress repeated geiger chirps when driving away from camera past 350ft
if (approach && approach.suppressAlert && distFt > 350) {
return;
}
const maxChirpDist = (radarRangeMode === 'highway') ? 2200 : 1200;
if (distFt > maxChirpDist || distFt <= 0) {
stationaryChirpCount = 0;
@@ -1202,30 +1355,25 @@
lastChimeTime = now;
try {
if (!audioContext) {
audioContext = new (window.AudioContext || window.webkitAudioContext)();
}
if (audioContext.state === 'suspended') {
audioContext.resume();
}
const ctx = getAudioContext();
// Synthesize clean two-tone radar alert
const osc = audioContext.createOscillator();
const gain = audioContext.createGain();
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(880, audioContext.currentTime);
osc.frequency.setValueAtTime(659.25, audioContext.currentTime + 0.12);
osc.frequency.setValueAtTime(880, audioContext.currentTime + 0.24);
osc.frequency.setValueAtTime(880, ctx.currentTime);
osc.frequency.setValueAtTime(659.25, ctx.currentTime + 0.12);
osc.frequency.setValueAtTime(880, ctx.currentTime + 0.24);
gain.gain.setValueAtTime(0.35, audioContext.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, audioContext.currentTime + 0.45);
gain.gain.setValueAtTime(0.35, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + 0.45);
osc.connect(gain);
gain.connect(audioContext.destination);
gain.connect(getAudioOutput());
osc.start();
osc.stop(audioContext.currentTime + 0.45);
osc.stop(ctx.currentTime + 0.45);
} catch (e) {
console.warn('Audio chime error:', e);
}
@@ -1242,17 +1390,23 @@
const utter = new SpeechSynthesisUtterance(text);
utter.rate = 1.05;
utter.pitch = 1.0;
utter.volume = 1.0;
utter.volume = isAudioBoostEnabled ? 1.0 : 0.9;
window.speechSynthesis.speak(utter);
} catch (e) {
console.warn('Speech error:', e);
}
}
function triggerZoneAlert(cam, distFt) {
function triggerZoneAlert(cam, distFt, approach) {
if (approach && approach.suppressAlert) return;
playRadarChime();
triggerHapticFeedback([180, 70, 180, 70, 350]);
if (approach && approach.type === 'cross') {
speakVoiceAlert("Caution. Cross-traffic Flock camera active on perpendicular avenue.");
} else {
speakVoiceAlert("Warning. Flock camera detected ahead. You are entering a recorded surveillance zone.");
}
}
// --- VIRAL SHARING HELPER ---
async function shareApp(customTitle, customText, customUrl) {
@@ -2413,6 +2567,267 @@ Generated via FlockRadar Civic Transparency Network (flockradar.org)`;
}, 100);
}
// ==========================================================================
// v27 DRIVER-CENTRIC POLISH REFINEMENTS
// 1. Instant 1-Tap Driving Sighting Stamp
// 2. Windshield Reflection Mirror Mode
// 3. Stealth AMOLED True Black Mode
// 4. Cabin Audio Gain Booster (+6dB)
// 5. Steering Haptic Vibration Feedback
// ==========================================================================
// --- 1-TAP "MARK CAMERA WHILE DRIVING" INSTANT SIGHTING STAMP ---
function loadQuickStamps() {
try {
const stored = localStorage.getItem('flock_quick_stamps');
savedQuickStamps = stored ? JSON.parse(stored) : [];
} catch (e) {
savedQuickStamps = [];
}
renderStampedSightings();
renderExistingStampMarkers();
}
function saveQuickStamps() {
try {
localStorage.setItem('flock_quick_stamps', JSON.stringify(savedQuickStamps));
} catch (e) {
console.warn('Failed to save quick stamps', e);
}
renderStampedSightings();
}
function renderExistingStampMarkers() {
if (!map) return;
stampMarkers.forEach(m => map.removeLayer(m));
stampMarkers = [];
savedQuickStamps.forEach(s => {
const stampIcon = L.divIcon({
className: 'stamp-marker-icon',
html: `<div class="stamp-pulse-pin" style="width:24px; height:24px; font-size:12px;">⚡</div>`,
iconSize: [24, 24],
iconAnchor: [12, 12]
});
const marker = L.marker([s.lat, s.lng], { icon: stampIcon }).addTo(map);
marker.bindPopup(`
<div style="font-family:inherit; min-width:180px; font-size:0.8rem; color:#0f172a; padding:4px;">
<strong style="color:#d97706; display:flex; align-items:center; gap:4px;">
⚡ Stamped Sighting (Draft)
</strong>
<p style="margin:4px 0 6px 0; font-size:0.75rem; color:#475569;">
Logged at: <strong>${s.timeStr}</strong><br>
Speed: <strong>${s.speed} MPH</strong> • Heading: <strong>${s.heading}°</strong><br>
Coords: <code>${s.lat}, ${s.lng}</code>
</p>
<span style="font-size:0.72rem; color:#0284c7; font-weight:600;">Saved in Local Drafts</span>
</div>
`);
stampMarkers.push(marker);
});
}
function quickMarkAtGps() {
const lat = currentPosition.lat;
const lng = currentPosition.lng;
const speed = currentSpeed || 0;
const heading = lastGpsHeading || 0;
const now = new Date();
const timeStr = now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
const stamp = {
id: 'stamp_' + Date.now(),
lat: Number(lat.toFixed(6)),
lng: Number(lng.toFixed(6)),
speed: Math.round(speed),
heading: Math.round(heading),
timestamp: now.toISOString(),
timeStr: timeStr
};
savedQuickStamps.unshift(stamp);
saveQuickStamps();
// Instant Audio Affirmation (Dual high-pitch chirp)
playLogConfirmationChirp();
// Haptic steering vibration pulse
triggerHapticFeedback([100, 50, 120]);
// Drop marker on Leaflet map
if (map) {
const stampIcon = L.divIcon({
className: 'stamp-marker-icon',
html: `<div class="stamp-pulse-pin" style="width:26px; height:26px;">⚡</div>`,
iconSize: [26, 26],
iconAnchor: [13, 13]
});
const marker = L.marker([stamp.lat, stamp.lng], { icon: stampIcon }).addTo(map);
marker.bindPopup(`
<div style="font-family:inherit; min-width:180px; font-size:0.8rem; color:#0f172a; padding:4px;">
<strong style="color:#d97706; display:flex; align-items:center; gap:4px;">
⚡ Stamped Sighting (Draft)
</strong>
<p style="margin:4px 0 6px 0; font-size:0.75rem; color:#475569;">
Logged at: <strong>${timeStr}</strong><br>
Speed: <strong>${speed} MPH</strong> • Heading: <strong>${heading}°</strong><br>
Coords: <code>${stamp.lat}, ${stamp.lng}</code>
</p>
<span style="font-size:0.72rem; color:#0284c7; font-weight:600;">Saved in Local Drafts</span>
</div>
`).openPopup();
stampMarkers.push(marker);
}
showToast(`⚡ Camera Sighting Stamped at ${stamp.lat}, ${stamp.lng}! Saved to drafts.`);
}
function clearAllStamps() {
if (!savedQuickStamps.length) return;
if (confirm('Clear all saved camera sighting stamps?')) {
savedQuickStamps = [];
saveQuickStamps();
stampMarkers.forEach(m => {
if (map && m) map.removeLayer(m);
});
stampMarkers = [];
showToast('Cleared all local camera stamps.');
}
}
function renderStampedSightings() {
if (el.stampsCount) {
el.stampsCount.textContent = savedQuickStamps.length;
}
if (!el.stampsListContainer) return;
if (!savedQuickStamps.length) {
el.stampsListContainer.innerHTML = `<p class="stamps-empty-hint">No quick stamps yet. Tap ⚡ Stamp while driving to record GPS instantly.</p>`;
return;
}
el.stampsListContainer.innerHTML = savedQuickStamps.map(s => `
<div class="stamp-item-card" data-id="${s.id}">
<div class="stamp-item-left">
<span class="stamp-badge-zap">⚡</span>
<div>
<strong>${s.timeStr}</strong> (${s.speed} MPH)
<div style="font-size:0.72rem; color:#94a3b8;">${s.lat.toFixed(4)}, ${s.lng.toFixed(4)}</div>
</div>
</div>
<button type="button" class="btn-mini-link" style="color:#38bdf8; background:none; border:none; cursor:pointer; font-size:0.72rem; font-weight:600;" onclick="window.zoomToStamp(${s.lat}, ${s.lng})">Zoom</button>
</div>
`).join('');
}
window.zoomToStamp = function(lat, lng) {
if (map) {
map.setView([lat, lng], 17, { animate: true });
showToast(`Zoomed to stamped camera at ${lat}, ${lng}`);
}
};
// --- WINDSHIELD REFLECTION MIRROR MODE ---
function applyHudMirror(enabled, showFeedback = true) {
isHudMirrored = enabled;
localStorage.setItem('flock_hud_mirror', enabled ? 'true' : 'false');
if (el.driverHud) {
el.driverHud.classList.toggle('mirror-hud', enabled);
}
if (el.btnMirrorHud) {
el.btnMirrorHud.classList.toggle('active', enabled);
}
if (el.mirrorHudLabel) {
el.mirrorHudLabel.textContent = enabled ? 'Windshield Mirror: ON' : 'Windshield Mirror: OFF';
}
if (showFeedback) {
if (enabled) {
showToast('🪞 Windshield HUD Mirror ON: Lay phone flat on dashboard under glass!');
} else {
showToast('🪞 Windshield HUD Mirror OFF');
}
}
}
function toggleMirrorHud() {
applyHudMirror(!isHudMirrored, true);
}
// --- STEALTH AMOLED NIGHT MODE (TRUE #000000) ---
function applyStealthAmoled(enabled, showFeedback = true) {
isStealthAmoled = enabled;
localStorage.setItem('flock_stealth_amoled', enabled ? 'true' : 'false');
document.body.classList.toggle('stealth-amoled', enabled);
if (el.btnStealthAmoled) {
el.btnStealthAmoled.classList.toggle('active', enabled);
}
if (el.stealthAmoledLabel) {
el.stealthAmoledLabel.textContent = enabled ? 'AMOLED Pitch Dark: ON' : 'AMOLED Pitch Dark: OFF';
}
if (showFeedback) {
if (enabled) {
showToast('🌙 AMOLED Stealth Mode Activated: Zero Cockpit Glare (True #000000)');
} else {
showToast('☀️ AMOLED Stealth Mode Deactivated');
}
}
}
function toggleStealthAmoled() {
applyStealthAmoled(!isStealthAmoled, true);
}
// --- CABIN AUDIO GAIN BOOSTER (+6dB) ---
function applyAudioBoost(enabled, showFeedback = true) {
isAudioBoostEnabled = enabled;
localStorage.setItem('flock_radar_audio_boost', enabled ? 'true' : 'false');
if (audioContext && masterGainNode) {
masterGainNode.gain.setValueAtTime(enabled ? 1.6 : 1.0, audioContext.currentTime);
}
if (el.btnToggleAudioBoost) {
el.btnToggleAudioBoost.classList.toggle('active', enabled);
}
if (el.audioBoostLabel) {
el.audioBoostLabel.textContent = enabled ? 'Audio Boost (+6dB): ON' : 'Audio Boost (+6dB): OFF';
}
if (showFeedback) {
playRadarTick(enabled ? 1100 : 700, 0.3);
if (enabled) {
showToast('🔊 Cabin Audio Boost (+6dB / 160%) ENABLED');
} else {
showToast('🔈 Audio Volume Set to Standard Level');
}
}
}
function toggleAudioBoost() {
applyAudioBoost(!isAudioBoostEnabled, true);
}
// --- HAPTIC STEERING VIBRATION FEEDBACK ---
function applyHapticBoost(enabled, showFeedback = true) {
isHapticBoostEnabled = enabled;
localStorage.setItem('flock_radar_haptic_boost', enabled ? 'true' : 'false');
if (el.btnToggleHapticBoost) {
el.btnToggleHapticBoost.classList.toggle('active', enabled);
}
if (el.hapticBoostLabel) {
el.hapticBoostLabel.textContent = enabled ? 'Steering Haptics: ON' : 'Steering Haptics: OFF';
}
if (showFeedback) {
if (enabled) {
triggerHapticFeedback([100, 50, 150]);
showToast('📳 Haptic Steering Vibration Alerts ENABLED');
} else {
showToast('📴 Steering Haptic Alerts Disabled');
}
}
}
function toggleHapticBoost() {
applyHapticBoost(!isHapticBoostEnabled, true);
}
// --- SURVEILLANCE ZONE HARDWARE PROOF MODAL ---
function openCameraDetailModal(cam) {
if (!cam) return;
@@ -2944,6 +3359,14 @@ Generated via FlockRadar Civic Transparency Network (flockradar.org)`;
if (el.btnSensCity) el.btnSensCity.addEventListener('click', () => setRadarSensitivity('city'));
if (el.btnSensHwy) el.btnSensHwy.addEventListener('click', () => setRadarSensitivity('highway'));
// v27 Driving Polish & Cockpit Listeners
if (el.btnQuickMarkDash) el.btnQuickMarkDash.addEventListener('click', quickMarkAtGps);
if (el.btnClearStamps) el.btnClearStamps.addEventListener('click', clearAllStamps);
if (el.btnMirrorHud) el.btnMirrorHud.addEventListener('click', toggleMirrorHud);
if (el.btnStealthAmoled) el.btnStealthAmoled.addEventListener('click', toggleStealthAmoled);
if (el.btnToggleAudioBoost) el.btnToggleAudioBoost.addEventListener('click', toggleAudioBoost);
if (el.btnToggleHapticBoost) el.btnToggleHapticBoost.addEventListener('click', toggleHapticBoost);
// Camera Hardware Proof & Voting Events
if (el.camDetailCloseBtn) el.camDetailCloseBtn.addEventListener('click', closeCameraDetailModal);
+13 -13
View File
@@ -1,20 +1,20 @@
// Service Worker for FlockRadar Offline Driving & Live Updates
const CACHE_NAME = 'flockradar-v26';
const CACHE_NAME = 'flockradar-v27';
const ASSETS_TO_CACHE = [
'./',
'./index.html',
'./css/styles.css?v=26',
'./js/icons.js?v=26',
'./js/app.js?v=26',
'./js/auth.js?v=26',
'./js/radar.js?v=26',
'./data/portals.js?v=26',
'./favicon.svg?v=26',
'./favicon.ico?v=26',
'./apple-touch-icon.png?v=26',
'./icon-192.png?v=26',
'./icon-512.png?v=26',
'./og-image.png?v=26',
'./css/styles.css?v=27',
'./js/icons.js?v=27',
'./js/app.js?v=27',
'./js/auth.js?v=27',
'./js/radar.js?v=27',
'./data/portals.js?v=27',
'./favicon.svg?v=27',
'./favicon.ico?v=27',
'./apple-touch-icon.png?v=27',
'./icon-192.png?v=27',
'./icon-512.png?v=27',
'./og-image.png?v=27',
'./assets/img/logo.svg',
'./assets/img/flock_pole_cam_palmdale.jpg',
'./manifest.json'