feat: add Space/CelesTrak as 27th intelligence source

- 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
This commit is contained in:
calesthio
2026-03-15 08:19:23 -07:00
parent d09ecc72a1
commit debc44fee0
4 changed files with 231 additions and 5 deletions

View File

@@ -37,7 +37,10 @@ import { briefing as reddit } from './sources/reddit.mjs';
import { briefing as telegram } from './sources/telegram.mjs';
import { briefing as kiwisdr } from './sources/kiwisdr.mjs';
// === Tier 4: Live Market Data ===
// === Tier 4: Space & Satellites ===
import { briefing as space } from './sources/space.mjs';
// === Tier 5: Live Market Data ===
import { briefing as yfinance } from './sources/yfinance.mjs';
export async function runSource(name, fn, ...args) {
@@ -51,7 +54,7 @@ export async function runSource(name, fn, ...args) {
}
export async function fullBriefing() {
console.error('[Crucix] Starting intelligence sweep — 26 sources...');
console.error('[Crucix] Starting intelligence sweep — 27 sources...');
const start = Date.now();
const results = await Promise.allSettled([
@@ -86,7 +89,10 @@ export async function fullBriefing() {
runSource('Telegram', telegram),
runSource('KiwiSDR', kiwisdr),
// Tier 4: Live Market Data
// Tier 4: Space & Satellites
runSource('Space', space),
// Tier 5: Live Market Data
runSource('YFinance', yfinance),
]);

190
apis/sources/space.mjs Normal file
View File

@@ -0,0 +1,190 @@
// Space/CelesTrak — Satellite Activity Monitoring
// No API key required. Uses CelesTrak for public TLE data and launch info.
// Tracks: Recent launches, ISS position, satellite decay alerts, space debris.
import { safeFetch } from '../utils/fetch.mjs';
const CELESTRAK_BASE = 'https://celestrak.org';
// Satellite categories for monitoring
const SAT_CATEGORIES = {
stations: '/NORAD/elements/gp.php?GROUP=stations&FORMAT=json',
lastDay: '/NORAD/elements/gp.php?GROUP=last-30-days&FORMAT=json',
military: '/NORAD/elements/gp.php?GROUP=military&FORMAT=json',
gps: '/NORAD/elements/gp.php?GROUP=gps-ops&FORMAT=json',
starlink: '/NORAD/elements/gp.php?GROUP=starlink&FORMAT=json',
oneweb: '/NORAD/elements/gp.php?GROUP=oneweb&FORMAT=json',
};
// Get TLE data for a category
async function getTLEs(category) {
const path = SAT_CATEGORIES[category];
if (!path) return { error: 'Invalid category' };
const data = await safeFetch(`${CELESTRAK_BASE}${path}`, { timeout: 20000 });
return data;
}
// Get recent launches (from last 30 days TLEs)
async function getRecentLaunches() {
const data = await getTLEs('lastDay');
if (data.error || !Array.isArray(data)) {
return { error: data.error || 'Failed to fetch launch data' };
}
const launches = data.map(sat => ({
name: sat.OBJECT_NAME,
noradId: sat.NORAD_CAT_ID,
classification: sat.CLASSIFICATION_TYPE,
launchDate: sat.LAUNCH_DATE,
decayDate: sat.DECAY_DATE,
period: sat.PERIOD,
inclination: sat.INCLINATION,
apogee: sat.APOAPSIS,
perigee: sat.PERIAPSIS,
epoch: sat.EPOCH,
country: sat.COUNTRY_CODE,
objectType: sat.OBJECT_TYPE,
})).filter(s => s.name && s.noradId);
launches.sort((a, b) => new Date(b.epoch || 0) - new Date(a.epoch || 0));
const byCountry = {};
launches.forEach(l => {
const country = l.country || 'UNK';
byCountry[country] = (byCountry[country] || 0) + 1;
});
return { totalObjects: launches.length, recentLaunches: launches.slice(0, 25), byCountry };
}
// Get space station data
async function getStationData() {
const data = await getTLEs('stations');
if (data.error || !Array.isArray(data)) {
return { error: data.error || 'Failed to fetch station data' };
}
const stations = data.map(sat => ({
name: sat.OBJECT_NAME,
noradId: sat.NORAD_CAT_ID,
apogee: sat.APOAPSIS,
perigee: sat.PERIAPSIS,
inclination: sat.INCLINATION,
period: sat.PERIOD,
epoch: sat.EPOCH,
})).filter(s => s.name);
const iss = stations.find(s => s.name.includes('ISS') || s.noradId === 25544);
return { totalStations: stations.length, stations: stations.slice(0, 10), iss };
}
// Get military satellite count
async function getMilitaryCount() {
const data = await getTLEs('military');
if (data.error || !Array.isArray(data)) {
return { count: 0, error: data.error };
}
const byCountry = {};
data.forEach(sat => {
const country = sat.COUNTRY_CODE || 'UNK';
byCountry[country] = (byCountry[country] || 0) + 1;
});
return { count: data.length, byCountry };
}
// Get mega-constellation stats (Starlink, OneWeb)
async function getConstellationStats() {
const [starlink, oneweb] = await Promise.all([
getTLEs('starlink'),
getTLEs('oneweb'),
]);
return {
starlink: Array.isArray(starlink) ? starlink.length : 0,
oneweb: Array.isArray(oneweb) ? oneweb.length : 0,
};
}
// Generate signals
function generateSignals(data) {
const signals = [];
if (data.launches?.totalObjects > 50) {
signals.push(`HIGH LAUNCH TEMPO: ${data.launches.totalObjects} new objects tracked in last 30 days`);
}
const byCountry = data.launches?.byCountry || {};
const cnLaunches = byCountry['PRC'] || byCountry['CN'] || 0;
const ruLaunches = byCountry['CIS'] || byCountry['RU'] || 0;
if (cnLaunches > 10) {
signals.push(`CHINA SPACE ACTIVITY: ${cnLaunches} objects launched recently`);
}
if (ruLaunches > 5) {
signals.push(`RUSSIA SPACE ACTIVITY: ${ruLaunches} objects launched recently`);
}
if (data.military?.count > 500) {
signals.push(`MILITARY CONSTELLATION: ${data.military.count} tracked military satellites`);
}
if (data.constellations?.starlink > 6000) {
signals.push(`STARLINK MEGA-CONSTELLATION: ${data.constellations.starlink} active satellites`);
}
return signals;
}
// Briefing export
export async function briefing() {
try {
const [launches, stations, military, constellations] = await Promise.all([
getRecentLaunches(),
getStationData(),
getMilitaryCount(),
getConstellationStats(),
]);
const hasData = !launches.error || !stations.error;
if (!hasData) {
return {
source: 'Space/CelesTrak',
timestamp: new Date().toISOString(),
status: 'error',
error: launches.error || stations.error || 'Failed to fetch space data',
};
}
const data = { launches, stations, military, constellations };
const signals = generateSignals(data);
return {
source: 'Space/CelesTrak',
timestamp: new Date().toISOString(),
status: 'active',
recentLaunches: launches.recentLaunches || [],
totalNewObjects: launches.totalObjects || 0,
launchByCountry: launches.byCountry || {},
spaceStations: stations.stations || [],
iss: stations.iss || null,
militarySatellites: military.count || 0,
militaryByCountry: military.byCountry || {},
constellations: constellations || {},
signals,
};
} catch (e) {
return {
source: 'Space/CelesTrak',
timestamp: new Date().toISOString(),
status: 'error',
error: e.message,
};
}
}
if (process.argv[1]?.endsWith('space.mjs')) {
const data = await briefing();
console.log(JSON.stringify(data, null, 2));
}

View File

@@ -319,6 +319,22 @@ export async function synthesize(data) {
}));
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: [] } : {
@@ -391,7 +407,7 @@ export async function synthesize(data) {
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,
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)

View File

@@ -79,6 +79,7 @@ html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--s
.ldot.maritime{background:#b388ff;box-shadow:0 0 6px rgba(179,136,255,0.4)}
.ldot.health{background:#69f0ae;box-shadow:0 0 6px rgba(105,240,174,0.4)}
.ldot.news{background:#81d4fa;box-shadow:0 0 6px rgba(129,212,250,0.4)}
.ldot.space{background:#e0b0ff;box-shadow:0 0 6px rgba(224,176,255,0.4)}
.layer-name{font-size:12px;font-weight:500}
.layer-sub{font-size:10px;color:var(--dim)}
.layer-count{font-family:var(--mono);font-size:13px;font-weight:600;color:var(--accent)}
@@ -328,7 +329,8 @@ function renderLeftRail(){
{name:'Conflict Events',count:conflictEvents,dot:'thermal',sub:`${conflictFatal.toLocaleString()} fatalities`},
{name:'Health Watch',count:D.who.length,dot:'health',sub:'WHO alerts'},
{name:'World News',count:newsCount,dot:'news',sub:'RSS geolocated'},
{name:'OSINT Feed',count:D.tg.posts,dot:'incident',sub:`${D.tg.urgent.length} urgent`}
{name:'OSINT Feed',count:D.tg.posts,dot:'incident',sub:`${D.tg.urgent.length} urgent`},
{name:'Satellites',count:D.space?.militarySats||0,dot:'space',sub:`${D.space?.totalNewObjects||0} new (30d)`}
];
const allNormal=D.nuke.every(s=>!s.anom);
const nukeHtml=D.nuke.map(s=>`<div class="site-row"><span>${s.site}</span><span class="site-val">${s.n>0?(s.cpm?.toFixed(1)||'--')+' CPM':'No data'}</span></div>`).join('');
@@ -358,6 +360,18 @@ function renderLeftRail(){
<div class="econ-row"><span class="elabel">30Y Mortgage</span><span class="eval">${mort?.value||'--'}%</span></div>
<div class="econ-row"><span class="elabel">M2 Supply</span><span class="eval">$${(m2?.value/1000)?.toFixed(1)||'--'}T</span></div>
<div class="econ-row"><span class="elabel">Nat. Debt</span><span class="eval">$${(parseFloat(D.treasury.totalDebt)/1e12).toFixed(2)}T</span></div>
</div>
<div class="g-panel">
<div class="sec-head"><h3>Space Watch</h3><span class="badge">CELESTRAK</span></div>
${D.space ? `
<div class="econ-row"><span class="elabel">New Objects (30d)</span><span class="eval" style="color:var(--accent2)">${D.space.totalNewObjects||0}</span></div>
<div class="econ-row"><span class="elabel">Military Sats</span><span class="eval">${D.space.militarySats||0}</span></div>
<div class="econ-row"><span class="elabel">Starlink</span><span class="eval">${D.space.constellations?.starlink||0}</span></div>
<div class="econ-row"><span class="elabel">OneWeb</span><span class="eval">${D.space.constellations?.oneweb||0}</span></div>
${D.space.iss ? `<div class="econ-row"><span class="elabel">ISS</span><span class="eval" style="color:var(--accent)">ALT ${((D.space.iss.apogee+D.space.iss.perigee)/2).toFixed(0)} km</span></div>` : ''}
${Object.entries(D.space.militaryByCountry||{}).sort((a,b)=>b[1]-a[1]).slice(0,4).map(([c,n])=>`<div class="econ-row"><span class="elabel" style="padding-left:8px">${c}</span><span class="eval" style="font-size:10px">${n} mil sats</span></div>`).join('')}
${(D.space.signals||[]).length ? `<div style="margin-top:6px;padding:6px 8px;border:1px solid rgba(68,204,255,0.2);background:rgba(68,204,255,0.04);font-family:var(--mono);font-size:9px;color:var(--accent2);line-height:1.5">${D.space.signals.slice(0,2).join('<br>')}</div>` : ''}
` : '<div style="font-family:var(--mono);font-size:10px;color:var(--dim)">NO SPACE DATA</div>'}
</div>`;
}