Academy v4: 3 research-backed lessons (30 slides + audio); new bean logo; custom plan/lot names
Test and deploy / test-and-deploy (push) Successful in 59s

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
Shane Maynard
2026-08-09 14:30:42 -04:00
co-authored by Claude Fable 5
parent ec078116b4
commit 8b80c3fa6f
65 changed files with 1315 additions and 78 deletions
+47 -6
View File
@@ -28,6 +28,9 @@ const STORAGE_PREFIX = "roastPlannerPlan.v2";
let storageKey = null;
let remotePlanId = null;
let draftSyncedAt = null;
// The custom name of the currently loaded plan (server column, not part of the plan JSONB —
// see roast_plans.name). Header display prefers this over worksheet field 0.1 when set.
let currentPlanName = "";
let plans = [];
let lastDrawerOpener = null;
let deferredInstallPrompt = null;
@@ -321,7 +324,7 @@ export function renderFormFromPlan() {
}
updateBlendVisibility(state.plan.fields["2.1"]);
document.getElementById("header-coffee-name").textContent =
state.plan.fields["0.1"] || "New plan";
currentPlanName || state.plan.fields["0.1"] || "New plan";
}
function fmtOut(id, text) {
@@ -389,9 +392,9 @@ function renderLedger() {
fmtOut("pa-drop", d(ledger.D));
document.getElementById("back-coffee-name").textContent =
state.plan.fields["0.1"] || "";
currentPlanName || state.plan.fields["0.1"] || "";
document.getElementById("header-coffee-name").textContent =
state.plan.fields["0.1"] || "New plan";
currentPlanName || state.plan.fields["0.1"] || "New plan";
return ledger;
}
@@ -580,6 +583,7 @@ function syncPlan(snapshot = structuredClone(state.plan)) {
const body = await response.json();
remotePlanId = body.plan.id;
draftSyncedAt = body.plan.updated_at;
currentPlanName = body.plan.name ?? currentPlanName;
if (storageKey)
localStorage.setItem(
storageKey,
@@ -838,6 +842,11 @@ function wireDrawers() {
return { open, closeAll };
}
// The custom `name` column wins over worksheet field 0.1 wherever a plan's title is shown —
// mirrors the server's COALESCE(NULLIF(name,''), plan->'fields'->>'0.1') fallback.
function planDisplayTitle(plan) {
return plan.name || plan.plan?.fields?.["0.1"] || "Untitled plan";
}
async function loadPlans() {
if (!navigator.onLine) return;
const response = await fetch("/api/plans");
@@ -853,12 +862,42 @@ async function loadPlans() {
button.className = "plan-list-item";
button.classList.toggle("active", plan.id === remotePlanId);
const title = document.createElement("strong");
title.textContent = plan.plan?.fields?.["0.1"] || "Untitled plan";
title.textContent = planDisplayTitle(plan);
const updated = document.createElement("span");
updated.textContent = `Updated ${new Date(plan.updated_at).toLocaleDateString()}`;
button.append(title, updated);
button.addEventListener("click", () => selectPlan(plan));
const renameBtn = document.createElement("button");
renameBtn.type = "button";
renameBtn.className = "icon-btn plan-list-rename";
renameBtn.style.flexShrink = "0";
renameBtn.style.alignSelf = "center";
renameBtn.setAttribute("aria-label", "Rename plan");
renameBtn.textContent = "✎";
// A sibling of the select button, not nested inside it — stopPropagation alone
// wouldn't be enough to keep a click here from also selecting the plan otherwise.
renameBtn.addEventListener("click", async (event) => {
event.stopPropagation();
const next = prompt("Plan name", plan.name || "");
if (next === null) return;
try {
await api(`/api/plans/${plan.id}`, {
method: "PUT",
body: JSON.stringify({ name: next.trim() }),
});
} catch (error) {
showToast(error.message, "fail");
return;
}
if (plan.id === remotePlanId) {
currentPlanName = next.trim();
renderFormFromPlan();
renderLedger();
}
await loadPlans();
});
const deleteBtn = document.createElement("button");
deleteBtn.type = "button";
deleteBtn.className = "icon-btn plan-list-delete";
@@ -871,7 +910,7 @@ async function loadPlans() {
deletePlan(plan);
});
item.append(button, deleteBtn);
item.append(button, renameBtn, deleteBtn);
return item;
}),
);
@@ -880,6 +919,7 @@ async function selectPlan(plan) {
if (!(await flushCurrentPlan())) return;
remotePlanId = plan.id;
draftSyncedAt = plan.updated_at;
currentPlanName = plan.name || "";
state.plan = { ...blankPlan(), ...plan.plan };
history.replaceState(
null,
@@ -903,6 +943,7 @@ async function selectPlan(plan) {
function resetToBlankPlan() {
remotePlanId = null;
draftSyncedAt = null;
currentPlanName = "";
state.plan = blankPlan();
history.replaceState(null, "", "/app");
renderBlend();
@@ -922,7 +963,7 @@ async function newPlan() {
document.querySelector("[data-close-drawer]")?.click();
}
async function deletePlan(plan) {
const name = plan.plan?.fields?.["0.1"] || "Untitled plan";
const name = planDisplayTitle(plan);
if (!confirm(`Delete "${name}"? This cannot be undone.`)) return;
try {
await api(`/api/plans/${plan.id}`, { method: "DELETE" });