Observed on 9.7.0, in a multi-page takeoff document.
Repro:
- Open a multi-page PDF in the takeoff viewer with the page sidebar visible.
- Every page's thumbnail fills in except the currently open page, which shows the spinner over a blank tile indefinitely.
- Navigate to another page: the previously current page's thumbnail now loads (and the new current page's thumbnail gets stuck instead).
Root cause (frontend/src/modules/pdf-takeoff/TakeoffViewerModule.tsx):
- The thumbnail effect (
:1006-1042) renders pages nearest-first (frontend/src/features/takeoff/lib/takeoff-thumbnails.ts:51-64), so it always starts with the current page. That is the one page the main viewer is rendering at the same time (main render: getPage at :963, page.render at :983).
- Any error in a thumbnail render lands in an empty
catch {} (:1030) and the loop moves on. The effect only re-runs when its deps change ([pdfDoc, showThumbnails, currentPage, totalPages]), so a failed current-page render is never retried while you stay on that page.
- The tile shows
thumbs[n] if present, otherwise the spinner (:5027-5033). Since thumbs[currentPage] is never set, the spinner never goes away.
Why the current page specifically: it is the only page rendered concurrently by two pipelines; every other page renders solo and succeeds. The exact rejection path inside pdf.js is not pinned down here, and the diagnosis does not depend on it. Whatever makes the doubly-rendered page's thumbnail render fail, the catch at :1030 swallows the failure and nothing retries it while the page stays current. Navigating away re-runs the effect, the page now renders solo, and its thumbnail appears, which matches the observed behavior.
Suggested fix, cheapest first:
- Do not re-render the current page for its thumbnail at all: when
n === currentPage and the main canvas has rendered, downscale it (drawImage from canvasRef to the offscreen canvas, then toDataURL). This removes the concurrent same-page render entirely and saves a full PDF render for the page already on screen.
- Alternatively, retry the current page after the main render settles (the
catch at :1030 currently drops it silently).
- Alternatively, render thumbnails from a second
getDocument() instance so the two pipelines never share page state.
Observed on 9.7.0, in a multi-page takeoff document.
Repro:
Root cause (
frontend/src/modules/pdf-takeoff/TakeoffViewerModule.tsx)::1006-1042) renders pages nearest-first (frontend/src/features/takeoff/lib/takeoff-thumbnails.ts:51-64), so it always starts with the current page. That is the one page the main viewer is rendering at the same time (main render:getPageat:963,page.renderat:983).catch {}(:1030) and the loop moves on. The effect only re-runs when its deps change ([pdfDoc, showThumbnails, currentPage, totalPages]), so a failed current-page render is never retried while you stay on that page.thumbs[n]if present, otherwise the spinner (:5027-5033). Sincethumbs[currentPage]is never set, the spinner never goes away.Why the current page specifically: it is the only page rendered concurrently by two pipelines; every other page renders solo and succeeds. The exact rejection path inside pdf.js is not pinned down here, and the diagnosis does not depend on it. Whatever makes the doubly-rendered page's thumbnail render fail, the
catchat:1030swallows the failure and nothing retries it while the page stays current. Navigating away re-runs the effect, the page now renders solo, and its thumbnail appears, which matches the observed behavior.Suggested fix, cheapest first:
n === currentPageand the main canvas has rendered, downscale it (drawImagefromcanvasRefto the offscreen canvas, thentoDataURL). This removes the concurrent same-page render entirely and saves a full PDF render for the page already on screen.catchat:1030currently drops it silently).getDocument()instance so the two pipelines never share page state.