Heater and Stirrer can be real serial hardware (hendi, Pololu1376), but there was no connection concept at all - the constructors opened the port and crashed the whole server if the device was missing, with no way to see connection status or firmware version and no way to reconnect without a restart. Adds an observable Connectable mixin (components/connectable.py) shared by AHeater/AStirrer; real devices defer opening the serial port to an explicit connect(), auto-connect on server startup, and surface Connected/FirmwareVersion/Simulated plus manual Connect/Disconnect over the web GUI. Heating/stirring and Sud Start are all gated on connection state, and a disconnect mid-brew force-stops the run via the same path as a manual Stop. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YaPLuRPpyjWcwhMvCvpHCL
414 lines
9.8 KiB
CSS
414 lines
9.8 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;
|
|
--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; }
|
|
|
|
.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 {
|
|
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 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;
|
|
gap: 0.6em;
|
|
margin: 0.4em 0;
|
|
font-size: 0.9em;
|
|
}
|
|
.device-status-row button { padding: 0.15em 0.6em; font-size: 0.9em; }
|
|
.device-firmware { color: var(--text-muted); }
|
|
|
|
/* --- 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; }
|
|
|
|
/* --- Message dialog --- */
|
|
|
|
#sud-message-dialog {
|
|
background: var(--surface);
|
|
color: var(--text);
|
|
border: 1px solid var(--border);
|
|
border-radius: 8px;
|
|
padding: 1.2em 1.6em;
|
|
max-width: 24em;
|
|
box-shadow: 0 8px 32px rgba(0,0,0,0.7);
|
|
}
|
|
#sud-message-dialog::backdrop { background: rgba(0,0,0,0.65); }
|
|
#sud-message-dialog form { margin-top: 1em; text-align: right; }
|
|
|
|
/* --- 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);
|
|
}
|