The isServer detection required location.hostname === 'localhost', which fails in Docker containers where the hostname is the container ID. Now any non-file: protocol correctly triggers API fetch and SSE. Fixes #3
1210 lines
68 KiB
HTML
1210 lines
68 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))}
|
|
|
|
/* 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-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-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}
|
|
/* 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}
|
|
.lower .lp-delta{flex:1 1 200px;max-width:300px}
|
|
.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}
|
|
.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: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.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)}
|
|
|
|
/* 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){.grid{grid-template-columns:1fr}.lower .lp-ticker,.lower .lp-delta,.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)}}
|
|
|
|
/* 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-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-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>
|
|
<script>
|
|
// === DATA ===
|
|
let D = null;
|
|
// === GLOBALS ===
|
|
let globe = null;
|
|
let flightsVisible = true;
|
|
let isFlat = true;
|
|
let flatSvg, flatProjection, flatPath, flatG, flatZoom, flatW, flatH;
|
|
const regionPOV = {
|
|
world: { lat: 20, lng: 20, altitude: 2.5 },
|
|
americas: { lat: 15, lng: -80, altitude: 1.6 },
|
|
europe: { lat: 50, lng: 15, altitude: 1.2 },
|
|
middleEast: { lat: 28, lng: 45, altitude: 1.4 },
|
|
asiaPacific: { lat: 25, lng: 110, altitude: 1.6 },
|
|
africa: { lat: 5, lng: 20, altitude: 1.5 }
|
|
};
|
|
|
|
// === TOPBAR ===
|
|
function renderTopbar(){
|
|
const ts = new Date(D.meta.timestamp);
|
|
const d = ts.toLocaleDateString('en-US',{month:'short',day:'numeric',year:'numeric'}).toUpperCase();
|
|
const t = 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>
|
|
<div class="top-center">
|
|
${['world','americas','europe','middleEast','asiaPacific','africa'].map(r=>
|
|
`<button class="region-btn ${r==='world'?'active':''}" data-region="${r}" onclick="setRegion('${r}')">${r==='middleEast'?'MIDDLE EAST':r==='asiaPacific'?'ASIA PACIFIC':r.toUpperCase()}</button>`
|
|
).join('')}
|
|
</div>
|
|
<div class="top-right">
|
|
<span class="meta-pill">SWEEP <span class="v">${(D.meta.totalDurationMs/1000).toFixed(1)}s</span></span>
|
|
<span class="meta-pill">${d} <span class="v">${t}</span></span>
|
|
<span class="meta-pill">SOURCES <span class="v">${D.meta.sourcesOk}/${D.meta.sourcesQueried}</span></span>
|
|
${D.delta?.summary ? `<span class="meta-pill">DELTA <span class="v">${D.delta.summary.direction==='risk-off'?'▲ RISK-OFF':D.delta.summary.direction==='risk-on'?'▼ RISK-ON':'◆ MIXED'}</span></span>` : ''}
|
|
<span class="alert-badge">HIGH ALERT</span>
|
|
</div>`;
|
|
}
|
|
|
|
// === LEFT RAIL ===
|
|
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=[
|
|
{name:'Air Activity',count:totalAir,dot:'air',sub:`${D.air.length} theaters`},
|
|
{name:'Thermal Spikes',count:totalThermal.toLocaleString(),dot:'thermal',sub:`${totalNight.toLocaleString()} night det.`},
|
|
{name:'SDR Coverage',count:D.sdr.total,dot:'sdr',sub:`${D.sdr.online} online`},
|
|
{name:'Maritime Watch',count:D.chokepoints.length,dot:'maritime',sub:'chokepoints'},
|
|
{name:'Nuclear Sites',count:D.nuke.length,dot:'nuke',sub:'monitors'},
|
|
{name:'Conflict Events',count:conflictEvents,dot:'thermal',sub:`${conflictFatal.toLocaleString()} fatalities`},
|
|
{name:'Health Watch',count:D.who.length,dot:'health',sub:'WHO alerts'},
|
|
{name:'World News',count:newsCount,dot:'news',sub:'RSS geolocated'},
|
|
{name:'OSINT Feed',count:D.tg.posts,dot:'incident',sub:`${D.tg.urgent.length} urgent`},
|
|
{name:'Satellites',count:D.space?.militarySats||0,dot:'space',sub:`${D.space?.totalNewObjects||0} 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>Sensor Grid</h3><span class="badge">LIVE</span></div>
|
|
${layers.map(l=>`<div class="layer-item"><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></div><div class="layer-count">${l.count}</div></div>`).join('')}
|
|
</div>
|
|
<div class="g-panel">
|
|
<div class="sec-head"><h3>Nuclear Watch</h3><span class="badge">RADIATION</span></div>
|
|
<div class="nuke-ok">${allNormal?'● ALL SITES NORMAL':'⚠ ANOMALY DETECTED'}</div>
|
|
${nukeHtml}
|
|
</div>
|
|
<div class="g-panel">
|
|
<div class="sec-head"><h3>Risk Gauges</h3><span class="badge">STRESS</span></div>
|
|
<div class="econ-row"><span class="elabel">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">HY Spread</span><span class="eval">${hy?.value||'--'}</span></div>
|
|
<div class="econ-row"><span class="elabel">USD Index</span><span class="eval">${usd?.value?.toFixed(1)||'--'}</span></div>
|
|
<div class="econ-row"><span class="elabel">Jobless Claims</span><span class="eval">${claims?.value?.toLocaleString()||'--'}</span></div>
|
|
<div class="econ-row"><span class="elabel">30Y Mortgage</span><span class="eval">${mort?.value||'--'}%</span></div>
|
|
<div class="econ-row"><span class="elabel">M2 Supply</span><span class="eval">$${(m2?.value/1000)?.toFixed(1)||'--'}T</span></div>
|
|
<div class="econ-row"><span class="elabel">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>Space Watch</h3><span class="badge">CELESTRAK</span></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 ===
|
|
function initMap(){
|
|
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); })
|
|
.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 = true;
|
|
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(() => { globe.controls().autoRotate = true; }, 10000);
|
|
});
|
|
|
|
// Resize handler
|
|
window.addEventListener('resize', () => {
|
|
const c = document.getElementById('mapContainer');
|
|
globe.width(c.clientWidth).height(c.clientHeight || 560);
|
|
});
|
|
|
|
// Plot globe markers (preloaded but hidden)
|
|
plotMarkers();
|
|
|
|
// Start in flat mode — hide globe, show flat map
|
|
document.getElementById('globeViz').style.display = 'none';
|
|
document.getElementById('flatMapSvg').style.display = 'block';
|
|
initFlatMap();
|
|
|
|
// Legend
|
|
document.getElementById('mapLegend').innerHTML=
|
|
[{c:'#64f0c8',l:'Air Traffic'},{c:'#ff5f63',l:'Thermal/Fire'},{c:'rgba(255,120,80,0.8)',l:'Conflict'},{c:'#44ccff',l:'SDR Receiver'},
|
|
{c:'#ffe082',l:'Nuclear Site'},{c:'#b388ff',l:'Chokepoint'},{c:'#ffb84c',l:'OSINT Event'},{c:'#69f0ae',l:'Health Alert'},{c:'#81d4fa',l:'World News'}]
|
|
.map(x=>`<div class="leg-item"><div class="leg-dot" style="background:${x.c}"></div>${x.l}</div>`).join('');
|
|
}
|
|
|
|
function plotMarkers(){
|
|
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}];
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
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',
|
|
popHead:n.source+' NEWS', popMeta:n.region+' · '+getAge(n.date),
|
|
popText:cleanText(n.title)
|
|
});
|
|
});
|
|
|
|
// Set points on globe
|
|
globe.pointsData(points);
|
|
globe.labelsData(labels);
|
|
|
|
// === ACLED CONFLICT EVENTS (pulsing rings) ===
|
|
const conflictRings = (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 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}
|
|
];
|
|
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}
|
|
];
|
|
const arcs = [];
|
|
// 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
|
|
});
|
|
});
|
|
});
|
|
globe.arcsData(arcs);
|
|
}
|
|
|
|
function showPopup(event,head,text,meta){
|
|
const popup=document.getElementById('mapPopup');
|
|
const container=document.getElementById('mapContainer');
|
|
const rect=container.getBoundingClientRect();
|
|
let left, top;
|
|
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(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:[[-170,-56],[-30,72]], 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){
|
|
globeEl.style.display = 'none';
|
|
flatEl.style.display = 'block';
|
|
if(!flatSvg) initFlatMap();
|
|
else { flatG.selectAll('*').remove(); drawFlatMap(); }
|
|
} else {
|
|
globeEl.style.display = 'block';
|
|
flatEl.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
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');
|
|
});
|
|
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)=>{
|
|
const[x,y]=proj([lon,lat]);if(!x||!y)return null;
|
|
const g=mg.append('g').attr('transform',`translate(${x},${y})`).style('cursor','pointer');
|
|
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}];
|
|
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'));
|
|
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
|
|
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'));
|
|
}));
|
|
// Chokepoints
|
|
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')
|
|
.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}];
|
|
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'))});
|
|
// 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'))}));
|
|
// 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}];
|
|
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`))});
|
|
// 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}];
|
|
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'))});
|
|
// 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))});
|
|
// ACLED
|
|
(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')
|
|
.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
|
|
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}];
|
|
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){
|
|
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 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 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?'▲':'▼';
|
|
return `<div class="mc"><div class="ml">${q.name||q.symbol}</div><span class="mv" style="color:${clr}">${q.symbol.includes('BTC')||q.symbol.includes('ETH')?'$'+q.price.toLocaleString():'$'+q.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 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:'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};
|
|
|
|
// 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, 40);
|
|
const srcClass = s => {
|
|
if (!s) return 'other';
|
|
const sl = s.toLowerCase();
|
|
if (sl.includes('bbc')) return 'bbc';
|
|
if (sl.includes('nyt') || sl.includes('times')) return 'nyt';
|
|
if (sl.includes('jazeera') || sl.includes('alj')) return 'alj';
|
|
if (sl.includes('gdelt')) return 'gdelt';
|
|
if (sl.includes('telegram')) return 'tg';
|
|
return 'other';
|
|
};
|
|
const tickerCards = feed.map(n => {
|
|
const sc = srcClass(n.source);
|
|
const age = n.timestamp ? getAge(n.timestamp) : '';
|
|
return `<div class="tk-card ${n.urgent?'urgent':''}"><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></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>`;
|
|
|
|
// 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">No changes since last sweep</div>';
|
|
|
|
document.getElementById('lowerGrid').innerHTML=`
|
|
<div class="g-panel lp-ticker" style="display:flex;flex-direction:column">
|
|
<div class="sec-head"><h3>Live News Ticker</h3><span class="badge">${feed.length} ITEMS</span></div>
|
|
<div class="ticker-wrap" style="--ticker-duration:${tickerDuration}s">
|
|
<div class="ticker-track">${tickerCards}${tickerCards}</div>
|
|
</div>
|
|
</div>
|
|
<div class="g-panel lp-delta">
|
|
<div class="sec-head"><h3>Sweep Delta</h3><span class="badge ${dirClass}">${dirEmoji} ${ds.direction?ds.direction.toUpperCase():'BASELINE'}</span></div>
|
|
${hasDelta?`<div style="display:flex;gap:12px;margin-bottom:6px;font-family:var(--mono);font-size:10px">
|
|
<span style="color:var(--dim)">Changes: <span style="color:var(--accent)">${ds.totalChanges}</span></span>
|
|
<span style="color:var(--dim)">Critical: <span style="color:${ds.criticalChanges>0?'var(--warn)':'var(--dim)'}">${ds.criticalChanges||0}</span></span>
|
|
${ds.signalBreakdown?`<span style="color:var(--dim)">New: <span style="color:#4dd0e1">${ds.signalBreakdown.new}</span> ↑${ds.signalBreakdown.escalated} ↓${ds.signalBreakdown.deescalated}</span>`:''}
|
|
</div>`:''}
|
|
<div class="delta-list">${deltaHtml}</div>
|
|
</div>
|
|
<div class="g-panel lp-macro">
|
|
<div class="sec-head"><h3>Macro + Markets</h3><span class="badge">${mkt.timestamp?'LIVE':'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 + 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>
|
|
<div class="g-panel lp-ideas">
|
|
<div class="sec-head"><h3>Leverageable Ideas</h3>${D.ideasSource==='llm'?'<span class="ideas-src llm">AI ENHANCED</span>':D.ideasSource==='disabled'?'<span class="ideas-src static">LLM OFF</span>':'<span class="ideas-src static">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>`;
|
|
}
|
|
|
|
// === RIGHT RAIL ===
|
|
function renderRight(){
|
|
// 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 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);
|
|
|
|
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}
|
|
];
|
|
|
|
document.getElementById('rightRail').innerHTML=`
|
|
<div class="g-panel">
|
|
<div class="sec-head"><h3>Cross-Source Signals</h3><span class="badge">WORLDVIEW</span></div>
|
|
${signals}
|
|
</div>
|
|
<div class="g-panel" style="display:flex;flex-direction:column">
|
|
<div class="sec-head"><h3>OSINT Stream</h3><span class="badge">${D.tg.urgent.length} URGENT</span></div>
|
|
<div class="ticker-wrap" style="--ticker-duration:${osintDuration}s;max-height:260px">
|
|
<div class="ticker-track">${osintCards}${osintCards}</div>
|
|
</div>
|
|
</div>
|
|
<div class="g-panel">
|
|
<div class="sec-head"><h3>Signal Core</h3><span class="badge">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>`;
|
|
}
|
|
|
|
// === 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,'')}
|
|
|
|
// === 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:'INITIALIZING CRUCIX ENGINE v2.1.0',delay:0},
|
|
{text:`CONNECTING ${D.meta.sourcesQueried} OSINT SOURCES...`,delay:400},
|
|
{text:'├─ OPENSKY · FIRMS · KIWISDR · MARITIME',delay:700},
|
|
{text:'├─ FRED · BLS · EIA · TREASURY · GSCPI',delay:900},
|
|
{text:'├─ TELEGRAM · SAFECAST · EPA · WHO · OFAC',delay:1100},
|
|
{text:'└─ GDELT · NOAA · PATENTS · BLUESKY · REDDIT',delay:1300},
|
|
{text:`SWEEP COMPLETE — <span class="count">${D.meta.sourcesOk}/${D.meta.sourcesQueried}</span> SOURCES <span class="ok">OK</span>`,delay:1700},
|
|
{text:`ACLED CONFLICT LAYER: ${acledStatus}`,delay:1900},
|
|
{text:'FLIGHT CORRIDORS: <span class="ok">ACTIVE</span> · DUAL PROJECTION: <span class="ok">READY</span>',delay:2100},
|
|
{text:'INTELLIGENCE SYNTHESIS: <span class="ok">ACTIVE</span>',delay:2400},
|
|
];
|
|
const container=document.getElementById('bootLines');
|
|
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});
|
|
},null,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);
|
|
}
|
|
|
|
// === 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();
|
|
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();
|
|
});
|
|
}
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
const isServer = location.protocol !== 'file:';
|
|
|
|
if (isServer) {
|
|
// Server mode: always fetch live data from API (ignore any stale inline D)
|
|
fetch('/api/data')
|
|
.then(r => 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 (D && D.meta) {
|
|
// File mode: use inline data
|
|
init();
|
|
}
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|