feat(studio): Einstellungen-Reiter je Fall, KI-Weg wählbar und sichtbar
Das Studio konnte Fälle bisher nur anlegen, nicht bearbeiten. Es gab kein Einstellungs-Panel, alle Felder waren nach dem Anlegen eingefroren. Wer etwas ändern wollte, musste zurück in die klassische Ansicht. Für die geplante Ablösung der klassischen Ansicht ist das der erste Blocker. Neuer Reiter Einstellungen in der Fallnavigation, unter Export. Er führt alle Felder des klassischen Bearbeiten-Modals: Titel, Beschreibung, die drei Quellenschalter, Sichtbarkeit, KI-Verarbeitung, Aktualisierung mit Intervall und Startzeit, Aufbewahrung und die drei E-Mail-Benachrichtigungen. Gespeichert wird nur, was sich geändert hat. Zwei Gründe. Der Server aktualisiert feldweise, unverändert zurückgereichte Werte würden trotzdem geschrieben. Und eine unbeabsichtigte Änderung am Intervall kann einen laufenden Zeitplan verstellen. Die Wahl des KI-Wegs, also Anthropic gegen die EU-Fassung über Bedrock Frankfurt, fehlte im Studio vollständig. Sie steht jetzt im Reiter und im Anlege-Dialog, wortgleich zur klassischen Ansicht. Beim Umstellen erscheint ein Hinweis, dass bereits erzeugte Inhalte auf dem bisherigen Weg entstanden sind und die Änderung ab dem nächsten Lauf greift. Läuft gerade ein Schritt, sagt der Reiter das ebenfalls. Dazu die Kennzeichen EU und Anthropic in Fallliste und Kopfzeile. Sie erscheinen nur, wenn der Fall ausdrücklich auf einen Weg gestellt ist. Ohne Angabe gilt die Vorgabe der Organisation, dann bleibt es unbeschriftet. Zwei Feinheiten. Das Intervall wird in der größten glatt aufgehenden Einheit gezeigt, 1440 Minuten also als ein Tag. Und der X-Schalter bleibt bedienbar, wenn der Fall X bereits nutzt, sonst ließe er sich nach dem Wegfall des letzten Zugangs nicht mehr abschalten. Prüfungen ohne Browser und ohne Netzzugriff. 57 Fälle für den Reiter, dazu 29 für die Struktur beider Oberflächen, die prüfen, dass jedes onclick auf eine vorhandene Methode zeigt, dass keine ID doppelt vergeben ist und dass jedes Design-Token ohne Fallback auch definiert ist. Cache-Buster auf 20260802b. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Dieser Commit ist enthalten in:
140
tests/test_studio_struktur.py
Normale Datei
140
tests/test_studio_struktur.py
Normale Datei
@@ -0,0 +1,140 @@
|
||||
"""Prueft die Struktur der Oberflaechen ohne Browser.
|
||||
|
||||
Faengt genau die Fehler, die eine Syntaxpruefung durchlaesst. Ein onclick, das
|
||||
auf eine nicht vorhandene Methode zeigt. Ein Feld, das das Skript sucht, das
|
||||
im Dokument aber fehlt. Eine ID, die zweimal vergeben ist. Ein vergessener
|
||||
Cache-Buster.
|
||||
|
||||
Aufruf aus dem Projektstamm:
|
||||
python3 tests/test_studio_struktur.py
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
STAMM = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
STATIC = os.path.join(STAMM, "src", "static")
|
||||
|
||||
ok = 0
|
||||
fehl = 0
|
||||
|
||||
|
||||
def pruefe(name, bedingung, extra=None):
|
||||
global ok, fehl
|
||||
if bedingung:
|
||||
ok += 1
|
||||
print(f" OK {name}")
|
||||
else:
|
||||
fehl += 1
|
||||
print(f" FEHL {name}" + (f" {extra}" if extra is not None else ""))
|
||||
|
||||
|
||||
def lies(*teile):
|
||||
with open(os.path.join(STATIC, *teile), encoding="utf-8") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
studio_html = lies("studio.html")
|
||||
studio_js = lies("js", "studio.js")
|
||||
dash_html = lies("dashboard.html")
|
||||
app_js = lies("js", "app.js")
|
||||
comp_js = lies("js", "components.js")
|
||||
|
||||
|
||||
def ids(html):
|
||||
return re.findall(r'\bid="([^"]+)"', html)
|
||||
|
||||
|
||||
def doppelte(liste):
|
||||
gesehen, doppelt = set(), []
|
||||
for x in liste:
|
||||
if x in gesehen:
|
||||
doppelt.append(x)
|
||||
gesehen.add(x)
|
||||
return doppelt
|
||||
|
||||
|
||||
print("\nA) IDs sind eindeutig")
|
||||
for name, html in [("studio.html", studio_html), ("dashboard.html", dash_html)]:
|
||||
d = doppelte(ids(html))
|
||||
pruefe(f"A keine doppelte ID in {name}", not d, d)
|
||||
|
||||
print("\nB) Der Einstellungen-Reiter ist vollstaendig verdrahtet")
|
||||
# Alle Felder, die die Logik anfasst, muessen im Dokument stehen.
|
||||
gesucht = sorted(set(re.findall(r"_setEl\('([^']+)'\)", studio_js)))
|
||||
vorhanden = set(ids(studio_html))
|
||||
fehlend = [f for f in gesucht if f not in vorhanden]
|
||||
pruefe("B1 jedes angesprochene Feld existiert", not fehlend, fehlend)
|
||||
pruefe("B2 es sind auch wirklich welche geprueft", len(gesucht) >= 15, len(gesucht))
|
||||
|
||||
# Panel und Navigationsknopf haengen am selben Schluessel.
|
||||
pruefe("B3 Navigationsknopf vorhanden",
|
||||
'data-art="settings" onclick="Studio.openTab(\'settings\')"' in studio_html)
|
||||
pruefe("B4 Panel vorhanden", '<div class="tab-panel" data-art="settings">' in studio_html)
|
||||
pruefe("B5 activateTab kennt den Reiter",
|
||||
"if (key === 'settings') this.loadSettings();" in studio_js)
|
||||
|
||||
print("\nC) Jedes onclick zeigt auf eine vorhandene Methode")
|
||||
for name, html, js, praefix in [
|
||||
("studio", studio_html, studio_js, "Studio"),
|
||||
("dashboard", dash_html, app_js + comp_js, "App"),
|
||||
]:
|
||||
aufgerufen = sorted(set(re.findall(praefix + r"\.([a-zA-Z_][a-zA-Z0-9_]*)\s*\(", html)))
|
||||
fehlend = []
|
||||
for m in aufgerufen:
|
||||
# Methoden stehen als " name(" oder " async name(" im Objektliteral
|
||||
if not re.search(r"^\s+(async\s+)?" + re.escape(m) + r"\s*\(", js, re.M):
|
||||
fehlend.append(m)
|
||||
pruefe(f"C {name}.html ruft nur vorhandene Methoden ({len(aufgerufen)} geprueft)",
|
||||
not fehlend, fehlend)
|
||||
|
||||
print("\nD) Der KI-Weg ist in beiden Ansichten gleich angeboten")
|
||||
for name, html, feld in [("studio.html", studio_html, "set-ai-backend"),
|
||||
("studio.html anlegen", studio_html, "inc-ai-backend"),
|
||||
("dashboard.html", dash_html, "inc-ai-backend")]:
|
||||
block = re.search(r'<select id="' + feld + r'".*?</select>', html, re.S)
|
||||
pruefe(f"D {name} bietet drei Werte an",
|
||||
bool(block) and block.group(0).count("<option") == 3)
|
||||
if block:
|
||||
werte = re.findall(r'value="([^"]*)"', block.group(0))
|
||||
pruefe(f"D {name} genau die erlaubten Werte", werte == ["", "cli", "bedrock"], werte)
|
||||
|
||||
print("\nE) Cache-Buster sind einheitlich")
|
||||
for name, html in [("studio.html", studio_html), ("dashboard.html", dash_html)]:
|
||||
stempel = set(re.findall(r"/static/(?:js|css)/[a-z0-9_.-]+\?v=([0-9a-z]+)", html))
|
||||
# Fremde Bausteine (i18n, api, ws) haben eigene Staende, das ist in Ordnung.
|
||||
# Geprueft wird nur, dass die in diesem Zug geaenderten Dateien mitgezogen sind.
|
||||
eigene = re.findall(r"/static/(?:js|css)/(studio|app|style|components|studio)\.(?:js|css)\?v=([0-9a-z]+)", html)
|
||||
pruefe(f"E {name} nennt Stempel", bool(stempel), stempel)
|
||||
pruefe(f"E {name} geaenderte Dateien tragen 20260802b",
|
||||
all(v == "20260802b" for _, v in eigene), eigene)
|
||||
|
||||
print("\nF) Tags im neuen Panel sind ausgeglichen")
|
||||
i = studio_html.index('<div class="tab-panel" data-art="settings">')
|
||||
j = studio_html.index('<div class="tab-panel" data-art="export">')
|
||||
teil = studio_html[i:j]
|
||||
for tag in ["div", "select", "label", "textarea"]:
|
||||
auf = len(re.findall(r"<" + tag + r"[\s>]", teil))
|
||||
zu = len(re.findall(r"</" + tag + r">", teil))
|
||||
pruefe(f"F {tag} ausgeglichen ({auf})", auf == zu, (auf, zu))
|
||||
|
||||
print("\nG) Das Kennzeichen nutzt vorhandene Stile")
|
||||
style = lies("css", "style.css")
|
||||
studio_css = lies("css", "studio.css")
|
||||
pruefe("G1 incident-ai-badge ist definiert", ".incident-ai-badge" in style)
|
||||
pruefe("G2 case-ai-tag ist definiert", ".case-ai-tag" in studio_css)
|
||||
pruefe("G3 studio.html bindet style.css ein", "/static/css/style.css" in studio_html)
|
||||
pruefe("G4 Kennzeichenplatz in der Kopfzeile", 'id="studio-ai-badge"' in studio_html)
|
||||
|
||||
print("\nH) Keine unbekannten Design-Tokens")
|
||||
# var(--x, wert) ist auch ohne Definition gueltig, der Fallback greift.
|
||||
# Geprueft wird deshalb nur, was ohne Fallback benutzt wird.
|
||||
ohne_fallback = set(re.findall(r"var\((--[a-z0-9-]+)\s*\)", studio_css))
|
||||
definiert = set(re.findall(r"^\s*(--[a-z0-9-]+)\s*:", style, re.M))
|
||||
definiert |= set(re.findall(r"^\s*(--[a-z0-9-]+)\s*:", studio_css, re.M))
|
||||
unbekannt = sorted(ohne_fallback - definiert)
|
||||
pruefe("H1 jedes Token ohne Fallback ist definiert", not unbekannt, unbekannt)
|
||||
pruefe("H2 es sind auch wirklich welche geprueft", len(ohne_fallback) >= 10, len(ohne_fallback))
|
||||
|
||||
print(f"\nErgebnis: {ok} bestanden, {fehl} fehlgeschlagen")
|
||||
sys.exit(1 if fehl else 0)
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren