#!/usr/bin/env python3 """ Automatisches Screenshot-Tool für Mitarbeiterschulungen ====================================================== Dieses Tool erstellt automatisch Screenshots von allen verfügbaren Seiten der Webanwendung für Schulungszwecke und Präsentationsmaterial. Funktionen: - Automatische Erkennung aller verfügbaren Routen - Screenshots in verschiedenen Auflösungen (Desktop, Tablet, Mobile) - Strukturierte Ordnerorganisation für Schulungen - Automatischer Login als Admin - Detaillierter Bericht über erstellte Screenshots - Behandlung von geschützten und öffentlichen Bereichen Autor: KI-Assistant Datum: 2025-01-16 """ import os import sys import time import json import logging from datetime import datetime, timedelta from typing import List, Dict, Tuple, Optional, Set from urllib.parse import urljoin, urlparse import traceback import subprocess # Selenium Imports try: from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.support import expected_conditions as EC from selenium.webdriver.chrome.options import Options as ChromeOptions from selenium.webdriver.firefox.options import Options as FirefoxOptions from selenium.webdriver.chrome.service import Service as ChromeService from selenium.webdriver.firefox.service import Service as FirefoxService from selenium.common.exceptions import TimeoutException, WebDriverException, NoSuchElementException SELENIUM_AVAILABLE = True except ImportError as e: print(f"⚠️ Selenium nicht verfügbar: {e}") print("💡 Installieren Sie Selenium: pip install selenium") SELENIUM_AVAILABLE = False # Flask App Import sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) try: from app import app, User from models import get_db_session FLASK_APP_AVAILABLE = True except ImportError as e: print(f"⚠️ Flask-App nicht verfügbar: {e}") FLASK_APP_AVAILABLE = False # Logging-Konfiguration logging.basicConfig( level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s', handlers=[ logging.FileHandler('screenshot_tool.log'), logging.StreamHandler() ] ) logger = logging.getLogger(__name__) class ScreenshotConfiguration: """Konfiguration für das Screenshot-Tool""" # Basis-Konfiguration BASE_URL = "http://localhost:5000" ADMIN_EMAIL = "admin@example.com" # Anpassen falls nötig ADMIN_PASSWORD = "admin123" # Anpassen falls nötig # Screenshot-Ordner SCREENSHOT_BASE_DIR = os.path.join("docs", "schulung", "screenshots") # Browser-Konfiguration BROWSER_TYPE = "chrome" # "chrome" oder "firefox" HEADLESS = True # Für Server ohne GUI # Auflösungen für verschiedene Geräte RESOLUTIONS = { "desktop": (1920, 1080), "tablet": (1024, 768), "mobile": (375, 667) } # Wartezeiten PAGE_LOAD_TIMEOUT = 15 ELEMENT_WAIT_TIMEOUT = 10 SCREENSHOT_DELAY = 2 # Zu ignorierende Routen (z.B. API-Endpunkte) IGNORED_ROUTES = { "/api/", "/auth/api/", "/static/", "/favicon.ico", "/robots.txt", "/sitemap.xml" } # Routen die spezielle Behandlung benötigen SPECIAL_ROUTES = { "/admin": "Benötigt Admin-Rechte", "/user/": "Benötigt User-Login", "/guest": "Öffentlich zugänglich" } class RouteDiscovery: """Klasse zur automatischen Erkennung aller verfügbaren Routen""" def __init__(self): self.routes = set() self.protected_routes = set() self.public_routes = set() def discover_routes_from_app(self) -> List[str]: """Extrahiert alle Routen aus der Flask-App""" if not FLASK_APP_AVAILABLE: logger.warning("Flask-App nicht verfügbar - verwende Standard-Routen") return self._get_default_routes() discovered_routes = [] try: with app.app_context(): # Alle URL-Regeln der App durchgehen for rule in app.url_map.iter_rules(): # Nur GET-Routen für Screenshots if 'GET' in rule.methods: route = str(rule.rule) # Dynamische Parameter ersetzen if '<' in route: resolved_routes = self._resolve_dynamic_route(route) if resolved_routes: for resolved_route in resolved_routes: discovered_routes.append(resolved_route) self._categorize_route(resolved_route) else: discovered_routes.append(route) self._categorize_route(route) logger.info(f"✅ {len(discovered_routes)} Routen aus Flask-App extrahiert") return discovered_routes except Exception as e: logger.error(f"❌ Fehler beim Extrahieren der Routen: {e}") return self._get_default_routes() def _resolve_dynamic_route(self, route: str) -> Optional[List[str]]: """Löst dynamische Routen mit Beispieldaten auf""" resolved_routes = [] try: if not FLASK_APP_AVAILABLE: return None with app.app_context(): db_session = get_db_session() # User-ID Parameter if '' in route: users = db_session.query(User).limit(3).all() for user in users: resolved_routes.append(route.replace('', str(user.id))) # Printer-ID Parameter elif '' in route: # Beispiel-Printer-IDs (1, 2, 3) for printer_id in [1, 2, 3]: resolved_routes.append(route.replace('', str(printer_id))) # Job-ID Parameter elif '' in route: # Beispiel-Job-IDs (1, 2, 3) for job_id in [1, 2, 3]: resolved_routes.append(route.replace('', str(job_id))) # Andere Parameter mit Standard-Werten elif '<' in route: # Generische Behandlung import re pattern = r'<[^>]+>' resolved_route = re.sub(pattern, '1', route) resolved_routes.append(resolved_route) db_session.close() except Exception as e: logger.warning(f"⚠️ Fehler beim Auflösen der dynamischen Route {route}: {e}") return resolved_routes if resolved_routes else None def _categorize_route(self, route: str): """Kategorisiert Routen als öffentlich oder geschützt""" if any(protected in route for protected in ['/admin', '/user/', '/dashboard']): self.protected_routes.add(route) elif any(public in route for public in ['/guest', '/privacy', '/terms', '/imprint']): self.public_routes.add(route) else: # Standardmäßig als geschützt behandeln self.protected_routes.add(route) def _get_default_routes(self) -> List[str]: """Fallback-Routen falls automatische Erkennung fehlschlägt""" return [ "/", "/dashboard", "/admin", "/admin-dashboard", "/printers", "/jobs", "/jobs/new", "/stats", "/user/profile", "/user/settings", "/admin/users/add", "/admin/printers/add", "/reports", "/maintenance", "/guest", "/guest-status", "/privacy", "/terms", "/imprint", "/demo" ] class BrowserManager: """Verwaltet den Webbrowser für Screenshots""" def __init__(self, config: ScreenshotConfiguration): self.config = config self.driver = None def initialize_browser(self) -> bool: """Initialisiert den Webbrowser""" try: if self.config.BROWSER_TYPE.lower() == "chrome": self.driver = self._setup_chrome() elif self.config.BROWSER_TYPE.lower() == "firefox": self.driver = self._setup_firefox() else: raise ValueError(f"Unbekannter Browser-Typ: {self.config.BROWSER_TYPE}") # Browser-Konfiguration self.driver.set_page_load_timeout(self.config.PAGE_LOAD_TIMEOUT) self.driver.implicitly_wait(self.config.ELEMENT_WAIT_TIMEOUT) logger.info(f"✅ Browser {self.config.BROWSER_TYPE} erfolgreich initialisiert") return True except Exception as e: logger.error(f"❌ Fehler beim Initialisieren des Browsers: {e}") return False def _setup_chrome(self): """Konfiguriert Chrome-Browser""" options = ChromeOptions() if self.config.HEADLESS: options.add_argument('--headless') # Weitere Chrome-Optionen für Stabilität options.add_argument('--no-sandbox') options.add_argument('--disable-dev-shm-usage') options.add_argument('--disable-gpu') options.add_argument('--window-size=1920,1080') options.add_argument('--disable-notifications') options.add_argument('--disable-popup-blocking') try: # Automatische Erkennung des ChromeDriver return webdriver.Chrome(options=options) except Exception as e: logger.warning(f"⚠️ ChromeDriver automatisch nicht gefunden: {e}") logger.info("💡 Installieren Sie ChromeDriver oder verwenden Sie webdriver-manager") raise def _setup_firefox(self): """Konfiguriert Firefox-Browser""" options = FirefoxOptions() if self.config.HEADLESS: options.add_argument('--headless') try: return webdriver.Firefox(options=options) except Exception as e: logger.warning(f"⚠️ GeckoDriver nicht gefunden: {e}") logger.info("💡 Installieren Sie GeckoDriver für Firefox") raise def set_resolution(self, resolution_name: str): """Setzt die Browser-Auflösung""" if resolution_name in self.config.RESOLUTIONS: width, height = self.config.RESOLUTIONS[resolution_name] self.driver.set_window_size(width, height) logger.debug(f"📱 Auflösung gesetzt: {resolution_name} ({width}x{height})") def close(self): """Schließt den Browser""" if self.driver: try: self.driver.quit() logger.info("✅ Browser geschlossen") except Exception as e: logger.warning(f"⚠️ Fehler beim Schließen des Browsers: {e}") class ScreenshotTool: """Hauptklasse für das Screenshot-Tool""" def __init__(self, config: ScreenshotConfiguration = None): self.config = config or ScreenshotConfiguration() self.browser = BrowserManager(self.config) self.route_discovery = RouteDiscovery() self.results = { "success": [], "failed": [], "skipped": [], "total_screenshots": 0, "start_time": None, "end_time": None } def run_screenshot_session(self) -> Dict: """Führt eine komplette Screenshot-Session durch""" logger.info("🚀 Starte automatische Screenshot-Erstellung für Schulungen") self.results["start_time"] = datetime.now() try: # 1. Vorbereitung if not self._prepare_environment(): return self.results # 2. Browser initialisieren if not self.browser.initialize_browser(): return self.results # 3. Routen entdecken routes = self.route_discovery.discover_routes_from_app() logger.info(f"📋 {len(routes)} Routen gefunden") # 4. Login durchführen if not self._perform_admin_login(): logger.error("❌ Admin-Login fehlgeschlagen") return self.results # 5. Screenshots erstellen self._create_screenshots_for_routes(routes) # 6. Bericht generieren self._generate_report() except KeyboardInterrupt: logger.info("⏹️ Screenshot-Session vom Benutzer abgebrochen") except Exception as e: logger.error(f"❌ Unerwarteter Fehler: {e}") logger.debug(traceback.format_exc()) finally: self.browser.close() self.results["end_time"] = datetime.now() return self.results def _prepare_environment(self) -> bool: """Bereitet die Umgebung für Screenshots vor""" try: # Screenshot-Ordner erstellen os.makedirs(self.config.SCREENSHOT_BASE_DIR, exist_ok=True) # Unterordner für verschiedene Kategorien categories = ["admin", "benutzer", "oeffentlich", "alle_auflösungen"] for category in categories: category_dir = os.path.join(self.config.SCREENSHOT_BASE_DIR, category) os.makedirs(category_dir, exist_ok=True) # Auflösungs-Unterordner for resolution in self.config.RESOLUTIONS.keys(): resolution_dir = os.path.join(category_dir, resolution) os.makedirs(resolution_dir, exist_ok=True) logger.info(f"✅ Screenshot-Ordner vorbereitet: {self.config.SCREENSHOT_BASE_DIR}") return True except Exception as e: logger.error(f"❌ Fehler bei der Umgebungsvorbereitung: {e}") return False def _perform_admin_login(self) -> bool: """Führt den Admin-Login durch""" try: login_url = urljoin(self.config.BASE_URL, "/auth/login") self.browser.driver.get(login_url) # Warten bis Login-Formular geladen ist wait = WebDriverWait(self.browser.driver, self.config.ELEMENT_WAIT_TIMEOUT) # Email eingeben email_field = wait.until(EC.presence_of_element_located((By.NAME, "email"))) email_field.clear() email_field.send_keys(self.config.ADMIN_EMAIL) # Passwort eingeben password_field = self.browser.driver.find_element(By.NAME, "password") password_field.clear() password_field.send_keys(self.config.ADMIN_PASSWORD) # Login-Button klicken login_button = self.browser.driver.find_element(By.CSS_SELECTOR, "button[type='submit']") login_button.click() # Warten auf Weiterleitung zum Dashboard wait.until(lambda driver: "/dashboard" in driver.current_url or "/admin" in driver.current_url) logger.info("✅ Admin-Login erfolgreich") return True except TimeoutException: logger.error("❌ Timeout beim Admin-Login") return False except NoSuchElementException as e: logger.error(f"❌ Login-Element nicht gefunden: {e}") return False except Exception as e: logger.error(f"❌ Fehler beim Admin-Login: {e}") return False def _create_screenshots_for_routes(self, routes: List[str]): """Erstellt Screenshots für alle angegebenen Routen""" total_routes = len(routes) for i, route in enumerate(routes, 1): logger.info(f"📸 Screenshot {i}/{total_routes}: {route}") # Route ignorieren falls in Ignore-Liste if any(ignored in route for ignored in self.config.IGNORED_ROUTES): logger.debug(f"⏭️ Route ignoriert: {route}") self.results["skipped"].append({"route": route, "reason": "In Ignore-Liste"}) continue # Screenshots für alle Auflösungen erstellen for resolution_name in self.config.RESOLUTIONS.keys(): try: self._take_screenshot_for_route(route, resolution_name) self.results["total_screenshots"] += 1 except Exception as e: logger.error(f"❌ Fehler bei Screenshot für {route} ({resolution_name}): {e}") self.results["failed"].append({ "route": route, "resolution": resolution_name, "error": str(e) }) def _take_screenshot_for_route(self, route: str, resolution_name: str): """Erstellt einen Screenshot für eine spezifische Route und Auflösung""" # Browser-Auflösung setzen self.browser.set_resolution(resolution_name) # Zur Seite navigieren full_url = urljoin(self.config.BASE_URL, route) self.browser.driver.get(full_url) # Warten bis Seite geladen ist time.sleep(self.config.SCREENSHOT_DELAY) # Kategorie bestimmen category = self._determine_route_category(route) # Dateiname generieren safe_route_name = self._sanitize_filename(route) timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") filename = f"{safe_route_name}_{resolution_name}_{timestamp}.png" # Screenshot-Pfad screenshot_path = os.path.join( self.config.SCREENSHOT_BASE_DIR, category, resolution_name, filename ) # Screenshot erstellen self.browser.driver.save_screenshot(screenshot_path) # Erfolg protokollieren self.results["success"].append({ "route": route, "resolution": resolution_name, "file_path": screenshot_path, "file_size": os.path.getsize(screenshot_path), "timestamp": datetime.now().isoformat() }) logger.debug(f"✅ Screenshot gespeichert: {screenshot_path}") def _determine_route_category(self, route: str) -> str: """Bestimmt die Kategorie einer Route für die Ordnerorganisation""" if any(admin_route in route for admin_route in ['/admin', '/admin-dashboard']): return "admin" elif any(user_route in route for user_route in ['/user/', '/dashboard', '/printers', '/jobs']): return "benutzer" elif any(public_route in route for public_route in ['/guest', '/privacy', '/terms', '/imprint', '/']): return "oeffentlich" else: return "benutzer" # Standard-Kategorie def _sanitize_filename(self, route: str) -> str: """Bereinigt Routennamen für Dateinamen""" import re # Entferne gefährliche Zeichen safe_name = re.sub(r'[<>:"/\\|?*]', '_', route) # Ersetze Slashes mit Unterstrichen safe_name = safe_name.replace('/', '_') # Entferne führende/nachfolgende Unterstriche safe_name = safe_name.strip('_') # Leere Namen behandeln if not safe_name or safe_name == '_': safe_name = "home" return safe_name def _generate_report(self): """Generiert einen detaillierten Bericht über die Screenshot-Session""" duration = self.results["end_time"] - self.results["start_time"] report = { "session_info": { "start_time": self.results["start_time"].isoformat(), "end_time": self.results["end_time"].isoformat(), "duration_seconds": duration.total_seconds(), "duration_formatted": str(duration) }, "statistics": { "total_screenshots": self.results["total_screenshots"], "successful_screenshots": len(self.results["success"]), "failed_screenshots": len(self.results["failed"]), "skipped_routes": len(self.results["skipped"]) }, "results": self.results } # Bericht als JSON speichern report_path = os.path.join(self.config.SCREENSHOT_BASE_DIR, "screenshot_report.json") with open(report_path, 'w', encoding='utf-8') as f: json.dump(report, f, indent=2, ensure_ascii=False, default=str) # Menschenlesbaren Bericht erstellen self._generate_human_readable_report(report) logger.info(f"📊 Bericht gespeichert: {report_path}") def _generate_human_readable_report(self, report: Dict): """Erstellt einen menschenlesbaren Bericht""" report_path = os.path.join(self.config.SCREENSHOT_BASE_DIR, "screenshot_bericht.md") with open(report_path, 'w', encoding='utf-8') as f: f.write("# Screenshot-Bericht für Mitarbeiterschulungen\n\n") f.write(f"**Erstellt am:** {datetime.now().strftime('%d.%m.%Y um %H:%M:%S')}\n\n") # Zusammenfassung f.write("## Zusammenfassung\n\n") f.write(f"- **Gesamte Screenshots:** {report['statistics']['total_screenshots']}\n") f.write(f"- **Erfolgreich:** {report['statistics']['successful_screenshots']}\n") f.write(f"- **Fehlgeschlagen:** {report['statistics']['failed_screenshots']}\n") f.write(f"- **Übersprungen:** {report['statistics']['skipped_routes']}\n") f.write(f"- **Dauer:** {report['session_info']['duration_formatted']}\n\n") # Erfolgreiche Screenshots if self.results["success"]: f.write("## Erfolgreich erstellte Screenshots\n\n") for item in self.results["success"]: f.write(f"- **{item['route']}** ({item['resolution']})\n") f.write(f" - Datei: `{item['file_path']}`\n") f.write(f" - Größe: {item['file_size']:,} Bytes\n\n") # Fehlgeschlagene Screenshots if self.results["failed"]: f.write("## Fehlgeschlagene Screenshots\n\n") for item in self.results["failed"]: f.write(f"- **{item['route']}** ({item['resolution']})\n") f.write(f" - Fehler: {item['error']}\n\n") # Verwendungshinweise f.write("## Verwendung für Schulungen\n\n") f.write("### Ordnerstruktur\n\n") f.write("```\n") f.write("docs/schulung/screenshots/\n") f.write("├── admin/ # Administrator-Bereich\n") f.write("│ ├── desktop/ # Desktop-Auflösung (1920x1080)\n") f.write("│ ├── tablet/ # Tablet-Auflösung (1024x768)\n") f.write("│ └── mobile/ # Mobile-Auflösung (375x667)\n") f.write("├── benutzer/ # Benutzer-Bereich\n") f.write("│ ├── desktop/\n") f.write("│ ├── tablet/\n") f.write("│ └── mobile/\n") f.write("└── oeffentlich/ # Öffentlicher Bereich\n") f.write(" ├── desktop/\n") f.write(" ├── tablet/\n") f.write(" └── mobile/\n") f.write("```\n\n") f.write("### Empfehlungen für Präsentationen\n\n") f.write("1. **Desktop-Screenshots** für Hauptpräsentationen verwenden\n") f.write("2. **Mobile-Screenshots** für Responsive-Design-Demonstrationen\n") f.write("3. **Admin-Ordner** für Administratoren-Schulungen\n") f.write("4. **Benutzer-Ordner** für allgemeine Mitarbeiterschulungen\n") f.write("5. **Öffentlich-Ordner** für Gäste-/Kunden-Präsentationen\n\n") logger.info(f"📝 Menschenlesbarer Bericht gespeichert: {report_path}") def load_config_from_file(config_file: str = "screenshot_config.json") -> ScreenshotConfiguration: """Lädt Konfiguration aus JSON-Datei""" config = ScreenshotConfiguration() if os.path.exists(config_file): try: with open(config_file, 'r', encoding='utf-8') as f: config_data = json.load(f) # Server-Konfiguration laden if 'server' in config_data: server_config = config_data['server'] config.BASE_URL = server_config.get('base_url', config.BASE_URL) config.ADMIN_EMAIL = server_config.get('admin_email', config.ADMIN_EMAIL) config.ADMIN_PASSWORD = server_config.get('admin_password', config.ADMIN_PASSWORD) # Browser-Konfiguration laden if 'browser' in config_data: browser_config = config_data['browser'] config.BROWSER_TYPE = browser_config.get('type', config.BROWSER_TYPE) config.HEADLESS = browser_config.get('headless', config.HEADLESS) config.PAGE_LOAD_TIMEOUT = browser_config.get('page_load_timeout', config.PAGE_LOAD_TIMEOUT) config.ELEMENT_WAIT_TIMEOUT = browser_config.get('element_wait_timeout', config.ELEMENT_WAIT_TIMEOUT) config.SCREENSHOT_DELAY = browser_config.get('screenshot_delay', config.SCREENSHOT_DELAY) # Output-Konfiguration laden if 'output' in config_data: output_config = config_data['output'] config.SCREENSHOT_BASE_DIR = output_config.get('base_directory', config.SCREENSHOT_BASE_DIR) # Auflösungen laden if 'resolutions' in config_data: resolutions_config = config_data['resolutions'] new_resolutions = {} for name, res_data in resolutions_config.items(): if isinstance(res_data, dict) and 'width' in res_data and 'height' in res_data: new_resolutions[name] = (res_data['width'], res_data['height']) if new_resolutions: config.RESOLUTIONS = new_resolutions logger.info(f"✅ Konfiguration aus {config_file} geladen") except Exception as e: logger.warning(f"⚠️ Fehler beim Laden der Konfiguration aus {config_file}: {e}") logger.info("💡 Verwende Standard-Konfiguration") else: logger.info(f"📋 Keine Konfigurationsdatei {config_file} gefunden - verwende Standard-Werte") return config def main(): """Hauptfunktion des Screenshot-Tools""" print("=" * 60) print("🎯 AUTOMATISCHES SCREENSHOT-TOOL FÜR SCHULUNGEN") print("=" * 60) # Abhängigkeiten prüfen if not SELENIUM_AVAILABLE: print("❌ Selenium ist nicht installiert!") print("💡 Führen Sie aus: pip install selenium") return 1 # Konfiguration aus Datei laden config = load_config_from_file() # Benutzer-Eingaben für Konfiguration (optional) print("\n📋 KONFIGURATION") print("-" * 30) base_url = input(f"Server-URL [{config.BASE_URL}]: ").strip() if base_url: config.BASE_URL = base_url admin_email = input(f"Admin-Email [{config.ADMIN_EMAIL}]: ").strip() if admin_email: config.ADMIN_EMAIL = admin_email admin_password = input(f"Admin-Passwort [{config.ADMIN_PASSWORD}]: ").strip() if admin_password: config.ADMIN_PASSWORD = admin_password headless_input = input(f"Headless-Modus (ohne GUI) [{'Ja' if config.HEADLESS else 'Nein'}]: ").strip().lower() if headless_input in ['nein', 'n', 'no', 'false']: config.HEADLESS = False elif headless_input in ['ja', 'j', 'yes', 'true']: config.HEADLESS = True print(f"\n✅ Konfiguration abgeschlossen") print(f"📂 Screenshots werden gespeichert in: {config.SCREENSHOT_BASE_DIR}") # Screenshot-Tool starten tool = ScreenshotTool(config) results = tool.run_screenshot_session() # Ergebnisse anzeigen print("\n" + "=" * 60) print("📊 ERGEBNISSE") print("=" * 60) print(f"✅ Erfolgreich: {len(results['success'])} Screenshots") print(f"❌ Fehlgeschlagen: {len(results['failed'])} Screenshots") print(f"⏭️ Übersprungen: {len(results['skipped'])} Routen") print(f"📸 Gesamt: {results['total_screenshots']} Screenshots") if results["start_time"] and results["end_time"]: duration = results["end_time"] - results["start_time"] print(f"⏱️ Dauer: {duration}") print(f"\n📁 Alle Screenshots verfügbar in:") print(f" {os.path.abspath(config.SCREENSHOT_BASE_DIR)}") return 0 if results["total_screenshots"] > 0 else 1 if __name__ == "__main__": sys.exit(main())