<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Phoenix Intelligence Dashboard</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'><text y='14' font-size='14'>🔥</text></svg>">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Chakra+Petch:wght@400;500;600;700&family=Share+Tech+Mono&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.5.3/dist/MarkerCluster.css" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
<script src="https://unpkg.com/leaflet.markercluster@1.5.3/dist/leaflet.markercluster.js"></script>
<script src="https://cdn.jsdelivr.net/npm/dompurify@3/dist/purify.min.js"></script>
<style>
/* ═══════════════ VARIABLES ═══════════════ */
:root {
--bg: #070B14;
--panel: rgba(10, 15, 28, 0.82);
--card: rgba(12, 18, 32, 0.95);
--border: rgba(0, 230, 255, 0.07);
--glow: rgba(0, 230, 255, 0.15);
--text: #A8B8CC;
--dim: #4A5C72;
--bright: #E2ECF5;
--accent: #00E5FF;
--red: #FF3B3B;
--amber: #FF9F1C;
--blue: #4DA8FF;
--green: #34D399;
--purple: #A78BFA;
--teal: #2DD4BF;
--gold: #F0B840;
--pink: #FF6B9D;
--ui: 'Chakra Petch', sans-serif;
--mono: 'Share Tech Mono', monospace;
--radius: 10px;
--ease: cubic-bezier(0.25, 0.46, 0.45, 0.94);
}
/* ═══════════════ RESET ═══════════════ */
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
html, body { height: 100%; overflow: hidden; }
body { font-family: var(--ui); background: var(--bg); color: var(--text); line-height: 1.5; }
a { color: var(--accent); text-decoration: none; }
::-webkit-scrollbar { width: 4px; }
::-webkit-scrollbar-thumb { background: var(--dim); border-radius: 2px; }
/* ═══════════════ LOADING ═══════════════ */
#loading {
position: fixed; inset: 0; z-index: 500;
display: flex; flex-direction: column; align-items: center; justify-content: center;
background: var(--bg); transition: opacity 0.8s;
}
#loading.gone { opacity: 0; pointer-events: none; }
.load-ring {
width: 64px; height: 64px;
border: 2px solid rgba(0,229,255,0.08);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 1.2s linear infinite;
}
.load-text {
margin-top: 1.5rem; font-family: var(--mono);
font-size: 0.7rem; letter-spacing: 3px; color: var(--dim);
}
@keyframes spin { to { transform: rotate(360deg); } }
/* ═══════════════ MAP ═══════════════ */
#map { position: fixed; inset: 0; z-index: 1; }
.leaflet-container { background: var(--bg) !important; }
.leaflet-control-zoom {
margin-bottom: 50px !important; /* clear bottom ticker */
}
.leaflet-control-zoom a {
background: var(--panel) !important; color: var(--accent) !important;
border-color: var(--border) !important; backdrop-filter: blur(12px);
}
.leaflet-control-zoom a:hover { background: rgba(0,229,255,0.12) !important; }
/* ═══════════════ VIGNETTE ═══════════════ */
#vignette {
position: fixed; inset: 0; z-index: 2; pointer-events: none;
background: radial-gradient(ellipse at center, transparent 35%, rgba(7,11,20,0.55) 100%);
}
/* ═══════════════ GLASS BASE ═══════════════ */
.glass {
background: var(--panel);
backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
border: 1px solid var(--border); border-radius: var(--radius);
}
/* ═══════════════ TOP BAR ═══════════════ */
#topBar {
position: fixed; top: 10px; left: 10px; right: 10px; z-index: 50;
display: flex; align-items: center; justify-content: space-between;
padding: 8px 16px; gap: 8px;
flex-wrap: wrap;
}
.hud-logo { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
.logo-mark { font-size: 1.1rem; line-height: 1; }
.logo-text { font-weight: 700; font-size: 0.9rem; letter-spacing: 2px; color: var(--bright); }
.logo-dim { font-weight: 400; color: var(--dim); }
.hud-stats { display: flex; gap: 5px; flex-wrap: wrap; justify-content: center; flex: 1; min-width: 0; max-height: 52px; overflow: hidden; }
.stat-pill {
padding: 3px 10px; border-radius: 6px;
background: rgba(0,0,0,0.35); border: 1px solid var(--border);
font-family: var(--mono); font-size: 0.68rem;
display: flex; align-items: center; gap: 6px; white-space: nowrap;
}
.stat-pill .v { color: var(--bright); font-weight: 600; }
.stat-pill .l { color: var(--dim); text-transform: uppercase; letter-spacing: 0.5px; font-size: 0.6rem; }
.stat-pill .v.warn { color: var(--amber); }
.stat-pill .v.crit { color: var(--red); }
.hud-status {
display: flex; align-items: center; gap: 8px;
font-size: 0.68rem; font-family: var(--mono); color: var(--dim); flex-shrink: 0;
}
.conn-dot {
width: 7px; height: 7px; border-radius: 50%;
background: var(--green); box-shadow: 0 0 8px var(--green);
animation: pulse-dot 2s ease-in-out infinite;
}
.conn-dot.err { background: var(--red); box-shadow: 0 0 8px var(--red); }
@keyframes pulse-dot { 0%,100% { opacity: 1; } 50% { opacity: 0.4; } }
.sep { width: 1px; height: 12px; background: var(--border); }
/* ═══════════════ LAYER PANEL ═══════════════ */
#layers {
position: fixed; top: var(--layers-top, 66px); left: 10px; z-index: 40;
width: 200px; padding: 12px 14px;
transition: top 0.2s;
}
.panel-title {
font-size: 0.62rem; font-weight: 600; letter-spacing: 2.5px;
color: var(--dim); margin-bottom: 10px; text-transform: uppercase;
}
.layer-row {
display: flex; align-items: center; gap: 8px;
padding: 5px 0; cursor: pointer; transition: opacity 0.2s;
}
.layer-row:hover { opacity: 0.85; }
.rgn-btn {
font-family: var(--mono); font-size: 0.6rem; letter-spacing: 0.5px;
padding: 3px 7px; border: 1px solid rgba(255,255,255,0.12); border-radius: 4px;
background: rgba(255,255,255,0.04); color: var(--dim); cursor: pointer;
transition: all 0.15s;
}
.rgn-btn:hover { background: rgba(255,255,255,0.1); color: var(--text); }
.rgn-btn.active { background: rgba(0,229,255,0.15); color: var(--accent); border-color: rgba(0,229,255,0.3); }
.layer-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
.layer-label { font-size: 0.75rem; color: var(--text); flex: 1; }
.layer-count {
font-family: var(--mono); font-size: 0.62rem; color: var(--dim);
min-width: 22px; text-align: right;
}
.toggle {
position: relative; width: 32px; height: 17px;
border-radius: 9px; background: #1a2030; flex-shrink: 0;
transition: background 0.3s, box-shadow 0.3s;
}
.toggle.on { box-shadow: 0 0 10px color-mix(in srgb, var(--lc, var(--accent)) 40%, transparent); }
.toggle .knob {
position: absolute; top: 2px; left: 2px;
width: 13px; height: 13px; border-radius: 50%;
background: #8899aa; transition: transform 0.3s var(--ease), background 0.3s;
}
.toggle.on .knob { transform: translateX(15px); background: #fff; }
/* ═══════════════ DATA DRAWER ═══════════════ */
#drawer {
position: fixed; top: 10px; right: 10px; bottom: 46px; z-index: 40;
width: 340px; display: flex; flex-direction: column;
transition: transform 0.4s var(--ease);
}
#drawer.closed { transform: translateX(360px); }
.drawer-tab {
position: absolute; left: -34px; top: 50%; transform: translateY(-50%);
width: 30px; height: 56px;
background: var(--panel); border: 1px solid var(--border);
border-right: none; border-radius: var(--radius) 0 0 var(--radius);
color: var(--accent); cursor: pointer;
display: flex; align-items: center; justify-content: center;
font-size: 0.75rem; backdrop-filter: blur(16px);
transition: background 0.2s;
}
.drawer-tab:hover { background: rgba(0,229,255,0.06); }
.drawer-inner {
flex: 1; overflow-y: auto; overflow-x: hidden;
padding: 12px 14px;
}
.sh {
position: sticky; top: -12px; z-index: 5;
padding: 8px 14px 5px; margin: 12px -14px 6px;
font-size: 0.62rem; font-weight: 600; letter-spacing: 2.5px;
color: var(--accent); text-transform: uppercase;
border-bottom: 1px solid var(--border);
background: var(--panel); backdrop-filter: blur(20px);
}
.sh:first-child { margin-top: 0; }
.dtable { width: 100%; border-collapse: collapse; font-family: var(--mono); font-size: 0.68rem; margin: 4px 0 10px; }
.dtable th { color: var(--dim); font-weight: 400; text-align: left; padding: 2px 4px; font-size: 0.58rem; text-transform: uppercase; letter-spacing: 0.5px; }
.dtable td { padding: 2px 4px; border-top: 1px solid rgba(255,255,255,0.02); }
.dtable tr:hover td { background: rgba(255,255,255,0.015); }
.mini-row { display: flex; gap: 5px; margin: 4px 0 10px; flex-wrap: wrap; }
.mini-box {
flex: 1; min-width: 60px; background: rgba(0,0,0,0.25);
border: 1px solid var(--border); border-radius: 6px;
padding: 5px; text-align: center;
}
.mini-box .v { font-family: var(--mono); font-size: 0.85rem; font-weight: 600; color: var(--bright); }
.mini-box .l { font-size: 0.55rem; color: var(--dim); text-transform: uppercase; letter-spacing: 0.5px; margin-top: 1px; }
.heatmap { display: flex; flex-wrap: wrap; gap: 3px; margin: 4px 0 10px; }
.hm {
padding: 3px 7px; border-radius: 4px;
font-family: var(--mono); font-size: 0.64rem; font-weight: 600;
min-width: 62px; text-align: center;
}
.tags { display: flex; flex-wrap: wrap; gap: 3px; margin: 4px 0 10px; }
.tag {
padding: 1px 7px; border-radius: 4px; font-size: 0.64rem;
background: rgba(255,255,255,0.04); color: var(--text);
border: 1px solid var(--border);
}
.tag-hot { background: rgba(255,59,59,0.1); color: var(--red); border-color: rgba(255,59,59,0.15); }
.sub { font-size: 0.6rem; color: var(--dim); margin: 8px 0 3px; text-transform: uppercase; letter-spacing: 0.5px; }
/* ═══════════════ DETAIL MODAL ═══════════════ */
#detail {
position: fixed; top: 0; right: 0; bottom: 0; z-index: 100;
width: 400px; background: var(--card);
border-left: 1px solid var(--glow);
transform: translateX(100%); transition: transform 0.35s var(--ease);
overflow-y: auto; box-shadow: -8px 0 40px rgba(0,0,0,0.4);
}
#detail.open { transform: translateX(0); }
.detail-close {
position: sticky; top: 0; float: right; z-index: 5;
background: none; border: none; color: var(--dim);
font-size: 1.5rem; cursor: pointer; padding: 12px 16px;
transition: color 0.2s;
}
.detail-close:hover { color: var(--bright); }
.detail-header { padding: 20px 24px 16px; border-bottom: 1px solid var(--border); clear: both; }
.detail-cat { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
.detail-cat-dot {
width: 10px; height: 10px; border-radius: 50%;
box-shadow: 0 0 10px var(--cat-color);
}
.detail-cat-label { font-size: 0.62rem; letter-spacing: 2.5px; text-transform: uppercase; color: var(--dim); }
.detail-title { font-size: 1.1rem; font-weight: 700; color: var(--bright); line-height: 1.3; }
.detail-subtitle { font-family: var(--mono); font-size: 0.72rem; color: var(--dim); margin-top: 4px; }
.detail-body { padding: 16px 24px 24px; }
.df {
display: flex; justify-content: space-between; align-items: baseline;
padding: 8px 0; border-bottom: 1px solid rgba(255,255,255,0.025);
}
.df-l { font-size: 0.7rem; color: var(--dim); text-transform: uppercase; letter-spacing: 0.5px; }
.df-v { font-family: var(--mono); font-size: 0.8rem; color: var(--bright); text-align: right; max-width: 60%; }
.df-v.crit { color: var(--red); }
.df-v.high { color: var(--amber); }
.df-v.ok { color: var(--green); }
.detail-link {
display: inline-block; margin-top: 14px; padding: 6px 14px;
background: rgba(0,229,255,0.08); border: 1px solid rgba(0,229,255,0.15);
border-radius: 6px; font-size: 0.72rem; color: var(--accent);
transition: background 0.2s;
}
.detail-link:hover { background: rgba(0,229,255,0.15); }
/* ═══════════════ CLICKABLE DRAWER ROWS ═══════════════ */
.dtable tr[data-click] { cursor: pointer; }
.dtable tr[data-click]:hover td { background: rgba(0,229,255,0.04) !important; }
.drawer-item { cursor: pointer; transition: background 0.15s; border-radius: 4px; padding: 4px 6px; margin: 0 -6px; }
.drawer-item:hover { background: rgba(0,229,255,0.04); }
/* ═══════════════ BOTTOM TICKER ═══════════════ */
#ticker {
position: fixed; bottom: 0; left: 0; right: 0; z-index: 50;
height: 34px; display: flex; align-items: center; overflow: hidden;
}
.ticker-label {
padding: 0 14px; font-size: 0.6rem; font-weight: 700;
letter-spacing: 2.5px; color: var(--accent); white-space: nowrap;
border-right: 1px solid var(--border); height: 100%;
display: flex; align-items: center; flex-shrink: 0;
}
.ticker-track { flex: 1; overflow: hidden; position: relative; }
.ticker-scroll {
display: flex; gap: 36px; white-space: nowrap;
animation: tickerScroll var(--ticker-dur, 90s) linear infinite;
}
@keyframes tickerScroll { from { transform: translateX(0); } to { transform: translateX(-50%); } }
.ticker-item { font-family: var(--mono); font-size: 0.66rem; color: var(--text); }
.ticker-item .src { color: var(--dim); margin-left: 6px; }
.ticker-item .sep { color: rgba(0,229,255,0.2); margin: 0 4px; }
/* ═══════════════ ALERT BANNER ═══════════════ */
#alertBanner {
position: fixed; top: var(--layers-top, 66px); left: 220px; right: 360px; z-index: 45;
max-height: 0; overflow: hidden; transition: max-height 0.4s var(--ease), top 0.2s;
}
#alertBanner.show { max-height: 90px; }
.alert-strip {
display: flex; align-items: center; gap: 8px;
padding: 3px 10px; border-radius: 6px; margin-bottom: 3px;
font-family: var(--mono); font-size: 0.58rem;
background: rgba(255,59,59,0.06); border: 1px solid rgba(255,59,59,0.12);
}
.alert-strip.critical { background: rgba(255,59,59,0.1); border-color: rgba(255,59,59,0.2); }
.alert-strip.high { background: rgba(255,159,28,0.08); border-color: rgba(255,159,28,0.15); }
.alert-strip.medium { background: rgba(77,168,255,0.06); border-color: rgba(77,168,255,0.1); }
.alert-dot { width: 5px; height: 5px; border-radius: 50%; flex-shrink: 0; animation: pulse-dot 2s ease-in-out infinite; }
.alert-strip.critical .alert-dot { background: var(--red); box-shadow: 0 0 8px var(--red); }
.alert-strip.high .alert-dot { background: var(--amber); box-shadow: 0 0 6px var(--amber); }
.alert-strip.medium .alert-dot { background: var(--blue); box-shadow: 0 0 4px var(--blue); }
.alert-msg { flex: 1; color: var(--bright); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.alert-domain { color: var(--dim); font-size: 0.5rem; text-transform: uppercase; letter-spacing: 0.5px; flex-shrink: 0; }
/* ═══════════════ RISK BAR ═══════════════ */
.risk-bar { position: relative; height: 6px; background: rgba(255,255,255,0.04); border-radius: 3px; margin: 2px 0; overflow: hidden; }
.risk-fill { height: 100%; border-radius: 3px; transition: width 0.5s var(--ease); }
.risk-fill.crit { background: linear-gradient(90deg, var(--red), #ff6666); }
.risk-fill.high { background: linear-gradient(90deg, var(--amber), #ffcc66); }
.risk-fill.med { background: linear-gradient(90deg, var(--blue), #80c4ff); }
.risk-fill.low { background: linear-gradient(90deg, var(--green), #66e6b3); }
/* ═══════════════ GAUGE ═══════════════ */
.gauge-row { display: flex; align-items: center; gap: 8px; margin: 6px 0 10px; }
.gauge-track { flex: 1; height: 8px; background: rgba(255,255,255,0.04); border-radius: 4px; overflow: hidden; }
.gauge-fill { height: 100%; border-radius: 4px; transition: width 0.6s var(--ease), background 0.3s; }
.gauge-label { font-family: var(--mono); font-size: 0.75rem; font-weight: 600; min-width: 32px; text-align: right; }
/* ═══════════════ MAP MARKERS ═══════════════ */
.mk-quake {
border-radius: 50%;
background: radial-gradient(circle, rgba(255,59,59,0.85) 20%, rgba(255,59,59,0) 70%);
border: 1.5px solid rgba(255,59,59,0.5);
cursor: pointer; transition: transform 0.15s;
}
.mk-quake:hover { transform: scale(1.3); }
.mk-quake.crit { animation: glow-red 1.8s ease-in-out infinite; }
@keyframes glow-red {
0%,100% { box-shadow: 0 0 4px rgba(255,59,59,0.3); }
50% { box-shadow: 0 0 22px rgba(255,59,59,0.8); }
}
.mk-mil {
border-radius: 50%;
background: radial-gradient(circle, rgba(77,168,255,0.8) 25%, rgba(77,168,255,0) 70%);
border: 1.5px solid rgba(77,168,255,0.45);
cursor: pointer; transition: transform 0.15s;
}
.mk-mil:hover { transform: scale(1.3); }
.mk-conflict {
border-radius: 2px; transform: rotate(45deg);
background: radial-gradient(circle, rgba(255,159,28,0.85) 20%, rgba(255,159,28,0) 70%);
border: 1.5px solid rgba(255,159,28,0.45);
cursor: pointer;
}
.mk-conflict:hover { box-shadow: 0 0 14px rgba(255,159,28,0.6); }
.mk-fire {
border-radius: 50%;
background: radial-gradient(circle, rgba(240,184,64,0.9) 20%, rgba(255,100,0,0) 70%);
border: 1.5px solid rgba(240,160,40,0.5);
cursor: pointer; transition: transform 0.15s;
}
.mk-fire:hover { transform: scale(1.3); }
.mk-conv {
border-radius: 50%;
border: 2px dashed rgba(167,139,250,0.35);
background: rgba(167,139,250,0.04);
cursor: pointer; transition: border-color 0.2s, background 0.2s;
}
.mk-conv:hover { border-color: rgba(167,139,250,0.6); background: rgba(167,139,250,0.1); }
.mk-nuclear {
border-radius: 50%;
background: radial-gradient(circle, rgba(52,211,153,0.8) 15%, rgba(52,211,153,0) 70%);
border: 2px solid rgba(52,211,153,0.5);
cursor: pointer; transition: transform 0.15s;
box-shadow: 0 0 6px rgba(52,211,153,0.3);
}
.mk-nuclear:hover { transform: scale(1.3); }
.mk-nuclear.crit { animation: glow-green 1.8s ease-in-out infinite; }
.mk-base {
width: 8px; height: 8px; border-radius: 2px;
background: var(--teal); opacity: 0.7;
border: 1px solid rgba(45,212,191,0.5);
cursor: pointer; transition: transform 0.15s, opacity 0.15s;
}
.mk-base:hover { transform: scale(1.5); opacity: 1; }
.mk-port {
width: 7px; height: 7px; border-radius: 50%;
background: rgba(77,168,255,0.6);
border: 1px solid rgba(77,168,255,0.4);
cursor: pointer; transition: transform 0.15s;
}
.mk-port:hover { transform: scale(1.5); }
.mk-nuke-fac {
width: 9px; height: 9px; border-radius: 50%;
background: radial-gradient(circle, rgba(240,184,64,0.8) 30%, rgba(240,184,64,0) 70%);
border: 1.5px solid rgba(240,184,64,0.5);
cursor: pointer; transition: transform 0.15s;
}
.mk-nuke-fac:hover { transform: scale(1.4); }
.mk-nuke-fac.hot { animation: glow-gold 2s ease-in-out infinite; }
.pipeline-line { cursor: pointer; transition: opacity 0.15s; }
.pipeline-line:hover { opacity: 1 !important; stroke-width: 3 !important; }
.mk-waterway {
width: 10px; height: 10px; background: var(--accent); opacity: 0.8;
transform: rotate(45deg); border: 1.5px solid rgba(0,229,255,0.6);
cursor: pointer; transition: transform 0.15s, opacity 0.15s;
}
.mk-waterway:hover { transform: rotate(45deg) scale(1.5); opacity: 1; }
.mk-chokepoint {
width: 8px; height: 8px; background: #ff5722; border-radius: 50%; opacity: 0.9;
border: 1.5px solid rgba(255,87,34,0.6); cursor: pointer; transition: transform 0.15s;
}
.mk-chokepoint:hover { transform: scale(1.8); }
.mk-canal {
width: 8px; height: 8px; background: #ffab00; border-radius: 2px; opacity: 0.9;
border: 1.5px solid rgba(255,171,0,0.6); cursor: pointer; transition: transform 0.15s;
}
.mk-canal:hover { transform: scale(1.8); }
.mk-sealane {
width: 6px; height: 6px; background: #00bfa5; border-radius: 50%; opacity: 0.7;
border: 1px solid rgba(0,191,165,0.5); cursor: pointer; transition: transform 0.15s;
}
.mk-sealane:hover { transform: scale(1.8); }
.cable-corridor { cursor: pointer; transition: fill-opacity 0.15s; }
.cable-corridor:hover { fill-opacity: 0.1 !important; stroke-opacity: 0.5 !important; }
.mk-plane {
width: 4px; height: 4px; border-radius: 50%;
background: rgba(0,200,255,0.5); cursor: pointer;
transition: transform 0.15s;
}
.mk-plane.comm { background: rgba(0,200,255,0.7); }
.mk-plane:hover { transform: scale(3); }
.mk-traffic {
border-radius: 50%;
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); }
.mk-traffic.crit { border-color: rgba(255,59,59,0.6); background: radial-gradient(circle, rgba(255,59,59,0.5) 30%, rgba(255,59,59,0) 70%); }
.mk-navwarn {
width: 6px; height: 6px; border-radius: 1px;
transform: rotate(45deg);
background: rgba(255,200,50,0.7);
border: 1px solid rgba(255,200,50,0.5);
cursor: pointer; transition: transform 0.15s;
}
.mk-navwarn:hover { transform: rotate(45deg) scale(1.8); }
.mk-cam {
width: 7px; height: 7px; border-radius: 50%;
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); }
.mk-climate {
border-radius: 50%;
cursor: pointer; transition: transform 0.15s;
}
.mk-climate:hover { transform: scale(1.3); }
.mk-climate.hot { background: radial-gradient(circle, rgba(255,68,68,0.8) 20%, rgba(255,68,68,0) 70%); border: 2px solid rgba(255,68,68,0.6); }
.mk-climate.warm { background: radial-gradient(circle, rgba(255,159,28,0.7) 20%, rgba(255,159,28,0) 70%); border: 2px solid rgba(255,159,28,0.5); }
.mk-climate.cool { background: radial-gradient(circle, rgba(100,181,246,0.7) 20%, rgba(100,181,246,0) 70%); border: 2px solid rgba(100,181,246,0.5); }
.mk-climate.cold { background: radial-gradient(circle, rgba(77,168,255,0.8) 20%, rgba(77,168,255,0) 70%); border: 2px solid rgba(77,168,255,0.6); }
.mk-climate.sig { animation: glow-climate 2s ease-in-out infinite; }
.mk-news {
width: 6px; height: 6px; border-radius: 50%;
background: rgba(0,229,255,0.7);
border: 1px solid rgba(0,229,255,0.5);
cursor: pointer; transition: transform 0.15s;
}
.mk-news:hover { transform: scale(2); }
@keyframes glow-climate {
0%,100% { box-shadow: 0 0 4px rgba(255,107,107,0.3); }
50% { box-shadow: 0 0 18px rgba(255,107,107,0.7); }
}
@keyframes glow-gold {
0%,100% { box-shadow: 0 0 4px rgba(240,184,64,0.3); }
50% { box-shadow: 0 0 16px rgba(240,184,64,0.7); }
}
@keyframes glow-green {
0%,100% { box-shadow: 0 0 4px rgba(52,211,153,0.3); }
50% { box-shadow: 0 0 22px rgba(52,211,153,0.8); }
}
/* Leaflet tooltip override */
.leaflet-tooltip.mk-tip {
background: var(--card); color: var(--bright);
border: 1px solid var(--border); border-radius: 6px;
font-family: var(--mono); font-size: 0.68rem;
padding: 3px 8px; box-shadow: 0 4px 20px rgba(0,0,0,0.4);
}
.leaflet-tooltip.mk-tip::before { border-top-color: var(--border); }
/* ═══════════════ UTILITY ═══════════════ */
.up { color: var(--green); } .down { color: var(--red); }
.warn { color: var(--amber); } .dim { color: var(--dim); }
.bright { color: var(--bright); } .accent { color: var(--accent); }
/* ═══════════════ RESPONSIVE ═══════════════ */
@media (max-width: 900px) {
#drawer { width: 100%; top: auto; bottom: 34px; height: 45vh; right: 0; left: 0; border-radius: var(--radius) var(--radius) 0 0; }
#drawer.closed { transform: translateY(100%); }
.drawer-tab { left: 50%; top: -30px; transform: translateX(-50%); border-radius: var(--radius) var(--radius) 0 0; border: 1px solid var(--border); border-bottom: none; width: 56px; height: 26px; }
#detail { width: 100%; }
#layers { width: 140px; max-height: 36px; overflow: hidden; transition: max-height 0.3s var(--ease); }
#layers.layers-open { max-height: 600px; }
.panel-title { cursor: pointer; position: relative; }
.panel-title::after { content: '\25BE'; position: absolute; right: 0; opacity: 0.4; font-size: 0.5rem; }
#layers.layers-open .panel-title::after { content: '\25B4'; }
.layer-row { padding: 6px 0; }
.hud-stats { display: none; }
#alertBanner { left: 10px; right: 10px; }
}
/* ═══════════════ SEARCH ═══════════════ */
.search-box { flex-shrink: 0; }
.search-input {
width: 160px; padding: 4px 10px;
background: rgba(0,0,0,0.35); border: 1px solid var(--border);
border-radius: 6px; color: var(--bright);
font-family: var(--mono); font-size: 0.68rem;
outline: none; transition: border-color 0.3s, width 0.3s;
}
.search-input::placeholder { color: var(--dim); }
.search-input:focus { border-color: rgba(0,229,255,0.3); width: 220px; }
@media (max-width: 900px) {
.search-input { width: 100px; }
.search-input:focus { width: 140px; }
}
/* ═══════════════ COLLAPSIBLE SECTIONS ═══════════════ */
.sh { cursor: pointer; user-select: none; }
.sh::after { content: '\25BE'; float: right; font-size: 0.55rem; opacity: 0.3; transition: transform 0.2s; }
.sh.collapsed::after { transform: rotate(-90deg); }
/* ═══════════════ HUD PILL CLICKABLE ═══════════════ */
.stat-pill { cursor: pointer; transition: background 0.15s, border-color 0.15s; }
.stat-pill:hover { background: rgba(0,229,255,0.08); border-color: rgba(0,229,255,0.15); }
/* ═══════════════ CLUSTER STYLES ═══════════════ */
.marker-cluster-small, .marker-cluster-medium, .marker-cluster-large {
background: rgba(14,20,33,0.7); border: 1px solid rgba(255,255,255,0.15); border-radius: 50%;
}
.marker-cluster-small div, .marker-cluster-medium div, .marker-cluster-large div {
background: rgba(0,200,255,0.25); color: #8cf; font-size: 11px; font-weight: 600;
font-family: var(--mono); border-radius: 50%; width: 30px; height: 30px;
line-height: 30px; text-align: center; margin: 4px;
}
.marker-cluster-medium div { background: rgba(0,200,255,0.35); color: #5ef; }
.marker-cluster-large div { background: rgba(0,200,255,0.5); color: #fff; }
.cluster-conflict div { background: rgba(245,158,11,0.3) !important; color: var(--amber) !important; }
.cluster-fire div { background: rgba(240,184,64,0.3) !important; color: var(--gold) !important; }
.cluster-military div { background: rgba(96,165,250,0.3) !important; color: var(--blue) !important; }
</style>
</head>
<body>
<!-- LOADING -->
<div id="loading">
<div class="load-ring"></div>
<div class="load-text">INITIALIZING INTELLIGENCE FEEDS</div>
</div>
<!-- FULL-SCREEN MAP -->
<div id="map"></div>
<div id="vignette"></div>
<!-- TOP BAR -->
<header id="topBar" class="glass">
<div class="hud-logo">
<span class="logo-mark">🔥</span>
<span class="logo-text">PHOENIX<span class="logo-dim"> INTEL</span></span>
</div>
<div class="search-box">
<input class="search-input" id="searchInput" type="text" placeholder="/ Search..." spellcheck="false" autocomplete="off" />
</div>
<div class="hud-stats" id="hudStats"></div>
<div class="hud-status">
<span class="conn-dot" id="connDot"></span>
<span id="connLabel">CONNECTING</span>
<span class="sep"></span>
<span id="feedCount">--</span>
<span class="sep"></span>
<span id="lastUpdate">--</span>
</div>
</header>
<!-- ALERT BANNER -->
<div id="alertBanner" class="glass"></div>
<!-- LAYER CONTROLS -->
<aside id="layers" class="glass">
<div class="panel-title">LAYERS</div>
<div class="layer-row" data-layer="quakes">
<span class="layer-dot" style="background:var(--red)"></span>
<span class="layer-label">Earthquakes</span>
<span class="layer-count" id="cntQuakes">0</span>
<div class="toggle on" style="--lc:var(--red)"><div class="knob"></div></div>
</div>
<div class="layer-row" data-layer="military">
<span class="layer-dot" style="background:var(--blue)"></span>
<span class="layer-label">Military</span>
<span class="layer-count" id="cntMil">0</span>
<div class="toggle on" style="--lc:var(--blue)"><div class="knob"></div></div>
</div>
<div class="layer-row" data-layer="conflict">
<span class="layer-dot" style="background:var(--amber)"></span>
<span class="layer-label">Conflict</span>
<span class="layer-count" id="cntConflict">0</span>
<div class="toggle on" style="--lc:var(--amber)"><div class="knob"></div></div>
</div>
<div class="layer-row" data-layer="fires">
<span class="layer-dot" style="background:var(--gold)"></span>
<span class="layer-label">Wildfires</span>
<span class="layer-count" id="cntFires">0</span>
<div class="toggle on" style="--lc:var(--gold)"><div class="knob"></div></div>
</div>
<div class="layer-row" data-layer="convergence">
<span class="layer-dot" style="background:var(--purple)"></span>
<span class="layer-label">Convergence</span>
<span class="layer-count" id="cntConv">0</span>
<div class="toggle on" style="--lc:var(--purple)"><div class="knob"></div></div>
</div>
<div class="layer-row" data-layer="nuclear">
<span class="layer-dot" style="background:var(--green)"></span>
<span class="layer-label">Nuclear Sites</span>
<span class="layer-count" id="cntNuclear">0</span>
<div class="toggle on" style="--lc:var(--green)"><div class="knob"></div></div>
</div>
<div class="layer-row" data-layer="infra">
<span class="layer-dot" style="background:var(--teal)"></span>
<span class="layer-label">Infrastructure</span>
<span class="layer-count" id="cntInfra">0</span>
<div class="toggle on" style="--lc:var(--teal)"><div class="knob"></div></div>
</div>
<div class="layer-row" data-layer="exposure">
<span class="layer-dot" style="background:#e040fb"></span>
<span class="layer-label">Pop Exposure</span>
<span class="layer-count" id="cntExposure">0</span>
<div class="toggle on" style="--lc:#e040fb"><div class="knob"></div></div>
</div>
<div class="layer-row" data-layer="airtraffic">
<span class="layer-dot" style="background:#00c8ff"></span>
<span class="layer-label">Air Traffic</span>
<span class="layer-count" id="cntAirTraffic">0</span>
<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:#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>
</div>
<div class="layer-row" data-layer="navwarnings">
<span class="layer-dot" style="background:#ffc832"></span>
<span class="layer-label">Nav Warnings</span>
<span class="layer-count" id="cntNavWarn">0</span>
<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:#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>
</div>
<div class="layer-row" data-layer="climate">
<span class="layer-dot" style="background:#ff6b6b"></span>
<span class="layer-label">Climate</span>
<span class="layer-count" id="cntClimate">0</span>
<div class="toggle on" style="--lc:#ff6b6b"><div class="knob"></div></div>
</div>
<div class="layer-row" data-layer="news">
<span class="layer-dot" style="background:var(--accent)"></span>
<span class="layer-label">News</span>
<span class="layer-count" id="cntNews">0</span>
<div class="toggle on" style="--lc:var(--accent)"><div class="knob"></div></div>
</div>
<div style="border-top:1px solid rgba(255,255,255,0.08);margin-top:4px;padding-top:6px">
<div style="font-size:0.55rem;letter-spacing:1px;color:rgba(255,255,255,0.35);margin-bottom:4px">REGION</div>
<div id="regionPresets" style="display:flex;flex-wrap:wrap;gap:2px">
<button class="rgn-btn active" data-region="global">Globe</button>
<button class="rgn-btn" data-region="americas">Americas</button>
<button class="rgn-btn" data-region="europe">Europe</button>
<button class="rgn-btn" data-region="mena">MENA</button>
<button class="rgn-btn" data-region="asia">Asia-Pac</button>
<button class="rgn-btn" data-region="africa">Africa</button>
</div>
</div>
<div style="border-top:1px solid rgba(255,255,255,0.08);margin-top:4px;padding-top:6px">
<div style="font-size:0.55rem;letter-spacing:1px;color:rgba(255,255,255,0.35);margin-bottom:4px">TIME WINDOW</div>
<div id="timeFilter" style="display:flex;gap:2px">
<button class="rgn-btn" data-hours="1">1h</button>
<button class="rgn-btn" data-hours="6">6h</button>
<button class="rgn-btn active" data-hours="24">24h</button>
<button class="rgn-btn" data-hours="168">7d</button>
<button class="rgn-btn" data-hours="0">All</button>
</div>
</div>
</aside>
<!-- DATA DRAWER -->
<aside id="drawer" class="glass closed">
<button class="drawer-tab" id="drawerToggle" title="Toggle data panel">◀</button>
<div class="drawer-inner" id="drawerBody"></div>
</aside>
<!-- DETAIL MODAL -->
<div id="detail">
<button class="detail-close" id="detailClose">×</button>
<div class="detail-header" id="detailHeader"></div>
<div class="detail-body" id="detailBody"></div>
</div>
<!-- BOTTOM TICKER -->
<div id="ticker" class="glass">
<div class="ticker-label">INTEL</div>
<div class="ticker-track">
<div class="ticker-scroll" id="tickerContent"></div>
</div>
</div>
<script>
// ════════════════════════════════════════════════════════════════════════════
// Phoenix Intelligence Dashboard v3 — Map-first global intelligence
// All innerHTML assignments use DOMPurify.sanitize() for XSS safety.
// ════════════════════════════════════════════════════════════════════════════
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','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); }
function fmtBig(n) { if (n == null) return '\u2014'; var a = Math.abs(n); if (a >= 1e12) return '$'+(n/1e12).toFixed(1)+'T'; if (a >= 1e9) return '$'+(n/1e9).toFixed(1)+'B'; if (a >= 1e6) return '$'+(n/1e6).toFixed(1)+'M'; return '$'+fmtNum(n,0); }
function fmtBigPlain(n) { if (n == null) return '\u2014'; var a = Math.abs(n); if (a >= 1e9) return (n/1e9).toFixed(1)+'B'; if (a >= 1e6) return (n/1e6).toFixed(1)+'M'; if (a >= 1e3) return (n/1e3).toFixed(1)+'K'; return String(n); }
function cls(n) { return n == null ? '' : n >= 0 ? 'up' : 'down'; }
function esc(s) { var d = document.createElement('div'); d.textContent = s || ''; return d.innerHTML; }
function trunc(s, n) { n = n || 50; s = s || ''; return s.length > n ? s.slice(0, n) + '\u2026' : s; }
function ago(ts) { if (!ts) return '\u2014'; var s = Math.floor((Date.now() - new Date(ts).getTime()) / 1000); if (s < 60) return s + 's'; if (s < 3600) return Math.floor(s/60) + 'm'; if (s < 86400) return Math.floor(s/3600) + 'h'; return Math.floor(s/86400) + 'd'; }
// ════════════ STATE ════════════
var latestData = null;
var map = null;
var mapLayers = {};
var layerState = { quakes: true, military: true, conflict: true, fires: true, convergence: true, nuclear: true, infra: true, exposure: true, airtraffic: true, traffic: true, navwarnings: true, webcams: true, climate: true, news: true };
// Restore saved layer toggles from localStorage
try { var _saved = JSON.parse(localStorage.getItem('phoenix-layers')); if (_saved) { for (var _k in _saved) { if (layerState.hasOwnProperty(_k)) layerState[_k] = _saved[_k]; } } } catch(e) {}
var drawerOpen = false;
// ════════════ MAP INIT ════════════
function initMap() {
map = L.map('map', { zoomControl: false, attributionControl: false, worldCopyJump: true }).setView([20, 0], 2.5);
L.control.zoom({ position: 'bottomleft' }).addTo(map);
L.tileLayer('https://{s}.basemaps.cartocdn.com/dark_all/{z}/{x}/{y}{r}.png', { maxZoom: 18 }).addTo(map);
var clusterOpts = function(cls, size) {
return {
maxClusterRadius: size || 40,
spiderfyOnMaxZoom: true,
showCoverageOnHover: false,
iconCreateFunction: function(cluster) {
var n = cluster.getChildCount();
var sz = n < 50 ? 'small' : n < 200 ? 'medium' : 'large';
return L.divIcon({ html: '<div>' + n + '</div>', className: 'marker-cluster marker-cluster-' + sz + (cls ? ' ' + cls : ''), iconSize: L.point(38, 38) });
}
};
};
mapLayers.quakes = L.layerGroup().addTo(map);
mapLayers.military = L.markerClusterGroup(clusterOpts('cluster-military', 50)).addTo(map);
mapLayers.conflict = L.markerClusterGroup(clusterOpts('cluster-conflict', 45)).addTo(map);
mapLayers.fires = L.markerClusterGroup(clusterOpts('cluster-fire', 45)).addTo(map);
mapLayers.convergence = L.layerGroup().addTo(map);
mapLayers.nuclear = L.layerGroup().addTo(map);
mapLayers.infra = L.layerGroup().addTo(map);
mapLayers.exposure = L.layerGroup().addTo(map);
mapLayers.airtraffic = L.markerClusterGroup(clusterOpts(null, 60)).addTo(map);
mapLayers.traffic = L.layerGroup().addTo(map);
mapLayers.navwarnings = L.markerClusterGroup(clusterOpts(null, 40)).addTo(map);
mapLayers.webcams = L.layerGroup().addTo(map);
mapLayers.climate = L.layerGroup().addTo(map);
mapLayers.news = L.markerClusterGroup(clusterOpts(null, 35)).addTo(map);
}
// ════════════ LAYER TOGGLES ════════════
$$('.layer-row').forEach(function(row) {
row.addEventListener('click', function(e) {
if (e.target.closest('.toggle')) { /* handled below */ }
var layer = row.dataset.layer;
var toggle = row.querySelector('.toggle');
toggle.classList.toggle('on');
layerState[layer] = toggle.classList.contains('on');
if (layerState[layer]) { mapLayers[layer].addTo(map); }
else { map.removeLayer(mapLayers[layer]); }
try { localStorage.setItem('phoenix-layers', JSON.stringify(layerState)); } catch(e2) {}
});
});
// ════════════ DRAWER TOGGLE ════════════
$('#drawerToggle').addEventListener('click', function() {
drawerOpen = !drawerOpen;
$('#drawer').classList.toggle('closed', !drawerOpen);
this.innerHTML = drawerOpen ? '▶' : '◀';
setTimeout(function() { if (map) map.invalidateSize(); }, 400);
});
// ════════════ 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: '#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) {
var title = '', subtitle = '', fields = [], link = '';
var c = COLORS[type] || 'var(--accent)';
if (type === 'earthquake') {
var mag = d.magnitude || 0;
title = 'M' + (typeof mag === 'number' ? mag.toFixed(1) : mag) + ' \u2014 ' + (d.place || 'Unknown');
subtitle = ago(d.time) + ' ago';
fields = [
['Magnitude', typeof mag === 'number' ? mag.toFixed(1) : mag, mag >= 6 ? 'crit' : mag >= 5 ? 'high' : ''],
['Location', d.place || '\u2014'],
['Depth', d.depth_km != null ? d.depth_km.toFixed(0) + ' km' : '\u2014'],
['Time', d.time ? new Date(d.time).toLocaleString() : '\u2014'],
['Coordinates', d.latitude != null ? d.latitude.toFixed(3) + ', ' + d.longitude.toFixed(3) : '\u2014'],
['Tsunami Alert', d.tsunami_alert ? 'YES' : 'No', d.tsunami_alert ? 'crit' : 'ok'],
['Felt Reports', d.felt_reports || '\u2014'],
['Alert Level', d.alert_level || 'None', d.alert_level === 'red' ? 'crit' : d.alert_level === 'orange' ? 'high' : '']
];
if (d.url) link = d.url;
} else if (type === 'military') {
title = d.callsign || d.icao24 || 'Unknown Aircraft';
subtitle = d.origin_country || '';
fields = [
['Callsign', d.callsign || '\u2014'],
['ICAO24', d.icao24 || '\u2014'],
['Origin', d.origin_country || '\u2014'],
['Altitude', d.altitude_m != null ? Math.round(d.altitude_m) + ' m (' + Math.round(d.altitude_m * 3.281) + ' ft)' : '\u2014'],
['Speed', d.velocity_ms != null ? Math.round(d.velocity_ms) + ' m/s (' + Math.round(d.velocity_ms * 1.944) + ' kts)' : '\u2014'],
['Heading', d.heading != null ? Math.round(d.heading) + '\u00B0' : '\u2014'],
['On Ground', d.on_ground ? 'Yes' : 'No'],
['Squawk', d.squawk || '\u2014'],
['Coordinates', d.latitude != null ? d.latitude.toFixed(3) + ', ' + d.longitude.toFixed(3) : '\u2014']
];
} else if (type === 'conflict') {
var fat = d.fatalities || 0;
title = (d.event_type || d.type || 'Conflict Event');
subtitle = (d.country || '') + (d.event_date ? ' \u2014 ' + d.event_date : '');
fields = [
['Event Type', d.event_type || d.type || '\u2014'],
['Country', d.country || '\u2014'],
['Location', d.location || d.admin1 || '\u2014'],
['Fatalities', String(fat), fat > 10 ? 'crit' : fat > 0 ? 'high' : ''],
['Date', d.event_date || '\u2014'],
['Actor 1', d.actor1 || d.side_a || '\u2014'],
['Actor 2', d.actor2 || d.side_b || '\u2014'],
['Source', d.source || '\u2014'],
['Notes', trunc(d.notes || '', 120)]
];
} else if (type === 'fire') {
title = 'Fire Cluster \u2014 ' + d.fire_count + ' detections';
subtitle = d.lat.toFixed(2) + ', ' + d.lon.toFixed(2);
fields = [
['Fire Count', String(d.fire_count), d.fire_count > 50 ? 'crit' : d.fire_count > 10 ? 'high' : ''],
['Max FRP', d.max_frp != null ? d.max_frp.toFixed(1) + ' MW' : '\u2014', d.max_frp > 100 ? 'high' : ''],
['Coordinates', d.lat.toFixed(4) + ', ' + d.lon.toFixed(4)],
['Region', d._region || '\u2014']
];
} else if (type === 'convergence') {
var score = d.convergence_score || 0;
title = d.name || 'Convergence Zone';
subtitle = 'Score: ' + score.toFixed(1);
fields = [
['Convergence Score', score.toFixed(1), score >= 7 ? 'crit' : score >= 4 ? 'high' : '']
];
var signals = d.signals || {};
for (var sk in signals) {
if (signals.hasOwnProperty(sk)) {
fields.push([sk.charAt(0).toUpperCase() + sk.slice(1), String(signals[sk])]);
}
}
if (d.lat != null) fields.push(['Coordinates', d.lat.toFixed(3) + ', ' + d.lon.toFixed(3)]);
} else if (type === 'nuclear') {
var cs = d.concern_score || 0;
title = (d.site || 'Unknown Site') + ' \u2014 M' + (d.magnitude || 0).toFixed(1);
subtitle = d.site_country || '';
fields = [
['Concern Score', cs.toFixed(1), cs >= 70 ? 'crit' : cs >= 50 ? 'high' : cs >= 30 ? 'high' : ''],
['Concern Level', d.concern_level || '\u2014', d.concern_level === 'critical' ? 'crit' : d.concern_level === 'high' ? 'high' : ''],
['Magnitude', (d.magnitude || 0).toFixed(1)],
['Depth', d.depth_km != null ? d.depth_km + ' km' : '\u2014', d.depth_km <= 5 ? 'high' : ''],
['Distance to Site', d.distance_km != null ? d.distance_km + ' km' : '\u2014'],
['Location', d.place || '\u2014'],
['Time', d.time || '\u2014'],
['Coordinates', d.latitude != null ? d.latitude.toFixed(3) + ', ' + d.longitude.toFixed(3) : '\u2014']
];
} else if (type === 'military_base') {
title = d.name || 'Base';
subtitle = d.operator + ' \u2014 ' + (d.country || '');
fields = [
['Name', d.name || '\u2014'],
['Operator', d.operator || '\u2014'],
['Host Country', d.country || '\u2014'],
['Type', (d.type || '\u2014').replace(/_/g, ' ')],
['Branch', d.branch || '\u2014'],
['Notes', d.notes || '\u2014'],
['Coordinates', d.latitude != null ? d.latitude.toFixed(3) + ', ' + d.longitude.toFixed(3) : '\u2014']
];
} else if (type === 'port') {
title = d.name || 'Port';
subtitle = (d.type || '').replace(/_/g, ' ') + ' \u2014 ' + (d.country || '');
fields = [
['Name', d.name || '\u2014'],
['Country', d.country || '\u2014'],
['Type', (d.type || '\u2014').replace(/_/g, ' ')],
['Throughput', d.throughput || '\u2014'],
['Notes', d.notes || '\u2014'],
['Coordinates', d.latitude != null ? d.latitude.toFixed(3) + ', ' + d.longitude.toFixed(3) : '\u2014']
];
} else if (type === 'nuclear_facility') {
title = d.name || 'Facility';
subtitle = (d.type || '').replace(/_/g, ' ') + ' \u2014 ' + (d.country || '');
var st = d.status || '';
fields = [
['Name', d.name || '\u2014'],
['Country', d.country || '\u2014'],
['Type', (d.type || '\u2014').replace(/_/g, ' ')],
['Status', st.replace(/_/g, ' '), st === 'occupied' ? 'crit' : st === 'decommissioning' ? 'high' : ''],
['Capacity', d.capacity_mw ? d.capacity_mw + ' MW' : 'N/A'],
['Operator', d.operator || '\u2014'],
['Notes', d.notes || '\u2014'],
['Coordinates', d.latitude != null ? d.latitude.toFixed(3) + ', ' + d.longitude.toFixed(3) : '\u2014']
];
} else if (type === 'exposure') {
title = d.city || 'Exposed City';
subtitle = (d.country || '') + ' \u2014 ' + (d.population || '');
fields = [
['City', d.city || '\u2014'],
['Country', d.country || '\u2014'],
['Population', d.population || '\u2014'],
['Nearest Event', d.nearest_event || '\u2014', d.nearest_event === 'conflict' ? 'crit' : 'high'],
['Event Detail', d.event_detail || '\u2014'],
['Distance', d.distance_km != null ? d.distance_km + ' km' : '\u2014']
];
} else if (type === 'airtraffic') {
title = d.callsign || 'Aircraft';
subtitle = d.origin || '';
fields = [
['Callsign', d.callsign || '\u2014'],
['Origin', d.origin || '\u2014'],
['Altitude', d.alt != null ? d.alt + ' m (' + Math.round(d.alt * 3.281) + ' ft)' : '\u2014'],
['Type', d.commercial ? 'Commercial' : 'General Aviation'],
['Coordinates', d.lat != null ? d.lat + ', ' + d.lon : '\u2014']
];
} else if (type === 'traffic_city') {
title = d.name || 'City';
subtitle = d.country || '';
var congPct2 = d.congestion_pct || 0;
fields = [
['City', d.name || '\u2014'],
['Country', d.country || '\u2014'],
['Congestion', congPct2 + '%', congPct2 >= 50 ? 'crit' : congPct2 >= 25 ? 'high' : ''],
['Current Speed', d.current_speed_kmh + ' km/h'],
['Free Flow', d.free_flow_speed_kmh + ' km/h'],
['Coordinates', d.lat + ', ' + d.lon]
];
} else if (type === 'webcam') {
title = d.title || 'Camera';
subtitle = (d.city || '') + (d.country ? ', ' + d.country : '');
fields = [
['City', d.city || '\u2014'],
['Country', d.country || '\u2014'],
['Status', d.status || '\u2014'],
['Coordinates', d.lat != null ? d.lat + ', ' + d.lon : '\u2014']
];
link = d.player_url || '';
} else if (type === 'news') {
title = d.title || 'Article';
subtitle = (d.feed_name || '') + (d._geo_country ? ' \u2014 ' + d._geo_country : '');
fields = [
['Title', d.title || '\u2014'],
['Source', d.feed_name || '\u2014'],
['Category', d.category || '\u2014'],
['Published', d.published ? new Date(d.published).toLocaleString() : '\u2014'],
['Summary', trunc(d.summary || '', 200)],
['Tier', d.source_tier || '\u2014'],
['Region', d._geo_country || '\u2014']
];
link = d.link || '';
} else if (type === 'navwarn') {
title = 'NAVAREA ' + (d.navarea || '?') + ' \u2014 ' + (d.id || '');
subtitle = d.status || '';
fields = [
['ID', d.id || '\u2014'],
['NAVAREA', d.navarea || '\u2014'],
['Subregion', d.subregion || '\u2014'],
['Status', d.status || '\u2014'],
['Issued', d.issue_date || '\u2014'],
['Authority', d.authority || '\u2014'],
['Text', d.text || '\u2014']
];
} else if (type === 'climate') {
var ta = d.temp_anomaly_c || 0;
title = (d.name || d.zone || 'Climate Zone');
subtitle = (ta > 0 ? '+' : '') + ta.toFixed(1) + '\u00B0C anomaly';
fields = [
['Zone', d.name || d.zone || '\u2014'],
['Temp Anomaly', (ta > 0 ? '+' : '') + ta.toFixed(1) + '\u00B0C', Math.abs(ta) > 2 ? 'crit' : Math.abs(ta) > 1 ? 'high' : ''],
['Current Avg', d.current_avg_temp_c != null ? d.current_avg_temp_c.toFixed(1) + '\u00B0C' : '\u2014'],
['Baseline Avg', d.baseline_avg_temp_c != null ? d.baseline_avg_temp_c.toFixed(1) + '\u00B0C' : '\u2014'],
['Precip Anomaly', d.precip_anomaly_pct != null ? (d.precip_anomaly_pct > 0 ? '+' : '') + d.precip_anomaly_pct.toFixed(0) + '%' : '\u2014', Math.abs(d.precip_anomaly_pct || 0) > 50 ? 'high' : ''],
['Significant', d.is_significant ? 'YES' : 'No', d.is_significant ? 'crit' : 'ok'],
['Coordinates', d.lat != null ? d.lat.toFixed(3) + ', ' + d.lon.toFixed(3) : '\u2014']
];
}
var hdr = '<div class="detail-cat"><span class="detail-cat-dot" style="background:' + c + ';--cat-color:' + c + '"></span><span class="detail-cat-label">' + esc(LABELS[type] || type) + '</span></div>' +
'<div class="detail-title">' + esc(title) + '</div>' +
(subtitle ? '<div class="detail-subtitle">' + esc(subtitle) + '</div>' : '');
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('');
if (link) body += '<a class="detail-link" href="' + esc(link) + '" data-external="1">View Source \u2192</a>';
$('#detailHeader').innerHTML = safe(hdr);
$('#detailBody').innerHTML = safe(body);
$('#detail').classList.add('open');
// Auto-refresh webcam preview every 15s
_clearWebcamRefresh();
if (type === 'webcam' && d.preview_url) {
_webcamRefreshInterval = setInterval(function() {
var img = document.querySelector('#detailBody img');
if (img) img.src = d.preview_url + (d.preview_url.indexOf('?') >= 0 ? '&' : '?') + '_t=' + Date.now();
}, 15000);
}
}
var _webcamRefreshInterval = null;
function _clearWebcamRefresh() { if (_webcamRefreshInterval) { clearInterval(_webcamRefreshInterval); _webcamRefreshInterval = null; } }
function closeDetail() { _clearWebcamRefresh(); $('#detail').classList.remove('open'); }
$('#detailClose').addEventListener('click', closeDetail);
// ════════════ DRAWER DETAIL TYPES ════════════
// Extend showDetail to handle drawer item types
COLORS.climate = '#ff6b6b'; LABELS.climate = 'CLIMATE ZONE';
COLORS.news = 'var(--accent)'; LABELS.news = 'NEWS ARTICLE';
COLORS.cyber = 'var(--red)'; LABELS.cyber = 'CYBER THREAT';
COLORS.health = 'var(--pink)'; LABELS.health = 'HEALTH ALERT';
COLORS.ai = 'var(--purple)'; LABELS.ai = 'AI / ML';
COLORS.social = 'var(--teal)'; LABELS.social = 'SOCIAL SIGNAL';
COLORS.prediction = 'var(--gold)'; LABELS.prediction = 'PREDICTION MARKET';
COLORS.election = 'var(--amber)'; LABELS.election = 'ELECTION';
COLORS.outage = 'var(--amber)'; LABELS.outage = 'INFRASTRUCTURE';
COLORS.navwarning = 'var(--blue)'; LABELS.navwarning = 'NAV WARNING';
COLORS.displacement = 'var(--pink)'; LABELS.displacement = 'DISPLACEMENT';
COLORS.space = 'var(--gold)'; LABELS.space = 'SPACE WEATHER';
COLORS.shipping = 'var(--teal)'; LABELS.shipping = 'SHIPPING';
COLORS.service = 'var(--blue)'; LABELS.service = 'SERVICE STATUS';
COLORS.pipeline = 'var(--purple)'; LABELS.pipeline = 'PIPELINE';
var _drawerItems = {};
var _drawerIdx = 0;
function _storeItem(type, data) {
var id = 'di_' + (++_drawerIdx);
_drawerItems[id] = {type: type, data: data};
return id;
}
function showDrawerDetail(id) {
var item = _drawerItems[id];
if (!item) return;
var type = item.type, d = item.data;
var title = '', subtitle = '', fields = [], link = '';
var c = COLORS[type] || 'var(--accent)';
if (type === 'news') {
title = d.title || 'Article';
subtitle = d.feed_name || d.source || '';
fields = [
['Source', d.feed_name || d.source || '\u2014'],
['Published', d.published ? new Date(d.published).toLocaleString() : (d.pub_date || '\u2014')],
['Category', d.category || d.feed_category || '\u2014'],
['Source Tier', d.source_tier || '\u2014']
];
if (d.summary || d.description) fields.push(['Summary', trunc(d.summary || d.description || '', 500)]);
link = d.link || d.url || '';
} else if (type === 'cyber') {
title = d.indicator || d.ioc || d.url || 'Threat';
subtitle = d.threat_type || d.type || '';
fields = [
['Indicator', d.indicator || d.ioc || d.url || '\u2014'],
['Type', d.threat_type || d.type || '\u2014'],
['Severity', d.severity || '\u2014', (d.severity||'').toLowerCase() === 'critical' ? 'crit' : (d.severity||'').toLowerCase() === 'high' ? 'high' : ''],
['Source', d.source || d.reporter || '\u2014'],
['First Seen', d.first_seen || d.date || '\u2014'],
['Tags', (d.tags || []).join(', ') || '\u2014']
];
link = d.reference || d.reference_url || '';
} else if (type === 'health') {
title = d.title || 'Health Alert';
subtitle = d.organization || '';
fields = [
['Organization', d.organization || '\u2014'],
['Published', d.published ? new Date(d.published).toLocaleString() : '\u2014'],
['High Concern', d.high_concern ? 'YES' : 'No', d.high_concern ? 'crit' : 'ok'],
['Summary', trunc(d.summary || d.description || '', 500)]
];
link = d.link || d.url || '';
} else if (type === 'ai') {
title = d.title || 'AI Paper';
subtitle = d.feed_name || '';
fields = [
['Source', d.feed_name || '\u2014'],
['Published', d.published ? new Date(d.published).toLocaleString() : '\u2014'],
['Category', d.category || '\u2014'],
['Summary', trunc(d.summary || d.description || '', 500)]
];
link = d.link || d.url || '';
} else if (type === 'social') {
title = d.title || 'Post';
subtitle = 'r/' + (d.subreddit || '?') + ' \u2022 Score: ' + (d.score || 0);
fields = [
['Subreddit', 'r/' + (d.subreddit || '\u2014')],
['Score', String(d.score || 0)],
['Comments', String(d.num_comments || d.comments || 0)],
['Author', d.author || '\u2014'],
['Created', d.created ? new Date(d.created * 1000).toLocaleString() : '\u2014']
];
if (d.selftext) fields.push(['Text', trunc(d.selftext, 500)]);
link = d.url || d.permalink || '';
} else if (type === 'prediction') {
var prob = d.probability || d.yes_price;
title = d.question || d.title || 'Market';
subtitle = prob != null ? 'Probability: ' + (typeof prob === 'number' ? (prob * 100).toFixed(0) + '%' : String(prob)) : '';
fields = [
['Question', d.question || d.title || '\u2014'],
['Probability', prob != null ? (typeof prob === 'number' ? (prob * 100).toFixed(1) + '%' : String(prob)) : '\u2014'],
['Volume', d.volume ? fmtBig(d.volume) : '\u2014'],
['Market', d.market_slug || d.platform || '\u2014'],
['End Date', d.end_date || d.close_time || '\u2014']
];
link = d.url || '';
} else if (type === 'election') {
title = (d.country || '?') + ' \u2014 ' + (d.election_type || 'Election');
subtitle = d.date || '';
fields = [
['Country', d.country || '\u2014'],
['Type', d.election_type || '\u2014'],
['Date', d.date || '\u2014'],
['Days Until', d.days_until != null ? String(d.days_until) : '\u2014'],
['Risk Score', d.risk_score != null ? String(d.risk_score) : '\u2014', (d.risk_score||0) >= 80 ? 'crit' : (d.risk_score||0) >= 50 ? 'high' : ''],
['Impact', d.instability_impact || '\u2014'],
['Description', d.description || '\u2014']
];
} else if (type === 'outage') {
title = d.entity_name || d.country || d.location || 'Outage';
subtitle = d.entity_type || '';
fields = [
['Entity', d.entity_name || '\u2014'],
['Type', d.entity_type || '\u2014'],
['Country', d.country || '\u2014'],
['Score', d.overall_score != null ? String(d.overall_score) : '\u2014'],
['Start', d.from || d.start || '\u2014'],
['End', d.until || d.end || '\u2014']
];
} else if (type === 'navwarning') {
title = (d.navArea || d.area || 'Nav Warning');
subtitle = '';
fields = [
['Area', d.navArea || d.area || '\u2014'],
['Number', d.number || d.id || '\u2014'],
['Issued', d.dtg || d.date || '\u2014'],
['Text', d.text || d.description || '\u2014']
];
} else if (type === 'pipeline') {
title = d.name || 'Pipeline';
subtitle = d.route || '';
var ps = d.status || '';
fields = [
['Name', d.name || '\u2014'],
['Route', d.route || '\u2014'],
['Type', (d.type || '\u2014')],
['Capacity', d.capacity || '\u2014'],
['Status', ps.replace(/_/g, ' '), ps === 'destroyed' ? 'crit' : ps === 'terminated' ? 'crit' : ps === 'stalled' ? 'high' : ''],
['Notes', d.notes || '\u2014']
];
} else if (type === 'service') {
title = (d.provider || '?') + ' \u2014 ' + (d.title || 'Incident');
subtitle = d.severity || '';
fields = [
['Provider', d.provider || '\u2014'],
['Title', d.title || '\u2014'],
['Severity', d.severity || '\u2014', d.severity === 'critical' ? 'crit' : d.severity === 'high' ? 'high' : ''],
['Published', d.published ? new Date(d.published).toLocaleString() : '\u2014'],
['Summary', trunc(d.summary || '', 500)]
];
link = d.link || '';
} else if (type === 'airtraffic') {
title = d.callsign || 'Aircraft';
subtitle = d.origin || '';
fields = [
['Callsign', d.callsign || '\u2014'],
['Origin', d.origin || '\u2014'],
['Altitude', d.alt != null ? d.alt + ' m (' + Math.round(d.alt * 3.281) + ' ft)' : '\u2014'],
['Type', d.commercial ? 'Commercial' : 'General Aviation'],
['Coordinates', d.lat != null ? d.lat + ', ' + d.lon : '\u2014']
];
} else if (type === 'traffic_city') {
title = d.name || 'City';
subtitle = d.country || '';
var congPct = d.congestion_pct || 0;
fields = [
['City', d.name || '\u2014'],
['Country', d.country || '\u2014'],
['Congestion', congPct + '%', congPct >= 50 ? 'crit' : congPct >= 25 ? 'high' : ''],
['Current Speed', d.current_speed_kmh + ' km/h'],
['Free Flow', d.free_flow_speed_kmh + ' km/h'],
['Coordinates', d.lat + ', ' + d.lon]
];
} else if (type === 'webcam') {
title = d.title || 'Camera';
subtitle = (d.city || '') + (d.country ? ', ' + d.country : '');
fields = [
['City', d.city || '\u2014'],
['Country', d.country || '\u2014'],
['Status', d.status || '\u2014'],
['Coordinates', d.lat != null ? d.lat + ', ' + d.lon : '\u2014']
];
link = d.player_url || '';
} else if (type === 'navwarn') {
title = 'NAVAREA ' + (d.navarea || '?') + ' \u2014 ' + (d.id || '');
subtitle = d.status || '';
fields = [
['ID', d.id || '\u2014'],
['NAVAREA', d.navarea || '\u2014'],
['Subregion', d.subregion || '\u2014'],
['Status', d.status || '\u2014'],
['Issued', d.issue_date || '\u2014'],
['Authority', d.authority || '\u2014'],
['Text', d.text || '\u2014']
];
} else {
title = d.title || d.name || JSON.stringify(d).slice(0, 80);
fields = Object.keys(d).slice(0, 12).map(function(k) { return [k, trunc(String(d[k] || ''), 120)]; });
}
var hdr = '<div class="detail-cat"><span class="detail-cat-dot" style="background:' + c + ';--cat-color:' + c + '"></span><span class="detail-cat-label">' + esc(LABELS[type] || type.toUpperCase()) + '</span></div>' +
'<div class="detail-title">' + esc(title) + '</div>' +
(subtitle ? '<div class="detail-subtitle">' + esc(subtitle) + '</div>' : '');
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('');
if (link) body += '<a class="detail-link" href="' + esc(link) + '" data-external="1">View Source \u2192</a>';
$('#detailHeader').innerHTML = safe(hdr);
$('#detailBody').innerHTML = safe(body);
$('#detail').classList.add('open');
_clearWebcamRefresh();
if (type === 'webcam' && d.preview_url) {
_webcamRefreshInterval = setInterval(function() {
var img = document.querySelector('#detailBody img');
if (img) img.src = d.preview_url + (d.preview_url.indexOf('?') >= 0 ? '&' : '?') + '_t=' + Date.now();
}, 15000);
}
}
// ════════════ GLOBAL LINK INTERCEPTOR ════════════
document.addEventListener('click', function(e) {
var a = e.target.closest('a[href]');
if (!a) return;
var href = a.getAttribute('href');
if (!href || href === '#' || href.startsWith('javascript:')) return;
// External links open in new tab
if (href.startsWith('http://') || href.startsWith('https://') || a.hasAttribute('data-external')) {
e.preventDefault();
e.stopPropagation();
window.open(href, '_blank', 'noopener,noreferrer');
}
});
// ════════════ KEYBOARD SHORTCUTS ════════════
var _helpOverlay = null;
function _buildHelpOverlay() {
var overlay = document.createElement('div');
overlay.style.cssText = 'position:fixed;inset:0;z-index:20000;background:rgba(0,0,0,.85);display:flex;align-items:center;justify-content:center;';
var box = document.createElement('div');
box.style.cssText = 'background:#1a1a2e;border:1px solid #333;border-radius:8px;padding:24px 32px;max-width:520px;width:90%;color:#e0e0e0;font-family:monospace;font-size:13px;';
// Header
var hdr = document.createElement('div');
hdr.style.cssText = 'display:flex;justify-content:space-between;align-items:center;margin-bottom:16px;';
var title = document.createElement('span');
title.style.cssText = 'font-size:16px;font-weight:bold;color:#00d4ff;';
title.textContent = 'Keyboard Shortcuts';
var closeBtn = document.createElement('span');
closeBtn.style.cssText = 'cursor:pointer;color:#888;font-size:18px;';
closeBtn.textContent = '\u2715';
closeBtn.addEventListener('click', function() { overlay.remove(); _helpOverlay = null; });
hdr.appendChild(title);
hdr.appendChild(closeBtn);
box.appendChild(hdr);
// Table
var tbl = document.createElement('table');
tbl.style.cssText = 'width:100%;border-collapse:collapse;';
var rows = [
['__section', 'Navigation'],
['?', 'Toggle this help'],
['/', 'Focus search'],
['Esc', 'Close panels / blur search'],
['d', 'Toggle drawer'],
['l', 'Toggle layer panel'],
['r', 'Reset globe view'],
['+ / -', 'Zoom in / out'],
['__section', 'Map Layers (1-9, 0)'],
['1', 'Earthquakes'], ['2', 'Military flights'], ['3', 'Conflict zones'],
['4', 'Wildfires'], ['5', 'Signal convergence'], ['6', 'Nuclear monitor'],
['7', 'Infrastructure'], ['8', 'Population exposure'], ['9', 'Air traffic'],
['0', 'News geolocation'],
['__section', 'Actions'],
['a', 'Toggle all layers on/off'],
['f', 'Toggle fullscreen'],
];
rows.forEach(function(r) {
var tr = document.createElement('tr');
if (r[0] === '__section') {
var td = document.createElement('td');
td.colSpan = 2;
td.style.cssText = 'padding:10px 0 6px;color:#888;border-bottom:1px solid #333;';
td.textContent = r[1];
tr.appendChild(td);
} else {
var kd = document.createElement('td');
kd.style.cssText = 'padding:4px 0;';
var kbd = document.createElement('kbd');
kbd.style.cssText = 'background:#333;padding:2px 6px;border-radius:3px;';
kbd.textContent = r[0];
kd.appendChild(kbd);
var vd = document.createElement('td');
vd.textContent = r[1];
tr.appendChild(kd);
tr.appendChild(vd);
}
tbl.appendChild(tr);
});
box.appendChild(tbl);
overlay.appendChild(box);
overlay.addEventListener('click', function(ev) { if (ev.target === overlay) { overlay.remove(); _helpOverlay = null; } });
return overlay;
}
function _toggleHelp() {
if (_helpOverlay) { _helpOverlay.remove(); _helpOverlay = null; return; }
_helpOverlay = _buildHelpOverlay();
document.body.appendChild(_helpOverlay);
}
var _layerKeys = ['quakes','military','conflict','fires','convergence','nuclear','infra','exposure','airtraffic','news'];
function _toggleLayerByIndex(idx) {
var key = _layerKeys[idx];
if (!key || !mapLayers[key]) return;
var row = document.querySelector('.layer-row[data-layer="' + key + '"]');
if (!row) return;
var toggle = row.querySelector('.toggle');
toggle.classList.toggle('on');
layerState[key] = toggle.classList.contains('on');
if (layerState[key]) { mapLayers[key].addTo(map); } else { map.removeLayer(mapLayers[key]); }
try { localStorage.setItem('phoenix-layers', JSON.stringify(layerState)); } catch(e) {}
}
var _allLayersOn = true;
function _toggleAllLayers() {
_allLayersOn = !_allLayersOn;
_layerKeys.forEach(function(key) {
if (!mapLayers[key]) return;
layerState[key] = _allLayersOn;
if (_allLayersOn) { mapLayers[key].addTo(map); } else { map.removeLayer(mapLayers[key]); }
var row = document.querySelector('.layer-row[data-layer="' + key + '"]');
if (row) { var t = row.querySelector('.toggle'); if (t) { if (_allLayersOn) t.classList.add('on'); else t.classList.remove('on'); } }
});
try { localStorage.setItem('phoenix-layers', JSON.stringify(layerState)); } catch(e) {}
}
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
if (_helpOverlay) { _helpOverlay.remove(); _helpOverlay = null; return; }
closeDetail();
if (!$('#drawer').classList.contains('closed')) {
drawerOpen = false;
$('#drawer').classList.add('closed');
$('#drawerToggle').textContent = '\u25C0';
}
$('#searchInput').blur();
return;
}
if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return;
if (e.key === '?') { _toggleHelp(); return; }
if (e.key === '/') { e.preventDefault(); $('#searchInput').focus(); return; }
if (e.key === 'd') {
drawerOpen = !drawerOpen;
$('#drawer').classList.toggle('closed', !drawerOpen);
$('#drawerToggle').textContent = drawerOpen ? '\u25B6' : '\u25C0';
return;
}
if (e.key === 'l') {
var lp = $('#layerPanel');
if (lp) lp.classList.toggle('collapsed');
return;
}
if (e.key === 'r') { map.setView([20, 0], 2.5); return; }
if (e.key === '=' || e.key === '+') { map.zoomIn(); return; }
if (e.key === '-') { map.zoomOut(); return; }
if (e.key === 'f') {
if (!document.fullscreenElement) { document.documentElement.requestFullscreen().catch(function(){}); }
else { document.exitFullscreen(); }
return;
}
if (e.key === 'a') { _toggleAllLayers(); return; }
if (e.key >= '1' && e.key <= '9') { _toggleLayerByIndex(parseInt(e.key) - 1); return; }
if (e.key === '0') { _toggleLayerByIndex(9); return; }
});
// ════════════ MAP MARKER MANAGEMENT ════════════
function updateMapQuakes(data) {
mapLayers.quakes.clearLayers();
if (!data || data.error) { $('#cntQuakes').textContent = '0'; return; }
var quakes = (data.earthquakes || []).filter(function(q) {
return _withinTimeWindow(q.time || q.timestamp);
});
$('#cntQuakes').textContent = quakes.length;
quakes.slice(0, 100).forEach(function(q) {
if (q.latitude == null || q.longitude == null) return;
var mag = q.magnitude || 1;
var size = Math.max(10, mag * 5);
var mk = L.marker([q.latitude, q.longitude], {
icon: L.divIcon({ className: 'mk-quake' + (mag >= 5 ? ' crit' : ''), iconSize: [size, size], iconAnchor: [size/2, size/2] })
});
mk.bindTooltip('M' + (typeof mag === 'number' ? mag.toFixed(1) : mag) + ' ' + trunc(q.place || '', 25), { className: 'mk-tip', direction: 'top', offset: [0, -size/2] });
mk.on('click', function() { showDetail('earthquake', q); });
mk.addTo(mapLayers.quakes);
});
}
function updateMapMilitary(data) {
mapLayers.military.clearLayers();
if (!data || data.error) { $('#cntMil').textContent = '0'; return; }
var aircraft = data.aircraft || [];
$('#cntMil').textContent = data.count || aircraft.length;
aircraft.slice(0, 100).forEach(function(f) {
if (f.latitude == null || f.longitude == null) return;
var mk = L.marker([f.latitude, f.longitude], {
icon: L.divIcon({ className: 'mk-mil', iconSize: [12, 12], iconAnchor: [6, 6] })
});
mk.bindTooltip((f.callsign || f.icao24 || '?') + ' \u2014 ' + (f.origin_country || ''), { className: 'mk-tip', direction: 'top', offset: [0, -6] });
mk.on('click', function() { showDetail('military', f); });
mk.addTo(mapLayers.military);
});
}
function updateMapConflict(data) {
mapLayers.conflict.clearLayers();
if (!data || data.error) { $('#cntConflict').textContent = '0'; return; }
var events = data.events || [];
$('#cntConflict').textContent = data.count || events.length;
events.slice(0, 200).forEach(function(e) {
var lat = e.latitude || e.lat, lon = e.longitude || e.lon;
if (lat == null || lon == null) return;
var fat = e.fatalities || e.best || 0;
var esc_level = e.escalation || 0;
var size = fat > 0 ? Math.max(8, Math.min(18, fat / 2 + 8)) : Math.max(8, esc_level * 3 + 6);
var label = e.event_type || e.type_of_violence_label || '?';
var mk = L.marker([Number(lat), Number(lon)], {
icon: L.divIcon({ className: 'mk-conflict', iconSize: [size, size], iconAnchor: [size/2, size/2] })
});
var tip = label + ' \u2014 ' + (e.country || '');
if (fat > 0) tip += ' (' + fat + ' fatal)';
else if (e.severity) tip += ' [' + e.severity + ']';
mk.bindTooltip(tip, { className: 'mk-tip', direction: 'top', offset: [0, -size/2] });
mk.on('click', function() { showDetail('conflict', e); });
mk.addTo(mapLayers.conflict);
});
}
function updateMapFires(data) {
mapLayers.fires.clearLayers();
if (!data || data.error) { $('#cntFires').textContent = '0'; return; }
var regions = data.fires_by_region || {};
var total = 0;
for (var rName in regions) {
if (!regions.hasOwnProperty(rName)) continue;
var r = regions[rName];
total += r.count || 0;
var clusters = r.top_clusters || [];
clusters.forEach(function(c) {
if (c.lat == null || c.lon == null) return;
var size = Math.max(8, Math.min(20, c.fire_count / 3 + 6));
var obj = { lat: c.lat, lon: c.lon, fire_count: c.fire_count, max_frp: c.max_frp, _region: rName };
var mk = L.marker([c.lat, c.lon], {
icon: L.divIcon({ className: 'mk-fire', iconSize: [size, size], iconAnchor: [size/2, size/2] })
});
mk.bindTooltip(c.fire_count + ' fires \u2014 ' + rName, { className: 'mk-tip', direction: 'top', offset: [0, -size/2] });
mk.on('click', function() { showDetail('fire', obj); });
mk.addTo(mapLayers.fires);
});
}
var clusterCount = mapLayers.fires.getLayers().length;
// Show cluster count for sidebar (what's on the map), full total in tooltip
$('#cntFires').textContent = clusterCount;
$('#cntFires').title = (data.total_fires || total) + ' individual fire detections';
}
function updateMapConvergence(data) {
mapLayers.convergence.clearLayers();
if (!data || data.error) { $('#cntConv').textContent = '0'; return; }
var hotspots = data.hotspots || [];
$('#cntConv').textContent = hotspots.length;
hotspots.forEach(function(h) {
if (h.lat == null || h.lon == null) return;
var score = h.convergence_score || 0;
var size = 28 + score * 8;
var mk = L.marker([h.lat, h.lon], {
icon: L.divIcon({ className: 'mk-conv', iconSize: [size, size], iconAnchor: [size/2, size/2] })
});
mk.bindTooltip((h.name || '?') + ' \u2014 Score ' + score.toFixed(1), { className: 'mk-tip', direction: 'top', offset: [0, -size/2] });
mk.on('click', function() { showDetail('convergence', h); });
mk.addTo(mapLayers.convergence);
});
}
function updateMapNuclear(data) {
mapLayers.nuclear.clearLayers();
if (!data || data.error) { $('#cntNuclear').textContent = '0'; return; }
var sites = data.sites || [];
var flagged = 0;
sites.forEach(function(site) {
if (site.lat == null || site.lon == null) return;
var evts = site.events_detected || 0;
flagged += evts;
var highest = site.highest_concern;
var isCrit = highest && highest.concern_level === 'critical';
var size = evts > 0 ? Math.max(14, Math.min(24, evts * 3 + 10)) : 10;
var mk = L.marker([site.lat, site.lon], {
icon: L.divIcon({ className: 'mk-nuclear' + (isCrit ? ' crit' : ''), iconSize: [size, size], iconAnchor: [size/2, size/2] })
});
var tip = esc(site.name) + ' (' + esc(site.status) + ')';
if (evts > 0) tip += ' \u2014 ' + evts + ' events';
mk.bindTooltip(tip, { className: 'mk-tip', direction: 'top', offset: [0, -size/2] });
mk.on('click', function() {
if (highest) { showDetail('nuclear', highest); }
});
mk.addTo(mapLayers.nuclear);
});
$('#cntNuclear').textContent = flagged;
}
function updateMapInfra(data) {
mapLayers.infra.clearLayers();
var total = 0;
// Military bases
var bases = (data.military_bases && data.military_bases.bases) || [];
bases.forEach(function(b) {
if (b.lat == null || b.lon == null) return;
total++;
var mk = L.marker([b.lat, b.lon], {
icon: L.divIcon({ className: 'mk-base', iconSize: [8, 8], iconAnchor: [4, 4] })
});
mk.bindTooltip(esc(b.name) + ' (' + esc(b.operator) + ')', { className: 'mk-tip', direction: 'top', offset: [0, -4] });
mk.on('click', function() {
showDetail('military_base', {
name: b.name, country: b.country, operator: b.operator,
type: b.type, branch: b.branch, notes: b.notes,
latitude: b.lat, longitude: b.lon
});
});
mk.addTo(mapLayers.infra);
});
// Strategic ports
var ports = (data.strategic_ports && data.strategic_ports.ports) || [];
ports.forEach(function(p) {
if (p.lat == null || p.lon == null) return;
total++;
var mk = L.marker([p.lat, p.lon], {
icon: L.divIcon({ className: 'mk-port', iconSize: [7, 7], iconAnchor: [3.5, 3.5] })
});
mk.bindTooltip(esc(p.name) + ' (' + esc(p.type) + ')', { className: 'mk-tip', direction: 'top', offset: [0, -4] });
mk.on('click', function() {
showDetail('port', {
name: p.name, country: p.country, type: p.type,
throughput: p.throughput, notes: p.notes,
latitude: p.lat, longitude: p.lon
});
});
mk.addTo(mapLayers.infra);
});
// Nuclear facilities
var nukes = (data.nuclear_facilities && data.nuclear_facilities.facilities) || [];
nukes.forEach(function(n) {
if (n.lat == null || n.lon == null) return;
total++;
var isHot = n.status === 'occupied' || n.type === 'enrichment';
var mk = L.marker([n.lat, n.lon], {
icon: L.divIcon({ className: 'mk-nuke-fac' + (isHot ? ' hot' : ''), iconSize: [9, 9], iconAnchor: [4.5, 4.5] })
});
mk.bindTooltip(esc(n.name) + ' (' + esc(n.status) + ')', { className: 'mk-tip', direction: 'top', offset: [0, -5] });
mk.on('click', function() {
showDetail('nuclear_facility', {
name: n.name, country: n.country, type: n.type,
capacity_mw: n.capacity_mw, status: n.status,
operator: n.operator, notes: n.notes,
latitude: n.lat, longitude: n.lon
});
});
mk.addTo(mapLayers.infra);
});
// Pipelines (polylines: lat_start/lon_start → lat_end/lon_end)
var pipeColors = { oil: '#ff9100', gas: '#4da8ff', hydrogen: '#34d399', lng: '#2dd4bf' };
var pipeStatusOpacity = { active: 0.55, destroyed: 0.25, terminated: 0.25, cancelled: 0.2, intermittent: 0.45, reduced: 0.4, stalled: 0.3, construction: 0.35, proposed: 0.2 };
var pipes = (data.pipelines && data.pipelines.pipelines) || [];
pipes.forEach(function(p) {
if (p.lat_start == null || p.lon_start == null || p.lat_end == null || p.lon_end == null) return;
total++;
var color = pipeColors[p.type] || '#a78bfa';
var opacity = pipeStatusOpacity[p.status] || 0.4;
var dash = (p.status === 'proposed' || p.status === 'construction') ? '6 4' : (p.status === 'destroyed' || p.status === 'terminated' || p.status === 'cancelled') ? '3 6' : null;
var opts = { color: color, weight: 2, opacity: opacity, className: 'pipeline-line' };
if (dash) opts.dashArray = dash;
var line = L.polyline([[p.lat_start, p.lon_start], [p.lat_end, p.lon_end]], opts);
line.bindTooltip(esc(p.name) + '<br><span style="opacity:0.7">' + esc(p.type) + ' \u2022 ' + esc(p.status) + '</span>', { className: 'mk-tip', sticky: true });
line.on('click', function() {
showDetail('pipeline', {
name: p.name, route: p.route, type: p.type,
capacity: p.capacity, status: p.status, notes: p.notes
});
});
line.addTo(mapLayers.infra);
});
// Strategic waterways (diamond markers at chokepoints)
var wws = (data.waterways && data.waterways.waterways) || [];
wws.forEach(function(w) {
if (w.lat == null || w.lon == null) return;
total++;
var mk = L.marker([w.lat, w.lon], {
icon: L.divIcon({ className: 'mk-waterway', iconSize: [10, 10], iconAnchor: [5, 5] })
});
mk.bindTooltip('<b>' + esc(w.name) + '</b><br><span style="opacity:0.7">' + esc(w.throughput || '') + '</span>', { className: 'mk-tip', direction: 'top', offset: [0, -6] });
mk.addTo(mapLayers.infra);
});
// Trade routes / maritime chokepoints (anchor markers)
var trs = (data.trade_routes && data.trade_routes.routes) || [];
trs.forEach(function(tr) {
if (tr.lat == null || tr.lon == null) return;
total++;
var iconCls = tr.type === 'chokepoint' ? 'mk-chokepoint' : tr.type === 'canal' ? 'mk-canal' : 'mk-sealane';
var mk = L.marker([tr.lat, tr.lon], {
icon: L.divIcon({ className: iconCls, iconSize: [8, 8], iconAnchor: [4, 4] })
});
mk.bindTooltip('<b>' + esc(tr.name) + '</b><br>' +
'<span style="opacity:0.7">' + esc(tr.type) + ' · ' + (tr.oil_flow_mbd || 0) + ' mbd oil · ~' + (tr.daily_vessel_transits || 0) + ' vessels/day</span>',
{ className: 'mk-tip', direction: 'top', offset: [0, -5] });
mk.on('click', function() {
showDetail('trade_route', {
name: tr.name, type: tr.type, oil_flow_mbd: tr.oil_flow_mbd,
daily_transits: tr.daily_vessel_transits, trade_value_pct: tr.trade_value_pct,
countries: (tr.countries || []).join(', '), notes: tr.notes
});
});
mk.addTo(mapLayers.infra);
});
// Submarine cable corridors (shaded rectangles)
var cables = (data.cable_corridors && data.cable_corridors.corridors) || [];
var cableStyle = { color: '#4da8ff', weight: 1, opacity: 0.2, fillColor: '#4da8ff', fillOpacity: 0.04, dashArray: '4 3', className: 'cable-corridor', interactive: false };
cables.forEach(function(c) {
if (!c.lat_range || !c.lon_range) return;
total++;
// Handle antimeridian crossing (lon_start > lon_end means wrapping past 180)
if (c.lon_range[0] > c.lon_range[1]) {
L.rectangle([[c.lat_range[0], c.lon_range[0]], [c.lat_range[1], 180]], cableStyle).addTo(mapLayers.infra);
L.rectangle([[c.lat_range[0], -180], [c.lat_range[1], c.lon_range[1]]], cableStyle).addTo(mapLayers.infra);
} else {
L.rectangle([[c.lat_range[0], c.lon_range[0]], [c.lat_range[1], c.lon_range[1]]], cableStyle).addTo(mapLayers.infra);
}
});
$('#cntInfra').textContent = total;
}
function updateMapExposure(data) {
mapLayers.exposure.clearLayers();
if (!data || data.error) { $('#cntExposure').textContent = '0'; return; }
var cities = data.exposed_cities || [];
var evtColors = { conflict: '#ff1744', earthquake: '#ff9100', wildfire: '#ffea00' };
$('#cntExposure').textContent = cities.length;
cities.forEach(function(c) {
if (c.lat == null || c.lon == null) return;
var color = evtColors[c.nearest_event] || '#e040fb';
var pop = c.population || 0;
var radius = Math.max(5, Math.min(22, Math.sqrt(pop / 400000)));
var circle = L.circleMarker([c.lat, c.lon], {
radius: radius, color: color, fillColor: color, fillOpacity: 0.3,
weight: 2, opacity: 0.7
});
var popStr = pop >= 1000000 ? (pop / 1000000).toFixed(1) + 'M' : (pop / 1000).toFixed(0) + 'K';
circle.bindTooltip(
'<b>' + esc(c.city) + '</b> (' + popStr + ')<br>' +
'<span style="color:' + color + '">' + esc(c.nearest_event) + '</span> ' + c.distance_km + 'km',
{ className: 'mk-tip', direction: 'top' }
);
circle.on('click', function() {
showDetail('exposure', {
city: c.city, country: c.country, population: popStr,
nearest_event: c.nearest_event, event_detail: c.event_detail,
distance_km: c.distance_km
});
});
circle.addTo(mapLayers.exposure);
});
}
function updateMapAirTraffic(data) {
mapLayers.airtraffic.clearLayers();
if (!data || data.error) { $('#cntAirTraffic').textContent = '0'; return; }
var positions = data.positions || [];
$('#cntAirTraffic').textContent = data.total_aircraft || positions.length;
var markers = [];
positions.forEach(function(p) {
if (p.lat == null || p.lon == null) return;
var mk = L.marker([p.lat, p.lon], {
icon: L.divIcon({ className: 'mk-plane' + (p.commercial ? ' comm' : ''), iconSize: [4, 4], iconAnchor: [2, 2] })
});
var tip = (p.callsign || '?') + ' \u2014 ' + (p.origin || '?');
if (p.alt) tip += ' @ ' + p.alt + 'm';
mk.bindTooltip(tip, { className: 'mk-tip', direction: 'top', offset: [0, -3] });
mk.on('click', function() { showDetail('airtraffic', p); });
markers.push(mk);
});
mapLayers.airtraffic.addLayers(markers);
}
function updateMapTraffic(data) {
mapLayers.traffic.clearLayers();
if (!data || data.error) { $('#cntTraffic').textContent = '0'; return; }
var cities = data.cities || [];
$('#cntTraffic').textContent = cities.length;
cities.forEach(function(c) {
if (c.lat == null || c.lon == null) return;
var pct = c.congestion_pct || 0;
var size = Math.max(14, Math.min(30, pct / 2 + 10));
var cls = pct >= 50 ? ' crit' : '';
var mk = L.marker([c.lat, c.lon], {
icon: L.divIcon({ className: 'mk-traffic' + cls, iconSize: [size, size], iconAnchor: [size/2, size/2] })
});
mk.bindTooltip(esc(c.name) + ' ' + pct + '% congestion', { className: 'mk-tip', direction: 'top', offset: [0, -size/2] });
mk.on('click', function() { showDetail('traffic_city', c); });
mk.addTo(mapLayers.traffic);
});
}
function updateMapNavWarnings(data) {
mapLayers.navwarnings.clearLayers();
if (!data || data.error) { $('#cntNavWarn').textContent = '0'; return; }
var warnings = data.warnings || [];
var mapped = 0;
warnings.forEach(function(w) {
if (w.lat == null || w.lon == null) return;
mapped++;
var mk = L.marker([w.lat, w.lon], {
icon: L.divIcon({ className: 'mk-navwarn', iconSize: [6, 6], iconAnchor: [3, 3] })
});
var tip = 'NAVAREA ' + (w.navarea || '?') + ' ' + (w.id || '');
mk.bindTooltip(tip, { className: 'mk-tip', direction: 'top', offset: [0, -4] });
mk.on('click', function() { showDetail('navwarn', w); });
mk.addTo(mapLayers.navwarnings);
});
$('#cntNavWarn').textContent = mapped;
}
function updateMapWebcams(data) {
mapLayers.webcams.clearLayers();
if (!data || data.error) { $('#cntCams').textContent = '0'; return; }
var cams = data.cameras || [];
$('#cntCams').textContent = cams.length;
cams.forEach(function(cam) {
if (cam.lat == null || cam.lon == null) return;
var mk = L.marker([cam.lat, cam.lon], {
icon: L.divIcon({ className: 'mk-cam', iconSize: [7, 7], iconAnchor: [3.5, 3.5] })
});
mk.bindTooltip(esc(cam.title || 'Camera') + '<br><span style="opacity:0.7">' + esc(cam.city || '') + '</span>', { className: 'mk-tip', direction: 'top', offset: [0, -4] });
mk.on('click', function() { showDetail('webcam', cam); });
mk.addTo(mapLayers.webcams);
});
}
function updateMapClimate(data) {
mapLayers.climate.clearLayers();
if (!data || data.error) { $('#cntClimate').textContent = '0'; return; }
var zones = data.anomalies || data.zones || data.data || [];
if (!Array.isArray(zones)) zones = Object.values(zones);
$('#cntClimate').textContent = zones.length;
zones.forEach(function(z) {
if (z.lat == null || z.lon == null) return;
var ta = z.temp_anomaly_c != null ? z.temp_anomaly_c : (z.temperature_anomaly || z.temp_anomaly || 0);
var abs = Math.abs(ta);
var cls = ta > 2 ? 'hot' : ta > 0 ? 'warm' : ta < -2 ? 'cold' : 'cool';
if (z.is_significant) cls += ' sig';
var size = Math.max(16, Math.min(32, abs * 6 + 14));
var mk = L.marker([z.lat, z.lon], {
icon: L.divIcon({ className: 'mk-climate ' + cls, iconSize: [size, size], iconAnchor: [size/2, size/2] })
});
var tip = esc(z.name || z.zone || '?') + ' ' + (ta > 0 ? '+' : '') + ta.toFixed(1) + '\u00B0C';
if (z.is_significant) tip += ' \u26A0';
mk.bindTooltip(tip, { className: 'mk-tip', direction: 'top', offset: [0, -size/2] });
mk.on('click', function() { showDetail('climate', z); });
mk.addTo(mapLayers.climate);
});
}
// Country name → centroid for news geolocation
var COUNTRY_CENTROIDS = {
'ukraine':[48.4,31.2],'russia':[61.5,105],'china':[35.9,104.2],'taiwan':[23.7,121],
'iran':[32.4,53.7],'israel':[31.0,34.8],'palestine':[31.9,35.2],'gaza':[31.4,34.4],
'syria':[35,38.9],'iraq':[33.2,43.7],'yemen':[15.6,48.5],'lebanon':[33.9,35.9],
'north korea':[40,127],'south korea':[35.9,128],'japan':[36.2,138.3],'india':[20.6,79],
'pakistan':[30.4,69.3],'afghanistan':[33.9,67.7],'myanmar':[19.8,96.7],
'ethiopia':[9.1,40.5],'sudan':[12.9,30.2],'somalia':[5.2,46.2],'nigeria':[9.1,8.7],
'congo':[-4.0,21.8],'mali':[17.6,-4],'libya':[26.3,17.2],'mozambique':[-18.7,35.5],
'united states':[37.1,-95.7],'usa':[37.1,-95.7],'u.s.':[37.1,-95.7],
'united kingdom':[55.4,-3.4],'uk':[55.4,-3.4],'britain':[55.4,-3.4],
'france':[46.2,2.2],'germany':[51.2,10.4],'italy':[41.9,12.6],'spain':[40.5,-3.7],
'turkey':[39,35.2],'egypt':[26.8,30.8],'saudi arabia':[23.9,45.1],
'brazil':[-14.2,-51.9],'mexico':[23.6,-102.6],'canada':[56.1,-106.3],
'australia':[-25.3,133.8],'south africa':[-30.6,22.9],'colombia':[4.6,-74.3],
'venezuela':[6.4,-66.6],'argentina':[-38.4,-63.6],'peru':[-9.2,-75],
'philippines':[12.9,122],'indonesia':[-0.8,113.9],'thailand':[15.9,100.9],
'vietnam':[14.1,108.3],'malaysia':[4.2,101.9],'singapore':[1.4,103.8],
'poland':[51.9,19.1],'romania':[45.9,25],'greece':[39.1,21.8],
'sweden':[60.1,18.6],'norway':[60.5,8.5],'finland':[61.9,25.7],
'netherlands':[52.1,5.3],'belgium':[50.5,4.5],'switzerland':[46.8,8.2],
'austria':[47.5,14.6],'hungary':[47.2,19.5],'czech':[49.8,15.5],
'kenya':[-0.02,37.9],'ghana':[7.9,-1.02],'senegal':[14.5,-14.5],
'morocco':[31.8,-7.1],'algeria':[28.0,1.7],'tunisia':[33.9,9.5],
'new zealand':[-40.9,174.9],'chile':[-35.7,-71.5]
};
function _geolocateArticle(article) {
var text = ((article.title || '') + ' ' + (article.summary || '')).toLowerCase();
for (var country in COUNTRY_CENTROIDS) {
if (COUNTRY_CENTROIDS.hasOwnProperty(country) && text.indexOf(country) >= 0) {
return { country: country, lat: COUNTRY_CENTROIDS[country][0], lon: COUNTRY_CENTROIDS[country][1] };
}
}
return null;
}
function updateMapNews(data) {
mapLayers.news.clearLayers();
if (!data || data.error) { $('#cntNews').textContent = '0'; return; }
var articles = (data.articles || data.items || []).filter(function(a) {
return _withinTimeWindow(a.published || a.pub_date || a.timestamp);
});
var placed = 0;
articles.forEach(function(a) {
var geo = _geolocateArticle(a);
if (!geo) return;
placed++;
// Jitter to avoid exact overlap
var jlat = geo.lat + (Math.random() - 0.5) * 2;
var jlon = geo.lon + (Math.random() - 0.5) * 2;
a._geo_country = geo.country;
a._geo_lat = jlat;
a._geo_lon = jlon;
var mk = L.marker([jlat, jlon], {
icon: L.divIcon({ className: 'mk-news', iconSize: [6, 6], iconAnchor: [3, 3] })
});
mk.bindTooltip(esc(trunc(a.title || '?', 40)) + '<br><span style="opacity:0.6">' + esc(a.feed_name || '') + '</span>', { className: 'mk-tip', direction: 'top', offset: [0, -4] });
mk.on('click', function() { showDetail('news', a); });
mk.addTo(mapLayers.news);
});
$('#cntNews').textContent = placed;
}
function updateAlertBanner(data) {
var el = $('#alertBanner');
if (!data || data.error || !data.alerts || !data.alerts.length) {
el.classList.remove('show');
el.textContent = '';
return;
}
var alerts = data.alerts.slice(0, 3);
var html = alerts.map(function(a) {
var p = a.priority || 'medium';
return '<div class="alert-strip ' + esc(p) + '">' +
'<span class="alert-dot"></span>' +
'<span class="alert-msg">' + esc(a.message || '') + '</span>' +
'<span class="alert-domain">' + esc(a.domain || '') + '</span>' +
'</div>';
}).join('');
el.innerHTML = safe(html);
el.classList.add('show');
}
// ════════════ TOP BAR STATS ════════════
function updateHudStats(data) {
var pills = [];
if (data.earthquakes && !data.earthquakes.error) {
var qc = (data.earthquakes.earthquakes || []).length;
pills.push('<div class="stat-pill"><span class="v">' + qc + '</span><span class="l">Quakes</span></div>');
}
if (data.military_flights && !data.military_flights.error) {
pills.push('<div class="stat-pill"><span class="v">' + (data.military_flights.count || 0) + '</span><span class="l">Aircraft</span></div>');
}
var conflictSrc = (data.acled_events && !data.acled_events.error && data.acled_events.count > 0) ? data.acled_events
: (data.ucdp_events && !data.ucdp_events.error && data.ucdp_events.count > 0) ? data.ucdp_events
: data.conflict_zones;
if (conflictSrc && !conflictSrc.error) {
var ac = conflictSrc.count || 0;
pills.push('<div class="stat-pill"><span class="v' + (ac > 100 ? ' warn' : '') + '">' + ac + '</span><span class="l">Conflict</span></div>');
}
if (data.cyber_threats && !data.cyber_threats.error) {
pills.push('<div class="stat-pill"><span class="v">' + ((data.cyber_threats.threats || []).length) + '</span><span class="l">Cyber IOC</span></div>');
}
if (data.wildfires && !data.wildfires.error) {
var fbr = data.wildfires.fires_by_region || {};
var fireClusterCount = 0;
for (var rr in fbr) { if (fbr.hasOwnProperty(rr)) fireClusterCount += (fbr[rr].top_clusters || []).length; }
pills.push('<div class="stat-pill"><span class="v' + (fireClusterCount > 500 ? ' crit' : fireClusterCount > 100 ? ' warn' : '') + '">' + fireClusterCount + '</span><span class="l">Fires</span></div>');
}
if (data.displacement && !data.displacement.error && data.displacement.global_totals) {
pills.push('<div class="stat-pill"><span class="v warn">' + fmtBigPlain(data.displacement.global_totals.grand_total || 0) + '</span><span class="l">Displaced</span></div>');
}
if (data.space_weather && !data.space_weather.error && data.space_weather.current_kp != null) {
var swKp = data.space_weather.current_kp;
pills.push('<div class="stat-pill"><span class="v' + (swKp >= 5 ? ' warn' : '') + '">' + swKp.toFixed(0) + '</span><span class="l">Kp</span></div>');
}
if (data.ai_watch && !data.ai_watch.error) {
pills.push('<div class="stat-pill"><span class="v">' + (data.ai_watch.count || 0) + '</span><span class="l">AI Papers</span></div>');
}
if (data.alert_digest && !data.alert_digest.error && data.alert_digest.alert_count > 0) {
var ac = data.alert_digest.alert_count;
var hasCrit = (data.alert_digest.by_priority || {}).critical > 0;
pills.push('<div class="stat-pill"><span class="v' + (hasCrit ? ' crit' : ' warn') + '">' + ac + '</span><span class="l">Alerts</span></div>');
}
if (data.shipping_index && !data.shipping_index.error) {
var ss = data.shipping_index.stress_score || 0;
pills.push('<div class="stat-pill"><span class="v' + (ss >= 60 ? ' crit' : ss >= 30 ? ' warn' : '') + '">' + ss + '</span><span class="l">Ship Stress</span></div>');
}
if (data.disease_outbreaks && !data.disease_outbreaks.error) {
var hc = data.disease_outbreaks.high_concern_count || 0;
if (hc > 0) pills.push('<div class="stat-pill"><span class="v crit">' + hc + '</span><span class="l">Outbreak</span></div>');
}
if (data.nuclear_monitor && !data.nuclear_monitor.error && data.nuclear_monitor.critical_flags > 0) {
pills.push('<div class="stat-pill"><span class="v crit">' + data.nuclear_monitor.critical_flags + '</span><span class="l">Nuke Flag</span></div>');
}
if (data.strategic_posture && !data.strategic_posture.error) {
var ps = data.strategic_posture.composite_score || 0;
var psCls = ps >= 55 ? ' crit' : ps >= 35 ? ' warn' : '';
pills.push('<div class="stat-pill"><span class="v' + psCls + '">' + ps.toFixed(0) + '</span><span class="l">Posture</span></div>');
}
if (data.population_exposure && !data.population_exposure.error && data.population_exposure.exposed_city_count > 0) {
pills.push('<div class="stat-pill"><span class="v warn">' + (data.population_exposure.total_exposed_population_formatted || '0') + '</span><span class="l">Exposed</span></div>');
}
if (data.domestic_flights && !data.domestic_flights.error && data.domestic_flights.total_aircraft > 0) {
pills.push('<div class="stat-pill"><span class="v">' + fmtBigPlain(data.domestic_flights.total_aircraft) + '</span><span class="l">Airborne</span></div>');
}
if (data.traffic_flow && !data.traffic_flow.error && data.traffic_flow.count > 0) {
var tAvg = data.traffic_flow.global_avg_congestion || 0;
pills.push('<div class="stat-pill"><span class="v' + (tAvg >= 40 ? ' crit' : tAvg >= 20 ? ' warn' : '') + '">' + tAvg.toFixed(0) + '%</span><span class="l">Traffic</span></div>');
}
if (data.webcams && !data.webcams.error && data.webcams.count > 0) {
pills.push('<div class="stat-pill"><span class="v">' + data.webcams.count + '</span><span class="l">Cams</span></div>');
}
$('#hudStats').innerHTML = safe(pills.join(''));
}
// ════════════ DATA DRAWER ════════════
function updateDrawer(data) {
_drawerItems = {};
_drawerIdx = 0;
var h = '';
// ── STRATEGIC POSTURE ──
if (data.strategic_posture && !data.strategic_posture.error) {
var sp = data.strategic_posture;
var compScore = sp.composite_score || 0;
var riskLvl = sp.risk_level || 'UNKNOWN';
var riskColor = riskLvl === 'CRITICAL' ? 'var(--red)' : riskLvl === 'HIGH' ? 'var(--amber)' : riskLvl === 'ELEVATED' ? '#ff9100' : 'var(--green)';
h += '<div class="sh">STRATEGIC POSTURE</div>';
h += '<div style="display:flex;align-items:center;gap:12px;padding:6px 0">';
h += '<div style="font-size:2rem;font-weight:700;color:' + riskColor + '">' + compScore.toFixed(0) + '</div>';
h += '<div><div style="font-size:0.85rem;font-weight:600;color:' + riskColor + '">' + esc(riskLvl) + '</div>';
h += '<div class="dim" style="font-size:0.6rem">' + (sp.domains_assessed || 0) + ' domains assessed</div></div>';
h += '</div>';
var ds = sp.domain_scores || {};
h += '<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:2px">';
for (var dk in ds) {
if (ds.hasOwnProperty(dk)) {
var dInfo = ds[dk];
var dScore = dInfo.score || 0;
var dColor = dScore >= 55 ? 'var(--red)' : dScore >= 35 ? 'var(--amber)' : 'var(--green)';
h += '<div style="text-align:center;padding:4px 2px;border-radius:4px;background:rgba(255,255,255,0.02)">';
h += '<div style="font-size:0.75rem;font-weight:600;color:' + dColor + '">' + dScore.toFixed(0) + '</div>';
h += '<div class="dim" style="font-size:0.5rem;text-transform:uppercase;letter-spacing:0.5px">' + esc(dk) + '</div>';
h += '</div>';
}
}
h += '</div>';
var threats = sp.top_threats || [];
if (threats.length) {
h += '<div class="sub" style="margin-top:6px">Top Threats</div>';
threats.slice(0, 5).forEach(function(t) {
h += '<div style="display:flex;align-items:center;gap:6px;padding:2px 0;font-size:0.62rem">';
h += '<span class="dim" style="min-width:50px;text-transform:uppercase;letter-spacing:0.5px">' + esc(t.domain) + '</span>';
h += '<span style="color:var(--bright);flex:1">' + esc(t.signal) + '</span>';
h += '</div>';
});
}
}
// ── ALERTS ──
if (data.alert_digest && !data.alert_digest.error && data.alert_digest.alert_count > 0) {
h += '<div class="sh">ALERTS</div>';
var byPri = data.alert_digest.by_priority || {};
h += '<div class="mini-row">';
if (byPri.critical) h += '<div class="mini-box"><div class="v" style="color:var(--red)">' + byPri.critical + '</div><div class="l">Critical</div></div>';
if (byPri.high) h += '<div class="mini-box"><div class="v" style="color:var(--amber)">' + byPri.high + '</div><div class="l">High</div></div>';
if (byPri.medium) h += '<div class="mini-box"><div class="v" style="color:var(--blue)">' + byPri.medium + '</div><div class="l">Medium</div></div>';
h += '</div>';
var alertItems = data.alert_digest.alerts || [];
alertItems.forEach(function(a) {
var pc = a.priority === 'critical' ? 'down' : a.priority === 'high' ? 'warn' : 'dim';
h += '<div style="display:flex;align-items:center;gap:8px;padding:4px 0;border-bottom:1px solid rgba(255,255,255,0.02)">';
h += '<span style="width:6px;height:6px;border-radius:50%;flex-shrink:0;background:' + (a.priority === 'critical' ? 'var(--red)' : a.priority === 'high' ? 'var(--amber)' : 'var(--blue)') + '"></span>';
h += '<span style="flex:1;font-size:0.68rem;color:var(--bright)">' + esc(a.message || '') + '</span>';
h += '<span class="dim" style="font-size:0.56rem;text-transform:uppercase;letter-spacing:0.5px">' + esc(a.domain || '') + '</span>';
h += '</div>';
});
}
// ── MARKETS ──
h += '<div class="sh">MARKETS</div>';
if (data.market_quotes && !data.market_quotes.error) {
var quotes = data.market_quotes.quotes || data.market_quotes.data || [];
if (quotes.length) {
h += '<div class="sub">Equity Indices</div><table class="dtable"><thead><tr><th>Sym</th><th>Price</th><th>%</th></tr></thead><tbody>';
quotes.slice(0, 10).forEach(function(q) {
var pct = q.change_pct || q.regularMarketChangePercent || 0;
h += '<tr><td class="bright">' + esc(q.symbol || '?') + '</td><td>' + fmtNum(q.price || q.regularMarketPrice || 0) + '</td><td class="' + cls(pct) + '">' + fmtPct(pct) + '%</td></tr>';
});
h += '</tbody></table>';
}
}
if (data.crypto_quotes && !data.crypto_quotes.error) {
var coins = data.crypto_quotes.coins || data.crypto_quotes.data || [];
if (coins.length) {
h += '<div class="sub">Crypto</div><table class="dtable"><thead><tr><th>Coin</th><th>Price</th><th>24h</th></tr></thead><tbody>';
coins.slice(0, 10).forEach(function(c) {
var chg = c.price_change_percentage_24h || c.change_24h || 0;
h += '<tr><td class="bright">' + esc((c.symbol || c.id || '?').toUpperCase()) + '</td><td>$' + fmtNum(c.current_price || c.price || 0) + '</td><td class="' + cls(chg) + '">' + fmtPct(chg) + '%</td></tr>';
});
h += '</tbody></table>';
}
}
if (data.stablecoin_status && !data.stablecoin_status.error) {
var sc = data.stablecoin_status.stablecoins || [];
if (sc.length) {
h += '<div class="sub">Stablecoin Peg</div><table class="dtable"><thead><tr><th>Coin</th><th>$</th><th>Dev</th><th></th></tr></thead><tbody>';
sc.forEach(function(s) {
var dep = s.is_depegged;
h += '<tr><td class="bright">' + esc((s.id || '?').toUpperCase()) + '</td><td>' + fmtNum(s.price, 4) + '</td><td class="' + ((s.peg_deviation_pct || 0) > 0.5 ? 'warn' : 'dim') + '">' + fmtNum(s.peg_deviation_pct || 0, 3) + '%</td><td class="' + (dep ? 'down' : 'up') + '">' + (dep ? 'DEPEG' : 'OK') + '</td></tr>';
});
h += '</tbody></table>';
}
}
if (data.sector_heatmap && !data.sector_heatmap.error) {
var secs = data.sector_heatmap.sectors || data.sector_heatmap.data || [];
if (secs.length) {
h += '<div class="sub">Sector Performance</div><div class="heatmap">';
secs.forEach(function(s) {
var pct = s.change_pct || 0;
var bg = pct >= 1 ? '#166534' : pct >= 0 ? '#1a2e1a' : pct >= -1 ? '#2e1a1a' : '#7f1d1d';
var fg = pct >= 0 ? '#86efac' : '#fca5a5';
h += '<div class="hm" style="background:' + bg + ';color:' + fg + '">' + esc(s.symbol || s.name || '?') + '<br>' + (pct >= 0 ? '+' : '') + pct.toFixed(1) + '%</div>';
});
h += '</div>';
}
}
if (data.macro_signals && !data.macro_signals.error) {
var signals = data.macro_signals.signals || data.macro_signals;
if (typeof signals === 'object' && !Array.isArray(signals)) {
h += '<div class="sub">Macro Signals</div><div class="mini-row">';
var mc = 0;
for (var mn in signals) {
if (!signals.hasOwnProperty(mn) || mc >= 4) break;
var info = signals[mn];
var val = typeof info === 'object' && !Array.isArray(info) ? Object.values(info)[0] : info;
h += '<div class="mini-box"><div class="v">' + (typeof val === 'number' ? fmtNum(val, val < 1 ? 4 : 2) : esc(String(val || '\u2014'))) + '</div><div class="l">' + esc(mn.replace(/_/g, ' ')) + '</div></div>';
mc++;
}
h += '</div>';
}
}
if (data.energy_prices && !data.energy_prices.error) {
var ep = data.energy_prices;
var eRows = [];
if (ep.oil) {
if (ep.oil.brent && ep.oil.brent.price != null) eRows.push({name: 'Brent Crude', price: ep.oil.brent.price, date: ep.oil.brent.date});
if (ep.oil.wti && ep.oil.wti.price != null) eRows.push({name: 'WTI Crude', price: ep.oil.wti.price, date: ep.oil.wti.date});
}
if (ep.natural_gas && ep.natural_gas.price != null) eRows.push({name: 'Natural Gas', price: ep.natural_gas.price, date: ep.natural_gas.date});
if (eRows.length) {
h += '<div class="sub">Energy</div><table class="dtable"><thead><tr><th>Commodity</th><th>Price</th><th>Date</th></tr></thead><tbody>';
eRows.forEach(function(item) {
h += '<tr><td>' + esc(item.name) + '</td><td class="bright">$' + fmtNum(item.price) + '</td><td class="dim">' + esc(item.date || '\u2014') + '</td></tr>';
});
h += '</tbody></table>';
}
}
if (data.etf_flows && !data.etf_flows.error) {
var etfs = data.etf_flows.etfs || data.etf_flows.data || [];
if (etfs.length) {
h += '<div class="sub">ETF Flows</div><table class="dtable"><thead><tr><th>ETF</th><th>$</th><th>%</th><th>Vol</th></tr></thead><tbody>';
etfs.slice(0, 8).forEach(function(e) {
h += '<tr><td class="bright">' + esc(e.symbol || '?') + '</td><td>' + fmtNum(e.price) + '</td><td class="' + cls(e.change_pct) + '">' + fmtPct(e.change_pct) + '%</td><td class="dim">' + fmtBigPlain(e.volume) + '</td></tr>';
});
h += '</tbody></table>';
}
}
// ── SECURITY ──
h += '<div class="sh">SECURITY</div>';
if (data.cyber_threats && !data.cyber_threats.error) {
var threats = data.cyber_threats.threats || [];
var bySev = data.cyber_threats.by_severity || {};
h += '<div class="mini-row">';
h += '<div class="mini-box"><div class="v" style="color:var(--red)">' + (bySev.critical || 0) + '</div><div class="l">Critical</div></div>';
h += '<div class="mini-box"><div class="v" style="color:var(--amber)">' + (bySev.high || 0) + '</div><div class="l">High</div></div>';
h += '<div class="mini-box"><div class="v">' + (bySev.medium || 0) + '</div><div class="l">Medium</div></div>';
h += '</div>';
h += '<table class="dtable"><thead><tr><th>IOC</th><th>Type</th><th>Sev</th></tr></thead><tbody>';
threats.slice(0, 15).forEach(function(t) {
var sev = (t.severity || '').toLowerCase();
var sc = sev === 'critical' ? 'down' : sev === 'high' ? 'warn' : 'dim';
var did = _storeItem('cyber', t);
h += '<tr data-click="' + did + '"><td>' + esc(trunc(t.indicator || t.url || t.ioc || '?', 30)) + '</td><td class="dim">' + esc(t.threat_type || t.type || '\u2014') + '</td><td class="' + sc + '">' + esc(t.severity || '\u2014') + '</td></tr>';
});
h += '</tbody></table>';
}
if (data.internet_outages && !data.internet_outages.error) {
var outages = data.internet_outages.outages || [];
var oc = data.internet_outages.ongoing_count || outages.length;
h += '<div class="sub">Infrastructure</div>';
h += '<div class="mini-row"><div class="mini-box"><div class="v' + (oc > 0 ? ' warn' : '') + '">' + oc + '</div><div class="l">Outages</div></div>';
if (data.cable_health && !data.cable_health.error) {
var cs = data.cable_health.health_score || data.cable_health.overall_score;
h += '<div class="mini-box"><div class="v">' + (cs != null ? cs + '%' : '\u2014') + '</div><div class="l">Cable Health</div></div>';
}
h += '</div>';
}
if (data.airport_delays && !data.airport_delays.error) {
var delays = data.airport_delays.delays || [];
if (delays.length) {
h += '<div class="sub">Airport Delays</div><table class="dtable"><thead><tr><th>Airport</th><th>Reason</th></tr></thead><tbody>';
delays.slice(0, 8).forEach(function(d) {
h += '<tr><td class="bright">' + esc(d.airport || d.iata || '?') + '</td><td class="dim">' + esc(trunc(d.reason || d.type || '\u2014', 30)) + '</td></tr>';
});
h += '</tbody></table>';
}
}
if (data.service_status && !data.service_status.error) {
var svcInc = data.service_status.incidents || [];
var activeSvc = data.service_status.active_incidents || 0;
if (svcInc.length) {
h += '<div class="sub">Cloud Services' + (activeSvc > 0 ? ' (' + activeSvc + ' active)' : '') + '</div><table class="dtable"><thead><tr><th>Provider</th><th>Incident</th><th>Sev</th></tr></thead><tbody>';
svcInc.slice(0, 10).forEach(function(s) {
var sevCls = s.severity === 'critical' ? 'down' : s.severity === 'high' ? 'warn' : 'dim';
var did = _storeItem('service', s);
h += '<tr data-click="' + did + '"><td class="bright">' + esc(s.provider || '?') + '</td><td>' + esc(trunc(s.title || '\u2014', 30)) + '</td><td class="' + sevCls + '">' + esc(s.severity || '\u2014') + '</td></tr>';
});
h += '</tbody></table>';
}
}
if (data.nav_warnings && !data.nav_warnings.error) {
var navs = data.nav_warnings.warnings || [];
if (navs.length) {
h += '<div class="sub">Nav Warnings (' + navs.length + ')</div><table class="dtable"><thead><tr><th>Area</th><th>Warning</th></tr></thead><tbody>';
navs.slice(0, 8).forEach(function(w) {
var did = _storeItem('navwarning', w);
h += '<tr data-click="' + did + '"><td class="bright">' + esc(w.navArea || w.area || '?') + '</td><td class="dim">' + esc(trunc(w.text || w.description || '\u2014', 40)) + '</td></tr>';
});
h += '</tbody></table>';
}
}
if (data.risk_scores && !data.risk_scores.error) {
var countries = data.risk_scores.countries || [];
if (countries.length) {
h += '<div class="sub">Risk Scores</div><table class="dtable"><thead><tr><th>Country</th><th>Score</th><th>Level</th></tr></thead><tbody>';
countries.slice(0, 10).forEach(function(c) {
var rl = (c.risk_level || '').toLowerCase();
var rc = rl === 'extreme' || rl === 'critical' ? 'down' : rl === 'high' ? 'warn' : rl === 'elevated' ? 'warn' : 'dim';
h += '<tr><td class="bright">' + esc(c.country || '?') + '</td><td>' + fmtNum(c.risk_score, 1) + '</td><td class="' + rc + '">' + esc(c.risk_level || '\u2014') + '</td></tr>';
});
h += '</tbody></table>';
}
}
if (data.space_weather && !data.space_weather.error) {
var sw = data.space_weather;
h += '<div class="sub">Space Weather</div><div class="mini-row">';
var kpVal = sw.current_kp;
var kpCls = kpVal >= 7 ? ' crit' : kpVal >= 5 ? ' warn' : '';
h += '<div class="mini-box"><div class="v' + kpCls + '">' + (kpVal != null ? fmtNum(kpVal, 1) : '\u2014') + '</div><div class="l">Kp Index</div></div>';
h += '<div class="mini-box"><div class="v">' + esc(sw.kp_level || '\u2014') + '</div><div class="l">Geo Level</div></div>';
h += '<div class="mini-box"><div class="v">' + esc(sw.latest_flare_class || '\u2014') + '</div><div class="l">X-Ray Flux</div></div>';
h += '</div>';
var swAlerts = sw.alerts || [];
if (swAlerts.length) {
h += '<table class="dtable"><thead><tr><th>Alert</th><th>Time</th></tr></thead><tbody>';
swAlerts.slice(0, 5).forEach(function(a) {
h += '<tr><td class="warn">' + esc(trunc(a.message || '?', 50)) + '</td><td class="dim">' + esc(ago(a.issue_datetime)) + '</td></tr>';
});
h += '</tbody></table>';
}
}
// ── INTELLIGENCE ──
h += '<div class="sh">INTELLIGENCE</div>';
if (data.trending_keywords && !data.trending_keywords.error) {
var kws = data.trending_keywords.keywords || data.trending_keywords.data || [];
if (kws.length) {
h += '<div class="sub">Trending</div><div class="tags">';
kws.slice(0, 20).forEach(function(k) {
var word = typeof k === 'string' ? k : (k.keyword || k.word || k.term || '?');
var cnt = typeof k === 'object' ? (k.count || k.frequency || 0) : 0;
h += '<span class="tag' + (cnt > 5 ? ' tag-hot' : '') + '">' + esc(word) + (cnt ? ' (' + cnt + ')' : '') + '</span>';
});
h += '</div>';
}
}
if (data.prediction_markets && !data.prediction_markets.error) {
var mkts = data.prediction_markets.markets || [];
if (mkts.length) {
h += '<div class="sub">Prediction Markets</div><table class="dtable"><thead><tr><th>Market</th><th>Prob</th><th>Vol</th></tr></thead><tbody>';
mkts.slice(0, 10).forEach(function(m) {
var prob = m.probability || m.yes_price;
var did = _storeItem('prediction', m);
h += '<tr data-click="' + did + '"><td>' + esc(trunc(m.question || m.title || '?', 35)) + '</td><td class="bright">' + (prob != null ? (typeof prob === 'number' ? (prob * 100).toFixed(0) + '%' : esc(String(prob))) : '\u2014') + '</td><td class="dim">' + fmtBig(m.volume || 0) + '</td></tr>';
});
h += '</tbody></table>';
}
}
if (data.ucdp_events && !data.ucdp_events.error) {
var ucdp = data.ucdp_events.events || [];
if (ucdp.length) {
h += '<div class="sub">UCDP Events (' + (data.ucdp_events.count || ucdp.length) + ')</div><table class="dtable"><thead><tr><th>Conflict</th><th>Deaths</th></tr></thead><tbody>';
ucdp.slice(0, 10).forEach(function(e) {
var best = e.best || e.best_fatality_estimate || 0;
var did = _storeItem('conflict', e);
h += '<tr data-click="' + did + '"><td>' + esc(trunc(e.dyad_name || e.side_a || '?', 30)) + '</td><td class="' + (best > 10 ? 'down' : best > 0 ? 'warn' : 'dim') + '">' + best + '</td></tr>';
});
h += '</tbody></table>';
}
}
if (data.conflict_zones && !data.conflict_zones.error) {
var czones = data.conflict_zones.events || [];
if (czones.length) {
h += '<div class="sub">Active Conflict Zones (' + (data.conflict_zones.count || czones.length) + ')</div><table class="dtable"><thead><tr><th>Zone</th><th>Severity</th></tr></thead><tbody>';
czones.forEach(function(e) {
var sev = e.severity || 'unknown';
var cls = sev === 'critical' ? 'down' : sev === 'high' ? 'warn' : 'dim';
var did = _storeItem('conflict', e);
h += '<tr data-click="' + did + '"><td>' + esc(trunc(e.country || '?', 25)) + '</td><td class="' + cls + '">' + esc(sev) + '</td></tr>';
});
h += '</tbody></table>';
}
}
if (data.displacement && !data.displacement.error) {
var totals = data.displacement.global_totals || {};
h += '<div class="sub">Global Displacement</div><div class="mini-row">';
h += '<div class="mini-box"><div class="v">' + fmtBigPlain(totals.total_refugees || 0) + '</div><div class="l">Refugees</div></div>';
h += '<div class="mini-box"><div class="v">' + fmtBigPlain(totals.total_idps || 0) + '</div><div class="l">IDPs</div></div>';
h += '<div class="mini-box"><div class="v warn">' + fmtBigPlain(totals.grand_total || 0) + '</div><div class="l">Total</div></div>';
h += '</div>';
var origins = data.displacement.by_origin || [];
if (origins.length) {
h += '<table class="dtable"><thead><tr><th>Origin</th><th>Refugees</th><th>IDPs</th></tr></thead><tbody>';
origins.slice(0, 8).forEach(function(o) {
h += '<tr><td class="bright">' + esc(o.country_name || o.country || '?') + '</td><td>' + fmtBigPlain(o.refugees || 0) + '</td><td>' + fmtBigPlain(o.internally_displaced || o.idps || 0) + '</td></tr>';
});
h += '</tbody></table>';
}
}
if (data.signal_convergence && !data.signal_convergence.error) {
var hs = data.signal_convergence.hotspots || [];
if (hs.length) {
h += '<div class="sub">Signal Convergence</div><table class="dtable"><thead><tr><th>Region</th><th>Score</th><th>Signals</th></tr></thead><tbody>';
hs.slice(0, 10).forEach(function(hp) {
var score = hp.convergence_score || 0;
var sigs = hp.signals || {};
var sigStr = Object.entries(sigs).map(function(e) { return e[0] + ':' + e[1]; }).join(', ');
h += '<tr><td class="bright">' + esc(hp.name || '?') + '</td><td class="' + (score >= 7 ? 'down' : score >= 4 ? 'warn' : 'dim') + '">' + fmtNum(score, 1) + '</td><td class="dim">' + esc(trunc(sigStr, 25)) + '</td></tr>';
});
h += '</tbody></table>';
}
}
if (data.climate_anomalies && !data.climate_anomalies.error) {
var src = data.climate_anomalies.anomalies || data.climate_anomalies.zones || data.climate_anomalies.data || [];
var anomalies = Array.isArray(src) ? src : (typeof src === 'object' ? Object.values(src) : []);
if (anomalies.length) {
h += '<div class="sub">Climate Anomalies</div><table class="dtable"><thead><tr><th>Zone</th><th>Temp</th><th>Precip</th></tr></thead><tbody>';
anomalies.slice(0, 8).forEach(function(a) {
var temp = a.temp_anomaly_c != null ? a.temp_anomaly_c : (a.temperature_anomaly || a.temp_anomaly || a.temp_deviation);
var precip = a.precip_anomaly_pct != null ? a.precip_anomaly_pct : (a.precipitation_anomaly || a.precip_anomaly || a.precip_deviation);
h += '<tr><td>' + esc(a.zone || a.name || a.region || '?') + '</td><td class="' + (temp > 0 ? 'warn' : 'up') + '">' + (temp != null ? (temp > 0 ? '+' : '') + fmtNum(temp, 1) + '\u00B0C' : '\u2014') + '</td><td class="' + (precip > 50 ? 'warn' : precip < -50 ? 'down' : 'dim') + '">' + (precip != null ? (precip > 0 ? '+' : '') + fmtNum(precip, 0) + '%' : '\u2014') + '</td></tr>';
});
h += '</tbody></table>';
}
}
// ── AGI WATCH ──
h += '<div class="sh">AGI WATCH</div>';
if (data.ai_watch && !data.ai_watch.error) {
var aiw = data.ai_watch;
var labTrend = aiw.lab_trending || [];
if (labTrend.length) {
h += '<div class="sub">Lab Activity</div><div class="tags">';
labTrend.slice(0, 12).forEach(function(l) {
h += '<span class="tag' + (l.mentions > 3 ? ' tag-hot' : '') + '">' + esc(l.lab) + ' (' + l.mentions + ')</span>';
});
h += '</div>';
}
var byCat = aiw.by_category || {};
if (Object.keys(byCat).length) {
h += '<div class="mini-row">';
for (var catKey in byCat) {
if (byCat.hasOwnProperty(catKey)) {
h += '<div class="mini-box"><div class="v">' + byCat[catKey] + '</div><div class="l">' + esc(catKey) + '</div></div>';
}
}
h += '</div>';
}
var aiItems = aiw.items || [];
if (aiItems.length) {
h += '<table class="dtable"><thead><tr><th>Paper/Post</th><th>Source</th><th>Age</th></tr></thead><tbody>';
aiItems.slice(0, 12).forEach(function(item) {
var did = _storeItem('ai', item);
h += '<tr data-click="' + did + '"><td class="accent">' + esc(trunc(item.title || '?', 40)) + '</td><td class="dim">' + esc(item.feed_name || '\u2014') + '</td><td class="dim">' + ago(item.published) + '</td></tr>';
});
h += '</tbody></table>';
}
} else {
h += '<div class="dim" style="font-size:0.7rem;padding:4px 0">Loading AI feeds...</div>';
}
// ── HEALTH ──
if (data.disease_outbreaks && !data.disease_outbreaks.error) {
var dob = data.disease_outbreaks;
h += '<div class="sh">HEALTH</div>';
h += '<div class="mini-row">';
h += '<div class="mini-box"><div class="v">' + (dob.count || 0) + '</div><div class="l">Alerts</div></div>';
h += '<div class="mini-box"><div class="v' + ((dob.high_concern_count || 0) > 0 ? ' crit' : '') + '">' + (dob.high_concern_count || 0) + '</div><div class="l">High Concern</div></div>';
h += '</div>';
var byOrg = dob.by_organization || {};
if (Object.keys(byOrg).length) {
h += '<div class="mini-row">';
for (var orgK in byOrg) {
if (byOrg.hasOwnProperty(orgK)) {
h += '<div class="mini-box"><div class="v">' + byOrg[orgK] + '</div><div class="l">' + esc(orgK) + '</div></div>';
}
}
h += '</div>';
}
var healthItems = dob.items || [];
if (healthItems.length) {
h += '<table class="dtable"><thead><tr><th>Outbreak</th><th>Org</th><th>Age</th></tr></thead><tbody>';
healthItems.slice(0, 12).forEach(function(item) {
var isHigh = item.high_concern;
var did = _storeItem('health', item);
h += '<tr data-click="' + did + '"><td' + (isHigh ? ' class="down"' : '') + '>' + esc(trunc(item.title || '?', 40)) + '</td><td class="dim">' + esc(item.organization || '\u2014') + '</td><td class="dim">' + ago(item.published) + '</td></tr>';
});
h += '</tbody></table>';
}
}
// ── ELECTIONS ──
if (data.election_calendar && !data.election_calendar.error) {
var ecal = data.election_calendar;
var elections = ecal.elections || [];
if (elections.length) {
h += '<div class="sh">ELECTIONS</div>';
var hiRisk = ecal.highest_risk;
if (hiRisk) {
h += '<div class="mini-row"><div class="mini-box"><div class="v warn">' + fmtNum(hiRisk.risk_score || 0, 0) + '</div><div class="l">Top Risk</div></div>';
h += '<div class="mini-box"><div class="v">' + esc(hiRisk.country || '?') + '</div><div class="l">' + esc(hiRisk.election_type || '') + '</div></div></div>';
}
h += '<table class="dtable"><thead><tr><th>Country</th><th>Type</th><th>Date</th><th>Risk</th></tr></thead><tbody>';
elections.slice(0, 12).forEach(function(e) {
var rs = e.risk_score || 0;
var rCls = rs >= 80 ? 'crit' : rs >= 50 ? 'high' : rs >= 30 ? 'high' : '';
var riskPct = Math.min(100, rs);
var did = _storeItem('election', e);
h += '<tr data-click="' + did + '"><td class="bright">' + esc(e.country || '?') + '</td><td class="dim">' + esc(e.election_type || '\u2014') + '</td><td class="dim">' + esc(e.date || '\u2014') + '</td><td>' +
'<div class="risk-bar"><div class="risk-fill ' + (rs >= 80 ? 'crit' : rs >= 50 ? 'high' : rs >= 20 ? 'med' : 'low') + '" style="width:' + riskPct + '%"></div></div>' +
'</td></tr>';
});
h += '</tbody></table>';
}
}
// ── SHIPPING ──
if (data.shipping_index && !data.shipping_index.error) {
var ship = data.shipping_index;
h += '<div class="sh">SHIPPING</div>';
var stress = ship.stress_score || 0;
var stressColor = stress >= 60 ? 'var(--red)' : stress >= 30 ? 'var(--amber)' : 'var(--green)';
h += '<div class="gauge-row">';
h += '<span style="font-size:0.68rem;color:var(--dim)">Stress</span>';
h += '<div class="gauge-track"><div class="gauge-fill" style="width:' + stress + '%;background:' + stressColor + '"></div></div>';
h += '<span class="gauge-label" style="color:' + stressColor + '">' + stress + '</span>';
h += '</div>';
h += '<div class="mini-row"><div class="mini-box"><div class="v">' + esc(ship.assessment || '\u2014') + '</div><div class="l">Assessment</div></div></div>';
var sigs = ship.signals || [];
if (sigs.length) {
h += '<div class="tags">';
sigs.forEach(function(s) { h += '<span class="tag tag-hot">' + esc(s) + '</span>'; });
h += '</div>';
}
var shipQuotes = ship.quotes || [];
if (shipQuotes.length) {
h += '<table class="dtable"><thead><tr><th>Sym</th><th>Price</th><th>%</th></tr></thead><tbody>';
shipQuotes.forEach(function(q) {
h += '<tr><td class="bright">' + esc(q.symbol || '?') + '</td><td>' + fmtNum(q.price) + '</td><td class="' + cls(q.change_pct) + '">' + fmtPct(q.change_pct) + '%</td></tr>';
});
h += '</tbody></table>';
}
}
// ── FLEET ──
if (data.fleet_report && !data.fleet_report.error) {
var fr = data.fleet_report;
h += '<div class="sh">FLEET</div>';
var fScore = fr.readiness_score || 0;
var fColor = fScore >= 70 ? 'var(--red)' : fScore >= 40 ? 'var(--amber)' : 'var(--green)';
h += '<div class="mini-row">';
h += '<div class="mini-box"><div class="v" style="color:' + fColor + '">' + fScore + '</div><div class="l">Readiness</div></div>';
h += '<div class="mini-box"><div class="v">' + esc(fr.readiness_level || '\u2014') + '</div><div class="l">Level</div></div>';
h += '<div class="mini-box"><div class="v">' + (fr.total_tracked_aircraft || 0) + '</div><div class="l">Aircraft</div></div>';
h += '<div class="mini-box"><div class="v">' + (fr.surge_count || 0) + '</div><div class="l">Surges</div></div>';
h += '</div>';
var wws = fr.waterway_summary || [];
if (wws.length) {
h += '<div class="sub">Waterways</div><table class="dtable"><thead><tr><th>Waterway</th><th>Status</th><th>Warnings</th></tr></thead><tbody>';
wws.forEach(function(w) {
var sCls = w.status === 'critical' ? 'down' : w.status === 'elevated' ? 'warn' : w.status === 'advisory' ? 'warn' : 'dim';
h += '<tr><td class="bright">' + esc(trunc(w.name || '?', 22)) + '</td><td class="' + sCls + '">' + esc(w.status || '\u2014') + '</td><td class="dim">' + (w.warning_count || 0) + '</td></tr>';
});
h += '</tbody></table>';
}
}
// ── SOCIAL ──
if (data.social_signals && !data.social_signals.error) {
var soc = data.social_signals;
h += '<div class="sh">SOCIAL</div>';
var vel = soc.velocity_metrics || {};
if (Object.keys(vel).length) {
h += '<div class="mini-row">';
for (var vk in vel) {
if (vel.hasOwnProperty(vk)) {
h += '<div class="mini-box"><div class="v">' + (typeof vel[vk] === 'number' ? fmtNum(vel[vk], 0) : esc(String(vel[vk]))) + '</div><div class="l">' + esc(vk.replace(/_/g, ' ')) + '</div></div>';
}
}
h += '</div>';
}
var posts = soc.posts || [];
if (posts.length) {
h += '<table class="dtable"><thead><tr><th>Post</th><th>Score</th><th>Sub</th></tr></thead><tbody>';
posts.slice(0, 10).forEach(function(p) {
var did = _storeItem('social', p);
h += '<tr data-click="' + did + '"><td class="accent">' + esc(trunc(p.title || '?', 35)) + '</td><td class="bright">' + fmtBigPlain(p.score || 0) + '</td><td class="dim">' + esc(p.subreddit || '\u2014') + '</td></tr>';
});
h += '</tbody></table>';
}
}
// ── NUCLEAR ──
if (data.nuclear_monitor && !data.nuclear_monitor.error) {
var nuc = data.nuclear_monitor;
h += '<div class="sh">NUCLEAR MONITOR</div>';
h += '<div class="mini-row">';
h += '<div class="mini-box"><div class="v">' + (nuc.total_flagged_events || 0) + '</div><div class="l">Flagged</div></div>';
h += '<div class="mini-box"><div class="v' + ((nuc.critical_flags || 0) > 0 ? ' crit' : '') + '">' + (nuc.critical_flags || 0) + '</div><div class="l">Critical</div></div>';
h += '<div class="mini-box"><div class="v">' + ((nuc.sites || []).length) + '</div><div class="l">Sites</div></div>';
h += '</div>';
var nucSites = nuc.sites || [];
if (nucSites.length) {
h += '<table class="dtable"><thead><tr><th>Site</th><th>Status</th><th>Events</th><th>Concern</th></tr></thead><tbody>';
nucSites.forEach(function(s) {
var concern = s.highest_concern ? s.highest_concern.concern_level : 'none';
var cc = concern === 'critical' ? 'down' : concern === 'high' ? 'warn' : concern === 'elevated' ? 'warn' : 'dim';
h += '<tr><td class="bright">' + esc(s.name || '?') + '</td><td class="dim">' + esc(s.status || '\u2014') + '</td><td>' + (s.events_detected || 0) + '</td><td class="' + cc + '">' + esc(concern) + '</td></tr>';
});
h += '</tbody></table>';
}
}
// ── POPULATION EXPOSURE ──
if (data.population_exposure && !data.population_exposure.error && data.population_exposure.exposed_city_count > 0) {
var pe = data.population_exposure;
h += '<div class="sh">POPULATION EXPOSURE</div>';
h += '<div class="mini-row">';
h += '<div class="mini-box"><div class="v warn">' + esc(pe.total_exposed_population_formatted || '0') + '</div><div class="l">Total Exposed</div></div>';
h += '<div class="mini-box"><div class="v">' + (pe.exposed_city_count || 0) + '</div><div class="l">Cities</div></div>';
h += '<div class="mini-box"><div class="v">' + (pe.events_analyzed || 0) + '</div><div class="l">Events</div></div>';
h += '<div class="mini-box"><div class="v">' + (pe.radius_km || 200) + 'km</div><div class="l">Radius</div></div>';
h += '</div>';
var byType = pe.by_event_type || {};
if (Object.keys(byType).length) {
h += '<div class="mini-row">';
for (var etk in byType) {
if (byType.hasOwnProperty(etk)) {
var etColor = etk === 'conflict' ? 'var(--red)' : etk === 'earthquake' ? 'var(--amber)' : '#ffea00';
h += '<div class="mini-box"><div class="v" style="color:' + etColor + '">' + esc(byType[etk]) + '</div><div class="l">' + esc(etk) + '</div></div>';
}
}
h += '</div>';
}
var exCities = pe.exposed_cities || [];
if (exCities.length) {
h += '<table class="dtable"><thead><tr><th>City</th><th>Pop</th><th>Threat</th><th>Dist</th></tr></thead><tbody>';
exCities.slice(0, 15).forEach(function(c) {
var popStr = (c.population || 0) >= 1000000 ? ((c.population / 1000000).toFixed(1) + 'M') : ((c.population / 1000).toFixed(0) + 'K');
var tColor = c.nearest_event === 'conflict' ? 'down' : c.nearest_event === 'earthquake' ? 'warn' : 'warn';
h += '<tr><td class="bright">' + esc(c.city || '?') + '</td><td class="dim">' + popStr + '</td><td class="' + tColor + '">' + esc(c.nearest_event || '\u2014') + '</td><td class="dim">' + (c.distance_km || 0) + 'km</td></tr>';
});
h += '</tbody></table>';
}
}
// ── GEOSPATIAL ──
if (data.military_bases || data.strategic_ports || data.pipelines || data.nuclear_facilities) {
h += '<div class="sh">GEOSPATIAL</div>';
h += '<div class="mini-row">';
h += '<div class="mini-box"><div class="v">' + ((data.military_bases || {}).count || 0) + '</div><div class="l">Bases</div></div>';
h += '<div class="mini-box"><div class="v">' + ((data.strategic_ports || {}).count || 0) + '</div><div class="l">Ports</div></div>';
h += '<div class="mini-box"><div class="v">' + ((data.pipelines || {}).count || 0) + '</div><div class="l">Pipelines</div></div>';
h += '<div class="mini-box"><div class="v">' + ((data.nuclear_facilities || {}).count || 0) + '</div><div class="l">Nuke Sites</div></div>';
h += '</div>';
// Pipeline summary
var pipes = (data.pipelines && data.pipelines.pipelines) || [];
if (pipes.length) {
h += '<div class="sub">Key Pipelines</div><table class="dtable"><thead><tr><th>Name</th><th>Type</th><th>Status</th></tr></thead><tbody>';
pipes.slice(0, 12).forEach(function(p) {
var sCls = p.status === 'destroyed' ? 'down' : p.status === 'active' ? 'up' : p.status === 'terminated' ? 'down' : 'dim';
var did = _storeItem('pipeline', p);
h += '<tr data-click="' + did + '"><td>' + esc(trunc(p.name || '?', 25)) + '</td><td class="dim">' + esc(p.type || '') + '</td><td class="' + sCls + '">' + esc(p.status || '') + '</td></tr>';
});
h += '</tbody></table>';
}
}
// ── TRENDS ──
if (data.weekly_trends && !data.weekly_trends.error) {
var wt = data.weekly_trends;
h += '<div class="sh">TRENDS</div>';
h += '<div class="mini-row">';
h += '<div class="mini-box"><div class="v">' + (wt.trend_count || 0) + '</div><div class="l">Metrics</div></div>';
h += '<div class="mini-box"><div class="v' + ((wt.current_anomaly_count || 0) > 0 ? ' warn' : '') + '">' + (wt.current_anomaly_count || 0) + '</div><div class="l">Anomalies</div></div>';
h += '</div>';
var anomalies = wt.current_anomalies || [];
if (anomalies.length) {
h += '<div class="sub">Active Anomalies</div><table class="dtable"><thead><tr><th>Metric</th><th>Region</th><th>Dev</th></tr></thead><tbody>';
anomalies.slice(0, 10).forEach(function(a) {
var dev = a.z_score || a.deviation || 0;
h += '<tr><td class="bright">' + esc(a.event_type || a.metric || '?') + '</td><td class="dim">' + esc(a.region || '\u2014') + '</td><td class="' + (Math.abs(dev) > 3 ? 'down' : Math.abs(dev) > 2 ? 'warn' : 'dim') + '">' + (dev > 0 ? '+' : '') + fmtNum(dev, 1) + '\u03C3</td></tr>';
});
h += '</tbody></table>';
}
var trends = wt.trends || [];
if (trends.length) {
h += '<div class="sub">Most Volatile</div><table class="dtable"><thead><tr><th>Metric</th><th>CV%</th><th>Mean</th></tr></thead><tbody>';
trends.slice(0, 8).forEach(function(t) {
h += '<tr><td class="bright">' + esc(t.metric || '?') + '</td><td class="' + (t.volatility_cv > 50 ? 'warn' : 'dim') + '">' + fmtNum(t.volatility_cv, 1) + '</td><td class="dim">' + fmtNum(t.mean, 1) + '</td></tr>';
});
h += '</tbody></table>';
}
}
// ── DOMESTIC AIR TRAFFIC ──
if (data.domestic_flights && !data.domestic_flights.error && data.domestic_flights.total_aircraft > 0) {
var df = data.domestic_flights;
h += '<div class="sh">AIR TRAFFIC</div>';
h += '<div class="mini-row"><div class="mini-box"><div class="v">' + fmtBigPlain(df.total_aircraft) + '</div><div class="l">Airborne</div></div></div>';
var regions = df.by_region || {};
h += '<table class="dtable"><thead><tr><th>Region</th><th>Total</th><th>Commercial</th><th>General</th></tr></thead><tbody>';
var rKeys = Object.keys(regions).sort(function(a, b) { return (regions[b].count || 0) - (regions[a].count || 0); });
rKeys.forEach(function(rk) {
var rv = regions[rk];
h += '<tr><td class="bright">' + esc(rk.replace(/_/g, ' ')) + '</td><td>' + (rv.count || 0) + '</td><td class="dim">' + (rv.commercial || 0) + '</td><td class="dim">' + (rv.general || 0) + '</td></tr>';
});
h += '</tbody></table>';
var busiest = df.busiest_origins || [];
if (busiest.length) {
h += '<div class="sub">Busiest Origins</div><table class="dtable"><thead><tr><th>Country</th><th>Aircraft</th></tr></thead><tbody>';
busiest.slice(0, 10).forEach(function(b) {
h += '<tr><td class="bright">' + esc(b.country) + '</td><td>' + b.count + '</td></tr>';
});
h += '</tbody></table>';
}
}
// ── TRAFFIC ──
if (data.traffic_flow && !data.traffic_flow.error && data.traffic_flow.count > 0) {
var tf = data.traffic_flow;
h += '<div class="sh">ROAD TRAFFIC</div>';
h += '<div class="mini-row">';
var avgCls = tf.global_avg_congestion >= 40 ? ' crit' : tf.global_avg_congestion >= 20 ? ' warn' : '';
h += '<div class="mini-box"><div class="v' + avgCls + '">' + fmtNum(tf.global_avg_congestion, 0) + '%</div><div class="l">Avg Congestion</div></div>';
h += '<div class="mini-box"><div class="v">' + tf.count + '</div><div class="l">Cities</div></div>';
h += '</div>';
h += '<table class="dtable"><thead><tr><th>City</th><th>Cong%</th><th>Speed</th></tr></thead><tbody>';
(tf.cities || []).forEach(function(c) {
var cls = c.congestion_pct >= 50 ? 'crit' : c.congestion_pct >= 25 ? 'warn' : 'dim';
h += '<tr><td class="bright">' + esc(c.name) + ' <span class="dim">' + c.country + '</span></td><td class="' + cls + '">' + c.congestion_pct + '%</td><td class="dim">' + c.current_speed_kmh + '</td></tr>';
});
h += '</tbody></table>';
}
// ── WEBCAMS ──
if (data.webcams && !data.webcams.error && data.webcams.count > 0) {
var wc = data.webcams;
h += '<div class="sh">CCTV / WEBCAMS</div>';
h += '<div class="dim" style="font-size:0.65rem;padding:2px 0">' + wc.count + ' cameras (' + esc(wc.category || 'traffic') + ')</div>';
(wc.cameras || []).slice(0, 12).forEach(function(cam) {
var did = _storeItem('webcam', cam);
h += '<div class="drawer-item" data-click="' + did + '" style="padding:3px 0;border-bottom:1px solid rgba(255,255,255,0.04)">';
h += '<div class="bright" style="font-size:0.7rem">' + esc(cam.title || 'Camera') + '</div>';
h += '<div class="dim" style="font-size:0.6rem">' + esc(cam.city || '') + (cam.country ? ', ' + esc(cam.country) : '') + '</div>';
h += '</div>';
});
}
// ── USNI FLEET TRACKER ──
if (data.usni_fleet && !data.usni_fleet.error && data.usni_fleet.ship_count > 0) {
var uf = data.usni_fleet;
h += '<div class="sh">USNI FLEET TRACKER</div>';
h += '<div class="dim" style="font-size:0.6rem;padding:2px 0">' + esc(uf.report_title || '') + '</div>';
if (uf.force_totals && uf.force_totals.battle_force) {
var bf = uf.force_totals.battle_force;
var dep = uf.force_totals.deployed || {};
var uw = uf.force_totals.underway || {};
h += '<div class="mini-boxes" style="margin:6px 0">';
h += '<div class="mini-box"><div class="v">' + bf.total + '</div><div class="l">Battle Force</div></div>';
h += '<div class="mini-box"><div class="v">' + (dep.total || '?') + '</div><div class="l">Deployed</div></div>';
h += '<div class="mini-box"><div class="v">' + (uw.total || '?') + '</div><div class="l">Underway</div></div>';
h += '</div>';
}
if (uf.region_breakdown) {
var regions = Object.entries(uf.region_breakdown).sort(function(a,b){return b[1]-a[1];});
regions.forEach(function(r) {
h += '<div style="font-size:0.65rem;padding:1px 0"><span class="bright">' + esc(r[0]) + '</span> <span class="dim">' + r[1] + ' ships</span></div>';
});
}
(uf.ships || []).slice(0, 15).forEach(function(s) {
var cls = s.type === 'Aircraft Carrier' ? 'crit' : 'bright';
h += '<div style="font-size:0.65rem;padding:1px 0"><span class="' + cls + '">' + esc(s.name) + '</span> <span class="dim">(' + esc(s.hull_number) + ') ' + esc(s.region) + '</span></div>';
});
}
// ── BTC TECHNICALS ──
if (data.btc_technicals && !data.btc_technicals.error && data.btc_technicals.price) {
var bt = data.btc_technicals;
h += '<div class="sh">BTC TECHNICALS</div>';
var crossCls = bt.cross_signal === 'golden_cross' ? 'good' : bt.cross_signal === 'death_cross' ? 'crit' : 'dim';
var crossLabel = bt.cross_signal === 'golden_cross' ? 'GOLDEN CROSS' : bt.cross_signal === 'death_cross' ? 'DEATH CROSS' : 'NEUTRAL';
h += '<div class="mini-boxes" style="margin:6px 0">';
h += '<div class="mini-box"><div class="v">$' + num(bt.price) + '</div><div class="l">BTC Price</div></div>';
h += '<div class="mini-box"><div class="v">' + (bt.mayer_multiple || '—') + '</div><div class="l">Mayer Multiple</div></div>';
h += '<div class="mini-box"><div class="v ' + crossCls + '">' + crossLabel + '</div><div class="l">Signal</div></div>';
h += '</div>';
h += '<div style="font-size:0.65rem;padding:2px 0"><span class="bright">SMA-50</span> <span class="dim">$' + num(bt.sma_50) + '</span></div>';
if (bt.sma_200) h += '<div style="font-size:0.65rem;padding:1px 0"><span class="bright">SMA-200</span> <span class="dim">$' + num(bt.sma_200) + '</span></div>';
if (bt.change_7d_pct != null) {
var c7 = bt.change_7d_pct >= 0 ? 'good' : 'crit';
h += '<div style="font-size:0.65rem;padding:1px 0"><span class="bright">7d</span> <span class="' + c7 + '">' + (bt.change_7d_pct > 0 ? '+' : '') + bt.change_7d_pct + '%</span>';
if (bt.change_30d_pct != null) {
var c30 = bt.change_30d_pct >= 0 ? 'good' : 'crit';
h += ' <span class="bright">30d</span> <span class="' + c30 + '">' + (bt.change_30d_pct > 0 ? '+' : '') + bt.change_30d_pct + '%</span>';
}
h += '</div>';
}
if (bt.ath_distance_pct != null) h += '<div style="font-size:0.65rem;padding:1px 0"><span class="bright">From ATH</span> <span class="dim">' + bt.ath_distance_pct + '%</span></div>';
}
// ── CENTRAL BANK RATES ──
if (data.central_bank_rates && !data.central_bank_rates.error && (data.central_bank_rates.rates || []).length > 0) {
var cbr = data.central_bank_rates;
h += '<div class="sh">CENTRAL BANK RATES</div>';
h += '<div class="dim" style="font-size:0.6rem;padding:2px 0">' + cbr.count + ' banks' + (cbr.fred_available ? ' (FRED live)' : ' (curated)') + '</div>';
cbr.rates.forEach(function(r) {
var rateCls = r.rate >= 10 ? 'crit' : r.rate >= 5 ? 'warn' : r.rate >= 2 ? 'bright' : 'good';
h += '<div style="font-size:0.65rem;padding:1px 0">';
h += '<span class="' + rateCls + '" style="min-width:40px;display:inline-block;text-align:right">' + r.rate.toFixed(2) + '%</span> ';
h += '<span class="bright">' + esc(r.label) + '</span>';
if (r.country) h += ' <span class="dim">(' + esc(r.country) + ')</span>';
h += '</div>';
});
}
// ── DATA FRESHNESS ──
if (data.cache_freshness && Object.keys(data.cache_freshness).length > 0) {
var cf = data.cache_freshness;
var sources = Object.entries(cf).sort(function(a,b) { return a[1].last_updated_s_ago - b[1].last_updated_s_ago; });
var staleCount = sources.filter(function(s) { return s[1].is_stale; }).length;
h += '<div class="sh">DATA FRESHNESS</div>';
h += '<div class="dim" style="font-size:0.6rem;padding:2px 0">' + sources.length + ' sources tracked, ' + staleCount + ' stale</div>';
sources.forEach(function(s) {
var name = s[0], info = s[1];
var ageMin = Math.round(info.last_updated_s_ago / 60);
var cls = info.is_stale ? 'crit' : ageMin > 10 ? 'warn' : 'dim';
var label = ageMin < 1 ? '<1m' : ageMin + 'm';
h += '<div style="font-size:0.65rem;padding:1px 0"><span class="bright">' + esc(name) + '</span> <span class="' + cls + '">' + label + (info.is_stale ? ' STALE' : '') + '</span></div>';
});
}
// ── AI SITUATION BRIEF ──
if (data.situation_brief && !data.situation_brief.error && data.situation_brief.brief) {
var sb = data.situation_brief;
h += '<div class="sh">AI SITUATION BRIEF</div>';
var aiTag = sb.ai_generated ? '<span style="color:var(--purple);font-size:0.55rem;font-weight:600;letter-spacing:0.5px"> ' + esc(sb.model) + '</span>' : '<span class="dim" style="font-size:0.55rem"> metrics fallback</span>';
h += '<div style="font-size:0.6rem;padding:2px 0;opacity:0.5">Generated ' + ago(new Date(sb.timestamp).getTime()) + ' ago ' + aiTag + '</div>';
h += '<div style="font-size:0.7rem;line-height:1.5;padding:4px 0;white-space:pre-wrap;color:rgba(255,255,255,0.85)">' + esc(sb.brief) + '</div>';
}
$('#drawerBody').innerHTML = safe(h);
// Restore collapsed drawer sections from localStorage
try {
var _collapsed = JSON.parse(localStorage.getItem('phoenix-drawer-collapsed')) || [];
if (_collapsed.length) {
$$('#drawerBody .sh').forEach(function(sh) {
if (_collapsed.indexOf(sh.textContent.trim()) >= 0) {
sh.classList.add('collapsed');
var next = sh.nextElementSibling;
while (next && !next.classList.contains('sh')) { next.style.display = 'none'; next = next.nextElementSibling; }
}
});
}
} catch(e) {}
}
// ════════════ NEWS TICKER ════════════
function updateTicker(data) {
if (!data.news_feed || data.news_feed.error) return;
var articles = data.news_feed.articles || data.news_feed.items || [];
if (!articles.length) return;
var items = articles.slice(0, 30).map(function(a) {
return '<span class="ticker-item">' + esc(trunc(a.title || '?', 70)) + '<span class="src">' + esc(a.feed_name || a.source || a.feed || '') + '</span></span>';
});
// Duplicate for seamless loop
var all = items.join('<span class="ticker-item sep">\u2022</span>');
var doubled = all + '<span class="ticker-item sep">\u2022</span>' + all;
// Adjust speed based on content length
var dur = Math.max(40, articles.length * 4);
$('#tickerContent').style.setProperty('--ticker-dur', dur + 's');
$('#tickerContent').innerHTML = safe(doubled);
}
// ════════════ MASTER UPDATE ════════════
function refreshDashboard(data) { updateAll(data); }
function updateAll(data) {
latestData = data;
window._lastSSEData = data;
// Map layers (each wrapped to prevent cascade failures)
var conflictData = (data.acled_events && !data.acled_events.error && data.acled_events.count > 0) ? data.acled_events
: (data.ucdp_events && !data.ucdp_events.error && data.ucdp_events.count > 0) ? data.ucdp_events
: data.conflict_zones;
var mapUpdates = [
['quakes', function() { updateMapQuakes(data.earthquakes); }],
['military', function() { updateMapMilitary(data.military_flights); }],
['conflict', function() { updateMapConflict(conflictData); }],
['fires', function() { updateMapFires(data.wildfires); }],
['convergence', function() { updateMapConvergence(data.signal_convergence); }],
['nuclear', function() { updateMapNuclear(data.nuclear_monitor); }],
['infra', function() { updateMapInfra(data); }],
['exposure', function() { updateMapExposure(data.population_exposure); }],
['airtraffic', function() { updateMapAirTraffic(data.domestic_flights); }],
['traffic', function() { updateMapTraffic(data.traffic_flow); }],
['navwarnings', function() { updateMapNavWarnings(data.nav_warnings); }],
['webcams', function() { updateMapWebcams(data.webcams); }],
['climate', function() { updateMapClimate(data.climate_anomalies); }],
['news', function() { updateMapNews(data.news_feed); }]
];
mapUpdates.forEach(function(pair) {
try { pair[1](); } catch(e) { console.warn('updateAll: ' + pair[0] + ' failed:', e); }
});
// Alert banner
try { updateAlertBanner(data.alert_digest); } catch(e) { console.warn('updateAll: alert_banner failed:', e); }
// HUD
try { updateHudStats(data); } catch(e) { console.warn('updateAll: hud failed:', e); }
// Drawer
try { updateDrawer(data); } catch(e) { console.warn('updateAll: drawer failed:', e); }
// Ticker
try { updateTicker(data); } catch(e) { console.warn('updateAll: ticker failed:', e); }
// Feed health
var feedKeys = Object.keys(data).filter(function(k) { return k !== 'source_health' && k !== 'cache_stats' && k !== 'timestamp'; });
var okCount = feedKeys.filter(function(k) { return data[k] && !data[k].error; }).length;
$('#feedCount').textContent = okCount + '/' + feedKeys.length + ' feeds';
if (data.timestamp) { $('#lastUpdate').textContent = new Date(data.timestamp).toLocaleTimeString(); }
// Reposition layer panel after HUD stats may have changed topbar height
if (typeof _syncLayersTop === 'function') setTimeout(_syncLayersTop, 50);
}
// ════════════ SSE CONNECTION ════════════
var reconnectDelay = 1000;
function connectSSE() {
var source = new EventSource('/api/stream');
source.onopen = function() {
$('#connDot').className = 'conn-dot';
$('#connLabel').textContent = 'LIVE';
reconnectDelay = 1000;
};
source.onmessage = function(event) {
try {
var data = JSON.parse(event.data);
if (data.error && !data.market_quotes) return;
updateAll(data);
document.getElementById('loading').classList.add('gone');
} catch (e) { console.error('Parse error:', e); }
};
source.onerror = function() {
source.close();
$('#connDot').className = 'conn-dot err';
$('#connLabel').textContent = 'RECONNECTING';
setTimeout(connectSSE, reconnectDelay);
reconnectDelay = Math.min(reconnectDelay * 2, 30000);
};
}
// ════════════ BOOT ════════════
initMap();
// Restore saved layer visibility
(function() {
for (var lk in layerState) {
if (!layerState[lk] && mapLayers[lk]) {
map.removeLayer(mapLayers[lk]);
var row = document.querySelector('.layer-row[data-layer="' + lk + '"]');
if (row) { var t = row.querySelector('.toggle'); if (t) t.classList.remove('on'); }
}
}
})();
// Drawer click delegation (attached once — not per SSE update)
$('#drawerBody').addEventListener('click', function(e) {
var row = e.target.closest('[data-click]');
if (row) { e.preventDefault(); e.stopPropagation(); showDrawerDetail(row.getAttribute('data-click')); return; }
var sh = e.target.closest('.sh');
if (sh) {
sh.classList.toggle('collapsed');
var hide = sh.classList.contains('collapsed');
var next = sh.nextElementSibling;
while (next && !next.classList.contains('sh')) { next.style.display = hide ? 'none' : ''; next = next.nextElementSibling; }
try {
var names = []; $$('#drawerBody .sh.collapsed').forEach(function(s) { names.push(s.textContent.trim()); });
localStorage.setItem('phoenix-drawer-collapsed', JSON.stringify(names));
} catch(e2) {}
}
});
// HUD pill click-to-zoom
var HUD_LAYER_MAP = { 'Quakes': 'quakes', 'Aircraft': 'military', 'Conflict': 'conflict', 'Fires': 'fires', 'Exposed': 'exposure', 'Airborne': 'airtraffic', 'Traffic': 'traffic', 'Cams': 'webcams', 'Nuke Flag': 'nuclear', 'Climate': 'climate', 'News': 'news' };
$('#hudStats').addEventListener('click', function(e) {
var pill = e.target.closest('.stat-pill');
if (!pill) return;
var label = pill.querySelector('.l');
if (!label) return;
var layerKey = HUD_LAYER_MAP[label.textContent.trim()];
if (!layerKey || !mapLayers[layerKey]) return;
// Ensure layer is visible
if (!layerState[layerKey]) {
layerState[layerKey] = true;
mapLayers[layerKey].addTo(map);
var row = document.querySelector('.layer-row[data-layer="' + layerKey + '"]');
if (row) { var t = row.querySelector('.toggle'); if (t) t.classList.add('on'); }
try { localStorage.setItem('phoenix-layers', JSON.stringify(layerState)); } catch(e2) {}
}
try {
var bounds = mapLayers[layerKey].getBounds();
if (bounds && bounds.isValid()) map.fitBounds(bounds, { padding: [50, 50], maxZoom: 6 });
} catch(e2) {}
});
// Search
var _searchTimeout = null;
$('#searchInput').addEventListener('input', function() {
clearTimeout(_searchTimeout);
var q = this.value.trim().toLowerCase();
if (q.length < 2) return;
_searchTimeout = setTimeout(function() { performSearch(q); }, 500);
});
$('#searchInput').addEventListener('keydown', function(e) {
if (e.key === 'Enter') { e.preventDefault(); clearTimeout(_searchTimeout); var q = this.value.trim().toLowerCase(); if (q.length >= 2) performSearch(q); }
e.stopPropagation();
});
function _matchText(item, q) {
var fields = [item.country, item.place, item.city, item.name, item.callsign, item.origin_country,
item.title, item.location, item.admin1, item.event_type, item.indicator, item.subreddit,
item.site, item.site_country, item.operator, item.side_a, item.side_b, item.dyad_name,
item.actor1, item.actor2, item.notes];
for (var i = 0; i < fields.length; i++) { if (fields[i] && String(fields[i]).toLowerCase().indexOf(q) >= 0) return true; }
// Search array fields (associated_countries, tags) with ISO3→name expansion
var _iso = {UKR:'ukraine',RUS:'russia',CHN:'china',TWN:'taiwan',IRN:'iran',ISR:'israel',PSE:'palestine',PRK:'north korea',KOR:'south korea',MMR:'myanmar',ETH:'ethiopia',SDN:'sudan',SSD:'south sudan',SOM:'somalia',YEM:'yemen',SYR:'syria',AFG:'afghanistan',LBY:'libya',MLI:'mali',MOZ:'mozambique',NGA:'nigeria',COD:'congo',COL:'colombia',VEN:'venezuela',IND:'india',PAK:'pakistan',PHL:'philippines',LBN:'lebanon',IRQ:'iraq',USA:'united states',GBR:'united kingdom',FRA:'france',DEU:'germany',JPN:'japan',SAU:'saudi arabia',TUR:'turkey',EGY:'egypt',BRA:'brazil',MEX:'mexico',AUS:'australia',CAN:'canada',ZAF:'south africa'};
var assoc = item.associated_countries;
if (Array.isArray(assoc)) {
for (var j = 0; j < assoc.length; j++) {
var code = assoc[j];
if (code && code.toLowerCase().indexOf(q) >= 0) return true;
if (_iso[code] && _iso[code].indexOf(q) >= 0) return true;
}
}
if (Array.isArray(item.tags) && item.tags.join(' ').toLowerCase().indexOf(q) >= 0) return true;
return false;
}
function performSearch(q) {
if (!latestData) return;
var coords = [];
function _scan(items, latK, lonK) {
(items || []).forEach(function(it) {
if (_matchText(it, q) && it[latK] != null && it[lonK] != null) coords.push([Number(it[latK]), Number(it[lonK])]);
});
}
_scan((latestData.earthquakes || {}).earthquakes, 'latitude', 'longitude');
_scan((latestData.military_flights || {}).aircraft, 'latitude', 'longitude');
var cSrc = (latestData.acled_events && !latestData.acled_events.error && (latestData.acled_events.count||0) > 0) ? latestData.acled_events
: (latestData.ucdp_events && !latestData.ucdp_events.error && (latestData.ucdp_events.count||0) > 0) ? latestData.ucdp_events
: latestData.conflict_zones;
_scan((cSrc || {}).events, 'latitude', 'longitude');
_scan((cSrc || {}).events, 'lat', 'lon');
var fbr = (latestData.wildfires || {}).fires_by_region || {};
for (var rk in fbr) { if (fbr.hasOwnProperty(rk)) { (fbr[rk].top_clusters || []).forEach(function(c) { if (rk.toLowerCase().indexOf(q) >= 0 && c.lat != null) coords.push([c.lat, c.lon]); }); } }
_scan((latestData.signal_convergence || {}).hotspots, 'lat', 'lon');
_scan((latestData.traffic_flow || {}).cities, 'lat', 'lon');
_scan((latestData.webcams || {}).cameras, 'lat', 'lon');
_scan((latestData.domestic_flights || {}).positions, 'lat', 'lon');
_scan((latestData.nav_warnings || {}).warnings, 'lat', 'lon');
_scan((latestData.military_bases || {}).bases, 'lat', 'lon');
_scan((latestData.strategic_ports || {}).ports, 'lat', 'lon');
_scan((latestData.nuclear_facilities || {}).facilities, 'lat', 'lon');
_scan((latestData.nuclear_monitor || {}).sites, 'lat', 'lon');
var climSrc = latestData.climate_anomalies || {};
var climZones = climSrc.anomalies || climSrc.zones || climSrc.data || [];
if (!Array.isArray(climZones) && typeof climZones === 'object') climZones = Object.values(climZones);
_scan(climZones, 'lat', 'lon');
// News articles with geolocation
var newsArticles = (latestData.news_feed || {}).articles || (latestData.news_feed || {}).items || [];
newsArticles.forEach(function(a) { if (a._geo_lat && _matchText(a, q)) coords.push([a._geo_lat, a._geo_lon]); });
if (coords.length > 0) {
map.fitBounds(L.latLngBounds(coords), { padding: [60, 60], maxZoom: coords.length === 1 ? 10 : 6 });
$('#searchInput').style.borderColor = 'rgba(0,229,255,0.4)';
} else {
$('#searchInput').style.borderColor = 'rgba(255,59,59,0.4)';
}
setTimeout(function() { $('#searchInput').style.borderColor = ''; }, 2000);
}
// Mobile: collapsible layer panel
$('#layers .panel-title').addEventListener('click', function() {
$('#layers').classList.toggle('layers-open');
});
// Auto-position layer panel + alert banner below topbar
function _syncLayersTop() {
var tb = $('#topBar');
if (tb) {
var h = tb.offsetHeight;
var bannerTop = h + 16;
document.documentElement.style.setProperty('--layers-top', bannerTop + 'px');
// Push layers below the alert banner if it's showing
var banner = $('#alertBanner');
var bannerH = (banner && banner.classList.contains('show')) ? banner.offsetHeight + 4 : 0;
$('#layers').style.top = (bannerTop + bannerH) + 'px';
}
}
_syncLayersTop();
window.addEventListener('resize', _syncLayersTop);
// Show loading indicator until SSE data arrives
$('#hudStats').innerHTML = '<div class="stat-pill"><span class="l" style="animation:pulse-dot 2s ease-in-out infinite">LOADING LIVE FEEDS</span></div>';
$('#drawerBody').innerHTML = '<div style="padding:30px;text-align:center"><div class="load-ring" style="width:28px;height:28px;margin:0 auto 10px"></div><div class="dim" style="font-size:0.6rem;letter-spacing:1.5px">LOADING INTELLIGENCE FEEDS</div></div>';
// ═══════════ REGIONAL PRESETS ═══════════
var REGION_VIEWS = {
global: { center: [20, 0], zoom: 3 },
americas: { center: [10, -80], zoom: 4 },
europe: { center: [50, 15], zoom: 5 },
mena: { center: [28, 40], zoom: 5 },
asia: { center: [20, 110], zoom: 4 },
africa: { center: [0, 25], zoom: 4 },
};
$$('#regionPresets .rgn-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
var region = btn.dataset.region;
var view = REGION_VIEWS[region];
if (!view) return;
$$('#regionPresets .rgn-btn').forEach(function(b) { b.classList.remove('active'); });
btn.classList.add('active');
map.flyTo(view.center, view.zoom, { duration: 1.2 });
try { localStorage.setItem('phoenix-region', region); } catch(e) {}
});
});
// Restore saved region
try {
var savedRegion = localStorage.getItem('phoenix-region');
if (savedRegion && REGION_VIEWS[savedRegion]) {
$$('#regionPresets .rgn-btn').forEach(function(b) {
b.classList.toggle('active', b.dataset.region === savedRegion);
});
var sv = REGION_VIEWS[savedRegion];
map.setView(sv.center, sv.zoom);
}
} catch(e) {}
// ═══════════ TIME WINDOW FILTER ═══════════
var _timeFilterHours = 24;
$$('#timeFilter .rgn-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
_timeFilterHours = parseInt(btn.dataset.hours) || 0;
$$('#timeFilter .rgn-btn').forEach(function(b) { b.classList.remove('active'); });
btn.classList.add('active');
try { localStorage.setItem('phoenix-time-filter', String(_timeFilterHours)); } catch(e) {}
// Re-render with current data if available
if (window._lastSSEData) refreshDashboard(window._lastSSEData);
});
});
// Restore saved time filter
try {
var savedTime = localStorage.getItem('phoenix-time-filter');
if (savedTime !== null) {
_timeFilterHours = parseInt(savedTime) || 0;
$$('#timeFilter .rgn-btn').forEach(function(b) {
b.classList.toggle('active', b.dataset.hours === savedTime);
});
}
} catch(e) {}
// Time filter helper: check if an ISO timestamp is within the active window
function _withinTimeWindow(isoTimestamp) {
if (_timeFilterHours === 0) return true; // "All" = no filter
if (!isoTimestamp) return true; // No timestamp = show
try {
var ts = new Date(isoTimestamp).getTime();
var cutoff = Date.now() - (_timeFilterHours * 3600000);
return ts >= cutoff;
} catch(e) { return true; }
}
// Load static geospatial data immediately (bases, ports, nuclear facilities).
fetch('/api/static')
.then(function(r) { return r.json(); })
.then(function(data) {
try { updateMapInfra(data); } catch(e) { console.warn('static infra failed:', e); }
document.getElementById('loading').classList.add('gone');
})
.catch(function(e) { console.warn('static fetch failed:', e); });
connectSSE();
</script>
</body>
</html>