On a low-end laptop the globe (mapbox-gl + deck.gl ≈ 2.7 MB) was eagerly modulepreloaded and constructed synchronously during renderLayout, so nothing painted until ~4.1 MB of JS downloaded + parsed + WebGL initialised (≈17.7 s to app-shell under 6× CPU throttle on the live build). This code-splits the map and degrades gracefully for weak hardware. - Map loads via dynamic import() in App.mountMap(); init awaits it AFTER the shell paints, before map-dependent wiring. Every this.map access was already null-guarded, and mountMap replays the initEscalation / onTimeRangeChanged / initial-URL-state wiring that used to run inline in createPanels. - Isolate Vite's __vitePreload helper into its own chunk (manualChunks): it had been co-located INTO the 'map' chunk, so the entry statically imported 'map' just for that helper and Vite modulepreloaded the whole 2.7 MB before paint — THE reason the map stayed eager despite the dynamic import. - @/components barrel re-exports the map classes as TYPES only (their modules have top-level side effects; value re-exports also dragged 'map' eager). - Defer @sentry/browser (~460 KB raw / ~130 KB gzip) out of the entry to requestIdleCallback, with a synchronous early-error buffer replayed on init so no boot error is lost. - New device-tier util (hardwareConcurrency + deviceMemory + reduced-motion, ?tier= override): low-end drops the deck.gl overlay DPR 2→1, skips the idle globe auto-spin, and scales real-time refresh intervals up to 2×. Eager JS before first paint: 4136 KB → 1440 KB (raw). Map (2.7 MB) + Sentry now load after first paint.
885 lines
34 KiB
TypeScript
885 lines
34 KiB
TypeScript
import { defineConfig, type Plugin } from 'vite';
|
|
import { VitePWA } from 'vite-plugin-pwa';
|
|
import { resolve } from 'path';
|
|
import pkg from './package.json';
|
|
|
|
const isE2E = process.env.VITE_E2E === '1';
|
|
|
|
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: 'Hanzo World — Real-Time Global Intelligence',
|
|
description: 'Hanzo World — AI-powered real-time global intelligence dashboard: live news, markets, strategic posture, infrastructure and geopolitical data in one view.',
|
|
keywords: 'Hanzo, global intelligence, geopolitical dashboard, world news, market data, strategic posture, real-time monitoring, situation awareness, prediction markets',
|
|
url: 'https://world.hanzo.ai/',
|
|
siteName: 'Hanzo World',
|
|
shortName: 'HanzoWorld',
|
|
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',
|
|
],
|
|
},
|
|
saas: {
|
|
title: 'Hanzo Cloud - Live SaaS Platform Metrics & Usage',
|
|
description: 'Live Hanzo Cloud metrics: platform requests, models served, fleet nodes and GPUs across regions, plus your org usage and bill drill-down.',
|
|
keywords: 'hanzo cloud, saas metrics, ai inference, model usage, gpu fleet, cloud regions, request volume, usage dashboard, billing, api.hanzo.ai',
|
|
url: 'https://world.hanzo.ai/?variant=saas',
|
|
siteName: 'Hanzo Cloud',
|
|
shortName: 'HanzoCloud',
|
|
subject: 'Hanzo Cloud Platform Metrics and Org Usage',
|
|
classification: 'Cloud Dashboard, SaaS Metrics, AI Infrastructure',
|
|
categories: ['business', 'productivity'],
|
|
features: [
|
|
'Platform request volume',
|
|
'Models served',
|
|
'Fleet nodes & GPUs',
|
|
'Region mapping',
|
|
'Live request-rate ticker',
|
|
'Org usage & bill drill-down',
|
|
],
|
|
},
|
|
ai: {
|
|
title: 'AI Monitor - Real-Time AI, ML & Compute Dashboard',
|
|
description: 'Real-time AI dashboard tracking AI/ML news, research, model and inference markets, chips, cloud infrastructure, datacenters, policy and AI prediction markets worldwide.',
|
|
keywords: 'AI dashboard, machine learning, LLM news, AI research, arxiv, inference, GPU, datacenters, cloud regions, AI policy, AI regulation, model releases, OpenAI, Anthropic, semiconductors, AI predictions',
|
|
url: 'https://world.hanzo.ai/?variant=ai',
|
|
siteName: 'AI Monitor',
|
|
shortName: 'AIMonitor',
|
|
subject: 'AI, Machine Learning, and Compute Infrastructure Intelligence',
|
|
classification: 'AI Dashboard, ML Tracker, Compute Intelligence',
|
|
categories: ['news', 'business'],
|
|
features: [
|
|
'AI/ML news aggregation',
|
|
'AI research feeds (ArXiv)',
|
|
'Chips & inference tracking',
|
|
'Cloud & datacenter mapping',
|
|
'AI policy & regulation',
|
|
'AI prediction markets',
|
|
'GitHub trending',
|
|
'Tech layoff tracking',
|
|
],
|
|
},
|
|
fund: {
|
|
title: 'Lux Macro — Real-Time Rotation Terminal',
|
|
description: 'Lux Fund macro terminal: a 3D-globe rotation model reading capital flows between AI, semis and the energy complex in real time, with the Lux Book — the top-10 model allocation, rebalanced live.',
|
|
keywords: 'macro dashboard, sector rotation, relative rotation graph, RRG, AI trade, semiconductors, energy, natural gas, uranium, nuclear power, model allocation, market radar, macro research, fund positioning, Lux',
|
|
url: 'https://lux.fund/',
|
|
siteName: 'Lux Macro',
|
|
shortName: 'LuxMacro',
|
|
subject: 'Macro Rotation Intelligence and Model Allocation',
|
|
classification: 'Macro Dashboard, Rotation Model, Fund Positioning',
|
|
categories: ['finance', 'news'],
|
|
features: [
|
|
'3D-globe macro terminal',
|
|
'Relative rotation graph (RRG)',
|
|
'AI → energy rotation detection',
|
|
'The Lux Book — top-10 model allocation',
|
|
'Commodity & energy hub mapping',
|
|
'Real-time market radar',
|
|
'Central bank & macro monitoring',
|
|
'Investment news over the globe',
|
|
],
|
|
},
|
|
crypto: {
|
|
title: 'Crypto Monitor - Real-Time Digital Assets & Markets Dashboard',
|
|
description: 'Real-time crypto dashboard tracking CoinGecko prices, BTC dominance and funding, ETF flows, stablecoins, market radar, sector heatmap, sentiment and prediction markets worldwide.',
|
|
keywords: 'crypto dashboard, bitcoin, ethereum, digital assets, BTC ETF, stablecoins, DeFi, funding rates, BTC dominance, crypto news, coingecko, market radar, sector heatmap, crypto regulation, prediction markets',
|
|
url: 'https://world.hanzo.ai/?variant=crypto',
|
|
siteName: 'Crypto Monitor',
|
|
shortName: 'CryptoMonitor',
|
|
subject: 'Crypto, Digital Assets, and Market Intelligence',
|
|
classification: 'Crypto Dashboard, Digital Asset Tracker, Market Intelligence',
|
|
categories: ['finance', 'news'],
|
|
features: [
|
|
'Live CoinGecko prices',
|
|
'BTC ETF flow tracking',
|
|
'Stablecoin monitoring',
|
|
'Trader desk (dominance & funding)',
|
|
'Market radar signals',
|
|
'Sector heatmap',
|
|
'Crypto news & regulation',
|
|
'Crypto prediction markets',
|
|
],
|
|
},
|
|
};
|
|
|
|
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 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 {
|
|
// Use YouTube's oEmbed to check if a video is valid/live
|
|
// For now, return null to use fallback - will implement proper detection later
|
|
res.setHeader('Content-Type', 'application/json');
|
|
res.setHeader('Cache-Control', 'public, max-age=300');
|
|
res.end(JSON.stringify({ 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(),
|
|
youtubeLivePlugin(),
|
|
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,html,ico,png,svg,woff2}'],
|
|
// index.html (+ the settings shell) are deliberately NOT precached. With
|
|
// several releases a day, a precached shell served a stale index.html that
|
|
// pointed at hashed bundles the newer deploy had already replaced — the app
|
|
// flashed the old build, then went black as old and new chunks mixed. They
|
|
// are served network-first (runtimeCaching below) instead; only immutable,
|
|
// content-hashed assets live in the precache, so an old-HTML/new-chunk mix
|
|
// is impossible. offline.html stays precached as the sole offline shell.
|
|
globIgnores: ['**/ml-*.js', '**/onnx*.wasm', 'index.html', 'settings.html'],
|
|
// mapbox-gl v3 pushes the map chunk past workbox's 2 MiB default; precache
|
|
// it so the offline shell still loads the renderer.
|
|
maximumFileSizeToCacheInBytes: 3 * 1024 * 1024,
|
|
// Disable the navigation fallback (vite-plugin-pwa defaults it to
|
|
// 'index.html'). A fallback is precache/cache-first and would reintroduce
|
|
// the stale-shell flash; instead navigations go network-first (runtimeCaching
|
|
// below) and the Go/edge server returns index.html for client-routed paths.
|
|
// `null` is the workbox-build sentinel for "no navigation fallback".
|
|
navigateFallback: null,
|
|
skipWaiting: true,
|
|
clientsClaim: true,
|
|
cleanupOutdatedCaches: true,
|
|
|
|
runtimeCaching: [
|
|
// Data plane + discovery docs: always straight to network, never cached
|
|
// or intercepted by the SW. Registered first so navigations to these
|
|
// paths never fall through to the HTML handler below.
|
|
{
|
|
urlPattern: /^https?:\/\/.*\/v1\/world\//i,
|
|
handler: 'NetworkOnly',
|
|
},
|
|
{
|
|
urlPattern: /^https?:\/\/.*\/\.well-known\//i,
|
|
handler: 'NetworkOnly',
|
|
},
|
|
{
|
|
urlPattern: /^https?:\/\/.*\/api\/.*/i,
|
|
handler: 'NetworkOnly',
|
|
},
|
|
{
|
|
urlPattern: /^https?:\/\/.*\/rss\/.*/i,
|
|
handler: 'NetworkOnly',
|
|
},
|
|
// HTML shell: network-first so the served markup always names the current
|
|
// hashed bundles; falls back to the last good copy only when offline.
|
|
{
|
|
urlPattern: ({ request }: { request: Request }) => request.mode === 'navigate',
|
|
handler: 'NetworkFirst',
|
|
options: {
|
|
cacheName: 'html-navigation',
|
|
networkTimeoutSeconds: 3,
|
|
},
|
|
},
|
|
{
|
|
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: /\.(?: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'),
|
|
},
|
|
},
|
|
// Pre-bundle the deck.gl / luma.gl / mapbox graph at server start. These are
|
|
// heavy, deeply-nested deps; left to on-demand discovery vite finds a leaf like
|
|
// @luma.gl/webgl's `webgl-device` only when the globe first imports it mid-load,
|
|
// then re-optimizes and force-reloads. That reload races GlobeNative's `new Deck()`
|
|
// on a cold dev/e2e server, so the 3D globe intermittently never initializes (its
|
|
// `__globeNative` hook never appears and the render e2e times out at go3D). Listing
|
|
// the graph here — including the transitive luma leaves that carry the device
|
|
// adapter — makes the optimize pass complete before the first page load, so a
|
|
// cold-start dev server and the globe e2e are deterministic. Prod is unaffected
|
|
// (a production build bundles with Rollup and never runs the dep optimizer).
|
|
optimizeDeps: {
|
|
include: [
|
|
'deck.gl',
|
|
'@deck.gl/core',
|
|
'@deck.gl/layers',
|
|
'@deck.gl/geo-layers',
|
|
'@deck.gl/mesh-layers',
|
|
'@deck.gl/aggregation-layers',
|
|
'@deck.gl/mapbox',
|
|
'@luma.gl/core',
|
|
'@luma.gl/engine',
|
|
'@luma.gl/webgl',
|
|
'mapbox-gl',
|
|
],
|
|
},
|
|
build: {
|
|
rollupOptions: {
|
|
input: {
|
|
main: resolve(__dirname, 'index.html'),
|
|
settings: resolve(__dirname, 'settings.html'),
|
|
},
|
|
output: {
|
|
manualChunks(id) {
|
|
// Vite's __vitePreload helper is a shared runtime module used by every
|
|
// dynamic import(). Left alone, Rollup co-locates it into the 'map'
|
|
// manual chunk (a dynamic-import target), so the ENTRY statically
|
|
// imports 'map' just for that one helper — and Vite then modulepreloads
|
|
// the whole ~2.7 MB deck.gl + mapbox chunk before first paint. Pin the
|
|
// helper to its own tiny chunk so the entry never drags 'map' eagerly;
|
|
// the map then loads lazily via mountMap()'s dynamic import.
|
|
if (id.includes('vite/preload-helper')) {
|
|
return 'vite-preload';
|
|
}
|
|
if (id.includes('node_modules')) {
|
|
if (id.includes('/@xenova/transformers/') || id.includes('/onnxruntime-web/')) {
|
|
return 'ml';
|
|
}
|
|
if (id.includes('/@deck.gl/') || id.includes('/mapbox-gl/') || id.includes('/maplibre-gl/') || id.includes('/h3-js/')) {
|
|
return 'map';
|
|
}
|
|
if (id.includes('/d3/')) {
|
|
return 'd3';
|
|
}
|
|
if (id.includes('/topojson-client/')) {
|
|
return 'topojson';
|
|
}
|
|
}
|
|
return undefined;
|
|
},
|
|
},
|
|
},
|
|
},
|
|
server: {
|
|
port: 3000,
|
|
open: !isE2E,
|
|
hmr: isE2E ? false : undefined,
|
|
watch: {
|
|
ignored: [
|
|
'**/test-results/**',
|
|
'**/playwright-report/**',
|
|
'**/.playwright-mcp/**',
|
|
],
|
|
},
|
|
proxy: {
|
|
// Same-origin data backend (/v1/world/*): npm run dev has no local Go
|
|
// server, so proxy to production (override with VITE_DEV_API_PROXY to
|
|
// point at a local `go run ./cmd/world`).
|
|
'/v1': {
|
|
target: process.env.VITE_DEV_API_PROXY || 'https://world.hanzo.ai',
|
|
changeOrigin: true,
|
|
},
|
|
// Yahoo Finance API
|
|
'/api/yahoo': {
|
|
target: 'https://query1.finance.yahoo.com',
|
|
changeOrigin: true,
|
|
rewrite: (path) => path.replace(/^\/api\/yahoo/, ''),
|
|
},
|
|
// CoinGecko API
|
|
'/api/coingecko': {
|
|
target: 'https://api.coingecko.com',
|
|
changeOrigin: true,
|
|
rewrite: (path) => {
|
|
const idx = path.indexOf('?');
|
|
const qs = idx >= 0 ? path.substring(idx) : '';
|
|
const params = new URLSearchParams(qs);
|
|
if (params.get('endpoint') === 'markets') {
|
|
params.delete('endpoint');
|
|
const vs = params.get('vs_currencies') || 'usd';
|
|
params.delete('vs_currencies');
|
|
params.set('vs_currency', vs);
|
|
params.set('sparkline', 'true');
|
|
params.set('order', 'market_cap_desc');
|
|
return `/api/v3/coins/markets?${params.toString()}`;
|
|
}
|
|
return `/api/v3/simple/price${qs}`;
|
|
},
|
|
},
|
|
// Polymarket API — proxy through production Vercel edge function
|
|
// Direct gamma-api.polymarket.com is blocked by Cloudflare JA3 fingerprinting
|
|
'/api/polymarket': {
|
|
target: 'https://worldmonitor.app',
|
|
changeOrigin: true,
|
|
configure: (proxy) => {
|
|
proxy.on('error', (err) => {
|
|
console.log('Polymarket proxy error:', err.message);
|
|
});
|
|
},
|
|
},
|
|
// 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/, ''),
|
|
},
|
|
// ACLED - Armed Conflict Location & Event Data (protests, riots)
|
|
'/api/acled': {
|
|
target: 'https://acleddata.com',
|
|
changeOrigin: true,
|
|
rewrite: (path) => path.replace(/^\/api\/acled/, ''),
|
|
},
|
|
// GDELT GEO 2.0 API - Geolocation endpoint (must come before /api/gdelt)
|
|
'/api/gdelt-geo': {
|
|
target: 'https://api.gdeltproject.org',
|
|
changeOrigin: true,
|
|
rewrite: (path) => path.replace(/^\/api\/gdelt-geo/, '/api/v2/geo/geo'),
|
|
},
|
|
// 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);
|
|
});
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|