const csrf = () => document.cookie .split("; ") .find((v) => v.startsWith("rp_csrf=")) ?.split("=")[1] || ""; const setText = (el, text, error = false) => { const node = document.createElement("p"); node.textContent = text; if (error) node.style.color = "#a8371a"; el.replaceChildren(node); }; export function initAlogPanel({ state, recompute, getRemotePlanId, flushCurrentPlan, }) { const fileInput = document.getElementById("alog-file"), libraryBtn = document.getElementById("alog-library-refresh"), libraryList = document.getElementById("alog-library-list"), resultEl = document.getElementById("alog-result"), actualInput = document.getElementById("actual-alog-file"), actualResultEl = document.getElementById("actual-alog-result"); fileInput.addEventListener("change", async (e) => { const file = e.target.files?.[0]; if (!file) return; setText(resultEl, "Parsing…"); try { const res = await fetch("/api/alog", { method: "POST", headers: { "content-type": "application/json", "x-csrf-token": csrf() }, body: JSON.stringify({ filename: file.name, content: await file.text(), }), }); applyResult(await res.json()); } catch (err) { setText(resultEl, err.message, true); } }); libraryBtn.addEventListener("click", async () => { libraryList.classList.remove("hidden"); libraryList.replaceChildren( Object.assign(document.createElement("li"), { textContent: "Loading…" }), ); try { const body = await (await fetch("/api/alog/library")).json(); if (!body.ok || !body.files.length) { libraryList.replaceChildren( Object.assign(document.createElement("li"), { textContent: "No .alog files found. Set ALOG_DIR or drop files in ~/Roastetta.", }), ); return; } libraryList.replaceChildren( ...body.files.map((f) => { const li = document.createElement("li"); li.textContent = `${f.filename} (${Math.round(f.sizeBytes / 1024)} KB)`; li.onclick = async () => { setText(resultEl, "Loading…"); applyResult( await ( await fetch( `/api/alog/library/${encodeURIComponent(f.filename)}`, ) ).json(), ); }; return li; }), ); } catch (err) { const li = document.createElement("li"); li.textContent = err.message; li.style.color = "#a8371a"; libraryList.replaceChildren(li); } }); // Finished-roast uploads: each file becomes an actual_roasts row attached to the open plan // (which must exist server-side first — hence the flush), and the LLM reviews it // asynchronously; the roast history page is where results land. actualInput?.addEventListener("change", async (e) => { const files = [...(e.target.files ?? [])]; e.target.value = ""; if (!files.length) return; setText(actualResultEl, "Syncing plan…"); await flushCurrentPlan(); const planId = getRemotePlanId(); if (!planId) { setText( actualResultEl, "Could not sync this plan to the server — connect and try again, or upload from the Roasts page without a plan.", true, ); return; } let uploaded = 0; for (const file of files) { setText(actualResultEl, `Uploading ${file.name}…`); try { const res = await fetch("/api/roasts", { method: "POST", headers: { "content-type": "application/json", "x-csrf-token": csrf() }, body: JSON.stringify({ roastPlanId: planId, filename: file.name, content: await file.text(), }), }); const body = await res.json(); if (!body.ok) throw new Error(body.error ?? body.code); uploaded++; } catch (err) { setText(actualResultEl, `${file.name}: ${err.message}`, true); return; } } const node = document.createElement("p"); node.append( `Uploaded ${uploaded} roast${uploaded === 1 ? "" : "s"} to this plan. `, ); const link = document.createElement("a"); link.href = "/roasts"; link.textContent = "See the review →"; node.append(link); actualResultEl.replaceChildren(node); }); function applyResult(body) { if (!body.ok) { setText(resultEl, body.error ?? body.code, true); return; } state.plan.reference = body; recompute(); setText( resultEl, `${body.roast.title} — ${body.roast.roastDate || "no date"}; first crack ${fmt(body.derived?.firstCrackS)}; development ${fmt(body.derived?.developmentS)}; drop ${fmt(body.derived?.dropS)}; DTR ${body.derived?.dtrPct ?? "—"}%.`, ); } } function fmt(seconds) { if (seconds == null) return "—"; const s = Math.round(seconds); return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`; }