diff --git a/vehicle-data-platform/apps/web/index.html b/vehicle-data-platform/apps/web/index.html index 83c5c03e..dc84da70 100644 --- a/vehicle-data-platform/apps/web/index.html +++ b/vehicle-data-platform/apps/web/index.html @@ -4,9 +4,55 @@ 车辆数据管理中台 + -
+
车辆数据中台正在加载工作台…
+ + diff --git a/vehicle-data-platform/apps/web/scripts/verify-build.mjs b/vehicle-data-platform/apps/web/scripts/verify-build.mjs index da7eb9ad..1f07add1 100644 --- a/vehicle-data-platform/apps/web/scripts/verify-build.mjs +++ b/vehicle-data-platform/apps/web/scripts/verify-build.mjs @@ -8,6 +8,7 @@ const mileageWorkers = assets.filter((name) => /^mileageExport\.worker-[\w-]+\.j const monitorAssets = assets.filter((name) => /^MonitorPage-[\w-]+\.js$/.test(name)); const runtimeConfig = readFileSync(resolve(process.cwd(), 'dist/app-config.js'), 'utf8'); const safeRuntimeTemplate = readFileSync(resolve(process.cwd(), 'public/app-config.example.js'), 'utf8'); +const builtIndex = readFileSync(resolve(process.cwd(), 'dist/index.html'), 'utf8'); const releaseManifest = readFileSync(resolve(process.cwd(), 'dist/.release-assets'), 'utf8').split(/\r?\n/).filter(Boolean); if (excelAssets.length !== 1) { @@ -22,6 +23,16 @@ if (monitorAssets.length !== 1) { if (runtimeConfig !== safeRuntimeTemplate) { throw new Error('production dist/app-config.js must match the credential-free runtime template'); } +if (!builtIndex.includes('id="platform-boot"') + || !builtIndex.includes('vehicle-platform:ready') + || !builtIndex.includes('window.setTimeout(fail, 10_000)')) { + throw new Error('production index must retain the static boot recovery boundary'); +} +const mainAsset = builtIndex.match(/]+type="module"[^>]+src="\/assets\/([^"/]+\.js)"/)?.[1]; +if (!mainAsset || !assets.includes(mainAsset) + || !readFileSync(resolve(assetsDirectory, mainAsset), 'utf8').includes('vehicle-platform:ready')) { + throw new Error('production entry must retain the React boot-ready handshake'); +} if (releaseManifest.length !== assets.length || releaseManifest.some((name, index) => name !== [...assets].sort()[index])) { throw new Error('production dist/.release-assets must list every generated asset exactly once in sorted order'); } @@ -50,4 +61,4 @@ if (monitorBytes > 35_000) { throw new Error(`monitor critical-path asset exceeds 35000 bytes: ${monitorBytes}`); } const qrBytes = statSync(resolve(assetsDirectory, qrAssets[0])).size; -process.stdout.write(`web_build_gate=ok release_assets=${releaseManifest.length} exceljs_assets=1 exceljs_bytes=${excelBytes} mileage_workers=1 worker_bytes=${workerBytes} monitor_bytes=${monitorBytes} qr_deferred=1 qr_bytes=${qrBytes} runtime_config_sanitized=1\n`); +process.stdout.write(`web_build_gate=ok release_assets=${releaseManifest.length} exceljs_assets=1 exceljs_bytes=${excelBytes} mileage_workers=1 worker_bytes=${workerBytes} monitor_bytes=${monitorBytes} qr_deferred=1 qr_bytes=${qrBytes} runtime_config_sanitized=1 boot_recovery=1\n`); diff --git a/vehicle-data-platform/apps/web/src/v2/AppV2.tsx b/vehicle-data-platform/apps/web/src/v2/AppV2.tsx index 799028a4..38527861 100644 --- a/vehicle-data-platform/apps/web/src/v2/AppV2.tsx +++ b/vehicle-data-platform/apps/web/src/v2/AppV2.tsx @@ -1,8 +1,9 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { useEffect } from 'react'; import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom'; import { AppShell } from './layout/AppShell'; import { AuthGate } from './auth/AuthGate'; -import { RoutePage } from './routing/RouteBoundary'; +import { PlatformErrorBoundary, RoutePage } from './routing/RouteBoundary'; import { RoutePages } from './routing/routeModules'; import { ROUTER_FUTURE } from './routing/routerConfig'; import { QUERY_MEMORY } from './queryPolicy'; @@ -25,26 +26,37 @@ export function createPlatformQueryClient() { } const queryClient = createPlatformQueryClient(); +export const PLATFORM_READY_EVENT = 'vehicle-platform:ready'; + +function PlatformReadySignal() { + useEffect(() => { + window.dispatchEvent(new Event(PLATFORM_READY_EVENT)); + }, []); + return null; +} export function AppV2() { return ( - - - }> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - - - + + + + + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + + ); } diff --git a/vehicle-data-platform/apps/web/src/v2/bootRecovery.test.ts b/vehicle-data-platform/apps/web/src/v2/bootRecovery.test.ts new file mode 100644 index 00000000..339f72c1 --- /dev/null +++ b/vehicle-data-platform/apps/web/src/v2/bootRecovery.test.ts @@ -0,0 +1,49 @@ +import { afterEach, expect, test, vi } from 'vitest'; +import indexSource from '../../index.html?raw'; + +const bootScript = indexSource.match(/