feat: webcam preview images in detail panel + unique layer indicator colors

- Webcam clicks now show the live camera feed image from Windy API
- Added img to DOMPurify allowed tags for preview rendering
- Road Traffic layer dot → white (#e0e0e0), Webcams → grey (#909090)
- All 12 layers now have visually distinct indicator colors

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Marc Shade
2026-02-24 13:13:06 -05:00
parent 54e4f5935e
commit 907351ed30
+19 -12
View File
@@ -410,8 +410,8 @@ a { color: var(--accent); text-decoration: none; }
.mk-plane:hover { transform: scale(3); }
.mk-traffic {
border-radius: 50%;
border: 2px solid rgba(255,160,0,0.5);
background: radial-gradient(circle, rgba(255,160,0,0.5) 30%, rgba(255,160,0,0) 70%);
border: 2px solid rgba(224,224,224,0.5);
background: radial-gradient(circle, rgba(224,224,224,0.5) 30%, rgba(224,224,224,0) 70%);
cursor: pointer; transition: transform 0.15s;
}
.mk-traffic:hover { transform: scale(1.3); }
@@ -426,8 +426,8 @@ a { color: var(--accent); text-decoration: none; }
.mk-navwarn:hover { transform: rotate(45deg) scale(1.8); }
.mk-cam {
width: 7px; height: 7px; border-radius: 50%;
background: rgba(120,220,255,0.6);
border: 1px solid rgba(120,220,255,0.4);
background: rgba(144,144,144,0.7);
border: 1px solid rgba(144,144,144,0.5);
cursor: pointer; transition: transform 0.15s;
}
.mk-cam:hover { transform: scale(1.8); }
@@ -556,7 +556,7 @@ a { color: var(--accent); text-decoration: none; }
<div class="toggle on" style="--lc:#00c8ff"><div class="knob"></div></div>
</div>
<div class="layer-row" data-layer="traffic">
<span class="layer-dot" style="background:#ffa000"></span>
<span class="layer-dot" style="background:#e0e0e0"></span>
<span class="layer-label">Road Traffic</span>
<span class="layer-count" id="cntTraffic">0</span>
<div class="toggle on" style="--lc:#ffa000"><div class="knob"></div></div>
@@ -568,7 +568,7 @@ a { color: var(--accent); text-decoration: none; }
<div class="toggle on" style="--lc:#ffc832"><div class="knob"></div></div>
</div>
<div class="layer-row" data-layer="webcams">
<span class="layer-dot" style="background:#78dcff"></span>
<span class="layer-dot" style="background:#909090"></span>
<span class="layer-label">Webcams</span>
<span class="layer-count" id="cntCams">0</span>
<div class="toggle on" style="--lc:#78dcff"><div class="knob"></div></div>
@@ -604,7 +604,7 @@ a { color: var(--accent); text-decoration: none; }
var $ = function(s) { return document.querySelector(s); };
var $$ = function(s) { return document.querySelectorAll(s); };
var safe = function(h) { return DOMPurify.sanitize(h, {ALLOWED_TAGS: ['tr','td','th','table','thead','tbody','a','span','div','br'], ALLOWED_ATTR: ['class','style','href','data-click','data-external']}); };
var safe = function(h) { return DOMPurify.sanitize(h, {ALLOWED_TAGS: ['tr','td','th','table','thead','tbody','a','span','div','br','img'], ALLOWED_ATTR: ['class','style','href','data-click','data-external','src','alt']}); };
function fmtNum(n, d) { d = d != null ? d : 2; return n == null ? '\u2014' : Number(n).toLocaleString(undefined, {minimumFractionDigits: d, maximumFractionDigits: d}); }
function fmtPct(n) { return n == null ? '\u2014' : (n >= 0 ? '+' : '') + fmtNum(n); }
@@ -663,7 +663,7 @@ $('#drawerToggle').addEventListener('click', function() {
});
// ════════════ DETAIL MODAL ════════════
var COLORS = { earthquake: 'var(--red)', military: 'var(--blue)', conflict: 'var(--amber)', fire: 'var(--gold)', convergence: 'var(--purple)', nuclear: 'var(--green)', military_base: 'var(--teal)', port: 'var(--blue)', nuclear_facility: 'var(--gold)', pipeline: 'var(--purple)', exposure: '#e040fb', waterway: 'var(--accent)', cable: 'var(--blue)', airtraffic: '#00c8ff', traffic_city: '#ffa000', navwarn: '#ffc832', webcam: '#78dcff' };
var COLORS = { earthquake: 'var(--red)', military: 'var(--blue)', conflict: 'var(--amber)', fire: 'var(--gold)', convergence: 'var(--purple)', nuclear: 'var(--green)', military_base: 'var(--teal)', port: 'var(--blue)', nuclear_facility: 'var(--gold)', pipeline: 'var(--purple)', exposure: '#e040fb', waterway: 'var(--accent)', cable: 'var(--blue)', airtraffic: '#00c8ff', traffic_city: '#e0e0e0', navwarn: '#ffc832', webcam: '#909090' };
var LABELS = { earthquake: 'EARTHQUAKE', military: 'MILITARY AIRCRAFT', conflict: 'CONFLICT EVENT', fire: 'WILDFIRE CLUSTER', convergence: 'SIGNAL CONVERGENCE', nuclear: 'NUCLEAR MONITOR', military_base: 'MILITARY BASE', port: 'STRATEGIC PORT', nuclear_facility: 'NUCLEAR FACILITY', pipeline: 'PIPELINE', exposure: 'POPULATION EXPOSURE', waterway: 'STRATEGIC WATERWAY', cable: 'CABLE CORRIDOR', airtraffic: 'AIRCRAFT', traffic_city: 'TRAFFIC CONGESTION', navwarn: 'NAVIGATION WARNING', webcam: 'WEBCAM' };
function showDetail(type, d) {
@@ -825,7 +825,6 @@ function showDetail(type, d) {
title = d.title || 'Camera';
subtitle = (d.city || '') + (d.country ? ', ' + d.country : '');
fields = [
['Title', d.title || '\u2014'],
['City', d.city || '\u2014'],
['Country', d.country || '\u2014'],
['Status', d.status || '\u2014'],
@@ -850,7 +849,12 @@ function showDetail(type, d) {
'<div class="detail-title">' + esc(title) + '</div>' +
(subtitle ? '<div class="detail-subtitle">' + esc(subtitle) + '</div>' : '');
var body = fields.map(function(f) {
var body = '';
// Webcam: show preview image at top of detail panel
if (type === 'webcam' && d.preview_url) {
body += '<img src="' + esc(d.preview_url) + '" style="width:100%;border-radius:6px;margin-bottom:10px;" alt="' + esc(d.title || 'Webcam') + '" onerror="this.style.display=\'none\'">';
}
body += fields.map(function(f) {
return '<div class="df"><span class="df-l">' + esc(f[0]) + '</span><span class="df-v ' + (f[2] || '') + '">' + esc(String(f[1])) + '</span></div>';
}).join('');
@@ -1044,7 +1048,6 @@ function showDrawerDetail(id) {
title = d.title || 'Camera';
subtitle = (d.city || '') + (d.country ? ', ' + d.country : '');
fields = [
['Title', d.title || '\u2014'],
['City', d.city || '\u2014'],
['Country', d.country || '\u2014'],
['Status', d.status || '\u2014'],
@@ -1072,7 +1075,11 @@ function showDrawerDetail(id) {
'<div class="detail-title">' + esc(title) + '</div>' +
(subtitle ? '<div class="detail-subtitle">' + esc(subtitle) + '</div>' : '');
var body = fields.map(function(f) {
var body = '';
if (type === 'webcam' && d.preview_url) {
body += '<img src="' + esc(d.preview_url) + '" style="width:100%;border-radius:6px;margin-bottom:10px;" alt="' + esc(d.title || 'Webcam') + '" onerror="this.style.display=\'none\'">';
}
body += fields.map(function(f) {
return '<div class="df"><span class="df-l">' + esc(f[0]) + '</span><span class="df-v ' + (f[2] || '') + '">' + esc(String(f[1])) + '</span></div>';
}).join('');