""" 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: Connecté") else: self.connection_status_label.setText("Statut: Déconnecté") 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: {uptime_pct:.1f}%") 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()