Release 0.0.8: periodic update checks + "Run with admin" health checks
release / release (push) Successful in 22s
release / release (push) Successful in 22s
- GUI re-checks for new releases while running (every update_check_minutes, default 30; 0 disables), so a newly published version is detected without a restart; re-checks pause after an update is applied (awaiting restart) - Health page "Run with admin" button: runs all checks incl. root-only SMART via `pkexec rigdoctor report --json`, so the full report is available from the UI (cancel keeps prior results) - version 0.0.8, CHANGELOG Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
(`MAJOR.MINOR.PATCH`, pre-1.0). `__version__` and `pyproject.toml` must match the git
|
||||||
release tag (so the auto-updater, D18, can compare versions).
|
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
|
## [0.0.7] - 2026-05-21
|
||||||
### Added
|
### Added
|
||||||
- **User-local installer** `install.sh` (no root): creates a private venv, links
|
- **User-local installer** `install.sh` (no root): creates a private venv, links
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "rigdoctor"
|
name = "rigdoctor"
|
||||||
version = "0.0.7"
|
version = "0.0.8"
|
||||||
description = "Modular hardware monitoring & crash diagnostics for Linux gamers."
|
description = "Modular hardware monitoring & crash diagnostics for Linux gamers."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
"""RigDoctor — modular hardware monitoring & crash diagnostics for Linux gamers."""
|
"""RigDoctor — modular hardware monitoring & crash diagnostics for Linux gamers."""
|
||||||
|
|
||||||
__version__ = "0.0.7"
|
__version__ = "0.0.8"
|
||||||
|
|||||||
@@ -137,6 +137,7 @@ DEFAULTS: dict = {
|
|||||||
"interval": 1.0, # sampling interval in seconds (default ≤1 Hz — NFR)
|
"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_max_bytes": 20_000_000, # rotate a log segment past this size
|
||||||
"log_backups": 10, # keep this many rotated segments (bounds disk use)
|
"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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,11 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
|
|
||||||
@@ -72,6 +77,11 @@ class HealthPage(QWidget):
|
|||||||
self._status = QLabel("")
|
self._status = QLabel("")
|
||||||
self._status.setObjectName("Muted")
|
self._status.setObjectName("Muted")
|
||||||
header.addWidget(self._status)
|
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 = QPushButton("Run health report")
|
||||||
self._run_btn.setObjectName("PrimaryButton")
|
self._run_btn.setObjectName("PrimaryButton")
|
||||||
self._run_btn.clicked.connect(self._run)
|
self._run_btn.clicked.connect(self._run)
|
||||||
@@ -106,7 +116,34 @@ class HealthPage(QWidget):
|
|||||||
findings = []
|
findings = []
|
||||||
self._result.emit(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:
|
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():
|
while self._list.count():
|
||||||
item = self._list.takeAt(0)
|
item = self._list.takeAt(0)
|
||||||
w = item.widget()
|
w = item.widget()
|
||||||
@@ -122,4 +159,3 @@ class HealthPage(QWidget):
|
|||||||
for finding in findings:
|
for finding in findings:
|
||||||
self._list.addWidget(_finding_widget(finding))
|
self._list.addWidget(_finding_widget(finding))
|
||||||
self._list.addStretch(1)
|
self._list.addStretch(1)
|
||||||
self._run_btn.setEnabled(True)
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import threading
|
import threading
|
||||||
|
|
||||||
from PySide6.QtCore import Qt, Signal
|
from PySide6.QtCore import Qt, QTimer, Signal
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QButtonGroup,
|
QButtonGroup,
|
||||||
QFrame,
|
QFrame,
|
||||||
@@ -18,6 +18,7 @@ from PySide6.QtWidgets import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
from .. import __version__
|
from .. import __version__
|
||||||
|
from ..config import load_config
|
||||||
from ..core import updates
|
from ..core import updates
|
||||||
from .dashboard import Dashboard
|
from .dashboard import Dashboard
|
||||||
from .health_page import HealthPage
|
from .health_page import HealthPage
|
||||||
@@ -71,11 +72,19 @@ class MainWindow(QMainWindow):
|
|||||||
self._worker.sampled.connect(self.dashboard.update_sample)
|
self._worker.sampled.connect(self.dashboard.update_sample)
|
||||||
self._worker.start()
|
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._latest_tag = None
|
||||||
|
self._applied = False
|
||||||
self._update_checked.connect(self._show_update_state)
|
self._update_checked.connect(self._show_update_state)
|
||||||
self._update_applied.connect(self._on_update_applied)
|
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:
|
def _build_sidebar(self) -> QFrame:
|
||||||
bar = QFrame()
|
bar = QFrame()
|
||||||
@@ -134,16 +143,24 @@ class MainWindow(QMainWindow):
|
|||||||
|
|
||||||
def _on_update_applied(self, rc: int) -> None:
|
def _on_update_applied(self, rc: int) -> None:
|
||||||
if rc == 0:
|
if rc == 0:
|
||||||
|
self._applied = True
|
||||||
self._update_label.setText("updated — restart RigDoctor")
|
self._update_label.setText("updated — restart RigDoctor")
|
||||||
self._update_btn.setVisible(False)
|
self._update_btn.setVisible(False)
|
||||||
|
if hasattr(self, "_update_timer"):
|
||||||
|
self._update_timer.stop()
|
||||||
else:
|
else:
|
||||||
self._update_label.setText("update failed")
|
self._update_label.setText("update failed")
|
||||||
self._update_btn.setEnabled(True)
|
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:
|
def _check_updates(self) -> None:
|
||||||
self._update_checked.emit(updates.update_state())
|
self._update_checked.emit(updates.update_state())
|
||||||
|
|
||||||
def _show_update_state(self, result) -> None:
|
def _show_update_state(self, result) -> None:
|
||||||
|
if self._applied: # an update was applied this session; awaiting restart
|
||||||
|
return
|
||||||
state, tag = result
|
state, tag = result
|
||||||
self._latest_tag = tag
|
self._latest_tag = tag
|
||||||
self._update_btn.setVisible(False)
|
self._update_btn.setVisible(False)
|
||||||
|
|||||||
Reference in New Issue
Block a user