Ctrl K
ComunidadGuías…

start_dashboard.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""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)