diff --git a/src/display/core/Controller.cpp b/src/display/core/Controller.cpp index a8d9a2aa6..2f72da1d7 100644 --- a/src/display/core/Controller.cpp +++ b/src/display/core/Controller.cpp @@ -645,6 +645,25 @@ 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. + // + // 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) { + Profile &p = profileManager->getSelectedProfile(); + p = Profile{}; + profileManager->loadSelectedProfile(p); + } } 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(); diff --git a/web/src/pages/Home/CompactProcessControls.jsx b/web/src/pages/Home/CompactProcessControls.jsx index b0482c744..6eb85711f 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, @@ -32,10 +33,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 +59,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 +150,7 @@ const InfoView = ({ title, hint }) => (
); -const BrewIdleView = ({ s, brewTarget, sendTarget }) => ( +const BrewIdleView = ({ s, brewTarget, sendTarget, send }) => (
( - {s.volumetricAvailable && } + {/* 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. */} +
+ 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). */} + send('req:lower-brew-target')} + onIncrease={() => send('req:raise-brew-target')} + /> +
); @@ -241,7 +295,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 +338,16 @@ 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 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 && !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 4bdbc2397..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); @@ -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,27 @@ const ProcessControls = props => {
- {status.value.currentPressure?.toFixed(1) || 0} /{' '} + {/* 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 + ? 'text-error font-semibold' + : '' + } + > + {status.value.currentPressure?.toFixed(1) || 0} + + {' / '} {status.value.targetPressure?.toFixed(1) || 0} bar
@@ -476,6 +496,81 @@ const ProcessControls = props => { )} {/* Controls for different modes */} + {mode === 1 && !active && !finished && ( +
+ {/* 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. */} +
+
+
+ TEMPERATURE +
+
+ + + +
+ {status.value.targetTemperature || 0}°C +
+ + + +
+
+
+
+ {brewTarget ? 'TARGET WEIGHT' : 'TARGET TIME'} +
+
+ + + +
+ {brewTarget + ? `${(status.value.targetWeight ?? 0).toFixed(0)}g` + : fmtDuration(status.value.brewTargetDuration ?? 0)} +
+ + + +
+
+
+
+ )} {mode === 2 && (
{/* Temperature adjustment controls for steam mode */} 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' > -
+
-
+
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`;