feat(ui): redesign dashboard in app-style shell
Some checks failed
Build / test-and-image (pull_request) Failing after 18s
Codex Template Compliance / template-compliance (pull_request) Successful in 5s

This commit is contained in:
MrSphay
2026-05-17 20:49:58 +02:00
parent 6a9918bc98
commit 7d2acca4e3
3 changed files with 298 additions and 54 deletions

View File

@@ -2,7 +2,7 @@
# Intelligence Terminal
**Self-hosted intelligence dashboard. 27 open sources. Docker-first. No telemetry.**
**Modrinth-app-inspired operator dashboard. 27 open sources. Docker-first. No telemetry.**
[![Gitea Repository](https://img.shields.io/badge/source-Gitea-609926?style=for-the-badge&logo=gitea&logoColor=white)](https://git.wilkensxl.de/MrSphay/intelligence-terminal)
[![Docker Image](https://img.shields.io/badge/image-Gitea%20Registry-0b1220?style=for-the-badge&logo=docker&logoColor=white)](https://git.wilkensxl.de/MrSphay/-/packages/container/intelligence-terminal/latest)
@@ -34,8 +34,10 @@
> Runtime data stays in your configured `runs/` volume and API keys are operator-owned.
> **Source:** [git.wilkensxl.de/MrSphay/intelligence-terminal](https://git.wilkensxl.de/MrSphay/intelligence-terminal)
> Pull the image or clone the repository to run Intelligence Terminal on your own infrastructure.
>
> **Design transparency:** the dashboard is inspired by app-style marketplace UX patterns, especially dark desktop app shells with a strong left navigation. It does not use Modrinth branding, logos, or assets and is not affiliated with Modrinth.
Intelligence Terminal pulls satellite fire detection, flight tracking, radiation monitoring, satellite constellation tracking, economic indicators, live market prices, conflict data, sanctions lists, and social sentiment from 27 open-source intelligence feeds in parallel, every 15 minutes, and renders everything on a single self-contained dashboard.
Intelligence Terminal pulls satellite fire detection, flight tracking, radiation monitoring, satellite constellation tracking, economic indicators, live market prices, conflict data, sanctions lists, and social sentiment from 27 open-source intelligence feeds in parallel, every 15 minutes, and renders everything in a dark, app-style operator workspace.
Hook it up to an LLM and it becomes a **two-way intelligence assistant**: pushing multi-tier alerts to Telegram and Discord when something meaningful changes, responding to commands like `/brief` and `/sweep` from your phone, and generating trade ideas grounded in real cross-domain data.
@@ -299,26 +301,21 @@ Gitea Actions publishes the same image automatically when the repository secret
## What You Get
### Live Dashboard
A self-contained Jarvis-style HUD with:
- **3D WebGL globe** (Globe.gl) with atmosphere glow, star field, and smooth rotation — plus a classic flat map toggle
- **9 marker types** across both views: fire detections, air traffic, radiation sites, maritime chokepoints, SDR receivers, OSINT events, health alerts, geolocated news, conflict events
- **Animated 3D flight corridor arcs** between air traffic hotspots and global hubs
- **Region filters** (World, Americas, Europe, Middle East, Asia Pacific, Africa) — rotates the globe or zooms the flat map
- **Live market data** — indexes, crypto, energy, commodities via Yahoo Finance (no API key needed)
- **Risk gauges** — VIX, high-yield spread, supply chain pressure index
- **OSINT feed** — English-language posts from 17 Telegram intelligence channels (expandable)
- **News ticker** — merged RSS + GDELT headlines + Telegram posts, auto-scrolling
- **Sweep delta** — live panel showing what changed since last sweep (new signals, escalations, de-escalations with severity)
- **Cross-source signals** — correlated intelligence across satellite, economic, conflict, and social domains
- **Nuclear watch** — real-time radiation readings from Safecast + EPA RadNet
- **Space watch** — CelesTrak satellite tracking: recent launches, ISS, military constellations, Starlink/OneWeb counts
- **Leverageable ideas** — AI-generated trade ideas (with LLM) or signal-correlated ideas (without)
A self-contained app-style operator dashboard with dark mode by default, a large left navigation rail, rounded content surfaces, and view-specific panels:
- **Home** - sweep status, alert posture, latest feed, source count, macro summary, and high-level signal state
- **Worldview** - Globe.gl 3D globe or flat map, regional focus controls, 9 marker types, flight corridors, and layer focus/hide controls
- **Sources** - sensor grid, source health, API-key degradation signals, and transparent partial-data states
- **Signals** - cross-source signals, sweep delta, scenario watchlist, OSINT feed, and escalation/de-escalation context
- **Markets** - live indexes, crypto, energy, commodities, VIX, high-yield spread, supply-chain pressure, and LLM-assisted ideas
- **Ops** - browser-triggered terminal actions, performance mode, Telegram/Discord operator workflows, and system status
The UI keeps the existing operational features: `/api/data`, SSE live refresh, globe/flat map mode, layer focus/hide, terminal actions, low-performance mode, LLM output, Telegram and Discord alerting, and scenario watchlist data.
### Performance Modes
The `VISUALS FULL` / `VISUALS LITE` button in the top bar only changes rendering behavior - it does **not** remove data sources or reduce sweep coverage.
When you switch to **VISUALS LITE**, the dashboard:
- Disables decorative background effects such as the radial/grid overlays and scanlines
- Disables decorative background effects such as radial and grid overlays
- Removes expensive blur/backdrop-filter effects on panels and overlays
- Stops non-essential animations like the logo ring blink, conflict rings, and corridor flow effects
- Disables globe auto-rotation and turns off animated flight-arc dashes
@@ -506,7 +503,7 @@ intelligence-terminal/
├── dashboard/
│ ├── inject.mjs # Data synthesis + standalone HTML injection
│ └── public/
│ └── jarvis.html # Self-contained Jarvis HUD
│ └── jarvis.html # Self-contained app-style operator dashboard
├── lib/
│ ├── llm/ # LLM abstraction (8 providers, raw fetch, no SDKs)
@@ -645,7 +642,7 @@ When running `npm run dev`:
| Endpoint | Description |
|----------|-------------|
| `GET /` | Jarvis HUD dashboard |
| `GET /` | App-style operator dashboard |
| `GET /api/data` | Current synthesized intelligence data (JSON) |
| `GET /api/health` | Server status, uptime, source count, LLM status |
| `GET /events` | SSE stream for live push updates |
@@ -724,16 +721,16 @@ Check these in order:
## Screenshots
The `docs/` folder contains dashboard screenshots referenced by this README:
The `docs/` folder contains dashboard screenshots referenced by this README. The hero screenshot has been refreshed for the app-style shell; regenerate the supporting map/globe images from a running instance when those views materially change.
| File | Description |
|------|-------------|
| `docs/dashboard.png` | Full dashboard hero image at the top of this README |
| `docs/boot.png` | Cinematic boot sequence animation |
| `docs/map.png` | D3 world map with marker types and flight arcs |
| `docs/dashboard.png` | Full operator dashboard - hero image at the top of this README |
| `docs/boot.png` | Boot sequence animation |
| `docs/map.png` | Worldview map with marker types and flight arcs |
| `docs/globe.png` | 3D WebGL globe view with atmosphere glow and markers |
To update them: run the dashboard, wait for a sweep to complete, then use your browser's DevTools (`F12` `Ctrl+Shift+P` "Capture full size screenshot") or a tool like [LICEcap](https://www.cockos.com/licecap/) for GIFs.
For a fresh capture, run the dashboard, wait for a sweep to complete, then use your browser's DevTools (`F12` -> `Ctrl+Shift+P` -> "Capture full size screenshot") or a tool like [LICEcap](https://www.cockos.com/licecap/) for GIFs.
---

View File

@@ -3,9 +3,9 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CRUCIX — Intelligence Terminal</title>
<title>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">
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;500;600;700&family=Inter:wght@400;500;600;700;800&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>
@@ -17,7 +17,7 @@
--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;
--warn:#ffb84c;--danger:#ff5f63;--mono:'IBM Plex Mono',monospace;--sans:'Inter',system-ui,-apple-system,BlinkMacSystemFont,'Segoe UI',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;
@@ -25,10 +25,6 @@ html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--s
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}
@@ -274,7 +270,7 @@ html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--s
.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 .bg-grid,body.low-perf .bg-radial{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}
@@ -335,18 +331,200 @@ body.low-perf .ticker-wrap::-webkit-scrollbar-thumb{background:rgba(100,240,200,
/* 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}
/* APP-STYLE REDESIGN */
:root{
--app-bg:#17191f;
--app-surface:#22252d;
--app-surface-2:#2b2f38;
--app-card:#2a2d35;
--app-card-soft:#242830;
--app-border:#3a3f49;
--app-border-soft:#343942;
--app-text:#f3f5f7;
--app-muted:#aeb6c2;
--app-dim:#77818f;
--app-green:#1bd96a;
--app-green-soft:rgba(27,217,106,0.17);
--app-blue:#5fb4ff;
--app-red:#ff5f6f;
--app-yellow:#ffcc66;
}
body[data-theme="dark"]{
--bg:var(--app-bg);
--panel:var(--app-surface);
--glass:var(--app-card);
--border:var(--app-border);
--border-bright:rgba(27,217,106,0.55);
--text:var(--app-text);
--dim:var(--app-muted);
--accent:var(--app-green);
--accent2:var(--app-blue);
background:var(--app-bg);
}
body[data-theme="light"]{
--bg:#eef1f4;
--panel:#ffffff;
--glass:#ffffff;
--border:#d6dde5;
--border-bright:rgba(18,177,88,0.55);
--text:#161a21;
--dim:#5e6875;
--accent:#11b858;
--accent2:#2677c9;
--app-bg:#eef1f4;
--app-surface:#ffffff;
--app-surface-2:#f5f7fa;
--app-card:#ffffff;
--app-card-soft:#f3f6f9;
--app-border:#d6dde5;
--app-border-soft:#e1e7ee;
--app-text:#161a21;
--app-muted:#5e6875;
--app-dim:#788391;
color:#161a21;
}
body{font-family:var(--sans);letter-spacing:0;background:var(--app-bg)}
.bg-grid,.bg-radial{display:none}
#main.app-root{display:grid;grid-template-columns:112px minmax(0,1fr);gap:18px;min-height:100vh;padding:0;background:var(--app-bg)}
.app-sidebar{position:sticky;top:0;height:100vh;padding:26px 18px 24px;border-right:1px solid rgba(255,255,255,0.05);background:#20232b;display:flex;flex-direction:column;align-items:center;gap:28px}
body[data-theme="light"] .app-sidebar{background:#f8fafc;border-right-color:#dde3ea}
.app-brand-mark{width:54px;height:54px;border:3px solid var(--app-green);border-radius:18px;display:grid;place-items:center;color:var(--app-green);font-weight:800;font-size:19px;letter-spacing:0;background:rgba(27,217,106,0.06)}
.app-brand-mark span{line-height:1}
.app-nav{display:flex;flex-direction:column;gap:12px;width:100%;align-items:center}
.nav-item{width:74px;height:74px;border:0;border-radius:999px;background:transparent;color:var(--app-muted);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:5px;cursor:pointer;transition:background .18s ease,color .18s ease,transform .18s ease}
.nav-item span{width:28px;height:28px;display:grid;place-items:center;font-weight:800;font-size:17px;border:2px solid currentColor;border-radius:10px;line-height:1}
.nav-item small{font-size:9px;font-weight:700;letter-spacing:0;text-transform:none}
.nav-item:hover{background:rgba(255,255,255,0.06);color:var(--app-text);transform:translateY(-1px)}
.nav-item.active{background:rgba(27,217,106,0.22);color:var(--app-green)}
.sidebar-status{margin-top:auto;width:74px;min-height:42px;border-top:1px solid var(--app-border);padding-top:14px;display:flex;align-items:center;justify-content:center;gap:7px;color:var(--app-muted);font-size:11px;font-weight:700}
.sidebar-status-dot{width:8px;height:8px;border-radius:50%;background:var(--app-green);box-shadow:0 0 16px rgba(27,217,106,.5)}
.app-shell{min-width:0;margin:24px 24px 24px 0;border:1px solid var(--app-border);border-radius:30px 0 0 30px;background:#181b21;overflow:hidden;box-shadow:0 24px 70px rgba(0,0,0,.26)}
body[data-theme="light"] .app-shell{background:#eef1f4;box-shadow:0 20px 50px rgba(33,43,54,.12)}
.topbar{border:0;border-bottom:1px solid var(--app-border);border-radius:0;background:var(--app-surface);padding:26px 36px;backdrop-filter:none;display:grid;grid-template-columns:minmax(220px,340px) minmax(240px,1fr);gap:18px;align-items:center}
.top-left,.top-center,.top-right{width:auto}
.top-left{align-items:flex-start;flex-direction:column;gap:6px}
.brand{font-family:var(--sans);font-size:34px;font-weight:800;letter-spacing:0;text-transform:none;color:var(--app-text);line-height:1}
.view-subtitle{font-size:14px;line-height:1.4;color:var(--app-muted);font-weight:600}
.regime-chip{border:0;border-radius:999px;background:var(--app-green-soft);color:var(--app-green);font-family:var(--sans);font-size:12px;letter-spacing:0;text-transform:none;font-weight:800;padding:7px 12px}
.regime-chip .blink{background:var(--app-green);box-shadow:0 0 10px rgba(27,217,106,.6)}
.app-search{height:46px;border:1px solid var(--app-border);border-radius:999px;background:var(--app-card-soft);color:var(--app-dim);display:flex;align-items:center;padding:0 18px;font-size:14px;font-weight:600;min-width:0}
.app-search span{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.top-right{grid-column:1/-1;justify-content:flex-start;flex-wrap:wrap}
@media(min-width:1240px){.topbar{grid-template-columns:minmax(260px,1fr) minmax(260px,420px) auto}.top-right{grid-column:auto;justify-content:flex-end}}
.meta-pill,.guide-btn,.alert-badge,.perf-pill{border:1px solid var(--app-border);border-radius:999px;background:var(--app-card-soft);color:var(--app-muted);font-family:var(--sans);font-size:12px;letter-spacing:0;text-transform:none;font-weight:700;padding:8px 12px}
.meta-pill .v{color:var(--app-text)}
.guide-btn{color:var(--app-blue)}
.alert-badge{border-color:rgba(255,95,111,.35);background:rgba(255,95,111,.1);color:#ff9da8}
.theme-switch{display:flex;align-items:center;gap:4px;padding:4px;border:1px solid var(--app-border);border-radius:999px;background:var(--app-card-soft)}
.theme-btn{border:0;border-radius:999px;background:transparent;color:var(--app-muted);font:700 11px var(--sans);padding:7px 10px;cursor:pointer}
.theme-btn.active{background:var(--app-green);color:#06120b}
.grid{display:grid;margin:0;padding:24px;grid-template-columns:300px minmax(0,1fr);gap:16px;min-height:calc(100vh - 122px);background:#181b21}
body[data-theme="light"] .grid{background:#eef1f4}
.col{gap:16px;min-width:0}
#leftRail,#centerCol,#rightRail{order:0}
#rightRail{grid-column:1/-1;display:grid;grid-template-columns:repeat(auto-fit,minmax(300px,1fr));gap:16px}
@media(min-width:1320px){.grid{grid-template-columns:280px minmax(0,1fr) 360px}#rightRail{grid-column:auto;display:flex;flex-direction:column}}
.g-panel,.map-region-bar,.map-container,.glossary-panel{border:1px solid var(--app-border);border-radius:18px;background:var(--app-card);box-shadow:none;backdrop-filter:none}
.g-panel{padding:18px;overflow:hidden}
.g-panel::before{display:none}
.sec-head{margin-bottom:14px;gap:10px}
.sec-head h3{font-family:var(--sans);font-size:17px;font-weight:800;letter-spacing:0;text-transform:none;color:var(--app-text)}
.badge{border:1px solid var(--app-border);border-radius:999px;background:var(--app-card-soft);font-family:var(--sans);font-size:11px;font-weight:800;color:var(--app-green);padding:4px 9px}
.layer-item,.site-row,.econ-row,.src-item,.mc,.signal-row,.sm,.idea-card,.ic,.tk-card{border-color:var(--app-border-soft);border-radius:12px;background:var(--app-card-soft)}
.layer-item{margin-bottom:8px;padding:11px}
.layer-item.focused{border-color:var(--app-green);background:var(--app-green-soft)}
.layer-name,.idea-title{font-weight:800;color:var(--app-text)}
.layer-sub,.layer-mode,.idea-text,.ic .ic-t,.tk-head{color:var(--app-muted)}
.layer-count,.site-val,.eval,.sm .smv{color:var(--app-green)}
.mini-btn,.action-btn,.region-btn,.map-ctrl-btn,.proj-toggle,.glossary-close{border:1px solid var(--app-border);border-radius:10px;background:var(--app-card-soft);font-family:var(--sans);letter-spacing:0;color:var(--app-muted)}
.action-grid{grid-template-columns:repeat(3,minmax(0,1fr));gap:8px}
.action-btn{font-size:12px;font-weight:800;padding:10px 8px}
.action-btn:hover,.mini-btn:hover,.region-btn:hover{border-color:var(--app-green);color:var(--app-green);background:var(--app-green-soft)}
.terminal-output{border:1px solid var(--app-border);border-radius:14px;background:#151820;color:var(--app-muted)}
body[data-theme="light"] .terminal-output{background:#f6f8fb}
.map-region-bar{padding:12px;gap:8px;margin-bottom:0}
.region-btn{font-weight:800;text-transform:none;font-size:12px;padding:8px 12px}
.region-btn.active{background:var(--app-green);color:#06120b;border-color:var(--app-green)}
.map-container{min-height:590px;background:#101319}
body[data-theme="light"] .map-container{background:#e7ecf3}
.map-legend{border:1px solid var(--app-border);border-radius:14px;background:rgba(24,27,33,.86);padding:9px 11px;backdrop-filter:blur(8px)}
.map-hint,.map-hint-id{top:14px;right:18px;color:var(--app-muted);letter-spacing:0;font-family:var(--sans);font-weight:700}
.lower{gap:16px;margin-top:16px}
.lower .lp-ticker{flex:1.2 1 300px;max-width:460px}
.lower .lp-macro{flex:2.4 1 520px}
.lower .lp-ideas{flex:1.4 1 340px}
.lower .source-health{flex:1.5 1 420px}
.metrics-row{gap:8px}
.src-grid{grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:8px}
.feed{max-height:none}
body[data-view="worldview"] .grid{grid-template-columns:300px minmax(0,1fr)}
body[data-view="worldview"] #rightRail,body[data-view="worldview"] .lower{display:none}
body[data-view="worldview"] .map-container{min-height:calc(100vh - 250px)}
body[data-view="sources"] .grid{grid-template-columns:360px minmax(0,1fr)}
body[data-view="sources"] #rightRail,body[data-view="sources"] #mapContainer,body[data-view="sources"] #mapRegionBar{display:none!important}
body[data-view="sources"] .lower .g-panel:not(.source-health){display:none}
body[data-view="sources"] .lower{margin-top:0}
body[data-view="signals"] .grid,body[data-view="ops"] .grid{display:block}
body[data-view="signals"] #leftRail,body[data-view="signals"] #centerCol{display:none}
body[data-view="signals"] #rightRail{display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:16px}
body[data-view="signals"] #rightRail .right-actions{display:none}
body[data-view="ops"] #leftRail,body[data-view="ops"] #centerCol{display:none}
body[data-view="ops"] #rightRail{display:grid;grid-template-columns:repeat(auto-fit,minmax(320px,1fr));gap:16px}
body[data-view="ops"] #rightRail .right-signals,body[data-view="ops"] #rightRail .right-scenarios,body[data-view="ops"] #rightRail .right-delta,body[data-view="ops"] #rightRail .right-osint{display:none}
body[data-view="markets"] .grid{display:block}
body[data-view="markets"] #leftRail,body[data-view="markets"] #rightRail,body[data-view="markets"] #mapContainer,body[data-view="markets"] #mapRegionBar{display:none!important}
body[data-view="markets"] .lower{margin-top:0}
body[data-view="markets"] .lower .g-panel:not(.lp-macro):not(.lp-ideas){display:none}
body[data-view="markets"] .lp-macro,body[data-view="markets"] .lp-ideas{max-width:none}
@media(max-width:760px){
#main.app-root{display:block;padding:0 0 86px}
.app-sidebar{position:fixed;z-index:30;left:0;right:0;bottom:0;top:auto;height:76px;flex-direction:row;padding:8px 12px;border-right:0;border-top:1px solid var(--app-border);gap:12px}
.app-brand-mark,.sidebar-status{display:none}
.app-nav{flex-direction:row;justify-content:space-between;gap:6px;width:100%}
.nav-item{width:54px;height:54px;border-radius:18px}
.nav-item span{width:23px;height:23px;font-size:13px;border-radius:8px}
.nav-item small{display:none}
.app-shell{margin:10px;border-radius:24px;min-height:calc(100vh - 96px)}
.topbar{grid-template-columns:1fr;padding:22px;gap:12px}
.brand{font-size:30px}
.top-center{display:flex;width:100%;overflow:auto}
.top-right{justify-content:flex-start}
.grid{padding:14px;display:flex;flex-direction:column}
body[data-view="worldview"] .grid,body[data-view="sources"] .grid{display:flex}
body[data-view="signals"] #rightRail,body[data-view="ops"] #rightRail{display:flex;flex-direction:column}
.map-container{min-height:420px}
.map-legend{left:8px;right:8px;bottom:8px}
}
</style>
</head>
<body>
<body data-theme="dark" data-view="home">
<div id="boot">
<div class="logo-ring"><span class="logo-text">CRUCIX</span></div>
<div class="logo-ring"><span class="logo-text">IT</span></div>
<div id="bootLines"></div>
<div id="bootFinal">TERMINAL ACTIVE</div>
<div id="bootFinal">APP READY</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 id="main" class="app-root">
<aside class="app-sidebar" aria-label="Primary views">
<div class="app-brand-mark"><span>IT</span></div>
<nav class="app-nav" id="appNav">
<button class="nav-item active" data-view-target="home" onclick="setAppView('home')" title="Home"><span>H</span><small>Home</small></button>
<button class="nav-item" data-view-target="worldview" onclick="setAppView('worldview')" title="Worldview"><span>W</span><small>World</small></button>
<button class="nav-item" data-view-target="sources" onclick="setAppView('sources')" title="Sources"><span>S</span><small>Sources</small></button>
<button class="nav-item" data-view-target="signals" onclick="setAppView('signals')" title="Signals"><span>I</span><small>Signals</small></button>
<button class="nav-item" data-view-target="markets" onclick="setAppView('markets')" title="Markets"><span>M</span><small>Markets</small></button>
<button class="nav-item" data-view-target="ops" onclick="setAppView('ops')" title="Ops"><span>O</span><small>Ops</small></button>
</nav>
<div class="sidebar-status">
<span class="sidebar-status-dot"></span>
<span id="sidebarStatus">Live</span>
</div>
</aside>
<main class="app-shell">
<div class="topbar" id="topbar"></div>
<div class="grid">
<div class="col" id="leftRail"></div>
@@ -370,6 +548,7 @@ body.low-perf .ticker-wrap::-webkit-scrollbar-thumb{background:rgba(100,240,200,
</div>
<div class="col" id="rightRail"></div>
</div>
</main>
</div>
<div class="glossary-overlay" id="glossaryOverlay" onclick="if(event.target===this) closeGlossary()">
<div class="glossary-panel">
@@ -434,6 +613,56 @@ let currentRegion = 'world';
let flatSvg, flatProjection, flatPath, flatG, flatZoom, flatW, flatH;
const terminalActionTokenKey = 'crucix_sweep_token';
const appViews = {
home: { title: 'Home', subtitle: 'Sweep summary, alert posture, source status, and the latest operator feed.' },
worldview: { title: 'Worldview', subtitle: 'Globe or flat map with regional focus and layer controls.' },
sources: { title: 'Sources', subtitle: 'Sensor grid, source health, API-key degradation, and data coverage.' },
signals: { title: 'Signals', subtitle: 'Cross-source signals, sweep delta, scenario watchlist, and OSINT context.' },
markets: { title: 'Markets', subtitle: 'Macro indicators, live markets, volatility gauges, and AI-assisted ideas.' },
ops: { title: 'Ops', subtitle: 'Terminal actions, integration state, and low-performance controls.' }
};
let currentView = localStorage.getItem('intelligence_terminal_view') || 'home';
if(!appViews[currentView]) currentView = 'home';
let themePreference = localStorage.getItem('intelligence_terminal_theme') || 'dark';
function resolveTheme(pref){
if(pref === 'auto'){
return window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
}
return pref === 'light' ? 'light' : 'dark';
}
function applyTheme(pref = themePreference){
themePreference = pref;
document.body.dataset.theme = resolveTheme(pref);
document.querySelectorAll('.theme-btn').forEach(btn => btn.classList.toggle('active', btn.dataset.themeChoice === pref));
}
function setTheme(pref){
themePreference = pref;
localStorage.setItem('intelligence_terminal_theme', pref);
applyTheme(pref);
renderTopbar();
}
function renderAppNav(){
document.body.dataset.view = currentView;
document.querySelectorAll('.nav-item[data-view-target]').forEach(btn => {
btn.classList.toggle('active', btn.dataset.viewTarget === currentView);
});
const status = document.getElementById('sidebarStatus');
if(status) status.textContent = currentView === 'home' ? 'Live' : appViews[currentView].title;
}
function setAppView(view){
if(!appViews[view]) return;
currentView = view;
localStorage.setItem('intelligence_terminal_view', view);
renderAppNav();
renderTopbar();
refreshMapViewport(true);
}
const layerTypeMap = {
air: ['air'],
thermal: ['thermal'],
@@ -634,23 +863,35 @@ function renderTopbar(){
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});
const hasActionToken = !!getTerminalActionToken();
const view = appViews[currentView] || appViews.home;
const direction = D.delta?.summary?.direction;
const deltaLabel = direction === 'risk-off' ? '&#x25B2; '+t('dashboard.riskOff','RISK-OFF') : direction === 'risk-on' ? '&#x25BC; '+t('dashboard.riskOn','RISK-ON') : '&#x25C6; '+t('dashboard.mixed','MIXED');
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>
<span class="brand">${view.title}</span>
<span class="view-subtitle">${view.subtitle}</span>
<span class="regime-chip"><span class="blink"></span>Operator dashboard</span>
</div>
<div class="app-search"><span>Search sources, signals, regions, markets...</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>
<div class="theme-switch" aria-label="Theme selector">
<button class="theme-btn" data-theme-choice="dark" onclick="setTheme('dark')">Dark</button>
<button class="theme-btn" data-theme-choice="auto" onclick="setTheme('auto')">Auto</button>
<button class="theme-btn" data-theme-choice="light" onclick="setTheme('light')">Light</button>
</div>
<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'?'&#x25B2; '+t('dashboard.riskOff','RISK-OFF'):D.delta.summary.direction==='risk-on'?'&#x25BC; '+t('dashboard.riskOn','RISK-ON'):'&#x25C6; '+t('dashboard.mixed','MIXED')}</span></span>` : ''}
<button class="guide-btn" onclick="configureTerminalActionToken()" title="Configure SWEEP_TOKEN for protected terminal actions">${hasActionToken?'TOKEN SET':'SET TOKEN'}</button>
${D.delta?.summary ? `<span class="meta-pill">${t('dashboard.delta','DELTA')} <span class="v">${deltaLabel}</span></span>` : ''}
<button class="guide-btn" onclick="configureTerminalActionToken()" title="Configure SWEEP_TOKEN for protected terminal actions">${hasActionToken?'Token set':'Set token'}</button>
<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();
renderAppNav();
applyTheme(themePreference);
}
function getTerminalActionToken(){
@@ -734,16 +975,16 @@ function renderLeftRail(){
const claims=D.fred.find(f=>f.id==='ICSA');
document.getElementById('leftRail').innerHTML=`
<div class="g-panel">
<div class="g-panel source-layers">
<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="g-panel nuclear-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?'&#9679; '+t('nuclear.allSitesNormal','ALL SITES NORMAL'):'&#9888; '+t('nuclear.anomalyDetected','ANOMALY DETECTED')}</div>
${nukeHtml}
</div>
<div class="g-panel">
<div class="g-panel risk-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>
@@ -753,7 +994,7 @@ function renderLeftRail(){
<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="g-panel space-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>
@@ -1603,7 +1844,12 @@ function renderLower(){
${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}`;
const sourcePanel = `<div class="g-panel source-health">
<div class="sec-head"><h3>Source Health</h3><span class="badge">${D.meta.sourcesOk}/${D.meta.sourcesQueried} online</span></div>
<div class="src-grid">${srcHtml || '<div class="src-item"><div class="sd err"></div><span>No source snapshot</span></div>'}</div>
<div class="disclosure">Sources that require API keys degrade visibly here while the rest of the sweep continues. The dashboard stays useful with partial data instead of hiding failures.</div>
</div>`;
document.getElementById('lowerGrid').innerHTML=`${tickerPanel}${osintPanel}${macroPanel}${ideasPanel}${sourcePanel}`;
}
async function runTerminalAction(action){
@@ -1756,7 +2002,7 @@ function safeExternalUrl(raw){try{const u=new URL(raw,location.href);return u.pr
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.initializing','INITIALIZING INTELLIGENCE TERMINAL'),delay:0},
{text:t('boot.connecting','CONNECTING {count} OSINT SOURCES...').replace('{count}',D.meta.sourcesQueried),delay:400},
{text:'&#9500;&#9472; '+t('boot.sourceGroup1','OPENSKY · FIRMS · KIWISDR · MARITIME'),delay:700},
{text:'&#9500;&#9472; '+t('boot.sourceGroup2','FRED · BLS · EIA · TREASURY · GSCPI'),delay:900},
@@ -1768,7 +2014,7 @@ function runBoot(){
{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');
document.getElementById('bootFinal').textContent=t('dashboard.terminalActive','APP READY');
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);
@@ -1783,7 +2029,6 @@ function runBoot(){
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'});
@@ -1798,7 +2043,7 @@ function runBoot(){
},[],4.0);
}
function isMobileLayout(){ return window.innerWidth <= 1100; }
function isMobileLayout(){ return window.innerWidth <= 760; }
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));
@@ -1935,6 +2180,8 @@ function connectSSE(){
// === INIT ===
let booted = false;
function init(){
applyTheme(themePreference);
renderAppNav();
renderTopbar();renderLeftRail();renderLower();renderRight();
renderGlossary();
initMap();

Binary file not shown.

Before

Width:  |  Height:  |  Size: 881 KiB

After

Width:  |  Height:  |  Size: 64 KiB