Files
worldmonitor/api/hapi.js
Elie Habib c353cf2070 Reduce egress costs, add PWA support, fix Polymarket and Railway relay
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
2026-02-14 19:53:04 +04:00

144 lines
4.8 KiB
JavaScript

// HDX HAPI (Humanitarian API) proxy
// Returns aggregated conflict event counts per country
// Source: ACLED data aggregated monthly by HDX
export const config = { runtime: 'edge' };
import { getCachedJson, setCachedJson } from './_upstash-cache.js';
import { recordCacheTelemetry } from './_cache-telemetry.js';
const CACHE_KEY = 'hapi:conflict-events:v2';
const CACHE_TTL_SECONDS = 6 * 60 * 60; // 6 hours
const CACHE_TTL_MS = CACHE_TTL_SECONDS * 1000;
const RESPONSE_CACHE_CONTROL = 'public, max-age=1800';
// 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.countries)
);
}
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/hapi', '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/hapi', 'MEMORY-HIT');
return Response.json(fallbackCache.data, {
status: 200,
headers: {
'Access-Control-Allow-Origin': '*',
'Cache-Control': RESPONSE_CACHE_CONTROL,
'X-Cache': 'MEMORY-HIT',
},
});
}
try {
const appId = btoa('worldmonitor:monitor@worldmonitor.app');
const response = await fetch(
`https://hapi.humdata.org/api/v2/coordination-context/conflict-events?output_format=json&limit=1000&offset=0&app_identifier=${appId}`,
{
headers: {
'Accept': 'application/json',
},
}
);
if (!response.ok) {
throw new Error(`HAPI API error: ${response.status}`);
}
const rawData = await response.json();
const records = rawData.data || [];
// Each record is (country, event_type, month) — aggregate across event types per country
// Keep only the most recent month per country
const byCountry = {};
for (const r of records) {
const iso3 = r.location_code || '';
if (!iso3) continue;
const month = r.reference_period_start || '';
const eventType = (r.event_type || '').toLowerCase();
const events = r.events || 0;
const fatalities = r.fatalities || 0;
if (!byCountry[iso3]) {
byCountry[iso3] = { iso3, locationName: r.location_name || '', month, eventsTotal: 0, eventsPoliticalViolence: 0, eventsCivilianTargeting: 0, eventsDemonstrations: 0, fatalitiesTotalPoliticalViolence: 0, fatalitiesTotalCivilianTargeting: 0 };
}
const c = byCountry[iso3];
if (month > c.month) {
// Newer month — reset
c.month = month;
c.eventsTotal = 0; c.eventsPoliticalViolence = 0; c.eventsCivilianTargeting = 0; c.eventsDemonstrations = 0; c.fatalitiesTotalPoliticalViolence = 0; c.fatalitiesTotalCivilianTargeting = 0;
}
if (month === c.month) {
c.eventsTotal += events;
if (eventType.includes('political_violence')) { c.eventsPoliticalViolence += events; c.fatalitiesTotalPoliticalViolence += fatalities; }
if (eventType.includes('civilian_targeting')) { c.eventsCivilianTargeting += events; c.fatalitiesTotalCivilianTargeting += fatalities; }
if (eventType.includes('demonstration')) { c.eventsDemonstrations += events; }
}
}
const result = {
success: true,
count: Object.keys(byCountry).length,
countries: Object.values(byCountry),
cached_at: new Date().toISOString(),
};
fallbackCache = { data: result, timestamp: now };
void setCachedJson(CACHE_KEY, result, CACHE_TTL_SECONDS);
recordCacheTelemetry('/api/hapi', '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/hapi', 'STALE');
return Response.json(fallbackCache.data, {
status: 200,
headers: {
'Access-Control-Allow-Origin': '*',
'Cache-Control': 'public, max-age=300, s-maxage=300, stale-while-revalidate=60',
'X-Cache': 'STALE',
},
});
}
recordCacheTelemetry('/api/hapi', 'ERROR');
return Response.json({ error: `Fetch failed: ${toErrorMessage(error)}`, countries: [] }, {
status: 500,
headers: { 'Access-Control-Allow-Origin': '*' },
});
}
}