From 84ce75dc141f18df3ae1ca00a1befe43dd4dbe57 Mon Sep 17 00:00:00 2001 From: Shane Maynard Date: Fri, 31 Jul 2026 15:34:56 -0400 Subject: [PATCH] Fix New plan feeling like a no-op; add plan delete and why-panel collapse New plan reset the form correctly but left the Plans drawer open over it, so the reset was invisible until the user closed the drawer themselves. Now closes it immediately, matching selectPlan(). Also adds a delete button per plan in the Plans drawer (server route already existed) - deleting the currently open plan resets to a fresh blank one, matching New plan. Why-panel teaching callouts ("WHY CULTIVAR SETS THE CLOCK" etc.) now default collapsed instead of wall-to-wall expanded on first load, with an "Expand all" button in the header on both the planner and cupping pages. Persistence flips from tracking collapsed-vs-default-open to expanded-vs-default-closed to match. Co-Authored-By: Claude Sonnet 5 --- public/app.css | 12 ++++++++++- public/cupping.html | 9 +++++--- public/index.html | 17 +++++++++------ public/js/main.js | 48 +++++++++++++++++++++++++++++++++++------ public/js/why-panels.js | 45 ++++++++++++++++++++++++++------------ 5 files changed, 100 insertions(+), 31 deletions(-) diff --git a/public/app.css b/public/app.css index 4a082a1..b0dd01b 100644 --- a/public/app.css +++ b/public/app.css @@ -1706,6 +1706,11 @@ body.show-fids .field[data-fid]::after { display: grid; gap: 8px; } +.plan-list-row { + display: flex; + align-items: stretch; + gap: 6px; +} .plan-list-item { width: 100%; display: grid; @@ -1719,6 +1724,10 @@ body.show-fids .field[data-fid]::after { font: inherit; cursor: pointer; } +.plan-list-delete { + flex-shrink: 0; + align-self: center; +} .plan-list-item:hover, .plan-list-item.active { border-color: var(--ember); @@ -2603,7 +2612,8 @@ body.show-fids .field[data-fid]::after { flex: 1 1 auto; justify-content: center; } - .header-actions #btn-print { + .header-actions #btn-print, + .header-actions #btn-expand-why { flex: 0 0 auto; min-height: 44px; padding: 8px 16px; diff --git a/public/cupping.html b/public/cupping.html index d8c49f3..e6ec5b2 100644 --- a/public/cupping.html +++ b/public/cupping.html @@ -88,6 +88,9 @@ >Not saved yet + @@ -160,7 +163,7 @@
-
+
Why the scale starts at 6

The 6–10 range is a specialty-grading convention: @@ -186,7 +189,7 @@

Flavors

-
+
Tag what you can point at

A tag is only useful if you could defend it to @@ -206,7 +209,7 @@

Notes

-
+
Notes outlive scores

Numbers compare roasts; sentences explain them. Write diff --git a/public/index.html b/public/index.html index 4b05dd1..baa0230 100644 --- a/public/index.html +++ b/public/index.html @@ -129,6 +129,9 @@ >Not saved yet + @@ -372,7 +375,7 @@

The Coffee

-
+
Why cultivar sets the clock

A roast plan is three durations laid end to end: drying + @@ -543,7 +546,7 @@

-
+
Why blends combine times, never temperatures

The drum has one heat setting and one clock — you cannot @@ -615,7 +618,7 @@

Roast Target

-
+
Why processing moves development, not first crack

Processing changed the chemistry that browning starts @@ -743,7 +746,7 @@

Bean Condition

-
+
Why bean condition is worth seconds, not a new plan

Moisture, density and screen size change how much energy the @@ -826,7 +829,7 @@

Machine Plan

-
+
Why the rate of rise must fall, and never touch zero @@ -994,7 +997,7 @@ during the roast
-
+
Record what you observed, never what you planned

A planned milestone written into the log as though it @@ -1136,7 +1139,7 @@ after cupping

-
+
One change per batch — arithmetic, not patience

Move two variables and there are four possible diff --git a/public/js/main.js b/public/js/main.js index dd532b9..372c107 100644 --- a/public/js/main.js +++ b/public/js/main.js @@ -16,10 +16,11 @@ import { initPrefillPanel } from "./prefill-ui.js?v=__ASSET_VERSION__"; import { initAlogPanel } from "./alog-ui.js?v=__ASSET_VERSION__"; import { initPrint } from "./print.js?v=__ASSET_VERSION__"; import { initLotPicker } from "./lot-picker.js?v=__ASSET_VERSION__"; -import { protectedFetch, csrfToken } from "./api.js?v=__ASSET_VERSION__"; +import { api, protectedFetch, csrfToken } from "./api.js?v=__ASSET_VERSION__"; import { initSideNav } from "./nav.js?v=__ASSET_VERSION__"; import { wireWhyPanels } from "./why-panels.js?v=__ASSET_VERSION__"; import { initFieldHelp } from "./field-help.js?v=__ASSET_VERSION__"; +import { showToast } from "./toast.js?v=__ASSET_VERSION__"; const FIELD_ID_SET = new Set(FIELD_IDS); const STORAGE_PREFIX = "roastPlannerPlan.v2"; @@ -823,6 +824,7 @@ async function loadPlans() { list.replaceChildren( ...plans.map((plan) => { const item = document.createElement("li"); + item.className = "plan-list-row"; const button = document.createElement("button"); button.type = "button"; button.className = "plan-list-item"; @@ -833,7 +835,20 @@ async function loadPlans() { updated.textContent = `Updated ${new Date(plan.updated_at).toLocaleDateString()}`; button.append(title, updated); button.addEventListener("click", () => selectPlan(plan)); - item.append(button); + + const deleteBtn = document.createElement("button"); + deleteBtn.type = "button"; + deleteBtn.className = "icon-btn plan-list-delete"; + deleteBtn.setAttribute("aria-label", `Delete ${title.textContent}`); + deleteBtn.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. + deleteBtn.addEventListener("click", (event) => { + event.stopPropagation(); + deletePlan(plan); + }); + + item.append(button, deleteBtn); return item; }), ); @@ -860,10 +875,9 @@ async function selectPlan(plan) { lotPicker?.refreshRefineSuggestion(); document.querySelector("[data-close-drawer]")?.click(); } -async function newPlan() { - if (!confirm("Start a new plan? Your current plan is already saved locally.")) - return; - if (!(await flushCurrentPlan())) return; +// Shared by newPlan() and deletePlan() (when the deleted plan was the one on screen) — resets +// the whole app to a fresh, unsynced plan. +function resetToBlankPlan() { remotePlanId = null; draftSyncedAt = null; state.plan = blankPlan(); @@ -875,6 +889,28 @@ async function newPlan() { lotPicker?.renderOptions(); lotPicker?.refreshRefineSuggestion(); } +async function newPlan() { + if (!confirm("Start a new plan? Your current plan is already saved locally.")) + return; + if (!(await flushCurrentPlan())) return; + resetToBlankPlan(); + // Without this, the Plans drawer stays open over the now-blank form — the reset happens but + // is invisible, so it doesn't read as "starting something new" until the user closes it themselves. + document.querySelector("[data-close-drawer]")?.click(); +} +async function deletePlan(plan) { + const name = plan.plan?.fields?.["0.1"] || "Untitled plan"; + if (!confirm(`Delete "${name}"? This cannot be undone.`)) return; + try { + await api(`/api/plans/${plan.id}`, { method: "DELETE" }); + } catch (error) { + showToast(error.message, "fail"); + return; + } + showToast("Plan deleted."); + if (plan.id === remotePlanId) resetToBlankPlan(); + await loadPlans(); +} function wireToolbar() { document.getElementById("btn-toggle-fids").addEventListener("click", (e) => { diff --git a/public/js/why-panels.js b/public/js/why-panels.js index 5838f72..379c80d 100644 --- a/public/js/why-panels.js +++ b/public/js/why-panels.js @@ -1,25 +1,42 @@ // Shared teaching-layer collapse memory for every page that uses `.why-panel` (`

` // with a `data-why` id): the planner and the cupping session view. One localStorage key across -// both so a user's dismissals carry over between pages. -const WHY_COLLAPSED_KEY = "roastPlannerWhyCollapsed.v1"; +// both so a user's choices carry over between pages. +// +// Panels are collapsed by default (see index.html/cupping.html) so the form isn't wall-to-wall +// explanatory text on first load; this tracks which ones a user has explicitly opened, rather +// than which they've closed, so it stays meaningful against a closed-by-default baseline. +const WHY_EXPANDED_KEY = "roastPlannerWhyExpanded.v1"; export function wireWhyPanels() { - let collapsed; + let expanded; try { - collapsed = new Set(JSON.parse(localStorage.getItem(WHY_COLLAPSED_KEY)) || []); + expanded = new Set(JSON.parse(localStorage.getItem(WHY_EXPANDED_KEY)) || []); } catch { - collapsed = new Set(); + expanded = new Set(); } - for (const panel of document.querySelectorAll(".why-panel")) { - if (collapsed.has(panel.dataset.why)) panel.open = false; + const persist = () => { + try { + localStorage.setItem(WHY_EXPANDED_KEY, JSON.stringify([...expanded])); + } catch { + /* unavailable storage */ + } + }; + + const panels = [...document.querySelectorAll(".why-panel")]; + for (const panel of panels) { + if (expanded.has(panel.dataset.why)) panel.open = true; panel.addEventListener("toggle", () => { - if (panel.open) collapsed.delete(panel.dataset.why); - else collapsed.add(panel.dataset.why); - try { - localStorage.setItem(WHY_COLLAPSED_KEY, JSON.stringify([...collapsed])); - } catch { - /* unavailable storage */ - } + if (panel.open) expanded.add(panel.dataset.why); + else expanded.delete(panel.dataset.why); + persist(); }); } + + document.getElementById("btn-expand-why")?.addEventListener("click", () => { + for (const panel of panels) { + panel.open = true; + expanded.add(panel.dataset.why); + } + persist(); + }); }