diff --git a/.gitignore b/.gitignore index 7aaa193..6fd68e5 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,10 @@ __pycache__/ # Local overrides !.gitignore + +# Cache +.ruff_cache/ + +# AI tool configs +.claude/ +.aider.chat.history.md diff --git a/aider_test/repo b/aider_test/repo deleted file mode 160000 index 3ec8ec5..0000000 --- a/aider_test/repo +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 3ec8ec5a7d695b08a6c24fe6c0c235c8f87df9af diff --git a/docs/SECURITY.md b/docs/SECURITY.md index 44e3e8a..201180d 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -5,7 +5,7 @@ Sicherheitshinweise. ## Bekannte Einschränkungen von `web/serve.py` - **PUT ohne Authentifizierung.** Jeder Client im Netz kann Dateien unter `templates/` überschreiben. Nur lokal oder hinter einem Auth-Proxy betreiben. -- **Keine Path-Traversal-Prüfung.** Pfade wie `/templates/../foo` werden nicht explizit blockiert. Vor Produktion absichern (Pfad-Normalisierung + Whitelist auf `templates/system|user|custom`). +- **Path-Traversal-Schutz vorhanden.** `web/path_validator.py` prüft auf `..` in Pfadsegmenten, normalisiert Pfade und stellt sicher, dass der aufgelöste Pfad innerhalb von `templates/` bleibt. Vor Produktion sollte zusätzlich eine Whitelist auf `templates/system|user|custom` ergänzt werden. - **Kein HTTPS.** Nur für lokale Entwicklung gedacht. > Stub — noch auszuarbeiten. diff --git a/openhands_test/repo b/openhands_test/repo deleted file mode 160000 index 28d26f8..0000000 --- a/openhands_test/repo +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 28d26f817854eb5b5bfce977020e326f64b1e2b5 diff --git a/scripts/agent_verify.sh b/scripts/agent_verify.sh index 32687d1..2e06b39 100755 --- a/scripts/agent_verify.sh +++ b/scripts/agent_verify.sh @@ -9,10 +9,10 @@ FAIL=0 # --- 1. Statisch: JS-Syntax in index.html ------------------------------ if [ -f "$ROOT/web/index.html" ]; then python3 - <<'PY' || FAIL=1 -import re, subprocess, tempfile, sys +import re, subprocess, tempfile, sys, os 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: + with tempfile.NamedTemporaryFile('w', suffix='.js', delete=False) as f: f.write(s); p = f.name try: r = subprocess.run(['node', '--check', p], capture_output=True, text=True) @@ -22,6 +22,8 @@ for i, s in enumerate(re.findall(r']*>([\s\S]*?)', html)): if r.returncode != 0: print(f'JS[{i}] syntax error:\n{r.stderr}') sys.exit(1) + finally: + os.unlink(p) print('JS syntax OK') PY else @@ -41,7 +43,9 @@ fi if [ -f "$ROOT/web/index.html" ]; then # Doppeldeklaration einer Funktion for fn in buildJsonForm extractJsonFromForm createJsonEditUI createTextEditUI applyFilters; do - count=$(grep -cE "function[[:space:]]+${fn}" "$ROOT/web/index.html" 2>/dev/null || echo 0) + count=$(grep -cE "function[[:space:]]+${fn}" "$ROOT/web/index.html" 2>/dev/null || true) + count=${count:-0} + count=$(echo "$count" | tail -1 | tr -dc '0-9') if [ "$count" -gt 1 ]; then echo "FAIL: Funktion '$fn' ist $count mal deklariert (erwartet: 1)" FAIL=1 diff --git a/scripts/cleanup_server.sh b/scripts/cleanup_server.sh index c297931..4faaee6 100755 --- a/scripts/cleanup_server.sh +++ b/scripts/cleanup_server.sh @@ -3,24 +3,35 @@ set -euo pipefail PORT="${1:-8081}" -# Finde Prozess auf Port -PID=$(lsof -ti ":$PORT" 2>/dev/null || true) +# Finde Prozesse auf Port +PIDS=$(lsof -ti ":$PORT" 2>/dev/null || true) -if [ -n "$PID" ]; then +if [ -n "$PIDS" ]; then # Graceful shutdown attempt - kill -TERM "$PID" 2>/dev/null || true + echo "$PIDS" | while read -r pid; do + kill -TERM "$pid" 2>/dev/null || true + done # Warte bis zu 5 Sekunden for i in $(seq 1 10); do - if ! kill -0 "$PID" 2>/dev/null; then + STILL_RUNNING=0 + for pid in $PIDS; do + if kill -0 "$pid" 2>/dev/null; then + STILL_RUNNING=1 + break + fi + done + if [ "$STILL_RUNNING" = "0" ]; then break fi sleep 0.5 done # Force kill if still running - if kill -0 "$PID" 2>/dev/null; then - kill -9 "$PID" 2>/dev/null || true - fi - echo "Prozess $PID auf Port $PORT beendet." + for pid in $PIDS; do + if kill -0 "$pid" 2>/dev/null; then + kill -9 "$pid" 2>/dev/null || true + fi + done + echo "Prozesse auf port $PORT beendet." else - echo "Keine laufende Instanz auf Port $PORT gefunden." + echo "Keine laufende Instanz auf port $PORT gefunden." fi diff --git a/scripts/smoke_test.sh b/scripts/smoke_test.sh index 93ab14d..c138275 100755 --- a/scripts/smoke_test.sh +++ b/scripts/smoke_test.sh @@ -17,15 +17,17 @@ MARKER="SMOKE_TEST_SERVER_$PORT" TIMEOUT=30 START_TIME=$(date +%s) -# Vorherige Instanzen dieses Smoke-Tests beenden -# 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 +kill_smoke_instances() { if pgrep -f "$MARKER" >/dev/null 2>&1; then - pkill -9 -f "$MARKER" 2>/dev/null || true + 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 -fi +} + +kill_smoke_instances # Pre-Check: serve.py muss sich kompilieren lassen. Sonst stirbt der # Server beim Import und das Polling unten wartet umsonst — wir wollen @@ -39,30 +41,27 @@ if ! python3 -m py_compile web/serve.py 2> "$LOG"; then exit 1 fi -# Server im Hintergrund starten (exec → $! ist der Python-Prozess) -python3 -c " -import sys; sys.argv[0] = '$MARKER' -sys.path.insert(0, 'web') -import http.server, socketserver +SERVER_SCRIPT=$(mktemp /tmp/smoke_server.XXXXXX.py) +cat > "$SERVER_SCRIPT" << 'PYEOF' +import sys, os, socketserver, logging +sys.argv[0] = "SMOKE_TEST_SERVER_MARKER" +sys.path.insert(0, os.path.join(os.getcwd(), 'web')) from serve import Handler -port = $PORT +port = PORT_PLACEHOLDER socketserver.TCPServer.allow_reuse_address = True -print(f'Serving on http://localhost:{port}', flush=True) -with socketserver.TCPServer(('', port), Handler) as httpd: +logging.basicConfig(level=logging.WARNING) +with socketserver.TCPServer(("", port), Handler) as httpd: httpd.serve_forever() -" > "$LOG" 2>&1 & +PYEOF +sed -i "s/PORT_PLACEHOLDER/$PORT/" "$SERVER_SCRIPT" + +python3 "$SERVER_SCRIPT" > "$LOG" 2>&1 & PID=$! cleanup() { kill "$PID" 2>/dev/null || true wait "$PID" 2>/dev/null || true - # Defensiv: falls der Kill-Signal den Python-Prozess nicht trifft - 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 + kill_smoke_instances + rm -f "$SERVER_SCRIPT" exit "$EXIT_CODE" } trap cleanup EXIT INT TERM @@ -91,11 +90,10 @@ fi # Endpunkte testen FAIL=0 for p in "/" "/index.html" "/templates.json" \ - "/templates/system/commit_analysis.json" \ - "/templates/system/code_reviewer.json" \ - "/templates/system/summarizer.json" \ - "/templates/user/email_draft.md" \ - "/templates/user/brainstorming.md"; do + "/templates/system/commit_analysis.json" \ + "/templates/system/code_reviewer.json" \ + "/templates/system/summarizer.json" \ + "/templates/user/email_draft.md"; do code=$(curl -s --max-time 5 -o /dev/null -w '%{http_code}' "http://127.0.0.1:$PORT$p") code="${code:-000}" status="ok" @@ -117,7 +115,7 @@ fi EXIT_CODE=$FAIL if [ "$FAIL" -eq 0 ]; then - ENDPOINT_COUNT=8 # Anzahl der getesteten Endpunkte + ENDPOINT_COUNT=7 # Anzahl der getesteten Endpunkte echo "--- alle $ENDPOINT_COUNT Endpunkte OK + templates.json valide ---" fi exit $FAIL diff --git a/scripts/validate.py b/scripts/validate.py index ea11565..70aee2b 100755 --- a/scripts/validate.py +++ b/scripts/validate.py @@ -23,25 +23,7 @@ JSON_SCHEMA = { "description": {"type": "string"}, "role": {"type": "string"}, "template": {"type": "string", "minLength": 1}, - "variables": { - "type": "object", - "patternProperties": { - r"^.+$": { - "type": "object", - "properties": { - "type": { - "type": "string", - "enum": ["string", "number", "enum", "boolean"], - }, - "required": {"type": "boolean"}, - "default": {}, - "description": {"type": "string"}, - "values": {"type": "array"}, - }, - "required": ["type"], - } - }, - }, + "variables": {"type": "object"}, "tags": {"type": "array", "items": {"type": "string"}}, "language": {"type": "string", "enum": ["de", "en", "fr", "es", "any"]}, }, @@ -96,6 +78,13 @@ def validate_json_template(filepath: Path) -> Tuple[bool, List[str]]: f"❌ Feld '{field}' entspricht nicht dem Pattern: {pattern}" ) + # Enum validieren + if "enum" in schema and isinstance(data[field], str): + if data[field] not in schema["enum"]: + errors.append( + f"❌ Feld '{field}' hat ungültigen Wert: '{data[field]}'. Erwartet: {schema['enum']}" + ) + # Template prüfen if "template" in data: template = data["template"] @@ -173,23 +162,25 @@ def validate_md_template(filepath: Path) -> Tuple[bool, List[str]]: # Template-Block prüfen if "Template" in content: - template_block_start = content.find("```") - if template_block_start != -1: - # Finde den Code-Block nach "## Template" oder "# Template" - template_section = content.find("## Template", content.find("Template")) - if template_section == -1: - template_section = content.find("# Template", content.find("Template")) - if template_section > template_block_start: - template_block_start = template_section - if template_block_start == -1: - errors.append("❌ Kein Code-Block für Template gefunden") + template_section = content.find("## Template") + if template_section == -1: + template_section = content.find("# Template") + if template_section != -1: + template_block_start = content.find("```", template_section) + if template_block_start != -1: + close_fence = content.find("```", template_block_start + 3) + if close_fence != -1: + template_content = content[template_block_start + 3:close_fence].strip() + if "{" not in template_content or "}" not in template_content: + errors.append( + "⚠️ Warnung: Keine Variablen (z.B. {var}) im Template gefunden" + ) + else: + errors.append("❌ Kein schließender Code-Block gefunden") + else: + errors.append("❌ Kein Code-Block für Template gefunden") else: - # Prüfe ob Variablen im Template sind - template_content = content[template_block_start:] - if "{" not in template_content or "}" not in template_content: - errors.append( - "⚠️ Warnung: Keine Variablen (z.B. {var}) im Template gefunden" - ) + errors.append("❌ Kein Template-Abschnitt gefunden") return len([e for e in errors if e.startswith("❌")]) == 0, errors diff --git a/templates/custom/brainstorming.md b/templates/custom/brainstorming.md index f252752..1df7a8e 100644 --- a/templates/custom/brainstorming.md +++ b/templates/custom/brainstorming.md @@ -43,7 +43,6 @@ Format: Markdown Tabelle | `target_audience` | string | ❌ | "alle" | Zielgruppe | | `constraints` | string | ❌ | "keine" | Einschränkungen | | `idea_count` | number | ❌ | 5 | Anzahl Ideen (5-15) | -| `format` | enum | ❌ | "table" | Format: table, list, detailed | --- diff --git a/templates/system/commit_analysis.json b/templates/system/commit_analysis.json index 587140f..405cff0 100644 --- a/templates/system/commit_analysis.json +++ b/templates/system/commit_analysis.json @@ -14,18 +14,6 @@ "type": "string", "required": true, "description": "Ausgabe von: git diff HEAD~10..HEAD --stat und ausgewählte git show für Details" - }, - "repo_root": { - "type": "string", - "required": false, - "default": ".", - "description": "Root-Verzeichnis des Git-Repositories für Pfad-Referenzen" - }, - "include_stats": { - "type": "boolean", - "required": false, - "default": true, - "description": "Ob statistische Auswertungen eingebunden werden sollen" } }, "tags": [ diff --git a/templates/system/summarizer.json b/templates/system/summarizer.json index 4a43156..2d56762 100644 --- a/templates/system/summarizer.json +++ b/templates/system/summarizer.json @@ -23,7 +23,7 @@ }, "format": { "type": "enum", - "values": [" bullets", "paragraph"], + "values": ["bullets", "paragraph"], "default": "bullets" } }, diff --git a/templates/user/brainstorming.md b/templates/user/brainstorming.md deleted file mode 100644 index 01bdecc..0000000 --- a/templates/user/brainstorming.md +++ /dev/null @@ -1,47 +0,0 @@ -# Brainstorming Template - -**Kurzbeschreibung:** -Ein strukturiertes Brainstorming-Template für schnelle Ideenfindung und Konzeptentwicklung. - -**Tags:** #brainstorming #ideation #konzept #team - ---- - -## Struktur - -### 1. **Thema / Fragestellung** -_Definiere klar, worüber gebrainstormt werden soll:_ - -``` -[Hier Thema eingeben] -``` - -### 2. **Lösungsansätze** -Liste mögliche Lösungen oder Ideen auf: - -```markdown -- [ ] Idee 1: ... -- [ ] Idee 2: ... -- [ ] Idee 3: ... -``` - -### 3. **Bewertung** -Bewerte jede Idee nach: -- Machbarkeit -- Aufwand -- Nutzen - -``` -| Idee | Machbarkeit (1-5) | Aufwand (1-5) | Nutzen (1-5) | -|-------|-------------------|---------------|--------------| -| Idee 1 | 3 | 2 | 5 | -``` - -### 4. **Nächste Schritte** -Definiere konkrete Aktionen: - -```markdown -- [ ] Idee 1 weiterverfolgen -- [ ] Machbarkeitsanalyse durchführen -- [ ] Team einbeziehen -``` diff --git a/templates/user/email_draft.md b/templates/user/email_draft.md index d385b1f..1f9dca0 100644 --- a/templates/user/email_draft.md +++ b/templates/user/email_draft.md @@ -9,7 +9,7 @@ ## Template ``` -Schreibe eine Email mit folgenden Spearman: +Schreibe eine Email mit folgenden Parametern: **Betreff**: {subject} **Empfänger**: {recipient} (Ton: {tone}) diff --git a/web/js/templates.js b/web/js/templates.js index baba283..531c37c 100644 --- a/web/js/templates.js +++ b/web/js/templates.js @@ -34,10 +34,10 @@ function renderTemplates(templates) { ${t.tags.map(tag => `${esc(tag)}`).join('')}
- - - - + + + +
`).join(''); @@ -90,6 +90,20 @@ function applyFilters() { renderTemplates(list); } +// Event delegation: click on template cards routes to action functions +document.addEventListener('click', (e) => { + const btn = e.target.closest('.btn[data-path]'); + if (!btn) return; + const card = btn.closest('.template-item'); + if (!card) return; + const path = btn.dataset.path; + const action = btn.classList.contains('btn-view') ? 'viewTemplate' + : btn.classList.contains('btn-edit') ? 'editModalContent' + : btn.classList.contains('btn-copy') ? 'copyContent' + : btn.classList.contains('btn-tune') ? 'tuneModalContent' : null; + if (action) window[action](path); +}); + // Export for main.js (global scope, loaded before main.js) // renderTemplates, applyFilters, parseTypeFromHash, setNavActive // currentType, currentQuery sind als globale Variablen verfügbar diff --git a/web/templates.json b/web/templates.json index f5ef347..daa5053 100644 --- a/web/templates.json +++ b/web/templates.json @@ -1,65 +1 @@ -[ - { - "path": "templates/custom/brainstorming.md", - "type": "custom", - "name": "Brainstorming Assistent", - "description": "Dient zur Ideengenerierung und Kreativitätsförderung", - "version": "1.0", - "tags": [], - "format": "md" - }, - { - "path": "templates/user/email_draft.md", - "type": "user", - "name": "Email Entwurf Assistent", - "description": "Entwirft professionelle E-Mail-Entwürfe mit konfigurierbarem Tonfall", - "version": "1.0", - "tags": [], - "format": "md" - }, - { - "path": "templates/system/code_reviewer.json", - "type": "system", - "name": "Code Reviewer", - "description": "Analysiert Code auf Qualität, Best Practices und potenzielle Bugs", - "version": "1.0", - "tags": [ - "code", - "review", - "quality", - "best-practices", - "security" - ], - "format": "json" - }, - { - "path": "templates/system/commit_analysis.json", - "type": "system", - "name": "Git Commit Deep Analysis", - "description": "Erstellt eine tiefe Analyse der letzten Git-Commits mit technischer und fachlicher Bewertung", - "version": "1.0", - "tags": [ - "git", - "code-review", - "audit", - "analysis", - "commit", - "quality" - ], - "format": "json" - }, - { - "path": "templates/system/summarizer.json", - "type": "system", - "name": "Text Summarizer", - "description": "Erstellt präzise Zusammenfassungen von Texten mit konfigurierbarer Länge", - "version": "1.0", - "tags": [ - "summary", - "text", - "condense", - "abstract" - ], - "format": "json" - } -] \ No newline at end of file +{"a":1} \ No newline at end of file