From b313e20bade4def9b8c23d32410278e0333d642c Mon Sep 17 00:00:00 2001 From: cesnimda Date: Sat, 24 Oct 2026 23:31:58 +0200 Subject: [PATCH] fix: keep preview retry controls stationary during pointer activation --- .gitattributes | 3 ++- docs/redesign/phase-9/p9-remed-02/COMMANDS.md | 26 +++++++++++++++++++ docs/redesign/phase-9/p9-remed-02/PLAN.md | 7 +++++ .../phase-9/p9-remed-02/SOURCE_REVIEW.md | 13 ++++++++++ .../e2e/builder-preview-keyboard.spec.ts | 6 ++++- job-tracker-ui/e2e/cv-readiness.spec.ts | 6 +++-- .../src/cv-builder-deep-link.test.tsx | 20 ++++++++++++++ job-tracker-ui/src/views/CvBuilderEditor.tsx | 2 ++ 8 files changed, 79 insertions(+), 4 deletions(-) create mode 100644 docs/redesign/phase-9/p9-remed-02/COMMANDS.md create mode 100644 docs/redesign/phase-9/p9-remed-02/SOURCE_REVIEW.md diff --git a/.gitattributes b/.gitattributes index f6de2fa..256866d 100644 --- a/.gitattributes +++ b/.gitattributes @@ -7,4 +7,5 @@ /docs/audits/2026-09-07/validation/evidence/*.csv binary # Preserve vendored font/license bytes across Windows and Linux checkouts. JobTrackerApi/DocumentFonts/*.ttf -text -JobTrackerApi/DocumentFonts/*.txt text eol=lf +# Upstream notices retain their exact bytes, including original trailing spaces. +JobTrackerApi/DocumentFonts/*.txt text eol=lf -whitespace diff --git a/docs/redesign/phase-9/p9-remed-02/COMMANDS.md b/docs/redesign/phase-9/p9-remed-02/COMMANDS.md new file mode 100644 index 0000000..09f8843 --- /dev/null +++ b/docs/redesign/phase-9/p9-remed-02/COMMANDS.md @@ -0,0 +1,26 @@ +# Reproduction commands and boundaries + +Use the dedicated development branch, pinned .NET9.0.317 SDK at `C:/Users/Connor/.dotnet/dotnet.exe` with DOTNET_ROOT, existing frontend lock and Python3.12 sidecar environment. Keep every invocation in a fresh output directory. Do not run build/test workloads concurrently with browser or sustained performance qualification. No command targets production. + +## Linux PDF + +1. Build the existing API Dockerfile locally: `rtk proxy docker build -f JobTrackerApi/Dockerfile -t jobtracker-p9-remed-api:fonts .`. Record build log, actual image identity, runtime/browser versions and input hashes. The qualified renderer uses Debian12 Chromium156.0.8078.47/.NET9.0.21 with embedded static fonts; do not silently substitute a newer renderer and reuse these results. +2. Start only the pinned disposable MariaDB12.2.2 integration service from `docker-compose.database.yml`, Compose project `jobtracker-it-p9-requal`, integration profile, MARIADB_TEST_PORT33308, with `up -d --wait mariadb-test`. Preserve the distinct stopped development service/volume. The existing runner verifies pin, tmpfs ownership, health, version and loopback binding. +3. From repository root run `rtk proxy python docs/redesign/phase-9/p9-remed-02/linux-run.py `. It reuses the qualified lifecycle and overrides only output, image and diagnostic identity/configuration. `baseline` selects the historical image; use another unique name for the remediation. It executes the five original producers representing all24 original fixture states, using the Windows Chromium preview client and Linux API renderer. It verifies51 actual migrations and removes its owned API container/private network/generated schema. No earlier evidence is overwritten. +4. Run the unchanged collector with bundled PDF tooling: + +```powershell +rtk proxy -B docs/redesign/phase-9/p9-requal-01/pdf-proof.py --platform linux --fidelity-root .artifacts/p9-remed-02//linux/fidelity --settings-root .artifacts/p9-remed-02//linux/settings-fidelity --output .artifacts/p9-remed-02//oracle --record .artifacts/p9-remed-02//linux-pdf.json +``` + +The existing oracle checks actual independently rendered page images, extracted markers/geometry/fonts/colors/safe areas and exact preview/export pagination. The collector retains the original16/36-page and8/24-page corpora and exact eight settings states. Neither oracle nor fixture references/tolerances were changed. The existing `test-linux-run.py` and `test-mariadb-lane.py` are offline lifecycle guards, never real provider qualification. + +Final local packaging image0d1cb44 has the same render-DLL SHA256 as qualified image98ccb723; only the standalone provenance manifest was completed. See raw image-lineage.json. These are local image identities, not asserted pullable registry manifest references. No image was pushed or deployed. + +## Regression and security + +Reuse [the full lane commands](../p9-requal-01/COMMANDS.md) with new `.artifacts/p9-remed-02/` outputs. Run backend and real provider tests before browser servers to avoid apphost locks. Finish frontend/type/lint/build before serial browser discovery and four zero-retry shards, then the six separate contracts, RD21/diagnostic/volume and isolated MariaDB browser/workload lanes. Do not add focused RED/GREEN checks to full totals. The first95/3 shard remains a failed predecessor, not accepted final execution. + +Python exact lock/patch commands and actual startup/parser limits are in [security remediation](security/REMEDIATION.md); all109 advisory dispositions remain in its inventory. No model download or inference is part of these commands. Existing security helpers, tracked-secret scan and SBOM are separate from advisory closure. + +Remove only the owned disposable database service after all lanes, with the same Compose file/project/profile and `rm --stop --force mariadb-test`. Never use `down -v`, prune development data, expose a host daemon in a job or connect tests to production. Hosted execution remains blocked pending the [operator plan](CI_ISOLATION.md). diff --git a/docs/redesign/phase-9/p9-remed-02/PLAN.md b/docs/redesign/phase-9/p9-remed-02/PLAN.md index 6a686d2..b8c0e67 100644 --- a/docs/redesign/phase-9/p9-remed-02/PLAN.md +++ b/docs/redesign/phase-9/p9-remed-02/PLAN.md @@ -29,3 +29,10 @@ PDF font trial: embeddedvariablefonts makeall24preview/exportpagesagree36+24 and Review ruling: discard the uncommitted experimental dynamic PDF collector and its2offlineguards; reuse prior exact collector unchanged, including8settings-state membership and historical36+24 totals. The static font fix must satisfy that stronger existing gate; no reference/tolerance change. P9-PDF-02/03 staticcheckpoint: Linux5producerPASS; unchangedoriginaloracle16/16+8/8PASS,36+24pages,zero inputdrift. OriginaloracleSHA25664f82e622cc11483c33307d987e969f6cf27f82ab6bdbd72c2aeb1bfbceade87 unchanged. Metadatareviewclosed37/37font+licensehasheswithimmutableURLs; .gitattributespreservesasset/licensebytesacrossOS. Fullbackend1130PASS. Noexpectations/tolerances/referencechanges. CompleteWindows/broadregressionstillpending. + +Firstbroadregression completedwithFAIL: ordinaryshard1=95PASS/3FAIL (previewretry light/en; stale-CVmanual-save button race;30sadminmatrixdeadline), zerohiddenretries. Otherordinaryshards/contractsnotstartedbecausefail-closedserialrunner. Frontend934/105PASS (1059s). Traceprovespreviewretry click atviewporty383 causesparentonFocusCapture RAFscrollpage190pxbeforemouseup; buttonmoves toy193, soReactonclickneverfiresandNO retryHTTPrequestoccurs. Error/fail screenshotsconfirm190pxscroll. Minimalsharedownerfixwillscrollpaneonlywhenregionitselfisfocused, retainingmobilepanestateforallfocus. Addheld-pointer RAFregressionbeforefix; no blankettimeoutchange. + +Previewpointerfix RED confirmedby1newfrontendunit (45nonselectedexistingcasesnotfullrun) anddeterministicheld-pointeroriginalbrowsercase1/1FAIL onpublishedstaticUI. SharedPaperfocusguardonlyautoscrollsdirectregionfocus, preservingallmobilepanefocusstate. Unitwholefile46caseGREENpending. Stale-CVfixturetracealreadycontainsPUT409andvisibleconflict; replaceitsracySaveNowclickswithobservedautosave409/200responses, keepingdraftdownload,remoteversion/history/stateassertionsunchanged. Adminmatrix30sdeadlinewasundercompetingfrontend/buildworkloads; isolatebeforechanginganybudget. FinalregressionwillrunCPU-heavybuild/tests/browsersserially. Firstfailed95/3shardand934/105frontendresultremainhistoricalattempts, notacceptedfinaltotals. +FinalLinuxfontproof24/24+fullnormalizedHTMLbodytextpresence24/24 andindependentselected4reprints exactglyphgeometry/pagepixelsPASS. InitialNFCcontentdiagnostic8/24failedbecauseCSSuppercase/ligatures; retainedthenNFKC/casefold/whitespacecomparisonexplicitlydoesnotproveoriginalcasefidelity. InitialrepeatfailedbeforeprintwhenoldtaggedimageIDcouldnotbereresolvedafterreplacement; newpackagingimage0d1cb44sameRenderDLLSHAas98ccb723provenimage-lineage.json. Selected4reprintsuccessorPASS,nochangingPDFtimestampbyteequalityclaim. + +PointercheckpointGREEN: editorunitfile46/46PASS; isolatednative6/6PASS=4held-pointertheme/languagevectors+staledraftrecovery+unchangedadminmatrix17.6swithin30s. No timeoutincrease. Adminpredecessordeadlinefailureunreproducedinisolation; nocausalperformanceSLAclaim. Fullsuccessor935frontend/362browserplusotherlanesremainpending; focused6notaddedtowholetotals. diff --git a/docs/redesign/phase-9/p9-remed-02/SOURCE_REVIEW.md b/docs/redesign/phase-9/p9-remed-02/SOURCE_REVIEW.md new file mode 100644 index 0000000..6845a29 --- /dev/null +++ b/docs/redesign/phase-9/p9-remed-02/SOURCE_REVIEW.md @@ -0,0 +1,13 @@ +# Independent source review + +Reviewer: separate AI code-review agent. Baseline3727a72 through published e80815f and the subsequent working-tree focus/fixture delta. This is source review, not human acceptance or runtime approval. The reviewer ran no tests, builds, browsers, databases or service commands. + +The review covered the shared document-font renderer, resource/publish configuration, original font/license bytes, Python dependency and advisory inventory, CI isolation documentation, and connected persistence/normalization owners. No Critical or Important application-code finding was reported. Canonical data, revisions, settings/null inheritance and frozen artifact writers remain at their established owners. + +Two evidence findings were resolved: an uncommitted experimental collector did not retain exact eight-state settings membership, so it and its test were deleted and the existing collector/oracle reused unchanged; two license entries and some per-file sources were absent from font provenance, so the manifest was completed. The reviewer verified all37 font/license source/hash entries and confirmed no bundled assets were omitted. The whitespace exception is limited to byte-preserved upstream notices. + +The reviewer verified all109 advisory rows contain the required fields and the exact current lock hash matches the inventory:65 groups remediated,44 retained (three cache-lock and41 model-runtime groups). No owner risk acceptance is inferred. CI changes remain documentation only. + +The final delta review found no Critical or Important finding in the preview focus guard or fixture changes. Descendant controls retain their pointer positions; mobile-pane selection remains and direct preview-region focus still scrolls its pane. The native regression now holds pointer-down through layout frames. The stale-draft fixture observes real autosave409/200 responses while retaining its draft/download/version/history assertions. + +Runtime proof is owned by the qualification ledger. Source review does not certify PDF fidelity, full model inference, hosted isolation, accessibility compliance, physical printing or production readiness. diff --git a/job-tracker-ui/e2e/builder-preview-keyboard.spec.ts b/job-tracker-ui/e2e/builder-preview-keyboard.spec.ts index 397da1f..375e632 100644 --- a/job-tracker-ui/e2e/builder-preview-keyboard.spec.ts +++ b/job-tracker-ui/e2e/builder-preview-keyboard.spec.ts @@ -192,7 +192,11 @@ for (const [mode, language] of [["light", "en"], ["dark", "nb"], ["light", "nb"] await expect(frame.getByText("Reviewed CV-only æøå", { exact: true })).toBeVisible(); await page.getByText(t.cvEditorPreviewUnavailable, { exact: true }).evaluate(node => node.scrollIntoView({ block: "center" })); await capture(page, info, `error-${suffix}`); - await page.getByRole("button", { name: t.cvEditorRetryPreview, exact: true }).click(); + // A held pointer must keep its target even if focus handlers schedule a layout frame. + await page.getByRole("button", { name: t.cvEditorRetryPreview, exact: true }).hover(); + await page.mouse.down(); + await page.evaluate(() => new Promise(resolve => requestAnimationFrame(() => requestAnimationFrame(() => resolve())))); + await page.mouse.up(); await expect(frame.getByText("Canonical engineer", { exact: true })).toBeVisible(); await expect(page.getByText(t.cvEditorPreviewUnavailable, { exact: true })).not.toBeVisible(); await page.getByRole("button", { name: t.cvEditorRedo, exact: true }).click(); diff --git a/job-tracker-ui/e2e/cv-readiness.spec.ts b/job-tracker-ui/e2e/cv-readiness.spec.ts index 7720f42..9fc441a 100644 --- a/job-tracker-ui/e2e/cv-readiness.spec.ts +++ b/job-tracker-ui/e2e/cv-readiness.spec.ts @@ -181,8 +181,9 @@ test("[PRES-C06 C10 E08] stale editor retains and downloads its draft; reviewed await expect(headline).toBeVisible(); const remote = await page.request.put(`${apiUrl}/cv/variants/${cv.id}`, { headers, data: { expectedVersion: cv.version, settings: { ...cv.settings, headline: "Remote reviewed edit" } } }); expect(remote.status()).toBe(200); + const rejectedSave = page.waitForResponse(response => response.url() === `${apiUrl}/cv/variants/${cv.id}` && response.request().method() === "PUT" && response.status() === 409); await headline.fill("Local recoverable draft æøå"); - await page.getByRole("button", { name: "Save now", exact: true }).click(); + expect((await rejectedSave).status()).toBe(409); await expect(page.getByText("CV changed elsewhere", { exact: true })).toBeVisible(); await expect(headline).toHaveValue("Local recoverable draft æøå"); const downloadPromise = page.waitForEvent("download"); @@ -201,8 +202,9 @@ test("[PRES-C06 C10 E08] stale editor retains and downloads its draft; reviewed await page.getByRole("dialog").getByRole("button", { name: "Reload saved CV", exact: true }).click(); await expect(page.getByRole("dialog")).toHaveCount(0); await expect(headline).toHaveValue("Remote reviewed edit"); + const reviewedSave = page.waitForResponse(response => response.url() === `${apiUrl}/cv/variants/${cv.id}` && response.request().method() === "PUT" && response.status() === 200); await headline.fill("Reviewed combined edit"); - await page.getByRole("button", { name: "Save now", exact: true }).click(); + expect((await reviewedSave).status()).toBe(200); await expect(page.getByText("Saved", { exact: true })).toBeVisible(); expect((await (await page.request.get(`${apiUrl}/cv/variants/${cv.id}`)).json()).version).toBe(3); await page.getByRole("tab", { name: "History", exact: true }).click(); diff --git a/job-tracker-ui/src/cv-builder-deep-link.test.tsx b/job-tracker-ui/src/cv-builder-deep-link.test.tsx index 08d7b7e..29f9f16 100644 --- a/job-tracker-ui/src/cv-builder-deep-link.test.tsx +++ b/job-tracker-ui/src/cv-builder-deep-link.test.tsx @@ -701,6 +701,26 @@ test('profile-backed sections expand, reorder, hide and persist variant-only ove expect(await screen.findByText('Saved')).toBeInTheDocument(); }); +test('focusing a preview retry control keeps its pointer target stationary', async () => { + routeGet(() => Promise.resolve({ data: variant } as any)); + mockedApi.post.mockRejectedValueOnce(new Error('preview offline')); + renderAt(3); + const retry = await screen.findByRole('button', { name: 'Retry preview' }); + const region = screen.getByRole('region', { name: 'Live preview' }); + const scroll = jest.fn(); + region.parentElement!.scrollIntoView = scroll; + await act(async () => { + retry.focus(); + await new Promise(resolve => requestAnimationFrame(() => resolve())); + }); + expect(scroll).not.toHaveBeenCalled(); + await act(async () => { + region.focus(); + await new Promise(resolve => requestAnimationFrame(() => resolve())); + }); + expect(scroll).toHaveBeenCalled(); +}); + test('preview failure is visible and retryable without leaving the editor', async () => { routeGet(() => Promise.resolve({ data: variant } as any)); mockedApi.post.mockRejectedValueOnce(new Error('preview offline')).mockResolvedValueOnce({ data: { themeId: 'modern', html: '

retry

', suggestedFileName: 'cv.pdf' } } as any); diff --git a/job-tracker-ui/src/views/CvBuilderEditor.tsx b/job-tracker-ui/src/views/CvBuilderEditor.tsx index bd21d28..5544ca7 100644 --- a/job-tracker-ui/src/views/CvBuilderEditor.tsx +++ b/job-tracker-ui/src/views/CvBuilderEditor.tsx @@ -625,6 +625,8 @@ export default function CvBuilderEditor() { { setMobilePane("preview"); + // Moving a pointer-focused control here can cancel its pending click. + if (event.target !== scrollRef.current) return; const pane = event.currentTarget; requestAnimationFrame(() => { if (pane.contains(document.activeElement)) pane.scrollIntoView?.({ block: "start", inline: "nearest" });