This commit is contained in:
2026-07-15 18:40:04 -05:00
parent 5c69c40453
commit 84b7ee0a47
2 changed files with 98 additions and 86 deletions

View File

@@ -2,6 +2,7 @@ let state;
let groups = []; let groups = [];
let selectedIndex = -1; let selectedIndex = -1;
let pdfViewer = null; let pdfViewer = null;
let currentPdfKey = null;
let loadRequestId = 0; let loadRequestId = 0;
const people = document.querySelector("#people"); const people = document.querySelector("#people");
@@ -112,69 +113,23 @@ function renderList() {
updateStatus(shown.length); updateStatus(shown.length);
} }
async function loadPdf(index) { async function openDocument(index, { force = false } = {}) {
const doc = state.documents[index]; const doc = state.documents[index];
if (!doc) return; if (!doc) return;
// Deduplicate: don't reload the same document const key = `${doc._letter}/${doc.file_name}`;
if (index === selectedIndex && pdfViewer) {
// Deduplicate: ignore click on already-loaded document unless forced
if (!force && key === currentPdfKey) {
return; return;
} }
if (pdfViewer) { // Mark as loading this document
pdfViewer.destroy(); currentPdfKey = key;
}
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) {
selectedIndex = index; selectedIndex = index;
const doc = state.documents[index]; renderList();
if (!doc) return;
// Update detail panel
let rowIdx = 0; let rowIdx = 0;
fields.innerHTML = ` fields.innerHTML = `
<h2>${esc(doc.name_from_filename)}</h2> <h2>${esc(doc.name_from_filename)}</h2>
@@ -202,8 +157,64 @@ function select(index) {
}).join("") }).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) => { people.addEventListener("click", (event) => {

View File

@@ -39,10 +39,8 @@ class PdfViewer {
} }
_setState(state) { _setState(state) {
this.loadingDiv.hidden = true; this.loadingDiv.hidden = state !== "loading";
this.errorDiv.hidden = true; this.errorDiv.hidden = state !== "error";
if (state === "loading") this.loadingDiv.hidden = false;
if (state === "error") this.errorDiv.hidden = false;
this._viewerState = state; this._viewerState = state;
} }
@@ -59,7 +57,6 @@ class PdfViewer {
`[BizMatch QC] PDF viewer load started: generation ${generation}`, `[BizMatch QC] PDF viewer load started: generation ${generation}`,
); );
// Await cleanup of previous document (promises may be involved)
await this._disposeCurrentDocument(); await this._disposeCurrentDocument();
if (generation !== this.generation) return; if (generation !== this.generation) return;
@@ -90,14 +87,14 @@ class PdfViewer {
const viewerWidth = this._getViewportWidth(); const viewerWidth = this._getViewportWidth();
console.log(`[BizMatch QC] PDF usable render width: ${viewerWidth}px`); 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); await this._renderPage(doc, 1, viewerWidth, generation);
if (generation !== this.generation) return; if (generation !== this.generation) return;
this._setState("ready");
this._setState(null); // Remove loading overlay
const firstPageMs = Math.round(performance.now() - pageStart);
console.log( 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( console.log(
`[BizMatch QC] First page visible after ${ `[BizMatch QC] First page visible after ${
@@ -127,15 +124,12 @@ class PdfViewer {
); );
} catch (err) { } catch (err) {
if (generation !== this.generation) return; if (generation !== this.generation) return;
// Distinguish cancellation from real errors
if (isCancellationError(err)) { if (isCancellationError(err)) {
console.log( console.log(`[BizMatch QC] PDF generation ${generation} cancelled`);
`[BizMatch QC] PDF generation ${generation} cancelled`,
);
return; return;
} }
const msg = errorMsg(err); 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.errorMsg.textContent = `Unable to open PDF: ${msg}`;
this._setState("error"); this._setState("error");
} }
@@ -197,19 +191,20 @@ class PdfViewer {
const fitScale = viewerWidth / vp1.width; const fitScale = viewerWidth / vp1.width;
const viewport = page.getViewport({ scale: fitScale }); 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.width = Math.floor(viewport.width * dpr);
canvas.height = Math.floor(viewport.height * dpr); canvas.height = Math.floor(viewport.height * dpr);
canvas.style.width = `${Math.floor(viewport.width)}px`; canvas.style.width = `${Math.floor(viewport.width)}px`;
canvas.style.height = `${Math.floor(viewport.height)}px`; canvas.style.height = `${Math.floor(viewport.height)}px`;
const ctx = canvas.getContext("2d"); 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({ const renderTask = page.render({
canvasContext: ctx, canvas,
viewport, viewport,
transform, transform: undefined,
}); });
this.renderTasks.add(renderTask); this.renderTasks.add(renderTask);
@@ -220,21 +215,27 @@ class PdfViewer {
pageDiv.dataset.renderState = "rendered"; pageDiv.dataset.renderState = "rendered";
} catch (err) { } catch (err) {
if (generation !== this.generation) return; if (generation !== this.generation) return;
if (isCancellationError(err)) return; if (isCancellationError(err)) throw err;
pageDiv.dataset.renderState = "error";
console.error( console.error(
`[BizMatch QC] Failed to render PDF page ${pageNum}`, `[BizMatch QC] Page ${pageNum} render failed`,
err, err,
err?.stack,
); );
if (pageNum === 1) { if (pageNum === 1) {
this.errorMsg.textContent = `Unable to open PDF: ${errorMsg(err)}`; throw new Error(
this._setState("error"); `Failed to render page 1: ${errorMsg(err)}`,
} else { { cause: err },
pageDiv.textContent = `Error rendering page ${pageNum}`; );
pageDiv.style.padding = "20px";
pageDiv.style.color = "#f88";
pageDiv.style.textAlign = "center";
} }
// 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";
} }
} }