Distributed confirmation dialog: all GUIs open on WAIT_USER, server closes all

- 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
This commit is contained in:
2026-06-25 22:21:41 +02:00
co-authored by Claude Sonnet 4.6
parent dabec1fd31
commit 0215bac13e
4 changed files with 32 additions and 12 deletions
+21 -5
View File
@@ -421,6 +421,7 @@ class Window(QtWidgets.QMainWindow, Ui_MainWindow):
self.sud_empty = True self.sud_empty = True
self.sud_state = None self.sud_state = None
self.sud_user_message = None self.sud_user_message = None
self._confirm_box = None
# Mirrors the server's TempController.enabled - gates the # Mirrors the server's TempController.enabled - gates the
# temp/heatrate setpoint controls (see on_tempctrl_changed()). # temp/heatrate setpoint controls (see on_tempctrl_changed()).
self.tc_enabled = False self.tc_enabled = False
@@ -732,6 +733,7 @@ class Window(QtWidgets.QMainWindow, Ui_MainWindow):
self.sud_empty = True self.sud_empty = True
self.sud_state = None self.sud_state = None
self.sud_user_message = None self.sud_user_message = None
self._close_confirm_dialog()
self.tc_enabled = False self.tc_enabled = False
self.checkbox_tc_enable.blockSignals(True) self.checkbox_tc_enable.blockSignals(True)
self.checkbox_tc_enable.setChecked(False) self.checkbox_tc_enable.setChecked(False)
@@ -875,11 +877,23 @@ class Window(QtWidgets.QMainWindow, Ui_MainWindow):
self.msg_sud.send({'Stop': True}) self.msg_sud.send({'Stop': True})
def show_user_message(self, message): def show_user_message(self, message):
# on_sud_changed() already runs on the GUI thread (via recv_signal), if self._confirm_box is not None:
# so this can show the dialog directly. The Sud is already blocked return
# in WAIT_USER server-side; closing this dialog is what unblocks it. dlg = QtWidgets.QDialog(self)
QtWidgets.QMessageBox.information(self, "Sud", message) dlg.setWindowTitle("Sud")
self.msg_sud.send({'Confirm': True}) dlg.setWindowModality(QtCore.Qt.NonModal)
layout = QtWidgets.QVBoxLayout(dlg)
layout.addWidget(QtWidgets.QLabel(message))
btn = QtWidgets.QPushButton("OK")
btn.clicked.connect(lambda: self.msg_sud.send({'Confirm': True}))
layout.addWidget(btn)
self._confirm_box = dlg
dlg.show()
def _close_confirm_dialog(self):
if self._confirm_box is not None:
self._confirm_box.close()
self._confirm_box = None
def on_action_sud_new(self): def on_action_sud_new(self):
# Sends an empty schedule via the same Load path Load Sud uses, with # Sends an empty schedule via the same Load path Load Sud uses, with
@@ -1088,6 +1102,8 @@ class Window(QtWidgets.QMainWindow, Ui_MainWindow):
if self.sud_state == SUD_WAIT_USER_STATE and prev_state != SUD_WAIT_USER_STATE and self.sud_user_message: if self.sud_state == SUD_WAIT_USER_STATE and prev_state != SUD_WAIT_USER_STATE and self.sud_user_message:
self.show_user_message(self.sud_user_message) self.show_user_message(self.sud_user_message)
elif prev_state == SUD_WAIT_USER_STATE and self.sud_state != SUD_WAIT_USER_STATE:
self._close_confirm_dialog()
self._update_step_plates() self._update_step_plates()
elif "UserMessage" in key: elif "UserMessage" in key:
self.sud_user_message = msg['UserMessage'] self.sud_user_message = msg['UserMessage']
+5 -1
View File
@@ -483,6 +483,9 @@ function onSudChanged(msg) {
sudState = newState; sudState = newState;
if (newState === WAIT_USER_STATE && prevState !== WAIT_USER_STATE && sudUserMessage) { if (newState === WAIT_USER_STATE && prevState !== WAIT_USER_STATE && sudUserMessage) {
showUserMessage(sudUserMessage); showUserMessage(sudUserMessage);
} else if (prevState === WAIT_USER_STATE && newState !== WAIT_USER_STATE) {
const dlg = document.getElementById('sud-message-dialog');
if (dlg.open) dlg.close();
} }
updateStepPlates(); updateStepPlates();
updateSudActions(); updateSudActions();
@@ -678,7 +681,8 @@ document.getElementById('btn-sud-pause').addEventListener('click', () => {
document.getElementById('btn-sud-stop').addEventListener('click', () => { document.getElementById('btn-sud-stop').addEventListener('click', () => {
sendMsg('Sud', {Stop: true}); sendMsg('Sud', {Stop: true});
}); });
document.getElementById('sud-message-dialog').addEventListener('close', () => { document.getElementById('sud-message-dialog').addEventListener('cancel', e => e.preventDefault());
document.getElementById('sud-message-ok').addEventListener('click', () => {
sendMsg('Sud', {Confirm: true}); sendMsg('Sud', {Confirm: true});
}); });
+2 -2
View File
@@ -100,8 +100,8 @@
<dialog id="sud-message-dialog"> <dialog id="sud-message-dialog">
<p id="sud-message-text"></p> <p id="sud-message-text"></p>
<form method="dialog"> <form>
<button value="ok">OK</button> <button id="sud-message-ok" type="button">OK</button>
</form> </form>
</dialog> </dialog>
+4 -4
View File
@@ -17,7 +17,7 @@ class WsClient:
def bg_run(self, uri): def bg_run(self, uri):
print("bg_run: started") print("bg_run: started")
asyncio.set_event_loop(self.loop) asyncio.set_event_loop(self.loop)
fut = asyncio.ensure_future(self.run_client(uri), loop=self.loop) fut = asyncio.ensure_future(self.run_client(uri))
self.loop.run_until_complete(fut) self.loop.run_until_complete(fut)
self.stop = None self.stop = None
print("bg_run: terminated") print("bg_run: terminated")
@@ -38,7 +38,7 @@ class WsClient:
async def run_client(self, uri): async def run_client(self, uri):
try: try:
websocket = await websockets.connect(uri, loop=self.loop) websocket = await websockets.connect(uri)
except Exception as exc: except Exception as exc:
print(f"Failed to connect to {uri}: {exc}.") print(f"Failed to connect to {uri}: {exc}.")
return return
@@ -47,8 +47,8 @@ class WsClient:
await self.listener.on_connect() await self.listener.on_connect()
try: try:
path = "/" path = "/"
consumer_task = asyncio.ensure_future(self.handler_recv(websocket, path), loop=self.loop) consumer_task = asyncio.ensure_future(self.handler_recv(websocket, path))
producer_task = asyncio.ensure_future(self.handler_send(websocket, path), loop=self.loop) producer_task = asyncio.ensure_future(self.handler_send(websocket, path))
done, pending = await asyncio.wait([consumer_task, producer_task, self.stop], return_when=asyncio.FIRST_COMPLETED,) done, pending = await asyncio.wait([consumer_task, producer_task, self.stop], return_when=asyncio.FIRST_COMPLETED,)
for task in pending: for task in pending:
task.cancel() task.cancel()