Files
roast_command_center/public/cupping.html
T
Shane MaynardandClaude Fable 5 38c7d01e03
Test and deploy / test-and-deploy (push) Successful in 1m6s
Add brewing section, plan chat, roaster learning, API tokens, Swagger docs, and full backup
Brewing:
- roasted_beans + brews tables; /beans (bean management with LLM URL
  prefill) and /brews (silhouette brewer picker across immersion/
  percolation/espresso, recipe fields, auto ratio, 0-10 rating, tasting
  notes); bean remaining weight derived from logged brew doses
- Green inventory lot form also prefills from a product URL

Navigation/UX:
- Side nav is now generated from one definition in nav.js, grouped
  Roasting / Brewing / account, consistent on every page

LLM:
- 'Ask the LLM' chat drawer on the planner (stateless /api/plan-chat)
  grounded in the plan, computed ledger, learned pace, and a new
  roaster-behavior profile aggregated from uploaded .alogs
  (/api/roaster-profile: TP lag, phase RoR, median milestone temps)
- The profile also feeds roast reviews and the planner curve's fallback
  milestone temps

API platform:
- User-generated bearer tokens (rpt_…) with account-page management;
  token requests skip CSRF; hand-authored OpenAPI 3 spec at
  /api/openapi.json rendered by self-hosted Swagger UI at /api-docs
- Full-database backup export/import (admin) + per-user data export

Co-Authored-By: Claude Fable 5 <[email protected]>
2026-08-08 22:37:42 -04:00

244 lines
9.7 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Cupping — Roast Planner</title>
<link rel="stylesheet" href="/app.css?v=__ASSET_VERSION__" />
<link rel="icon" href="/icon.svg?v=__ASSET_VERSION__" type="image/svg+xml" />
<meta name="theme-color" content="#A8481A" />
</head>
<body>
<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 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 id="cupping-title">Cupping</h1>
<p class="brand-sub" id="cupping-subtitle">
Tasting and scoring sessions
</p>
</div>
</div>
</div>
<div class="header-actions hidden" id="session-autosave-wrap">
<span class="autosave-chip" id="cupping-autosave-status"
><span class="dot"></span
><span class="autosave-text">Not saved yet</span></span
>
<button id="btn-expand-why" type="button" class="ghost-btn">
Expand all "why" notes
</button>
</div>
</header>
<div class="drawer-overlay hidden" id="drawer-overlay"></div>
<main class="page-content wide" id="cupping-list-view">
<section class="panel-card" id="cupping-sessions-list">
<div class="panel-head"><h2>Cupping sessions</h2></div>
<div class="panel-body">
<div class="table-wrap">
<table class="data-table" id="sessions-table">
<thead>
<tr>
<th>Coffee</th>
<th class="num">Score</th>
<th class="num">Cups</th>
<th>Flavors</th>
<th>Updated</th>
<th></th>
</tr>
</thead>
<tbody id="sessions-body"></tbody>
</table>
</div>
</div>
</section>
<section class="panel-card" id="new-session-card">
<div class="panel-head"><h2>New session</h2></div>
<div class="panel-body">
<form id="new-session-form" class="inline-form">
<label class="field"
><span class="field-label">Roast (optional)</span>
<select class="field-input" id="new-session-plan">
<option value="">— no linked roast —</option>
</select></label
>
<label class="field"
><span class="field-label">Cups</span
><input
class="field-input"
type="number"
id="new-session-cups"
min="1"
max="12"
value="5"
/></label>
<button class="primary-btn" type="submit">
Start cupping
</button>
</form>
</div>
</section>
</main>
<main class="page-content wide hidden" id="cupping-session-view">
<div class="workspace-grid">
<div class="form-col">
<section class="panel-card" id="cup-scores">
<div class="panel-head">
<h2>Scores</h2>
<label class="field" style="margin:0">
<span class="field-label">Cups</span>
<input
class="field-input sm"
type="number"
id="cup-count"
min="1"
max="12"
/>
</label>
</div>
<div class="panel-body">
<details class="why-panel" data-why="cup-scores">
<summary>Why the scale starts at 6</summary>
<p>
The 610 range is a specialty-grading convention:
anything below specialty grade simply isn't scored, so
6 is the floor of the scale, not "zero". Score in
quarter-point steps and leave an attribute unscored
(—) rather than guessing — an unscored attribute
contributes nothing, which is honest; a guessed 7.5
pollutes every comparison you make later. The tick
rows count cups, not quality: five clean cups out of
five is full marks even if the coffee is merely good.
</p>
</details>
<div id="cup-score-rows"></div>
<div id="cup-tick-rows"></div>
<details class="cupping-defects">
<summary>Defects</summary>
<div id="cup-defect-rows"></div>
</details>
</div>
</section>
<section class="panel-card" id="cup-flavors">
<div class="panel-head"><h2>Flavors</h2></div>
<div class="panel-body">
<details class="why-panel" data-why="cup-flavors">
<summary>Tag what you can point at</summary>
<p>
A tag is only useful if you could defend it to
another taster with the cup in front of you. Two or
three confident descriptors beat a dozen hopeful ones
— the list is capped at 32, but a session that needs
10 is already suspect.
</p>
</details>
<p class="field-note hidden" id="flavor-limit-note">
32-tag limit reached.
</p>
<div id="flavor-families"></div>
</div>
</section>
<section class="panel-card" id="cup-notes">
<div class="panel-head"><h2>Notes</h2></div>
<div class="panel-body">
<details class="why-panel" data-why="cup-notes">
<summary>Notes outlive scores</summary>
<p>
Numbers compare roasts; sentences explain them. Write
what you'd want to read before roasting this coffee
again — texture, where it fell apart as it cooled,
what you'd change.
</p>
</details>
<textarea
class="field-input"
id="cup-notes-text"
maxlength="4000"
rows="5"
aria-label="Cupping notes"
></textarea>
<p class="field-note"><span id="cup-notes-count">0</span>/4000</p>
</div>
</section>
</div>
<aside class="dock">
<div class="dock-card">
<div class="dock-card-head"><h2>Score</h2></div>
<div class="stat-tile" style="box-shadow:none;border:none;padding:0">
<div class="stat-tile-label">Total score</div>
<div class="stat-tile-value" id="cup-total">0.00</div>
</div>
<p class="dock-note">Recomputed by the server on every save.</p>
<div class="chip-group" id="flavor-chips"></div>
</div>
<div class="dock-card">
<div class="dock-card-head"><h2>Radar</h2></div>
<svg
id="cup-radar"
viewBox="0 0 240 240"
role="img"
aria-label="Cupping score radar chart"
></svg>
</div>
</aside>
</div>
</main>
</div>
</div>
<script>
// Runs synchronously at parse time (before the deferred module script below) so opening
// /cupping?session=… never flashes the empty sessions list first while cupping.js's own
// async init (which awaits the auth check) is still getting underway.
if (new URLSearchParams(location.search).get("session")) {
document.getElementById("cupping-list-view").classList.add("hidden");
document.getElementById("cupping-session-view").classList.remove("hidden");
}
</script>
<script type="module" src="/js/cupping.js?v=__ASSET_VERSION__"></script>
</body>
</html>