Files
worldmonitor/vite.config.ts
Elie Habib b8be2f6890 fix: sentry triage + SW POST method for PostHog ingest (#246)
- Fix PostHog /ingest 404: Workbox registerRoute defaults to GET only,
  PostHog sends POST. Add POST routes for /api/ and /ingest/.
- Fix fullscreen crash: optional chaining on exitFullscreen()?.catch()
  for browsers returning undefined instead of Promise.
- Add 6 noise filters: __firefox__, ifameElement.contentDocument,
  Invalid video id, Fetch is aborted, Stylesheet append timeout,
  Cannot assign to read only property.
- Widen Program failed to link filter (remove ": null" suffix).
2026-02-23 08:26:54 +00:00

1053 lines
41 KiB
TypeScript

import { defineConfig, type Plugin } from 'vite';
import { VitePWA } from 'vite-plugin-pwa';
import { resolve, dirname, extname } from 'path';
import { mkdir, readFile, writeFile } from 'fs/promises';
import { brotliCompress } from 'zlib';
import { promisify } from 'util';
import pkg from './package.json';
const isE2E = process.env.VITE_E2E === '1';
const brotliCompressAsync = promisify(brotliCompress);
const BROTLI_EXTENSIONS = new Set(['.js', '.mjs', '.css', '.html', '.svg', '.json', '.txt', '.xml', '.wasm']);
function brotliPrecompressPlugin(): Plugin {
return {
name: 'brotli-precompress',
apply: 'build',
async writeBundle(outputOptions, bundle) {
const outDir = outputOptions.dir;
if (!outDir) return;
await Promise.all(Object.keys(bundle).map(async (fileName) => {
const extension = extname(fileName).toLowerCase();
if (!BROTLI_EXTENSIONS.has(extension)) return;
const sourcePath = resolve(outDir, fileName);
const compressedPath = `${sourcePath}.br`;
const sourceBuffer = await readFile(sourcePath);
if (sourceBuffer.length < 1024) return;
const compressedBuffer = await brotliCompressAsync(sourceBuffer);
await mkdir(dirname(compressedPath), { recursive: true });
await writeFile(compressedPath, compressedBuffer);
}));
},
};
}
const VARIANT_META: Record<string, {
title: string;
description: string;
keywords: string;
url: string;
siteName: string;
shortName: string;
subject: string;
classification: string;
categories: string[];
features: string[];
}> = {
full: {
title: 'World Monitor - Real-Time Global Intelligence Dashboard',
description: 'Real-time global intelligence dashboard with live news, markets, military tracking, infrastructure monitoring, and geopolitical data. OSINT in one view.',
keywords: 'global intelligence, geopolitical dashboard, world news, market data, military bases, nuclear facilities, undersea cables, conflict zones, real-time monitoring, situation awareness, OSINT, flight tracking, AIS ships, earthquake monitor, protest tracker, power outages, oil prices, government spending, polymarket predictions',
url: 'https://worldmonitor.app/',
siteName: 'World Monitor',
shortName: 'WorldMonitor',
subject: 'Real-Time Global Intelligence and Situation Awareness',
classification: 'Intelligence Dashboard, OSINT Tool, News Aggregator',
categories: ['news', 'productivity'],
features: [
'Real-time news aggregation',
'Stock market tracking',
'Military flight monitoring',
'Ship AIS tracking',
'Earthquake alerts',
'Protest tracking',
'Power outage monitoring',
'Oil price analytics',
'Government spending data',
'Prediction markets',
'Infrastructure monitoring',
'Geopolitical intelligence',
],
},
tech: {
title: 'Tech Monitor - Real-Time AI & Tech Industry Dashboard',
description: 'Real-time AI and tech industry dashboard tracking tech giants, AI labs, startup ecosystems, funding rounds, and tech events worldwide.',
keywords: 'tech dashboard, AI industry, startup ecosystem, tech companies, AI labs, venture capital, tech events, tech conferences, cloud infrastructure, datacenters, tech layoffs, funding rounds, unicorns, FAANG, tech HQ, accelerators, Y Combinator, tech news',
url: 'https://tech.worldmonitor.app/',
siteName: 'Tech Monitor',
shortName: 'TechMonitor',
subject: 'AI, Tech Industry, and Startup Ecosystem Intelligence',
classification: 'Tech Dashboard, AI Tracker, Startup Intelligence',
categories: ['news', 'business'],
features: [
'Tech news aggregation',
'AI lab tracking',
'Startup ecosystem mapping',
'Tech HQ locations',
'Conference & event calendar',
'Cloud infrastructure monitoring',
'Datacenter mapping',
'Tech layoff tracking',
'Funding round analytics',
'Tech stock tracking',
'Service status monitoring',
],
},
finance: {
title: 'Finance Monitor - Real-Time Markets & Trading Dashboard',
description: 'Real-time finance and trading dashboard tracking global markets, stock exchanges, central banks, commodities, forex, crypto, and economic indicators worldwide.',
keywords: 'finance dashboard, trading dashboard, stock market, forex, commodities, central banks, crypto, economic indicators, market news, financial centers, stock exchanges, bonds, derivatives, fintech, hedge funds, IPO tracker, market analysis',
url: 'https://finance.worldmonitor.app/',
siteName: 'Finance Monitor',
shortName: 'FinanceMonitor',
subject: 'Global Markets, Trading, and Financial Intelligence',
classification: 'Finance Dashboard, Market Tracker, Trading Intelligence',
categories: ['finance', 'news'],
features: [
'Real-time market data',
'Stock exchange mapping',
'Central bank monitoring',
'Commodity price tracking',
'Forex & currency news',
'Crypto & digital assets',
'Economic indicator alerts',
'IPO & earnings tracking',
'Financial center mapping',
'Sector heatmap',
'Market radar signals',
],
},
};
const activeVariant = process.env.VITE_VARIANT || 'full';
const activeMeta = VARIANT_META[activeVariant] || VARIANT_META.full;
function htmlVariantPlugin(): Plugin {
return {
name: 'html-variant',
transformIndexHtml(html) {
return html
.replace(/<title>.*?<\/title>/, `<title>${activeMeta.title}</title>`)
.replace(/<meta name="title" content=".*?" \/>/, `<meta name="title" content="${activeMeta.title}" />`)
.replace(/<meta name="description" content=".*?" \/>/, `<meta name="description" content="${activeMeta.description}" />`)
.replace(/<meta name="keywords" content=".*?" \/>/, `<meta name="keywords" content="${activeMeta.keywords}" />`)
.replace(/<link rel="canonical" href=".*?" \/>/, `<link rel="canonical" href="${activeMeta.url}" />`)
.replace(/<meta name="application-name" content=".*?" \/>/, `<meta name="application-name" content="${activeMeta.siteName}" />`)
.replace(/<meta property="og:url" content=".*?" \/>/, `<meta property="og:url" content="${activeMeta.url}" />`)
.replace(/<meta property="og:title" content=".*?" \/>/, `<meta property="og:title" content="${activeMeta.title}" />`)
.replace(/<meta property="og:description" content=".*?" \/>/, `<meta property="og:description" content="${activeMeta.description}" />`)
.replace(/<meta property="og:site_name" content=".*?" \/>/, `<meta property="og:site_name" content="${activeMeta.siteName}" />`)
.replace(/<meta name="subject" content=".*?" \/>/, `<meta name="subject" content="${activeMeta.subject}" />`)
.replace(/<meta name="classification" content=".*?" \/>/, `<meta name="classification" content="${activeMeta.classification}" />`)
.replace(/<meta name="twitter:url" content=".*?" \/>/, `<meta name="twitter:url" content="${activeMeta.url}" />`)
.replace(/<meta name="twitter:title" content=".*?" \/>/, `<meta name="twitter:title" content="${activeMeta.title}" />`)
.replace(/<meta name="twitter:description" content=".*?" \/>/, `<meta name="twitter:description" content="${activeMeta.description}" />`)
.replace(/"name": "World Monitor"/, `"name": "${activeMeta.siteName}"`)
.replace(/"alternateName": "WorldMonitor"/, `"alternateName": "${activeMeta.siteName.replace(' ', '')}"`)
.replace(/"url": "https:\/\/worldmonitor\.app\/"/, `"url": "${activeMeta.url}"`)
.replace(/"description": "Real-time global intelligence dashboard with live news, markets, military tracking, infrastructure monitoring, and geopolitical data."/, `"description": "${activeMeta.description}"`)
.replace(/"featureList": \[[\s\S]*?\]/, `"featureList": ${JSON.stringify(activeMeta.features, null, 8).replace(/\n/g, '\n ')}`);
},
};
}
function polymarketPlugin(): Plugin {
const GAMMA_BASE = 'https://gamma-api.polymarket.com';
const ALLOWED_ORDER = ['volume', 'liquidity', 'startDate', 'endDate', 'spread'];
return {
name: 'polymarket-dev',
configureServer(server) {
server.middlewares.use(async (req, res, next) => {
if (!req.url?.startsWith('/api/polymarket')) return next();
const url = new URL(req.url, 'http://localhost');
const endpoint = url.searchParams.get('endpoint') || 'markets';
const closed = ['true', 'false'].includes(url.searchParams.get('closed') ?? '') ? url.searchParams.get('closed') : 'false';
const order = ALLOWED_ORDER.includes(url.searchParams.get('order') ?? '') ? url.searchParams.get('order') : 'volume';
const ascending = ['true', 'false'].includes(url.searchParams.get('ascending') ?? '') ? url.searchParams.get('ascending') : 'false';
const rawLimit = parseInt(url.searchParams.get('limit') ?? '', 10);
const limit = isNaN(rawLimit) ? 50 : Math.max(1, Math.min(100, rawLimit));
const params = new URLSearchParams({ closed: closed!, order: order!, ascending: ascending!, limit: String(limit) });
if (endpoint === 'events') {
const tag = (url.searchParams.get('tag') ?? '').replace(/[^a-z0-9-]/gi, '').slice(0, 100);
if (tag) params.set('tag_slug', tag);
}
const gammaUrl = `${GAMMA_BASE}/${endpoint === 'events' ? 'events' : 'markets'}?${params}`;
res.setHeader('Content-Type', 'application/json');
try {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), 8000);
const resp = await fetch(gammaUrl, { headers: { Accept: 'application/json' }, signal: controller.signal });
clearTimeout(timer);
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
const data = await resp.text();
res.setHeader('Cache-Control', 'public, max-age=120');
res.setHeader('X-Polymarket-Source', 'gamma');
res.end(data);
} catch {
// Expected: Cloudflare JA3 blocks server-side TLS — return empty array
res.setHeader('Cache-Control', 'public, max-age=300');
res.end('[]');
}
});
},
};
}
/**
* Vite dev server plugin for sebuf API routes.
*
* Intercepts requests matching /api/{domain}/v1/* and routes them through
* the same handler pipeline as the Vercel catch-all gateway. Other /api/*
* paths fall through to existing proxy rules.
*/
function sebufApiPlugin(): Plugin {
// Cache router across requests (H-13 fix). Invalidated by Vite's module graph on HMR.
let cachedRouter: Awaited<ReturnType<typeof buildRouter>> | null = null;
let cachedCorsMod: any = null;
async function buildRouter() {
const [
routerMod, corsMod, errorMod,
seismologyServerMod, seismologyHandlerMod,
wildfireServerMod, wildfireHandlerMod,
climateServerMod, climateHandlerMod,
predictionServerMod, predictionHandlerMod,
displacementServerMod, displacementHandlerMod,
aviationServerMod, aviationHandlerMod,
researchServerMod, researchHandlerMod,
unrestServerMod, unrestHandlerMod,
conflictServerMod, conflictHandlerMod,
maritimeServerMod, maritimeHandlerMod,
cyberServerMod, cyberHandlerMod,
economicServerMod, economicHandlerMod,
infrastructureServerMod, infrastructureHandlerMod,
marketServerMod, marketHandlerMod,
newsServerMod, newsHandlerMod,
intelligenceServerMod, intelligenceHandlerMod,
militaryServerMod, militaryHandlerMod,
] = await Promise.all([
import('./server/router'),
import('./server/cors'),
import('./server/error-mapper'),
import('./src/generated/server/worldmonitor/seismology/v1/service_server'),
import('./server/worldmonitor/seismology/v1/handler'),
import('./src/generated/server/worldmonitor/wildfire/v1/service_server'),
import('./server/worldmonitor/wildfire/v1/handler'),
import('./src/generated/server/worldmonitor/climate/v1/service_server'),
import('./server/worldmonitor/climate/v1/handler'),
import('./src/generated/server/worldmonitor/prediction/v1/service_server'),
import('./server/worldmonitor/prediction/v1/handler'),
import('./src/generated/server/worldmonitor/displacement/v1/service_server'),
import('./server/worldmonitor/displacement/v1/handler'),
import('./src/generated/server/worldmonitor/aviation/v1/service_server'),
import('./server/worldmonitor/aviation/v1/handler'),
import('./src/generated/server/worldmonitor/research/v1/service_server'),
import('./server/worldmonitor/research/v1/handler'),
import('./src/generated/server/worldmonitor/unrest/v1/service_server'),
import('./server/worldmonitor/unrest/v1/handler'),
import('./src/generated/server/worldmonitor/conflict/v1/service_server'),
import('./server/worldmonitor/conflict/v1/handler'),
import('./src/generated/server/worldmonitor/maritime/v1/service_server'),
import('./server/worldmonitor/maritime/v1/handler'),
import('./src/generated/server/worldmonitor/cyber/v1/service_server'),
import('./server/worldmonitor/cyber/v1/handler'),
import('./src/generated/server/worldmonitor/economic/v1/service_server'),
import('./server/worldmonitor/economic/v1/handler'),
import('./src/generated/server/worldmonitor/infrastructure/v1/service_server'),
import('./server/worldmonitor/infrastructure/v1/handler'),
import('./src/generated/server/worldmonitor/market/v1/service_server'),
import('./server/worldmonitor/market/v1/handler'),
import('./src/generated/server/worldmonitor/news/v1/service_server'),
import('./server/worldmonitor/news/v1/handler'),
import('./src/generated/server/worldmonitor/intelligence/v1/service_server'),
import('./server/worldmonitor/intelligence/v1/handler'),
import('./src/generated/server/worldmonitor/military/v1/service_server'),
import('./server/worldmonitor/military/v1/handler'),
]);
const serverOptions = { onError: errorMod.mapErrorToResponse };
const allRoutes = [
...seismologyServerMod.createSeismologyServiceRoutes(seismologyHandlerMod.seismologyHandler, serverOptions),
...wildfireServerMod.createWildfireServiceRoutes(wildfireHandlerMod.wildfireHandler, serverOptions),
...climateServerMod.createClimateServiceRoutes(climateHandlerMod.climateHandler, serverOptions),
...predictionServerMod.createPredictionServiceRoutes(predictionHandlerMod.predictionHandler, serverOptions),
...displacementServerMod.createDisplacementServiceRoutes(displacementHandlerMod.displacementHandler, serverOptions),
...aviationServerMod.createAviationServiceRoutes(aviationHandlerMod.aviationHandler, serverOptions),
...researchServerMod.createResearchServiceRoutes(researchHandlerMod.researchHandler, serverOptions),
...unrestServerMod.createUnrestServiceRoutes(unrestHandlerMod.unrestHandler, serverOptions),
...conflictServerMod.createConflictServiceRoutes(conflictHandlerMod.conflictHandler, serverOptions),
...maritimeServerMod.createMaritimeServiceRoutes(maritimeHandlerMod.maritimeHandler, serverOptions),
...cyberServerMod.createCyberServiceRoutes(cyberHandlerMod.cyberHandler, serverOptions),
...economicServerMod.createEconomicServiceRoutes(economicHandlerMod.economicHandler, serverOptions),
...infrastructureServerMod.createInfrastructureServiceRoutes(infrastructureHandlerMod.infrastructureHandler, serverOptions),
...marketServerMod.createMarketServiceRoutes(marketHandlerMod.marketHandler, serverOptions),
...newsServerMod.createNewsServiceRoutes(newsHandlerMod.newsHandler, serverOptions),
...intelligenceServerMod.createIntelligenceServiceRoutes(intelligenceHandlerMod.intelligenceHandler, serverOptions),
...militaryServerMod.createMilitaryServiceRoutes(militaryHandlerMod.militaryHandler, serverOptions),
];
cachedCorsMod = corsMod;
return routerMod.createRouter(allRoutes);
}
return {
name: 'sebuf-api',
configureServer(server) {
// Invalidate cached router on HMR updates to server/ files
server.watcher.on('change', (file) => {
if (file.includes('/server/') || file.includes('/src/generated/server/')) {
cachedRouter = null;
}
});
server.middlewares.use(async (req, res, next) => {
// Only intercept sebuf routes: /api/{domain}/v1/*
if (!req.url || !/^\/api\/[a-z]+\/v1\//.test(req.url)) {
return next();
}
try {
// Build router once, reuse across requests (H-13 fix)
if (!cachedRouter) {
cachedRouter = await buildRouter();
}
const router = cachedRouter;
const corsMod = cachedCorsMod;
// Convert Connect IncomingMessage to Web Standard Request
const port = server.config.server.port || 3000;
const url = new URL(req.url, `http://localhost:${port}`);
// Read body for POST requests
let body: string | undefined;
if (req.method === 'POST' || req.method === 'PUT' || req.method === 'PATCH') {
const chunks: Buffer[] = [];
for await (const chunk of req) {
chunks.push(typeof chunk === 'string' ? Buffer.from(chunk) : chunk);
}
body = Buffer.concat(chunks).toString();
}
// Extract headers from IncomingMessage
const headers: Record<string, string> = {};
for (const [key, value] of Object.entries(req.headers)) {
if (typeof value === 'string') {
headers[key] = value;
} else if (Array.isArray(value)) {
headers[key] = value.join(', ');
}
}
const webRequest = new Request(url.toString(), {
method: req.method,
headers,
body: body || undefined,
});
const corsHeaders = corsMod.getCorsHeaders(webRequest);
// OPTIONS preflight
if (req.method === 'OPTIONS') {
res.statusCode = 204;
for (const [key, value] of Object.entries(corsHeaders)) {
res.setHeader(key, value);
}
res.end();
return;
}
// Origin check
if (corsMod.isDisallowedOrigin(webRequest)) {
res.statusCode = 403;
res.setHeader('Content-Type', 'application/json');
for (const [key, value] of Object.entries(corsHeaders)) {
res.setHeader(key, value);
}
res.end(JSON.stringify({ error: 'Origin not allowed' }));
return;
}
// Route matching
const matchedHandler = router.match(webRequest);
if (!matchedHandler) {
res.statusCode = 404;
res.setHeader('Content-Type', 'application/json');
for (const [key, value] of Object.entries(corsHeaders)) {
res.setHeader(key, value);
}
res.end(JSON.stringify({ error: 'Not found' }));
return;
}
// Execute handler
const response = await matchedHandler(webRequest);
// Write response
res.statusCode = response.status;
response.headers.forEach((value, key) => {
res.setHeader(key, value);
});
for (const [key, value] of Object.entries(corsHeaders)) {
res.setHeader(key, value);
}
res.end(await response.text());
} catch (err) {
console.error('[sebuf-api] Error:', err);
res.statusCode = 500;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ error: 'Internal server error' }));
}
});
},
};
}
function youtubeLivePlugin(): Plugin {
return {
name: 'youtube-live',
configureServer(server) {
server.middlewares.use(async (req, res, next) => {
if (!req.url?.startsWith('/api/youtube/live')) {
return next();
}
const url = new URL(req.url, 'http://localhost');
const channel = url.searchParams.get('channel');
if (!channel) {
res.statusCode = 400;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ error: 'Missing channel parameter' }));
return;
}
try {
const channelHandle = channel.startsWith('@') ? channel : `@${channel}`;
const liveUrl = `https://www.youtube.com/${channelHandle}/live`;
const ytRes = await fetch(liveUrl, {
headers: {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
},
redirect: 'follow',
});
if (!ytRes.ok) {
res.setHeader('Content-Type', 'application/json');
res.setHeader('Cache-Control', 'public, max-age=300');
res.end(JSON.stringify({ videoId: null, channel }));
return;
}
const html = await ytRes.text();
// Scope both fields to the same videoDetails block so we don't
// combine a videoId from one object with isLive from another.
let videoId: string | null = null;
const detailsIdx = html.indexOf('"videoDetails"');
if (detailsIdx !== -1) {
const block = html.substring(detailsIdx, detailsIdx + 5000);
const vidMatch = block.match(/"videoId":"([a-zA-Z0-9_-]{11})"/);
const liveMatch = block.match(/"isLive"\s*:\s*true/);
if (vidMatch && liveMatch) {
videoId = vidMatch[1];
}
}
res.setHeader('Content-Type', 'application/json');
res.setHeader('Cache-Control', 'public, max-age=300');
res.end(JSON.stringify({ videoId, isLive: videoId !== null, channel }));
} catch (error) {
console.error(`[YouTube Live] Error:`, error);
res.statusCode = 500;
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ error: 'Failed to fetch', videoId: null }));
}
});
},
};
}
export default defineConfig({
define: {
__APP_VERSION__: JSON.stringify(pkg.version),
},
plugins: [
htmlVariantPlugin(),
polymarketPlugin(),
youtubeLivePlugin(),
sebufApiPlugin(),
brotliPrecompressPlugin(),
VitePWA({
registerType: 'autoUpdate',
injectRegister: false,
includeAssets: [
'favico/favicon.ico',
'favico/apple-touch-icon.png',
'favico/favicon-32x32.png',
],
manifest: {
name: `${activeMeta.siteName} - ${activeMeta.subject}`,
short_name: activeMeta.shortName,
description: activeMeta.description,
start_url: '/',
scope: '/',
display: 'standalone',
orientation: 'any',
theme_color: '#0a0f0a',
background_color: '#0a0f0a',
categories: activeMeta.categories,
icons: [
{ src: '/favico/android-chrome-192x192.png', sizes: '192x192', type: 'image/png' },
{ src: '/favico/android-chrome-512x512.png', sizes: '512x512', type: 'image/png' },
{ src: '/favico/android-chrome-512x512.png', sizes: '512x512', type: 'image/png', purpose: 'maskable' },
],
},
workbox: {
globPatterns: ['**/*.{js,css,ico,png,svg,woff2}'],
globIgnores: ['**/ml*.js', '**/onnx*.wasm', '**/locale-*.js'],
navigateFallback: null,
skipWaiting: true,
clientsClaim: true,
cleanupOutdatedCaches: true,
runtimeCaching: [
{
urlPattern: ({ request }: { request: Request }) => request.mode === 'navigate',
handler: 'NetworkFirst',
options: {
cacheName: 'html-navigation',
networkTimeoutSeconds: 3,
},
},
{
urlPattern: /^https?:\/\/.*\/api\/.*/i,
handler: 'NetworkOnly',
method: 'GET',
},
{
urlPattern: /^https?:\/\/.*\/api\/.*/i,
handler: 'NetworkOnly',
method: 'POST',
},
{
urlPattern: /^https?:\/\/.*\/ingest\/.*/i,
handler: 'NetworkOnly',
method: 'GET',
},
{
urlPattern: /^https?:\/\/.*\/ingest\/.*/i,
handler: 'NetworkOnly',
method: 'POST',
},
{
urlPattern: /^https?:\/\/.*\/rss\/.*/i,
handler: 'NetworkOnly',
method: 'GET',
},
{
urlPattern: /^https:\/\/api\.maptiler\.com\//,
handler: 'CacheFirst',
options: {
cacheName: 'map-tiles',
expiration: { maxEntries: 500, maxAgeSeconds: 30 * 24 * 60 * 60 },
cacheableResponse: { statuses: [0, 200] },
},
},
{
urlPattern: /^https:\/\/[abc]\.basemaps\.cartocdn\.com\//,
handler: 'CacheFirst',
options: {
cacheName: 'carto-tiles',
expiration: { maxEntries: 500, maxAgeSeconds: 30 * 24 * 60 * 60 },
cacheableResponse: { statuses: [0, 200] },
},
},
{
urlPattern: /^https:\/\/fonts\.googleapis\.com\//,
handler: 'StaleWhileRevalidate',
options: {
cacheName: 'google-fonts-css',
expiration: { maxEntries: 10, maxAgeSeconds: 365 * 24 * 60 * 60 },
},
},
{
urlPattern: /^https:\/\/fonts\.gstatic\.com\//,
handler: 'CacheFirst',
options: {
cacheName: 'google-fonts-woff',
expiration: { maxEntries: 30, maxAgeSeconds: 365 * 24 * 60 * 60 },
cacheableResponse: { statuses: [0, 200] },
},
},
{
urlPattern: /\/assets\/locale-.*\.js$/i,
handler: 'CacheFirst',
options: {
cacheName: 'locale-files',
expiration: { maxEntries: 20, maxAgeSeconds: 30 * 24 * 60 * 60 },
cacheableResponse: { statuses: [0, 200] },
},
},
{
urlPattern: /\.(?:png|jpg|jpeg|svg|gif|webp)$/i,
handler: 'StaleWhileRevalidate',
options: {
cacheName: 'images',
expiration: { maxEntries: 100, maxAgeSeconds: 7 * 24 * 60 * 60 },
},
},
],
},
devOptions: {
enabled: false,
},
}),
],
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
},
},
build: {
rollupOptions: {
input: {
main: resolve(__dirname, 'index.html'),
settings: resolve(__dirname, 'settings.html'),
},
output: {
manualChunks(id) {
if (id.includes('node_modules')) {
if (id.includes('/@xenova/transformers/') || id.includes('/onnxruntime-web/')) {
return 'ml';
}
if (id.includes('/@deck.gl/') || id.includes('/maplibre-gl/') || id.includes('/h3-js/')) {
return 'map';
}
if (id.includes('/d3/')) {
return 'd3';
}
if (id.includes('/topojson-client/')) {
return 'topojson';
}
if (id.includes('/i18next')) {
return 'i18n';
}
if (id.includes('/@sentry/')) {
return 'sentry';
}
}
if (id.includes('/src/components/') && id.endsWith('Panel.ts')) {
return 'panels';
}
// Give lazy-loaded locale chunks a recognizable prefix so the
// service worker can exclude them from precache (en.json is
// statically imported into the main bundle).
const localeMatch = id.match(/\/locales\/(\w+)\.json$/);
if (localeMatch && localeMatch[1] !== 'en') {
return `locale-${localeMatch[1]}`;
}
return undefined;
},
},
},
},
server: {
port: 3000,
open: !isE2E,
hmr: isE2E ? false : undefined,
watch: {
ignored: [
'**/test-results/**',
'**/playwright-report/**',
'**/.playwright-mcp/**',
],
},
proxy: {
// Yahoo Finance API
'/api/yahoo': {
target: 'https://query1.finance.yahoo.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api\/yahoo/, ''),
},
// Polymarket handled by polymarketPlugin() — no prod proxy needed
// USGS Earthquake API
'/api/earthquake': {
target: 'https://earthquake.usgs.gov',
changeOrigin: true,
timeout: 30000,
rewrite: (path) => path.replace(/^\/api\/earthquake/, ''),
configure: (proxy) => {
proxy.on('error', (err) => {
console.log('Earthquake proxy error:', err.message);
});
},
},
// PizzINT - Pentagon Pizza Index
'/api/pizzint': {
target: 'https://www.pizzint.watch',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api\/pizzint/, '/api'),
configure: (proxy) => {
proxy.on('error', (err) => {
console.log('PizzINT proxy error:', err.message);
});
},
},
// FRED Economic Data - handled by Vercel serverless function in prod
// In dev, we proxy to the API directly with the key from .env
'/api/fred-data': {
target: 'https://api.stlouisfed.org',
changeOrigin: true,
rewrite: (path) => {
const url = new URL(path, 'http://localhost');
const seriesId = url.searchParams.get('series_id');
const start = url.searchParams.get('observation_start');
const end = url.searchParams.get('observation_end');
const apiKey = process.env.FRED_API_KEY || '';
return `/fred/series/observations?series_id=${seriesId}&api_key=${apiKey}&file_type=json&sort_order=desc&limit=10${start ? `&observation_start=${start}` : ''}${end ? `&observation_end=${end}` : ''}`;
},
},
// RSS Feeds - BBC
'/rss/bbc': {
target: 'https://feeds.bbci.co.uk',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/rss\/bbc/, ''),
},
// RSS Feeds - Guardian
'/rss/guardian': {
target: 'https://www.theguardian.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/rss\/guardian/, ''),
},
// RSS Feeds - NPR
'/rss/npr': {
target: 'https://feeds.npr.org',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/rss\/npr/, ''),
},
// RSS Feeds - AP News
'/rss/apnews': {
target: 'https://rsshub.app/apnews',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/rss\/apnews/, ''),
},
// RSS Feeds - Al Jazeera
'/rss/aljazeera': {
target: 'https://www.aljazeera.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/rss\/aljazeera/, ''),
},
// RSS Feeds - CNN
'/rss/cnn': {
target: 'http://rss.cnn.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/rss\/cnn/, ''),
},
// RSS Feeds - Hacker News
'/rss/hn': {
target: 'https://hnrss.org',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/rss\/hn/, ''),
},
// RSS Feeds - Ars Technica
'/rss/arstechnica': {
target: 'https://feeds.arstechnica.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/rss\/arstechnica/, ''),
},
// RSS Feeds - The Verge
'/rss/verge': {
target: 'https://www.theverge.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/rss\/verge/, ''),
},
// RSS Feeds - CNBC
'/rss/cnbc': {
target: 'https://www.cnbc.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/rss\/cnbc/, ''),
},
// RSS Feeds - MarketWatch
'/rss/marketwatch': {
target: 'https://feeds.marketwatch.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/rss\/marketwatch/, ''),
},
// RSS Feeds - Defense/Intel sources
'/rss/defenseone': {
target: 'https://www.defenseone.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/rss\/defenseone/, ''),
},
'/rss/warontherocks': {
target: 'https://warontherocks.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/rss\/warontherocks/, ''),
},
'/rss/breakingdefense': {
target: 'https://breakingdefense.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/rss\/breakingdefense/, ''),
},
'/rss/bellingcat': {
target: 'https://www.bellingcat.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/rss\/bellingcat/, ''),
},
// RSS Feeds - TechCrunch (layoffs)
'/rss/techcrunch': {
target: 'https://techcrunch.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/rss\/techcrunch/, ''),
},
// Google News RSS
'/rss/googlenews': {
target: 'https://news.google.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/rss\/googlenews/, ''),
},
// AI Company Blogs
'/rss/openai': {
target: 'https://openai.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/rss\/openai/, ''),
},
'/rss/anthropic': {
target: 'https://www.anthropic.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/rss\/anthropic/, ''),
},
'/rss/googleai': {
target: 'https://blog.google',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/rss\/googleai/, ''),
},
'/rss/deepmind': {
target: 'https://deepmind.google',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/rss\/deepmind/, ''),
},
'/rss/huggingface': {
target: 'https://huggingface.co',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/rss\/huggingface/, ''),
},
'/rss/techreview': {
target: 'https://www.technologyreview.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/rss\/techreview/, ''),
},
'/rss/arxiv': {
target: 'https://rss.arxiv.org',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/rss\/arxiv/, ''),
},
// Government
'/rss/whitehouse': {
target: 'https://www.whitehouse.gov',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/rss\/whitehouse/, ''),
},
'/rss/statedept': {
target: 'https://www.state.gov',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/rss\/statedept/, ''),
},
'/rss/state': {
target: 'https://www.state.gov',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/rss\/state/, ''),
},
'/rss/defense': {
target: 'https://www.defense.gov',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/rss\/defense/, ''),
},
'/rss/justice': {
target: 'https://www.justice.gov',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/rss\/justice/, ''),
},
'/rss/cdc': {
target: 'https://tools.cdc.gov',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/rss\/cdc/, ''),
},
'/rss/fema': {
target: 'https://www.fema.gov',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/rss\/fema/, ''),
},
'/rss/dhs': {
target: 'https://www.dhs.gov',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/rss\/dhs/, ''),
},
'/rss/fedreserve': {
target: 'https://www.federalreserve.gov',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/rss\/fedreserve/, ''),
},
'/rss/sec': {
target: 'https://www.sec.gov',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/rss\/sec/, ''),
},
'/rss/treasury': {
target: 'https://home.treasury.gov',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/rss\/treasury/, ''),
},
'/rss/cisa': {
target: 'https://www.cisa.gov',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/rss\/cisa/, ''),
},
// Think Tanks
'/rss/brookings': {
target: 'https://www.brookings.edu',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/rss\/brookings/, ''),
},
'/rss/cfr': {
target: 'https://www.cfr.org',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/rss\/cfr/, ''),
},
'/rss/csis': {
target: 'https://www.csis.org',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/rss\/csis/, ''),
},
// Defense
'/rss/warzone': {
target: 'https://www.thedrive.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/rss\/warzone/, ''),
},
'/rss/defensegov': {
target: 'https://www.defense.gov',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/rss\/defensegov/, ''),
},
// Security
'/rss/krebs': {
target: 'https://krebsonsecurity.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/rss\/krebs/, ''),
},
// Finance
'/rss/yahoonews': {
target: 'https://finance.yahoo.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/rss\/yahoonews/, ''),
},
// Diplomat
'/rss/diplomat': {
target: 'https://thediplomat.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/rss\/diplomat/, ''),
},
// VentureBeat
'/rss/venturebeat': {
target: 'https://venturebeat.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/rss\/venturebeat/, ''),
},
// Foreign Policy
'/rss/foreignpolicy': {
target: 'https://foreignpolicy.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/rss\/foreignpolicy/, ''),
},
// Financial Times
'/rss/ft': {
target: 'https://www.ft.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/rss\/ft/, ''),
},
// Reuters
'/rss/reuters': {
target: 'https://www.reutersagency.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/rss\/reuters/, ''),
},
// Cloudflare Radar - Internet outages
'/api/cloudflare-radar': {
target: 'https://api.cloudflare.com',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api\/cloudflare-radar/, ''),
},
// NGA Maritime Safety Information - Navigation Warnings
'/api/nga-msi': {
target: 'https://msi.nga.mil',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api\/nga-msi/, ''),
},
// GDELT GEO 2.0 API - Global event data
'/api/gdelt': {
target: 'https://api.gdeltproject.org',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api\/gdelt/, ''),
},
// AISStream WebSocket proxy for live vessel tracking
'/ws/aisstream': {
target: 'wss://stream.aisstream.io',
changeOrigin: true,
ws: true,
rewrite: (path) => path.replace(/^\/ws\/aisstream/, ''),
},
// FAA NASSTATUS - Airport delays and closures
'/api/faa': {
target: 'https://nasstatus.faa.gov',
changeOrigin: true,
secure: true,
rewrite: (path) => path.replace(/^\/api\/faa/, ''),
configure: (proxy) => {
proxy.on('error', (err) => {
console.log('FAA NASSTATUS proxy error:', err.message);
});
},
},
// OpenSky Network - Aircraft tracking (military flight detection)
'/api/opensky': {
target: 'https://opensky-network.org/api',
changeOrigin: true,
secure: true,
rewrite: (path) => path.replace(/^\/api\/opensky/, ''),
configure: (proxy) => {
proxy.on('error', (err) => {
console.log('OpenSky proxy error:', err.message);
});
},
},
// ADS-B Exchange - Military aircraft tracking (backup/supplement)
'/api/adsb-exchange': {
target: 'https://adsbexchange.com/api',
changeOrigin: true,
secure: true,
rewrite: (path) => path.replace(/^\/api\/adsb-exchange/, ''),
configure: (proxy) => {
proxy.on('error', (err) => {
console.log('ADS-B Exchange proxy error:', err.message);
});
},
},
},
},
});