From 14eefa18f6148bbae1c6566ce7fb9e3dab441b04 Mon Sep 17 00:00:00 2001 From: Claude Project Manager Date: Mon, 10 Nov 2025 03:09:35 +0100 Subject: [PATCH] Abuse-Schutz fertig --- .claude/settings.local.json | 9 +- ROADMAP.md | 4 +- browser/playwright_manager.py | 34 ++- config/email_config.json | 2 +- .../platform_controllers/base_controller.py | 14 + .../base_worker_thread.py | 16 +- controllers/session_controller.py | 57 +++- database/accounts.db | Bin 389120 -> 389120 bytes main.py | 5 + utils/email_handler.py | 15 +- utils/process_guard.py | 266 ++++++++++++++++++ views/tabs/generator_tab.py | 67 ++++- views/widgets/account_card.py | 97 ++++++- 13 files changed, 553 insertions(+), 33 deletions(-) create mode 100644 utils/process_guard.py diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 44850e9..6a24f8f 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -20,7 +20,14 @@ "Bash(python tests:*)", "Bash(python tests/test_generator_tab_factory.py)", "Bash(git push:*)", - "Bash(git remote set-url:*)" + "Bash(git remote set-url:*)", + "WebSearch", + "Bash(find:*)", + "Bash(mount:*)", + "Read(//mnt/a/**)", + "Read(//mnt/c/Users/Administrator/AppData/Local/Programs/Python/Python310/**)", + "Read(//mnt/c/Users/Administrator/**)", + "Bash(/mnt/c/Users/Administrator/AppData/Local/Programs/Python/Python310/python.exe -c \"\nimport sys\nsys.dont_write_bytecode = True # Verhindere .pyc Erstellung\nimport utils.email_handler as eh\nhandler = eh.EmailHandler()\npw = handler.config[''imap_pass'']\nprint(f''Windows Python lädt Passwort: {pw[:4]}...{pw[-4:]}'')\nif ''GZsg'' in pw:\n print(''✅ NEUES Passwort wird geladen!'')\nelse:\n print(''❌ ALTES Passwort wird noch geladen!'')\n\")" ], "deny": [], "defaultMode": "acceptEdits", diff --git a/ROADMAP.md b/ROADMAP.md index a8edf1c..31ebcee 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -558,8 +558,8 @@ if registration_successful: ## 🛡️ Feature 5: Software Missbrauch Schutz **Priorität:** Hoch -**Status:** Geplant -**Geschätzter Aufwand:** 2-3 Tage +**Status:** ✅ Abgeschlossen (2025-11-10) +**Tatsächlicher Aufwand:** 2 Tage ### Beschreibung Schutzmaßnahmen gegen versehentlichen oder absichtlichen Missbrauch der Software durch parallele Prozesse, zu viele Browser-Instanzen oder wiederholte Fehlversuche. diff --git a/browser/playwright_manager.py b/browser/playwright_manager.py index d858482..ba34ce9 100644 --- a/browser/playwright_manager.py +++ b/browser/playwright_manager.py @@ -21,7 +21,10 @@ class PlaywrightManager: """ Verwaltet Browser-Sitzungen mit Playwright, einschließlich Stealth-Modus und Proxy-Einstellungen. """ - + + # Klassen-Variable: Zählt aktive Browser-Instanzen (Feature 5: Browser-Instanz Schutz) + _active_count = 0 + def __init__(self, headless: bool = False, proxy: Optional[Dict[str, str]] = None, @@ -128,13 +131,23 @@ class PlaywrightManager: def start(self) -> Page: """ Startet die Playwright-Sitzung und gibt die Browser-Seite zurück. - + Returns: Page: Die Browser-Seite + + Raises: + RuntimeError: Wenn bereits eine Browser-Instanz aktiv ist """ if self.page is not None: return self.page - + + # Feature 5: Browser-Instanz Schutz - Nur eine Instanz gleichzeitig + if PlaywrightManager._active_count >= 1: + raise RuntimeError( + "Browser bereits aktiv. Nur eine Browser-Instanz gleichzeitig erlaubt. " + "Beenden Sie den aktuellen Prozess." + ) + try: self.playwright = sync_playwright().start() @@ -240,10 +253,14 @@ class PlaywrightManager: # Neue Seite erstellen self.page = self.context.new_page() - + # Event-Listener für Konsolen-Logs self.page.on("console", lambda msg: logger.debug(f"BROWSER CONSOLE: {msg.text}")) - + + # Feature 5: Browser-Instanz Counter erhöhen + PlaywrightManager._active_count += 1 + logger.info(f"Browser gestartet (aktive Instanzen: {PlaywrightManager._active_count})") + return self.page except Exception as e: @@ -993,7 +1010,12 @@ class PlaywrightManager: except Exception as e2: logger.error(f"Force stop fehlgeschlagen: {e2}") self.playwright = None - + + # Feature 5: Browser-Instanz Counter dekrementieren + if PlaywrightManager._active_count > 0: + PlaywrightManager._active_count -= 1 + logger.info(f"Browser geschlossen (aktive Instanzen: {PlaywrightManager._active_count})") + logger.info("Browser-Sitzung erfolgreich geschlossen") except Exception as e: diff --git a/config/email_config.json b/config/email_config.json index 3aa5d81..e08dda9 100644 --- a/config/email_config.json +++ b/config/email_config.json @@ -2,5 +2,5 @@ "imap_server": "imap.ionos.de", "imap_port": 993, "imap_user": "info@z5m7q9dk3ah2v1plx6ju.com", - "imap_pass": "cz&ie.O9$!:!tYY@" + "imap_pass": "GZsg9:66@a@M%etP" } \ No newline at end of file diff --git a/controllers/platform_controllers/base_controller.py b/controllers/platform_controllers/base_controller.py index 982c0b2..54b9c09 100644 --- a/controllers/platform_controllers/base_controller.py +++ b/controllers/platform_controllers/base_controller.py @@ -112,6 +112,20 @@ class BasePlatformController(QObject): Args: params: Parameter für die Account-Erstellung """ + # Feature 5: Process Guard - Prüfe ob Prozess starten darf + from utils.process_guard import get_guard + guard = get_guard() + + can_start, error_msg = guard.can_start("Account-Erstellung", self.platform_name) + if not can_start: + # Zeige Fehlermeldung + generator_tab = self.get_generator_tab() + generator_tab.show_error(error_msg) + return + + # Guard Lock setzen + guard.start("Account-Erstellung", self.platform_name) + self.logger.info(f"Account-Erstellung für {self.platform_name} gestartet") # In Unterklassen implementieren diff --git a/controllers/platform_controllers/base_worker_thread.py b/controllers/platform_controllers/base_worker_thread.py index 5a9d7df..f18dc12 100644 --- a/controllers/platform_controllers/base_worker_thread.py +++ b/controllers/platform_controllers/base_worker_thread.py @@ -48,6 +48,9 @@ class BaseAccountCreationWorkerThread(QThread): def run(self): """Gemeinsame Logik für Account-Erstellung - IDENTISCH zum Original""" + # Feature 5: Tracke Erfolg für Process Guard + success = False + try: self.update_signal.emit("Status: Initialisierung...") self.log_signal.emit(f"{self.platform_name}-Account-Erstellung gestartet...") @@ -185,6 +188,9 @@ class BaseAccountCreationWorkerThread(QThread): if save_result is not None: result["save_result"] = save_result + # Feature 5: Markiere als erfolgreich für Process Guard + success = True + self.finished_signal.emit(result) else: error_msg = result.get("error", "Unbekannter Fehler") @@ -197,11 +203,17 @@ class BaseAccountCreationWorkerThread(QThread): error_msg = str(e) self.log_signal.emit(f"Schwerwiegender Fehler: {error_msg}") self.log_signal.emit(traceback.format_exc()) - + interpreted_error = self._interpret_error(error_msg) self.error_signal.emit(interpreted_error) self.progress_signal.emit(0) # Reset progress on error - + + finally: + # Feature 5: Process Guard freigeben + from utils.process_guard import get_guard + guard = get_guard() + guard.end(success) + def _interpret_error(self, error_message: str) -> str: """Interpretiert Fehler mit Fuzzy-Matching""" error_lower = error_message.lower() diff --git a/controllers/session_controller.py b/controllers/session_controller.py index 31651f4..de7c095 100644 --- a/controllers/session_controller.py +++ b/controllers/session_controller.py @@ -43,7 +43,7 @@ class SessionController(QObject): def perform_one_click_login(self, account_data: Dict[str, Any]): """ Führt Ein-Klick-Login für einen Account durch. - + Args: account_data: Dict mit Account-Daten inkl. id, platform, username, etc. """ @@ -51,8 +51,23 @@ class SessionController(QObject): platform = account_data.get("platform", "") username = account_data.get("username", "") logger.info(f"Ein-Klick-Login für Account {username} (ID: {account_id}) auf {platform}") + + # Feature 5: Process Guard - Prüfe ob Login starten darf + from utils.process_guard import get_guard + guard = get_guard() + + can_start, error_msg = guard.can_start("Account-Login", platform) + if not can_start: + logger.warning(f"Login blockiert durch Guard: {error_msg}") + self.login_failed.emit(account_id, error_msg) + return + + # Guard Lock setzen + guard.start("Account-Login", platform) + logger.info(f"Guard locked für Login: {platform}") + self.login_started.emit(account_id) - + try: # Stelle sicher, dass Account einen Fingerprint hat fingerprint_id = account_data.get("fingerprint_id") @@ -103,19 +118,39 @@ class SessionController(QObject): else: error_msg = f"Account mit ID {account_id} nicht gefunden" logger.error(error_msg) + # Feature 5: Guard freigeben da Worker nicht gestartet wird + guard.end(success=False) self.login_failed.emit(account_id, error_msg) - + except Exception as e: logger.error(f"Fehler beim Ein-Klick-Login: {e}") + # Feature 5: Guard freigeben bei Fehler vor Worker-Start + guard.end(success=False) self.login_failed.emit(account_id, str(e)) def _cancel_login(self, account_id: str): """Bricht den Login-Prozess ab""" logger.info(f"Login für Account {account_id} wurde abgebrochen") + + # Feature 5: Guard freigeben bei Cancel + # HINWEIS: Worker Thread gibt Guard in seinem finally-Block frei + # Nur freigeben wenn Worker noch nicht gestartet (Guard locked aber kein Worker) + from utils.process_guard import get_guard + guard = get_guard() + if guard.is_locked() and not (hasattr(self, 'login_worker') and self.login_worker and self.login_worker.isRunning()): + logger.warning("Guard war locked aber Worker nicht aktiv - gebe Guard frei") + guard.end(success=False) + if hasattr(self, 'login_dialog') and self.login_dialog: self.login_dialog.close() self.login_dialog = None - # TODO: Login-Worker stoppen falls vorhanden + + # Login-Worker stoppen falls vorhanden + if hasattr(self, 'login_worker') and self.login_worker: + if self.login_worker.isRunning(): + logger.info("Stoppe laufenden Login-Worker") + self.login_worker.terminate() + self.login_worker.wait(2000) # Warte max 2 Sekunden def create_and_save_account(self, platform: str, account_data: Dict[str, Any]): """ @@ -220,6 +255,9 @@ class SessionController(QObject): self.automation = automation # Verwende bereitgestellte Automation oder erstelle neue def run(self): + # Feature 5: Tracke Erfolg für Process Guard + success = False + try: # Verwende bereitgestellte Automation oder erstelle neue if not self.automation: @@ -298,13 +336,22 @@ class SessionController(QObject): if result['success']: # Session-Speicherung komplett entfernt - nur Login-Erfolg melden logger.info(f"Login erfolgreich für Account {self.account_id} - Session-Speicherung deaktiviert") + # Feature 5: Markiere als erfolgreich für Process Guard + success = True self.login_completed.emit(self.account_id, result) else: self.login_failed.emit(self.account_id, result.get('error', 'Login fehlgeschlagen')) - + except Exception as e: logger.error(f"Fehler beim normalen Login: {e}") self.login_failed.emit(self.account_id, str(e)) + + finally: + # Feature 5: Process Guard freigeben + from utils.process_guard import get_guard + guard = get_guard() + guard.end(success) + logger.info(f"Guard freigegeben nach Login (success={success})") def cleanup(self): """Browser NICHT schließen - User soll Kontrolle behalten""" diff --git a/database/accounts.db b/database/accounts.db index be920cae1f9ed16ace7759262f9f7a6f128aa5b6..aeb978d75f23dba1923ecf662bf48f15a6d0d5c3 100644 GIT binary patch delta 110 zcmV-!0FnQI;1_`47mynPxRD$~0l0x+g3;1BGA&wO|FLUmy(y`2Y_>4x|nb z4Pp$r3?mDL3O@)W23;1BGA&wO|FLUmy+z`2Y_>4x|nb z4Pp$r3?mDL3giku2qXyT2WbYd1^KfPFx3UO`DF!_0|Ed5w~%QCQ~?Wt9%K*z88IPW Q040}@YXug!1#1QSgutmHJpcdz diff --git a/main.py b/main.py index b9aa71d..9287a46 100644 --- a/main.py +++ b/main.py @@ -29,6 +29,11 @@ def main(): logger = setup_logger() logger.info("Anwendung wird gestartet...") print("\n=== AccountForger wird gestartet ===") + + # Feature 5: Process Guard initialisieren + from utils.process_guard import get_guard + get_guard().reset() + logger.info("Process Guard initialisiert") # QApplication erstellen app = QApplication(sys.argv) diff --git a/utils/email_handler.py b/utils/email_handler.py index fc4ba28..9a8fd6f 100644 --- a/utils/email_handler.py +++ b/utils/email_handler.py @@ -106,7 +106,7 @@ class EmailHandler: "imap_server": "imap.ionos.de", "imap_port": 993, "imap_user": "info@z5m7q9dk3ah2v1plx6ju.com", - "imap_pass": "cz&ie.O9$!:!tYY@" + "imap_pass": "GZsg9:66@a@M%etP" } try: @@ -225,10 +225,13 @@ class EmailHandler: """ try: logger.info(f"Teste Verbindung zu {self.config['imap_server']}:{self.config['imap_port']}") - + + # DEBUG: Zeige geladene Config + logger.info(f"IMAP Config geladen: Server={self.config['imap_server']}, Port={self.config['imap_port']}, User={self.config['imap_user']}, Pass={self.config['imap_pass'][:4]}...{self.config['imap_pass'][-4:]}") + # SSL-Verbindung zum IMAP-Server herstellen mail = imaplib.IMAP4_SSL(self.config["imap_server"], self.config["imap_port"]) - + # Anmelden mail.login(self.config["imap_user"], self.config["imap_pass"]) @@ -292,9 +295,13 @@ class EmailHandler: List[Dict[str, Any]]: Liste der gefundenen E-Mails """ try: + # DEBUG: Zeige Login-Daten SOFORT (ohne komplettes Passwort) + print(f"[EMAIL-DEBUG] IMAP Login-Versuch: Server={self.config['imap_server']}, Port={self.config['imap_port']}, User={self.config['imap_user']}, Pass={self.config['imap_pass'][:4]}...{self.config['imap_pass'][-4:]}") + logger.info(f"IMAP Login-Versuch: Server={self.config['imap_server']}, Port={self.config['imap_port']}, User={self.config['imap_user']}, Pass={self.config['imap_pass'][:4]}...{self.config['imap_pass'][-4:]}") + # Verbindung zum IMAP-Server herstellen mail = imaplib.IMAP4_SSL(self.config["imap_server"], self.config["imap_port"]) - + # Anmelden mail.login(self.config["imap_user"], self.config["imap_pass"]) diff --git a/utils/process_guard.py b/utils/process_guard.py new file mode 100644 index 0000000..15835e6 --- /dev/null +++ b/utils/process_guard.py @@ -0,0 +1,266 @@ +""" +Process Guard - Schützt vor parallelen Prozessen und Fehler-Spam. + +Dieser Guard verhindert: +- Parallele Prozesse (nur ein Vorgang gleichzeitig) +- Zu viele Fehlversuche (Zwangspause nach 3 Fehlern) +- Mehrere Browser-Instanzen gleichzeitig + +Clean Code & YAGNI: Nur das Nötigste, keine Über-Engineering. +""" + +import json +import logging +from datetime import datetime, timedelta +from pathlib import Path +from typing import Optional, Tuple + +logger = logging.getLogger(__name__) + + +class ProcessGuard: + """ + Einfacher Guard für Prozess-Locks und Fehler-Tracking. + + Verantwortlichkeiten: + - Process Lock Management (nur ein Prozess gleichzeitig) + - Fehler-Tracking (Zwangspause nach 3 Fehlern) + - Persistierung der Pause-Zeit über Neustarts + """ + + # Konfiguration + MAX_FAILURES = 3 + PAUSE_DURATION_HOURS = 1 + + def __init__(self): + """Initialisiert den Process Guard.""" + # Process Lock + self._is_locked = False + self._current_process = None + self._current_platform = None + + # Error Tracking + self._failure_count = 0 + self._pause_until = None + + # Config File + self._config_file = Path("config/.process_guard") + + def can_start(self, process_type: str, platform: str) -> Tuple[bool, Optional[str]]: + """ + Prüft ob ein Prozess gestartet werden darf. + + Args: + process_type: Art des Prozesses (z.B. "Account-Erstellung", "Login") + platform: Plattform (z.B. "Instagram", "Facebook") + + Returns: + (erlaubt: bool, fehler_nachricht: Optional[str]) + - (True, None) wenn erlaubt + - (False, "Fehlermeldung") wenn blockiert + """ + # 1. Prüfe Zwangspause + if self._is_paused(): + remaining_min = self._get_pause_remaining_minutes() + error_msg = ( + f"⏸ Zwangspause aktiv\n\n" + f"Nach 3 fehlgeschlagenen Versuchen ist eine Pause erforderlich.\n" + f"Verbleibende Zeit: {remaining_min} Minuten\n\n" + f"Empfehlung:\n" + f"• Proxy-Einstellungen prüfen\n" + f"• Internetverbindung prüfen\n" + f"• Plattform-Status überprüfen" + ) + return False, error_msg + + # 2. Prüfe Process Lock + if self._is_locked: + error_msg = ( + f"⚠ Prozess läuft bereits\n\n" + f"Aktuell aktiv: {self._current_process} ({self._current_platform})\n\n" + f"Bitte warten Sie bis der aktuelle Vorgang abgeschlossen ist." + ) + return False, error_msg + + return True, None + + def start(self, process_type: str, platform: str): + """ + Startet einen Prozess (setzt den Lock). + + Args: + process_type: Art des Prozesses + platform: Plattform + """ + self._is_locked = True + self._current_process = process_type + self._current_platform = platform + logger.info(f"Process locked: {process_type} ({platform})") + + def end(self, success: bool): + """ + Beendet einen Prozess (gibt den Lock frei). + + Args: + success: War der Prozess erfolgreich? + """ + # Lock freigeben + process_info = f"{self._current_process} ({self._current_platform})" + self._is_locked = False + self._current_process = None + self._current_platform = None + + # Fehler-Tracking + if success: + if self._failure_count > 0: + logger.info(f"Fehler-Counter zurückgesetzt nach Erfolg (war: {self._failure_count})") + self._failure_count = 0 + self._save_pause_state() + else: + self._failure_count += 1 + logger.warning(f"Fehlschlag #{self._failure_count} bei {process_info}") + + if self._failure_count >= self.MAX_FAILURES: + self._activate_pause() + + logger.info(f"Process unlocked: {process_info} (success={success})") + + def reset(self): + """ + Reset beim App-Start. + Lädt Pause-State, resettet aber Lock (da Lock nicht über Neustarts persistiert). + """ + self._is_locked = False + self._current_process = None + self._current_platform = None + self._load_pause_state() + + if self._is_paused(): + remaining = self._get_pause_remaining_minutes() + logger.warning(f"Zwangspause aktiv: noch {remaining} Minuten") + + logger.info("Process Guard initialisiert") + + def is_locked(self) -> bool: + """ + Gibt zurück ob aktuell ein Prozess läuft. + + Returns: + True wenn ein Prozess aktiv ist + """ + return self._is_locked + + def is_paused(self) -> bool: + """ + Gibt zurück ob Zwangspause aktiv ist. + + Returns: + True wenn Pause aktiv ist + """ + return self._is_paused() + + def get_status_message(self) -> Optional[str]: + """ + Gibt Status-Nachricht zurück wenn blockiert. + + Returns: + None wenn nicht blockiert, sonst Nachricht + """ + if self._is_paused(): + remaining = self._get_pause_remaining_minutes() + return f"Zwangspause aktiv (noch {remaining} Min)" + + if self._is_locked: + return f"'{self._current_process}' läuft" + + return None + + # Private Methoden + + def _is_paused(self) -> bool: + """Prüft ob Zwangspause aktiv ist.""" + if not self._pause_until: + return False + + # Prüfe ob Pause abgelaufen + if datetime.now() >= self._pause_until: + logger.info("Zwangspause ist abgelaufen") + self._pause_until = None + self._failure_count = 0 + self._save_pause_state() + return False + + return True + + def _activate_pause(self): + """Aktiviert Zwangspause.""" + self._pause_until = datetime.now() + timedelta(hours=self.PAUSE_DURATION_HOURS) + self._save_pause_state() + + pause_until_str = self._pause_until.strftime('%H:%M') + logger.error( + f"⏸ ZWANGSPAUSE AKTIVIERT bis {pause_until_str} " + f"nach {self.MAX_FAILURES} Fehlschlägen" + ) + + def _get_pause_remaining_minutes(self) -> int: + """Gibt verbleibende Minuten der Pause zurück.""" + if not self._pause_until: + return 0 + + remaining_seconds = (self._pause_until - datetime.now()).total_seconds() + remaining_minutes = max(0, int(remaining_seconds / 60)) + return remaining_minutes + + def _save_pause_state(self): + """Speichert Pause-State in Datei (nur wenn nötig).""" + try: + # Erstelle config-Verzeichnis falls nicht vorhanden + self._config_file.parent.mkdir(exist_ok=True) + + data = { + 'pause_until': self._pause_until.isoformat() if self._pause_until else None, + 'failure_count': self._failure_count, + 'last_update': datetime.now().isoformat() + } + + self._config_file.write_text(json.dumps(data, indent=2)) + logger.debug(f"Pause-State gespeichert: {data}") + + except Exception as e: + logger.error(f"Fehler beim Speichern des Pause-State: {e}") + + def _load_pause_state(self): + """Lädt Pause-State aus Datei.""" + if not self._config_file.exists(): + logger.debug("Keine gespeicherte Pause-State gefunden") + return + + try: + data = json.loads(self._config_file.read_text()) + + # Lade Pause-Zeit + if data.get('pause_until'): + self._pause_until = datetime.fromisoformat(data['pause_until']) + self._failure_count = data.get('failure_count', 0) + logger.info(f"Pause-State geladen: Pause bis {self._pause_until}, Failures: {self._failure_count}") + + except Exception as e: + logger.error(f"Fehler beim Laden des Pause-State: {e}") + + +# Globale Instanz (YAGNI: Kein komplexes Singleton-Pattern nötig) +_guard_instance = None + + +def get_guard() -> ProcessGuard: + """ + Gibt die globale ProcessGuard-Instanz zurück. + + Returns: + ProcessGuard: Die globale Guard-Instanz + """ + global _guard_instance + if _guard_instance is None: + _guard_instance = ProcessGuard() + return _guard_instance diff --git a/views/tabs/generator_tab.py b/views/tabs/generator_tab.py index c6945a1..b8df8d2 100644 --- a/views/tabs/generator_tab.py +++ b/views/tabs/generator_tab.py @@ -11,7 +11,7 @@ from PyQt5.QtWidgets import ( QCheckBox, QComboBox, QPushButton, QProgressBar, QMessageBox, QSizePolicy, QSpacerItem ) -from PyQt5.QtCore import Qt, pyqtSignal +from PyQt5.QtCore import Qt, pyqtSignal, QTimer logger = logging.getLogger("generator_tab") @@ -34,6 +34,14 @@ class GeneratorTab(QWidget): self.language_manager.language_changed.connect(self.update_texts) self.update_texts() + # Feature 5: Timer für periodische Guard-Status-Prüfung + self.guard_check_timer = QTimer() + self.guard_check_timer.timeout.connect(self.update_button_states) + self.guard_check_timer.start(2000) # Alle 2 Sekunden prüfen + + # Feature 5: Initiale Button-Status-Prüfung nach UI-Initialisierung + self.update_button_states() + def init_ui(self): """Initialisiert die Benutzeroberfläche.""" layout = QVBoxLayout(self) @@ -114,9 +122,18 @@ class GeneratorTab(QWidget): def on_start_clicked(self): """Wird aufgerufen, wenn der Start-Button geklickt wird.""" + # Feature 5: Prüfe Guard-Status vor Start + from utils.process_guard import get_guard + guard = get_guard() + + can_start, error_msg = guard.can_start("Account-Erstellung", "Unknown") + if not can_start: + self.show_error(error_msg) + return + # Parameter sammeln params = self.get_params() - + # Eingaben validieren valid, error_msg = self.validate_inputs(params) if not valid: @@ -125,7 +142,7 @@ class GeneratorTab(QWidget): # Status aktualisieren self.set_status("Starte Account-Erstellung...") - + # Signal auslösen self.start_requested.emit(params) @@ -237,15 +254,53 @@ class GeneratorTab(QWidget): def set_running(self, running: bool): """Setzt den Status auf 'Wird ausgeführt' oder 'Bereit'.""" - self.start_button.setEnabled(not running) - self.stop_button.setEnabled(running) - + # Feature 5: Prüfe Guard-Status bevor Buttons aktiviert werden + if not running: + # Prozess ist beendet, prüfe ob Guard erlaubt + self.update_button_states() + else: + # Prozess läuft + self.start_button.setEnabled(False) + self.stop_button.setEnabled(True) + if running: self.set_status("Läuft...") else: self.progress_bar.setValue(0) self.progress_bar.hide() + def update_button_states(self): + """ + Feature 5: Aktualisiert Button-Status basierend auf Process Guard. + Deaktiviert Buttons wenn Guard locked ist oder Pause aktiv ist. + """ + from utils.process_guard import get_guard + guard = get_guard() + + # Prüfe ob blockiert + is_locked = guard.is_locked() + is_paused = guard.is_paused() + is_blocked = is_locked or is_paused + + # Buttons entsprechend setzen + self.start_button.setEnabled(not is_blocked) + self.stop_button.setEnabled(False) # Stop nur wenn running + + # Tooltip setzen + if is_blocked: + status_msg = guard.get_status_message() + self.start_button.setToolTip(f"⚠ {status_msg}") + else: + # Reset Tooltip + if self.language_manager: + tooltip = self.language_manager.get_text( + "buttons.create", + "Account erstellen" + ) + else: + tooltip = "Account erstellen" + self.start_button.setToolTip(tooltip) + def set_progress(self, value: int): """Setzt den Fortschritt der Fortschrittsanzeige.""" if value > 0: diff --git a/views/widgets/account_card.py b/views/widgets/account_card.py index fb13b3e..70a1da9 100644 --- a/views/widgets/account_card.py +++ b/views/widgets/account_card.py @@ -34,20 +34,75 @@ class AccountCard(QFrame): self.email_copy_timer.timeout.connect(self._restore_email_copy_icon) self.password_copy_timer = QTimer() self.password_copy_timer.timeout.connect(self._restore_password_copy_icon) - + + # Feature 5: Timer für periodische Guard-Status-Prüfung + self.guard_check_timer = QTimer() + self.guard_check_timer.timeout.connect(self.update_login_button_state) + self.guard_check_timer.start(2000) # Alle 2 Sekunden prüfen + # Original Icons speichern self.copy_icon = None self.check_icon = None - + self.init_ui() - + if self.language_manager: self.language_manager.language_changed.connect(self.update_texts) self.update_texts() + + # Feature 5: Initiale Button-Status-Prüfung + self.update_login_button_state() def _on_login_clicked(self): """Handler für Login-Button""" + # Feature 5: Prüfe Guard-Status vor Login + from utils.process_guard import get_guard + guard = get_guard() + + platform = self.account_data.get("platform", "Unknown") + can_start, error_msg = guard.can_start("Account-Login", platform) + + if not can_start: + # Zeige benutzerfreundliche Fehlermeldung + from views.widgets.modern_message_box import show_error + show_error(self, "Login blockiert", error_msg) + return + self.login_requested.emit(self.account_data) + + def _on_export_clicked(self): + """Handler für Export-Button""" + # Feature 5: Prüfe Guard-Status vor Export + from utils.process_guard import get_guard + guard = get_guard() + + platform = self.account_data.get("platform", "Unknown") + can_start, error_msg = guard.can_start("Profil-Export", platform) + + if not can_start: + # Zeige benutzerfreundliche Fehlermeldung + from views.widgets.modern_message_box import show_error + show_error(self, "Export blockiert", error_msg) + return + + self.export_requested.emit(self.account_data) + + def _on_delete_clicked(self): + """Handler für Delete-Button""" + # Feature 5: Prüfe Guard-Status vor Löschen + from utils.process_guard import get_guard + guard = get_guard() + + platform = self.account_data.get("platform", "Unknown") + can_start, error_msg = guard.can_start("Account-Löschen", platform) + + if not can_start: + # Zeige benutzerfreundliche Fehlermeldung + from views.widgets.modern_message_box import show_error + show_error(self, "Löschen blockiert", error_msg) + return + + self.delete_requested.emit(self.account_data) def init_ui(self): """Initialisiert die UI nach Styleguide""" @@ -191,7 +246,7 @@ class AccountCard(QFrame): self.export_btn = QPushButton("Profil\nexportieren") self.export_btn.setCursor(Qt.PointingHandCursor) self.export_btn.setObjectName("account_export_btn") # For QSS targeting - self.export_btn.clicked.connect(lambda: self.export_requested.emit(self.account_data)) + self.export_btn.clicked.connect(self._on_export_clicked) actions_layout.addWidget(self.export_btn) actions_layout.addStretch() @@ -200,7 +255,7 @@ class AccountCard(QFrame): self.delete_btn = QPushButton("Löschen") self.delete_btn.setCursor(Qt.PointingHandCursor) self.delete_btn.setObjectName("account_delete_btn") # For QSS targeting - self.delete_btn.clicked.connect(lambda: self.delete_requested.emit(self.account_data)) + self.delete_btn.clicked.connect(self._on_delete_clicked) actions_layout.addWidget(self.delete_btn) layout.addLayout(actions_layout) @@ -239,7 +294,37 @@ class AccountCard(QFrame): # Timer starten um nach 2 Sekunden zurückzuwechseln self.email_copy_timer.stop() self.email_copy_timer.start(2000) - + + def update_login_button_state(self): + """ + Feature 5: Aktualisiert ALLE Button-Status basierend auf Process Guard. + Deaktiviert Login, Export und Delete wenn Guard locked ist oder Pause aktiv ist. + """ + from utils.process_guard import get_guard + guard = get_guard() + + # Prüfe ob blockiert + is_locked = guard.is_locked() + is_paused = guard.is_paused() + is_blocked = is_locked or is_paused + + # Alle Buttons entsprechend setzen + self.login_btn.setEnabled(not is_blocked) + self.export_btn.setEnabled(not is_blocked) + self.delete_btn.setEnabled(not is_blocked) + + # Tooltips setzen + if is_blocked: + status_msg = guard.get_status_message() + self.login_btn.setToolTip(f"⚠ {status_msg}") + self.export_btn.setToolTip(f"⚠ {status_msg}") + self.delete_btn.setToolTip(f"⚠ {status_msg}") + else: + # Reset Tooltips + self.login_btn.setToolTip("Login durchführen") + self.export_btn.setToolTip("Profil exportieren") + self.delete_btn.setToolTip("Account löschen") + def update_texts(self): """Aktualisiert die Texte gemäß der aktuellen Sprache""" if not self.language_manager: