Add a browser client (web/) alongside the desktop GUI

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
This commit is contained in:
2026-06-24 20:12:01 +02:00
co-authored by Claude Sonnet 4.6
parent 2913b41cab
commit 787f02b8dc
7 changed files with 981 additions and 7 deletions
+56 -7
View File
@@ -98,6 +98,14 @@ config.json.templ Configuration template - the "sim" component
simulation; point Stirrer/Heater at real
serial ports and plant_name at a real
backend to switch to hardware.
web/ Browser client - static HTML/CSS/JS (no build
step, no dependencies), speaking the exact
same WebSocket protocol as client/brewpi_gui.py
(see "Browser client" below). Served by
server/brewpi.py itself over plain HTTP, so a
browser anywhere on the network can reach the
rig with nothing installed beyond the browser.
```
### Data flow
@@ -165,7 +173,9 @@ no-op rather than an empty dialog.
- Server (`server/requirements.txt`): `numpy`, `scipy`, `matplotlib` (only
for `scripts/demos/*`, not the running server itself), `websockets`,
`dpath`, and `pyserial`/`spidev` if using real hardware backends.
- Client (`client/requirements.txt`): `PyQt5`.
- Desktop client (`client/requirements.txt`): `PyQt5`.
- Browser client (`web/`): nothing - just a browser. Served by
`server/brewpi.py` itself.
Install with:
@@ -190,23 +200,28 @@ pip install -r client/requirements.txt
./brewpi.py
```
This serves the WebSocket on `ws://0.0.0.0:8765`, ticking in real time
(1 simulated second per tick, no speedup) by default. `--dt` (simulated
seconds/tick) and `--sim-warp-factor` (how many of those fit into one
real second) are run-mode knobs, not `config.json` material - a
plant/hardware description shouldn't change just because of how a
This serves the WebSocket on `ws://0.0.0.0:8765` and the browser client
(see "Browser client" below) on `http://0.0.0.0:8080`, ticking in real
time (1 simulated second per tick, no speedup) by default. `--dt`
(simulated seconds/tick) and `--sim-warp-factor` (how many of those fit
into one real second) are run-mode knobs, not `config.json` material -
a plant/hardware description shouldn't change just because of how a
particular run happens to be invoked - so they're CLI-only:
```bash
./brewpi.py --sim-warp-factor 50 # 50x speedup, e.g. for dev/testing
./brewpi.py --http-port 8888 # change the browser client's port
```
3. Start the GUI client and connect to the server's URI:
3. Either connect with the desktop GUI:
```bash
cd client
./brewpi_gui.py
```
or open `http://<server-host>:8080/` in any browser (same machine or
anywhere else on the network) for the browser client.
`server/brewpi.sh` shows how this is wired up to run under a `virtualenvwrapper`
environment (`$WORKON_HOME`/`$BREWPI_HOME`) on a Raspberry Pi-style deployment.
@@ -502,6 +517,40 @@ one, since energy is banked per step rather than accumulated as a grand
total - summed client-side instead, from every finished step's own Wh
total plus whatever the active one has used so far).
### Browser client
`web/` is a second, independent client - plain HTML/CSS/JS, no build step,
no dependencies - for the same server, added so the rig is reachable from
any browser on the network rather than only from a machine with the PyQt5
desktop app installed. It speaks the exact same WebSocket pub/sub protocol
`client/brewpi_gui.py` does (same subscribe-on-connect handshake, same
message shapes on every channel), so nothing about the protocol or
`server/brewpi.py`'s data flow needed to change for it to exist - only a
plain static-file HTTP server (`http.server.ThreadingHTTPServer`, stdlib,
in its own daemon thread, `--http-port`, default `8080`) needed adding,
deliberately independent of the existing `websockets`-based asyncio server
so neither can affect the other's behavior or availability.
v1 covers only the Manual tab (direct heater/stirrer/controller control)
and the Progress tab (above) - the two most actionable surfaces, and
neither needs a charting library. `web/app.js` ports the relevant logic
from `client/brewpi_gui.py` directly: `components/sud.py`'s
`_build_step()`/`_merge_defaults()` (the raw doc from `Sud.Json` is
unresolved - every step still needs `default.step` merged in), and the
`StepPlate`/`_update_step_plates()`/`update_status_step_label()` logic
behind the Progress tab and status line. Deliberately deferred rather than
stubbed: the Automatic tab's forecast-vs-actual plot and the live Plot
tab's strip charts (both need a charting approach - revisit once Manual +
Progress are validated), and Sud file management (New/Load/Save dialogs -
v1 only *displays* whatever schedule the server already has loaded, fetched
via the same `{"Sud": {"Save": true}}` request `Window.connect()` sends).
No authentication - matching the existing WebSocket server's own complete
lack of it, not a new gap. Worth keeping in mind regardless: "browser,
reachable from anywhere on the network" is a meaningfully wider exposure
than "desktop app, reachable from wherever its process happens to be
running" if this is ever reachable off a trusted LAN.
## Logging & analysis
`tasks/sud_log.py`'s `SudLogTask` records every Sud run's measured data -