587568e574
Reshape the IA so it reads by intent instead of a flat pile of pages. - Grouped sidebar: Monitor / Diagnose / System / App (section headers). - Renames: Health → System Health, Environment → Tuning, Logs → Recordings, Setup → Settings. - Settings absorbs Notifications (alerts) as a section; Notifications dropped as a separate page (notifications_page.py removed; SetupPage gains the alerts card + `changed` signal wired to the live alert monitor). - Recordings is now a hub: a source dropdown to view any captured log (always-on / last diagnostic / preserved crash) + Analyze-crash in place, plus the recorder controls; status line now shows the captured game. - main_window nav is data-driven (_NAV groups → _PAGES order → stack); show_page, badges, and tray flows updated. GUI smoke test asserts the new page set. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
97 lines
3.0 KiB
Python
97 lines
3.0 KiB
Python
"""Health page (M4 in the GUI): runs the health checks and shows findings as cards."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import threading
|
|
import time
|
|
|
|
from PySide6.QtCore import Qt, QTimer, Signal
|
|
from PySide6.QtWidgets import (
|
|
QFrame,
|
|
QHBoxLayout,
|
|
QLabel,
|
|
QPushButton,
|
|
QScrollArea,
|
|
QVBoxLayout,
|
|
QWidget,
|
|
)
|
|
|
|
from .widgets import finding_card
|
|
|
|
|
|
class HealthPage(QWidget):
|
|
_result = Signal(object) # list[Finding]
|
|
|
|
def __init__(self) -> None:
|
|
super().__init__()
|
|
self.setObjectName("Page")
|
|
self._result.connect(self._render_findings)
|
|
|
|
root = QVBoxLayout(self)
|
|
root.setContentsMargins(20, 18, 20, 18)
|
|
root.setSpacing(16)
|
|
|
|
header = QHBoxLayout()
|
|
title = QLabel("System Health")
|
|
title.setObjectName("PageTitle")
|
|
header.addWidget(title)
|
|
header.addStretch(1)
|
|
self._status = QLabel("")
|
|
self._status.setObjectName("Muted")
|
|
header.addWidget(self._status)
|
|
self._run_btn = QPushButton("Run health report")
|
|
self._run_btn.setObjectName("PrimaryButton")
|
|
self._run_btn.clicked.connect(self._run)
|
|
header.addWidget(self._run_btn)
|
|
root.addLayout(header)
|
|
|
|
scroll = QScrollArea()
|
|
scroll.setWidgetResizable(True)
|
|
scroll.setFrameShape(QFrame.Shape.NoFrame)
|
|
scroll.setStyleSheet("background: transparent;")
|
|
self._container = QWidget()
|
|
self._list = QVBoxLayout(self._container)
|
|
self._list.setContentsMargins(0, 0, 0, 0)
|
|
self._list.setSpacing(10)
|
|
self._list.setAlignment(Qt.AlignmentFlag.AlignTop)
|
|
scroll.setWidget(self._container)
|
|
root.addWidget(scroll, 1)
|
|
|
|
QTimer.singleShot(300, self._run) # auto-run shortly after the window opens
|
|
|
|
def _run(self) -> None:
|
|
self._run_btn.setEnabled(False)
|
|
self._status.setText("Scanning logs, SMART, and driver…")
|
|
threading.Thread(target=self._work, daemon=True).start()
|
|
|
|
def _work(self) -> None:
|
|
from ..core.health import run_health_checks
|
|
|
|
try:
|
|
findings = run_health_checks()
|
|
except Exception:
|
|
findings = []
|
|
self._result.emit(findings)
|
|
|
|
def _render_findings(self, findings) -> None:
|
|
self._run_btn.setEnabled(True)
|
|
if findings is None: # collection failed — keep current results
|
|
self._status.setText("check failed")
|
|
return
|
|
|
|
while self._list.count():
|
|
item = self._list.takeAt(0)
|
|
w = item.widget()
|
|
if w is not None:
|
|
w.deleteLater()
|
|
|
|
crit = sum(1 for f in findings if f.severity == "critical")
|
|
warn = sum(1 for f in findings if f.severity == "warning")
|
|
self._status.setText(
|
|
f"{crit} critical · {warn} warning · {len(findings)} checks · "
|
|
f"{time.strftime('%H:%M:%S')}"
|
|
)
|
|
for finding in findings:
|
|
self._list.addWidget(finding_card(finding))
|
|
self._list.addStretch(1)
|