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";
}
}