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
@@ -151,6 +151,55 @@ function wireLlmSave() {
|
||||
});
|
||||
}
|
||||
|
||||
function wireBackupImport() {
|
||||
const input = document.getElementById("backup-import-file");
|
||||
const note = document.getElementById("backup-note");
|
||||
input.addEventListener("change", async (event) => {
|
||||
const file = event.target.files?.[0];
|
||||
event.target.value = "";
|
||||
if (!file) return;
|
||||
let backup;
|
||||
try {
|
||||
backup = JSON.parse(await file.text());
|
||||
} catch {
|
||||
note.textContent = "That file is not valid JSON.";
|
||||
return;
|
||||
}
|
||||
if (backup.format !== "roast-planner-backup") {
|
||||
note.textContent =
|
||||
"That file is not a Roast Planner backup export (expected the file downloaded by “Export full backup”).";
|
||||
return;
|
||||
}
|
||||
const userCount = backup.tables?.users?.length ?? 0;
|
||||
if (
|
||||
!confirm(
|
||||
`Replace the ENTIRE database with "${file.name}" (${userCount} user${userCount === 1 ? "" : "s"}, exported ${backup.exportedAt ?? "unknown date"})?\n\nEverything currently stored will be deleted. This cannot be undone.`,
|
||||
)
|
||||
)
|
||||
return;
|
||||
note.textContent = "Importing…";
|
||||
try {
|
||||
const result = await api("/api/admin/backup/import", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(backup),
|
||||
});
|
||||
note.textContent = `Imported: ${Object.entries(result.counts)
|
||||
.map(([table, count]) => `${table} ${count}`)
|
||||
.join(", ")}.`;
|
||||
showToast("Backup imported.");
|
||||
if (!result.sessionKept) {
|
||||
alert("The restored data does not include your current session's account — log in again with the restored credentials.");
|
||||
location.assign("/login");
|
||||
return;
|
||||
}
|
||||
await Promise.all([loadMetrics(), loadUsers(), loadPlans(), loadAudit()]);
|
||||
} catch (error) {
|
||||
note.textContent = `Import failed: ${error.message}. Nothing was changed.`;
|
||||
showToast(error.message, "fail");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function roleBadge(user) {
|
||||
return user.role === "admin"
|
||||
? `<span class="badge badge-admin">Admin</span>`
|
||||
@@ -376,6 +425,7 @@ async function init() {
|
||||
currentUserId = user.id;
|
||||
wireSignupToggle();
|
||||
wireLlmSave();
|
||||
wireBackupImport();
|
||||
await Promise.all([
|
||||
loadMetrics(),
|
||||
loadResetLinks(),
|
||||
|
||||
Reference in New Issue
Block a user