Add green-bean inventory and cupping features
Ports inventory management and SCA-style cupping scoring from hope_roaster: lot tracking with audit-logged consumption, cupping sessions with server-authoritative scoring and a live radar chart, and links from the planner (draw-from-lot, open-cupping-session). Also fixes the Blend/Single-origin toggle layout, replaces tooltips with an in-context "why" teaching layer, and stages the planner UI into pre-roast vs. post-roast phases.
This commit is contained in:
+478
-126
@@ -14,71 +14,138 @@
|
||||
<link rel="stylesheet" href="/app.css" />
|
||||
</head>
|
||||
<body>
|
||||
<header class="app-header">
|
||||
<div class="brand">
|
||||
<span class="brand-mark" aria-hidden="true">◐</span>
|
||||
<div class="brand-text">
|
||||
<h1>Roast Planner</h1>
|
||||
<p class="brand-sub" id="header-coffee-name">New plan</p>
|
||||
<div class="app-shell">
|
||||
<nav class="side-nav" id="side-nav" aria-label="Main navigation">
|
||||
<div class="side-nav-brand">
|
||||
<span class="brand-mark" aria-hidden="true">◐</span>
|
||||
<span class="brand-name">Roast Planner</span>
|
||||
</div>
|
||||
<div class="nav-group">
|
||||
<a class="nav-item" href="/app" aria-current="page"
|
||||
><span class="nav-icon" aria-hidden="true">◐</span
|
||||
><span class="nav-label">Planner</span></a
|
||||
>
|
||||
<button class="nav-item" type="button" id="nav-plans">
|
||||
<span class="nav-icon" aria-hidden="true">▤</span
|
||||
><span class="nav-label">Plans</span>
|
||||
</button>
|
||||
<a class="nav-item" href="/inventory"
|
||||
><span class="nav-icon" aria-hidden="true">▥</span
|
||||
><span class="nav-label">Inventory</span></a
|
||||
>
|
||||
<a class="nav-item" href="/cupping"
|
||||
><span class="nav-icon" aria-hidden="true">◒</span
|
||||
><span class="nav-label">Cupping</span></a
|
||||
>
|
||||
<a class="nav-item" href="/account"
|
||||
><span class="nav-icon" aria-hidden="true">◔</span
|
||||
><span class="nav-label">Account</span></a
|
||||
>
|
||||
<a class="nav-item hidden" id="nav-admin" href="/admin"
|
||||
><span class="nav-icon" aria-hidden="true">⚙</span
|
||||
><span class="nav-label">Admin</span></a
|
||||
>
|
||||
</div>
|
||||
<div class="nav-group">
|
||||
<div class="nav-group-title">Tools</div>
|
||||
<button class="nav-item" type="button" id="nav-prefill">
|
||||
<span class="nav-icon" aria-hidden="true">↗</span
|
||||
><span class="nav-label">Prefill from URL</span>
|
||||
</button>
|
||||
<button class="nav-item" type="button" id="nav-alog">
|
||||
<span class="nav-icon" aria-hidden="true">∿</span
|
||||
><span class="nav-label">Reference curve</span>
|
||||
</button>
|
||||
<button class="nav-item" type="button" id="nav-import-export">
|
||||
<span class="nav-icon" aria-hidden="true">⇅</span
|
||||
><span class="nav-label">Import / export</span>
|
||||
</button>
|
||||
<button
|
||||
class="nav-item"
|
||||
type="button"
|
||||
id="btn-toggle-fids"
|
||||
aria-pressed="false"
|
||||
title="Show the worksheet reference codes (e.g. 1.4) on every field"
|
||||
>
|
||||
<span class="nav-icon" aria-hidden="true">#</span
|
||||
><span class="nav-label">Field IDs</span>
|
||||
</button>
|
||||
<button class="nav-item" type="button" id="nav-settings">
|
||||
<span class="nav-icon" aria-hidden="true">◎</span
|
||||
><span class="nav-label">Settings & sync</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="nav-spacer"></div>
|
||||
<button
|
||||
class="icon-btn nav-collapse-toggle"
|
||||
type="button"
|
||||
id="nav-collapse"
|
||||
aria-label="Collapse navigation"
|
||||
title="Collapse navigation"
|
||||
>
|
||||
«
|
||||
</button>
|
||||
<div class="nav-user">
|
||||
<div class="nav-user-avatar" id="nav-user-avatar" aria-hidden="true"></div>
|
||||
<div class="nav-user-detail">
|
||||
<span class="nav-user-email" id="account-email"></span>
|
||||
<button class="nav-user-logout" type="button" id="btn-logout">
|
||||
Log out
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="section-nav" id="section-nav" aria-label="Plan sections">
|
||||
<a href="#sec-coffee">Plan</a>
|
||||
<a href="#sec-bean">Bean condition</a>
|
||||
<a href="#sec-machine">Roast</a>
|
||||
<a href="#sec-after">Learn</a>
|
||||
</nav>
|
||||
|
||||
<div class="header-actions">
|
||||
<button id="btn-plans" type="button" class="ghost-btn">Plans</button>
|
||||
<button
|
||||
id="btn-toggle-fids"
|
||||
type="button"
|
||||
class="ghost-btn"
|
||||
aria-pressed="false"
|
||||
title="Show the worksheet reference codes (e.g. 1.4) on every field"
|
||||
>
|
||||
Field IDs
|
||||
</button>
|
||||
<button id="btn-toggle-prefill" type="button" class="ghost-btn">
|
||||
Prefill from URL
|
||||
</button>
|
||||
<button id="btn-toggle-alog" type="button" class="ghost-btn">
|
||||
Reference curve
|
||||
</button>
|
||||
<span class="header-divider" aria-hidden="true"></span>
|
||||
<button id="btn-load" type="button" class="ghost-btn">Load</button>
|
||||
<input id="file-load" type="file" accept="application/json" />
|
||||
<button id="btn-save" type="button" class="ghost-btn">Download</button>
|
||||
<details class="account-menu">
|
||||
<summary class="ghost-btn" id="account-summary">Account</summary>
|
||||
<div class="account-menu-popover">
|
||||
<p class="account-email" id="account-email"></p>
|
||||
<button id="btn-settings" type="button">Settings & sync</button>
|
||||
<a id="menu-admin" class="hidden" href="/admin">Admin</a>
|
||||
<button id="btn-logout" type="button">Log out</button>
|
||||
<div class="app-workspace" id="app-workspace">
|
||||
<header class="app-header">
|
||||
<div class="header-row-top">
|
||||
<button
|
||||
class="icon-btn nav-hamburger"
|
||||
type="button"
|
||||
id="nav-hamburger"
|
||||
aria-label="Open navigation"
|
||||
aria-expanded="false"
|
||||
>
|
||||
☰
|
||||
</button>
|
||||
<div class="brand">
|
||||
<div class="brand-text">
|
||||
<h1>Roast Planner</h1>
|
||||
<p class="brand-sub" id="header-coffee-name">New plan</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
<button id="btn-print" type="button" class="primary-btn">Print</button>
|
||||
<span class="autosave-chip" id="autosave-status"
|
||||
><span class="dot"></span
|
||||
><span class="autosave-text">Not saved yet</span></span
|
||||
>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div
|
||||
class="connection-status hidden"
|
||||
id="connection-status"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
></div>
|
||||
<div class="drawer-overlay hidden" id="drawer-overlay"></div>
|
||||
<nav class="section-nav" id="section-nav" aria-label="Plan sections">
|
||||
<a href="#sec-coffee">Plan</a>
|
||||
<a href="#sec-bean">Bean</a>
|
||||
<a href="#sec-machine">Machine plan</a>
|
||||
<a href="#sec-roastlog">Roast log</a>
|
||||
<a href="#sec-after">After</a>
|
||||
</nav>
|
||||
|
||||
<aside
|
||||
class="drawer hidden"
|
||||
id="panel-prefill"
|
||||
<div class="header-actions">
|
||||
<span class="autosave-chip" id="autosave-status"
|
||||
><span class="dot"></span
|
||||
><span class="autosave-text">Not saved yet</span></span
|
||||
>
|
||||
<button id="btn-print" type="button" class="primary-btn">
|
||||
Print
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div
|
||||
class="connection-status hidden"
|
||||
id="connection-status"
|
||||
role="status"
|
||||
aria-live="polite"
|
||||
></div>
|
||||
<div class="drawer-overlay hidden" id="drawer-overlay"></div>
|
||||
|
||||
<aside
|
||||
class="drawer hidden"
|
||||
id="panel-prefill"
|
||||
aria-hidden="true"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
@@ -184,6 +251,39 @@
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<aside
|
||||
class="drawer hidden"
|
||||
id="panel-import-export"
|
||||
aria-hidden="true"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="import-export-title"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div class="drawer-head">
|
||||
<h3 id="import-export-title">Import / export</h3>
|
||||
<button
|
||||
class="icon-btn"
|
||||
type="button"
|
||||
data-close-drawer
|
||||
aria-label="Close"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<div class="drawer-body">
|
||||
<p class="stack-label">Load a plan from a JSON file</p>
|
||||
<button id="btn-load" type="button" class="primary-btn">
|
||||
Choose file…
|
||||
</button>
|
||||
<input id="file-load" type="file" accept="application/json" />
|
||||
<p class="stack-label">Save the current plan</p>
|
||||
<button id="btn-save" type="button" class="ghost-btn">
|
||||
Download as JSON
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<aside
|
||||
class="drawer hidden"
|
||||
id="panel-settings"
|
||||
@@ -230,6 +330,28 @@
|
||||
<section class="panel-card" id="sec-coffee">
|
||||
<div class="panel-head"><h2>The Coffee</h2></div>
|
||||
<div class="panel-body">
|
||||
<details class="why-panel" data-why="sec-coffee" open>
|
||||
<summary>Why cultivar sets the clock</summary>
|
||||
<p>
|
||||
A roast plan is three durations laid end to end: drying +
|
||||
Maillard + development = the total roast. Cultivar is the
|
||||
one input that sets a time you cannot negotiate — when
|
||||
first crack lands. The seed's genetics fix how much sugar
|
||||
and acid it carries and how heat gets into it, so each
|
||||
cultivar browns and builds its aromatics on its own
|
||||
schedule. A Bourbon rushed to a 7:30 first crack tastes
|
||||
thin and papery no matter how well you handle the rest; a
|
||||
Gesha dragged to 9:00 goes savory and loses the florals you
|
||||
paid for.
|
||||
</p>
|
||||
<p>
|
||||
That is why the FC anchor below feeds Time Ledger line 1
|
||||
and everything else in the plan is a correction of seconds
|
||||
around it. Treat the cultivar on the bag as your best
|
||||
hypothesis, not a fact — labels are wrong often enough that
|
||||
the "After the Roast" section exists to correct them.
|
||||
</p>
|
||||
</details>
|
||||
<div class="field-grid">
|
||||
<label class="field" data-fid="0.1"
|
||||
><span class="field-label">Coffee / lot</span
|
||||
@@ -254,6 +376,16 @@
|
||||
>
|
||||
</div></label
|
||||
>
|
||||
<label class="field" id="lot-picker-field">
|
||||
<span class="field-label">From inventory lot</span>
|
||||
<select class="field-input" name="inventory.lotId" id="lot-select">
|
||||
<option value="">— none —</option>
|
||||
</select>
|
||||
<span class="field-note" id="lot-picker-note"
|
||||
>Optional — link a lot and the Roast Log can draw the
|
||||
green weight down when you charge.</span
|
||||
>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="subhead">Cultivar</div>
|
||||
@@ -267,14 +399,14 @@
|
||||
placeholder="e.g. Caturra"
|
||||
autocomplete="off"
|
||||
/></label>
|
||||
<div class="field" data-fid="1.3">
|
||||
<label class="field" data-fid="1.3">
|
||||
<span class="field-label">Origin</span
|
||||
><input
|
||||
class="field-input"
|
||||
name="1.3"
|
||||
placeholder="e.g. Huila, Colombia"
|
||||
/>
|
||||
</div>
|
||||
</label>
|
||||
<div class="field wide" data-fid="1.2">
|
||||
<span class="field-label">Group</span>
|
||||
<div class="segmented" data-radio-group="1.2">
|
||||
@@ -303,13 +435,14 @@
|
||||
</div>
|
||||
<div class="field-grid">
|
||||
<label class="field" data-fid="1.4"
|
||||
><span class="field-label"
|
||||
>FC anchor
|
||||
<span class="hint" title="Feeds Time Ledger line 1"
|
||||
>ⓘ</span
|
||||
></span
|
||||
><input class="field-input" name="1.4" placeholder="8:45"
|
||||
/></label>
|
||||
><span class="field-label">FC anchor</span
|
||||
><input class="field-input" name="1.4" placeholder="8:45" />
|
||||
<span class="field-note"
|
||||
>Ledger line 1 — the whole plan hangs from this. Use
|
||||
your cultivar's own row (autofills from the list),
|
||||
never a group average.</span
|
||||
></label
|
||||
>
|
||||
<label class="field" data-fid="1.5"
|
||||
><span class="field-label">Profile mm:ss|mm:ss|mm:ss</span
|
||||
><input
|
||||
@@ -318,25 +451,23 @@
|
||||
placeholder="4:15|3:15|1:30"
|
||||
/></label>
|
||||
<label class="field" data-fid="1.6"
|
||||
><span class="field-label"
|
||||
>± Refine
|
||||
<span
|
||||
class="hint"
|
||||
title="0 on the first cook of this cultivar, otherwise carried from Step 12"
|
||||
>ⓘ</span
|
||||
></span
|
||||
><input class="field-input" name="1.6" placeholder="0"
|
||||
/></label>
|
||||
><span class="field-label">± Refine</span
|
||||
><input class="field-input" name="1.6" placeholder="0" />
|
||||
<span class="field-note"
|
||||
>0 the first time you roast this coffee. Afterwards,
|
||||
carry "One change next batch" here — this is how the
|
||||
plan learns.</span
|
||||
></label
|
||||
>
|
||||
<label class="field" data-fid="1.7"
|
||||
><span class="field-label"
|
||||
>± Dev modifier
|
||||
<span
|
||||
class="hint"
|
||||
title="Step 1's profile 3rd number, minus 1:30"
|
||||
>ⓘ</span
|
||||
></span
|
||||
><input class="field-input" name="1.7"
|
||||
/></label>
|
||||
><span class="field-label">± Dev modifier</span
|
||||
><input class="field-input" name="1.7" />
|
||||
<span class="field-note"
|
||||
>Your cultivar profile's third number minus 1:30 —
|
||||
autofilled; 0 for most cultivars. Feeds ledger line
|
||||
9.</span
|
||||
></label
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -344,20 +475,41 @@
|
||||
<section class="panel-card" id="sec-blend">
|
||||
<div class="panel-head">
|
||||
<h2>Blend</h2>
|
||||
<div class="segmented small" data-radio-group="2.1">
|
||||
<label class="seg-opt"
|
||||
><input type="radio" name="2.1" value="single" checked /><span
|
||||
>Single-origin</span
|
||||
></label
|
||||
>
|
||||
<label class="seg-opt"
|
||||
><input type="radio" name="2.1" value="blend" /><span
|
||||
>Blend</span
|
||||
></label
|
||||
>
|
||||
<div class="field" data-fid="2.1">
|
||||
<div class="segmented small" data-radio-group="2.1">
|
||||
<label class="seg-opt"
|
||||
><input type="radio" name="2.1" value="single" checked /><span
|
||||
>Single-origin</span
|
||||
></label
|
||||
>
|
||||
<label class="seg-opt"
|
||||
><input type="radio" name="2.1" value="blend" /><span
|
||||
>Blend</span
|
||||
></label
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-body" id="blend-body">
|
||||
<details class="why-panel" data-why="sec-blend" open>
|
||||
<summary>Why blends combine times, never temperatures</summary>
|
||||
<p>
|
||||
The drum has one heat setting and one clock — you cannot
|
||||
give 55% of the mass a different schedule than the other
|
||||
45%. Times are additive, so a weighted average of two
|
||||
first-crack anchors is another valid time on the same
|
||||
clock. An average of two temperatures means nothing: first
|
||||
crack is a property of the chemistry, not a dial you set.
|
||||
</p>
|
||||
<p>
|
||||
If one component is 60% or more of the batch, roast the
|
||||
whole batch as that coffee and let the rest ride along. If
|
||||
no component dominates, weight each anchor by its share.
|
||||
And if components disagree by more than about a minute at
|
||||
first crack, split-roast and blend after — one compromise
|
||||
profile serves neither half.
|
||||
</p>
|
||||
</details>
|
||||
<div class="blend-cards" id="blend-cards"></div>
|
||||
<div class="blend-total" id="blend-total">
|
||||
<div class="blend-total-bar">
|
||||
@@ -394,8 +546,12 @@
|
||||
<div class="field-grid">
|
||||
<label class="field" data-fid="2.4"
|
||||
><span class="field-label">Resulting FC anchor</span
|
||||
><input class="field-input" name="2.4"
|
||||
/></label>
|
||||
><input class="field-input" name="2.4" />
|
||||
<span class="field-note"
|
||||
>Replaces 1.4 as ledger line 1 while "Blend" is
|
||||
selected.</span
|
||||
></label
|
||||
>
|
||||
<label class="field" data-fid="2.5"
|
||||
><span class="field-label">Resulting processing modifier</span
|
||||
><input class="field-input" name="2.5"
|
||||
@@ -407,6 +563,30 @@
|
||||
<section class="panel-card" id="sec-process">
|
||||
<div class="panel-head"><h2>Roast Target</h2></div>
|
||||
<div class="panel-body">
|
||||
<details class="why-panel" data-why="sec-process" open>
|
||||
<summary>Why processing moves development, not first crack</summary>
|
||||
<p>
|
||||
Processing changed the chemistry that browning starts
|
||||
from. Naturals and dark honeys keep far more free
|
||||
fructose, and fructose browns faster than sucrose — the
|
||||
same profile lands visibly darker. That is why natural and
|
||||
honey lots take 15–30 seconds <em>less</em> development
|
||||
and want a slightly earlier drop, and why processing never
|
||||
touches the first-crack anchor. Change development and
|
||||
drop, hold everything else, and the experiment stays
|
||||
readable.
|
||||
</p>
|
||||
<p>
|
||||
Roast level is the one input that is purely your decision
|
||||
— and development is a bell curve, not a slider. Shorter
|
||||
development pushes acidity up, longer pulls it down; but
|
||||
too short reads sharp and astringent with no fruit behind
|
||||
it, and too long goes flat and dull. The pleasant region is
|
||||
in the middle and narrower than the acidity curve
|
||||
suggests, which is why the fix is always a 15-second step
|
||||
with a cupping in between, never a big swing.
|
||||
</p>
|
||||
</details>
|
||||
<div class="field wide" data-fid="3.1">
|
||||
<span class="field-label">Process</span>
|
||||
<div class="chip-group" data-radio-group="3.1">
|
||||
@@ -442,8 +622,13 @@
|
||||
<div class="field-grid">
|
||||
<label class="field" data-fid="3.2"
|
||||
><span class="field-label">Dev modifier</span
|
||||
><input class="field-input" name="3.2"
|
||||
/></label>
|
||||
><input class="field-input" name="3.2" />
|
||||
<span class="field-note"
|
||||
>Washed 0 · natural / dark honey −0:15 to −0:30 ·
|
||||
anaerobic a further −0:10 to −0:15. Feeds ledger line
|
||||
8.</span
|
||||
></label
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="subhead">Roast level</div>
|
||||
@@ -483,26 +668,49 @@
|
||||
<input class="field-input" name="4.2" /><span class="unit"
|
||||
>%</span
|
||||
>
|
||||
</div></label
|
||||
</div>
|
||||
<span class="field-note"
|
||||
>Light 11–13% · medium 14–16% · dark 17–18%. Your
|
||||
after-roast scale check against this is the honest
|
||||
measure of roast degree.</span
|
||||
></label
|
||||
>
|
||||
<label class="field" data-fid="4.3"
|
||||
><span class="field-label">Dev base</span
|
||||
><input class="field-input" name="4.3"
|
||||
/></label>
|
||||
><input class="field-input" name="4.3" />
|
||||
<span class="field-note"
|
||||
>Ledger line 7. Light 1:15–2:00 · medium 2:00–3:00 ·
|
||||
dark 3:00–4:00. Development past 4:30 mutes any
|
||||
roast.</span
|
||||
></label
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel-card" id="sec-bean">
|
||||
<div class="panel-head"><h2>Bean Condition</h2></div>
|
||||
<div class="unknown-condition" role="note">
|
||||
<strong>Don't have lab measurements?</strong> Moisture and density
|
||||
are optional. Leave them blank and record only evidence you have
|
||||
(supplier COA, lot notes, or your next roast). They never change
|
||||
the ledger automatically. Use Net correction only when you can
|
||||
explain the observation behind it.
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<details class="why-panel" data-why="sec-bean" open>
|
||||
<summary>Why bean condition is worth seconds, not a new plan</summary>
|
||||
<p>
|
||||
Moisture, density and screen size change how much energy the
|
||||
plan costs, not its shape. Wet greens (over 11.5%) stall as
|
||||
they near 100 °C while water boils off — expect the flat
|
||||
spot and don't panic-add heat. Dense, high-grown seed
|
||||
tolerates a hard early push; soft, low-grown seed gives you
|
||||
defects instead of speed. Very large beans scorch outside
|
||||
while the core lags — the safe way to go faster is more
|
||||
airflow at a lower inlet temperature, never more burner.
|
||||
</p>
|
||||
<p>
|
||||
None of these auto-adjust the ledger, and that is
|
||||
deliberate: they inform the ±0:15 judgment in Net correction
|
||||
and your heat/fan choices, nothing more. Don't have lab
|
||||
numbers? Leave moisture and density blank — record only
|
||||
evidence you actually have.
|
||||
</p>
|
||||
</details>
|
||||
<div class="field-grid">
|
||||
<label class="field" data-fid="5.1"
|
||||
><span class="field-label">Moisture</span>
|
||||
@@ -551,15 +759,14 @@
|
||||
</div>
|
||||
</div>
|
||||
<label class="field" data-fid="5.6"
|
||||
><span class="field-label"
|
||||
>± Net correction
|
||||
<span
|
||||
class="hint"
|
||||
title="Judgment call — rarely more than ±0:15"
|
||||
>ⓘ</span
|
||||
></span
|
||||
><input class="field-input" name="5.6"
|
||||
/></label>
|
||||
><span class="field-label">± Net correction</span
|
||||
><input class="field-input" name="5.6" />
|
||||
<span class="field-note"
|
||||
>Rarely more than ±0:15, and only for an observation
|
||||
you can name — moisture and density never create this
|
||||
number automatically. Feeds ledger line 3.</span
|
||||
></label
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -567,6 +774,31 @@
|
||||
<section class="panel-card" id="sec-machine">
|
||||
<div class="panel-head"><h2>Machine Plan</h2></div>
|
||||
<div class="panel-body">
|
||||
<details class="why-panel" data-why="sec-machine" open>
|
||||
<summary>
|
||||
Why the rate of rise must fall, and never touch zero
|
||||
</summary>
|
||||
<p>
|
||||
A bean that is still gaining heat, but gaining it more
|
||||
slowly, is moving through its reactions in order. Two
|
||||
shapes break that. The crash: RoR falls off a cliff just
|
||||
after first crack, when the exotherm ends and an oversized
|
||||
heat cut lands at the same moment. The bake: RoR flattens
|
||||
toward zero, the bean sits at temperature without
|
||||
progressing, and the cup goes flat, papery and
|
||||
sweetness-free — nothing after the drop rescues a bake.
|
||||
</p>
|
||||
<p>
|
||||
So the rule is: decline steadily from the
|
||||
post-turning-point peak, through small frequent heat cuts
|
||||
rather than a few big ones, and stay ahead of the
|
||||
machine's roughly 60-second lag — what you change now
|
||||
shows up a minute from now, so cut <em>before</em> the
|
||||
exotherm, gently. If a phase would need a rate outside the
|
||||
proven bands to hit its milestone, the plan is not
|
||||
reachable — fix the ledger, not the roaster.
|
||||
</p>
|
||||
</details>
|
||||
<div class="subhead">Temperatures & rate-of-rise</div>
|
||||
<div class="milestone-list">
|
||||
<div
|
||||
@@ -587,6 +819,7 @@
|
||||
class="field-input sm"
|
||||
name="temps.charge.tempC"
|
||||
placeholder="°C"
|
||||
aria-label="Charge planned temperature"
|
||||
/><span class="unit">°C</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -600,6 +833,7 @@
|
||||
class="field-input sm milestone-time"
|
||||
name="temps.tp.time"
|
||||
placeholder="m:ss"
|
||||
aria-label="Turning point planned time"
|
||||
/>
|
||||
<div class="band-track">
|
||||
<div class="band-range"></div>
|
||||
@@ -610,6 +844,7 @@
|
||||
class="field-input sm"
|
||||
name="temps.tp.tempC"
|
||||
placeholder="°C"
|
||||
aria-label="Turning point planned temperature"
|
||||
/><span class="unit">°C</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -629,6 +864,7 @@
|
||||
class="field-input sm"
|
||||
name="temps.yellow.tempC"
|
||||
placeholder="°C"
|
||||
aria-label="Yellow planned temperature"
|
||||
/><span class="unit">°C</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -648,6 +884,7 @@
|
||||
class="field-input sm"
|
||||
name="temps.fc.tempC"
|
||||
placeholder="°C"
|
||||
aria-label="First crack planned temperature"
|
||||
/><span class="unit">°C</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -667,12 +904,18 @@
|
||||
class="field-input sm"
|
||||
name="temps.drop.tempC"
|
||||
placeholder="°C"
|
||||
aria-label="Drop planned temperature"
|
||||
/><span class="unit">°C</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="subhead">Actuator schedule (planned)</div>
|
||||
<p class="field-note">
|
||||
The timing of your first heat cut is the main first-crack
|
||||
lever — later cut, earlier crack. Say why you make each
|
||||
change; that's what makes this log readable next week.
|
||||
</p>
|
||||
<div class="actuator-timeline" id="actuator-timeline"></div>
|
||||
<button
|
||||
type="button"
|
||||
@@ -684,9 +927,34 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel-card" id="sec-roastlog">
|
||||
<div class="panel-head"><h2>Roast Log — Plan vs. Actual</h2></div>
|
||||
<div class="phase-divider" role="separator" aria-label="Roast day begins">
|
||||
<span class="phase-divider-label">Roast day</span>
|
||||
<span class="phase-divider-text"
|
||||
>Everything above is decided before you charge. Fill in from
|
||||
here down at the machine, then after cupping. Print takes the
|
||||
plan with you.</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<section class="panel-card phase-later" id="sec-roastlog">
|
||||
<div class="panel-head">
|
||||
<h2>Roast Log — Plan vs. Actual</h2>
|
||||
<span class="phase-chip">during the roast</span>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<details class="why-panel" data-why="sec-roastlog" open>
|
||||
<summary>Record what you observed, never what you planned</summary>
|
||||
<p>
|
||||
A planned milestone written into the log as though it
|
||||
happened destroys the only evidence you have. If you never
|
||||
actually heard first crack, write that down — "not heard,
|
||||
called at 8:10 by smell" is a useful record. "8:00"
|
||||
because that was the plan is worse than nothing, because
|
||||
next batch you will trust it. The plan column stays fixed
|
||||
beside your entries precisely so the gap is visible. The
|
||||
gap is the data.
|
||||
</p>
|
||||
</details>
|
||||
<div class="milestone-list">
|
||||
<div class="milestone-row log-row">
|
||||
<div class="milestone-label">Charge</div>
|
||||
@@ -697,16 +965,19 @@
|
||||
class="field-input sm"
|
||||
name="planActual.charge.actualTime"
|
||||
placeholder="0:00"
|
||||
aria-label="Charge actual time"
|
||||
/>
|
||||
<input
|
||||
class="field-input sm"
|
||||
name="planActual.charge.actualBt"
|
||||
placeholder="°C"
|
||||
aria-label="Charge actual temperature"
|
||||
/>
|
||||
<input
|
||||
class="field-input note"
|
||||
name="planActual.charge.note"
|
||||
placeholder="Note"
|
||||
aria-label="Charge note"
|
||||
/>
|
||||
</div>
|
||||
<div class="milestone-row log-row">
|
||||
@@ -716,16 +987,19 @@
|
||||
class="field-input sm"
|
||||
name="planActual.tp.actualTime"
|
||||
placeholder="0:00"
|
||||
aria-label="Turning point actual time"
|
||||
/>
|
||||
<input
|
||||
class="field-input sm"
|
||||
name="planActual.tp.actualBt"
|
||||
placeholder="°C"
|
||||
aria-label="Turning point actual temperature"
|
||||
/>
|
||||
<input
|
||||
class="field-input note"
|
||||
name="planActual.tp.note"
|
||||
placeholder="Note"
|
||||
aria-label="Turning point note"
|
||||
/>
|
||||
</div>
|
||||
<div class="milestone-row log-row">
|
||||
@@ -735,16 +1009,19 @@
|
||||
class="field-input sm"
|
||||
name="planActual.yellow.actualTime"
|
||||
placeholder="0:00"
|
||||
aria-label="Yellow actual time"
|
||||
/>
|
||||
<input
|
||||
class="field-input sm"
|
||||
name="planActual.yellow.actualBt"
|
||||
placeholder="°C"
|
||||
aria-label="Yellow actual temperature"
|
||||
/>
|
||||
<input
|
||||
class="field-input note"
|
||||
name="planActual.yellow.note"
|
||||
placeholder="Note"
|
||||
aria-label="Yellow note"
|
||||
/>
|
||||
</div>
|
||||
<div class="milestone-row log-row">
|
||||
@@ -754,16 +1031,19 @@
|
||||
class="field-input sm"
|
||||
name="planActual.fc.actualTime"
|
||||
placeholder="0:00"
|
||||
aria-label="First crack actual time"
|
||||
/>
|
||||
<input
|
||||
class="field-input sm"
|
||||
name="planActual.fc.actualBt"
|
||||
placeholder="°C"
|
||||
aria-label="First crack actual temperature"
|
||||
/>
|
||||
<input
|
||||
class="field-input note"
|
||||
name="planActual.fc.note"
|
||||
placeholder="Note"
|
||||
aria-label="First crack note"
|
||||
/>
|
||||
</div>
|
||||
<div class="milestone-row log-row">
|
||||
@@ -773,25 +1053,61 @@
|
||||
class="field-input sm"
|
||||
name="planActual.drop.actualTime"
|
||||
placeholder="0:00"
|
||||
aria-label="Drop actual time"
|
||||
/>
|
||||
<input
|
||||
class="field-input sm"
|
||||
name="planActual.drop.actualBt"
|
||||
placeholder="°C"
|
||||
aria-label="Drop actual temperature"
|
||||
/>
|
||||
<input
|
||||
class="field-input note"
|
||||
name="planActual.drop.note"
|
||||
placeholder="Note"
|
||||
aria-label="Drop note"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="inventory-consume hidden" id="inventory-consume">
|
||||
<span class="inventory-consume-label" id="inventory-consume-label"></span>
|
||||
<button type="button" class="ghost-btn small" id="btn-consume">
|
||||
Draw from lot
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel-card" id="sec-after">
|
||||
<div class="panel-head"><h2>After the Roast</h2></div>
|
||||
<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" open>
|
||||
<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>
|
||||
@@ -867,6 +1183,12 @@
|
||||
><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>
|
||||
@@ -874,6 +1196,10 @@
|
||||
<aside class="dock" aria-label="Live plan feedback">
|
||||
<div class="dock-card ledger-card" id="plan-now">
|
||||
<div class="dock-card-head"><h2>Time Ledger</h2></div>
|
||||
<p class="dock-note">
|
||||
Drying + Maillard + Development = the whole roast. Cultivar
|
||||
sets first crack; process and level set development.
|
||||
</p>
|
||||
|
||||
<div class="ledger-group">
|
||||
<div class="ledger-row">
|
||||
@@ -894,7 +1220,13 @@
|
||||
<div class="ledger-row">
|
||||
<span class="l-fid">6.4</span
|
||||
><span class="l-label">± Batch size</span
|
||||
><input class="ledger-input" name="6.4" placeholder="0" />
|
||||
><input
|
||||
class="ledger-input"
|
||||
name="6.4"
|
||||
placeholder="0"
|
||||
aria-label="Batch-size correction"
|
||||
title="225 g lands first crack ~1:30–2:00 later than 200 g"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ledger-total hero">
|
||||
@@ -952,21 +1284,37 @@
|
||||
<span class="check-name">Drying share</span>
|
||||
<output class="check-value" data-out="check-drying">—</output>
|
||||
<span class="check-band">target 43–51%</span>
|
||||
<span class="check-why"
|
||||
>high = late yellow, browning starved · low = rushed
|
||||
before internal pressure built</span
|
||||
>
|
||||
</div>
|
||||
<div class="check-tile unknown" data-pass="maillard">
|
||||
<span class="check-name">Maillard share</span>
|
||||
<output class="check-value" data-out="check-maillard">—</output>
|
||||
<span class="check-band">target 33–39%</span>
|
||||
<span class="check-why"
|
||||
>low = too small a browning window for sweetness and
|
||||
complexity</span
|
||||
>
|
||||
</div>
|
||||
<div class="check-tile unknown" data-pass="dtr">
|
||||
<span class="check-name">Development ratio</span>
|
||||
<output class="check-value" data-out="check-dtr">—</output>
|
||||
<span class="check-band">target 12–20%</span>
|
||||
<span class="check-why"
|
||||
>under 12% = under-developed — sharp, astringent acidity
|
||||
with no fruit behind it</span
|
||||
>
|
||||
</div>
|
||||
<div class="check-tile unknown" data-pass="ceiling">
|
||||
<span class="check-name">Development ceiling</span>
|
||||
<output class="check-value" data-out="check-ceiling">—</output>
|
||||
<span class="check-band">under 4:30</span>
|
||||
<span class="check-why"
|
||||
>over 4:30 mutes any roast — nothing good is past this
|
||||
line</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1116,8 +1464,12 @@
|
||||
</aside>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<!-- /app-workspace -->
|
||||
</div>
|
||||
<!-- /app-shell -->
|
||||
|
||||
<!-- Kept OUTSIDE #plan-form on purpose: identically-named radios here must not fight for
|
||||
<!-- Kept OUTSIDE #plan-form and OUTSIDE .app-shell on purpose: identically-named radios here must not fight for
|
||||
mutual exclusivity with the screen form's radios (same name + same form owner would
|
||||
silently uncheck one another). This whole block is display:none except when printing;
|
||||
main.js re-syncs it from state.plan on beforeprint. -->
|
||||
|
||||
Reference in New Issue
Block a user