: null}{!loading && !rows.length ? : null} : null}
;
}
@@ -95,19 +110,17 @@ const MemoFleetMap = memo(FleetMap);
function VehicleDetailCard({
vehicle,
- detail,
- activeAlerts,
- address,
onCollapse,
onClear
}: {
vehicle: VehicleRealtimeRow;
- detail?: VehicleDetailData;
- activeAlerts?: { items: AlertEvent[]; total: number };
- address?: MapReverseGeocode;
onCollapse: () => void;
onClear: () => void;
}) {
+ const card = useMonitorVehicleCard(vehicle.vin, vehicle, true);
+ const detail = card.detail.data;
+ const activeAlerts = card.activeAlerts.data;
+ const address = card.address.data;
const status = vehicleStatus(vehicle);
const dailyMileage = detail?.mileage.items[0]?.dailyMileageKm;
const latestAlert = activeAlerts?.items[0];
@@ -188,7 +201,8 @@ export default function MonitorPage() {
const realtimeListQuery = useQuery({
queryKey: ['monitor', 'vehicle-list', listParams.toString()],
queryFn: ({ signal }) => api.vehicleRealtime(listParams, signal),
- enabled: mode === 'list', placeholderData: (previous) => previous, refetchInterval: mode === 'list' ? MONITOR_REFRESH.fleet : false
+ enabled: mode === 'list', placeholderData: (previous) => previous, refetchInterval: mode === 'list' ? MONITOR_REFRESH.fleet : false,
+ gcTime: QUERY_MEMORY.highVolumeGcTime
});
const rows = useMemo(() => {
const data = vehicles.data?.items ?? [];
@@ -234,7 +248,6 @@ export default function MonitorPage() {
const selectMapVehicle = useCallback((vehicle: VehicleRealtimeRow) => selectVehicle(vehicle.vin), [selectVehicle]);
const collapseDetail = useCallback(() => setDetailOpen(false), []);
const expandDetail = useCallback(() => setDetailOpen(true), []);
- const card = useMonitorVehicleCard(selected?.vin ?? '', selected, Boolean(selectedVin));
const driving = rows.filter((vehicle) => vehicleStatus(vehicle) === 'driving').length;
const idle = rows.filter((vehicle) => vehicleStatus(vehicle) === 'idle').length;
const offline = rows.filter((vehicle) => vehicleStatus(vehicle) === 'offline').length;
@@ -307,9 +320,6 @@ export default function MonitorPage() {
{selected && detailOpen ? (
diff --git a/vehicle-data-platform/docs/frontend-production-readiness.md b/vehicle-data-platform/docs/frontend-production-readiness.md
index d21e8130..1a20bceb 100644
--- a/vehicle-data-platform/docs/frontend-production-readiness.md
+++ b/vehicle-data-platform/docs/frontend-production-readiness.md
@@ -2,6 +2,17 @@
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: 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`:
+
+-
+-
+
## 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.