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
+55
View File
@@ -0,0 +1,55 @@
// Chat-with-the-LLM drawer on the planner: stateless per page load, grounded server-side in
// the current plan + learned profiles. The whole visible conversation is resent each turn.
import { api } from "./api.js?v=__ASSET_VERSION__";
export function initPlanChat({ getPlan }) {
const thread = document.getElementById("chat-thread");
const form = document.getElementById("chat-form");
const input = document.getElementById("chat-input");
const send = document.getElementById("chat-send");
if (!thread || !form) return;
const messages = [];
function bubble(role, content, pending = false) {
const div = document.createElement("div");
div.className = `chat-msg ${role}${pending ? " pending" : ""}`;
div.textContent = content;
thread.append(div);
thread.scrollTop = thread.scrollHeight;
return div;
}
bubble(
"assistant",
"Ask me anything about this plan — why a number is what it is, what to change for a different cup, or how your machine's history should shape it.",
);
form.addEventListener("submit", async (event) => {
event.preventDefault();
const content = input.value.trim();
if (!content || send.disabled) return;
input.value = "";
messages.push({ role: "user", content });
bubble("user", content);
const pending = bubble("assistant", "Thinking…", true);
send.disabled = true;
try {
const body = await api("/api/plan-chat", {
method: "POST",
body: JSON.stringify({ plan: getPlan(), messages }),
});
messages.push({ role: "assistant", content: body.reply });
pending.classList.remove("pending");
pending.textContent = body.reply;
} catch (error) {
messages.pop(); // keep history consistent with what's on screen
pending.classList.remove("pending");
pending.textContent =
error.code === "no_model"
? "No LLM model is configured on the server — ask an admin to set one on the Admin page."
: `That didn't work: ${error.message}`;
} finally {
send.disabled = false;
thread.scrollTop = thread.scrollHeight;
}
});
}