Erster Stand des öffentlichen Ausschnitts aus dem Vault. Die .gitignore arbeitet als Positivliste: ausgeschlossen ist alles, eingeschlossen nur, was ausdrücklich wieder aufgenommen wird. Notizen, Projekte, Finanzen, das Sitzungsgedächtnis und settings.local.json bleiben damit außen vor. Der Kundenname in AI/Regelwerk/seo.md ist durch eine neutrale Bezeichnung ersetzt; die Regeln selbst sind unverändert. Co-Authored-By: Claude Opus 5 <[email protected]>
129 lines
6.2 KiB
Python
129 lines
6.2 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>
|
|
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 json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
from playwright.sync_api import TimeoutError as PlaywrightTimeout
|
|
from playwright.sync_api import sync_playwright
|
|
|
|
BREITEN = [375, 768, 1024, 1440]
|
|
|
|
# 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": []}
|
|
|
|
with sync_playwright() as spiel:
|
|
browser = spiel.chromium.launch()
|
|
for breite in BREITEN:
|
|
seite = browser.new_page(viewport={"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}")
|
|
|
|
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())
|