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
This commit is contained in:
Elie Habib
2026-02-17 11:30:58 +04:00
parent 367adab9ad
commit a2a59f9d56
35 changed files with 221 additions and 115 deletions
+82 -77
View File
@@ -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);
});
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

+66 -12
View File
@@ -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();
+9
View File
@@ -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();
+12
View File
@@ -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": [
+52 -26
View File
@@ -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',