bb3217bfea
- SSE endpoint /sse/vector-analytics: pushes stats, domain summary, trend detection every 30s via asyncio.gather() - POST /api/vector-search: semantic search with query, hours, category filters - Glassmorphic Vector Intelligence panel (bottom-left): - Total vector points + collection status - Per-category data point counts (top 12) - Trend tags: SURGE (red), ELEVATED (orange), NORMAL (green), DROP (blue) - Inline semantic search with score-ranked results - Graceful degradation when Qdrant unavailable (panel stays hidden) - All innerHTML uses safe() wrapper (DOMPurify)
3301 lines
174 KiB
HTML
3301 lines
174 KiB
HTML
<!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); }
|
|
|
|
/* ═══════════════ VECTOR INTELLIGENCE PANEL ═══════════════ */
|
|
#vectorPanel {
|
|
position: fixed; bottom: 44px; left: 10px; z-index: 40;
|
|
width: 310px; max-height: 420px;
|
|
overflow-y: auto; overflow-x: hidden;
|
|
padding: 12px 14px;
|
|
transition: transform 0.4s var(--ease), opacity 0.3s;
|
|
}
|
|
#vectorPanel.hidden { transform: translateY(20px); opacity: 0; pointer-events: none; }
|
|
.vec-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
|
|
.vec-title { font-size: 0.62rem; font-weight: 600; letter-spacing: 2.5px; color: var(--accent); text-transform: uppercase; }
|
|
.vec-stat { font-family: var(--mono); font-size: 0.6rem; color: var(--dim); }
|
|
.vec-stat .v { color: var(--bright); font-weight: 600; }
|
|
.vec-cats { margin: 6px 0 8px; }
|
|
.vec-cat-row {
|
|
display: flex; align-items: center; gap: 6px; padding: 3px 0;
|
|
font-size: 0.66rem; border-bottom: 1px solid rgba(255,255,255,0.02);
|
|
}
|
|
.vec-cat-row .cat-name { flex: 1; color: var(--text); }
|
|
.vec-cat-row .cat-count { font-family: var(--mono); color: var(--bright); min-width: 28px; text-align: right; font-weight: 600; }
|
|
.vec-trend-tag {
|
|
display: inline-block; padding: 1px 6px; border-radius: 3px;
|
|
font-family: var(--mono); font-size: 0.56rem; font-weight: 600;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
.vec-trend-tag.surge { background: rgba(255,59,59,0.12); color: var(--red); }
|
|
.vec-trend-tag.elevated { background: rgba(255,159,28,0.1); color: var(--amber); }
|
|
.vec-trend-tag.normal { background: rgba(52,211,153,0.08); color: var(--green); }
|
|
.vec-trend-tag.drop { background: rgba(77,168,255,0.1); color: var(--blue); }
|
|
.vec-trend-tag.declining { background: rgba(77,168,255,0.08); color: var(--blue); opacity: 0.7; }
|
|
.vec-search-wrap {
|
|
display: flex; gap: 4px; margin: 8px 0 4px;
|
|
}
|
|
.vec-search-input {
|
|
flex: 1; padding: 5px 8px; border-radius: 5px;
|
|
background: rgba(0,0,0,0.35); border: 1px solid var(--border);
|
|
color: var(--bright); font-family: var(--mono); font-size: 0.65rem;
|
|
outline: none; transition: border-color 0.2s;
|
|
}
|
|
.vec-search-input:focus { border-color: rgba(0,229,255,0.3); }
|
|
.vec-search-input::placeholder { color: var(--dim); }
|
|
.vec-search-btn {
|
|
padding: 5px 10px; border-radius: 5px;
|
|
background: rgba(0,229,255,0.08); border: 1px solid rgba(0,229,255,0.15);
|
|
color: var(--accent); font-family: var(--mono); font-size: 0.6rem;
|
|
cursor: pointer; transition: background 0.2s;
|
|
}
|
|
.vec-search-btn:hover { background: rgba(0,229,255,0.15); }
|
|
.vec-search-btn:disabled { opacity: 0.4; cursor: default; }
|
|
.vec-results { margin-top: 6px; }
|
|
.vec-result-item {
|
|
padding: 5px 0; border-bottom: 1px solid rgba(255,255,255,0.02);
|
|
font-size: 0.64rem;
|
|
}
|
|
.vec-result-item .r-score { font-family: var(--mono); color: var(--accent); font-weight: 600; }
|
|
.vec-result-item .r-domain { font-family: var(--mono); color: var(--dim); font-size: 0.56rem; text-transform: uppercase; letter-spacing: 0.5px; }
|
|
.vec-result-item .r-text { color: var(--text); margin-top: 2px; line-height: 1.3; }
|
|
.vec-result-item .r-time { font-family: var(--mono); color: var(--dim); font-size: 0.56rem; }
|
|
.vec-unavail { font-size: 0.65rem; color: var(--dim); padding: 8px 0; text-align: center; }
|
|
|
|
/* ═══════════════ 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>
|
|
|
|
<!-- VECTOR INTELLIGENCE PANEL -->
|
|
<div id="vectorPanel" class="glass hidden">
|
|
<div class="vec-header">
|
|
<span class="vec-title">Vector Intelligence</span>
|
|
<span class="vec-stat" id="vecPointCount"></span>
|
|
</div>
|
|
<div id="vecContent"><div class="vec-unavail">Connecting...</div></div>
|
|
<div class="vec-search-wrap">
|
|
<input type="text" class="vec-search-input" id="vecSearchInput" placeholder="Semantic search..." autocomplete="off">
|
|
<button class="vec-search-btn" id="vecSearchBtn">Search</button>
|
|
</div>
|
|
<div class="vec-results" id="vecResults"></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();
|
|
|
|
// ════════════ VECTOR INTELLIGENCE SSE ════════════
|
|
|
|
function updateVectorPanel(vd) {
|
|
var panel = $('#vectorPanel');
|
|
if (!vd || !vd.available) {
|
|
panel.classList.add('hidden');
|
|
return;
|
|
}
|
|
panel.classList.remove('hidden');
|
|
|
|
// Points count in header
|
|
var stats = vd.stats || {};
|
|
var pts = stats.points_count || 0;
|
|
var status = stats.status || 'unknown';
|
|
var statusColor = status === 'green' ? 'var(--green)' : status === 'yellow' ? 'var(--amber)' : 'var(--dim)';
|
|
$('#vecPointCount').innerHTML = safe(
|
|
'<span class="v">' + fmtNum(pts) + '</span> pts ' +
|
|
'<span style="color:' + statusColor + '">' + esc(status) + '</span>'
|
|
);
|
|
|
|
var h = '';
|
|
|
|
// Domain summary table
|
|
var ds = vd.domain_summary || {};
|
|
var cats = ds.summary || [];
|
|
if (cats.length > 0) {
|
|
h += '<div class="vec-cats">';
|
|
cats.slice(0, 12).forEach(function(c) {
|
|
h += '<div class="vec-cat-row">';
|
|
h += '<span class="cat-name">' + esc(c.category) + '</span>';
|
|
h += '<span class="cat-count">' + (c.data_points || 0) + '</span>';
|
|
h += '</div>';
|
|
});
|
|
h += '</div>';
|
|
}
|
|
|
|
// Trend indicators
|
|
var trends = vd.trends || {};
|
|
var trendList = trends.trends || [];
|
|
if (trendList.length > 0) {
|
|
h += '<div style="font-size:0.56rem;color:var(--dim);text-transform:uppercase;letter-spacing:0.5px;margin:4px 0 3px">Activity Trends (6h vs 48h)</div>';
|
|
h += '<div style="display:flex;flex-wrap:wrap;gap:3px;margin-bottom:4px">';
|
|
trendList.forEach(function(t) {
|
|
var tcls = t.trend === 'SURGE' ? 'surge' : t.trend === 'ELEVATED' ? 'elevated' : t.trend === 'DROP' ? 'drop' : t.trend === 'DECLINING' ? 'declining' : 'normal';
|
|
var arrow = t.trend === 'SURGE' ? '\u2191\u2191' : t.trend === 'ELEVATED' ? '\u2191' : t.trend === 'DROP' ? '\u2193\u2193' : t.trend === 'DECLINING' ? '\u2193' : '\u2022';
|
|
var shortCat = t.category.length > 16 ? t.category.substring(0, 14) + '..' : t.category;
|
|
h += '<span class="vec-trend-tag ' + tcls + '" title="' + esc(t.category) + ': ' + (t.change_pct > 0 ? '+' : '') + t.change_pct + '%">';
|
|
h += arrow + ' ' + esc(shortCat);
|
|
h += '</span>';
|
|
});
|
|
h += '</div>';
|
|
}
|
|
|
|
$('#vecContent').innerHTML = safe(h);
|
|
}
|
|
|
|
// Vector search
|
|
var _vecSearching = false;
|
|
function doVecSearch() {
|
|
var q = $('#vecSearchInput').value.trim();
|
|
if (!q || _vecSearching) return;
|
|
_vecSearching = true;
|
|
$('#vecSearchBtn').disabled = true;
|
|
$('#vecSearchBtn').textContent = '...';
|
|
$('#vecResults').innerHTML = safe('<div class="vec-unavail">Searching...</div>');
|
|
|
|
fetch('/api/vector-search', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ query: q, limit: 10 })
|
|
})
|
|
.then(function(r) { return r.json(); })
|
|
.then(function(data) {
|
|
var h = '';
|
|
if (data.error) {
|
|
h = '<div class="vec-unavail">' + esc(data.error) + '</div>';
|
|
} else {
|
|
var results = data.results || [];
|
|
if (results.length === 0) {
|
|
h = '<div class="vec-unavail">No results found</div>';
|
|
} else {
|
|
results.forEach(function(r) {
|
|
h += '<div class="vec-result-item">';
|
|
h += '<div style="display:flex;justify-content:space-between;align-items:baseline">';
|
|
h += '<span class="r-score">' + r.score + '</span>';
|
|
h += '<span class="r-domain">' + esc(r.category || r.domain) + '</span>';
|
|
h += '</div>';
|
|
h += '<div class="r-text">' + esc(trunc(r.text || '', 150)) + '</div>';
|
|
if (r.datetime) {
|
|
h += '<div class="r-time">' + ago(new Date(r.datetime).getTime()) + ' ago</div>';
|
|
}
|
|
h += '</div>';
|
|
});
|
|
}
|
|
}
|
|
$('#vecResults').innerHTML = safe(h);
|
|
})
|
|
.catch(function(e) {
|
|
$('#vecResults').innerHTML = safe('<div class="vec-unavail">Search failed: ' + esc(String(e)) + '</div>');
|
|
})
|
|
.finally(function() {
|
|
_vecSearching = false;
|
|
$('#vecSearchBtn').disabled = false;
|
|
$('#vecSearchBtn').textContent = 'Search';
|
|
});
|
|
}
|
|
|
|
$('#vecSearchBtn').addEventListener('click', doVecSearch);
|
|
$('#vecSearchInput').addEventListener('keydown', function(e) {
|
|
if (e.key === 'Enter') { e.preventDefault(); doVecSearch(); }
|
|
e.stopPropagation();
|
|
});
|
|
|
|
// SSE connection for vector analytics
|
|
var vecReconnectDelay = 1000;
|
|
function connectVectorSSE() {
|
|
var source = new EventSource('/sse/vector-analytics');
|
|
source.onmessage = function(event) {
|
|
try {
|
|
var data = JSON.parse(event.data);
|
|
updateVectorPanel(data);
|
|
} catch (e) { console.error('Vector SSE parse error:', e); }
|
|
};
|
|
source.onerror = function() {
|
|
source.close();
|
|
// Hide panel on disconnect — it will re-appear on reconnect
|
|
setTimeout(connectVectorSSE, vecReconnectDelay);
|
|
vecReconnectDelay = Math.min(vecReconnectDelay * 2, 30000);
|
|
};
|
|
source.onopen = function() { vecReconnectDelay = 1000; };
|
|
}
|
|
connectVectorSSE();
|
|
</script>
|
|
</body>
|
|
</html>
|