- 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]>
72 lines
2.6 KiB
JavaScript
72 lines
2.6 KiB
JavaScript
// 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;
|
|
}
|