v0.7.0 : instance unique, handshake ré-affiché, sudoers fiabilisé, À propos refaite
- Instance unique : un second lancement réveille l'instance active. - Autostart et reconnexion auto activés par défaut (nouvelle installation). - Page « À propos » refaite en 5 onglets (contenu séparé dans app_info.py). - Corrige le handshake/RX-TX qui ne s'affichaient plus (wg show sans élévation). - Corrige le diagnostic sudo trompeur (sondes qui ne matchaient jamais les règles). - Corrige make setup-sudoers bloqué hors session graphique (pkexec -> sudo), idempotent. - Corrige le README : Kill Switch inexistant retiré, ordre d'élévation réel documenté.
This commit is contained in:
+241
-58
@@ -1,3 +1,5 @@
|
||||
import re
|
||||
|
||||
from PyQt6.QtWidgets import (
|
||||
QDialog, QVBoxLayout, QHBoxLayout, QTabWidget, QWidget,
|
||||
QLabel, QLineEdit, QPushButton, QSpinBox, QCheckBox,
|
||||
@@ -861,69 +863,250 @@ class AdminWindow(QDialog):
|
||||
# Onglet À propos
|
||||
# ------------------------------------------------------------------ #
|
||||
def _tab_about(self) -> QWidget:
|
||||
w = QWidget()
|
||||
w.setStyleSheet(theme.TAB_CSS)
|
||||
w.setAutoFillBackground(True)
|
||||
layout = QVBoxLayout(w)
|
||||
layout.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
layout.setSpacing(12)
|
||||
layout.setContentsMargins(30, 24, 30, 24)
|
||||
"""Page « À propos », en cinq sous-onglets (modèle QuiOulé/TuxMarket).
|
||||
|
||||
from app.ui.icons import icon_app
|
||||
logo_lbl = QLabel()
|
||||
logo_lbl.setPixmap(icon_app().pixmap(86, 86))
|
||||
logo_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
layout.addWidget(logo_lbl)
|
||||
Le contenu (fonctions, garanties, pile technique, auteur) vit dans
|
||||
`app/app_info.py`, séparé de sa présentation ici — le rendre
|
||||
modifiable sans toucher au Qt. Le journal est lu depuis
|
||||
CHANGELOG.md à l'affichage, jamais recopié.
|
||||
"""
|
||||
from app import app_info
|
||||
w, lay = self._dark_page("ℹ️ À propos", theme.BANNER["about"])
|
||||
lay.setContentsMargins(0, 0, 0, 0)
|
||||
lay.setSpacing(0)
|
||||
|
||||
name_lbl = QLabel("WGSecure")
|
||||
name_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
f = QFont(); f.setPointSize(22); f.setBold(True)
|
||||
name_lbl.setFont(f)
|
||||
name_lbl.setStyleSheet("color: white;")
|
||||
layout.addWidget(name_lbl)
|
||||
lay.addWidget(self._about_header(app_info))
|
||||
|
||||
version_lbl = QLabel("Version 0.6.0")
|
||||
version_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
version_lbl.setStyleSheet("color: rgba(255,255,255,0.7); font-size: 13px;")
|
||||
layout.addWidget(version_lbl)
|
||||
|
||||
sep = QFrame(); sep.setFrameShape(QFrame.Shape.HLine)
|
||||
sep.setStyleSheet("color: rgba(255,255,255,0.15);")
|
||||
layout.addWidget(sep)
|
||||
|
||||
desc_lbl = QLabel(
|
||||
"Interface graphique WireGuard avec surcouche MFA TOTP.\n"
|
||||
"Compatible Linux et Windows."
|
||||
)
|
||||
desc_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
desc_lbl.setWordWrap(True)
|
||||
desc_lbl.setStyleSheet("color: white; font-size: 13px;")
|
||||
layout.addWidget(desc_lbl)
|
||||
|
||||
sep2 = QFrame(); sep2.setFrameShape(QFrame.Shape.HLine)
|
||||
sep2.setStyleSheet("color: rgba(255,255,255,0.15);")
|
||||
layout.addWidget(sep2)
|
||||
|
||||
author_lbl = QLabel("Développé par")
|
||||
author_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
author_lbl.setStyleSheet("color: rgba(255,255,255,0.7); font-size: 12px;")
|
||||
layout.addWidget(author_lbl)
|
||||
|
||||
brand_lbl = QLabel("JT-Tools by Johnny")
|
||||
brand_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
fb = QFont(); fb.setPointSize(15); fb.setBold(True)
|
||||
brand_lbl.setFont(fb)
|
||||
brand_lbl.setStyleSheet("color: #5dade2;")
|
||||
layout.addWidget(brand_lbl)
|
||||
|
||||
date_lbl = QLabel("Septembre 2026")
|
||||
date_lbl.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
date_lbl.setStyleSheet("color: rgba(255,255,255,0.55); font-size: 12px;")
|
||||
layout.addWidget(date_lbl)
|
||||
|
||||
layout.addStretch()
|
||||
sub = QTabWidget()
|
||||
sub.setStyleSheet(theme.TABBAR_CSS)
|
||||
sub.addTab(self._subtab_about_features(app_info), "⭐ Fonctions")
|
||||
sub.addTab(self._subtab_about_guarantees(app_info), "🛡️ Garanties")
|
||||
sub.addTab(self._subtab_about_tech(app_info), "🧱 Technique")
|
||||
sub.addTab(self._subtab_about_changelog(app_info), "🕓 Journal")
|
||||
sub.addTab(self._subtab_about_author(app_info), "👤 Auteur")
|
||||
lay.addWidget(sub)
|
||||
return w
|
||||
|
||||
# ── En-tête identité : icône, nom, accroche, pastilles d'état ────────
|
||||
def _about_header(self, info) -> QWidget:
|
||||
from app.ui.icons import icon_app
|
||||
box = QWidget()
|
||||
box.setStyleSheet(f"background: {theme.BG_RAISED};")
|
||||
v = QVBoxLayout(box)
|
||||
v.setContentsMargins(20, 16, 20, 14)
|
||||
v.setSpacing(6)
|
||||
v.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
logo = QLabel()
|
||||
logo.setPixmap(icon_app().pixmap(72, 72))
|
||||
logo.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
v.addWidget(logo)
|
||||
|
||||
name = QLabel(info.APP_NAME)
|
||||
name.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
nf = QFont(); nf.setPointSize(18); nf.setBold(True)
|
||||
name.setFont(nf)
|
||||
name.setStyleSheet(f"color: {theme.TEXT};")
|
||||
v.addWidget(name)
|
||||
|
||||
tagline = QLabel(info.TAGLINE)
|
||||
tagline.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
tagline.setWordWrap(True)
|
||||
tagline.setStyleSheet(f"color: {theme.TEXT_MUTED}; font-size: 12px;")
|
||||
v.addWidget(tagline)
|
||||
|
||||
pills = QWidget()
|
||||
pills.setStyleSheet("background: transparent;")
|
||||
ph = QHBoxLayout(pills)
|
||||
ph.setContentsMargins(0, 4, 0, 0)
|
||||
ph.setSpacing(8)
|
||||
ph.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
for text, color in self._about_pills(info):
|
||||
pill = QLabel(text)
|
||||
pill.setStyleSheet(
|
||||
f"background: {color}; color: white; border-radius: 9px;"
|
||||
" padding: 3px 10px; font-size: 11px; font-weight: bold;"
|
||||
)
|
||||
ph.addWidget(pill)
|
||||
v.addWidget(pills)
|
||||
return box
|
||||
|
||||
def _about_pills(self, info) -> list[tuple[str, str]]:
|
||||
"""Pastilles de version/licence, plus l'état réel de MFA et des
|
||||
droits sudo — lu en direct, jamais une valeur supposée."""
|
||||
from app.utils.platform_utils import (
|
||||
has_root_privileges, is_windows, privilege_report,
|
||||
)
|
||||
pills = [
|
||||
(f"v{info.VERSION}", theme.ACCENT),
|
||||
(info.LICENSE, "#5b6b7a"),
|
||||
]
|
||||
if self._cfg.mfa_enabled:
|
||||
pills.append(("🔐 MFA activée", theme.OK_SOLID))
|
||||
else:
|
||||
pills.append(("🔐 MFA désactivée", "#5b6b7a"))
|
||||
|
||||
if not is_windows():
|
||||
if has_root_privileges():
|
||||
pills.append(("⚠️ Exécuté en root", theme.WARN_SOLID))
|
||||
else:
|
||||
rows = privilege_report()
|
||||
if rows and all(ok for _, ok, _ in rows):
|
||||
pills.append(("✅ sudoers configuré", theme.OK_SOLID))
|
||||
elif rows:
|
||||
pills.append(("⚠️ dialogue à chaque appel", theme.WARN_SOLID))
|
||||
return pills
|
||||
|
||||
# ── Sous-onglet : fonctions ───────────────────────────────────────────
|
||||
def _subtab_about_features(self, info) -> QWidget:
|
||||
page, v = self._test_page()
|
||||
area = QScrollArea()
|
||||
area.setWidgetResizable(True)
|
||||
area.setStyleSheet(theme.scrollarea_style())
|
||||
inner = QWidget()
|
||||
inner.setStyleSheet(f"background: {theme.BG};")
|
||||
iv = QVBoxLayout(inner)
|
||||
iv.setContentsMargins(0, 0, 8, 0)
|
||||
iv.setSpacing(10)
|
||||
for title, desc in info.FEATURES:
|
||||
iv.addWidget(self._feature_row(title, desc))
|
||||
iv.addStretch()
|
||||
area.setWidget(inner)
|
||||
v.addWidget(area)
|
||||
return page
|
||||
|
||||
def _feature_row(self, title: str, desc: str) -> QWidget:
|
||||
row = QWidget()
|
||||
row.setStyleSheet("background: transparent;")
|
||||
rv = QVBoxLayout(row)
|
||||
rv.setContentsMargins(0, 0, 0, 0)
|
||||
rv.setSpacing(2)
|
||||
t = QLabel(title)
|
||||
tf = QFont(); tf.setBold(True)
|
||||
t.setFont(tf)
|
||||
t.setStyleSheet(f"color: {theme.TEXT};")
|
||||
rv.addWidget(t)
|
||||
d = QLabel(desc)
|
||||
d.setWordWrap(True)
|
||||
d.setStyleSheet(f"color: {theme.TEXT_MUTED}; font-size: 11px;")
|
||||
rv.addWidget(d)
|
||||
return row
|
||||
|
||||
# ── Sous-onglet : garanties ───────────────────────────────────────────
|
||||
def _subtab_about_guarantees(self, info) -> QWidget:
|
||||
page, v = self._test_page()
|
||||
v.addWidget(self._hint(info.GUARANTEES_INTRO))
|
||||
area = QScrollArea()
|
||||
area.setWidgetResizable(True)
|
||||
area.setStyleSheet(theme.scrollarea_style())
|
||||
inner = QWidget()
|
||||
inner.setStyleSheet(f"background: {theme.BG};")
|
||||
iv = QVBoxLayout(inner)
|
||||
iv.setContentsMargins(0, 0, 8, 0)
|
||||
iv.setSpacing(8)
|
||||
for item in info.GUARANTEES:
|
||||
lbl = QLabel("• " + item)
|
||||
lbl.setWordWrap(True)
|
||||
lbl.setStyleSheet(f"color: {theme.TEXT}; font-size: 12px;")
|
||||
iv.addWidget(lbl)
|
||||
iv.addStretch()
|
||||
area.setWidget(inner)
|
||||
v.addWidget(area)
|
||||
return page
|
||||
|
||||
# ── Sous-onglet : pile technique ──────────────────────────────────────
|
||||
def _subtab_about_tech(self, info) -> QWidget:
|
||||
page, v = self._test_page()
|
||||
grp = QGroupBox("Pile technique")
|
||||
g = QFormLayout(grp)
|
||||
for layer, tool in info.TECH_STACK:
|
||||
layer_lbl = QLabel(layer)
|
||||
lf = QFont(); lf.setBold(True)
|
||||
layer_lbl.setFont(lf)
|
||||
tool_lbl = QLabel(tool)
|
||||
tool_lbl.setWordWrap(True)
|
||||
tool_lbl.setStyleSheet(f"color: {theme.TEXT_MUTED};")
|
||||
g.addRow(layer_lbl, tool_lbl)
|
||||
v.addWidget(grp)
|
||||
v.addStretch()
|
||||
return page
|
||||
|
||||
# ── Sous-onglet : journal (lu depuis CHANGELOG.md) ────────────────────
|
||||
def _subtab_about_changelog(self, info) -> QWidget:
|
||||
page, v = self._test_page()
|
||||
view = QTextEdit()
|
||||
view.setReadOnly(True)
|
||||
view.setStyleSheet(theme.result_view_style("idle", font_size=12))
|
||||
parts = []
|
||||
for entry in info.parse_changelog():
|
||||
parts.append(
|
||||
f"<p style='color:{theme.ACCENT_LIGHT};font-weight:bold;"
|
||||
f"font-size:13px;margin-bottom:2px'>{entry['title']}</p>"
|
||||
)
|
||||
for section in entry["sections"]:
|
||||
parts.append(f"<b>{section['name']}</b><ul style='margin-top:2px'>")
|
||||
for item in section["items"]:
|
||||
parts.append(
|
||||
"<li>" + re.sub(r"\*\*(.+?)\*\*", r"<b>\1</b>", item) + "</li>"
|
||||
)
|
||||
parts.append("</ul>")
|
||||
view.setHtml("".join(parts) or "<i>Aucun changelog trouvé.</i>")
|
||||
v.addWidget(view)
|
||||
return page
|
||||
|
||||
# ── Sous-onglet : auteur ───────────────────────────────────────────────
|
||||
def _subtab_about_author(self, info) -> QWidget:
|
||||
page, v = self._test_page()
|
||||
v.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
card = QWidget()
|
||||
card.setStyleSheet(f"background: {theme.BG_RAISED}; border-radius: 8px;")
|
||||
cv = QVBoxLayout(card)
|
||||
cv.setContentsMargins(24, 20, 24, 20)
|
||||
cv.setSpacing(6)
|
||||
cv.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
initial = QLabel(info.AUTHOR_NAME[:1].upper())
|
||||
initial.setFixedSize(56, 56)
|
||||
initial.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
initial.setStyleSheet(
|
||||
f"background: {theme.ACCENT}; color: white; border-radius: 28px;"
|
||||
" font-size: 22px; font-weight: bold;"
|
||||
)
|
||||
cv.addWidget(initial, alignment=Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
name = QLabel(info.AUTHOR_NAME)
|
||||
nf = QFont(); nf.setPointSize(14); nf.setBold(True)
|
||||
name.setFont(nf)
|
||||
name.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
cv.addWidget(name)
|
||||
|
||||
role = QLabel(info.AUTHOR_ROLE)
|
||||
role.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
role.setStyleSheet(f"color: {theme.TEXT_MUTED};")
|
||||
cv.addWidget(role)
|
||||
|
||||
repo = QLineEdit(info.REPO_URL)
|
||||
repo.setReadOnly(True)
|
||||
repo.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
repo.setFixedWidth(320)
|
||||
cv.addWidget(repo, alignment=Qt.AlignmentFlag.AlignCenter)
|
||||
|
||||
disclosure = QLabel(
|
||||
"Application développée avec l'assistance de l'IA, sous la "
|
||||
f"direction et la supervision de {info.AUTHOR_NAME}."
|
||||
)
|
||||
disclosure.setWordWrap(True)
|
||||
disclosure.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
disclosure.setFixedWidth(320)
|
||||
disclosure.setStyleSheet(f"color: {theme.TEXT_FAINT}; font-size: 11px;")
|
||||
cv.addWidget(disclosure)
|
||||
|
||||
v.addStretch()
|
||||
v.addWidget(card, alignment=Qt.AlignmentFlag.AlignCenter)
|
||||
v.addStretch()
|
||||
return page
|
||||
|
||||
# ------------------------------------------------------------------ #
|
||||
# Chargement / sauvegarde
|
||||
# ------------------------------------------------------------------ #
|
||||
|
||||
@@ -480,7 +480,7 @@ class MainWindow(QMainWindow):
|
||||
|
||||
# Auto-reconnect timer
|
||||
if self._cfg.get("ui", "auto_reconnect"):
|
||||
interval = int(self._cfg.get("ui", "reconnect_interval") or 30) * 1000
|
||||
interval = int(self._cfg.get("ui", "reconnect_interval") or 15) * 1000
|
||||
if not self._reconnect_timer.isActive():
|
||||
self._reconnect_timer.start(interval)
|
||||
else:
|
||||
|
||||
Reference in New Issue
Block a user