feat: initial release of FlockRadar driving app and national transparency directory for flockradar.org

This commit is contained in:
2026-09-12 08:50:24 -07:00
commit b000c7dda6
12 changed files with 5563 additions and 0 deletions
+4
View File
@@ -0,0 +1,4 @@
cert.pem
key.pem
.DS_Store
node_modules/
+32
View File
@@ -0,0 +1,32 @@
# Flock Safety Transparency Portals Directory
A high-performance, public-interest web application that provides direct public access to over **1,439+ official Flock Safety Transparency Portals** across 45 U.S. states.
## Live Metrics Aggregated
* **1,439+ Verified Transparency Portals**
* **37,250+ Reported ALPR Cameras**
* **346.7 Million+ Vehicles Tracked** (rolling 30-day window)
* **256,900+ Law Enforcement Searches**
* **10.2 Million+ Hotlist Hits**
## Key Capabilities
1. **Instant Search & Filter:** Search by City, County, Agency Name, State, or URL slug.
2. **State Quick Chips:** One-click filter by top states (CA, TX, GA, IL, OH, IN, NC, FL, MO, SC, WA, etc.).
3. **Deep Agency Inspection Modal:**
* Direct link to `https://transparency.flocksafety.com/[slug]`.
* Camera numbers, 30-day captures, search counts, and hotlist alerts.
* Stated search justifications (e.g., Stolen Vehicle, Burglary, Homicide, Missing Person).
* Inter-agency sharing network (with partner agency search filter).
* Prohibited uses policies.
* One-click Google Search for local city council contracts and minutes.
4. **CSV Export:** Download the filtered directory dataset with one click.
5. **Civil Liberties Educational Section:** Covers Vehicle Fingerprinting, Fourth Amendment case law (*Carpenter v. United States*, *United States v. Jones*), and audit log search tools.
## Running Locally
From this directory:
```bash
# Start a simple local HTTP server
python3 -m http.server 8080
```
Then open `http://localhost:8080` in your web browser.
+2138
View File
File diff suppressed because it is too large Load Diff
+1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+757
View File
@@ -0,0 +1,757 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FlockRadar | Live ALPR Camera Awareness & National Transparency Directory</title>
<meta name="description" content="A Waze-style real-time ALPR radar navigation app alerting drivers to active Flock camera recorded zones with live proximity alerts, audio warnings, and a 1,439-portal public directory.">
<meta name="keywords" content="Flock Safety, ALPR, camera radar, waze for flock, live radar, license plate readers, police surveillance, camera alerts">
<meta name="author" content="Public ALPR Transparency Project">
<!-- Open Graph -->
<meta property="og:title" content="FlockRadar | Live ALPR Camera Awareness & Directory">
<meta property="og:description" content="Drive with real-time awareness of Flock camera recorded zones with live proximity HUD alerts, audio warnings, and public portal data.">
<meta property="og:type" content="website">
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400;600;700&family=Outfit:wght@500;600;700;800&display=swap" rel="stylesheet">
<!-- Leaflet Map CSS -->
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin=""/>
<!-- PWA & Mobile Web App Meta -->
<link rel="manifest" href="manifest.json">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="FlockRadar">
<!-- Main Stylesheet -->
<link rel="stylesheet" href="css/styles.css">
</head>
<body>
<!-- ========================================================================
SITE NAVIGATION & APP BAR
======================================================================== -->
<nav class="site-nav" aria-label="Main Navigation">
<div class="container nav-content">
<div class="nav-left">
<a href="#" class="brand-wrapper" id="brand-link">
<div class="radar-icon-box" aria-hidden="true">
<div class="pulse-dot"></div>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#ef4444" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<path d="m12 12 5-5"></path>
</svg>
</div>
<div class="brand-text">
<span class="brand-title">FLOCKRADAR</span>
<span class="brand-tagline">ALPR Awareness Network</span>
</div>
</a>
<!-- View Mode Switcher Pills -->
<div class="nav-mode-switcher" role="tablist">
<button id="nav-map-btn" class="nav-mode-btn active" role="tab" aria-selected="true">
<span class="mode-icon">🗺️</span>
<span>Live Radar Map</span>
</button>
<button id="nav-dir-btn" class="nav-mode-btn" role="tab" aria-selected="false">
<span class="mode-icon">📋</span>
<span>National Directory (1,439)</span>
</button>
</div>
</div>
<div class="nav-right">
<div class="badge-live-pulse" title="Database synced with official public transparency portals">
<span class="live-indicator-dot"></span>
<span>RADAR ACTIVE</span>
</div>
<!-- Driver Profile / Login Button -->
<button id="driver-profile-btn" class="user-profile-pill" title="Driver Profile & Settings">
<span id="user-profile-pill">
<span class="avatar-dot">👤</span>
<span>Driver Login</span>
</span>
</button>
</div>
</div>
</nav>
<!-- ========================================================================
VIEW 1: LIVE RADAR MAP & NAVIGATION INTERFACE (WAZE-STYLE)
======================================================================== -->
<section id="radar-view" class="radar-view-container">
<!-- Leaflet Map Canvas -->
<div id="radar-map" class="radar-map-canvas" aria-label="Interactive Radar Map"></div>
<!-- Floating Driver HUD (Top-Left) -->
<aside class="driver-hud" aria-live="polite">
<!-- Status Badge -->
<div id="hud-zone-badge" class="hud-badge badge-clear">
<span class="badge-dot dot-clear"></span>
<span>ALL CLEAR</span>
</div>
<!-- Metrics Row: Speed + Nearest Distance -->
<div class="hud-metrics-row">
<div class="hud-metric-box">
<div class="hud-val-group">
<span class="hud-metric-number" id="hud-speed-val">0</span>
<span class="hud-metric-unit">MPH</span>
</div>
<span class="hud-metric-label">Vehicle Speed</span>
</div>
<div class="hud-metric-box">
<div class="hud-val-group">
<span class="hud-metric-number" id="hud-dist-val"></span>
<span class="hud-metric-unit" id="hud-dist-sub">FT</span>
</div>
<span class="hud-metric-label">Nearest Camera</span>
</div>
</div>
<!-- Graduated Radar Detector Signal Gauge (Escort / Valentine One Style) -->
<div class="hud-radar-gauge-container">
<div class="hud-radar-gauge-header">
<span class="hud-gauge-title">SURVEILLANCE RADAR SIGNAL</span>
<span id="hud-gauge-state" class="hud-gauge-status status-green">ALL CLEAR</span>
</div>
<div class="hud-signal-meter" id="hud-signal-meter">
<div class="signal-bar bar-1 active-green" title="Signal Level 1"></div>
<div class="signal-bar bar-2" title="Signal Level 2"></div>
<div class="signal-bar bar-3" title="Signal Level 3"></div>
<div class="signal-bar bar-4" title="Signal Level 4"></div>
<div class="signal-bar bar-5" title="Signal Level 5"></div>
</div>
</div>
<!-- Current Agency / Street Monitoring -->
<div class="hud-location-bar">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="var(--accent-cyan)" stroke-width="2">
<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>
</div>
<!-- Alert Sensitivity Selector (City Mode vs Highway Mode) -->
<div class="hud-sensitivity-row">
<span class="sensitivity-label">RADAR RANGE:</span>
<div class="sensitivity-toggle-group">
<button id="btn-sens-city" class="sens-btn active" title="Alert within 1,000 ft (Optimal for city driving)">🏙️ City (1k ft)</button>
<button id="btn-sens-hwy" class="sens-btn" title="Alert within 2,500 ft (Optimal for freeway/SR-14 speeds)">🛣️ Highway (2.5k ft)</button>
</div>
</div>
<!-- High-Priority Pulsing Warning Banner -->
<div id="hud-banner-alert" class="hud-banner-alert">
<span class="banner-pulse-icon">⚠️</span>
<div>
<strong>FLOCK CAMERA ACTIVE ZONE</strong>
<p>Vehicle Profile & Plate Logging Active</p>
</div>
</div>
<!-- Live Auto-Spotted Status Badge -->
<div id="hud-spotted-badge" class="hud-spotted-badge" title="Location auto-detected via network & GPS">
<span>🛰️</span>
<span id="hud-spotted-text">Spotting Antelope Valley Location...</span>
</div>
<!-- Quick Antelope Valley Selector Bar -->
<div class="hud-location-selector">
<div class="location-quick-chips">
<button id="btn-locate-me" class="loc-chip active" title="Re-detect your location via IP & GPS">
<span>🎯</span> Auto-Detect (GPS)
</button>
<button id="btn-loc-palmdale" class="loc-chip" title="Jump to Palmdale Blvd & 10th St W">
<span>📍</span> Palmdale (Center)
</button>
<button id="btn-loc-lancaster" class="loc-chip" title="Jump to Lancaster ALPR Grid (Ave L & 10th St W)">
<span>📍</span> Lancaster (Center)
</button>
<button id="btn-loc-palmdale-east" class="loc-chip" title="Jump to East Palmdale (Ave R & 47th St E)">
<span>📍</span> East AV (Ave R)
</button>
<button id="btn-loc-palmdale-south" class="loc-chip" title="Jump to South Palmdale (Ave S)">
<span>📍</span> South AV (Ave S)
</button>
<button id="btn-loc-rancho" class="loc-chip" title="Jump to West AV / Rancho Vista Blvd & Mall">
<span>📍</span> West AV (Mall)
</button>
</div>
<form id="map-search-form" class="map-search-box">
<input type="text" id="map-search-input" placeholder="🔍 Search Antelope Valley street or address..." autocomplete="off">
<button type="submit" id="btn-search-go">Jump</button>
</form>
</div>
</aside>
<!-- Floating Navigation & Drive Simulator Controls (Top-Right) -->
<div class="map-top-controls">
<button id="btn-follow-car" class="btn-hud-action active" title="Automatically keep map centered on your vehicle">
<span>🎯</span>
<span>Follow Car: ON</span>
</button>
<button id="btn-live-gps" class="btn-hud-action" title="Start continuous real-time GPS tracking on your device">
<span>📡</span>
<span>Live GPS Drive</span>
</button>
<button id="btn-map-layer" class="btn-hud-action" title="Toggle between Dark Canvas and Satellite View">
<span>🛰️</span>
<span>Satellite / Dark</span>
</button>
<button id="btn-fullscreen-mount" class="btn-hud-action" title="Toggle Fullscreen Dashboard Mount Mode">
<span></span>
<span>Dashboard Fullscreen</span>
</button>
<button id="btn-test-camera" class="btn-hud-action" title="Drop a test camera 400ft ahead of your current location to test proximity alerts">
<span></span>
<span>Place Camera 400ft Ahead</span>
</button>
<button id="btn-test-alert" class="btn-hud-action" title="Immediately test voice alert and radar siren">
<span>🔊</span>
<span>Test Alert Voice</span>
</button>
<button id="btn-sim-drive" class="btn-hud-action" title="Simulate driving through Palmdale camera corridors">
<span></span>
<span>Simulate Palmdale Drive</span>
</button>
<button id="btn-reset-sim" class="btn-hud-action" title="Reset drive position">
<span></span>
</button>
<button id="btn-mute-sound" class="btn-hud-action" title="Toggle audio alert chime and voice">
<span>🔔</span>
<span>Sound: ON</span>
</button>
<button id="btn-mobile-connect" class="btn-hud-action" title="Show mobile network link to open on phone in car">
<span>📱</span>
<span>Phone Link</span>
</button>
</div>
<!-- Floating Map Legend (Bottom-Left) -->
<div class="map-legend">
<div class="legend-header">
<span class="legend-pulse-dot"></span>
<span>RADAR ZONE LEGEND</span>
</div>
<div class="legend-content">
<div class="legend-item">
<span class="legend-sample-zone"></span>
<span>Camera Detection Zone (280-450m)</span>
</div>
<div class="legend-item">
<span class="legend-sample-pin"></span>
<span>ALPR Camera Node</span>
</div>
<div class="legend-item">
<span class="legend-sample-car"></span>
<span>Your Vehicle Heading</span>
</div>
</div>
</div>
<!-- Floating Recenter Pill (Appears when map is dragged away from car) -->
<button id="btn-recenter-car" class="btn-recenter-pill hidden" title="Re-center map on your vehicle">
<span>🎯</span>
<span>Recenter on Vehicle</span>
</button>
<!-- Giant 1-Tap Floating Action Button: Log Flock Camera (Bottom-Right) -->
<div class="driver-fab-container">
<button id="btn-quick-log-cam" class="btn-driver-fab" title="1-Tap: Log a Flock camera at your exact current GPS location">
<div class="fab-icon-glow">📸</div>
<div class="fab-text-group">
<span class="fab-main-title">+ LOG FLOCK CAMERA</span>
<span class="fab-sub-title">1-Tap Pin at GPS Location</span>
</div>
</button>
</div>
</section>
<!-- ========================================================================
VIEW 2: NATIONAL DIRECTORY (ORIGINAL PORTAL DATABASE)
======================================================================== -->
<div id="directory-view" style="display: none;">
<!-- Hero & Aggregate KPI Metrics -->
<header class="hero-section">
<div class="container">
<div class="hero-pill">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path>
</svg>
<span>CIVIC SURVEILLANCE TRANSPARENCY</span>
</div>
<h1 class="hero-title">
The Public Directory of <span class="gradient-text">Flock Safety Transparency Portals</span>
</h1>
<p class="hero-subtitle">
Search 1,439+ official police transparency portals detailing camera counts, 30-day vehicle detection volumes, database queries, and inter-agency sharing networks.
</p>
<!-- Stats Grid -->
<div class="stats-grid" id="stats-grid">
<div class="stat-card">
<div class="stat-number" id="stat-portals">1,439</div>
<div class="stat-label">Public Portals</div>
<div class="stat-subtext">Across 45 U.S. States</div>
</div>
<div class="stat-card radar-card">
<div class="stat-number" id="stat-cameras">37,250+</div>
<div class="stat-label">Cameras Reported</div>
<div class="stat-subtext">Active Roadway ALPR Nodes</div>
</div>
<div class="stat-card">
<div class="stat-number" id="stat-vehicles">346.7M</div>
<div class="stat-label">Vehicles Tracked</div>
<div class="stat-subtext">Rolling 30-Day Detections</div>
</div>
<div class="stat-card">
<div class="stat-number" id="stat-searches">256,957</div>
<div class="stat-label">Police Searches</div>
<div class="stat-subtext">Database Queries Executed</div>
</div>
<div class="stat-card radar-card">
<div class="stat-number" id="stat-hits">10.2M+</div>
<div class="stat-label">Hotlist Hits</div>
<div class="stat-subtext">Alerts Pushed to Cruisers</div>
</div>
</div>
</div>
</header>
<!-- Main Directory Content -->
<main class="container" id="directory-section">
<!-- State Quick Jump Bar -->
<div class="state-chips-wrapper">
<div class="state-chips-header">
<span class="chips-title">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"></polygon>
</svg>
Quick Filter by Top States
</span>
<button class="btn-secondary" id="btn-reset-all" style="padding: 4px 10px; font-size: 0.75rem;">Reset All Filters</button>
</div>
<div class="state-chips-scroll" id="state-chips-scroll"></div>
</div>
<!-- Search & Controls Panel -->
<div class="controls-panel">
<div class="search-input-wrapper">
<span class="search-icon" aria-hidden="true">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
</span>
<input
type="text"
id="search-input"
class="search-input"
placeholder="Search by city, county, agency name, or state (e.g. Lexington, Cobb County, Fort Worth)..."
autocomplete="off"
spellcheck="false"
>
<button id="clear-search-btn" class="clear-search-btn" title="Clear search"></button>
<span class="search-shortcut">/</span>
</div>
<div class="filter-controls-grid">
<div class="control-group">
<label for="state-filter" class="control-label">State</label>
<select id="state-filter" class="select-custom">
<option value="ALL">All States (45)</option>
</select>
</div>
<div class="control-group">
<label for="type-filter" class="control-label">Agency Type</label>
<select id="type-filter" class="select-custom">
<option value="ALL">All Agency Types</option>
<option value="PD">Police Department (PD)</option>
<option value="SO">Sheriff's Office (SO)</option>
<option value="OTHER">Campus / Airport / Other</option>
</select>
</div>
<div class="control-group">
<label for="sort-select" class="control-label">Sort Order</label>
<select id="sort-select" class="select-custom">
<option value="cameras-desc">Most Cameras Reported</option>
<option value="vehicles-desc">Most Vehicles Tracked</option>
<option value="searches-desc">Most Police Searches</option>
<option value="hits-desc">Most Hotlist Alerts</option>
<option value="alpha-asc">Agency Name (A to Z)</option>
<option value="alpha-desc">Agency Name (Z to A)</option>
</select>
</div>
<div class="control-group">
<label class="checkbox-toggle-label">
<input type="checkbox" id="audit-toggle">
<span class="toggle-switch"></span>
<span>Public Audit Available Only</span>
</label>
</div>
</div>
<div class="action-controls-row">
<div class="results-counter" id="results-counter">
Showing <strong id="showing-count">0</strong> of <strong id="total-count">1,439</strong> portals
</div>
<div class="view-actions-group">
<button class="btn-secondary" id="btn-export-csv" title="Download filtered dataset as CSV">
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="7 10 12 15 17 10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line>
</svg>
Export CSV
</button>
<div class="view-toggle-btns" role="radiogroup" aria-label="View Switcher">
<button class="view-btn active" id="view-cards-btn" title="Card View" aria-checked="true" role="radio">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<rect x="3" y="3" width="7" height="7"></rect>
<rect x="14" y="3" width="7" height="7"></rect>
<rect x="14" y="14" width="7" height="7"></rect>
<rect x="3" y="14" width="7" height="7"></rect>
</svg>
</button>
<button class="view-btn" id="view-table-btn" title="Table View" aria-checked="false" role="radio">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="8" y1="6" x2="21" y2="6"></line>
<line x1="8" y1="12" x2="21" y2="12"></line>
<line x1="8" y1="18" x2="21" y2="18"></line>
<line x1="3" y1="6" x2="3.01" y2="6"></line>
<line x1="3" y1="12" x2="3.01" y2="12"></line>
<line x1="3" y1="18" x2="3.01" y2="18"></line>
</svg>
</button>
</div>
</div>
</div>
</div>
<!-- Cards Grid View -->
<div class="portals-grid" id="portals-grid" aria-live="polite"></div>
<!-- Table View -->
<div class="table-container" id="table-container">
<table class="portals-table">
<thead>
<tr>
<th>Agency / Department</th>
<th>State</th>
<th>Cameras</th>
<th>30d Vehicles</th>
<th>Searches</th>
<th>Hotlist Alerts</th>
<th>Sharing</th>
<th>Action</th>
</tr>
</thead>
<tbody id="table-body"></tbody>
</table>
</div>
<!-- Pagination -->
<div class="pagination-wrapper" id="pagination-controls"></div>
</main>
<!-- Educational & Civil Liberties Section -->
<section class="education-section" id="education-section">
<div class="container">
<div class="education-header">
<h2 class="education-title">What You Need to Know About Flock Surveillance</h2>
<p class="education-desc">
Automated License Plate Readers (ALPR) represent one of the fastest-growing mass surveillance networks in United States history. Understanding how the technology works is essential for civic transparency.
</p>
</div>
<div class="education-grid">
<article class="edu-card">
<div class="edu-card-icon" aria-hidden="true">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"></path>
<circle cx="12" cy="13" r="4"></circle>
</svg>
</div>
<h3 class="edu-card-title">Beyond Plates: "Vehicle Fingerprint"</h3>
<p class="edu-card-text">
Flock cameras do not merely log plate numbers. Their computer vision models extract a full visual profile on every passing car:
</p>
<ul class="edu-card-list">
<li>Make, model, color, and trim styling</li>
<li>Bumper stickers, window decals, and roof racks</li>
<li>Dents, damage, aftermarket wheels, and missing hubcaps</li>
<li>Temporary and dealer paper tags</li>
</ul>
</article>
<article class="edu-card">
<div class="edu-card-icon" aria-hidden="true" style="background: rgba(239, 68, 68, 0.12); border-color: rgba(239, 68, 68, 0.3); color: var(--accent-radar);">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"></path>
</svg>
</div>
<h3 class="edu-card-title">Fourth Amendment & Warrantless Tracking</h3>
<p class="edu-card-text">
Civil liberties advocates argue that interconnected ALPR grids violate constitutional protections against warrantless dragnet tracking:
</p>
<ul class="edu-card-list">
<li><strong>Carpenter v. United States (2018):</strong> Individuals maintain a reasonable expectation of privacy in the whole of their physical movements.</li>
<li><strong>United States v. Jones (2012):</strong> Prolonged automated surveillance exceeds traditional visual police monitoring.</li>
<li><strong>Cross-County Sharing:</strong> Inter-agency networks allow out-of-town agencies to track movements across jurisdictions without warrants.</li>
</ul>
</article>
<article class="edu-card">
<div class="edu-card-icon" aria-hidden="true" style="background: rgba(99, 102, 241, 0.12); border-color: rgba(99, 102, 241, 0.3); color: #a5b4fc;">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="10"></circle>
<line x1="12" y1="16" x2="12" y2="12"></line>
<line x1="12" y1="8" x2="12.01" y2="8"></line>
</svg>
</div>
<h3 class="edu-card-title">How Transparency Portals Work</h3>
<p class="edu-card-text">
Transparency Portals provide valuable aggregate metrics, but have critical limitations that the public should understand:
</p>
<ul class="edu-card-list">
<li><strong>Portals Are Voluntary:</strong> Police departments are not required to publish them. Many agencies keep deployments confidential.</li>
<li><strong>Aggregate Only:</strong> Portals show monthly sums, not individual license plate queries.</li>
<li><strong>Private HOA Feeds:</strong> Cameras purchased by private homeowners associations and linked to police are often unlisted.</li>
</ul>
</article>
</div>
</div>
</section>
<!-- Footer -->
<footer class="site-footer">
<div class="container footer-content">
<div class="footer-disclaimer">
<strong>About This Directory:</strong> Independent public interest directory aggregating publicly accessible Flock Safety transparency portals. Data is sourced from municipal portals and civic surveillance watchdogs.
</div>
<div class="footer-links">
<a href="https://transparency.flocksafety.com" target="_blank" rel="noopener noreferrer" class="footer-link">Flock Transparency</a>
<a href="https://eyesonflock.com" target="_blank" rel="noopener noreferrer" class="footer-link">Eyes On Flock</a>
<a href="https://haveibeenflocked.com" target="_blank" rel="noopener noreferrer" class="footer-link">Audit Search</a>
</div>
</div>
</footer>
</div>
<!-- ========================================================================
MODAL 1: DEEP AGENCY INSPECTION MODAL (<dialog>)
======================================================================== -->
<dialog id="agency-modal" class="agency-dialog" aria-labelledby="modal-agency-name">
<div class="modal-header">
<div class="modal-header-titles">
<h2 id="modal-agency-name" class="modal-agency-name">Agency Name</h2>
<div class="modal-agency-meta">
<span id="modal-agency-location">City, ST</span>
<span></span>
<span id="modal-agency-slug" style="font-family: var(--font-mono); color: var(--accent-cyan);">slug</span>
</div>
</div>
<button id="modal-close-btn" class="close-modal-btn" aria-label="Close modal"></button>
</div>
<div class="modal-body">
<div class="modal-cta-box">
<div class="modal-cta-text">
<h4>Official Flock Transparency Portal</h4>
<p>Access the live, unedited municipal transparency dashboard directly on Flocks servers.</p>
</div>
<a href="#" id="modal-portal-url" target="_blank" rel="noopener noreferrer" class="btn-open-flock">
<span>Open Official Portal</span>
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
<polyline points="15 3 21 3 21 9"></polyline>
<line x1="10" y1="14" x2="21" y2="3"></line>
</svg>
</a>
</div>
<div class="modal-stats-grid">
<div class="modal-stat-box">
<div class="modal-stat-num" id="modal-stat-cameras">0</div>
<div class="modal-stat-label">Cameras Reported</div>
</div>
<div class="modal-stat-box">
<div class="modal-stat-num" id="modal-stat-vehicles">0</div>
<div class="modal-stat-label">30-Day Captures</div>
</div>
<div class="modal-stat-box">
<div class="modal-stat-num" id="modal-stat-searches">0</div>
<div class="modal-stat-label">Searches Conducted</div>
</div>
<div class="modal-stat-box">
<div class="modal-stat-num" id="modal-stat-hits">0</div>
<div class="modal-stat-label">Hotlist Alerts</div>
</div>
</div>
<div class="modal-section" id="modal-reasons-section">
<h3 class="modal-section-title">Reported Search Justifications</h3>
<div class="reasons-list" id="modal-reasons-list"></div>
</div>
<div class="modal-section">
<h3 class="modal-section-title">
Inter-Agency Sharing Network (<span id="modal-sharing-count">0</span> Departments)
</h3>
<p class="policy-text" style="margin-bottom: 12px; font-size: 0.8rem;">
Outside law enforcement agencies that can search vehicles logged by this agency's cameras.
</p>
<input type="text" id="modal-sharing-search" class="sharing-search-input" placeholder="Filter partner agencies...">
<div class="sharing-partners-box" id="modal-sharing-box"></div>
</div>
<div class="modal-section">
<h3 class="modal-section-title">Department Policy & Prohibited Uses</h3>
<p class="policy-text" id="modal-prohibited-text">Standard policy restrictions apply.</p>
</div>
<div class="modal-section">
<h3 class="modal-section-title">Public Records & Civic Research</h3>
<div class="research-tools-row">
<a href="#" id="modal-google-contract-link" target="_blank" rel="noopener noreferrer" class="btn-research">
Search City Council Contract & Minutes ↗
</a>
<button id="modal-copy-link-btn" class="btn-research">Copy Direct Portal Link</button>
</div>
</div>
</div>
</dialog>
<!-- ========================================================================
MODAL 2: WAZE-STYLE DRIVER LOGIN & PROFILE MODAL (<dialog>)
======================================================================== -->
<dialog id="login-modal" class="agency-dialog" aria-labelledby="login-modal-title">
<div class="modal-header">
<div class="modal-header-titles">
<h2 id="login-modal-title" class="modal-agency-name">Driver Profile & Scout Login</h2>
<div class="modal-agency-meta">Join the community awareness grid to contribute camera sightings</div>
</div>
<button id="login-close-btn" class="close-modal-btn" aria-label="Close modal"></button>
</div>
<div class="modal-body">
<form id="login-form">
<div class="form-group" style="margin-bottom: 16px;">
<label class="control-label" for="login-username" style="display:block; margin-bottom: 6px;">Driver Handle / Callsign</label>
<input type="text" id="login-username" class="search-input" style="padding: 12px;" placeholder="e.g. NightRider, HighwayScout" required>
</div>
<div class="form-group" style="margin-bottom: 16px;">
<label class="control-label" for="login-vehicle" style="display:block; margin-bottom: 6px;">Vehicle Description (For Scout Profile)</label>
<input type="text" id="login-vehicle" class="search-input" style="padding: 12px;" placeholder="e.g. 2022 Honda Civic (Blue)" value="Sedan (Silver)">
</div>
<div class="form-group" style="margin-bottom: 24px;">
<label class="checkbox-toggle-label" style="margin-top: 0;">
<input type="checkbox" id="login-sound" checked>
<span class="toggle-switch"></span>
<span>Enable Audio Chimes on Zone Entry</span>
</label>
</div>
<div style="display: flex; gap: 12px; flex-wrap: wrap;">
<button type="submit" class="btn-open-flock" style="flex: 1; justify-content: center; cursor: pointer; border: none;">
Save Profile & Connect
</button>
<button type="button" id="login-guest-btn" class="btn-secondary" style="padding: 10px 18px;">
Continue as Guest Scout
</button>
</div>
</form>
</div>
</dialog>
<!-- ========================================================================
MODAL 3: CROWDSOURCED CAMERA REPORT MODAL (<dialog>)
======================================================================== -->
<dialog id="report-modal" class="agency-dialog" aria-labelledby="report-modal-title">
<div class="modal-header">
<div class="modal-header-titles">
<h2 id="report-modal-title" class="modal-agency-name">Report Flock Camera Sighting</h2>
<div class="modal-agency-meta">Help alert the driver network to new ALPR surveillance installations</div>
</div>
<button id="report-close-btn" class="close-modal-btn" aria-label="Close modal"></button>
</div>
<div class="modal-body">
<form id="report-form">
<div class="form-group" style="margin-bottom: 16px;">
<label class="control-label" for="report-mount-type" style="display:block; margin-bottom: 6px;">Camera Installation Type</label>
<select id="report-mount-type" class="select-custom" style="width: 100%;">
<option value="Solar Pole Mount">Solar Pole Mount (Standard Falcon)</option>
<option value="Traffic Signal Mount">Traffic Signal / Utility Pole Mount</option>
<option value="Raven Audio Node">Raven Audio Sensor + Camera Combo</option>
<option value="Mobile Trailer ALPR">Mobile Surveillance Trailer</option>
</select>
</div>
<div class="form-group" style="margin-bottom: 16px;">
<label class="control-label" for="report-intersection" style="display:block; margin-bottom: 6px;">Intersection / Highway Marker</label>
<input type="text" id="report-intersection" class="search-input" style="padding: 12px;" placeholder="e.g. Main St & 4th Ave, Mile Marker 104" required>
</div>
<div class="form-group" style="margin-bottom: 16px;">
<label class="control-label" for="report-notes" style="display:block; margin-bottom: 6px;">Observations / Direction Facing</label>
<input type="text" id="report-notes" class="search-input" style="padding: 12px;" placeholder="e.g. Northbound lanes, black solar panel on pole">
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 24px;">
<div>
<label class="control-label" for="report-lat" style="display:block; margin-bottom: 6px;">Latitude</label>
<input type="number" step="0.00001" id="report-lat" class="search-input" style="padding: 10px;" required>
</div>
<div>
<label class="control-label" for="report-lng" style="display:block; margin-bottom: 6px;">Longitude</label>
<input type="number" step="0.00001" id="report-lng" class="search-input" style="padding: 10px;" required>
</div>
</div>
<button type="submit" class="btn-open-flock" style="width: 100%; justify-content: center; cursor: pointer; border: none;">
Broadcast Camera Alert to Network (+50 pts)
</button>
</form>
</div>
</dialog>
<!-- Toast Container -->
<div id="toast-container" class="toast-container" aria-live="assertive"></div>
<!-- Leaflet Map JS -->
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>
<!-- Data & Application Logic -->
<script src="data/portals.js"></script>
<script src="js/auth.js"></script>
<script src="js/radar.js"></script>
<script src="js/app.js"></script>
</body>
</html>
+931
View File
@@ -0,0 +1,931 @@
/**
* FLOCK SAFETY TRANSPARENCY PORTALS DIRECTORY
* Client Application Logic
* High-performance search, filtering, pagination, modal inspector, and CSV export.
*/
(function() {
'use strict';
// --- APPLICATION STATE ---
const state = {
allPortals: [],
orgsLookup: [],
summary: {},
filteredPortals: [],
currentPage: 1,
pageSize: 24,
currentView: 'cards', // 'cards' or 'table'
searchQuery: '',
selectedState: 'ALL',
selectedType: 'ALL',
auditOnly: false,
sortBy: 'cameras-desc',
activeModalPortal: null
};
// --- DOM ELEMENTS ---
const el = {
// Search & Filters
searchInput: document.getElementById('search-input'),
clearSearchBtn: document.getElementById('clear-search-btn'),
stateFilter: document.getElementById('state-filter'),
typeFilter: document.getElementById('type-filter'),
sortSelect: document.getElementById('sort-select'),
auditToggle: document.getElementById('audit-toggle'),
btnResetAll: document.getElementById('btn-reset-all'),
stateChipsScroll: document.getElementById('state-chips-scroll'),
// Stats Counters
statPortals: document.getElementById('stat-portals'),
statCameras: document.getElementById('stat-cameras'),
statVehicles: document.getElementById('stat-vehicles'),
statSearches: document.getElementById('stat-searches'),
statHits: document.getElementById('stat-hits'),
// Views & Results
resultsCounter: document.getElementById('results-counter'),
showingCount: document.getElementById('showing-count'),
totalCount: document.getElementById('total-count'),
portalsGrid: document.getElementById('portals-grid'),
tableContainer: document.getElementById('table-container'),
tableBody: document.getElementById('table-body'),
paginationControls: document.getElementById('pagination-controls'),
viewCardsBtn: document.getElementById('view-cards-btn'),
viewTableBtn: document.getElementById('view-table-btn'),
btnExportCsv: document.getElementById('btn-export-csv'),
// Modal Elements
agencyModal: document.getElementById('agency-modal'),
modalCloseBtn: document.getElementById('modal-close-btn'),
modalAgencyName: document.getElementById('modal-agency-name'),
modalAgencyLocation: document.getElementById('modal-agency-location'),
modalAgencySlug: document.getElementById('modal-agency-slug'),
modalPortalUrl: document.getElementById('modal-portal-url'),
modalStatCameras: document.getElementById('modal-stat-cameras'),
modalStatVehicles: document.getElementById('modal-stat-vehicles'),
modalStatSearches: document.getElementById('modal-stat-searches'),
modalStatHits: document.getElementById('modal-stat-hits'),
modalReasonsSection: document.getElementById('modal-reasons-section'),
modalReasonsList: document.getElementById('modal-reasons-list'),
modalSharingCount: document.getElementById('modal-sharing-count'),
modalSharingSearch: document.getElementById('modal-sharing-search'),
modalSharingBox: document.getElementById('modal-sharing-box'),
modalProhibitedText: document.getElementById('modal-prohibited-text'),
modalGoogleContractLink: document.getElementById('modal-google-contract-link'),
modalCopyLinkBtn: document.getElementById('modal-copy-link-btn'),
// Toast Container
toastContainer: document.getElementById('toast-container')
};
// --- INITIALIZATION ---
function init() {
if (!window.FLOCK_DATA || !window.FLOCK_DATA.portals) {
showToast('Error: Dataset failed to load.', 'error');
console.error('FLOCK_DATA not found');
return;
}
state.allPortals = window.FLOCK_DATA.portals || [];
state.orgsLookup = window.FLOCK_DATA.orgs || [];
state.summary = window.FLOCK_DATA.summary || {};
// Restore saved view preference
const savedView = localStorage.getItem('flock_dir_view');
if (savedView === 'table' || savedView === 'cards') {
state.currentView = savedView;
}
renderHeroStats();
populateStateFilter();
renderStateChips();
bindEvents();
// Initial Filter & Render
applyFilters();
updateViewMode();
// Check URL Hash for deep link
checkUrlHash();
}
// --- HERO STATS FORMATTING ---
function renderHeroStats() {
const s = state.summary;
if (!s) return;
if (s.total_portals_found) {
el.statPortals.textContent = s.total_portals_found.toLocaleString();
}
if (s.total_cameras) {
el.statCameras.textContent = s.total_cameras.toLocaleString() + '+';
}
if (s.total_vehicles_captured) {
const millions = (s.total_vehicles_captured / 1000000).toFixed(1);
el.statVehicles.textContent = millions + 'M';
}
if (s.total_searches) {
el.statSearches.textContent = s.total_searches.toLocaleString();
}
if (s.total_hotlist_hits) {
const millions = (s.total_hotlist_hits / 1000000).toFixed(1);
el.statHits.textContent = millions + 'M+';
}
}
// --- POPULATE STATE SELECTOR & CHIPS ---
function populateStateFilter() {
const stateCounts = {};
state.allPortals.forEach(p => {
const st = p.state || 'OTHER';
stateCounts[st] = (stateCounts[st] || 0) + 1;
});
const sortedStates = Object.keys(stateCounts).sort();
// Clear existing options except ALL
el.stateFilter.innerHTML = `<option value="ALL">All States (${state.allPortals.length} portals)</option>`;
sortedStates.forEach(st => {
const opt = document.createElement('option');
opt.value = st;
opt.textContent = `${getStateFullName(st)} (${st}) — ${stateCounts[st]} portals`;
el.stateFilter.appendChild(opt);
});
}
function renderStateChips() {
const stateCounts = {};
state.allPortals.forEach(p => {
const st = p.state || 'OTHER';
stateCounts[st] = (stateCounts[st] || 0) + 1;
});
// Top states by portal count
const topStates = Object.entries(stateCounts)
.sort((a, b) => b[1] - a[1])
.slice(0, 16);
let chipsHtml = `
<button class="state-chip ${state.selectedState === 'ALL' ? 'active' : ''}" data-state="ALL">
<span>All States</span>
<span class="chip-count">(${state.allPortals.length})</span>
</button>
`;
topStates.forEach(([st, count]) => {
const isActive = state.selectedState === st;
chipsHtml += `
<button class="state-chip ${isActive ? 'active' : ''}" data-state="${st}">
<span>${st}</span>
<span class="chip-count">(${count})</span>
</button>
`;
});
el.stateChipsScroll.innerHTML = chipsHtml;
}
// --- STATE FULL NAME HELPER ---
const stateNames = {
AL: 'Alabama', AK: 'Alaska', AZ: 'Arizona', AR: 'Arkansas', CA: 'California',
CO: 'Colorado', CT: 'Connecticut', DE: 'Delaware', FL: 'Florida', GA: 'Georgia',
HI: 'Hawaii', ID: 'Idaho', IL: 'Illinois', IN: 'Indiana', IA: 'Iowa',
KS: 'Kansas', KY: 'Kentucky', LA: 'Louisiana', ME: 'Maine', MD: 'Maryland',
MA: 'Massachusetts', MI: 'Michigan', MN: 'Minnesota', MS: 'Mississippi', MO: 'Missouri',
MT: 'Montana', NE: 'Nebraska', NV: 'Nevada', NH: 'New Hampshire', NJ: 'New Jersey',
NM: 'New Mexico', NY: 'New York', NC: 'North Carolina', ND: 'North Dakota', OH: 'Ohio',
OK: 'Oklahoma', OR: 'Oregon', PA: 'Pennsylvania', RI: 'Rhode Island', SC: 'South Carolina',
SD: 'South Dakota', TN: 'Tennessee', TX: 'Texas', UT: 'Utah', VT: 'Vermont',
VA: 'Virginia', WA: 'Washington', WV: 'West Virginia', WI: 'Wisconsin', WY: 'Wyoming'
};
function getStateFullName(abbr) {
return stateNames[abbr] || abbr;
}
// --- FILTER & SORT LOGIC ---
function applyFilters() {
const q = state.searchQuery.trim().toLowerCase();
const st = state.selectedState;
const deptType = state.selectedType;
const auditOnly = state.auditOnly;
let filtered = state.allPortals.filter(p => {
// Search query filter
if (q) {
const matchName = (p.name || '').toLowerCase().includes(q);
const matchLoc = (p.location || '').toLowerCase().includes(q);
const matchSlug = (p.slug || '').toLowerCase().includes(q);
const matchState = (p.state || '').toLowerCase().includes(q);
const matchStateFull = getStateFullName(p.state).toLowerCase().includes(q);
if (!matchName && !matchLoc && !matchSlug && !matchState && !matchStateFull) {
return false;
}
}
// State filter
if (st !== 'ALL' && p.state !== st) {
return false;
}
// Department type filter
if (deptType !== 'ALL') {
const pType = (p.dept_type || '').toUpperCase();
if (deptType === 'PD' && pType !== 'PD') return false;
if (deptType === 'SO' && pType !== 'SO') return false;
if (deptType === 'OTHER' && (pType === 'PD' || pType === 'SO')) return false;
}
// Audit status filter
if (auditOnly && !p.has_search_audit) {
return false;
}
return true;
});
// Sorting
filtered.sort((a, b) => {
switch (state.sortBy) {
case 'cameras-desc':
return (b.cameras || 0) - (a.cameras || 0);
case 'vehicles-desc':
return (b.vehicles_captured || 0) - (a.vehicles_captured || 0);
case 'searches-desc':
return (b.searches || 0) - (a.searches || 0);
case 'hits-desc':
return (b.hotlist_hits || 0) - (a.hotlist_hits || 0);
case 'alpha-asc':
return (a.name || '').localeCompare(b.name || '');
case 'alpha-desc':
return (b.name || '').localeCompare(a.name || '');
default:
return 0;
}
});
state.filteredPortals = filtered;
state.currentPage = 1;
renderDirectory();
}
// --- RENDER MAIN DIRECTORY ---
function renderDirectory() {
const total = state.filteredPortals.length;
el.totalCount.textContent = state.allPortals.length.toLocaleString();
el.showingCount.textContent = total.toLocaleString();
// Pagination slice
const startIndex = (state.currentPage - 1) * state.pageSize;
const endIndex = Math.min(startIndex + state.pageSize, total);
const pagePortals = state.filteredPortals.slice(startIndex, endIndex);
if (total === 0) {
const emptyHtml = `
<div style="grid-column: 1 / -1; text-align: center; padding: 60px 20px; background: var(--bg-surface); border: 1px dashed var(--border-subtle); border-radius: var(--radius-lg);">
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="var(--text-dim)" stroke-width="1.5" style="margin-bottom: 16px;">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
<h3 style="font-size: 1.25rem; font-weight: 700; margin-bottom: 8px; color: #ffffff;">No Matching Portals Found</h3>
<p style="color: var(--text-muted); max-width: 450px; margin: 0 auto 20px;">
No Flock transparency portals matched your criteria: "${state.searchQuery || state.selectedState}".
</p>
<button class="btn-secondary" id="btn-empty-reset">Clear Filters & View All</button>
</div>
`;
el.portalsGrid.innerHTML = emptyHtml;
el.tableBody.innerHTML = `<tr><td colspan="8" style="text-align: center; padding: 40px; color: var(--text-muted);">No portals found matching search criteria.</td></tr>`;
el.paginationControls.innerHTML = '';
document.getElementById('btn-empty-reset')?.addEventListener('click', resetAllFilters);
return;
}
if (state.currentView === 'cards') {
renderCards(pagePortals);
} else {
renderTable(pagePortals);
}
renderPagination(total);
}
// --- RENDER CARDS VIEW ---
function renderCards(portals) {
const cardsHtml = portals.map(p => {
const camerasStr = p.cameras ? p.cameras.toLocaleString() : '—';
const vehiclesStr = p.vehicles_captured ? formatNumberCompact(p.vehicles_captured) : '—';
const searchesStr = p.searches ? p.searches.toLocaleString() : '—';
const hitsStr = p.hotlist_hits ? p.hotlist_hits.toLocaleString() : '—';
const sharedCount = p.shared_count || 0;
return `
<article class="portal-card" data-slug="${escapeHtml(p.slug)}">
<div>
<div class="card-top-bar">
<span class="agency-state-badge">${escapeHtml(p.state)}</span>
<span class="agency-type-tag">${escapeHtml(p.dept_type || 'PD')}</span>
</div>
<div class="card-title-area">
<h3 class="agency-name">${escapeHtml(p.name)}</h3>
<div class="agency-location">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"></path>
<circle cx="12" cy="10" r="3"></circle>
</svg>
<span>${escapeHtml(p.location || p.name)}, ${escapeHtml(p.state)}</span>
</div>
</div>
<!-- Metrics Grid -->
<div class="card-metrics-grid">
<div class="card-metric-item">
<span class="card-metric-val">${camerasStr}</span>
<span class="card-metric-lbl">Cameras</span>
</div>
<div class="card-metric-item">
<span class="card-metric-val">${vehiclesStr}</span>
<span class="card-metric-lbl">30d Captures</span>
</div>
<div class="card-metric-item">
<span class="card-metric-val">${searchesStr}</span>
<span class="card-metric-lbl">Searches</span>
</div>
<div class="card-metric-item">
<span class="card-metric-val">${hitsStr}</span>
<span class="card-metric-lbl">Hotlist Hits</span>
</div>
</div>
<!-- Flags -->
<div class="card-flags-row">
${p.has_search_audit ? `
<span class="flag-badge audit-true" title="Agency publishes public search audit logs">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
Search Audit Public
</span>
` : ''}
${sharedCount > 0 ? `
<span class="flag-badge sharing-network" title="Shares camera data with outside agencies">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"></path>
<circle cx="9" cy="7" r="4"></circle>
</svg>
Shares with ${sharedCount.toLocaleString()} Agenc${sharedCount === 1 ? 'y' : 'ies'}
</span>
` : ''}
</div>
</div>
<!-- Card Actions -->
<div class="card-actions-row">
<button class="btn-inspect" data-action="inspect" data-slug="${escapeHtml(p.slug)}">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="11" cy="11" r="8"></circle>
<line x1="21" y1="21" x2="16.65" y2="16.65"></line>
</svg>
<span>Inspect Data</span>
</button>
<a
href="${escapeHtml(p.url)}"
target="_blank"
rel="noopener noreferrer"
class="btn-direct-link"
title="Open live portal on transparency.flocksafety.com"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
<polyline points="15 3 21 3 21 9"></polyline>
<line x1="10" y1="14" x2="21" y2="3"></line>
</svg>
</a>
</div>
</article>
`;
}).join('');
el.portalsGrid.innerHTML = cardsHtml;
}
// --- RENDER TABLE VIEW ---
function renderTable(portals) {
const tableHtml = portals.map(p => {
const camerasStr = p.cameras ? p.cameras.toLocaleString() : '—';
const vehiclesStr = p.vehicles_captured ? p.vehicles_captured.toLocaleString() : '—';
const searchesStr = p.searches ? p.searches.toLocaleString() : '—';
const hitsStr = p.hotlist_hits ? p.hotlist_hits.toLocaleString() : '—';
const sharedCount = p.shared_count || 0;
return `
<tr>
<td>
<div class="table-agency-cell">
<span class="table-agency-title">${escapeHtml(p.name)}</span>
<span class="table-agency-slug">${escapeHtml(p.slug)}</span>
</div>
</td>
<td><span class="agency-state-badge">${escapeHtml(p.state)}</span></td>
<td style="font-family: var(--font-mono); font-weight: 700;">${camerasStr}</td>
<td style="font-family: var(--font-mono);">${vehiclesStr}</td>
<td style="font-family: var(--font-mono);">${searchesStr}</td>
<td style="font-family: var(--font-mono); color: var(--accent-radar);">${hitsStr}</td>
<td>
<span style="font-size: 0.78rem; color: #a5b4fc;">${sharedCount} orgs</span>
</td>
<td>
<div style="display: flex; gap: 6px;">
<button class="btn-secondary" style="padding: 4px 10px; font-size: 0.75rem;" data-action="inspect" data-slug="${escapeHtml(p.slug)}">Inspect</button>
<a href="${escapeHtml(p.url)}" target="_blank" rel="noopener noreferrer" class="btn-direct-link" style="padding: 4px 8px;" title="Open Portal">↗</a>
</div>
</td>
</tr>
`;
}).join('');
el.tableBody.innerHTML = tableHtml;
}
// --- PAGINATION RENDERER ---
function renderPagination(total) {
const totalPages = Math.ceil(total / state.pageSize);
if (totalPages <= 1) {
el.paginationControls.innerHTML = '';
return;
}
let pagesHtml = '';
// Previous Button
pagesHtml += `
<button class="pagination-btn" id="pg-prev" ${state.currentPage === 1 ? 'disabled' : ''}>
← Prev
</button>
`;
// Windowed Page Numbers
const current = state.currentPage;
let startPage = Math.max(1, current - 2);
let endPage = Math.min(totalPages, current + 2);
if (startPage > 1) {
pagesHtml += `<button class="pagination-btn" data-page="1">1</button>`;
if (startPage > 2) pagesHtml += `<span style="color: var(--text-dim); padding: 0 4px;">...</span>`;
}
for (let i = startPage; i <= endPage; i++) {
pagesHtml += `
<button class="pagination-btn ${i === current ? 'active' : ''}" data-page="${i}">
${i}
</button>
`;
}
if (endPage < totalPages) {
if (endPage < totalPages - 1) pagesHtml += `<span style="color: var(--text-dim); padding: 0 4px;">...</span>`;
pagesHtml += `<button class="pagination-btn" data-page="${totalPages}">${totalPages}</button>`;
}
// Next Button
pagesHtml += `
<button class="pagination-btn" id="pg-next" ${state.currentPage === totalPages ? 'disabled' : ''}>
Next →
</button>
`;
el.paginationControls.innerHTML = pagesHtml;
}
// --- DEEP AGENCY MODAL HANDLER ---
function openAgencyModal(slug) {
const portal = state.allPortals.find(p => p.slug === slug);
if (!portal) {
showToast('Agency record not found.', 'error');
return;
}
state.activeModalPortal = portal;
// Fill Basic Details
el.modalAgencyName.textContent = portal.name;
el.modalAgencyLocation.textContent = `${portal.location || portal.name}, ${portal.state}`;
el.modalAgencySlug.textContent = `transparency.flocksafety.com/${portal.slug}`;
el.modalPortalUrl.href = portal.url;
// Stats
el.modalStatCameras.textContent = portal.cameras ? portal.cameras.toLocaleString() : 'Not Reported';
el.modalStatVehicles.textContent = portal.vehicles_captured ? portal.vehicles_captured.toLocaleString() : 'Not Reported';
el.modalStatSearches.textContent = portal.searches ? portal.searches.toLocaleString() : '0';
el.modalStatHits.textContent = portal.hotlist_hits ? portal.hotlist_hits.toLocaleString() : '0';
// Search Justifications / Reasons
const reasons = portal.top_search_reasons || {};
const reasonEntries = Object.entries(reasons).sort((a, b) => b[1] - a[1]);
if (reasonEntries.length > 0) {
el.modalReasonsSection.style.display = 'block';
el.modalReasonsList.innerHTML = reasonEntries.map(([reason, count]) => `
<div class="reason-item">
<span class="reason-name">${escapeHtml(reason)}</span>
<span class="reason-count">${count.toLocaleString()} searches</span>
</div>
`).join('');
} else {
el.modalReasonsSection.style.display = 'none';
}
// Inter-Agency Sharing Network
const sharedIds = portal.shared_org_ids || [];
el.modalSharingCount.textContent = sharedIds.length.toLocaleString();
renderModalSharingTags(sharedIds, '');
// Reset sharing search input
el.modalSharingSearch.value = '';
// Prohibited Uses Text
el.modalProhibitedText.textContent = portal.prohibited_uses ||
'Prohibited uses standardly include: Immigration enforcement, general traffic speed citations, harassment, or personal/non-official inquiries.';
// Research Tool Links
const googleQuery = encodeURIComponent(`"${portal.name}" "Flock Safety" contract OR council OR resolution`);
el.modalGoogleContractLink.href = `https://www.google.com/search?q=${googleQuery}`;
// Set URL Hash
window.location.hash = portal.slug;
// Open Native Dialog
if (typeof el.agencyModal.showModal === 'function') {
el.agencyModal.showModal();
} else {
el.agencyModal.setAttribute('open', '');
}
}
function renderModalSharingTags(sharedIds, query) {
if (!sharedIds || sharedIds.length === 0) {
el.modalSharingBox.innerHTML = `<span style="font-size: 0.8rem; color: var(--text-dim); padding: 8px;">No external sharing partners published on portal.</span>`;
return;
}
const q = (query || '').toLowerCase().trim();
let orgNames = sharedIds.map(id => state.orgsLookup[id] || 'Unknown Agency');
if (q) {
orgNames = orgNames.filter(name => name.toLowerCase().includes(q));
}
if (orgNames.length === 0) {
el.modalSharingBox.innerHTML = `<span style="font-size: 0.8rem; color: var(--text-dim); padding: 8px;">No matching partner agencies.</span>`;
return;
}
// Show up to 200 partner agencies in the box
const displayOrgs = orgNames.slice(0, 250);
const tagsHtml = displayOrgs.map(name => `
<span class="sharing-tag">${escapeHtml(name)}</span>
`).join('');
const moreNotice = orgNames.length > 250
? `<span style="font-size: 0.72rem; color: var(--text-dim); padding: 4px;">...and ${(orgNames.length - 250).toLocaleString()} more. Use search to filter.</span>`
: '';
el.modalSharingBox.innerHTML = tagsHtml + moreNotice;
}
function closeAgencyModal() {
if (typeof el.agencyModal.close === 'function') {
el.agencyModal.close();
} else {
el.agencyModal.removeAttribute('open');
}
state.activeModalPortal = null;
// Clear hash without reloading
history.replaceState(null, document.title, window.location.pathname + window.location.search);
}
// --- CSV EXPORT FUNCTIONALITY ---
function exportFilteredToCsv() {
const list = state.filteredPortals;
if (!list || list.length === 0) {
showToast('No portals to export.', 'warning');
return;
}
const headers = [
'Agency Name',
'State',
'Location',
'Department Type',
'Cameras Reported',
'30-Day Vehicle Captures',
'Searches Conducted',
'Hotlist Alerts',
'Public Search Audit',
'Shared Agency Count',
'Transparency Portal URL'
];
const rows = list.map(p => [
`"${(p.name || '').replace(/"/g, '""')}"`,
`"${p.state || ''}"`,
`"${(p.location || '').replace(/"/g, '""')}"`,
`"${p.dept_type || ''}"`,
p.cameras || 0,
p.vehicles_captured || 0,
p.searches || 0,
p.hotlist_hits || 0,
p.has_search_audit ? 'Yes' : 'No',
p.shared_count || 0,
`"${p.url || ''}"`
]);
const csvContent = [headers.join(','), ...rows.map(r => r.join(','))].join('\n');
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
const url = URL.createObjectURL(blob);
const link = document.createElement('a');
const timestamp = new Date().toISOString().slice(0, 10);
link.setAttribute('href', url);
link.setAttribute('download', `flock_transparency_portals_${state.selectedState.toLowerCase()}_${timestamp}.csv`);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
showToast(`Exported ${list.length.toLocaleString()} portals to CSV.`, 'success');
}
// --- TOAST NOTIFICATIONS ---
function showToast(message, type = 'info') {
const toast = document.createElement('div');
toast.className = 'toast';
let icon = '️';
if (type === 'success') icon = '✓';
if (type === 'error') icon = '⚠️';
if (type === 'warning') icon = '⚡';
toast.innerHTML = `<span>${icon}</span><span>${escapeHtml(message)}</span>`;
el.toastContainer.appendChild(toast);
setTimeout(() => {
toast.style.opacity = '0';
toast.style.transform = 'translateY(8px)';
toast.style.transition = '0.25s ease';
setTimeout(() => toast.remove(), 250);
}, 3200);
}
// --- VIEW MODE TOGGLE ---
function updateViewMode() {
if (state.currentView === 'cards') {
el.portalsGrid.style.display = 'grid';
el.tableContainer.style.display = 'none';
el.viewCardsBtn.classList.add('active');
el.viewTableBtn.classList.remove('active');
el.viewCardsBtn.setAttribute('aria-checked', 'true');
el.viewTableBtn.setAttribute('aria-checked', 'false');
} else {
el.portalsGrid.style.display = 'none';
el.tableContainer.style.display = 'block';
el.viewCardsBtn.classList.remove('active');
el.viewTableBtn.classList.add('active');
el.viewCardsBtn.setAttribute('aria-checked', 'false');
el.viewTableBtn.setAttribute('aria-checked', 'true');
}
localStorage.setItem('flock_dir_view', state.currentView);
}
// --- RESET FILTERS ---
function resetAllFilters() {
state.searchQuery = '';
state.selectedState = 'ALL';
state.selectedType = 'ALL';
state.auditOnly = false;
state.sortBy = 'cameras-desc';
el.searchInput.value = '';
el.clearSearchBtn.style.display = 'none';
el.stateFilter.value = 'ALL';
el.typeFilter.value = 'ALL';
el.sortSelect.value = 'cameras-desc';
el.auditToggle.checked = false;
renderStateChips();
applyFilters();
showToast('Filters reset to default view.');
}
// --- URL HASH CHECK ---
function checkUrlHash() {
const hash = window.location.hash.replace('#', '').trim();
if (hash) {
const match = state.allPortals.find(p => p.slug === hash);
if (match) {
openAgencyModal(hash);
}
}
}
// --- EVENT BINDINGS ---
function bindEvents() {
// Search input with debounce
let debounceTimer;
el.searchInput.addEventListener('input', e => {
clearTimeout(debounceTimer);
state.searchQuery = e.target.value;
el.clearSearchBtn.style.display = state.searchQuery ? 'block' : 'none';
debounceTimer = setTimeout(() => {
applyFilters();
}, 150);
});
// Clear search button
el.clearSearchBtn.addEventListener('click', () => {
el.searchInput.value = '';
state.searchQuery = '';
el.clearSearchBtn.style.display = 'none';
el.searchInput.focus();
applyFilters();
});
// Keyboard shortcut '/' to search
window.addEventListener('keydown', e => {
if (e.key === '/' && document.activeElement !== el.searchInput) {
e.preventDefault();
el.searchInput.focus();
el.searchInput.select();
}
});
// State dropdown filter
el.stateFilter.addEventListener('change', e => {
state.selectedState = e.target.value;
renderStateChips();
applyFilters();
});
// State chips click
el.stateChipsScroll.addEventListener('click', e => {
const chip = e.target.closest('.state-chip');
if (!chip) return;
const st = chip.getAttribute('data-state');
if (!st) return;
state.selectedState = st;
el.stateFilter.value = st;
renderStateChips();
applyFilters();
});
// Department type filter
el.typeFilter.addEventListener('change', e => {
state.selectedType = e.target.value;
applyFilters();
});
// Sort select
el.sortSelect.addEventListener('change', e => {
state.sortBy = e.target.value;
applyFilters();
});
// Public Audit toggle
el.auditToggle.addEventListener('change', e => {
state.auditOnly = e.target.checked;
applyFilters();
});
// Reset all filters button
el.btnResetAll.addEventListener('click', resetAllFilters);
// View toggle buttons
el.viewCardsBtn.addEventListener('click', () => {
state.currentView = 'cards';
updateViewMode();
renderDirectory();
});
el.viewTableBtn.addEventListener('click', () => {
state.currentView = 'table';
updateViewMode();
renderDirectory();
});
// Export CSV button
el.btnExportCsv.addEventListener('click', exportFilteredToCsv);
// Card / Table click delegation for Inspect buttons
document.addEventListener('click', e => {
const btn = e.target.closest('[data-action="inspect"]');
if (btn) {
const slug = btn.getAttribute('data-slug');
if (slug) openAgencyModal(slug);
}
});
// Pagination button clicks
el.paginationControls.addEventListener('click', e => {
const btn = e.target.closest('.pagination-btn');
if (!btn || btn.disabled) return;
if (btn.id === 'pg-prev') {
state.currentPage = Math.max(1, state.currentPage - 1);
} else if (btn.id === 'pg-next') {
const maxPage = Math.ceil(state.filteredPortals.length / state.pageSize);
state.currentPage = Math.min(maxPage, state.currentPage + 1);
} else {
const pageNum = parseInt(btn.getAttribute('data-page'), 10);
if (pageNum) state.currentPage = pageNum;
}
renderDirectory();
// Smooth scroll back to top of directory
document.getElementById('directory-section').scrollIntoView({ behavior: 'smooth' });
});
// Modal Close button
el.modalCloseBtn.addEventListener('click', closeAgencyModal);
// Modal Click-Outside Backdrop Dismissal
el.agencyModal.addEventListener('click', e => {
const rect = el.agencyModal.getBoundingClientRect();
const isInDialog = (
rect.top <= e.clientY &&
e.clientY <= rect.top + rect.height &&
rect.left <= e.clientX &&
e.clientX <= rect.left + rect.width
);
if (!isInDialog) {
closeAgencyModal();
}
});
// Modal ESC Key
el.agencyModal.addEventListener('cancel', e => {
e.preventDefault();
closeAgencyModal();
});
// Modal Sharing Partners live search input
el.modalSharingSearch.addEventListener('input', e => {
if (!state.activeModalPortal) return;
const sharedIds = state.activeModalPortal.shared_org_ids || [];
renderModalSharingTags(sharedIds, e.target.value);
});
// Modal Copy Link Button
el.modalCopyLinkBtn.addEventListener('click', () => {
if (!state.activeModalPortal) return;
const shareUrl = `${window.location.origin}${window.location.pathname}#${state.activeModalPortal.slug}`;
navigator.clipboard.writeText(shareUrl).then(() => {
showToast('Direct link copied to clipboard!', 'success');
}).catch(() => {
showToast(`Link: ${shareUrl}`, 'info');
});
});
// Hash change event (forward/back browser navigation)
window.addEventListener('hashchange', checkUrlHash);
}
// --- UTILITY HELPERS ---
function escapeHtml(str) {
if (!str) return '';
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
function formatNumberCompact(num) {
if (num >= 1000000) {
return (num / 1000000).toFixed(1) + 'M';
}
if (num >= 1000) {
return (num / 1000).toFixed(0) + 'K';
}
return num.toLocaleString();
}
// --- START APP ON DOM READY ---
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();
+146
View File
@@ -0,0 +1,146 @@
/**
* FLOCKRADAR - USER AUTHENTICATION & COMMUNITY STATE MANAGER
* Waze-style driver profile, preferences, and crowdsourced reporting store.
*/
(function() {
'use strict';
const STORAGE_KEY_USER = 'flock_radar_user';
const STORAGE_KEY_REPORTS = 'flock_community_reports';
// Default Guest Profile
const defaultUser = {
isLoggedIn: false,
username: 'Guest Driver',
callsign: 'SCOUT-104',
level: 1,
points: 120,
vehicle: 'Sedan (Silver)',
soundEnabled: true,
alertsMuted: false,
reportsCount: 0
};
// State
let currentUser = null;
let communityReports = [];
function init() {
loadUser();
loadReports();
}
function loadUser() {
try {
const stored = localStorage.getItem(STORAGE_KEY_USER);
if (stored) {
currentUser = JSON.parse(stored);
} else {
currentUser = { ...defaultUser };
}
} catch (e) {
currentUser = { ...defaultUser };
}
}
function saveUser() {
try {
localStorage.setItem(STORAGE_KEY_USER, JSON.stringify(currentUser));
window.dispatchEvent(new CustomEvent('flock_user_updated', { detail: currentUser }));
} catch (e) {
console.error('Failed to save user session', e);
}
}
function login(username, vehicle, soundEnabled) {
currentUser = {
isLoggedIn: true,
username: username || 'Citizen Scout',
callsign: 'RADAR-' + Math.floor(100 + Math.random() * 900),
level: 2,
points: 250,
vehicle: vehicle || 'Standard Vehicle',
soundEnabled: soundEnabled !== false,
alertsMuted: false,
reportsCount: communityReports.length
};
saveUser();
return currentUser;
}
function logout() {
currentUser = { ...defaultUser };
saveUser();
}
function loadReports() {
try {
const stored = localStorage.getItem(STORAGE_KEY_REPORTS);
if (stored) {
communityReports = JSON.parse(stored);
} else {
communityReports = [];
}
} catch (e) {
communityReports = [];
}
}
function saveReports() {
try {
localStorage.setItem(STORAGE_KEY_REPORTS, JSON.stringify(communityReports));
window.dispatchEvent(new CustomEvent('flock_reports_updated', { detail: communityReports }));
} catch (e) {
console.error('Failed to save community reports', e);
}
}
function addReport(report) {
const newReport = {
id: 'rpt-' + Date.now(),
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',
timestamp: new Date().toISOString(),
confirmations: 1
};
communityReports.push(newReport);
saveReports();
if (currentUser) {
currentUser.points = (currentUser.points || 0) + 50;
currentUser.reportsCount = (currentUser.reportsCount || 0) + 1;
saveUser();
}
return newReport;
}
function getUser() {
if (!currentUser) loadUser();
return currentUser;
}
function getReports() {
if (!communityReports) loadReports();
return communityReports;
}
// Export API to global scope
window.FlockAuth = {
init,
getUser,
login,
logout,
saveUser,
getReports,
addReport
};
init();
})();
+1453
View File
File diff suppressed because it is too large Load Diff
+17
View File
@@ -0,0 +1,17 @@
{
"name": "FlockRadar - ALPR Awareness & Live Navigation",
"short_name": "FlockRadar",
"description": "Real-time ALPR radar navigation app alerting drivers to active Flock camera recorded zones.",
"start_url": "./",
"display": "standalone",
"orientation": "portrait-primary",
"background_color": "#07090e",
"theme_color": "#07090e",
"icons": [
{
"src": "data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'%3E%3Crect width='100' height='100' rx='20' fill='%2307090e'/%3E%3Ccircle cx='50' cy='50' r='38' fill='%23ef4444' opacity='0.25'/%3E%3Ccircle cx='50' cy='50' r='20' fill='%23ef4444'/%3E%3Ccircle cx='50' cy='50' r='8' fill='%23ffffff'/%3E%3C/svg%3E",
"sizes": "192x192 512x512",
"type": "image/svg+xml"
}
]
}
+48
View File
@@ -0,0 +1,48 @@
import http.server
import socketserver
import ssl
import threading
import sys
import os
os.chdir(os.path.dirname(os.path.abspath(__file__)))
class NoCacheHandler(http.server.SimpleHTTPRequestHandler):
def end_headers(self):
self.send_header('Cache-Control', 'no-cache, no-store, must-revalidate')
self.send_header('Pragma', 'no-cache')
self.send_header('Expires', '0')
self.send_header('Access-Control-Allow-Origin', '*')
super().end_headers()
def run_http(port=8085):
try:
class ReusableServer(socketserver.TCPServer):
allow_reuse_address = True
with ReusableServer(('', port), NoCacheHandler) as httpd:
print(f'Serving HTTP on port {port} (http://localhost:{port} and http://192.168.1.249:{port})')
sys.stdout.flush()
httpd.serve_forever()
except Exception as e:
print(f'HTTP Server on {port} notice: {e}')
sys.stdout.flush()
def run_https(port=8443):
try:
class ReusableServer(socketserver.TCPServer):
allow_reuse_address = True
with ReusableServer(('', port), NoCacheHandler) as httpd:
ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)
ctx.load_cert_chain(certfile='cert.pem', keyfile='key.pem')
httpd.socket = ctx.wrap_socket(httpd.socket, server_side=True)
print(f'Serving HTTPS on port {port} (https://localhost:{port} and https://192.168.1.249:{port})')
sys.stdout.flush()
httpd.serve_forever()
except Exception as e:
print(f'HTTPS Server on {port} notice: {e}')
sys.stdout.flush()
if __name__ == '__main__':
t_https = threading.Thread(target=run_https, args=(8443,), daemon=True)
t_https.start()
run_http(8085)
+35
View File
@@ -0,0 +1,35 @@
// Service Worker for FlockRadar Offline Driving & Caching
const CACHE_NAME = 'flockradar-v2';
const ASSETS_TO_CACHE = [
'./',
'./index.html',
'./css/styles.css',
'./js/app.js',
'./js/auth.js',
'./js/radar.js',
'./data/portals.js',
'./manifest.json'
];
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => cache.addAll(ASSETS_TO_CACHE))
);
self.skipWaiting();
});
self.addEventListener('activate', event => {
event.waitUntil(
caches.keys().then(keys => Promise.all(
keys.filter(key => key !== CACHE_NAME).map(key => caches.delete(key))
))
);
self.clients.claim();
});
self.addEventListener('fetch', event => {
// Network first, fallback to cache
event.respondWith(
fetch(event.request).catch(() => caches.match(event.request))
);
});