Repositions #user-confirm-row from the header's own row to directly
under #header-countdown's "hold" row, sharing a new #countdown-confirm-
col column so header-countdown's 'hidden' toggle (only shown once a
step is active) doesn't hide the confirm control along with it - a
WAIT_USER confirm can happen before that countdown is visible.
Kept the max-width + wrap safety (now min(60vw, 20em)) so a long
confirmation message still wraps instead of pushing the header sideways
on an iPad's narrower viewport, per the earlier upper-right-corner
placement this replaces.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01M2ierBoxW3v7nUbDw3M2pE
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
Firmware version is already printed to the log at connect() time
(HendiCtrl/Pololu1376) - remove the live GUI/websocket firmware
reporting entirely (Connectable, HeaterHendi/StirrerPololu1376,
Heater/StirrerTask, web GUI) as redundant.
Also move the Heater panel's Closed-loop checkbox into the Controller
panel and rename it "Enable" - the Controller panel now shows a
live Enabled (green) / Disabled (gray) status badge next to its
heading, matching the Connected/Disconnected badges on the Heater/
Stirrer panels.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YaPLuRPpyjWcwhMvCvpHCL
The Connected/Disconnected badge, F/W version text, and both Connect/
Disconnect buttons were crammed into one row, clipping the Disconnect
button off-panel. Move the status badge into the h3 heading itself
(right-aligned) so the row below has room for the F/W text and both
buttons.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YaPLuRPpyjWcwhMvCvpHCL
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
- Power soll label centered below flame area (amber, clears when off)
- Stirrer RPM label next to motor housing (clears when stopped)
- Flame base moved 20 units closer to pot bottom
- Drop Ist/Soll LCD header row to save vertical space
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SH2D4LRuCnhLSzoYerAfJX
- Add viewport meta tag so Safari uses device CSS pixels
- Move LCD units to a dedicated right-hand column; strip them from labels
- Tighten LCD grid: 4-column layout, narrower gap, font-size 1.9em
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SH2D4LRuCnhLSzoYerAfJX
Step plates now show "Ramp h:mm:ss" (ramp time only) during ramp phase
and "Hold h:mm:ss" (hold remaining) during hold phase, both in the same
digit area in green (#4ade80). Inactive steps show planned ramp duration.
Header countdown gains a "hold" row alongside the existing "ramp" (renamed
from "step") and "total" rows. All three use the same h:mm:ss format.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SH2D4LRuCnhLSzoYerAfJX
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
Closed-loop (default, checkbox checked): TC has exclusive control over
the heater; temp-soll and heatrate-soll sliders are active; direct
heater power entry is disabled. TC now stays enabled at server startup
and whenever closed-loop mode is active — SudTask no longer disables it
on IDLE/DONE, so adjusting temp-soll manually with no sud running
immediately drives the heater via the TC.
Open-loop (checkbox unchecked, only selectable when paused/stopped):
TC is disconnected from the heater; only the heater-power slider is
active. Switching back to closed-loop resets power_soll to 0 and
re-enables the TC.
On play, the mode is automatically reset to closed-loop.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SH2D4LRuCnhLSzoYerAfJX
Shows remaining time for the current step and the entire sud in large
accent-coloured monospace text to the right of the transport buttons,
matching the per-step plate colour. Hidden until a step is active.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SH2D4LRuCnhLSzoYerAfJX
- Remove tab system (nav#tabs, tab-btn, tab-content) from HTML/CSS/JS
- Permanent 3-column grid: manual controls (left, 16em) | LCD+pot (center) | progress (right, fills remaining)
- LCD panel stacks above pot SVG in center column, both hidden until connected
- Step plates no longer max-width capped, fill the right column
- Slider labels moved above the slider+readout row via flex-direction: column
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DF16nV2ispNw1C9SbvqbRQ
Drops the tc-enable, heater-activate and stirrer-activate checkboxes
and all associated state, signal connections, initial-sync flags, and
channel-handler logic. Regenerates main_window.py from the updated .ui.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DF16nV2ispNw1C9SbvqbRQ
Replace clip-rect height manipulation with translate-scale-translate SVG
transform on fire-group, pivoting at the fixed base coordinate y=248.
Eliminates base-hopping caused by fill-box bounding-box drift from
individual flame flicker animations. Clip rect is now static (guards
against flicker overshoot above the pot bottom). Minimum visible flame
size floored at the scale corresponding to 1200W.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JLR2133M2PyjiYq584i2G7
Water fill height now reflects water_mass + grain_mass*0.7 (grain
displacement). A small blue tick and label on the left pot wall shows
the utilized volume in litres, hidden when the pot is unconfigured.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JLR2133M2PyjiYq584i2G7
Replace scaleY group transform with a dynamic clipPath that reveals
flame height proportional to heater power. Base at y=256 stays fixed;
only the tips grow with intensity. Includes flicker overshoot buffer.
Fire paths shifted +8px down, viewBox extended to 265.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DGKe8JzTM2Gr1fdx9wYXTi
35 particles distributed across full pot height, clipped to dynamic
water area via water-clip-rect updated alongside water fill level.
Particle opacity transitions on grain_mass changes per step.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DGKe8JzTM2Gr1fdx9wYXTi
- SVG fire group with 3 layered flame paths (red/orange/yellow gradients)
- CSS keyframe flicker animations with different timings per flame layer
- JS controls fire opacity and scaleY proportional to heater power
- Fix buildStep() reading grain_mass/water_mass from nested step.pot
instead of flat step keys, matching the JSON structure
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DGKe8JzTM2Gr1fdx9wYXTi
- Add volumen field to all sud JSON pot entries (30 L)
- Fix parseSudDoc to read from nested doc.pot (was using stale flat keys)
- SVG widget shows transparent rectangular pot with stirrer, thermometer
with live temperature, and water fill level (water_mass/volumen) when
a sud is loaded; no water shown when no sud is loaded
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DGKe8JzTM2Gr1fdx9wYXTi
- Browser: OK button sends Confirm without closing dialog; Escape blocked;
dialog closes when state leaves WAIT_USER (server-driven).
- PyQt: non-blocking QDialog replaces blocking QMessageBox; OK sends Confirm
without closing; dialog closed by server state change or on disconnect.
- ws_client.py: remove deprecated loop= params from websockets.connect() and
asyncio.ensure_future() (same fix already applied to ws_server.py) so the
PyQt client can actually connect with websockets >= 10.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tqxrk8uj4M3w3d3eXm3xK8
Mirrors client/brewpi_gui.py's actionPause: btn-sud-pause sends
{"Sud": {"Pause": true}} and is only enabled while actually running
(not idle, not already paused), matching update_sud_actions()'s
enabled/disabled logic - Start already doubles as Resume while paused,
so no separate Resume control is needed.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0189FkmyJkY77HqsNomr1DwV
New/Load/Save/Start/Stop now live in #sud-controls inside
#connection-bar, visible regardless of which tab is active, instead of
a standalone panel above the tabs. Drops the now-unused .button-row
rule along with it.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0189FkmyJkY77HqsNomr1DwV
Ports show_user_message() from client/brewpi_gui.py: a plain <dialog>
pops with the step's user_message the instant WAIT_USER is newly
entered (tracked via a prevState comparison in onSudChanged()'s State
branch), and sends {"Sud": {"Confirm": true}} on the dialog's close
event - covers both the OK button and Escape dismissal.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0189FkmyJkY77HqsNomr1DwV
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
A new HTML/CSS/JS client, added next to client/brewpi_gui.py rather
than replacing it, speaking the exact same WebSocket pub/sub protocol
- no server-side protocol changes needed. server/brewpi.py gains a
--http-port (default 8080) stdlib http.server.ThreadingHTTPServer,
in its own daemon thread and deliberately decoupled from the existing
asyncio WebSocket server, serving web/ statically.
v1 covers the Manual tab (direct heater/stirrer/controller control)
and the new Progress tab - the two most actionable surfaces, neither
needing a charting library. web/app.js ports the relevant logic from
brewpi_gui.py directly: components/sud.py's _build_step()/
_merge_defaults() for resolving the raw schedule doc, and the
StepPlate/_update_step_plates()/update_status_step_label() logic
behind the Progress tab and status line.
Deliberately deferred rather than stubbed (see web/TODO.md for the
full parity backlog against the Qt GUI): Sud control actions (Start/
Pause/Stop/Confirm), the Automatic tab's forecast plot and the Plot
tab's live strip charts, Sud file management, and auth (matching the
WebSocket server's own existing lack of it).
No new Python dependencies - functools/threading/http.server are all
stdlib.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019qvu5giu7gvRCyEWzf2Vpx