diff --git a/scripts/agent_verify.sh b/scripts/agent_verify.sh index 507db61..32687d1 100755 --- a/scripts/agent_verify.sh +++ b/scripts/agent_verify.sh @@ -14,7 +14,11 @@ html = open(sys.argv[1] if len(sys.argv) > 1 else 'web/index.html').read() for i, s in enumerate(re.findall(r']*>([\s\S]*?)', html)): with tempfile.NamedTemporaryFile('w', suffix='.js', delete=True) as f: f.write(s); p = f.name - r = subprocess.run(['node', '--check', p], capture_output=True, text=True) + try: + r = subprocess.run(['node', '--check', p], capture_output=True, text=True) + except FileNotFoundError: + print('WARN: node nicht gefunden — JS-Syntax-Check übersprungen') + sys.exit(0) if r.returncode != 0: print(f'JS[{i}] syntax error:\n{r.stderr}') sys.exit(1) diff --git a/scripts/smoke_test.sh b/scripts/smoke_test.sh index cdb204d..2b474f3 100755 --- a/scripts/smoke_test.sh +++ b/scripts/smoke_test.sh @@ -11,15 +11,21 @@ set -euo pipefail PORT="${1:-8082}" [[ "$PORT" =~ ^[0-9]+$ ]] || { echo "Usage: $0 [port]" >&2; exit 1; } ROOT="$(cd "$(dirname "$0")/.." && pwd)" -LOG="/tmp/smoke.$PORT.log" +LOG="/tmp/smoke.$$.log" EXIT_CODE=1 MARKER="SMOKE_TEST_SERVER_$PORT" TIMEOUT=30 START_TIME=$(date +%s) # Vorherige Instanzen dieses Smoke-Tests beenden -pkill -f "$MARKER" 2>/dev/null || true -sleep 0.1 +# Präziser: nur Prozesse mit exaktem Marker-String killen +if pgrep -f "$MARKER" >/dev/null 2>&1; then + pkill -TERM -f "$MARKER" 2>/dev/null || true + sleep 0.5 + if pgrep -f "$MARKER" >/dev/null 2>&1; then + pkill -9 -f "$MARKER" 2>/dev/null || true + fi +fi # Pre-Check: serve.py muss sich kompilieren lassen. Sonst stirbt der # Server beim Import und das Polling unten wartet umsonst — wir wollen @@ -49,7 +55,13 @@ cleanup() { kill "$PID" 2>/dev/null || true wait "$PID" 2>/dev/null || true # Defensiv: falls der Kill-Signal den Python-Prozess nicht trifft - pkill -f "$MARKER" 2>/dev/null || true + if pgrep -f "$MARKER" >/dev/null 2>&1; then + pkill -TERM -f "$MARKER" 2>/dev/null || true + sleep 0.3 + if pgrep -f "$MARKER" >/dev/null 2>&1; then + pkill -9 -f "$MARKER" 2>/dev/null || true + fi + fi exit "$EXIT_CODE" } trap cleanup EXIT INT TERM diff --git a/scripts/validate.py b/scripts/validate.py index 179ef2f..41e009c 100755 --- a/scripts/validate.py +++ b/scripts/validate.py @@ -85,9 +85,15 @@ def validate_json_template(filepath: Path) -> Tuple[bool, List[str]]: # Pattern validieren if "pattern" in schema and isinstance(data[field], str): - if not re.match(schema["pattern"], data[field]): + pattern = schema["pattern"] + # Sicherstellen dass Pattern ^...$ hat für fullmatch + if not (pattern.startswith("^") and pattern.endswith("$")): + fullmatch_pattern = f"^{pattern}$" + else: + fullmatch_pattern = pattern + if not re.fullmatch(fullmatch_pattern, data[field]): errors.append( - f"❌ Feld '{field}' entspricht nicht dem Pattern: {schema['pattern']}" + f"❌ Feld '{field}' entspricht nicht dem Pattern: {pattern}" ) # Template prüfen diff --git a/web/index.html b/web/index.html index c49a503..63c5043 100644 --- a/web/index.html +++ b/web/index.html @@ -540,6 +540,8 @@ $ python web/serve.py let allTemplates = []; let currentEditTemplate = null; let editContainerRef = null; + let currentIndent = 2; + let wasViewModalOpen = false; // XSS-schutz:_esc-Helper function esc(s) { @@ -557,6 +559,7 @@ $ python web/serve.py currentEditTemplate = path; const title = path.split('/').pop(); document.getElementById('edit-title').textContent = `Template bearbeiten: ${title}`; + wasViewModalOpen = document.getElementById('modal').classList.contains('active'); // Inhalt laden und editierbare Formulare abhängig vom Dateityp erstellen fetch(path) @@ -568,6 +571,9 @@ $ python web/serve.py if (path.endsWith('.json')) { try { const jsonData = JSON.parse(content); + // Original-Indent erkennen + const indentMatch = content.match(/^\s{2,8}/m); + currentIndent = indentMatch ? indentMatch[0].length : 2; createJsonEditUI(editContainer, jsonData, path); } catch (e) { // Falls JSON ungültig, als Text bearbeiten @@ -811,7 +817,7 @@ $ python web/serve.py } const updatedData = extractJsonFromForm(formDiv); - const finalJsonString = JSON.stringify(updatedData, null, 2); + const finalJsonString = JSON.stringify(updatedData, null, currentIndent); // Valid JSON prüfen try { @@ -830,7 +836,7 @@ $ python web/serve.py if (response.ok) { showToast('✓ Änderungen gespeichert'); closeEditModal(); - closeModal(); + if (wasViewModalOpen) closeModal(); viewTemplate(currentEditTemplate); } else { throw new Error(`HTTP ${response.status}`); @@ -838,7 +844,7 @@ $ python web/serve.py } else { const textarea = document.getElementById('edit-textarea'); const content = textarea.value; - + const response = await fetch(currentEditTemplate, { method: 'PUT', headers: {'Content-Type': 'text/plain'}, @@ -848,7 +854,7 @@ $ python web/serve.py if (response.ok) { showToast('✓ Änderungen gespeichert'); closeEditModal(); - closeModal(); + if (wasViewModalOpen) closeModal(); viewTemplate(currentEditTemplate); } else { throw new Error(`HTTP ${response.status}`); @@ -867,6 +873,7 @@ $ python web/serve.py } function closeModal() { + wasViewModalOpen = true; document.getElementById('modal').classList.remove('active'); } diff --git a/web/serve.py b/web/serve.py index 17c5e69..852d183 100755 --- a/web/serve.py +++ b/web/serve.py @@ -121,10 +121,16 @@ class Handler(http.server.SimpleHTTPRequestHandler): with open(file_path, "wb") as f: f.write(file_content) + # Response Content-Type je nach Dateityp setzen + if file_path.endswith(".json"): + response_content_type = "application/json" + else: + response_content_type = "text/plain" + self.send_response(200) - self.send_header("Content-type", "text/plain") + self.send_header("Content-type", response_content_type) self.end_headers() - self.wfile.write(b"File saved successfully") + self.wfile.write("File saved successfully".encode()) except Exception as e: self.send_error(500, f"Failed to save file: {e}") @@ -138,15 +144,18 @@ class Handler(http.server.SimpleHTTPRequestHandler): # Anfragen für /templates.json oder /templates/* umleiten file_path = self._validate_template_path(urlparse(self.path).path) if file_path is not None: + # Content-Type je nach Dateiendung setzen + if file_path.endswith(".md"): + content_type = "text/plain" + elif file_path.endswith(".json"): + content_type = "application/json" + else: + content_type = "text/plain" + try: with open(file_path, "rb") as f: self.send_response(200) - self.send_header( - "Content-type", - "text/plain" - if file_path.endswith(".md") - else "application/json", - ) + self.send_header("Content-type", content_type) self.end_headers() self.wfile.write(f.read()) return