Both clients used a modal OK dialog for a Sud step's WAIT_USER confirmation (e.g. "Bitte Malz einfuellen und bestaetigen"). Replaced with a persistent "Confirm" control in the top toolbar/header instead: flashes yellow with the step's message while pending, and modally disables the rest of the GUI (main content plus Start/Pause/Stop/ New/Save/Load, and Connect) until it's clicked - forcing the pending step to be dealt with first. Disabled and blank otherwise. client/brewpi_gui.py: set_user_confirm_pending() drives a QTimer blink (same pattern as the existing "Cool down" indicator) and disables centralwidget + the relevant toolbar QActions individually, so the new button (a toolbar sibling) stays enabled; Start/Pause/Stop are restored via update_sud_actions() rather than a blanket re-enable. web/app.js: setUserConfirmPending() toggles a 'confirm-pending' class (CSS @keyframes flash, matching the file's existing animation-by-class convention) and a 'modally-disabled' class on #layout; also wired into ws.onclose so a dropped connection doesn't leave the page stuck modally disabled on a confirm that'll never resolve. Also fixes a message-ordering race in both clients (found testing the web version on an iPad, connecting fresh while a step was already WAIT_USER): a step's UserMessage is set once when the step starts, well before State later flips to WAIT_USER, so an already-connected client sees them at genuinely different times - but a client connecting fresh gets both bundled into one replayed snapshot, where State's key can sort before UserMessage's. Processing State first checked the pending guard before that same message had set the text, silently failing it and leaving the button looking merely disabled. Both onSudChanged()/ on_sud_changed() now force UserMessage (and, in the web client, Json) to process before State/Step regardless of the bundled dict's key order, same pattern already used for the pre-existing Json-before-Step case. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M2ierBoxW3v7nUbDw3M2pE
301 lines
14 KiB
HTML
301 lines
14 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>BrewPi</title>
|
|
<link rel="stylesheet" href="style.css">
|
|
</head>
|
|
<body>
|
|
|
|
<header id="connection-bar">
|
|
<div id="conn-row">
|
|
<label>Host <input id="host" type="text"></label>
|
|
<button id="btn-connect">Connect</button>
|
|
<span id="conn-status" class="status-disconnected">Disconnected</span>
|
|
<span id="sud-controls">
|
|
<button id="btn-sud-start" class="transport-btn transport-start" title="Start / Resume">▶</button>
|
|
<button id="btn-sud-pause" class="transport-btn transport-pause" title="Pause">▮▮</button>
|
|
<button id="btn-sud-stop" class="transport-btn transport-stop" title="Stop">■</button>
|
|
<span id="header-countdown" class="hidden">
|
|
<span class="hdr-cdown-row"><span class="hdr-cdown-lbl">total</span><span id="hdr-total-remaining">-:--:--</span></span>
|
|
<span class="hdr-cdown-row"><span class="hdr-cdown-lbl">ramp</span><span id="hdr-step-remaining">-:--:--</span></span>
|
|
<span class="hdr-cdown-row"><span class="hdr-cdown-lbl">hold</span><span id="hdr-hold-remaining">-:--:--</span></span>
|
|
</span>
|
|
</span>
|
|
</div>
|
|
<div id="file-row">
|
|
<button id="btn-sud-new">New</button>
|
|
<button id="btn-sud-load">Load…</button>
|
|
<input type="file" id="sud-file-input" accept="application/json" class="hidden">
|
|
<button id="btn-sud-save">Save…</button>
|
|
</div>
|
|
<div id="user-confirm-row">
|
|
<span id="user-confirm-text"></span>
|
|
<button id="btn-user-confirm" type="button" disabled>Confirm</button>
|
|
</div>
|
|
</header>
|
|
|
|
<main id="layout">
|
|
|
|
<section id="col-manual">
|
|
<button id="btn-manual-toggle" title="Collapse manual panel">◀</button>
|
|
<div id="manual-panels">
|
|
<div class="panel">
|
|
<h3>Controller <span id="closed-loop-status" class="panel-status status-connected">Enabled</span></h3>
|
|
<label class="checkbox-label">
|
|
<input type="checkbox" id="closed-loop" checked> Enable
|
|
</label>
|
|
<label>Temperature [°C]
|
|
<div class="slider-row">
|
|
<input type="range" id="temp-soll" min="0" max="100" step="1">
|
|
<span id="temp-soll-readout">--</span>
|
|
</div>
|
|
</label>
|
|
<label>Heat rate [°C/min]
|
|
<input type="number" id="heatrate-soll" min="0" max="3" step="0.1">
|
|
</label>
|
|
</div>
|
|
<div class="panel">
|
|
<h3>Heater <span id="heater-status" class="panel-status status-disconnected">Disconnected</span></h3>
|
|
<div class="device-status-row">
|
|
<button id="btn-heater-connect">Connect</button>
|
|
<button id="btn-heater-disconnect" disabled>Disconnect</button>
|
|
</div>
|
|
<label>Power [W]
|
|
<div class="slider-row">
|
|
<input type="range" id="heater-power" min="0" max="100" step="1">
|
|
<span id="heater-power-readout">--</span>
|
|
</div>
|
|
</label>
|
|
</div>
|
|
<div class="panel">
|
|
<h3>Stirrer <span id="stirrer-status" class="panel-status status-disconnected">Disconnected</span></h3>
|
|
<div class="device-status-row">
|
|
<button id="btn-stirrer-connect">Connect</button>
|
|
<button id="btn-stirrer-disconnect" disabled>Disconnect</button>
|
|
</div>
|
|
<label>Speed [%]
|
|
<div class="slider-row">
|
|
<input type="range" id="stirrer-speed" min="0" max="100" step="1">
|
|
<span id="stirrer-speed-readout">--</span>
|
|
</div>
|
|
</label>
|
|
</div>
|
|
<div class="panel">
|
|
<h3>Environment</h3>
|
|
<label>Ambient [°C]
|
|
<input type="number" id="ambient-temp" step="0.1">
|
|
</label>
|
|
<div id="pot-reset-row" class="hidden">
|
|
<label>Pot [°C]
|
|
<input type="number" id="pot-temp" step="0.1">
|
|
</label>
|
|
<button id="btn-pot-reset">Reset</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<div id="col-center">
|
|
<div id="lcd-pot-row">
|
|
<div id="lcd-panel">
|
|
<div class="lcd-row">
|
|
<div class="lcd-label">Temperature</div>
|
|
<div class="lcd" id="lcd-temp-ist">--</div>
|
|
<div class="lcd" id="lcd-temp-soll">--</div>
|
|
<div class="lcd-unit">°C</div>
|
|
</div>
|
|
<div class="lcd-row">
|
|
<div class="lcd-label">Heat rate</div>
|
|
<div class="lcd" id="lcd-rate-ist">--</div>
|
|
<div class="lcd" id="lcd-rate-soll">--</div>
|
|
<div class="lcd-unit">°C/min</div>
|
|
</div>
|
|
<div class="lcd-row">
|
|
<div class="lcd-label">Power</div>
|
|
<div class="lcd" id="lcd-power-ist">--</div>
|
|
<div class="lcd" id="lcd-power-soll">--</div>
|
|
<div class="lcd-unit">W</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="pot-view">
|
|
<svg id="pot-svg" viewBox="0 0 290 255" xmlns="http://www.w3.org/2000/svg">
|
|
<defs>
|
|
<clipPath id="pot-clip">
|
|
<rect x="63" y="30" width="114" height="142"/>
|
|
</clipPath>
|
|
<clipPath id="tube-clip">
|
|
<rect x="215" y="28" width="8" height="146"/>
|
|
</clipPath>
|
|
<clipPath id="water-clip">
|
|
<rect id="water-clip-rect" x="63" y="172" width="114" height="0"/>
|
|
</clipPath>
|
|
<clipPath id="fire-clip">
|
|
<rect x="55" y="175" width="140" height="78"/>
|
|
</clipPath>
|
|
<!-- Flame gradients: solid at base (y=1), transparent at tip (y=0) -->
|
|
<linearGradient id="fg-red" x1="0" y1="1" x2="0" y2="0">
|
|
<stop offset="0%" stop-color="#ff2200" stop-opacity="0.95"/>
|
|
<stop offset="100%" stop-color="#ff5500" stop-opacity="0"/>
|
|
</linearGradient>
|
|
<linearGradient id="fg-orange" x1="0" y1="1" x2="0" y2="0">
|
|
<stop offset="0%" stop-color="#ff7700" stop-opacity="0.9"/>
|
|
<stop offset="100%" stop-color="#ffaa00" stop-opacity="0"/>
|
|
</linearGradient>
|
|
<linearGradient id="fg-yellow" x1="0" y1="1" x2="0" y2="0">
|
|
<stop offset="0%" stop-color="#ffe000" stop-opacity="0.85"/>
|
|
<stop offset="100%" stop-color="#ffffff" stop-opacity="0"/>
|
|
</linearGradient>
|
|
</defs>
|
|
|
|
<!-- Fire (shown when heater is on, JS controls scale+opacity) -->
|
|
<g id="fire-group" clip-path="url(#fire-clip)">
|
|
<!-- Outer red flame -->
|
|
<path id="flame-outer" class="flame"
|
|
d="M76,248 C79,215 92,192 101,208 C106,192 113,177 120,180 C127,177 134,192 139,208 C148,192 161,215 164,248 Z"
|
|
fill="url(#fg-red)"/>
|
|
<!-- Middle orange flame -->
|
|
<path id="flame-mid" class="flame"
|
|
d="M88,248 C91,222 101,204 109,217 C113,204 118,191 122,195 C126,191 131,204 135,217 C143,204 153,222 156,248 Z"
|
|
fill="url(#fg-orange)"/>
|
|
<!-- Inner yellow flame -->
|
|
<path id="flame-inner" class="flame"
|
|
d="M100,248 C102,230 110,215 116,224 C118,215 121,205 124,208 C127,205 130,215 132,224 C138,215 147,230 148,248 Z"
|
|
fill="url(#fg-yellow)"/>
|
|
</g>
|
|
|
|
<!-- Power soll label (below flame area, updated by JS) -->
|
|
<text id="svg-power-soll" x="120" y="244" font-size="10"
|
|
fill="#ffe0b2" text-anchor="middle" dominant-baseline="auto"></text>
|
|
|
|
<!-- Water fill (behind pot walls) -->
|
|
<rect id="water-fill" x="63" y="172" width="114" height="0"
|
|
fill="#1a4a72" clip-path="url(#pot-clip)"/>
|
|
<path id="water-wave" d=""
|
|
fill="none" stroke="#4a9ed8" stroke-width="2"
|
|
clip-path="url(#pot-clip)"/>
|
|
|
|
<!-- Grain particles (shown when grain_mass > 0, clipped to water area) -->
|
|
<g id="grain-particles" clip-path="url(#water-clip)" opacity="0">
|
|
<circle class="grain-p" cx="80" cy="165" r="2.5" style="--fp-dur:3.2s;--fp-del:-0.5s"/>
|
|
<circle class="grain-p" cx="155" cy="158" r="2" style="--fp-dur:4.0s;--fp-del:-1.2s"/>
|
|
<circle class="grain-p" cx="105" cy="150" r="2.5" style="--fp-dur:2.8s;--fp-del:-2.1s"/>
|
|
<circle class="grain-p" cx="140" cy="143" r="2" style="--fp-dur:3.6s;--fp-del:-0.8s"/>
|
|
<circle class="grain-p" cx="75" cy="135" r="2.5" style="--fp-dur:4.2s;--fp-del:-1.5s"/>
|
|
<circle class="grain-p" cx="125" cy="125" r="2" style="--fp-dur:3.0s;--fp-del:-2.8s"/>
|
|
<circle class="grain-p" cx="90" cy="115" r="2.5" style="--fp-dur:3.8s;--fp-del:-0.3s"/>
|
|
<circle class="grain-p" cx="160" cy="105" r="2" style="--fp-dur:2.6s;--fp-del:-1.9s"/>
|
|
<circle class="grain-p" cx="110" cy="90" r="2.5" style="--fp-dur:3.4s;--fp-del:-1.1s"/>
|
|
<circle class="grain-p" cx="82" cy="75" r="2" style="--fp-dur:4.1s;--fp-del:-2.4s"/>
|
|
<circle class="grain-p" cx="148" cy="68" r="2.5" style="--fp-dur:2.9s;--fp-del:-0.7s"/>
|
|
<circle class="grain-p" cx="118" cy="55" r="2" style="--fp-dur:3.5s;--fp-del:-3.0s"/>
|
|
<circle class="grain-p" cx="72" cy="48" r="2.5" style="--fp-dur:4.3s;--fp-del:-1.6s"/>
|
|
<circle class="grain-p" cx="163" cy="40" r="2" style="--fp-dur:3.1s;--fp-del:-2.2s"/>
|
|
<circle class="grain-p" cx="118" cy="168" r="2" style="--fp-dur:3.7s;--fp-del:-0.9s"/>
|
|
<circle class="grain-p" cx="170" cy="162" r="2.5" style="--fp-dur:2.7s;--fp-del:-2.5s"/>
|
|
<circle class="grain-p" cx="68" cy="155" r="2" style="--fp-dur:4.4s;--fp-del:-1.0s"/>
|
|
<circle class="grain-p" cx="133" cy="148" r="2.5" style="--fp-dur:3.3s;--fp-del:-2.9s"/>
|
|
<circle class="grain-p" cx="95" cy="140" r="2" style="--fp-dur:2.5s;--fp-del:-0.4s"/>
|
|
<circle class="grain-p" cx="165" cy="132" r="2.5" style="--fp-dur:3.9s;--fp-del:-1.7s"/>
|
|
<circle class="grain-p" cx="78" cy="122" r="2" style="--fp-dur:4.5s;--fp-del:-2.3s"/>
|
|
<circle class="grain-p" cx="145" cy="112" r="2.5" style="--fp-dur:3.0s;--fp-del:-0.6s"/>
|
|
<circle class="grain-p" cx="100" cy="100" r="2" style="--fp-dur:2.7s;--fp-del:-1.4s"/>
|
|
<circle class="grain-p" cx="170" cy="88" r="2.5" style="--fp-dur:4.2s;--fp-del:-2.7s"/>
|
|
<circle class="grain-p" cx="68" cy="80" r="2" style="--fp-dur:3.6s;--fp-del:-0.2s"/>
|
|
<circle class="grain-p" cx="133" cy="70" r="2.5" style="--fp-dur:2.8s;--fp-del:-1.8s"/>
|
|
<circle class="grain-p" cx="95" cy="60" r="2" style="--fp-dur:4.0s;--fp-del:-2.6s"/>
|
|
<circle class="grain-p" cx="165" cy="52" r="2.5" style="--fp-dur:3.2s;--fp-del:-1.3s"/>
|
|
<circle class="grain-p" cx="78" cy="42" r="2" style="--fp-dur:2.6s;--fp-del:-3.1s"/>
|
|
<circle class="grain-p" cx="145" cy="35" r="2.5" style="--fp-dur:3.8s;--fp-del:-0.8s"/>
|
|
<circle class="grain-p" cx="112" cy="130" r="2" style="--fp-dur:4.1s;--fp-del:-2.0s"/>
|
|
<circle class="grain-p" cx="88" cy="97" r="2.5" style="--fp-dur:3.4s;--fp-del:-1.5s"/>
|
|
<circle class="grain-p" cx="155" cy="78" r="2" style="--fp-dur:2.9s;--fp-del:-0.1s"/>
|
|
<circle class="grain-p" cx="70" cy="62" r="2.5" style="--fp-dur:4.3s;--fp-del:-2.4s"/>
|
|
<circle class="grain-p" cx="138" cy="45" r="2" style="--fp-dur:3.5s;--fp-del:-1.0s"/>
|
|
</g>
|
|
|
|
<!-- Pot outline (transparent walls) -->
|
|
<line x1="60" y1="28" x2="60" y2="174" stroke="#bbb" stroke-width="4" stroke-linecap="round"/>
|
|
<line x1="180" y1="28" x2="180" y2="174" stroke="#bbb" stroke-width="4" stroke-linecap="round"/>
|
|
<line x1="60" y1="174" x2="180" y2="174" stroke="#bbb" stroke-width="4" stroke-linecap="round"/>
|
|
<line x1="60" y1="28" x2="180" y2="28" stroke="#bbb" stroke-width="4" stroke-linecap="round"/>
|
|
|
|
<!-- Utilized volume level indicator (JS-positioned) -->
|
|
<line id="level-tick" x1="52" y1="172" x2="60" y2="172"
|
|
stroke="#4a9ed8" stroke-width="1.5" stroke-linecap="round" style="display:none"/>
|
|
<text id="level-label" x="50" y="172" font-size="8" fill="#4a9ed8"
|
|
dominant-baseline="middle" text-anchor="end" style="display:none">0.0 L</text>
|
|
|
|
<!-- Stirrer blades (inside pot) -->
|
|
<g id="stirrer-blades" clip-path="url(#pot-clip)">
|
|
<rect x="87" y="133" width="31" height="5" fill="#666" rx="1"/>
|
|
<rect x="122" y="133" width="31" height="5" fill="#666" rx="1"/>
|
|
</g>
|
|
|
|
<!-- Lid -->
|
|
<rect x="56" y="20" width="128" height="10" fill="#444" rx="2"/>
|
|
<rect x="56" y="20" width="128" height="3" fill="#555"/>
|
|
|
|
<!-- Stirrer shaft (over lid) -->
|
|
<rect x="118" y="2" width="4" height="134" fill="#777"/>
|
|
|
|
<!-- Motor housing -->
|
|
<rect x="106" y="2" width="28" height="20" fill="#333" rx="3"/>
|
|
<rect x="110" y="6" width="10" height="8" fill="#4a4a4a" rx="1"/>
|
|
<circle cx="129" cy="11" r="3" fill="#cc2222"/>
|
|
<!-- Stirrer RPM label (updated by JS) -->
|
|
<text id="svg-stirrer-rpm" x="137" y="12" font-size="9"
|
|
fill="#9e9e9e" text-anchor="start" dominant-baseline="middle"></text>
|
|
|
|
<!-- Thermometer tube -->
|
|
<rect x="212" y="28" width="14" height="146" rx="7"
|
|
fill="#1e1e1e" stroke="#aaa" stroke-width="1.5"/>
|
|
<!-- Thermometer bulb -->
|
|
<circle cx="219" cy="178" r="13" fill="#1e1e1e" stroke="#aaa" stroke-width="1.5"/>
|
|
<!-- Mercury: bulb always red -->
|
|
<circle cx="219" cy="178" r="10" fill="#e0190a"/>
|
|
<!-- Mercury column (JS sets y and height) -->
|
|
<rect id="thermo-mercury" x="215" y="174" width="8" height="0"
|
|
fill="#e0190a" clip-path="url(#tube-clip)"/>
|
|
|
|
<!-- Soll temperature marker (left side, orange, JS-positioned) -->
|
|
<line id="thermo-soll-tick" x1="203" y1="174" x2="212" y2="174"
|
|
stroke="#ff7043" stroke-width="2" stroke-linecap="round"/>
|
|
<text id="thermo-soll-label" x="201" y="174" font-size="9"
|
|
fill="#ff7043" text-anchor="end" dominant-baseline="middle"></text>
|
|
|
|
<!-- Scale ticks -->
|
|
<line x1="227" y1="28" x2="236" y2="28" stroke="#9e9e9e" stroke-width="1.5"/>
|
|
<text x="239" y="32" font-size="9" fill="#9e9e9e">100</text>
|
|
<line x1="227" y1="65" x2="232" y2="65" stroke="#9e9e9e" stroke-width="1"/>
|
|
<line x1="227" y1="101" x2="236" y2="101" stroke="#9e9e9e" stroke-width="1.5"/>
|
|
<text x="239" y="105" font-size="9" fill="#9e9e9e">50</text>
|
|
<line x1="227" y1="137" x2="232" y2="137" stroke="#9e9e9e" stroke-width="1"/>
|
|
<line x1="227" y1="174" x2="236" y2="174" stroke="#9e9e9e" stroke-width="1.5"/>
|
|
<text x="239" y="178" font-size="9" fill="#9e9e9e">0 °C</text>
|
|
|
|
<!-- Current temperature label -->
|
|
<text id="thermo-temp-label" x="219" y="201" font-size="10"
|
|
fill="#e0e0e0" text-anchor="middle">--</text>
|
|
</svg>
|
|
</div>
|
|
</div><!-- #lcd-pot-row -->
|
|
</div><!-- #col-center -->
|
|
|
|
<section id="col-progress">
|
|
<div id="step-plates"></div>
|
|
</section>
|
|
|
|
</main>
|
|
|
|
<footer>
|
|
<div id="sud-status-line"></div>
|
|
<div id="env-status-line"></div>
|
|
</footer>
|
|
|
|
<script src="app.js"></script>
|
|
</body>
|
|
</html>
|