"""Guided troubleshooting panel: renders a diag.guides.GuideRunner.""" from __future__ import annotations from PySide6.QtCore import QSize, Qt from PySide6.QtGui import QBrush, QColor, QIcon, QLinearGradient, QPainter, QPixmap from PySide6.QtWidgets import ( QFrame, QHBoxLayout, QLabel, QListWidget, QListWidgetItem, QProgressBar, QPushButton, QVBoxLayout, QWidget, ) from ..diag.engine import LED, DiagnosticsEngine from ..diag.guides import GuideRunner, recommended from ..matter.payload import SetupPayload from .matter_qr import MatterQrWidget END_STYLE = { # end severity -> (border, background) "ok": ("#2e9e44", "#e3f4e7"), "info": ("#2e6da4", "#e3edf7"), "warning": ("#c77c02", "#fdf0d6"), "error": ("#d9534f", "#f9e0df"), "critical": ("#b03a2e", "#f6d5d1"), } def _swatch(key: str, size: int = 22) -> QIcon: """Round LED-colour icon; CYCLING is drawn as a blue-dark-red gradient.""" pm = QPixmap(size, size) pm.fill(QColor(0, 0, 0, 0)) p = QPainter(pm) p.setRenderHint(QPainter.RenderHint.Antialiasing) if key == "CYCLING": g = QLinearGradient(0, 0, size, 0) for stop, c in ((0.0, "#2f6fd6"), (0.5, "#444444"), (1.0, "#d64541")): g.setColorAt(stop, QColor(c)) p.setBrush(QBrush(g)) else: p.setBrush(QColor(LED.get(key, ("", "#888888"))[1])) p.setPen(QColor("#777777")) p.drawEllipse(1, 1, size - 2, size - 2) p.end() return QIcon(pm) def _esc(s: str) -> str: return s.replace("&", "&").replace("<", "<").replace(">", ">") class GuidePanel(QWidget): def __init__(self, runner: GuideRunner, engine: DiagnosticsEngine, parent: QWidget | None = None, matter_payload=None): super().__init__(parent) self.runner, self.engine = runner, engine self._matter_payload = matter_payload or SetupPayload # callable -> SetupPayload self._shown_key: tuple | None = None # --- picker (no guide running) self.picker = QWidget() pl = QVBoxLayout(self.picker) pl.setContentsMargins(0, 0, 0, 0) pl.addWidget(QLabel("Recommended checks for this box (based on the log):")) self.guide_list = QListWidget() self.guide_list.itemDoubleClicked.connect(lambda it: self._start(it.data(Qt.ItemDataRole.UserRole))) pl.addWidget(self.guide_list, 1) self.start_btn = QPushButton("Start selected check") self.start_btn.clicked.connect(self._start_selected) pl.addWidget(self.start_btn) # --- running guide self.card = QFrame() self.card.setFrameShape(QFrame.Shape.StyledPanel) cl = QVBoxLayout(self.card) self.guide_title = QLabel() self.guide_title.setStyleSheet("color:#666") self.step_text = QLabel(wordWrap=True) f = self.step_text.font() f.setPointSize(f.pointSize() + 2) self.step_text.setFont(f) self.step_text.setTextFormat(Qt.TextFormat.RichText) self.options = QVBoxLayout() self.wait_bar = QProgressBar() self.wait_bar.setTextVisible(False) self.wait_label = QLabel() self.note = QLabel(wordWrap=True) self.note.setStyleSheet("color:#b03a2e") self.continue_btn = QPushButton("Continue") self.continue_btn.clicked.connect(self._proceed) self.skip_btn = QPushButton("Skip waiting / it did not work") self.skip_btn.clicked.connect(self._proceed) self.end_box = QLabel(wordWrap=True) self.end_box.setTextFormat(Qt.TextFormat.RichText) self.end_box.setMargin(10) self.qr = MatterQrWidget(SetupPayload(), 220) for w in (self.guide_title, self.step_text, self.qr): cl.addWidget(w) cl.addLayout(self.options) for w in (self.wait_label, self.wait_bar, self.note, self.continue_btn, self.skip_btn, self.end_box): cl.addWidget(w) cl.addStretch(1) nav = QHBoxLayout() self.back_btn = QPushButton("← Back") self.back_btn.clicked.connect(self._back) self.done_btn = QPushButton("Back to checks list") self.done_btn.clicked.connect(self._cancel) nav.addWidget(self.back_btn) nav.addStretch(1) nav.addWidget(self.done_btn) cl.addLayout(nav) lay = QVBoxLayout(self) lay.setContentsMargins(4, 0, 0, 0) lay.addWidget(self.picker, 1) lay.addWidget(self.card, 1) self.refresh() # ------------------------------------------------------------ actions def _start_selected(self) -> None: it = self.guide_list.currentItem() or self.guide_list.item(0) if it: self._start(it.data(Qt.ItemDataRole.UserRole)) def _start(self, guide_id: str) -> None: self.runner.start(guide_id) self.refresh() def _answer(self, key: str) -> None: self.runner.answer(key) self.refresh() def _proceed(self) -> None: self.runner.proceed() self.refresh() def _back(self) -> None: self.runner.back() self.refresh() def _cancel(self) -> None: self.runner.cancel() self.refresh() # ------------------------------------------------------------- render def refresh(self) -> None: """Called after every interaction and periodically (timeouts, countdown).""" self.runner.tick() v = self.runner.view() running = v.guide is not None self.picker.setVisible(not running) self.card.setVisible(running) if not running: self._fill_picker() self._shown_key = None return key = (v.guide.id, v.step.id if v.step else None, v.end.id if v.end else None) if key != self._shown_key: self._shown_key = key self._render_step(v) if v.waiting: total = v.step.timeout_s self.wait_bar.setMaximum(int(total)) self.wait_bar.setValue(int(total - v.remaining_s)) m, s = divmod(int(v.remaining_s), 60) self.wait_label.setText(f"Watching the live log... {m}:{s:02d} left") self.note.setText(v.note) self.note.setVisible(bool(v.note)) self.back_btn.setEnabled(v.can_back) def _fill_picker(self) -> None: wanted = [(g.id, g.title, g.summary) for g in recommended(self.runner.guides, self.engine)] current = [(self.guide_list.item(i).data(Qt.ItemDataRole.UserRole)) for i in range(self.guide_list.count())] if [w[0] for w in wanted] == current: return self.guide_list.clear() for gid, title, summary in wanted: it = QListWidgetItem(f"{title}\n {summary}") it.setData(Qt.ItemDataRole.UserRole, gid) self.guide_list.addItem(it) self.guide_list.setCurrentRow(0) def _render_step(self, v) -> None: self.guide_title.setText(v.guide.title) while self.options.count(): w = self.options.takeAt(0).widget() if w: # Detach now; deleteLater alone leaves it visible until the # event loop processes the deferred delete. w.hide() w.setParent(None) w.deleteLater() is_end = v.end is not None self.step_text.setVisible(not is_end) self.step_text.setText(_esc(v.text)) show_qr = bool(v.step and v.step.show_qr) if show_qr: self.qr.set_payload(self._matter_payload()) self.qr.setVisible(show_qr) for opt_key, label in v.options: b = QPushButton(label) b.setMinimumHeight(34) if v.step and v.step.type == "led": b.setIcon(_swatch(opt_key)) b.setIconSize(QSize(22, 22)) b.setStyleSheet("QPushButton { text-align:left; padding-left:10px; }") b.clicked.connect(lambda _=False, k=opt_key: self._answer(k)) self.options.addWidget(b) step_type = v.step.type if v.step else None self.continue_btn.setVisible(step_type == "info") self.skip_btn.setVisible(step_type == "wait") self.wait_bar.setVisible(step_type == "wait") self.wait_label.setVisible(step_type == "wait") self.end_box.setVisible(is_end) if is_end: e = v.end border, bg = END_STYLE.get(e.severity, END_STYLE["info"]) acts = "".join(f"
{_esc(e.text)}
" + (f"