import { Elysia, t } from "elysia"; import { BaseHtml } from "../components/base"; import { Header } from "../components/header"; import db from "../db/db"; import { Filename, Jobs } from "../db/types"; import { ALLOW_UNAUTHENTICATED, WEBROOT } from "../helpers/env"; import { DownloadIcon } from "../icons/download"; import { DeleteIcon } from "../icons/delete"; import { EyeIcon } from "../icons/eye"; import { ShareIcon } from "../icons/share"; import { userService } from "./user"; import { outputDir } from ".."; import { sendFileToErugo } from "../helpers/erugo"; type JobsWithOptionalNumFiles = Jobs & { num_files?: number; }; function ResultsArticle({ job, files, outputPath, }: { job: JobsWithOptionalNumFiles; files: Filename[]; outputPath: string; }) { // If the DB schema includes jobs.num_files use it; otherwise fall back to files.length const maxFiles = typeof job.num_files === "number" && Number.isFinite(job.num_files) ? Number(job.num_files) : Number(files.length); const doneFiles = Number( files.filter((f) => String(f.status ?? "").toLowerCase() === "done").length, ); const isDone = maxFiles > 0 ? doneFiles >= maxFiles : true; const disabledLinkClass = "pointer-events-none opacity-50"; const busyAttrs = { disabled: true, "aria-busy": "true" } as const; return (

Results

{files.map((file) => ( ))}
Converted File Name Status Actions
{file.output_file_name} {file.status}
{/* Share Modal (hidden by default) */}
); } /** * IMPORTANT FIX: * Results HTML is re-rendered via /progress/:jobId and replaces the article + modal. * So we MUST NOT keep stale DOM references. We re-query elements on each action. */ const shareJs = ` (function () { const WEBROOT = ${JSON.stringify(WEBROOT)}; function getEl(id) { return document.getElementById(id); } function getRefs() { return { modal: getEl("cxShareModal"), closeBtn: getEl("cxShareClose"), cancelBtn: getEl("cxShareCancel"), submitBtn: getEl("cxShareSubmit"), statusEl: getEl("cxShareStatus"), emailEl: getEl("cxShareEmail"), nameEl: getEl("cxShareName"), descEl: getEl("cxShareDescription"), linkBlock: getEl("cxShareLinkBlock"), linkEl: getEl("cxShareLink"), copyBtn: getEl("cxShareCopy"), }; } let currentJobId = null; let currentFileName = null; function openModal(jobId, fileName) { const r = getRefs(); if (!r.modal || !r.emailEl || !r.nameEl || !r.descEl || !r.statusEl || !r.linkBlock || !r.linkEl) { console.warn("[ConvertX] Share modal elements not found (DOM may be mid-refresh)."); return; } currentJobId = jobId; currentFileName = fileName; r.nameEl.value = fileName || ""; r.emailEl.value = ""; r.descEl.value = ""; r.linkBlock.classList.add("hidden"); r.linkEl.value = ""; r.statusEl.textContent = ""; r.modal.classList.remove("hidden"); r.modal.classList.add("flex"); r.emailEl.focus(); } function closeModal() { const r = getRefs(); if (!r.modal) return; r.modal.classList.add("hidden"); r.modal.classList.remove("flex"); currentJobId = null; currentFileName = null; } // Delegated: always works even after /progress replaces the article document.addEventListener("click", (e) => { const t = e.target; const btn = t && (t.closest ? t.closest('[data-share="true"]') : null); if (!btn) return; // kill old handlers (e.g. alert() from older results.js) e.preventDefault(); e.stopPropagation(); if (e.stopImmediatePropagation) e.stopImmediatePropagation(); const jobId = btn.getAttribute("data-job-id"); const fileName = btn.getAttribute("data-file-name"); openModal(jobId, fileName); }, true); // Delegated close (because modal DOM is replaced during progress polling) document.addEventListener("click", (e) => { const id = e.target && e.target.id; if (id === "cxShareClose" || id === "cxShareCancel") { e.preventDefault(); closeModal(); } if (id === "cxShareModal") { // click outside dialog closes closeModal(); } }, true); document.addEventListener("keydown", (e) => { const r = getRefs(); if (e.key === "Escape" && r.modal && !r.modal.classList.contains("hidden")) closeModal(); }); // Delegated copy document.addEventListener("click", async (e) => { const t = e.target; if (!t || t.id !== "cxShareCopy") return; e.preventDefault(); const r = getRefs(); if (!r.linkEl || !r.statusEl) return; try { await navigator.clipboard.writeText(r.linkEl.value || ""); r.statusEl.textContent = "Copied."; } catch (err) { r.linkEl.focus(); r.linkEl.select(); r.statusEl.textContent = "Select + copy (Ctrl/Cmd+C)."; } }, true); // Delegated submit document.addEventListener("click", async (e) => { const t = e.target; if (!t || t.id !== "cxShareSubmit") return; e.preventDefault(); const r = getRefs(); if (!r.submitBtn || !r.statusEl || !r.emailEl || !r.nameEl || !r.descEl || !r.linkBlock || !r.linkEl) return; if (!currentJobId || !currentFileName) return; r.submitBtn.disabled = true; r.submitBtn.setAttribute("aria-busy", "true"); r.statusEl.textContent = "Sending..."; try { const email = (r.emailEl.value || "").trim(); const shareName = (r.nameEl.value || "").trim(); const description = (r.descEl.value || "").trim(); const payload = { fileName: currentFileName, ...(email ? { recipientEmail: email } : {}), ...(shareName ? { shareName } : {}), ...(description ? { description } : {}), }; const res = await fetch(\`\${WEBROOT}/share-to-erugo/\${currentJobId}\`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); const text = await res.text(); let json; try { json = JSON.parse(text); } catch (_) { json = { raw: text }; } if (!res.ok) { console.error("[ConvertX] share-to-erugo error", res.status, json); r.statusEl.textContent = "Failed. See logs."; return; } const url = json?.share_url || json?.share_link || json?.data?.url || json?.data?.share?.url || null; if (url) { r.linkEl.value = url; r.linkBlock.classList.remove("hidden"); } r.statusEl.textContent = email ? (url ? "Sent. Link also shown below." : "Sent. (No link returned.)") : (url ? "Created. Copy the link below." : "Created, but no link returned."); } catch (err) { console.error(err); r.statusEl.textContent = "Failed. See logs."; } finally { r.submitBtn.disabled = false; r.submitBtn.removeAttribute("aria-busy"); } }, true); // Download All: delegated, because button is replaced during progress polling document.addEventListener("click", (e) => { const t = e.target; const btn = t && (t.closest ? t.closest("#cxDownloadAll") : null); if (!btn) return; e.preventDefault(); try { if (typeof window.downloadAll === "function") { window.downloadAll(); } else { console.warn("[ConvertX] downloadAll() not found. Ensure results.js is loaded."); } } catch (err) { console.error("[ConvertX] downloadAll() failed", err); } }, true); })(); `.trim(); export const results = new Elysia() .use(userService) .get( "/results-share.js", () => new Response(shareJs, { headers: { "content-type": "text/javascript; charset=utf-8", "cache-control": "no-store", }, }), { auth: true }, ) .get( "/results/:jobId", async ({ params, set, cookie: { job_id }, user }) => { if (job_id?.value) job_id.remove(); const job = db .query("SELECT * FROM jobs WHERE user_id = ? AND id = ?") .as(Jobs) .get(user.id, params.jobId) as JobsWithOptionalNumFiles | null; if (!job) { set.status = 404; return { message: "Job not found." }; } const outputPath = `${user.id}/${params.jobId}/`; const files = db .query("SELECT * FROM file_names WHERE job_id = ?") .as(Filename) .all(params.jobId); return ( <>
{/* keep existing file */}