Move After-the-Roast onto actual roasts; add Artisan-importable updated .alog download
Test and deploy / test-and-deploy (push) Successful in 56s

One system: post-roast observations (weights, colour, DTR, cup notes,
'one change next batch', disproof) now live on the uploaded roast
(actual_roasts.after, migration 011), edited on the roast detail view
with weights/DTR prefilled from the .alog itself. The planner's screen
section is removed (the print worksheet keeps its hand-fill copy), and
the lot 'last refine' suggestion reads the note from both the new home
and legacy plans, newest wins.

Every roast now downloads two ways: the untouched original .alog, and
an updated supplemental copy with the app's data written back as valid
Artisan fields (weight, beans, roastingnotes incl. the LLM review,
cuppingnotes incl. linked cupping score/flavors) — serialized as a
Python literal so Artisan's ast.literal_eval re-imports it.

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
Shane Maynard
2026-08-09 08:35:11 -04:00
co-authored by Claude Fable 5
parent 9b6ea9880e
commit 4a3d192c2c
12 changed files with 395 additions and 161 deletions
+3 -4
View File
@@ -314,10 +314,9 @@ export const HELP_TOPICS = {
"symptom-fix": {
title: "Symptom → cause → the single number to move",
targets: [
'label[data-fid="1.6"] .field-label',
"#sec-after .field.accent .field-label",
],
// The After-the-Roast section moved to the Roasts page; this help stays reachable from
// the ± Refine field, which is the planner-side half of the feedback loop.
targets: ['label[data-fid="1.6"] .field-label'],
render() {
return frag(
p(
-6
View File
@@ -537,12 +537,6 @@ export function recompute() {
"has-data",
hasVal(state.plan.planActual, BLANK_PLAN.planActual),
);
document
.getElementById("sec-after")
?.classList.toggle(
"has-data",
hasVal(state.plan.afterRoast, BLANK_PLAN.afterRoast),
);
lotPicker?.updateConsumeRow();
autosave();
}
+46
View File
@@ -359,6 +359,9 @@ async function openDetail(id, { keepScroll = false } = {}) {
}
document.getElementById("detail-download").href =
`/api/roasts/${encodeURIComponent(id)}/download`;
document.getElementById("detail-download-updated").href =
`/api/roasts/${encodeURIComponent(id)}/download?variant=updated`;
renderAfterForm(detail);
renderGraph(detail);
renderStats(detail);
renderEvaluation(detail);
@@ -366,6 +369,49 @@ async function openDetail(id, { keepScroll = false } = {}) {
if (!keepScroll) card.scrollIntoView({ behavior: "smooth", block: "start" });
}
// ---- after the roast (moved here from the plan worksheet) -------------------
const AFTER_FIELDS = [
"greenIn", "out", "weightLossPct", "vsTarget", "actualDtrPct", "colour",
"restedDays", "brewRatio", "method", "cupNotes", "oneChange", "disproof",
];
function renderAfterForm(detail) {
const form = document.getElementById("after-form");
const after = detail.after ?? {};
// Prefill measurable values straight from the .alog so the user only types what the
// machine couldn't know (colour, cup notes, the one change).
const auto = {
greenIn: detail.parsed?.roast?.weightInG || "",
out: detail.parsed?.roast?.weightOutG || "",
weightLossPct: detail.parsed?.roast?.weightLossPct ?? "",
actualDtrPct: detail.parsed?.derived?.dtrPct ?? "",
};
for (const field of AFTER_FIELDS)
form[field].value = after[field] ?? (auto[field] === 0 ? "" : (auto[field] ?? ""));
}
document.getElementById("after-form").addEventListener("submit", async (event) => {
event.preventDefault();
if (!openId) return;
const form = event.target;
const after = {};
for (const field of AFTER_FIELDS) after[field] = form[field].value;
const button = document.getElementById("after-save");
button.disabled = true;
try {
await api(`/api/roasts/${openId}`, {
method: "PUT",
body: JSON.stringify({ after }),
});
showToast("After-roast saved — included in the updated .alog.");
} catch (error) {
showToast(error.message, "fail");
} finally {
button.disabled = false;
}
});
function closeDetail() {
openId = null;
history.replaceState(null, "", "/roasts");