Add brewing section, plan chat, roaster learning, API tokens, Swagger docs, and full backup
Test and deploy / test-and-deploy (push) Successful in 1m6s
Test and deploy / test-and-deploy (push) Successful in 1m6s
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]>
This commit is contained in:
co-authored by
Claude Fable 5
parent
5efaeb63c9
commit
38c7d01e03
@@ -0,0 +1,190 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>Brews — 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>Brews</h1>
|
||||
<p class="brand-sub">Log every cup — method, recipe, and how it tasted</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<div class="drawer-overlay hidden" id="drawer-overlay"></div>
|
||||
|
||||
<main class="page-content">
|
||||
<section class="panel-card" id="brew-form-card">
|
||||
<div class="panel-head">
|
||||
<h2 id="brew-form-title">Log a brew</h2>
|
||||
<button class="ghost-btn small hidden" type="button" id="brew-form-cancel">
|
||||
Cancel edit
|
||||
</button>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<form id="brew-form">
|
||||
<label class="field" style="max-width:420px"
|
||||
><span class="field-label">Bean</span>
|
||||
<select class="field-input" name="beanId" id="brew-bean-select">
|
||||
<option value="">— no bean selected —</option>
|
||||
</select></label
|
||||
>
|
||||
<div id="brewer-picker" style="margin:12px 0"></div>
|
||||
<div class="field-grid">
|
||||
<label class="field"
|
||||
><span class="field-label">Dose</span>
|
||||
<div class="unit-input">
|
||||
<input class="field-input" type="number" name="doseG" inputmode="decimal" min="0" step="any" /><span class="unit">g</span>
|
||||
</div></label
|
||||
>
|
||||
<label class="field" data-espresso-hide
|
||||
><span class="field-label">Water</span>
|
||||
<div class="unit-input">
|
||||
<input class="field-input" type="number" name="waterG" inputmode="decimal" min="0" step="any" /><span class="unit">g</span>
|
||||
</div></label
|
||||
>
|
||||
<label class="field hidden" data-espresso-show
|
||||
><span class="field-label">Yield (out)</span>
|
||||
<div class="unit-input">
|
||||
<input class="field-input" type="number" name="yieldG" inputmode="decimal" min="0" step="any" /><span class="unit">g</span>
|
||||
</div></label
|
||||
>
|
||||
<label class="field"
|
||||
><span class="field-label">Ratio</span
|
||||
><input class="field-input" id="brew-ratio" readonly placeholder="—"
|
||||
/></label>
|
||||
<label class="field"
|
||||
><span class="field-label">Water temp</span>
|
||||
<div class="unit-input">
|
||||
<input class="field-input" type="number" name="waterTempC" inputmode="decimal" min="0" max="100" step="any" /><span class="unit">°C</span>
|
||||
</div></label
|
||||
>
|
||||
<label class="field"
|
||||
><span class="field-label">Grinder</span
|
||||
><input class="field-input" name="grinder" placeholder="e.g. Comandante C40"
|
||||
/></label>
|
||||
<label class="field"
|
||||
><span class="field-label">Grind setting</span
|
||||
><input class="field-input" name="grindSetting" placeholder="e.g. 22 clicks"
|
||||
/></label>
|
||||
<label class="field"
|
||||
><span class="field-label">Total time</span
|
||||
><input class="field-input" name="brewTime" placeholder="m:ss"
|
||||
/></label>
|
||||
<label class="field" data-espresso-hide
|
||||
><span class="field-label">Bloom</span
|
||||
><input class="field-input" name="bloomTime" placeholder="m:ss"
|
||||
/></label>
|
||||
</div>
|
||||
<label class="field" style="margin-top:6px"
|
||||
><span class="field-label"
|
||||
>Rating <span class="rating-pill" id="rating-value">–</span></span
|
||||
>
|
||||
<input
|
||||
type="range"
|
||||
name="rating"
|
||||
id="brew-rating"
|
||||
min="0"
|
||||
max="10"
|
||||
step="0.5"
|
||||
value="5"
|
||||
style="width:min(360px,100%)"
|
||||
/>
|
||||
</label>
|
||||
<label class="field"
|
||||
><span class="field-label">Tasting notes</span
|
||||
><textarea
|
||||
class="field-input"
|
||||
name="tastingNotes"
|
||||
rows="2"
|
||||
placeholder="What you actually tasted — sweetness, acidity, body, finish…"
|
||||
></textarea></label
|
||||
>
|
||||
<label class="field"
|
||||
><span class="field-label">Notes / next time</span
|
||||
><input class="field-input" name="notes" placeholder="e.g. grind finer next time"
|
||||
/></label>
|
||||
<button class="primary-btn" type="submit" id="brew-form-submit">
|
||||
Save brew
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel-card" id="brew-history">
|
||||
<div class="panel-head">
|
||||
<h2>Brew history</h2>
|
||||
<button class="ghost-btn small hidden" id="clear-bean-filter">
|
||||
Clear bean filter
|
||||
</button>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<div class="table-wrap">
|
||||
<table class="data-table" id="brews-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>When</th>
|
||||
<th>Method</th>
|
||||
<th>Bean</th>
|
||||
<th>Recipe</th>
|
||||
<th>Rating</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="brews-body"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="/js/brews.js?v=__ASSET_VERSION__"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user