docs: add registry dockge and dashboard operations
All checks were successful
Build / test-and-image (push) Successful in 2m39s
All checks were successful
Build / test-and-image (push) Successful in 2m39s
This commit is contained in:
82
.env.example
82
.env.example
@@ -1,49 +1,49 @@
|
|||||||
# ============================================
|
# Intelligence Terminal / Crucix configuration
|
||||||
# Crucix Intelligence Engine — Configuration
|
# Copy to .env. Keep comments on separate lines; Docker env_file treats inline comments as values.
|
||||||
# ============================================
|
|
||||||
# Copy this file to .env and fill in your keys.
|
|
||||||
# Keys are optional — sources without keys degrade gracefully.
|
|
||||||
#
|
|
||||||
# IMPORTANT: Do NOT put comments on the same line as values.
|
|
||||||
# Docker env_file treats inline comments as part of the value.
|
|
||||||
|
|
||||||
# === OSINT Source API Keys ===
|
# Server
|
||||||
|
|
||||||
# Federal Reserve Economic Data (free: fred.stlouisfed.org/docs/api)
|
|
||||||
FRED_API_KEY=
|
|
||||||
# NASA FIRMS fire data (free: firms.modaps.eosdis.nasa.gov/api)
|
|
||||||
FIRMS_MAP_KEY=
|
|
||||||
# Energy Information Administration (free: api.eia.gov/register)
|
|
||||||
EIA_API_KEY=
|
|
||||||
# Maritime AIS data (aisstream.io)
|
|
||||||
AISSTREAM_API_KEY=
|
|
||||||
# Armed Conflict Location & Event Data (acleddata.com/user/register)
|
|
||||||
ACLED_EMAIL=
|
|
||||||
# OAuth2 password grant (API keys deprecated Sept 2025)
|
|
||||||
ACLED_PASSWORD=
|
|
||||||
# Cloudflare Radar internet outages & traffic anomalies (free: dash.cloudflare.com/profile/api-tokens, Account Analytics Read)
|
|
||||||
CLOUDFLARE_API_TOKEN=
|
|
||||||
|
|
||||||
# === Server Configuration ===
|
|
||||||
|
|
||||||
# Dashboard server port
|
|
||||||
PORT=3117
|
PORT=3117
|
||||||
# Auto-refresh interval (minutes)
|
|
||||||
REFRESH_INTERVAL_MINUTES=15
|
REFRESH_INTERVAL_MINUTES=15
|
||||||
|
AUTO_OPEN_BROWSER=false
|
||||||
|
STALE_DATA_MAX_AGE_MINUTES=60
|
||||||
|
SWEEP_TOKEN=
|
||||||
|
BRIEF_VERBOSITY=standard
|
||||||
|
|
||||||
# === LLM Layer (optional) ===
|
# LLM layer
|
||||||
# Enables AI-enhanced trade ideas and breaking news Telegram alerts.
|
# Providers: openrouter | openai-compatible | lmstudio | ollama | openai | anthropic | gemini | mistral | minimax | grok | codex
|
||||||
# Provider options: anthropic | openai | gemini | codex | openrouter | minimax | mistral | ollama | grok
|
LLM_PROVIDER=openrouter
|
||||||
LLM_PROVIDER=
|
LLM_BASE_URL=https://openrouter.ai/api/v1
|
||||||
# Not needed for codex (uses ~/.codex/auth.json) or ollama (local)
|
|
||||||
LLM_API_KEY=
|
LLM_API_KEY=
|
||||||
# Optional override. Each provider has a sensible default:
|
LLM_MODEL=openrouter/free
|
||||||
# anthropic: claude-sonnet-4-6 | openai: gpt-5.4 | gemini: gemini-3.1-pro | codex: gpt-5.3-codex | openrouter: openrouter/auto | minimax: MiniMax-M2.5 | ollama: llama3.1:8b | grok: grok-4-latest
|
LLM_TEMPERATURE=0.2
|
||||||
LLM_MODEL=
|
LLM_MAX_TOKENS=2000
|
||||||
# Ollama base URL (only needed if not using default http://localhost:11434)
|
LLM_TIMEOUT_MS=90000
|
||||||
OLLAMA_BASE_URL=
|
OPENROUTER_SITE_URL=https://git.wilkensxl.de/MrSphay/intelligence-terminal
|
||||||
|
OPENROUTER_APP_NAME=Intelligence Terminal
|
||||||
|
|
||||||
# === Telegram Alerts (optional, requires LLM) ===
|
# Local OpenAI-compatible examples
|
||||||
# Create a bot via @BotFather, get chat ID via @userinfobot
|
# LM Studio: LLM_PROVIDER=lmstudio, LLM_BASE_URL=http://host.docker.internal:1234/v1, LLM_MODEL=local-model
|
||||||
|
# Ollama: LLM_PROVIDER=ollama, LLM_BASE_URL=http://host.docker.internal:11434, LLM_MODEL=llama3.1:8b
|
||||||
|
# Generic: LLM_PROVIDER=openai-compatible, LLM_BASE_URL=http://host.docker.internal:8000/v1, LLM_MODEL=your-model
|
||||||
|
|
||||||
|
# Core OSINT / market source keys
|
||||||
|
FRED_API_KEY=
|
||||||
|
FIRMS_MAP_KEY=
|
||||||
|
EIA_API_KEY=
|
||||||
|
AISSTREAM_API_KEY=
|
||||||
|
ACLED_EMAIL=
|
||||||
|
ACLED_PASSWORD=
|
||||||
|
CLOUDFLARE_API_TOKEN=
|
||||||
|
BLS_API_KEY=
|
||||||
|
|
||||||
|
# Telegram bot and alerts
|
||||||
TELEGRAM_BOT_TOKEN=
|
TELEGRAM_BOT_TOKEN=
|
||||||
TELEGRAM_CHAT_ID=
|
TELEGRAM_CHAT_ID=
|
||||||
|
TELEGRAM_POLL_INTERVAL=5000
|
||||||
|
TELEGRAM_CHANNELS=
|
||||||
|
|
||||||
|
# Discord bot/webhook
|
||||||
|
DISCORD_BOT_TOKEN=
|
||||||
|
DISCORD_CHANNEL_ID=
|
||||||
|
DISCORD_GUILD_ID=
|
||||||
|
DISCORD_WEBHOOK_URL=
|
||||||
|
|||||||
42
.gitea/workflows/build.yml
Normal file
42
.gitea/workflows/build.yml
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
name: Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
- main
|
||||||
|
- codex/production-intelligence-terminal
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
test-and-image:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Set up Node
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "22"
|
||||||
|
cache: npm
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Unit tests
|
||||||
|
run: npm run test:unit
|
||||||
|
|
||||||
|
- name: Compose config
|
||||||
|
run: docker compose config
|
||||||
|
|
||||||
|
- name: Build Docker image
|
||||||
|
run: docker build -t git.wilkensxl.de/mrsphay/intelligence-terminal:${{ github.sha }} .
|
||||||
|
|
||||||
|
- name: Publish Docker image
|
||||||
|
if: ${{ secrets.REGISTRY_TOKEN != '' }}
|
||||||
|
run: |
|
||||||
|
echo "${{ secrets.REGISTRY_TOKEN }}" | docker login git.wilkensxl.de -u "${{ github.repository_owner }}" --password-stdin
|
||||||
|
docker tag git.wilkensxl.de/mrsphay/intelligence-terminal:${{ github.sha }} git.wilkensxl.de/mrsphay/intelligence-terminal:latest
|
||||||
|
docker push git.wilkensxl.de/mrsphay/intelligence-terminal:${{ github.sha }}
|
||||||
|
docker push git.wilkensxl.de/mrsphay/intelligence-terminal:latest
|
||||||
120
README.md
120
README.md
@@ -97,13 +97,105 @@ The dashboard opens automatically at `http://localhost:3117` and immediately beg
|
|||||||
### Docker
|
### Docker
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/calesthio/Crucix.git
|
docker pull git.wilkensxl.de/mrsphay/intelligence-terminal:latest
|
||||||
cd Crucix
|
|
||||||
cp .env.example .env # add your API keys
|
|
||||||
docker compose up -d
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Dashboard at `http://localhost:3117`. Sweep data persists in `./runs/` via volume mount. Includes a health check endpoint.
|
Dashboard at `http://localhost:3117`. Sweep data persists in `./runs/` via volume mount. The container disables browser auto-open by default, exposes `/api/health` and `/api/metrics`, and is suitable for Dockge/Pangolin.
|
||||||
|
|
||||||
|
#### Dockge / Pangolin Compose
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
services:
|
||||||
|
intelligence-terminal:
|
||||||
|
image: git.wilkensxl.de/mrsphay/intelligence-terminal:latest
|
||||||
|
container_name: intelligence-terminal
|
||||||
|
env_file:
|
||||||
|
- path: .env
|
||||||
|
required: false
|
||||||
|
environment:
|
||||||
|
PORT: ${PORT:-3117}
|
||||||
|
AUTO_OPEN_BROWSER: "false"
|
||||||
|
ports:
|
||||||
|
- "${PORT:-3117}:${PORT:-3117}"
|
||||||
|
volumes:
|
||||||
|
- ./runs:/app/runs
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:'+(process.env.PORT||3117)+'/api/health').then(r=>{if(![200,503].includes(r.status))process.exit(1);return r.json()}).then(j=>{if(j.status==='error')process.exit(1)}).catch(()=>process.exit(1))"]
|
||||||
|
interval: 60s
|
||||||
|
timeout: 10s
|
||||||
|
start_period: 45s
|
||||||
|
retries: 3
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Required `.env` Example
|
||||||
|
|
||||||
|
```env
|
||||||
|
PORT=3117
|
||||||
|
REFRESH_INTERVAL_MINUTES=15
|
||||||
|
AUTO_OPEN_BROWSER=false
|
||||||
|
STALE_DATA_MAX_AGE_MINUTES=60
|
||||||
|
SWEEP_TOKEN=
|
||||||
|
BRIEF_VERBOSITY=standard
|
||||||
|
|
||||||
|
LLM_PROVIDER=openrouter
|
||||||
|
LLM_BASE_URL=https://openrouter.ai/api/v1
|
||||||
|
LLM_API_KEY=
|
||||||
|
LLM_MODEL=openrouter/free
|
||||||
|
LLM_TEMPERATURE=0.2
|
||||||
|
LLM_MAX_TOKENS=2000
|
||||||
|
LLM_TIMEOUT_MS=90000
|
||||||
|
OPENROUTER_SITE_URL=https://git.wilkensxl.de/MrSphay/intelligence-terminal
|
||||||
|
OPENROUTER_APP_NAME=Intelligence Terminal
|
||||||
|
|
||||||
|
FRED_API_KEY=
|
||||||
|
FIRMS_MAP_KEY=
|
||||||
|
EIA_API_KEY=
|
||||||
|
AISSTREAM_API_KEY=
|
||||||
|
ACLED_EMAIL=
|
||||||
|
ACLED_PASSWORD=
|
||||||
|
CLOUDFLARE_API_TOKEN=
|
||||||
|
BLS_API_KEY=
|
||||||
|
|
||||||
|
TELEGRAM_BOT_TOKEN=
|
||||||
|
TELEGRAM_CHAT_ID=
|
||||||
|
DISCORD_BOT_TOKEN=
|
||||||
|
DISCORD_CHANNEL_ID=
|
||||||
|
DISCORD_GUILD_ID=
|
||||||
|
DISCORD_WEBHOOK_URL=
|
||||||
|
```
|
||||||
|
|
||||||
|
Local LLM examples:
|
||||||
|
|
||||||
|
```env
|
||||||
|
# LM Studio
|
||||||
|
LLM_PROVIDER=lmstudio
|
||||||
|
LLM_BASE_URL=http://host.docker.internal:1234/v1
|
||||||
|
LLM_MODEL=local-model
|
||||||
|
|
||||||
|
# Ollama
|
||||||
|
LLM_PROVIDER=ollama
|
||||||
|
LLM_BASE_URL=http://host.docker.internal:11434
|
||||||
|
LLM_MODEL=llama3.1:8b
|
||||||
|
|
||||||
|
# Generic OpenAI-compatible endpoint
|
||||||
|
LLM_PROVIDER=openai-compatible
|
||||||
|
LLM_BASE_URL=http://host.docker.internal:8000/v1
|
||||||
|
LLM_API_KEY=
|
||||||
|
LLM_MODEL=your-model
|
||||||
|
```
|
||||||
|
|
||||||
|
For Pangolin or another reverse proxy, forward HTTP traffic to `intelligence-terminal:3117` (or the `PORT` you set). Missing API keys do not crash sweeps; affected sources are reported as degraded in `/api/health`.
|
||||||
|
|
||||||
|
#### Build And Publish Your Gitea Image
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker login git.wilkensxl.de
|
||||||
|
docker build -t git.wilkensxl.de/mrsphay/intelligence-terminal:latest .
|
||||||
|
docker tag git.wilkensxl.de/mrsphay/intelligence-terminal:latest git.wilkensxl.de/mrsphay/intelligence-terminal:20260516
|
||||||
|
docker push git.wilkensxl.de/mrsphay/intelligence-terminal:latest
|
||||||
|
docker push git.wilkensxl.de/mrsphay/intelligence-terminal:20260516
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -186,12 +278,26 @@ Alerts are delivered as rich embeds with color-coded sidebars: red for FLASH, ye
|
|||||||
**Optional dependency:** The full bot requires `discord.js`. Install it with `npm install discord.js`. If it's not installed, Crucix automatically falls back to webhook-only mode.
|
**Optional dependency:** The full bot requires `discord.js`. Install it with `npm install discord.js`. If it's not installed, Crucix automatically falls back to webhook-only mode.
|
||||||
|
|
||||||
### Optional LLM Layer
|
### Optional LLM Layer
|
||||||
Connect any of 8 LLM providers for enhanced analysis:
|
Connect cloud or local OpenAI-compatible LLM providers for enhanced analysis:
|
||||||
- **AI trade ideas** — quantitative analyst producing 5-8 actionable ideas citing specific data
|
- **AI trade ideas** — quantitative analyst producing 5-8 actionable ideas citing specific data
|
||||||
- **Smarter alert evaluation** — LLM classifies signals into FLASH/PRIORITY/ROUTINE tiers with cross-domain correlation and confidence scoring
|
- **Smarter alert evaluation** — LLM classifies signals into FLASH/PRIORITY/ROUTINE tiers with cross-domain correlation and confidence scoring
|
||||||
- Providers: Anthropic Claude, OpenAI, Google Gemini, OpenRouter (Unified API), OpenAI Codex (ChatGPT subscription), MiniMax, Mistral, Grok
|
- Providers: OpenRouter, OpenAI-compatible APIs, LM Studio, Ollama, OpenAI, Anthropic Claude, Google Gemini, OpenAI Codex, MiniMax, Mistral, Grok
|
||||||
- Graceful fallback — when LLM is unavailable, a rule-based engine takes over alert evaluation. LLM failures never crash the sweep cycle.
|
- Graceful fallback — when LLM is unavailable, a rule-based engine takes over alert evaluation. LLM failures never crash the sweep cycle.
|
||||||
|
|
||||||
|
Primary env keys:
|
||||||
|
|
||||||
|
```env
|
||||||
|
LLM_PROVIDER=openrouter
|
||||||
|
LLM_BASE_URL=https://openrouter.ai/api/v1
|
||||||
|
LLM_API_KEY=
|
||||||
|
LLM_MODEL=openrouter/free
|
||||||
|
LLM_TEMPERATURE=0.2
|
||||||
|
LLM_MAX_TOKENS=2000
|
||||||
|
LLM_TIMEOUT_MS=90000
|
||||||
|
```
|
||||||
|
|
||||||
|
OpenRouter also supports model ids ending in `:free`; use the model id shown in your OpenRouter account when you want a specific free model.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## API Keys Setup
|
## API Keys Setup
|
||||||
|
|||||||
@@ -75,6 +75,14 @@ html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--s
|
|||||||
|
|
||||||
/* LEFT RAIL */
|
/* LEFT RAIL */
|
||||||
.layer-item{display:flex;align-items:center;justify-content:space-between;padding:8px;border:1px solid rgba(255,255,255,0.04);background:rgba(255,255,255,0.02);margin-bottom:4px}
|
.layer-item{display:flex;align-items:center;justify-content:space-between;padding:8px;border:1px solid rgba(255,255,255,0.04);background:rgba(255,255,255,0.02);margin-bottom:4px}
|
||||||
|
.layer-item{cursor:pointer;transition:border-color .15s ease,background .15s ease,opacity .15s ease}
|
||||||
|
.layer-item:hover{border-color:rgba(100,240,200,0.24);background:rgba(100,240,200,0.04)}
|
||||||
|
.layer-item.focused{border-color:rgba(100,240,200,0.6);background:rgba(100,240,200,0.08)}
|
||||||
|
.layer-item.hidden-layer{opacity:.45;border-color:rgba(255,95,99,0.18)}
|
||||||
|
.layer-mode{font-family:var(--mono);font-size:8px;color:var(--dim);margin-top:2px;text-transform:uppercase}
|
||||||
|
.sensor-actions{display:flex;gap:6px;align-items:center}
|
||||||
|
.mini-btn{border:1px solid rgba(100,240,200,0.18);background:rgba(100,240,200,0.04);color:var(--dim);font-family:var(--mono);font-size:9px;padding:3px 6px;cursor:pointer}
|
||||||
|
.mini-btn:hover{color:var(--accent);border-color:rgba(100,240,200,0.4)}
|
||||||
.layer-left{display:flex;align-items:center;gap:8px}
|
.layer-left{display:flex;align-items:center;gap:8px}
|
||||||
.ldot{width:10px;height:10px;border-radius:50%;flex-shrink:0}
|
.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.air{background:var(--accent);box-shadow:0 0 6px rgba(100,240,200,0.4)}
|
||||||
@@ -394,8 +402,23 @@ let globeInitialized = false;
|
|||||||
let flightsVisible = true;
|
let flightsVisible = true;
|
||||||
let lowPerfMode = localStorage.getItem('crucix_low_perf') === 'true';
|
let lowPerfMode = localStorage.getItem('crucix_low_perf') === 'true';
|
||||||
let isFlat = shouldStartFlat();
|
let isFlat = shouldStartFlat();
|
||||||
|
let layerModes = JSON.parse(localStorage.getItem('crucix_layer_modes') || '{}');
|
||||||
|
let spaceDisplayMode = localStorage.getItem('crucix_space_display') || 'icons';
|
||||||
let currentRegion = 'world';
|
let currentRegion = 'world';
|
||||||
let flatSvg, flatProjection, flatPath, flatG, flatZoom, flatW, flatH;
|
let flatSvg, flatProjection, flatPath, flatG, flatZoom, flatW, flatH;
|
||||||
|
|
||||||
|
const layerTypeMap = {
|
||||||
|
air: ['air'],
|
||||||
|
thermal: ['thermal'],
|
||||||
|
sdr: ['sdr'],
|
||||||
|
maritime: ['maritime'],
|
||||||
|
nuke: ['nuke', 'radiation'],
|
||||||
|
conflict: ['conflict'],
|
||||||
|
health: ['health'],
|
||||||
|
news: ['news', 'gdelt'],
|
||||||
|
osint: ['osint'],
|
||||||
|
space: ['space'],
|
||||||
|
};
|
||||||
const signalGuideItems = [
|
const signalGuideItems = [
|
||||||
{
|
{
|
||||||
term:'No Callsign',
|
term:'No Callsign',
|
||||||
@@ -602,6 +625,44 @@ function renderTopbar(){
|
|||||||
}
|
}
|
||||||
|
|
||||||
// === LEFT RAIL ===
|
// === LEFT RAIL ===
|
||||||
|
function layerMode(key){ return layerModes[key] || 'normal'; }
|
||||||
|
function layerModeLabel(key){ return layerMode(key) === 'focus' ? 'focused' : layerMode(key) === 'hidden' ? 'hidden' : 'normal'; }
|
||||||
|
function setLayerMode(key, mode){
|
||||||
|
if(mode === 'normal') delete layerModes[key]; else layerModes[key] = mode;
|
||||||
|
localStorage.setItem('crucix_layer_modes', JSON.stringify(layerModes));
|
||||||
|
renderLeftRail();
|
||||||
|
if(isFlat && flatG){ flatG.selectAll('*').remove(); drawFlatMap(); }
|
||||||
|
else plotMarkers();
|
||||||
|
}
|
||||||
|
function cycleLayerMode(key, event){
|
||||||
|
const current = layerMode(key);
|
||||||
|
const next = event && (event.shiftKey || event.ctrlKey || event.metaKey)
|
||||||
|
? (current === 'hidden' ? 'normal' : 'hidden')
|
||||||
|
: (current === 'focus' ? 'normal' : 'focus');
|
||||||
|
setLayerMode(key, next);
|
||||||
|
}
|
||||||
|
function resetLayerModes(){
|
||||||
|
layerModes = {};
|
||||||
|
localStorage.removeItem('crucix_layer_modes');
|
||||||
|
renderLeftRail();
|
||||||
|
if(isFlat && flatG){ flatG.selectAll('*').remove(); drawFlatMap(); }
|
||||||
|
else plotMarkers();
|
||||||
|
}
|
||||||
|
function toggleSpaceDisplay(){
|
||||||
|
spaceDisplayMode = spaceDisplayMode === 'icons' ? 'orbits' : 'icons';
|
||||||
|
localStorage.setItem('crucix_space_display', spaceDisplayMode);
|
||||||
|
renderLeftRail();
|
||||||
|
if(isFlat && flatG){ flatG.selectAll('*').remove(); drawFlatMap(); }
|
||||||
|
else plotMarkers();
|
||||||
|
}
|
||||||
|
function shouldShowType(type){
|
||||||
|
const focused = Object.entries(layerModes).filter(([,mode]) => mode === 'focus').flatMap(([key]) => layerTypeMap[key] || []);
|
||||||
|
const hidden = Object.entries(layerModes).filter(([,mode]) => mode === 'hidden').flatMap(([key]) => layerTypeMap[key] || []);
|
||||||
|
if(hidden.includes(type)) return false;
|
||||||
|
if(focused.length > 0) return focused.includes(type);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
function renderLeftRail(){
|
function renderLeftRail(){
|
||||||
const totalAir=D.air.reduce((s,a)=>s+a.total,0);
|
const totalAir=D.air.reduce((s,a)=>s+a.total,0);
|
||||||
const totalThermal=D.thermal.reduce((s,t)=>s+t.det,0);
|
const totalThermal=D.thermal.reduce((s,t)=>s+t.det,0);
|
||||||
@@ -610,16 +671,16 @@ function renderLeftRail(){
|
|||||||
const conflictEvents = D.acled?.totalEvents || 0;
|
const conflictEvents = D.acled?.totalEvents || 0;
|
||||||
const conflictFatal = D.acled?.totalFatalities || 0;
|
const conflictFatal = D.acled?.totalFatalities || 0;
|
||||||
const layers=[
|
const layers=[
|
||||||
{name:t('layers.airActivity','Air Activity'),count:totalAir,dot:'air',sub:`${D.air.length} ${t('layers.theaters','theaters')}`},
|
{key:'air',name:t('layers.airActivity','Air Activity'),count:totalAir,dot:'air',sub:`${D.air.length} ${t('layers.theaters','theaters')}`},
|
||||||
{name:t('layers.thermalSpikes','Thermal Spikes'),count:totalThermal.toLocaleString(),dot:'thermal',sub:`${totalNight.toLocaleString()} ${t('layers.nightDet','night det.')}`},
|
{key:'thermal',name:t('layers.thermalSpikes','Thermal Spikes'),count:totalThermal.toLocaleString(),dot:'thermal',sub:`${totalNight.toLocaleString()} ${t('layers.nightDet','night det.')}`},
|
||||||
{name:t('layers.sdrCoverage','SDR Coverage'),count:D.sdr.total,dot:'sdr',sub:`${D.sdr.online} ${t('layers.online','online')}`},
|
{key:'sdr',name:t('layers.sdrCoverage','SDR Coverage'),count:D.sdr.total,dot:'sdr',sub:`${D.sdr.online} ${t('layers.online','online')}`},
|
||||||
{name:t('layers.maritimeWatch','Maritime Watch'),count:D.chokepoints.length,dot:'maritime',sub:t('layers.chokepoints','chokepoints')},
|
{key:'maritime',name:t('layers.maritimeWatch','Maritime Watch'),count:D.chokepoints.length,dot:'maritime',sub:t('layers.chokepoints','chokepoints')},
|
||||||
{name:t('layers.nuclearSites','Nuclear Sites'),count:D.nuke.length,dot:'nuke',sub:t('layers.monitors','monitors')},
|
{key:'nuke',name:t('layers.nuclearSites','Nuclear Sites'),count:D.nuke.length,dot:'nuke',sub:t('layers.monitors','monitors')},
|
||||||
{name:t('layers.conflictEvents','Conflict Events'),count:conflictEvents,dot:'thermal',sub:`${conflictFatal.toLocaleString()} ${t('layers.fatalities','fatalities')}`},
|
{key:'conflict',name:t('layers.conflictEvents','Conflict Events'),count:conflictEvents,dot:'thermal',sub:`${conflictFatal.toLocaleString()} ${t('layers.fatalities','fatalities')}`},
|
||||||
{name:t('layers.healthWatch','Health Watch'),count:D.who.length,dot:'health',sub:t('layers.whoAlerts','WHO alerts')},
|
{key:'health',name:t('layers.healthWatch','Health Watch'),count:D.who.length,dot:'health',sub:t('layers.whoAlerts','WHO alerts')},
|
||||||
{name:t('layers.worldNews','World News'),count:newsCount,dot:'news',sub:t('layers.rssGeolocated','RSS geolocated')},
|
{key:'news',name:t('layers.worldNews','World News'),count:newsCount,dot:'news',sub:t('layers.rssGeolocated','RSS geolocated')},
|
||||||
{name:t('layers.osintFeed','OSINT Feed'),count:D.tg.posts,dot:'incident',sub:`${D.tg.urgent.length} ${t('badges.urgent','urgent').toLowerCase()}`},
|
{key:'osint',name:t('layers.osintFeed','OSINT Feed'),count:D.tg.posts,dot:'incident',sub:`${D.tg.urgent.length} ${t('badges.urgent','urgent').toLowerCase()}`},
|
||||||
{name:t('layers.spaceActivity','Satellites'),count:D.space?.militarySats||0,dot:'space',sub:`${D.space?.totalNewObjects||0} ${t('space.newLast30d','new (30d)')}`}
|
{key:'space',name:t('layers.spaceActivity','Satellites'),count:D.space?.militarySats||0,dot:'space',sub:`${D.space?.totalNewObjects||0} ${t('space.newLast30d','new (30d)')}`}
|
||||||
];
|
];
|
||||||
const allNormal=D.nuke.every(s=>!s.anom);
|
const 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 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('');
|
||||||
@@ -632,8 +693,8 @@ function renderLeftRail(){
|
|||||||
|
|
||||||
document.getElementById('leftRail').innerHTML=`
|
document.getElementById('leftRail').innerHTML=`
|
||||||
<div class="g-panel">
|
<div class="g-panel">
|
||||||
<div class="sec-head"><h3>${t('panels.sensorGrid','Sensor Grid')}</h3><span class="badge">${t('badges.live','LIVE')}</span></div>
|
<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"><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('')}
|
${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>
|
||||||
<div class="g-panel">
|
<div class="g-panel">
|
||||||
<div class="sec-head"><h3>${t('panels.nuclearWatch','Nuclear Watch')}</h3><span class="badge">${t('badges.radiation','RADIATION')}</span></div>
|
<div class="sec-head"><h3>${t('panels.nuclearWatch','Nuclear Watch')}</h3><span class="badge">${t('badges.radiation','RADIATION')}</span></div>
|
||||||
@@ -651,7 +712,7 @@ function renderLeftRail(){
|
|||||||
<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 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>
|
||||||
<div class="g-panel">
|
<div class="g-panel">
|
||||||
<div class="sec-head"><h3>${t('panels.spaceWatch','Space Watch')}</h3><span class="badge">${t('badges.orbital','CELESTRAK')}</span></div>
|
<div class="sec-head"><h3>${t('panels.spaceWatch','Space Watch')}</h3><button class="mini-btn" onclick="toggleSpaceDisplay()">${spaceDisplayMode.toUpperCase()}</button></div>
|
||||||
${D.space ? `
|
${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">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">Military Sats</span><span class="eval">${D.space.militarySats||0}</span></div>
|
||||||
@@ -953,7 +1014,7 @@ function plotMarkers(){
|
|||||||
});
|
});
|
||||||
|
|
||||||
// === ISS + Space Stations (bright white, pulsing) ===
|
// === ISS + Space Stations (bright white, pulsing) ===
|
||||||
(D.space?.stationPositions||[]).forEach(s=>{
|
if(spaceDisplayMode === 'icons') (D.space?.stationPositions||[]).forEach(s=>{
|
||||||
points.push({
|
points.push({
|
||||||
lat:s.lat, lng:s.lon, size:0.4, alt:0.04,
|
lat:s.lat, lng:s.lon, size:0.4, alt:0.04,
|
||||||
color:'rgba(255,255,255,0.95)', type:'space', priority:1,
|
color:'rgba(255,255,255,0.95)', type:'space', priority:1,
|
||||||
@@ -973,12 +1034,15 @@ function plotMarkers(){
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Set points on globe
|
|
||||||
globe.pointsData(points);
|
|
||||||
globe.labelsData(labels);
|
|
||||||
|
|
||||||
// === ACLED CONFLICT EVENTS (pulsing rings) ===
|
// === ACLED CONFLICT EVENTS (pulsing rings) ===
|
||||||
const conflictRings = (D.acled?.deadliestEvents || []).filter(e => e.lat && e.lon).map(e => {
|
const visiblePoints = points.filter(p => shouldShowType(p.type));
|
||||||
|
const visibleLabels = labels.filter(l => !l.type || shouldShowType(l.type));
|
||||||
|
|
||||||
|
// Set points on globe
|
||||||
|
globe.pointsData(visiblePoints);
|
||||||
|
globe.labelsData(visibleLabels);
|
||||||
|
|
||||||
|
const conflictRings = shouldShowType('conflict') ? (D.acled?.deadliestEvents || []).filter(e => e.lat && e.lon).map(e => {
|
||||||
const logFatal = Math.log2(Math.max(e.fatalities, 1));
|
const logFatal = Math.log2(Math.max(e.fatalities, 1));
|
||||||
return {
|
return {
|
||||||
lat: e.lat, lng: e.lon,
|
lat: e.lat, lng: e.lon,
|
||||||
@@ -988,12 +1052,12 @@ function plotMarkers(){
|
|||||||
popHead: e.type || 'CONFLICT', popMeta: 'ACLED Conflict Data',
|
popHead: e.type || 'CONFLICT', popMeta: 'ACLED Conflict Data',
|
||||||
popText: `${e.fatalities} fatalities<br>${e.location}, ${e.country}<br>Date: ${e.date}`
|
popText: `${e.fatalities} fatalities<br>${e.location}, ${e.country}<br>Date: ${e.date}`
|
||||||
};
|
};
|
||||||
});
|
}) : [];
|
||||||
globe.ringsData(conflictRings);
|
globe.ringsData(conflictRings);
|
||||||
|
|
||||||
// === FLIGHT CORRIDORS (3D arcs) ===
|
// === FLIGHT CORRIDORS (3D arcs) ===
|
||||||
const arcs = [];
|
const arcs = [];
|
||||||
if(flightsVisible){
|
if(flightsVisible && shouldShowType('air')){
|
||||||
const airCoordsFlight = [
|
const airCoordsFlight = [
|
||||||
{region:'Middle East',lat:30,lon:44}, {region:'Taiwan Strait',lat:24,lon:120},
|
{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:'Ukraine Region',lat:49,lon:32}, {region:'Baltic Region',lat:57,lon:24},
|
||||||
@@ -1040,6 +1104,17 @@ function plotMarkers(){
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if(spaceDisplayMode === 'orbits' && shouldShowType('space')){
|
||||||
|
(D.space?.stationPositions||[]).forEach(s=>{
|
||||||
|
arcs.push({
|
||||||
|
startLat: Math.max(-65, s.lat - 18), startLng: s.lon - 80,
|
||||||
|
endLat: Math.min(65, s.lat + 18), endLng: s.lon + 80,
|
||||||
|
color: ['rgba(224,176,255,0.55)','rgba(224,176,255,0.12)'],
|
||||||
|
stroke: 0.7,
|
||||||
|
label: `${s.name} approximate orbital track`
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
globe.arcsData(arcs);
|
globe.arcsData(arcs);
|
||||||
|
|
||||||
// Zoom-aware marker sizing: scale markers and labels with camera altitude
|
// Zoom-aware marker sizing: scale markers and labels with camera altitude
|
||||||
@@ -1055,11 +1130,11 @@ function plotMarkers(){
|
|||||||
globe.arcDashAnimateTime(lowPerfMode ? 0 : 2000);
|
globe.arcDashAnimateTime(lowPerfMode ? 0 : 2000);
|
||||||
// Priority-based point visibility: hide low-priority markers when zoomed out
|
// Priority-based point visibility: hide low-priority markers when zoomed out
|
||||||
if(alt > 2.0){
|
if(alt > 2.0){
|
||||||
globe.pointsData(points.filter(p => (p.priority||3) <= 1));
|
globe.pointsData(visiblePoints.filter(p => (p.priority||3) <= 1));
|
||||||
} else if(alt > 1.2){
|
} else if(alt > 1.2){
|
||||||
globe.pointsData(points.filter(p => (p.priority||3) <= 2));
|
globe.pointsData(visiblePoints.filter(p => (p.priority||3) <= 2));
|
||||||
} else {
|
} else {
|
||||||
globe.pointsData(points);
|
globe.pointsData(visiblePoints);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
if(typeof globe.onZoom==='function') globe.onZoom(onGlobeZoom);
|
if(typeof globe.onZoom==='function') globe.onZoom(onGlobeZoom);
|
||||||
@@ -1178,7 +1253,7 @@ function initFlatMap(){
|
|||||||
flatG.selectAll('.marker-circle').attr('r',function(){return +this.dataset.baseR/Math.sqrt(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')
|
flatG.selectAll('.marker-label').style('font-size',Math.max(7,9/Math.sqrt(k))+'px')
|
||||||
.style('display',k>=2.5?'block':'none');
|
.style('display',k>=2.5?'block':'none');
|
||||||
});
|
}).on('end',()=>{ flatG.selectAll('.marker-label').style('display','block'); });
|
||||||
flatSvg.call(flatZoom);
|
flatSvg.call(flatZoom);
|
||||||
drawFlatMap();
|
drawFlatMap();
|
||||||
}
|
}
|
||||||
@@ -1197,7 +1272,8 @@ function drawFlatMap(){
|
|||||||
function plotFlatMarkers(){
|
function plotFlatMarkers(){
|
||||||
const mg=flatG.append('g').attr('class','markers');
|
const mg=flatG.append('g').attr('class','markers');
|
||||||
const proj=flatProjection;
|
const proj=flatProjection;
|
||||||
const addPt=(lat,lon,r,fill,stroke,onClick,priority)=>{
|
const addPt=(lat,lon,r,fill,stroke,onClick,priority,type)=>{
|
||||||
|
if(type && !shouldShowType(type)) return null;
|
||||||
const[x,y]=proj([lon,lat]);if(!x||!y)return null;
|
const[x,y]=proj([lon,lat]);if(!x||!y)return null;
|
||||||
const g=mg.append('g').attr('transform',`translate(${x},${y})`).style('cursor','pointer').attr('data-priority',priority||3);
|
const g=mg.append('g').attr('transform',`translate(${x},${y})`).style('cursor','pointer').attr('data-priority',priority||3);
|
||||||
if(onClick) g.on('click',ev=>{ev.stopPropagation();onClick(ev)});
|
if(onClick) g.on('click',ev=>{ev.stopPropagation();onClick(ev)});
|
||||||
@@ -1215,12 +1291,12 @@ function plotFlatMarkers(){
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
// Thermal
|
// Thermal
|
||||||
D.thermal.forEach(t=>t.fires.forEach(f=>{
|
if(shouldShowType('thermal')) D.thermal.forEach(t=>t.fires.forEach(f=>{
|
||||||
addPt(f.lat,f.lon,2+Math.min(f.frp/50,5),'rgba(255,95,99,0.6)','rgba(255,95,99,0.2)',
|
addPt(f.lat,f.lon,2+Math.min(f.frp/50,5),'rgba(255,95,99,0.6)','rgba(255,95,99,0.2)',
|
||||||
ev=>showPopup(ev,'Thermal',`${t.region}<br>FRP: ${f.frp.toFixed(1)} MW`,'FIRMS'),3);
|
ev=>showPopup(ev,'Thermal',`${t.region}<br>FRP: ${f.frp.toFixed(1)} MW`,'FIRMS'),3);
|
||||||
}));
|
}));
|
||||||
// Chokepoints
|
// Chokepoints
|
||||||
D.chokepoints.forEach(cp=>{
|
if(shouldShowType('maritime')) D.chokepoints.forEach(cp=>{
|
||||||
const[x,y]=proj([cp.lon,cp.lat]);if(!x||!y)return;
|
const[x,y]=proj([cp.lon,cp.lat]);if(!x||!y)return;
|
||||||
const g=mg.append('g').attr('transform',`translate(${x},${y})`).style('cursor','pointer').attr('data-priority',1)
|
const g=mg.append('g').attr('transform',`translate(${x},${y})`).style('cursor','pointer').attr('data-priority',1)
|
||||||
.on('click',ev=>{ev.stopPropagation();showPopup(ev,cp.label,cp.note,'Maritime')});
|
.on('click',ev=>{ev.stopPropagation();showPopup(ev,cp.label,cp.note,'Maritime')});
|
||||||
@@ -1229,30 +1305,30 @@ function plotFlatMarkers(){
|
|||||||
});
|
});
|
||||||
// Nuclear
|
// 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}];
|
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'),2)});
|
if(shouldShowType('nuke')) D.nuke.forEach((n,i)=>{const c=nukeCoords[i];if(!c)return;addPt(c.lat,c.lon,4,'rgba(255,224,130,0.7)','rgba(255,224,130,0.3)',ev=>showPopup(ev,n.site,`CPM: ${n.cpm?.toFixed(1)||'--'}`,'Radiation'),2)});
|
||||||
// SDR
|
// SDR
|
||||||
D.sdr.zones.forEach(z=>z.receivers.forEach(r=>{addPt(r.lat,r.lon,2.5,'rgba(68,204,255,0.5)','rgba(68,204,255,0.2)',ev=>showPopup(ev,'SDR',`${r.name}<br>${z.region}`,'KiwiSDR'),3)}));
|
if(shouldShowType('sdr')) D.sdr.zones.forEach(z=>z.receivers.forEach(r=>{addPt(r.lat,r.lon,2.5,'rgba(68,204,255,0.5)','rgba(68,204,255,0.2)',ev=>showPopup(ev,'SDR',`${r.name}<br>${z.region}`,'KiwiSDR'),3)}));
|
||||||
// OSINT
|
// 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}];
|
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`),2)});
|
if(shouldShowType('osint')) osintGeo.forEach(o=>{const p=D.tg.urgent[o.idx];if(!p)return;addPt(o.lat,o.lon,4,'rgba(255,184,76,0.7)','rgba(255,184,76,0.3)',ev=>showPopup(ev,(p.channel||'').toUpperCase(),cleanText(p.text?.substring(0,200)||''),`${p.views||'?'} views`),2)});
|
||||||
// WHO
|
// 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}];
|
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'),2)});
|
if(shouldShowType('health')) D.who.slice(0,10).forEach((w,i)=>{const c=whoGeo[i];if(!c)return;addPt(c.lat,c.lon,3.5,'rgba(105,240,174,0.6)','rgba(105,240,174,0.2)',ev=>showPopup(ev,w.title,w.summary||'','WHO'),2)});
|
||||||
// News
|
// News
|
||||||
(D.news||[]).forEach(n=>{addPt(n.lat,n.lon,3,'rgba(129,212,250,0.6)','rgba(129,212,250,0.2)',ev=>showPopup(ev,n.source+' NEWS',cleanText(n.title),n.region),3)});
|
if(shouldShowType('news')) (D.news||[]).forEach(n=>{addPt(n.lat,n.lon,3,'rgba(129,212,250,0.6)','rgba(129,212,250,0.2)',ev=>showPopup(ev,n.source+' NEWS',cleanText(n.title),n.region),3)});
|
||||||
// NOAA weather
|
// NOAA weather
|
||||||
(D.noaa?.alerts||[]).forEach(a=>{addPt(a.lat,a.lon,4,'rgba(255,152,0,0.7)','rgba(255,152,0,0.3)',ev=>showPopup(ev,a.event,a.headline||'','NOAA/NWS'),2)});
|
if(shouldShowType('weather')) (D.noaa?.alerts||[]).forEach(a=>{addPt(a.lat,a.lon,4,'rgba(255,152,0,0.7)','rgba(255,152,0,0.3)',ev=>showPopup(ev,a.event,a.headline||'','NOAA/NWS'),2)});
|
||||||
// EPA RadNet
|
// EPA RadNet
|
||||||
(D.epa?.stations||[]).forEach(s=>{addPt(s.lat,s.lon,3,'rgba(205,220,57,0.6)','rgba(205,220,57,0.2)',ev=>showPopup(ev,'RadNet: '+s.location,`${s.analyte||'--'}: ${s.result||'--'} ${s.unit||''}`,'EPA'),3)});
|
if(shouldShowType('environment')) (D.epa?.stations||[]).forEach(s=>{addPt(s.lat,s.lon,3,'rgba(205,220,57,0.6)','rgba(205,220,57,0.2)',ev=>showPopup(ev,'RadNet: '+s.location,`${s.analyte||'--'}: ${s.result||'--'} ${s.unit||''}`,'EPA'),3)});
|
||||||
// Space stations
|
// Space stations
|
||||||
(D.space?.stationPositions||[]).forEach(s=>{
|
if(spaceDisplayMode === 'icons' && shouldShowType('space')) (D.space?.stationPositions||[]).forEach(s=>{
|
||||||
const g=addPt(s.lat,s.lon,5,'rgba(255,255,255,0.9)','rgba(255,255,255,0.4)',ev=>showPopup(ev,s.name,'Orbital position estimate','Space Station'),1);
|
const g=addPt(s.lat,s.lon,5,'rgba(255,255,255,0.9)','rgba(255,255,255,0.4)',ev=>showPopup(ev,s.name,'Orbital position estimate','Space Station'),1);
|
||||||
if(g) g.append('text').attr('class','marker-label').attr('x',8).attr('y',3).attr('fill','rgba(255,255,255,0.7)').attr('font-size','8px').attr('font-family','var(--mono)').text(s.name.split('(')[0].trim());
|
if(g) g.append('text').attr('class','marker-label').attr('x',8).attr('y',3).attr('fill','rgba(255,255,255,0.7)').attr('font-size','8px').attr('font-family','var(--mono)').text(s.name.split('(')[0].trim());
|
||||||
});
|
});
|
||||||
// GDELT geo events
|
// GDELT geo events
|
||||||
(D.gdelt?.geoPoints||[]).forEach(g=>{addPt(g.lat,g.lon,2.5,'rgba(100,149,237,0.5)','rgba(100,149,237,0.2)',ev=>showPopup(ev,'GDELT Event',g.name||'','GDELT · '+g.count+' reports'),3)});
|
if(shouldShowType('gdelt')) (D.gdelt?.geoPoints||[]).forEach(g=>{addPt(g.lat,g.lon,2.5,'rgba(100,149,237,0.5)','rgba(100,149,237,0.2)',ev=>showPopup(ev,'GDELT Event',g.name||'','GDELT · '+g.count+' reports'),3)});
|
||||||
// ACLED
|
// ACLED
|
||||||
(D.acled?.deadliestEvents||[]).filter(e=>e.lat&&e.lon).forEach(e=>{
|
if(shouldShowType('conflict')) (D.acled?.deadliestEvents||[]).filter(e=>e.lat&&e.lon).forEach(e=>{
|
||||||
const[x,y]=proj([e.lon,e.lat]);if(!x||!y)return;
|
const[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 r=Math.max(4,Math.min(14,2+Math.log2(Math.max(e.fatalities,1))*1.5));
|
||||||
const g=mg.append('g').attr('transform',`translate(${x},${y})`).style('cursor','pointer').attr('data-priority',1)
|
const g=mg.append('g').attr('transform',`translate(${x},${y})`).style('cursor','pointer').attr('data-priority',1)
|
||||||
@@ -1261,7 +1337,7 @@ function plotFlatMarkers(){
|
|||||||
g.append('circle').attr('r',r*0.4).attr('fill','rgba(255,120,80,0.3)');
|
g.append('circle').attr('r',r*0.4).attr('fill','rgba(255,120,80,0.3)');
|
||||||
});
|
});
|
||||||
// Flight corridors
|
// Flight corridors
|
||||||
if(flightsVisible){
|
if(flightsVisible && shouldShowType('air')){
|
||||||
const airCoordsFlight=[{lat:30,lon:44},{lat:24,lon:120},{lat:49,lon:32},{lat:57,lon:24},{lat:14,lon:114},{lat:37,lon:127},{lat:25,lon:-80},{lat:4,lon:2},{lat:-34,lon:18},{lat:10,lon:51}];
|
const airCoordsFlight=[{lat:30,lon:44},{lat:24,lon:120},{lat:49,lon:32},{lat:57,lon:24},{lat:14,lon:114},{lat:37,lon:127},{lat:25,lon:-80},{lat:4,lon:2},{lat:-34,lon:18},{lat:10,lon:51}];
|
||||||
const hubs=[{lat:40.6,lon:-73.8},{lat:51.5,lon:-0.5},{lat:25.3,lon:55.4},{lat:1.4,lon:103.8},{lat:-33.9,lon:151.2},{lat:-23.4,lon:-46.5}];
|
const 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');
|
const cG=flatG.append('g').attr('class','corridors-layer');
|
||||||
|
|||||||
18
docs/sources/README.md
Normal file
18
docs/sources/README.md
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
# Source Operations
|
||||||
|
|
||||||
|
Each source should fail visibly and safely. A missing key or remote outage must produce `degraded` source health instead of silent demo data.
|
||||||
|
|
||||||
|
Check runtime state:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://localhost:3117/api/health
|
||||||
|
curl http://localhost:3117/api/metrics
|
||||||
|
```
|
||||||
|
|
||||||
|
Source docs:
|
||||||
|
|
||||||
|
- [OpenSky](opensky.md)
|
||||||
|
- [ACLED](acled.md)
|
||||||
|
- [Telegram](telegram.md)
|
||||||
|
- [FIRMS](firms.md)
|
||||||
|
- [Maritime](maritime.md)
|
||||||
9
docs/sources/acled.md
Normal file
9
docs/sources/acled.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# ACLED
|
||||||
|
|
||||||
|
Provides conflict events, fatalities, event types, and locations.
|
||||||
|
|
||||||
|
- Auth: `ACLED_EMAIL` and `ACLED_PASSWORD`.
|
||||||
|
- Flow: OAuth password grant is tried first, then cookie session fallback.
|
||||||
|
- Failure modes: missing credentials, terms/profile not completed, expired token, account missing API access.
|
||||||
|
- Behavior: missing or rejected credentials produce degraded source health with the ACLED error text.
|
||||||
|
- Test: set credentials, run `node apis/sources/acled.mjs`, then check `/api/health`.
|
||||||
8
docs/sources/firms.md
Normal file
8
docs/sources/firms.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# FIRMS
|
||||||
|
|
||||||
|
Provides NASA satellite fire and thermal detections.
|
||||||
|
|
||||||
|
- Auth: `FIRMS_MAP_KEY` recommended.
|
||||||
|
- Failure modes: missing key, timeout, regional API throttle.
|
||||||
|
- Behavior: source health reports degraded or failed; dashboard panels stay visible and show available sources.
|
||||||
|
- Test: set `FIRMS_MAP_KEY`, run a sweep, inspect thermal markers and `/api/metrics`.
|
||||||
8
docs/sources/maritime.md
Normal file
8
docs/sources/maritime.md
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# Maritime
|
||||||
|
|
||||||
|
Provides chokepoint context and optional AIS-related maritime data.
|
||||||
|
|
||||||
|
- Auth: `AISSTREAM_API_KEY` enables richer AIS behavior where supported.
|
||||||
|
- Failure modes: missing key, remote stream unavailable, no vessels in a monitored region.
|
||||||
|
- Behavior: static chokepoint context remains available, live source health reports degraded when remote data is unavailable.
|
||||||
|
- Test: run a sweep and inspect maritime markers, source health, and network metrics.
|
||||||
9
docs/sources/opensky.md
Normal file
9
docs/sources/opensky.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# OpenSky
|
||||||
|
|
||||||
|
Provides public aircraft state data for regional air-activity hotspots.
|
||||||
|
|
||||||
|
- Auth: public queries work without credentials.
|
||||||
|
- Failure modes: timeouts, `HTTP 429`, and empty regions.
|
||||||
|
- Behavior: source health is marked degraded on API errors. The dashboard may use the most recent non-empty air snapshot from `runs/` and marks it in `airMeta.fallback`.
|
||||||
|
- Test: start a sweep and inspect `/api/health` plus `airMeta` from `/api/data`.
|
||||||
|
- Operator note: Crucix does not bypass OpenSky throttles. Increase `REFRESH_INTERVAL_MINUTES` if the source degrades repeatedly.
|
||||||
9
docs/sources/telegram.md
Normal file
9
docs/sources/telegram.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
# Telegram
|
||||||
|
|
||||||
|
Provides OSINT posts and bot commands/alerts.
|
||||||
|
|
||||||
|
- Source collection uses configured public/channel inputs where available.
|
||||||
|
- Bot alerts require `TELEGRAM_BOT_TOKEN` and `TELEGRAM_CHAT_ID`.
|
||||||
|
- Polling failures are non-fatal and use backoff logging to avoid log spam.
|
||||||
|
- `/brief` includes source integrity, evidence links, event id, why-it-matters, and next-step context.
|
||||||
|
- Test: configure bot variables, start the container, send `/status` and `/brief`.
|
||||||
Reference in New Issue
Block a user