Add brewing section, plan chat, roaster learning, API tokens, Swagger docs, and full backup
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:
Shane Maynard
2026-08-08 22:37:42 -04:00
co-authored by Claude Fable 5
parent 5efaeb63c9
commit 38c7d01e03
33 changed files with 3498 additions and 205 deletions
+32
View File
@@ -250,6 +250,38 @@ document.getElementById("lot-form").addEventListener("submit", (event) => {
});
});
// The LLM prefill endpoint returns raw `extracted` page facts — map the green-relevant ones
// into the lot form (only overwriting what the page actually stated).
document.getElementById("lot-prefill-btn").addEventListener("click", (event) =>
guarded(event.currentTarget, async () => {
const url = document.getElementById("lot-prefill-url").value.trim();
const note = document.getElementById("lot-prefill-note");
if (!url) return;
note.classList.remove("hidden");
note.textContent = "Reading the page with the LLM…";
try {
const body = await api("/api/prefill", {
method: "POST",
body: JSON.stringify({ url }),
});
const x = body.extracted ?? {};
const form = document.getElementById("lot-form");
if (x.origin) form.origin.value = x.origin;
if (x.cultivar) form.variety.value = x.cultivar;
if (x.process) form.process.value = x.process;
if (x.producer) form.producer.value = x.producer;
if (x.moisturePct) form.moisturePct.value = x.moisturePct;
const filled = ["origin", "cultivar", "process", "producer", "moisturePct"]
.filter((k) => x[k]).length;
note.textContent = filled
? `Prefilled ${filled} field${filled === 1 ? "" : "s"} — check them, then save.`
: "The page didn't state anything usable — fill the form by hand.";
} catch (error) {
note.textContent = `Prefill failed: ${error.message}`;
}
}),
);
document.getElementById("btn-logout").addEventListener("click", async () => {
await protectedFetch("/api/auth/logout", { method: "POST" });
location.assign("/");