feat: add secure auth, admin and postgres persistence
This commit is contained in:
+7
-77
@@ -1,79 +1,9 @@
|
||||
// Wires the ".alog reference curve" panel: local file upload, and browsing a server-side
|
||||
// library directory (e.g. wherever the roastetta skill already downloaded logs).
|
||||
|
||||
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 }) {
|
||||
const fileInput = document.getElementById("alog-file");
|
||||
const libraryBtn = document.getElementById("alog-library-refresh");
|
||||
const libraryList = document.getElementById("alog-library-list");
|
||||
const resultEl = document.getElementById("alog-result");
|
||||
|
||||
fileInput.addEventListener("change", async (e) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (!file) return;
|
||||
resultEl.innerHTML = "<p>Parsing…</p>";
|
||||
try {
|
||||
const content = await file.text();
|
||||
const res = await fetch("/api/alog", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ filename: file.name, content }),
|
||||
});
|
||||
const body = await res.json();
|
||||
applyResult(body);
|
||||
} catch (err) {
|
||||
resultEl.innerHTML = `<p style="color:#a8371a">${escapeHtml(err.message)}</p>`;
|
||||
}
|
||||
});
|
||||
|
||||
libraryBtn.addEventListener("click", async () => {
|
||||
libraryList.classList.remove("hidden");
|
||||
libraryList.innerHTML = "<li>Loading…</li>";
|
||||
try {
|
||||
const res = await fetch("/api/alog/library");
|
||||
const body = await res.json();
|
||||
if (!body.ok || body.files.length === 0) {
|
||||
libraryList.innerHTML = "<li>No .alog files found. Set ALOG_DIR or drop files in ~/Roastetta.</li>";
|
||||
return;
|
||||
}
|
||||
libraryList.innerHTML = "";
|
||||
for (const f of body.files) {
|
||||
const li = document.createElement("li");
|
||||
li.textContent = `${f.filename} (${Math.round(f.sizeBytes / 1024)} KB)`;
|
||||
li.addEventListener("click", async () => {
|
||||
resultEl.innerHTML = "<p>Loading…</p>";
|
||||
const r = await fetch(`/api/alog/library/${encodeURIComponent(f.filename)}`);
|
||||
applyResult(await r.json());
|
||||
});
|
||||
libraryList.appendChild(li);
|
||||
}
|
||||
} catch (err) {
|
||||
libraryList.innerHTML = `<li style="color:#a8371a">${escapeHtml(err.message)}</li>`;
|
||||
}
|
||||
});
|
||||
|
||||
function applyResult(body) {
|
||||
if (!body.ok) {
|
||||
resultEl.innerHTML = `<p style="color:#a8371a">${escapeHtml(body.error ?? body.code)}</p>`;
|
||||
return;
|
||||
}
|
||||
state.plan.reference = body;
|
||||
recompute();
|
||||
const warnings = (body.warnings ?? []).map((w) => `<li>${escapeHtml(w)}</li>`).join("");
|
||||
resultEl.innerHTML = `
|
||||
<p><strong>${escapeHtml(body.roast.title)}</strong> — ${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 ?? "—"}%</p>
|
||||
${warnings ? `<ul class="warnings">${warnings}</ul>` : ""}
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
function fmt(seconds) {
|
||||
if (seconds === null || seconds === undefined) return "—";
|
||||
const s = Math.round(seconds);
|
||||
return `${Math.floor(s / 60)}:${String(s % 60).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
function escapeHtml(s) {
|
||||
return String(s ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
|
||||
const fileInput=document.getElementById("alog-file"), libraryBtn=document.getElementById("alog-library-refresh"), libraryList=document.getElementById("alog-library-list"), resultEl=document.getElementById("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);}});
|
||||
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")}`;}
|
||||
|
||||
Reference in New Issue
Block a user