Surface WAIT_USER and add Confirm to the browser client

Ports show_user_message() from client/brewpi_gui.py: a plain <dialog>
pops with the step's user_message the instant WAIT_USER is newly
entered (tracked via a prevState comparison in onSudChanged()'s State
branch), and sends {"Sud": {"Confirm": true}} on the dialog's close
event - covers both the OK button and Escape dismissal.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0189FkmyJkY77HqsNomr1DwV
This commit is contained in:
2026-06-25 00:21:49 +02:00
co-authored by Claude Sonnet 4.6
parent 83fd16e46c
commit 0e343655b7
5 changed files with 49 additions and 18 deletions
+5 -3
View File
@@ -540,9 +540,11 @@ unresolved - every step still needs `default.step` merged in), and the
`StepPlate`/`_update_step_plates()`/`update_status_step_label()` logic `StepPlate`/`_update_step_plates()`/`update_status_step_label()` logic
behind the Progress tab and status line. New/Load/Save (a plain behind the Progress tab and status line. New/Load/Save (a plain
`<input type="file">`/`FileReader` and a `Blob`/`<a download>` standing in `<input type="file">`/`FileReader` and a `Blob`/`<a download>` standing in
for the desktop app's native file dialogs) and Start/Stop are wired up too for the desktop app's native file dialogs), Start/Stop, and Confirm (a
- see `web/TODO.md` for what's still missing (Pause/Confirm, `WAIT_USER` plain `<dialog>` pops with the step's message on `WAIT_USER` and sends
surfacing, and more). Deliberately deferred rather than stubbed: the Confirm when dismissed) are all wired up too - see `web/TODO.md` for
what's still missing (Pause, and more). Deliberately deferred rather than
stubbed: the
Automatic tab's forecast-vs-actual plot and the live Plot tab's strip Automatic tab's forecast-vs-actual plot and the live Plot tab's strip
charts - both need a charting approach, revisit once Manual + Progress are charts - both need a charting approach, revisit once Manual + Progress are
validated. validated.
+12 -15
View File
@@ -7,22 +7,19 @@ flagged as deferred in the plan, or only noticed once the port was done
and checked against the Qt source side by side. and checked against the Qt source side by side.
- [x] **No way to actually run a brew.** ~~There's no Start/Pause/Stop/ - [x] **No way to actually run a brew.** ~~There's no Start/Pause/Stop/
Confirm anywhere in `web/index.html`~~ Start/Stop are now wired up Confirm anywhere in `web/index.html`~~ Start/Stop/Confirm are now wired
(`btn-sud-start`/`btn-sud-stop` in `app.js`, gated by `updateSudActions()` up (`btn-sud-start`/`btn-sud-stop` in `app.js`, gated by
mirroring `update_sud_actions()`'s enabled/disabled logic). Still `updateSudActions()` mirroring `update_sud_actions()`'s
missing: Pause, and `{"Sud": {"Confirm": true}}` - that one needs enabled/disabled logic; Confirm via the `#sud-message-dialog` below).
`sudUserMessage`/`sudState` (next item) to know *when* it's actually Still missing: Pause - trivial to add the same way as Start/Stop
waiting on one. whenever it's actually needed.
- [ ] **`WAIT_USER` never surfaces to the user.** `sudUserMessage` is - [x] **`WAIT_USER` never surfaces to the user.** `showUserMessage()`
tracked in `app.js` but nothing ever shows it - `show_user_message()` pops `#sud-message-dialog` (a plain `<dialog>`, no library) the instant
(`client/brewpi_gui.py:877-882`) pops a modal the instant `WAIT_USER` is `WAIT_USER` is newly entered (`onSudChanged()`'s `State` branch tracks
newly entered (`prev_state != WAIT_USER`) and sends `Confirm` itself `prevState` for this), and sends `{"Sud": {"Confirm": true}}` on the
when the user dismisses it. Needs the same "newly entered" edge dialog's `close` event - covers both the OK button and Escape, mirrors
detection in `onSudChanged()`'s `State` branch (the `wasRunning` `show_user_message()` (`client/brewpi_gui.py:877-882`).
tracking there is close, but checks running-vs-not, not specifically
the `WAIT_USER` transition) plus a modal (a plain `<dialog>` works,
no library needed) that sends `Confirm` on close.
- [x] **No schedule management.** New/Load/Save are now wired up in - [x] **No schedule management.** New/Load/Save are now wired up in
`app.js`: Load is `<input type="file">` + `FileReader` + `app.js`: Load is `<input type="file">` + `FileReader` +
+16
View File
@@ -9,6 +9,7 @@ const CHANNELS = ['Pot', 'Sensor', 'Heater', 'Stirrer', 'TempCtrl', 'Sud', 'Syst
// "running" - mirrors client/brewpi_gui.py's SUD_RUNNING_STATES. // "running" - mirrors client/brewpi_gui.py's SUD_RUNNING_STATES.
const RUNNING_STATES = new Set(['SudState.RAMPING', 'SudState.HOLDING', 'SudState.WAIT_USER']); const RUNNING_STATES = new Set(['SudState.RAMPING', 'SudState.HOLDING', 'SudState.WAIT_USER']);
const PAUSED_STATE = 'SudState.PAUSED'; const PAUSED_STATE = 'SudState.PAUSED';
const WAIT_USER_STATE = 'SudState.WAIT_USER';
let ws = null; let ws = null;
let connected = false; let connected = false;
@@ -306,6 +307,14 @@ function updateSudActions() {
document.getElementById('btn-sud-stop').disabled = !(running || paused); document.getElementById('btn-sud-stop').disabled = !(running || paused);
} }
// Mirrors client/brewpi_gui.py's show_user_message(): the Sud is already
// blocked in WAIT_USER server-side - dismissing this dialog (OK or Escape,
// both fire the dialog's 'close' event) is what unblocks it.
function showUserMessage(message) {
document.getElementById('sud-message-text').textContent = message;
document.getElementById('sud-message-dialog').showModal();
}
function downloadJson(doc) { function downloadJson(doc) {
const blob = new Blob([JSON.stringify(doc, null, '\t')], {type: 'application/json'}); const blob = new Blob([JSON.stringify(doc, null, '\t')], {type: 'application/json'});
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
@@ -462,6 +471,7 @@ function onSudChanged(msg) {
} }
} else if (key === 'State') { } else if (key === 'State') {
const newState = msg.State; const newState = msg.State;
const prevState = sudState;
const isRunningNow = RUNNING_STATES.has(newState); const isRunningNow = RUNNING_STATES.has(newState);
if (isRunningNow && !wasRunning) { if (isRunningNow && !wasRunning) {
elapsed = 0; elapsed = 0;
@@ -470,6 +480,9 @@ function onSudChanged(msg) {
} }
wasRunning = isRunningNow; wasRunning = isRunningNow;
sudState = newState; sudState = newState;
if (newState === WAIT_USER_STATE && prevState !== WAIT_USER_STATE && sudUserMessage) {
showUserMessage(sudUserMessage);
}
updateStepPlates(); updateStepPlates();
updateSudActions(); updateSudActions();
} else if (key === 'UserMessage') { } else if (key === 'UserMessage') {
@@ -661,6 +674,9 @@ document.getElementById('btn-sud-start').addEventListener('click', () => {
document.getElementById('btn-sud-stop').addEventListener('click', () => { document.getElementById('btn-sud-stop').addEventListener('click', () => {
sendMsg('Sud', {Stop: true}); sendMsg('Sud', {Stop: true});
}); });
document.getElementById('sud-message-dialog').addEventListener('close', () => {
sendMsg('Sud', {Confirm: true});
});
document.querySelectorAll('.tab-btn').forEach((btn) => { document.querySelectorAll('.tab-btn').forEach((btn) => {
btn.addEventListener('click', () => { btn.addEventListener('click', () => {
+7
View File
@@ -102,6 +102,13 @@
</section> </section>
</main> </main>
<dialog id="sud-message-dialog">
<p id="sud-message-text"></p>
<form method="dialog">
<button value="ok">OK</button>
</form>
</dialog>
<footer> <footer>
<div id="sud-status-line"></div> <div id="sud-status-line"></div>
<div id="env-status-line"></div> <div id="env-status-line"></div>
+9
View File
@@ -123,6 +123,15 @@ nav#tabs {
} }
.step-plate .span2 { grid-column: span 2; } .step-plate .span2 { grid-column: span 2; }
#sud-message-dialog {
border: 1px solid #ccc;
border-radius: 4px;
padding: 1em 1.4em;
max-width: 24em;
}
#sud-message-dialog::backdrop { background: rgba(0, 0, 0, 0.3); }
#sud-message-dialog form { margin-top: 1em; text-align: right; }
footer { footer {
position: fixed; position: fixed;
bottom: 0; bottom: 0;