1929 lines
168 KiB
HTML
1929 lines
168 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>CRUCIX — Intelligence Terminal</title>
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;500;600;700&family=Space+Grotesk:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.5/gsap.min.js"></script>
|
||
<script src="https://d3js.org/d3.v7.min.js"></script>
|
||
<script src="https://d3js.org/topojson.v3.min.js"></script>
|
||
<script src="https://unpkg.com/three@0.160.0/build/three.min.js"></script>
|
||
<script src="https://unpkg.com/globe.gl@2.33.0"></script>
|
||
<style>
|
||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||
:root{
|
||
--bg:#020408;--panel:rgba(6,14,22,0.82);--glass:rgba(10,20,32,0.55);
|
||
--border:rgba(100,240,200,0.12);--border-bright:rgba(100,240,200,0.3);
|
||
--text:#e8f4f0;--dim:#6a8a82;--accent:#64f0c8;--accent2:#44ccff;
|
||
--warn:#ffb84c;--danger:#ff5f63;--mono:'IBM Plex Mono',monospace;--sans:'Space Grotesk',sans-serif;
|
||
}
|
||
html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--sans);overflow-x:hidden}
|
||
.bg-grid{position:fixed;inset:0;pointer-events:none;opacity:0;
|
||
background-image:linear-gradient(rgba(100,240,200,0.03) 1px,transparent 1px),linear-gradient(90deg,rgba(100,240,200,0.03) 1px,transparent 1px);
|
||
background-size:60px 60px;mask-image:radial-gradient(ellipse at 50% 30%,black 20%,transparent 70%)}
|
||
.bg-radial{position:fixed;inset:0;pointer-events:none;opacity:0;
|
||
background:radial-gradient(ellipse at 50% 0%,rgba(40,120,100,0.15),transparent 50%),radial-gradient(ellipse at 80% 20%,rgba(40,100,180,0.08),transparent 40%)}
|
||
.scanline{position:fixed;inset:0;pointer-events:none;overflow:hidden;opacity:0}
|
||
.scanline::after{content:'';position:absolute;left:0;width:100%;height:2px;background:linear-gradient(90deg,transparent,rgba(100,240,200,0.12),transparent);animation:scanMove 4s linear infinite}
|
||
@keyframes scanMove{0%{top:-2px}100%{top:100%}}
|
||
|
||
/* BOOT */
|
||
#boot{position:fixed;inset:0;z-index:1000;display:flex;flex-direction:column;align-items:center;justify-content:center;background:var(--bg)}
|
||
.logo-ring{width:120px;height:120px;border:2px solid var(--border);border-radius:50%;display:flex;align-items:center;justify-content:center;position:relative;opacity:0}
|
||
.logo-ring::before{content:'';position:absolute;inset:-8px;border:1px solid var(--border);border-radius:50%;border-top-color:var(--accent);animation:spin 2s linear infinite}
|
||
.logo-ring::after{content:'';position:absolute;inset:-16px;border:1px solid rgba(100,240,200,0.06);border-radius:50%;border-bottom-color:rgba(100,240,200,0.15);animation:spin 3s linear infinite reverse}
|
||
@keyframes spin{to{transform:rotate(360deg)}}
|
||
.logo-text{font-family:var(--mono);font-size:18px;font-weight:700;letter-spacing:0.2em;color:var(--accent)}
|
||
#bootLines{margin-top:32px;font-family:var(--mono);font-size:12px;color:var(--dim);text-align:left;line-height:2;min-width:340px;opacity:0}
|
||
#bootLines .ok{color:var(--accent)}
|
||
#bootLines .count{color:var(--accent);font-weight:600}
|
||
#bootFinal{margin-top:24px;font-family:var(--mono);font-size:14px;font-weight:600;color:var(--accent);letter-spacing:0.15em;opacity:0}
|
||
|
||
/* MAIN */
|
||
#main{opacity:0;min-height:100vh;position:relative;padding:10px 12px}
|
||
|
||
/* TOPBAR */
|
||
.topbar{display:flex;align-items:center;justify-content:space-between;padding:12px 18px;border:1px solid var(--border);background:var(--panel);backdrop-filter:blur(20px);flex-wrap:wrap;gap:10px}
|
||
.top-left{display:flex;align-items:center;gap:14px}
|
||
.brand{font-family:var(--mono);font-weight:700;font-size:15px;letter-spacing:0.12em;text-transform:uppercase}
|
||
.regime-chip{display:inline-flex;align-items:center;gap:6px;padding:5px 12px;font-family:var(--mono);font-size:11px;letter-spacing:0.1em;text-transform:uppercase;border:1px solid rgba(255,95,99,0.3);color:#ffd8d9;background:rgba(255,95,99,0.08)}
|
||
.regime-chip .blink{width:6px;height:6px;border-radius:50%;background:var(--danger);box-shadow:0 0 8px var(--danger);animation:pulse-blink 1.5s ease-in-out infinite}
|
||
@keyframes pulse-blink{0%,100%{opacity:1}50%{opacity:0.3}}
|
||
.top-center{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
||
.region-btn{border:1px solid var(--border);background:rgba(255,255,255,0.02);color:var(--dim);font-family:var(--mono);font-size:11px;padding:6px 12px;letter-spacing:0.08em;text-transform:uppercase;cursor:pointer;transition:all 0.2s}
|
||
.region-btn:hover{border-color:var(--accent);color:var(--text)}
|
||
.region-btn.active{color:#03140d;background:var(--accent);border-color:var(--accent)}
|
||
.top-right{display:flex;align-items:center;gap:10px}
|
||
.meta-pill{font-family:var(--mono);font-size:11px;color:var(--dim);letter-spacing:0.06em;padding:5px 10px;border:1px solid var(--border)}
|
||
.meta-pill .v{color:var(--text);font-weight:500}
|
||
.alert-badge{padding:5px 12px;font-family:var(--mono);font-size:11px;font-weight:700;letter-spacing:0.1em;text-transform:uppercase;border:1px solid rgba(255,95,99,0.4);color:#fff;background:linear-gradient(135deg,rgba(255,95,99,0.2),rgba(255,95,99,0.08))}
|
||
.guide-btn{padding:5px 12px;font-family:var(--mono);font-size:11px;font-weight:600;letter-spacing:0.08em;text-transform:uppercase;border:1px solid rgba(68,204,255,0.28);color:var(--accent2);background:rgba(68,204,255,0.07);cursor:pointer;transition:all 0.2s}
|
||
.guide-btn:hover{border-color:rgba(68,204,255,0.5);background:rgba(68,204,255,0.12);color:#d9f7ff}
|
||
.perf-pill{cursor:pointer;background:rgba(255,255,255,0.05);transition:all 0.2s}
|
||
.perf-pill:hover{border-color:var(--accent2);color:var(--text);background:rgba(68,204,255,0.08)}
|
||
|
||
/* GRID */
|
||
.grid{display:grid;grid-template-columns:240px 1fr 340px;gap:10px;margin-top:10px;min-height:calc(100vh - 100px)}
|
||
.col{display:flex;flex-direction:column;gap:10px}
|
||
.g-panel{border:1px solid var(--border);background:var(--glass);backdrop-filter:blur(16px);padding:12px;position:relative;overflow:hidden}
|
||
.g-panel::before{content:'';position:absolute;top:0;left:0;right:0;height:1px;background:linear-gradient(90deg,transparent,rgba(100,240,200,0.15),transparent);pointer-events:none}
|
||
.sec-head{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px}
|
||
.sec-head h3{font-family:var(--mono);font-size:10px;font-weight:600;letter-spacing:0.14em;text-transform:uppercase;color:var(--dim)}
|
||
.badge{font-family:var(--mono);font-size:10px;padding:2px 7px;border:1px solid var(--border);color:var(--accent)}
|
||
|
||
/* LEFT RAIL */
|
||
.layer-item{display:flex;align-items:center;justify-content:space-between;padding:8px;border:1px solid rgba(255,255,255,0.04);background:rgba(255,255,255,0.02);margin-bottom:4px}
|
||
.layer-item{cursor:pointer;transition:border-color .15s ease,background .15s ease,opacity .15s ease}
|
||
.layer-item:hover{border-color:rgba(100,240,200,0.24);background:rgba(100,240,200,0.04)}
|
||
.layer-item.focused{border-color:rgba(100,240,200,0.6);background:rgba(100,240,200,0.08)}
|
||
.layer-item.hidden-layer{opacity:.45;border-color:rgba(255,95,99,0.18)}
|
||
.layer-mode{font-family:var(--mono);font-size:8px;color:var(--dim);margin-top:2px;text-transform:uppercase}
|
||
.sensor-actions{display:flex;gap:6px;align-items:center}
|
||
.mini-btn{border:1px solid rgba(100,240,200,0.18);background:rgba(100,240,200,0.04);color:var(--dim);font-family:var(--mono);font-size:9px;padding:3px 6px;cursor:pointer}
|
||
.mini-btn:hover{color:var(--accent);border-color:rgba(100,240,200,0.4)}
|
||
.action-grid{display:grid;grid-template-columns:repeat(3,1fr);gap:6px;margin-bottom:10px}
|
||
.action-btn{border:1px solid rgba(68,204,255,0.24);background:rgba(68,204,255,0.06);color:var(--text);font-family:var(--mono);font-size:9px;padding:7px 6px;cursor:pointer;text-transform:uppercase;letter-spacing:.08em}
|
||
.action-btn:hover{border-color:rgba(68,204,255,0.55);color:var(--accent2);background:rgba(68,204,255,0.12)}
|
||
.action-btn[disabled]{opacity:.45;cursor:wait}
|
||
.terminal-output{min-height:58px;max-height:180px;overflow:auto;border:1px solid rgba(255,255,255,0.05);background:rgba(0,0,0,0.22);padding:8px;font-family:var(--mono);font-size:10px;line-height:1.45;color:var(--dim);white-space:pre-wrap}
|
||
.terminal-output strong{color:var(--accent)}
|
||
.terminal-output .err{color:var(--danger)}
|
||
.layer-left{display:flex;align-items:center;gap:8px}
|
||
.ldot{width:10px;height:10px;border-radius:50%;flex-shrink:0}
|
||
.ldot.air{background:var(--accent);box-shadow:0 0 6px rgba(100,240,200,0.4)}
|
||
.ldot.thermal{background:var(--danger);box-shadow:0 0 6px rgba(255,95,99,0.4)}
|
||
.ldot.sdr{background:var(--accent2);box-shadow:0 0 6px rgba(68,204,255,0.4)}
|
||
.ldot.nuke{background:#ffe082;box-shadow:0 0 6px rgba(255,224,130,0.4)}
|
||
.ldot.incident{background:var(--warn);box-shadow:0 0 6px rgba(255,184,76,0.4)}
|
||
.ldot.maritime{background:#b388ff;box-shadow:0 0 6px rgba(179,136,255,0.4)}
|
||
.ldot.health{background:#69f0ae;box-shadow:0 0 6px rgba(105,240,174,0.4)}
|
||
.ldot.news{background:#81d4fa;box-shadow:0 0 6px rgba(129,212,250,0.4)}
|
||
.ldot.space{background:#e0b0ff;box-shadow:0 0 6px rgba(224,176,255,0.4)}
|
||
.layer-name{font-size:12px;font-weight:500}
|
||
.layer-sub{font-size:10px;color:var(--dim)}
|
||
.layer-count{font-family:var(--mono);font-size:13px;font-weight:600;color:var(--accent)}
|
||
.nuke-ok{padding:8px;border:1px solid rgba(100,240,200,0.15);background:rgba(100,240,200,0.04);font-family:var(--mono);font-size:10px;color:var(--accent);letter-spacing:0.08em;text-transform:uppercase;margin-bottom:8px}
|
||
.site-row{padding:6px 8px;border-bottom:1px solid rgba(255,255,255,0.04);font-size:11px;display:flex;justify-content:space-between}
|
||
.site-val{font-family:var(--mono);color:var(--accent);font-size:10px}
|
||
.econ-row{display:flex;justify-content:space-between;padding:5px 0;border-bottom:1px solid rgba(255,255,255,0.04);font-size:11px}
|
||
.econ-row .elabel{color:var(--dim)}
|
||
.econ-row .eval{font-family:var(--mono);font-weight:600}
|
||
|
||
/* CENTER: MAP */
|
||
.map-region-bar{display:flex;align-items:center;gap:8px;flex-wrap:wrap;padding:10px 12px;border:1px solid var(--border);background:var(--panel);backdrop-filter:blur(20px)}
|
||
.map-container{flex:1;min-height:560px;border:1px solid var(--border);background:radial-gradient(ellipse at center,rgba(4,12,20,1),rgba(2,4,8,1));position:relative;overflow:hidden}
|
||
#globeViz{width:100%;height:100%;cursor:grab}
|
||
#globeViz:active{cursor:grabbing}
|
||
#globeViz canvas{outline:none}
|
||
#flatMapSvg .land{fill:rgba(180,200,210,0.08);stroke:rgba(200,220,230,0.15);stroke-width:0.5}
|
||
#flatMapSvg .land:hover{fill:rgba(100,240,200,0.08)}
|
||
#flatMapSvg .graticule{fill:none;stroke:rgba(100,240,200,0.04);stroke-width:0.4}
|
||
#flatMapSvg .border{fill:none;stroke:rgba(200,220,230,0.08);stroke-width:0.3}
|
||
@keyframes pulse-conflict{0%,100%{opacity:0.5;stroke-width:1.5}50%{opacity:0.9;stroke-width:2.5}}
|
||
.conflict-ring{animation:pulse-conflict 2.5s ease-in-out infinite}
|
||
@keyframes dash-flow{to{stroke-dashoffset:-20}}
|
||
.corridor-flow{animation:dash-flow 2s linear infinite}
|
||
.map-legend{position:absolute;bottom:10px;left:12px;display:flex;gap:14px;font-family:var(--mono);font-size:10px;color:var(--dim);letter-spacing:0.06em;text-transform:uppercase;z-index:5;flex-wrap:wrap}
|
||
.leg-item{display:flex;align-items:center;gap:5px}
|
||
.leg-dot{width:8px;height:8px;border-radius:50%}
|
||
.map-hint{position:absolute;top:8px;right:12px;font-family:var(--mono);font-size:9px;color:var(--dim);z-index:5;opacity:0.6;letter-spacing:0.05em}
|
||
.map-hint-id{position:absolute;top:8px;right:12px}
|
||
.map-controls{position:absolute;top:8px;left:12px;z-index:6;display:flex;flex-direction:column;gap:4px}
|
||
.map-ctrl-btn{width:28px;height:28px;border:1px solid var(--border);background:rgba(0,0,0,0.6);color:var(--dim);font-size:16px;cursor:pointer;display:flex;align-items:center;justify-content:center;backdrop-filter:blur(8px);transition:all 0.2s;font-family:var(--mono)}
|
||
.map-ctrl-btn:hover{color:var(--accent);border-color:var(--border-bright);background:rgba(100,240,200,0.06)}
|
||
.map-ctrl-btn.map-toggle{font-size:14px}
|
||
.map-ctrl-btn.off{opacity:0.4}
|
||
/* Map popup */
|
||
.map-popup{position:absolute;z-index:20;width:280px;padding:12px;background:rgba(6,10,14,0.95);border:1px solid rgba(100,240,200,0.25);backdrop-filter:blur(12px);box-shadow:0 8px 32px rgba(0,0,0,0.5);pointer-events:auto;display:none}
|
||
.map-popup.show{display:block}
|
||
.map-popup .pp-head{font-family:var(--mono);font-size:10px;font-weight:600;color:var(--accent);letter-spacing:0.1em;text-transform:uppercase;margin-bottom:4px}
|
||
.map-popup .pp-text{font-size:11px;line-height:1.4;color:#c8d8d2}
|
||
.map-popup .pp-meta{font-family:var(--mono);font-size:10px;color:var(--dim);margin-top:6px}
|
||
.map-popup .pp-close{position:absolute;top:6px;right:8px;background:none;border:none;color:var(--dim);font-size:14px;cursor:pointer}
|
||
.glossary-overlay{position:fixed;inset:0;z-index:1200;background:rgba(2,6,10,0.72);backdrop-filter:blur(10px);opacity:0;pointer-events:none;transition:opacity 0.25s ease}
|
||
.glossary-overlay.show{opacity:1;pointer-events:auto}
|
||
.glossary-panel{position:absolute;top:18px;right:18px;width:min(420px,calc(100vw - 32px));max-height:calc(100vh - 36px);display:flex;flex-direction:column;border:1px solid rgba(68,204,255,0.22);background:rgba(5,12,19,0.96);box-shadow:0 18px 48px rgba(0,0,0,0.45)}
|
||
.glossary-head{display:flex;align-items:flex-start;justify-content:space-between;gap:12px;padding:14px 16px 10px;border-bottom:1px solid rgba(255,255,255,0.06)}
|
||
.glossary-kicker{font-family:var(--mono);font-size:10px;letter-spacing:0.12em;text-transform:uppercase;color:var(--accent2);margin-bottom:5px}
|
||
.glossary-title{font-size:18px;font-weight:600;line-height:1.15}
|
||
.glossary-sub{font-size:11px;line-height:1.45;color:var(--dim);margin-top:6px}
|
||
.glossary-close{border:1px solid var(--border);background:rgba(255,255,255,0.03);color:var(--dim);width:30px;height:30px;font-size:18px;cursor:pointer;flex-shrink:0}
|
||
.glossary-body{overflow:auto;padding:12px 16px 16px;display:flex;flex-direction:column;gap:10px}
|
||
.glossary-card{padding:12px;border:1px solid rgba(255,255,255,0.05);background:rgba(255,255,255,0.02)}
|
||
.glossary-term{display:flex;align-items:center;justify-content:space-between;gap:10px;margin-bottom:7px}
|
||
.glossary-term strong{font-family:var(--mono);font-size:11px;letter-spacing:0.08em;text-transform:uppercase}
|
||
.glossary-tag{font-family:var(--mono);font-size:9px;letter-spacing:0.08em;text-transform:uppercase;padding:2px 6px;border:1px solid rgba(100,240,200,0.18);color:var(--accent);background:rgba(100,240,200,0.05)}
|
||
.glossary-line{font-size:11px;line-height:1.5;color:#c8d8d2}
|
||
.glossary-line + .glossary-line{margin-top:5px}
|
||
.glossary-label{font-family:var(--mono);font-size:9px;letter-spacing:0.08em;text-transform:uppercase;color:var(--dim);margin-right:6px}
|
||
.glossary-foot{padding:10px 16px 14px;border-top:1px solid rgba(255,255,255,0.06);font-family:var(--mono);font-size:9px;line-height:1.5;color:rgba(106,138,130,0.8)}
|
||
.map-loading{position:absolute;inset:0;z-index:12;display:none;align-items:center;justify-content:center;background:linear-gradient(180deg,rgba(2,6,10,0.78),rgba(2,6,10,0.9));backdrop-filter:blur(10px)}
|
||
.map-loading.show{display:flex}
|
||
.map-loading-card{display:flex;flex-direction:column;align-items:center;gap:10px;padding:16px 18px;border:1px solid rgba(68,204,255,0.18);background:rgba(6,14,22,0.88);box-shadow:0 12px 32px rgba(0,0,0,0.35)}
|
||
.map-loading-ring{width:28px;height:28px;border:2px solid rgba(68,204,255,0.16);border-top-color:var(--accent2);border-radius:50%;animation:spin 1s linear infinite}
|
||
.map-loading-text{font-family:var(--mono);font-size:10px;letter-spacing:0.12em;text-transform:uppercase;color:var(--accent2)}
|
||
/* News label on map */
|
||
.news-icon{fill:rgba(129,212,250,0.8);filter:drop-shadow(0 0 3px rgba(129,212,250,0.4));transition:fill .2s}
|
||
.news-icon:hover{fill:rgba(129,212,250,1)}
|
||
|
||
/* LOWER GRID — flex layout for responsive panel sizing */
|
||
.lower{display:flex;flex-wrap:wrap;gap:10px;margin-top:10px;align-items:flex-start}
|
||
.lower .g-panel{min-width:0;box-sizing:border-box}
|
||
.lower .lp-ticker{flex:1.2 1 240px;max-width:380px}
|
||
.right-delta .delta-list{max-height:200px}
|
||
.lower .lp-macro{flex:2.5 1 360px}
|
||
.lower .lp-ideas{flex:1.5 1 300px}
|
||
.lower-wide{width:100%}
|
||
.metrics-row{display:grid;grid-template-columns:repeat(auto-fill,minmax(130px,1fr));gap:6px}
|
||
.mc{padding:10px;border:1px solid rgba(255,255,255,0.05);background:rgba(255,255,255,0.02)}
|
||
.mc .ml{font-family:var(--mono);font-size:9px;text-transform:uppercase;letter-spacing:0.08em;color:var(--dim)}
|
||
.mc .mv{font-family:var(--mono);font-size:18px;font-weight:600;margin-top:6px;display:block}
|
||
.mc .ms{font-family:var(--mono);font-size:9px;color:var(--dim);margin-top:4px;display:block}
|
||
.mc .mbar{height:3px;margin-top:8px;background:rgba(255,255,255,0.06);border-radius:1px;overflow:hidden}
|
||
.mc .mbar span{display:block;height:100%;border-radius:1px;background:linear-gradient(90deg,rgba(68,204,255,0.4),var(--accent))}
|
||
.spark{display:flex;align-items:flex-end;gap:2px;height:24px;margin-top:6px}
|
||
.spark-bar{width:6px;background:linear-gradient(to top,rgba(100,240,200,0.3),var(--accent));border-radius:1px 1px 0 0;transition:height 0.3s}
|
||
.signal-row{padding:8px 10px;border-left:2px solid rgba(100,240,200,0.2);margin-bottom:4px;background:rgba(255,255,255,0.02)}
|
||
.signal-row strong{font-family:var(--mono);font-size:10px;text-transform:uppercase;letter-spacing:0.05em;display:block;margin-bottom:2px}
|
||
.signal-row p{font-size:11px;line-height:1.35;color:#c8d8d2}
|
||
.src-grid{display:grid;grid-template-columns:repeat(4,1fr);gap:4px}
|
||
.src-item{display:flex;align-items:center;gap:6px;padding:6px 8px;border:1px solid rgba(255,255,255,0.04);font-size:11px}
|
||
.sd{width:5px;height:5px;border-radius:50%}
|
||
.sd.ok{background:var(--accent);box-shadow:0 0 4px rgba(100,240,200,0.4)}
|
||
.sd.err{background:var(--danger);box-shadow:0 0 4px rgba(255,95,99,0.4)}
|
||
|
||
/* RIGHT: OSINT FEED */
|
||
.feed{flex:1;overflow-y:auto;max-height:calc(100vh - 160px);padding-right:3px}
|
||
.feed::-webkit-scrollbar{width:3px}
|
||
.feed::-webkit-scrollbar-thumb{background:rgba(100,240,200,0.2);border-radius:2px}
|
||
.ic{padding:10px;border:1px solid rgba(255,255,255,0.05);border-left:2px solid rgba(68,204,255,0.4);background:rgba(255,255,255,0.02);margin-bottom:6px}
|
||
.ic.urgent{border-left-color:var(--danger)}
|
||
.ic .ic-ch{font-family:var(--mono);font-size:9px;font-weight:600;color:var(--accent);letter-spacing:0.08em;text-transform:uppercase;display:flex;justify-content:space-between;margin-bottom:3px}
|
||
.ic .ic-v{color:var(--warn);font-weight:700;padding:1px 5px;border:1px solid rgba(255,184,76,0.3);background:rgba(255,184,76,0.08)}
|
||
.ic .ic-t{font-size:11px;line-height:1.4;color:#c8d8d2}
|
||
.ic .ic-m{font-family:var(--mono);font-size:9px;color:var(--dim);margin-top:4px;display:flex;gap:6px}
|
||
.ic .ic-fl{color:var(--accent2)}
|
||
.sm{display:flex;align-items:center;justify-content:space-between;padding:8px;border:1px solid rgba(255,255,255,0.04);margin-bottom:4px}
|
||
.sm .sml{font-family:var(--mono);font-size:10px;text-transform:uppercase;letter-spacing:0.05em}
|
||
.sm .smb{flex:1;height:4px;margin:0 10px;background:rgba(255,255,255,0.06);border-radius:2px;overflow:hidden}
|
||
.sm .smb span{display:block;height:100%;border-radius:2px;background:linear-gradient(90deg,rgba(68,204,255,0.3),var(--accent))}
|
||
.sm .smv{font-family:var(--mono);font-size:14px;font-weight:700;color:var(--accent);min-width:32px;text-align:center;padding:3px 6px;border:1px solid var(--border-bright);background:rgba(100,240,200,0.06)}
|
||
|
||
/* LEVERAGEABLE IDEAS */
|
||
.idea-card{padding:10px;border:1px solid rgba(100,240,200,0.1);background:rgba(100,240,200,0.03);margin-bottom:6px}
|
||
.idea-card .idea-type{font-family:var(--mono);font-size:9px;letter-spacing:0.1em;text-transform:uppercase;padding:2px 6px;border:1px solid;display:inline-block;margin-bottom:4px}
|
||
.idea-card .idea-type.long{color:var(--accent);border-color:rgba(100,240,200,0.3)}
|
||
.idea-card .idea-type.short{color:var(--danger);border-color:rgba(255,95,99,0.3)}
|
||
.idea-card .idea-type.hedge{color:var(--warn);border-color:rgba(255,184,76,0.3)}
|
||
.idea-card .idea-type.watch{color:var(--accent2);border-color:rgba(68,204,255,0.3)}
|
||
.idea-card .idea-type.avoid{color:#b0bec5;border-color:rgba(176,190,197,0.3)}
|
||
.idea-card .idea-title{font-size:12px;font-weight:600;margin-bottom:3px}
|
||
.idea-card .idea-text{font-size:10px;line-height:1.4;color:var(--dim)}
|
||
.idea-card .idea-conf{font-family:var(--mono);font-size:9px;color:var(--dim);margin-top:4px}
|
||
.disclosure{font-family:var(--mono);font-size:8px;color:rgba(106,138,130,0.6);line-height:1.4;padding:6px;border-top:1px solid rgba(255,255,255,0.04);margin-top:6px}
|
||
|
||
/* NEWS TICKER */
|
||
.ticker-wrap{overflow:hidden;max-height:320px;position:relative;border:1px solid rgba(100,240,200,0.08);background:rgba(0,0,0,0.15)}
|
||
.ticker-wrap::before,.ticker-wrap::after{content:'';position:absolute;left:0;right:0;height:30px;z-index:2;pointer-events:none}
|
||
.ticker-wrap::before{top:0;background:linear-gradient(to bottom,rgba(14,17,22,0.95),transparent)}
|
||
.ticker-wrap::after{bottom:0;background:linear-gradient(to top,rgba(14,17,22,0.95),transparent)}
|
||
.ticker-track{display:flex;flex-direction:column;animation:tickerScroll var(--ticker-duration,30s) linear infinite;will-change:transform;contain:layout style}
|
||
.ticker-wrap:hover .ticker-track{animation-play-state:paused}
|
||
@keyframes tickerScroll{0%{transform:translateY(0)}100%{transform:translateY(-50%)}}
|
||
.tk-card{padding:8px 10px;border-bottom:1px solid rgba(255,255,255,0.03);cursor:default;transition:background 0.2s}
|
||
.tk-card.clickable{cursor:pointer}
|
||
.tk-card .tk-link{display:none;margin-left:auto;font-size:10px;color:var(--dim);transition:color 0.2s}
|
||
.tk-card.clickable .tk-link{display:inline-flex;align-items:center}
|
||
.tk-card.clickable:hover .tk-link{color:var(--accent)}
|
||
.tk-card:hover{background:rgba(100,240,200,0.04)}
|
||
.tk-card.urgent{border-left:2px solid var(--danger)}
|
||
.tk-src{font-family:var(--mono);font-size:8px;letter-spacing:0.08em;text-transform:uppercase;padding:1px 5px;border:1px solid;display:inline-block;margin-right:4px}
|
||
.tk-src.bbc{color:#64b5f6;border-color:rgba(100,181,246,0.3)}
|
||
.tk-src.nyt{color:#b0bec5;border-color:rgba(176,190,197,0.3)}
|
||
.tk-src.alj{color:#ffd54f;border-color:rgba(255,213,79,0.3)}
|
||
.tk-src.gdelt{color:#4dd0e1;border-color:rgba(77,208,225,0.3)}
|
||
.tk-src.tg{color:#ffb74d;border-color:rgba(255,183,77,0.3)}
|
||
.tk-src.dw{color:#ef9a9a;border-color:rgba(239,154,154,0.3)}
|
||
.tk-src.eu{color:#ce93d8;border-color:rgba(206,147,216,0.3)}
|
||
.tk-src.af{color:#a5d6a7;border-color:rgba(165,214,167,0.3)}
|
||
.tk-src.sa{color:#ffab91;border-color:rgba(255,171,145,0.3)}
|
||
.tk-src.ind{color:#ffcc80;border-color:rgba(255,204,128,0.3)}
|
||
.tk-src.anz{color:#80cbc4;border-color:rgba(128,203,196,0.3)}
|
||
.tk-src.us{color:#90caf9;border-color:rgba(144,202,249,0.3)}
|
||
.tk-src.other{color:#b0bec5;border-color:rgba(176,190,197,0.2)}
|
||
.tk-head{font-size:11px;line-height:1.35;color:#c8d8d2;margin-top:3px}
|
||
.tk-time{font-family:var(--mono);font-size:8px;color:var(--dim);margin-top:2px}
|
||
|
||
/* DELTA BADGES */
|
||
.delta-badge{font-family:var(--mono);font-size:8px;letter-spacing:0.05em;padding:1px 4px;margin-left:4px;border-radius:2px;vertical-align:middle}
|
||
.delta-badge.up{color:#81c784;border:1px solid rgba(129,199,132,0.3);background:rgba(129,199,132,0.08)}
|
||
.delta-badge.down{color:#ef5350;border:1px solid rgba(239,83,80,0.3);background:rgba(239,83,80,0.08)}
|
||
.delta-badge.new{color:#4dd0e1;border:1px solid rgba(77,208,225,0.3);background:rgba(77,208,225,0.08);animation:pulse-new 2s ease infinite}
|
||
.delta-list{max-height:160px;overflow-y:auto;scrollbar-width:thin;scrollbar-color:rgba(0,229,255,0.2) transparent}
|
||
.delta-row{display:flex;align-items:center;gap:6px;padding:3px 0;font-family:var(--mono);font-size:10px;border-bottom:1px solid rgba(255,255,255,0.04)}
|
||
.delta-row.new{background:rgba(77,208,225,0.04)}
|
||
.delta-label{flex:1;color:var(--text);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||
.delta-val{color:var(--dim);font-size:9px;white-space:nowrap}
|
||
@keyframes pulse-new{0%,100%{opacity:0.7}50%{opacity:1}}
|
||
|
||
/* IDEAS SOURCE BADGE */
|
||
.ideas-src{font-family:var(--mono);font-size:8px;letter-spacing:0.08em;padding:2px 6px;border:1px solid;display:inline-block;margin-left:6px}
|
||
.ideas-src.llm{color:#ce93d8;border-color:rgba(206,147,216,0.4);background:rgba(206,147,216,0.08)}
|
||
.ideas-src.static{color:var(--dim);border-color:rgba(255,255,255,0.1);background:rgba(255,255,255,0.03)}
|
||
|
||
/* LOW PERFORMANCE MODE */
|
||
body.low-perf .bg-grid,body.low-perf .bg-radial,body.low-perf .scanline{display:none!important}
|
||
body.low-perf .topbar,body.low-perf .g-panel,body.low-perf .map-popup,body.low-perf .map-loading{backdrop-filter:none!important}
|
||
body.low-perf .logo-ring::before,body.low-perf .logo-ring::after,body.low-perf .regime-chip .blink,body.low-perf .conflict-ring,body.low-perf .corridor-flow{animation:none!important}
|
||
body.low-perf .ticker-wrap{overflow-y:auto;scrollbar-width:thin;scrollbar-color:rgba(100,240,200,0.2) transparent}
|
||
body.low-perf .ticker-track{animation:none!important;display:block!important}
|
||
body.low-perf .ticker-wrap::before,body.low-perf .ticker-wrap::after{display:none}
|
||
body.low-perf .ticker-wrap::-webkit-scrollbar{width:4px}
|
||
body.low-perf .ticker-wrap::-webkit-scrollbar-track{background:transparent}
|
||
body.low-perf .ticker-wrap::-webkit-scrollbar-thumb{background:rgba(100,240,200,0.2);border-radius:2px}
|
||
|
||
/* RESPONSIVE */
|
||
@media(max-width:1400px){.grid{grid-template-columns:240px 1fr 320px}.metrics-row{grid-template-columns:repeat(3,1fr)}.src-grid{grid-template-columns:repeat(3,1fr)}}
|
||
@media(max-width:1100px){
|
||
#main{padding:8px}
|
||
.topbar{padding:10px 12px}
|
||
.top-left,.top-center,.top-right{width:100%}
|
||
.top-center{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:6px}
|
||
.map-region-bar{display:none}
|
||
.top-right{gap:6px ; flex-wrap: wrap; justify-content: center;}
|
||
.region-btn,.meta-pill,.alert-badge,.guide-btn{font-size:10px}
|
||
.grid{display:flex;flex-direction:column}
|
||
#centerCol{order:1}
|
||
#rightRail{order:2}
|
||
#leftRail{order:3}
|
||
.map-container{min-height:420px}
|
||
.map-hint{font-size:8px;right:8px}
|
||
.map-legend{left:8px;right:8px;bottom:8px;gap:4px}
|
||
.leg-item{font-size:8px}
|
||
.lower .lp-ticker,.lower .lp-osint,.lower .lp-macro,.lower .lp-ideas{flex:1 1 100%;max-width:none}
|
||
.metrics-row{grid-template-columns:repeat(2,1fr)}
|
||
.src-grid{grid-template-columns:repeat(2,1fr)}
|
||
.glossary-panel{top:auto;right:0;left:0;bottom:0;width:100%;max-height:min(72vh,720px);border-left:none;border-right:none;border-bottom:none}
|
||
}
|
||
|
||
/* CONFLICT LAYER */
|
||
@keyframes pulse-conflict{0%,100%{opacity:0.5;stroke-width:1.5}50%{opacity:0.9;stroke-width:2.5}}
|
||
.conflict-ring{animation:pulse-conflict 2.5s ease-in-out infinite}
|
||
|
||
/* FLIGHT CORRIDORS */
|
||
.corridor-line{fill:none;opacity:0.6;pointer-events:none}
|
||
@keyframes flicker-ghost{0%,100%{opacity:0.15}30%{opacity:0.45}60%{opacity:0.1}85%{opacity:0.35}}
|
||
.ghost-marker{animation:flicker-ghost 2s ease-in-out infinite}
|
||
@keyframes dash-flow{to{stroke-dashoffset:-20}}
|
||
.corridor-flow{animation:dash-flow 2s linear infinite}
|
||
|
||
/* SPARKLINES */
|
||
.spark-svg{display:inline-block;width:52px;height:18px;vertical-align:middle;margin-left:4px}
|
||
.spark-line{fill:none;stroke-width:1.5;stroke-linecap:round}
|
||
.spark-good{stroke:var(--accent)}
|
||
.spark-bad{stroke:var(--danger)}
|
||
.spark-dot{r:2}
|
||
|
||
/* FLAT/GLOBE TOGGLE */
|
||
.proj-toggle{position:absolute;top:8px;left:48px;z-index:6;padding:5px 10px;border:1px solid var(--border);background:rgba(0,0,0,0.6);font-family:var(--mono);font-size:9px;cursor:pointer;color:var(--dim);letter-spacing:0.08em;text-transform:uppercase;transition:all 0.2s;backdrop-filter:blur(8px)}
|
||
.proj-toggle:hover{border-color:var(--accent);color:var(--accent)}
|
||
.proj-toggle.active{color:var(--bg);background:var(--accent);border-color:var(--accent)}
|
||
/* GLOBE.GL overrides */
|
||
#globeViz .scene-tooltip{font-family:var(--mono)!important;font-size:10px!important;background:rgba(6,10,14,0.9)!important;border:1px solid rgba(100,240,200,0.3)!important;color:var(--text)!important;padding:4px 8px!important;letter-spacing:0.05em}
|
||
|
||
/* IDEA HORIZON BADGE */
|
||
.idea-horizon{font-family:var(--mono);font-size:8px;letter-spacing:0.08em;text-transform:uppercase;padding:1px 5px;border:1px solid rgba(100,240,200,0.15);color:var(--dim);margin-left:6px}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div id="boot">
|
||
<div class="logo-ring"><span class="logo-text">CRUCIX</span></div>
|
||
<div id="bootLines"></div>
|
||
<div id="bootFinal">TERMINAL ACTIVE</div>
|
||
</div>
|
||
<div class="bg-radial" id="bgRadial"></div>
|
||
<div class="bg-grid" id="bgGrid"></div>
|
||
<div class="scanline" id="scanline"></div>
|
||
<div id="main">
|
||
<div class="topbar" id="topbar"></div>
|
||
<div class="grid">
|
||
<div class="col" id="leftRail"></div>
|
||
<div class="col" id="centerCol">
|
||
<div class="map-region-bar" id="mapRegionBar"></div>
|
||
<div class="map-container" id="mapContainer">
|
||
<div id="globeViz"></div>
|
||
<svg id="flatMapSvg" style="display:none;width:100%;height:100%;position:absolute;top:0;left:0;cursor:grab"></svg>
|
||
<div class="map-loading" id="mapLoading"><div class="map-loading-card"><div class="map-loading-ring"></div><div class="map-loading-text" id="mapLoadingText">Initializing 3D Globe</div></div></div>
|
||
<div class="map-legend" id="mapLegend"></div>
|
||
<div class="map-hint" id="mapHint">SCROLL TO ZOOM · DRAG TO PAN</div>
|
||
<div class="map-controls">
|
||
<button class="map-ctrl-btn" onclick="mapZoom(1.5)" title="Zoom in">+</button>
|
||
<button class="map-ctrl-btn" onclick="mapZoom(0.67)" title="Zoom out">−</button>
|
||
<button class="map-ctrl-btn map-toggle" id="flightToggle" onclick="toggleFlights()" title="Toggle flight routes">✈</button>
|
||
</div>
|
||
<button class="proj-toggle" id="projToggle" onclick="toggleMapMode()">GLOBE MODE</button>
|
||
<div class="map-popup" id="mapPopup"><button class="pp-close" onclick="closePopup()">×</button><div class="pp-head"></div><div class="pp-text"></div><div class="pp-meta"></div></div>
|
||
</div>
|
||
<div class="lower" id="lowerGrid"></div>
|
||
</div>
|
||
<div class="col" id="rightRail"></div>
|
||
</div>
|
||
</div>
|
||
<div class="glossary-overlay" id="glossaryOverlay" onclick="if(event.target===this) closeGlossary()">
|
||
<div class="glossary-panel">
|
||
<div class="glossary-head">
|
||
<div>
|
||
<div class="glossary-kicker">Signal Guide</div>
|
||
<div class="glossary-title">What the signals actually mean</div>
|
||
<div class="glossary-sub">Plain-English interpretation, why it matters, and what you should not infer from it.</div>
|
||
</div>
|
||
<button class="glossary-close" onclick="closeGlossary()" aria-label="Close signal guide">×</button>
|
||
</div>
|
||
<div class="glossary-body" id="glossaryBody"></div>
|
||
<div class="glossary-foot">Treat these as interpretation guides, not conclusions. Stronger judgments should come from corroboration across multiple layers, not from a single signal viewed in isolation.</div>
|
||
</div>
|
||
</div>
|
||
<script>
|
||
// === DATA ===
|
||
let D = {"meta":{"version":"2.0.0","timestamp":"2026-04-03T16:18:10.188Z","totalDurationMs":30079,"sourcesQueried":29,"sourcesOk":28,"sourcesFailed":1},"air":[{"region":"Middle East","total":134,"noCallsign":5,"highAlt":4,"top":[["Turkey",30],["United Arab Emirates",24],["Qatar",6],["United Kingdom",6],["Russian Federation",5]]},{"region":"Taiwan Strait","total":56,"noCallsign":5,"highAlt":4,"top":[["China",19],["Taiwan",14],["Republic of Korea",10],["Japan",5],["Luxembourg",2]]},{"region":"Ukraine Region","total":46,"noCallsign":1,"highAlt":5,"top":[["Turkey",8],["Romania",6],["Hungary",4],["Poland",4],["Germany",3]]},{"region":"Baltic Region","total":31,"noCallsign":0,"highAlt":0,"top":[["Russian Federation",7],["Poland",5],["Latvia",5],["Finland",3],["Sweden",2]]},{"region":"South China Sea","total":115,"noCallsign":5,"highAlt":3,"top":[["China",56],["Republic of Korea",12],["Philippines",12],["Malaysia",9],["Japan",6]]},{"region":"Korean Peninsula","total":8,"noCallsign":0,"highAlt":3,"top":[["United States",4],["China",1],["Taiwan",1],["Japan",1],["Republic of Korea",1]]},{"region":"Caribbean","total":891,"noCallsign":19,"highAlt":38,"top":[["United States",825],["Canada",32],["Mexico",16],["Panama",5],["Colombia",3]]},{"region":"Gulf of Guinea","total":0,"noCallsign":0,"highAlt":0,"top":[]},{"region":"Cape Route","total":11,"noCallsign":1,"highAlt":0,"top":[["South Africa",9],["Germany",2]]},{"region":"Horn of Africa","total":0,"noCallsign":0,"highAlt":0,"top":[]}],"thermal":[{"region":"Middle East","det":1271,"night":689,"hc":51,"fires":[{"lat":23.28064,"lon":54.26164,"frp":180.3},{"lat":23.84427,"lon":53.65954,"frp":88.63},{"lat":23.27888,"lon":54.26292,"frp":65.43},{"lat":23.83226,"lon":53.62622,"frp":64.25},{"lat":23.2719,"lon":54.26493,"frp":57.8},{"lat":23.2758,"lon":54.26994,"frp":56.46},{"lat":30.92245,"lon":48.85786,"frp":53.54},{"lat":13.19805,"lon":32.94167,"frp":49.55}]},{"region":"Ukraine","det":245,"night":59,"hc":7,"fires":[{"lat":51.08384,"lon":34.85754,"frp":98.78},{"lat":50.17256,"lon":36.91306,"frp":57.85},{"lat":51.08468,"lon":34.86295,"frp":38.7},{"lat":48.91101,"lon":30.10451,"frp":31.15},{"lat":52.6921,"lon":30.74334,"frp":29.37},{"lat":52.07915,"lon":25.69872,"frp":27.33},{"lat":51.08794,"lon":34.86167,"frp":23.45},{"lat":52.31651,"lon":32.55792,"frp":20.73}]},{"region":"Iran","det":740,"night":452,"hc":17,"fires":[{"lat":30.92245,"lon":48.85786,"frp":53.54},{"lat":30.75065,"lon":48.28501,"frp":40.64},{"lat":30.75481,"lon":48.28442,"frp":40.64},{"lat":30.71966,"lon":49.82515,"frp":37.38},{"lat":30.74971,"lon":48.27633,"frp":35.02},{"lat":30.75386,"lon":48.27575,"frp":35.02},{"lat":31.2724,"lon":48.12039,"frp":33.11},{"lat":27.50295,"lon":52.63748,"frp":32.89}]},{"region":"Sudan / Horn of Africa","det":2147,"night":96,"hc":92,"fires":[{"lat":9.36072,"lon":36.87687,"frp":112.91},{"lat":2.30776,"lon":22.57457,"frp":96.78},{"lat":7.13573,"lon":28.22997,"frp":84.94},{"lat":7.12821,"lon":28.23156,"frp":80.97},{"lat":2.97582,"lon":21.70991,"frp":67.59},{"lat":3.96735,"lon":22.66164,"frp":64.27},{"lat":11.37062,"lon":35.81662,"frp":63.07},{"lat":3.6656,"lon":32.33914,"frp":61.45}]},{"region":"Myanmar","det":27005,"night":5285,"hc":384,"fires":[{"lat":17.69662,"lon":97.04257,"frp":541.71},{"lat":18.07759,"lon":97.47091,"frp":493.31},{"lat":18.03867,"lon":97.66878,"frp":467.74},{"lat":21.99852,"lon":92.34525,"frp":435.64},{"lat":18.44615,"lon":96.3231,"frp":404.83},{"lat":18.44998,"lon":96.32241,"frp":404.83},{"lat":18.4765,"lon":96.27098,"frp":373.22},{"lat":18.15929,"lon":97.44369,"frp":371.63}]},{"region":"South Asia","det":19132,"night":3268,"hc":331,"fires":[{"lat":17.69662,"lon":97.04257,"frp":541.71},{"lat":18.07759,"lon":97.47091,"frp":493.31},{"lat":18.03867,"lon":97.66878,"frp":467.74},{"lat":21.99852,"lon":92.34525,"frp":435.64},{"lat":18.44615,"lon":96.3231,"frp":404.83},{"lat":18.44998,"lon":96.32241,"frp":404.83},{"lat":18.4765,"lon":96.27098,"frp":373.22},{"lat":18.15929,"lon":97.44369,"frp":371.63}]}],"tSignals":["HIGH INTENSITY FIRES in Middle East: 15 detections >10MW FRP","ELEVATED NIGHT ACTIVITY in Middle East: 689 night detections (potential strikes/combat)","HIGH INTENSITY FIRES in Ukraine: 15 detections >10MW FRP","ELEVATED NIGHT ACTIVITY in Ukraine: 59 night detections (potential strikes/combat)","HIGH INTENSITY FIRES in Iran: 15 detections >10MW FRP","ELEVATED NIGHT ACTIVITY in Iran: 452 night detections (potential strikes/combat)","HIGH INTENSITY FIRES in Sudan / Horn of Africa: 15 detections >10MW FRP","ELEVATED NIGHT ACTIVITY in Sudan / Horn of Africa: 96 night detections (potential strikes/combat)","HIGH INTENSITY FIRES in Myanmar: 15 detections >10MW FRP","ELEVATED NIGHT ACTIVITY in Myanmar: 5285 night detections (potential strikes/combat)","HIGH INTENSITY FIRES in South Asia: 15 detections >10MW FRP","ELEVATED NIGHT ACTIVITY in South Asia: 3268 night detections (potential strikes/combat)"],"chokepoints":[{"label":"Strait of Hormuz","note":"20% of world oil","lat":26.5,"lon":56.5},{"label":"Suez Canal","note":"12% of world trade","lat":30.5,"lon":32.3},{"label":"Strait of Gibraltar","note":"Gateway to Mediterranean, ~10-20% global trade influence","lat":36,"lon":-5.7},{"label":"Strait of Malacca","note":"25% of world trade","lat":2.5,"lon":101.5},{"label":"Bab el-Mandeb","note":"Red Sea gateway","lat":12.6,"lon":43.3},{"label":"Taiwan Strait","note":"88% of largest container ships","lat":24,"lon":119},{"label":"Bosphorus","note":"Black Sea access","lat":41.1,"lon":29.1},{"label":"Panama Canal","note":"5% of world trade","lat":9.1,"lon":-79.7},{"label":"Cape of Good Hope","note":"Suez alternative","lat":-34.4,"lon":18.5}],"nuke":[{"site":"Zaporizhzhia NPP (Ukraine)","anom":false,"cpm":33.92,"n":25},{"site":"Chernobyl Exclusion Zone","anom":false,"cpm":33.28,"n":25},{"site":"Bushehr NPP (Iran)","anom":false,"cpm":null,"n":0},{"site":"Yongbyon (North Korea)","anom":false,"cpm":null,"n":0},{"site":"Fukushima Daiichi","anom":false,"cpm":69.54,"n":25},{"site":"Dimona (Israel)","anom":false,"cpm":29.52,"n":25}],"nukeSignals":["All monitored nuclear sites within normal radiation levels"],"airMeta":{"fallback":false,"liveTotal":1292,"timestamp":"2026-04-03T16:17:41.390Z","source":"OpenSky"},"sdr":{"total":892,"online":892,"zones":[{"region":"Middle East","count":3,"receivers":[{"name":"0-30 MHz SDR | Cyprus","lat":34.76,"lon":32.53},{"name":"0-30 MHz SDR | Doha, Qatar","lat":25.2854,"lon":51.531},{"name":"0-30 MHz SDR | Baghdad, IRAQ","lat":33.3355,"lon":44.387736}]},{"region":"Ukraine / Eastern Europe","count":5,"receivers":[{"name":"The Real VLF-HF SDR Receiver 8KHz-30MHz | 16m LongWire@8m Height | Ground | BleedResistor | Attentua","lat":44.53,"lon":26.23},{"name":"0-30 MHz -YO8SGV- KN37EX- KiwiSDR | Dorohoi, Romania","lat":47.96881,"lon":26.379005},{"name":"The Real VLF-HF SDR Receiver 8KHz-30MHz | 16m LongWire@8m Height | Ground | BleedResistor | Attentua","lat":44.53,"lon":26.23},{"name":"0-30 MHz SDR | SLANIC PH","lat":45.24,"lon":25.94},{"name":"The Real VLF-HF SDR Receiver 8KHz-30MHz | 16m LongWire@8m Height | Ground | BleedResistor | Attentua","lat":44.53,"lon":26.23}]},{"region":"Taiwan Strait","count":12,"receivers":[{"name":"0-30 MHz SDR ,BV3UN 桃園龜山 | Taoyuan, Taiwan","lat":25.005928,"lon":121.343095},{"name":"0-30 MHz SDR ,BV3UN 桃園龜山 | Taoyuan, Taiwan","lat":25.005928,"lon":121.343095},{"name":"0-30 MHz SDR, BM2KVV Taiwan #1","lat":25.106238,"lon":121.816775},{"name":"0-30 MHz SDR, BM2KVV Taiwan #2","lat":25.11,"lon":121},{"name":"0-30 MHz SDR, BM2KVV Taiwan #3","lat":25.106,"lon":121.82}]},{"region":"Baltic Region","count":0,"receivers":[]},{"region":"South China Sea","count":6,"receivers":[{"name":"0-30 MHz SDR, VR2BG | Hong Kong","lat":22.35,"lon":114.13},{"name":"0-30 MHz SDR,BV7AU Pingtung taiwan","lat":22.79,"lon":120.58},{"name":"0-30 MHz SDR,Pingtung taiwan -3","lat":22.7943,"lon":120.5373},{"name":"0-30 MHz SDR,Pingtung taiwan-2(BV7AU) 屏東 臺灣","lat":22.791,"lon":120.581},{"name":"[Feline] 0-30MHz SDR | Waterfall Bay, Hong Kong","lat":22.25,"lon":114.135}]},{"region":"Korean Peninsula","count":8,"receivers":[{"name":"0-30 MHz KIWI SDR( 4ch ), HL5NTR, Daegu_City Republic of Korea. Loop-type Antenna.","lat":35.888778,"lon":128.574964},{"name":"0-30 MHz KIWI SDR( 8ch ), HL5NTR, Daegu_City Republic of Korea. 85M Loop-type Antenna.","lat":35.888778,"lon":128.574964},{"name":"0-30 MHz KIWI SDR( 8ch ), HL5NTR, Daegu_City Republic of Korea. 85M Loop-type Antenna.","lat":35.888778,"lon":128.574964},{"name":"0-30 MHz KIWI SDR( 8ch ), HL5NTR, Daegu_City Republic of Korea. 85M Loop-type Antenna.","lat":35.888778,"lon":128.574964},{"name":"0-30 MHz KIWI SDR( 4ch ), HL5NTR, Daegu_City Republic of Korea. Loop-type Antenna.","lat":35.888778,"lon":128.574964}]},{"region":"Iran","count":2,"receivers":[{"name":"0-30 MHz SDR | Doha, Qatar","lat":25.2854,"lon":51.531},{"name":"0-30 MHz SDR | Baghdad, IRAQ","lat":33.3355,"lon":44.387736}]},{"region":"Sahel / West Africa","count":0,"receivers":[]}]},"tg":{"posts":176,"urgent":[{"channel":"intelslava","text":"🇮🇷🇮🇱The strike of Iranian ballistic missiles on Petah Tikva precisely hit a drone production plant belonging to the Israeli company AeroSol.","views":33300,"date":"2026-04-03T10:13:58+00:00","urgentFlags":["missile","strike","drone"]},{"channel":"intelslava","text":"🇺🇸🇮🇷Trump announced new strikes on Iran. He confirmed that they attacked several bridges, and the next targets will be the energy sector.\n\nAll of this is in the event that Iran does not agree to a","views":33900,"date":"2026-04-03T10:14:47+00:00","urgentFlags":["confirmed","strike"]},{"channel":"intelslava","text":"🇺🇸🇮🇷The Iranians found the ejection seat of one of the ejected pilots of the American F-15E, but not the pilot himself.\n\nEither he has already been picked up by rescuers, or he is still hiding som","views":28100,"date":"2026-04-03T12:20:12+00:00","urgentFlags":["strike","advance"]},{"channel":"intelslava","text":"🇲🇬⚡️The authorities of Madagascar have prevented an attempt at a coup d'état and an assassination attempt on the president. Charges have been brought against 13 people, according to prosecutor Narin","views":28300,"date":"2026-04-03T12:05:57+00:00","urgentFlags":["coup","assassination"]},{"channel":"intelslava","text":"🇺🇸🇮🇷F-35 fighter jets and MQ-9 Reaper drones joined the operation to rescue the F-15E pilots who were shot down over Iran.","views":28000,"date":"2026-04-03T12:21:01+00:00","urgentFlags":["drone"]},{"channel":"intelslava","text":"🇮🇷🇦🇪Abu Dhabi suspended the operation of its largest gas processing complex \"after the fall of debris from an Iranian missile\" - Bloomberg.","views":27400,"date":"2026-04-03T12:03:54+00:00","urgentFlags":["missile"]},{"channel":"intelslava","text":"🇮🇷🇺🇸One \"Buk\" missile or its analogue could easily take out all three of them at once, not to mention more powerful air defense systems.\n\nHowever, it seems that the Americans are taking advantage ","views":24400,"date":"2026-04-03T13:33:34+00:00","urgentFlags":["missile"]},{"channel":"wartranslated","text":"Ukraine says air defenses have neutralized 26 missiles and 515 drones after a massive attack. In total, 579 aerial threats were recorded, including 10 Iskander-M ballistic missiles, 25 Kh-101 cruise m","views":890,"date":"2026-04-03T11:22:55+00:00","urgentFlags":["missile","drone"]},{"channel":"wartranslated","text":"Ukraine says air defenses have neutralized 26 missiles and 515 drones after a massive attack. In total, 579 aerial threats were recorded, including 10 Iskander-M ballistic missiles, 25 Kh-101 cruise m","views":741,"date":"2026-04-03T13:09:55+00:00","urgentFlags":["missile","drone"]},{"channel":"wartranslated","text":"RFI says Ukraine has bases in Libya to counter Russia, citing sources. It claims 200+ Ukrainian officers and experts are there with Tripoli’s approval and are training Libyan troops on drones. RFI als","views":668,"date":"2026-04-03T14:15:11+00:00","urgentFlags":["strike","drone"]},{"channel":"CIG_telegram","text":"🇺🇸⚔️🇮🇷 Additional footage shows a U.S. MQ‑9 Reaper drone and an A‑10C Thunderbolt II attack aircraft providing support for a likely C-SAR operation in Iran.\n\n🔗 Egypt's Intel Observer (@EGYOSINT)","views":8140.000000000001,"date":"2026-04-03T12:11:18+00:00","urgentFlags":["drone"]},{"channel":"CIG_telegram","text":"🇺🇸⚔️🇮🇷 An F-35 II Lightning fighter jet and an MQ-9 Reaper were spotted flying over Iran supporting ongoing search & rescue efforts.\n\nIranian channels reports a mass mobilization among not just th","views":7730,"date":"2026-04-03T11:52:28+00:00","urgentFlags":["mobilization"]},{"channel":"wartranslated","text":"Zelensky says no single country can lift the Strait of Hormuz blockade alone and only joint action can work. He points to Ukraine’s Black Sea grain corridor as a model and says Kyiv is ready to help w","views":662,"date":"2026-04-03T13:56:58+00:00","urgentFlags":["intercept","blockade"]},{"channel":"CIG_telegram","text":"—❗️🇺🇸/🇮🇷 BREAKING: Axios now confirms that an American F-15E fighter jet was shot down in Iran, and that rescue efforts are ‘ongoing’\n\n@Middle_East_Spectator","views":5950,"date":"2026-04-03T13:04:19+00:00","urgentFlags":["breaking"]},{"channel":"ukraine_frontline","text":"‼️🔥⚠️ Heavy artilery bombardment over Kharkov, Ukraine early morning 02.28.22.\nt.me/ukraine_frontline","views":2890,"date":"2022-02-28T10:03:27+00:00","urgentFlags":["bombardment"]}],"topPosts":[{"channel":"intelslava","text":"🇮🇷🇮🇱The strike of Iranian ballistic missiles on Petah Tikva precisely hit a drone production plant belonging to the Israeli company AeroSol.","views":33300,"date":"2026-04-03T10:13:58+00:00","urgentFlags":[]},{"channel":"intelslava","text":"🇺🇸🇮🇷Trump announced new strikes on Iran. He confirmed that they attacked several bridges, and the next targets will be the energy sector.\n\nAll of this is in the event that Iran does not agree to a","views":33900,"date":"2026-04-03T10:14:47+00:00","urgentFlags":[]}]},"who":[],"fred":[{"id":"DFF","label":"Fed Funds Rate","value":3.64,"date":"2026-04-01","recent":[3.64,3.64,3.64,3.64,3.64]},{"id":"DGS2","label":"2-Year Treasury Yield","value":3.81,"date":"2026-04-01","recent":[3.81,3.79,3.82,3.88,3.96]},{"id":"DGS10","label":"10-Year Treasury Yield","value":4.33,"date":"2026-04-01","recent":[4.33,4.3,4.35,4.44,4.42]},{"id":"DGS30","label":"30-Year Treasury Yield","value":4.91,"date":"2026-04-01","recent":[4.91,4.88,4.91,4.98,4.93]},{"id":"T10Y2Y","label":"10Y-2Y Spread (Yield Curve)","value":0.52,"date":"2026-04-02","recent":[0.52,0.52,0.51,0.53,0.56]},{"id":"T10Y3M","label":"10Y-3M Spread","value":0.61,"date":"2026-04-02","recent":[0.61,0.63,0.6,0.64,0.71]},{"id":"CPIAUCSL","label":"CPI All Items","value":327.46,"date":"2026-02-01","recent":[327.46,326.588]},{"id":"CPILFESL","label":"Core CPI (ex Food & Energy)","value":333.512,"date":"2026-02-01","recent":[333.512,332.793]},{"id":"PCEPI","label":"PCE Price Index","value":128.969,"date":"2026-01-01","recent":[128.969]},{"id":"MICH","label":"Michigan Inflation Expectations","value":3.4,"date":"2026-02-01","recent":[3.4,4]},{"id":"UNRATE","label":"Unemployment Rate","value":4.3,"date":"2026-03-01","recent":[4.3,4.4,4.3]},{"id":"PAYEMS","label":"Nonfarm Payrolls","value":158637,"date":"2026-03-01","recent":[158637,158459,158592]},{"id":"ICSA","label":"Initial Jobless Claims","value":202000,"date":"2026-03-28","recent":[202000,211000,205000,213000,214000]},{"id":"M2SL","label":"M2 Money Supply","value":22667.3,"date":"2026-02-01","recent":[22667.3,22469.1]},{"id":"WALCL","label":"Fed Balance Sheet Total Assets","value":6675344,"date":"2026-04-01","recent":[6675344,6657161,6655939,6646344,6628894]},{"id":"VIXCLS","label":"VIX (Fear Index)","value":24.54,"date":"2026-04-01","recent":[24.54,25.25,30.61,31.05,27.44]},{"id":"BAMLH0A0HYM2","label":"High Yield Spread (Credit Stress)","value":3.16,"date":"2026-04-01","recent":[3.16,3.28,3.46,3.42,3.21]},{"id":"DCOILWTICO","label":"WTI Crude Oil","value":104.69,"date":"2026-03-30","recent":[104.69,101.26,96.18,91.51,93.18]},{"id":"MORTGAGE30US","label":"30-Year Mortgage Rate","value":6.46,"date":"2026-04-02","recent":[6.46,6.38,6.22,6.11,6]},{"id":"DTWEXBGS","label":"USD Trade Weighted Index","value":120.8851,"date":"2026-03-27","recent":[120.8851,120.389,120.1282,120.1295,119.9371]}],"energy":{"wti":112.06,"brent":109.05,"natgas":2.81,"crudeStocks":461636,"wtiRecent":[102.88,101.38,100.12,111.54],"signals":["WTI crude above $100 at $104.69/bbl","Brent-WTI spread wide at $17.19 — supply/logistics divergence","Large crude inventory build: +5.5M barrels"]},"bls":[{"id":"CUUR0000SA0","label":"CPI-U All Items","value":326.785,"period":"2026-M02","date":"2026-02","momChange":1.533,"momChangePct":0.4713},{"id":"CUUR0000SA0L1E","label":"CPI-U Core (ex Food & Energy)","value":333.242,"period":"2026-M02","date":"2026-02","momChange":1.292,"momChangePct":0.3892},{"id":"LNS14000000","label":"Unemployment Rate","value":4.3,"period":"2026-M03","date":"2026-03","momChange":-0.1,"momChangePct":-2.2727},{"id":"CES0000000001","label":"Nonfarm Payrolls (thousands)","value":158637,"period":"2026-M03","date":"2026-03","momChange":178,"momChangePct":0.1123},{"id":"WPUFD49104","label":"PPI Final Demand","value":152.168,"period":"2026-M02","date":"2026-02","momChange":0.885,"momChangePct":0.585}],"treasury":{"totalDebt":"39016191534959.54","signals":["National debt at $39.02T"]},"gscpi":{"value":0.49,"date":"2026-02","interpretation":"above average"},"defense":[{"recipient":"LAWRENCE LIVERMORE NATIONAL SECURITY, LL","amount":40777168345.14,"desc":"TAS::89 0240::TAS THIS PERFORMANCE-BASED MANAGEMENT CONTRACT (PBMC) IS FOR THE M"},{"recipient":"FLUOR MARINE PROPULSION, LLC","amount":14773195999.25,"desc":"MANAGEMENT AND OPERATION OF THE NAVAL NUCLEAR LABORATORY AND NAVAL NUCLEAR PROPU"},{"recipient":"THE BOEING COMPANY","amount":10462406426,"desc":"PROVIDE DEVELOPMENTAL HARDWARE AND TEST ARTICLES, AND MANUFACTURE AND ASSEMBLE A"},{"recipient":"NORTHROP GRUMMAN SYSTEMS CORPORATION","amount":4420784857,"desc":"FIRST DDT AND E, ARES I-X, AND FLIGHT TESTS. FIRST STAGE WILL BE A FIVE SEGMENT"},{"recipient":"THE BOEING COMPANY","amount":2382243189,"desc":"SPACE LAUNCH SYSTEM (SLS) STAGES PRODUCTION AND EVOLUTION CONTRACT (SPEC)"}],"noaa":{"totalAlerts":0,"alerts":[]},"epa":{"totalReadings":0,"stations":[]},"acled":{"totalEvents":0,"totalFatalities":0,"byRegion":{},"byType":{},"deadliestEvents":[]},"gdelt":{"totalArticles":0,"conflicts":0,"economy":0,"health":0,"crisis":0,"topTitles":[],"geoPoints":[]},"space":{"totalNewObjects":343,"militarySats":22,"militaryByCountry":{"UNK":22},"constellations":{"starlink":10119,"oneweb":651},"iss":{"name":"ISS (ZARYA)","noradId":25544,"inclination":51.6327,"epoch":"2026-04-03T02:25:58.213344"},"issPosition":{"lat":14.74,"lon":60.19,"name":"ISS (ZARYA)"},"stationPositions":[{"lat":14.74,"lon":60.19,"name":"ISS (ZARYA)"},{"lat":14.74,"lon":60.19,"name":"POISK"},{"lat":-39.63,"lon":-146.58,"name":"CSS (TIANHE)"},{"lat":14.74,"lon":60.19,"name":"ISS (NAUKA)"},{"lat":-12.04,"lon":88.33,"name":"FREGAT DEB"}],"recentLaunches":[{"name":"ELECTRON KICK STAGE R/B","epoch":"2026-04-03T10:37:35.976864"},{"name":"STARLINK-37058","epoch":"2026-04-03T07:46:56.298144"},{"name":"CZ-8A R/B","epoch":"2026-04-03T07:28:54.487200"},{"name":"STARLINK-36244","epoch":"2026-04-03T07:27:33.442272"},{"name":"2026-052A","epoch":"2026-04-03T07:26:12.242688"},{"name":"YUXING-3 06","epoch":"2026-04-03T07:23:30.781824"},{"name":"STARLINK-36241","epoch":"2026-04-03T07:21:57.610656"},{"name":"STARLINK-37127","epoch":"2026-04-03T07:21:36.080640"},{"name":"2026-052C","epoch":"2026-04-03T07:21:17.870976"},{"name":"2026-052D","epoch":"2026-04-03T07:18:05.770944"}],"launchByCountry":{"UNK":343},"signals":["HIGH LAUNCH TEMPO: 343 new objects tracked in last 30 days","STARLINK MEGA-CONSTELLATION: 10119 active satellites"]},"health":[{"n":"OpenSky","err":false,"stale":false},{"n":"FIRMS","err":false,"stale":false},{"n":"Maritime","err":false,"stale":false},{"n":"Safecast","err":false,"stale":false},{"n":"ACLED","err":true,"stale":false},{"n":"ReliefWeb","err":false,"stale":false},{"n":"WHO","err":false,"stale":false},{"n":"OFAC","err":false,"stale":false},{"n":"OpenSanctions","err":false,"stale":false},{"n":"ADS-B","err":false,"stale":false},{"n":"FRED","err":false,"stale":false},{"n":"Treasury","err":false,"stale":false},{"n":"BLS","err":false,"stale":false},{"n":"EIA","err":false,"stale":false},{"n":"GSCPI","err":false,"stale":false},{"n":"USAspending","err":false,"stale":false},{"n":"Comtrade","err":false,"stale":false},{"n":"NOAA","err":false,"stale":false},{"n":"EPA","err":false,"stale":false},{"n":"Patents","err":false,"stale":false},{"n":"Bluesky","err":false,"stale":false},{"n":"Reddit","err":false,"stale":false},{"n":"Telegram","err":false,"stale":false},{"n":"KiwiSDR","err":false,"stale":false},{"n":"Space","err":false,"stale":false},{"n":"YFinance","err":false,"stale":false},{"n":"CISA-KEV","err":false,"stale":false},{"n":"Cloudflare-Radar","err":false,"stale":false}],"news":[{"title":"Bolivia: probe into ring that stole and adulterated fuel imported through Chilean ports","source":"MercoPress","date":"Wed, 01 Apr 2026 10:37:00 GMT","url":"https://en.mercopress.com/2026/04/01/bolivia-probe-into-ring-that-stole-and-adulterated-fuel-imported-through-chilean-ports?utm_source=feed&utm_medium=rss&utm_content=latin-america&utm_campaign=rss","lat":-34.81161432550125,"lon":-71.94455413872535,"region":"Chile"},{"title":"Brazil hit hardest as Latin America adjusts to Trump tariffs after one year","source":"MercoPress","date":"Mon, 30 Mar 2026 15:01:00 GMT","url":"https://en.mercopress.com/2026/03/30/brazil-hit-hardest-as-latin-america-adjusts-to-trump-tariffs-after-one-year?utm_source=feed&utm_medium=rss&utm_content=latin-america&utm_campaign=rss","lat":-13.722939434973625,"lon":-51.73457896188707,"region":"Brazil"},{"title":"Will challenge Gujarat UCC in court: AIMPLB","source":"Indian Express","date":"Fri, 03 Apr 2026 15:16:15 +0000","url":"https://indianexpress.com/article/india/aimplb-challenge-gujarat-ucc-bill-high-court-constitutionally-flawed-10617804/","lat":28.43814557970436,"lon":76.98811638736568,"region":"India"},{"title":"EC suspends West Bengal official for canvassing for TMC","source":"Indian Express","date":"Fri, 03 Apr 2026 14:56:37 +0000","url":"https://indianexpress.com/article/india/ec-suspends-west-bengal-official-for-canvassing-for-tmc-10617769/","lat":28.159093746635413,"lon":77.95716560157726,"region":"India"},{"title":"What Trump's 100 per cent pharmaceutical tariff means for Australia","source":"SBS Australia","date":"Fri, 3 Apr 2026 00:16:59 +0000","url":"https://www.sbs.com.au/news/article/trump-puts-100-per-cent-tariff-on-pharmaceutical-imports/8tlh6pxha","lat":-25.985459271712585,"lon":133.52672704134727,"region":"Australia"},{"title":"A 'wake-up call': How can Australia realistically reduce its reliance on oil?","source":"SBS Australia","date":"Thu, 2 Apr 2026 18:33:54 +0000","url":"https://www.sbs.com.au/news/article/australia-reduce-reliance-on-oil/vjkuuh0f2","lat":-24.784009969727375,"lon":134.53929188641516,"region":"Australia"},{"title":"Iran War Live Updates: American Fighter Jet Downed Over Iran, U.S. Officials Say","source":"NYT","date":"Fri, 03 Apr 2026 16:15:03 +0000","url":"https://www.nytimes.com/live/2026/04/03/world/iran-war-trump-oil","lat":31.68837142833114,"lon":53.386185981517805,"region":"Iran"},{"title":"Baltic Sea strikes: How Ukraine aims to undermine Russia's oil profits from Iran war","source":"France 24","date":"Fri, 03 Apr 2026 16:03:45 GMT","url":"https://www.france24.com/en/europe/20260403-baltic-sea-strikes-how-ukraine-aims-to-undermine-russia-oil-profits-from-iran-war","lat":48.750125643320835,"lon":31.50405983830201,"region":"Ukraine"},{"title":"Fact check: How can a country actually withdraw from NATO?","source":"Euronews","date":"Fri, 03 Apr 2026 18:00:44 +0200","url":"http://www.euronews.com/my-europe/2026/04/03/fact-check-how-can-a-country-actually-withdraw-from-nato","lat":50.20353285633464,"lon":3.8121191043998612,"region":"NATO"},{"title":"Drone footage shows a factory in Israel damaged by Iranian missile debris","source":"Al Jazeera","date":"Fri, 03 Apr 2026 15:51:15 +0000","url":"https://www.aljazeera.com/video/newsfeed/2026/4/3/drone-footage-shows-a-factory-in-israel-damaged-by-iranian-missile-debris?traffic_source=rss","lat":31.75995094400568,"lon":53.40997892086658,"region":"Iran"},{"title":"Trump seeks historic $1.5 trillion for military in Congress budget request","source":"Al Jazeera","date":"Fri, 03 Apr 2026 15:48:57 +0000","url":"https://www.aljazeera.com/news/2026/4/3/trump-seeks-historic-1-5-trillion-for-military-in-congress-budget-request?traffic_source=rss","lat":38.64917710837783,"lon":-77.55690888766377,"region":"Trump"},{"title":"Trump seeks massive $1.5tn for defence alongside cuts in domestic spending","source":"BBC","date":"Fri, 03 Apr 2026 15:45:25 GMT","url":"https://www.bbc.com/news/articles/crr1q4kjvn2o?at_medium=RSS&at_campaign=rss","lat":38.69376446111843,"lon":-76.46425528541243,"region":"Trump"},{"title":"Ukraine slows enemy advances, liberates land, drains Russia’s war chest","source":"Al Jazeera","date":"Fri, 03 Apr 2026 15:44:16 +0000","url":"https://www.aljazeera.com/features/2026/4/3/ukraine-slows-enemy-advances-liberates-land-drains-russias-war-chest?traffic_source=rss","lat":48.00033817379557,"lon":31.64314089243382,"region":"Ukraine"},{"title":"Iranian forces launch search for crew of downed US fighter jet","source":"Al Jazeera","date":"Fri, 03 Apr 2026 15:43:22 +0000","url":"https://www.aljazeera.com/news/2026/4/3/iranian-forces-launch-search-for-pilot-of-downed-us-fighter-jet?traffic_source=rss","lat":32.44979451205133,"lon":52.64848869364308,"region":"Iran"},{"title":"Iranian media outlets say helicopters are searching for the crew of the downed jet.","source":"NYT","date":"Fri, 03 Apr 2026 15:30:47 +0000","url":"https://www.nytimes.com/live/2026/04/03/world/iran-war-trump-oil/iranian-media-outlets-say-helicopters-are-searching-for-the-crew-of-the-downed-jet","lat":31.330671466694223,"lon":53.840079179715744,"region":"Iran"},{"title":"On the ground: 'Massive explosion' rocks northern Tehran","source":"France 24","date":"Fri, 03 Apr 2026 15:26:45 GMT","url":"https://www.france24.com/en/video/20260403-on-the-ground-massive-explosion-rocks-northern-tehran","lat":35.254661607828666,"lon":52.332822213612246,"region":"Tehran"},{"title":"Trump says with more time, US can ‘take the oil’ in Iran","source":"Al Jazeera","date":"Fri, 03 Apr 2026 15:18:34 +0000","url":"https://www.aljazeera.com/news/2026/4/3/trump-says-with-more-time-us-can-take-the-oil-in-iran?traffic_source=rss","lat":31.11176059103011,"lon":53.3094389390466,"region":"Iran"},{"title":"Panama Papers: 10 years on","source":"Al Jazeera","date":"Fri, 03 Apr 2026 15:17:26 +0000","url":"https://www.aljazeera.com/video/newsfeed/2026/4/3/panama-papers-10-years-on?traffic_source=rss","lat":9.841154610691891,"lon":-79.24903356034724,"region":"Panama"},{"title":"Rare mountain bongos return to Kenya","source":"Africa News","date":"Fri, 03 Apr 2026 15:14:55 +0000","url":"http://www.africanews.com/2026/04/03/rare-mountain-bongos-return-to-kenya/","lat":-0.08726543010088461,"lon":38.30757729570934,"region":"Kenya"},{"title":"Red Crescent supply warehouse attacked in southwest Iran","source":"Al Jazeera","date":"Fri, 03 Apr 2026 15:13:12 +0000","url":"https://www.aljazeera.com/video/newsfeed/2026/4/3/red-crescent-supply-warehouse-attacked-in-southwest-iran?traffic_source=rss","lat":32.865532313927275,"lon":52.088459318056,"region":"Iran"},{"title":"Iran release videos of US aircraft searching for pilots shot down in Iran","source":"Al Jazeera","date":"Fri, 03 Apr 2026 15:11:44 +0000","url":"https://www.aljazeera.com/video/newsfeed/2026/4/3/iran-release-videos-of-us-aircraft-searching-for-pilots-shot-down-in-iran?traffic_source=rss","lat":32.36467177720083,"lon":53.82531149346571,"region":"Iran"},{"title":"US launches rescue operation after state TV says American fighter jet downed in Iran","source":"Euronews","date":"Fri, 03 Apr 2026 17:06:09 +0200","url":"http://www.euronews.com/2026/04/03/media-say-army-searching-for-pilot-of-us-fighter-jet-allegedly-downed-over-iran","lat":32.682288924856394,"lon":52.76996036075043,"region":"Iran"},{"title":"Italy's famed Uffizi admits cyber-attack but denies security breach","source":"BBC","date":"Fri, 03 Apr 2026 15:04:39 GMT","url":"https://www.bbc.com/news/articles/cy51wzeq6g5o?at_medium=RSS&at_campaign=rss","lat":42.57358619647256,"lon":11.495160364862468,"region":"Italy"},{"title":"Cuba Says It Is Pardoning More Than 2,000 Prisoners","source":"NYT","date":"Fri, 03 Apr 2026 14:59:02 +0000","url":"https://www.nytimes.com/2026/04/02/world/americas/cuba-prisoner-release.html","lat":21.495322324268997,"lon":-80.22165700386736,"region":"Cuba"},{"title":"How Do You Count 1.4 Billion People? India Is Trying.","source":"NYT","date":"Fri, 03 Apr 2026 14:56:41 +0000","url":"https://www.nytimes.com/2026/04/03/world/asia/india-census-population.html","lat":19.250502417473236,"lon":77.94223986988854,"region":"India"},{"title":"A U.S. jet goes down over Iran, a U.S. official confirms","source":"NPR","date":"Fri, 03 Apr 2026 10:46:48 -0400","url":"https://www.npr.org/2026/04/03/nx-s1-5773108/american-jet-shot-down-iran","lat":31.92406787760425,"lon":52.70960435955099,"region":"Iran"},{"title":"Israel threatens to destroy more Lebanon bridges as crisis mounts","source":"Al Jazeera","date":"Fri, 03 Apr 2026 14:43:39 +0000","url":"https://www.aljazeera.com/news/2026/4/3/israel-threatens-to-destroy-more-lebanon-bridges-as-crisis-mounts?traffic_source=rss","lat":32.277641926454535,"lon":35.99471663448209,"region":"Israel"},{"title":"France's Ligue 1 trials live broadcast of ref exchanges in PSG-Toulouse clash","source":"RFI","date":"Fri, 03 Apr 2026 14:35:25 GMT","url":"https://www.rfi.fr/en/sports/20260403-france-s-ligue-1-trials-live-broadcast-of-ref-exchanges-in-psg-toulouse-clash","lat":46.87705647591926,"lon":1.154412300217873,"region":"France"},{"title":"Rome court rules Netflix price hikes illegal, opening door to €500 refunds","source":"Euronews","date":"Fri, 03 Apr 2026 16:35:16 +0200","url":"http://www.euronews.com/business/2026/04/03/rome-court-rules-netflix-price-hikes-illegal-opening-door-to-500-refunds","lat":41.78872903967678,"lon":11.957164564910382,"region":"Rome"},{"title":"Mirwaiz flags ‘civilian killing’ in Ganderbal – ‘Killed in cold blood’","source":"Indian Express","date":"Fri, 03 Apr 2026 14:21:45 +0000","url":"https://indianexpress.com/article/india/mirwaiz-flags-civilian-killing-curbs-on-jamia-masjid-killed-in-cold-blood-10617729/","lat":28.049331642194076,"lon":76.55455787245545,"region":"India"},{"title":"Cuba to release more than 2,000 prisoners as US pressure mounts","source":"BBC","date":"Fri, 03 Apr 2026 14:18:04 GMT","url":"https://www.bbc.com/news/articles/cwy3r3w4zl8o?at_medium=RSS&at_campaign=rss","lat":21.934263858798072,"lon":-79.18288583805432,"region":"Cuba"},{"title":"New Paris mayor vows to end all sexual violence in schools with €20ml action plan","source":"Euronews","date":"Fri, 03 Apr 2026 16:17:45 +0200","url":"http://www.euronews.com/2026/04/03/new-paris-mayor-vows-to-end-all-sexual-violence-in-schools-with-20ml-action-plan","lat":49.490583921286074,"lon":2.4278675508276755,"region":"Paris"},{"title":"How a Russian influence network is spreading anti-Ukrainian propaganda in Ivory Coast","source":"France 24","date":"Fri, 03 Apr 2026 14:17:04 GMT","url":"https://www.france24.com/en/africa/20260403-russian-influence-network-spreading-anti-ukrainian-propaganda-ivory-coast","lat":55.84486600546132,"lon":37.24980962717258,"region":"Russia"},{"title":"From 250 to 700 dollars per article: How Russia influenced West African media content","source":"France 24","date":"Fri, 03 Apr 2026 14:16:36 GMT","url":"https://www.france24.com/en/africa/20260403-article-russia-influenced-west-african-media-content","lat":56.5111071343658,"lon":38.545908476640946,"region":"Russia"},{"title":"Elephant herd goes on rampage in Jharkhand villages: 3 men killed, a woman injured in 1 day","source":"Indian Express","date":"Fri, 03 Apr 2026 14:10:02 +0000","url":"https://indianexpress.com/article/india/jharkhand-ramgarh-elephant-attacks-gola-forest-deaths-compensation-protest-10617717/","lat":27.9305890619843,"lon":76.41769853908099,"region":"India"},{"title":"Border town of Tiné caught in the crossfire as Sudan war spills into Chad","source":"France 24","date":"Fri, 03 Apr 2026 13:56:57 GMT","url":"https://www.france24.com/en/tv-shows/focus/20260403-tin%C3%A9-caught-in-the-crossfire-war-in-sudan-spilling-over-into-chad","lat":13.489752227657167,"lon":29.411970212979814,"region":"Sudan"},{"title":"Trump says US can open Hormuz with 'a little more time' and asks Congress for $1.5tr for defence","source":"Euronews","date":"Fri, 03 Apr 2026 15:39:31 +0200","url":"http://www.euronews.com/2026/04/03/trump-says-us-can-open-hormuz-with-a-little-more-time-and-asks-congress-for-15tr-for-defen","lat":39.283396005178645,"lon":-97.20739225909496,"region":"US"},{"title":"US jobs surge unexpectedly in March despite Iran war","source":"BBC","date":"Fri, 03 Apr 2026 13:35:11 GMT","url":"https://www.bbc.com/news/articles/crk165g4vv3o?at_medium=RSS&at_campaign=rss","lat":32.10040367081072,"lon":52.7395243584897,"region":"Iran"},{"title":"Finnish conscripts train for all-out war with Russia","source":"Euronews","date":"Fri, 03 Apr 2026 15:34:40 +0200","url":"http://www.euronews.com/2026/04/03/finnish-conscripts-train-for-all-out-war-with-russia","lat":56.12826509899116,"lon":37.81747916130468,"region":"Russia"},{"title":"Italy's head coach quits after team fails to qualify for World Cup","source":"France 24","date":"Fri, 03 Apr 2026 13:34:36 GMT","url":"https://www.france24.com/en/sport/20260403-italy-head-coach-quits-after-team-fails-to-qualify-for-world-cup","lat":42.180753019790885,"lon":11.896210150032074,"region":"Italy"},{"title":"Congo declares end of two-year mpox outbreak that killed over 2,000","source":"Africa News","date":"Fri, 03 Apr 2026 13:29:03 +0000","url":"http://www.africanews.com/2026/04/03/congo-declares-end-of-two-year-mpox-outbreak-that-killed-over-2000/","lat":-3.515887276744196,"lon":21.090991751335505,"region":"Congo"},{"title":"Danish Warship Sunk by Britain’s Lord Nelson 225 Years Ago Is Found","source":"NYT","date":"Fri, 03 Apr 2026 13:28:59 +0000","url":"https://www.nytimes.com/2026/04/02/world/europe/danish-warship-britain-nelson-copenhagen.html","lat":53.756760791396005,"lon":-2.152247816600729,"region":"Britain"},{"title":"‘Allocate a day in each House session to discuss issues related to women’: SP’s Iq","source":"Indian Express","date":"Fri, 03 Apr 2026 13:25:02 +0000","url":"https://indianexpress.com/article/india/iqra-choudhary-letter-om-birla-lok-sabha-women-child-development-dedicated-day-10617599/","lat":28.91668577375544,"lon":77.74896836526128,"region":"India"},{"title":"Gattuso out as Italy’s coach after failure to qualify for 2026 World Cup","source":"Al Jazeera","date":"Fri, 03 Apr 2026 13:17:23 +0000","url":"https://www.aljazeera.com/sports/2026/4/3/gattuso-out-as-italys-coach-after-failure-to-qualify-for-2026-world-cup?traffic_source=rss","lat":41.692881886306374,"lon":11.673611150917017,"region":"Italy"},{"title":"Experts dispute US account of deadly Iran sports hall strike in Lamerd","source":"BBC","date":"Fri, 03 Apr 2026 13:16:33 GMT","url":"https://www.bbc.com/news/articles/c4gx8e1x5j3o?at_medium=RSS&at_campaign=rss","lat":32.48749589296635,"lon":52.53228572040381,"region":"Iran"},{"title":"Islamist rebels kill at least 43 in overnight raid in DR Congo","source":"Africa News","date":"Fri, 03 Apr 2026 13:13:33 +0000","url":"http://www.africanews.com/2026/04/03/islamist-rebels-kill-at-least-43-in-overnight-raid-in-dr-congo/","lat":-4.2796724344660895,"lon":21.48822883582613,"region":"Congo"},{"title":"Trump budget seeks $1.5 trillion in defense spending alongside domestic program cuts","source":"NPR","date":"Fri, 03 Apr 2026 09:08:05 -0400","url":"https://www.npr.org/2026/04/03/nx-s1-5772701/trump-budget-defense-spending","lat":39.51370894509898,"lon":-76.55925889668224,"region":"Trump"},{"title":"Italy part company with manager after failing to qualify for World Cup","source":"BBC","date":"Fri, 03 Apr 2026 13:04:44 GMT","url":"https://www.bbc.com/sport/football/articles/cevk0mn93ejo?at_medium=RSS&at_campaign=rss","lat":42.03002490896037,"lon":12.140717161076905,"region":"Italy"},{"title":"Pete Hegseth asks US Army's top general to step down","source":"BBC","date":"Fri, 03 Apr 2026 13:03:57 GMT","url":"https://www.bbc.com/news/articles/cn8d63v058zo?at_medium=RSS&at_campaign=rss","lat":38.95242472819529,"lon":-97.64321087224609,"region":"US"},{"title":"A young girl’s murder unravels into macabre ‘sacrificial killing’, turning Hazaribagh into political","source":"Indian Express","date":"Fri, 03 Apr 2026 12:57:15 +0000","url":"https://indianexpress.com/article/india/young-girl-murder-sacrificial-killing-hazaribagh-10617593/","lat":29.237356161532336,"lon":77.45887687651887,"region":"India"}],"markets":{"indexes":[{"symbol":"^GSPC","name":"S&P 500","price":6582.69,"change":105.53,"changePct":1.63,"history":[{"date":"2026-03-27","close":6368.85},{"date":"2026-03-30","close":6343.72},{"date":"2026-03-31","close":6528.52},{"date":"2026-04-01","close":6575.32},{"date":"2026-04-02","close":6582.69}]},{"symbol":"^IXIC","name":"Nasdaq Composite","price":21879.18,"change":471.1,"changePct":2.2,"history":[{"date":"2026-03-27","close":20948.36},{"date":"2026-03-30","close":20794.64},{"date":"2026-03-31","close":21590.63},{"date":"2026-04-01","close":21840.95},{"date":"2026-04-02","close":21879.18}]},{"symbol":"^DJI","name":"Dow Jones","price":46504.67,"change":544.56,"changePct":1.18,"history":[{"date":"2026-03-27","close":45166.64},{"date":"2026-03-30","close":45216.14},{"date":"2026-03-31","close":46341.51},{"date":"2026-04-01","close":46565.74},{"date":"2026-04-02","close":46504.67}]},{"symbol":"^RUT","name":"Russell 2000","price":2530.04,"change":36.72,"changePct":1.47,"history":[{"date":"2026-03-27","close":2449.7},{"date":"2026-03-30","close":2414.01},{"date":"2026-03-31","close":2496.37},{"date":"2026-04-01","close":2512.37},{"date":"2026-04-02","close":2530.04}]}],"rates":[{"symbol":"TLT","name":"20Y+ Treasury","price":86.79,"change":0.68,"changePct":0.79},{"symbol":"HYG","name":"High Yield Corp","price":79.56,"change":0.64,"changePct":0.81},{"symbol":"LQD","name":"IG Corporate","price":109.12,"change":1.24,"changePct":1.15}],"commodities":[{"symbol":"GC=F","name":"Gold","price":4702.7,"change":176.7,"changePct":3.9,"history":[{"date":"2026-03-30","close":4526},{"date":"2026-03-31","close":4647.6},{"date":"2026-04-01","close":4783.2},{"date":"2026-04-02","close":4651.5}]},{"symbol":"SI=F","name":"Silver","price":73.17,"change":2.85,"changePct":4.05,"history":[{"date":"2026-03-30","close":70.32},{"date":"2026-03-31","close":74.69},{"date":"2026-04-01","close":75.87},{"date":"2026-04-02","close":72.74}]},{"symbol":"CL=F","name":"WTI Crude","price":112.06,"change":9.18,"changePct":8.92,"history":[{"date":"2026-03-30","close":102.88},{"date":"2026-03-31","close":101.38},{"date":"2026-04-01","close":100.12},{"date":"2026-04-02","close":111.54}]},{"symbol":"BZ=F","name":"Brent Crude","price":109.05,"change":-3.73,"changePct":-3.31,"history":[{"date":"2026-03-30","close":112.78},{"date":"2026-03-31","close":118.35},{"date":"2026-04-01","close":101.16},{"date":"2026-04-02","close":109.03}]},{"symbol":"NG=F","name":"Natural Gas","price":2.81,"change":-0.08,"changePct":-2.77,"history":[{"date":"2026-03-30","close":2.89},{"date":"2026-03-31","close":2.88},{"date":"2026-04-01","close":2.82},{"date":"2026-04-02","close":2.8}]}],"crypto":[{"symbol":"BTC-USD","name":"Bitcoin","price":66895.18,"change":203.73,"changePct":0.31},{"symbol":"ETH-USD","name":"Ethereum","price":2052.04,"change":28.52,"changePct":1.41}],"vix":{"value":23.87,"change":-6.74,"changePct":-22.02},"timestamp":"2026-04-03T16:17:41.213Z"},"ideas":[{"title":"Long crude on Iran escalation","type":"LONG","ticker":"CL=F","confidence":"HIGH","rationale":"Geopolitical escalation is acute: OSINT reports Iranian missile strikes in Israel and stated U.S. plans to target Iran’s energy sector if talks fail, while rescue ops involve F-35/MQ-9 after an F-15E shootdown. Oil is already elevated (WTI $112.06, Brent $109.05), and conflict expansion raises disruption risk in regional production/export flows. With VIX at 24.54 and above-average supply-chain stress (GSCPI 0.49), energy shock pass-through risk is high.","risk":"Rapid de-escalation or surprise diplomatic breakthrough could trigger a sharp oil risk-premium unwind.","horizon":"Days","signals":["WTI=112.06 and Brent=109.05 with active military escalation","U.S. warning of potential strikes on Iran energy sector"],"source":"llm"},{"title":"Long oil volatility convexity","type":"HEDGE","ticker":"BNO","confidence":"MEDIUM","rationale":"Brent-linked exposure is attractive as a hedge against further Middle East supply shocks given 1,271 thermal detections in the Middle East (51 high-confidence) and 134 aircraft tracked in-region. Elevated macro vol (VIX 24.54) and high USD (DTWEXBGS 120.8851) indicate broad risk repricing conditions where geopolitical oil spikes can gap higher. BNO provides cleaner Brent beta than broad equities.","risk":"Global demand fears or coordinated SPR/OPEC response could cap Brent upside despite conflict headlines.","horizon":"Days","signals":["Middle East thermal detections=1271 (51 high-conf)","AIR_ACTIVITY Middle East=134 aircraft"],"source":"llm"},{"title":"Long defense primes","type":"LONG","ticker":"ITA","confidence":"HIGH","rationale":"Conflict intensity is rising (Iran-Israel exchanges, U.S. strike signaling, active CSAR operations), supporting defense demand and procurement momentum. Contract flow is very strong: $10,462M to Boeing plus much larger nuclear/naval awards to Lawrence Livermore and Fluor Marine Propulsion, reinforcing sustained defense spending. ITA captures broad U.S. defense upside with less single-name risk.","risk":"If hostilities cool quickly, defense names may consolidate after headline-driven gains.","horizon":"Weeks","signals":["Defense contracts: $10.462B to Boeing, $40.777B LLNS, $14.773B Fluor Marine","Escalatory OSINT: missile strikes, rescue operations, threatened energy targeting"],"source":"llm"},{"title":"Short airlines on fuel shock","type":"SHORT","ticker":"JETS","confidence":"MEDIUM","rationale":"Jet-fuel input risk is rising with crude above $110 and active military escalation in the Middle East. Higher oil alongside elevated market volatility (VIX 24.54) pressures airline margins and demand sentiment simultaneously. Broad airline ETF exposure avoids idiosyncratic carrier noise while expressing energy-cost squeeze.","risk":"If fuel hedges and resilient travel demand dominate near term, airlines can outperform despite high crude.","horizon":"Days","signals":["WTI=112.06 and Brent=109.05 raise fuel costs","VIX=24.54 indicates risk-off pressure on cyclicals"],"source":"llm"},{"title":"Long inflation hedge via gold","type":"LONG","ticker":"GLD","confidence":"MEDIUM","rationale":"Geopolitical tail risk is intensifying and could reinforce inflation via energy, while CPI is already high (CUUR0000SA0=326.785) and core-like measure remains elevated (CUUR0000SA0L1E=333.242). Real-economy pressure signs (mortgage 6.46%, unemployment 4.3%) plus risk aversion (VIX 24.54) support defensive hard-asset allocation. GLD serves as a liquid hedge against conflict-driven macro instability.","risk":"A stronger dollar (DTWEXBGS 120.8851) and higher real yields could limit gold upside.","horizon":"Weeks","signals":["CPI level elevated: 326.785 with geopolitical energy shock risk","VIX=24.54 and USD index=120.8851 reflect macro stress regime"],"source":"llm"},{"title":"Avoid long duration Treasuries","type":"AVOID","ticker":"TLT","confidence":"HIGH","rationale":"Rates regime remains unfriendly to duration: 10Y yield is 4.33% with 2Y at 3.81% and Fed funds 3.64%, while curve is positively sloped (T10Y2Y=0.52), signaling no immediate deep-cut backdrop. Inflation-sensitive inputs remain firm (WTI $112.06; producer measure WPUFD49104=152.168), which can keep term premium elevated. Avoid adding long-duration exposure until inflation/energy pressures ease.","risk":"A sudden growth shock or sharp risk-off flight-to-quality could rally long bonds aggressively.","horizon":"Weeks","signals":["DGS10=4.33, DGS2=3.81, T10Y2Y=0.52","Oil at $112 and producer price index proxy at 152.168"],"source":"llm"}],"ideasSource":"llm","newsFeed":[{"headline":"Bolivia: probe into ring that stole and adulterated fuel imported through Chilean ports","source":"MercoPress","type":"rss","timestamp":"Wed, 01 Apr 2026 10:37:00 GMT","region":"Chile","urgent":false,"url":"https://en.mercopress.com/2026/04/01/bolivia-probe-into-ring-that-stole-and-adulterated-fuel-imported-through-chilean-ports?utm_source=feed&utm_medium=rss&utm_content=latin-america&utm_campaign=rss"},{"headline":"Brazil hit hardest as Latin America adjusts to Trump tariffs after one year","source":"MercoPress","type":"rss","timestamp":"Mon, 30 Mar 2026 15:01:00 GMT","region":"Brazil","urgent":false,"url":"https://en.mercopress.com/2026/03/30/brazil-hit-hardest-as-latin-america-adjusts-to-trump-tariffs-after-one-year?utm_source=feed&utm_medium=rss&utm_content=latin-america&utm_campaign=rss"},{"headline":"Will challenge Gujarat UCC in court: AIMPLB","source":"Indian Express","type":"rss","timestamp":"Fri, 03 Apr 2026 15:16:15 +0000","region":"India","urgent":false,"url":"https://indianexpress.com/article/india/aimplb-challenge-gujarat-ucc-bill-high-court-constitutionally-flawed-10617804/"},{"headline":"EC suspends West Bengal official for canvassing for TMC","source":"Indian Express","type":"rss","timestamp":"Fri, 03 Apr 2026 14:56:37 +0000","region":"India","urgent":false,"url":"https://indianexpress.com/article/india/ec-suspends-west-bengal-official-for-canvassing-for-tmc-10617769/"},{"headline":"What Trump's 100 per cent pharmaceutical tariff means for Australia","source":"SBS Australia","type":"rss","timestamp":"Fri, 3 Apr 2026 00:16:59 +0000","region":"Australia","urgent":false,"url":"https://www.sbs.com.au/news/article/trump-puts-100-per-cent-tariff-on-pharmaceutical-imports/8tlh6pxha"},{"headline":"A 'wake-up call': How can Australia realistically reduce its reliance on oil?","source":"SBS Australia","type":"rss","timestamp":"Thu, 2 Apr 2026 18:33:54 +0000","region":"Australia","urgent":false,"url":"https://www.sbs.com.au/news/article/australia-reduce-reliance-on-oil/vjkuuh0f2"},{"headline":"Iran War Live Updates: American Fighter Jet Downed Over Iran, U.S. Officials Say","source":"NYT","type":"rss","timestamp":"Fri, 03 Apr 2026 16:15:03 +0000","region":"Iran","urgent":false,"url":"https://www.nytimes.com/live/2026/04/03/world/iran-war-trump-oil"},{"headline":"Baltic Sea strikes: How Ukraine aims to undermine Russia's oil profits from Iran war","source":"France 24","type":"rss","timestamp":"Fri, 03 Apr 2026 16:03:45 GMT","region":"Ukraine","urgent":false,"url":"https://www.france24.com/en/europe/20260403-baltic-sea-strikes-how-ukraine-aims-to-undermine-russia-oil-profits-from-iran-war"},{"headline":"Fact check: How can a country actually withdraw from NATO?","source":"Euronews","type":"rss","timestamp":"Fri, 03 Apr 2026 18:00:44 +0200","region":"NATO","urgent":false,"url":"http://www.euronews.com/my-europe/2026/04/03/fact-check-how-can-a-country-actually-withdraw-from-nato"},{"headline":"Drone footage shows a factory in Israel damaged by Iranian missile debris","source":"Al Jazeera","type":"rss","timestamp":"Fri, 03 Apr 2026 15:51:15 +0000","region":"Iran","urgent":false,"url":"https://www.aljazeera.com/video/newsfeed/2026/4/3/drone-footage-shows-a-factory-in-israel-damaged-by-iranian-missile-debris?traffic_source=rss"},{"headline":"Trump seeks historic $1.5 trillion for military in Congress budget request","source":"Al Jazeera","type":"rss","timestamp":"Fri, 03 Apr 2026 15:48:57 +0000","region":"Trump","urgent":false,"url":"https://www.aljazeera.com/news/2026/4/3/trump-seeks-historic-1-5-trillion-for-military-in-congress-budget-request?traffic_source=rss"},{"headline":"Trump seeks massive $1.5tn for defence alongside cuts in domestic spending","source":"BBC","type":"rss","timestamp":"Fri, 03 Apr 2026 15:45:25 GMT","region":"Trump","urgent":false,"url":"https://www.bbc.com/news/articles/crr1q4kjvn2o?at_medium=RSS&at_campaign=rss"},{"headline":"Ukraine slows enemy advances, liberates land, drains Russia’s war chest","source":"Al Jazeera","type":"rss","timestamp":"Fri, 03 Apr 2026 15:44:16 +0000","region":"Ukraine","urgent":false,"url":"https://www.aljazeera.com/features/2026/4/3/ukraine-slows-enemy-advances-liberates-land-drains-russias-war-chest?traffic_source=rss"},{"headline":"Iranian forces launch search for crew of downed US fighter jet","source":"Al Jazeera","type":"rss","timestamp":"Fri, 03 Apr 2026 15:43:22 +0000","region":"Iran","urgent":false,"url":"https://www.aljazeera.com/news/2026/4/3/iranian-forces-launch-search-for-pilot-of-downed-us-fighter-jet?traffic_source=rss"},{"headline":"Iranian media outlets say helicopters are searching for the crew of the downed jet.","source":"NYT","type":"rss","timestamp":"Fri, 03 Apr 2026 15:30:47 +0000","region":"Iran","urgent":false,"url":"https://www.nytimes.com/live/2026/04/03/world/iran-war-trump-oil/iranian-media-outlets-say-helicopters-are-searching-for-the-crew-of-the-downed-jet"},{"headline":"On the ground: 'Massive explosion' rocks northern Tehran","source":"France 24","type":"rss","timestamp":"Fri, 03 Apr 2026 15:26:45 GMT","region":"Tehran","urgent":false,"url":"https://www.france24.com/en/video/20260403-on-the-ground-massive-explosion-rocks-northern-tehran"},{"headline":"Trump says with more time, US can ‘take the oil’ in Iran","source":"Al Jazeera","type":"rss","timestamp":"Fri, 03 Apr 2026 15:18:34 +0000","region":"Iran","urgent":false,"url":"https://www.aljazeera.com/news/2026/4/3/trump-says-with-more-time-us-can-take-the-oil-in-iran?traffic_source=rss"},{"headline":"Panama Papers: 10 years on","source":"Al Jazeera","type":"rss","timestamp":"Fri, 03 Apr 2026 15:17:26 +0000","region":"Panama","urgent":false,"url":"https://www.aljazeera.com/video/newsfeed/2026/4/3/panama-papers-10-years-on?traffic_source=rss"},{"headline":"Rare mountain bongos return to Kenya","source":"Africa News","type":"rss","timestamp":"Fri, 03 Apr 2026 15:14:55 +0000","region":"Kenya","urgent":false,"url":"http://www.africanews.com/2026/04/03/rare-mountain-bongos-return-to-kenya/"},{"headline":"Red Crescent supply warehouse attacked in southwest Iran","source":"Al Jazeera","type":"rss","timestamp":"Fri, 03 Apr 2026 15:13:12 +0000","region":"Iran","urgent":false,"url":"https://www.aljazeera.com/video/newsfeed/2026/4/3/red-crescent-supply-warehouse-attacked-in-southwest-iran?traffic_source=rss"},{"headline":"Iran release videos of US aircraft searching for pilots shot down in Iran","source":"Al Jazeera","type":"rss","timestamp":"Fri, 03 Apr 2026 15:11:44 +0000","region":"Iran","urgent":false,"url":"https://www.aljazeera.com/video/newsfeed/2026/4/3/iran-release-videos-of-us-aircraft-searching-for-pilots-shot-down-in-iran?traffic_source=rss"},{"headline":"US launches rescue operation after state TV says American fighter jet downed in Iran","source":"Euronews","type":"rss","timestamp":"Fri, 03 Apr 2026 17:06:09 +0200","region":"Iran","urgent":false,"url":"http://www.euronews.com/2026/04/03/media-say-army-searching-for-pilot-of-us-fighter-jet-allegedly-downed-over-iran"},{"headline":"Italy's famed Uffizi admits cyber-attack but denies security breach","source":"BBC","type":"rss","timestamp":"Fri, 03 Apr 2026 15:04:39 GMT","region":"Italy","urgent":false,"url":"https://www.bbc.com/news/articles/cy51wzeq6g5o?at_medium=RSS&at_campaign=rss"},{"headline":"Cuba Says It Is Pardoning More Than 2,000 Prisoners","source":"NYT","type":"rss","timestamp":"Fri, 03 Apr 2026 14:59:02 +0000","region":"Cuba","urgent":false,"url":"https://www.nytimes.com/2026/04/02/world/americas/cuba-prisoner-release.html"},{"headline":"How Do You Count 1.4 Billion People? India Is Trying.","source":"NYT","type":"rss","timestamp":"Fri, 03 Apr 2026 14:56:41 +0000","region":"India","urgent":false,"url":"https://www.nytimes.com/2026/04/03/world/asia/india-census-population.html"},{"headline":"A U.S. jet goes down over Iran, a U.S. official confirms","source":"NPR","type":"rss","timestamp":"Fri, 03 Apr 2026 10:46:48 -0400","region":"Iran","urgent":false,"url":"https://www.npr.org/2026/04/03/nx-s1-5773108/american-jet-shot-down-iran"},{"headline":"Israel threatens to destroy more Lebanon bridges as crisis mounts","source":"Al Jazeera","type":"rss","timestamp":"Fri, 03 Apr 2026 14:43:39 +0000","region":"Israel","urgent":false,"url":"https://www.aljazeera.com/news/2026/4/3/israel-threatens-to-destroy-more-lebanon-bridges-as-crisis-mounts?traffic_source=rss"},{"headline":"France's Ligue 1 trials live broadcast of ref exchanges in PSG-Toulouse clash","source":"RFI","type":"rss","timestamp":"Fri, 03 Apr 2026 14:35:25 GMT","region":"France","urgent":false,"url":"https://www.rfi.fr/en/sports/20260403-france-s-ligue-1-trials-live-broadcast-of-ref-exchanges-in-psg-toulouse-clash"},{"headline":"Rome court rules Netflix price hikes illegal, opening door to €500 refunds","source":"Euronews","type":"rss","timestamp":"Fri, 03 Apr 2026 16:35:16 +0200","region":"Rome","urgent":false,"url":"http://www.euronews.com/business/2026/04/03/rome-court-rules-netflix-price-hikes-illegal-opening-door-to-500-refunds"},{"headline":"Mirwaiz flags ‘civilian killing’ in Ganderbal – ‘Killed in cold blood’","source":"Indian Express","type":"rss","timestamp":"Fri, 03 Apr 2026 14:21:45 +0000","region":"India","urgent":false,"url":"https://indianexpress.com/article/india/mirwaiz-flags-civilian-killing-curbs-on-jamia-masjid-killed-in-cold-blood-10617729/"},{"headline":"Cuba to release more than 2,000 prisoners as US pressure mounts","source":"BBC","type":"rss","timestamp":"Fri, 03 Apr 2026 14:18:04 GMT","region":"Cuba","urgent":false,"url":"https://www.bbc.com/news/articles/cwy3r3w4zl8o?at_medium=RSS&at_campaign=rss"},{"headline":"New Paris mayor vows to end all sexual violence in schools with €20ml action plan","source":"Euronews","type":"rss","timestamp":"Fri, 03 Apr 2026 16:17:45 +0200","region":"Paris","urgent":false,"url":"http://www.euronews.com/2026/04/03/new-paris-mayor-vows-to-end-all-sexual-violence-in-schools-with-20ml-action-plan"},{"headline":"How a Russian influence network is spreading anti-Ukrainian propaganda in Ivory Coast","source":"France 24","type":"rss","timestamp":"Fri, 03 Apr 2026 14:17:04 GMT","region":"Russia","urgent":false,"url":"https://www.france24.com/en/africa/20260403-russian-influence-network-spreading-anti-ukrainian-propaganda-ivory-coast"},{"headline":"From 250 to 700 dollars per article: How Russia influenced West African media content","source":"France 24","type":"rss","timestamp":"Fri, 03 Apr 2026 14:16:36 GMT","region":"Russia","urgent":false,"url":"https://www.france24.com/en/africa/20260403-article-russia-influenced-west-african-media-content"},{"headline":"RFI says Ukraine has bases in Libya to counter Russia, citing sources. It claims 200+ Ukrainian offi","source":"WARTRANSLATED","type":"telegram","timestamp":"2026-04-03T14:15:11+00:00","region":"OSINT","urgent":true},{"headline":"Elephant herd goes on rampage in Jharkhand villages: 3 men killed, a woman injured in 1 day","source":"Indian Express","type":"rss","timestamp":"Fri, 03 Apr 2026 14:10:02 +0000","region":"India","urgent":false,"url":"https://indianexpress.com/article/india/jharkhand-ramgarh-elephant-attacks-gola-forest-deaths-compensation-protest-10617717/"},{"headline":"Border town of Tiné caught in the crossfire as Sudan war spills into Chad","source":"France 24","type":"rss","timestamp":"Fri, 03 Apr 2026 13:56:57 GMT","region":"Sudan","urgent":false,"url":"https://www.france24.com/en/tv-shows/focus/20260403-tin%C3%A9-caught-in-the-crossfire-war-in-sudan-spilling-over-into-chad"},{"headline":"Trump says US can open Hormuz with 'a little more time' and asks Congress for $1.5tr for defence","source":"Euronews","type":"rss","timestamp":"Fri, 03 Apr 2026 15:39:31 +0200","region":"US","urgent":false,"url":"http://www.euronews.com/2026/04/03/trump-says-us-can-open-hormuz-with-a-little-more-time-and-asks-congress-for-15tr-for-defen"},{"headline":"US jobs surge unexpectedly in March despite Iran war","source":"BBC","type":"rss","timestamp":"Fri, 03 Apr 2026 13:35:11 GMT","region":"Iran","urgent":false,"url":"https://www.bbc.com/news/articles/crk165g4vv3o?at_medium=RSS&at_campaign=rss"},{"headline":"Finnish conscripts train for all-out war with Russia","source":"Euronews","type":"rss","timestamp":"Fri, 03 Apr 2026 15:34:40 +0200","region":"Russia","urgent":false,"url":"http://www.euronews.com/2026/04/03/finnish-conscripts-train-for-all-out-war-with-russia"},{"headline":"Italy's head coach quits after team fails to qualify for World Cup","source":"France 24","type":"rss","timestamp":"Fri, 03 Apr 2026 13:34:36 GMT","region":"Italy","urgent":false,"url":"https://www.france24.com/en/sport/20260403-italy-head-coach-quits-after-team-fails-to-qualify-for-world-cup"},{"headline":"One \"Buk\" missile or its analogue could easily take out all three of them at once, not to mention mo","source":"INTELSLAVA","type":"telegram","timestamp":"2026-04-03T13:33:34+00:00","region":"OSINT","urgent":true},{"headline":"Congo declares end of two-year mpox outbreak that killed over 2,000","source":"Africa News","type":"rss","timestamp":"Fri, 03 Apr 2026 13:29:03 +0000","region":"Congo","urgent":false,"url":"http://www.africanews.com/2026/04/03/congo-declares-end-of-two-year-mpox-outbreak-that-killed-over-2000/"},{"headline":"Danish Warship Sunk by Britain’s Lord Nelson 225 Years Ago Is Found","source":"NYT","type":"rss","timestamp":"Fri, 03 Apr 2026 13:28:59 +0000","region":"Britain","urgent":false,"url":"https://www.nytimes.com/2026/04/02/world/europe/danish-warship-britain-nelson-copenhagen.html"},{"headline":"‘Allocate a day in each House session to discuss issues related to women’: SP’s Iq","source":"Indian Express","type":"rss","timestamp":"Fri, 03 Apr 2026 13:25:02 +0000","region":"India","urgent":false,"url":"https://indianexpress.com/article/india/iqra-choudhary-letter-om-birla-lok-sabha-women-child-development-dedicated-day-10617599/"},{"headline":"Gattuso out as Italy’s coach after failure to qualify for 2026 World Cup","source":"Al Jazeera","type":"rss","timestamp":"Fri, 03 Apr 2026 13:17:23 +0000","region":"Italy","urgent":false,"url":"https://www.aljazeera.com/sports/2026/4/3/gattuso-out-as-italys-coach-after-failure-to-qualify-for-2026-world-cup?traffic_source=rss"},{"headline":"Experts dispute US account of deadly Iran sports hall strike in Lamerd","source":"BBC","type":"rss","timestamp":"Fri, 03 Apr 2026 13:16:33 GMT","region":"Iran","urgent":false,"url":"https://www.bbc.com/news/articles/c4gx8e1x5j3o?at_medium=RSS&at_campaign=rss"},{"headline":"Islamist rebels kill at least 43 in overnight raid in DR Congo","source":"Africa News","type":"rss","timestamp":"Fri, 03 Apr 2026 13:13:33 +0000","region":"Congo","urgent":false,"url":"http://www.africanews.com/2026/04/03/islamist-rebels-kill-at-least-43-in-overnight-raid-in-dr-congo/"},{"headline":"Ukraine says air defenses have neutralized 26 missiles and 515 drones after a massive attack. In tot","source":"WARTRANSLATED","type":"telegram","timestamp":"2026-04-03T13:09:55+00:00","region":"OSINT","urgent":true},{"headline":"Trump budget seeks $1.5 trillion in defense spending alongside domestic program cuts","source":"NPR","type":"rss","timestamp":"Fri, 03 Apr 2026 09:08:05 -0400","region":"Trump","urgent":false,"url":"https://www.npr.org/2026/04/03/nx-s1-5772701/trump-budget-defense-spending"}]};
|
||
// === I18N ===
|
||
const L = window.__CRUCIX_LOCALE__ || {};
|
||
function t(keyPath, fallback) {
|
||
const keys = keyPath.split('.');
|
||
let value = L;
|
||
for (const key of keys) {
|
||
if (value && typeof value === 'object' && key in value) {
|
||
value = value[key];
|
||
} else {
|
||
return fallback || keyPath;
|
||
}
|
||
}
|
||
return typeof value === 'string' ? value : (fallback || keyPath);
|
||
}
|
||
|
||
// === GLOBALS ===
|
||
let globe = null;
|
||
let globeInitialized = false;
|
||
let flightsVisible = true;
|
||
let lowPerfMode = localStorage.getItem('crucix_low_perf') === 'true';
|
||
let isFlat = shouldStartFlat();
|
||
let layerModes = JSON.parse(localStorage.getItem('crucix_layer_modes') || '{}');
|
||
let spaceDisplayMode = localStorage.getItem('crucix_space_display') || 'icons';
|
||
let terminalOutput = 'Ready. Live data is loaded from /api/data in server mode.';
|
||
let terminalBusy = false;
|
||
let currentRegion = 'world';
|
||
let flatSvg, flatProjection, flatPath, flatG, flatZoom, flatW, flatH;
|
||
|
||
const layerTypeMap = {
|
||
air: ['air'],
|
||
thermal: ['thermal'],
|
||
sdr: ['sdr'],
|
||
maritime: ['maritime'],
|
||
nuke: ['nuke', 'radiation'],
|
||
conflict: ['conflict'],
|
||
health: ['health'],
|
||
news: ['news', 'gdelt'],
|
||
osint: ['osint'],
|
||
space: ['space'],
|
||
};
|
||
const signalGuideItems = [
|
||
{
|
||
term:'No Callsign',
|
||
category:'Air',
|
||
meaning:'OpenSky received an aircraft track without a usable callsign or flight ID in that record.',
|
||
matters:'Useful as an opacity signal. A cluster of missing callsigns can indicate incomplete transponder metadata or less transparent traffic.',
|
||
notMeaning:'Not proof of military, covert, or hostile activity on its own.',
|
||
example:'South China Sea: No Callsign 6 of 152 means 6 tracks in that theater had no usable callsign in the feed.'
|
||
},
|
||
{
|
||
term:'High Altitude',
|
||
category:'Air',
|
||
meaning:'Aircraft above 12,000 meters, roughly 39,000 feet, in the current OpenSky snapshot.',
|
||
matters:'Separates cruise-level traffic from lower-altitude local or regional movement.',
|
||
notMeaning:'Not a danger score and not inherently unusual. Commercial jets commonly operate here.',
|
||
example:'High Altitude 2 means only 2 tracked aircraft in that hotspot were above the cruise threshold at that snapshot.'
|
||
},
|
||
{
|
||
term:'Top Countries',
|
||
category:'Air',
|
||
meaning:'The most common OpenSky origin_country values among aircraft in that hotspot.',
|
||
matters:'Useful for understanding the rough composition of traffic flowing through a theater.',
|
||
notMeaning:'Not who is controlling the aircraft right now and not a direct indicator of military ownership.',
|
||
example:'China (61), Philippines (39), Taiwan (17) means those were the top registered origin countries in the snapshot.'
|
||
},
|
||
{
|
||
term:'FRP',
|
||
category:'Thermal',
|
||
meaning:'Fire Radiative Power. This is the intensity of one specific FIRMS hotspot, measured in megawatts.',
|
||
matters:'Higher FRP usually means a hotter, larger, or more energetic fire event at that exact point.',
|
||
notMeaning:'Not the intensity of the whole region and not automatic proof of conflict activity.',
|
||
example:'Sudan / Horn of Africa: FRP 92.3 MW describes that one hotspot, while Total 1,451 describes the entire regional detection count.'
|
||
},
|
||
{
|
||
term:'Total Detections',
|
||
category:'Thermal',
|
||
meaning:'The total number of FIRMS thermal detections in the entire region bucket for the current sweep.',
|
||
matters:'Useful for spotting unusually active fire clusters, especially when compared with historical baselines or night activity.',
|
||
notMeaning:'Not a count for the single map point you clicked and not necessarily a conflict count.',
|
||
example:'Total 1,451 means the whole Sudan / Horn of Africa bucket had 1,451 detections in that sweep.'
|
||
},
|
||
{
|
||
term:'Night Detections',
|
||
category:'Thermal',
|
||
meaning:'Thermal detections tagged as occurring at night inside the broader FIRMS region bucket.',
|
||
matters:'Nighttime heat can be more noteworthy because it is less likely to be routine daytime land burning.',
|
||
notMeaning:'Not a direct combat indicator. It still needs context from location, baseline, and corroborating sources.',
|
||
example:'Night 140 means 140 of the 1,451 regional detections were nighttime detections in that sweep.'
|
||
},
|
||
{
|
||
term:'Chokepoint',
|
||
category:'Maritime',
|
||
meaning:'A strategic maritime corridor or passage where trade and energy flows can be delayed, diverted, or disrupted.',
|
||
matters:'These nodes matter because a disruption here can affect shipping costs, transit times, and commodity pricing globally.',
|
||
notMeaning:'Not proof that disruption is happening now. It is a strategic watch location.',
|
||
example:'Bab el-Mandeb or the Strait of Hormuz matter because shipping and energy flows concentrate there.'
|
||
},
|
||
{
|
||
term:'SDR Receiver',
|
||
category:'Signals',
|
||
meaning:'A publicly reachable software-defined radio receiver in or near a region of interest.',
|
||
matters:'Dense receiver coverage can give you more ability to monitor communications or signal activity in a theater.',
|
||
notMeaning:'Not evidence of hostile emissions or a threat by itself. It is an observation and monitoring layer.',
|
||
example:'South China Sea SDR count means publicly accessible KiwiSDR receivers are available in or near that zone.'
|
||
},
|
||
{
|
||
term:'CPM',
|
||
category:'Radiation',
|
||
meaning:'Counts per minute from a radiation monitoring source, used here for relative radiation status at a site.',
|
||
matters:'Useful for spotting anomalies against the site’s normal range or comparing consecutive readings.',
|
||
notMeaning:'Not a direct safety verdict on its own. Interpretation depends on local baseline and trend, not the raw number alone.',
|
||
example:'A site reading 33 CPM can be normal if that location’s usual background level is in the same range.'
|
||
},
|
||
{
|
||
term:'HY Spread',
|
||
category:'Macro',
|
||
meaning:'High-yield credit spread, shown here as a stress proxy from FRED credit data.',
|
||
matters:'When spreads widen, markets are usually pricing more credit stress and tighter financial conditions.',
|
||
notMeaning:'Not a recession call by itself. It is one stress signal among many.',
|
||
example:'A rising HY Spread alongside higher VIX and weaker equities is a stronger risk-off pattern than HY alone.'
|
||
},
|
||
{
|
||
term:'VIX',
|
||
category:'Macro',
|
||
meaning:'The CBOE Volatility Index, commonly used as a market-implied fear or volatility gauge.',
|
||
matters:'Higher VIX often means more expected equity volatility and more defensive market positioning.',
|
||
notMeaning:'Not a direct forecast of a crash and not a geopolitical indicator by itself.',
|
||
example:'VIX above 20 with widening HY spreads is a stronger stress pattern than VIX alone.'
|
||
},
|
||
{
|
||
term:'GSCPI',
|
||
category:'Macro',
|
||
meaning:'The Global Supply Chain Pressure Index, a broad indicator of global supply-chain strain.',
|
||
matters:'It helps translate geopolitical or weather disruptions into likely pressure on shipping, inventory, and pricing.',
|
||
notMeaning:'Not a live market price and not a company-specific supply-chain score by itself.',
|
||
example:'A higher GSCPI makes route or energy shocks more likely to spill into broader cost pressure.'
|
||
},
|
||
{
|
||
term:'WHO Alert',
|
||
category:'Health',
|
||
meaning:'A WHO Disease Outbreak News item or outbreak-related bulletin surfaced in the health layer.',
|
||
matters:'Useful for watching outbreaks that could affect travel, supply chains, humanitarian stress, or regional operating conditions.',
|
||
notMeaning:'Not a pandemic declaration and not automatically high severity.',
|
||
example:'A WHO alert in a port-heavy region matters more if it overlaps shipping, border controls, or local instability signals.'
|
||
},
|
||
{
|
||
term:'Sweep Delta',
|
||
category:'Platform',
|
||
meaning:'The change summary between the current sweep and the previous one, including new, escalated, and de-escalated signals.',
|
||
matters:'Useful for spotting what changed recently instead of re-reading the full dashboard from scratch.',
|
||
notMeaning:'Not a full risk model. It is a directional change layer on top of the raw signals.',
|
||
example:'A delta marked risk-off with several new and escalated items means the latest sweep materially worsened the signal mix.'
|
||
}
|
||
];
|
||
const regionPOV = {
|
||
world: { lat: 20, lng: 20, altitude: 1.8 },
|
||
americas: { lat: 35, lng: -95, altitude: 1.0 },
|
||
europe: { lat: 50, lng: 15, altitude: 1.0 },
|
||
middleEast: { lat: 28, lng: 45, altitude: 1.1 },
|
||
asiaPacific: { lat: 25, lng: 110, altitude: 1.2 },
|
||
africa: { lat: 5, lng: 20, altitude: 1.2 }
|
||
};
|
||
|
||
if(lowPerfMode) document.body.classList.add('low-perf');
|
||
|
||
function isWeakMobileDevice(){
|
||
const reducedMotion = typeof window.matchMedia === 'function' && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||
const memory = navigator.deviceMemory || 0;
|
||
const cores = navigator.hardwareConcurrency || 0;
|
||
return reducedMotion || (memory > 0 && memory <= 4) || (cores > 0 && cores <= 4);
|
||
}
|
||
|
||
function shouldStartFlat(){
|
||
if(!isMobileLayout()) return true;
|
||
return lowPerfMode || isWeakMobileDevice();
|
||
}
|
||
|
||
function setMapLoading(show, text='Initializing 3D Globe'){
|
||
const overlay = document.getElementById('mapLoading');
|
||
const label = document.getElementById('mapLoadingText');
|
||
if(!overlay || !label) return;
|
||
label.textContent = text;
|
||
overlay.classList.toggle('show', show);
|
||
}
|
||
|
||
function togglePerfMode(){
|
||
lowPerfMode = !lowPerfMode;
|
||
localStorage.setItem('crucix_low_perf', String(lowPerfMode));
|
||
document.body.classList.toggle('low-perf', lowPerfMode);
|
||
const perfStatus = document.getElementById('perfStatus');
|
||
if(perfStatus) perfStatus.textContent = lowPerfMode ? 'LITE' : 'FULL';
|
||
if(globe){
|
||
globe.controls().autoRotate = !lowPerfMode;
|
||
globe.arcDashAnimateTime(lowPerfMode ? 0 : 2000);
|
||
}
|
||
if(lowPerfMode && isMobileLayout() && !isFlat){
|
||
toggleMapMode();
|
||
} else {
|
||
renderLower();
|
||
renderRight();
|
||
}
|
||
}
|
||
|
||
// === TOPBAR ===
|
||
function getRegionControlsMarkup(){
|
||
return ['world','americas','europe','middleEast','asiaPacific','africa'].map(r=>
|
||
`<button class="region-btn ${r===currentRegion?'active':''}" data-region="${r}" onclick="setRegion('${r}')">${r==='middleEast'?'MIDDLE EAST':r==='asiaPacific'?'ASIA PACIFIC':r.toUpperCase()}</button>`
|
||
).join('');
|
||
}
|
||
|
||
function renderRegionControls(){
|
||
const mapRegionBar = document.getElementById('mapRegionBar');
|
||
if(!mapRegionBar) return;
|
||
if(isMobileLayout()){
|
||
mapRegionBar.innerHTML = '';
|
||
mapRegionBar.style.display = 'none';
|
||
return;
|
||
}
|
||
mapRegionBar.innerHTML = getRegionControlsMarkup();
|
||
mapRegionBar.style.display = 'flex';
|
||
}
|
||
|
||
function renderTopbar(){
|
||
const mobile = isMobileLayout();
|
||
const ts = new Date(D.meta.timestamp);
|
||
const d = ts.toLocaleDateString('en-US',{month:'short',day:'numeric',year:'numeric'}).toUpperCase();
|
||
const timeStr = ts.toLocaleTimeString('en-US',{hour:'2-digit',minute:'2-digit',hour12:true});
|
||
document.getElementById('topbar').innerHTML=`
|
||
<div class="top-left">
|
||
<span class="brand">CRUCIX MONITOR</span>
|
||
<span class="regime-chip"><span class="blink"></span>WARTIME STAGFLATION RISK</span>
|
||
</div>
|
||
${mobile ? `<div class="top-center">${getRegionControlsMarkup()}</div>` : ''}
|
||
<div class="top-right">
|
||
<button class="meta-pill perf-pill" onclick="togglePerfMode()" title="Reduce visual effects and start mobile in flat mode">${t('dashboard.visuals','VISUALS')} <span class="v" id="perfStatus">${lowPerfMode?t('dashboard.visualsLite','LITE'):t('dashboard.visualsFull','FULL')}</span></button>
|
||
<span class="meta-pill">${t('dashboard.sweep','SWEEP')} <span class="v">${(D.meta.totalDurationMs/1000).toFixed(1)}s</span></span>
|
||
<span class="meta-pill">${d} <span class="v">${timeStr}</span></span>
|
||
<span class="meta-pill">${t('dashboard.sources','SOURCES')} <span class="v">${D.meta.sourcesOk}/${D.meta.sourcesQueried}</span></span>
|
||
${D.delta?.summary ? `<span class="meta-pill">${t('dashboard.delta','DELTA')} <span class="v">${D.delta.summary.direction==='risk-off'?'▲ '+t('dashboard.riskOff','RISK-OFF'):D.delta.summary.direction==='risk-on'?'▼ '+t('dashboard.riskOn','RISK-ON'):'◆ '+t('dashboard.mixed','MIXED')}</span></span>` : ''}
|
||
<button class="guide-btn" onclick="openGlossary()">${t('dashboard.guideBtn','What Signals Mean')}</button>
|
||
<span class="alert-badge">${t('dashboard.highAlert','HIGH ALERT')}</span>
|
||
</div>`;
|
||
renderRegionControls();
|
||
}
|
||
|
||
// === LEFT RAIL ===
|
||
function layerMode(key){ return layerModes[key] || 'normal'; }
|
||
function layerModeLabel(key){ return layerMode(key) === 'focus' ? 'focused' : layerMode(key) === 'hidden' ? 'hidden' : 'normal'; }
|
||
function setLayerMode(key, mode){
|
||
if(mode === 'normal') delete layerModes[key]; else layerModes[key] = mode;
|
||
localStorage.setItem('crucix_layer_modes', JSON.stringify(layerModes));
|
||
renderLeftRail();
|
||
if(isFlat && flatG){ flatG.selectAll('*').remove(); drawFlatMap(); }
|
||
else plotMarkers();
|
||
}
|
||
function cycleLayerMode(key, event){
|
||
const current = layerMode(key);
|
||
const next = event && (event.shiftKey || event.ctrlKey || event.metaKey)
|
||
? (current === 'hidden' ? 'normal' : 'hidden')
|
||
: (current === 'focus' ? 'normal' : 'focus');
|
||
setLayerMode(key, next);
|
||
}
|
||
function resetLayerModes(){
|
||
layerModes = {};
|
||
localStorage.removeItem('crucix_layer_modes');
|
||
renderLeftRail();
|
||
if(isFlat && flatG){ flatG.selectAll('*').remove(); drawFlatMap(); }
|
||
else plotMarkers();
|
||
}
|
||
function toggleSpaceDisplay(){
|
||
spaceDisplayMode = spaceDisplayMode === 'icons' ? 'orbits' : 'icons';
|
||
localStorage.setItem('crucix_space_display', spaceDisplayMode);
|
||
renderLeftRail();
|
||
if(isFlat && flatG){ flatG.selectAll('*').remove(); drawFlatMap(); }
|
||
else plotMarkers();
|
||
}
|
||
function shouldShowType(type){
|
||
const focused = Object.entries(layerModes).filter(([,mode]) => mode === 'focus').flatMap(([key]) => layerTypeMap[key] || []);
|
||
const hidden = Object.entries(layerModes).filter(([,mode]) => mode === 'hidden').flatMap(([key]) => layerTypeMap[key] || []);
|
||
if(hidden.includes(type)) return false;
|
||
if(focused.length > 0) return focused.includes(type);
|
||
return true;
|
||
}
|
||
|
||
function renderLeftRail(){
|
||
const totalAir=D.air.reduce((s,a)=>s+a.total,0);
|
||
const totalThermal=D.thermal.reduce((s,t)=>s+t.det,0);
|
||
const totalNight=D.thermal.reduce((s,t)=>s+t.night,0);
|
||
const newsCount=(D.news||[]).length;
|
||
const conflictEvents = D.acled?.totalEvents || 0;
|
||
const conflictFatal = D.acled?.totalFatalities || 0;
|
||
const layers=[
|
||
{key:'air',name:t('layers.airActivity','Air Activity'),count:totalAir,dot:'air',sub:`${D.air.length} ${t('layers.theaters','theaters')}`},
|
||
{key:'thermal',name:t('layers.thermalSpikes','Thermal Spikes'),count:totalThermal.toLocaleString(),dot:'thermal',sub:`${totalNight.toLocaleString()} ${t('layers.nightDet','night det.')}`},
|
||
{key:'sdr',name:t('layers.sdrCoverage','SDR Coverage'),count:D.sdr.total,dot:'sdr',sub:`${D.sdr.online} ${t('layers.online','online')}`},
|
||
{key:'maritime',name:t('layers.maritimeWatch','Maritime Watch'),count:D.chokepoints.length,dot:'maritime',sub:t('layers.chokepoints','chokepoints')},
|
||
{key:'nuke',name:t('layers.nuclearSites','Nuclear Sites'),count:D.nuke.length,dot:'nuke',sub:t('layers.monitors','monitors')},
|
||
{key:'conflict',name:t('layers.conflictEvents','Conflict Events'),count:conflictEvents,dot:'thermal',sub:`${conflictFatal.toLocaleString()} ${t('layers.fatalities','fatalities')}`},
|
||
{key:'health',name:t('layers.healthWatch','Health Watch'),count:D.who.length,dot:'health',sub:t('layers.whoAlerts','WHO alerts')},
|
||
{key:'news',name:t('layers.worldNews','World News'),count:newsCount,dot:'news',sub:t('layers.rssGeolocated','RSS geolocated')},
|
||
{key:'osint',name:t('layers.osintFeed','OSINT Feed'),count:D.tg.posts,dot:'incident',sub:`${D.tg.urgent.length} ${t('badges.urgent','urgent').toLowerCase()}`},
|
||
{key:'space',name:t('layers.spaceActivity','Satellites'),count:D.space?.militarySats||0,dot:'space',sub:`${D.space?.totalNewObjects||0} ${t('space.newLast30d','new (30d)')}`}
|
||
];
|
||
const allNormal=D.nuke.every(s=>!s.anom);
|
||
const nukeHtml=D.nuke.map(s=>`<div class="site-row"><span>${s.site}</span><span class="site-val">${s.n>0?(s.cpm?.toFixed(1)||'--')+' CPM':'No data'}</span></div>`).join('');
|
||
const vix=D.fred.find(f=>f.id==='VIXCLS');
|
||
const hy=D.fred.find(f=>f.id==='BAMLH0A0HYM2');
|
||
const usd=D.fred.find(f=>f.id==='DTWEXBGS');
|
||
const m2=D.fred.find(f=>f.id==='M2SL');
|
||
const mort=D.fred.find(f=>f.id==='MORTGAGE30US');
|
||
const claims=D.fred.find(f=>f.id==='ICSA');
|
||
|
||
document.getElementById('leftRail').innerHTML=`
|
||
<div class="g-panel">
|
||
<div class="sec-head"><h3>${t('panels.sensorGrid','Sensor Grid')}</h3><div class="sensor-actions"><button class="mini-btn" onclick="resetLayerModes();event.stopPropagation()">RESET</button><span class="badge">${t('badges.live','LIVE')}</span></div></div>
|
||
${layers.map(l=>`<div class="layer-item ${layerMode(l.key)==='focus'?'focused':''} ${layerMode(l.key)==='hidden'?'hidden-layer':''}" onclick="cycleLayerMode('${l.key}',event)" title="Click to focus. Shift/Ctrl-click to hide."><div class="layer-left"><div class="ldot ${l.dot}"></div><div><div class="layer-name">${l.name}</div><div class="layer-sub">${l.sub}</div><div class="layer-mode">${layerModeLabel(l.key)}</div></div></div><div class="layer-count">${l.count}</div></div>`).join('')}
|
||
</div>
|
||
<div class="g-panel">
|
||
<div class="sec-head"><h3>${t('panels.nuclearWatch','Nuclear Watch')}</h3><span class="badge">${t('badges.radiation','RADIATION')}</span></div>
|
||
<div class="nuke-ok">${allNormal?'● '+t('nuclear.allSitesNormal','ALL SITES NORMAL'):'⚠ '+t('nuclear.anomalyDetected','ANOMALY DETECTED')}</div>
|
||
${nukeHtml}
|
||
</div>
|
||
<div class="g-panel">
|
||
<div class="sec-head"><h3>${t('panels.riskGauges','Risk Gauges')}</h3><span class="badge">${t('badges.stress','STRESS')}</span></div>
|
||
<div class="econ-row"><span class="elabel">${t('metrics.vix','VIX')} (Fear)</span><span class="eval" style="color:${vix?.value>20?'var(--warn)':'var(--accent)'}">${vix?.value||'--'}</span></div>
|
||
<div class="econ-row"><span class="elabel">${t('metrics.hySpread','HY Spread')}</span><span class="eval">${hy?.value||'--'}</span></div>
|
||
<div class="econ-row"><span class="elabel">${t('metrics.usdIndex','USD Index')}</span><span class="eval">${usd?.value?.toFixed(1)||'--'}</span></div>
|
||
<div class="econ-row"><span class="elabel">${t('metrics.joblessClaims','Jobless Claims')}</span><span class="eval">${claims?.value?.toLocaleString()||'--'}</span></div>
|
||
<div class="econ-row"><span class="elabel">${t('metrics.mortgage30y','30Y Mortgage')}</span><span class="eval">${mort?.value||'--'}%</span></div>
|
||
<div class="econ-row"><span class="elabel">${t('metrics.m2Supply','M2 Supply')}</span><span class="eval">$${(m2?.value/1000)?.toFixed(1)||'--'}T</span></div>
|
||
<div class="econ-row"><span class="elabel">${t('metrics.natDebt','Nat. Debt')}</span><span class="eval">$${(parseFloat(D.treasury.totalDebt)/1e12).toFixed(2)}T</span></div>
|
||
</div>
|
||
<div class="g-panel">
|
||
<div class="sec-head"><h3>${t('panels.spaceWatch','Space Watch')}</h3><button class="mini-btn" onclick="toggleSpaceDisplay()">${spaceDisplayMode.toUpperCase()}</button></div>
|
||
${D.space ? `
|
||
<div class="econ-row"><span class="elabel">New Objects (30d)</span><span class="eval" style="color:var(--accent2)">${D.space.totalNewObjects||0}</span></div>
|
||
<div class="econ-row"><span class="elabel">Military Sats</span><span class="eval">${D.space.militarySats||0}</span></div>
|
||
<div class="econ-row"><span class="elabel">Starlink</span><span class="eval">${D.space.constellations?.starlink||0}</span></div>
|
||
<div class="econ-row"><span class="elabel">OneWeb</span><span class="eval">${D.space.constellations?.oneweb||0}</span></div>
|
||
${D.space.iss ? `<div class="econ-row"><span class="elabel">ISS</span><span class="eval" style="color:var(--accent)">ALT ${((D.space.iss.apogee+D.space.iss.perigee)/2).toFixed(0)} km</span></div>` : ''}
|
||
${Object.entries(D.space.militaryByCountry||{}).sort((a,b)=>b[1]-a[1]).slice(0,4).map(([c,n])=>`<div class="econ-row"><span class="elabel" style="padding-left:8px">${c}</span><span class="eval" style="font-size:10px">${n} mil sats</span></div>`).join('')}
|
||
${(D.space.signals||[]).length ? `<div style="margin-top:6px;padding:6px 8px;border:1px solid rgba(68,204,255,0.2);background:rgba(68,204,255,0.04);font-family:var(--mono);font-size:9px;color:var(--accent2);line-height:1.5">${D.space.signals.slice(0,2).join('<br>')}</div>` : ''}
|
||
` : '<div style="font-family:var(--mono);font-size:10px;color:var(--dim)">NO SPACE DATA</div>'}
|
||
</div>`;
|
||
}
|
||
|
||
// === MAP ===
|
||
let mapLifecycleBound = false;
|
||
|
||
function bindMapLifecycleEvents(){
|
||
if(mapLifecycleBound) return;
|
||
mapLifecycleBound = true;
|
||
window.addEventListener('resize', () => syncResponsiveLayout());
|
||
window.addEventListener('orientationchange', () => setTimeout(() => syncResponsiveLayout(true), 150));
|
||
document.addEventListener('visibilitychange', () => {
|
||
if(!document.hidden) setTimeout(() => syncResponsiveLayout(true), 150);
|
||
});
|
||
window.addEventListener('pageshow', () => setTimeout(() => syncResponsiveLayout(true), 150));
|
||
}
|
||
|
||
function renderMapLegend(){
|
||
document.getElementById('mapLegend').innerHTML=
|
||
[{c:'#64f0c8',l:t('map.airTraffic','Air Traffic')},{c:'#ff5f63',l:t('map.thermalFire','Thermal/Fire')},{c:'rgba(255,120,80,0.8)',l:t('map.conflict','Conflict')},{c:'#44ccff',l:t('map.sdrReceiver','SDR Receiver')},
|
||
{c:'#ffe082',l:t('map.nuclearSite','Nuclear Site')},{c:'#b388ff',l:t('map.chokepoint','Chokepoint')},{c:'#ffb84c',l:t('map.osintEvent','OSINT Event')},{c:'#69f0ae',l:t('map.healthAlert','Health Alert')},{c:'#81d4fa',l:t('map.worldNews','World News')},{c:'#ff9800',l:t('map.weatherAlert','Weather Alert')},{c:'#cddc39',l:t('map.epaRadNet','EPA RadNet')},{c:'#ffffff',l:t('map.spaceStation','Space Station')},{c:'#6495ed',l:t('map.gdeltEvent','GDELT Event')}]
|
||
.map(x=>`<div class="leg-item"><div class="leg-dot" style="background:${x.c}"></div>${x.l}</div>`).join('');
|
||
}
|
||
|
||
function initMap(){
|
||
bindMapLifecycleEvents();
|
||
renderMapLegend();
|
||
if(isFlat){
|
||
if(globe && typeof globe.pauseAnimation === 'function') globe.pauseAnimation();
|
||
document.getElementById('globeViz').style.display = 'none';
|
||
document.getElementById('flatMapSvg').style.display = 'block';
|
||
document.getElementById('projToggle').textContent = 'GLOBE MODE';
|
||
document.getElementById('mapHint').textContent = 'SCROLL TO ZOOM · DRAG TO PAN';
|
||
if(!flatSvg) initFlatMap();
|
||
else { flatG.selectAll('*').remove(); drawFlatMap(); }
|
||
setMapLoading(false);
|
||
return;
|
||
}
|
||
setMapLoading(true, 'Initializing 3D Globe');
|
||
requestAnimationFrame(() => {
|
||
try {
|
||
initGlobe();
|
||
setMapLoading(false);
|
||
} catch {
|
||
isFlat = true;
|
||
document.getElementById('globeViz').style.display = 'none';
|
||
document.getElementById('flatMapSvg').style.display = 'block';
|
||
document.getElementById('projToggle').textContent = 'GLOBE MODE';
|
||
document.getElementById('mapHint').textContent = '3D LOAD FAILED · FLAT MODE';
|
||
if(!flatSvg) initFlatMap();
|
||
else { flatG.selectAll('*').remove(); drawFlatMap(); }
|
||
setMapLoading(false);
|
||
}
|
||
});
|
||
}
|
||
|
||
function initGlobe(){
|
||
if(globeInitialized && globe){
|
||
if(typeof globe.resumeAnimation === 'function') globe.resumeAnimation();
|
||
document.getElementById('globeViz').style.display = 'block';
|
||
document.getElementById('flatMapSvg').style.display = 'none';
|
||
document.getElementById('projToggle').textContent = 'FLAT MODE';
|
||
document.getElementById('mapHint').textContent = 'DRAG TO ROTATE · SCROLL TO ZOOM';
|
||
return;
|
||
}
|
||
const container = document.getElementById('mapContainer');
|
||
const w = container.clientWidth;
|
||
const h = container.clientHeight || 560;
|
||
|
||
globe = Globe()
|
||
.width(w)
|
||
.height(h)
|
||
.globeImageUrl('//unpkg.com/three-globe@2.33.0/example/img/earth-night.jpg')
|
||
.bumpImageUrl('//unpkg.com/three-globe@2.33.0/example/img/earth-topology.png')
|
||
.backgroundImageUrl('')
|
||
.backgroundColor('rgba(0,0,0,0)')
|
||
.atmosphereColor('#64f0c8')
|
||
.atmosphereAltitude(0.18)
|
||
.showGraticules(true)
|
||
// Points layer (main markers)
|
||
.pointAltitude(d => d.alt || 0.01)
|
||
.pointRadius(d => d.size || 0.3)
|
||
.pointColor(d => d.color)
|
||
.pointLabel(d => `<b>${d.popHead||''}</b><br><span style="opacity:0.7">${d.popMeta||''}</span>`)
|
||
.onPointClick((pt, ev) => { showPopup(ev, pt.popHead, pt.popText, pt.popMeta, pt.lat, pt.lng, pt.alt); })
|
||
.onPointHover(pt => { document.getElementById('globeViz').style.cursor = pt ? 'pointer' : 'grab'; })
|
||
// Arcs layer (flight corridors)
|
||
.arcColor(d => d.color)
|
||
.arcStroke(d => d.stroke || 0.4)
|
||
.arcDashLength(0.4)
|
||
.arcDashGap(0.2)
|
||
.arcDashAnimateTime(2000)
|
||
.arcAltitudeAutoScale(0.3)
|
||
.arcLabel(d => d.label || '')
|
||
// Rings layer (pulsing conflict events)
|
||
.ringColor(d => t => `rgba(255,120,80,${1-t})`)
|
||
.ringMaxRadius(d => d.maxR || 3)
|
||
.ringPropagationSpeed(d => d.speed || 2)
|
||
.ringRepeatPeriod(d => d.period || 800)
|
||
// Labels layer
|
||
.labelText(d => d.text)
|
||
.labelSize(d => d.size || 0.4)
|
||
.labelColor(d => d.color || 'rgba(106,138,130,0.9)')
|
||
.labelDotRadius(0)
|
||
.labelAltitude(0.012)
|
||
.labelResolution(2)
|
||
(document.getElementById('globeViz'));
|
||
|
||
// Style the WebGL scene
|
||
const scene = globe.scene();
|
||
const renderer = globe.renderer();
|
||
renderer.setClearColor(0x000000, 0);
|
||
|
||
// Add subtle stars background
|
||
const starGeom = new THREE.BufferGeometry();
|
||
const starVerts = [];
|
||
for(let i=0; i<2000; i++){
|
||
const r = 800 + Math.random()*200;
|
||
const theta = Math.random()*Math.PI*2;
|
||
const phi = Math.acos(2*Math.random()-1);
|
||
starVerts.push(r*Math.sin(phi)*Math.cos(theta), r*Math.sin(phi)*Math.sin(theta), r*Math.cos(phi));
|
||
}
|
||
starGeom.setAttribute('position', new THREE.Float32BufferAttribute(starVerts, 3));
|
||
const starMat = new THREE.PointsMaterial({color:0x88bbaa, size:0.8, transparent:true, opacity:0.6});
|
||
scene.add(new THREE.Points(starGeom, starMat));
|
||
|
||
// Customize graticule color
|
||
scene.traverse(obj => {
|
||
if(obj.material && obj.type === 'Line'){
|
||
obj.material.color.set(0x1a3a2a);
|
||
obj.material.opacity = 0.3;
|
||
obj.material.transparent = true;
|
||
}
|
||
});
|
||
|
||
// Set initial POV
|
||
globe.pointOfView(regionPOV.world, 0);
|
||
|
||
// Auto-rotate slowly
|
||
globe.controls().autoRotate = !lowPerfMode;
|
||
globe.controls().autoRotateSpeed = 0.3;
|
||
globe.controls().enableDamping = true;
|
||
globe.controls().dampingFactor = 0.1;
|
||
|
||
// Stop auto-rotate on interaction, resume after 10s
|
||
let rotateTimeout;
|
||
const el = document.getElementById('globeViz');
|
||
el.addEventListener('mousedown', () => {
|
||
globe.controls().autoRotate = false;
|
||
clearTimeout(rotateTimeout);
|
||
});
|
||
el.addEventListener('mouseup', () => {
|
||
rotateTimeout = setTimeout(() => { if(globe && !lowPerfMode) globe.controls().autoRotate = true; }, 10000);
|
||
});
|
||
|
||
// Plot globe markers (preloaded but hidden)
|
||
plotMarkers();
|
||
|
||
// Start in flat mode — hide globe, show flat map
|
||
if(isFlat){
|
||
document.getElementById('globeViz').style.display = 'none';
|
||
document.getElementById('flatMapSvg').style.display = 'block';
|
||
initFlatMap();
|
||
} else {
|
||
document.getElementById('globeViz').style.display = 'block';
|
||
document.getElementById('flatMapSvg').style.display = 'none';
|
||
document.getElementById('projToggle').textContent = 'FLAT MODE';
|
||
document.getElementById('mapHint').textContent = 'DRAG TO ROTATE · SCROLL TO ZOOM';
|
||
}
|
||
|
||
globeInitialized = true;
|
||
}
|
||
|
||
function plotMarkers(){
|
||
if(!globe) return;
|
||
const points = [];
|
||
const labels = [];
|
||
|
||
// === Air hotspots (green) ===
|
||
const airCoords=[{lat:30,lon:44},{lat:24,lon:120},{lat:49,lon:32},{lat:57,lon:24},{lat:14,lon:114},{lat:37,lon:127},{lat:25,lon:-80},{lat:4,lon:2},{lat:-34,lon:18},{lat:10,lon:51}];
|
||
if(flightsVisible) D.air.forEach((a,i)=>{
|
||
const c=airCoords[i]; if(!c) return;
|
||
points.push({
|
||
lat:c.lat, lng:c.lon, size:0.25+a.total/200, alt:0.015,
|
||
color:'rgba(100,240,200,0.8)', type:'air', priority:1,
|
||
label: a.region.replace(' Region','')+' '+a.total,
|
||
popHead: a.region, popMeta: 'Air Activity',
|
||
popText: `${a.total} aircraft tracked<br>No callsign: ${a.noCallsign}<br>High altitude: ${a.highAlt}<br>Top: ${a.top.slice(0,3).map(t=>t[0]+' ('+t[1]+')').join(', ')}`
|
||
});
|
||
labels.push({lat:c.lat, lng:c.lon+2, text:a.region.replace(' Region','')+' '+a.total, size:0.35, color:'rgba(106,138,130,0.8)'});
|
||
});
|
||
|
||
// === Thermal/fire (red) ===
|
||
D.thermal.forEach(t=>{
|
||
t.fires.forEach(f=>{
|
||
points.push({
|
||
lat:f.lat, lng:f.lon, size:0.12+Math.min(f.frp/200,0.3), alt:0.008,
|
||
color:'rgba(255,95,99,0.7)', type:'thermal', priority:3,
|
||
popHead:'Thermal Detection', popMeta:'FIRMS Satellite',
|
||
popText:`Region: ${t.region}<br>FRP: ${f.frp.toFixed(1)} MW<br>Total: ${t.det.toLocaleString()}<br>Night: ${t.night.toLocaleString()}`
|
||
});
|
||
});
|
||
});
|
||
|
||
// === Maritime chokepoints (purple) ===
|
||
D.chokepoints.forEach(cp=>{
|
||
points.push({
|
||
lat:cp.lat, lng:cp.lon, size:0.35, alt:0.02,
|
||
color:'rgba(179,136,255,0.8)', type:'maritime', priority:1,
|
||
popHead:cp.label, popMeta:'Maritime Intelligence', popText:cp.note
|
||
});
|
||
labels.push({lat:cp.lat, lng:cp.lon+1.5, text:cp.label, size:0.3, color:'rgba(179,136,255,0.6)'});
|
||
});
|
||
|
||
// === Nuclear sites (yellow) ===
|
||
const nukeCoords=[{lat:47.5,lon:34.6},{lat:51.4,lon:30.1},{lat:28.8,lon:50.9},{lat:39.8,lon:125.8},{lat:37.4,lon:141},{lat:31.0,lon:35.1}];
|
||
D.nuke.forEach((n,i)=>{
|
||
const c=nukeCoords[i]; if(!c) return;
|
||
points.push({
|
||
lat:c.lat, lng:c.lon, size:0.3, alt:0.012,
|
||
color: n.anom ? 'rgba(255,95,99,0.9)' : 'rgba(255,224,130,0.8)', type:'nuke', priority:2,
|
||
popHead:n.site, popMeta:'Radiation Monitoring',
|
||
popText:`Status: ${n.anom?'ANOMALY':'Normal'}<br>Avg CPM: ${n.cpm?.toFixed(1)||'No data'}<br>Readings: ${n.n}`
|
||
});
|
||
});
|
||
|
||
// === SDR receivers (cyan) ===
|
||
D.sdr.zones.forEach(z=>{
|
||
z.receivers.forEach(r=>{
|
||
points.push({
|
||
lat:r.lat, lng:r.lon, size:0.15, alt:0.005,
|
||
color:'rgba(68,204,255,0.6)', type:'sdr', priority:3,
|
||
popHead:'SDR Receiver', popMeta:'KiwiSDR Network',
|
||
popText:`${r.name}<br>Zone: ${z.region}<br>${z.count} in zone`
|
||
});
|
||
});
|
||
});
|
||
|
||
// === OSINT events from Telegram (orange) ===
|
||
const osintGeo=[{lat:45,lon:41,idx:0},{lat:48,lon:37,idx:1},{lat:48.5,lon:37.5,idx:2},{lat:45,lon:40.2,idx:3},{lat:50.6,lon:36.6,idx:5},{lat:48.5,lon:35,idx:6}];
|
||
osintGeo.forEach(o=>{
|
||
const post=D.tg.urgent[o.idx]; if(!post) return;
|
||
points.push({
|
||
lat:o.lat, lng:o.lon, size:0.3, alt:0.018,
|
||
color:'rgba(255,184,76,0.8)', type:'osint', priority:2,
|
||
popHead:(post.channel||'').toUpperCase(), popMeta:`${post.views?.toLocaleString()||'?'} views`,
|
||
popText:cleanText(post.text?.substring(0,200)||'')
|
||
});
|
||
});
|
||
|
||
// === WHO health alerts (green) ===
|
||
const whoGeo=[{lat:0.3,lon:32.6},{lat:-6.2,lon:106.8},{lat:-4.3,lon:15.3},{lat:35,lon:105},{lat:12.5,lon:105},{lat:35,lon:105},{lat:28,lon:84},{lat:24,lon:45},{lat:30,lon:70},{lat:-0.8,lon:11.6}];
|
||
D.who.slice(0,10).forEach((w,i)=>{
|
||
const c=whoGeo[i]; if(!c) return;
|
||
points.push({
|
||
lat:c.lat, lng:c.lon, size:0.25, alt:0.01,
|
||
color:'rgba(105,240,174,0.7)', type:'health', priority:2,
|
||
popHead:w.title, popMeta:'WHO Outbreak', popText:w.summary||''
|
||
});
|
||
});
|
||
|
||
// === News markers (light blue) ===
|
||
(D.news||[]).forEach(n=>{
|
||
points.push({
|
||
lat:n.lat, lng:n.lon, size:0.2, alt:0.008,
|
||
color:'rgba(129,212,250,0.7)', type:'news', priority:3,
|
||
popHead:n.source+' NEWS', popMeta:n.region+' · '+getAge(n.date),
|
||
popText:cleanText(n.title)
|
||
});
|
||
});
|
||
|
||
// === NOAA severe weather alerts (orange) ===
|
||
(D.noaa?.alerts||[]).forEach(a=>{
|
||
points.push({
|
||
lat:a.lat, lng:a.lon, size:0.22, alt:0.01,
|
||
color:'rgba(255,152,0,0.8)', type:'weather', priority:2,
|
||
popHead:a.event, popMeta:'NOAA/NWS · '+a.severity,
|
||
popText:a.headline||''
|
||
});
|
||
});
|
||
|
||
// === EPA RadNet stations (lime green) ===
|
||
(D.epa?.stations||[]).forEach(s=>{
|
||
points.push({
|
||
lat:s.lat, lng:s.lon, size:0.18, alt:0.006,
|
||
color:'rgba(205,220,57,0.7)', type:'radiation', priority:3,
|
||
popHead:'RadNet: '+s.location, popMeta:'EPA Radiation Monitor',
|
||
popText:`${s.analyte||'--'}: ${s.result||'--'} ${s.unit||''}<br>State: ${s.state}`
|
||
});
|
||
});
|
||
|
||
// === ISS + Space Stations (bright white, pulsing) ===
|
||
if(spaceDisplayMode === 'icons') (D.space?.stationPositions||[]).forEach(s=>{
|
||
points.push({
|
||
lat:s.lat, lng:s.lon, size:0.4, alt:0.04,
|
||
color:'rgba(255,255,255,0.95)', type:'space', priority:1,
|
||
popHead:s.name, popMeta:'Space Station (approx.)',
|
||
popText:`Orbital position estimate<br>Lat: ${s.lat}° Lon: ${s.lon}°`
|
||
});
|
||
labels.push({lat:s.lat, lng:s.lon+3, text:s.name.split('(')[0].trim(), size:0.35, color:'rgba(255,255,255,0.7)'});
|
||
});
|
||
|
||
// === GDELT geo events (steel blue) ===
|
||
(D.gdelt?.geoPoints||[]).forEach(g=>{
|
||
points.push({
|
||
lat:g.lat, lng:g.lon, size:0.15+Math.min(g.count/50,0.2), alt:0.007,
|
||
color:'rgba(100,149,237,0.6)', type:'gdelt', priority:3,
|
||
popHead:'GDELT Event', popMeta:g.count+' reports',
|
||
popText:g.name||'Global event detection'
|
||
});
|
||
});
|
||
|
||
// === ACLED CONFLICT EVENTS (pulsing rings) ===
|
||
const visiblePoints = points.filter(p => shouldShowType(p.type));
|
||
const visibleLabels = labels.filter(l => !l.type || shouldShowType(l.type));
|
||
|
||
// Set points on globe
|
||
globe.pointsData(visiblePoints);
|
||
globe.labelsData(visibleLabels);
|
||
|
||
const conflictRings = shouldShowType('conflict') ? (D.acled?.deadliestEvents || []).filter(e => e.lat && e.lon).map(e => {
|
||
const logFatal = Math.log2(Math.max(e.fatalities, 1));
|
||
return {
|
||
lat: e.lat, lng: e.lon,
|
||
maxR: Math.max(2, Math.min(6, 1 + logFatal)),
|
||
speed: 1.5 + Math.random(),
|
||
period: 600 + Math.random()*600,
|
||
popHead: e.type || 'CONFLICT', popMeta: 'ACLED Conflict Data',
|
||
popText: `${e.fatalities} fatalities<br>${e.location}, ${e.country}<br>Date: ${e.date}`
|
||
};
|
||
}) : [];
|
||
globe.ringsData(conflictRings);
|
||
|
||
// === FLIGHT CORRIDORS (3D arcs) ===
|
||
const arcs = [];
|
||
if(flightsVisible && shouldShowType('air')){
|
||
const airCoordsFlight = [
|
||
{region:'Middle East',lat:30,lon:44}, {region:'Taiwan Strait',lat:24,lon:120},
|
||
{region:'Ukraine Region',lat:49,lon:32}, {region:'Baltic Region',lat:57,lon:24},
|
||
{region:'South China Sea',lat:14,lon:114}, {region:'Korean Peninsula',lat:37,lon:127},
|
||
{region:'Caribbean',lat:25,lon:-80}, {region:'Gulf of Guinea',lat:4,lon:2},
|
||
{region:'Cape Route',lat:-34,lon:18}, {region:'Horn of Africa',lat:10,lon:51}
|
||
];
|
||
const globalHubs = [
|
||
{lat:40.6,lon:-73.8},{lat:51.5,lon:-0.5},{lat:25.3,lon:55.4},
|
||
{lat:1.4,lon:103.8},{lat:-33.9,lon:151.2},{lat:-23.4,lon:-46.5}
|
||
];
|
||
// Inter-hotspot corridors
|
||
for(let i=0; i<D.air.length; i++){
|
||
for(let j=i+1; j<D.air.length; j++){
|
||
const a=D.air[i], b=D.air[j];
|
||
const from=airCoordsFlight[i], to=airCoordsFlight[j];
|
||
if(!from||!to) continue;
|
||
const traffic=a.total+b.total;
|
||
if(traffic<30) continue;
|
||
const ncRatio=(a.noCallsign+b.noCallsign)/Math.max(traffic,1);
|
||
const color = ncRatio>0.15 ? ['rgba(255,95,99,0.6)','rgba(255,95,99,0.15)'] :
|
||
ncRatio>0.05 ? ['rgba(255,184,76,0.5)','rgba(255,184,76,0.1)'] :
|
||
['rgba(100,240,200,0.4)','rgba(100,240,200,0.08)'];
|
||
arcs.push({
|
||
startLat:from.lat, startLng:from.lon, endLat:to.lat, endLng:to.lon,
|
||
color, stroke:Math.max(0.3, Math.min(1.2, traffic/120)),
|
||
label:`${from.region} ↔ ${to.region}: ${traffic} aircraft`
|
||
});
|
||
}
|
||
}
|
||
// Hub corridors
|
||
D.air.forEach((a,i)=>{
|
||
if(!airCoordsFlight[i]||a.total<25) return;
|
||
globalHubs.forEach(hub=>{
|
||
const dLat=Math.abs(airCoordsFlight[i].lat-hub.lat);
|
||
const dLon=Math.abs(airCoordsFlight[i].lon-hub.lon);
|
||
if(dLat+dLon<20) return;
|
||
arcs.push({
|
||
startLat:airCoordsFlight[i].lat, startLng:airCoordsFlight[i].lon,
|
||
endLat:hub.lat, endLng:hub.lon,
|
||
color:['rgba(100,240,200,0.2)','rgba(100,240,200,0.05)'],
|
||
stroke:0.3
|
||
});
|
||
});
|
||
});
|
||
}
|
||
if(spaceDisplayMode === 'orbits' && shouldShowType('space')){
|
||
(D.space?.stationPositions||[]).forEach(s=>{
|
||
arcs.push({
|
||
startLat: Math.max(-65, s.lat - 18), startLng: s.lon - 80,
|
||
endLat: Math.min(65, s.lat + 18), endLng: s.lon + 80,
|
||
color: ['rgba(224,176,255,0.55)','rgba(224,176,255,0.12)'],
|
||
stroke: 0.7,
|
||
label: `${s.name} approximate orbital track`
|
||
});
|
||
});
|
||
}
|
||
globe.arcsData(arcs);
|
||
|
||
// Zoom-aware marker sizing: scale markers and labels with camera altitude
|
||
const onGlobeZoom = () => {
|
||
const alt = globe.pointOfView().altitude;
|
||
const sf = Math.max(0.6, Math.min(2.5, 1.5 / alt));
|
||
globe.pointRadius(d => (d.size || 0.3) * sf);
|
||
// Hide labels when zoomed far out to reduce clutter
|
||
const showLabels = alt < 1.8;
|
||
globe.labelSize(d => showLabels ? (d.size || 0.4) : 0);
|
||
// Scale arc strokes with zoom
|
||
globe.arcStroke(d => (d.stroke || 0.4) * Math.max(0.5, Math.min(1.5, 1.2 / alt)));
|
||
globe.arcDashAnimateTime(lowPerfMode ? 0 : 2000);
|
||
// Priority-based point visibility: hide low-priority markers when zoomed out
|
||
if(alt > 2.0){
|
||
globe.pointsData(visiblePoints.filter(p => (p.priority||3) <= 1));
|
||
} else if(alt > 1.2){
|
||
globe.pointsData(visiblePoints.filter(p => (p.priority||3) <= 2));
|
||
} else {
|
||
globe.pointsData(visiblePoints);
|
||
}
|
||
};
|
||
if(typeof globe.onZoom==='function') globe.onZoom(onGlobeZoom);
|
||
}
|
||
|
||
function showPopup(event,head,text,meta,lat,lng,alt){
|
||
const popup=document.getElementById('mapPopup');
|
||
const container=document.getElementById('mapContainer');
|
||
const rect=container.getBoundingClientRect();
|
||
let left, top;
|
||
if(!isFlat && lat!=null && globe && typeof globe.getScreenCoords==='function'){
|
||
const sc=globe.getScreenCoords(lat,lng,alt||0.01);
|
||
if(!sc||isNaN(sc.x)||isNaN(sc.y)||sc.x<0||sc.y<0||sc.x>rect.width||sc.y>rect.height){
|
||
if(event&&event.clientX!=null){left=event.clientX-rect.left+10;top=event.clientY-rect.top-10;}
|
||
else return;
|
||
} else {left=sc.x+10;top=sc.y-10;}
|
||
} else if(event && event.clientX != null){
|
||
left=event.clientX - rect.left + 10;
|
||
top=event.clientY - rect.top - 10;
|
||
} else {
|
||
left=rect.width/2 - 140; top=rect.height/2 - 60;
|
||
}
|
||
if(left+290>rect.width) left=left-300;
|
||
if(top+150>rect.height) top=top-160;
|
||
if(left<0) left=10;
|
||
if(top<0) top=10;
|
||
popup.style.left=left+'px';popup.style.top=top+'px';
|
||
popup.querySelector('.pp-head').textContent=head||'';
|
||
popup.querySelector('.pp-text').innerHTML=text||'';
|
||
popup.querySelector('.pp-meta').textContent=meta||'';
|
||
popup.classList.add('show');
|
||
}
|
||
function closePopup(){document.getElementById('mapPopup').classList.remove('show')}
|
||
|
||
// === MAP CONTROLS ===
|
||
function toggleFlights() {
|
||
flightsVisible = !flightsVisible;
|
||
const btn = document.getElementById('flightToggle');
|
||
btn.classList.toggle('off', !flightsVisible);
|
||
if(isFlat){
|
||
if(flatG){
|
||
flatG.selectAll('*').remove();
|
||
drawFlatMap();
|
||
}
|
||
return;
|
||
}
|
||
if(!globe){
|
||
return;
|
||
}
|
||
if(flightsVisible) {
|
||
plotMarkers(); // re-render with arcs
|
||
} else {
|
||
globe.arcsData([]); // hide arcs
|
||
// Remove air-type points
|
||
const pts = globe.pointsData().filter(p => p.type !== 'air');
|
||
globe.pointsData(pts);
|
||
const lbls = globe.labelsData().filter(l => l.text && !l.text.match(/\d+$/));
|
||
globe.labelsData(lbls);
|
||
}
|
||
}
|
||
|
||
// === FLAT/GLOBE TOGGLE ===
|
||
const flatRegionBounds = {
|
||
world:[[-180,-60],[180,80]], americas:[[-130,10],[-60,55]], europe:[[-12,34],[45,72]],
|
||
middleEast:[[24,10],[65,45]], asiaPacific:[[60,-12],[180,55]], africa:[[-20,-36],[55,38]]
|
||
};
|
||
|
||
function toggleMapMode(){
|
||
isFlat = !isFlat;
|
||
const btn = document.getElementById('projToggle');
|
||
const hint = document.getElementById('mapHint');
|
||
btn.textContent = isFlat ? 'GLOBE MODE' : 'FLAT MODE';
|
||
hint.textContent = isFlat ? 'SCROLL TO ZOOM · DRAG TO PAN' : 'DRAG TO ROTATE · SCROLL TO ZOOM';
|
||
const globeEl = document.getElementById('globeViz');
|
||
const flatEl = document.getElementById('flatMapSvg');
|
||
if(isFlat){
|
||
if(globe && typeof globe.pauseAnimation === 'function') globe.pauseAnimation();
|
||
globeEl.style.display = 'none';
|
||
flatEl.style.display = 'block';
|
||
setMapLoading(false);
|
||
if(!flatSvg) initFlatMap();
|
||
else { flatG.selectAll('*').remove(); drawFlatMap(); }
|
||
} else {
|
||
flatEl.style.display = 'none';
|
||
setMapLoading(true, 'Initializing 3D Globe');
|
||
requestAnimationFrame(() => {
|
||
try {
|
||
initGlobe();
|
||
if(globe && typeof globe.resumeAnimation === 'function') globe.resumeAnimation();
|
||
globeEl.style.display = 'block';
|
||
setMapLoading(false);
|
||
} catch {
|
||
isFlat = true;
|
||
globeEl.style.display = 'none';
|
||
flatEl.style.display = 'block';
|
||
btn.textContent = 'GLOBE MODE';
|
||
hint.textContent = '3D LOAD FAILED · FLAT MODE';
|
||
if(!flatSvg) initFlatMap();
|
||
else { flatG.selectAll('*').remove(); drawFlatMap(); }
|
||
setMapLoading(false);
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
function initFlatMap(){
|
||
const container = document.getElementById('mapContainer');
|
||
flatW = container.clientWidth; flatH = container.clientHeight || 560;
|
||
flatSvg = d3.select('#flatMapSvg').attr('viewBox',`0 0 ${flatW} ${flatH}`).attr('preserveAspectRatio','xMidYMid meet');
|
||
flatProjection = d3.geoNaturalEarth1().fitSize([flatW-20,flatH-20],{type:'Sphere'}).translate([flatW/2,flatH/2]);
|
||
flatPath = d3.geoPath(flatProjection);
|
||
flatG = flatSvg.append('g');
|
||
flatZoom = d3.zoom().scaleExtent([1,12]).on('zoom',(event)=>{
|
||
flatG.attr('transform',event.transform);
|
||
const k=event.transform.k;
|
||
flatG.selectAll('.marker-circle').attr('r',function(){return +this.dataset.baseR/Math.sqrt(k)});
|
||
flatG.selectAll('.marker-label').style('font-size',Math.max(7,9/Math.sqrt(k))+'px')
|
||
.style('display',k>=2.5?'block':'none');
|
||
}).on('end',()=>{ flatG.selectAll('.marker-label').style('display','block'); });
|
||
flatSvg.call(flatZoom);
|
||
drawFlatMap();
|
||
}
|
||
|
||
function drawFlatMap(){
|
||
flatG.append('path').datum(d3.geoGraticule()()).attr('class','graticule').attr('d',flatPath);
|
||
fetch('https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json')
|
||
.then(r=>r.json()).then(world=>{
|
||
const countries=topojson.feature(world,world.objects.countries);
|
||
flatG.selectAll('path.land').data(countries.features).enter().append('path').attr('class','land').attr('d',flatPath);
|
||
flatG.append('path').datum(topojson.mesh(world,world.objects.countries,(a,b)=>a!==b)).attr('class','border').attr('d',flatPath);
|
||
plotFlatMarkers();
|
||
});
|
||
}
|
||
|
||
function plotFlatMarkers(){
|
||
const mg=flatG.append('g').attr('class','markers');
|
||
const proj=flatProjection;
|
||
const addPt=(lat,lon,r,fill,stroke,onClick,priority,type)=>{
|
||
if(type && !shouldShowType(type)) return null;
|
||
const[x,y]=proj([lon,lat]);if(!x||!y)return null;
|
||
const g=mg.append('g').attr('transform',`translate(${x},${y})`).style('cursor','pointer').attr('data-priority',priority||3);
|
||
if(onClick) g.on('click',ev=>{ev.stopPropagation();onClick(ev)});
|
||
g.append('circle').attr('class','marker-circle').attr('r',r).attr('data-base-r',r).attr('fill',fill).attr('stroke',stroke).attr('stroke-width',0.8);
|
||
return g;
|
||
};
|
||
// Air
|
||
const airCoords=[{lat:30,lon:44},{lat:24,lon:120},{lat:49,lon:32},{lat:57,lon:24},{lat:14,lon:114},{lat:37,lon:127},{lat:25,lon:-80},{lat:4,lon:2},{lat:-34,lon:18},{lat:10,lon:51}];
|
||
if(flightsVisible){
|
||
D.air.forEach((a,i)=>{
|
||
const c=airCoords[i];if(!c)return;
|
||
const g=addPt(c.lat,c.lon,4+a.total/40,'rgba(100,240,200,0.7)','rgba(100,240,200,0.3)',
|
||
ev=>showPopup(ev,a.region,`${a.total} aircraft<br>No callsign: ${a.noCallsign}<br>High alt: ${a.highAlt}`,'Air Activity'),1);
|
||
if(g) g.append('text').attr('class','marker-label').attr('x',10).attr('y',3).attr('fill','var(--dim)').attr('font-size','9px').attr('font-family','var(--mono)').text(a.region.replace(' Region','')+' '+a.total);
|
||
});
|
||
}
|
||
// Thermal
|
||
if(shouldShowType('thermal')) D.thermal.forEach(t=>t.fires.forEach(f=>{
|
||
addPt(f.lat,f.lon,2+Math.min(f.frp/50,5),'rgba(255,95,99,0.6)','rgba(255,95,99,0.2)',
|
||
ev=>showPopup(ev,'Thermal',`${t.region}<br>FRP: ${f.frp.toFixed(1)} MW`,'FIRMS'),3);
|
||
}));
|
||
// Chokepoints
|
||
if(shouldShowType('maritime')) D.chokepoints.forEach(cp=>{
|
||
const[x,y]=proj([cp.lon,cp.lat]);if(!x||!y)return;
|
||
const g=mg.append('g').attr('transform',`translate(${x},${y})`).style('cursor','pointer').attr('data-priority',1)
|
||
.on('click',ev=>{ev.stopPropagation();showPopup(ev,cp.label,cp.note,'Maritime')});
|
||
g.append('rect').attr('x',-4).attr('y',-4).attr('width',8).attr('height',8).attr('fill','rgba(179,136,255,0.7)').attr('stroke','rgba(179,136,255,0.3)').attr('stroke-width',0.5).attr('transform','rotate(45)');
|
||
g.append('text').attr('class','marker-label').attr('x',8).attr('y',3).attr('fill','var(--dim)').attr('font-size','8px').attr('font-family','var(--mono)').text(cp.label);
|
||
});
|
||
// Nuclear
|
||
const nukeCoords=[{lat:47.5,lon:34.6},{lat:51.4,lon:30.1},{lat:28.8,lon:50.9},{lat:39.8,lon:125.8},{lat:37.4,lon:141},{lat:31.0,lon:35.1}];
|
||
if(shouldShowType('nuke')) D.nuke.forEach((n,i)=>{const c=nukeCoords[i];if(!c)return;addPt(c.lat,c.lon,4,'rgba(255,224,130,0.7)','rgba(255,224,130,0.3)',ev=>showPopup(ev,n.site,`CPM: ${n.cpm?.toFixed(1)||'--'}`,'Radiation'),2)});
|
||
// SDR
|
||
if(shouldShowType('sdr')) D.sdr.zones.forEach(z=>z.receivers.forEach(r=>{addPt(r.lat,r.lon,2.5,'rgba(68,204,255,0.5)','rgba(68,204,255,0.2)',ev=>showPopup(ev,'SDR',`${r.name}<br>${z.region}`,'KiwiSDR'),3)}));
|
||
// OSINT
|
||
const osintGeo=[{lat:45,lon:41,idx:0},{lat:48,lon:37,idx:1},{lat:48.5,lon:37.5,idx:2},{lat:45,lon:40.2,idx:3},{lat:50.6,lon:36.6,idx:5},{lat:48.5,lon:35,idx:6}];
|
||
if(shouldShowType('osint')) osintGeo.forEach(o=>{const p=D.tg.urgent[o.idx];if(!p)return;addPt(o.lat,o.lon,4,'rgba(255,184,76,0.7)','rgba(255,184,76,0.3)',ev=>showPopup(ev,(p.channel||'').toUpperCase(),cleanText(p.text?.substring(0,200)||''),`${p.views||'?'} views`),2)});
|
||
// WHO
|
||
const whoGeo=[{lat:0.3,lon:32.6},{lat:-6.2,lon:106.8},{lat:-4.3,lon:15.3},{lat:35,lon:105},{lat:12.5,lon:105},{lat:35,lon:105},{lat:28,lon:84},{lat:24,lon:45},{lat:30,lon:70},{lat:-0.8,lon:11.6}];
|
||
if(shouldShowType('health')) D.who.slice(0,10).forEach((w,i)=>{const c=whoGeo[i];if(!c)return;addPt(c.lat,c.lon,3.5,'rgba(105,240,174,0.6)','rgba(105,240,174,0.2)',ev=>showPopup(ev,w.title,w.summary||'','WHO'),2)});
|
||
// News
|
||
if(shouldShowType('news')) (D.news||[]).forEach(n=>{addPt(n.lat,n.lon,3,'rgba(129,212,250,0.6)','rgba(129,212,250,0.2)',ev=>showPopup(ev,n.source+' NEWS',cleanText(n.title),n.region),3)});
|
||
// NOAA weather
|
||
if(shouldShowType('weather')) (D.noaa?.alerts||[]).forEach(a=>{addPt(a.lat,a.lon,4,'rgba(255,152,0,0.7)','rgba(255,152,0,0.3)',ev=>showPopup(ev,a.event,a.headline||'','NOAA/NWS'),2)});
|
||
// EPA RadNet
|
||
if(shouldShowType('environment')) (D.epa?.stations||[]).forEach(s=>{addPt(s.lat,s.lon,3,'rgba(205,220,57,0.6)','rgba(205,220,57,0.2)',ev=>showPopup(ev,'RadNet: '+s.location,`${s.analyte||'--'}: ${s.result||'--'} ${s.unit||''}`,'EPA'),3)});
|
||
// Space stations
|
||
if(spaceDisplayMode === 'icons' && shouldShowType('space')) (D.space?.stationPositions||[]).forEach(s=>{
|
||
const g=addPt(s.lat,s.lon,5,'rgba(255,255,255,0.9)','rgba(255,255,255,0.4)',ev=>showPopup(ev,s.name,'Orbital position estimate','Space Station'),1);
|
||
if(g) g.append('text').attr('class','marker-label').attr('x',8).attr('y',3).attr('fill','rgba(255,255,255,0.7)').attr('font-size','8px').attr('font-family','var(--mono)').text(s.name.split('(')[0].trim());
|
||
});
|
||
// GDELT geo events
|
||
if(shouldShowType('gdelt')) (D.gdelt?.geoPoints||[]).forEach(g=>{addPt(g.lat,g.lon,2.5,'rgba(100,149,237,0.5)','rgba(100,149,237,0.2)',ev=>showPopup(ev,'GDELT Event',g.name||'','GDELT · '+g.count+' reports'),3)});
|
||
// ACLED
|
||
if(shouldShowType('conflict')) (D.acled?.deadliestEvents||[]).filter(e=>e.lat&&e.lon).forEach(e=>{
|
||
const[x,y]=proj([e.lon,e.lat]);if(!x||!y)return;
|
||
const r=Math.max(4,Math.min(14,2+Math.log2(Math.max(e.fatalities,1))*1.5));
|
||
const g=mg.append('g').attr('transform',`translate(${x},${y})`).style('cursor','pointer').attr('data-priority',1)
|
||
.on('click',ev=>{ev.stopPropagation();showPopup(ev,e.type||'CONFLICT',`${e.fatalities} fatalities<br>${e.location}, ${e.country}`,'ACLED')});
|
||
g.append('circle').attr('class','conflict-ring marker-circle').attr('r',r).attr('data-base-r',r).attr('fill','none').attr('stroke','rgba(255,120,80,0.7)').attr('stroke-width',1.5);
|
||
g.append('circle').attr('r',r*0.4).attr('fill','rgba(255,120,80,0.3)');
|
||
});
|
||
// Flight corridors
|
||
if(flightsVisible && shouldShowType('air')){
|
||
const airCoordsFlight=[{lat:30,lon:44},{lat:24,lon:120},{lat:49,lon:32},{lat:57,lon:24},{lat:14,lon:114},{lat:37,lon:127},{lat:25,lon:-80},{lat:4,lon:2},{lat:-34,lon:18},{lat:10,lon:51}];
|
||
const hubs=[{lat:40.6,lon:-73.8},{lat:51.5,lon:-0.5},{lat:25.3,lon:55.4},{lat:1.4,lon:103.8},{lat:-33.9,lon:151.2},{lat:-23.4,lon:-46.5}];
|
||
const cG=flatG.append('g').attr('class','corridors-layer');
|
||
for(let i=0;i<D.air.length;i++){for(let j=i+1;j<D.air.length;j++){
|
||
const a=D.air[i],b=D.air[j],from=airCoordsFlight[i],to=airCoordsFlight[j];
|
||
if(!from||!to)continue;const traffic=a.total+b.total;if(traffic<30)continue;
|
||
const ncR=(a.noCallsign+b.noCallsign)/Math.max(traffic,1);
|
||
const clr=ncR>0.15?'rgba(255,95,99,0.4)':ncR>0.05?'rgba(255,184,76,0.35)':'rgba(100,240,200,0.25)';
|
||
const interp=d3.geoInterpolate([from.lon,from.lat],[to.lon,to.lat]);
|
||
const coords=[];for(let k=0;k<=40;k++)coords.push(interp(k/40));
|
||
const feat={type:'Feature',geometry:{type:'LineString',coordinates:coords}};
|
||
cG.append('path').datum(feat).attr('d',flatPath).attr('fill','none').attr('stroke',clr).attr('stroke-width',Math.max(0.8,Math.min(3,traffic/80)));
|
||
}}
|
||
D.air.forEach((a,i)=>{if(!airCoordsFlight[i]||a.total<25)return;hubs.forEach(hub=>{
|
||
if(Math.abs(airCoordsFlight[i].lat-hub.lat)+Math.abs(airCoordsFlight[i].lon-hub.lon)<20)return;
|
||
const interp=d3.geoInterpolate([airCoordsFlight[i].lon,airCoordsFlight[i].lat],[hub.lon,hub.lat]);
|
||
const coords=[];for(let k=0;k<=40;k++)coords.push(interp(k/40));
|
||
cG.append('path').datum({type:'Feature',geometry:{type:'LineString',coordinates:coords}}).attr('d',flatPath).attr('fill','none').attr('stroke','rgba(100,240,200,0.15)').attr('stroke-width',0.6);
|
||
})});
|
||
}
|
||
}
|
||
|
||
// Update setRegion for flat mode
|
||
const _origSetRegion = setRegion;
|
||
|
||
// Override mapZoom for flat mode
|
||
const _origMapZoom = mapZoom;
|
||
|
||
function setRegion(r){
|
||
currentRegion = r;
|
||
document.querySelectorAll('.region-btn').forEach(b=>b.classList.toggle('active',b.dataset.region===r));
|
||
closePopup();
|
||
if(isFlat && flatSvg && flatZoom){
|
||
if(r==='world'){flatSvg.transition().duration(750).call(flatZoom.transform,d3.zoomIdentity);return;}
|
||
const bounds=flatRegionBounds[r];
|
||
const p0=flatProjection(bounds[0]),p1=flatProjection(bounds[1]);if(!p0||!p1)return;
|
||
const dx=Math.abs(p1[0]-p0[0]),dy=Math.abs(p1[1]-p0[1]);
|
||
const cx=(p0[0]+p1[0])/2,cy=(p0[1]+p1[1])/2;
|
||
const scale=Math.min(flatW/dx,flatH/dy)*0.85;
|
||
flatSvg.transition().duration(750).call(flatZoom.transform,d3.zoomIdentity.translate(flatW/2-scale*cx,flatH/2-scale*cy).scale(scale));
|
||
} else {
|
||
const pov=regionPOV[r]||regionPOV.world;
|
||
globe.pointOfView(pov,1000);
|
||
}
|
||
}
|
||
|
||
function mapZoom(factor){
|
||
if(isFlat && flatSvg && flatZoom){
|
||
flatSvg.transition().duration(300).call(flatZoom.scaleBy,factor);
|
||
} else if(globe){
|
||
const pov=globe.pointOfView();
|
||
globe.pointOfView({altitude:pov.altitude/factor},300);
|
||
}
|
||
}
|
||
|
||
// Sparkline SVG generator
|
||
function mkSparkSvg(values, isGood){
|
||
if(!values || values.length < 2) return '';
|
||
const w=52, h=18, pad=2;
|
||
const min=Math.min(...values), max=Math.max(...values);
|
||
const range=max-min||1;
|
||
const pts=values.map((v,i)=>{
|
||
const x=pad+(i/(values.length-1))*(w-pad*2);
|
||
const y=pad+((max-v)/range)*(h-pad*2);
|
||
return `${x.toFixed(1)},${y.toFixed(1)}`;
|
||
});
|
||
const cls=isGood?'spark-good':'spark-bad';
|
||
const last=pts[pts.length-1];
|
||
return `<svg class="spark-svg" viewBox="0 0 ${w} ${h}"><polyline class="spark-line ${cls}" points="${pts.join(' ')}"/><circle class="${cls} spark-dot" cx="${last.split(',')[0]}" cy="${last.split(',')[1]}" r="2" fill="${isGood?'var(--accent)':'var(--danger)'}"/></svg>`;
|
||
}
|
||
|
||
// === LOWER GRID ===
|
||
function renderLower(){
|
||
const mobile = isMobileLayout();
|
||
const spread=D.fred.find(f=>f.id==='T10Y2Y');
|
||
const ff=D.fred.find(f=>f.id==='DFF');
|
||
const ue=D.bls.find(b=>b.id==='LNS14000000');
|
||
const cpi=D.bls.find(b=>b.id==='CUUR0000SA0');
|
||
const payrolls=D.bls.find(b=>b.id==='CES0000000001');
|
||
const gscpi=D.gscpi;
|
||
const mkt=D.markets||{};
|
||
const metals=D.metals||{};
|
||
|
||
const wtiH = D.energy.wtiRecent||[];
|
||
const wtiMax=Math.max(...wtiH),wtiMin=Math.min(...wtiH);
|
||
const sparkHtml=wtiH.map(v=>{
|
||
const pct=wtiMax===wtiMin?50:((v-wtiMin)/(wtiMax-wtiMin))*100;
|
||
return `<div class="spark-bar" style="height:${Math.max(pct,8)}%"></div>`;
|
||
}).join('');
|
||
|
||
// Helper: format market quote card
|
||
const mktCard = (q) => {
|
||
if(!q||q.error) return '';
|
||
const clr = q.changePct>=0?'var(--accent)':'var(--warn)';
|
||
const arrow = q.changePct>=0?'▲':'▼';
|
||
const isCrypto = q.symbol.includes('BTC') || q.symbol.includes('ETH');
|
||
const isIndex = q.symbol.startsWith('^');
|
||
const price = isCrypto
|
||
? `$${q.price.toLocaleString()}`
|
||
: isIndex
|
||
? q.price.toLocaleString(undefined, {maximumFractionDigits: 2})
|
||
: `$${q.price}`;
|
||
return `<div class="mc"><div class="ml">${q.name||q.symbol}</div><span class="mv" style="color:${clr}">${price}</span><span class="ms" style="color:${clr}">${arrow} ${q.changePct>=0?'+':''}${q.changePct}%</span></div>`;
|
||
};
|
||
|
||
// VIX from Yahoo Finance live data (fallback to FRED)
|
||
const vixLive = mkt.vix;
|
||
const vixFred = D.fred.find(f=>f.id==='VIXCLS');
|
||
const vixVal = vixLive?.value || vixFred?.value;
|
||
const vixChg = vixLive?.changePct != null ? `${vixLive.changePct>=0?'+':''}${vixLive.changePct}%` : '';
|
||
const fmtMarketPrice = (price) => price != null ? `$${price.toLocaleString(undefined,{maximumFractionDigits:2})}` : '--';
|
||
const dayMove = (pct) => pct != null ? `${pct>=0?'+':''}${pct}% today` : '';
|
||
|
||
const metrics=[
|
||
{l:'WTI Crude',v:`$${D.energy.wti}`,s:'$/bbl',p:70},
|
||
{l:'Brent',v:`$${D.energy.brent}`,s:'$/bbl',p:75},
|
||
{l:'Nat Gas',v:`$${D.energy.natgas||'--'}`,s:'$/MMBtu',p:30},
|
||
{l:'Gold',v:fmtMarketPrice(metals.gold),s:dayMove(metals.goldChangePct)||'COMEX proxy',p:58},
|
||
{l:'Silver',v:fmtMarketPrice(metals.silver),s:dayMove(metals.silverChangePct)||'COMEX proxy',p:54},
|
||
{l:'VIX',v:vixVal?vixVal.toFixed(1):'--',s:vixChg||'volatility index',p:vixVal?Math.min(vixVal*2.5,100):30},
|
||
{l:'Fed Funds',v:ff?`${ff.value}%`:'--',s:ff?.date||'',p:36},
|
||
{l:'GSCPI',v:gscpi?gscpi.value.toFixed(2):'--',s:gscpi?.interpretation||'',p:49},
|
||
{l:'CPI MoM',v:cpi?`+${cpi.momChangePct?.toFixed(2)}%`:'--',s:cpi?.date||'',p:37},
|
||
{l:'Unemployment',v:ue?`${ue.value}%`:'--',s:ue?`${ue.momChange>0?'+':''}${ue.momChange} vs prior`:'',p:44},
|
||
];
|
||
|
||
// Attach sparklines from FRED recent data
|
||
const fredSpark = (id, up) => {
|
||
const f = D.fred.find(f=>f.id===id);
|
||
return f?.recent?.length > 1 ? {spark: f.recent, sparkUp: up} : {};
|
||
};
|
||
metrics[0] = {...metrics[0], spark: D.energy.wtiRecent, sparkUp: false};
|
||
metrics[3] = {...metrics[3], spark: metals.goldRecent, sparkUp: (metals.goldChangePct ?? 0) >= 0};
|
||
metrics[4] = {...metrics[4], spark: metals.silverRecent, sparkUp: (metals.silverChangePct ?? 0) >= 0};
|
||
|
||
// Build live market cards from Yahoo Finance
|
||
const indexCards = (mkt.indexes||[]).map(mktCard).join('');
|
||
const cryptoCards = (mkt.crypto||[]).map(mktCard).join('');
|
||
const rateCards = (mkt.rates||[]).map(mktCard).join('');
|
||
const hasMarkets = indexCards || cryptoCards;
|
||
|
||
const srcHtml=D.health.map(s=>`<div class="src-item"><div class="sd ${s.err?'err':'ok'}"></div><span>${s.n}</span></div>`).join('');
|
||
|
||
// NEWS TICKER — merges RSS + GDELT + Telegram into flowing cards (moved from right rail)
|
||
const feed = (D.newsFeed || []).slice(0, 20);
|
||
const srcClass = s => {
|
||
if (!s) return 'other';
|
||
const sl = s.toLowerCase();
|
||
// Africa-focused sources first (before generic DW/NYT)
|
||
if (sl.includes('dw africa') || sl.includes('africa news') || sl.includes('nyt africa') || sl.includes('rfi')) return 'af';
|
||
if (sl.includes('mercopress')) return 'sa';
|
||
if (sl.includes('indian express') || sl.includes('the hindu')) return 'ind';
|
||
if (sl.includes('sbs')) return 'anz';
|
||
if (sl.includes('bbc')) return 'bbc';
|
||
if (sl.includes('jazeera') || sl.includes('alj')) return 'alj';
|
||
if (sl.includes('gdelt')) return 'gdelt';
|
||
if (sl.includes('telegram')) return 'tg';
|
||
if (sl.includes('npr')) return 'us';
|
||
if (sl.includes('dw') || sl.includes('deutsche')) return 'dw';
|
||
if (sl.includes('france') || sl.includes('euronews')) return 'eu';
|
||
if (sl.includes('nyt') || sl.includes('times')) return 'nyt';
|
||
return 'other';
|
||
};
|
||
const tickerCards = feed.map(n => {
|
||
const sc = srcClass(n.source);
|
||
const age = n.timestamp ? getAge(n.timestamp) : '';
|
||
const urlAttr = n.url ? ` data-url="${String(n.url).replace(/&/g,'&').replace(/"/g,'"')}"` : '';
|
||
return `<div class="tk-card ${n.urgent?'urgent':''} ${n.url?'clickable':''}"${urlAttr}><span class="tk-src ${sc}">${(n.source||'NEWS').substring(0,12)}</span><span class="tk-time">${age}</span><div class="tk-head">${cleanText(n.headline||'')}</div>${n.url?'<span class="tk-link">↗</span>':''}</div>`;
|
||
}).join('');
|
||
const tickerDuration = Math.max(20, feed.length * 2.5);
|
||
|
||
// Leverageable Ideas (LLM-only feature)
|
||
const hasIdeas = D.ideas && D.ideas.length > 0;
|
||
const ideasHtml = hasIdeas ? (D.ideas||[]).map(idea=>`
|
||
<div class="idea-card">
|
||
<span class="idea-type ${(idea.type||'').toLowerCase()}">${(idea.type||'').toUpperCase()}</span>
|
||
${idea.ticker ? `<span class="idea-horizon">${idea.ticker}</span>` : ''}
|
||
${idea.horizon ? `<span class="idea-horizon">${idea.horizon}</span>` : ''}
|
||
<span class="idea-conf">${idea.confidence} confidence</span>
|
||
<div class="idea-title">${idea.title}</div>
|
||
<div class="idea-text">${idea.text||idea.rationale||''}</div>
|
||
${idea.risk ? `<div class="idea-text" style="color:var(--warn);margin-top:3px">Risk: ${idea.risk}</div>` : ''}
|
||
</div>`).join('') : `<div style="padding:20px;text-align:center;color:var(--dim);font-family:var(--mono);font-size:11px">
|
||
<div style="font-size:24px;margin-bottom:8px;opacity:0.3">⚠</div>
|
||
<div>LLM NOT CONFIGURED</div>
|
||
<div style="font-size:9px;margin-top:6px;opacity:0.6">Set LLM_PROVIDER + credentials in .env to enable AI-powered trade ideas</div>
|
||
</div>`;
|
||
|
||
|
||
const tickerPanel = `<div class="g-panel lp-ticker" style="display:flex;flex-direction:column">
|
||
<div class="sec-head"><h3>${t('panels.newsTicker','Live News Ticker')}</h3><span class="badge">${feed.length} ${t('badges.items','ITEMS')}</span></div>
|
||
<div class="ticker-wrap" style="--ticker-duration:${tickerDuration}s">
|
||
<div class="ticker-track">${tickerCards}${lowPerfMode ? '' : tickerCards}</div>
|
||
</div>
|
||
</div>`;
|
||
const osintPanel = mobile ? buildOsintPanel('lp-osint', 240) : '';
|
||
const macroPanel = `<div class="g-panel lp-macro">
|
||
<div class="sec-head"><h3>${t('panels.macroMarkets','Macro + Markets')}</h3><span class="badge">${mkt.timestamp?t('badges.live','LIVE'):t('badges.delayed','DELAYED')}</span></div>
|
||
${hasMarkets?`<div style="margin-bottom:8px">
|
||
<div style="font-family:var(--mono);font-size:9px;color:var(--dim);margin-bottom:4px;letter-spacing:1px">INDEXES</div>
|
||
<div class="metrics-row">${indexCards}</div>
|
||
</div>
|
||
<div style="margin-bottom:8px">
|
||
<div style="font-family:var(--mono);font-size:9px;color:var(--dim);margin-bottom:4px;letter-spacing:1px">CRYPTO</div>
|
||
<div class="metrics-row">${cryptoCards}</div>
|
||
</div>`:''}
|
||
<div style="margin-bottom:8px">
|
||
<div style="font-family:var(--mono);font-size:9px;color:var(--dim);margin-bottom:4px;letter-spacing:1px">ENERGY + METALS + MACRO</div>
|
||
<div class="metrics-row">${metrics.map(m=>{
|
||
const sparkSvg = m.spark ? mkSparkSvg(m.spark, m.sparkUp) : '';
|
||
return `<div class="mc"><div class="ml">${m.l}</div><span class="mv">${m.v}${sparkSvg}</span><span class="ms">${m.s}</span><div class="mbar"><span style="width:${m.p}%"></span></div></div>`;
|
||
}).join('')}</div>
|
||
</div>
|
||
<div style="margin-top:6px">
|
||
<div style="font-family:var(--mono);font-size:10px;color:var(--dim);margin-bottom:4px">WTI 5-DAY</div>
|
||
<div class="spark">${sparkHtml}</div>
|
||
</div>
|
||
</div>`;
|
||
const ideasPanel = `<div class="g-panel lp-ideas">
|
||
<div class="sec-head"><h3>${t('panels.tradeIdeas','Leverageable Ideas')}</h3>${D.ideasSource==='llm'?'<span class="ideas-src llm">'+t('ideas.aiEnhanced','AI ENHANCED')+'</span>':D.ideasSource==='disabled'?'<span class="ideas-src static">'+t('ideas.llmOff','LLM OFF')+'</span>':'<span class="ideas-src static">'+t('ideas.pending','PENDING')+'</span>'}</div>
|
||
${ideasHtml}
|
||
<div class="disclosure">FOR INFORMATIONAL PURPOSES ONLY. This is not financial advice, a recommendation to buy or sell any security, or a solicitation of any kind. All signal-based observations are derived from publicly available OSINT data and should not be relied upon for investment decisions. Consult a licensed financial advisor before making any investment. Past performance does not guarantee future results.</div>
|
||
</div>`;
|
||
document.getElementById('lowerGrid').innerHTML=`${tickerPanel}${osintPanel}${macroPanel}${ideasPanel}`;
|
||
}
|
||
|
||
async function runTerminalAction(action){
|
||
if(terminalBusy) return;
|
||
terminalBusy = true;
|
||
terminalOutput = `> ${action}\nRunning...`;
|
||
renderRight();
|
||
try{
|
||
const res = await fetch('/api/action', {
|
||
method:'POST',
|
||
headers:{
|
||
'Content-Type':'application/json',
|
||
...(localStorage.getItem('crucix_sweep_token') ? {'x-crucix-token': localStorage.getItem('crucix_sweep_token')} : {})
|
||
},
|
||
body:JSON.stringify({action})
|
||
});
|
||
const payload = await res.json().catch(()=>({error:'Invalid server response'}));
|
||
if(!res.ok) throw new Error(payload.error || `HTTP ${res.status}`);
|
||
if(action === 'status'){
|
||
const h = payload.health || {};
|
||
terminalOutput = [
|
||
'> status',
|
||
`State: ${h.status || '--'}`,
|
||
`Last sweep: ${h.lastSuccessfulSweep || h.lastSweep || '--'}`,
|
||
`Data age: ${h.dataAgeSeconds != null ? h.dataAgeSeconds + 's' : '--'}`,
|
||
`Sources: ${h.sourcesOk || 0} ok / ${h.sourcesDegraded || 0} degraded / ${h.sourcesFailed || 0} failed`,
|
||
`LLM: ${h.llm?.state || '--'}`,
|
||
`Sweep active: ${h.sweepInProgress ? 'yes' : 'no'}`
|
||
].join('\n');
|
||
}else if(action === 'brief'){
|
||
terminalOutput = `> brief\n${payload.text || 'No briefing text returned.'}`;
|
||
}else if(action === 'sweep'){
|
||
terminalOutput = `> sweep\n${payload.status === 'already_running' ? 'Sweep already running.' : 'Sweep accepted. The dashboard will update when the sweep finishes.'}`;
|
||
}
|
||
}catch(err){
|
||
terminalOutput = `> ${action}\nERROR: ${err.message}`;
|
||
}finally{
|
||
terminalBusy = false;
|
||
renderRight();
|
||
}
|
||
}
|
||
|
||
// === RIGHT RAIL ===
|
||
function renderRight(){
|
||
const mobile = isMobileLayout();
|
||
// CROSS-SOURCE SIGNALS — moved from lower grid to right rail
|
||
const signals=D.tSignals.slice(0,6).map((s,i)=>`<div class="signal-row"><strong>Signal ${i+1}</strong><p>${s}</p></div>`).join('');
|
||
|
||
// OSINT TICKER — Telegram + WHO as flowing cards
|
||
const signalMetrics=[
|
||
{l:'Incident Tempo',v:D.tg.urgent.length,p:70},
|
||
{l:'Air Theaters',v:D.air.length,p:60},
|
||
{l:'Thermal Spikes',v:D.thermal.reduce((s,t)=>s+t.hc,0),p:80},
|
||
{l:'SDR Nodes',v:D.sdr.total,p:92},
|
||
{l:'Chokepoints',v:D.chokepoints.length,p:50},
|
||
{l:'WHO Alerts',v:D.who.length,p:40}
|
||
];
|
||
|
||
// DELTA PANEL — what changed since last sweep
|
||
const delta = D.delta || {};
|
||
const ds = delta.summary || {};
|
||
const hasDelta = ds.totalChanges > 0;
|
||
const dirEmoji = {'risk-off':'▲','risk-on':'▼','mixed':'◆'}[ds.direction]||'◆';
|
||
const dirClass = {'risk-off':'up','risk-on':'down','mixed':''}[ds.direction]||'';
|
||
const escalated = (delta.signals?.escalated || []).slice(0,6);
|
||
const deescalated = (delta.signals?.deescalated || []).slice(0,4);
|
||
const newSigs = (delta.signals?.new || []).slice(0,4);
|
||
const deltaRows = [];
|
||
for(const s of newSigs){
|
||
deltaRows.push(`<div class="delta-row new"><span class="delta-badge new">NEW</span><span class="delta-label">${s.reason||s.label||s.key}</span></div>`);
|
||
}
|
||
for(const s of escalated){
|
||
const sev = s.severity==='critical'?'style="color:var(--warn);font-weight:600"':s.severity==='high'?'style="color:#ffab40"':'';
|
||
const val = s.pctChange!==undefined?`${s.pctChange>0?'+':''}${s.pctChange}%`:`${s.change>0?'+':''}${s.change}`;
|
||
deltaRows.push(`<div class="delta-row"><span class="delta-badge up">▲</span><span class="delta-label" ${sev}>${s.label}</span><span class="delta-val">${s.from}→${s.to} (${val})</span></div>`);
|
||
}
|
||
for(const s of deescalated){
|
||
const val = s.pctChange!==undefined?`${s.pctChange}%`:`${s.change}`;
|
||
deltaRows.push(`<div class="delta-row"><span class="delta-badge down">▼</span><span class="delta-label">${s.label||s.key}</span><span class="delta-val">${s.from}→${s.to} (${val})</span></div>`);
|
||
}
|
||
const deltaHtml = hasDelta ? deltaRows.join('') : `<div style="padding:12px;text-align:center;color:var(--dim);font-family:var(--mono);font-size:10px">${t('delta.noChanges','No changes since last sweep')}</div>`;
|
||
const scenarioItems = (D.scenarios?.items || []).filter(s => s.enabled || s.state !== 'dormant').slice(0,4);
|
||
const scenarioHtml = scenarioItems.length ? scenarioItems.map(s => `
|
||
<div class="signal-row">
|
||
<strong>${s.name} <span class="delta-badge ${s.changed?'new':''}">${(s.state||'dormant').toUpperCase()}</span></strong>
|
||
<p>${s.description || ''}</p>
|
||
<div class="layer-sub">${s.confidence || 0}% confidence · score ${s.score || 0}${s.lastTriggerTime ? ' · ' + getAge(s.lastTriggerTime) : ''}</div>
|
||
</div>
|
||
`).join('') : `<div style="padding:12px;text-align:center;color:var(--dim);font-family:var(--mono);font-size:10px">No active scenario watchlist items</div>`;
|
||
|
||
document.getElementById('rightRail').innerHTML=`
|
||
<div class="g-panel right-actions">
|
||
<div class="sec-head"><h3>Terminal Actions</h3><span class="badge">${terminalBusy?'RUNNING':'READY'}</span></div>
|
||
<div class="action-grid">
|
||
<button class="action-btn" ${terminalBusy?'disabled':''} onclick="runTerminalAction('status')">Status</button>
|
||
<button class="action-btn" ${terminalBusy?'disabled':''} onclick="runTerminalAction('sweep')">Sweep</button>
|
||
<button class="action-btn" ${terminalBusy?'disabled':''} onclick="runTerminalAction('brief')">Brief</button>
|
||
</div>
|
||
<div class="terminal-output">${terminalOutput.replace(/[&<>]/g,c=>({'&':'&','<':'<','>':'>'}[c])).replace(/\n/g,'<br>')}</div>
|
||
</div>
|
||
<div class="g-panel right-signals">
|
||
<div class="sec-head"><h3>${t('panels.crossSourceSignals','Cross-Source Signals')}</h3><span class="badge">${t('badges.worldview','WORLDVIEW')}</span></div>
|
||
${signals}
|
||
</div>
|
||
<div class="g-panel right-scenarios">
|
||
<div class="sec-head"><h3>Scenario Watchlist</h3><span class="badge">${D.scenarios?.available===false?'CONFIG':'LIVE'}</span></div>
|
||
${scenarioHtml}
|
||
</div>
|
||
${mobile ? '' : buildOsintPanel('right-osint', 260)}
|
||
<div class="g-panel right-core">
|
||
<div class="sec-head"><h3>${t('panels.signalCore','Signal Core')}</h3><span class="badge">${t('badges.hotMetrics','HOT METRICS')}</span></div>
|
||
${signalMetrics.map(s=>`<div class="sm"><span class="sml">${s.l}</span><div class="smb"><span style="width:${s.p}%"></span></div><span class="smv">${s.v}</span></div>`).join('')}
|
||
</div>
|
||
<div class="g-panel right-delta">
|
||
<div class="sec-head"><h3>${t('panels.sweepDelta','Sweep Delta')}</h3><span class="badge ${dirClass}">${dirEmoji} ${ds.direction?t('delta.'+ds.direction,ds.direction.toUpperCase()):t('delta.baseline','BASELINE')}</span></div>
|
||
${hasDelta?`<div style="display:flex;flex-wrap:wrap;gap:8px;margin-bottom:6px;font-family:var(--mono);font-size:10px">
|
||
<span style="color:var(--dim)">${t('delta.changes','Changes')}: <span style="color:var(--accent)">${ds.totalChanges}</span></span>
|
||
<span style="color:var(--dim)">${t('delta.critical','Critical')}: <span style="color:${ds.criticalChanges>0?'var(--warn)':'var(--dim)'}">${ds.criticalChanges||0}</span></span>
|
||
${ds.signalBreakdown?`<span style="color:var(--dim)">${t('delta.new','New')}: <span style="color:#4dd0e1">${ds.signalBreakdown.new}</span> ↑${ds.signalBreakdown.escalated} ↓${ds.signalBreakdown.deescalated}</span>`:''}
|
||
</div>`:''}
|
||
<div class="delta-list">${deltaHtml}</div>
|
||
</div>`;
|
||
}
|
||
|
||
// === HELPERS ===
|
||
function getAge(d){const ms=Date.now()-new Date(d).getTime();const h=Math.floor(ms/3600000);if(h<1)return 'just now';if(h<24)return h+'h ago';return Math.floor(h/24)+'d ago'}
|
||
function cleanText(t){return t.replace(/'/g,"'").replace(/!/g,"!").replace(/&/g,"&").replace(/<[^>]+>/g,'')}
|
||
function safeExternalUrl(raw){try{const u=new URL(raw,location.href);return u.protocol==='http:'||u.protocol==='https:'?u.toString():null}catch{return null}}
|
||
|
||
// === BOOT SEQUENCE ===
|
||
function runBoot(){
|
||
const acledStatus = D.acled?.totalEvents > 0 ? `<span class="ok">${D.acled.totalEvents} EVENTS</span>` : '<span style="color:var(--warn)">DEGRADED</span>';
|
||
const lines=[
|
||
{text:t('boot.initializing','INITIALIZING CRUCIX ENGINE v2.1.0'),delay:0},
|
||
{text:t('boot.connecting','CONNECTING {count} OSINT SOURCES...').replace('{count}',D.meta.sourcesQueried),delay:400},
|
||
{text:'├─ '+t('boot.sourceGroup1','OPENSKY · FIRMS · KIWISDR · MARITIME'),delay:700},
|
||
{text:'├─ '+t('boot.sourceGroup2','FRED · BLS · EIA · TREASURY · GSCPI'),delay:900},
|
||
{text:'├─ '+t('boot.sourceGroup3','TELEGRAM · SAFECAST · EPA · WHO · OFAC'),delay:1100},
|
||
{text:'└─ '+t('boot.sourceGroup4','GDELT · NOAA · PATENTS · BLUESKY · REDDIT'),delay:1300},
|
||
{text:t('boot.sweepComplete','SWEEP COMPLETE — {ok}/{total} SOURCES').replace('{ok}',`<span class="count">${D.meta.sourcesOk}</span>`).replace('{total}',D.meta.sourcesQueried)+' <span class="ok">'+t('boot.ok','OK')+'</span>',delay:1700},
|
||
{text:t('boot.acledLayer','ACLED CONFLICT LAYER')+': '+acledStatus,delay:1900},
|
||
{text:t('boot.flightCorridors','FLIGHT CORRIDORS')+': <span class="ok">'+t('boot.active','ACTIVE')+'</span> · '+t('boot.dualProjection','DUAL PROJECTION')+': <span class="ok">'+t('boot.ready','READY')+'</span>',delay:2100},
|
||
{text:t('boot.intelligenceSynthesis','INTELLIGENCE SYNTHESIS')+': <span class="ok">'+t('boot.active','ACTIVE')+'</span>',delay:2400},
|
||
];
|
||
const container=document.getElementById('bootLines');
|
||
document.getElementById('bootFinal').textContent=t('dashboard.terminalActive','TERMINAL ACTIVE');
|
||
const tl=gsap.timeline();
|
||
tl.to('.logo-ring',{opacity:1,duration:0.6,ease:'power2.out'},0);
|
||
tl.to(container,{opacity:1,duration:0.3},0.3);
|
||
lines.forEach(line=>{
|
||
tl.call(()=>{
|
||
const div=document.createElement('div');div.innerHTML=line.text;div.style.opacity='0';
|
||
container.appendChild(div);gsap.to(div,{opacity:1,duration:0.2});
|
||
},[],line.delay/1000+0.5);
|
||
});
|
||
tl.to('#bootFinal',{opacity:1,duration:0.4},3.1);
|
||
tl.to('#boot',{opacity:0,duration:0.5,ease:'power2.in'},3.7);
|
||
tl.set('#boot',{display:'none'},4.2);
|
||
tl.to('#bgRadial',{opacity:1,duration:1},3.8);
|
||
tl.to('#bgGrid',{opacity:1,duration:1.2},4.0);
|
||
tl.to('#scanline',{opacity:1,duration:0.8},4.3);
|
||
tl.to('#main',{opacity:1,duration:0.6},3.9);
|
||
tl.call(()=>{
|
||
gsap.from('.g-panel,.topbar,.map-container',{opacity:0,y:20,scale:0.97,duration:0.5,stagger:0.06,ease:'power2.out'});
|
||
setTimeout(()=>gsap.from('.layer-item,.site-row,.econ-row',{opacity:0,x:-12,duration:0.25,stagger:0.03,ease:'power1.out'}),500);
|
||
setTimeout(()=>gsap.from('.ic',{opacity:0,y:12,duration:0.25,stagger:0.03,ease:'power1.out'}),600);
|
||
setTimeout(()=>gsap.from('.mc',{opacity:0,y:8,duration:0.25,stagger:0.04,ease:'power1.out'}),800);
|
||
setTimeout(()=>gsap.from('.idea-card',{opacity:0,x:12,duration:0.3,stagger:0.06,ease:'power1.out'}),900);
|
||
setTimeout(()=>{
|
||
document.querySelectorAll('.mbar span,.smb span').forEach(bar=>{const w=bar.style.width;bar.style.width='0%';gsap.to(bar,{width:w,duration:1,ease:'power2.out'})});
|
||
document.querySelectorAll('.spark-bar').forEach(bar=>{const h=bar.style.height;bar.style.height='0%';gsap.to(bar,{height:h,duration:0.8,ease:'power2.out'})});
|
||
},1000);
|
||
},[],4.0);
|
||
}
|
||
|
||
function isMobileLayout(){ return window.innerWidth <= 1100; }
|
||
|
||
function buildOsintPanel(panelClass='', maxHeight=260){
|
||
const allPosts=[...D.tg.urgent,...D.tg.topPosts].sort((a,b)=>new Date(b.date||0)-new Date(a.date||0));
|
||
const whoItems=D.who.slice(0,4).map(w=>({channel:'WHO ALERT',text:w.title,date:w.date,isWho:true}));
|
||
const osintItems=[...allPosts.slice(0,15),...whoItems];
|
||
const osintCards=osintItems.map(p=>{
|
||
const isU=p.urgentFlags&&p.urgentFlags.length>0;
|
||
const views=p.views?p.views>=1000?`${(p.views/1000).toFixed(0)}K`:p.views:'';
|
||
const age=p.date?getAge(p.date):'';
|
||
const flags=(p.urgentFlags||[]).map(f=>`<span class="tk-src tg" style="margin-right:2px">${f}</span>`).join('');
|
||
const srcCls=p.isWho?'style="color:#69f0ae;border-color:rgba(105,240,174,0.4)"':'class="tk-src tg"';
|
||
return `<div class="tk-card ${isU?'urgent':''}"><span ${srcCls}>${(p.channel||'OSINT').toUpperCase().substring(0,14)}</span>${views?`<span class="tk-src other">${views}</span>`:''}<span class="tk-time">${age}</span>${flags}<div class="tk-head">${cleanText((p.text||'').substring(0,160))}</div></div>`;
|
||
}).join('');
|
||
const osintDuration=Math.max(25,osintItems.length*3);
|
||
return `<div class="g-panel ${panelClass}" style="display:flex;flex-direction:column">
|
||
<div class="sec-head"><h3>${t('panels.osintStream','OSINT Stream')}</h3><span class="badge">${D.tg.urgent.length} ${t('badges.urgent','URGENT')}</span></div>
|
||
<div class="ticker-wrap" style="--ticker-duration:${osintDuration}s;max-height:${maxHeight}px">
|
||
<div class="ticker-track">${osintCards}${lowPerfMode ? '' : osintCards}</div>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
function renderGlossary(){
|
||
const body = document.getElementById('glossaryBody');
|
||
if(!body) return;
|
||
body.innerHTML = signalGuideItems.map(item => `
|
||
<div class="glossary-card">
|
||
<div class="glossary-term">
|
||
<strong>${item.term}</strong>
|
||
<span class="glossary-tag">${item.category}</span>
|
||
</div>
|
||
<div class="glossary-line"><span class="glossary-label">Meaning</span>${item.meaning}</div>
|
||
<div class="glossary-line"><span class="glossary-label">Why it matters</span>${item.matters}</div>
|
||
<div class="glossary-line"><span class="glossary-label">Not proof of</span>${item.notMeaning}</div>
|
||
<div class="glossary-line"><span class="glossary-label">Example</span>${item.example}</div>
|
||
</div>
|
||
`).join('');
|
||
}
|
||
|
||
function openGlossary(){
|
||
const overlay = document.getElementById('glossaryOverlay');
|
||
if(!overlay) return;
|
||
overlay.classList.add('show');
|
||
document.body.style.overflow = 'hidden';
|
||
}
|
||
|
||
function closeGlossary(){
|
||
const overlay = document.getElementById('glossaryOverlay');
|
||
if(!overlay) return;
|
||
overlay.classList.remove('show');
|
||
document.body.style.overflow = '';
|
||
}
|
||
|
||
function refreshMapViewport(forceGlobeReflow=false){
|
||
const container = document.getElementById('mapContainer');
|
||
if(!container) return;
|
||
const width = container.clientWidth;
|
||
const height = container.clientHeight || (isMobileLayout() ? 420 : 560);
|
||
if(globe){
|
||
globe.width(width).height(height);
|
||
if(forceGlobeReflow && !isFlat){
|
||
const globeEl = document.getElementById('globeViz');
|
||
globeEl.style.display = 'none';
|
||
requestAnimationFrame(() => {
|
||
globeEl.style.display = 'block';
|
||
globe.width(width).height(height);
|
||
});
|
||
}
|
||
}
|
||
if(flatSvg){
|
||
flatW = width;
|
||
flatH = height;
|
||
flatSvg.attr('viewBox',`0 0 ${flatW} ${flatH}`).attr('preserveAspectRatio','xMidYMid meet');
|
||
if(flatProjection && flatG){
|
||
flatProjection = d3.geoNaturalEarth1().fitSize([flatW-20,flatH-20],{type:'Sphere'}).translate([flatW/2,flatH/2]);
|
||
flatPath = d3.geoPath(flatProjection);
|
||
flatG.selectAll('*').remove();
|
||
drawFlatMap();
|
||
}
|
||
}
|
||
}
|
||
|
||
let lastResponsiveMobile = null;
|
||
function syncResponsiveLayout(force=false){
|
||
const mobileNow = isMobileLayout();
|
||
if(force || lastResponsiveMobile === null || mobileNow !== lastResponsiveMobile){
|
||
lastResponsiveMobile = mobileNow;
|
||
renderTopbar();
|
||
renderLeftRail();
|
||
renderLower();
|
||
renderRight();
|
||
}
|
||
refreshMapViewport(force && !isFlat);
|
||
}
|
||
|
||
// === REINIT (for live updates without boot sequence) ===
|
||
function reinit(){
|
||
renderTopbar();renderLeftRail();renderLower();renderRight();
|
||
plotMarkers();
|
||
}
|
||
|
||
// === SSE: Live Updates from Server ===
|
||
function connectSSE(){
|
||
if (typeof EventSource === 'undefined') return;
|
||
// Only connect if served from localhost (not file://)
|
||
if (location.protocol === 'file:') return;
|
||
|
||
const es = new EventSource('/events');
|
||
es.onmessage = (e) => {
|
||
try {
|
||
const msg = JSON.parse(e.data);
|
||
if (msg.type === 'update' && msg.data) {
|
||
D = msg.data;
|
||
reinit();
|
||
// Flash the topbar to indicate update
|
||
const topbar = document.querySelector('.topbar');
|
||
if (topbar) {
|
||
topbar.style.borderColor = 'var(--accent)';
|
||
setTimeout(() => topbar.style.borderColor = '', 1500);
|
||
}
|
||
} else if (msg.type === 'sweep_start') {
|
||
const badge = document.querySelector('.alert-badge');
|
||
if (badge) { badge.textContent = 'SWEEPING...'; badge.style.borderColor = 'var(--accent)'; }
|
||
}
|
||
} catch {}
|
||
};
|
||
es.onerror = () => {
|
||
// Reconnect after 5s on error
|
||
es.close();
|
||
setTimeout(connectSSE, 5000);
|
||
};
|
||
}
|
||
|
||
// === INIT ===
|
||
let booted = false;
|
||
function init(){
|
||
renderTopbar();renderLeftRail();renderLower();renderRight();
|
||
renderGlossary();
|
||
initMap();
|
||
if (!booted) { runBoot(); booted = true; }
|
||
// Close popup on click outside markers
|
||
document.getElementById('mapContainer').addEventListener('click',e=>{
|
||
if(!e.target.closest('.map-popup')) closePopup();
|
||
});
|
||
// Open article links from ticker cards
|
||
document.addEventListener('click',e=>{
|
||
const card=e.target.closest('.tk-card[data-url]');
|
||
if(card){
|
||
const url=safeExternalUrl(card.dataset.url);
|
||
if(url) window.open(url,'_blank','noopener');
|
||
}
|
||
});
|
||
document.addEventListener('keydown',e=>{
|
||
if(e.key === 'Escape') closeGlossary();
|
||
});
|
||
syncResponsiveLayout(true);
|
||
}
|
||
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
const hasInlineData = !!(D && D.meta);
|
||
const canProbeApi = location.protocol !== 'file:';
|
||
|
||
if (canProbeApi) {
|
||
// Server mode: always fetch live data from API (ignore any stale inline D)
|
||
fetch('/api/data')
|
||
.then(r => { if(!r.ok) throw new Error(`HTTP ${r.status}`); return r.json(); })
|
||
.then(data => { D = data; init(); connectSSE(); })
|
||
.catch(() => {
|
||
// Should not reach here — server routes to loading.html when no data
|
||
if (D && D.meta) { init(); connectSSE(); }
|
||
});
|
||
} else if (hasInlineData) {
|
||
// File mode: use inline data
|
||
init();
|
||
}
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|