Dashboard reparieren: Escape-Sequenz zerlegte das eingebettete Skript

Das Dashboard blieb bei "lädt ..." stehen und zeigte weder Kacheln noch die
Steuerungs-Abschnitte.

Ursache: _DASHBOARD ist ein Python-String. Der in d081edf ergänzte
prompt()-Dialog für die Live-Bestätigung enthält \n, das Python zu echten
Zeilenumbrüchen aufgelöst hat. Das dadurch offene JavaScript-String-Literal ist
ein Syntaxfehler, der den gesamten <script>-Block mitreißt - im Browser waren
refresh und $ schlicht undefined, es lief kein einziger Ausdruck. Behoben durch
ein r-Präfix am Template, mit Kommentar an Ort und Stelle.

Warum das durchgerutscht ist: Die bisherige Prüfung hat nur kontrolliert, ob die
Bedienelemente im HTML stehen und ob die HTTP-Endpunkte antworten. Beides war
grün, während die Seite tot war. Markup-Präsenz ersetzt nicht das Ausführen der
Seite.

Neu: tests/test_dashboard.py
- Mini-Lexer über das ausgelieferte Skript, der String-Literale findet, die vor
  dem Zeilenende nicht geschlossen werden (Backtick-Template-Literale dürfen
  mehrzeilig sein). Gegenprobe im Test enthalten; gegen den echten Defekt
  geprüft, er wird erkannt.
- Ausgeglichene Klammern, jede im Skript referenzierte Element-ID existiert im
  HTML, und \n kommt als zwei Zeichen beim Browser an.

186 Tests (12 neue), ruff sauber. Im Browser durchgeklickt: Skript lädt, 10
Kacheln, Handel pausieren/starten und der Lernschalter ändern den Serverzustand
und die Anzeige, historisches Training läuft durch bis "Fertig in 0.8s: +146
Beobachtungen, gespeichert", null Loop-Fehler.
This commit is contained in:
Tobias Zimmermann
2026-08-23 12:59:39 +02:00
parent d081edfbd3
commit 4551339632
2 changed files with 126 additions and 1 deletions
+122
View File
@@ -0,0 +1,122 @@
r"""Das eingebettete Dashboard-Skript muss syntaktisch heil beim Browser ankommen.
Hintergrund: ``_DASHBOARD`` ist ein Python-String. Fehlt das r-Präfix, macht Python aus
einem ``\n``, das eigentlich dem JavaScript gehört, einen echten Zeilenumbruch. Das offene
String-Literal reißt dann das gesamte ``<script>`` mit die Seite lädt, zeigt aber nur
"lädt …", weil kein einziger Ausdruck ausgeführt wird. Genau das ist schon passiert.
"""
from __future__ import annotations
import re
import pytest
from trademind.server import _DASHBOARD
SCRIPT_RE = re.compile(r"<script>(.*?)</script>", re.DOTALL)
def script_body() -> str:
match = SCRIPT_RE.search(_DASHBOARD)
assert match is not None, "Das Dashboard enthält keinen <script>-Block"
return match.group(1)
def unterminated_string(js: str) -> str | None:
"""Findet ein String-Literal, das vor dem Zeilenende nicht geschlossen wird.
Nur Template-Literale (Backticks) dürfen über mehrere Zeilen gehen. Gibt eine
Beschreibung der Fundstelle zurück oder ``None``, wenn alles sauber ist.
"""
state: str | None = None # None | " | ' | ` | // | /*
opened_at = 0
line = 1
i = 0
while i < len(js):
char = js[i]
nxt = js[i + 1 : i + 2]
if state is None:
if char == "/" and nxt == "/":
state, i = "//", i + 1
elif char == "/" and nxt == "*":
state, i = "/*", i + 1
elif char in "\"'`":
state, opened_at = char, line
elif char == "\n":
line += 1
elif state == "//":
if char == "\n":
state, line = None, line + 1
elif state == "/*":
if char == "*" and nxt == "/":
state, i = None, i + 1
elif char == "\n":
line += 1
else: # innerhalb eines String-Literals
if char == "\\":
i += 1 # nächstes Zeichen ist escaped
elif char == state:
state = None
elif char == "\n":
if state != "`":
return f"String mit {state} in Zeile {opened_at} endet nicht auf derselben Zeile"
line += 1
i += 1
if state in ("\"", "'", "`"):
return f"String mit {state} ab Zeile {opened_at} wird nie geschlossen"
return None
def test_script_has_no_unterminated_string_literal():
assert unterminated_string(script_body()) is None
def test_javascript_escapes_survive_python():
r"""Ein \n im Quelltext muss als zwei Zeichen beim Browser ankommen."""
assert "\\n" in _DASHBOARD, "Escape-Sequenz wurde von Python aufgelöst r-String fehlt?"
def test_braces_are_balanced():
js = script_body()
depth = 0
for char in js:
depth += char == "{"
depth -= char == "}"
assert depth >= 0, "Mehr schließende als öffnende Klammern"
assert depth == 0, f"Klammern unausgeglichen (Differenz {depth})"
def test_every_referenced_element_exists():
"""Die Skript-Initialisierung greift auf feste IDs zu fehlt eine, bricht alles ab."""
js = script_body()
referenced = set(re.findall(r"""\$\(["']([\w-]+)["']\)""", js))
referenced |= set(re.findall(r"""getElementById\(["']([\w-]+)["']\)""", js))
assert referenced, "Keine Element-Referenzen gefunden Test greift ins Leere"
for element_id in sorted(referenced):
assert f'id="{element_id}"' in _DASHBOARD, f"Element #{element_id} fehlt im HTML"
@pytest.mark.parametrize(
"needle",
[
"Automatisierter Handel",
"Training starten",
"Kontinuierliches Lernen",
"control/trading",
"control/train/history",
"control/train/live",
"X-TradeMind-Token",
],
)
def test_dashboard_wires_up_the_controls(needle: str):
assert needle in _DASHBOARD
def test_broken_example_is_detected():
"""Gegenprobe: Der Prüfer muss den ursprünglichen Fehler erkennen."""
broken = 'const a = prompt("erste Zeile\nzweite Zeile");'
assert unterminated_string(broken) is not None
assert unterminated_string('const a = prompt("erste Zeile\\nzweite Zeile");') is None
assert unterminated_string("const a = `mehrere\nZeilen sind hier erlaubt`;") is None