diff --git a/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.test.tsx b/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.test.tsx index b4b4a599..3eef4e4e 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.test.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.test.tsx @@ -116,7 +116,7 @@ test('switches to a lightweight realtime list and resolves addresses only on dem vi.spyOn(api, 'vehicleRealtime').mockResolvedValue({ items: [row], total: 1, limit: 50, offset: 0 }); const reverseGeocode = vi.spyOn(api, 'reverseGeocode').mockResolvedValue({ provider: 'AMap', longitude: 113.26, latitude: 23.13, formattedAddress: '广东省广州市天河区测试路' }); const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); - render(); + const view = render(); fireEvent.click(screen.getByRole('button', { name: /列表/ })); expect(await screen.findByText('车辆实时列表')).toBeInTheDocument(); expect(screen.getByText('速度、里程和坐标实时刷新,文字地址按需解析')).toBeInTheDocument(); @@ -129,9 +129,14 @@ test('switches to a lightweight realtime list and resolves addresses only on dem fireEvent.click(screen.getByRole('button', { name: '解析粤A12345位置' })); expect(await screen.findByText('广东省广州市天河区测试路')).toBeInTheDocument(); expect(reverseGeocode).toHaveBeenCalledTimes(1); + expect(queryClient.getQueryCache().findAll({ queryKey: ['monitor', 'list-address'] })).toHaveLength(1); expect(screen.queryByText('综合状态')).not.toBeInTheDocument(); expect(screen.queryByText('三路正常')).not.toBeInTheDocument(); expect(screen.queryByTestId('fleet-map')).not.toBeInTheDocument(); + + fireEvent.click(screen.getByRole('button', { name: /地图/ })); + await waitFor(() => expect(queryClient.getQueryCache().findAll({ queryKey: ['monitor', 'list-address'] })).toHaveLength(0)); + view.unmount(); }); test('mounts only the mobile list representation and removes its viewport listener', async () => { diff --git a/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.tsx b/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.tsx index 18821d2c..0b90d849 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/MonitorPage.tsx @@ -70,7 +70,7 @@ const MonitorAddressCell = memo(function MonitorAddressCell({ vehicle }: { vehic }), signal), enabled: Boolean(requested), staleTime: 6 * 60 * 60_000, - gcTime: 24 * 60 * 60_000, + gcTime: QUERY_MEMORY.highVolumeGcTime, refetchOnWindowFocus: false, retry: 1 }); diff --git a/vehicle-data-platform/apps/web/src/v2/pages/TrackPage.test.tsx b/vehicle-data-platform/apps/web/src/v2/pages/TrackPage.test.tsx index 93377b02..51331086 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/TrackPage.test.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/TrackPage.test.tsx @@ -46,10 +46,14 @@ test('renders a map-first replay workspace and connects stop, event, and panel i expect(screen.getByTestId('track-map')).toHaveAttribute('data-active-index', '0'); expect(screen.getByText('停留 00:05:00 · 1 个点')).toBeInTheDocument(); expect(screen.queryByRole('button', { name: '暂停轨迹播放' })).not.toBeInTheDocument(); + await waitFor(() => expect(mocks.reverseGeocode).toHaveBeenCalledTimes(1)); + expect(client.getQueryCache().findAll({ queryKey: ['track-address'] })).toHaveLength(1); fireEvent.click(screen.getByRole('button', { name: /事件点/ })); fireEvent.click(screen.getByRole('button', { name: /急加速/ })); expect(screen.getByTestId('track-map')).toHaveAttribute('data-active-index', '1'); + await waitFor(() => expect(mocks.reverseGeocode).toHaveBeenCalledTimes(2)); + await waitFor(() => expect(client.getQueryCache().findAll({ queryKey: ['track-address'] })).toHaveLength(1)); fireEvent.click(screen.getByRole('button', { name: '收起查询面板' })); expect(screen.getByRole('button', { name: /查询与明细/ })).toBeInTheDocument(); @@ -62,4 +66,5 @@ test('renders a map-first replay workspace and connects stop, event, and panel i expect(client.getQueryCache().findAll({ queryKey: ['track-playback'] })).toHaveLength(1); view.unmount(); await waitFor(() => expect(client.getQueryCache().findAll({ queryKey: ['track-playback'] })).toHaveLength(0)); + await waitFor(() => expect(client.getQueryCache().findAll({ queryKey: ['track-address'] })).toHaveLength(0)); }); diff --git a/vehicle-data-platform/apps/web/src/v2/pages/TrackPage.tsx b/vehicle-data-platform/apps/web/src/v2/pages/TrackPage.tsx index 369c4424..faf884de 100644 --- a/vehicle-data-platform/apps/web/src/v2/pages/TrackPage.tsx +++ b/vehicle-data-platform/apps/web/src/v2/pages/TrackPage.tsx @@ -218,7 +218,8 @@ export default function TrackPage() { const addressQuery = useQuery({ queryKey: ['track-address', addressPoint?.longitude.toFixed(6), addressPoint?.latitude.toFixed(6)], enabled: Boolean(addressPoint), staleTime: 60 * 60 * 1000, - queryFn: ({ signal }) => api.reverseGeocode(new URLSearchParams({ longitude: addressPoint!.longitude.toFixed(6), latitude: addressPoint!.latitude.toFixed(6) }), signal) + queryFn: ({ signal }) => api.reverseGeocode(new URLSearchParams({ longitude: addressPoint!.longitude.toFixed(6), latitude: addressPoint!.latitude.toFixed(6) }), signal), + gcTime: QUERY_MEMORY.highVolumeGcTime }); useEffect(() => { setActiveIndex(0); setPlaying(false); setFollow(true); if (track?.points.length) setRailCollapsed(window.matchMedia('(max-width: 700px)').matches); }, [track?.asOf]); diff --git a/vehicle-data-platform/docs/frontend-production-readiness.md b/vehicle-data-platform/docs/frontend-production-readiness.md index 7756ec34..996c03d0 100644 --- a/vehicle-data-platform/docs/frontend-production-readiness.md +++ b/vehicle-data-platform/docs/frontend-production-readiness.md @@ -2,6 +2,12 @@ This document records verified risks, the production controls that address them, and the next evidence to collect. It is intentionally operational: a passing build alone is not proof that the browser application is production-ready. +## 2026-07-16: high-cardinality address cache lifecycle + +The monitor list previously retained every manually resolved coordinate for 24 hours, and track replay inherited the global five-minute cache for every paused point. Long-running dispatch sessions could therefore accumulate address payloads across pages and arbitrary playback coordinates even after their components disappeared. + +Both query families now use zero inactive-cache retention: the visible list row or current paused track point keeps its address while mounted, but switching monitor mode, selecting another point, or leaving the route immediately releases the previous browser entry. This does not cause repeated AMap quota consumption because the authenticated server adapter independently owns the one-hour, 4,096-entry LRU cache and collapses concurrent misses. Component tests prove list addresses reach zero after returning to map mode, track address churn retains only one active key, and route unmount leaves no track address entry. + ## 2026-07-16: complete release-asset compatibility gate Release `web-release-smoke-20260716015150` installs `deploy/verify-web-release.sh` as a production release gate. It verifies the exact root document, runtime configuration object, every original asset in the current `.release-assets` manifest and every original asset in the immediately previous manifest. Asset responses are compared byte-for-byte with the active release files; HTTP 200 with an SPA HTML fallback therefore fails instead of masking a missing lazy route chunk.