- shared/ ports the worksheet's ledger math, time parsing, and reference tables (cultivars/processes/roast-levels/machine bands) as browser-safe ESM, imported by both the server and the browser so the arithmetic can't drift between them. - server/prefill.js runs a zero-tool Pi Coding Agent SDK turn to extract page facts from a bean product URL, then derives worksheet field IDs deterministically from reference-data.js — the model never invents a first-crack anchor or a modifier. - server/alog.js ports the Python alog_parser.py's format handling, including the tokenizer-based Python-dict-literal-to-JSON conversion the real files need. - public/ is the worksheet reproduced as a live HTML form: worksheet.css is a verbatim copy of the paper worksheet's print CSS, print.js bakes values into the print layout so the same DOM renders both on screen and on paper. - Ledger math verified against both of the paper worksheet's worked examples; alog parser verified against all 14 real logs in ref/roasts/. Co-Authored-By: Claude Sonnet 5 <[email protected]>
80 lines
3.1 KiB
JavaScript
80 lines
3.1 KiB
JavaScript
// 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).
|
|
|
|
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]));
|
|
}
|