"""Browser-Lauf zu Teil 3b mit Playwright (Windows-Python), gegen ein laufendes Backend. Koordinator-Pruefskript, nicht fuers Repo. Aufruf: python -X utf8 browserlauf.py [] Mit zugang.json (aus der Ablage von start_probe_zugang.py) meldet sich der Lauf einmal an und nutzt die Sitzung fuer alle Breiten (seit 2026-09-18, weil das Dashboard Anmeldung verlangt); dazu kommt je Breite die Agenten-Ansicht mit der Walk-Forward-Zeichnung, wenn das Register die Kampagne aus HISTORIE_MARKE traegt. Prueft je Breite: kein waagerechter Scroll, keine Konsolenfehler, Ansicht "Laeufe" zeigt Zeilen, Ansicht "Markt" zeichnet (Canvas nicht leer). Schreibt Screenshots und eine ergebnis.json. Was es nicht sieht: ob die Zahlen stimmen (dafuer pruef_teil3b.py) und wie es aussieht (dafuer die Screenshots, die der Koordinator selbst ansieht). """ import base64 import hashlib import hmac import json import re import struct import sys import time from pathlib import Path from playwright.sync_api import TimeoutError as PlaywrightTimeout from playwright.sync_api import sync_playwright BREITEN = [375, 768, 1024, 1440] # aria-label aus Agenten.tsx: `Historie ${agent}/${version}`; die Kampagne mit Verhaltensblock. HISTORIE_MARKE = "Historie aapl-minute-ppo/v20260914-132555" def totp(geheimnis: str, zeit_s: float) -> str: """Eigener Nachbau nach RFC 6238, wie in angriffe.py.""" schluessel = base64.b32decode(geheimnis.upper() + "=" * (-len(geheimnis) % 8)) mac = hmac.new(schluessel, struct.pack(">Q", int(zeit_s // 30)), hashlib.sha1).digest() versatz = mac[-1] & 0x0F zahl = struct.unpack(">I", mac[versatz : versatz + 4])[0] & 0x7FFFFFFF return f"{zahl % 1_000_000:06d}" # Je ein Minuten- und ein Tagesdatensatz; die Namen stehen so im Katalog (gemessen 2026-09-17). MARKT_DATENSAETZE = [ ("minuten", "AAPL.XNAS-1-MINUTE-LAST-EXTERNAL"), ("tage", "MSFT.XNAS-1-DAY-LAST-EXTERNAL"), ] # aria-label der Knoepfe aus Laeufe.tsx: `Lauf ${quelle}/${datei}`. LAEUFE_EINZEL = [ ("tage", "Lauf signal/tage-teil9-20260917-030800.jsonl"), ("kampagne", "Lauf laeufe/aapl-minute-ppo-v20260914-132555.jsonl"), ] CANVAS_PROBE = """ () => { const flaechen = Array.from(document.querySelectorAll('canvas')); let groesste = null; for (const c of flaechen) { if (!groesste || c.width * c.height > groesste.width * groesste.height) groesste = c; } if (!groesste || groesste.width === 0 || groesste.height === 0) return {anzahl: flaechen.length, farben: 0}; const kontext = groesste.getContext('2d'); const daten = kontext.getImageData(0, 0, groesste.width, groesste.height).data; const farben = new Set(); const schritt = Math.max(4, Math.floor(daten.length / 4 / 20000) * 4); for (let i = 0; i < daten.length; i += schritt) { farben.add(`${daten[i]},${daten[i+1]},${daten[i+2]},${daten[i+3]}`); } return {anzahl: flaechen.length, breite: groesste.width, hoehe: groesste.height, farben: farben.size}; } """ SCROLL_PROBE = "() => ({scroll: document.documentElement.scrollWidth, sicht: document.documentElement.clientWidth})" def main() -> int: basis = sys.argv[1].rstrip("/") ziel = Path(sys.argv[2]) ziel.mkdir(parents=True, exist_ok=True) ergebnis: dict = {"basis": basis, "breiten": {}, "befunde": []} zugang = json.loads(Path(sys.argv[3]).read_text(encoding="utf-8")) if len(sys.argv) > 3 else None with sync_playwright() as spiel: browser = spiel.chromium.launch() # Ein Kontext fuer alle Breiten: Ein TOTP-Code gilt einmal, vier Anmeldungen in einer Minute gingen # also nicht. Die Sitzung haengt am Cookie des Kontexts. kontext = browser.new_context() if zugang is not None: seite = kontext.new_page() seite.goto(basis + "/", wait_until="networkidle") seite.get_by_label("Benutzer").fill(zugang["benutzer"]) seite.get_by_label("Passwort").fill(zugang["passwort"]) seite.get_by_label("Code").fill(totp(zugang["geheimnis"], time.time())) seite.get_by_role("button", name="Anmelden", exact=True).click() try: seite.get_by_role("button", name="Abmelden").wait_for(timeout=10000) except PlaywrightTimeout: ergebnis["befunde"].append("Anmeldung: die App erscheint nicht") seite.close() for breite in BREITEN: seite = kontext.new_page() seite.set_viewport_size({"width": breite, "height": 900}) meldungen: list[str] = [] seite.on("console", lambda m, liste=meldungen: liste.append(f"{m.type}: {m.text}") if m.type in ("error", "warning") else None) seite.on("pageerror", lambda f, liste=meldungen: liste.append(f"pageerror: {f}")) seite.goto(basis + "/", wait_until="networkidle") eintrag: dict = {} def messe(name: str) -> None: masse = seite.evaluate(SCROLL_PROBE) eintrag[f"scroll_{name}"] = masse if masse["scroll"] > masse["sicht"] + 1: ergebnis["befunde"].append(f"{breite}px {name}: waagerechter Scroll {masse}") alarme = seite.get_by_role("alert").count() if alarme: ergebnis["befunde"].append(f"{breite}px {name}: {alarme} Alarm(e): {seite.get_by_role('alert').first.inner_text()[:160]}") seite.screenshot(path=str(ziel / f"{name}-{breite}.png"), full_page=True) for ansicht in ("Läufe", "Markt"): seite.get_by_role("button", name=ansicht, exact=True).click() seite.wait_for_timeout(1500) if ansicht == "Markt": # Die Ansicht laedt nach Plan nichts von selbst: erst die Auswahl holt Kerzen. eintrag["canvas"] = {} for kurz, bar_type in MARKT_DATENSAETZE: try: seite.get_by_label("Datensatz").select_option(bar_type) seite.wait_for_selector("canvas", timeout=20000) seite.wait_for_timeout(2000) except PlaywrightTimeout: pass probe = seite.evaluate(CANVAS_PROBE) eintrag["canvas"][kurz] = probe if probe["farben"] < 5: ergebnis["befunde"].append(f"{breite}px Markt/{kurz}: kein gezeichnetes Canvas ({probe})") messe(f"markt-{kurz}") else: eintrag["laeufe_zeilen"] = seite.locator("table tbody tr").count() if eintrag["laeufe_zeilen"] == 0: ergebnis["befunde"].append(f"{breite}px Laeufe: keine Tabellenzeile") messe("laeufe") for kurz, marke in LAEUFE_EINZEL: try: seite.get_by_role("button", name=marke, exact=True).click(timeout=8000) seite.wait_for_load_state("networkidle", timeout=20000) seite.wait_for_timeout(1200) except PlaywrightTimeout: ergebnis["befunde"].append(f"{breite}px Laeufe/{kurz}: Knopf '{marke}' nicht bedienbar") continue eintrag[f"einzel_{kurz}_tabellen"] = seite.locator("table").count() if eintrag[f"einzel_{kurz}_tabellen"] < 2: ergebnis["befunde"].append(f"{breite}px Laeufe/{kurz}: die Einzelansicht zeigt keine eigene Tabelle") messe(f"laeufe-{kurz}") # Seit 2026-09-18: die Agenten-Ansicht mit der Walk-Forward-Zeichnung (verkettete Episoden). # Nur mit einem Register, das Versionen traegt; sonst bleibt es beim Bildschirmfoto der leeren Ansicht. seite.get_by_role("button", name="Agenten", exact=True).click() seite.wait_for_timeout(1500) historie = seite.get_by_role("button", name=HISTORIE_MARKE, exact=True) if historie.count() == 1: historie.click() try: seite.get_by_role("img", name="Kapitalkurven Fenster 1", exact=True).wait_for(timeout=15000) eintrag["walkforward_zeichnungen"] = seite.get_by_role("img", name=re.compile("^Kapitalkurven Fenster")).count() except PlaywrightTimeout: ergebnis["befunde"].append(f"{breite}px Agenten: keine Walk-Forward-Zeichnung") messe("agenten") eintrag["konsole"] = meldungen if any(m.startswith(("error", "pageerror")) for m in meldungen): ergebnis["befunde"].append(f"{breite}px: Konsolenfehler {meldungen[:3]}") ergebnis["breiten"][str(breite)] = eintrag seite.close() browser.close() (ziel / "ergebnis.json").write_text(json.dumps(ergebnis, indent=2, ensure_ascii=False), encoding="utf-8") print(json.dumps(ergebnis, indent=2, ensure_ascii=False)) return 2 if ergebnis["befunde"] else 0 if __name__ == "__main__": sys.exit(main())