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
446 lines
10 KiB
CSS
446 lines
10 KiB
CSS
:root {
|
|
--bg: #121212;
|
|
--surface: #1e1e1e;
|
|
--surface-2: #2a2a2a;
|
|
--border: #383838;
|
|
--text: #e0e0e0;
|
|
--text-muted: #9e9e9e;
|
|
--accent: #ff7043;
|
|
--green: #4caf50;
|
|
--green-dim: #1b5e20;
|
|
--orange: #ffa726;
|
|
--orange-dim: #6d3b00;
|
|
--red: #f44336;
|
|
--yellow: #ffeb3b;
|
|
--led-off: #444444;
|
|
--led-green: var(--green);
|
|
--led-green-dim: var(--green-dim);
|
|
--led-orange: var(--orange);
|
|
--led-orange-dim: var(--orange-dim);
|
|
}
|
|
|
|
*, *::before, *::after { box-sizing: border-box; }
|
|
|
|
body {
|
|
font-family: 'Segoe UI', system-ui, sans-serif;
|
|
margin: 0;
|
|
padding: 0.5em 1em 4em 1em;
|
|
background: var(--bg);
|
|
color: var(--text);
|
|
}
|
|
|
|
/* --- Inputs & buttons --- */
|
|
|
|
input[type="text"],
|
|
input[type="number"] {
|
|
background: var(--surface-2);
|
|
border: 1px solid var(--border);
|
|
border-radius: 4px;
|
|
color: var(--text);
|
|
padding: 0.3em 0.5em;
|
|
}
|
|
input[type="text"]:focus,
|
|
input[type="number"]:focus {
|
|
outline: none;
|
|
border-color: var(--accent);
|
|
}
|
|
|
|
button {
|
|
background: var(--surface-2);
|
|
border: 1px solid var(--border);
|
|
border-radius: 4px;
|
|
color: var(--text);
|
|
padding: 0.3em 0.8em;
|
|
cursor: pointer;
|
|
transition: background 0.1s, border-color 0.1s;
|
|
}
|
|
button:hover:not(:disabled) {
|
|
background: #333;
|
|
border-color: #555;
|
|
}
|
|
button:disabled { opacity: 0.4; cursor: default; }
|
|
|
|
/* --- Header --- */
|
|
|
|
header#connection-bar {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.4em;
|
|
padding: 0.6em 0;
|
|
border-bottom: 1px solid var(--border);
|
|
}
|
|
#conn-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 1em;
|
|
}
|
|
#file-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.4em;
|
|
}
|
|
#host { width: 18em; }
|
|
|
|
/* --- User-confirm control (replaces a modal OK dialog) --- */
|
|
|
|
#user-confirm-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.6em;
|
|
}
|
|
#user-confirm-text {
|
|
color: var(--yellow);
|
|
font-weight: bold;
|
|
}
|
|
#btn-user-confirm {
|
|
font-weight: bold;
|
|
border: none;
|
|
border-radius: 4px;
|
|
padding: 0.4em 1em;
|
|
cursor: pointer;
|
|
background: var(--surface-2);
|
|
color: var(--text-muted);
|
|
}
|
|
#btn-user-confirm.confirm-pending {
|
|
color: #000;
|
|
cursor: pointer;
|
|
animation: confirm-flash 1s step-start infinite;
|
|
}
|
|
@keyframes confirm-flash {
|
|
0%, 49% { background: var(--surface-2); }
|
|
50%, 100% { background: var(--yellow); }
|
|
}
|
|
|
|
/* Modally disabled while a user-confirm is pending - #user-confirm-row
|
|
(and its Confirm button) stays outside #layout so it's untouched. */
|
|
#layout.modally-disabled {
|
|
opacity: 0.5;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.status-connected { color: var(--green); font-weight: bold; }
|
|
.status-disconnected { color: var(--text-muted); }
|
|
|
|
/* --- Sud controls section --- */
|
|
|
|
#sud-controls {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.4em;
|
|
padding-left: 1em;
|
|
border-left: 1px solid var(--border);
|
|
}
|
|
|
|
/* --- Header countdown --- */
|
|
|
|
#header-countdown {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.05em;
|
|
padding-left: 0.8em;
|
|
border-left: 1px solid var(--border);
|
|
}
|
|
.hdr-cdown-row {
|
|
display: flex;
|
|
align-items: baseline;
|
|
gap: 0.35em;
|
|
}
|
|
.hdr-cdown-lbl {
|
|
font-size: 0.65em;
|
|
color: var(--text-muted);
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.05em;
|
|
width: 2.8em;
|
|
}
|
|
#hdr-step-remaining,
|
|
#hdr-total-remaining,
|
|
#hdr-hold-remaining {
|
|
font-family: 'Courier New', 'Lucida Console', monospace;
|
|
font-weight: bold;
|
|
font-size: 1.35em;
|
|
color: var(--accent);
|
|
}
|
|
|
|
|
|
/* --- Transport buttons (circular, coloured) --- */
|
|
|
|
.transport-btn {
|
|
width: 76px;
|
|
height: 76px;
|
|
border-radius: 50%;
|
|
border: none;
|
|
font-size: 30px;
|
|
cursor: pointer;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
padding: 0;
|
|
transition: filter 0.15s, box-shadow 0.15s;
|
|
flex-shrink: 0;
|
|
}
|
|
.transport-btn:not(:disabled):hover {
|
|
filter: brightness(1.25);
|
|
box-shadow: 0 0 10px rgba(255,255,255,0.18);
|
|
}
|
|
.transport-btn:disabled { opacity: 0.35; cursor: default; }
|
|
|
|
.transport-start { background: var(--green); color: #fff; }
|
|
.transport-pause { background: var(--orange); color: #fff; }
|
|
.transport-stop { background: var(--red); color: #fff; }
|
|
|
|
/* --- LCD panel --- */
|
|
|
|
#lcd-panel {
|
|
display: grid;
|
|
grid-template-columns: 7em 4em 4em auto;
|
|
gap: 0.2em 0.3em;
|
|
font-size: 1.9em;
|
|
}
|
|
.lcd-row { display: contents; }
|
|
.lcd-header { font-weight: bold; color: var(--text-muted); font-size: 0.85em; text-transform: uppercase; letter-spacing: 0.06em; }
|
|
.lcd-label { color: var(--text-muted); font-size: 0.9em; align-self: center; }
|
|
.lcd-unit { color: var(--text-muted); font-size: 0.7em; align-self: center; white-space: nowrap; }
|
|
.lcd {
|
|
font-family: 'Courier New', 'Lucida Console', monospace;
|
|
font-size: 1.1em;
|
|
background: #091209;
|
|
border: 1px solid #2a6e2a;
|
|
padding: 0.15em 0.4em;
|
|
text-align: right;
|
|
color: #7dfa7d;
|
|
text-shadow: 0 0 6px rgba(100,255,100,0.45);
|
|
border-radius: 3px;
|
|
}
|
|
|
|
/* --- 3-column layout --- */
|
|
|
|
#layout {
|
|
display: grid;
|
|
grid-template-columns: 16em auto 1fr;
|
|
gap: 1.5em;
|
|
align-items: start;
|
|
margin-top: 1em;
|
|
transition: grid-template-columns 0.2s ease;
|
|
}
|
|
#layout.manual-collapsed {
|
|
grid-template-columns: 2em auto 1fr;
|
|
}
|
|
|
|
/* --- Manual column collapse --- */
|
|
|
|
#col-manual {
|
|
display: flex;
|
|
flex-direction: column;
|
|
overflow: hidden;
|
|
}
|
|
#btn-manual-toggle {
|
|
align-self: flex-start;
|
|
width: 2em;
|
|
height: 2em;
|
|
padding: 0;
|
|
font-size: 0.9em;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
flex-shrink: 0;
|
|
margin-bottom: 0.4em;
|
|
}
|
|
#layout.manual-collapsed #manual-panels {
|
|
display: none;
|
|
}
|
|
|
|
#col-progress { overflow-y: auto; }
|
|
|
|
/* --- LCD + pot (center column) --- */
|
|
|
|
#sud-status-line, #env-status-line { visibility: hidden; }
|
|
|
|
#lcd-pot-row {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: flex-start;
|
|
gap: 1em;
|
|
visibility: hidden;
|
|
}
|
|
|
|
/* --- Pot visualization --- */
|
|
|
|
#pot-view {
|
|
flex-shrink: 0;
|
|
}
|
|
#pot-svg {
|
|
width: 576px;
|
|
height: auto;
|
|
display: block;
|
|
}
|
|
|
|
/* Grain particle animation */
|
|
@keyframes float-p {
|
|
0%, 100% { transform: translate(0px, 0px); }
|
|
25% { transform: translate(1.5px, -3px); }
|
|
50% { transform: translate(-1px, 2px); }
|
|
75% { transform: translate(2px, -1.5px); }
|
|
}
|
|
.grain-p {
|
|
fill: #c8a060;
|
|
animation: float-p var(--fp-dur, 3s) ease-in-out infinite var(--fp-del, 0s);
|
|
animation-play-state: paused;
|
|
}
|
|
#grain-particles.stirring .grain-p { animation-play-state: running; }
|
|
#grain-particles { transition: opacity 0.5s; }
|
|
|
|
/* Stirrer animation */
|
|
@keyframes stir-rotate {
|
|
to { transform: rotate(360deg); }
|
|
}
|
|
#stirrer-blades {
|
|
transform-box: fill-box;
|
|
transform-origin: 50% 50%;
|
|
animation: stir-rotate var(--stir-duration, 1s) linear infinite;
|
|
animation-play-state: paused;
|
|
}
|
|
#stirrer-blades.spinning {
|
|
animation-play-state: running;
|
|
}
|
|
|
|
/* Fire animations */
|
|
@keyframes flicker-1 {
|
|
0%, 100% { transform: scaleY(1.00) scaleX(1.00); }
|
|
25% { transform: scaleY(1.09) scaleX(0.93); }
|
|
50% { transform: scaleY(0.93) scaleX(1.06); }
|
|
75% { transform: scaleY(1.06) scaleX(0.96); }
|
|
}
|
|
@keyframes flicker-2 {
|
|
0%, 100% { transform: scaleY(0.95) scaleX(1.04); }
|
|
30% { transform: scaleY(1.10) scaleX(0.92); }
|
|
65% { transform: scaleY(0.91) scaleX(1.07); }
|
|
}
|
|
@keyframes flicker-3 {
|
|
0%, 100% { transform: scaleY(1.05) scaleX(0.95); }
|
|
40% { transform: scaleY(0.89) scaleX(1.08); }
|
|
70% { transform: scaleY(1.12) scaleX(0.91); }
|
|
}
|
|
|
|
.flame {
|
|
transform-box: fill-box;
|
|
transform-origin: 50% 100%;
|
|
}
|
|
#flame-outer { animation: flicker-1 0.70s ease-in-out infinite; }
|
|
#flame-mid { animation: flicker-2 0.55s ease-in-out infinite 0.08s; }
|
|
#flame-inner { animation: flicker-3 0.45s ease-in-out infinite 0.15s; }
|
|
|
|
#fire-group {
|
|
opacity: 0;
|
|
transition: opacity 0.4s;
|
|
}
|
|
|
|
/* --- Manual panels --- */
|
|
|
|
.panel {
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
padding: 0.8em 1.1em;
|
|
margin-bottom: 0.8em;
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.45);
|
|
}
|
|
.panel h3 {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
align-items: baseline;
|
|
margin: 0 0 0.5em 0;
|
|
font-size: 0.85em;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.07em;
|
|
color: var(--accent);
|
|
border-bottom: 1px solid var(--border);
|
|
padding-bottom: 0.35em;
|
|
}
|
|
.panel-status {
|
|
text-transform: none;
|
|
letter-spacing: normal;
|
|
font-size: 1rem;
|
|
}
|
|
.panel label {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.2em;
|
|
margin: 0.4em 0;
|
|
}
|
|
.slider-row {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.6em;
|
|
}
|
|
.slider-row input[type="range"] { flex: 1; accent-color: var(--accent); }
|
|
.panel input[type="checkbox"] { accent-color: var(--accent); width: 15px; height: 15px; }
|
|
.panel label.checkbox-label { flex-direction: row; align-items: center; gap: 0.5em; cursor: pointer; }
|
|
.panel input[type="number"] { width: 6em; }
|
|
|
|
#pot-reset-row { display: flex; align-items: center; gap: 0.6em; margin: 0.4em 0; }
|
|
|
|
.device-status-row {
|
|
display: flex;
|
|
align-items: center;
|
|
flex-wrap: wrap;
|
|
gap: 0.6em;
|
|
margin: 0.4em 0;
|
|
font-size: 0.9em;
|
|
}
|
|
.device-status-row button { padding: 0.15em 0.6em; font-size: 0.9em; }
|
|
|
|
/* --- Hidden --- */
|
|
.hidden { display: none !important; }
|
|
|
|
/* --- Progress tab: step plates --- */
|
|
|
|
#step-plates { display: flex; flex-direction: column; gap: 0.5em; }
|
|
|
|
.step-plate {
|
|
background: var(--surface);
|
|
border: 1px solid var(--border);
|
|
border-radius: 6px;
|
|
padding: 0.55em 0.9em;
|
|
display: grid;
|
|
grid-template-columns: 1.4em 1fr auto;
|
|
gap: 0.2em 0.6em;
|
|
align-items: center;
|
|
box-shadow: 0 2px 6px rgba(0,0,0,0.35);
|
|
}
|
|
.step-plate .led {
|
|
width: 14px;
|
|
height: 14px;
|
|
border-radius: 50%;
|
|
background: var(--led-off);
|
|
border: 1px solid #555;
|
|
}
|
|
.step-plate .title {
|
|
font-weight: bold;
|
|
grid-column: 2;
|
|
}
|
|
.step-plate .remaining {
|
|
font-weight: bold;
|
|
text-align: right;
|
|
font-family: 'Courier New', monospace;
|
|
color: #4ade80;
|
|
}
|
|
.step-plate .field {
|
|
color: var(--text-muted);
|
|
font-size: 0.88em;
|
|
}
|
|
.step-plate .span2 { grid-column: span 2; }
|
|
|
|
/* --- Footer --- */
|
|
|
|
footer {
|
|
position: fixed;
|
|
bottom: 0; left: 0; right: 0;
|
|
background: var(--surface);
|
|
border-top: 1px solid var(--border);
|
|
padding: 0.35em 1em;
|
|
display: flex;
|
|
justify-content: space-between;
|
|
font-size: 0.82em;
|
|
color: var(--text-muted);
|
|
}
|