feat(directory): list specific camera nodes with cross-streets and photos in city search

This commit is contained in:
2026-09-12 09:43:21 -07:00
parent 0e47dfa1d8
commit be8981d024
5 changed files with 440 additions and 11 deletions
+196
View File
@@ -1277,6 +1277,202 @@ body {
color: var(--text-main); color: var(--text-main);
} }
/* Directory Specific Camera Nodes & Hardware Images Grid */
.directory-cameras-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 20px;
margin-bottom: 24px;
}
.dir-cam-card {
background: rgba(15, 23, 42, 0.85);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(56, 189, 248, 0.25);
border-radius: var(--radius-lg);
overflow: hidden;
display: flex;
flex-direction: column;
transition: transform 0.2s ease, border-color 0.2s ease, box-shadow 0.2s ease;
position: relative;
}
.dir-cam-card:hover {
transform: translateY(-4px);
border-color: #38bdf8;
box-shadow: 0 12px 30px rgba(0, 0, 0, 0.5), 0 0 20px rgba(6, 182, 212, 0.2);
}
.dir-cam-image-container {
width: 100%;
height: 180px;
background: #090d16;
position: relative;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
}
.dir-cam-img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
cursor: pointer;
}
.dir-cam-card:hover .dir-cam-img {
transform: scale(1.04);
}
.dir-cam-placeholder {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
background: linear-gradient(135deg, rgba(30, 41, 59, 0.6), rgba(15, 23, 42, 0.9));
color: #94a3b8;
padding: 16px;
text-align: center;
}
.dir-cam-placeholder-icon {
font-size: 2.2rem;
opacity: 0.8;
}
.dir-cam-badge {
position: absolute;
top: 10px;
left: 10px;
font-size: 0.72rem;
font-weight: 700;
padding: 4px 10px;
border-radius: 9999px;
backdrop-filter: blur(8px);
z-index: 2;
}
.dir-cam-badge.verified {
background: rgba(16, 185, 129, 0.85);
color: #fff;
border: 1px solid #34d399;
}
.dir-cam-badge.unverified {
background: rgba(245, 158, 11, 0.85);
color: #fff;
border: 1px solid #fcd34d;
}
.dir-cam-body {
padding: 18px 20px;
display: flex;
flex-direction: column;
flex-grow: 1;
justify-content: space-between;
}
.dir-cam-intersection {
font-family: var(--font-heading);
font-size: 1.05rem;
font-weight: 700;
color: #ffffff;
margin-bottom: 6px;
line-height: 1.35;
}
.dir-cam-agency-meta {
font-size: 0.82rem;
color: #38bdf8;
margin-bottom: 12px;
display: flex;
align-items: center;
gap: 6px;
}
.dir-cam-specs-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
background: rgba(255, 255, 255, 0.03);
border: 1px solid rgba(255, 255, 255, 0.06);
border-radius: 8px;
padding: 10px 12px;
margin-bottom: 16px;
}
.dir-cam-spec-item {
display: flex;
flex-direction: column;
}
.dir-cam-spec-label {
font-size: 0.65rem;
color: #94a3b8;
font-family: var(--font-mono);
letter-spacing: 0.05em;
}
.dir-cam-spec-val {
font-size: 0.82rem;
font-weight: 700;
color: #f1f5f9;
}
.dir-cam-actions {
display: grid;
grid-template-columns: 1.2fr 1fr;
gap: 8px;
}
.btn-dir-cam-jump {
background: linear-gradient(135deg, #06b6d4, #0284c7);
color: #ffffff;
border: none;
border-radius: 8px;
padding: 9px 12px;
font-size: 0.82rem;
font-weight: 700;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
transition: all 0.2s ease;
}
.btn-dir-cam-jump:hover {
background: linear-gradient(135deg, #0891b2, #0369a1);
box-shadow: 0 4px 12px rgba(6, 182, 212, 0.4);
}
.btn-dir-cam-details {
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.15);
color: #e2e8f0;
border-radius: 8px;
padding: 9px 12px;
font-size: 0.82rem;
font-weight: 600;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
transition: all 0.2s ease;
}
.btn-dir-cam-details:hover {
background: rgba(255, 255, 255, 0.12);
}
/* Portals Grid */ /* Portals Grid */
.portals-grid { .portals-grid {
display: grid; display: grid;
+29 -5
View File
@@ -28,7 +28,7 @@
<meta name="apple-mobile-web-app-title" content="FlockRadar"> <meta name="apple-mobile-web-app-title" content="FlockRadar">
<!-- Main Stylesheet --> <!-- Main Stylesheet -->
<link rel="stylesheet" href="css/styles.css?v=10"> <link rel="stylesheet" href="css/styles.css?v=11">
</head> </head>
<body> <body>
@@ -487,6 +487,30 @@
</div> </div>
</div> </div>
<!-- Specific Camera Nodes & Hardware Images for Searched City / Location -->
<section id="directory-cameras-section" class="directory-cameras-section" style="display: none; margin-bottom: 36px;">
<div class="dir-cam-header" style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; padding: 14px 18px; background: rgba(6, 182, 212, 0.08); border: 1px solid rgba(6, 182, 212, 0.25); border-radius: 14px;">
<div style="display: flex; align-items: center; gap: 10px;">
<span style="font-size: 1.4rem;">📸</span>
<div>
<h3 style="margin: 0; font-size: 1.1rem; color: #38bdf8; font-family: var(--font-heading);">
Identified Camera Nodes in <span id="dir-cam-query-title">City</span>
</h3>
<span id="dir-cam-subtitle" style="font-size: 0.8rem; color: #94a3b8;">
Mapped intersections, cross streets, and physical hardware photos
</span>
</div>
</div>
<span id="dir-cam-badge-count" style="background: #06b6d4; color: #07090e; font-weight: 800; font-size: 0.78rem; padding: 4px 12px; border-radius: 9999px;">
0 Cameras
</span>
</div>
<div id="directory-cameras-grid" class="directory-cameras-grid">
<!-- Dynamically populated camera cards with photos, cross streets, and jump-to-radar buttons -->
</div>
</section>
<!-- Cards Grid View --> <!-- Cards Grid View -->
<div class="portals-grid" id="portals-grid" aria-live="polite"></div> <div class="portals-grid" id="portals-grid" aria-live="polite"></div>
@@ -1031,10 +1055,10 @@
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script> <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 --> <!-- Data & Application Logic with Cache Busting -->
<script src="data/portals.js?v=10"></script> <script src="data/portals.js?v=11"></script>
<script src="js/auth.js?v=10"></script> <script src="js/auth.js?v=11"></script>
<script src="js/radar.js?v=10"></script> <script src="js/radar.js?v=11"></script>
<script src="js/app.js?v=10"></script> <script src="js/app.js?v=11"></script>
<!-- Service Worker Instant Cache Invalidation --> <!-- Service Worker Instant Cache Invalidation -->
<script> <script>
+191
View File
@@ -75,6 +75,13 @@
modalGoogleContractLink: document.getElementById('modal-google-contract-link'), modalGoogleContractLink: document.getElementById('modal-google-contract-link'),
modalCopyLinkBtn: document.getElementById('modal-copy-link-btn'), modalCopyLinkBtn: document.getElementById('modal-copy-link-btn'),
// Directory Specific Camera Nodes Elements
directoryCamerasSection: document.getElementById('directory-cameras-section'),
dirCamQueryTitle: document.getElementById('dir-cam-query-title'),
dirCamSubtitle: document.getElementById('dir-cam-subtitle'),
dirCamBadgeCount: document.getElementById('dir-cam-badge-count'),
directoryCamerasGrid: document.getElementById('directory-cameras-grid'),
// Toast Container // Toast Container
toastContainer: document.getElementById('toast-container') toastContainer: document.getElementById('toast-container')
}; };
@@ -91,6 +98,58 @@
state.orgsLookup = window.FLOCK_DATA.orgs || []; state.orgsLookup = window.FLOCK_DATA.orgs || [];
state.summary = window.FLOCK_DATA.summary || {}; state.summary = window.FLOCK_DATA.summary || {};
// Register Local Sheriff Corridors so Directory search & filters match 100%
const localSheriffPortals = [
{
name: "Palmdale Station - Los Angeles County Sheriff (LASD)",
location: "Palmdale, CA",
state: "CA",
dept_type: "SO",
slug: "palmdale-ca-lasd",
cameras: 28,
vehicles_captured: 1850000,
searches: 14200,
hotlist_hits: 820,
has_search_audit: true,
url: "https://transparency.flocksafety.com/palmdale-ca-lasd",
shared_org_ids: [15, 73, 102, 145, 180, 204],
top_search_reasons: {
"10851 CVC Stolen Vehicle": 4120,
"Felony Warrant / BOLO": 2180,
"Amber Alert / Missing Person": 410,
"Robbery / Assault Investigation": 1890
},
prohibited_uses: "Prohibited from: Traffic speed ticketing, red light ticketing, facial recognition, or immigration enforcement."
},
{
name: "Lancaster Station - Los Angeles County Sheriff (LASD)",
location: "Lancaster, CA",
state: "CA",
dept_type: "SO",
slug: "lancaster-ca-lasd",
cameras: 32,
vehicles_captured: 2100000,
searches: 16800,
hotlist_hits: 950,
has_search_audit: true,
url: "https://transparency.flocksafety.com/lancaster-ca-lasd",
shared_org_ids: [15, 73, 102, 145, 180, 204],
top_search_reasons: {
"10851 CVC Stolen Vehicle": 4850,
"Felony Warrant / BOLO": 2400,
"Amber Alert / Missing Person": 520,
"Commercial Burglary Investigation": 2100
},
prohibited_uses: "Prohibited from: Traffic speed ticketing, red light ticketing, facial recognition, or immigration enforcement."
}
];
localSheriffPortals.forEach(lp => {
if (!state.allPortals.some(p => p.slug === lp.slug)) {
state.allPortals.unshift(lp);
}
});
// Restore saved view preference // Restore saved view preference
const savedView = localStorage.getItem('flock_dir_view'); const savedView = localStorage.getItem('flock_dir_view');
if (savedView === 'table' || savedView === 'cards') { if (savedView === 'table' || savedView === 'cards') {
@@ -273,8 +332,140 @@
renderDirectory(); renderDirectory();
} }
// --- RENDER SPECIFIC CAMERA NODES & HARDWARE IMAGES IN DIRECTORY ---
function renderDirectoryCameras(rawQuery, filteredPortals) {
if (!el.directoryCamerasSection || !el.directoryCamerasGrid) return;
const q = (rawQuery || '').trim().toLowerCase();
const allCams = (typeof window.getFlockAllCameras === 'function')
? window.getFlockAllCameras()
: [];
if (allCams.length === 0) {
el.directoryCamerasSection.style.display = 'none';
return;
}
let matchingCams = [];
if (q) {
matchingCams = allCams.filter(c => {
const nameMatch = (c.name || '').toLowerCase().includes(q);
const agencyMatch = (c.agency || '').toLowerCase().includes(q);
const slugMatch = (c.slug || '').toLowerCase().includes(q);
const notesMatch = (c.notes || '').toLowerCase().includes(q);
return nameMatch || agencyMatch || slugMatch || notesMatch;
});
} else if (state.selectedState && state.selectedState !== 'ALL') {
matchingCams = allCams.filter(c => {
if (state.selectedState === 'CA') {
return c.id.includes('plm') || c.id.includes('lan') || c.id.includes('cal') || (c.agency && c.agency.includes('LASD'));
}
if (state.selectedState === 'KY') return c.id.includes('lex');
if (state.selectedState === 'GA') return c.id.includes('atl');
if (state.selectedState === 'TX') return c.id.includes('dfw');
return false;
});
}
if (matchingCams.length === 0) {
el.directoryCamerasSection.style.display = 'none';
return;
}
// Show section
el.directoryCamerasSection.style.display = 'block';
// Set Header Title
let titleText = 'All Mapped Surveillance Corridors';
if (q) {
titleText = `"${escapeHtml(rawQuery)}"`;
} else if (state.selectedState !== 'ALL') {
titleText = `${state.selectedState} Corridor Locations`;
}
el.dirCamQueryTitle.innerHTML = titleText;
el.dirCamBadgeCount.textContent = `${matchingCams.length} Camera Nodes Mapped`;
// Render Camera Cards with Images, Cross-Streets, and Live Actions
const cardsHtml = matchingCams.map(cam => {
const meta = (window.FlockAuth && window.FlockAuth.getCameraMeta)
? window.FlockAuth.getCameraMeta(cam.id)
: { confirmations: 1, hasPhoto: false, isContested: false };
const photoUrl = meta.photoUrl || cam.photoUrl || null;
const hasPhoto = !!photoUrl;
const badgeHtml = meta.isContested
? `<span class="dir-cam-badge unverified">🟠 CONTESTED</span>`
: (hasPhoto ? `<span class="dir-cam-badge verified">🟢 PHOTO VERIFIED</span>` : `<span class="dir-cam-badge unverified">⚠️ FLOCK RECORDED</span>`);
const imageContainerHtml = hasPhoto
? `<div class="dir-cam-image-container">
<img src="${photoUrl}" class="dir-cam-img" alt="Camera at ${escapeHtml(cam.name)}" onclick="window.open('${photoUrl}', '_blank')" title="Click to view full photo proof">
${badgeHtml}
</div>`
: `<div class="dir-cam-image-container">
<div class="dir-cam-placeholder">
<span class="dir-cam-placeholder-icon">📸</span>
<strong style="font-size: 0.82rem; color: #cbd5e1;">Hardware Photo Pending</strong>
<span style="font-size: 0.72rem; color: #94a3b8;">Mounted on utility / signal pole</span>
</div>
${badgeHtml}
</div>`;
return `
<article class="dir-cam-card" id="dir-cam-node-${escapeHtml(cam.id)}">
${imageContainerHtml}
<div class="dir-cam-body">
<div>
<h4 class="dir-cam-intersection">${escapeHtml(cam.name)}</h4>
<div class="dir-cam-agency-meta">
<span>🏛️ ${escapeHtml(cam.agency || 'Local Law Enforcement')}</span>
</div>
<div class="dir-cam-specs-row">
<div class="dir-cam-spec-item">
<span class="dir-cam-spec-label">HARDWARE</span>
<span class="dir-cam-spec-val">${escapeHtml(cam.type || 'Falcon Solar ALPR')}</span>
</div>
<div class="dir-cam-spec-item">
<span class="dir-cam-spec-label">MONTHLY SCANS</span>
<span class="dir-cam-spec-val" style="color: #34d399;">${escapeHtml(cam.captures30d ? `${cam.captures30d}` : '300,000+')}</span>
</div>
<div class="dir-cam-spec-item">
<span class="dir-cam-spec-label">COORDINATES</span>
<span class="dir-cam-spec-val" style="font-family: var(--font-mono); font-size: 0.75rem;">${cam.lat.toFixed(4)}, ${cam.lng.toFixed(4)}</span>
</div>
<div class="dir-cam-spec-item">
<span class="dir-cam-spec-label">CONSENSUS</span>
<span class="dir-cam-spec-val" style="color: #38bdf8;">👍 ${meta.confirmations || 1} Confirmed</span>
</div>
</div>
</div>
<div class="dir-cam-actions">
<button type="button" class="btn-dir-cam-jump" onclick="window.jumpToRadarCamera('${escapeHtml(cam.id)}')">
<span>🗺️</span>
<span>View on Live Radar</span>
</button>
<button type="button" class="btn-dir-cam-details" onclick="window.openCameraHardwareProof('${escapeHtml(cam.id)}')">
<span>📸</span>
<span>Camera Info</span>
</button>
</div>
</div>
</article>
`;
}).join('');
el.directoryCamerasGrid.innerHTML = cardsHtml;
}
// --- RENDER MAIN DIRECTORY --- // --- RENDER MAIN DIRECTORY ---
function renderDirectory() { function renderDirectory() {
// Render specific camera nodes with photos and cross streets if matching
renderDirectoryCameras(state.searchQuery, state.filteredPortals);
const total = state.filteredPortals.length; const total = state.filteredPortals.length;
el.totalCount.textContent = state.allPortals.length.toLocaleString(); el.totalCount.textContent = state.allPortals.length.toLocaleString();
el.showingCount.textContent = total.toLocaleString(); el.showingCount.textContent = total.toLocaleString();
+18
View File
@@ -269,6 +269,7 @@
zoomControl: false, zoomControl: false,
attributionControl: false attributionControl: false
}); });
window.flockMap = map;
// High-Resolution Radar Dark Tiles (Zero API Key, Zero Watermarks, Sub-Meter Street Clarity up to Zoom 19) // High-Resolution Radar Dark Tiles (Zero API Key, Zero Watermarks, Sub-Meter Street Clarity up to Zoom 19)
darkTileLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { darkTileLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
@@ -2089,6 +2090,23 @@
window.openAgencyModal(slug); window.openAgencyModal(slug);
} }
}; };
// Global camera list provider for Directory integration
window.getFlockAllCameras = function() {
return [...allCameras];
};
// Global jump from Directory straight to specific camera on Radar
window.jumpToRadarCamera = function(camId) {
if (el.navMapBtn) el.navMapBtn.click();
setTimeout(() => {
const cam = allCameras.find(c => c.id === camId);
if (cam && map) {
map.flyTo([cam.lat, cam.lng], 16, { animate: true, duration: 1.0 });
}
openCameraHardwareProof(camId);
}, 150);
};
} }
function escapeHtml(str) { function escapeHtml(str) {
+6 -6
View File
@@ -1,13 +1,13 @@
// Service Worker for FlockRadar Offline Driving & Live Updates // Service Worker for FlockRadar Offline Driving & Live Updates
const CACHE_NAME = 'flockradar-v10'; const CACHE_NAME = 'flockradar-v11';
const ASSETS_TO_CACHE = [ const ASSETS_TO_CACHE = [
'./', './',
'./index.html', './index.html',
'./css/styles.css?v=10', './css/styles.css?v=11',
'./js/app.js?v=10', './js/app.js?v=11',
'./js/auth.js?v=10', './js/auth.js?v=11',
'./js/radar.js?v=10', './js/radar.js?v=11',
'./data/portals.js?v=10', './data/portals.js?v=11',
'./manifest.json' './manifest.json'
]; ];