Files
worldmonitor/api/satellites.js
Elie Habib 9772548d83 feat: add orbital surveillance layer with real-time satellite tracking (#1278)
Track ~80-120 intelligence-relevant satellites on the 3D globe using CelesTrak
TLE data and client-side SGP4 propagation (satellite.js). Satellites render at
actual orbital altitude with country-coded colors, 15-min orbit trails, and
ground footprint projections.

Architecture: Railway seeds TLEs every 2h → Redis → Vercel CDN (1h cache) →
browser does SGP4 math every 3s (zero server cost for real-time movement).

- New relay seed loop (ais-relay.cjs) fetching military + resource groups
- New edge handler (api/satellites.js) with 10min cache + negative cache
- Frontend service with circuit breaker and propagation lifecycle
- GlobeMap integration: markers, trails (pathsData), footprints, tooltips
- Layer registry as globe-only "Orbital Surveillance" with i18n (21 locales)
- Full documentation at docs/ORBITAL_SURVEILLANCE.md with roadmap
- Fix pre-existing SearchModal TS error (non-null assertion)
2026-03-08 21:55:46 +04:00

70 lines
2.1 KiB
JavaScript

import { getCorsHeaders, isDisallowedOrigin } from './_cors.js';
export const config = { runtime: 'edge' };
const REDIS_KEY = 'intelligence:satellites:tle:v1';
let cached = null;
let cachedAt = 0;
const CACHE_TTL = 600_000;
let negUntil = 0;
const NEG_TTL = 60_000;
async function readFromRedis(key) {
const url = process.env.UPSTASH_REDIS_REST_URL;
const token = process.env.UPSTASH_REDIS_REST_TOKEN;
if (!url || !token) return null;
const resp = await fetch(`${url}/get/${encodeURIComponent(key)}`, {
headers: { Authorization: `Bearer ${token}` },
signal: AbortSignal.timeout(3_000),
});
if (!resp.ok) return null;
const data = await resp.json();
if (!data.result) return null;
try { return JSON.parse(data.result); } catch { return null; }
}
async function fetchSatelliteData() {
const now = Date.now();
if (cached && now - cachedAt < CACHE_TTL) return cached;
if (now < negUntil) return null;
let data;
try { data = await readFromRedis(REDIS_KEY); } catch { data = null; }
if (!data) {
negUntil = now + NEG_TTL;
return null;
}
cached = data;
cachedAt = now;
return data;
}
export default async function handler(req) {
const corsHeaders = getCorsHeaders(req, 'GET, OPTIONS');
if (req.method === 'OPTIONS') {
return new Response(null, { status: 204, headers: corsHeaders });
}
if (isDisallowedOrigin(req)) {
return new Response(JSON.stringify({ error: 'Origin not allowed' }), {
status: 403,
headers: { 'Content-Type': 'application/json', ...corsHeaders },
});
}
const data = await fetchSatelliteData();
if (!data) {
return new Response(JSON.stringify({ error: 'Satellite data temporarily unavailable' }), {
status: 503,
headers: { 'Content-Type': 'application/json', 'Cache-Control': 'no-cache, no-store', ...corsHeaders },
});
}
return new Response(JSON.stringify(data), {
status: 200,
headers: {
'Content-Type': 'application/json',
'Cache-Control': 's-maxage=3600, stale-while-revalidate=1800, stale-if-error=3600',
...corsHeaders,
},
});
}