Files
claude-regelwerk/.vault-tools/trading-plattform-pruefung/browserlauf.py
T
Pascal MichelandClaude Opus 5 c115a13190 Automatischer Push: 11 Datei(en) geändert
- .vault-tools/trading-plattform-pruefung/LIESMICH.md
- .vault-tools/trading-plattform-pruefung/abendkontrolle.py
- .vault-tools/trading-plattform-pruefung/abendkontrolle_angriffe.py
- .vault-tools/trading-plattform-pruefung/abendkontrolle_mutanten.py
- .vault-tools/trading-plattform-pruefung/abendkontrolle_wsl.py
- .vault-tools/trading-plattform-pruefung/browserlauf.py
- .vault-tools/trading-plattform-pruefung/browserlauf_zugang.py
- .vault-tools/trading-plattform-pruefung/ha_totmann.py
- .vault-tools/trading-plattform-pruefung/ha_totmann_wsl.py
- .vault-tools/trading-plattform-pruefung/scan_push.sh
- AI/Regelwerk/codeworkflow.md

Erzeugt vom PostToolUse-Hook .vault-tools/auto-push/auto_push.py.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-26 04:00:02 +02:00

181 lines
9.0 KiB
Python

"""Browser-Lauf zu Teil 3b mit Playwright (Windows-Python), gegen ein laufendes Backend.
Koordinator-Pruefskript, nicht fuers Repo. Aufruf:
python -X utf8 browserlauf.py <basis-url> <ausgabeordner> [<zugang.json>]
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())