"""PBP Dashboard Launcher - startet das Web-Dashboard auf Port 8200.""" import os, shutil, subprocess, sys # Die Ausgabe nennt Pfade, und die tragen den Benutzernamen: ein Zeichen ausserhalb der Zeichentabelle (ł, ş) darf das Drucken # bei umgeleiteter Ausgabe nicht zum Absturz bringen (L44). for _strom in (sys.stdout, sys.stderr): try: _strom.reconfigure(errors="backslashreplace") except Exception: pass # Finde src-Verzeichnis relativ zu diesem Script SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) SRC_DIR = os.path.join(SCRIPT_DIR, "src") sys.path.insert(0, SRC_DIR) # Datenverzeichnis setzen os.environ.setdefault( "BA_DATA_DIR", os.path.join(os.environ.get("LOCALAPPDATA", os.path.expanduser("~")), "BewerbungsAssistent", "data") ) # Zentrales Logging aktivieren (schreibt in %BA_DATA_DIR%/logs/pbp.log) from bewerbungs_assistent.logging_config import setup_logging, get_log_path # Im Fenster stehen nur Warnungen und Fehler; die Log-Datei bekommt weiter alles (PP14: Protokollzeilen schoben sich in die Frage # "Claude jetzt neu starten?", und der erste Start zeigte Dutzende Zeilen "Safety-Net ... nachgezogen"). Wer alles sehen will: # BA_CONSOLE_LEVEL=INFO setzen. setup_logging(console=True, console_level=os.environ.get("BA_CONSOLE_LEVEL", "WARNING")) import logging logger = logging.getLogger("bewerbungs_assistent") data_dir = os.environ["BA_DATA_DIR"] log_path = get_log_path() logger.info("=== PBP Dashboard Start ===") logger.info("Python: %s", sys.version) logger.info("Daten: %s", data_dir) logger.info("Log: %s", log_path) def _find_chrome() -> str | None: """Find Chrome executable. Supports Windows, macOS and Linux.""" if sys.platform == "darwin": mac_chrome = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" if os.path.isfile(mac_chrome): return mac_chrome return shutil.which("google-chrome") or shutil.which("chromium-browser") if sys.platform != "win32": return shutil.which("google-chrome") or shutil.which("chromium-browser") # Typische Chrome-Installationspfade auf Windows candidates = [ os.path.join(os.environ.get("PROGRAMFILES", ""), "Google", "Chrome", "Application", "chrome.exe"), os.path.join(os.environ.get("PROGRAMFILES(X86)", ""), "Google", "Chrome", "Application", "chrome.exe"), os.path.join(os.environ.get("LOCALAPPDATA", ""), "Google", "Chrome", "Application", "chrome.exe"), ] for path in candidates: if path and os.path.isfile(path): return path return None def _open_in_chrome(url: str) -> None: """Open URL in Chrome if available, otherwise fall back to system default.""" # Proben und Tests (Auto-Update, #1093) starten das Dashboard als echten Prozess, ohne dass sich ein Fenster # des Rechners oeffnet; ebenso der Installer, der den Browser selbst oeffnet, sobald das Dashboard antwortet # (sonst entstuende ein zweiter Tab, PP15). Fuer Menschen, die das Dashboard selbst starten, ist die Variable nie gesetzt. if os.environ.get("PBP_KEIN_BROWSER"): logger.info("PBP_KEIN_BROWSER gesetzt - der Browser wird nicht geoeffnet") return chrome = _find_chrome() if chrome: logger.info("Oeffne Dashboard in Chrome: %s", chrome) try: subprocess.Popen([chrome, url], start_new_session=True) return except Exception as e: logger.warning("Chrome-Start fehlgeschlagen: %s — nutze Fallback", e) # Fallback: System-Standard-Browser logger.info("Chrome nicht gefunden — oeffne im Standard-Browser") if sys.platform == "win32": os.startfile(url) else: import webbrowser webbrowser.open(url) def _enter_zum_schliessen() -> None: """Haelt das Fenster offen, wenn ein Mensch davor sitzt. Ohne Eingabe (NUL, Pipe) endet es sofort, nie mit einem Fehler.""" if sys.stdin is not None and sys.stdin.isatty(): try: input(" Druecke Enter zum Schliessen...") except (EOFError, OSError): pass def _ohne_schuld(grund: str) -> None: """Dieser Start endet am Port, nicht an der Fassung: er zaehlt beim Startbaustein nicht als Fehlstart (#1290).""" try: from bewerbungs_assistent.services.auto_update.start_zaehler import start_ohne_schuld start_ohne_schuld(grund) except Exception as e: # nie den Start daran scheitern lassen logger.warning("Startzaehler nicht angepasst: %s", e) try: import socket port = int(os.environ.get("BA_DASHBOARD_PORT", "8200")) # Prüfe ob der Port bereits belegt ist (z.B. durch Claude Desktop) from bewerbungs_assistent.services.dashboard_halter import port_belegt if port_belegt(port): # nur eine abgelehnte Verbindung heisst frei (#1290) # #1149/#1290: ein belegter Port ist noch kein laufendes PBP, und "keine Antwort" ist nicht "fremd". # Hier entscheidet `port_pruefen`: PBP antwortet / frei geworden (haengender eigener Prozess beendet) / bleibt belegt. from bewerbungs_assistent.services.port_selbsthilfe import port_pruefen, PBP, FREI ergebnis = port_pruefen(port) if ergebnis == FREI: logger.info("Port %d ist wieder frei, das Dashboard startet hier", port) elif ergebnis != PBP: # Der Start endet am Port, PBP lief nie an: das ist kein Fehlstart dieser Fassung (#1290). _ohne_schuld("port_belegt") _enter_zum_schliessen() sys.exit(1) else: print() print(" ====================================================") print(f" PBP laeuft bereits auf http://localhost:{port}") print() print(" Das passiert, wenn Claude Desktop PBP als MCP-Server") print(" gestartet hat. Das Dashboard ist schon erreichbar!") print() print(f" Oeffne einfach: http://localhost:{port}") print() print(" Falls du PBP ohne Claude starten willst:") print(" 1. Schliesse Claude Desktop") print(" 2. Starte PBP erneut ueber diesen Link") print(" ====================================================") print() _ohne_schuld("pbp_laeuft_bereits") # Browser trotzdem öffnen, damit der Nutzer direkt zum Dashboard kommt _open_in_chrome(f"http://localhost:{port}") _enter_zum_schliessen() sys.exit(0) from bewerbungs_assistent.database import Database from bewerbungs_assistent.dashboard import start_dashboard db = Database() db.initialize() logger.info("Datenbank initialisiert") print() print(f" Dashboard: http://localhost:{port}") print(f" Daten: {data_dir}") print(f" Log: {log_path}") quit_hint = "Strg+C" if sys.platform != "darwin" else "Ctrl+C oder Cmd+Q" print(f" Beenden: Dieses Fenster schliessen oder {quit_hint}") print() # Der Browser oeffnet sich erst, wenn der Server antwortet (PP15): vorher stand der Aufruf HIER, und auf einem frischen Rechner # zeigte Chrome "Verbindung verweigert", weil der erste Start laenger dauert. Die Logik steht in services/browser_oeffnen.py. from bewerbungs_assistent.services.browser_oeffnen import oeffnen_sobald_bereit oeffnen_sobald_bereit(f"http://localhost:{port}", port, _open_in_chrome) # Claude Desktop Neustart anbieten, damit PBP dort als Werkzeug geladen wird (#1149 Punkt 10). # Die Frage hat die Vorgabe NEIN und wird im HINTERGRUND gestellt, erst wenn der Server laeuft: sie stand vorher VOR # dem Start, und solange niemand antwortete, lief kein Server (Praxisprobe 1.8, 05.10.2026). # Die Logik steht in services/claude_neustart.py. try: from bewerbungs_assistent.services.claude_neustart import neustart_im_hintergrund neustart_im_hintergrund() except Exception as e: logger.warning("Claude-Check fehlgeschlagen: %s", e) start_dashboard(db, port=port) except Exception as e: logger.exception("Dashboard-Fehler: %s", e) print() print(f" FEHLER: {e}") print(f" Details in: {log_path}") print() if os.environ.get("PBP_FASSUNG"): # Gestartet ueber den Startbaustein (Auto-Update, #1093): der entscheidet, ob die vorige Fassung einspringt # (ein verschluckter Importfehler haette das verhindert). Offen haelt das Fenster `Dashboard starten.bat`. raise if sys.platform == "win32": input(" Druecke Enter zum Schliessen...") sys.exit(1)