Skip to content

feat(dashboard): Brew-mode quick-adjust steppers + over-pressure alert#712

Open
FR4NKLN wants to merge 6 commits into
jniebuhr:masterfrom
FR4NKLN:feat/dashboard-quick-edit
Open

feat(dashboard): Brew-mode quick-adjust steppers + over-pressure alert#712
FR4NKLN wants to merge 6 commits into
jniebuhr:masterfrom
FR4NKLN:feat/dashboard-quick-edit

Conversation

@FR4NKLN

@FR4NKLN FR4NKLN commented May 26, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a Brew-mode quick-adjust workflow to the web Dashboard plus a visual cue when the boiler is still pressurised before a shot. Behaviour is consistent with the device's on-screen gear-icon BrewScreen Settings panel — no firmware semantics change beyond wiring up two missing WebSocket handlers.

1. Brew-mode +/- steppers (TEMP + WEIGHT/TIME)

When in Brew mode with no process running, the Process Controls card shows a TEMP stepper plus a second slot that morphs based on the firmware status flag bt = isVolumetricAvailable && profile.isVolumetric:

  • bt=true → label TARGET WEIGHT, value Xg, +/ adjusts the profile's volumetric target
  • bt=false → label TARGET TIME, value M:SS, +/ adjusts the profile's phase duration

Both modes call the same req:raise-brew-target / req:lower-brew-target WS messages; Controller::raiseBrewTarget / lowerBrewTarget decide internally whether to adjust the profile's volumetric value or its phase durations based on the same condition. This is the exact logic the device's gear-icon BrewScreen Settings panel already uses — users get consistent affordances across both interfaces, and time-based profiles (or any brew without a connected scale) are no longer second-class citizens in the dashboard.

These are transient "next-brew-only" adjustments, not a profile editor. The firmware reloads the selected profile from disk after each brew completes (Controller::deactivate), so values revert. Permanent edits still go through the Profiles page.

Implemented in both ProcessControls (full layout) and CompactProcessControls (responsive layout, slimmer MiniStepper so two fit side-by-side).

2. Over-pressure red alert (idle only)

In Brew mode, the current pressure value turns text-error (red, semibold) when above 1.0 bar and the brew is idle — i.e. no shot active and no finished view shown. The cue is suppressed during the brew itself and the immediate post-brew finished view, since 8+ bar is the expected state during the shot and pressure takes a moment to bleed off after. The cue returns automatically once the user clears the finished view and the card is back to idle.

Visual purpose: warn the user that the boiler/group is still pressurised from a prior shot or heat-up before starting the next brew, without adding red noise during the periods when high pressure is normal.

Two reasons this matters:

  • Pressure-aware profiles depend on a clean baseline. Adaptive / pressure-driven profiles read current pressure to drive phase transitions, predictive delay, and decline behaviour. Residual pressure above the profile's preinfusion thresholds at process start can skip or misadvance phases, producing a shot that doesn't follow the intended curve.
  • Back-pressurised group heads channel. Common physical cause of blown pucks and uneven extraction, especially first shot of a session or after steaming.

Localised to the current pressure reading only (target + unit stay normal colour), and only triggers in Brew mode (>1 bar is normal in Steam).

3. Bonus fix: potrait typo in responsive variant

While working on the dashboard I noticed Home/index.jsx has potrait:md:hidden and potrait:md:contents. Tailwind silently ignores undefined variants, so the portrait switch between Compact and full layouts never worked — Compact was always shown in portrait orientation, even on tablets at widths where the full layout fits comfortably. Renamed to portrait: so the intended responsive behaviour activates.

Firmware changes (minimal)

  • WebUIPlugin::handleWebSocketData: add req:raise-brew-target / req:lower-brew-target handlers. The web client already sent these via the existing raiseTarget() callback (the Grind +/- buttons use the same pattern) but firmware had no matching handler — they were silently dropped.
  • Controller::deactivate: reload the selected profile from disk on brew end, so stepper adjustments are transient.

Test plan

  • In Brew mode (idle), /+ adjusts TEMP — value updates immediately
  • In Brew mode with BLE scale connected and a volumetric profile → second stepper labelled TARGET WEIGHT, /+ adjusts weight
  • In Brew mode without scale (or with a non-volumetric profile) → second stepper labelled TARGET TIME (M:SS), /+ adjusts duration
  • Disconnect/reconnect the scale mid-session → second stepper morphs between WEIGHT and TIME automatically on the next status tick
  • Start a brew with adjusted values, let it finish — values revert to profile defaults afterward
  • Pressure > 1 bar in Brew mode while idle → current value renders in red
  • Pressure > 1 bar during an active brew or in the finished post-brew view → renders normal colour (red suppressed)
  • Same > 1 bar value in Steam/Water/Standby/Grind → no red regardless of state
  • Open dashboard in portrait orientation on a tablet ≥ 768px wide → full ProcessControls renders (not Compact)
  • Compact view (narrow window / phone) shows the steppers side-by-side via MiniStepper

Summary by CodeRabbit

  • New Features

    • Brew target raise/lower controls now work from the UI.
    • Compact stepper controls for temperature and weight/time in brew idle.
    • Quick brew-mode controls added; time stepper displays M:SS format.
  • Bug Fixes

    • Fixed responsive layout class name typo.
  • Improvements

    • Pressure value shows alert styling when high during idle.
    • Profile values automatically revert to saved settings after each brew.

Review Change Stack

FR4NKLN added 3 commits May 26, 2026 19:35
…e after brew

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.
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.
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 e95b844 "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.
@cla-bot

cla-bot Bot commented May 26, 2026

Copy link
Copy Markdown

We require contributors to sign our Contributor License Agreement, and we don't have yours on file. In order for us to review and merge your code, please contact @jniebuhr (mdwasp) on Discord to get yourself added.

@coderabbitai

coderabbitai Bot commented May 26, 2026

Copy link
Copy Markdown
Contributor

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 28bce9e2-7cce-4ef7-b88c-0a8ff941e18a

📥 Commits

Reviewing files that changed from the base of the PR and between 4c06098 and 1932beb.

📒 Files selected for processing (1)
  • src/display/core/Controller.cpp
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/display/core/Controller.cpp

📝 Walkthrough

Walkthrough

Adds compact and full UI steppers for brew-mode TEMP/WEIGHT/TIME adjustments, routes two new WebSocket brew-target messages to the Controller, reloads the selected profile after a brew completes, and applies conditional pressure error styling and a responsive classname fix.

Changes

Brew-mode Idle Control Flow

Layer / File(s) Summary
Backend profile reload after brew
src/display/core/Controller.cpp
After a brew completes, Controller::deactivate() resets the in-memory selected Profile and calls profileManager->loadSelectedProfile(...) so temporary dashboard/display adjustments revert to saved profile values.
WebSocket message routing for brew-target adjustments
src/display/plugins/WebUIPlugin.cpp
WebSocket handler recognizes req:raise-brew-target and req:lower-brew-target and routes them to Controller brew-target adjustment methods.
Compact UI stepper controls and metric styling
web/src/pages/Home/CompactProcessControls.jsx
Adds MiniStepper, passes a new send prop into BrewIdleView, wires brew idle TEMP and brew-target +/- to req:lower/raise-temp and req:lower/raise-brew-target, and allows Metric to accept currentClassName for conditional pressure styling.
Full UI brew controls and error pressure styling
web/src/pages/Home/ProcessControls.jsx
Imports fmtDuration, unifies / separators, adds a Brew-mode quick-stepper block (TEMP + TARGET WEIGHT/TIME with tooltips), and conditionally applies text-error font-semibold to pressure when brew mode is idle/unfinished and pressure > 1.0.
Utilities and responsive classname fix
web/src/pages/Home/utils.js, web/src/pages/Home/index.jsx
Adds fmtDuration(s) to format seconds as M:SS for time-mode steppers and fixes potrait:*portrait:* breakpoint utility classnames for control container visibility.

Sequence Diagram(s)

sequenceDiagram
  participant WebClient
  participant WebUIPlugin
  participant Controller
  participant ProfileManager
  WebClient->>WebUIPlugin: req:raise-brew-target / req:lower-brew-target
  WebUIPlugin->>Controller: raiseBrewTarget() / lowerBrewTarget()
  Note over Controller: brew completes
  Controller->>Controller: deactivate()
  Controller->>ProfileManager: loadSelectedProfile()
  ProfileManager-->>Controller: persisted profile
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • jniebuhr/gaggimate#512: Touches the same compact dashboard controls and stepper wiring in CompactProcessControls.jsx.

Suggested labels

cla-signed

Poem

🐇 Hops and steppers click with cheer,

Temp and weight now swift and near,
When brew is done the profile returns,
Pressure warns as red light burns,
Portrait fixed — the controls appear!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and specifically summarizes the main changes: adding Brew-mode quick-adjust steppers for temperature and brew target adjustments, plus an over-pressure alert visual indicator.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@FR4NKLN
FR4NKLN marked this pull request as draft May 27, 2026 01:42
…pressure trigger

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.
@cla-bot

cla-bot Bot commented May 27, 2026

Copy link
Copy Markdown

We require contributors to sign our Contributor License Agreement, and we don't have yours on file. In order for us to review and merge your code, please contact @jniebuhr (mdwasp) on Discord to get yourself added.

…ET WEIGHT/TIME)

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.
@cla-bot

cla-bot Bot commented May 27, 2026

Copy link
Copy Markdown

We require contributors to sign our Contributor License Agreement, and we don't have yours on file. In order for us to review and merge your code, please contact @jniebuhr (mdwasp) on Discord to get yourself added.

@FR4NKLN

FR4NKLN commented May 27, 2026

Copy link
Copy Markdown
Contributor Author

Why "TARGET" rather than just "weight" or "time"

The value is the setpoint the firmware is aiming for, not a guarantee of brew outcome. Under common conditions on certain profile shapes, the actual cup weight or brew time can land dramatically far from the value shown. Two concrete cases that come up in normal use:

  • TARGET WEIGHT 28g → actual cup 40g+. Whether the firmware can guard against this depends on how the profile was authored. Profiles imported via the Gaggiuino, Decent TCL, or Meticulous JSON importers all carry a per-phase volumetric overflow cap by convention (Gaggiuino since PR Add advanced profiles #365 / v1.6.0; Decent and Meticulous since PRs fix(profile-import): repair Decent (TCL) profile converter #708 and feat(profile-import): add Meticulous JSON profile importer #709 extended the same pattern). Hand-authored Gaggimate profiles vary — most guard only the final brew phase. On a profile of the latter shape, a low-resistance or channeled puck can dump 25-35g into the cup before the final-phase volumetric guard activates, and the brew sees the cup already past target on entry to the final phase.
  • TARGET TIME 1:40 → brew completes in 0:16. Per-phase exit conditions (pressure >= N, flow >= N, pumped >= N) advance through phases much faster than their declared durations. A 100-second profile can resolve in 15-20 seconds when the pressure curve climbs fast against a free-flowing puck.

type: "standard" profiles (e.g., the bundled 9 Bar Espresso) are the structural exception: Phase::isFinished has an early-return at profile.h:109-111 that makes the volumetric target the only escape (besides a 5-minute safety cap). Standard profiles do stop at the target weight reliably.

Worth flagging for the maintainer concerning uniformity and intention

@FR4NKLN
FR4NKLN marked this pull request as ready for review May 27, 2026 07:22
…o-revert

The auto-revert added in d02a961 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.
@cla-bot

cla-bot Bot commented May 27, 2026

Copy link
Copy Markdown

We require contributors to sign our Contributor License Agreement, and we don't have yours on file. In order for us to review and merge your code, please contact @jniebuhr (mdwasp) on Discord to get yourself added.

@sonarqubecloud

Copy link
Copy Markdown

@FR4NKLN

FR4NKLN commented May 29, 2026

Copy link
Copy Markdown
Contributor Author

@cla-bot check

@cla-bot cla-bot Bot added the cla-signed label May 29, 2026
@cla-bot

cla-bot Bot commented May 29, 2026

Copy link
Copy Markdown

The cla-bot has been summoned, and re-checked this pull request!

FR4NKLN added a commit to FR4NKLN/gaggimate that referenced this pull request May 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant