Files
brewpi/web/index.html
T
jensandClaude Sonnet 4.6 b54cda2f97 Browser GUI: swap total/step countdown order; always use h:mm:ss format
Total remaining now appears above step remaining in the header. Both
countdowns and all other duration displays always use h:mm:ss format
regardless of whether hours is zero, so the layout never shifts width.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SH2D4LRuCnhLSzoYerAfJX
2026-06-28 12:00:11 +02:00

282 lines
12 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>BrewPi</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<header id="connection-bar">
<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-new">New</button>
<button id="btn-sud-load">Load&hellip;</button>
<input type="file" id="sud-file-input" accept="application/json" class="hidden">
<button id="btn-sud-save">Save&hellip;</button>
<button id="btn-sud-start" class="transport-btn transport-start" title="Start / Resume">&#9654;</button>
<button id="btn-sud-pause" class="transport-btn transport-pause" title="Pause">&#9646;&#9646;</button>
<button id="btn-sud-stop" class="transport-btn transport-stop" title="Stop">&#9632;</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">step</span><span id="hdr-step-remaining">-:--:--</span></span>
</span>
</span>
</header>
<main id="layout">
<section id="col-manual">
<div class="panel">
<h3>Controller</h3>
<label>Temperature [&deg;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 [&deg;C/min]
<input type="number" id="heatrate-soll" min="0" max="3" step="0.1">
</label>
</div>
<div class="panel">
<h3>Heater</h3>
<label class="checkbox-label">
<input type="checkbox" id="closed-loop" checked> Closed-loop
</label>
<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</h3>
<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 [&deg;C]
<input type="number" id="ambient-temp" step="0.1">
</label>
<div id="pot-reset-row" class="hidden">
<label>Pot [&deg;C]
<input type="number" id="pot-temp" step="0.1">
</label>
<button id="btn-pot-reset">Reset</button>
</div>
</div>
</section>
<div id="col-center">
<div id="lcd-pot-row">
<div id="lcd-panel">
<div class="lcd-row lcd-header">
<div class="lcd-label"></div>
<div>Ist</div>
<div>Soll</div>
</div>
<div class="lcd-row">
<div class="lcd-label">Temperature [&deg;C]</div>
<div class="lcd" id="lcd-temp-ist">--</div>
<div class="lcd" id="lcd-temp-soll">--</div>
</div>
<div class="lcd-row">
<div class="lcd-label">Heat rate [&deg;C/min]</div>
<div class="lcd" id="lcd-rate-ist">--</div>
<div class="lcd" id="lcd-rate-soll">--</div>
</div>
<div class="lcd-row">
<div class="lcd-label">Power [W]</div>
<div class="lcd" id="lcd-power-ist">--</div>
<div class="lcd" id="lcd-power-soll">--</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>
<!-- 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"/>
<!-- 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>
<dialog id="sud-message-dialog">
<p id="sud-message-text"></p>
<form>
<button id="sud-message-ok" type="button">OK</button>
</form>
</dialog>
<footer>
<div id="sud-status-line"></div>
<div id="env-status-line"></div>
</footer>
<script src="app.js"></script>
</body>
</html>