L'application ne demarrait pas du tout : gui/app.py importait pyqtSignal, qui est le nom PyQt du signal, absent de PySide6. Trois autres defauts bloquaient juste derriere, une fois celui-ci leve : - MPLBACKEND etait fixe a 'module://backend_interagg', le backend interne de PyCharm, indisponible hors de cet IDE ; - gui/app.py et monitor/monitor.py utilisaient des imports relatifs remontant au-dela du paquet racine charge par main.py ; - core/ n'avait pas de __init__.py. Corrige aussi get_statistics(), dont le retour anticipe "historique vide" omettait connection_loss_count et uptime_percentage : le KeyError qui en resultait cassait silencieusement tout l'onglet Statistiques a chaque rafraichissement. Les deux chemins renvoient desormais les memes cles. Ajoute le build Windows (StabilityTester.spec + make build-windows), qui produit un .exe autonome depuis Linux via Wine. PySide6 y est epingle a 6.9.3 : a partir de 6.10, Qt6Core.dll depend de l'ICU systeme de Windows que Wine n'implemente pas. La sortie standard est reconfiguree en UTF-8 dans main.py, les emojis des messages faisant echouer cp1252 sous Windows. make test etait casse par la meme confusion de paquet et passe a nouveau. Makefile : libgl1-mesa-glx, retire des depots Debian/Ubuntu recents, remplace par libgl1 et les dependances Qt6 manquantes.
1453 lines
55 KiB
Python
1453 lines
55 KiB
Python
"""
|
|
StabilityTester - Interface Graphique avec PySide6
|
|
Application moderne pour surveiller la stabilite de connexion internet
|
|
"""
|
|
|
|
import sys
|
|
import os
|
|
from datetime import datetime, timedelta
|
|
import threading
|
|
import time
|
|
import json
|
|
|
|
# ============================================================================
|
|
# CONFIGURATION MATPLOTLIB - Doit être fait AVANT tout autre import de matplotlib
|
|
# ============================================================================
|
|
# Forcer matplotlib à utiliser PySide6 comme backend Qt
|
|
# C'est CRUCIAL pour éviter les erreurs : "Failed to import PyQt5, PySide2"
|
|
|
|
# Méthode 1: Définir la variable d'environnement (la plus fiable)
|
|
os.environ.setdefault('MPLBACKEND', 'QtAgg')
|
|
|
|
# Méthode 2: Importer matplotlib et définir le backend
|
|
import matplotlib
|
|
|
|
# Essayer les backends compatibles avec PySide6 (Qt6)
|
|
# Note: QtAgg est compatible avec Qt5 et Qt6
|
|
backends_to_try = ["QtAgg"]
|
|
for backend in backends_to_try:
|
|
try:
|
|
matplotlib.use(backend)
|
|
break
|
|
except Exception as e:
|
|
print(f"Warning: Cannot use matplotlib backend {backend}: {e}")
|
|
continue
|
|
else:
|
|
# Si QtAgg échoue, essayer Qt5Agg
|
|
try:
|
|
matplotlib.use("Qt5Agg")
|
|
print("Using Qt5Agg backend (Qt5)")
|
|
except Exception:
|
|
# Fallback sans Qt (pour les graphiques non-interactifs)
|
|
matplotlib.use("Agg")
|
|
print("Using Agg backend (non-interactive)")
|
|
|
|
# Maintenant importer les modules matplotlib de base
|
|
import matplotlib.pyplot as plt
|
|
from matplotlib.figure import Figure
|
|
|
|
# ============================================================================
|
|
# IMPORT PYSIDE6 - Doit être fait APRES la configuration de matplotlib
|
|
# ============================================================================
|
|
try:
|
|
from PySide6.QtWidgets import (
|
|
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
|
QLabel, QPushButton, QTabWidget, QFrame, QGroupBox,
|
|
QRadioButton, QCheckBox, QButtonGroup, QSpinBox, QDoubleSpinBox,
|
|
QTextEdit, QTreeWidget, QTreeWidgetItem, QScrollArea, QFileDialog,
|
|
QMessageBox, QProgressBar, QSizePolicy, QSplitter, QStyleFactory
|
|
)
|
|
from PySide6.QtCore import Qt, QTimer
|
|
from PySide6.QtGui import (
|
|
QFont, QColor, QPalette, QPainter, QLinearGradient, QBrush, QPen, QIcon
|
|
)
|
|
|
|
# ========================================================================
|
|
# IMPORT MATPLOTLIB QT BACKEND - Doit être fait APRES PySide6
|
|
# ========================================================================
|
|
# Avec PySide6 (Qt6) le backend est backend_qtagg ; backend_qt5agg sert de repli.
|
|
try:
|
|
from matplotlib.backends.backend_qtagg import FigureCanvasQTAgg
|
|
except ImportError:
|
|
try:
|
|
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg
|
|
except ImportError:
|
|
# Fallback pour les systèmes sans Qt
|
|
print("Warning: No Qt backend available for matplotlib, using Agg")
|
|
from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvasQTAgg
|
|
|
|
PYQt_AVAILABLE = True
|
|
|
|
except ImportError as e:
|
|
print(f"Erreur: PySide6 non disponible: {e}")
|
|
print("Installez PySide6 avec: pip install PySide6")
|
|
print("Si le problème persiste, essayez: pip install --upgrade PySide6 matplotlib")
|
|
PYQt_AVAILABLE = False
|
|
sys.exit(1)
|
|
|
|
# Import de la logique metier
|
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
from core.network_tester import NetworkTester, TestResult
|
|
from monitor.monitor import StabilityMonitor, MonitorConfig, Alert, AlertLevel, AlertHandler
|
|
|
|
|
|
class StabilityScoreBar(QWidget):
|
|
"""Barre de progression personnalisée pour le score de stabilité"""
|
|
|
|
def __init__(self, parent=None):
|
|
super().__init__(parent)
|
|
self.score = 0
|
|
self.level = "Inconnu"
|
|
self._setup_ui()
|
|
|
|
def _setup_ui(self):
|
|
layout = QHBoxLayout(self)
|
|
layout.setContentsMargins(0, 0, 0, 0)
|
|
layout.setSpacing(10)
|
|
|
|
# Label du score
|
|
self.score_label = QLabel("Score: 0/100")
|
|
self.score_label.setFont(QFont("Segoe UI", 10, QFont.Weight.Bold))
|
|
layout.addWidget(self.score_label)
|
|
|
|
# Barre de progression
|
|
self.progress_bar = QProgressBar()
|
|
self.progress_bar.setRange(0, 100)
|
|
self.progress_bar.setValue(0)
|
|
self.progress_bar.setTextVisible(False)
|
|
self.progress_bar.setFixedHeight(20)
|
|
|
|
# Style personnalisé
|
|
self.progress_bar.setStyleSheet("""
|
|
QProgressBar {
|
|
border: 1px solid #ccc;
|
|
border-radius: 5px;
|
|
background-color: #f0f0f0;
|
|
text-align: center;
|
|
}
|
|
QProgressBar::chunk {
|
|
border-radius: 4px;
|
|
margin: 0.5px;
|
|
}
|
|
""")
|
|
layout.addWidget(self.progress_bar, 1)
|
|
|
|
# Label du niveau
|
|
self.level_label = QLabel("Niveau: Inconnu")
|
|
self.level_label.setFont(QFont("Segoe UI", 10, QFont.Weight.Bold))
|
|
layout.addWidget(self.level_label)
|
|
|
|
self.setLayout(layout)
|
|
|
|
def set_score(self, score, level="Inconnu", color="gray"):
|
|
"""Met à jour le score et le niveau"""
|
|
self.score = score
|
|
self.level = level
|
|
self.score_label.setText(f"Score: {score:.1f}/100")
|
|
self.level_label.setText(f"Niveau: {level}")
|
|
self.progress_bar.setValue(int(score))
|
|
|
|
# Définir la couleur de la barre
|
|
colors = {
|
|
"green": "#5cb85c",
|
|
"orange": "#f0ad4e",
|
|
"red": "#d9534f",
|
|
"gray": "#999999"
|
|
}
|
|
color_value = colors.get(color, "#999999")
|
|
|
|
# Appliquer le style avec la couleur
|
|
style = f"""
|
|
QProgressBar::chunk {{
|
|
background-color: {color_value};
|
|
}}
|
|
"""
|
|
self.progress_bar.setStyleSheet("""
|
|
QProgressBar {
|
|
border: 1px solid #ccc;
|
|
border-radius: 5px;
|
|
background-color: #f0f0f0;
|
|
text-align: center;
|
|
}
|
|
""" + style)
|
|
|
|
|
|
class DashboardTab(QWidget):
|
|
"""Onglet Tableau de bord"""
|
|
|
|
def __init__(self, monitor, parent=None):
|
|
super().__init__(parent)
|
|
self.monitor = monitor
|
|
self._setup_ui()
|
|
|
|
def _setup_ui(self):
|
|
layout = QVBoxLayout()
|
|
layout.setContentsMargins(15, 15, 15, 15)
|
|
layout.setSpacing(15)
|
|
|
|
# Frame des indicateurs principaux
|
|
indicators_frame = QFrame()
|
|
indicators_layout = QHBoxLayout()
|
|
indicators_layout.setSpacing(20)
|
|
|
|
# Statut de connexion
|
|
self.connection_status_label = QLabel("Statut: Inconnu")
|
|
self.connection_status_label.setFont(QFont("Segoe UI", 14, QFont.Weight.Bold))
|
|
indicators_layout.addWidget(self.connection_status_label)
|
|
|
|
# Temps de disponibilité
|
|
self.uptime_label = QLabel("Disponibilité: 0s")
|
|
self.uptime_label.setFont(QFont("Segoe UI", 12))
|
|
indicators_layout.addWidget(self.uptime_label)
|
|
|
|
# Temps d'indisponibilité
|
|
self.downtime_label = QLabel("Indisponibilité: 0s")
|
|
self.downtime_label.setFont(QFont("Segoe UI", 12))
|
|
indicators_layout.addWidget(self.downtime_label)
|
|
|
|
# Pourcentage de disponibilité
|
|
self.uptime_pct_label = QLabel("Dispo: 100%")
|
|
self.uptime_pct_label.setFont(QFont("Segoe UI", 12))
|
|
indicators_layout.addWidget(self.uptime_pct_label)
|
|
|
|
indicators_frame.setLayout(indicators_layout)
|
|
layout.addWidget(indicators_frame)
|
|
|
|
# Barre de score de stabilité
|
|
self.score_bar = StabilityScoreBar()
|
|
layout.addWidget(self.score_bar)
|
|
|
|
# Séparateur
|
|
layout.addWidget(QFrame()) # Espace vide
|
|
|
|
# Métriques principales
|
|
metrics_frame = QFrame()
|
|
metrics_layout = QHBoxLayout()
|
|
metrics_layout.setSpacing(10)
|
|
|
|
# Latence
|
|
latency_group = QGroupBox("Latence")
|
|
latency_vbox = QVBoxLayout()
|
|
self.latency_label = QLabel("-- ms")
|
|
self.latency_label.setFont(QFont("Segoe UI", 14, QFont.Weight.Bold))
|
|
self.latency_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
latency_vbox.addWidget(self.latency_label)
|
|
latency_group.setLayout(latency_vbox)
|
|
metrics_layout.addWidget(latency_group, 1)
|
|
|
|
# Download
|
|
download_group = QGroupBox("Download")
|
|
download_vbox = QVBoxLayout()
|
|
self.download_label = QLabel("-- Mbps")
|
|
self.download_label.setFont(QFont("Segoe UI", 14, QFont.Weight.Bold))
|
|
self.download_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
download_vbox.addWidget(self.download_label)
|
|
download_group.setLayout(download_vbox)
|
|
metrics_layout.addWidget(download_group, 1)
|
|
|
|
# Upload
|
|
upload_group = QGroupBox("Upload")
|
|
upload_vbox = QVBoxLayout()
|
|
self.upload_label = QLabel("-- Mbps")
|
|
self.upload_label.setFont(QFont("Segoe UI", 14, QFont.Weight.Bold))
|
|
self.upload_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
upload_vbox.addWidget(self.upload_label)
|
|
upload_group.setLayout(upload_vbox)
|
|
metrics_layout.addWidget(upload_group, 1)
|
|
|
|
# Jitter
|
|
jitter_group = QGroupBox("Jitter")
|
|
jitter_vbox = QVBoxLayout()
|
|
self.jitter_label = QLabel("-- ms")
|
|
self.jitter_label.setFont(QFont("Segoe UI", 14, QFont.Weight.Bold))
|
|
self.jitter_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
|
jitter_vbox.addWidget(self.jitter_label)
|
|
jitter_group.setLayout(jitter_vbox)
|
|
metrics_layout.addWidget(jitter_group, 1)
|
|
|
|
metrics_frame.setLayout(metrics_layout)
|
|
layout.addWidget(metrics_frame)
|
|
|
|
self.setLayout(layout)
|
|
|
|
def update_status(self, status):
|
|
"""Met à jour les indicateurs de statut"""
|
|
if status.get('connected', False):
|
|
self.connection_status_label.setText("Statut: <span style='color: #5cb85c;'>Connecté</span>")
|
|
else:
|
|
self.connection_status_label.setText("Statut: <span style='color: #d9534f;'>Déconnecté</span>")
|
|
|
|
self.uptime_label.setText(f"Disponibilité: {self._format_timedelta(status.get('uptime', timedelta(0)))}")
|
|
self.downtime_label.setText(f"Indisponibilité: {self._format_timedelta(status.get('downtime', timedelta(0)))}")
|
|
|
|
total = status['uptime'].total_seconds() + status['downtime'].total_seconds()
|
|
uptime_pct = (status['uptime'].total_seconds() / total * 100) if total > 0 else 100
|
|
|
|
if uptime_pct > 95:
|
|
color = "#5cb85c"
|
|
elif uptime_pct > 80:
|
|
color = "#f0ad4e"
|
|
else:
|
|
color = "#d9534f"
|
|
|
|
self.uptime_pct_label.setText(f"Dispo: <span style='color: {color};'>{uptime_pct:.1f}%</span>")
|
|
|
|
def update_metrics(self, result):
|
|
"""Met à jour les métriques"""
|
|
if result.latency_ms is not None:
|
|
self.latency_label.setText(f"{result.latency_ms:.1f} ms")
|
|
if result.jitter is not None:
|
|
self.jitter_label.setText(f"{result.jitter:.1f} ms")
|
|
if result.download_speed is not None:
|
|
self.download_label.setText(f"{result.download_speed:.2f} Mbps")
|
|
if result.upload_speed is not None:
|
|
self.upload_label.setText(f"{result.upload_speed:.2f} Mbps")
|
|
|
|
def update_score(self, score_data):
|
|
"""Met à jour la barre de score"""
|
|
score = score_data.get('score', 0)
|
|
level = score_data.get('stability_level', 'Inconnu')
|
|
color = score_data.get('color', 'gray')
|
|
self.score_bar.set_score(score, level, color)
|
|
|
|
def _format_timedelta(self, td):
|
|
ts = int(td.total_seconds())
|
|
if ts < 60:
|
|
return f"{ts}s"
|
|
elif ts < 3600:
|
|
return f"{ts//60}m {ts%60}s"
|
|
else:
|
|
return f"{ts//3600}h {(ts%3600)//60}m"
|
|
|
|
|
|
class TestsTab(QWidget):
|
|
"""Onglet Tests"""
|
|
|
|
def __init__(self, monitor, tester, parent=None):
|
|
super().__init__(parent)
|
|
self.monitor = monitor
|
|
self.tester = tester
|
|
self._setup_ui()
|
|
|
|
def _setup_ui(self):
|
|
layout = QVBoxLayout()
|
|
layout.setContentsMargins(15, 15, 15, 15)
|
|
layout.setSpacing(15)
|
|
|
|
# Frame de contrôle
|
|
control_frame = QFrame()
|
|
control_layout = QHBoxLayout()
|
|
|
|
# Sélection du type de test
|
|
test_label = QLabel("Type de test:")
|
|
control_layout.addWidget(test_label)
|
|
|
|
self.test_type_group = QButtonGroup()
|
|
test_types = [
|
|
("Complet", "comprehensive"),
|
|
("Ping", "ping"),
|
|
("Connexion", "connection"),
|
|
("DNS", "dns"),
|
|
("Download", "download"),
|
|
("Upload", "upload"),
|
|
("Speedtest", "full_speed"),
|
|
("Ping tous", "ping_all")
|
|
]
|
|
|
|
for label, value in test_types:
|
|
rb = QRadioButton(label)
|
|
rb.setProperty("test_value", value)
|
|
self.test_type_group.addButton(rb)
|
|
control_layout.addWidget(rb)
|
|
|
|
# Bouton Exécuter
|
|
self.run_button = QPushButton("Exécuter")
|
|
self.run_button.setStyleSheet("""
|
|
QPushButton {
|
|
background-color: #007bff;
|
|
color: white;
|
|
border: none;
|
|
padding: 8px 16px;
|
|
border-radius: 4px;
|
|
}
|
|
QPushButton:hover {
|
|
background-color: #0056b3;
|
|
}
|
|
""")
|
|
self.run_button.clicked.connect(self._run_selected_test)
|
|
control_layout.addWidget(self.run_button)
|
|
|
|
control_frame.setLayout(control_layout)
|
|
layout.addWidget(control_frame)
|
|
|
|
# Zone de résultats
|
|
self.result_text = QTextEdit()
|
|
self.result_text.setReadOnly(True)
|
|
self.result_text.setFont(QFont("Courier New", 10))
|
|
layout.addWidget(self.result_text)
|
|
|
|
self.setLayout(layout)
|
|
|
|
def _run_selected_test(self):
|
|
"""Exécute le test sélectionné"""
|
|
test_type = self.test_type_group.checkedButton()
|
|
if not test_type:
|
|
return
|
|
|
|
test_value = test_type.property("test_value")
|
|
self.result_text.clear()
|
|
self.result_text.append("Démarrage du test...")
|
|
|
|
def run():
|
|
try:
|
|
if test_value == "comprehensive":
|
|
results = self.monitor.run_single_test()
|
|
elif test_value == "ping":
|
|
results = {'ping': self.tester.ping_test()}
|
|
elif test_value == "connection":
|
|
results = {'connection': self.tester.connection_test()}
|
|
elif test_value == "dns":
|
|
results = {'dns': self.tester.dns_test()}
|
|
elif test_value == "download":
|
|
results = {'download': self.tester.download_speed_test(duration=3)}
|
|
elif test_value == "upload":
|
|
results = {'upload': self.tester.upload_speed_test(data_size=256*1024)}
|
|
elif test_value == "full_speed":
|
|
results = {'full_speed': self.tester.full_speed_test()}
|
|
elif test_value == "ping_all":
|
|
results = {f'ping_{i}': r for i, r in enumerate(self.tester.ping_all_servers())}
|
|
else:
|
|
results = {}
|
|
|
|
for test_name, result in results.items():
|
|
self.result_text.append(f"\n{'-'*40}")
|
|
self.result_text.append(f"Test: {result.test_type}")
|
|
self.result_text.append(f" Statut: {'OK' if result.success else 'ERROR'}")
|
|
self.result_text.append(f" Heure: {result.timestamp.strftime('%H:%M:%S')}")
|
|
|
|
if result.latency_ms is not None:
|
|
self.result_text.append(f" Latence: {result.latency_ms:.2f} ms")
|
|
if result.jitter is not None:
|
|
self.result_text.append(f" Jitter: {result.jitter:.2f} ms")
|
|
if result.download_speed is not None:
|
|
self.result_text.append(f" Download: {result.download_speed:.2f} Mbps")
|
|
if result.upload_speed is not None:
|
|
self.result_text.append(f" Upload: {result.upload_speed:.2f} Mbps")
|
|
if result.error_message:
|
|
self.result_text.append(f" Erreur: {result.error_message}")
|
|
|
|
self.result_text.append(f"\n{'='*40}")
|
|
self.result_text.append("Test terminé.")
|
|
|
|
except Exception as e:
|
|
self.result_text.append(f"Erreur: {str(e)}")
|
|
|
|
thread = threading.Thread(target=run, daemon=True)
|
|
thread.start()
|
|
|
|
|
|
class HistoryTab(QWidget):
|
|
"""Onglet Historique"""
|
|
|
|
def __init__(self, monitor, parent=None):
|
|
super().__init__(parent)
|
|
self.monitor = monitor
|
|
self._setup_ui()
|
|
|
|
def _setup_ui(self):
|
|
layout = QVBoxLayout()
|
|
layout.setContentsMargins(15, 15, 15, 15)
|
|
layout.setSpacing(10)
|
|
|
|
# Frame de contrôle
|
|
control_frame = QFrame()
|
|
control_layout = QHBoxLayout()
|
|
|
|
# Période
|
|
control_layout.addWidget(QLabel("Période:"))
|
|
|
|
self.period_group = QButtonGroup()
|
|
periods = [
|
|
("Tout", "all"),
|
|
("1h", "hour"),
|
|
("24h", "day"),
|
|
("Semaine", "week")
|
|
]
|
|
|
|
for label, value in periods:
|
|
rb = QRadioButton(label)
|
|
rb.setProperty("period_value", value)
|
|
self.period_group.addButton(rb)
|
|
control_layout.addWidget(rb)
|
|
|
|
# Bouton Rafraîchir
|
|
refresh_button = QPushButton("Rafraîchir")
|
|
refresh_button.clicked.connect(self._refresh)
|
|
control_layout.addWidget(refresh_button)
|
|
|
|
control_frame.setLayout(control_layout)
|
|
layout.addWidget(control_frame)
|
|
|
|
# Arbre de l'historique
|
|
self.tree = QTreeWidget()
|
|
self.tree.setColumnCount(6)
|
|
self.tree.setHeaderLabels(["Heure", "Type", "Statut", "Latence", "Download", "Upload"])
|
|
self.tree.setColumnWidth(0, 150)
|
|
self.tree.setColumnWidth(1, 120)
|
|
self.tree.setColumnWidth(2, 80)
|
|
self.tree.setColumnWidth(3, 100)
|
|
self.tree.setColumnWidth(4, 100)
|
|
self.tree.setColumnWidth(5, 100)
|
|
|
|
layout.addWidget(self.tree)
|
|
self.setLayout(layout)
|
|
|
|
# Timer pour rafraîchissement automatique
|
|
self.refresh_timer = QTimer()
|
|
self.refresh_timer.timeout.connect(self._refresh)
|
|
self.refresh_timer.start(5000) # 5 secondes
|
|
|
|
def _refresh(self):
|
|
"""Rafraîchit l'historique"""
|
|
self.tree.clear()
|
|
|
|
now = datetime.now()
|
|
period = self.period_group.checkedButton()
|
|
if period:
|
|
period_value = period.property("period_value")
|
|
else:
|
|
period_value = "all"
|
|
|
|
for results in self.monitor.get_test_history():
|
|
for result in results.values():
|
|
if period_value == "hour" and (now - result.timestamp).total_seconds() > 3600:
|
|
continue
|
|
elif period_value == "day" and (now - result.timestamp).total_seconds() > 86400:
|
|
continue
|
|
elif period_value == "week" and (now - result.timestamp).total_seconds() > 604800:
|
|
continue
|
|
|
|
item = QTreeWidgetItem()
|
|
item.setText(0, result.timestamp.strftime('%H:%M:%S'))
|
|
item.setText(1, result.test_type)
|
|
item.setText(2, 'OK' if result.success else 'ERROR')
|
|
|
|
# Couleur selon le statut
|
|
if result.success:
|
|
item.setTextColor(2, QColor("#5cb85c"))
|
|
else:
|
|
item.setTextColor(2, QColor("#d9534f"))
|
|
|
|
# Valeurs
|
|
if result.latency_ms is not None:
|
|
item.setText(3, f"{result.latency_ms:.0f}ms")
|
|
if result.download_speed is not None:
|
|
item.setText(4, f"{result.download_speed:.1f}Mbps")
|
|
if result.upload_speed is not None:
|
|
item.setText(5, f"{result.upload_speed:.1f}Mbps")
|
|
|
|
self.tree.addTopLevelItem(item)
|
|
|
|
|
|
class AlertsTab(QWidget):
|
|
"""Onglet Alertes"""
|
|
|
|
def __init__(self, monitor, parent=None):
|
|
super().__init__(parent)
|
|
self.monitor = monitor
|
|
self._setup_ui()
|
|
|
|
def _setup_ui(self):
|
|
layout = QVBoxLayout()
|
|
layout.setContentsMargins(15, 15, 15, 15)
|
|
layout.setSpacing(10)
|
|
|
|
# Frame de contrôle
|
|
control_frame = QFrame()
|
|
control_layout = QHBoxLayout()
|
|
|
|
control_layout.addWidget(QLabel("Niveau:"))
|
|
|
|
self.level_group = QButtonGroup()
|
|
levels = [
|
|
("Toutes", "all"),
|
|
("Critique", "critical"),
|
|
("Warning", "warning"),
|
|
("Info", "info")
|
|
]
|
|
|
|
for label, value in levels:
|
|
rb = QRadioButton(label)
|
|
rb.setProperty("level_value", value)
|
|
self.level_group.addButton(rb)
|
|
control_layout.addWidget(rb)
|
|
|
|
# Boutons
|
|
clear_button = QPushButton("Effacer")
|
|
clear_button.clicked.connect(self._clear_alerts)
|
|
control_layout.addWidget(clear_button)
|
|
|
|
refresh_button = QPushButton("Rafraîchir")
|
|
refresh_button.clicked.connect(self._refresh)
|
|
control_layout.addWidget(refresh_button)
|
|
|
|
control_frame.setLayout(control_layout)
|
|
layout.addWidget(control_frame)
|
|
|
|
# Arbre des alertes
|
|
self.tree = QTreeWidget()
|
|
self.tree.setColumnCount(5)
|
|
self.tree.setHeaderLabels(["Heure", "Niveau", "Message", "Type", "Valeur"])
|
|
self.tree.setColumnWidth(0, 150)
|
|
self.tree.setColumnWidth(1, 80)
|
|
self.tree.setColumnWidth(2, 300)
|
|
self.tree.setColumnWidth(3, 120)
|
|
self.tree.setColumnWidth(4, 100)
|
|
|
|
layout.addWidget(self.tree)
|
|
self.setLayout(layout)
|
|
|
|
# Timer pour rafraîchissement automatique
|
|
self.refresh_timer = QTimer()
|
|
self.refresh_timer.timeout.connect(self._refresh)
|
|
self.refresh_timer.start(5000)
|
|
|
|
def _refresh(self):
|
|
"""Rafraîchit les alertes"""
|
|
self.tree.clear()
|
|
|
|
level = self.level_group.checkedButton()
|
|
level_value = level.property("level_value") if level else "all"
|
|
|
|
for alert in self.monitor.alert_handler.get_alert_history():
|
|
if level_value != "all" and alert.level.value != level_value:
|
|
continue
|
|
|
|
item = QTreeWidgetItem()
|
|
item.setText(0, alert.timestamp.strftime('%H:%M:%S'))
|
|
item.setText(1, alert.level.value.upper())
|
|
item.setText(2, alert.message[:50])
|
|
item.setText(3, alert.test_type)
|
|
item.setText(4, f"{alert.value:.2f}" if alert.value else "--")
|
|
|
|
# Couleur selon le niveau
|
|
if alert.level == AlertLevel.CRITICAL:
|
|
item.setTextColor(1, QColor("#dc3545"))
|
|
elif alert.level == AlertLevel.WARNING:
|
|
item.setTextColor(1, QColor("#ffc107"))
|
|
else:
|
|
item.setTextColor(1, QColor("#17a2b8"))
|
|
|
|
self.tree.addTopLevelItem(item)
|
|
|
|
def _clear_alerts(self):
|
|
"""Efface l'historique des alertes"""
|
|
reply = QMessageBox.question(
|
|
self, "Confirmer",
|
|
"Voulez-vous vraiment effacer l'historique des alertes?",
|
|
QMessageBox.Yes | QMessageBox.No
|
|
)
|
|
if reply == QMessageBox.Yes:
|
|
self.monitor.alert_handler.clear_history()
|
|
self._refresh()
|
|
|
|
|
|
class SettingsTab(QWidget):
|
|
"""Onglet Paramètres"""
|
|
|
|
def __init__(self, monitor, parent=None):
|
|
super().__init__(parent)
|
|
self.monitor = monitor
|
|
self._setup_ui()
|
|
|
|
def _setup_ui(self):
|
|
layout = QVBoxLayout()
|
|
layout.setContentsMargins(15, 15, 15, 15)
|
|
layout.setSpacing(15)
|
|
|
|
# Paramètres généraux
|
|
general_group = QGroupBox("Paramètres généraux")
|
|
general_layout = QVBoxLayout()
|
|
|
|
# Intervalle
|
|
interval_layout = QHBoxLayout()
|
|
interval_layout.addWidget(QLabel("Intervalle (secondes):"))
|
|
self.interval_spin = QSpinBox()
|
|
self.interval_spin.setRange(10, 3600)
|
|
self.interval_spin.setValue(self.monitor.config.interval)
|
|
interval_layout.addWidget(self.interval_spin)
|
|
general_layout.addLayout(interval_layout)
|
|
|
|
# Historique max
|
|
history_layout = QHBoxLayout()
|
|
history_layout.addWidget(QLabel("Historique maximal:"))
|
|
self.max_history_spin = QSpinBox()
|
|
self.max_history_spin.setRange(10, 1000)
|
|
self.max_history_spin.setValue(self.monitor.config.max_history)
|
|
history_layout.addWidget(self.max_history_spin)
|
|
general_layout.addLayout(history_layout)
|
|
|
|
general_group.setLayout(general_layout)
|
|
layout.addWidget(general_group)
|
|
|
|
# Seuils
|
|
thresholds_group = QGroupBox("Seuils d'alerte")
|
|
thresholds_layout = QVBoxLayout()
|
|
|
|
# Latence
|
|
ping_layout = QHBoxLayout()
|
|
ping_layout.addWidget(QLabel("Latence (ms):"))
|
|
self.ping_threshold_spin = QDoubleSpinBox()
|
|
self.ping_threshold_spin.setRange(1, 1000)
|
|
self.ping_threshold_spin.setValue(self.monitor.config.ping_threshold)
|
|
ping_layout.addWidget(self.ping_threshold_spin)
|
|
thresholds_layout.addLayout(ping_layout)
|
|
|
|
# Jitter
|
|
jitter_layout = QHBoxLayout()
|
|
jitter_layout.addWidget(QLabel("Jitter (ms):"))
|
|
self.jitter_threshold_spin = QDoubleSpinBox()
|
|
self.jitter_threshold_spin.setRange(1, 500)
|
|
self.jitter_threshold_spin.setValue(self.monitor.config.jitter_threshold)
|
|
jitter_layout.addWidget(self.jitter_threshold_spin)
|
|
thresholds_layout.addLayout(jitter_layout)
|
|
|
|
# Download
|
|
download_layout = QHBoxLayout()
|
|
download_layout.addWidget(QLabel("Download (Mbps):"))
|
|
self.download_threshold_spin = QDoubleSpinBox()
|
|
self.download_threshold_spin.setRange(0.1, 100)
|
|
self.download_threshold_spin.setValue(self.monitor.config.download_threshold)
|
|
download_layout.addWidget(self.download_threshold_spin)
|
|
thresholds_layout.addLayout(download_layout)
|
|
|
|
# Upload
|
|
upload_layout = QHBoxLayout()
|
|
upload_layout.addWidget(QLabel("Upload (Mbps):"))
|
|
self.upload_threshold_spin = QDoubleSpinBox()
|
|
self.upload_threshold_spin.setRange(0.1, 100)
|
|
self.upload_threshold_spin.setValue(self.monitor.config.upload_threshold)
|
|
upload_layout.addWidget(self.upload_threshold_spin)
|
|
thresholds_layout.addLayout(upload_layout)
|
|
|
|
thresholds_group.setLayout(thresholds_layout)
|
|
layout.addWidget(thresholds_group)
|
|
|
|
# Notifications
|
|
notification_group = QGroupBox("Notifications")
|
|
notification_layout = QVBoxLayout()
|
|
|
|
self.enable_sound_check = QCheckBox("Alertes sonores")
|
|
self.enable_sound_check.setChecked(self.monitor.config.enable_sound)
|
|
notification_layout.addWidget(self.enable_sound_check)
|
|
|
|
self.enable_notifications_check = QCheckBox("Notifications système")
|
|
self.enable_notifications_check.setChecked(self.monitor.config.enable_notifications)
|
|
notification_layout.addWidget(self.enable_notifications_check)
|
|
|
|
notification_group.setLayout(notification_layout)
|
|
layout.addWidget(notification_group)
|
|
|
|
# Types de tests
|
|
tests_group = QGroupBox("Types de tests à exécuter")
|
|
tests_layout = QVBoxLayout()
|
|
|
|
self.test_checks = {}
|
|
test_types = [
|
|
("Ping", "ping"),
|
|
("Connexion HTTP", "connection"),
|
|
("Résolution DNS", "dns"),
|
|
("Test Download", "download"),
|
|
("Test Upload", "upload"),
|
|
("Speedtest complet", "full_speed")
|
|
]
|
|
|
|
for label, key in test_types:
|
|
cb = QCheckBox(label)
|
|
cb.setChecked(key in self.monitor.config.test_types)
|
|
self.test_checks[key] = cb
|
|
tests_layout.addWidget(cb)
|
|
|
|
tests_group.setLayout(tests_layout)
|
|
layout.addWidget(tests_group)
|
|
|
|
# Boutons
|
|
button_layout = QHBoxLayout()
|
|
apply_button = QPushButton("Appliquer")
|
|
apply_button.clicked.connect(self._apply_settings)
|
|
button_layout.addWidget(apply_button)
|
|
|
|
reset_button = QPushButton("Réinitialiser")
|
|
reset_button.clicked.connect(self._reset_settings)
|
|
button_layout.addWidget(reset_button)
|
|
|
|
layout.addLayout(button_layout)
|
|
self.setLayout(layout)
|
|
|
|
def _apply_settings(self):
|
|
"""Applique les paramètres"""
|
|
try:
|
|
test_types = [
|
|
key for key, cb in self.test_checks.items()
|
|
if cb.isChecked()
|
|
]
|
|
|
|
new_config = MonitorConfig(
|
|
interval=self.interval_spin.value(),
|
|
ping_threshold=self.ping_threshold_spin.value(),
|
|
download_threshold=self.download_threshold_spin.value(),
|
|
upload_threshold=self.upload_threshold_spin.value(),
|
|
jitter_threshold=self.jitter_threshold_spin.value(),
|
|
enable_sound=self.enable_sound_check.isChecked(),
|
|
enable_notifications=self.enable_notifications_check.isChecked(),
|
|
max_history=self.max_history_spin.value(),
|
|
test_types=test_types
|
|
)
|
|
|
|
self.monitor.config = new_config
|
|
self.monitor.alert_handler = AlertHandler(new_config)
|
|
QMessageBox.information(self, "Succès", "Paramètres appliqués!")
|
|
|
|
except Exception as e:
|
|
QMessageBox.critical(self, "Erreur", f"Erreur: {str(e)}")
|
|
|
|
def _reset_settings(self):
|
|
"""Réinitialise les paramètres"""
|
|
reply = QMessageBox.question(
|
|
self, "Confirmer",
|
|
"Voulez-vous vraiment réinitialiser les paramètres?",
|
|
QMessageBox.Yes | QMessageBox.No
|
|
)
|
|
if reply == QMessageBox.Yes:
|
|
self.interval_spin.setValue(60)
|
|
self.max_history_spin.setValue(100)
|
|
self.ping_threshold_spin.setValue(100.0)
|
|
self.jitter_threshold_spin.setValue(50.0)
|
|
self.download_threshold_spin.setValue(5.0)
|
|
self.upload_threshold_spin.setValue(2.0)
|
|
self.enable_sound_check.setChecked(True)
|
|
self.enable_notifications_check.setChecked(True)
|
|
for cb in self.test_checks.values():
|
|
cb.setChecked(True)
|
|
|
|
|
|
class StatisticsTab(QWidget):
|
|
"""Onglet Statistiques avec graphiques"""
|
|
|
|
def __init__(self, monitor, parent=None):
|
|
super().__init__(parent)
|
|
self.monitor = monitor
|
|
self._setup_ui()
|
|
|
|
def _setup_ui(self):
|
|
layout = QVBoxLayout()
|
|
layout.setContentsMargins(15, 15, 15, 15)
|
|
layout.setSpacing(15)
|
|
|
|
# Statistiques globales
|
|
stats_frame = QFrame()
|
|
stats_layout = QHBoxLayout()
|
|
stats_layout.setSpacing(20)
|
|
|
|
self.total_tests_label = QLabel("Tests: 0")
|
|
self.total_tests_label.setFont(QFont("Segoe UI", 12))
|
|
stats_layout.addWidget(self.total_tests_label)
|
|
|
|
self.success_rate_label = QLabel("Succès: 0%")
|
|
self.success_rate_label.setFont(QFont("Segoe UI", 12))
|
|
stats_layout.addWidget(self.success_rate_label)
|
|
|
|
self.connection_loss_label = QLabel("Pertes: 0")
|
|
self.connection_loss_label.setFont(QFont("Segoe UI", 12))
|
|
stats_layout.addWidget(self.connection_loss_label)
|
|
|
|
self.avg_latency_label = QLabel("Latence avg: 0ms")
|
|
self.avg_latency_label.setFont(QFont("Segoe UI", 12))
|
|
stats_layout.addWidget(self.avg_latency_label)
|
|
|
|
stats_frame.setLayout(stats_layout)
|
|
layout.addWidget(stats_frame)
|
|
|
|
# Onglets des graphiques
|
|
self.chart_tabs = QTabWidget()
|
|
layout.addWidget(self.chart_tabs)
|
|
|
|
# Graphique de latence
|
|
self._create_latency_chart()
|
|
|
|
# Graphique de vitesse
|
|
self._create_speed_chart()
|
|
|
|
# Graphique de connectivité
|
|
self._create_connectivity_chart()
|
|
|
|
self.setLayout(layout)
|
|
|
|
# Timer pour mise à jour
|
|
self.update_timer = QTimer()
|
|
self.update_timer.timeout.connect(self._update_statistics)
|
|
self.update_timer.start(5000)
|
|
|
|
def _create_latency_chart(self):
|
|
"""Crée le graphique de latence"""
|
|
chart_widget = QWidget()
|
|
chart_layout = QVBoxLayout()
|
|
|
|
self.latency_fig = Figure(figsize=(8, 4), dpi=100)
|
|
self.latency_ax = self.latency_fig.add_subplot(111)
|
|
self.latency_ax.set_title("Latence (ms)")
|
|
self.latency_ax.set_ylim(0, 500)
|
|
self.latency_ax.grid(True, alpha=0.3)
|
|
|
|
self.latency_canvas = FigureCanvasQTAgg(self.latency_fig)
|
|
chart_layout.addWidget(self.latency_canvas)
|
|
|
|
chart_widget.setLayout(chart_layout)
|
|
self.chart_tabs.addTab(chart_widget, "Latence")
|
|
|
|
self.latency_times = []
|
|
self.latency_values = []
|
|
|
|
def _create_speed_chart(self):
|
|
"""Crée le graphique de vitesse"""
|
|
chart_widget = QWidget()
|
|
chart_layout = QVBoxLayout()
|
|
|
|
self.speed_fig = Figure(figsize=(8, 4), dpi=100)
|
|
self.speed_ax = self.speed_fig.add_subplot(111)
|
|
self.speed_ax.set_title("Vitesses (Mbps)")
|
|
self.speed_ax.set_ylim(0, 100)
|
|
self.speed_ax.grid(True, alpha=0.3)
|
|
|
|
self.speed_canvas = FigureCanvasQTAgg(self.speed_fig)
|
|
chart_layout.addWidget(self.speed_canvas)
|
|
|
|
chart_widget.setLayout(chart_layout)
|
|
self.chart_tabs.addTab(chart_widget, "Vitesse")
|
|
|
|
self.speed_times = []
|
|
self.download_values = []
|
|
self.upload_values = []
|
|
|
|
def _create_connectivity_chart(self):
|
|
"""Crée le graphique de connectivité"""
|
|
chart_widget = QWidget()
|
|
chart_layout = QVBoxLayout()
|
|
|
|
self.connectivity_fig = Figure(figsize=(8, 4), dpi=100)
|
|
self.connectivity_ax = self.connectivity_fig.add_subplot(111)
|
|
self.connectivity_ax.set_title("Connectivité")
|
|
self.connectivity_ax.set_ylim(-0.5, 1.5)
|
|
self.connectivity_ax.set_yticks([0, 1])
|
|
self.connectivity_ax.set_yticklabels(['Déconnecté', 'Connecté'])
|
|
self.connectivity_ax.grid(True, alpha=0.3)
|
|
|
|
self.connectivity_canvas = FigureCanvasQTAgg(self.connectivity_fig)
|
|
chart_layout.addWidget(self.connectivity_canvas)
|
|
|
|
chart_widget.setLayout(chart_layout)
|
|
self.chart_tabs.addTab(chart_widget, "Connectivité")
|
|
|
|
self.connectivity_times = []
|
|
self.connectivity_values = []
|
|
|
|
def _update_statistics(self):
|
|
"""Met à jour les statistiques"""
|
|
try:
|
|
stats = self.monitor.get_statistics()
|
|
self.total_tests_label.setText(f"Tests: {stats['total_tests']}")
|
|
self.success_rate_label.setText(f"Succès: {stats['success_rate']:.1f}%")
|
|
self.connection_loss_label.setText(f"Pertes: {stats['connection_loss_count']}")
|
|
self.avg_latency_label.setText(f"Latence avg: {stats['avg_latency']:.1f}ms")
|
|
|
|
# Mettre à jour les graphiques
|
|
self._update_latency_chart()
|
|
self._update_speed_chart()
|
|
self._update_connectivity_chart()
|
|
|
|
except Exception as e:
|
|
print(f"Erreur dans _update_statistics: {e}")
|
|
|
|
def _update_latency_chart(self):
|
|
"""Met à jour le graphique de latence"""
|
|
try:
|
|
self.latency_times.clear()
|
|
self.latency_values.clear()
|
|
|
|
for i, results in enumerate(self.monitor.get_test_history(limit=20)):
|
|
for result in results.values():
|
|
if result.latency_ms is not None:
|
|
self.latency_times.append(i)
|
|
self.latency_values.append(result.latency_ms)
|
|
break
|
|
|
|
if self.latency_times:
|
|
self.latency_ax.clear()
|
|
self.latency_ax.plot(
|
|
self.latency_times,
|
|
self.latency_values,
|
|
'b-',
|
|
linewidth=2
|
|
)
|
|
self.latency_ax.fill_between(
|
|
self.latency_times,
|
|
self.latency_values,
|
|
alpha=0.3
|
|
)
|
|
self.latency_ax.axhline(
|
|
y=self.monitor.config.ping_threshold,
|
|
color='r',
|
|
linestyle='--'
|
|
)
|
|
self.latency_ax.set_title("Latence (ms)")
|
|
self.latency_ax.set_ylim(0, max(max(self.latency_values) * 1.2, 100))
|
|
self.latency_ax.grid(True, alpha=0.3)
|
|
self.latency_canvas.draw()
|
|
except Exception as e:
|
|
print(f"Erreur dans _update_latency_chart: {e}")
|
|
|
|
def _update_speed_chart(self):
|
|
"""Met à jour le graphique de vitesse"""
|
|
try:
|
|
self.speed_times.clear()
|
|
self.download_values.clear()
|
|
self.upload_values.clear()
|
|
|
|
for i, results in enumerate(self.monitor.get_test_history(limit=20)):
|
|
for result in results.values():
|
|
if result.download_speed is not None:
|
|
self.speed_times.append(i)
|
|
self.download_values.append(result.download_speed)
|
|
if result.upload_speed is not None:
|
|
if len(self.upload_values) <= i:
|
|
self.upload_values.append(result.upload_speed)
|
|
else:
|
|
self.upload_values[i] = result.upload_speed
|
|
|
|
if self.speed_times:
|
|
self.speed_ax.clear()
|
|
if self.download_values:
|
|
self.speed_ax.plot(
|
|
self.speed_times,
|
|
self.download_values,
|
|
'g-',
|
|
label='Download',
|
|
linewidth=2
|
|
)
|
|
if self.upload_values:
|
|
self.speed_ax.plot(
|
|
self.speed_times,
|
|
self.upload_values,
|
|
'r-',
|
|
label='Upload',
|
|
linewidth=2
|
|
)
|
|
|
|
self.speed_ax.axhline(
|
|
y=self.monitor.config.download_threshold,
|
|
color='g',
|
|
linestyle='--',
|
|
alpha=0.5
|
|
)
|
|
self.speed_ax.axhline(
|
|
y=self.monitor.config.upload_threshold,
|
|
color='r',
|
|
linestyle='--',
|
|
alpha=0.5
|
|
)
|
|
|
|
self.speed_ax.set_title("Vitesses (Mbps)")
|
|
max_speed = max(
|
|
max(self.download_values or [0]) * 1.2,
|
|
max(self.upload_values or [0]) * 1.2,
|
|
10
|
|
)
|
|
self.speed_ax.set_ylim(0, max_speed)
|
|
self.speed_ax.legend()
|
|
self.speed_ax.grid(True, alpha=0.3)
|
|
self.speed_canvas.draw()
|
|
except Exception as e:
|
|
print(f"Erreur dans _update_speed_chart: {e}")
|
|
|
|
def _update_connectivity_chart(self):
|
|
"""Met à jour le graphique de connectivité"""
|
|
try:
|
|
ch = (self.monitor.connection_history[-20:]
|
|
if len(self.monitor.connection_history) > 20
|
|
else self.monitor.connection_history)
|
|
|
|
self.connectivity_times = list(range(len(ch)))
|
|
self.connectivity_values = [1 if c else 0 for c in ch]
|
|
|
|
if self.connectivity_times:
|
|
self.connectivity_ax.clear()
|
|
self.connectivity_ax.step(
|
|
self.connectivity_times,
|
|
self.connectivity_values,
|
|
where='post',
|
|
color='blue',
|
|
linewidth=2
|
|
)
|
|
|
|
for i in range(len(self.connectivity_times) - 1):
|
|
color = 'red' if self.connectivity_values[i] == 0 else 'green'
|
|
self.connectivity_ax.axvspan(
|
|
self.connectivity_times[i],
|
|
self.connectivity_times[i + 1],
|
|
facecolor=color,
|
|
alpha=0.3
|
|
)
|
|
|
|
self.connectivity_ax.set_title("Connectivité")
|
|
self.connectivity_ax.set_ylim(-0.5, 1.5)
|
|
self.connectivity_ax.set_yticks([0, 1])
|
|
self.connectivity_ax.set_yticklabels(['Déconnecté', 'Connecté'])
|
|
self.connectivity_ax.grid(True, alpha=0.3)
|
|
self.connectivity_canvas.draw()
|
|
except Exception as e:
|
|
print(f"Erreur dans _update_connectivity_chart: {e}")
|
|
|
|
|
|
class StabilityTesterWindow(QMainWindow):
|
|
"""Fenêtre principale de l'application"""
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.setWindowTitle("StabilityTester")
|
|
self.setGeometry(100, 100, 1000, 700)
|
|
self.setMinimumSize(800, 600)
|
|
|
|
# Initialisation
|
|
self.monitor = StabilityMonitor()
|
|
self.tester = NetworkTester()
|
|
self.running = False
|
|
|
|
# Configuration du style
|
|
self._configure_style()
|
|
|
|
# Création de l'interface
|
|
self._create_widgets()
|
|
|
|
# Connexion des signaux
|
|
self.monitor.register_status_callback(self._update_status)
|
|
self.monitor.register_alert_callback(self._show_alert)
|
|
|
|
# Timer pour mise à jour
|
|
self.update_timer = QTimer()
|
|
self.update_timer.timeout.connect(self._update_ui)
|
|
self.update_timer.start(1000) # 1 seconde
|
|
|
|
# Gestion de la fermeture
|
|
self.setWindowIconText("StabilityTester")
|
|
|
|
def _configure_style(self):
|
|
"""Configure le style de l'application"""
|
|
# Utiliser le style Fusion pour un look moderne
|
|
QApplication.setStyle(QStyleFactory.create("Fusion"))
|
|
|
|
# Palette moderne
|
|
palette = QPalette()
|
|
palette.setColor(QPalette.Window, QColor(248, 249, 250))
|
|
palette.setColor(QPalette.WindowText, QColor(51, 51, 51))
|
|
palette.setColor(QPalette.Base, QColor(255, 255, 255))
|
|
palette.setColor(QPalette.AlternateBase, QColor(240, 240, 240))
|
|
palette.setColor(QPalette.ToolTipBase, QColor(255, 255, 255))
|
|
palette.setColor(QPalette.ToolTipText, QColor(0, 0, 0))
|
|
palette.setColor(QPalette.Text, QColor(51, 51, 51))
|
|
palette.setColor(QPalette.Button, QColor(240, 240, 240))
|
|
palette.setColor(QPalette.ButtonText, QColor(51, 51, 51))
|
|
palette.setColor(QPalette.BrightText, QColor(255, 255, 255))
|
|
palette.setColor(QPalette.Link, QColor(0, 100, 180))
|
|
palette.setColor(QPalette.Highlight, QColor(0, 120, 200))
|
|
palette.setColor(QPalette.HighlightedText, QColor(255, 255, 255))
|
|
|
|
QApplication.setPalette(palette)
|
|
|
|
def _create_widgets(self):
|
|
"""Crée tous les widgets"""
|
|
# Widget central
|
|
central_widget = QWidget()
|
|
central_layout = QVBoxLayout()
|
|
central_layout.setContentsMargins(0, 0, 0, 0)
|
|
central_layout.setSpacing(0)
|
|
|
|
# Barre d'outils
|
|
self._create_toolbar(central_layout)
|
|
|
|
# Onglets
|
|
self.tab_widget = QTabWidget()
|
|
central_layout.addWidget(self.tab_widget)
|
|
|
|
# Créer les onglets
|
|
self.dashboard_tab = DashboardTab(self.monitor)
|
|
self.tab_widget.addTab(self.dashboard_tab, "📋 Tableau de bord")
|
|
|
|
self.tests_tab = TestsTab(self.monitor, self.tester)
|
|
self.tab_widget.addTab(self.tests_tab, "🧪 Tests")
|
|
|
|
self.history_tab = HistoryTab(self.monitor)
|
|
self.tab_widget.addTab(self.history_tab, "📜 Historique")
|
|
|
|
self.alerts_tab = AlertsTab(self.monitor)
|
|
self.tab_widget.addTab(self.alerts_tab, "⚠️ Alertes")
|
|
|
|
self.settings_tab = SettingsTab(self.monitor)
|
|
self.tab_widget.addTab(self.settings_tab, "⚙️ Paramètres")
|
|
|
|
self.statistics_tab = StatisticsTab(self.monitor)
|
|
self.tab_widget.addTab(self.statistics_tab, "📈 Statistiques")
|
|
|
|
central_widget.setLayout(central_layout)
|
|
self.setCentralWidget(central_widget)
|
|
|
|
# Mettre à jour l'UI
|
|
self._update_ui()
|
|
|
|
def _create_toolbar(self, layout):
|
|
"""Crée la barre d'outils"""
|
|
toolbar = QWidget()
|
|
toolbar_layout = QHBoxLayout()
|
|
toolbar_layout.setContentsMargins(15, 10, 15, 10)
|
|
toolbar_layout.setSpacing(10)
|
|
|
|
# Bouton Démarrer
|
|
self.start_button = QPushButton("🟢 Démarrer")
|
|
self.start_button.setFixedHeight(35)
|
|
self.start_button.clicked.connect(self._start_monitoring)
|
|
toolbar_layout.addWidget(self.start_button)
|
|
|
|
# Bouton Arrêter
|
|
self.stop_button = QPushButton("🔴 Arrêter")
|
|
self.stop_button.setFixedHeight(35)
|
|
self.stop_button.setEnabled(False)
|
|
self.stop_button.clicked.connect(self._stop_monitoring)
|
|
toolbar_layout.addWidget(self.stop_button)
|
|
|
|
# Bouton Test unique
|
|
test_button = QPushButton("🧪 Test unique")
|
|
test_button.setFixedHeight(35)
|
|
test_button.clicked.connect(self._run_single_test)
|
|
toolbar_layout.addWidget(test_button)
|
|
|
|
# Séparateur
|
|
toolbar_layout.addStretch(1)
|
|
|
|
# Boutons Exporter, Sauvegarder, Charger
|
|
export_button = QPushButton("📤 Exporter")
|
|
export_button.setFixedHeight(35)
|
|
export_button.clicked.connect(self._export_data)
|
|
toolbar_layout.addWidget(export_button)
|
|
|
|
save_button = QPushButton("💾 Sauvegarder")
|
|
save_button.setFixedHeight(35)
|
|
save_button.clicked.connect(self._save_state)
|
|
toolbar_layout.addWidget(save_button)
|
|
|
|
load_button = QPushButton("📂 Charger")
|
|
load_button.setFixedHeight(35)
|
|
load_button.clicked.connect(self._load_state)
|
|
toolbar_layout.addWidget(load_button)
|
|
|
|
# Séparateur
|
|
toolbar_layout.addStretch(1)
|
|
|
|
# Bouton Minimiser
|
|
minimize_button = QPushButton("📉 Minimiser")
|
|
minimize_button.setFixedHeight(35)
|
|
minimize_button.clicked.connect(self.showMinimized)
|
|
toolbar_layout.addWidget(minimize_button)
|
|
|
|
# Bouton Quitter
|
|
quit_button = QPushButton("❌ Quitter")
|
|
quit_button.setFixedHeight(35)
|
|
quit_button.clicked.connect(self.close)
|
|
toolbar_layout.addWidget(quit_button)
|
|
|
|
# Indicateurs d'état
|
|
toolbar_layout.addStretch(1)
|
|
|
|
self.connection_indicator = QLabel("● Déconnecté")
|
|
self.connection_indicator.setStyleSheet("color: #d9534f; font-weight: bold;")
|
|
toolbar_layout.addWidget(self.connection_indicator)
|
|
|
|
self.monitor_indicator = QLabel("● Surveillance: Inactive")
|
|
self.monitor_indicator.setStyleSheet("color: #999; font-weight: bold;")
|
|
toolbar_layout.addWidget(self.monitor_indicator)
|
|
|
|
toolbar.setLayout(toolbar_layout)
|
|
layout.addWidget(toolbar)
|
|
|
|
def _start_monitoring(self):
|
|
"""Démarre la surveillance"""
|
|
if not self.running:
|
|
self.running = True
|
|
self.start_button.setEnabled(False)
|
|
self.stop_button.setEnabled(True)
|
|
self.monitor_indicator.setText("● Surveillance: Active")
|
|
self.monitor_indicator.setStyleSheet("color: #5cb85c; font-weight: bold;")
|
|
self.monitor.start()
|
|
|
|
def _stop_monitoring(self):
|
|
"""Arrête la surveillance"""
|
|
if self.running:
|
|
self.running = False
|
|
self.start_button.setEnabled(True)
|
|
self.stop_button.setEnabled(False)
|
|
self.monitor_indicator.setText("● Surveillance: Inactive")
|
|
self.monitor_indicator.setStyleSheet("color: #999; font-weight: bold;")
|
|
self.monitor.stop()
|
|
|
|
def _run_single_test(self):
|
|
"""Effectue un test unique"""
|
|
def run():
|
|
try:
|
|
results = self.monitor.run_single_test()
|
|
for test_name, result in results.items():
|
|
self.dashboard_tab.update_metrics(result)
|
|
if result.test_type in ["connection", "ping"]:
|
|
if result.success:
|
|
self.connection_indicator.setText("● Connecté")
|
|
self.connection_indicator.setStyleSheet("color: #5cb85c; font-weight: bold;")
|
|
else:
|
|
self.connection_indicator.setText("● Déconnecté")
|
|
self.connection_indicator.setStyleSheet("color: #d9534f; font-weight: bold;")
|
|
except Exception as e:
|
|
print(f"Erreur lors du test: {e}")
|
|
|
|
thread = threading.Thread(target=run, daemon=True)
|
|
thread.start()
|
|
|
|
def _update_status(self, status):
|
|
"""Met à jour l'état"""
|
|
self.dashboard_tab.update_status(status)
|
|
if status.get('connected', False):
|
|
self.connection_indicator.setText("● Connecté")
|
|
self.connection_indicator.setStyleSheet("color: #5cb85c; font-weight: bold;")
|
|
else:
|
|
self.connection_indicator.setText("● Déconnecté")
|
|
self.connection_indicator.setStyleSheet("color: #d9534f; font-weight: bold;")
|
|
|
|
def _show_alert(self, alert):
|
|
"""Affiche une alerte"""
|
|
print(f"[ALERTE {alert.level.value.upper()}] {alert.message}")
|
|
# Clignoter l'onglet Alertes
|
|
if self.tab_widget.currentIndex() != 3:
|
|
self._blink_tab(3)
|
|
|
|
def _blink_tab(self, tab_index):
|
|
"""Fait clignoter un onglet"""
|
|
original_text = self.tab_widget.tabText(tab_index)
|
|
for _ in range(3):
|
|
self.tab_widget.setTabText(tab_index, f"⚠️ {original_text}")
|
|
QApplication.processEvents()
|
|
time.sleep(0.3)
|
|
self.tab_widget.setTabText(tab_index, original_text)
|
|
QApplication.processEvents()
|
|
time.sleep(0.3)
|
|
|
|
def _update_ui(self):
|
|
"""Met à jour l'interface"""
|
|
try:
|
|
# Mettre à jour le score de stabilité
|
|
score_data = self.monitor.get_stability_score()
|
|
self.dashboard_tab.update_score(score_data)
|
|
|
|
# Mettre à jour les statistiques
|
|
self.statistics_tab._update_statistics()
|
|
|
|
# Mettre à jour l'historique
|
|
self.history_tab._refresh()
|
|
|
|
# Mettre à jour les alertes
|
|
self.alerts_tab._refresh()
|
|
|
|
except Exception as e:
|
|
print(f"Erreur dans _update_ui: {e}")
|
|
|
|
def _export_data(self):
|
|
"""Exporte les données"""
|
|
filepath, _ = QFileDialog.getSaveFileName(
|
|
self, "Exporter les données",
|
|
"", "JSON Files (*.json);;All Files (*)"
|
|
)
|
|
if filepath:
|
|
try:
|
|
data = {
|
|
'config': self.monitor.config.to_dict(),
|
|
'status': self.monitor.get_current_status(),
|
|
'stats': self.monitor.get_statistics(),
|
|
'history': [
|
|
{n: r.to_dict() for n, r in res.items()}
|
|
for res in self.monitor.get_test_history()
|
|
],
|
|
'alerts': [
|
|
a.to_dict() for a in self.monitor.alert_handler.get_alert_history()
|
|
],
|
|
'exported_at': datetime.now().isoformat()
|
|
}
|
|
with open(filepath, 'w') as f:
|
|
json.dump(data, f, indent=2)
|
|
QMessageBox.information(self, "Succès", f"Données exportées vers {filepath}")
|
|
except Exception as e:
|
|
QMessageBox.critical(self, "Erreur", f"Erreur lors de l'export: {str(e)}")
|
|
|
|
def _save_state(self):
|
|
"""Sauvegarde l'état"""
|
|
try:
|
|
self.monitor.save_state()
|
|
QMessageBox.information(self, "Succès", "État sauvegardé!")
|
|
except Exception as e:
|
|
QMessageBox.critical(self, "Erreur", f"Erreur lors de la sauvegarde: {str(e)}")
|
|
|
|
def _load_state(self):
|
|
"""Charge l'état"""
|
|
try:
|
|
if self.monitor.load_state():
|
|
QMessageBox.information(self, "Succès", "État chargé!")
|
|
self._update_ui()
|
|
|
|
# Mettre à jour les paramètres de l'interface
|
|
self.settings_tab.interval_spin.setValue(self.monitor.config.interval)
|
|
self.settings_tab.max_history_spin.setValue(self.monitor.config.max_history)
|
|
self.settings_tab.ping_threshold_spin.setValue(self.monitor.config.ping_threshold)
|
|
self.settings_tab.jitter_threshold_spin.setValue(self.monitor.config.jitter_threshold)
|
|
self.settings_tab.download_threshold_spin.setValue(self.monitor.config.download_threshold)
|
|
self.settings_tab.upload_threshold_spin.setValue(self.monitor.config.upload_threshold)
|
|
self.settings_tab.enable_sound_check.setChecked(self.monitor.config.enable_sound)
|
|
self.settings_tab.enable_notifications_check.setChecked(self.monitor.config.enable_notifications)
|
|
for key, cb in self.settings_tab.test_checks.items():
|
|
cb.setChecked(key in self.monitor.config.test_types)
|
|
else:
|
|
QMessageBox.information(self, "Info", "Aucun état trouvé, valeurs par défaut")
|
|
except Exception as e:
|
|
QMessageBox.critical(self, "Erreur", f"Erreur lors du chargement: {str(e)}")
|
|
|
|
def closeEvent(self, event):
|
|
"""Gère la fermeture de la fenêtre"""
|
|
reply = QMessageBox.question(
|
|
self, "Quitter",
|
|
"Voulez-vous vraiment quitter StabilityTester?",
|
|
QMessageBox.Yes | QMessageBox.No
|
|
)
|
|
if reply == QMessageBox.Yes:
|
|
self._stop_monitoring()
|
|
event.accept()
|
|
else:
|
|
event.ignore()
|
|
|
|
|
|
def run_application():
|
|
"""Lance l'application PySide6"""
|
|
app = QApplication(sys.argv)
|
|
app.setApplicationName("StabilityTester")
|
|
app.setApplicationVersion("1.0.1")
|
|
|
|
window = StabilityTesterWindow()
|
|
window.show()
|
|
|
|
sys.exit(app.exec())
|
|
|
|
|
|
if __name__ == "__main__":
|
|
run_application()
|