diff --git a/web/app.js b/web/app.js index 4830f5d..62a6866 100644 --- a/web/app.js +++ b/web/app.js @@ -2,6 +2,7 @@ let state; let groups = []; let selectedIndex = -1; let pdfViewer = null; +let currentPdfKey = null; let loadRequestId = 0; const people = document.querySelector("#people"); @@ -112,69 +113,23 @@ function renderList() { updateStatus(shown.length); } -async function loadPdf(index) { +async function openDocument(index, { force = false } = {}) { const doc = state.documents[index]; if (!doc) return; - // Deduplicate: don't reload the same document - if (index === selectedIndex && pdfViewer) { + const key = `${doc._letter}/${doc.file_name}`; + + // Deduplicate: ignore click on already-loaded document unless forced + if (!force && key === currentPdfKey) { return; } - if (pdfViewer) { - pdfViewer.destroy(); - } - pdfViewer = new PdfViewer(pdfContainer); - viewer.classList.remove("loaded"); - pdfMessage.hidden = false; - pdfMessage.textContent = "Loading PDF\u2026"; - - const reqId = "pdf-" + (++loadRequestId); - console.log( - `[BizMatch QC] PDF ${reqId} selection: ${doc._letter}/${doc.file_name}`, - ); - console.log(`[BizMatch QC] PDF ${reqId} prepare requested`); - - const startTime = performance.now(); - try { - const response = await fetch("/api/pdf/prepare", { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify({ index, requestId: reqId }), - }); - if (!response.ok) { - const body = await response.json().catch(() => ({})); - throw new Error(body.error || `Prepare failed: HTTP ${response.status}`); - } - const data = await response.json(); - const prepareMs = Math.round(performance.now() - startTime); - console.log( - `[BizMatch QC] PDF ${reqId} prepare: ${prepareMs}ms, disk cache ${data.cacheStatus}`, - ); - - if (index !== selectedIndex) { - // A newer selection happened while preparing, don't show stale result - return; - } - - viewer.classList.add("loaded"); - pdfMessage.hidden = true; - console.log(`[BizMatch QC] PDF ${reqId} viewer load started`); - await pdfViewer.load(data.url, data.size); - console.log(`[BizMatch QC] PDF ${reqId} rendering complete`); - } catch (error) { - if (index !== selectedIndex) return; - const message = error instanceof Error ? error.message : String(error); - pdfMessage.textContent = `Cannot open PDF: ${message}`; - pdfMessage.hidden = false; - } -} - -function select(index) { + // Mark as loading this document + currentPdfKey = key; selectedIndex = index; - const doc = state.documents[index]; - if (!doc) return; + renderList(); + // Update detail panel let rowIdx = 0; fields.innerHTML = `

${esc(doc.name_from_filename)}

@@ -202,8 +157,64 @@ function select(index) { }).join("") } `; - void loadPdf(index); - renderList(); + + // Load PDF + if (pdfViewer) { + pdfViewer.destroy(); + } + pdfViewer = new PdfViewer(pdfContainer); + viewer.classList.remove("loaded"); + pdfMessage.hidden = false; + pdfMessage.textContent = "Loading PDF\u2026"; + + const reqId = "pdf-" + (++loadRequestId); + console.log( + `[BizMatch QC] Document selected: ${key}`, + ); + console.log(`[BizMatch QC] PDF prepare starting: ${reqId}`); + + const startTime = performance.now(); + try { + const response = await fetch("/api/pdf/prepare", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ index, requestId: reqId }), + }); + if (!response.ok) { + const body = await response.json().catch(() => ({})); + throw new Error(body.error || `Prepare failed: HTTP ${response.status}`); + } + const data = await response.json(); + const prepareMs = Math.round(performance.now() - startTime); + console.log( + `[BizMatch QC] PDF prepare: ${prepareMs}ms, disk cache ${data.cacheStatus}`, + ); + console.log(`[BizMatch QC] PDF viewer URL received: ${reqId}`); + + if (key !== currentPdfKey) { + // A newer document was selected while preparing + return; + } + + viewer.classList.add("loaded"); + pdfMessage.hidden = true; + await pdfViewer.load(data.url, data.size); + console.log(`[BizMatch QC] PDF ${reqId} rendering complete`); + } catch (error) { + if (key !== currentPdfKey) return; + const message = error instanceof Error ? error.message : String(error); + pdfMessage.textContent = `Cannot open PDF: ${message}`; + pdfMessage.hidden = false; + } +} + +function select(index) { + openDocument(index); +} + +// Exposed for future retry from viewer; use openDocument(_, {force:true}) +function _selectForced(index) { + openDocument(index, { force: true }); } people.addEventListener("click", (event) => { diff --git a/web/pdf_viewer.js b/web/pdf_viewer.js index 570de0c..d59d622 100644 --- a/web/pdf_viewer.js +++ b/web/pdf_viewer.js @@ -39,10 +39,8 @@ class PdfViewer { } _setState(state) { - this.loadingDiv.hidden = true; - this.errorDiv.hidden = true; - if (state === "loading") this.loadingDiv.hidden = false; - if (state === "error") this.errorDiv.hidden = false; + this.loadingDiv.hidden = state !== "loading"; + this.errorDiv.hidden = state !== "error"; this._viewerState = state; } @@ -59,7 +57,6 @@ class PdfViewer { `[BizMatch QC] PDF viewer load started: generation ${generation}`, ); - // Await cleanup of previous document (promises may be involved) await this._disposeCurrentDocument(); if (generation !== this.generation) return; @@ -90,14 +87,14 @@ class PdfViewer { const viewerWidth = this._getViewportWidth(); console.log(`[BizMatch QC] PDF usable render width: ${viewerWidth}px`); - const pageStart = performance.now(); + // Render page 1 -- MUST succeed before we show ready state await this._renderPage(doc, 1, viewerWidth, generation); if (generation !== this.generation) return; - - this._setState(null); // Remove loading overlay - const firstPageMs = Math.round(performance.now() - pageStart); + this._setState("ready"); console.log( - `[BizMatch QC] PDF page 1 rendered in ${firstPageMs}ms`, + `[BizMatch QC] PDF page 1 rendered in ${ + Math.round(performance.now() - loadStart) + }ms`, ); console.log( `[BizMatch QC] First page visible after ${ @@ -127,15 +124,12 @@ class PdfViewer { ); } catch (err) { if (generation !== this.generation) return; - // Distinguish cancellation from real errors if (isCancellationError(err)) { - console.log( - `[BizMatch QC] PDF generation ${generation} cancelled`, - ); + console.log(`[BizMatch QC] PDF generation ${generation} cancelled`); return; } const msg = errorMsg(err); - console.error(`[BizMatch QC] PDF load failed:`, msg); + console.error(`[BizMatch QC] PDF load failed:`, msg, err?.stack); this.errorMsg.textContent = `Unable to open PDF: ${msg}`; this._setState("error"); } @@ -197,19 +191,20 @@ class PdfViewer { const fitScale = viewerWidth / vp1.width; const viewport = page.getViewport({ scale: fitScale }); - const dpr = Math.min(globalThis.devicePixelRatio || 1, 2); + // Use DPR 1 for initial diagnosis; restore DPR scaling after content renders + const dpr = 1; canvas.width = Math.floor(viewport.width * dpr); canvas.height = Math.floor(viewport.height * dpr); canvas.style.width = `${Math.floor(viewport.width)}px`; canvas.style.height = `${Math.floor(viewport.height)}px`; const ctx = canvas.getContext("2d"); - const transform = dpr !== 1 ? [dpr, 0, 0, dpr, 0, 0] : undefined; + if (!ctx) throw new Error("2D canvas context is unavailable"); const renderTask = page.render({ - canvasContext: ctx, + canvas, viewport, - transform, + transform: undefined, }); this.renderTasks.add(renderTask); @@ -220,21 +215,27 @@ class PdfViewer { pageDiv.dataset.renderState = "rendered"; } catch (err) { if (generation !== this.generation) return; - if (isCancellationError(err)) return; - pageDiv.dataset.renderState = "error"; + if (isCancellationError(err)) throw err; + console.error( - `[BizMatch QC] Failed to render PDF page ${pageNum}`, + `[BizMatch QC] Page ${pageNum} render failed`, err, + err?.stack, ); + if (pageNum === 1) { - this.errorMsg.textContent = `Unable to open PDF: ${errorMsg(err)}`; - this._setState("error"); - } else { - pageDiv.textContent = `Error rendering page ${pageNum}`; - pageDiv.style.padding = "20px"; - pageDiv.style.color = "#f88"; - pageDiv.style.textAlign = "center"; + throw new Error( + `Failed to render page 1: ${errorMsg(err)}`, + { cause: err }, + ); } + + // Inline error for later pages + pageDiv.textContent = `Error rendering page ${pageNum}`; + pageDiv.style.padding = "20px"; + pageDiv.style.color = "#f88"; + pageDiv.style.textAlign = "center"; + pageDiv.dataset.renderState = "error"; } }