Serial monitor redesign: Test box button, InfluxDB row in the checklist

- Test box: restart + start-up capture + online InfluxDB check (data newer
  than the restart), with a status strip; checklist and findings are the
  main view, live log / guided troubleshooting / box details are tabs,
  rarely used actions in a More menu
- light palette on all systems (macOS dark mode made the colours unreadable)
- new rule influx.invalid_value: -inf from a silent microphone makes
  InfluxDB reject every upload (HTTP 400); guide end names it
- exclusive serial port on macOS/Linux: two tool windows no longer split
  the log between them
- InfluxDB check: `since` filter, neutral wording when nothing new arrives

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
KrzyszurandClaude Opus 5.5 committed 2026-10-01 00:06:48 +02:00
1 parent 66cb804102
commit bb933ff748
13 files changed
+422 -83

No files matched your search

+55 -19
View File
@@ -1,9 +1,11 @@
"""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.
Layout: verdict banner, the test checklist (start-up milestones plus the
online InfluxDB check), a findings tree (root causes; their consequences as
children; titles only) and a wrapped detail pane for the selected finding.
`details` is a separate widget for the host to place: expected LED (colour +
pattern + meaning when the box reports it), firmware and the box's own
self-report (KOTA lines).
"""
from __future__ import annotations
@@ -17,6 +19,7 @@ from PySide6.QtWidgets import (
QListWidget,
QListWidgetItem,
QPushButton,
QScrollArea,
QSizePolicy,
QSplitter,
QTextBrowser,
@@ -43,13 +46,19 @@ BANNER = {
}
_KEY = Qt.ItemDataRole.UserRole
BOX_STATE = {"ok": ("#1b7a33", "✔"), "warn": ("#c77c02", "!"), "bad": ("#b03a2e", "✖"), "info": ("#2e6da4", "i")}
# Extra checklist rows (e.g. the InfluxDB check): state -> (mark, colour)
ROW_STATE = {"ok": ("✔", "#1b7a33"), "bad": ("✖", "#b03a2e"), "warn": ("!", "#a86400"),
"running": ("…", "#2e6da4"), "pending": ("○", "#6b6b6b"), "off": ("–", "#6b6b6b"),
"note": ("", "#444444")}
def _heading(text: str) -> QLabel:
lbl = QLabel(text)
f = lbl.font()
f.setBold(True)
f.setPointSize(f.pointSize() + 1)
lbl.setFont(f)
lbl.setContentsMargins(0, 6, 0, 2)
return lbl
@@ -87,7 +96,14 @@ class DiagnosticsPanel(QWidget):
self.milestones = QListWidget()
self.milestones.setFrameShape(QFrame.Shape.NoFrame)
self.milestones.setSelectionMode(QListWidget.SelectionMode.NoSelection)
self.milestones.setFocusPolicy(Qt.FocusPolicy.NoFocus)
self.milestones.setWordWrap(True)
self.milestones.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)
self.milestones.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Maximum)
self.milestones.setStyleSheet("QListWidget { background: transparent; } QListWidget::item { padding: 2px; }")
mf = self.milestones.font()
mf.setPointSize(mf.pointSize() + 1)
self.milestones.setFont(mf)
self.tree = QTreeWidget()
self.tree.setHeaderHidden(True)
@@ -101,22 +117,31 @@ class DiagnosticsPanel(QWidget):
findings.setSizes([220, 260])
lay = QVBoxLayout(self)
lay.setContentsMargins(4, 0, 0, 0)
lay.setContentsMargins(4, 0, 4, 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(_heading("Test checklist (current boot)"))
lay.addWidget(self.milestones)
lay.addWidget(_heading("Findings - root causes first; click one for details"))
lay.addWidget(findings, 1)
# Box details: placed by the host (a separate tab).
inner = QWidget()
dl = QVBoxLayout(inner)
dl.addWidget(_heading("Status LED should be"))
dl.addLayout(led_row)
dl.addWidget(_heading("Firmware"))
dl.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")
dl.addWidget(self.qr_btn, 0, Qt.AlignmentFlag.AlignLeft)
dl.addWidget(self.box_title)
dl.addWidget(self.box)
dl.addStretch(1)
self.details = QScrollArea()
self.details.setWidgetResizable(True)
self.details.setFrameShape(QFrame.Shape.NoFrame)
self.details.setWidget(inner)
self._findings: dict[str, Finding] = {}
self._selected: str | None = None
# key -> expanded, chosen by the user; survives the rebuild on refresh.
@@ -126,7 +151,8 @@ class DiagnosticsPanel(QWidget):
self.tree.itemExpanded.connect(lambda it: self._remember(it, True))
# ------------------------------------------------------------------
def show_report(self, r: Report) -> None:
def show_report(self, r: Report, extra_rows: list[tuple[str, str]] = ()) -> None:
"""extra_rows: (state, text) appended to the checklist, state in ROW_STATE."""
bg, fg = BANNER.get(r.verdict_severity, BANNER[Severity.WARNING])
if r.lines == 0:
bg, fg = "#e6e6e6", "#333333"
@@ -161,10 +187,20 @@ class DiagnosticsPanel(QWidget):
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")))
it.setForeground(QBrush(QColor("#1b7a33" if done else "#6b6b6b")))
self.milestones.addItem(it)
rows = min(max(self.milestones.count(), 1), 15)
self.milestones.setFixedHeight(rows * max(self.milestones.sizeHintForRow(0), 16) + 6)
for state, text in extra_rows:
mark, colour = ROW_STATE[state]
it = QListWidgetItem(f"{mark} {text}" if mark else text)
it.setForeground(QBrush(QColor(colour)))
if state != "note":
f = it.font()
f.setBold(True)
it.setFont(f)
it.setToolTip(text)
self.milestones.addItem(it)
height = sum(max(self.milestones.sizeHintForRow(i), 16) for i in range(self.milestones.count()))
self.milestones.setFixedHeight(min(max(height, 16), 560) + 6)
self._fill_tree(r)