Each connection used to run its own handler_send loop consuming from
the shared dispatcher queue. With zero clients connected (e.g. an
iPad that silently drops on sleep), nothing drained the queue, so
state updates piled up unbounded and got replayed as a burst on
reconnect. Now one pump task drains the queue continuously regardless
of connection count, and reconnecting clients resync from
global_state instead of a backlog.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TvgC7oy9MxaA4ZQxXqkNdS
Catch ConnectionClosed in User.send() and handler_recv() so that an
unclean client disconnect no longer produces "Task exception was never
retrieved" / "connection handler failed" log noise.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SH2D4LRuCnhLSzoYerAfJX
- 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
Both handler loops caught all exceptions, printed e, and broke — hiding
errors entirely. Let exceptions propagate naturally; the parent handler's
FIRST_COMPLETED logic already handles task cleanup on disconnect.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tqxrk8uj4M3w3d3eXm3xK8
MessageDispatcherSync gains an optional on_connect_changed(bool)
callback, fired from the websocket client's background loop once the
connection is actually live/lost, so the button only flips state on a
real connection change rather than optimistically on click.
- ws/message.py, ws/server/ws_server_multi_user.py: drop the loop= kwarg
and replace asyncio.wait(coroutines) with asyncio.gather(), both of
which were removed/forbidden in Python 3.10+.
- brewpi/requirements.txt: pin websockets==10.4, the latest version that
still supports this code's serve()/handler API.
- config.json.sim: move Model to the top level and add the missing gain
field so it matches the schema brewpi.py and config.json.templ expect.
- brewpi/brewpi.py: add -c/-m/-s/-d CLI args for selecting config/model
files, simulation mode, and debug.
- .gitignore: exclude __pycache__, logs, the local config.json, and
editor/venv directories.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Splits the server, GUI client, and websocket layer into their own
packages with dedicated requirements.txt files, and adds a README
documenting the project's architecture and usage.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>