From d02a961d10413699c15aa06ad95af6262d058580 Mon Sep 17 00:00:00 2001 From: FR4NKLN <47734451+FR4NKLN@users.noreply.github.com> Date: Tue, 26 May 2026 19:35:18 -0400 Subject: [PATCH 1/6] feat(firmware): wire web brew-target stepper handlers + revert profile after brew MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two coupled changes that back the new web dashboard "quick adjust" +/- stepper for Brew mode. WebUIPlugin::handleWebSocketData Add handlers for `req:raise-brew-target` and `req:lower-brew-target`. The web client has been sending these messages via its existing `raiseTarget()` / `lowerTarget()` callbacks (mirroring how the Grind +/- buttons already work), but the firmware had no matching handler so the messages were silently dropped. The new handlers call `Controller::raiseBrewTarget` / `lowerBrewTarget` — the exact same methods the device's gear-icon BrewScreen invokes — so behaviour is identical whether the user adjusts from the web UI or the on-device display. Controller::deactivate After a brew finishes, reload the selected profile from disk. Any in-memory target temp / target weight adjustments made via the dashboard stepper or the display's gear-icon BrewScreen Settings panel revert back to the profile's on-disk values. This is what makes the steppers behave as transient "for this next brew only" adjustments rather than silent, persistent modifications to the active profile. For permanent changes the user goes to the Profiles page (web) or hits Save on the display's BrewScreen Settings panel. --- src/display/core/Controller.cpp | 9 +++++++++ src/display/plugins/WebUIPlugin.cpp | 7 +++++++ 2 files changed, 16 insertions(+) diff --git a/src/display/core/Controller.cpp b/src/display/core/Controller.cpp index a8d9a2aa6..4f89b0111 100644 --- a/src/display/core/Controller.cpp +++ b/src/display/core/Controller.cpp @@ -645,6 +645,15 @@ void Controller::deactivate() { currentProcess = nullptr; if (lastProcess->getType() == MODE_BREW) { pluginManager->trigger("controller:brew:end"); + // Reload the selected profile from disk so any temporary temp/weight + // adjustments made via the dashboard +/- steppers or the display's + // gear-icon BrewScreen Settings panel (raise/lowerTemp, + // raise/lowerBrewTarget) revert back to the profile's saved values + // after each brew. For permanent changes the user should save via + // the Profiles page or the display's Save button. + if (profileManager) { + profileManager->loadSelectedProfile(profileManager->getSelectedProfile()); + } } else if (lastProcess->getType() == MODE_GRIND) { pluginManager->trigger("controller:grind:end"); } diff --git a/src/display/plugins/WebUIPlugin.cpp b/src/display/plugins/WebUIPlugin.cpp index ec4992891..95998bba3 100644 --- a/src/display/plugins/WebUIPlugin.cpp +++ b/src/display/plugins/WebUIPlugin.cpp @@ -339,6 +339,13 @@ void WebUIPlugin::handleWebSocketData(AsyncWebSocket *server, AsyncWebSocketClie controller->raiseGrindTarget(); } else if (msgType == "req:lower-grind-target") { controller->lowerGrindTarget(); + } else if (msgType == "req:raise-brew-target") { + // Web dashboard Brew-mode +/- buttons. The web client has + // long been sending this via raiseTarget() but no firmware + // handler existed, so the message was silently dropped. + controller->raiseBrewTarget(); + } else if (msgType == "req:lower-brew-target") { + controller->lowerBrewTarget(); } else if (msgType == "req:change-mode") { if (doc["mode"].is()) { auto mode = doc["mode"].as(); From 6c3868bbf733f2cb1bc927cd6eb4553e492a82ca Mon Sep 17 00:00:00 2001 From: FR4NKLN <47734451+FR4NKLN@users.noreply.github.com> Date: Tue, 26 May 2026 19:39:24 -0400 Subject: [PATCH 2/6] feat(web/dashboard): Brew-mode +/- steppers + over-pressure red alert MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two related additions to the Process Controls card on the Dashboard, applied to both the full `ProcessControls` and the responsive `CompactProcessControls` so behaviour is consistent across desktop / tablet / phone viewports. Brew-mode +/- steppers (TEMP + WEIGHT) When in Brew mode with no process running, show TEMP and TARGET WEIGHT steppers under the profile chart (full view) or under the profile/toggle block (compact view). Each tap calls the same Controller method (`raiseTemp` / `lowerTemp` / `raiseBrewTarget` / `lowerBrewTarget`) that the device's gear-icon BrewScreen invokes, so the web and the on-device display behave identically. The weight stepper appears only when the volumetric scale is connected and the profile has a weight target (matches the existing condition used by the header weight metric). These are intentionally transient "next-brew-only" adjustments, not a profile editor: the firmware reverts the in-memory profile to its saved on-disk values after each brew completes (see the matching Controller::deactivate change). The Profiles page remains the place for permanent edits. This mirrors the existing display-side gear icon flow where users can tweak a shot without dirtying the saved profile. Compact view uses a slimmer inline `MiniStepper` so two steppers fit side-by-side in the narrow card without wrapping; the full view uses the standard styling consistent with the existing Steam / Water temp steppers. Over-pressure red alert The current pressure value in the metric header turns text-error (bright red, semibold) when above 1.0 bar in Brew mode. This is a visual cue that the boiler/group is still pressurised from a prior shot or heat-up cycle and should be released — typically by a short flush or by opening the steam wand — before the next brew is started. Two reasons this matters and why it's worth flagging proactively: 1. Pressure-aware profiles depend on a clean baseline. Adaptive / pressure-driven profiles read the current pressure to drive phase transitions, predictive delay, and decline behaviour. If residual pressure is already above the profile's preinfusion or comparison thresholds at process start, those phases can advance incorrectly or skip altogether, producing a shot that doesn't follow the intended curve. 2. Brewing into a back-pressurised group head is a common physical cause of channeling, blown pucks, and uneven extraction — especially on the first shot of a session or after steaming. The colour change is intentionally localised to the *current* pressure value only: the target value and the unit stay normal colour so the visual change is precise (it draws attention to the reading itself, not the whole metric block), and it only triggers in Brew mode — >1 bar in Steam mode is normal and expected. --- web/src/pages/Home/CompactProcessControls.jsx | 62 +++++++++++- web/src/pages/Home/ProcessControls.jsx | 97 ++++++++++++++++++- 2 files changed, 150 insertions(+), 9 deletions(-) diff --git a/web/src/pages/Home/CompactProcessControls.jsx b/web/src/pages/Home/CompactProcessControls.jsx index b0482c744..e80dfec7e 100644 --- a/web/src/pages/Home/CompactProcessControls.jsx +++ b/web/src/pages/Home/CompactProcessControls.jsx @@ -32,10 +32,10 @@ import { const status = computed(() => machine.value.status); -const Metric = ({ icon, current, target, unit, rotation }) => ( +const Metric = ({ icon, current, target, unit, rotation, currentClassName = 'text-base-content' }) => (
- {current} + {current} / {target} {unit} @@ -58,6 +58,29 @@ const TargetToggle = ({ value, onChange }) => { ); }; +// Compact two-up stepper for Brew mode (TEMP + WEIGHT side-by-side). +// Slimmer than the full so two of them fit in the narrow Compact +// card width even on smaller tablets / portrait windows. +const MiniStepper = ({ label, value, onDecrease, onIncrease }) => { + const btn = 'btn btn-ghost btn-xs flex h-6 w-6 items-center justify-center rounded-full p-0'; + return ( +
+
{label}
+
+ +
+ {value} +
+ +
+
+ ); +}; + const Adjuster = ({ label, value, onDecrease, onIncrease }) => { const btn = 'btn btn-ghost btn-sm flex h-8 w-8 items-center justify-center rounded-full p-0'; return ( @@ -126,7 +149,7 @@ const InfoView = ({ title, hint }) => (
); -const BrewIdleView = ({ s, brewTarget, sendTarget }) => ( +const BrewIdleView = ({ s, brewTarget, sendTarget, send }) => (
( + {/* Same +/- steppers as the full ProcessControls — temp left, weight + right. Calls the same Controller methods that the device's gear-icon + BrewScreen uses; firmware reverts profile to disk values after each + brew (Controller::deactivate). Uses a slimmer inline layout (not the + shared ) so two steppers fit side-by-side in the narrow + Compact container even on tight viewports. */} +
+ send('req:lower-temp')} + onIncrease={() => send('req:raise-temp')} + /> + {brewTarget && s.volumetricAvailable && ( + send('req:lower-brew-target')} + onIncrease={() => send('req:raise-brew-target')} + /> + )} +
{s.volumetricAvailable && }
); @@ -241,7 +286,8 @@ export default function CompactProcessControls({ brew, mode, changeMode }) { if (processRunning && finished) return ; if (processRunning) return ; if (mode === 0) return ; - if (mode === 1) return ; + if (mode === 1) + return ; if (mode === 2 || mode === 3) return ; if (grind && !grindAvailable) return ; @@ -283,6 +329,14 @@ export default function CompactProcessControls({ brew, mode, changeMode }) { current={(s.currentPressure ?? 0).toFixed(1)} target={(s.targetPressure ?? 0).toFixed(1)} unit=' bar' + /* Red alert when over 1 bar in Brew mode — boiler is likely + over-pressurized from previous shot/heat-up; flush or open + steam wand before brewing. */ + currentClassName={ + brew && (s.currentPressure ?? 0) > 1.0 + ? 'text-error font-semibold' + : 'text-base-content' + } /> diff --git a/web/src/pages/Home/ProcessControls.jsx b/web/src/pages/Home/ProcessControls.jsx index 4bdbc2397..5a1aa3add 100644 --- a/web/src/pages/Home/ProcessControls.jsx +++ b/web/src/pages/Home/ProcessControls.jsx @@ -321,8 +321,8 @@ const ProcessControls = props => { {status.value.currentTemperature.toFixed(1) || 0} - {' '} - / {status.value.targetTemperature || 0}°C + {' / '} + {status.value.targetTemperature || 0}°C {status.value.volumetricAvailable && mode !== 0 && ( @@ -334,8 +334,8 @@ const ProcessControls = props => { {(status.value.currentWeight ?? 0).toFixed(1)}g - {' '} - / {(status.value.targetWeight ?? 0).toFixed(0)}g + {' / '} + {(status.value.targetWeight ?? 0).toFixed(0)}g )} @@ -344,7 +344,21 @@ const ProcessControls = props => {
- {status.value.currentPressure?.toFixed(1) || 0} /{' '} + {/* Only the CURRENT pressure value turns red when above 1 bar in + Brew mode — typically means the boiler is over-pressurized + from the previous shot or heat-up cycle, and the user should + flush or open the steam wand before brewing to avoid wrecking + the puck. The target pressure and units stay normal color. */} + 1.0 + ? 'text-error font-semibold' + : '' + } + > + {status.value.currentPressure?.toFixed(1) || 0} + + {' / '} {status.value.targetPressure?.toFixed(1) || 0} bar
@@ -476,6 +490,79 @@ const ProcessControls = props => { )} {/* Controls for different modes */} + {mode === 1 && !active && !finished && ( +
+ {/* Brew mode: quick stepper adjustments for the active profile's + target temp and weight, laid out side-by-side (temp left, + weight right). Each tap calls the same raiseTemp / lowerTemp + / raiseBrewTarget / lowerBrewTarget controller methods that + the device's gear-icon BrewScreen uses, so the behaviour is + identical across web + display. Changes mutate the in-memory + profile and the firmware reverts to the on-disk profile after + the next brew completes (Controller::deactivate) — for + permanent changes use the Profiles page. */} +
+
+
+ TEMPERATURE +
+
+ + + +
+ {status.value.targetTemperature || 0}°C +
+ + + +
+
+ {brewTarget && status.value.volumetricAvailable && ( +
+
+ TARGET WEIGHT +
+
+ + + +
+ {(status.value.targetWeight ?? 0).toFixed(0)}g +
+ + + +
+
+ )} +
+
+ )} {mode === 2 && (
{/* Temperature adjustment controls for steam mode */} From c7b65569b3455da78e84405625f74470bcca9280 Mon Sep 17 00:00:00 2001 From: FR4NKLN <47734451+FR4NKLN@users.noreply.github.com> Date: Tue, 26 May 2026 19:39:50 -0400 Subject: [PATCH 3/6] fix(web/dashboard): correct portrait variant typo (was potrait) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The responsive switch between `CompactProcessControls` and the full `ProcessControls` in `Home/index.jsx` uses `landscape:hmd:...` and `portrait:md:...` Tailwind variants. The portrait classes were misspelled `potrait:md:hidden` and `potrait:md:contents` (introduced in e95b844f3 "fix: Fix some responsive things"). Tailwind silently ignores undefined variants, so these classes never applied. Result: in portrait orientation, `ProcessControls` was hidden by its base `hidden` class and `CompactProcessControls` was shown instead — at every width, even on a tablet in portrait where the full layout would fit comfortably (iPad portrait, tablet split-screen, resized desktop window in portrait aspect, etc.). Renaming `potrait` -> `portrait` activates the intended responsive behaviour: at portrait + md width (>= 768px) the full `ProcessControls` renders; below that, `CompactProcessControls` does, matching what was originally intended. --- web/src/pages/Home/index.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/web/src/pages/Home/index.jsx b/web/src/pages/Home/index.jsx index b34168ece..d79b9ea2d 100644 --- a/web/src/pages/Home/index.jsx +++ b/web/src/pages/Home/index.jsx @@ -63,10 +63,10 @@ export function Home() { className={`landscape:max-lg:min-h-0 landscape:sm:col-span-5 ${dashboardLayout === DASHBOARD_LAYOUTS.ORDER_FIRST ? 'order-first' : 'order-last'}`} title='Process Controls' > -
+
-
+
From 2bd681427ce5b81a97270cc8afe9a5d504c09e5d Mon Sep 17 00:00:00 2001 From: FR4NKLN <47734451+FR4NKLN@users.noreply.github.com> Date: Wed, 27 May 2026 00:24:59 -0400 Subject: [PATCH 4/6] refactor(web/dashboard): morphing weight/time stepper + tighter over-pressure trigger MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refines the Brew-mode quick-adjust UI to mirror the device's gear-icon BrewScreen Settings panel exactly, and narrows the over-pressure red alert to the state where it is actually actionable. ### Morphing TARGET WEIGHT / TARGET TIME stepper Previously the Brew-mode card showed a fixed TEMP + TARGET WEIGHT pair. When no scale was connected or the profile wasn't volumetric, the weight stepper was hidden and the second slot collapsed — leaving a lone TEMP stepper and no way to surface the time-based target. The second slot now morphs based on the firmware status flag \`bt = isVolumetricAvailable && profile.isVolumetric\`: - \`bt=true\` → label "TARGET WEIGHT", value \`Xg\`, +/- adjusts volumetric - \`bt=false\` → label "TARGET TIME", value \`M:SS\`, +/- adjusts duration Both modes send the same \`req:raise-brew-target\` / \`req:lower-brew-target\` WS messages; \`Controller::raiseBrewTarget\` / \`lowerBrewTarget\` decide internally whether to mutate the profile's volumetric value or its phase durations based on the same condition. Format is the only visible difference. This is the exact logic the display's BrewScreen Settings panel already uses. Added a \`fmtDuration(seconds)\` helper alongside \`fmtElapsed(ms)\` in \`utils.js\` since the firmware \`btd\` field reports seconds whereas \`processInfo.e\` reports milliseconds. Applied symmetrically to \`ProcessControls\` (full layout) and \`CompactProcessControls\` (via the existing \`MiniStepper\` component). ### Tightened over-pressure red alert The red \`text-error\` styling on the current pressure value now requires \`brew && !active && !finished\`. Original behaviour fired red whenever pressure > 1 bar in Brew mode, including during the shot itself — where 8+ bar is the expected state and the red noise was actively misleading. Narrowed to the only state where the cue is actionable: sitting idle in Brew mode before the next shot, with residual boiler pressure that should be released by flushing or opening the steam wand. During an active brew and immediately after (while the user reviews the post-brew stats), the value renders in normal colour regardless of pressure; the cue returns automatically once the finished view is cleared and the card is back to idle. --- web/src/pages/Home/CompactProcessControls.jsx | 44 +++++---- web/src/pages/Home/ProcessControls.jsx | 96 ++++++++++--------- web/src/pages/Home/utils.js | 8 ++ 3 files changed, 85 insertions(+), 63 deletions(-) diff --git a/web/src/pages/Home/CompactProcessControls.jsx b/web/src/pages/Home/CompactProcessControls.jsx index e80dfec7e..2257cf2e3 100644 --- a/web/src/pages/Home/CompactProcessControls.jsx +++ b/web/src/pages/Home/CompactProcessControls.jsx @@ -22,6 +22,7 @@ import PropTypes from 'prop-types'; import { ApiServiceContext, machine } from '../../services/ApiService.js'; import { ModeTab } from './ModeTab.jsx'; import { + fmtDuration, fmtElapsed, fmtPhaseTarget, getPhaseLabel, @@ -168,12 +169,14 @@ const BrewIdleView = ({ s, brewTarget, sendTarget, send }) => ( - {/* Same +/- steppers as the full ProcessControls — temp left, weight - right. Calls the same Controller methods that the device's gear-icon - BrewScreen uses; firmware reverts profile to disk values after each - brew (Controller::deactivate). Uses a slimmer inline layout (not the - shared ) so two steppers fit side-by-side in the narrow - Compact container even on tight viewports. */} + {/* Mirrors the device's gear-icon BrewScreen Settings panel exactly: + always show TEMP, plus a second slot that morphs between WEIGHT and + TIME based on `brewTarget` (firmware's `bt` flag = scale connected + AND profile is volumetric). Both modes call the SAME Controller + methods — `raiseBrewTarget` / `lowerBrewTarget` — which decide + internally whether to adjust the profile's volumetric target or its + phase duration based on the same condition. Format is the only + visible difference. */}
( onDecrease={() => send('req:lower-temp')} onIncrease={() => send('req:raise-temp')} /> - {brewTarget && s.volumetricAvailable && ( - send('req:lower-brew-target')} - onIncrease={() => send('req:raise-brew-target')} - /> - )} + send('req:lower-brew-target')} + onIncrease={() => send('req:raise-brew-target')} + />
- {s.volumetricAvailable && }
); @@ -329,11 +333,13 @@ export default function CompactProcessControls({ brew, mode, changeMode }) { current={(s.currentPressure ?? 0).toFixed(1)} target={(s.targetPressure ?? 0).toFixed(1)} unit=' bar' - /* Red alert when over 1 bar in Brew mode — boiler is likely - over-pressurized from previous shot/heat-up; flush or open - steam wand before brewing. */ + /* Red alert when over 1 bar while sitting idle in Brew mode — + boiler is likely over-pressurized from previous shot/heat-up; + flush or open steam wand before brewing. Suppressed during an + active brew (high pressure is expected) and immediately after + (pressure takes a moment to bleed off). */ currentClassName={ - brew && (s.currentPressure ?? 0) > 1.0 + brew && !active && !finished && (s.currentPressure ?? 0) > 1.0 ? 'text-error font-semibold' : 'text-base-content' } diff --git a/web/src/pages/Home/ProcessControls.jsx b/web/src/pages/Home/ProcessControls.jsx index 5a1aa3add..1ddf24bbf 100644 --- a/web/src/pages/Home/ProcessControls.jsx +++ b/web/src/pages/Home/ProcessControls.jsx @@ -17,7 +17,7 @@ import { ProcessProfileChart } from '../../components/ProcessProfileChart.jsx'; import { faPlus } from '@fortawesome/free-solid-svg-icons/faPlus'; import { faMinus } from '@fortawesome/free-solid-svg-icons/faMinus'; import { Tooltip } from '../../components/Tooltip.jsx'; -import { MODES } from './utils.js'; +import { fmtDuration, MODES } from './utils.js'; import { ModeTab } from './ModeTab.jsx'; const status = computed(() => machine.value.status); @@ -344,14 +344,20 @@ const ProcessControls = props => {
- {/* Only the CURRENT pressure value turns red when above 1 bar in - Brew mode — typically means the boiler is over-pressurized - from the previous shot or heat-up cycle, and the user should - flush or open the steam wand before brewing to avoid wrecking - the puck. The target pressure and units stay normal color. */} + {/* Only the CURRENT pressure value turns red when above 1 bar + while sitting idle in Brew mode — typically means the boiler + is over-pressurized from the previous shot or heat-up cycle, + and the user should flush or open the steam wand before + brewing to avoid wrecking the puck. Suppressed once a brew + is active or just finished, since high pressure is expected + during the shot and takes a moment to bleed off after. The + target pressure and units always stay normal color. */} 1.0 + brew && + !active && + !finished && + (status.value.currentPressure ?? 0) > 1.0 ? 'text-error font-semibold' : '' } @@ -492,15 +498,17 @@ const ProcessControls = props => { {/* Controls for different modes */} {mode === 1 && !active && !finished && (
- {/* Brew mode: quick stepper adjustments for the active profile's - target temp and weight, laid out side-by-side (temp left, - weight right). Each tap calls the same raiseTemp / lowerTemp - / raiseBrewTarget / lowerBrewTarget controller methods that - the device's gear-icon BrewScreen uses, so the behaviour is - identical across web + display. Changes mutate the in-memory - profile and the firmware reverts to the on-disk profile after - the next brew completes (Controller::deactivate) — for - permanent changes use the Profiles page. */} + {/* Brew mode quick steppers — mirrors the device's gear-icon + BrewScreen Settings panel: always show TEMP, plus a single + second slot that morphs between WEIGHT and TIME based on + `brewTarget` (firmware's `bt` flag = scale connected AND + profile is volumetric). Both modes call the same Controller + methods (raiseBrewTarget / lowerBrewTarget) which decide + internally whether to adjust the profile's volumetric target + or its phase duration based on the same condition. Changes + are transient — firmware reverts the in-memory profile to its + on-disk values after each brew (Controller::deactivate). For + permanent edits use the Profiles page. */}
@@ -530,36 +538,36 @@ const ProcessControls = props => {
- {brewTarget && status.value.volumetricAvailable && ( -
-
- TARGET WEIGHT -
-
- - - -
- {(status.value.targetWeight ?? 0).toFixed(0)}g -
- - - +
+
+ {brewTarget ? 'TARGET WEIGHT' : 'TARGET TIME'} +
+
+ + + +
+ {brewTarget + ? `${(status.value.targetWeight ?? 0).toFixed(0)}g` + : fmtDuration(status.value.brewTargetDuration ?? 0)}
+ + +
- )} +
)} diff --git a/web/src/pages/Home/utils.js b/web/src/pages/Home/utils.js index 0afe36e38..f24b66d9a 100644 --- a/web/src/pages/Home/utils.js +++ b/web/src/pages/Home/utils.js @@ -20,6 +20,14 @@ export const fmtElapsed = (ms = 0) => { return `${Math.floor(secs / 60)}:${String(secs % 60).padStart(2, '0')}`; }; +// fmtDuration takes a value in SECONDS (vs fmtElapsed which takes ms) and +// renders M:SS. Used by the Brew-mode stepper when the profile/scale combo +// puts it in time mode rather than weight mode. +export const fmtDuration = (s = 0) => { + const secs = Math.floor(s); + return `${Math.floor(secs / 60)}:${String(secs % 60).padStart(2, '0')}`; +}; + export const fmtPhaseTarget = (p, grind) => { if (p?.tt === 'time') return `${(p.pt / 1000).toFixed(0)}s`; if (p?.tt === 'volumetric') return `${p.pt.toFixed(grind ? 1 : 0)}g`; From 4c06098b135c4bf0625324dc9d6ad91c9c1fedeb Mon Sep 17 00:00:00 2001 From: FR4NKLN <47734451+FR4NKLN@users.noreply.github.com> Date: Wed, 27 May 2026 00:59:27 -0400 Subject: [PATCH 5/6] fix(web/dashboard): align Compact stepper labels to full layout (TARGET WEIGHT/TIME) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CompactProcessControls' MiniStepper for the morphing second slot was labelled bare \`WEIGHT\` / \`TIME\`, which reads like a current measurement rather than the profile-driven stop condition the +/- buttons actually mutate. ProcessControls (full layout) already uses \`TARGET WEIGHT\` / \`TARGET TIME\` for the same control. Aligned the Compact label set so both layouts describe the same thing the same way, and the user can tell at a glance that the value below the label is the brew-stopping condition for the current profile + scale state — not a live reading of cup weight or elapsed shot time. \`TEMP\` (vs full layout's \`TEMPERATURE\`) kept short since temperature has no read/target ambiguity at this position in the card. --- web/src/pages/Home/CompactProcessControls.jsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/web/src/pages/Home/CompactProcessControls.jsx b/web/src/pages/Home/CompactProcessControls.jsx index 2257cf2e3..6eb85711f 100644 --- a/web/src/pages/Home/CompactProcessControls.jsx +++ b/web/src/pages/Home/CompactProcessControls.jsx @@ -184,8 +184,13 @@ const BrewIdleView = ({ s, brewTarget, sendTarget, send }) => ( onDecrease={() => send('req:lower-temp')} onIncrease={() => send('req:raise-temp')} /> + {/* "TARGET" prefix on the morphing label makes the read honest: + the value is the brew-stopping condition the firmware will use, + and that condition depends on the loaded profile (volumetric or + duration). Matches the wording the full-layout ProcessControls + already uses (TARGET WEIGHT / TARGET TIME). */} Date: Wed, 27 May 2026 11:40:07 -0400 Subject: [PATCH 6/6] fix(firmware): reset selected profile before reload in deactivate auto-revert The auto-revert added in d02a961d passed the already-populated in-memory selectedProfile by reference into loadProfile / parseProfile. parseProfile appends to profile.phases rather than replacing, so every brew completion duplicated the phase set. Reset the selectedProfile in place before reload (p = Profile{}), mirroring the pattern ProfileManager::selectProfile already uses. --- src/display/core/Controller.cpp | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/display/core/Controller.cpp b/src/display/core/Controller.cpp index 4f89b0111..2f72da1d7 100644 --- a/src/display/core/Controller.cpp +++ b/src/display/core/Controller.cpp @@ -651,8 +651,18 @@ void Controller::deactivate() { // raise/lowerBrewTarget) revert back to the profile's saved values // after each brew. For permanent changes the user should save via // the Profiles page or the display's Save button. + // + // Reset the profile in place before reload: parseProfile() appends to + // profile.phases rather than replacing, so feeding it the already- + // populated selectedProfile would double the phases on every brew + // (60s utility profile reading 1:00 → 2:00 → 3:00 across runs, and + // the brew loop actually executing the duplicated phases). Mirrors + // the `selectedProfile = Profile{}` pattern that + // ProfileManager::selectProfile already uses for the same reason. if (profileManager) { - profileManager->loadSelectedProfile(profileManager->getSelectedProfile()); + Profile &p = profileManager->getSelectedProfile(); + p = Profile{}; + profileManager->loadSelectedProfile(p); } } else if (lastProcess->getType() == MODE_GRIND) { pluginManager->trigger("controller:grind:end");