From 1177e4d1434d23ce8e8855f2401393d713623757 Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Fri, 2 Oct 2026 15:23:47 +0100 Subject: [PATCH] Preserve History pointer inspection during matching live refreshes Reset pointer state only when the selection changes, not on supplied sample updates. Add a mounted pointer-refresh regression and recheck delayed-target browser states. Change-source: pulse-maintainer --- .../subsystems/frontend-primitives.md | 3 ++- frontend-modern/browser-verification.json | 6 ++--- .../shared/__tests__/HistoryChart.test.tsx | 26 +++++++++++++++++++ .../components/shared/useHistoryChartState.ts | 18 +++++++++++-- 4 files changed, 47 insertions(+), 6 deletions(-) diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index 1465e603f..14db02fad 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -7675,7 +7675,8 @@ metric, range, sampling cap, supplied-data mode or access state changes. Late successes and failures cannot replace the current selection's samples, loading or error state, even if cancellation is ignored. Selection changes clear old readings and hover state while loading; current initial failures remain visible. -Matching background refresh failures retain already loaded samples. Polls never +Matching supplied-sample refreshes retain pointer inspection; changing the selection +clears it. Matching background refresh failures retain already loaded samples. Polls never overlap and stop for supplied data (including empty arrays), unavailable targets, locked ranges and unmount. diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json index ded7b81d0..5342ba6fd 100644 --- a/frontend-modern/browser-verification.json +++ b/frontend-modern/browser-verification.json @@ -1,13 +1,13 @@ { "version": 1, - "base_sha": "9dc6bd0ffe4de520bf9afc43ad10fb169026325d", - "verified_at": "2026-10-02T14:15:24.462877Z", + "base_sha": "93531c9cd3bfc9278633476ba26a1c77e66bc802", + "verified_at": "2026-10-02T14:23:47.145834Z", "result": "passed", "changed_paths": [ "frontend-modern/src/components/shared/useHistoryChartState.ts" ], "content_sha256": { - "frontend-modern/src/components/shared/useHistoryChartState.ts": "ff9dff0db01ae3141b3a130477cb8128ea4da8ea4e93f23c360278f6a47b152a" + "frontend-modern/src/components/shared/useHistoryChartState.ts": "f10b5cfc7cb296c60ad7e87bf7c3001c4308d29af702b8c7a7e77e7c4c4c3fb6" }, "routes": [ "/browser-tests/history-selection.html" diff --git a/frontend-modern/src/components/shared/__tests__/HistoryChart.test.tsx b/frontend-modern/src/components/shared/__tests__/HistoryChart.test.tsx index e8f2f7c27..d90be587f 100644 --- a/frontend-modern/src/components/shared/__tests__/HistoryChart.test.tsx +++ b/frontend-modern/src/components/shared/__tests__/HistoryChart.test.tsx @@ -233,6 +233,32 @@ describe('HistoryChart', () => { rectSpy.mockRestore(); }); + it('keeps pointer inspection when matching supplied samples refresh', () => { + const rectSpy = vi.spyOn(HTMLCanvasElement.prototype, 'getBoundingClientRect').mockReturnValue({ + x: 0, + y: 0, + left: 0, + top: 0, + right: 400, + bottom: 120, + width: 400, + height: 120, + toJSON: () => ({}), + }); + const samples = (value: number) => + [1000, 2000, 3000].map((timestamp) => ({ timestamp, value, min: value, max: value })); + const [data, setData] = createSignal(samples(10)); + const { container } = render(() => ( + + )); + fireEvent.mouseMove(container.querySelector('canvas')!, { clientX: 220 }); + expect(container.querySelector('[data-history-chart-tooltip="true"]')).not.toBeNull(); + setData(samples(20)); + expect(container.querySelector('[data-history-chart-tooltip="true"]')).not.toBeNull(); + expect(container.querySelector('[data-history-chart-tooltip="true"]')).toHaveTextContent('20'); + rectSpy.mockRestore(); + }); + it('exposes the sub-day and Relay history ranges as first-class chart options', () => { expect(HISTORY_CHART_RANGES).toEqual(['1h', '6h', '12h', '24h', '7d', '14d', '30d', '90d']); }); diff --git a/frontend-modern/src/components/shared/useHistoryChartState.ts b/frontend-modern/src/components/shared/useHistoryChartState.ts index e32f39d77..51328aaa4 100644 --- a/frontend-modern/src/components/shared/useHistoryChartState.ts +++ b/frontend-modern/src/components/shared/useHistoryChartState.ts @@ -90,6 +90,8 @@ export function useHistoryChartState( const dataMin = createMemo(() => getHistoryChartDataMin(data())); const dataMax = createMemo(() => getHistoryChartDataMax(data())); + let previousSelection: string | undefined; + // One effect owns a selection, its request and its polling timer. Cleanup // invalidates completions even when a transport ignores cancellation. createEffect(() => { @@ -117,8 +119,20 @@ export function useHistoryChartState( setSource(suppliedData !== undefined ? 'live' : null); setError(null); setLoading(false); - setHoveredPoint(null); - setHoveredTimestamp(null); + const selection = JSON.stringify([ + resourceType, + resourceId, + metric, + chartRange, + pointsCap, + locked, + suppliedData !== undefined, + ]); + if (selection !== previousSelection) { + setHoveredPoint(null); + setHoveredTimestamp(null); + } + previousSelection = selection; if (suppliedData !== undefined || locked || !resourceId || !resourceType) return; const loadData = async () => {