Add Sud New/Load/Save and Start/Stop to the browser client
Ports on_action_sud_new/_save/_load() and on_action_sud_start/_stop() from client/brewpi_gui.py: Load/Save stand in for the native file dialogs with <input type="file">/FileReader and a Blob/<a download>, New sends the same hardcoded empty-doc Load, and Start/Stop are gated by updateSudActions() mirroring update_sud_actions()'s enabled/disabled logic. Also fixes the status line's step number being one below what the Progress tab's plates show for the same step (same bug just fixed in the desktop client). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0189FkmyJkY77HqsNomr1DwV
This commit is contained in:
+82
-2
@@ -8,6 +8,7 @@ const CHANNELS = ['Pot', 'Sensor', 'Heater', 'Stirrer', 'TempCtrl', 'Sud', 'Syst
|
||||
// str(SudState.X) values (as sent in the 'State' message) that count as
|
||||
// "running" - mirrors client/brewpi_gui.py's SUD_RUNNING_STATES.
|
||||
const RUNNING_STATES = new Set(['SudState.RAMPING', 'SudState.HOLDING', 'SudState.WAIT_USER']);
|
||||
const PAUSED_STATE = 'SudState.PAUSED';
|
||||
|
||||
let ws = null;
|
||||
let connected = false;
|
||||
@@ -62,6 +63,12 @@ let forecastFinished = false;
|
||||
let forecastFinalT = null;
|
||||
let energyByStep = {};
|
||||
let energyCurrent = 0;
|
||||
// Set right before sending {Sud: {Save: true}} - the next 'Json' push is
|
||||
// then this request's reply (not a schedule update to render), so it
|
||||
// triggers a file download instead - mirrors client/brewpi_gui.py's
|
||||
// sud_save_path flag (on_action_sud_save()/on_sud_changed()'s 'Json'
|
||||
// branch).
|
||||
let sudSavePending = false;
|
||||
|
||||
let stepPlates = [];
|
||||
let blinkOn = true;
|
||||
@@ -265,7 +272,11 @@ function updateStatusLine() {
|
||||
let text = '';
|
||||
let grain = sudGrainMass, water = sudWaterMass;
|
||||
if (sudStepDescr) {
|
||||
text = `Step ${sudStepIndex}: ${sudStepDescr}`;
|
||||
// +1: sudStepIndex is the 0-based index Sud.index uses, but the
|
||||
// Progress tab's step-plates (createStepPlate()) label 1-based -
|
||||
// match it here, same fix as client/brewpi_gui.py's
|
||||
// update_status_step_label().
|
||||
text = `Step ${sudStepIndex + 1}: ${sudStepDescr}`;
|
||||
if (sudStepType === 'hold') {
|
||||
const remaining = Math.max(sudHoldRemaining, 0);
|
||||
text += ` (${Math.floor(remaining / 60)}:${String(Math.floor(remaining % 60)).padStart(2, '0')} remaining)`;
|
||||
@@ -285,6 +296,26 @@ function updateStatusLine() {
|
||||
el.textContent = `${text} ${elapsedText} ${energyText}`;
|
||||
}
|
||||
|
||||
// Mirrors client/brewpi_gui.py's update_sud_actions(): Start also doubles
|
||||
// as Resume while paused; Stop can still abort a paused run.
|
||||
function updateSudActions() {
|
||||
const canRun = connected && !sudEmpty;
|
||||
const running = canRun && RUNNING_STATES.has(sudState);
|
||||
const paused = canRun && sudState === PAUSED_STATE;
|
||||
document.getElementById('btn-sud-start').disabled = !(canRun && !running);
|
||||
document.getElementById('btn-sud-stop').disabled = !(running || paused);
|
||||
}
|
||||
|
||||
function downloadJson(doc) {
|
||||
const blob = new Blob([JSON.stringify(doc, null, '\t')], {type: 'application/json'});
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${doc.Name || 'sud'}.json`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
function updateEnvStatusLine() {
|
||||
const el = document.getElementById('env-status-line');
|
||||
const ambient = ambientTemp !== null ? `${ambientTemp.toFixed(1)}°C` : '-';
|
||||
@@ -320,6 +351,7 @@ function updateSudForecast(doc) {
|
||||
}
|
||||
rebuildStepPlates();
|
||||
updateStatusLine();
|
||||
updateSudActions();
|
||||
document.title = sudName ? `BrewPi - ${sudName}` : 'BrewPi';
|
||||
}
|
||||
|
||||
@@ -419,7 +451,15 @@ function onTempCtrlChanged(msg) {
|
||||
function onSudChanged(msg) {
|
||||
for (const key of Object.keys(msg)) {
|
||||
if (key === 'Json') {
|
||||
updateSudForecast(msg.Json);
|
||||
// A standalone reply to a just-sent {Save: true} (see
|
||||
// btn-sud-save's handler below) - not a schedule update to
|
||||
// render, save it to disk instead.
|
||||
if (sudSavePending) {
|
||||
sudSavePending = false;
|
||||
downloadJson(msg.Json);
|
||||
} else {
|
||||
updateSudForecast(msg.Json);
|
||||
}
|
||||
} else if (key === 'State') {
|
||||
const newState = msg.State;
|
||||
const isRunningNow = RUNNING_STATES.has(newState);
|
||||
@@ -431,6 +471,7 @@ function onSudChanged(msg) {
|
||||
wasRunning = isRunningNow;
|
||||
sudState = newState;
|
||||
updateStepPlates();
|
||||
updateSudActions();
|
||||
} else if (key === 'UserMessage') {
|
||||
sudUserMessage = msg.UserMessage;
|
||||
} else if (key === 'Step') {
|
||||
@@ -507,6 +548,7 @@ function setConnected(isConnected) {
|
||||
const statusEl = document.getElementById('conn-status');
|
||||
statusEl.textContent = isConnected ? 'Connected' : 'Disconnected';
|
||||
statusEl.className = isConnected ? 'status-connected' : 'status-disconnected';
|
||||
updateSudActions();
|
||||
}
|
||||
|
||||
function connect() {
|
||||
@@ -584,6 +626,42 @@ document.getElementById('btn-pot-reset').addEventListener('click', () => {
|
||||
sendMsg('Pot', {Reset: Number(document.getElementById('pot-temp').value)});
|
||||
});
|
||||
|
||||
// New is the same hardcoded empty-doc Load the desktop GUI's
|
||||
// on_action_sud_new() sends - the server refuses it (like any Load) while a
|
||||
// run is in progress, surfacing an Error rather than guessing that here.
|
||||
document.getElementById('btn-sud-new').addEventListener('click', () => {
|
||||
sendMsg('Sud', {Load: {Name: '', Description: '', pot_mass: 0, pot_material: null, steps: []}});
|
||||
});
|
||||
document.getElementById('btn-sud-load').addEventListener('click', () => {
|
||||
document.getElementById('sud-file-input').click();
|
||||
});
|
||||
document.getElementById('sud-file-input').addEventListener('change', (e) => {
|
||||
const file = e.target.files[0];
|
||||
e.target.value = '';
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
try {
|
||||
sendMsg('Sud', {Load: JSON.parse(reader.result)});
|
||||
} catch (err) {
|
||||
alert(`Not a valid Sud file: ${err.message}`);
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
});
|
||||
document.getElementById('btn-sud-save').addEventListener('click', () => {
|
||||
sudSavePending = true;
|
||||
sendMsg('Sud', {Save: true});
|
||||
});
|
||||
document.getElementById('btn-sud-start').addEventListener('click', () => {
|
||||
sendMsg('Sud', {Start: true});
|
||||
});
|
||||
document.getElementById('btn-sud-stop').addEventListener('click', () => {
|
||||
sendMsg('Sud', {Stop: true});
|
||||
});
|
||||
|
||||
document.querySelectorAll('.tab-btn').forEach((btn) => {
|
||||
btn.addEventListener('click', () => {
|
||||
document.querySelectorAll('.tab-btn').forEach((b) => b.classList.remove('active'));
|
||||
@@ -597,3 +675,5 @@ setInterval(() => {
|
||||
blinkOn = !blinkOn;
|
||||
stepPlates.forEach(applyLedColor);
|
||||
}, 500);
|
||||
|
||||
updateSudActions();
|
||||
|
||||
Reference in New Issue
Block a user