mirror of
https://github.com/koala73/worldmonitor.git
synced 2026-05-13 02:26:22 +02:00
* feat(gpsjam): migrate GPS jamming from gpsjam.org to Wingbits API Replace gpsjam.org CSV scraping with Wingbits customer API for GPS/GNSS interference data. This is a proper API with structured JSON responses instead of fragile web scraping. Key changes: - Rewrite fetch-gpsjam.mjs seeder for Wingbits API (x-api-key auth) - Delete ~150-line gpsjam seed loop from ais-relay.cjs (now standalone cron) - Simplify api/gpsjam.js to Redis-only reads with v1→v2 fallback - Update data shape: pct/good/bad/total → npAvg/sampleCount/aircraftCount - Redis key: intelligence:gpsjam:v1 → v2 (with dual-write transition) - Add vite dev plugin for local development - Update all frontend components (MapPopup, DeckGLMap, GlobeMap, locales) Zero-downtime: seeder dual-writes both v1 and v2 keys, edge handler falls back to v1 with shape normalization. Remove v1 code after 24-72h. * fix(gpsjam): improve v1 fallback normalization and update all locale files - v1 fallback now derives npAvg from severity thresholds (high: 0.3, medium: 0.8) instead of hardcoding 0, uses bad/total for counts - Update all 20 non-English locale files to use new gpsJamming keys (navPerformance, samples, aircraft) with English fallback values * fix(gpsjam): dual-write v1 in old schema shape and catch Redis errors - Seeder now converts v2 data back to v1 shape (pct/good/bad/total) for the dual-write, so old deployments and rollbacks parse correctly - Edge handler wraps readFromRedis calls in try-catch so network/timeout errors return graceful 503 instead of platform 500s
111 lines
2.8 KiB
JavaScript
111 lines
2.8 KiB
JavaScript
import { getCorsHeaders, isDisallowedOrigin } from './_cors.js';
|
|
|
|
export const config = { runtime: 'edge' };
|
|
|
|
const REDIS_KEY = 'intelligence:gpsjam:v2';
|
|
const REDIS_KEY_V1 = 'intelligence:gpsjam:v1';
|
|
|
|
let cached = null;
|
|
let cachedAt = 0;
|
|
const CACHE_TTL = 300_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 fetchGpsJamData() {
|
|
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) {
|
|
let v1;
|
|
try { v1 = await readFromRedis(REDIS_KEY_V1); } catch { v1 = null; }
|
|
if (v1?.hexes) {
|
|
data = {
|
|
...v1,
|
|
source: v1.source || 'gpsjam.org (normalized)',
|
|
hexes: v1.hexes.map(hex => {
|
|
if ('npAvg' in hex) return hex;
|
|
const pct = hex.pct || 0;
|
|
return {
|
|
h3: hex.h3,
|
|
lat: hex.lat,
|
|
lon: hex.lon,
|
|
level: hex.level,
|
|
region: hex.region,
|
|
npAvg: pct > 10 ? 0.3 : pct >= 2 ? 0.8 : 1.5,
|
|
sampleCount: hex.bad || 0,
|
|
aircraftCount: hex.total || 0,
|
|
};
|
|
}),
|
|
};
|
|
}
|
|
}
|
|
|
|
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 fetchGpsJamData();
|
|
|
|
if (!data) {
|
|
return new Response(JSON.stringify({ error: 'GPS interference 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,
|
|
},
|
|
});
|
|
}
|