diff --git a/CHANGELOG.md b/CHANGELOG.md index 8846815..1546ebb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ All notable changes to RigDoctor are recorded here. Format follows (`MAJOR.MINOR.PATCH`, pre-1.0). `__version__` and `pyproject.toml` must match the git release tag (so the auto-updater, D18, can compare versions). +## [0.0.8] - 2026-05-21 +### Added +- **Periodic update checks**: the GUI now re-checks for new releases while running (every + `update_check_minutes`, default 30; 0 disables), so a newly published version is detected + without restarting. After applying an update, re-checks stop until restart. +- **"Run with admin" on the Health page**: runs all checks (including root-only SMART) via + `pkexec rigdoctor report --json`, so the full report — not just "SMART needs root" — is + available from the UI. + ## [0.0.7] - 2026-05-21 ### Added - **User-local installer** `install.sh` (no root): creates a private venv, links diff --git a/pyproject.toml b/pyproject.toml index 28ff49f..9db6673 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "rigdoctor" -version = "0.0.7" +version = "0.0.8" description = "Modular hardware monitoring & crash diagnostics for Linux gamers." readme = "README.md" requires-python = ">=3.11" diff --git a/src/rigdoctor/__init__.py b/src/rigdoctor/__init__.py index 706dd37..d0e04dd 100644 --- a/src/rigdoctor/__init__.py +++ b/src/rigdoctor/__init__.py @@ -1,3 +1,3 @@ """RigDoctor — modular hardware monitoring & crash diagnostics for Linux gamers.""" -__version__ = "0.0.7" +__version__ = "0.0.8" diff --git a/src/rigdoctor/config.py b/src/rigdoctor/config.py index 351f735..be17755 100644 --- a/src/rigdoctor/config.py +++ b/src/rigdoctor/config.py @@ -137,6 +137,7 @@ DEFAULTS: dict = { "interval": 1.0, # sampling interval in seconds (default ≤1 Hz — NFR) "log_max_bytes": 20_000_000, # rotate a log segment past this size "log_backups": 10, # keep this many rotated segments (bounds disk use) + "update_check_minutes": 30, # re-check for updates this often while running (0 = off) } diff --git a/src/rigdoctor/gui/health_page.py b/src/rigdoctor/gui/health_page.py index ec02e9b..e6e51ea 100644 --- a/src/rigdoctor/gui/health_page.py +++ b/src/rigdoctor/gui/health_page.py @@ -2,6 +2,11 @@ from __future__ import annotations +import json +import os +import shutil +import subprocess +import sys import threading import time @@ -72,6 +77,11 @@ class HealthPage(QWidget): self._status = QLabel("") self._status.setObjectName("Muted") header.addWidget(self._status) + self._admin_btn = QPushButton("Run with admin") + self._admin_btn.setToolTip("Run all checks with root (SMART needs it) — prompts for your password") + self._admin_btn.clicked.connect(self._run_admin) + self._admin_btn.setEnabled(shutil.which("pkexec") is not None) + header.addWidget(self._admin_btn) self._run_btn = QPushButton("Run health report") self._run_btn.setObjectName("PrimaryButton") self._run_btn.clicked.connect(self._run) @@ -106,7 +116,34 @@ class HealthPage(QWidget): findings = [] self._result.emit(findings) + def _run_admin(self) -> None: + self._run_btn.setEnabled(False) + self._admin_btn.setEnabled(False) + self._status.setText("Running all checks with admin (you'll be prompted)…") + threading.Thread(target=self._work_admin, daemon=True).start() + + def _work_admin(self) -> None: + from ..core.health import Finding + + cli = os.path.join(os.path.dirname(sys.executable), "rigdoctor") + if os.path.exists(cli): + cmd = ["pkexec", cli, "report", "--json"] + else: # dev / not on PATH next to python + cmd = ["pkexec", sys.executable, "-m", "rigdoctor", "report", "--json"] + try: + proc = subprocess.run(cmd, capture_output=True, text=True, timeout=180) + findings = [Finding(**d) for d in json.loads(proc.stdout)] if proc.returncode == 0 else None + except Exception: + findings = None # pkexec cancelled / failed / unparsable + self._result.emit(findings) + def _render_findings(self, findings) -> None: + self._run_btn.setEnabled(True) + self._admin_btn.setEnabled(shutil.which("pkexec") is not None) + if findings is None: # elevated run cancelled/failed — keep current results + self._status.setText("admin run cancelled") + return + while self._list.count(): item = self._list.takeAt(0) w = item.widget() @@ -122,4 +159,3 @@ class HealthPage(QWidget): for finding in findings: self._list.addWidget(_finding_widget(finding)) self._list.addStretch(1) - self._run_btn.setEnabled(True) diff --git a/src/rigdoctor/gui/main_window.py b/src/rigdoctor/gui/main_window.py index 41e47fa..077a90f 100644 --- a/src/rigdoctor/gui/main_window.py +++ b/src/rigdoctor/gui/main_window.py @@ -4,7 +4,7 @@ from __future__ import annotations import threading -from PySide6.QtCore import Qt, Signal +from PySide6.QtCore import Qt, QTimer, Signal from PySide6.QtWidgets import ( QButtonGroup, QFrame, @@ -18,6 +18,7 @@ from PySide6.QtWidgets import ( ) from .. import __version__ +from ..config import load_config from ..core import updates from .dashboard import Dashboard from .health_page import HealthPage @@ -71,11 +72,19 @@ class MainWindow(QMainWindow): self._worker.sampled.connect(self.dashboard.update_sample) self._worker.start() - # Background update check (M13); result lands in the sidebar. + # Update check (M13): once at launch, then periodically so a newly published + # release is detected without restarting (interval from config; 0 disables). self._latest_tag = None + self._applied = False self._update_checked.connect(self._show_update_state) self._update_applied.connect(self._on_update_applied) - threading.Thread(target=self._check_updates, daemon=True).start() + self._start_update_check() + minutes = float(load_config().get("update_check_minutes", 30) or 0) + if minutes > 0: + self._update_timer = QTimer(self) + self._update_timer.setInterval(int(minutes * 60_000)) + self._update_timer.timeout.connect(self._start_update_check) + self._update_timer.start() def _build_sidebar(self) -> QFrame: bar = QFrame() @@ -134,16 +143,24 @@ class MainWindow(QMainWindow): def _on_update_applied(self, rc: int) -> None: if rc == 0: + self._applied = True self._update_label.setText("updated — restart RigDoctor") self._update_btn.setVisible(False) + if hasattr(self, "_update_timer"): + self._update_timer.stop() else: self._update_label.setText("update failed") self._update_btn.setEnabled(True) + def _start_update_check(self) -> None: + threading.Thread(target=self._check_updates, daemon=True).start() + def _check_updates(self) -> None: self._update_checked.emit(updates.update_state()) def _show_update_state(self, result) -> None: + if self._applied: # an update was applied this session; awaiting restart + return state, tag = result self._latest_tag = tag self._update_btn.setVisible(False)