const form = document.querySelector("#archive-form"); const input = document.querySelector("#archive-url"); const button = document.querySelector("#archive-submit"); const progressWrap = document.querySelector("#progress-wrap"); const progressBar = document.querySelector("#progress-bar"); const statusLine = document.querySelector("#status-line"); let pollTimer = null; let visualTimer = null; let startedAt = Date.now(); form.addEventListener("submit", (event) => { event.preventDefault(); submitArchive(input.value); }); const pathUrl = urlFromPath(); if (pathUrl) { input.value = pathUrl; submitArchive(pathUrl); } else { input.focus(); } async function submitArchive(rawUrl) { stopTimers(); setBusy(true); setStatus("Checking", 8); try { const response = await fetch("/api/archives", { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ url: rawUrl }) }); const data = await readApiResponse(response); if (data.archive?.archiveUrl) { openArchive(data.archive.archiveUrl); return; } if (data.job?.id) { watchJob(data.job); return; } throw new Error(data.error || "Archive did not start"); } catch (error) { setError(error.message || "Archive failed"); setBusy(false); } } function watchJob(job) { startedAt = Date.parse(job.startedAt || job.createdAt) || Date.now(); updateFromJob(job); visualTimer = window.setInterval(updateVisualProgress, 250); pollTimer = window.setInterval(async () => { try { const response = await fetch(`/api/jobs/${encodeURIComponent(job.id)}`); const data = await readApiResponse(response); updateFromJob(data.job); } catch (error) { stopTimers(); setError(error.message || "Archive failed"); setBusy(false); } }, 850); } function updateFromJob(job) { if (job.status === "done" && job.archive?.archiveUrl) { stopTimers(); setStatus("Opening", 100); openArchive(job.archive.archiveUrl); return; } if (job.status === "failed") { stopTimers(); setError(job.error || "Archive failed"); setBusy(false); return; } startedAt = Date.parse(job.startedAt || job.createdAt) || startedAt; const elapsed = Math.max(0, Math.round((Date.now() - startedAt) / 1000)); const label = job.status === "queued" ? "Queued" : `Archiving ${elapsed}s`; setStatus(label, optimisticProgress()); } function updateVisualProgress() { if (!progressWrap.hidden) { progressBar.style.width = `${optimisticProgress()}%`; } } function optimisticProgress() { const elapsed = Math.max(0, (Date.now() - startedAt) / 1000); if (elapsed < 1) { return 12; } if (elapsed < 12) { return Math.min(88, 12 + elapsed * 6.3); } return Math.min(96, 88 + (elapsed - 12) * 0.6); } async function readApiResponse(response) { const data = await response.json().catch(() => null); if (!response.ok || data?.ok === false) { throw new Error(data?.error || `Request failed with ${response.status}`); } return data; } function setBusy(isBusy) { button.disabled = isBusy; input.readOnly = isBusy; } function setStatus(text, progress) { progressWrap.hidden = false; statusLine.classList.remove("error"); statusLine.textContent = text; progressBar.style.width = `${Math.max(0, Math.min(100, progress))}%`; } function setError(text) { progressWrap.hidden = false; statusLine.classList.add("error"); statusLine.textContent = text; progressBar.style.width = "100%"; } function stopTimers() { if (pollTimer) { window.clearInterval(pollTimer); pollTimer = null; } if (visualTimer) { window.clearInterval(visualTimer); visualTimer = null; } } function openArchive(archiveUrl) { window.location.assign(archiveUrl); } function urlFromPath() { const rawPath = window.location.pathname.replace(/^\/+/, ""); if (!rawPath || rawPath.startsWith("assets/") || rawPath.startsWith("api/") || rawPath.startsWith("archives/")) { return ""; } let decoded; try { decoded = decodeURIComponent(rawPath); } catch { return ""; } if (!/^https?:\/\//i.test(decoded)) { return ""; } return `${decoded}${window.location.search}${window.location.hash}`; }