54c0971ac3
release / release (push) Successful in 14s
Remove the per-page "Run with admin" buttons. At launch the GUI asks for the password once (pkexec) and collects root-only data (SMART + dmidecode board/ BIOS/RAM) via the internal `collect-priv` command, caching it for the session; Health and Inventory read that cache so they always show the full picture. - core/elevation.py: pkexec collect + session cache - cli: hidden `collect-priv` command (SMART + dmidecode -> JSON) - health/inventory: use the elevation cache when present, else non-root - main_window: collect at launch (config elevate_on_launch), then refresh Health/Inventory; falls back silently if cancelled/unavailable - config: elevate_on_launch (default true) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
130 lines
4.0 KiB
Python
130 lines
4.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 .theme import ACCENT, CRIT, GOOD, MUTED, WARN
|
|
|
|
_SEV = {
|
|
"critical": ("CRITICAL", CRIT),
|
|
"warning": ("WARNING", WARN),
|
|
"info": ("INFO", MUTED),
|
|
"ok": ("OK", GOOD),
|
|
}
|
|
|
|
|
|
def _finding_widget(finding) -> QFrame:
|
|
label, color = _SEV.get(finding.severity, ("?", MUTED))
|
|
card = QFrame()
|
|
card.setObjectName("Card")
|
|
v = QVBoxLayout(card)
|
|
v.setContentsMargins(16, 12, 16, 12)
|
|
v.setSpacing(4)
|
|
|
|
head = QLabel(f"{label} · {finding.category}: {finding.title}")
|
|
head.setStyleSheet(f"color: {color}; font-weight: 700; background: transparent;")
|
|
head.setWordWrap(True)
|
|
v.addWidget(head)
|
|
|
|
if finding.detail:
|
|
detail = QLabel(finding.detail)
|
|
detail.setObjectName("Muted")
|
|
detail.setWordWrap(True)
|
|
v.addWidget(detail)
|
|
if finding.suggestion:
|
|
suggestion = QLabel(f"→ {finding.suggestion}")
|
|
suggestion.setStyleSheet(f"color: {ACCENT}; background: transparent;")
|
|
suggestion.setWordWrap(True)
|
|
v.addWidget(suggestion)
|
|
return 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("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_widget(finding))
|
|
self._list.addStretch(1)
|