mirror of
https://github.com/koala73/worldmonitor.git
synced 2026-05-13 10:36:21 +02:00
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)
70 lines
2.1 KiB
JavaScript
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,
|
|
},
|
|
});
|
|
}
|