start_dashboard.py
Rama main · réplica de lectura del repositorio original.
"""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)