- templates/bot_runner.py (new, no Qt): runs the bot as a child process (unbuffered, UTF-8), tails its output, keeps raw lines in bot_raw.log, and turns lines into step/log/status events. Step markers and friendly rules use specific substrings, first match wins; unmatched error lines are always shown, other unmatched lines hidden. Dry run: python dashboard/bot_runner.py prints the events the page would get. - bridge.py: delegates to bot_runner, one run at a time, restores the saved dark mode on every page load (fixes sample-data mode in PyQt), adds load_latest_report / download_report slots the page calls. - SKILL.md: skip the stack question for a PyQt project; desktop wiring steps from the bot's real output; verify steps for the host. - tests: assert-based self-check for the line logic and run loop; Seam 1 run replay (status dot, Run button, steps, log) and wired-page mode; fixture b-pyqt-bot and its Seam 2 eval (all 14 checks pass). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
73 lines
2.6 KiB
Python
73 lines
2.6 KiB
Python
"""Minimal PyQt6 host for dashboard.html (desktop mode). Pattern from Tebra main_window.py.
|
|
|
|
The bot runs as a child process and this window shows its progress. The bot wiring
|
|
(command, step markers, friendly rules) lives in bot_runner.py, next to this file.
|
|
Run it with: python dashboard/bridge.py
|
|
"""
|
|
import json, sys, threading
|
|
from pathlib import Path
|
|
from PyQt6.QtCore import QObject, pyqtSlot, pyqtSignal, QUrl, QSettings
|
|
from PyQt6.QtWidgets import QApplication, QMainWindow
|
|
from PyQt6.QtWebEngineWidgets import QWebEngineView
|
|
from PyQt6.QtWebChannel import QWebChannel
|
|
|
|
import bot_runner
|
|
|
|
HTML_PATH = Path(__file__).with_name("dashboard.html")
|
|
|
|
|
|
def settings():
|
|
return QSettings("{{ORG}}", "{{APP_NAME}}")
|
|
|
|
|
|
class Bridge(QObject):
|
|
_emit = pyqtSignal(str) # thread-safe hop to the GUI thread
|
|
|
|
def __init__(self, view):
|
|
super().__init__()
|
|
self.view, self._one_run = view, threading.Lock()
|
|
self._emit.connect(lambda js: self.view.page().runJavaScript(f"window.__pyMsg({js})"))
|
|
# Restore the saved dark-mode choice on every page load, sample-data mode included.
|
|
view.loadFinished.connect(lambda _ok: self.send_to_js("apply_dark", {"on": settings().value("dark", False, bool)}))
|
|
|
|
def send_to_js(self, event, data):
|
|
self._emit.emit(json.dumps(json.dumps({"event": event, "data": data})))
|
|
|
|
@pyqtSlot()
|
|
def load_dashboard(self):
|
|
self.send_to_js("dashboard_data", {"batches": []}) # TODO: read from DB on a worker thread
|
|
|
|
@pyqtSlot()
|
|
def load_latest_report(self):
|
|
self.send_to_js("report_data", {"rows": []}) # TODO: read the latest report rows
|
|
|
|
@pyqtSlot()
|
|
def download_report(self):
|
|
pass # TODO: export the report rows to .xlsx
|
|
|
|
@pyqtSlot(bool)
|
|
def set_dark(self, on):
|
|
settings().setValue("dark", on)
|
|
|
|
@pyqtSlot()
|
|
def start_automation(self):
|
|
if self._one_run.acquire(blocking=False): # a second click can't start a second bot
|
|
threading.Thread(target=self._run, daemon=True).start()
|
|
|
|
def _run(self):
|
|
try:
|
|
bot_runner.run(self.send_to_js)
|
|
finally:
|
|
self._one_run.release()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
app = QApplication(sys.argv)
|
|
win, view = QMainWindow(), QWebEngineView()
|
|
bridge, channel = Bridge(view), QWebChannel()
|
|
channel.registerObject("pyBridge", bridge)
|
|
view.page().setWebChannel(channel)
|
|
view.setUrl(QUrl.fromLocalFile(str(HTML_PATH)))
|
|
win.setCentralWidget(view); win.resize(1280, 820); win.setWindowTitle("{{APP_NAME}}"); win.show()
|
|
sys.exit(app.exec())
|