Test and deploy / test-and-deploy (push) Successful in 59s
Co-Authored-By: Claude Fable 5 <[email protected]>
2505 lines
104 KiB
HTML
2505 lines
104 KiB
HTML
<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<title>Roast Planner</title>
|
||
<meta name="theme-color" content="#A8481A" />
|
||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
|
||
<link rel="manifest" href="/manifest.webmanifest?v=__ASSET_VERSION__" />
|
||
<link rel="icon" href="/icon.svg?v=__ASSET_VERSION__" type="image/svg+xml" />
|
||
<link rel="apple-touch-icon" href="/icon.svg?v=__ASSET_VERSION__" />
|
||
<link rel="stylesheet" href="/worksheet.css" media="print" />
|
||
<link rel="stylesheet" href="/app.css?v=__ASSET_VERSION__" />
|
||
</head>
|
||
<body>
|
||
<div class="app-shell">
|
||
<nav class="side-nav" id="side-nav" aria-label="Main navigation">
|
||
<div class="side-nav-brand">
|
||
<img class="brand-mark" src="/brand-mark.svg?v=__ASSET_VERSION__" alt="" aria-hidden="true" />
|
||
<span class="brand-name">Roast Planner</span>
|
||
</div>
|
||
<div id="nav-links"></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>
|
||
</nav>
|
||
|
||
<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>
|
||
|
||
<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</a>
|
||
<a href="#sec-roastlog">Log</a>
|
||
</nav>
|
||
|
||
<div class="header-actions header-tools">
|
||
<span class="autosave-chip" id="autosave-status"
|
||
><span class="dot"></span
|
||
><span class="autosave-text">Not saved yet</span></span
|
||
>
|
||
<button
|
||
id="btn-tools-menu"
|
||
type="button"
|
||
class="ghost-btn small header-tools-toggle"
|
||
aria-expanded="false"
|
||
aria-controls="header-tools-group"
|
||
>
|
||
⚙ Tools ▾
|
||
</button>
|
||
<div class="header-tools-group" id="header-tools-group">
|
||
<button id="nav-plans" type="button" class="ghost-btn small">
|
||
▤ Plans
|
||
</button>
|
||
<button id="nav-chat" type="button" class="ghost-btn small">
|
||
✳ Ask the LLM
|
||
</button>
|
||
<button id="nav-prefill" type="button" class="ghost-btn small">
|
||
↗ Prefill
|
||
</button>
|
||
<button id="nav-alog" type="button" class="ghost-btn small">
|
||
∿ Curves
|
||
</button>
|
||
<button id="nav-import-export" type="button" class="ghost-btn small">
|
||
⇅ Import / export
|
||
</button>
|
||
<button
|
||
id="btn-toggle-fids"
|
||
type="button"
|
||
class="ghost-btn small"
|
||
aria-pressed="false"
|
||
title="Show the worksheet reference codes (e.g. 1.4) on every field"
|
||
>
|
||
# IDs
|
||
</button>
|
||
<button id="nav-settings" type="button" class="ghost-btn small">
|
||
◎ Settings
|
||
</button>
|
||
<span class="header-divider" aria-hidden="true"></span>
|
||
<button id="btn-expand-why" type="button" class="ghost-btn small">
|
||
Expand "why" notes
|
||
</button>
|
||
</div>
|
||
<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"
|
||
aria-labelledby="prefill-title"
|
||
tabindex="-1"
|
||
>
|
||
<div class="drawer-head">
|
||
<h3 id="prefill-title">Prefill from a bean product URL</h3>
|
||
<button
|
||
class="icon-btn"
|
||
type="button"
|
||
data-close-drawer
|
||
aria-label="Close"
|
||
>
|
||
✕
|
||
</button>
|
||
</div>
|
||
<div class="drawer-body">
|
||
<label class="stack-label" for="prefill-url">Product page URL</label>
|
||
<input
|
||
type="url"
|
||
id="prefill-url"
|
||
class="text-input"
|
||
placeholder="https://roaster.example/shop/some-coffee"
|
||
/>
|
||
<label class="checkbox-row"
|
||
><input type="checkbox" id="prefill-overwrite" /> Overwrite
|
||
already-filled fields</label
|
||
>
|
||
<div class="drawer-actions">
|
||
<button id="prefill-go" type="button" class="primary-btn">
|
||
Fetch & prefill
|
||
</button>
|
||
<button id="prefill-undo" type="button" class="ghost-btn" disabled>
|
||
Undo prefill
|
||
</button>
|
||
</div>
|
||
<div id="prefill-result" class="drawer-result"></div>
|
||
</div>
|
||
</aside>
|
||
|
||
<aside
|
||
class="drawer hidden"
|
||
id="panel-alog"
|
||
aria-hidden="true"
|
||
role="dialog"
|
||
aria-modal="true"
|
||
aria-labelledby="alog-title"
|
||
tabindex="-1"
|
||
>
|
||
<div class="drawer-head">
|
||
<h3 id="alog-title">Attach an Artisan .alog reference curve</h3>
|
||
<button
|
||
class="icon-btn"
|
||
type="button"
|
||
data-close-drawer
|
||
aria-label="Close"
|
||
>
|
||
✕
|
||
</button>
|
||
</div>
|
||
<div class="drawer-body">
|
||
<label class="filebtn primary-btn"
|
||
>Choose .alog file<input type="file" id="alog-file" accept=".alog"
|
||
/></label>
|
||
<button id="alog-library-refresh" type="button" class="ghost-btn">
|
||
Show local library
|
||
</button>
|
||
<ul class="lib-list hidden" id="alog-library-list"></ul>
|
||
<div id="alog-result" class="drawer-result"></div>
|
||
<hr style="border:none;border-top:1px solid var(--line,#e5dcd2);margin:16px 0" />
|
||
<h4 style="margin:0 0 6px">Log a finished roast</h4>
|
||
<p class="field-note" style="margin-top:0">
|
||
Upload the actual roast's .alog to attach it to this plan — you can
|
||
upload several. The LLM reviews each one in the background.
|
||
</p>
|
||
<label class="filebtn primary-btn"
|
||
>Upload finished roast(s)<input
|
||
type="file"
|
||
id="actual-alog-file"
|
||
accept=".alog"
|
||
multiple
|
||
/></label>
|
||
<div id="actual-alog-result" class="drawer-result"></div>
|
||
<p class="field-note" style="margin-bottom:0">
|
||
<a href="/roasts">View roast history →</a>
|
||
</p>
|
||
</div>
|
||
</aside>
|
||
|
||
<aside
|
||
class="drawer hidden"
|
||
id="panel-chat"
|
||
aria-hidden="true"
|
||
role="dialog"
|
||
aria-modal="true"
|
||
aria-labelledby="chat-title"
|
||
tabindex="-1"
|
||
>
|
||
<div class="drawer-head">
|
||
<h3 id="chat-title">Ask the LLM about this plan</h3>
|
||
<button
|
||
class="icon-btn"
|
||
type="button"
|
||
data-close-drawer
|
||
aria-label="Close"
|
||
>
|
||
✕
|
||
</button>
|
||
</div>
|
||
<div class="drawer-body">
|
||
<div class="chat-thread" id="chat-thread"></div>
|
||
<form id="chat-form" class="chat-input-row">
|
||
<textarea
|
||
class="field-input"
|
||
id="chat-input"
|
||
rows="2"
|
||
placeholder="e.g. Why is first crack at 8:30? What should I change for more sweetness?"
|
||
></textarea>
|
||
<button class="primary-btn" type="submit" id="chat-send">Send</button>
|
||
</form>
|
||
<p class="field-note">
|
||
Answers are grounded in this plan's numbers, your learned pace, and
|
||
the roaster behavior learned from your uploaded roasts.
|
||
</p>
|
||
</div>
|
||
</aside>
|
||
|
||
<aside
|
||
class="drawer hidden"
|
||
id="panel-plans"
|
||
aria-hidden="true"
|
||
role="dialog"
|
||
aria-modal="true"
|
||
aria-labelledby="plans-title"
|
||
tabindex="-1"
|
||
>
|
||
<div class="drawer-head">
|
||
<h3 id="plans-title">Your plans</h3>
|
||
<button
|
||
class="icon-btn"
|
||
type="button"
|
||
data-close-drawer
|
||
aria-label="Close"
|
||
>
|
||
✕
|
||
</button>
|
||
</div>
|
||
<div class="drawer-body">
|
||
<button id="btn-new-plan" type="button" class="primary-btn">
|
||
New plan
|
||
</button>
|
||
<p class="drawer-result">
|
||
Choose a plan to continue. Every edit is stored on this device first
|
||
and then synced when online.
|
||
</p>
|
||
<ul class="plan-list" id="plan-list" aria-live="polite"></ul>
|
||
</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"
|
||
aria-hidden="true"
|
||
role="dialog"
|
||
aria-modal="true"
|
||
aria-labelledby="settings-title"
|
||
tabindex="-1"
|
||
>
|
||
<div class="drawer-head">
|
||
<h3 id="settings-title">Settings & sync</h3>
|
||
<button
|
||
class="icon-btn"
|
||
type="button"
|
||
data-close-drawer
|
||
aria-label="Close"
|
||
>
|
||
✕
|
||
</button>
|
||
</div>
|
||
<div class="drawer-body">
|
||
<p class="drawer-result">
|
||
<strong id="settings-email"></strong> is the active account. Plans are
|
||
saved locally first, then synced to your account whenever you are
|
||
online.
|
||
</p>
|
||
<div class="field">
|
||
<span class="field-label">Temperature unit</span>
|
||
<div
|
||
class="segmented small"
|
||
id="temp-unit-toggle"
|
||
role="radiogroup"
|
||
aria-label="Temperature unit"
|
||
>
|
||
<label class="seg-opt"
|
||
><input type="radio" name="tempUnit" value="C" checked /><span
|
||
>°C</span
|
||
></label
|
||
>
|
||
<label class="seg-opt"
|
||
><input type="radio" name="tempUnit" value="F" /><span
|
||
>°F</span
|
||
></label
|
||
>
|
||
</div>
|
||
<p class="field-note">
|
||
Changes how Machine Plan and Roast Log temperatures are shown.
|
||
Plans are always stored in °C, so switching back is lossless.
|
||
</p>
|
||
</div>
|
||
<p class="drawer-result">
|
||
Install Roast Planner from your browser menu for a focused workspace.
|
||
Updates wait for your confirmation so an in-progress plan is never
|
||
discarded.
|
||
</p>
|
||
<button id="btn-install" type="button" class="primary-btn hidden">
|
||
Install Roast Planner
|
||
</button>
|
||
<button id="btn-refresh" type="button" class="ghost-btn hidden">
|
||
Update ready — refresh safely
|
||
</button>
|
||
</div>
|
||
</aside>
|
||
|
||
<dialog class="help-dialog" id="field-help-dialog" aria-labelledby="help-dialog-title">
|
||
<div class="help-card">
|
||
<div class="help-head">
|
||
<div>
|
||
<span class="help-kicker">Field guide</span>
|
||
<h3 id="help-dialog-title"></h3>
|
||
</div>
|
||
<button class="icon-btn" type="button" data-close-help aria-label="Close">✕</button>
|
||
</div>
|
||
<div class="help-body" id="help-dialog-body"></div>
|
||
<p class="help-attrib">
|
||
Timing framework: Rob Hoos, <em>Cultivars: A Practical Guide for Coffee Roasters</em>.
|
||
Temperatures, bands and machine settings: your own logged roasts on the Hottop
|
||
KN-8828B-2K+ — they transfer to no other machine.
|
||
</p>
|
||
</div>
|
||
</dialog>
|
||
|
||
<form id="plan-form">
|
||
<div class="workspace-grid">
|
||
<div class="form-col">
|
||
<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">
|
||
<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
|
||
><input
|
||
class="field-input"
|
||
name="0.1"
|
||
placeholder="e.g. Finca El Ocaso"
|
||
/></label>
|
||
<label class="field" data-fid="0.2"
|
||
><span class="field-label">Date</span
|
||
><input class="field-input" type="date" name="0.2"
|
||
/></label>
|
||
<label class="field" data-fid="0.3"
|
||
><span class="field-label">Batch #</span
|
||
><input class="field-input" name="0.3"
|
||
/></label>
|
||
<label class="field" data-fid="0.4"
|
||
><span class="field-label">Green in</span>
|
||
<div class="unit-input">
|
||
<input class="field-input" name="0.4" /><span class="unit"
|
||
>g</span
|
||
>
|
||
</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>
|
||
<div class="field-grid">
|
||
<label class="field" data-fid="1.1"
|
||
><span class="field-label">Cultivar</span
|
||
><input
|
||
class="field-input"
|
||
name="1.1"
|
||
list="cultivar-list"
|
||
placeholder="e.g. Caturra"
|
||
autocomplete="off"
|
||
/></label>
|
||
<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"
|
||
/>
|
||
</label>
|
||
<div class="field wide" data-fid="1.2">
|
||
<span class="field-label">Group</span>
|
||
<div class="segmented" data-radio-group="1.2">
|
||
<label class="seg-opt"
|
||
><input type="radio" name="1.2" value="typica" /><span
|
||
>Typica</span
|
||
></label
|
||
>
|
||
<label class="seg-opt"
|
||
><input type="radio" name="1.2" value="bourbon" /><span
|
||
>Bourbon</span
|
||
></label
|
||
>
|
||
<label class="seg-opt"
|
||
><input type="radio" name="1.2" value="ethiopian" /><span
|
||
>Ethiopian</span
|
||
></label
|
||
>
|
||
<label class="seg-opt"
|
||
><input type="radio" name="1.2" value="hybrid" /><span
|
||
>Hybrid</span
|
||
></label
|
||
>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="field-grid">
|
||
<label class="field" data-fid="1.4"
|
||
><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
|
||
class="field-input"
|
||
name="1.5"
|
||
placeholder="4:15|3:15|1:30"
|
||
/></label>
|
||
<label class="field" data-fid="1.6"
|
||
><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
|
||
>
|
||
<div class="refine-suggestion hidden" id="refine-suggestion">
|
||
<span id="refine-suggestion-text"></span>
|
||
<button
|
||
type="button"
|
||
class="ghost-btn small hidden"
|
||
id="btn-apply-refine"
|
||
>
|
||
Apply
|
||
</button>
|
||
</div></label
|
||
>
|
||
<label class="field" data-fid="1.7"
|
||
><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>
|
||
|
||
<section class="panel-card" id="sec-blend">
|
||
<div class="panel-head">
|
||
<h2>Blend</h2>
|
||
<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">
|
||
<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">
|
||
<div class="blend-total-fill" id="blend-total-fill"></div>
|
||
</div>
|
||
<span class="blend-total-label" id="blend-total-label"
|
||
>0% of 100%</span
|
||
>
|
||
</div>
|
||
<button type="button" class="ghost-btn small" id="btn-add-blend">
|
||
+ Add component
|
||
</button>
|
||
|
||
<div class="subhead">How the components combine</div>
|
||
<div class="field wide" data-fid="2.3">
|
||
<div class="segmented" data-radio-group="2.3">
|
||
<label class="seg-opt"
|
||
><input type="radio" name="2.3" value="dominant" /><span
|
||
>Dominant (≥60%)</span
|
||
></label
|
||
>
|
||
<label class="seg-opt"
|
||
><input type="radio" name="2.3" value="weighted" /><span
|
||
>Weighted average</span
|
||
></label
|
||
>
|
||
<label class="seg-opt"
|
||
><input type="radio" name="2.3" value="split" /><span
|
||
>Split-roast, blend after</span
|
||
></label
|
||
>
|
||
</div>
|
||
</div>
|
||
<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" />
|
||
<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"
|
||
/></label>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<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">
|
||
<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">
|
||
<label class="chip-opt"
|
||
><input type="radio" name="3.1" value="washed" /><span
|
||
>Washed</span
|
||
></label
|
||
>
|
||
<label class="chip-opt"
|
||
><input type="radio" name="3.1" value="yellow-honey" /><span
|
||
>Yellow / white honey</span
|
||
></label
|
||
>
|
||
<label class="chip-opt"
|
||
><input
|
||
type="radio"
|
||
name="3.1"
|
||
value="red-black-honey"
|
||
/><span>Red / black honey</span></label
|
||
>
|
||
<label class="chip-opt"
|
||
><input type="radio" name="3.1" value="natural" /><span
|
||
>Natural</span
|
||
></label
|
||
>
|
||
<label class="chip-opt"
|
||
><input type="radio" name="3.1" value="anaerobic" /><span
|
||
>Anaerobic / CM</span
|
||
></label
|
||
>
|
||
</div>
|
||
</div>
|
||
<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" />
|
||
<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>
|
||
<div class="field wide" data-fid="4.1">
|
||
<div class="chip-group" data-radio-group="4.1">
|
||
<label class="chip-opt"
|
||
><input type="radio" name="4.1" value="light" /><span
|
||
>Light</span
|
||
></label
|
||
>
|
||
<label class="chip-opt"
|
||
><input type="radio" name="4.1" value="light-medium" /><span
|
||
>Light-medium</span
|
||
></label
|
||
>
|
||
<label class="chip-opt"
|
||
><input type="radio" name="4.1" value="medium" /><span
|
||
>Medium</span
|
||
></label
|
||
>
|
||
<label class="chip-opt"
|
||
><input type="radio" name="4.1" value="dark" /><span
|
||
>Dark</span
|
||
></label
|
||
>
|
||
<label class="chip-opt"
|
||
><input type="radio" name="4.1" value="very-dark" /><span
|
||
>Very dark</span
|
||
></label
|
||
>
|
||
</div>
|
||
</div>
|
||
<div class="field-grid">
|
||
<label class="field" data-fid="4.2"
|
||
><span class="field-label">Target weight loss</span>
|
||
<div class="unit-input">
|
||
<input class="field-input" name="4.2" /><span class="unit"
|
||
>%</span
|
||
>
|
||
</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" />
|
||
<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="panel-body">
|
||
<details class="why-panel" data-why="sec-bean">
|
||
<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>
|
||
<div class="unit-input">
|
||
<input class="field-input" name="5.1" /><span class="unit"
|
||
>%</span
|
||
>
|
||
</div></label
|
||
>
|
||
<label class="field" data-fid="5.2"
|
||
><span class="field-label">Density</span>
|
||
<div class="unit-input">
|
||
<input class="field-input" name="5.2" /><span class="unit"
|
||
>g/L</span
|
||
>
|
||
</div></label
|
||
>
|
||
<label class="field" data-fid="5.3"
|
||
><span class="field-label">Altitude</span
|
||
><input class="field-input" name="5.3"
|
||
/></label>
|
||
<label class="field" data-fid="5.4"
|
||
><span class="field-label">Screen</span
|
||
><input class="field-input" name="5.4"
|
||
/></label>
|
||
</div>
|
||
<div class="field-grid">
|
||
<div class="field" data-fid="5.5">
|
||
<span class="field-label">Sweet spot</span>
|
||
<div class="segmented" data-radio-group="5.5">
|
||
<label class="seg-opt"
|
||
><input type="radio" name="5.5" value="narrow" /><span
|
||
>Narrow</span
|
||
></label
|
||
>
|
||
<label class="seg-opt"
|
||
><input type="radio" name="5.5" value="med" /><span
|
||
>Medium</span
|
||
></label
|
||
>
|
||
<label class="seg-opt"
|
||
><input type="radio" name="5.5" value="wide" /><span
|
||
>Wide</span
|
||
></label
|
||
>
|
||
</div>
|
||
</div>
|
||
<label class="field" data-fid="5.6"
|
||
><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>
|
||
|
||
<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">
|
||
<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
|
||
class="milestone-row band-row"
|
||
data-band-lo="133"
|
||
data-band-hi="195"
|
||
>
|
||
<div class="milestone-label">Charge</div>
|
||
<output class="milestone-plan" data-out="t-charge"
|
||
>0:00</output
|
||
>
|
||
<div class="band-track">
|
||
<div class="band-range"></div>
|
||
<div class="band-marker"></div>
|
||
</div>
|
||
<div class="unit-input sm">
|
||
<input
|
||
class="field-input sm"
|
||
name="temps.charge.tempC"
|
||
placeholder="°C"
|
||
aria-label="Charge planned temperature"
|
||
/><span class="unit">°C</span>
|
||
</div>
|
||
</div>
|
||
<div
|
||
class="milestone-row band-row"
|
||
data-band-lo="77"
|
||
data-band-hi="100"
|
||
>
|
||
<div class="milestone-label">Turning point</div>
|
||
<input
|
||
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>
|
||
<div class="band-marker"></div>
|
||
</div>
|
||
<div class="unit-input sm">
|
||
<input
|
||
class="field-input sm"
|
||
name="temps.tp.tempC"
|
||
placeholder="°C"
|
||
aria-label="Turning point planned temperature"
|
||
/><span class="unit">°C</span>
|
||
</div>
|
||
</div>
|
||
<div
|
||
class="milestone-row band-row"
|
||
data-band-lo="148"
|
||
data-band-hi="166"
|
||
>
|
||
<div class="milestone-label">Yellow</div>
|
||
<output class="milestone-plan" data-out="t-yellow">—</output>
|
||
<div class="band-track">
|
||
<div class="band-range"></div>
|
||
<div class="band-marker"></div>
|
||
</div>
|
||
<div class="unit-input sm">
|
||
<input
|
||
class="field-input sm"
|
||
name="temps.yellow.tempC"
|
||
placeholder="°C"
|
||
aria-label="Yellow planned temperature"
|
||
/><span class="unit">°C</span>
|
||
</div>
|
||
</div>
|
||
<div
|
||
class="milestone-row band-row"
|
||
data-band-lo="176"
|
||
data-band-hi="187"
|
||
>
|
||
<div class="milestone-label">First crack</div>
|
||
<output class="milestone-plan" data-out="t-fc">—</output>
|
||
<div class="band-track">
|
||
<div class="band-range"></div>
|
||
<div class="band-marker"></div>
|
||
</div>
|
||
<div class="unit-input sm">
|
||
<input
|
||
class="field-input sm"
|
||
name="temps.fc.tempC"
|
||
placeholder="°C"
|
||
aria-label="First crack planned temperature"
|
||
/><span class="unit">°C</span>
|
||
</div>
|
||
</div>
|
||
<div
|
||
class="milestone-row band-row"
|
||
data-band-lo="183"
|
||
data-band-hi="197"
|
||
>
|
||
<div class="milestone-label">Drop</div>
|
||
<output class="milestone-plan" data-out="t-drop">—</output>
|
||
<div class="band-track">
|
||
<div class="band-range"></div>
|
||
<div class="band-marker"></div>
|
||
</div>
|
||
<div class="unit-input sm">
|
||
<input
|
||
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"
|
||
class="ghost-btn small"
|
||
id="btn-add-actuator"
|
||
>
|
||
+ Add step
|
||
</button>
|
||
</div>
|
||
</section>
|
||
|
||
<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">
|
||
<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>
|
||
<output class="milestone-plan" data-out="pa-charge"
|
||
>0:00</output
|
||
>
|
||
<input
|
||
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">
|
||
<div class="milestone-label">Turning point</div>
|
||
<output class="milestone-plan" data-out="pa-tp">—</output>
|
||
<input
|
||
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">
|
||
<div class="milestone-label">Yellow</div>
|
||
<output class="milestone-plan" data-out="pa-yellow">—</output>
|
||
<input
|
||
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">
|
||
<div class="milestone-label">First crack</div>
|
||
<output class="milestone-plan" data-out="pa-fc">—</output>
|
||
<input
|
||
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">
|
||
<div class="milestone-label">Drop</div>
|
||
<output class="milestone-plan" data-out="pa-drop">—</output>
|
||
<input
|
||
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
|
||
style="display:flex;gap:10px;align-items:center;flex-wrap:wrap;margin-top:12px"
|
||
>
|
||
<button type="button" class="ghost-btn" id="btn-open-cupping">
|
||
Open cupping session
|
||
</button>
|
||
<span class="field-note" id="cupping-link-note" style="margin:0"></span>
|
||
</div>
|
||
<p class="field-note">
|
||
After the roast, upload the .alog on the
|
||
<a href="/roasts">Roasts page</a> — weights, colour, cup notes,
|
||
and "one change next batch" now live there with the LLM review.
|
||
</p>
|
||
</div>
|
||
</section>
|
||
|
||
</div>
|
||
|
||
<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>
|
||
<p class="dock-note machine-profile-note" id="machine-profile-note"></p>
|
||
|
||
<div class="ledger-group">
|
||
<div class="ledger-row">
|
||
<span class="l-fid">1.4</span
|
||
><span class="l-label">Cultivar / blend anchor</span
|
||
><output class="l-val" data-out="l1">—</output>
|
||
</div>
|
||
<div class="ledger-row">
|
||
<span class="l-fid">1.6</span
|
||
><span class="l-label">± Refinement</span
|
||
><output class="l-val" data-out="l2">—</output>
|
||
</div>
|
||
<div class="ledger-row">
|
||
<span class="l-fid">5.6</span
|
||
><span class="l-label">± Bean condition</span
|
||
><output class="l-val" data-out="l3">—</output>
|
||
</div>
|
||
<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"
|
||
aria-label="Batch-size correction"
|
||
title="225 g lands first crack ~1:30–2:00 later than 200 g"
|
||
/>
|
||
</div>
|
||
<div class="ledger-row ledger-subtotal">
|
||
<span class="l-fid"></span
|
||
><span class="l-label">Subtotal (before pace)</span
|
||
><output class="l-val" data-out="subtotalA">—</output>
|
||
</div>
|
||
<div class="ledger-row ledger-pace">
|
||
<span class="l-fid"></span
|
||
><span class="l-label">× Machine pace</span
|
||
><output class="l-val" data-out="pace-factor">×1.00</output>
|
||
</div>
|
||
</div>
|
||
<div class="ledger-total hero">
|
||
<span>First crack</span><output data-out="A">—</output>
|
||
</div>
|
||
|
||
<div class="ledger-info">
|
||
<div class="info-pill">
|
||
<span>Yellow</span><output data-out="yellow">—</output>
|
||
</div>
|
||
<div class="info-pill">
|
||
<span>Maillard</span><output data-out="maillard">—</output>
|
||
</div>
|
||
<span class="info-note"
|
||
>informational — not summed into development</span
|
||
>
|
||
</div>
|
||
|
||
<div class="ledger-group">
|
||
<div class="ledger-row">
|
||
<span class="l-fid">4.3</span
|
||
><span class="l-label">Development base</span
|
||
><output class="l-val" data-out="l7">—</output>
|
||
</div>
|
||
<div class="ledger-row">
|
||
<span class="l-fid">3.2</span
|
||
><span class="l-label">± Processing modifier</span
|
||
><output class="l-val" data-out="l8">—</output>
|
||
</div>
|
||
<div class="ledger-row">
|
||
<span class="l-fid">1.7</span
|
||
><span class="l-label">± Cultivar dev modifier</span
|
||
><output class="l-val" data-out="l9">—</output>
|
||
</div>
|
||
</div>
|
||
<div class="ledger-total hero">
|
||
<span>Development</span><output data-out="C">—</output>
|
||
</div>
|
||
<div class="ledger-total hero drop">
|
||
<span>Drop (end of roast)</span><output data-out="D">—</output>
|
||
</div>
|
||
</div>
|
||
|
||
<div
|
||
class="ledger-warnings hidden"
|
||
id="ledger-warnings"
|
||
role="status"
|
||
aria-live="polite"
|
||
></div>
|
||
|
||
<div class="dock-card checks-card">
|
||
<div class="dock-card-head"><h2>Sanity Checks</h2></div>
|
||
<div class="checks-grid">
|
||
<div class="check-tile unknown" data-pass="drying">
|
||
<span class="check-name">Drying share</span>
|
||
<output class="check-value" data-out="check-drying">—</output>
|
||
<span class="check-band">informational, target 43–51%</span>
|
||
<span class="check-why"
|
||
>Derived from the development ratio (yellow is 0.56 ×
|
||
first crack, not entered independently) — this can't fail
|
||
on its own. 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">informational, target 33–39%</span>
|
||
<span class="check-why"
|
||
>Same derivation as drying share — informational, not an
|
||
independent check. 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>
|
||
|
||
<div class="dock-card curve-card">
|
||
<div class="dock-card-head">
|
||
<h2>The Curve</h2>
|
||
<span class="curve-legend"
|
||
><i class="sw sw-plan"></i>plan
|
||
<i class="sw sw-ref"></i>reference</span
|
||
>
|
||
</div>
|
||
<div class="curve-wrap">
|
||
<svg
|
||
id="curve-svg-live"
|
||
viewBox="0 0 714 334"
|
||
width="100%"
|
||
role="img"
|
||
aria-label="Roast curve, live plan"
|
||
>
|
||
<defs>
|
||
<style>
|
||
#curve-svg-live .maj {
|
||
stroke: var(--line);
|
||
stroke-width: 0.7;
|
||
}
|
||
#curve-svg-live .min {
|
||
stroke: var(--surface-sunken);
|
||
stroke-width: 0.6;
|
||
}
|
||
#curve-svg-live .ax {
|
||
stroke: var(--ink-3);
|
||
stroke-width: 1.1;
|
||
}
|
||
#curve-svg-live .al {
|
||
font-size: 8px;
|
||
fill: var(--ink-3);
|
||
}
|
||
#curve-svg-live .band {
|
||
fill: var(--ember);
|
||
opacity: 0.08;
|
||
}
|
||
#curve-svg-live .bandline {
|
||
font-size: 7.5px;
|
||
fill: var(--ember);
|
||
}
|
||
</style>
|
||
</defs>
|
||
<rect
|
||
x="52"
|
||
y="24"
|
||
width="600"
|
||
height="270"
|
||
fill="var(--surface-raised)"
|
||
/>
|
||
<rect class="band" x="52" y="65.4" width="600" height="19.8" />
|
||
<text
|
||
x="58"
|
||
y="62.4"
|
||
class="bandline"
|
||
data-band-lo="176"
|
||
data-band-hi="187"
|
||
data-band-prefix="first-crack band, "
|
||
>
|
||
first-crack band, 176–187 °C
|
||
</text>
|
||
<g class="min">
|
||
<line x1="72" y1="24" x2="72" y2="294" />
|
||
<line x1="112" y1="24" x2="112" y2="294" />
|
||
<line x1="152" y1="24" x2="152" y2="294" />
|
||
<line x1="192" y1="24" x2="192" y2="294" />
|
||
<line x1="232" y1="24" x2="232" y2="294" />
|
||
<line x1="272" y1="24" x2="272" y2="294" />
|
||
<line x1="312" y1="24" x2="312" y2="294" />
|
||
<line x1="352" y1="24" x2="352" y2="294" />
|
||
<line x1="392" y1="24" x2="392" y2="294" />
|
||
<line x1="432" y1="24" x2="432" y2="294" />
|
||
<line x1="472" y1="24" x2="472" y2="294" />
|
||
<line x1="512" y1="24" x2="512" y2="294" />
|
||
<line x1="552" y1="24" x2="552" y2="294" />
|
||
<line x1="592" y1="24" x2="592" y2="294" />
|
||
<line x1="632" y1="24" x2="632" y2="294" />
|
||
</g>
|
||
<g class="maj">
|
||
<line x1="92" y1="24" x2="92" y2="294" />
|
||
<line x1="132" y1="24" x2="132" y2="294" />
|
||
<line x1="172" y1="24" x2="172" y2="294" />
|
||
<line x1="212" y1="24" x2="212" y2="294" />
|
||
<line x1="252" y1="24" x2="252" y2="294" />
|
||
<line x1="292" y1="24" x2="292" y2="294" />
|
||
<line x1="332" y1="24" x2="332" y2="294" />
|
||
<line x1="372" y1="24" x2="372" y2="294" />
|
||
<line x1="412" y1="24" x2="412" y2="294" />
|
||
<line x1="452" y1="24" x2="452" y2="294" />
|
||
<line x1="492" y1="24" x2="492" y2="294" />
|
||
<line x1="532" y1="24" x2="532" y2="294" />
|
||
<line x1="572" y1="24" x2="572" y2="294" />
|
||
<line x1="612" y1="24" x2="612" y2="294" />
|
||
</g>
|
||
<g class="min">
|
||
<line x1="52" y1="276" x2="652" y2="276" />
|
||
<line x1="52" y1="240" x2="652" y2="240" />
|
||
<line x1="52" y1="204" x2="652" y2="204" />
|
||
<line x1="52" y1="168" x2="652" y2="168" />
|
||
<line x1="52" y1="132" x2="652" y2="132" />
|
||
<line x1="52" y1="96" x2="652" y2="96" />
|
||
<line x1="52" y1="60" x2="652" y2="60" />
|
||
</g>
|
||
<g class="maj">
|
||
<line x1="52" y1="258" x2="652" y2="258" />
|
||
<line x1="52" y1="222" x2="652" y2="222" />
|
||
<line x1="52" y1="186" x2="652" y2="186" />
|
||
<line x1="52" y1="150" x2="652" y2="150" />
|
||
<line x1="52" y1="114" x2="652" y2="114" />
|
||
<line x1="52" y1="78" x2="652" y2="78" />
|
||
<line x1="52" y1="42" x2="652" y2="42" />
|
||
</g>
|
||
<line class="ax" x1="52" y1="24" x2="52" y2="294" />
|
||
<line class="ax" x1="52" y1="294" x2="652" y2="294" />
|
||
<line class="ax" x1="652" y1="24" x2="652" y2="294" />
|
||
<g class="al curve-axis-temp" text-anchor="end">
|
||
<text data-tempc="60" x="47" y="296.8">60</text>
|
||
<text data-tempc="80" x="47" y="260.8">80</text>
|
||
<text data-tempc="100" x="47" y="224.8">100</text>
|
||
<text data-tempc="120" x="47" y="188.8">120</text>
|
||
<text data-tempc="140" x="47" y="152.8">140</text>
|
||
<text data-tempc="160" x="47" y="116.8">160</text>
|
||
<text data-tempc="180" x="47" y="80.8">180</text>
|
||
<text data-tempc="200" x="47" y="44.8">200</text>
|
||
</g>
|
||
<g class="al" text-anchor="middle">
|
||
<text x="52" y="304">0:00</text>
|
||
<text x="92" y="304">1:00</text>
|
||
<text x="132" y="304">2:00</text>
|
||
<text x="172" y="304">3:00</text>
|
||
<text x="212" y="304">4:00</text>
|
||
<text x="252" y="304">5:00</text>
|
||
<text x="292" y="304">6:00</text>
|
||
<text x="332" y="304">7:00</text>
|
||
<text x="372" y="304">8:00</text>
|
||
<text x="412" y="304">9:00</text>
|
||
<text x="452" y="304">10:00</text>
|
||
<text x="492" y="304">11:00</text>
|
||
<text x="532" y="304">12:00</text>
|
||
<text x="572" y="304">13:00</text>
|
||
<text x="612" y="304">14:00</text>
|
||
<text x="652" y="304">15:00</text>
|
||
</g>
|
||
<g id="plan-curve-live"></g>
|
||
<g id="ref-curve-live"></g>
|
||
</svg>
|
||
</div>
|
||
</div>
|
||
</aside>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
<!-- /app-workspace -->
|
||
</div>
|
||
<!-- /app-shell -->
|
||
|
||
<!-- 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. -->
|
||
<div id="print-sheet">
|
||
<div class="sheet-wrap">
|
||
<div class="sheet">
|
||
<!-- ══════════════════ FRONT ══════════════════ -->
|
||
<section class="first">
|
||
<div class="wsbar">
|
||
<span class="t">Manual Roast Plan — Worksheet</span>
|
||
<span class="r">Sheet 1 of 2 · Plan | Hottop KN-8828B-2K+</span>
|
||
</div>
|
||
|
||
<div class="wsbox">
|
||
<div class="hd">
|
||
<span class="id">THE COFFEE</span
|
||
><span class="ref"
|
||
>[n.n] = field ID · cited everywhere it feeds a number</span
|
||
>
|
||
</div>
|
||
<div class="bd wsline tightln">
|
||
<span class="fid">0.1</span><span class="lb">Coffee / lot</span>
|
||
<input
|
||
class="f ws-input"
|
||
style="min-width: 44mm"
|
||
name="0.1"
|
||
/><span class="pv"></span> <span class="sep">|</span
|
||
><span class="fid">0.2</span><span class="lb">Date</span>
|
||
<input class="f n ws-input" type="date" name="0.2" /><span
|
||
class="pv"
|
||
></span>
|
||
<span class="sep">|</span><span class="fid">0.3</span
|
||
><span class="lb">Batch #</span>
|
||
<input
|
||
class="f ws-input"
|
||
style="min-width: 10mm"
|
||
name="0.3"
|
||
/><span class="pv"></span> <span class="sep">|</span
|
||
><span class="fid">0.4</span><span class="lb">Green in</span>
|
||
<input class="f n ws-input" name="0.4" />g<span
|
||
class="pv"
|
||
></span
|
||
><br />
|
||
|
||
<span class="stepdot">1</span><span class="fid">1.1</span
|
||
><span class="lb">Cultivar</span>
|
||
<input
|
||
class="f ws-input"
|
||
style="min-width: 30mm"
|
||
name="1.1"
|
||
list="cultivar-list"
|
||
/><span class="pv"></span> <span class="sep">|</span
|
||
><span class="fid">1.2</span><span class="lb">Group</span>
|
||
<span class="circ" data-radio-group="1.2">
|
||
<label class="opt"
|
||
><input type="radio" name="1.2" value="typica" /><span
|
||
>Typica</span
|
||
></label
|
||
>
|
||
<label class="opt"
|
||
><input type="radio" name="1.2" value="bourbon" /><span
|
||
>Bourbon</span
|
||
></label
|
||
>
|
||
<label class="opt"
|
||
><input type="radio" name="1.2" value="ethiopian" /><span
|
||
>Ethiopian</span
|
||
></label
|
||
>
|
||
<label class="opt"
|
||
><input type="radio" name="1.2" value="hybrid" /><span
|
||
>Hybrid</span
|
||
></label
|
||
>
|
||
</span>
|
||
<span class="sep">|</span><span class="fid">1.3</span
|
||
><span class="lb">Origin</span>
|
||
<input
|
||
class="f ws-input"
|
||
style="min-width: 20mm"
|
||
name="1.3"
|
||
/><span class="pv"></span><br />
|
||
|
||
<span class="fid" style="margin-left: 4.6mm">1.4</span
|
||
><span class="lb">FC anchor</span>
|
||
<input class="f n ws-input" name="1.4" /><span
|
||
class="pv"
|
||
></span>
|
||
<span class="sep">|</span><span class="fid">1.5</span
|
||
><span class="lb">Profile mm:ss|mm:ss|mm:ss</span>
|
||
<input
|
||
class="f ws-input"
|
||
style="min-width: 22mm"
|
||
name="1.5"
|
||
/><span class="pv"></span> <span class="sep">|</span
|
||
><small>your cultivar's row, Step 1 — not a group number</small
|
||
><br />
|
||
|
||
<span class="fid" style="margin-left: 4.6mm">1.6</span
|
||
><span class="lb">± Refine → ledger 2</span>
|
||
<input class="f n ws-input" name="1.6" /><span
|
||
class="pv"
|
||
></span>
|
||
<small>0 first cook, else Step 12</small>
|
||
<span class="sep">|</span><span class="fid">1.7</span
|
||
><span class="lb">± Dev mod → ledger 9</span>
|
||
<input class="f n ws-input" name="1.7" /><span
|
||
class="pv"
|
||
></span>
|
||
<small>Step 1's profile 3rd # −1:30</small><br />
|
||
|
||
<span class="stepdot">3</span><span class="fid">3.1</span
|
||
><span class="lb">Process</span>
|
||
<span class="circ" data-radio-group="3.1">
|
||
<label class="opt"
|
||
><input type="radio" name="3.1" value="washed" /><span
|
||
>washed</span
|
||
></label
|
||
>
|
||
<label class="opt"
|
||
><input type="radio" name="3.1" value="yellow-honey" /><span
|
||
>yellow/white honey</span
|
||
></label
|
||
>
|
||
<label class="opt"
|
||
><input
|
||
type="radio"
|
||
name="3.1"
|
||
value="red-black-honey"
|
||
/><span>red/black honey</span></label
|
||
>
|
||
<label class="opt"
|
||
><input type="radio" name="3.1" value="natural" /><span
|
||
>natural</span
|
||
></label
|
||
>
|
||
<label class="opt"
|
||
><input type="radio" name="3.1" value="anaerobic" /><span
|
||
>anaerobic/CM</span
|
||
></label
|
||
>
|
||
</span>
|
||
<span class="sep">|</span><span class="fid">3.2</span
|
||
><span class="lb">Dev mod → ledger 8</span>
|
||
<input class="f n ws-input" name="3.2" /><span
|
||
class="pv"
|
||
></span>
|
||
<small>Step 3 table</small><br />
|
||
|
||
<span class="stepdot">4</span><span class="fid">4.1</span
|
||
><span class="lb">Roast level</span>
|
||
<span class="circ" data-radio-group="4.1">
|
||
<label class="opt"
|
||
><input type="radio" name="4.1" value="light" /><span
|
||
>light</span
|
||
></label
|
||
>
|
||
<label class="opt"
|
||
><input type="radio" name="4.1" value="light-medium" /><span
|
||
>light-med</span
|
||
></label
|
||
>
|
||
<label class="opt"
|
||
><input type="radio" name="4.1" value="medium" /><span
|
||
>medium</span
|
||
></label
|
||
>
|
||
<label class="opt"
|
||
><input type="radio" name="4.1" value="dark" /><span
|
||
>dark</span
|
||
></label
|
||
>
|
||
<label class="opt"
|
||
><input type="radio" name="4.1" value="very-dark" /><span
|
||
>very dark</span
|
||
></label
|
||
>
|
||
</span>
|
||
<span class="sep">|</span><span class="fid">4.2</span
|
||
><span class="lb">Target WL</span>
|
||
<input class="f n ws-input" name="4.2" />%<span
|
||
class="pv"
|
||
></span>
|
||
<span class="sep">|</span><span class="fid">4.3</span
|
||
><span class="lb">Dev base → ledger 7</span>
|
||
<input
|
||
class="f n ws-input"
|
||
style="min-width: 12mm"
|
||
name="4.3"
|
||
/><span class="pv"></span><br />
|
||
|
||
<span class="stepdot">5</span><span class="fid">5.1</span
|
||
><span class="lb">Moisture</span>
|
||
<input class="f n ws-input" name="5.1" />%<span
|
||
class="pv"
|
||
></span>
|
||
<span class="sep">|</span><span class="fid">5.2</span
|
||
><span class="lb">Density</span>
|
||
<input class="f n ws-input" name="5.2" />g/L<span
|
||
class="pv"
|
||
></span>
|
||
<span class="sep">|</span><span class="fid">5.3</span
|
||
><span class="lb">Altitude</span>
|
||
<input class="f n ws-input" name="5.3" /><span
|
||
class="pv"
|
||
></span>
|
||
<span class="sep">|</span><span class="fid">5.4</span
|
||
><span class="lb">Screen</span>
|
||
<input class="f n ws-input" name="5.4" /><span
|
||
class="pv"
|
||
></span>
|
||
<span class="sep">|</span><span class="fid">5.5</span
|
||
><span class="lb">Sweet</span>
|
||
<span class="circ" data-radio-group="5.5">
|
||
<label class="opt"
|
||
><input type="radio" name="5.5" value="narrow" /><span
|
||
>narrow</span
|
||
></label
|
||
>
|
||
<label class="opt"
|
||
><input type="radio" name="5.5" value="med" /><span
|
||
>med</span
|
||
></label
|
||
>
|
||
<label class="opt"
|
||
><input type="radio" name="5.5" value="wide" /><span
|
||
>wide</span
|
||
></label
|
||
> </span
|
||
><br />
|
||
<span class="fid" style="margin-left: 4.6mm">5.6</span
|
||
><span class="lb">± Net correction → ledger 3</span>
|
||
<input class="f n ws-input" name="5.6" /><span
|
||
class="pv"
|
||
></span>
|
||
<small>judgment, rarely >±0:15</small>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="wsbox">
|
||
<div class="hd">
|
||
<span class="n">2 · BLEND</span
|
||
><span class="ref">skip if single-origin → Step 2</span>
|
||
</div>
|
||
<div class="bd">
|
||
<div class="wsline tightln">
|
||
<span class="fid">2.1</span>This lot is:
|
||
<span class="circ" data-radio-group="2.1">
|
||
<label class="opt"
|
||
><input
|
||
type="radio"
|
||
name="2.1"
|
||
value="single"
|
||
checked
|
||
/><span>single-origin — skip this box</span></label
|
||
>
|
||
<label class="opt"
|
||
><input type="radio" name="2.1" value="blend" /><span
|
||
>blend or community lot</span
|
||
></label
|
||
>
|
||
</span>
|
||
</div>
|
||
<table class="tight">
|
||
<thead>
|
||
<tr>
|
||
<th><span class="fid">2.2</span>Component</th>
|
||
<th style="width: 22mm">Group</th>
|
||
<th style="width: 24mm">Process</th>
|
||
<th class="num" style="width: 14mm">Share %</th>
|
||
<th class="num" style="width: 20mm">FC anchor</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="blend-rows"></tbody>
|
||
</table>
|
||
<div class="wsline tightln">
|
||
<span class="fid">2.3</span>Method:
|
||
<span class="circ" data-radio-group="2.3">
|
||
<label class="opt"
|
||
><input type="radio" name="2.3" value="dominant" /><span
|
||
>dominant (≥60%)</span
|
||
></label
|
||
>
|
||
<label class="opt"
|
||
><input type="radio" name="2.3" value="weighted" /><span
|
||
>weighted average</span
|
||
></label
|
||
>
|
||
<label class="opt"
|
||
><input type="radio" name="2.3" value="split" /><span
|
||
>split-roast, blend after</span
|
||
></label
|
||
>
|
||
</span>
|
||
</div>
|
||
<div class="wsline tightln">
|
||
<span class="fid">2.4</span
|
||
><span class="lb">Resulting FC anchor → ledger line 1</span>
|
||
<input class="f n ws-input" name="2.4" /><span
|
||
class="pv"
|
||
></span>
|
||
<span class="sep">|</span><span class="fid">2.5</span
|
||
><span class="lb"
|
||
>Resulting processing modifier → ledger line 8</span
|
||
>
|
||
<input class="f n ws-input" name="2.5" /><span
|
||
class="pv"
|
||
></span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="wsbox">
|
||
<div class="hd">
|
||
<span class="n">6 · TIME LEDGER</span
|
||
><span class="ref">method & why → Step 6</span>
|
||
</div>
|
||
<div class="bd">
|
||
<table class="ledger">
|
||
<tr class="row">
|
||
<td class="ln">1</td>
|
||
<td class="desc">
|
||
Cultivar/blend anchor
|
||
<span class="sub">from 1.4, or 2.4 if blend</span>
|
||
</td>
|
||
<td class="val"><output data-out="l1">—</output></td>
|
||
</tr>
|
||
<tr class="row">
|
||
<td class="ln">2</td>
|
||
<td class="desc">
|
||
<span class="op">±</span> Refinement — from 1.6
|
||
<span class="sub">0 first cook, else Step 12</span>
|
||
</td>
|
||
<td class="val"><output data-out="l2">—</output></td>
|
||
</tr>
|
||
<tr class="row">
|
||
<td class="ln">3</td>
|
||
<td class="desc">
|
||
<span class="op">±</span> Bean condition — from 5.6
|
||
</td>
|
||
<td class="val"><output data-out="l3">—</output></td>
|
||
</tr>
|
||
<tr class="row">
|
||
<td class="ln">4</td>
|
||
<td class="desc">
|
||
<span class="op">±</span> Batch size vs. Step 9 reference
|
||
weights
|
||
<span class="sub"
|
||
>225 g ≈ 1:30–2:00 later than 200 g</span
|
||
>
|
||
</td>
|
||
<td class="val">
|
||
<input
|
||
class="f n ws-input"
|
||
name="6.4"
|
||
placeholder="0"
|
||
/><span class="pv"></span>
|
||
</td>
|
||
</tr>
|
||
<tr class="row">
|
||
<td class="ln"></td>
|
||
<td class="desc">Subtotal (before pace)</td>
|
||
<td class="val"><output data-out="subtotalA">—</output></td>
|
||
</tr>
|
||
<tr class="row">
|
||
<td class="ln"></td>
|
||
<td class="desc">
|
||
<span class="op">×</span> Machine pace
|
||
<span class="sub"
|
||
>learned from your own roasts, else ×1.00
|
||
(reference)</span
|
||
>
|
||
</td>
|
||
<td class="val"><output data-out="pace-factor">×1.00</output></td>
|
||
</tr>
|
||
<tr class="total">
|
||
<td class="ln">A</td>
|
||
<td class="desc"><strong>FIRST CRACK</strong></td>
|
||
<td class="val"><output data-out="A">—</output></td>
|
||
</tr>
|
||
<tr class="grp">
|
||
<td class="ln"></td>
|
||
<td class="desc" colspan="2">
|
||
B · YELLOW / MAILLARD — informational only. NOT summed
|
||
into C.
|
||
</td>
|
||
</tr>
|
||
<tr class="row">
|
||
<td class="ln">5</td>
|
||
<td class="desc"><strong>YELLOW</strong> = A × 0.56</td>
|
||
<td class="val"><output data-out="yellow">—</output></td>
|
||
</tr>
|
||
<tr class="row">
|
||
<td class="ln">6</td>
|
||
<td class="desc"><strong>MAILLARD</strong> = A − line 5</td>
|
||
<td class="val"><output data-out="maillard">—</output></td>
|
||
</tr>
|
||
<tr class="grp">
|
||
<td class="ln"></td>
|
||
<td class="desc" colspan="2">
|
||
C · DEVELOPMENT — sum ONLY rows 7 + 8 + 9
|
||
</td>
|
||
</tr>
|
||
<tr class="row">
|
||
<td class="ln">7</td>
|
||
<td class="desc">Development base — from 4.3</td>
|
||
<td class="val"><output data-out="l7">—</output></td>
|
||
</tr>
|
||
<tr class="row">
|
||
<td class="ln">8</td>
|
||
<td class="desc">
|
||
<span class="op">±</span> Processing modifier — from 3.2,
|
||
or 2.5 if blend
|
||
</td>
|
||
<td class="val"><output data-out="l8">—</output></td>
|
||
</tr>
|
||
<tr class="row">
|
||
<td class="ln">9</td>
|
||
<td class="desc">
|
||
<span class="op">±</span> Cultivar dev modifier — from 1.7
|
||
</td>
|
||
<td class="val"><output data-out="l9">—</output></td>
|
||
</tr>
|
||
<tr class="total">
|
||
<td class="ln">C</td>
|
||
<td class="desc">
|
||
<strong>DEVELOPMENT</strong> = 7 + 8 + 9 only
|
||
</td>
|
||
<td class="val"><output data-out="C">—</output></td>
|
||
</tr>
|
||
<tr class="total">
|
||
<td class="ln">D</td>
|
||
<td class="desc">
|
||
<strong>DROP (end of roast)</strong> = A + C
|
||
</td>
|
||
<td class="val"><output data-out="D">—</output></td>
|
||
</tr>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="wsbox">
|
||
<div class="hd">
|
||
<span class="n">7 · SANITY CHECKS</span
|
||
><span class="ref"
|
||
>a fail sends you back to box 6 → Step 7</span
|
||
>
|
||
</div>
|
||
<div class="bd">
|
||
<table class="tight">
|
||
<thead>
|
||
<tr>
|
||
<th style="width: 44mm">Check</th>
|
||
<th class="num" style="width: 24mm">Should be</th>
|
||
<th class="num" style="width: 24mm">Yours</th>
|
||
<th class="num" style="width: 16mm">Pass?</th>
|
||
<th>Failure means</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr>
|
||
<td>Drying share (informational)</td>
|
||
<td class="num">43–51 %</td>
|
||
<td class="num">
|
||
<output data-out="check-drying">—</output>
|
||
</td>
|
||
<td class="num passcell unknown" data-pass="drying"></td>
|
||
<td>can't fail on its own — see * below</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Maillard share (informational)</td>
|
||
<td class="num">33–39 %</td>
|
||
<td class="num">
|
||
<output data-out="check-maillard">—</output>
|
||
</td>
|
||
<td
|
||
class="num passcell unknown"
|
||
data-pass="maillard"
|
||
></td>
|
||
<td>can't fail on its own — see * below</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Development ratio</td>
|
||
<td class="num">12–20 %</td>
|
||
<td class="num">
|
||
<output data-out="check-dtr">—</output>
|
||
</td>
|
||
<td class="num passcell unknown" data-pass="dtr"></td>
|
||
<td>under 12 % = under-developed</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Development ceiling</td>
|
||
<td class="num">< 4:30</td>
|
||
<td class="num">
|
||
<output data-out="check-ceiling">—</output>
|
||
</td>
|
||
<td class="num passcell unknown" data-pass="ceiling"></td>
|
||
<td>over 4:30 mutes any roast</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
<p class="wsfoot">
|
||
* Drying share and Maillard share are always in-band
|
||
whenever Development ratio passes — yellow is calculated as
|
||
a fixed 56% of first crack, not measured independently, so
|
||
these two rows can't fail on their own. Only Development
|
||
ratio and Development ceiling are real pass/fail checks.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ══════════════════ BACK ══════════════════ -->
|
||
<section>
|
||
<div class="wsbar">
|
||
<span class="t">Roast & Record</span>
|
||
<span class="r"
|
||
>Sheet 2 of 2 | coffee: <span id="back-coffee-name"></span
|
||
></span>
|
||
</div>
|
||
|
||
<div class="wsbox">
|
||
<div class="hd">
|
||
<span class="n">8 · TEMPERATURES & RATE-OF-RISE</span
|
||
><span class="ref">→ Step 8</span>
|
||
</div>
|
||
<div class="bd">
|
||
<table class="tight">
|
||
<thead>
|
||
<tr>
|
||
<th style="width: 28mm">Milestone</th>
|
||
<th class="num" style="width: 20mm">Time</th>
|
||
<th class="num" style="width: 18mm" id="print-machine-temp-th">°C</th>
|
||
<th class="num" style="width: 36mm">Your logged band</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr>
|
||
<td>Charge</td>
|
||
<td class="num">
|
||
<output data-out="t-charge">0:00</output>
|
||
</td>
|
||
<td class="num">
|
||
<input
|
||
class="f n ws-input"
|
||
name="temps.charge.tempC"
|
||
/><span class="pv"></span>
|
||
</td>
|
||
<td class="num" data-band-lo="133" data-band-hi="195">133–195 °C</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Turning point</td>
|
||
<td class="num">
|
||
<input
|
||
class="f n ws-input"
|
||
name="temps.tp.time"
|
||
placeholder="m:ss"
|
||
/><span class="pv"></span>
|
||
</td>
|
||
<td class="num">
|
||
<input
|
||
class="f n ws-input"
|
||
name="temps.tp.tempC"
|
||
/><span class="pv"></span>
|
||
</td>
|
||
<td class="num" data-band-lo="77" data-band-hi="100" data-band-suffix=" @ 0:47–1:15">77–100 °C @ 0:47–1:15</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Yellow</td>
|
||
<td class="num">
|
||
<output data-out="t-yellow">—</output>
|
||
</td>
|
||
<td class="num">
|
||
<input
|
||
class="f n ws-input"
|
||
name="temps.yellow.tempC"
|
||
/><span class="pv"></span>
|
||
</td>
|
||
<td class="num" data-band-lo="148" data-band-hi="166">148–166 °C</td>
|
||
</tr>
|
||
<tr>
|
||
<td>First crack</td>
|
||
<td class="num"><output data-out="t-fc">—</output></td>
|
||
<td class="num">
|
||
<input
|
||
class="f n ws-input"
|
||
name="temps.fc.tempC"
|
||
/><span class="pv"></span>
|
||
</td>
|
||
<td class="num" data-band-lo="176" data-band-hi="187">176–187 °C</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Drop</td>
|
||
<td class="num"><output data-out="t-drop">—</output></td>
|
||
<td class="num">
|
||
<input
|
||
class="f n ws-input"
|
||
name="temps.drop.tempC"
|
||
/><span class="pv"></span>
|
||
</td>
|
||
<td class="num" data-band-lo="183" data-band-hi="197" data-band-suffix=" light">183–197 °C light</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="wsbox">
|
||
<div class="hd">
|
||
<span class="n">9 · ACTUATOR SCHEDULE (planned)</span
|
||
><span class="ref">→ Step 9</span>
|
||
</div>
|
||
<div class="bd">
|
||
<table class="tight">
|
||
<thead>
|
||
<tr>
|
||
<th class="num" style="width: 20mm">Time</th>
|
||
<th class="num" style="width: 18mm">Heat %</th>
|
||
<th class="num" style="width: 18mm">Fan %</th>
|
||
<th class="num" style="width: 26mm">Expected BT</th>
|
||
<th>Why this change</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="actuator-rows"></tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="wsbox wsgrid">
|
||
<div class="hd">
|
||
<span class="n">10 · THE CURVE</span
|
||
><span class="ref"
|
||
>plan drawn live · reference from an .alog if attached → Step
|
||
10</span
|
||
>
|
||
</div>
|
||
<div class="bd">
|
||
<div class="grid-wrap">
|
||
<svg
|
||
id="curve-svg"
|
||
viewBox="0 0 714 334"
|
||
width="100%"
|
||
role="img"
|
||
aria-label="Roast curve plotting grid"
|
||
>
|
||
<defs>
|
||
<style>
|
||
.maj {
|
||
stroke: #8b8078;
|
||
stroke-width: 0.7;
|
||
}
|
||
.min {
|
||
stroke: #e2dad2;
|
||
stroke-width: 0.45;
|
||
}
|
||
.ax {
|
||
stroke: #1a1512;
|
||
stroke-width: 1.3;
|
||
}
|
||
.al {
|
||
font-size: 8px;
|
||
fill: #5a5048;
|
||
}
|
||
.alr {
|
||
font-size: 8px;
|
||
fill: #7a3b12;
|
||
}
|
||
.band {
|
||
fill: #7a3b12;
|
||
opacity: 0.05;
|
||
}
|
||
.ttl {
|
||
font-size: 8px;
|
||
font-weight: 700;
|
||
fill: #1a1512;
|
||
letter-spacing: 0.5px;
|
||
}
|
||
</style>
|
||
</defs>
|
||
<rect x="52" y="24" width="600" height="270" fill="#fff" />
|
||
<rect
|
||
class="band"
|
||
x="52"
|
||
y="65.4"
|
||
width="600"
|
||
height="19.8"
|
||
/>
|
||
<text
|
||
x="58"
|
||
y="62.4"
|
||
class="alr"
|
||
style="font-size: 7px"
|
||
data-band-lo="176"
|
||
data-band-hi="187"
|
||
data-band-prefix="your first-crack band, "
|
||
>
|
||
your first-crack band, 176–187 °C
|
||
</text>
|
||
<g class="min">
|
||
<line x1="72" y1="24" x2="72" y2="294" />
|
||
<line x1="112" y1="24" x2="112" y2="294" />
|
||
<line x1="152" y1="24" x2="152" y2="294" />
|
||
<line x1="192" y1="24" x2="192" y2="294" />
|
||
<line x1="232" y1="24" x2="232" y2="294" />
|
||
<line x1="272" y1="24" x2="272" y2="294" />
|
||
<line x1="312" y1="24" x2="312" y2="294" />
|
||
<line x1="352" y1="24" x2="352" y2="294" />
|
||
<line x1="392" y1="24" x2="392" y2="294" />
|
||
<line x1="432" y1="24" x2="432" y2="294" />
|
||
<line x1="472" y1="24" x2="472" y2="294" />
|
||
<line x1="512" y1="24" x2="512" y2="294" />
|
||
<line x1="552" y1="24" x2="552" y2="294" />
|
||
<line x1="592" y1="24" x2="592" y2="294" />
|
||
<line x1="632" y1="24" x2="632" y2="294" />
|
||
</g>
|
||
<g class="maj">
|
||
<line x1="92" y1="24" x2="92" y2="294" />
|
||
<line x1="132" y1="24" x2="132" y2="294" />
|
||
<line x1="172" y1="24" x2="172" y2="294" />
|
||
<line x1="212" y1="24" x2="212" y2="294" />
|
||
<line x1="252" y1="24" x2="252" y2="294" />
|
||
<line x1="292" y1="24" x2="292" y2="294" />
|
||
<line x1="332" y1="24" x2="332" y2="294" />
|
||
<line x1="372" y1="24" x2="372" y2="294" />
|
||
<line x1="412" y1="24" x2="412" y2="294" />
|
||
<line x1="452" y1="24" x2="452" y2="294" />
|
||
<line x1="492" y1="24" x2="492" y2="294" />
|
||
<line x1="532" y1="24" x2="532" y2="294" />
|
||
<line x1="572" y1="24" x2="572" y2="294" />
|
||
<line x1="612" y1="24" x2="612" y2="294" />
|
||
</g>
|
||
<g class="min">
|
||
<line x1="52" y1="276" x2="652" y2="276" />
|
||
<line x1="52" y1="240" x2="652" y2="240" />
|
||
<line x1="52" y1="204" x2="652" y2="204" />
|
||
<line x1="52" y1="168" x2="652" y2="168" />
|
||
<line x1="52" y1="132" x2="652" y2="132" />
|
||
<line x1="52" y1="96" x2="652" y2="96" />
|
||
<line x1="52" y1="60" x2="652" y2="60" />
|
||
</g>
|
||
<g class="maj">
|
||
<line x1="52" y1="258" x2="652" y2="258" />
|
||
<line x1="52" y1="222" x2="652" y2="222" />
|
||
<line x1="52" y1="186" x2="652" y2="186" />
|
||
<line x1="52" y1="150" x2="652" y2="150" />
|
||
<line x1="52" y1="114" x2="652" y2="114" />
|
||
<line x1="52" y1="78" x2="652" y2="78" />
|
||
<line x1="52" y1="42" x2="652" y2="42" />
|
||
</g>
|
||
<line class="ax" x1="52" y1="24" x2="52" y2="294" />
|
||
<line class="ax" x1="52" y1="294" x2="652" y2="294" />
|
||
<line class="ax" x1="652" y1="24" x2="652" y2="294" />
|
||
<g class="al curve-axis-temp" text-anchor="end">
|
||
<text data-tempc="60" x="47" y="296.8">60</text>
|
||
<text data-tempc="80" x="47" y="260.8">80</text>
|
||
<text data-tempc="100" x="47" y="224.8">100</text>
|
||
<text data-tempc="120" x="47" y="188.8">120</text>
|
||
<text data-tempc="140" x="47" y="152.8">140</text>
|
||
<text data-tempc="160" x="47" y="116.8">160</text>
|
||
<text data-tempc="180" x="47" y="80.8">180</text>
|
||
<text data-tempc="200" x="47" y="44.8">200</text>
|
||
</g>
|
||
<text
|
||
x="14"
|
||
y="159"
|
||
class="ttl"
|
||
transform="rotate(-90 14 159)"
|
||
text-anchor="middle"
|
||
id="print-curve-axis-title"
|
||
>
|
||
BEAN TEMP °C
|
||
</text>
|
||
<g class="al" text-anchor="middle">
|
||
<text x="52" y="304">0:00</text>
|
||
<text x="92" y="304">1:00</text>
|
||
<text x="132" y="304">2:00</text>
|
||
<text x="172" y="304">3:00</text>
|
||
<text x="212" y="304">4:00</text>
|
||
<text x="252" y="304">5:00</text>
|
||
<text x="292" y="304">6:00</text>
|
||
<text x="332" y="304">7:00</text>
|
||
<text x="372" y="304">8:00</text>
|
||
<text x="412" y="304">9:00</text>
|
||
<text x="452" y="304">10:00</text>
|
||
<text x="492" y="304">11:00</text>
|
||
<text x="532" y="304">12:00</text>
|
||
<text x="572" y="304">13:00</text>
|
||
<text x="612" y="304">14:00</text>
|
||
<text x="652" y="304">15:00</text>
|
||
</g>
|
||
<text x="368" y="320" style="font-size: 8px; fill: #5a5048">
|
||
TIME FROM CHARGE · solid = plan · dashed = reference
|
||
(.alog)
|
||
</text>
|
||
<g id="plan-curve"></g>
|
||
<g id="ref-curve"></g>
|
||
</svg>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="wsbox">
|
||
<div class="hd">
|
||
<span class="n">11 · AT THE MACHINE — PLAN vs ACTUAL</span
|
||
><span class="ref">→ Step 11</span>
|
||
</div>
|
||
<div class="bd">
|
||
<table class="tight">
|
||
<thead>
|
||
<tr>
|
||
<th style="width: 28mm">Milestone</th>
|
||
<th class="num" style="width: 20mm">Plan time</th>
|
||
<th class="num" style="width: 22mm">Actual time</th>
|
||
<th class="num" style="width: 18mm">Actual BT</th>
|
||
<th>Note</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr>
|
||
<td>Charge</td>
|
||
<td class="num">
|
||
<output data-out="pa-charge">0:00</output>
|
||
</td>
|
||
<td class="num">
|
||
<input
|
||
class="f n ws-input"
|
||
name="planActual.charge.actualTime"
|
||
placeholder="0:00"
|
||
/><span class="pv"></span>
|
||
</td>
|
||
<td class="num">
|
||
<input
|
||
class="f n ws-input"
|
||
name="planActual.charge.actualBt"
|
||
/><span class="pv"></span>
|
||
</td>
|
||
<td>
|
||
<input
|
||
class="f ws-input"
|
||
style="min-width: 52mm"
|
||
name="planActual.charge.note"
|
||
/><span class="pv"></span>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Turning point</td>
|
||
<td class="num"><output data-out="pa-tp">—</output></td>
|
||
<td class="num">
|
||
<input
|
||
class="f n ws-input"
|
||
name="planActual.tp.actualTime"
|
||
/><span class="pv"></span>
|
||
</td>
|
||
<td class="num">
|
||
<input
|
||
class="f n ws-input"
|
||
name="planActual.tp.actualBt"
|
||
/><span class="pv"></span>
|
||
</td>
|
||
<td>
|
||
<input
|
||
class="f ws-input"
|
||
style="min-width: 52mm"
|
||
name="planActual.tp.note"
|
||
/><span class="pv"></span>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Yellow</td>
|
||
<td class="num">
|
||
<output data-out="pa-yellow">—</output>
|
||
</td>
|
||
<td class="num">
|
||
<input
|
||
class="f n ws-input"
|
||
name="planActual.yellow.actualTime"
|
||
/><span class="pv"></span>
|
||
</td>
|
||
<td class="num">
|
||
<input
|
||
class="f n ws-input"
|
||
name="planActual.yellow.actualBt"
|
||
/><span class="pv"></span>
|
||
</td>
|
||
<td>
|
||
<input
|
||
class="f ws-input"
|
||
style="min-width: 52mm"
|
||
name="planActual.yellow.note"
|
||
/><span class="pv"></span>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td>First crack</td>
|
||
<td class="num"><output data-out="pa-fc">—</output></td>
|
||
<td class="num">
|
||
<input
|
||
class="f n ws-input"
|
||
name="planActual.fc.actualTime"
|
||
/><span class="pv"></span>
|
||
</td>
|
||
<td class="num">
|
||
<input
|
||
class="f n ws-input"
|
||
name="planActual.fc.actualBt"
|
||
/><span class="pv"></span>
|
||
</td>
|
||
<td>
|
||
<input
|
||
class="f ws-input"
|
||
style="min-width: 52mm"
|
||
name="planActual.fc.note"
|
||
/><span class="pv"></span>
|
||
</td>
|
||
</tr>
|
||
<tr>
|
||
<td>Drop</td>
|
||
<td class="num"><output data-out="pa-drop">—</output></td>
|
||
<td class="num">
|
||
<input
|
||
class="f n ws-input"
|
||
name="planActual.drop.actualTime"
|
||
/><span class="pv"></span>
|
||
</td>
|
||
<td class="num">
|
||
<input
|
||
class="f n ws-input"
|
||
name="planActual.drop.actualBt"
|
||
/><span class="pv"></span>
|
||
</td>
|
||
<td>
|
||
<input
|
||
class="f ws-input"
|
||
style="min-width: 52mm"
|
||
name="planActual.drop.note"
|
||
/><span class="pv"></span>
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="wsbox">
|
||
<div class="hd">
|
||
<span class="n">12 · AFTER THE ROAST</span
|
||
><span class="ref">→ Step 12</span>
|
||
</div>
|
||
<div class="bd wsline tightln">
|
||
<span class="lb">Green in</span>
|
||
<input class="f n ws-input" name="afterRoast.greenIn" /><span
|
||
class="pv"
|
||
></span
|
||
>g <span class="sep">|</span><span class="lb">Out</span>
|
||
<input class="f n ws-input" name="afterRoast.out" /><span
|
||
class="pv"
|
||
></span
|
||
>g <span class="sep">|</span><span class="lb">Weight loss</span>
|
||
<input
|
||
class="f n ws-input"
|
||
name="afterRoast.weightLossPct"
|
||
/><span class="pv"></span>% <span class="sep">|</span
|
||
><span class="lb">Actual DTR</span>
|
||
<input
|
||
class="f n ws-input"
|
||
name="afterRoast.actualDtrPct"
|
||
/><span class="pv"></span>%<br />
|
||
<span class="lb">Colour WB/GR</span>
|
||
<input class="f ws-input" name="afterRoast.colour" /><span
|
||
class="pv"
|
||
></span>
|
||
<span class="sep">|</span><span class="lb">Rested</span>
|
||
<input class="f n ws-input" name="afterRoast.restedDays" /><span
|
||
class="pv"
|
||
></span
|
||
>days <span class="sep">|</span
|
||
><span class="lb">Brew ratio</span>
|
||
<input class="f ws-input" name="afterRoast.brewRatio" /><span
|
||
class="pv"
|
||
></span>
|
||
<span class="sep">|</span><span class="lb">Method</span>
|
||
<input class="f w ws-input" name="afterRoast.method" /><span
|
||
class="pv"
|
||
></span
|
||
><br />
|
||
<span class="lb">Cup notes</span>
|
||
<input
|
||
class="f xw ws-input"
|
||
style="min-width: 152mm"
|
||
name="afterRoast.cupNotes"
|
||
/><span class="pv"></span><br />
|
||
<span class="lb" style="color: var(--accent)"
|
||
>One change next batch</span
|
||
>
|
||
<input
|
||
class="f ws-input"
|
||
style="min-width: 58mm"
|
||
name="afterRoast.oneChange"
|
||
/><span class="pv"></span> <span class="sep">|</span
|
||
><span class="lb">What would prove it wrong</span>
|
||
<input
|
||
class="f ws-input"
|
||
style="min-width: 34mm"
|
||
name="afterRoast.disproof"
|
||
/><span class="pv"></span>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<datalist id="cultivar-list"></datalist>
|
||
|
||
<script type="module" src="/js/main.js?v=__ASSET_VERSION__"></script>
|
||
</body>
|
||
</html>
|