Initial roast planner webapp: fillable worksheet, live ledger, curve, URL prefill, .alog reference curve

- 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]>
This commit is contained in:
2026-07-29 15:41:35 -04:00
co-authored by Claude Sonnet 5
commit fedaf29847
22 changed files with 5000 additions and 0 deletions
+22
View File
@@ -0,0 +1,22 @@
// Bakes live input values into their sibling .pv span before printing (so text doesn't clip
// at a fixed .f width the way a raw <input> would), then clears them after. One DOM, one
// rendering — see app.css's @media print block for the corresponding display swap.
export function initPrint({ form }) {
document.getElementById("btn-print").addEventListener("click", () => window.print());
window.addEventListener("beforeprint", () => bakeValues(form));
window.addEventListener("afterprint", () => clearBaked(form));
}
function bakeValues(form) {
for (const el of form.querySelectorAll("input.ws-input, textarea.ws-input")) {
if (el.type === "radio" || el.type === "checkbox") continue;
const pv = el.nextElementSibling;
if (pv && pv.classList.contains("pv")) pv.textContent = el.value || "";
}
}
function clearBaked(form) {
for (const pv of form.querySelectorAll(".pv")) pv.textContent = "";
}