- New source: apis/sources/space.mjs (no API key required) - Tracks: recent launches, ISS, military sats, Starlink/OneWeb constellations - Wired into briefing.mjs (27 sources), inject.mjs synthesis, and dashboard - New Space Watch panel in left rail with military breakdown and signals - New Satellites layer in Sensor Grid
502 lines
21 KiB
JavaScript
502 lines
21 KiB
JavaScript
#!/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 };
|
|
|
|
// Space/CelesTrak satellite data
|
|
const spaceData = data.sources.Space || {};
|
|
const space = {
|
|
totalNewObjects: spaceData.totalNewObjects || 0,
|
|
militarySats: spaceData.militarySatellites || 0,
|
|
militaryByCountry: spaceData.militaryByCountry || {},
|
|
constellations: spaceData.constellations || {},
|
|
iss: spaceData.iss || null,
|
|
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 || [],
|
|
};
|
|
|
|
// 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, space, 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
|
|
// 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 ""' :
|
|
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();
|
|
}
|