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
+69
-1
@@ -331,6 +331,73 @@ function wirePwaButtons() {
|
||||
});
|
||||
}
|
||||
|
||||
async function loadTokens() {
|
||||
const body = document.getElementById("tokens-body");
|
||||
try {
|
||||
const { tokens } = await api("/api/tokens");
|
||||
if (!tokens.length) {
|
||||
body.innerHTML = `<tr><td colspan="4" class="empty-state">No API tokens yet.</td></tr>`;
|
||||
return;
|
||||
}
|
||||
body.replaceChildren(
|
||||
...tokens.map((token) => {
|
||||
const tr = document.createElement("tr");
|
||||
const name = document.createElement("td");
|
||||
name.textContent = token.name || "(unnamed)";
|
||||
const created = document.createElement("td");
|
||||
created.textContent = fmtDate(token.createdAt);
|
||||
const used = document.createElement("td");
|
||||
used.textContent = token.lastUsedAt ? fmtDate(token.lastUsedAt) : "Never";
|
||||
const actions = document.createElement("td");
|
||||
actions.className = "data-table-actions";
|
||||
const revoke = document.createElement("button");
|
||||
revoke.className = "ghost-btn small";
|
||||
revoke.type = "button";
|
||||
revoke.textContent = "Revoke";
|
||||
revoke.addEventListener("click", async () => {
|
||||
if (!confirm(`Revoke "${token.name || "this token"}"? Anything using it stops working immediately.`))
|
||||
return;
|
||||
try {
|
||||
await api(`/api/tokens/${token.id}`, { method: "DELETE" });
|
||||
showToast("Token revoked.");
|
||||
await loadTokens();
|
||||
} catch (error) {
|
||||
showToast(error.message, "fail");
|
||||
}
|
||||
});
|
||||
actions.append(revoke);
|
||||
tr.append(name, created, used, actions);
|
||||
return tr;
|
||||
}),
|
||||
);
|
||||
} catch {
|
||||
body.innerHTML = `<tr><td colspan="4" class="empty-state">Could not load tokens.</td></tr>`;
|
||||
}
|
||||
}
|
||||
|
||||
function wireTokenForm() {
|
||||
document.getElementById("token-form").addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
const button = document.getElementById("token-create");
|
||||
button.disabled = true;
|
||||
try {
|
||||
const created = await api("/api/tokens", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({ name: event.target.name.value.trim() }),
|
||||
});
|
||||
// Shown exactly once — the server only stores a hash.
|
||||
document.getElementById("token-reveal").textContent = created.token;
|
||||
document.getElementById("token-reveal-wrap").classList.remove("hidden");
|
||||
event.target.reset();
|
||||
await loadTokens();
|
||||
} catch (error) {
|
||||
showToast(error.message, "fail");
|
||||
} finally {
|
||||
button.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.getElementById("btn-logout").addEventListener("click", async () => {
|
||||
await protectedFetch("/api/auth/logout", { method: "POST" });
|
||||
location.assign("/");
|
||||
@@ -345,7 +412,8 @@ async function init() {
|
||||
await loadProfile(user);
|
||||
wireForms(user);
|
||||
wirePwaButtons();
|
||||
await Promise.all([loadSessions(), loadPlans()]);
|
||||
wireTokenForm();
|
||||
await Promise.all([loadSessions(), loadPlans(), loadTokens()]);
|
||||
}
|
||||
|
||||
init();
|
||||
|
||||
Reference in New Issue
Block a user