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
import { readFileSync , writeFileSync } from 'fs' ;
import { dirname , join } from 'path' ;
import { fileURLToPath } from 'url' ;
import { exec } from 'child_process' ;
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 ] ,
} ;
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 ;
}
// === 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 ( ) ;
const pubDate = block . match ( /<pubDate>(.*?)<\/pubDate>/ ) ? . [ 1 ] || '' ;
if ( title && title !== source ) items . push ( { title , date : pubDate , source } ) ;
}
return items ;
} catch ( e ) {
console . log ( ` RSS fetch failed ( ${ source } ): ` , e . message ) ;
return [ ] ;
}
}
export async function fetchAllNews ( ) {
const feeds = [
[ 'http://feeds.bbci.co.uk/news/world/rss.xml' , 'BBC' ] ,
[ 'https://rss.nytimes.com/services/xml/rss/nyt/World.xml' , 'NYT' ] ,
[ 'https://feeds.aljazeera.com/xml/rss/all.xml' , 'Al Jazeera' ] ,
] ;
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 ) ;
const geo = geoTagText ( item . title ) ;
if ( geo ) {
geoNews . push ( {
title : item . title . substring ( 0 , 100 ) ,
source : item . source ,
date : item . date ,
lat : geo . lat + ( Math . random ( ) - 0.5 ) * 2 ,
lon : geo . lon + ( Math . random ( ) - 0.5 ) * 2 ,
region : geo . region
} ) ;
}
}
geoNews . sort ( ( a , b ) => new Date ( b . date || 0 ) - new Date ( a . date || 0 ) ) ;
return geoNews . slice ( 0 , 30 ) ;
}
// === 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 ) {
const air = ( data . sources . OpenSky ? . 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 )
} ) ) ;
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 )
} ) ) ;
const noaa = { totalAlerts : data . sources . NOAA ? . totalSevereAlerts || 0 } ;
// 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
} ) )
} ;
// GDELT news articles
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 ,
topTitles : ( gdeltData . allArticles || [ ] ) . slice ( 0 , 5 ) . map ( a => a . title ? . substring ( 0 , 80 ) )
} ;
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 ,
} ;
// 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 ,
sdr : { total : sdrNet . totalReceivers || 0 , online : sdrNet . online || 0 , zones : sdrZones } ,
tg : { posts : tgData . totalPosts || 0 , urgent : tgUrgent , topPosts : tgTop } ,
who , fred , energy , bls , treasury , gscpi , defense , noaa , acled , gdelt , health , news ,
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' ,
timestamp : n . date , region : n . region , urgent : false
} ) ;
}
// GDELT top articles
for ( const title of ( gdeltData . allArticles || [ ] ) . slice ( 0 , 10 ) . map ( a => a . title ) ) {
if ( title ) {
const geo = geoTagText ( title ) ;
feed . push ( {
headline : title . substring ( 0 , 100 ) , source : 'GDELT' , type : 'gdelt' ,
timestamp : new Date ( ) . toISOString ( ) , region : geo ? . region || 'Global' , urgent : false
} ) ;
}
}
// 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
} ) ;
}
// Sort by timestamp descending, limit to 50
feed . sort ( ( a , b ) => new Date ( b . timestamp || 0 ) - new Date ( a . timestamp || 0 ) ) ;
return feed . slice ( 0 , 50 ) ;
}
// === CLI Mode: inject into HTML file ===
async function cliInject ( ) {
const data = JSON . parse ( readFileSync ( join ( ROOT , 'runs/latest.json' ) , 'utf8' ) ) ;
console . log ( 'Fetching RSS news feeds...' ) ;
const V2 = await synthesize ( data ) ;
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 ) ;
const htmlPath = join ( ROOT , 'dashboard/public/jarvis.html' ) ;
let html = readFileSync ( htmlPath , 'utf8' ) ;
html = html . replace ( /^(let|const) D = .*;\s*$/m , 'let D = ' + json + ';' ) ;
writeFileSync ( htmlPath , html ) ;
console . log ( 'Data injected into jarvis.html!' ) ;
// 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
const isMain = process . argv [ 1 ] && fileURLToPath ( import . meta . url ) . includes ( process . argv [ 1 ] . replace ( /\\/g , '/' ) ) ;
if ( isMain ) {
cliInject ( ) ;
}