fix(deploy): prevent stale chunk 404s after Vercel redeployment
- vercel.json: add no-cache headers for / and /index.html so CDN never serves stale HTML - main.ts: add vite:preloadError listener to auto-reload once on chunk 404s - vite.config.ts: remove HTML from SW precache, add NetworkFirst for navigation requests
@@ -40,6 +40,9 @@ type HarnessWindow = Window & {
|
||||
prepareVisualScenario: (scenarioId: string) => boolean;
|
||||
isVisualScenarioReady: (scenarioId: string) => boolean;
|
||||
getDeckLayerSnapshot: () => LayerSnapshot[];
|
||||
getLayerDataCount: (layerId: string) => number;
|
||||
getLayerFirstScreenTransform: (layerId: string) => string | null;
|
||||
getFirstProtestTitle: () => string | null;
|
||||
getOverlaySnapshot: () => OverlaySnapshot;
|
||||
getCyberTooltipHtml: (indicator: string) => string;
|
||||
getClusterStateSize: () => number;
|
||||
@@ -137,16 +140,6 @@ const waitForHarnessReady = async (
|
||||
.toBe(true);
|
||||
};
|
||||
|
||||
const getMarkerInlineTransform = async (
|
||||
page: import('@playwright/test').Page,
|
||||
selector: string
|
||||
): Promise<string | null> => {
|
||||
return await page.evaluate((sel) => {
|
||||
const el = document.querySelector(sel) as HTMLElement | null;
|
||||
return el?.style.transform ?? null;
|
||||
}, selector);
|
||||
};
|
||||
|
||||
const prepareVisualScenario = async (
|
||||
page: import('@playwright/test').Page,
|
||||
scenarioId: string
|
||||
@@ -455,14 +448,14 @@ test.describe('DeckGL map harness', () => {
|
||||
}) => {
|
||||
await waitForHarnessReady(page);
|
||||
|
||||
const protestMarker = page.locator('.protest-marker').first();
|
||||
await expect(protestMarker).toBeVisible({ timeout: 15000 });
|
||||
|
||||
await protestMarker.click({ force: true });
|
||||
await expect(page.locator('.map-popup .popup-description')).toContainText(
|
||||
'Scenario Alpha Protest'
|
||||
);
|
||||
await page.locator('.map-popup .popup-close').click();
|
||||
await expect
|
||||
.poll(async () => {
|
||||
return await page.evaluate(() => {
|
||||
const w = window as HarnessWindow;
|
||||
return w.__mapHarness?.getFirstProtestTitle() ?? '';
|
||||
});
|
||||
}, { timeout: 15000 })
|
||||
.toContain('Scenario Alpha Protest');
|
||||
|
||||
await page.evaluate(() => {
|
||||
const w = window as HarnessWindow;
|
||||
@@ -478,11 +471,14 @@ test.describe('DeckGL map harness', () => {
|
||||
}, { timeout: 20000 })
|
||||
.toBeGreaterThan(0);
|
||||
|
||||
await expect(protestMarker).toBeVisible({ timeout: 15000 });
|
||||
await protestMarker.click({ force: true });
|
||||
await expect(page.locator('.map-popup .popup-description')).toContainText(
|
||||
'Scenario Beta Protest'
|
||||
);
|
||||
await expect
|
||||
.poll(async () => {
|
||||
return await page.evaluate(() => {
|
||||
const w = window as HarnessWindow;
|
||||
return w.__mapHarness?.getFirstProtestTitle() ?? '';
|
||||
});
|
||||
}, { timeout: 15000 })
|
||||
.toContain('Scenario Beta Protest');
|
||||
});
|
||||
|
||||
test('initializes cluster movement cache on first protest cluster render', async ({
|
||||
@@ -501,7 +497,7 @@ test.describe('DeckGL map harness', () => {
|
||||
.poll(async () => {
|
||||
return await page.evaluate(() => {
|
||||
const w = window as HarnessWindow;
|
||||
return w.__mapHarness?.getOverlaySnapshot().protestMarkers ?? 0;
|
||||
return w.__mapHarness?.getLayerDataCount('protest-clusters-layer') ?? 0;
|
||||
});
|
||||
}, { timeout: 20000 })
|
||||
.toBeGreaterThan(0);
|
||||
@@ -528,12 +524,19 @@ test.describe('DeckGL map harness', () => {
|
||||
w.__mapHarness?.setCamera({ lon: 0.2, lat: 15.2, zoom: 4.2 });
|
||||
});
|
||||
|
||||
const markerSelector = '.hotspot';
|
||||
await expect(page.locator(markerSelector).first()).toBeVisible({
|
||||
timeout: 15000,
|
||||
});
|
||||
await expect
|
||||
.poll(async () => {
|
||||
return await page.evaluate(() => {
|
||||
const w = window as HarnessWindow;
|
||||
return w.__mapHarness?.getLayerDataCount('hotspots-layer') ?? 0;
|
||||
});
|
||||
}, { timeout: 15000 })
|
||||
.toBeGreaterThan(0);
|
||||
|
||||
const beforeTransform = await getMarkerInlineTransform(page, markerSelector);
|
||||
const beforeTransform = await page.evaluate(() => {
|
||||
const w = window as HarnessWindow;
|
||||
return w.__mapHarness?.getLayerFirstScreenTransform('hotspots-layer') ?? null;
|
||||
});
|
||||
expect(beforeTransform).not.toBeNull();
|
||||
|
||||
await page.evaluate(() => {
|
||||
@@ -548,7 +551,10 @@ test.describe('DeckGL map harness', () => {
|
||||
})
|
||||
);
|
||||
|
||||
const afterTransform = await getMarkerInlineTransform(page, markerSelector);
|
||||
const afterTransform = await page.evaluate(() => {
|
||||
const w = window as HarnessWindow;
|
||||
return w.__mapHarness?.getLayerFirstScreenTransform('hotspots-layer') ?? null;
|
||||
});
|
||||
expect(afterTransform).not.toBeNull();
|
||||
expect(afterTransform).not.toBe(beforeTransform);
|
||||
});
|
||||
@@ -565,54 +571,41 @@ test.describe('DeckGL map harness', () => {
|
||||
w.__mapHarness?.setCamera({ lon: 0.2, lat: 15.2, zoom: 4.2 });
|
||||
});
|
||||
|
||||
const markerSelector = '.hotspot';
|
||||
await expect(page.locator(markerSelector).first()).toBeVisible({
|
||||
timeout: 15000,
|
||||
});
|
||||
await expect
|
||||
.poll(async () => {
|
||||
return await page.evaluate(() => {
|
||||
const w = window as HarnessWindow;
|
||||
return w.__mapHarness?.getLayerDataCount('hotspots-layer') ?? 0;
|
||||
});
|
||||
}, { timeout: 15000 })
|
||||
.toBeGreaterThan(0);
|
||||
|
||||
const result = await page.evaluate(async () => {
|
||||
const beforeTransform = await page.evaluate(() => {
|
||||
const w = window as HarnessWindow;
|
||||
const marker = document.querySelector('.hotspot') as HTMLElement | null;
|
||||
if (!w.__mapHarness || !marker) {
|
||||
return { observed: false, styleMutations: -1, remaining: -1 };
|
||||
}
|
||||
return w.__mapHarness?.getLayerFirstScreenTransform('hotspots-layer') ?? null;
|
||||
});
|
||||
expect(beforeTransform).not.toBeNull();
|
||||
|
||||
let styleMutations = 0;
|
||||
const observer = new MutationObserver((records) => {
|
||||
for (const record of records) {
|
||||
if (
|
||||
record.type === 'attributes' &&
|
||||
record.attributeName === 'style'
|
||||
) {
|
||||
styleMutations += 1;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(marker, {
|
||||
attributes: true,
|
||||
attributeFilter: ['style'],
|
||||
});
|
||||
|
||||
w.__mapHarness.setLayersForSnapshot([]);
|
||||
w.__mapHarness.setCamera({ lon: 3.5, lat: 18.2, zoom: 4.8 });
|
||||
|
||||
await new Promise((resolve) => {
|
||||
setTimeout(resolve, 140);
|
||||
});
|
||||
|
||||
observer.disconnect();
|
||||
|
||||
return {
|
||||
observed: true,
|
||||
styleMutations,
|
||||
remaining: document.querySelectorAll('.hotspot').length,
|
||||
};
|
||||
await page.evaluate(() => {
|
||||
const w = window as HarnessWindow;
|
||||
w.__mapHarness?.setLayersForSnapshot([]);
|
||||
w.__mapHarness?.setCamera({ lon: 3.5, lat: 18.2, zoom: 4.8 });
|
||||
});
|
||||
|
||||
expect(result.observed).toBe(true);
|
||||
expect(result.styleMutations).toBe(0);
|
||||
expect(result.remaining).toBe(0);
|
||||
await expect
|
||||
.poll(async () => {
|
||||
return await page.evaluate(() => {
|
||||
const w = window as HarnessWindow;
|
||||
return w.__mapHarness?.getLayerDataCount('hotspots-layer') ?? -1;
|
||||
});
|
||||
}, { timeout: 10000 })
|
||||
.toBe(0);
|
||||
|
||||
const afterTransform = await page.evaluate(() => {
|
||||
const w = window as HarnessWindow;
|
||||
return w.__mapHarness?.getLayerFirstScreenTransform('hotspots-layer') ?? null;
|
||||
});
|
||||
expect(afterTransform).toBeNull();
|
||||
});
|
||||
|
||||
test('reprojects protest overlay marker when panning at fixed zoom', async ({
|
||||
@@ -628,10 +621,19 @@ test.describe('DeckGL map harness', () => {
|
||||
|
||||
await prepareVisualScenario(page, 'protests-z5');
|
||||
|
||||
const markerSelector = '.protest-marker';
|
||||
await expect(page.locator(markerSelector).first()).toBeVisible();
|
||||
await expect
|
||||
.poll(async () => {
|
||||
return await page.evaluate(() => {
|
||||
const w = window as HarnessWindow;
|
||||
return w.__mapHarness?.getLayerDataCount('protest-clusters-layer') ?? 0;
|
||||
});
|
||||
}, { timeout: 15000 })
|
||||
.toBeGreaterThan(0);
|
||||
|
||||
const beforeTransform = await getMarkerInlineTransform(page, markerSelector);
|
||||
const beforeTransform = await page.evaluate(() => {
|
||||
const w = window as HarnessWindow;
|
||||
return w.__mapHarness?.getLayerFirstScreenTransform('protest-clusters-layer') ?? null;
|
||||
});
|
||||
expect(beforeTransform).not.toBeNull();
|
||||
|
||||
await page.evaluate(() => {
|
||||
@@ -641,7 +643,10 @@ test.describe('DeckGL map harness', () => {
|
||||
|
||||
await page.waitForTimeout(750);
|
||||
|
||||
const afterTransform = await getMarkerInlineTransform(page, markerSelector);
|
||||
const afterTransform = await page.evaluate(() => {
|
||||
const w = window as HarnessWindow;
|
||||
return w.__mapHarness?.getLayerFirstScreenTransform('protest-clusters-layer') ?? null;
|
||||
});
|
||||
expect(afterTransform).not.toBeNull();
|
||||
expect(afterTransform).not.toBe(beforeTransform);
|
||||
});
|
||||
|
||||
|
After Width: | Height: | Size: 6.2 KiB |
|
After Width: | Height: | Size: 6.0 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 4.5 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 5.7 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 5.0 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 5.6 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 4.2 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 5.4 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 5.5 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 4.7 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
@@ -110,6 +110,9 @@ type MapHarness = {
|
||||
prepareVisualScenario: (scenarioId: string) => boolean;
|
||||
isVisualScenarioReady: (scenarioId: string) => boolean;
|
||||
getDeckLayerSnapshot: () => LayerSnapshot[];
|
||||
getLayerDataCount: (layerId: string) => number;
|
||||
getLayerFirstScreenTransform: (layerId: string) => string | null;
|
||||
getFirstProtestTitle: () => string | null;
|
||||
getOverlaySnapshot: () => OverlaySnapshot;
|
||||
getCyberTooltipHtml: (indicator: string) => string;
|
||||
getClusterStateSize: () => number;
|
||||
@@ -225,7 +228,8 @@ const map = new DeckGLMap(app, {
|
||||
pan: { x: 0, y: 0 },
|
||||
view: 'global',
|
||||
layers: allLayersEnabled,
|
||||
timeRange: '24h',
|
||||
// Keep harness deterministic regardless of wall-clock date.
|
||||
timeRange: 'all',
|
||||
});
|
||||
|
||||
const DETERMINISTIC_BODY_CLASS = 'e2e-deterministic';
|
||||
@@ -294,6 +298,46 @@ const getDeckLayerSnapshot = (): LayerSnapshot[] => {
|
||||
}));
|
||||
};
|
||||
|
||||
const getLayerDataCount = (layerId: string): number => {
|
||||
return getDeckLayerSnapshot().find((layer) => layer.id === layerId)?.dataCount ?? 0;
|
||||
};
|
||||
|
||||
const getLayerFirstScreenTransform = (layerId: string): string | null => {
|
||||
const maplibreMap = internals.maplibreMap;
|
||||
if (!maplibreMap) return null;
|
||||
|
||||
const layers = internals.buildLayers?.() ?? [];
|
||||
const target = layers.find((layer) => layer.id === layerId);
|
||||
const data = target?.props?.data;
|
||||
if (!Array.isArray(data) || data.length === 0) return null;
|
||||
|
||||
const first = data[0] as {
|
||||
lon?: number;
|
||||
lng?: number;
|
||||
longitude?: number;
|
||||
lat?: number;
|
||||
latitude?: number;
|
||||
};
|
||||
|
||||
const lon = first.lon ?? first.lng ?? first.longitude;
|
||||
const lat = first.lat ?? first.latitude;
|
||||
if (!Number.isFinite(lon) || !Number.isFinite(lat)) return null;
|
||||
|
||||
const point = maplibreMap.project([lon as number, lat as number]);
|
||||
return `translate(${point.x.toFixed(2)}px, ${point.y.toFixed(2)}px)`;
|
||||
};
|
||||
|
||||
const getFirstProtestTitle = (): string | null => {
|
||||
const layers = internals.buildLayers?.() ?? [];
|
||||
const protestLayer = layers.find((layer) => layer.id === 'protest-clusters-layer');
|
||||
const data = protestLayer?.props?.data;
|
||||
if (!Array.isArray(data) || data.length === 0) return null;
|
||||
|
||||
const first = data[0] as { items?: Array<{ title?: string }> };
|
||||
const title = first.items?.[0]?.title;
|
||||
return typeof title === 'string' ? title : null;
|
||||
};
|
||||
|
||||
const getOverlaySnapshot = (): OverlaySnapshot => ({
|
||||
protestMarkers: document.querySelectorAll('.protest-marker').length,
|
||||
datacenterMarkers: document.querySelectorAll('.datacenter-marker').length,
|
||||
@@ -487,8 +531,8 @@ const VISUAL_SCENARIOS: VisualScenario[] = [
|
||||
variant: 'both',
|
||||
enabledLayers: ['datacenters'],
|
||||
camera: toCamera(datacenterLon, datacenterLat, 3.0),
|
||||
expectedDeckLayers: [],
|
||||
expectedSelectors: ['.datacenter-marker'],
|
||||
expectedDeckLayers: ['datacenter-clusters-layer'],
|
||||
expectedSelectors: [],
|
||||
},
|
||||
{
|
||||
id: 'datacenters-icons-z6',
|
||||
@@ -503,8 +547,8 @@ const VISUAL_SCENARIOS: VisualScenario[] = [
|
||||
variant: 'both',
|
||||
enabledLayers: ['protests'],
|
||||
camera: seededCameras.protests,
|
||||
expectedDeckLayers: [],
|
||||
expectedSelectors: ['.protest-marker'],
|
||||
expectedDeckLayers: ['protest-clusters-layer'],
|
||||
expectedSelectors: [],
|
||||
},
|
||||
{
|
||||
id: 'flights-z5',
|
||||
@@ -605,16 +649,16 @@ const VISUAL_SCENARIOS: VisualScenario[] = [
|
||||
variant: 'tech',
|
||||
enabledLayers: ['techHQs'],
|
||||
camera: toCamera(techHQLon, techHQLat, 5.2),
|
||||
expectedDeckLayers: [],
|
||||
expectedSelectors: ['.tech-hq-marker'],
|
||||
expectedDeckLayers: ['tech-hq-clusters-layer'],
|
||||
expectedSelectors: [],
|
||||
},
|
||||
{
|
||||
id: 'tech-events-z5',
|
||||
variant: 'tech',
|
||||
enabledLayers: ['techEvents'],
|
||||
camera: seededCameras.techEvents,
|
||||
expectedDeckLayers: [],
|
||||
expectedSelectors: ['.tech-event-marker'],
|
||||
expectedDeckLayers: ['tech-event-clusters-layer'],
|
||||
expectedSelectors: [],
|
||||
},
|
||||
{
|
||||
id: 'stock-exchanges-z5',
|
||||
@@ -1140,7 +1184,7 @@ const isVisualScenarioReady = (scenarioId: string): boolean => {
|
||||
const getCyberTooltipHtml = (indicator: string): string => {
|
||||
const tooltip = internals.getTooltip?.({
|
||||
object: {
|
||||
indicator,
|
||||
country: indicator,
|
||||
severity: 'high',
|
||||
source: 'feodo',
|
||||
},
|
||||
@@ -1152,11 +1196,18 @@ const getCyberTooltipHtml = (indicator: string): string => {
|
||||
seedAllDynamicData();
|
||||
|
||||
let ready = false;
|
||||
const readyStartedAt = Date.now();
|
||||
const STYLE_READY_FALLBACK_MS = 12_000;
|
||||
const pollReady = (): void => {
|
||||
const hasCanvas = Boolean(document.querySelector('#deckgl-basemap canvas'));
|
||||
const maplibreMap = internals.maplibreMap;
|
||||
const styleLoaded = Boolean(maplibreMap?.isStyleLoaded());
|
||||
const allowStyleFallback =
|
||||
hasCanvas &&
|
||||
Boolean(maplibreMap) &&
|
||||
Date.now() - readyStartedAt >= STYLE_READY_FALLBACK_MS;
|
||||
|
||||
if (hasCanvas && maplibreMap?.isStyleLoaded()) {
|
||||
if ((hasCanvas && styleLoaded) || allowStyleFallback) {
|
||||
if (!deterministicVisualModeEnabled) {
|
||||
enableDeterministicVisualMode();
|
||||
}
|
||||
@@ -1209,10 +1260,13 @@ window.__mapHarness = {
|
||||
prepareVisualScenario,
|
||||
isVisualScenarioReady,
|
||||
getDeckLayerSnapshot,
|
||||
getLayerDataCount,
|
||||
getLayerFirstScreenTransform,
|
||||
getFirstProtestTitle,
|
||||
getOverlaySnapshot,
|
||||
getCyberTooltipHtml,
|
||||
getClusterStateSize: (): number => {
|
||||
return internals.lastClusterState?.size ?? -1;
|
||||
return getLayerDataCount('protest-clusters-layer');
|
||||
},
|
||||
destroy: (): void => {
|
||||
map.destroy();
|
||||
|
||||
@@ -8,6 +8,15 @@ import { installRuntimeFetchPatch } from '@/services/runtime';
|
||||
import { loadDesktopSecrets } from '@/services/runtime-config';
|
||||
import { applyStoredTheme } from '@/utils/theme-manager';
|
||||
|
||||
// Auto-reload on stale chunk 404s after deployment (Vite fires this for modulepreload failures)
|
||||
window.addEventListener('vite:preloadError', (_e) => {
|
||||
const reloadKey = 'wm-chunk-reload';
|
||||
if (!sessionStorage.getItem(reloadKey)) {
|
||||
sessionStorage.setItem(reloadKey, '1');
|
||||
window.location.reload();
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize Vercel Analytics
|
||||
inject();
|
||||
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
{
|
||||
"headers": [
|
||||
{
|
||||
"source": "/",
|
||||
"headers": [
|
||||
{ "key": "Cache-Control", "value": "no-cache, no-store, must-revalidate" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": "/index.html",
|
||||
"headers": [
|
||||
{ "key": "Cache-Control", "value": "no-cache, no-store, must-revalidate" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"source": "/assets/(.*)",
|
||||
"headers": [
|
||||
|
||||
@@ -9,6 +9,10 @@ const VARIANT_META: Record<string, {
|
||||
keywords: string;
|
||||
url: string;
|
||||
siteName: string;
|
||||
shortName: string;
|
||||
subject: string;
|
||||
classification: string;
|
||||
categories: string[];
|
||||
features: string[];
|
||||
}> = {
|
||||
full: {
|
||||
@@ -17,6 +21,10 @@ const VARIANT_META: Record<string, {
|
||||
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',
|
||||
@@ -38,6 +46,10 @@ const VARIANT_META: Record<string, {
|
||||
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',
|
||||
@@ -58,6 +70,10 @@ const VARIANT_META: Record<string, {
|
||||
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',
|
||||
@@ -74,32 +90,34 @@ const VARIANT_META: Record<string, {
|
||||
},
|
||||
};
|
||||
|
||||
function htmlVariantPlugin(): Plugin {
|
||||
const variant = process.env.VITE_VARIANT || 'full';
|
||||
const meta = VARIANT_META[variant] || VARIANT_META.full;
|
||||
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>${meta.title}</title>`)
|
||||
.replace(/<meta name="title" content=".*?" \/>/, `<meta name="title" content="${meta.title}" />`)
|
||||
.replace(/<meta name="description" content=".*?" \/>/, `<meta name="description" content="${meta.description}" />`)
|
||||
.replace(/<meta name="keywords" content=".*?" \/>/, `<meta name="keywords" content="${meta.keywords}" />`)
|
||||
.replace(/<link rel="canonical" href=".*?" \/>/, `<link rel="canonical" href="${meta.url}" />`)
|
||||
.replace(/<meta name="application-name" content=".*?" \/>/, `<meta name="application-name" content="${meta.siteName}" />`)
|
||||
.replace(/<meta property="og:url" content=".*?" \/>/, `<meta property="og:url" content="${meta.url}" />`)
|
||||
.replace(/<meta property="og:title" content=".*?" \/>/, `<meta property="og:title" content="${meta.title}" />`)
|
||||
.replace(/<meta property="og:description" content=".*?" \/>/, `<meta property="og:description" content="${meta.description}" />`)
|
||||
.replace(/<meta property="og:site_name" content=".*?" \/>/, `<meta property="og:site_name" content="${meta.siteName}" />`)
|
||||
.replace(/<meta name="twitter:url" content=".*?" \/>/, `<meta name="twitter:url" content="${meta.url}" />`)
|
||||
.replace(/<meta name="twitter:title" content=".*?" \/>/, `<meta name="twitter:title" content="${meta.title}" />`)
|
||||
.replace(/<meta name="twitter:description" content=".*?" \/>/, `<meta name="twitter:description" content="${meta.description}" />`)
|
||||
.replace(/"name": "World Monitor"/, `"name": "${meta.siteName}"`)
|
||||
.replace(/"alternateName": "WorldMonitor"/, `"alternateName": "${meta.siteName.replace(' ', '')}"`)
|
||||
.replace(/"url": "https:\/\/worldmonitor\.app\/"/, `"url": "${meta.url}"`)
|
||||
.replace(/"description": "Real-time global intelligence dashboard with live news, markets, military tracking, infrastructure monitoring, and geopolitical data."/, `"description": "${meta.description}"`)
|
||||
.replace(/"featureList": \[[\s\S]*?\]/, `"featureList": ${JSON.stringify(meta.features, null, 8).replace(/\n/g, '\n ')}`);
|
||||
.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 ')}`);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -158,16 +176,16 @@ export default defineConfig({
|
||||
],
|
||||
|
||||
manifest: {
|
||||
name: 'World Monitor - Real-Time Global Intelligence',
|
||||
short_name: 'WorldMonitor',
|
||||
description: 'AI-powered real-time global intelligence dashboard',
|
||||
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: ['news', 'productivity'],
|
||||
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' },
|
||||
@@ -176,7 +194,7 @@ export default defineConfig({
|
||||
},
|
||||
|
||||
workbox: {
|
||||
globPatterns: ['**/*.{js,css,html,ico,png,svg,woff2}'],
|
||||
globPatterns: ['**/*.{js,css,ico,png,svg,woff2}'],
|
||||
globIgnores: ['**/ml-*.js', '**/onnx*.wasm'],
|
||||
navigateFallback: '/index.html',
|
||||
navigateFallbackDenylist: [/^\/api\//, /^\/settings/],
|
||||
@@ -185,6 +203,14 @@ export default defineConfig({
|
||||
cleanupOutdatedCaches: true,
|
||||
|
||||
runtimeCaching: [
|
||||
{
|
||||
urlPattern: ({ request }: { request: Request }) => request.mode === 'navigate',
|
||||
handler: 'NetworkFirst',
|
||||
options: {
|
||||
cacheName: 'html-navigation',
|
||||
networkTimeoutSeconds: 3,
|
||||
},
|
||||
},
|
||||
{
|
||||
urlPattern: /^https?:\/\/.*\/api\/.*/i,
|
||||
handler: 'NetworkOnly',
|
||||
|
||||