Move After-the-Roast onto actual roasts; add Artisan-importable updated .alog download
Test and deploy / test-and-deploy (push) Successful in 56s
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:
co-authored by
Claude Fable 5
parent
9b6ea9880e
commit
4a3d192c2c
+1
-1
@@ -2594,7 +2594,7 @@ body.show-fids .field[data-fid]::after {
|
||||
order: 2;
|
||||
margin: 0;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 4px;
|
||||
}
|
||||
.section-nav a {
|
||||
|
||||
@@ -67,7 +67,6 @@
|
||||
<a href="#sec-bean">Bean</a>
|
||||
<a href="#sec-machine">Machine</a>
|
||||
<a href="#sec-roastlog">Log</a>
|
||||
<a href="#sec-after">After</a>
|
||||
</nav>
|
||||
|
||||
<div class="header-actions header-tools">
|
||||
@@ -1173,119 +1172,6 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel-card phase-later" id="sec-after">
|
||||
<div class="panel-head">
|
||||
<h2>After the Roast</h2>
|
||||
<span class="phase-chip">after cupping</span>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<details class="why-panel" data-why="sec-after">
|
||||
<summary>One change per batch — arithmetic, not patience</summary>
|
||||
<p>
|
||||
Move two variables and there are four possible
|
||||
explanations for what the cup does; move three and there
|
||||
are eight, and you can't separate them without more
|
||||
batches than you have coffee for. This worksheet makes
|
||||
single-variable experiments cheap: every number has one
|
||||
owner.
|
||||
</p>
|
||||
<p>
|
||||
Weight loss is the honest instrument — (green − roasted) ÷
|
||||
green × 100 rolls total time, development, and drop
|
||||
temperature into one number your scale can measure. Then
|
||||
match the cup to a symptom: papery or thin → first crack
|
||||
+0:30. Savory, florals gone → first crack −0:30. Sharp
|
||||
acidity with no fruit → development +0:15 (a DTR under 12%
|
||||
confirms it). Flat and dull → development −0:15. Flat with
|
||||
no clear defect → fix the RoR shape, not the times. Write
|
||||
the winner into "One change next batch" — and next time
|
||||
you roast this coffee, carry it into ± Refine (1.6). That
|
||||
handoff is how the plan learns.
|
||||
</p>
|
||||
</details>
|
||||
<div class="field-grid">
|
||||
<label class="field"
|
||||
><span class="field-label">Green in</span>
|
||||
<div class="unit-input">
|
||||
<input class="field-input" name="afterRoast.greenIn" /><span
|
||||
class="unit"
|
||||
>g</span
|
||||
>
|
||||
</div></label
|
||||
>
|
||||
<label class="field"
|
||||
><span class="field-label">Out</span>
|
||||
<div class="unit-input">
|
||||
<input class="field-input" name="afterRoast.out" /><span
|
||||
class="unit"
|
||||
>g</span
|
||||
>
|
||||
</div></label
|
||||
>
|
||||
<label class="field"
|
||||
><span class="field-label">Weight loss</span>
|
||||
<div class="unit-input">
|
||||
<input
|
||||
class="field-input"
|
||||
name="afterRoast.weightLossPct"
|
||||
/><span class="unit">%</span>
|
||||
</div></label
|
||||
>
|
||||
<label class="field"
|
||||
><span class="field-label">Actual DTR</span>
|
||||
<div class="unit-input">
|
||||
<input
|
||||
class="field-input"
|
||||
name="afterRoast.actualDtrPct"
|
||||
/><span class="unit">%</span>
|
||||
</div></label
|
||||
>
|
||||
</div>
|
||||
<div class="field-grid">
|
||||
<label class="field"
|
||||
><span class="field-label">Colour WB/GR</span
|
||||
><input class="field-input" name="afterRoast.colour"
|
||||
/></label>
|
||||
<label class="field"
|
||||
><span class="field-label">Rested</span>
|
||||
<div class="unit-input">
|
||||
<input
|
||||
class="field-input"
|
||||
name="afterRoast.restedDays"
|
||||
/><span class="unit">days</span>
|
||||
</div></label
|
||||
>
|
||||
<label class="field"
|
||||
><span class="field-label">Brew ratio</span
|
||||
><input class="field-input" name="afterRoast.brewRatio"
|
||||
/></label>
|
||||
<label class="field"
|
||||
><span class="field-label">Method</span
|
||||
><input class="field-input" name="afterRoast.method"
|
||||
/></label>
|
||||
</div>
|
||||
<label class="field"
|
||||
><span class="field-label">Cup notes</span
|
||||
><input class="field-input" name="afterRoast.cupNotes"
|
||||
/></label>
|
||||
<div class="field-grid">
|
||||
<label class="field accent"
|
||||
><span class="field-label">One change next batch</span
|
||||
><input class="field-input" name="afterRoast.oneChange"
|
||||
/></label>
|
||||
<label class="field"
|
||||
><span class="field-label">What would prove it wrong</span
|
||||
><input class="field-input" name="afterRoast.disproof"
|
||||
/></label>
|
||||
</div>
|
||||
<div class="cupping-link">
|
||||
<button type="button" class="ghost-btn" id="btn-open-cupping">
|
||||
Open cupping session
|
||||
</button>
|
||||
<span class="field-note" id="cupping-link-note"></span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<aside class="dock" aria-label="Live plan feedback">
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
+89
-1
@@ -103,7 +103,15 @@
|
||||
<h2 id="detail-title">Roast detail</h2>
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
||||
<a class="ghost-btn small" id="detail-download" href="#" download
|
||||
>Download original .alog</a
|
||||
>Original .alog</a
|
||||
>
|
||||
<a
|
||||
class="ghost-btn small"
|
||||
id="detail-download-updated"
|
||||
href="#"
|
||||
download
|
||||
title="The original file with this app's data (weights, notes, cupping) written back as Artisan fields — importable into Artisan"
|
||||
>Updated .alog</a
|
||||
>
|
||||
<button class="ghost-btn small" type="button" id="detail-reevaluate">
|
||||
Re-evaluate
|
||||
@@ -128,6 +136,86 @@
|
||||
></svg>
|
||||
</div>
|
||||
<div id="detail-stats" class="field-grid" style="margin-top:10px"></div>
|
||||
|
||||
<div id="detail-after" style="margin-top:16px">
|
||||
<h3 style="margin:0 0 4px">After the roast</h3>
|
||||
<p class="field-note" style="margin-top:0">
|
||||
One change per batch. Weight loss is the honest instrument —
|
||||
match the cup to a symptom, write the winner into "One
|
||||
change", and the planner's ± Refine picks it up next time
|
||||
you roast this lot. These values also go into the updated
|
||||
.alog download.
|
||||
</p>
|
||||
<form id="after-form">
|
||||
<div class="field-grid">
|
||||
<label class="field"
|
||||
><span class="field-label">Green in</span>
|
||||
<div class="unit-input">
|
||||
<input class="field-input" name="greenIn" /><span class="unit">g</span>
|
||||
</div></label
|
||||
>
|
||||
<label class="field"
|
||||
><span class="field-label">Out</span>
|
||||
<div class="unit-input">
|
||||
<input class="field-input" name="out" /><span class="unit">g</span>
|
||||
</div></label
|
||||
>
|
||||
<label class="field"
|
||||
><span class="field-label">Weight loss</span>
|
||||
<div class="unit-input">
|
||||
<input class="field-input" name="weightLossPct" /><span class="unit">%</span>
|
||||
</div></label
|
||||
>
|
||||
<label class="field"
|
||||
><span class="field-label">Actual DTR</span>
|
||||
<div class="unit-input">
|
||||
<input class="field-input" name="actualDtrPct" /><span class="unit">%</span>
|
||||
</div></label
|
||||
>
|
||||
<label class="field"
|
||||
><span class="field-label">vs target</span
|
||||
><input class="field-input" name="vsTarget"
|
||||
/></label>
|
||||
<label class="field"
|
||||
><span class="field-label">Colour WB/GR</span
|
||||
><input class="field-input" name="colour"
|
||||
/></label>
|
||||
<label class="field"
|
||||
><span class="field-label">Rested</span>
|
||||
<div class="unit-input">
|
||||
<input class="field-input" name="restedDays" /><span class="unit">days</span>
|
||||
</div></label
|
||||
>
|
||||
<label class="field"
|
||||
><span class="field-label">Brew ratio</span
|
||||
><input class="field-input" name="brewRatio"
|
||||
/></label>
|
||||
<label class="field"
|
||||
><span class="field-label">Method</span
|
||||
><input class="field-input" name="method"
|
||||
/></label>
|
||||
</div>
|
||||
<label class="field"
|
||||
><span class="field-label">Cup notes</span
|
||||
><textarea class="field-input" name="cupNotes" rows="2"></textarea></label
|
||||
>
|
||||
<label class="field"
|
||||
><span class="field-label">One change next batch</span
|
||||
><input
|
||||
class="field-input"
|
||||
name="oneChange"
|
||||
placeholder="e.g. first crack +0:30"
|
||||
/></label>
|
||||
<label class="field"
|
||||
><span class="field-label">What would disprove it</span
|
||||
><input class="field-input" name="disproof"
|
||||
/></label>
|
||||
<button class="primary-btn" type="submit" id="after-save">
|
||||
Save after-roast
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div id="detail-evaluation" style="margin-top:14px"></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user