Fix New plan feeling like a no-op; add plan delete and why-panel collapse
Test and deploy / test-and-deploy (push) Successful in 1m34s

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 <[email protected]>
This commit is contained in:
2026-07-31 15:34:56 -04:00
co-authored by Claude Sonnet 5
parent 2237c199c1
commit 84ce75dc14
5 changed files with 100 additions and 31 deletions
+11 -1
View File
@@ -1706,6 +1706,11 @@ body.show-fids .field[data-fid]::after {
display: grid; display: grid;
gap: 8px; gap: 8px;
} }
.plan-list-row {
display: flex;
align-items: stretch;
gap: 6px;
}
.plan-list-item { .plan-list-item {
width: 100%; width: 100%;
display: grid; display: grid;
@@ -1719,6 +1724,10 @@ body.show-fids .field[data-fid]::after {
font: inherit; font: inherit;
cursor: pointer; cursor: pointer;
} }
.plan-list-delete {
flex-shrink: 0;
align-self: center;
}
.plan-list-item:hover, .plan-list-item:hover,
.plan-list-item.active { .plan-list-item.active {
border-color: var(--ember); border-color: var(--ember);
@@ -2603,7 +2612,8 @@ body.show-fids .field[data-fid]::after {
flex: 1 1 auto; flex: 1 1 auto;
justify-content: center; justify-content: center;
} }
.header-actions #btn-print { .header-actions #btn-print,
.header-actions #btn-expand-why {
flex: 0 0 auto; flex: 0 0 auto;
min-height: 44px; min-height: 44px;
padding: 8px 16px; padding: 8px 16px;
+6 -3
View File
@@ -88,6 +88,9 @@
><span class="dot"></span ><span class="dot"></span
><span class="autosave-text">Not saved yet</span></span ><span class="autosave-text">Not saved yet</span></span
> >
<button id="btn-expand-why" type="button" class="ghost-btn">
Expand all "why" notes
</button>
</div> </div>
</header> </header>
<div class="drawer-overlay hidden" id="drawer-overlay"></div> <div class="drawer-overlay hidden" id="drawer-overlay"></div>
@@ -160,7 +163,7 @@
</label> </label>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<details class="why-panel" data-why="cup-scores" open> <details class="why-panel" data-why="cup-scores">
<summary>Why the scale starts at 6</summary> <summary>Why the scale starts at 6</summary>
<p> <p>
The 610 range is a specialty-grading convention: The 610 range is a specialty-grading convention:
@@ -186,7 +189,7 @@
<section class="panel-card" id="cup-flavors"> <section class="panel-card" id="cup-flavors">
<div class="panel-head"><h2>Flavors</h2></div> <div class="panel-head"><h2>Flavors</h2></div>
<div class="panel-body"> <div class="panel-body">
<details class="why-panel" data-why="cup-flavors" open> <details class="why-panel" data-why="cup-flavors">
<summary>Tag what you can point at</summary> <summary>Tag what you can point at</summary>
<p> <p>
A tag is only useful if you could defend it to A tag is only useful if you could defend it to
@@ -206,7 +209,7 @@
<section class="panel-card" id="cup-notes"> <section class="panel-card" id="cup-notes">
<div class="panel-head"><h2>Notes</h2></div> <div class="panel-head"><h2>Notes</h2></div>
<div class="panel-body"> <div class="panel-body">
<details class="why-panel" data-why="cup-notes" open> <details class="why-panel" data-why="cup-notes">
<summary>Notes outlive scores</summary> <summary>Notes outlive scores</summary>
<p> <p>
Numbers compare roasts; sentences explain them. Write Numbers compare roasts; sentences explain them. Write
+10 -7
View File
@@ -129,6 +129,9 @@
><span class="dot"></span ><span class="dot"></span
><span class="autosave-text">Not saved yet</span></span ><span class="autosave-text">Not saved yet</span></span
> >
<button id="btn-expand-why" type="button" class="ghost-btn">
Expand all "why" notes
</button>
<button id="btn-print" type="button" class="primary-btn"> <button id="btn-print" type="button" class="primary-btn">
Print Print
</button> </button>
@@ -372,7 +375,7 @@
<section class="panel-card" id="sec-coffee"> <section class="panel-card" id="sec-coffee">
<div class="panel-head"><h2>The Coffee</h2></div> <div class="panel-head"><h2>The Coffee</h2></div>
<div class="panel-body"> <div class="panel-body">
<details class="why-panel" data-why="sec-coffee" open> <details class="why-panel" data-why="sec-coffee">
<summary>Why cultivar sets the clock</summary> <summary>Why cultivar sets the clock</summary>
<p> <p>
A roast plan is three durations laid end to end: drying + A roast plan is three durations laid end to end: drying +
@@ -543,7 +546,7 @@
</div> </div>
</div> </div>
<div class="panel-body" id="blend-body"> <div class="panel-body" id="blend-body">
<details class="why-panel" data-why="sec-blend" open> <details class="why-panel" data-why="sec-blend">
<summary>Why blends combine times, never temperatures</summary> <summary>Why blends combine times, never temperatures</summary>
<p> <p>
The drum has one heat setting and one clock — you cannot The drum has one heat setting and one clock — you cannot
@@ -615,7 +618,7 @@
<section class="panel-card" id="sec-process"> <section class="panel-card" id="sec-process">
<div class="panel-head"><h2>Roast Target</h2></div> <div class="panel-head"><h2>Roast Target</h2></div>
<div class="panel-body"> <div class="panel-body">
<details class="why-panel" data-why="sec-process" open> <details class="why-panel" data-why="sec-process">
<summary>Why processing moves development, not first crack</summary> <summary>Why processing moves development, not first crack</summary>
<p> <p>
Processing changed the chemistry that browning starts Processing changed the chemistry that browning starts
@@ -743,7 +746,7 @@
<section class="panel-card" id="sec-bean"> <section class="panel-card" id="sec-bean">
<div class="panel-head"><h2>Bean Condition</h2></div> <div class="panel-head"><h2>Bean Condition</h2></div>
<div class="panel-body"> <div class="panel-body">
<details class="why-panel" data-why="sec-bean" open> <details class="why-panel" data-why="sec-bean">
<summary>Why bean condition is worth seconds, not a new plan</summary> <summary>Why bean condition is worth seconds, not a new plan</summary>
<p> <p>
Moisture, density and screen size change how much energy the Moisture, density and screen size change how much energy the
@@ -826,7 +829,7 @@
<section class="panel-card" id="sec-machine"> <section class="panel-card" id="sec-machine">
<div class="panel-head"><h2>Machine Plan</h2></div> <div class="panel-head"><h2>Machine Plan</h2></div>
<div class="panel-body"> <div class="panel-body">
<details class="why-panel" data-why="sec-machine" open> <details class="why-panel" data-why="sec-machine">
<summary> <summary>
Why the rate of rise must fall, and never touch zero Why the rate of rise must fall, and never touch zero
</summary> </summary>
@@ -994,7 +997,7 @@
<span class="phase-chip">during the roast</span> <span class="phase-chip">during the roast</span>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<details class="why-panel" data-why="sec-roastlog" open> <details class="why-panel" data-why="sec-roastlog">
<summary>Record what you observed, never what you planned</summary> <summary>Record what you observed, never what you planned</summary>
<p> <p>
A planned milestone written into the log as though it A planned milestone written into the log as though it
@@ -1136,7 +1139,7 @@
<span class="phase-chip">after cupping</span> <span class="phase-chip">after cupping</span>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<details class="why-panel" data-why="sec-after" open> <details class="why-panel" data-why="sec-after">
<summary>One change per batch — arithmetic, not patience</summary> <summary>One change per batch — arithmetic, not patience</summary>
<p> <p>
Move two variables and there are four possible Move two variables and there are four possible
+42 -6
View File
@@ -16,10 +16,11 @@ import { initPrefillPanel } from "./prefill-ui.js?v=__ASSET_VERSION__";
import { initAlogPanel } from "./alog-ui.js?v=__ASSET_VERSION__"; import { initAlogPanel } from "./alog-ui.js?v=__ASSET_VERSION__";
import { initPrint } from "./print.js?v=__ASSET_VERSION__"; import { initPrint } from "./print.js?v=__ASSET_VERSION__";
import { initLotPicker } from "./lot-picker.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 { initSideNav } from "./nav.js?v=__ASSET_VERSION__";
import { wireWhyPanels } from "./why-panels.js?v=__ASSET_VERSION__"; import { wireWhyPanels } from "./why-panels.js?v=__ASSET_VERSION__";
import { initFieldHelp } from "./field-help.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 FIELD_ID_SET = new Set(FIELD_IDS);
const STORAGE_PREFIX = "roastPlannerPlan.v2"; const STORAGE_PREFIX = "roastPlannerPlan.v2";
@@ -823,6 +824,7 @@ async function loadPlans() {
list.replaceChildren( list.replaceChildren(
...plans.map((plan) => { ...plans.map((plan) => {
const item = document.createElement("li"); const item = document.createElement("li");
item.className = "plan-list-row";
const button = document.createElement("button"); const button = document.createElement("button");
button.type = "button"; button.type = "button";
button.className = "plan-list-item"; button.className = "plan-list-item";
@@ -833,7 +835,20 @@ async function loadPlans() {
updated.textContent = `Updated ${new Date(plan.updated_at).toLocaleDateString()}`; updated.textContent = `Updated ${new Date(plan.updated_at).toLocaleDateString()}`;
button.append(title, updated); button.append(title, updated);
button.addEventListener("click", () => selectPlan(plan)); 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; return item;
}), }),
); );
@@ -860,10 +875,9 @@ async function selectPlan(plan) {
lotPicker?.refreshRefineSuggestion(); lotPicker?.refreshRefineSuggestion();
document.querySelector("[data-close-drawer]")?.click(); document.querySelector("[data-close-drawer]")?.click();
} }
async function newPlan() { // Shared by newPlan() and deletePlan() (when the deleted plan was the one on screen) — resets
if (!confirm("Start a new plan? Your current plan is already saved locally.")) // the whole app to a fresh, unsynced plan.
return; function resetToBlankPlan() {
if (!(await flushCurrentPlan())) return;
remotePlanId = null; remotePlanId = null;
draftSyncedAt = null; draftSyncedAt = null;
state.plan = blankPlan(); state.plan = blankPlan();
@@ -875,6 +889,28 @@ async function newPlan() {
lotPicker?.renderOptions(); lotPicker?.renderOptions();
lotPicker?.refreshRefineSuggestion(); 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() { function wireToolbar() {
document.getElementById("btn-toggle-fids").addEventListener("click", (e) => { document.getElementById("btn-toggle-fids").addEventListener("click", (e) => {
+31 -14
View File
@@ -1,25 +1,42 @@
// Shared teaching-layer collapse memory for every page that uses `.why-panel` (`<details>` // Shared teaching-layer collapse memory for every page that uses `.why-panel` (`<details>`
// with a `data-why` id): the planner and the cupping session view. One localStorage key across // 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. // both so a user's choices carry over between pages.
const WHY_COLLAPSED_KEY = "roastPlannerWhyCollapsed.v1"; //
// 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() { export function wireWhyPanels() {
let collapsed; let expanded;
try { try {
collapsed = new Set(JSON.parse(localStorage.getItem(WHY_COLLAPSED_KEY)) || []); expanded = new Set(JSON.parse(localStorage.getItem(WHY_EXPANDED_KEY)) || []);
} catch { } catch {
collapsed = new Set(); expanded = new Set();
} }
for (const panel of document.querySelectorAll(".why-panel")) { const persist = () => {
if (collapsed.has(panel.dataset.why)) panel.open = false; 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", () => { panel.addEventListener("toggle", () => {
if (panel.open) collapsed.delete(panel.dataset.why); if (panel.open) expanded.add(panel.dataset.why);
else collapsed.add(panel.dataset.why); else expanded.delete(panel.dataset.why);
try { persist();
localStorage.setItem(WHY_COLLAPSED_KEY, JSON.stringify([...collapsed]));
} catch {
/* unavailable storage */
}
}); });
} }
document.getElementById("btn-expand-why")?.addEventListener("click", () => {
for (const panel of panels) {
panel.open = true;
expanded.add(panel.dataset.why);
}
persist();
});
} }