"""Diagnostics side panel: renders a diag.engine.Report.
Layout: verdict banner, expected LED (colour + pattern + meaning when the
box reports it), firmware, the box's own self-report (KOTA lines), 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
BOX_STATE = {"ok": ("#1b7a33", "✔"), "warn": ("#c77c02", "!"), "bad": ("#b03a2e", "✖"), "info": ("#2e6da4", "i")}
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.box_title = _heading("Box self-report (KOTA lines)")
self.box = QLabel(wordWrap=True)
self.box.setTextFormat(Qt.TextFormat.RichText)
self.box.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(self.box_title)
lay.addWidget(self.box)
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"
pattern = f", {_esc(r.led_pattern)}" if r.led_pattern else ""
meaning = f" = {_esc(r.led_meaning)}" if r.led_meaning else ""
source = " (reported by the box)" if r.led_exact else ""
reason = f"
from: {_esc(r.led_reason)}" if r.led_reason else ""
self.led_text.setText(f"{name}{pattern}{meaning}{source}{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.box_title.setVisible(bool(r.box))
self.box.setVisible(bool(r.box))
self.box.setText("
| {BOX_STATE[st][1]} | " f"{_esc(topic)} | {_esc(text)} |
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"What it means: {_esc(f.meaning)}
" + (f"What to do:
Firmware error codes (error_ids.csv):
{seen}
" + (f"{evidence}
" if evidence else "") )