From 4182f79028af6449b68c0cb75775ac9a5da99aaa Mon Sep 17 00:00:00 2001 From: rcourtman Date: Sun, 23 Aug 2026 17:05:15 +0100 Subject: [PATCH] Eliminate workload virtualization scroll gaps --- .../subsystems/performance-and-scalability.md | 9 ++++ frontend-modern/browser-verification.json | 41 +++++++------- .../components/Workloads/WorkloadPanel.tsx | 43 ++++++++------- ...loadsSurface.performance.contract.test.tsx | 12 ++++- .../useGroupedTableWindowing.test.ts | 54 +++++++++++++++---- .../useWorkloadViewportSync.test.tsx | 9 ++++ .../Workloads/useGroupedTableWindowing.ts | 39 ++++++++++++-- .../Workloads/useWorkloadViewportSync.ts | 24 +++++++-- 8 files changed, 171 insertions(+), 60 deletions(-) diff --git a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md index 0a665ac87..1f5e95f70 100644 --- a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md +++ b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md @@ -801,6 +801,15 @@ change may globally weaken the Task 03 lifecycle-state idempotency invariant. already-spaced header. This keeps total table height stable while groups cross the virtual window and prevents the visible estate from bumping by a header row during continuous scrolling. + Virtualization must remain visually invisible under rapid wheel and + trackpad input. The windowing owner keeps a hysteretic directional runway + instead of shifting the slice for every visible-row change, and viewport + sync may prewarm that runway from one passive wheel listener before the + corresponding scroll event. Group and guest iteration must remain keyed so + overlapping rows survive window shifts rather than rebinding every mounted + row. This anticipation must preserve the existing bounded mounted-row + budget; rendering the whole estate or adding per-row observers, timers, or + scroll listeners is forbidden. That same viewport-sync owner may expose one passive app-shell scroll position signal and a smooth back-to-top action for the Workloads surface. The control must remain absent near the top, sit above the mobile navigation diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json index 156b2b482..6ca8379d7 100644 --- a/frontend-modern/browser-verification.json +++ b/frontend-modern/browser-verification.json @@ -1,17 +1,17 @@ { "version": 1, - "base_sha": "d8fe2a935875675b22a87c5471ffc923f7a0ab51", - "verified_at": "2026-08-23T16:50:00Z", + "base_sha": "dc129cd3a403e9fd2f9177ddbfcf18e5042445e5", + "verified_at": "2026-08-23T16:03:35Z", "result": "passed", "changed_paths": [ - "frontend-modern/src/features/proxmox/ProxmoxNodesTable.tsx", - "frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx", - "frontend-modern/src/features/proxmox/proxmoxPageModel.ts" + "frontend-modern/src/components/Workloads/WorkloadPanel.tsx", + "frontend-modern/src/components/Workloads/useGroupedTableWindowing.ts", + "frontend-modern/src/components/Workloads/useWorkloadViewportSync.ts" ], "content_sha256": { - "frontend-modern/src/features/proxmox/ProxmoxNodesTable.tsx": "5b2dce1ff4cae9624fa106ad9c01f29e0025b33e0def87cee30798e74fe31329", - "frontend-modern/src/features/proxmox/ProxmoxPageSurface.tsx": "27d2077c659d40e2c51472086e745311221d771129455a4bae2be816ab116dbe", - "frontend-modern/src/features/proxmox/proxmoxPageModel.ts": "f80b8e01fd31e7f2de55cda5a2f5e58005068b07daa77c3356811ad7f5919051" + "frontend-modern/src/components/Workloads/WorkloadPanel.tsx": "ea56e3d8a4e8c63baf1f99e1dea82d8e6ea2dd801b8eafaf4cb849ebf8612bad", + "frontend-modern/src/components/Workloads/useGroupedTableWindowing.ts": "76fd82b5a0baefdf09e37af1713681e4424c2762ece99652b2d998011a457ce8", + "frontend-modern/src/components/Workloads/useWorkloadViewportSync.ts": "0f2315ddf875206f5de6a97fc9a600b5b61aaa2ae910cc4353c5eb78eb5bfd18" }, "routes": [ "/proxmox/overview" @@ -22,22 +22,23 @@ "height": 720 }, { - "width": 390, - "height": 844 + "width": 375, + "height": 812 } ], "states": [ - "Default desktop Proxmox overview renders the 50-node mock estate with 8 clusters and 2 standalone nodes", - "Desktop search for pve31 renders one matching node as 1 of 50 and keeps the matching guest inventory visible", - "Narrow search for pve31 renders one matching node with the native pve31 identifier beside its friendly name", - "Narrow filtered layout has no horizontal overflow and retains the responsive node and workload columns", - "The browser verification run reported no console errors" + "The large Proxmox mock estate renders 929 guests while retaining the 140-row mounted DOM budget", + "After an 8000-pixel desktop wheel burst, 25 workload rows cover the full 1280x720 viewport from -6 to 723 pixels without exposing a spacer", + "After a 2800-pixel phone wheel burst, 23 workload rows cover the full 375x812 viewport from -21 to 821 pixels without exposing a spacer", + "Desktop scroll height remains 29470 pixels and phone scroll height remains 36372 pixels while virtual windows move", + "Rapid-scroll screenshots show continuous populated rows with the back-to-top control remaining above mobile navigation", + "The desktop and phone rapid-scroll runs reported no console errors" ], "interactions": [ - "Loaded the Proxmox overview and inspected the default 50-node desktop table", - "Entered pve31 in the shared workload search and verified the node table reduced to 1 of 50", - "Verified the same query left the matching Analytics A host and its pve31 workload rows visible", - "Applied a temporary 390x844 viewport override, reloaded the overview, entered pve31, and verified the narrow DOM and screenshot", - "Reset the temporary viewport override after verification" + "Loaded the large desktop mock estate and confirmed its initial 140-row virtual runway", + "Applied an 8000-pixel wheel burst and immediately measured and captured continuous visible-row coverage", + "Applied a temporary 375x812 viewport override and reloaded the 929-guest estate", + "Applied a 2800-pixel phone wheel burst and immediately measured and captured continuous visible-row coverage", + "Checked runtime error logs, reset the temporary viewport override, and closed the temporary test tab" ] } diff --git a/frontend-modern/src/components/Workloads/WorkloadPanel.tsx b/frontend-modern/src/components/Workloads/WorkloadPanel.tsx index 9d12dfb7d..7f66e02c6 100644 --- a/frontend-modern/src/components/Workloads/WorkloadPanel.tsx +++ b/frontend-modern/src/components/Workloads/WorkloadPanel.tsx @@ -1,4 +1,4 @@ -import { createMemo, Index, Show } from 'solid-js'; +import { createMemo, For, Show } from 'solid-js'; import { ComponentErrorBoundary } from '@/components/ErrorBoundary'; import { EnhancedCPUBar } from '@/components/Workloads/EnhancedCPUBar'; @@ -373,29 +373,33 @@ export function WorkloadPanel(props: WorkloadPanelProps) { - }> + }> {(groupKey) => { - const groupGuests = () => props.windowedGroupedGuests()[groupKey()] || []; - const fullGroupGuests = () => props.groupedGuests()[groupKey()] || []; - const node = () => props.nodeByInstance()[groupKey()]; + const groupGuests = () => props.windowedGroupedGuests()[groupKey] || []; + const fullGroupGuests = () => props.groupedGuests()[groupKey] || []; + const groupGuestById = createMemo( + () => new Map(groupGuests().map((guest) => [getCanonicalWorkloadId(guest), guest])), + ); + const groupGuestIds = createMemo(() => groupGuests().map(getCanonicalWorkloadId)); + const node = () => props.nodeByInstance()[groupKey]; const groupSummaryScope = createMemo(() => { if (props.groupingMode() !== 'grouped') { return null; } return buildWorkloadSummaryGroupScope( - groupKey(), + groupKey, fullGroupGuests(), - props.getGroupLabel(groupKey(), fullGroupGuests()), + props.getGroupLabel(groupKey, fullGroupGuests()), ); }); const isSummaryGroupHighlighted = createMemo( - () => props.activeSummaryWorkloadGroupScope()?.id === groupKey(), + () => props.activeSummaryWorkloadGroupScope()?.id === groupKey, ); const shouldShowNodeDrawer = createMemo( () => props.groupNodeDrawerMode() === 'inline' && Boolean(node()) && - props.focusedSummaryWorkloadGroupId() === groupKey() && + props.focusedSummaryWorkloadGroupId() === groupKey && props.selectedGuestId() === null, ); const canOpenNodeDrawer = () => props.groupNodeDrawerMode() === 'inline'; @@ -438,7 +442,7 @@ export function WorkloadPanel(props: WorkloadPanelProps) { ? getInteractiveGroupedTableRowClass() : getGroupedTableRowClass() } - data-summary-group-id={groupKey()} + data-summary-group-id={groupKey} data-summary-group-series-count={String( groupSummaryScope()?.seriesIds.length ?? 0, )} @@ -451,9 +455,9 @@ export function WorkloadPanel(props: WorkloadPanelProps) { class={getGroupedTableRowCellClass()} > {(() => { - const label = props.getGroupLabel(groupKey(), fullGroupGuests()); + const label = props.getGroupLabel(groupKey, fullGroupGuests()); const badges = props.groupLabelBadges(); - const badge = badges[groupKey()] ?? badges[groupKey().toLowerCase()]; + const badge = badges[groupKey] ?? badges[groupKey.toLowerCase()]; const badgeLabel = badge?.label || label.type; const badgeClass = badge?.classes || GROUPED_TABLE_ROW_BADGE_CLASS; return ( @@ -500,7 +504,7 @@ export function WorkloadPanel(props: WorkloadPanelProps) { ? 'true' : 'false' : undefined, - 'data-summary-group-id': groupKey(), + 'data-summary-group-id': groupKey, 'data-summary-group-series-count': String( groupSummaryScope()?.seriesIds.length ?? 0, ), @@ -513,7 +517,7 @@ export function WorkloadPanel(props: WorkloadPanelProps) { - }> - {(guest) => { - const guestId = createMemo(() => getCanonicalWorkloadId(guest())); + }> + {(keyedGuestId) => { + const guest = () => groupGuestById().get(keyedGuestId)!; + const guestId = () => keyedGuestId; const metadataIdCandidates = createMemo(() => getWorkloadMetadataIdCandidates(guest()), ); @@ -606,11 +611,11 @@ export function WorkloadPanel(props: WorkloadPanelProps) { ); }} - + ); }} - + 0}>