diff --git a/src/aare/gui/panels/developer_help_dialog.py b/src/aare/gui/panels/developer_help_dialog.py index 023ffad5..dcc7bfd1 100644 --- a/src/aare/gui/panels/developer_help_dialog.py +++ b/src/aare/gui/panels/developer_help_dialog.py @@ -1,10 +1,11 @@ from __future__ import annotations import json +import logging from typing import Dict -from PySide6.QtCore import Qt, Slot -from PySide6.QtGui import QGuiApplication +from PySide6.QtCore import Qt, Slot, QUrl +from PySide6.QtGui import QGuiApplication, QDesktopServices from PySide6.QtWidgets import ( QDialog, QVBoxLayout, @@ -24,6 +25,7 @@ from PySide6.QtWidgets import ( ) from aare.common.error_codes import error_code_help +from aare.common.logger_config import QtLogEmitter, QtLogHandler, find_existing_formatter, attach_to_logger from aare.gui.threads.daq_worker import DAQWorker @@ -110,6 +112,17 @@ class DeveloperHelpDialog(QDialog): self._copy_payload_btn.clicked.connect(self._copy_payload) top.addWidget(self._copy_payload_btn) + # Staff utilities: open log files + self._open_gui_log_btn = QPushButton("Open GUI log", self) + self._open_gui_log_btn.setVisible(self._is_staff) + self._open_gui_log_btn.clicked.connect(lambda: self._open_log_file_for_logger("aareGUI")) + top.addWidget(self._open_gui_log_btn) + + self._open_daq_log_btn = QPushButton("Open DAQ log", self) + self._open_daq_log_btn.setVisible(self._is_staff) + self._open_daq_log_btn.clicked.connect(lambda: self._open_log_file_for_logger("aareDAQ")) + top.addWidget(self._open_daq_log_btn) + # Tabs self._tabs = QTabWidget(self) root.addWidget(self._tabs, 1) @@ -125,7 +138,6 @@ class DeveloperHelpDialog(QDialog): self._codes_table.itemSelectionChanged.connect(self._update_code_details) self._codes_table.horizontalHeader().setStretchLastSection(True) - # Details panel (pretty form layout) self._details_frame = QFrame(self) self._details_frame.setFrameShape(QFrame.Shape.StyledPanel) self._details_frame.setStyleSheet( @@ -183,7 +195,12 @@ class DeveloperHelpDialog(QDialog): self._tabs.addTab(codes_container, "Error codes") - # Tab: last error payload (staff only) + # Tab: last error payload (staff only) + summary header + self._payload_summary = QLabel(self) + self._payload_summary.setVisible(self._is_staff) + self._payload_summary.setWordWrap(True) + self._payload_summary.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse) + self._payload_text = QTextEdit(self) self._payload_text.setReadOnly(True) self._payload_text.setLineWrapMode(QTextEdit.LineWrapMode.NoWrap) @@ -192,11 +209,48 @@ class DeveloperHelpDialog(QDialog): payload_container = QWidget(self) payload_layout = QVBoxLayout(payload_container) payload_layout.setContentsMargins(0, 0, 0, 0) + payload_layout.addWidget(self._payload_summary, 0) payload_layout.addWidget(self._payload_text, 1) self._tabs.addTab(payload_container, "Last error payload") else: self._payload_text.setPlainText("Hidden (staff only).") + # Tab: recent payloads (staff only) + self._payloads_text = QTextEdit(self) + self._payloads_text.setReadOnly(True) + self._payloads_text.setLineWrapMode(QTextEdit.LineWrapMode.NoWrap) + if self._is_staff: + payloads_container = QWidget(self) + payloads_layout = QVBoxLayout(payloads_container) + payloads_layout.setContentsMargins(0, 0, 0, 0) + payloads_layout.addWidget(self._payloads_text, 1) + self._tabs.addTab(payloads_container, "Recent payloads") + + # Tab: tracebacks (staff only) + self._tracebacks_text = QTextEdit(self) + self._tracebacks_text.setReadOnly(True) + self._tracebacks_text.setLineWrapMode(QTextEdit.LineWrapMode.NoWrap) + self._tracebacks_text.setPlainText("Tracebacks will appear here (last 10).") + + if self._is_staff: + tb_container = QWidget(self) + tb_layout = QVBoxLayout(tb_container) + tb_layout.setContentsMargins(0, 0, 0, 0) + tb_layout.addWidget(self._tracebacks_text, 1) + self._tabs.addTab(tb_container, "Tracebacks") + + # Tab: error log (staff only) – live view from python logging + self._error_log_text = QTextEdit(self) + self._error_log_text.setReadOnly(True) + self._error_log_text.setLineWrapMode(QTextEdit.LineWrapMode.NoWrap) + if self._is_staff: + log_container = QWidget(self) + log_layout = QVBoxLayout(log_container) + log_layout.setContentsMargins(0, 0, 0, 0) + log_layout.addWidget(self._error_log_text, 1) + self._tabs.addTab(log_container, "Error log") + + # Bottom button box buttons = QDialogButtonBox(QDialogButtonBox.StandardButton.Close, parent=self) buttons.rejected.connect(self.reject) @@ -206,10 +260,40 @@ class DeveloperHelpDialog(QDialog): # Wire signals self._daq.error_codes_loaded.connect(self.set_error_codes) self._daq.last_error_payload_changed.connect(self.set_last_error_payload) + self._daq.last_error_payloads_changed.connect(self.set_last_error_payloads) + + # Hook a Qt logging handler to show ERROR+ messages in the dialog + if self._is_staff: + self._qt_log_emitter = QtLogEmitter() + self._qt_log_emitter.message.connect(self._append_error_log_line) + + self._qt_log_handler = QtLogHandler(self._qt_log_emitter) + self._qt_log_handler.setLevel(logging.ERROR) + self._qt_log_handler.setFormatter(find_existing_formatter()) + + attach_to_logger("aareGUI", self._qt_log_handler) + attach_to_logger("aareDAQ", self._qt_log_handler) self._update_banner() self._update_code_details() + def _open_log_file_for_logger(self, logger_name: str) -> None: + """ + Opens the first FileHandler path attached to the given logger name. + """ + log = logging.getLogger(logger_name) + paths: list[str] = [] + for h in getattr(log, "handlers", []) or []: + p = getattr(h, "baseFilename", None) + if p and isinstance(p, str): + paths.append(p) + + if not paths: + self._banner.setText(f"No file handler found for logger '{logger_name}'.") + return + + QDesktopServices.openUrl(QUrl.fromLocalFile(paths[0])) + @Slot(bool) def _set_freeze_payload(self, enabled: bool) -> None: self._freeze_payload = bool(enabled) @@ -222,6 +306,8 @@ class DeveloperHelpDialog(QDialog): def refresh(self) -> None: self._daq.get_error_codes() self.set_last_error_payload(self._daq.get_last_error_payload()) + if self._is_staff: + self.set_last_error_payloads(self._daq.get_last_error_payloads()) @Slot(dict) def set_error_codes(self, codes: dict) -> None: @@ -238,9 +324,36 @@ class DeveloperHelpDialog(QDialog): self._last_payload = payload or {} pretty = json.dumps(self._last_payload, indent=2, sort_keys=True, default=str) self._payload_text.setPlainText(pretty) + + code, msg = self._extract_code_message() + url = str((self._last_payload or {}).get("url") or "") + status = (self._last_payload or {}).get("http_status") + self._payload_summary.setText( + f"URL: {url}\nHTTP: {status}\nCode: {code or '-'}\nMessage: {msg or '-'}" + ) + self._update_banner() self._select_code_from_last_error() + @Slot(list) + def set_last_error_payloads(self, payloads: list) -> None: + if not self._is_staff: + return + blocks: list[str] = [] + for i, p in enumerate(payloads[-10:], start=max(1, len(payloads) - 9)): + try: + pretty = json.dumps(p or {}, indent=2, sort_keys=True, default=str) + except Exception: + pretty = str(p) + blocks.append(f"#{i}\n{pretty}") + self._payloads_text.setPlainText("\n\n".join(blocks) if blocks else "(none captured yet)") + + @Slot(str) + def _append_error_log_line(self, line: str) -> None: + if not self._is_staff: + return + self._error_log_text.append(line) + def _code_name_for_value(self, value: str) -> str | None: for k, v in self._codes.items(): if v == value: