mirror of
https://github.com/koala73/worldmonitor.git
synced 2026-05-14 19:16:20 +02:00
Egress optimization: - Add s-maxage + stale-while-revalidate to all API endpoints for Vercel CDN caching - Add vercel.json with immutable caching for hashed assets - Add gzip compression to sidecar responses >1KB - Add gzip to Railway RSS responses (4 paths previously uncompressed) - Increase polling intervals: markets/crypto 60s→120s, ETF/macro/stablecoins 60s→180s - Remove hardcoded Railway URL from theater-posture.js (now env-var only) PWA / Service Worker: - Add vite-plugin-pwa with autoUpdate strategy - Cache map tiles (CacheFirst), fonts (StaleWhileRevalidate), static assets - NetworkOnly for all /api/* routes (real-time data must be fresh) - Manual SW registration (web only, skip Tauri) - Add offline fallback page - Replace manual manifest with plugin-generated manifest Polymarket fix: - Route dev proxy through production Vercel (bypasses JA3 blocking) - Add 4th fallback tier: production URL as absolute fallback Desktop/Sidecar: - Dual-backend cache (_upstash-cache.js): Redis cloud + in-memory+file desktop - Settings window OK/Cancel redesign - Runtime config and secret injection improvements
146 lines
4.4 KiB
JavaScript
146 lines
4.4 KiB
JavaScript
// UCDP (Uppsala Conflict Data Program) proxy
|
|
// Returns conflict classification per country with intensity levels
|
|
// No auth required - public API
|
|
export const config = { runtime: 'edge' };
|
|
|
|
import { getCachedJson, setCachedJson } from './_upstash-cache.js';
|
|
import { recordCacheTelemetry } from './_cache-telemetry.js';
|
|
|
|
const CACHE_KEY = 'ucdp:country-conflicts:v2';
|
|
const CACHE_TTL_SECONDS = 24 * 60 * 60; // 24 hours (annual data)
|
|
const CACHE_TTL_MS = CACHE_TTL_SECONDS * 1000;
|
|
const RESPONSE_CACHE_CONTROL = 'public, max-age=3600';
|
|
|
|
// In-memory fallback when Redis is unavailable.
|
|
let fallbackCache = { data: null, timestamp: 0 };
|
|
|
|
function isValidResult(data) {
|
|
return Boolean(
|
|
data &&
|
|
typeof data === 'object' &&
|
|
Array.isArray(data.conflicts)
|
|
);
|
|
}
|
|
|
|
function toErrorMessage(error) {
|
|
if (error instanceof Error) return error.message;
|
|
return String(error || 'unknown error');
|
|
}
|
|
|
|
export default async function handler(req) {
|
|
const now = Date.now();
|
|
const cached = await getCachedJson(CACHE_KEY);
|
|
if (isValidResult(cached)) {
|
|
recordCacheTelemetry('/api/ucdp', 'REDIS-HIT');
|
|
return Response.json(cached, {
|
|
status: 200,
|
|
headers: {
|
|
'Access-Control-Allow-Origin': '*',
|
|
'Cache-Control': RESPONSE_CACHE_CONTROL,
|
|
'X-Cache': 'REDIS-HIT',
|
|
},
|
|
});
|
|
}
|
|
|
|
if (isValidResult(fallbackCache.data) && now - fallbackCache.timestamp < CACHE_TTL_MS) {
|
|
recordCacheTelemetry('/api/ucdp', 'MEMORY-HIT');
|
|
return Response.json(fallbackCache.data, {
|
|
status: 200,
|
|
headers: {
|
|
'Access-Control-Allow-Origin': '*',
|
|
'Cache-Control': RESPONSE_CACHE_CONTROL,
|
|
'X-Cache': 'MEMORY-HIT',
|
|
},
|
|
});
|
|
}
|
|
|
|
try {
|
|
// Fetch all pages of conflicts
|
|
let allConflicts = [];
|
|
let page = 0;
|
|
let totalPages = 1;
|
|
|
|
while (page < totalPages) {
|
|
const response = await fetch(`https://ucdpapi.pcr.uu.se/api/ucdpprioconflict/24.1?pagesize=100&page=${page}`, {
|
|
headers: { 'Accept': 'application/json' },
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`UCDP API error: ${response.status}`);
|
|
}
|
|
|
|
const rawData = await response.json();
|
|
totalPages = rawData.TotalPages || 1;
|
|
const conflicts = rawData.Result || [];
|
|
allConflicts = allConflicts.concat(conflicts);
|
|
page++;
|
|
}
|
|
|
|
// Fields are snake_case: conflict_id, location, side_a, side_b, year, intensity_level, type_of_conflict
|
|
const countryConflicts = {};
|
|
for (const c of allConflicts) {
|
|
const name = c.location || '';
|
|
const year = parseInt(c.year, 10) || 0;
|
|
const intensity = parseInt(c.intensity_level, 10) || 0;
|
|
|
|
const entry = {
|
|
conflictId: parseInt(c.conflict_id, 10) || 0,
|
|
conflictName: c.side_b || '',
|
|
location: name,
|
|
year,
|
|
intensityLevel: intensity,
|
|
typeOfConflict: parseInt(c.type_of_conflict, 10) || 0,
|
|
startDate: c.start_date,
|
|
startDate2: c.start_date2,
|
|
sideA: c.side_a,
|
|
sideB: c.side_b,
|
|
region: c.region,
|
|
};
|
|
|
|
// Keep most recent / highest intensity per location
|
|
if (!countryConflicts[name] || year > countryConflicts[name].year ||
|
|
(year === countryConflicts[name].year && intensity > countryConflicts[name].intensityLevel)) {
|
|
countryConflicts[name] = entry;
|
|
}
|
|
}
|
|
|
|
const result = {
|
|
success: true,
|
|
count: Object.keys(countryConflicts).length,
|
|
conflicts: Object.values(countryConflicts),
|
|
cached_at: new Date().toISOString(),
|
|
};
|
|
|
|
fallbackCache = { data: result, timestamp: now };
|
|
void setCachedJson(CACHE_KEY, result, CACHE_TTL_SECONDS);
|
|
recordCacheTelemetry('/api/ucdp', 'MISS');
|
|
|
|
return Response.json(result, {
|
|
status: 200,
|
|
headers: {
|
|
'Access-Control-Allow-Origin': '*',
|
|
'Cache-Control': RESPONSE_CACHE_CONTROL,
|
|
'X-Cache': 'MISS',
|
|
},
|
|
});
|
|
} catch (error) {
|
|
if (isValidResult(fallbackCache.data)) {
|
|
recordCacheTelemetry('/api/ucdp', 'STALE');
|
|
return Response.json(fallbackCache.data, {
|
|
status: 200,
|
|
headers: {
|
|
'Access-Control-Allow-Origin': '*',
|
|
'Cache-Control': 'public, max-age=600, s-maxage=600, stale-while-revalidate=120',
|
|
'X-Cache': 'STALE',
|
|
},
|
|
});
|
|
}
|
|
|
|
recordCacheTelemetry('/api/ucdp', 'ERROR');
|
|
return Response.json({ error: `Fetch failed: ${toErrorMessage(error)}`, conflicts: [] }, {
|
|
status: 500,
|
|
headers: { 'Access-Control-Allow-Origin': '*' },
|
|
});
|
|
}
|
|
}
|