2026-03-12 23:45:46 -07:00
#!/usr/bin/env node
// Crucix Dashboard Data Synthesizer
// Reads runs/latest.json, fetches RSS news, generates signal-based ideas,
// and injects everything into dashboard/public/jarvis.html
//
// Exports synthesize(), generateIdeas(), fetchAllNews() for use by server.mjs
2026-03-19 11:57:22 -07:00
import { existsSync , readFileSync , readdirSync , writeFileSync } from 'fs' ;
2026-03-12 23:45:46 -07:00
import { dirname , join } from 'path' ;
import { fileURLToPath } from 'url' ;
import { exec } from 'child_process' ;
2026-03-17 13:47:55 -07:00
import config from '../crucix.config.mjs' ;
import { createLLMProvider } from '../lib/llm/index.mjs' ;
import { generateLLMIdeas } from '../lib/llm/ideas.mjs' ;
2026-03-12 23:45:46 -07:00
const _ _dirname = dirname ( fileURLToPath ( import . meta . url ) ) ;
const ROOT = join ( _ _dirname , '..' ) ;
// === Helpers ===
const cyrillic = /[\u0400-\u04FF]/ ;
function isEnglish ( text ) {
if ( ! text ) return false ;
return ! cyrillic . test ( text . substring ( 0 , 80 ) ) ;
}
// === Geo-tagging keyword map ===
const geoKeywords = {
'Ukraine' : [ 49 , 32 ] , 'Russia' : [ 56 , 38 ] , 'Moscow' : [ 55.7 , 37.6 ] , 'Kyiv' : [ 50.4 , 30.5 ] ,
'China' : [ 35 , 105 ] , 'Beijing' : [ 39.9 , 116.4 ] , 'Iran' : [ 32 , 53 ] , 'Tehran' : [ 35.7 , 51.4 ] ,
'Israel' : [ 31.5 , 35 ] , 'Gaza' : [ 31.4 , 34.4 ] , 'Palestine' : [ 31.9 , 35.2 ] ,
'Syria' : [ 35 , 38 ] , 'Iraq' : [ 33 , 44 ] , 'Saudi' : [ 24 , 45 ] , 'Yemen' : [ 15 , 48 ] , 'Lebanon' : [ 34 , 36 ] ,
'India' : [ 20 , 78 ] , 'Japan' : [ 36 , 138 ] , 'Korea' : [ 37 , 127 ] , 'Pyongyang' : [ 39 , 125.7 ] ,
'Taiwan' : [ 23.5 , 121 ] , 'Philippines' : [ 13 , 122 ] , 'Myanmar' : [ 20 , 96 ] ,
'Canada' : [ 56 , - 96 ] , 'Mexico' : [ 23 , - 102 ] , 'Brazil' : [ - 14 , - 51 ] , 'Argentina' : [ - 38 , - 63 ] ,
'Colombia' : [ 4 , - 74 ] , 'Venezuela' : [ 7 , - 66 ] , 'Cuba' : [ 22 , - 80 ] , 'Chile' : [ - 35 , - 71 ] ,
'Germany' : [ 51 , 10 ] , 'France' : [ 46 , 2 ] , 'UK' : [ 54 , - 2 ] , 'Britain' : [ 54 , - 2 ] , 'London' : [ 51.5 , - 0.1 ] ,
'Spain' : [ 40 , - 4 ] , 'Italy' : [ 42 , 12 ] , 'Poland' : [ 52 , 20 ] , 'NATO' : [ 50 , 4 ] , 'EU' : [ 50 , 4 ] ,
'Turkey' : [ 39 , 35 ] , 'Greece' : [ 39 , 22 ] , 'Romania' : [ 46 , 25 ] , 'Finland' : [ 64 , 26 ] , 'Sweden' : [ 62 , 15 ] ,
'Africa' : [ 0 , 20 ] , 'Nigeria' : [ 10 , 8 ] , 'South Africa' : [ - 30 , 25 ] , 'Kenya' : [ - 1 , 38 ] ,
'Egypt' : [ 27 , 30 ] , 'Libya' : [ 27 , 17 ] , 'Sudan' : [ 13 , 30 ] , 'Ethiopia' : [ 9 , 38 ] ,
'Somalia' : [ 5 , 46 ] , 'Congo' : [ - 4 , 22 ] , 'Uganda' : [ 1 , 32 ] , 'Morocco' : [ 32 , - 6 ] ,
'Pakistan' : [ 30 , 70 ] , 'Afghanistan' : [ 33 , 65 ] , 'Bangladesh' : [ 24 , 90 ] ,
'Australia' : [ - 25 , 134 ] , 'Indonesia' : [ - 2 , 118 ] , 'Thailand' : [ 15 , 100 ] ,
'US' : [ 39 , - 98 ] , 'America' : [ 39 , - 98 ] , 'Washington' : [ 38.9 , - 77 ] , 'Pentagon' : [ 38.9 , - 77 ] ,
'Trump' : [ 38.9 , - 77 ] , 'White House' : [ 38.9 , - 77 ] ,
'Wall Street' : [ 40.7 , - 74 ] , 'New York' : [ 40.7 , - 74 ] , 'California' : [ 37 , - 120 ] ,
'Nepal' : [ 28 , 84 ] , 'Cambodia' : [ 12.5 , 105 ] , 'Malawi' : [ - 13.5 , 34 ] , 'Burundi' : [ - 3.4 , 29.9 ] ,
'Oman' : [ 21 , 57 ] , 'Netherlands' : [ 52.1 , 5.3 ] , 'Gabon' : [ - 0.8 , 11.6 ] ,
'Peru' : [ - 10 , - 76 ] , 'Ecuador' : [ - 2 , - 78 ] , 'Bolivia' : [ - 17 , - 65 ] ,
'Singapore' : [ 1.35 , 103.8 ] , 'Malaysia' : [ 4.2 , 101.9 ] , 'Vietnam' : [ 16 , 108 ] ,
'Algeria' : [ 28 , 3 ] , 'Tunisia' : [ 34 , 9 ] , 'Zimbabwe' : [ - 20 , 30 ] , 'Mozambique' : [ - 18 , 35 ] ,
2026-03-15 09:17:05 -07:00
// Americas expansion
'Texas' : [ 31 , - 100 ] , 'Florida' : [ 28 , - 82 ] , 'Chicago' : [ 41.9 , - 87.6 ] , 'Los Angeles' : [ 34 , - 118 ] ,
'San Francisco' : [ 37.8 , - 122.4 ] , 'Seattle' : [ 47.6 , - 122.3 ] , 'Miami' : [ 25.8 , - 80.2 ] ,
'Toronto' : [ 43.7 , - 79.4 ] , 'Ottawa' : [ 45.4 , - 75.7 ] , 'Vancouver' : [ 49.3 , - 123.1 ] ,
'São Paulo' : [ - 23.5 , - 46.6 ] , 'Rio' : [ - 22.9 , - 43.2 ] , 'Buenos Aires' : [ - 34.6 , - 58.4 ] ,
'Bogotá' : [ 4.7 , - 74.1 ] , 'Lima' : [ - 12 , - 77 ] , 'Santiago' : [ - 33.4 , - 70.7 ] ,
'Caracas' : [ 10.5 , - 66.9 ] , 'Havana' : [ 23.1 , - 82.4 ] , 'Panama' : [ 9 , - 79.5 ] ,
'Guatemala' : [ 14.6 , - 90.5 ] , 'Honduras' : [ 14.1 , - 87.2 ] , 'El Salvador' : [ 13.7 , - 89.2 ] ,
'Costa Rica' : [ 10 , - 84 ] , 'Jamaica' : [ 18.1 , - 77.3 ] , 'Haiti' : [ 19 , - 72 ] ,
'Dominican' : [ 18.5 , - 70 ] , 'Puerto Rico' : [ 18.2 , - 66.5 ] ,
// More Asia-Pacific
'Sri Lanka' : [ 7 , 80 ] , 'Hong Kong' : [ 22.3 , 114.2 ] , 'Taipei' : [ 25 , 121.5 ] ,
'Seoul' : [ 37.6 , 127 ] , 'Osaka' : [ 34.7 , 135.5 ] , 'Mumbai' : [ 19.1 , 72.9 ] ,
'Delhi' : [ 28.6 , 77.2 ] , 'Shanghai' : [ 31.2 , 121.5 ] , 'Shenzhen' : [ 22.5 , 114.1 ] ,
'Auckland' : [ - 36.8 , 174.8 ] , 'Papua New Guinea' : [ - 6.3 , 147 ] ,
// More Europe
'Berlin' : [ 52.5 , 13.4 ] , 'Paris' : [ 48.9 , 2.3 ] , 'Madrid' : [ 40.4 , - 3.7 ] ,
'Rome' : [ 41.9 , 12.5 ] , 'Warsaw' : [ 52.2 , 21 ] , 'Prague' : [ 50.1 , 14.4 ] ,
'Vienna' : [ 48.2 , 16.4 ] , 'Budapest' : [ 47.5 , 19.1 ] , 'Bucharest' : [ 44.4 , 26.1 ] ,
'Kyiv' : [ 50.4 , 30.5 ] , 'Oslo' : [ 59.9 , 10.7 ] , 'Copenhagen' : [ 55.7 , 12.6 ] ,
'Brussels' : [ 50.8 , 4.4 ] , 'Zurich' : [ 47.4 , 8.5 ] , 'Dublin' : [ 53.3 , - 6.3 ] ,
'Lisbon' : [ 38.7 , - 9.1 ] , 'Athens' : [ 37.9 , 23.7 ] , 'Minsk' : [ 53.9 , 27.6 ] ,
// More Africa
'Nairobi' : [ - 1.3 , 36.8 ] , 'Lagos' : [ 6.5 , 3.4 ] , 'Accra' : [ 5.6 , - 0.2 ] ,
'Addis Ababa' : [ 9 , 38.7 ] , 'Cape Town' : [ - 33.9 , 18.4 ] , 'Johannesburg' : [ - 26.2 , 28 ] ,
'Kinshasa' : [ - 4.3 , 15.3 ] , 'Khartoum' : [ 15.6 , 32.5 ] , 'Mogadishu' : [ 2.1 , 45.3 ] ,
'Dakar' : [ 14.7 , - 17.5 ] , 'Abuja' : [ 9.1 , 7.5 ] ,
// Tech/Economy keywords with US locations
'Fed' : [ 38.9 , - 77 ] , 'Congress' : [ 38.9 , - 77 ] , 'Senate' : [ 38.9 , - 77 ] ,
'Silicon Valley' : [ 37.4 , - 122 ] , 'NASA' : [ 28.6 , - 80.6 ] , 'Pentagon' : [ 38.9 , - 77 ] ,
'IMF' : [ 38.9 , - 77 ] , 'World Bank' : [ 38.9 , - 77 ] , 'UN' : [ 40.7 , - 74 ] ,
2026-03-12 23:45:46 -07:00
} ;
function geoTagText ( text ) {
if ( ! text ) return null ;
for ( const [ keyword , [ lat , lon ] ] of Object . entries ( geoKeywords ) ) {
if ( text . includes ( keyword ) ) {
return { lat , lon , region : keyword } ;
}
}
return null ;
}
2026-03-15 17:27:07 -07:00
function sanitizeExternalUrl ( raw ) {
if ( ! raw ) return undefined ;
try {
const url = new URL ( raw ) ;
return url . protocol === 'http:' || url . protocol === 'https:' ? url . toString ( ) : undefined ;
} catch {
return undefined ;
}
}
2026-03-19 11:57:22 -07:00
function sumAirHotspots ( hotspots = [ ] ) {
return hotspots . reduce ( ( sum , hotspot ) => sum + ( hotspot . totalAircraft || 0 ) , 0 ) ;
}
function summarizeAirHotspots ( hotspots = [ ] ) {
return hotspots . map ( h => ( {
region : h . region ,
total : h . totalAircraft || 0 ,
noCallsign : h . noCallsign || 0 ,
highAlt : h . highAltitude || 0 ,
top : Object . entries ( h . byCountry || { } ) . sort ( ( a , b ) => b [ 1 ] - a [ 1 ] ) . slice ( 0 , 5 ) ,
} ) ) ;
}
function loadOpenSkyFallback ( currentTimestamp ) {
const runsDir = join ( ROOT , 'runs' ) ;
if ( ! existsSync ( runsDir ) ) return null ;
const currentMs = currentTimestamp ? new Date ( currentTimestamp ) . getTime ( ) : NaN ;
const files = readdirSync ( runsDir )
. filter ( name => / ^ briefing _ . * \ . json$ / . test ( name ) )
. sort ( )
. reverse ( ) ;
for ( const file of files ) {
const filePath = join ( runsDir , file ) ;
try {
const prior = JSON . parse ( readFileSync ( filePath , 'utf8' ) ) ;
const priorTimestamp = prior . sources ? . OpenSky ? . timestamp || prior . crucix ? . timestamp || null ;
if ( priorTimestamp && Number . isFinite ( currentMs ) && new Date ( priorTimestamp ) . getTime ( ) >= currentMs ) continue ;
const hotspots = prior . sources ? . OpenSky ? . hotspots || [ ] ;
if ( sumAirHotspots ( hotspots ) > 0 ) {
return { file , timestamp : priorTimestamp , hotspots } ;
}
} catch {
// Ignore unreadable historical runs and continue searching backward.
}
}
return null ;
}
2026-03-12 23:45:46 -07:00
// === RSS Fetching ===
async function fetchRSS ( url , source ) {
try {
const res = await fetch ( url , { signal : AbortSignal . timeout ( 8000 ) } ) ;
const xml = await res . text ( ) ;
const items = [ ] ;
const itemRegex = /<item>([\s\S]*?)<\/item>/g ;
let match ;
while ( ( match = itemRegex . exec ( xml ) ) !== null ) {
const block = match [ 1 ] ;
const title = ( block . match ( /<title>(?:<!\[CDATA\[)?(.*?)(?:\]\]>)?<\/title>/ ) ? . [ 1 ] || '' ) . trim ( ) ;
2026-03-15 17:27:07 -07:00
const link = sanitizeExternalUrl ( ( block . match ( /<link>(?:<!\[CDATA\[)?(.*?)(?:\]\]>)?<\/link>/ ) ? . [ 1 ] || '' ) . trim ( ) ) ;
2026-03-12 23:45:46 -07:00
const pubDate = block . match ( /<pubDate>(.*?)<\/pubDate>/ ) ? . [ 1 ] || '' ;
2026-03-15 19:49:18 +01:00
if ( title && title !== source ) items . push ( { title , date : pubDate , source , url : link || undefined } ) ;
2026-03-12 23:45:46 -07:00
}
return items ;
} catch ( e ) {
console . log ( ` RSS fetch failed ( ${ source } ): ` , e . message ) ;
return [ ] ;
}
}
2026-03-20 06:32:42 -07:00
const RSS _SOURCE _FALLBACKS = {
'SBS Australia' : { lat : - 35.2809 , lon : 149.13 , region : 'Australia' } ,
'Indian Express' : { lat : 28.6139 , lon : 77.209 , region : 'India' } ,
'The Hindu' : { lat : 13.0827 , lon : 80.2707 , region : 'India' } ,
'MercoPress' : { lat : - 34.9011 , lon : - 56.1645 , region : 'South America' }
} ;
const REGIONAL _NEWS _SOURCES = [ 'MercoPress' , 'Indian Express' , 'The Hindu' , 'SBS Australia' ] ;
2026-03-12 23:45:46 -07:00
export async function fetchAllNews ( ) {
const feeds = [
feat: add news sources, 30-day filter, and fix ticker performance
- Fix Al Jazeera RSS URL (feeds.aljazeera.com is dead, moved to www.aljazeera.com)
- Add 8 new RSS sources: DW, France 24, Euronews, DW Africa, RFI, Africa News,
NYT Africa, NPR — covering Germany, Europe, Africa, Cameroon region, and USA
- Filter WHO outbreak news and ticker feed to last 30 days (drops stale alerts
like 733-day-old WHO DONs)
- Fix ticker causing Chrome to crash: add will-change:transform and
contain:layout style for GPU compositing, reduce DOM nodes from 80 to 40
- Add badge styles for new source categories (DW, EU, Africa)
2026-03-16 16:21:17 +01:00
// Global
2026-03-12 23:45:46 -07:00
[ 'http://feeds.bbci.co.uk/news/world/rss.xml' , 'BBC' ] ,
[ 'https://rss.nytimes.com/services/xml/rss/nyt/World.xml' , 'NYT' ] ,
2026-03-17 06:11:16 -04:00
[ 'https://www.aljazeera.com/xml/rss/all.xml' , 'Al Jazeera' ] ,
feat: add news sources, 30-day filter, and fix ticker performance
- Fix Al Jazeera RSS URL (feeds.aljazeera.com is dead, moved to www.aljazeera.com)
- Add 8 new RSS sources: DW, France 24, Euronews, DW Africa, RFI, Africa News,
NYT Africa, NPR — covering Germany, Europe, Africa, Cameroon region, and USA
- Filter WHO outbreak news and ticker feed to last 30 days (drops stale alerts
like 733-day-old WHO DONs)
- Fix ticker causing Chrome to crash: add will-change:transform and
contain:layout style for GPU compositing, reduce DOM nodes from 80 to 40
- Add badge styles for new source categories (DW, EU, Africa)
2026-03-16 16:21:17 +01:00
// USA
[ 'https://feeds.npr.org/1001/rss.xml' , 'NPR' ] ,
2026-03-15 09:17:05 -07:00
[ 'https://feeds.bbci.co.uk/news/technology/rss.xml' , 'BBC Tech' ] ,
[ 'http://feeds.bbci.co.uk/news/science_and_environment/rss.xml' , 'BBC Science' ] ,
feat: add news sources, 30-day filter, and fix ticker performance
- Fix Al Jazeera RSS URL (feeds.aljazeera.com is dead, moved to www.aljazeera.com)
- Add 8 new RSS sources: DW, France 24, Euronews, DW Africa, RFI, Africa News,
NYT Africa, NPR — covering Germany, Europe, Africa, Cameroon region, and USA
- Filter WHO outbreak news and ticker feed to last 30 days (drops stale alerts
like 733-day-old WHO DONs)
- Fix ticker causing Chrome to crash: add will-change:transform and
contain:layout style for GPU compositing, reduce DOM nodes from 80 to 40
- Add badge styles for new source categories (DW, EU, Africa)
2026-03-16 16:21:17 +01:00
[ 'https://rss.nytimes.com/services/xml/rss/nyt/Americas.xml' , 'NYT Americas' ] ,
// Europe
[ 'https://rss.dw.com/rdf/rss-en-all' , 'DW' ] ,
[ 'https://www.france24.com/en/rss' , 'France 24' ] ,
[ 'https://www.euronews.com/rss?format=mrss' , 'Euronews' ] ,
// Africa & Cameroon region
[ 'https://rss.dw.com/rdf/rss-en-africa' , 'DW Africa' ] ,
[ 'https://www.rfi.fr/en/rss' , 'RFI' ] ,
[ 'https://www.africanews.com/feed/rss' , 'Africa News' ] ,
[ 'https://rss.nytimes.com/services/xml/rss/nyt/Africa.xml' , 'NYT Africa' ] ,
// Asia-Pacific
[ 'https://rss.nytimes.com/services/xml/rss/nyt/AsiaPacific.xml' , 'NYT Asia' ] ,
2026-03-20 06:32:42 -07:00
[ 'https://www.sbs.com.au/news/topic/australia/feed' , 'SBS Australia' ] ,
// India
[ 'https://indianexpress.com/section/india/feed/' , 'Indian Express' ] ,
[ 'https://www.thehindu.com/news/national/feeder/default.rss' , 'The Hindu' ] ,
// South America
[ 'https://en.mercopress.com/rss/latin-america' , 'MercoPress' ] ,
2026-03-12 23:45:46 -07:00
] ;
const results = await Promise . allSettled (
feeds . map ( ( [ url , source ] ) => fetchRSS ( url , source ) )
) ;
const allNews = results
. filter ( r => r . status === 'fulfilled' )
. flatMap ( r => r . value ) ;
// De-duplicate and geo-tag
const seen = new Set ( ) ;
const geoNews = [ ] ;
for ( const item of allNews ) {
const key = item . title . substring ( 0 , 40 ) . toLowerCase ( ) ;
if ( seen . has ( key ) ) continue ;
seen . add ( key ) ;
2026-03-20 06:32:42 -07:00
const geo = geoTagText ( item . title ) || RSS _SOURCE _FALLBACKS [ item . source ] ;
2026-03-12 23:45:46 -07:00
if ( geo ) {
geoNews . push ( {
title : item . title . substring ( 0 , 100 ) ,
source : item . source ,
date : item . date ,
2026-03-15 19:49:18 +01:00
url : item . url ,
2026-03-12 23:45:46 -07:00
lat : geo . lat + ( Math . random ( ) - 0.5 ) * 2 ,
lon : geo . lon + ( Math . random ( ) - 0.5 ) * 2 ,
region : geo . region
} ) ;
}
}
feat: add news sources, 30-day filter, and fix ticker performance
- Fix Al Jazeera RSS URL (feeds.aljazeera.com is dead, moved to www.aljazeera.com)
- Add 8 new RSS sources: DW, France 24, Euronews, DW Africa, RFI, Africa News,
NYT Africa, NPR — covering Germany, Europe, Africa, Cameroon region, and USA
- Filter WHO outbreak news and ticker feed to last 30 days (drops stale alerts
like 733-day-old WHO DONs)
- Fix ticker causing Chrome to crash: add will-change:transform and
contain:layout style for GPU compositing, reduce DOM nodes from 80 to 40
- Add badge styles for new source categories (DW, EU, Africa)
2026-03-16 16:21:17 +01:00
const cutoff = new Date ( Date . now ( ) - 30 * 24 * 60 * 60 * 1000 ) ;
const filtered = geoNews . filter ( n => ! n . date || new Date ( n . date ) >= cutoff ) ;
filtered . sort ( ( a , b ) => new Date ( b . date || 0 ) - new Date ( a . date || 0 ) ) ;
2026-03-20 06:32:42 -07:00
const selected = [ ] ;
const selectedKeys = new Set ( ) ;
const keyFor = item => ` ${ item . source } | ${ item . title } | ${ item . date } ` ;
const pushUnique = item => {
const key = keyFor ( item ) ;
if ( selectedKeys . has ( key ) ) return ;
selected . push ( item ) ;
selectedKeys . add ( key ) ;
} ;
// Reserve a little space so newly-added regional feeds are not crowded out by larger globals.
for ( const source of REGIONAL _NEWS _SOURCES ) {
filtered . filter ( item => item . source === source ) . slice ( 0 , 2 ) . forEach ( pushUnique ) ;
}
filtered . forEach ( pushUnique ) ;
return selected . slice ( 0 , 50 ) ;
2026-03-12 23:45:46 -07:00
}
// === Leverageable Ideas from Signals ===
export function generateIdeas ( V2 ) {
const ideas = [ ] ;
const vix = V2 . fred . find ( f => f . id === 'VIXCLS' ) ;
const hy = V2 . fred . find ( f => f . id === 'BAMLH0A0HYM2' ) ;
const spread = V2 . fred . find ( f => f . id === 'T10Y2Y' ) ;
if ( V2 . tg . urgent . length > 3 && V2 . energy . wti > 68 ) {
ideas . push ( {
title : 'Conflict-Energy Nexus Active' ,
text : ` ${ V2 . tg . urgent . length } urgent conflict signals with WTI at $ ${ V2 . energy . wti } . Geopolitical risk premium may expand. Consider energy exposure. ` ,
type : 'long' , confidence : 'Medium' , horizon : 'swing'
} ) ;
}
if ( vix && vix . value > 20 ) {
ideas . push ( {
title : 'Elevated Volatility Regime' ,
text : ` VIX at ${ vix . value } — fear premium elevated. Portfolio hedges justified. Short-term equity upside is capped. ` ,
type : 'hedge' , confidence : vix . value > 25 ? 'High' : 'Medium' , horizon : 'tactical'
} ) ;
}
if ( vix && vix . value > 20 && hy && hy . value > 3 ) {
ideas . push ( {
title : 'Safe Haven Demand Rising' ,
text : ` VIX ${ vix . value } + HY spread ${ hy . value } % = risk-off building. Gold, treasuries, quality dividends may outperform. ` ,
type : 'hedge' , confidence : 'Medium' , horizon : 'tactical'
} ) ;
}
if ( V2 . energy . wtiRecent . length > 1 ) {
const latest = V2 . energy . wtiRecent [ 0 ] ;
const oldest = V2 . energy . wtiRecent [ V2 . energy . wtiRecent . length - 1 ] ;
const pct = ( ( latest - oldest ) / oldest * 100 ) . toFixed ( 1 ) ;
if ( Math . abs ( pct ) > 3 ) {
ideas . push ( {
title : pct > 0 ? 'Oil Momentum Building' : 'Oil Under Pressure' ,
text : ` WTI moved ${ pct > 0 ? '+' : '' } ${ pct } % recently to $ ${ V2 . energy . wti } /bbl. ${ pct > 0 ? 'Energy and commodity names benefit.' : 'Demand concerns may be emerging.' } ` ,
type : pct > 0 ? 'long' : 'watch' , confidence : 'Medium' , horizon : 'swing'
} ) ;
}
}
if ( spread ) {
ideas . push ( {
title : spread . value > 0 ? 'Yield Curve Normalizing' : 'Yield Curve Inverted' ,
text : ` 10Y-2Y spread at ${ spread . value . toFixed ( 2 ) } . ${ spread . value > 0 ? 'Recession signal fading — cyclical rotation possible.' : 'Inversion persists — defensive positioning warranted.' } ` ,
type : 'watch' , confidence : 'Medium' , horizon : 'strategic'
} ) ;
}
const debt = parseFloat ( V2 . treasury . totalDebt ) ;
if ( debt > 35e12 ) {
ideas . push ( {
title : 'Fiscal Trajectory Supports Hard Assets' ,
text : ` National debt at $ ${ ( debt / 1e12 ) . toFixed ( 1 ) } T. Long-term gold, bitcoin, and real asset appreciation thesis intact. ` ,
type : 'long' , confidence : 'High' , horizon : 'strategic'
} ) ;
}
const totalThermal = V2 . thermal . reduce ( ( s , t ) => s + t . det , 0 ) ;
if ( totalThermal > 30000 && V2 . tg . urgent . length > 2 ) {
ideas . push ( {
title : 'Satellite Confirms Conflict Intensity' ,
text : ` ${ totalThermal . toLocaleString ( ) } thermal detections + ${ V2 . tg . urgent . length } urgent OSINT flags. Defense sector procurement may accelerate. ` ,
type : 'watch' , confidence : 'Medium' , horizon : 'swing'
} ) ;
}
// Yield Curve + Labor Interaction
const unemployment = V2 . bls . find ( b => b . id === 'LNS14000000' || b . id === 'UNRATE' ) ;
const payrolls = V2 . bls . find ( b => b . id === 'CES0000000001' || b . id === 'PAYEMS' ) ;
if ( spread && unemployment && payrolls ) {
const weakLabor = ( unemployment . value > 4.3 ) || ( payrolls . momChange && payrolls . momChange < - 50 ) ;
if ( spread . value > 0.3 && weakLabor ) {
ideas . push ( {
title : 'Steepening Curve Meets Weak Labor' ,
text : ` 10Y-2Y at ${ spread . value . toFixed ( 2 ) } + UE ${ unemployment . value } %. Curve steepening with deteriorating employment = recession positioning warranted. ` ,
type : 'hedge' , confidence : 'High' , horizon : 'tactical'
} ) ;
}
}
// ACLED Conflict + Energy Momentum
const conflictEvents = V2 . acled ? . totalEvents || 0 ;
if ( conflictEvents > 50 && V2 . energy . wtiRecent . length > 1 ) {
const wtiMove = V2 . energy . wtiRecent [ 0 ] - V2 . energy . wtiRecent [ V2 . energy . wtiRecent . length - 1 ] ;
if ( wtiMove > 2 ) {
ideas . push ( {
title : 'Conflict Fueling Energy Momentum' ,
text : ` ${ conflictEvents } ACLED events this week + WTI up $ ${ wtiMove . toFixed ( 1 ) } . Conflict-energy transmission channel active. ` ,
type : 'long' , confidence : 'Medium' , horizon : 'swing'
} ) ;
}
}
// Defense + Conflict Intensity
const totalFatalities = V2 . acled ? . totalFatalities || 0 ;
const totalThermalAll = V2 . thermal . reduce ( ( s , t ) => s + t . det , 0 ) ;
if ( totalFatalities > 500 && totalThermalAll > 20000 ) {
ideas . push ( {
title : 'Defense Procurement Acceleration Signal' ,
text : ` ${ totalFatalities . toLocaleString ( ) } conflict fatalities + ${ totalThermalAll . toLocaleString ( ) } thermal detections. Defense contractors may see accelerated procurement. ` ,
type : 'long' , confidence : 'Medium' , horizon : 'swing'
} ) ;
}
// HY Spread + VIX Divergence
if ( hy && vix ) {
const hyWide = hy . value > 3.5 ;
const vixLow = vix . value < 18 ;
const hyTight = hy . value < 2.5 ;
const vixHigh = vix . value > 25 ;
if ( hyWide && vixLow ) {
ideas . push ( {
title : 'Credit Stress Ignored by Equity Vol' ,
text : ` HY spread ${ hy . value . toFixed ( 1 ) } % (wide) but VIX only ${ vix . value . toFixed ( 0 ) } (complacent). Equity may be underpricing credit deterioration. ` ,
type : 'watch' , confidence : 'Medium' , horizon : 'tactical'
} ) ;
} else if ( hyTight && vixHigh ) {
ideas . push ( {
title : 'Equity Fear Exceeds Credit Stress' ,
text : ` VIX at ${ vix . value . toFixed ( 0 ) } but HY spread only ${ hy . value . toFixed ( 1 ) } %. Equity vol may be overshooting — credit markets aren't confirming. ` ,
type : 'watch' , confidence : 'Medium' , horizon : 'tactical'
} ) ;
}
}
// Supply Chain + Inflation Pipeline
const ppi = V2 . bls . find ( b => b . id === 'WPUFD49104' || b . id === 'PCU--PCU--' ) ;
const cpi = V2 . bls . find ( b => b . id === 'CUUR0000SA0' || b . id === 'CPIAUCSL' ) ;
if ( ppi && cpi && V2 . gscpi ) {
const supplyPressure = V2 . gscpi . value > 0.5 ;
const ppiRising = ppi . momChangePct > 0.3 ;
if ( supplyPressure && ppiRising ) {
ideas . push ( {
title : 'Inflation Pipeline Building Pressure' ,
text : ` GSCPI at ${ V2 . gscpi . value . toFixed ( 2 ) } ( ${ V2 . gscpi . interpretation } ) + PPI momentum + ${ ppi . momChangePct ? . toFixed ( 1 ) } %. Input costs flowing through — CPI may follow. ` ,
type : 'long' , confidence : 'Medium' , horizon : 'strategic'
} ) ;
}
}
return ideas . slice ( 0 , 8 ) ;
}
// === Synthesize raw sweep data into dashboard format ===
export async function synthesize ( data ) {
2026-03-19 11:57:22 -07:00
const liveAirHotspots = data . sources . OpenSky ? . hotspots || [ ] ;
const airFallback = sumAirHotspots ( liveAirHotspots ) > 0
? null
: loadOpenSkyFallback ( data . sources . OpenSky ? . timestamp || data . crucix ? . timestamp ) ;
const effectiveAirHotspots = airFallback ? . hotspots || liveAirHotspots ;
const air = summarizeAirHotspots ( effectiveAirHotspots ) ;
2026-03-12 23:45:46 -07:00
const thermal = ( data . sources . FIRMS ? . hotspots || [ ] ) . map ( h => ( {
region : h . region , det : h . totalDetections || 0 , night : h . nightDetections || 0 ,
hc : h . highConfidence || 0 ,
fires : ( h . highIntensity || [ ] ) . slice ( 0 , 8 ) . map ( f => ( { lat : f . lat , lon : f . lon , frp : f . frp || 0 } ) )
} ) ) ;
const tSignals = data . sources . FIRMS ? . signals || [ ] ;
const chokepoints = Object . values ( data . sources . Maritime ? . chokepoints || { } ) . map ( c => ( {
label : c . label || c . name , note : c . note || '' , lat : c . lat || 0 , lon : c . lon || 0
} ) ) ;
const nuke = ( data . sources . Safecast ? . sites || [ ] ) . map ( s => ( {
site : s . site , anom : s . anomaly || false , cpm : s . avgCPM , n : s . recentReadings || 0
} ) ) ;
const nukeSignals = ( data . sources . Safecast ? . signals || [ ] ) . filter ( s => s ) ;
const sdrData = data . sources . KiwiSDR || { } ;
const sdrNet = sdrData . network || { } ;
const sdrConflict = sdrData . conflictZones || { } ;
const sdrZones = Object . values ( sdrConflict ) . map ( z => ( {
region : z . region , count : z . count || 0 ,
receivers : ( z . receivers || [ ] ) . slice ( 0 , 5 ) . map ( r => ( { name : r . name || '' , lat : r . lat || 0 , lon : r . lon || 0 } ) )
} ) ) ;
const tgData = data . sources . Telegram || { } ;
const tgUrgent = ( tgData . urgentPosts || [ ] ) . filter ( p => isEnglish ( p . text ) ) . map ( p => ( {
channel : p . channel , text : p . text ? . substring ( 0 , 200 ) , views : p . views , date : p . date , urgentFlags : p . urgentFlags || [ ]
} ) ) ;
const tgTop = ( tgData . topPosts || [ ] ) . filter ( p => isEnglish ( p . text ) ) . map ( p => ( {
channel : p . channel , text : p . text ? . substring ( 0 , 200 ) , views : p . views , date : p . date , urgentFlags : [ ]
} ) ) ;
const who = ( data . sources . WHO ? . diseaseOutbreakNews || [ ] ) . slice ( 0 , 10 ) . map ( w => ( {
title : w . title ? . substring ( 0 , 120 ) , date : w . date , summary : w . summary ? . substring ( 0 , 150 )
} ) ) ;
const fred = ( data . sources . FRED ? . indicators || [ ] ) . map ( f => ( {
id : f . id , label : f . label , value : f . value , date : f . date ,
recent : f . recent || [ ] ,
momChange : f . momChange , momChangePct : f . momChangePct
} ) ) ;
const energyData = data . sources . EIA || { } ;
const oilPrices = energyData . oilPrices || { } ;
const wtiRecent = ( oilPrices . wti ? . recent || [ ] ) . map ( d => d . value ) ;
const energy = {
wti : oilPrices . wti ? . value , brent : oilPrices . brent ? . value ,
natgas : energyData . gasPrice ? . value , crudeStocks : energyData . inventories ? . crudeStocks ? . value ,
wtiRecent , signals : energyData . signals || [ ]
} ;
const bls = data . sources . BLS ? . indicators || [ ] ;
const treasuryData = data . sources . Treasury || { } ;
const debtArr = treasuryData . debt || [ ] ;
const treasury = { totalDebt : debtArr [ 0 ] ? . totalDebt || '0' , signals : treasuryData . signals || [ ] } ;
const gscpi = data . sources . GSCPI ? . latest || null ;
const defense = ( data . sources . USAspending ? . recentDefenseContracts || [ ] ) . slice ( 0 , 5 ) . map ( c => ( {
recipient : c . recipient ? . substring ( 0 , 40 ) , amount : c . amount , desc : c . description ? . substring ( 0 , 80 )
} ) ) ;
Add zoom-aware symbology, fix globe popovers, expand geo coverage (#34)
* Add zoom-aware symbology, fix globe popovers, expand geo coverage
Map rendering improvements based on user feedback:
- Globe markers now scale with camera altitude (onZoom hook)
- Priority-based visibility culls low-priority markers at world view
- Globe popovers use getScreenCoords for accurate positioning
- Flat map labels hidden at low zoom, revealed progressively
- Default globe altitude lowered from 2.5 to 1.8 for better fill
- Americas region zoom tightened to CONUS focus
Geographic coverage expansion:
- 4 new OpenSky air theaters: Caribbean, Gulf of Guinea, Cape Route, Horn of Africa
- Flight corridors now span Americas and Africa
- NOAA alerts extract centroid lat/lon from GeoJSON geometry
- EPA RadNet stations geocoded with hardcoded coords for 10 US cities
- ISS + Tiangong positions estimated from TLE orbital elements
- GDELT geoEvents() now called in briefing for mapped event points
- New legend entries: Weather Alert, EPA RadNet, Space Station, GDELT Event
* Fix null-safe coordinate checks and remove injected data blob
- Use `!= null` instead of truthy checks for lat/lon in noaa.mjs
and inject.mjs so valid 0-coordinates (equator/prime meridian)
are not silently dropped
- Reset jarvis.html `let D` back to null placeholder so generated
runtime data is not part of the PR diff
* Remove re-injected data blob from jarvis.html
Reset let D back to null — previous commit was correct but
inject.mjs build verification re-injected the payload.
2026-03-17 19:34:08 -07:00
const noaa = {
totalAlerts : data . sources . NOAA ? . totalSevereAlerts || 0 ,
alerts : ( data . sources . NOAA ? . topAlerts || [ ] ) . filter ( a => a . lat != null && a . lon != null ) . slice ( 0 , 10 ) . map ( a => ( {
event : a . event , severity : a . severity , headline : a . headline ? . substring ( 0 , 120 ) ,
lat : a . lat , lon : a . lon
} ) )
} ;
// EPA RadNet — pass through geo-tagged readings
const epaData = data . sources . EPA || { } ;
const epaStations = [ ] ;
const seenEpa = new Set ( ) ;
for ( const r of ( epaData . readings || [ ] ) ) {
if ( r . lat == null || r . lon == null ) continue ;
const key = ` ${ r . lat } , ${ r . lon } ` ;
if ( seenEpa . has ( key ) ) continue ;
seenEpa . add ( key ) ;
epaStations . push ( { location : r . location , state : r . state , lat : r . lat , lon : r . lon , analyte : r . analyte , result : r . result , unit : r . unit } ) ;
}
const epa = { totalReadings : epaData . totalReadings || 0 , stations : epaStations . slice ( 0 , 10 ) } ;
2026-03-12 23:45:46 -07:00
2026-03-15 08:19:23 -07:00
// Space/CelesTrak satellite data
const spaceData = data . sources . Space || { } ;
Add zoom-aware symbology, fix globe popovers, expand geo coverage (#34)
* Add zoom-aware symbology, fix globe popovers, expand geo coverage
Map rendering improvements based on user feedback:
- Globe markers now scale with camera altitude (onZoom hook)
- Priority-based visibility culls low-priority markers at world view
- Globe popovers use getScreenCoords for accurate positioning
- Flat map labels hidden at low zoom, revealed progressively
- Default globe altitude lowered from 2.5 to 1.8 for better fill
- Americas region zoom tightened to CONUS focus
Geographic coverage expansion:
- 4 new OpenSky air theaters: Caribbean, Gulf of Guinea, Cape Route, Horn of Africa
- Flight corridors now span Americas and Africa
- NOAA alerts extract centroid lat/lon from GeoJSON geometry
- EPA RadNet stations geocoded with hardcoded coords for 10 US cities
- ISS + Tiangong positions estimated from TLE orbital elements
- GDELT geoEvents() now called in briefing for mapped event points
- New legend entries: Weather Alert, EPA RadNet, Space Station, GDELT Event
* Fix null-safe coordinate checks and remove injected data blob
- Use `!= null` instead of truthy checks for lat/lon in noaa.mjs
and inject.mjs so valid 0-coordinates (equator/prime meridian)
are not silently dropped
- Reset jarvis.html `let D` back to null placeholder so generated
runtime data is not part of the PR diff
* Remove re-injected data blob from jarvis.html
Reset let D back to null — previous commit was correct but
inject.mjs build verification re-injected the payload.
2026-03-17 19:34:08 -07:00
// Approximate subsatellite position from TLE orbital elements
function estimateSatPosition ( sat ) {
if ( ! sat ? . inclination || ! sat ? . epoch ) return null ;
const epoch = new Date ( sat . epoch ) ;
const now = new Date ( ) ;
const elapsed = ( now - epoch ) / 1000 ;
const period = ( sat . period || 92.7 ) * 60 ; // minutes to seconds
const orbits = elapsed / period ;
const frac = orbits % 1 ;
const lat = sat . inclination * Math . sin ( frac * 2 * Math . PI ) ;
const lonShift = ( elapsed / 86400 ) * 360 ;
const orbitLon = frac * 360 ;
const lon = ( ( orbitLon - lonShift ) % 360 + 540 ) % 360 - 180 ;
return { lat : + lat . toFixed ( 2 ) , lon : + lon . toFixed ( 2 ) , name : sat . name } ;
}
const issPos = estimateSatPosition ( spaceData . iss ) ;
const spaceStations = ( spaceData . spaceStations || [ ] ) . map ( s => estimateSatPosition ( s ) ) . filter ( Boolean ) ;
2026-03-15 08:19:23 -07:00
const space = {
totalNewObjects : spaceData . totalNewObjects || 0 ,
militarySats : spaceData . militarySatellites || 0 ,
militaryByCountry : spaceData . militaryByCountry || { } ,
constellations : spaceData . constellations || { } ,
iss : spaceData . iss || null ,
Add zoom-aware symbology, fix globe popovers, expand geo coverage (#34)
* Add zoom-aware symbology, fix globe popovers, expand geo coverage
Map rendering improvements based on user feedback:
- Globe markers now scale with camera altitude (onZoom hook)
- Priority-based visibility culls low-priority markers at world view
- Globe popovers use getScreenCoords for accurate positioning
- Flat map labels hidden at low zoom, revealed progressively
- Default globe altitude lowered from 2.5 to 1.8 for better fill
- Americas region zoom tightened to CONUS focus
Geographic coverage expansion:
- 4 new OpenSky air theaters: Caribbean, Gulf of Guinea, Cape Route, Horn of Africa
- Flight corridors now span Americas and Africa
- NOAA alerts extract centroid lat/lon from GeoJSON geometry
- EPA RadNet stations geocoded with hardcoded coords for 10 US cities
- ISS + Tiangong positions estimated from TLE orbital elements
- GDELT geoEvents() now called in briefing for mapped event points
- New legend entries: Weather Alert, EPA RadNet, Space Station, GDELT Event
* Fix null-safe coordinate checks and remove injected data blob
- Use `!= null` instead of truthy checks for lat/lon in noaa.mjs
and inject.mjs so valid 0-coordinates (equator/prime meridian)
are not silently dropped
- Reset jarvis.html `let D` back to null placeholder so generated
runtime data is not part of the PR diff
* Remove re-injected data blob from jarvis.html
Reset let D back to null — previous commit was correct but
inject.mjs build verification re-injected the payload.
2026-03-17 19:34:08 -07:00
issPosition : issPos ,
stationPositions : spaceStations . slice ( 0 , 5 ) ,
2026-03-15 08:19:23 -07:00
recentLaunches : ( spaceData . recentLaunches || [ ] ) . slice ( 0 , 10 ) . map ( l => ( {
name : l . name , country : l . country , epoch : l . epoch ,
apogee : l . apogee , perigee : l . perigee , type : l . objectType
} ) ) ,
launchByCountry : spaceData . launchByCountry || { } ,
signals : spaceData . signals || [ ] ,
} ;
2026-03-12 23:45:46 -07:00
// ACLED conflict events
const acledData = data . sources . ACLED || { } ;
const acled = acledData . error ? { totalEvents : 0 , totalFatalities : 0 , byRegion : { } , byType : { } , deadliestEvents : [ ] } : {
totalEvents : acledData . totalEvents || 0 ,
totalFatalities : acledData . totalFatalities || 0 ,
byRegion : acledData . byRegion || { } ,
byType : acledData . byType || { } ,
deadliestEvents : ( acledData . deadliestEvents || [ ] ) . slice ( 0 , 15 ) . map ( e => ( {
date : e . date , type : e . type , country : e . country , location : e . location ,
fatalities : e . fatalities || 0 , lat : e . lat || null , lon : e . lon || null
} ) )
} ;
Add zoom-aware symbology, fix globe popovers, expand geo coverage (#34)
* Add zoom-aware symbology, fix globe popovers, expand geo coverage
Map rendering improvements based on user feedback:
- Globe markers now scale with camera altitude (onZoom hook)
- Priority-based visibility culls low-priority markers at world view
- Globe popovers use getScreenCoords for accurate positioning
- Flat map labels hidden at low zoom, revealed progressively
- Default globe altitude lowered from 2.5 to 1.8 for better fill
- Americas region zoom tightened to CONUS focus
Geographic coverage expansion:
- 4 new OpenSky air theaters: Caribbean, Gulf of Guinea, Cape Route, Horn of Africa
- Flight corridors now span Americas and Africa
- NOAA alerts extract centroid lat/lon from GeoJSON geometry
- EPA RadNet stations geocoded with hardcoded coords for 10 US cities
- ISS + Tiangong positions estimated from TLE orbital elements
- GDELT geoEvents() now called in briefing for mapped event points
- New legend entries: Weather Alert, EPA RadNet, Space Station, GDELT Event
* Fix null-safe coordinate checks and remove injected data blob
- Use `!= null` instead of truthy checks for lat/lon in noaa.mjs
and inject.mjs so valid 0-coordinates (equator/prime meridian)
are not silently dropped
- Reset jarvis.html `let D` back to null placeholder so generated
runtime data is not part of the PR diff
* Remove re-injected data blob from jarvis.html
Reset let D back to null — previous commit was correct but
inject.mjs build verification re-injected the payload.
2026-03-17 19:34:08 -07:00
// GDELT news articles + geo events
2026-03-12 23:45:46 -07:00
const gdeltData = data . sources . GDELT || { } ;
const gdelt = {
totalArticles : gdeltData . totalArticles || 0 ,
conflicts : ( gdeltData . conflicts || [ ] ) . length ,
economy : ( gdeltData . economy || [ ] ) . length ,
health : ( gdeltData . health || [ ] ) . length ,
crisis : ( gdeltData . crisis || [ ] ) . length ,
Add zoom-aware symbology, fix globe popovers, expand geo coverage (#34)
* Add zoom-aware symbology, fix globe popovers, expand geo coverage
Map rendering improvements based on user feedback:
- Globe markers now scale with camera altitude (onZoom hook)
- Priority-based visibility culls low-priority markers at world view
- Globe popovers use getScreenCoords for accurate positioning
- Flat map labels hidden at low zoom, revealed progressively
- Default globe altitude lowered from 2.5 to 1.8 for better fill
- Americas region zoom tightened to CONUS focus
Geographic coverage expansion:
- 4 new OpenSky air theaters: Caribbean, Gulf of Guinea, Cape Route, Horn of Africa
- Flight corridors now span Americas and Africa
- NOAA alerts extract centroid lat/lon from GeoJSON geometry
- EPA RadNet stations geocoded with hardcoded coords for 10 US cities
- ISS + Tiangong positions estimated from TLE orbital elements
- GDELT geoEvents() now called in briefing for mapped event points
- New legend entries: Weather Alert, EPA RadNet, Space Station, GDELT Event
* Fix null-safe coordinate checks and remove injected data blob
- Use `!= null` instead of truthy checks for lat/lon in noaa.mjs
and inject.mjs so valid 0-coordinates (equator/prime meridian)
are not silently dropped
- Reset jarvis.html `let D` back to null placeholder so generated
runtime data is not part of the PR diff
* Remove re-injected data blob from jarvis.html
Reset let D back to null — previous commit was correct but
inject.mjs build verification re-injected the payload.
2026-03-17 19:34:08 -07:00
topTitles : ( gdeltData . allArticles || [ ] ) . slice ( 0 , 5 ) . map ( a => a . title ? . substring ( 0 , 80 ) ) ,
geoPoints : ( gdeltData . geoPoints || [ ] ) . slice ( 0 , 20 ) . map ( p => ( {
lat : p . lat , lon : p . lon , name : ( p . name || '' ) . substring ( 0 , 80 ) , count : p . count || 1
} ) )
2026-03-12 23:45:46 -07:00
} ;
const health = Object . entries ( data . sources ) . map ( ( [ name , src ] ) => ( {
n : name , err : Boolean ( src . error ) , stale : Boolean ( src . stale )
} ) ) ;
// === Yahoo Finance live market data ===
const yfData = data . sources . YFinance || { } ;
const yfQuotes = yfData . quotes || { } ;
const markets = {
indexes : ( yfData . indexes || [ ] ) . map ( q => ( {
symbol : q . symbol , name : q . name , price : q . price ,
change : q . change , changePct : q . changePct , history : q . history || [ ]
} ) ) ,
rates : ( yfData . rates || [ ] ) . map ( q => ( {
symbol : q . symbol , name : q . name , price : q . price ,
change : q . change , changePct : q . changePct
} ) ) ,
commodities : ( yfData . commodities || [ ] ) . map ( q => ( {
symbol : q . symbol , name : q . name , price : q . price ,
change : q . change , changePct : q . changePct , history : q . history || [ ]
} ) ) ,
crypto : ( yfData . crypto || [ ] ) . map ( q => ( {
symbol : q . symbol , name : q . name , price : q . price ,
change : q . change , changePct : q . changePct
} ) ) ,
vix : yfQuotes [ '^VIX' ] ? {
value : yfQuotes [ '^VIX' ] . price ,
change : yfQuotes [ '^VIX' ] . change ,
changePct : yfQuotes [ '^VIX' ] . changePct ,
} : null ,
timestamp : yfData . summary ? . timestamp || null ,
} ;
2026-03-28 22:53:23 -07:00
const yfGold = yfQuotes [ 'GC=F' ] ;
const yfSilver = yfQuotes [ 'SI=F' ] ;
const metals = {
gold : yfGold ? . price ,
goldChange : yfGold ? . change ,
goldChangePct : yfGold ? . changePct ,
goldRecent : yfGold ? . history ? . map ( h => h . close ) || [ ] ,
silver : yfSilver ? . price ,
silverChange : yfSilver ? . change ,
silverChangePct : yfSilver ? . changePct ,
silverRecent : yfSilver ? . history ? . map ( h => h . close ) || [ ] ,
} ;
2026-03-12 23:45:46 -07:00
// Override stale EIA prices with live Yahoo Finance data if available
const yfWti = yfQuotes [ 'CL=F' ] ;
const yfBrent = yfQuotes [ 'BZ=F' ] ;
const yfNatgas = yfQuotes [ 'NG=F' ] ;
if ( yfWti ? . price ) energy . wti = yfWti . price ;
if ( yfBrent ? . price ) energy . brent = yfBrent . price ;
if ( yfNatgas ? . price ) energy . natgas = yfNatgas . price ;
if ( yfWti ? . history ? . length ) energy . wtiRecent = yfWti . history . map ( h => h . close ) ;
// Fetch RSS
const news = await fetchAllNews ( ) ;
const V2 = {
meta : data . crucix , air , thermal , tSignals , chokepoints , nuke , nukeSignals ,
2026-03-19 11:57:22 -07:00
airMeta : {
fallback : Boolean ( airFallback ) ,
liveTotal : sumAirHotspots ( liveAirHotspots ) ,
timestamp : airFallback ? . timestamp || data . sources . OpenSky ? . timestamp || data . crucix ? . timestamp || null ,
source : airFallback ? 'OpenSky fallback' : 'OpenSky' ,
... ( airFallback ? { fallbackFile : airFallback . file } : { } ) ,
... ( data . sources . OpenSky ? . error ? { error : data . sources . OpenSky . error } : { } ) ,
} ,
2026-03-12 23:45:46 -07:00
sdr : { total : sdrNet . totalReceivers || 0 , online : sdrNet . online || 0 , zones : sdrZones } ,
tg : { posts : tgData . totalPosts || 0 , urgent : tgUrgent , topPosts : tgTop } ,
2026-03-28 22:53:23 -07:00
who , fred , energy , metals , bls , treasury , gscpi , defense , noaa , epa , acled , gdelt , space , health , news ,
2026-03-12 23:45:46 -07:00
markets , // Live Yahoo Finance market data
ideas : [ ] , ideasSource : 'disabled' ,
// newsFeed for ticker (merged RSS + GDELT + Telegram)
newsFeed : buildNewsFeed ( news , gdeltData , tgUrgent , tgTop ) ,
} ;
return V2 ;
}
// === Unified News Feed for Ticker ===
function buildNewsFeed ( rssNews , gdeltData , tgUrgent , tgTop ) {
const feed = [ ] ;
// RSS news
for ( const n of rssNews ) {
feed . push ( {
headline : n . title , source : n . source , type : 'rss' ,
2026-03-15 19:49:18 +01:00
timestamp : n . date , region : n . region , urgent : false , url : n . url
2026-03-12 23:45:46 -07:00
} ) ;
}
// GDELT top articles
2026-03-15 19:49:18 +01:00
for ( const a of ( gdeltData . allArticles || [ ] ) . slice ( 0 , 10 ) ) {
if ( a . title ) {
const geo = geoTagText ( a . title ) ;
2026-03-12 23:45:46 -07:00
feed . push ( {
2026-03-15 19:49:18 +01:00
headline : a . title . substring ( 0 , 100 ) , source : 'GDELT' , type : 'gdelt' ,
2026-03-15 17:27:07 -07:00
timestamp : new Date ( ) . toISOString ( ) , region : geo ? . region || 'Global' , urgent : false , url : sanitizeExternalUrl ( a . url )
2026-03-12 23:45:46 -07:00
} ) ;
}
}
// Telegram urgent
for ( const p of tgUrgent . slice ( 0 , 10 ) ) {
const text = ( p . text || '' ) . replace ( /[\u{1F1E0}-\u{1F1FF}]/gu , '' ) . trim ( ) ;
feed . push ( {
headline : text . substring ( 0 , 100 ) , source : p . channel ? . toUpperCase ( ) || 'TELEGRAM' ,
type : 'telegram' , timestamp : p . date , region : 'OSINT' , urgent : true
} ) ;
}
// Telegram top (non-urgent)
for ( const p of tgTop . slice ( 0 , 5 ) ) {
const text = ( p . text || '' ) . replace ( /[\u{1F1E0}-\u{1F1FF}]/gu , '' ) . trim ( ) ;
feed . push ( {
headline : text . substring ( 0 , 100 ) , source : p . channel ? . toUpperCase ( ) || 'TELEGRAM' ,
type : 'telegram' , timestamp : p . date , region : 'OSINT' , urgent : false
} ) ;
}
feat: add news sources, 30-day filter, and fix ticker performance
- Fix Al Jazeera RSS URL (feeds.aljazeera.com is dead, moved to www.aljazeera.com)
- Add 8 new RSS sources: DW, France 24, Euronews, DW Africa, RFI, Africa News,
NYT Africa, NPR — covering Germany, Europe, Africa, Cameroon region, and USA
- Filter WHO outbreak news and ticker feed to last 30 days (drops stale alerts
like 733-day-old WHO DONs)
- Fix ticker causing Chrome to crash: add will-change:transform and
contain:layout style for GPU compositing, reduce DOM nodes from 80 to 40
- Add badge styles for new source categories (DW, EU, Africa)
2026-03-16 16:21:17 +01:00
// Filter to last 30 days, sort by timestamp descending, limit to 50
const cutoff = new Date ( Date . now ( ) - 30 * 24 * 60 * 60 * 1000 ) ;
const recent = feed . filter ( item => ! item . timestamp || new Date ( item . timestamp ) >= cutoff ) ;
recent . sort ( ( a , b ) => new Date ( b . timestamp || 0 ) - new Date ( a . timestamp || 0 ) ) ;
2026-03-20 06:32:42 -07:00
const selected = [ ] ;
const selectedKeys = new Set ( ) ;
const keyFor = item => ` ${ item . type } | ${ item . source } | ${ item . headline } | ${ item . timestamp } ` ;
const pushUnique = item => {
const key = keyFor ( item ) ;
if ( selectedKeys . has ( key ) ) return ;
selected . push ( item ) ;
selectedKeys . add ( key ) ;
} ;
for ( const source of REGIONAL _NEWS _SOURCES ) {
recent . filter ( item => item . source === source ) . slice ( 0 , 2 ) . forEach ( pushUnique ) ;
}
recent . forEach ( pushUnique ) ;
return selected . slice ( 0 , 50 ) ;
2026-03-12 23:45:46 -07:00
}
// === CLI Mode: inject into HTML file ===
2026-03-17 13:47:55 -07:00
function getCliArg ( flag ) {
const idx = process . argv . indexOf ( flag ) ;
return idx >= 0 ? process . argv [ idx + 1 ] : null ;
}
2026-03-12 23:45:46 -07:00
async function cliInject ( ) {
const data = JSON . parse ( readFileSync ( join ( ROOT , 'runs/latest.json' ) , 'utf8' ) ) ;
2026-03-17 13:47:55 -07:00
const htmlOverride = getCliArg ( '--html' ) ;
const shouldOpen = ! process . argv . includes ( '--no-open' ) ;
2026-03-12 23:45:46 -07:00
console . log ( 'Fetching RSS news feeds...' ) ;
const V2 = await synthesize ( data ) ;
2026-03-17 13:47:55 -07:00
const llmProvider = createLLMProvider ( config . llm ) ;
if ( llmProvider ? . isConfigured ) {
try {
console . log ( ` [LLM] Generating ideas via ${ llmProvider . name } ... ` ) ;
const llmIdeas = await generateLLMIdeas ( llmProvider , V2 , null , [ ] ) ;
if ( llmIdeas ? . length ) {
V2 . ideas = llmIdeas ;
V2 . ideasSource = 'llm' ;
console . log ( ` [LLM] Generated ${ llmIdeas . length } ideas ` ) ;
} else {
V2 . ideas = [ ] ;
V2 . ideasSource = 'llm-failed' ;
console . log ( '[LLM] No ideas returned' ) ;
}
} catch ( err ) {
V2 . ideas = [ ] ;
V2 . ideasSource = 'llm-failed' ;
console . log ( '[LLM] Idea generation failed:' , err . message ) ;
}
} else {
V2 . ideas = [ ] ;
V2 . ideasSource = 'disabled' ;
}
2026-03-12 23:45:46 -07:00
console . log ( ` Generated ${ V2 . ideas . length } leverageable ideas ` ) ;
const json = JSON . stringify ( V2 ) ;
console . log ( '\n--- Synthesis ---' ) ;
console . log ( 'Size:' , json . length , 'bytes | Air:' , V2 . air . length , '| Thermal:' , V2 . thermal . length ,
'| News:' , V2 . news . length , '| Ideas:' , V2 . ideas . length , '| Sources:' , V2 . health . length ) ;
2026-03-17 13:47:55 -07:00
const htmlPath = htmlOverride || join ( ROOT , 'dashboard/public/jarvis.html' ) ;
2026-03-12 23:45:46 -07:00
let html = readFileSync ( htmlPath , 'utf8' ) ;
2026-03-17 13:47:55 -07:00
// Use a replacer function so JSON is inserted literally even if it contains `$`.
html = html . replace ( /^(let|const) D = .*;\s*$/m , ( ) => 'let D = ' + json + ';' ) ;
2026-03-12 23:45:46 -07:00
writeFileSync ( htmlPath , html ) ;
console . log ( 'Data injected into jarvis.html!' ) ;
2026-03-17 13:47:55 -07:00
if ( ! shouldOpen ) return ;
2026-03-12 23:45:46 -07:00
// Auto-open dashboard in default browser
2026-03-14 00:35:31 -07:00
// NOTE: On Windows, `start` in PowerShell is an alias for Start-Service, not cmd's start.
// We must use `cmd /c start ""` to ensure it works in both cmd.exe and PowerShell.
const openCmd = process . platform === 'win32' ? 'cmd /c start ""' :
2026-03-12 23:45:46 -07:00
process . platform === 'darwin' ? 'open' : 'xdg-open' ;
const dashUrl = htmlPath . replace ( /\\/g , '/' ) ;
exec ( ` ${ openCmd } " ${ dashUrl } " ` , ( err ) => {
if ( err ) console . log ( 'Could not auto-open browser:' , err . message ) ;
else console . log ( 'Dashboard opened in browser!' ) ;
} ) ;
}
// Run CLI if invoked directly
2026-03-17 13:47:55 -07:00
const isMain = process . argv [ 1 ]
&& fileURLToPath ( import . meta . url ) . replace ( /\\/g , '/' ) === process . argv [ 1 ] . replace ( /\\/g , '/' ) ;
2026-03-12 23:45:46 -07:00
if ( isMain ) {
2026-03-17 13:47:55 -07:00
await cliInject ( ) ;
2026-03-12 23:45:46 -07:00
}