- 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
191 lines
5.7 KiB
JavaScript
191 lines
5.7 KiB
JavaScript
// 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));
|
|
}
|