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:
@@ -0,0 +1,71 @@
|
||||
// Browser-safe. The allowlist of every scalar worksheet field, and a blank plan factory.
|
||||
|
||||
export const FIELD_IDS = [
|
||||
"0.1", "0.2", "0.3", "0.4",
|
||||
"1.1", "1.2", "1.3", "1.4", "1.5", "1.6", "1.7",
|
||||
"2.1", "2.3", "2.4", "2.5",
|
||||
"3.1", "3.2",
|
||||
"4.1", "4.2", "4.3",
|
||||
"5.1", "5.2", "5.3", "5.4", "5.5", "5.6",
|
||||
"6.4",
|
||||
];
|
||||
|
||||
export function blankPlan() {
|
||||
const fields = {};
|
||||
for (const id of FIELD_IDS) fields[id] = "";
|
||||
fields["1.6"] = "0";
|
||||
fields["2.1"] = "single";
|
||||
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
fields,
|
||||
blendComponents: [
|
||||
{ cultivar: "", group: "", process: "", sharePct: "", fcAnchor: "" },
|
||||
{ cultivar: "", group: "", process: "", sharePct: "", fcAnchor: "" },
|
||||
],
|
||||
temps: {
|
||||
charge: { time: "0:00", tempC: "", tempF: "" },
|
||||
tp: { time: "", tempC: "", tempF: "" },
|
||||
yellow: { time: "", tempC: "", tempF: "" },
|
||||
fc: { time: "", tempC: "", tempF: "" },
|
||||
drop: { time: "", tempC: "", tempF: "" },
|
||||
},
|
||||
ror: {
|
||||
tpToYellow: { value: "", ok: false, note: "" },
|
||||
yellowToFc: { value: "", ok: false, note: "" },
|
||||
fcToDrop: { value: "", ok: false, note: "" },
|
||||
},
|
||||
actuators: [
|
||||
{ time: "0:00", heatPct: "", fanPct: "", expectedBt: "", why: "" },
|
||||
{ time: "", heatPct: "", fanPct: "", expectedBt: "", why: "" },
|
||||
{ time: "", heatPct: "", fanPct: "", expectedBt: "", why: "" },
|
||||
],
|
||||
planActual: {
|
||||
charge: { planTime: "0:00", planBt: "", actualTime: "0:00", actualBt: "", ror: "", note: "" },
|
||||
tp: { planTime: "", planBt: "", actualTime: "", actualBt: "", ror: "", note: "" },
|
||||
yellow: { planTime: "", planBt: "", actualTime: "", actualBt: "", ror: "", note: "" },
|
||||
fc: { planTime: "", planBt: "", actualTime: "", actualBt: "", ror: "", note: "" },
|
||||
drop: { planTime: "", planBt: "", actualTime: "", actualBt: "", ror: "", note: "" },
|
||||
},
|
||||
afterRoast: {
|
||||
greenIn: "", out: "", weightLossPct: "", vsTarget: "", actualDtrPct: "",
|
||||
colour: "", restedDays: "", brewRatio: "", method: "",
|
||||
cupNotes: "", cupNotesLine2: "", oneChange: "", disproof: "",
|
||||
},
|
||||
sanityOverride: { drying: null, maillard: null, dtr: null, ceiling: null },
|
||||
reference: null,
|
||||
prefill: null,
|
||||
};
|
||||
}
|
||||
|
||||
/** Keep only known FIELD_IDS, coerce every value to a string. */
|
||||
export function sanitizeFieldPatch(patch) {
|
||||
const out = {};
|
||||
if (!patch || typeof patch !== "object") return out;
|
||||
for (const id of FIELD_IDS) {
|
||||
if (Object.prototype.hasOwnProperty.call(patch, id) && patch[id] !== null && patch[id] !== undefined) {
|
||||
out[id] = String(patch[id]);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
Reference in New Issue
Block a user