diff --git a/vehicle-data-platform/apps/web/src/v2/productionEntry.test.ts b/vehicle-data-platform/apps/web/src/v2/productionEntry.test.ts index e9bc7511..a11d1e3f 100644 --- a/vehicle-data-platform/apps/web/src/v2/productionEntry.test.ts +++ b/vehicle-data-platform/apps/web/src/v2/productionEntry.test.ts @@ -1,9 +1,32 @@ +import { readFileSync } from 'node:fs'; +import { resolve } from 'node:path'; import { describe, expect, test } from 'vitest'; import mainSource from '../main.tsx?raw'; +const v2Styles = readFileSync(resolve(process.cwd(), 'src/v2/styles/v2.css'), 'utf8'); + describe('V2 production entry', () => { test('does not load the legacy Semi UI theme and global stylesheet', () => { expect(mainSource).not.toContain("./styles/global.css"); expect(mainSource).toContain("./v2/styles/v2.css"); }); + + test('applies rendering containment to off-screen vehicle items instead of the visible scroller', () => { + const ruleBody = (selector: string) => { + const start = v2Styles.indexOf(`${selector} {`); + expect(start).toBeGreaterThanOrEqual(0); + const end = v2Styles.indexOf('}', start); + expect(end).toBeGreaterThan(start); + return v2Styles.slice(start, end); + }; + const scrollRule = ruleBody('.v2-vehicle-scroll'); + const rowRule = ruleBody('.v2-vehicle-row'); + const mobileCardRule = ruleBody('.v2-monitor-mobile-cards > article'); + + expect(scrollRule).not.toContain('content-visibility'); + expect(rowRule).toContain('content-visibility: auto'); + expect(rowRule).toContain('contain-intrinsic-size: auto 60px'); + expect(mobileCardRule).toContain('content-visibility: auto'); + expect(mobileCardRule).toContain('contain-intrinsic-size: auto 154px'); + }); }); diff --git a/vehicle-data-platform/apps/web/src/v2/styles/v2.css b/vehicle-data-platform/apps/web/src/v2/styles/v2.css index fde7f63f..de0531af 100644 --- a/vehicle-data-platform/apps/web/src/v2/styles/v2.css +++ b/vehicle-data-platform/apps/web/src/v2/styles/v2.css @@ -108,8 +108,8 @@ button, a { -webkit-tap-highlight-color: transparent; } .v2-vehicle-rail > header span, .v2-vehicle-rail > footer { color: var(--v2-muted); font-size: 10px; } .v2-rail-search { display: flex; height: 34px; align-items: center; gap: 7px; margin: 0 9px 8px; border: 1px solid var(--v2-border); border-radius: 7px; padding: 0 9px; color: #8a98aa; font-size: 10px; } .v2-rail-search.has-missing { border-color: #fed7aa; background: #fffaf3; color: #b45309; } -.v2-vehicle-scroll { min-height: 0; flex: 1; overflow: auto; overscroll-behavior: contain; content-visibility: auto; } -.v2-vehicle-row { display: grid; width: 100%; min-height: 60px; grid-template-columns: 10px minmax(0, 1fr) auto; align-items: center; gap: 8px; border: 0; border-top: 1px solid #eef2f7; background: #fff; padding: 8px 10px; text-align: left; cursor: pointer; } +.v2-vehicle-scroll { min-height: 0; flex: 1; overflow: auto; overscroll-behavior: contain; } +.v2-vehicle-row { display: grid; width: 100%; min-height: 60px; grid-template-columns: 10px minmax(0, 1fr) auto; align-items: center; gap: 8px; border: 0; border-top: 1px solid #eef2f7; background: #fff; padding: 8px 10px; text-align: left; cursor: pointer; content-visibility: auto; contain-intrinsic-size: auto 60px; } .v2-vehicle-row:hover { background: #f8fbff; } .v2-vehicle-row.is-selected { position: relative; z-index: 1; background: var(--v2-blue-soft); box-shadow: inset 3px 0 var(--v2-blue); } .v2-status-dot { width: 7px; height: 7px; border-radius: 50%; background: #94a3b8; } @@ -1119,7 +1119,7 @@ button, a { -webkit-tap-highlight-color: transparent; } .v2-monitor-table-panel > header { align-items: flex-start; flex-direction: column; gap: 4px; padding: 10px 12px; } .v2-monitor-table-scroll { display: none; } .v2-monitor-mobile-cards { display: flex; min-height: 0; flex: 1; flex-direction: column; gap: 8px; overflow: auto; padding: 8px; background: #f5f7fa; } - .v2-monitor-mobile-cards > article { border: 1px solid #e0e6ee; border-radius: 10px; background: #fff; padding: 12px; box-shadow: 0 2px 8px rgba(21,32,51,.04); } + .v2-monitor-mobile-cards > article { border: 1px solid #e0e6ee; border-radius: 10px; background: #fff; padding: 12px; box-shadow: 0 2px 8px rgba(21,32,51,.04); content-visibility: auto; contain-intrinsic-size: auto 154px; } .v2-monitor-mobile-cards > article > header { display: flex; align-items: flex-start; justify-content: space-between; gap: 10px; } .v2-monitor-mobile-cards > article > header div { display: flex; min-width: 0; flex-direction: column; gap: 3px; } .v2-monitor-mobile-cards > article > header strong { font-size: 15px; }.v2-monitor-mobile-cards > article > header span { color: #7f8da1; font-size: 10px; } diff --git a/vehicle-data-platform/docs/frontend-production-readiness.md b/vehicle-data-platform/docs/frontend-production-readiness.md index 6b154deb..97971cf2 100644 --- a/vehicle-data-platform/docs/frontend-production-readiness.md +++ b/vehicle-data-platform/docs/frontend-production-readiness.md @@ -88,6 +88,8 @@ React documents that `lazy` caches the import promise and propagates a rejected The first continuity pass only warmed Monitor, Tracks, History and Statistics in the background. A production cold-cache audit with 450 ms simulated latency still measured roughly 1.66 seconds for `/monitor` to `/access`, leaving Vehicles, Alerts, Access and Operations dependent on click-time loading. The idle queue now covers all eight route sections in bounded batches of two. It still skips `saveData`, `slow-2g` and `2g` connections, while pointer/focus/down intent preloading remains the fastest path for an immediately selected destination. Unit coverage proves every inactive route is queued and that cleanup cancels the next idle batch. +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: . + 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: and . ### Remaining audit queue