fix(ui): rendre la saisie du code d'accès atteignable à tout moment (v0.8.2)

La fenêtre de saisie ne s'ouvrait qu'une fois, juste après une connexion
manuelle réussie. Un refus, une fenêtre fermée, une reconnexion automatique
— qui n'ouvre volontairement aucune modale — ou l'expiration de
l'autorisation au bout de 12 h laissaient un tunnel monté, un réseau muet
et aucun élément d'interface pour ressaisir un code.

Un bouton sous l'action de connexion et une entrée de systray apparaissent
désormais dès qu'une saisie a un sens : identifiant VPN configuré, tunnel
monté, accès non ouvert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-06 13:44:06 +02:00
co-authored by Claude Opus 5
parent cc9f521a95
commit 996bbe240d
7 changed files with 89 additions and 7 deletions
+63 -3
View File
@@ -238,6 +238,23 @@ class MainWindow(QMainWindow):
self._btn_connect.clicked.connect(self._on_connect)
s.addWidget(self._btn_connect)
# Saisie du code d'accès, atteignable à tout moment. Le tunnel monte
# en quarantaine et l'autorisation obtenue expire d'elle-même : sans
# ce bouton, un code refusé, une reconnexion automatique ou une
# autorisation échue ne laissaient d'autre issue que se déconnecter
# puis se reconnecter. Masqué tant qu'il n'y a rien à saisir, pour ne
# rien ajouter aux installations sans authentification serveur.
self._btn_access = QPushButton("🔐 Saisir le code d'accès")
self._btn_access.setFixedHeight(32)
self._btn_access.setStyleSheet(
f"QPushButton {{ background: {theme.WARN_SOLID}; color: {theme.TEXT};"
" font-size: 12px; font-weight: bold; border-radius: 6px; border: none; }"
"QPushButton:hover { background: #d68910; }"
)
self._btn_access.clicked.connect(self.request_access_code)
self._btn_access.setVisible(False)
s.addWidget(self._btn_access)
# Repli/dépli du détail (transfert, config, bande passante, journal) :
# rattaché à l'action qu'il détaille, pas à l'en-tête.
self._btn_toggle_details = QPushButton("▾ Afficher le détail")
@@ -599,6 +616,16 @@ class MainWindow(QMainWindow):
self._access_open = False
self._info_labels["access"].setText(self._access_state())
# Le bouton n'apparaît que lorsqu'il y a réellement un code à saisir ;
# sa hauteur entre et sort du calcul de la fenêtre, d'où le
# réajustement. `isHidden()` et non `isVisible()` : cette dernière est
# fausse dès que la fenêtre est repliée dans le systray, ce qui
# relancerait le calcul de hauteur à chaque cycle de rafraîchissement.
needed = self.access_code_needed()
if needed == self._btn_access.isHidden():
self._btn_access.setVisible(needed)
self._sync_window_height()
if connected:
if self._connected_since is None:
self._connected_since = QDateTime.currentDateTime()
@@ -691,6 +718,38 @@ class MainWindow(QMainWindow):
self._worker.finished.connect(self._worker.deleteLater)
self._worker.start()
def access_code_needed(self) -> bool:
"""Vrai quand une saisie de code aurait un sens ici et maintenant.
Trois conditions : un identifiant VPN configuré (sans quoi le serveur
ne gère pas ce poste), un tunnel monté (l'API n'est joignable que par
l'intérieur) et aucune autorisation en cours.
"""
return (bool((self._cfg.wg.get("vpn_username") or "").strip())
and self._is_connected and not self._access_open)
def request_access_code(self):
"""Ouvre la saisie du code, à la demande de l'utilisateur.
Distincte de l'enchaînement automatique après connexion : le tunnel
monte en quarantaine et l'autorisation expire d'elle-même, il faut
donc pouvoir revenir saisir un code à tout moment — après un refus,
après une reconnexion automatique, ou une fois l'autorisation échue.
"""
if not self._is_connected:
QMessageBox.information(
self, "Tunnel inactif",
"Le serveur d'authentification n'est joignable que depuis "
"l'intérieur du tunnel.\nConnectez-vous d'abord.")
return
if not (self._cfg.wg.get("vpn_username") or "").strip():
QMessageBox.information(
self, "Aucun identifiant VPN",
"Renseignez l'identifiant VPN connu du serveur dans le "
"panneau Administrateur.")
return
self._authenticate_with_server(silent=False)
def _authenticate_with_server(self, silent: bool):
"""Demande le code à 6 chiffres et le fait valider par le serveur.
@@ -703,9 +762,10 @@ class MainWindow(QMainWindow):
return
if silent:
# Reconnexion automatique : ouvrir une fenêtre modale sans
# personne devant l'écran laisserait l'application figée.
self._add_log("Code d'accès requis — reconnectez-vous manuellement",
"warning")
# personne devant l'écran laisserait l'application figée. Le
# bouton « Saisir le code d'accès » prend le relais.
self._add_log("Code d'accès requis — bouton « Saisir le code "
"d'accès »", "warning")
return
dlg = AccessCodeDialog(self._cfg, self)