Files
worldmonitor/api/gpsjam.js
Elie Habib 2f7fd6421f feat(gpsjam): migrate GPS jamming from gpsjam.org to Wingbits API (#1240)
* 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
2026-03-08 02:15:34 +04:00

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,
},
});
}