"""Diagnostics side panel: renders a diag.engine.Report. Layout: verdict banner, expected LED, firmware, start-up milestones, a findings tree (root causes; their consequences as children; titles only) and a wrapped detail pane for the selected finding. """ from __future__ import annotations from PySide6.QtCore import Qt from PySide6.QtGui import QBrush, QColor from PySide6.QtWidgets import ( QFrame, QHBoxLayout, QLabel, QListWidget, QListWidgetItem, QPushButton, QSizePolicy, QSplitter, QTextBrowser, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget, ) from ..diag.engine import LED, Finding, Report, Severity SEV_COLOR = { Severity.CRITICAL: "#b03a2e", Severity.ERROR: "#d9534f", Severity.WARNING: "#c77c02", Severity.INFO: "#2e6da4", } # verdict banner (background, text) by verdict severity; None = healthy BANNER = { Severity.CRITICAL: ("#f6d5d1", "#7b1e14"), Severity.ERROR: ("#f9dedc", "#8a2019"), Severity.WARNING: ("#fdebc8", "#6b4400"), None: ("#d8f0dd", "#1b5e2a"), } _KEY = Qt.ItemDataRole.UserRole def _heading(text: str) -> QLabel: lbl = QLabel(text) f = lbl.font() f.setBold(True) lbl.setFont(f) return lbl def _esc(s: str) -> str: return s.replace("&", "&").replace("<", "<").replace(">", ">") class DiagnosticsPanel(QWidget): def __init__(self, parent: QWidget | None = None): super().__init__(parent) self.setMinimumWidth(380) self.verdict = QLabel(wordWrap=True) self.verdict.setMargin(10) vf = self.verdict.font() vf.setPointSize(vf.pointSize() + 2) vf.setBold(True) self.verdict.setFont(vf) self.led_swatch = QLabel() self.led_swatch.setFixedSize(22, 22) self.led_text = QLabel(wordWrap=True) led_row = QHBoxLayout() led_row.addWidget(self.led_swatch, 0, Qt.AlignmentFlag.AlignTop) led_row.addWidget(self.led_text, 1) self.firmware = QLabel(wordWrap=True) self.firmware.setTextInteractionFlags(Qt.TextInteractionFlag.TextSelectableByMouse) self.milestones = QListWidget() self.milestones.setFrameShape(QFrame.Shape.NoFrame) self.milestones.setSelectionMode(QListWidget.SelectionMode.NoSelection) self.milestones.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Maximum) self.tree = QTreeWidget() self.tree.setHeaderHidden(True) self.tree.setIndentation(16) self.tree.setTextElideMode(Qt.TextElideMode.ElideRight) self.detail = QTextBrowser() self.detail.setOpenExternalLinks(False) findings = QSplitter(Qt.Orientation.Vertical) findings.addWidget(self.tree) findings.addWidget(self.detail) findings.setSizes([220, 260]) lay = QVBoxLayout(self) lay.setContentsMargins(4, 0, 0, 0) lay.addWidget(self.verdict) lay.addWidget(_heading("Status LED should be")) lay.addLayout(led_row) lay.addWidget(_heading("Firmware")) lay.addWidget(self.firmware) self.qr_btn = QPushButton("Matter pairing QR code...") self.qr_btn.setToolTip("Show the box's Matter QR code on screen to scan with the phone") lay.addWidget(self.qr_btn, 0, Qt.AlignmentFlag.AlignLeft) lay.addWidget(_heading("Start-up progress (current boot)")) lay.addWidget(self.milestones) lay.addWidget(_heading("Findings - root causes first; click one for details")) lay.addWidget(findings, 1) self._findings: dict[str, Finding] = {} self._selected: str | None = None # key -> expanded, chosen by the user; survives the rebuild on refresh. self._expanded: dict[str, bool] = {} self.tree.currentItemChanged.connect(self._on_select) self.tree.itemCollapsed.connect(lambda it: self._remember(it, False)) self.tree.itemExpanded.connect(lambda it: self._remember(it, True)) # ------------------------------------------------------------------ def show_report(self, r: Report) -> None: bg, fg = BANNER.get(r.verdict_severity, BANNER[Severity.WARNING]) if r.lines == 0: bg, fg = "#e6e6e6", "#333333" self.verdict.setText(r.verdict) self.verdict.setStyleSheet(f"QLabel {{ background:{bg}; color:{fg}; border-radius:6px; }}") name, colour = LED[r.led] self.led_swatch.setStyleSheet( f"QLabel {{ background:{colour}; border:1px solid #777; border-radius:11px; }}") ask = "" if r.led == "UNKNOWN" else " - check it matches the box" reason = f"
from: {_esc(r.led_reason)}" if r.led_reason else "" self.led_text.setText(f"{name}{ask}{reason}") fw = r.firmware sd = {True: "reads Wi-Fi settings from the SD card", False: "does not read Wi-Fi settings from the SD card", None: "Wi-Fi-from-SD support unknown"} self.firmware.setText(f"{_esc(fw.describe())}
{sd[fw.supports_sd_wifi]}") self.milestones.clear() for _, label, t in r.milestones: done = t is not None it = QListWidgetItem(("✔ " if done else "○ ") + label + (f" ({t / 1000:.1f} s)" if done else "")) it.setForeground(QBrush(QColor("#1b7a33" if done else "#8a8a8a"))) self.milestones.addItem(it) rows = min(max(self.milestones.count(), 1), 15) self.milestones.setFixedHeight(rows * max(self.milestones.sizeHintForRow(0), 16) + 6) self._fill_tree(r) def _fill_tree(self, r: Report) -> None: self.tree.blockSignals(True) self.tree.clear() self._findings = {} if not r.roots: it = QTreeWidgetItem(["No problems detected in the log so far."]) it.setForeground(0, QBrush(QColor("#1b7a33"))) self.tree.addTopLevelItem(it) for rc in r.roots: top = self._item(rc.finding, bold=True) self.tree.addTopLevelItem(top) for c in rc.consequences: top.addChild(self._item(c, prefix="↳ ")) top.setExpanded(self._expanded.get(rc.finding.key, True)) for title, key, items in (("Seen in earlier boots only", "grp:earlier", r.earlier), ("Resolved during this session", "grp:resolved", r.resolved)): if not items: continue grp = QTreeWidgetItem([f"{title} ({len(items)})"]) grp.setData(0, _KEY, key) grp.setForeground(0, QBrush(QColor("#666666"))) self.tree.addTopLevelItem(grp) for f in items: grp.addChild(self._item(f, prefix="✔ " if key == "grp:resolved" else "")) grp.setExpanded(self._expanded.get(key, False)) # Keep the selection across refreshes; default to the top root cause. target = self._selected if self._selected in self._findings else ( r.roots[0].finding.key if r.roots else None) self.tree.blockSignals(False) self._select(target) def _remember(self, item: QTreeWidgetItem, expanded: bool) -> None: if key := item.data(0, _KEY): self._expanded[key] = expanded def _item(self, f: Finding, prefix: str = "", bold: bool = False) -> QTreeWidgetItem: it = QTreeWidgetItem([f"{prefix}[{f.severity.label}] {f.title}"]) it.setData(0, _KEY, f.key) it.setForeground(0, QBrush(QColor(SEV_COLOR.get(f.severity, "#333333")))) it.setToolTip(0, f.title) if bold: font = it.font(0) font.setBold(True) it.setFont(0, font) self._findings[f.key] = f return it def _select(self, key: str | None) -> None: if key is None: self.detail.setHtml("

Nothing to show yet.

") return match = self.tree.findItems("*", Qt.MatchFlag.MatchWildcard | Qt.MatchFlag.MatchRecursive) for it in match: if it.data(0, _KEY) == key: self.tree.setCurrentItem(it) return def _on_select(self, cur: QTreeWidgetItem | None, _prev) -> None: key = cur.data(0, _KEY) if cur else None f = self._findings.get(key or "") if f is None: return self._selected = key self.detail.setHtml(_detail_html(f)) def _detail_html(f: Finding) -> str: colour = SEV_COLOR.get(f.severity, "#333") actions = "".join(f"
  • {_esc(a)}
  • " for a in f.actions) seen = f"Seen {f.count}x" if f.first_uptime_ms is not None: seen += f", first at {f.first_uptime_ms / 1000:.1f} s after start" if f.resolved and f.resolved_by: seen += f" - resolved ({_esc(f.resolved_by)})" evidence = "
    ".join(_esc(e) for e in f.evidence) return ( f"

    {_esc(f.title)}

    " f"

    What it means: {_esc(f.meaning)}

    " + (f"

    What to do:

      {actions}
    " if actions else "") + f"

    {seen}

    " + (f"

    {evidence}

    " if evidence else "") )