const form = document.querySelector("#uploadForm"); const pdfInput = document.querySelector("#pdfInput"); const fileName = document.querySelector("#fileName"); const startButton = document.querySelector("#startButton"); const statusText = document.querySelector("#statusText"); const batchText = document.querySelector("#batchText"); const progressBar = document.querySelector("#progressBar"); const jobIdText = document.querySelector("#jobIdText"); const pageCounter = document.querySelector("#pageCounter"); const tabCounter = document.querySelector("#tabCounter"); const messageText = document.querySelector("#messageText"); const downloadLink = document.querySelector("#downloadLink"); const downloadCsvLink = document.querySelector("#downloadCsvLink"); const previewBody = document.querySelector("#previewBody"); const rowCount = document.querySelector("#rowCount"); const browserViewStatus = document.querySelector("#browserViewStatus"); const browserViewImage = document.querySelector("#browserViewImage"); const browserViewEmpty = document.querySelector("#browserViewEmpty"); let pollTimer = null; pdfInput.addEventListener("change", () => { fileName.textContent = pdfInput.files[0]?.name || "Choose a bank statement PDF"; }); form.addEventListener("submit", async (event) => { event.preventDefault(); clearInterval(pollTimer); downloadLink.hidden = true; downloadCsvLink.hidden = true; startButton.disabled = true; setStatus({ id: "", status: "uploading", message: "Uploading PDF", progress: 2, completed_batches: 0, total_batches: 0 }); const formData = new FormData(form); try { const response = await fetch("/api/jobs", { method: "POST", body: formData }); if (!response.ok) throw new Error(await readError(response)); const payload = await response.json(); pollJob(payload.job_id); pollTimer = setInterval(() => pollJob(payload.job_id), 2500); } catch (error) { setStatus({ status: "error", message: error.message, progress: 100, completed_batches: 0, total_batches: 0 }); startButton.disabled = false; } }); async function pollJob(jobId) { try { const response = await fetch(`/api/jobs/${jobId}`); if (!response.ok) throw new Error(await readError(response)); const job = await response.json(); setStatus(job); updateBrowserView(job); if (job.status === "done") { clearInterval(pollTimer); startButton.disabled = false; downloadLink.hidden = false; downloadLink.href = `/api/jobs/${jobId}/download`; downloadCsvLink.hidden = false; downloadCsvLink.href = `/api/jobs/${jobId}/download.csv`; renderPreview(job.data_preview || []); } if (job.status === "error") { clearInterval(pollTimer); startButton.disabled = false; } } catch (error) { clearInterval(pollTimer); startButton.disabled = false; setStatus({ status: "error", message: error.message, progress: 100, completed_batches: 0, total_batches: 0 }); } } function setStatus(job) { statusText.textContent = titleCase(job.status || "ready"); jobIdText.textContent = job.id ? `Job ${job.id}` : "No job started"; pageCounter.textContent = `${job.completed_pages || 0} / ${job.total_pages || 0} pages`; tabCounter.textContent = `${job.active_tabs || 0} active tabs`; const pageText = job.current_pages ? ` Current pages: ${job.current_pages}.` : ""; messageText.textContent = `${job.error || job.message || ""}${pageText}`; batchText.textContent = `${job.completed_batches || 0} / ${job.total_batches || 0} waves`; progressBar.style.width = `${Math.max(0, Math.min(job.progress || 0, 100))}%`; } function updateBrowserView(job) { if (!job.id || !job.latest_screenshot) { browserViewStatus.textContent = "Waiting"; return; } browserViewStatus.textContent = "Latest screenshot"; browserViewImage.hidden = false; browserViewEmpty.hidden = true; browserViewImage.src = `/api/jobs/${job.id}/browser-view/latest?t=${Date.now()}`; } function renderPreview(rows) { rowCount.textContent = rows.length ? `First ${rows.length} rows` : "No rows returned"; if (!rows.length) { previewBody.innerHTML = '