Files
world-intel-mcp/src/world_intel_mcp/dashboard/index.html
T

1402 lines
74 KiB
HTML
Raw Normal View History

<!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'>&#x1F525;</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" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.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 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: 12px;
}
.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; }
.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: 66px; left: 10px; z-index: 40;
width: 200px; padding: 12px 14px;
}
.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; }
.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); }
/* ═══════════════ 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: 54px; left: 10px; right: 360px; z-index: 45;
max-height: 0; overflow: hidden; transition: max-height 0.4s var(--ease);
}
#alertBanner.show { max-height: 120px; }
#alertBanner.show + #layers { top: 108px; }
.alert-strip {
display: flex; align-items: center; gap: 10px;
padding: 6px 14px; border-radius: 8px; margin-bottom: 4px;
font-family: var(--mono); font-size: 0.68rem;
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: 6px; height: 6px; 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); }
.alert-domain { color: var(--dim); font-size: 0.58rem; 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; }
@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: 160px; }
.hud-stats { display: none; }
}
</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">&#x1F525;</span>
<span class="logo-text">PHOENIX<span class="logo-dim"> INTEL</span></span>
</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>
</aside>
<!-- DATA DRAWER -->
<aside id="drawer" class="glass closed">
<button class="drawer-tab" id="drawerToggle" title="Toggle data panel">&#x25C0;</button>
<div class="drawer-inner" id="drawerBody"></div>
</aside>
<!-- DETAIL MODAL -->
<div id="detail">
<button class="detail-close" id="detailClose">&times;</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'], ALLOWED_ATTR: ['class','style','href','target']}); };
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 };
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);
mapLayers.quakes = L.layerGroup().addTo(map);
mapLayers.military = L.layerGroup().addTo(map);
mapLayers.conflict = L.layerGroup().addTo(map);
mapLayers.fires = L.layerGroup().addTo(map);
mapLayers.convergence = L.layerGroup().addTo(map);
mapLayers.nuclear = L.layerGroup().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]); }
});
});
// ════════════ DRAWER TOGGLE ════════════
$('#drawerToggle').addEventListener('click', function() {
drawerOpen = !drawerOpen;
$('#drawer').classList.toggle('closed', !drawerOpen);
this.innerHTML = drawerOpen ? '&#x25B6;' : '&#x25C0;';
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)' };
var LABELS = { earthquake: 'EARTHQUAKE', military: 'MILITARY AIRCRAFT', conflict: 'CONFLICT EVENT', fire: 'WILDFIRE CLUSTER', convergence: 'SIGNAL CONVERGENCE', nuclear: 'NUCLEAR MONITOR' };
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']
];
}
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 = 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) + '" target="_blank">View Source \u2192</a>';
$('#detailHeader').innerHTML = safe(hdr);
$('#detailBody').innerHTML = safe(body);
$('#detail').classList.add('open');
}
function closeDetail() { $('#detail').classList.remove('open'); }
$('#detailClose').addEventListener('click', closeDetail);
document.addEventListener('keydown', function(e) { if (e.key === 'Escape') closeDetail(); });
// ════════════ MAP MARKER MANAGEMENT ════════════
function updateMapQuakes(data) {
mapLayers.quakes.clearLayers();
if (!data || data.error) { $('#cntQuakes').textContent = '0'; return; }
var quakes = data.earthquakes || [];
$('#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, 100).forEach(function(e) {
var lat = e.latitude || e.lat, lon = e.longitude || e.lon;
if (lat == null || lon == null) return;
var fat = e.fatalities || 0;
var size = Math.max(8, Math.min(18, fat / 2 + 8));
var mk = L.marker([Number(lat), Number(lon)], {
icon: L.divIcon({ className: 'mk-conflict', iconSize: [size, size], iconAnchor: [size/2, size/2] })
});
mk.bindTooltip((e.event_type || '?') + ' \u2014 ' + (e.country || '') + (fat > 0 ? ' (' + fat + ' fatal)' : ''), { 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);
});
}
$('#cntFires').textContent = data.total_fires || total;
}
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 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>');
}
if (data.acled_events && !data.acled_events.error) {
var ac = data.acled_events.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 fc = data.wildfires.total_fires || 0;
pills.push('<div class="stat-pill"><span class="v' + (fc > 500 ? ' crit' : fc > 100 ? ' warn' : '') + '">' + fc + '</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>');
}
$('#hudStats').innerHTML = safe(pills.join(''));
}
// ════════════ DATA DRAWER ════════════
function updateDrawer(data) {
var h = '';
// ── 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';
h += '<tr><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.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) {
h += '<tr><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;
h += '<tr><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;
h += '<tr><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.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) {
h += '<tr><td><a href="' + esc(item.link || '#') + '" target="_blank">' + esc(trunc(item.title || '?', 40)) + '</a></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;
h += '<tr><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);
h += '<tr><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>';
}
}
// ── 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) {
h += '<tr><td><a href="' + esc(p.url || '#') + '" target="_blank">' + esc(trunc(p.title || '?', 35)) + '</a></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>';
}
}
// ── 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>';
}
}
$('#drawerBody').innerHTML = safe(h);
}
// ════════════ 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 updateAll(data) {
latestData = data;
// Map layers
updateMapQuakes(data.earthquakes);
updateMapMilitary(data.military_flights);
updateMapConflict(data.acled_events);
updateMapFires(data.wildfires);
updateMapConvergence(data.signal_convergence);
updateMapNuclear(data.nuclear_monitor);
// Alert banner
updateAlertBanner(data.alert_digest);
// HUD
updateHudStats(data);
// Drawer
updateDrawer(data);
// Ticker
updateTicker(data);
// 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(); }
}
// ════════════ 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();
connectSSE();
</script>
</body>
</html>