Files
lingniu-vehicle-ingest/vehicle-data-platform/docs/frontend-production-readiness.md
2026-07-16 03:39:47 +08:00

136 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Frontend production-readiness audit
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: incremental fleet-map refresh
The 15-second monitor refresh previously treated every new response object as a complete visual change. Even when only `asOf` changed, it regenerated MassMarks styles/data, cleared both label layers and constructed every plate marker again. With hundreds of visible vehicles this caused avoidable canvas work and periodic main-thread pressure.
FleetMap now fingerprints only fields that affect map rendering. An unchanged refresh performs no AMap data/style/label mutation. A moving vehicle still updates MassMarks once, but the LabelsLayer removes and replaces only that vehicle's changed marker; full label-layer replacement is reserved for crossing the normal/dense zoom boundary. The selected ripple marker also skips redundant position writes. This uses the official AMap JS API 2.0 [`LabelsLayer.remove`](https://lbs.amap.com/api/javascript-api-v2/guide/amap-massmarker/label-marker) and `LabelMarker` update model instead of reconstructing all overlays. Unit tests assert zero map mutations for an `asOf`-only refresh and exactly one label replacement for one moved vehicle.
## 2026-07-16: bounded predictive route warming
The route shell split every primary page into a lazy chunk, but its idle queue then imported every inactive page during the first session. Successful ES-module imports cannot be unloaded, so this gradually made all route code resident even when an operator stayed on the monitor. It also let speculative parsing overlap live map work and defeated much of the memory benefit of route splitting.
Background warming is now predictive and bounded. Each active page has two likely next destinations; a fast visible session warms at most those two, one idle task at a time. A 3G or 4 GB device warms one, while `saveData`, 2G, hidden tabs and devices reporting 2 GB or less warm none. Pointer, focus and pointer-down intent still warm the exact destination immediately on capable connections, so deliberate navigation keeps the short transition while unused pages stay lazy. This follows web.dev's guidance to prefetch only high-confidence future resources, avoid slow or data-saving connections and prefer likely links over every link: <https://web.dev/articles/link-prefetch>, <https://web.dev/learn/performance/prefetching-prerendering-precaching>.
Unit tests protect route predictions, memory/network budgets, sequential scheduling, background-tab cancellation and cleanup. The production build remains guarded by a visible Suspense skeleton and the route chunk recovery boundary, so a page transition cannot become an empty content area while a non-warmed chunk loads.
## 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.
Behavioral tests cover a valid release, a missing compatibility file, a wrong HTTP 200 response body and an unsafe manifest path. The deployed gate passed all 20 current assets and all 20 compatibility assets from `mileage-export-worker-20260716014414`; the platform service remained active and the authenticated health endpoint reported the new release. The ECS host still runs Python 3.6, so the test fixture uses a working-directory subshell instead of the newer `http.server --directory` option and now proves the same cases on both the development host and ECS. The final release is `web-release-smoke-portable-20260716015528`.
## 2026-07-16: cancellable, off-main-thread mileage export
The full-fleet mileage export previously performed three long-running phases as one opaque foreground action: paginating vehicle identities, paginating daily mileage rows and building the ExcelJS workbook on the browser main thread. Navigating away did not cancel the export requests, and a large `writeBuffer()` could make controls and route transitions unresponsive.
The export now owns one `AbortController` for its complete lifecycle. Every vehicle and mileage request consumes that signal, leaving the route aborts the controller, and the action can be cancelled from the same button. The UI reports determinate progress while the response total is known and an indeterminate stage while Excel is generated. Workbook creation and serialization run in an ES-module Web Worker; the worker transfers the resulting `ArrayBuffer` back without copying and is terminated on completion, error or cancellation. ExcelJS remains action-only and does not enter the initial route bundle; a main-thread fallback is retained only for browsers without Worker support.
This matches the long-task feedback and isolation guidance used by mature design systems: Carbon recommends progress feedback for operations that last more than a moment, while Material UI explicitly recommends Web Workers or batching for processor-intensive work:
- <https://carbondesignsystem.com/patterns/loading-pattern/>
- <https://carbondesignsystem.com/components/progress-bar/usage/>
- <https://mui.com/material-ui/react-progress/>
The evidence gate covers request-signal propagation, user cancellation before workbook generation, immediate abort before download, worker bundling, production build output and an authenticated production interaction smoke.
Production release `mileage-export-worker-20260716014414` exercised the 90-day full-fleet path for 1,024 bound vehicles. The page exposed the cancel action immediately, returned to the normal export state with `已取消导出`, removed the progress indicator, kept the query controls responsive and emitted no console warning or error. A separate full 30-day export completed for all 1,024 vehicles, proving the Worker asset and ExcelJS action chunks load successfully in the deployed browser build.
## 2026-07-16: 50-cycle authenticated route soak
The production ECS application completed 50 authenticated `monitor -> history -> statistics -> tracks -> monitor` cycles. Chrome performance and heap metrics were sampled every five cycles. Because the Browser channel does not permit `HeapProfiler.collectGarbage`, the gate uses post-warmup and 15-second-idle low-water marks instead of comparing transient allocation peaks.
| Metric | After 10 warm cycles and natural collection | After 50 cycles and 15 seconds idle | Result |
| --- | ---: | ---: | --- |
| JS heap used | 26.52 MB | 28.53 MB | Stable; +2.01 MB |
| Event listeners | 880 | 686 | No monotonic growth |
| DOM nodes | 7,763 | 4,811 | No monotonic growth |
| Detached script states | 0 | 0 | Fully released |
| Live page DOM | — | 1 AMap iframe / 1 map container | Only the active monitor map remains |
Transient peaks reached 165.83 MB while route effects and AMap instances overlapped, then returned to the low-water mark after natural collection. The final page remained `/monitor`, rendered live fleet statistics, and had zero console warnings or errors. This proves that the explicit query, map and listener cleanup prevents sustained growth under this route pattern; it does not replace a future allocation profile for data-heavy operator interactions.
The audit found one remaining lifecycle asymmetry: track start, end and stop markers registered anonymous click handlers and relied on overlay removal to release them. Track overlays now retain each handler identity and call `off('click', handler)` before redraw and unmount. `TrackMap.test.tsx` asserts every marker handler, the map drag handler, overlays and the map instance are released.
## 2026-07-16: monitor lifecycle and responsive rendering
The monitor's high-volume fleet, map and selected-vehicle queries now use immediate inactive-cache collection instead of inheriting the global five-minute retention. The detail card owns its detail, alert and address queries, so collapsing or clearing the card unmounts its observers while the separate selected-vehicle stream continues to move the map. Repeated viewport and vehicle-selection tests assert that only the active payload remains and that detail-card queries reach zero after unmount.
The realtime list no longer renders the desktop table and mobile cards simultaneously. A viewport listener mounts exactly one representation, cutting a 50-row page from 100 row trees and 100 address query observers to 50. The listener is removed with the component. Fleet and track maps also keep stable event-handler identities and pair every AMap `on` with `off` before overlay removal and `destroy()`.
This follows TanStack Query's documented behavior that inactive queries remain cached for five minutes unless `gcTime` changes, and AMap JS API 2.0's requirement to unbind the same handler object with `off` that was registered with `on`:
- <https://tanstack.com/query/latest/docs/framework/react/guides/important-defaults>
- <https://lbs.amap.com/api/javascript-api-v2/guide/events/map_overlay>
## 2026-07-16: production stylesheet boundary
The production entry renders `AppV2` exclusively. V2 pages do not import Semi UI components, so loading the legacy `global.css` from `main.tsx` made every route download and parse the complete legacy Semi theme and thousands of unreachable V1 rules. The entry now imports only `v2/styles/v2.css`; the legacy stylesheet remains available to the explicit `test:legacy` compatibility suite but cannot enter the V2 production bundle. `productionEntry.test.ts` protects this boundary. The verified production CSS fell from 435.20 kB / 53.50 kB gzip to 168.91 kB / 27.72 kB gzip, and the Semi theme Sass deprecation warnings disappeared from the build.
The default `test` command is now the production V2 suite. The old `App.tsx` integration test remains runnable as `test:legacy`, and `test:all` is retained for combined audits. This separation makes deprecation and jsdom navigation warnings from the retired Semi UI shell visible without allowing them to hide regressions in the production application. The 2026-07-16 baseline is 139/139 passing production tests and 154/168 passing legacy assertions; the 14 legacy failures describe retired V1 mileage and hash-route expectations and do not gate V2 deployment.
## 2026-07-15: route continuity and monitor memory
### Resolved in this round
| Risk | User-visible failure | Control | Evidence gate |
| --- | --- | --- | --- |
| A stale lazy route asset disappears during release | Navigation leaves the content area blank | Preserve the previous build's original hashed assets for one generation; isolate every route with a recoverable error boundary and one-shot reload | Request an old route asset after the symlink switch; navigate through every primary route without a blank shell |
| A pasted batch search temporarily shows the previous fleet | Operators may mistake stale vehicles for batch matches | Hide placeholder rows while `keywords` is changing; report matched and missing plates after the request resolves | Paste newline/comma-separated plates with a duplicate and an unknown plate; verify exact results and missing-count feedback |
| URL filters remount the whole route | Pagination or tab changes lose local state and may flash a loading screen | Key the route recovery boundary by pathname only; keep the full URL only for chunk-reload diagnostics | Change search parameters inside a stateful route and assert one mount with preserved local state |
| Route-scoped reads continue after navigation | Obsolete responses consume bandwidth and remain in memory | Consume TanStack Query's `AbortSignal` in every V2 read path; immediately collect high-volume access and alert pages after they become inactive | Start a read, navigate away or change its query key, and assert the fetch receives the query signal |
| A route is first fetched only after click | Slow transitions and a longer loading flash | Deduplicated route import cache plus pointer, focus and pointer-down preload | Route module unit tests and rendered navigation loop |
| Rapid map pans retain large query payloads | Heap growth and eventual interaction jank | Consume React Query `AbortSignal` and immediately garbage-collect inactive map viewport queries | Query-cache churn test keeps exactly one map payload after repeated viewport changes |
| Superseded monitor requests continue in the background | Wasted server traffic and stale responses | Pass request cancellation signals through the API client | API client signal test and browser network/console smoke |
| QR generation resolves after the dialog unmounts | State update after lifecycle end | Cancel the asynchronous state write on cleanup | Monitor component test |
React documents that `lazy` caches the import promise and propagates a rejected module load to the nearest Error Boundary; this is why a route-level boundary is required rather than a loading fallback alone: <https://react.dev/reference/react/lazy>. React's Suspense documentation also distinguishes a loading fallback from error handling: <https://react.dev/reference/react/Suspense>.
The first continuity pass only warmed Monitor, Tracks, History and Statistics in the background. A later pass expanded that queue to every route to reduce a 450 ms simulated cold transition, but doing so made all successful ES-module imports resident for every session. The current policy keeps pointer/focus/down intent warming for the exact destination and limits idle warming to one or two likely routes according to visibility, memory and connection constraints. This preserves the no-white-screen loading boundary without paying the memory cost of importing every page speculatively.
A production route-memory loop did not prove a retained application leak: after natural collection, JS heap returned to roughly its initial 31 MB range and old map documents/frames were released. It did expose an ineffective rendering hint: `content-visibility: auto` was attached to the always-visible vehicle scroll viewport instead of the 200 independently off-screen rows. The containment boundary now lives on every desktop rail row and mobile list card, paired with `contain-intrinsic-size` so skipped content keeps stable scroll geometry. This follows the CSS Containment specification's long-scroll-list use case while avoiding the documented scrollbar jump caused by missing intrinsic size: <https://www.w3.org/TR/css-contain-2/#content-visibility>.
Excel export previously produced two independent 940 KB ExcelJS assets because the worker imported the browser download module while that module also retained a main-thread workbook fallback. Workbook construction now lives in a worker-only runtime module; unsupported legacy browsers receive an upgrade message instead of freezing the page with a main-thread build. The production output contains one ExcelJS asset, the Statistics route chunk is smaller, and `scripts/verify-build.mjs` makes exactly one ExcelJS asset plus one mileage worker a mandatory `pnpm build` gate.
The Alert Center previously mounted every tab's data dependencies together: the default event view fetched the metric catalog even though only the rule editor consumes it, while a direct notifications entry fetched both the unread-only collection and the full notification collection. Queries are now gated by the active workspace. Events load events, summary, rule names and the unread badge; rules add the metric catalog only when opened; notifications load one full collection and derive the unread badge from it. This follows TanStack Query's lazy-query `enabled` model and preserves Grafana's separation between alert triage, rule configuration and notification handling: <https://tanstack.com/query/latest/docs/framework/react/guides/disabling-queries>, <https://grafana.com/docs/grafana/latest/alerting/>. Request-count tests protect both the default event route and a direct notifications entry.
Vite previously copied the ignored developer-local `public/app-config.js` into `dist` unchanged. The API correctly intercepted production `/app-config.js` requests, but a local AMap security code still entered release archives and would become exposed if that interception ever regressed. The production build now overwrites the copied file with the credential-free example and verifies an exact byte match. The ECS release smoke gate independently requires the server-side `/_AMapService` host and rejects the security-code property, so both the artifact and the served runtime surface fail closed. This matches AMap's production guidance, which strongly recommends server proxy forwarding and explicitly classifies browser plaintext configuration as unsafe: <https://lbs.amap.com/api/javascript-api-v2/guide/abc/jscode>.
The monitor map screen previously fanned one refresh into three independent requests: summary, a 200-row rail and a map snapshot of up to 10,000 vehicles. Each server path repeated the realtime snapshot count and row query, and the browser created three overlapping payload lifecycles. `/api/v2/monitor/workspace` now reads the bounded snapshot once and derives all three coherent views from it; the client owns one abortable, zero-retention high-volume query. This applies the gateway aggregation/BFF pattern to a single data-heavy screen and follows Grafana's recommendation to share results instead of repeating data-source queries: <https://learn.microsoft.com/en-us/azure/architecture/patterns/gateway-aggregation>, <https://grafana.com/docs/grafana/latest/visualizations/panels-visualizations/query-transform-data/share-query/>.
The navigation and progressive-loading direction follows mature observability systems: persistent global controls, collapsible sections and loading only the content needed for the current task. Elastic documents these dashboard interaction and panel-organization patterns here: <https://www.elastic.co/docs/explore-analyze/dashboards/using> and <https://www.elastic.co/docs/explore-analyze/dashboards/arrange-panels>.
### Remaining audit queue
1. Capture an allocation profile for data-heavy operator interactions: loaded history series, long track playback and repeated Excel exports.
2. Bound or explicitly evict other high-cardinality query families, especially arbitrary history windows and track playback ranges.
3. Retire or migrate the remaining legacy `App.tsx` integration suite after its compatibility coverage is replaced in V2.
4. Profile repeated large exports after the Web Worker migration and define an explicit maximum supported row/column envelope for browser-side workbooks.
5. Run the automated byte-for-byte current/compatibility asset smoke in every future ECS release and promote it into CI/CD when the deployment pipeline is formalized.
## 2026-07-15: query memory and vehicle-count semantics
Production reconciliation proved that the three visible vehicle counts represent different populations rather than a data loss: 1,035 service identities equal 1,024 bound master vehicles plus 11 realtime identities awaiting binding; 738 vehicles currently have a realtime location row and can enter the global-monitor map. The UI must name these populations explicitly instead of presenting them as interchangeable totals.
High-volume history rows, aggregated series, track playback and daily-mileage matrices consume the request `AbortSignal` and use zero inactive-cache retention. Small vehicle-option and summary results retain only a bounded 3060 second cache. This preserves responsive repeated lookups without retaining multiple large arbitrary date-window payloads after their observer is gone.
## Release evidence template
- Commit and release identifier
- All non-legacy frontend tests
- Production V2 route tests
- TypeScript/Vite production build sizes
- Authenticated health response and `data.runtime.platformRelease`
- New main asset HTTP status
- Previous route asset HTTP status
- Browser route loop, blank-page check and console warnings/errors
- Desktop and mobile viewport screenshots