From 42b7fc202450e1105a02e249f9694367f5958a0f Mon Sep 17 00:00:00 2001 From: MrSphay Date: Sat, 16 May 2026 21:18:51 +0200 Subject: [PATCH] docs: add registry dockge and dashboard operations --- .env.example | 82 +++++++++---------- .gitea/workflows/build.yml | 42 ++++++++++ README.md | 120 +++++++++++++++++++++++++-- dashboard/public/jarvis.html | 154 ++++++++++++++++++++++++++--------- docs/sources/README.md | 18 ++++ docs/sources/acled.md | 9 ++ docs/sources/firms.md | 8 ++ docs/sources/maritime.md | 8 ++ docs/sources/opensky.md | 9 ++ docs/sources/telegram.md | 9 ++ 10 files changed, 372 insertions(+), 87 deletions(-) create mode 100644 .gitea/workflows/build.yml create mode 100644 docs/sources/README.md create mode 100644 docs/sources/acled.md create mode 100644 docs/sources/firms.md create mode 100644 docs/sources/maritime.md create mode 100644 docs/sources/opensky.md create mode 100644 docs/sources/telegram.md diff --git a/.env.example b/.env.example index 674e882..3ea7f45 100644 --- a/.env.example +++ b/.env.example @@ -1,49 +1,49 @@ -# ============================================ -# Crucix Intelligence Engine — Configuration -# ============================================ -# 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. +# Intelligence Terminal / Crucix configuration +# Copy to .env. Keep comments on separate lines; Docker env_file treats inline comments as values. -# === OSINT Source API Keys === - -# 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 +# Server PORT=3117 -# Auto-refresh interval (minutes) REFRESH_INTERVAL_MINUTES=15 +AUTO_OPEN_BROWSER=false +STALE_DATA_MAX_AGE_MINUTES=60 +SWEEP_TOKEN= +BRIEF_VERBOSITY=standard -# === LLM Layer (optional) === -# Enables AI-enhanced trade ideas and breaking news Telegram alerts. -# Provider options: anthropic | openai | gemini | codex | openrouter | minimax | mistral | ollama | grok -LLM_PROVIDER= -# Not needed for codex (uses ~/.codex/auth.json) or ollama (local) +# LLM layer +# Providers: openrouter | openai-compatible | lmstudio | ollama | openai | anthropic | gemini | mistral | minimax | grok | codex +LLM_PROVIDER=openrouter +LLM_BASE_URL=https://openrouter.ai/api/v1 LLM_API_KEY= -# Optional override. Each provider has a sensible default: -# 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_MODEL= -# Ollama base URL (only needed if not using default http://localhost:11434) -OLLAMA_BASE_URL= +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 -# === Telegram Alerts (optional, requires LLM) === -# Create a bot via @BotFather, get chat ID via @userinfobot +# Local OpenAI-compatible examples +# 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_CHAT_ID= +TELEGRAM_POLL_INTERVAL=5000 +TELEGRAM_CHANNELS= + +# Discord bot/webhook +DISCORD_BOT_TOKEN= +DISCORD_CHANNEL_ID= +DISCORD_GUILD_ID= +DISCORD_WEBHOOK_URL= diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml new file mode 100644 index 0000000..188020f --- /dev/null +++ b/.gitea/workflows/build.yml @@ -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 diff --git a/README.md b/README.md index a05f962..16d5db8 100644 --- a/README.md +++ b/README.md @@ -97,13 +97,105 @@ The dashboard opens automatically at `http://localhost:3117` and immediately beg ### Docker ```bash -git clone https://github.com/calesthio/Crucix.git -cd Crucix -cp .env.example .env # add your API keys -docker compose up -d +docker pull git.wilkensxl.de/mrsphay/intelligence-terminal:latest ``` -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 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 - **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. +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 diff --git a/dashboard/public/jarvis.html b/dashboard/public/jarvis.html index 05a5c7a..921b3fd 100644 --- a/dashboard/public/jarvis.html +++ b/dashboard/public/jarvis.html @@ -75,6 +75,14 @@ html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--s /* LEFT RAIL */ .layer-item{display:flex;align-items:center;justify-content:space-between;padding:8px;border:1px solid rgba(255,255,255,0.04);background:rgba(255,255,255,0.02);margin-bottom:4px} +.layer-item{cursor:pointer;transition:border-color .15s ease,background .15s ease,opacity .15s ease} +.layer-item:hover{border-color:rgba(100,240,200,0.24);background:rgba(100,240,200,0.04)} +.layer-item.focused{border-color:rgba(100,240,200,0.6);background:rgba(100,240,200,0.08)} +.layer-item.hidden-layer{opacity:.45;border-color:rgba(255,95,99,0.18)} +.layer-mode{font-family:var(--mono);font-size:8px;color:var(--dim);margin-top:2px;text-transform:uppercase} +.sensor-actions{display:flex;gap:6px;align-items:center} +.mini-btn{border:1px solid rgba(100,240,200,0.18);background:rgba(100,240,200,0.04);color:var(--dim);font-family:var(--mono);font-size:9px;padding:3px 6px;cursor:pointer} +.mini-btn:hover{color:var(--accent);border-color:rgba(100,240,200,0.4)} .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)} @@ -394,8 +402,23 @@ let globeInitialized = false; let flightsVisible = true; let lowPerfMode = localStorage.getItem('crucix_low_perf') === 'true'; let isFlat = shouldStartFlat(); +let layerModes = JSON.parse(localStorage.getItem('crucix_layer_modes') || '{}'); +let spaceDisplayMode = localStorage.getItem('crucix_space_display') || 'icons'; let currentRegion = 'world'; let flatSvg, flatProjection, flatPath, flatG, flatZoom, flatW, flatH; + +const layerTypeMap = { + air: ['air'], + thermal: ['thermal'], + sdr: ['sdr'], + maritime: ['maritime'], + nuke: ['nuke', 'radiation'], + conflict: ['conflict'], + health: ['health'], + news: ['news', 'gdelt'], + osint: ['osint'], + space: ['space'], +}; const signalGuideItems = [ { term:'No Callsign', @@ -602,6 +625,44 @@ function renderTopbar(){ } // === LEFT RAIL === +function layerMode(key){ return layerModes[key] || 'normal'; } +function layerModeLabel(key){ return layerMode(key) === 'focus' ? 'focused' : layerMode(key) === 'hidden' ? 'hidden' : 'normal'; } +function setLayerMode(key, mode){ + if(mode === 'normal') delete layerModes[key]; else layerModes[key] = mode; + localStorage.setItem('crucix_layer_modes', JSON.stringify(layerModes)); + renderLeftRail(); + if(isFlat && flatG){ flatG.selectAll('*').remove(); drawFlatMap(); } + else plotMarkers(); +} +function cycleLayerMode(key, event){ + const current = layerMode(key); + const next = event && (event.shiftKey || event.ctrlKey || event.metaKey) + ? (current === 'hidden' ? 'normal' : 'hidden') + : (current === 'focus' ? 'normal' : 'focus'); + setLayerMode(key, next); +} +function resetLayerModes(){ + layerModes = {}; + localStorage.removeItem('crucix_layer_modes'); + renderLeftRail(); + if(isFlat && flatG){ flatG.selectAll('*').remove(); drawFlatMap(); } + else plotMarkers(); +} +function toggleSpaceDisplay(){ + spaceDisplayMode = spaceDisplayMode === 'icons' ? 'orbits' : 'icons'; + localStorage.setItem('crucix_space_display', spaceDisplayMode); + renderLeftRail(); + if(isFlat && flatG){ flatG.selectAll('*').remove(); drawFlatMap(); } + else plotMarkers(); +} +function shouldShowType(type){ + const focused = Object.entries(layerModes).filter(([,mode]) => mode === 'focus').flatMap(([key]) => layerTypeMap[key] || []); + const hidden = Object.entries(layerModes).filter(([,mode]) => mode === 'hidden').flatMap(([key]) => layerTypeMap[key] || []); + if(hidden.includes(type)) return false; + if(focused.length > 0) return focused.includes(type); + return true; +} + function renderLeftRail(){ const totalAir=D.air.reduce((s,a)=>s+a.total,0); const totalThermal=D.thermal.reduce((s,t)=>s+t.det,0); @@ -610,16 +671,16 @@ function renderLeftRail(){ const conflictEvents = D.acled?.totalEvents || 0; const conflictFatal = D.acled?.totalFatalities || 0; const layers=[ - {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.')}`}, - {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')}, - {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')}`}, - {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')}, - {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:'air',name:t('layers.airActivity','Air Activity'),count:totalAir,dot:'air',sub:`${D.air.length} ${t('layers.theaters','theaters')}`}, + {key:'thermal',name:t('layers.thermalSpikes','Thermal Spikes'),count:totalThermal.toLocaleString(),dot:'thermal',sub:`${totalNight.toLocaleString()} ${t('layers.nightDet','night det.')}`}, + {key:'sdr',name:t('layers.sdrCoverage','SDR Coverage'),count:D.sdr.total,dot:'sdr',sub:`${D.sdr.online} ${t('layers.online','online')}`}, + {key:'maritime',name:t('layers.maritimeWatch','Maritime Watch'),count:D.chokepoints.length,dot:'maritime',sub:t('layers.chokepoints','chokepoints')}, + {key:'nuke',name:t('layers.nuclearSites','Nuclear Sites'),count:D.nuke.length,dot:'nuke',sub:t('layers.monitors','monitors')}, + {key:'conflict',name:t('layers.conflictEvents','Conflict Events'),count:conflictEvents,dot:'thermal',sub:`${conflictFatal.toLocaleString()} ${t('layers.fatalities','fatalities')}`}, + {key:'health',name:t('layers.healthWatch','Health Watch'),count:D.who.length,dot:'health',sub:t('layers.whoAlerts','WHO alerts')}, + {key:'news',name:t('layers.worldNews','World News'),count:newsCount,dot:'news',sub:t('layers.rssGeolocated','RSS geolocated')}, + {key:'osint',name:t('layers.osintFeed','OSINT Feed'),count:D.tg.posts,dot:'incident',sub:`${D.tg.urgent.length} ${t('badges.urgent','urgent').toLowerCase()}`}, + {key:'space',name:t('layers.spaceActivity','Satellites'),count:D.space?.militarySats||0,dot:'space',sub:`${D.space?.totalNewObjects||0} ${t('space.newLast30d','new (30d)')}`} ]; const allNormal=D.nuke.every(s=>!s.anom); const nukeHtml=D.nuke.map(s=>`
${s.site}${s.n>0?(s.cpm?.toFixed(1)||'--')+' CPM':'No data'}
`).join(''); @@ -632,8 +693,8 @@ function renderLeftRail(){ document.getElementById('leftRail').innerHTML=`
-

${t('panels.sensorGrid','Sensor Grid')}

${t('badges.live','LIVE')}
- ${layers.map(l=>`
${l.name}
${l.sub}
${l.count}
`).join('')} +

${t('panels.sensorGrid','Sensor Grid')}

${t('badges.live','LIVE')}
+ ${layers.map(l=>`
${l.name}
${l.sub}
${layerModeLabel(l.key)}
${l.count}
`).join('')}

${t('panels.nuclearWatch','Nuclear Watch')}

${t('badges.radiation','RADIATION')}
@@ -651,7 +712,7 @@ function renderLeftRail(){
${t('metrics.natDebt','Nat. Debt')}$${(parseFloat(D.treasury.totalDebt)/1e12).toFixed(2)}T
-

${t('panels.spaceWatch','Space Watch')}

${t('badges.orbital','CELESTRAK')}
+

${t('panels.spaceWatch','Space Watch')}

${D.space ? `
New Objects (30d)${D.space.totalNewObjects||0}
Military Sats${D.space.militarySats||0}
@@ -953,7 +1014,7 @@ function plotMarkers(){ }); // === ISS + Space Stations (bright white, pulsing) === - (D.space?.stationPositions||[]).forEach(s=>{ + if(spaceDisplayMode === 'icons') (D.space?.stationPositions||[]).forEach(s=>{ points.push({ lat:s.lat, lng:s.lon, size:0.4, alt:0.04, color:'rgba(255,255,255,0.95)', type:'space', priority:1, @@ -973,12 +1034,15 @@ function plotMarkers(){ }); }); - // 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 visiblePoints = points.filter(p => shouldShowType(p.type)); + const visibleLabels = labels.filter(l => !l.type || shouldShowType(l.type)); + + // Set points on globe + globe.pointsData(visiblePoints); + globe.labelsData(visibleLabels); + + const conflictRings = shouldShowType('conflict') ? (D.acled?.deadliestEvents || []).filter(e => e.lat && e.lon).map(e => { const logFatal = Math.log2(Math.max(e.fatalities, 1)); return { lat: e.lat, lng: e.lon, @@ -988,12 +1052,12 @@ function plotMarkers(){ popHead: e.type || 'CONFLICT', popMeta: 'ACLED Conflict Data', popText: `${e.fatalities} fatalities
${e.location}, ${e.country}
Date: ${e.date}` }; - }); + }) : []; globe.ringsData(conflictRings); // === FLIGHT CORRIDORS (3D arcs) === const arcs = []; - if(flightsVisible){ + if(flightsVisible && shouldShowType('air')){ const airCoordsFlight = [ {region:'Middle East',lat:30,lon:44}, {region:'Taiwan Strait',lat:24,lon:120}, {region:'Ukraine Region',lat:49,lon:32}, {region:'Baltic Region',lat:57,lon:24}, @@ -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); // Zoom-aware marker sizing: scale markers and labels with camera altitude @@ -1055,11 +1130,11 @@ function plotMarkers(){ globe.arcDashAnimateTime(lowPerfMode ? 0 : 2000); // Priority-based point visibility: hide low-priority markers when zoomed out 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){ - globe.pointsData(points.filter(p => (p.priority||3) <= 2)); + globe.pointsData(visiblePoints.filter(p => (p.priority||3) <= 2)); } else { - globe.pointsData(points); + globe.pointsData(visiblePoints); } }; 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-label').style('font-size',Math.max(7,9/Math.sqrt(k))+'px') .style('display',k>=2.5?'block':'none'); - }); + }).on('end',()=>{ flatG.selectAll('.marker-label').style('display','block'); }); flatSvg.call(flatZoom); drawFlatMap(); } @@ -1197,7 +1272,8 @@ function drawFlatMap(){ function plotFlatMarkers(){ const mg=flatG.append('g').attr('class','markers'); 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 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)}); @@ -1215,12 +1291,12 @@ function plotFlatMarkers(){ }); } // 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)', ev=>showPopup(ev,'Thermal',`${t.region}
FRP: ${f.frp.toFixed(1)} MW`,'FIRMS'),3); })); // 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 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')}); @@ -1229,30 +1305,30 @@ function plotFlatMarkers(){ }); // 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'),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 - 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}
${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}
${z.region}`,'KiwiSDR'),3)})); // OSINT const osintGeo=[{lat:45,lon:41,idx:0},{lat:48,lon:37,idx:1},{lat:48.5,lon:37.5,idx:2},{lat:45,lon:40.2,idx:3},{lat:50.6,lon:36.6,idx:5},{lat:48.5,lon:35,idx:6}]; - 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 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 - (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 - (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 - (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 - (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); 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 - (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 - (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 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) @@ -1261,7 +1337,7 @@ function plotFlatMarkers(){ g.append('circle').attr('r',r*0.4).attr('fill','rgba(255,120,80,0.3)'); }); // 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 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'); diff --git a/docs/sources/README.md b/docs/sources/README.md new file mode 100644 index 0000000..008b1f5 --- /dev/null +++ b/docs/sources/README.md @@ -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) diff --git a/docs/sources/acled.md b/docs/sources/acled.md new file mode 100644 index 0000000..c6ba1fb --- /dev/null +++ b/docs/sources/acled.md @@ -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`. diff --git a/docs/sources/firms.md b/docs/sources/firms.md new file mode 100644 index 0000000..c315972 --- /dev/null +++ b/docs/sources/firms.md @@ -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`. diff --git a/docs/sources/maritime.md b/docs/sources/maritime.md new file mode 100644 index 0000000..e8eafcb --- /dev/null +++ b/docs/sources/maritime.md @@ -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. diff --git a/docs/sources/opensky.md b/docs/sources/opensky.md new file mode 100644 index 0000000..19ee63f --- /dev/null +++ b/docs/sources/opensky.md @@ -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. diff --git a/docs/sources/telegram.md b/docs/sources/telegram.md new file mode 100644 index 0000000..ccb050f --- /dev/null +++ b/docs/sources/telegram.md @@ -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`.