A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.
This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).
Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
479 lines
17 KiB
Python
Executable File
479 lines
17 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
"""
|
|
Qwen Refactoring Orchestrator
|
|
=============================
|
|
Orchestriert Code-Refactoring via Qwen2.5:32B auf dem Mac Mini.
|
|
|
|
Workflow:
|
|
1. Liest große Dateien in Chunks
|
|
2. Sendet Refactoring-Prompts an Qwen via Ollama API
|
|
3. Validiert und integriert Ergebnisse
|
|
4. Führt Tests aus
|
|
|
|
Usage:
|
|
python qwen_refactor_orchestrator.py --file backend/frontend/static/js/studio.js
|
|
python qwen_refactor_orchestrator.py --all-large-files
|
|
python qwen_refactor_orchestrator.py --status
|
|
"""
|
|
|
|
import argparse
|
|
import asyncio
|
|
import json
|
|
import os
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
from dataclasses import dataclass, field
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
from typing import List, Optional, Dict, Any
|
|
import httpx
|
|
|
|
# Konfiguration
|
|
MAC_MINI_HOST = "mac-mini-von-benjamin.fritz.box"
|
|
OLLAMA_PORT = 11434
|
|
OLLAMA_URL = f"http://{MAC_MINI_HOST}:{OLLAMA_PORT}"
|
|
MODEL_NAME = "qwen2.5:32b"
|
|
MAX_CHUNK_LINES = 800 # ~1500 Tokens pro Chunk
|
|
MAX_CONTEXT_TOKENS = 28000 # Sicherheitsmarge für 32K Kontext
|
|
|
|
PROJECT_ROOT = Path(__file__).parent.parent
|
|
REFACTOR_OUTPUT_DIR = PROJECT_ROOT / "refactored"
|
|
REFACTOR_LOG_FILE = PROJECT_ROOT / "refactor_log.json"
|
|
|
|
# Top 10 große Dateien
|
|
LARGE_FILES = [
|
|
("backend/frontend/static/js/studio.js", 9787, "JavaScript"),
|
|
("website/components/admin/SystemInfoSection.tsx", 5690, "TypeScript/React"),
|
|
("backend/frontend/modules/companion.py", 5513, "Python"),
|
|
("backend/classroom_api.py", 4467, "Python"),
|
|
("backend/frontend/school.py", 3732, "Python"),
|
|
("backend/frontend/components/admin_panel.py", 3434, "Python"),
|
|
("backend/ai_processor.py", 2999, "Python"),
|
|
("website/app/admin/rag/page.tsx", 2964, "TypeScript/React"),
|
|
("backend/frontend/modules/alerts.py", 2902, "Python"),
|
|
("backend/frontend/meetings.py", 2847, "Python"),
|
|
]
|
|
|
|
|
|
@dataclass
|
|
class RefactorChunk:
|
|
"""Ein Chunk einer Datei für Refactoring"""
|
|
file_path: str
|
|
chunk_index: int
|
|
total_chunks: int
|
|
start_line: int
|
|
end_line: int
|
|
content: str
|
|
language: str
|
|
refactored_content: Optional[str] = None
|
|
status: str = "pending" # pending, processing, completed, failed
|
|
error: Optional[str] = None
|
|
|
|
|
|
@dataclass
|
|
class RefactorSession:
|
|
"""Eine Refactoring-Session für eine Datei"""
|
|
file_path: str
|
|
language: str
|
|
original_lines: int
|
|
chunks: List[RefactorChunk] = field(default_factory=list)
|
|
started_at: Optional[datetime] = None
|
|
completed_at: Optional[datetime] = None
|
|
status: str = "pending"
|
|
tests_passed: Optional[bool] = None
|
|
|
|
|
|
class QwenRefactorOrchestrator:
|
|
"""Orchestriert Refactoring via Qwen auf Mac Mini"""
|
|
|
|
def __init__(self):
|
|
self.sessions: Dict[str, RefactorSession] = {}
|
|
self.load_state()
|
|
|
|
def load_state(self):
|
|
"""Lädt den Zustand aus der Log-Datei"""
|
|
if REFACTOR_LOG_FILE.exists():
|
|
try:
|
|
with open(REFACTOR_LOG_FILE) as f:
|
|
data = json.load(f)
|
|
# Rekonstruiere Sessions aus JSON
|
|
for path, session_data in data.get("sessions", {}).items():
|
|
self.sessions[path] = RefactorSession(**session_data)
|
|
except Exception as e:
|
|
print(f"Warning: Could not load state: {e}")
|
|
|
|
def save_state(self):
|
|
"""Speichert den Zustand in die Log-Datei"""
|
|
data = {
|
|
"sessions": {
|
|
path: {
|
|
"file_path": s.file_path,
|
|
"language": s.language,
|
|
"original_lines": s.original_lines,
|
|
"status": s.status,
|
|
"started_at": s.started_at.isoformat() if s.started_at else None,
|
|
"completed_at": s.completed_at.isoformat() if s.completed_at else None,
|
|
"tests_passed": s.tests_passed,
|
|
"chunks": [
|
|
{
|
|
"chunk_index": c.chunk_index,
|
|
"total_chunks": c.total_chunks,
|
|
"start_line": c.start_line,
|
|
"end_line": c.end_line,
|
|
"status": c.status,
|
|
"error": c.error,
|
|
}
|
|
for c in s.chunks
|
|
],
|
|
}
|
|
for path, s in self.sessions.items()
|
|
},
|
|
"last_updated": datetime.now().isoformat(),
|
|
}
|
|
with open(REFACTOR_LOG_FILE, "w") as f:
|
|
json.dump(data, f, indent=2)
|
|
|
|
async def check_ollama_status(self) -> Dict[str, Any]:
|
|
"""Prüft ob Ollama auf dem Mac Mini erreichbar ist"""
|
|
try:
|
|
async with httpx.AsyncClient(timeout=10.0) as client:
|
|
response = await client.get(f"{OLLAMA_URL}/api/tags")
|
|
if response.status_code == 200:
|
|
models = response.json().get("models", [])
|
|
qwen_available = any(m.get("name", "").startswith("qwen2.5:32b") for m in models)
|
|
return {
|
|
"status": "online",
|
|
"models": [m.get("name") for m in models],
|
|
"qwen_available": qwen_available,
|
|
}
|
|
except Exception as e:
|
|
return {"status": "offline", "error": str(e)}
|
|
return {"status": "unknown"}
|
|
|
|
def split_file_into_chunks(self, file_path: str, language: str) -> List[RefactorChunk]:
|
|
"""Teilt eine Datei in logische Chunks auf"""
|
|
full_path = PROJECT_ROOT / file_path
|
|
if not full_path.exists():
|
|
raise FileNotFoundError(f"File not found: {full_path}")
|
|
|
|
with open(full_path) as f:
|
|
lines = f.readlines()
|
|
|
|
chunks = []
|
|
current_chunk_start = 0
|
|
current_chunk_lines = []
|
|
|
|
# Finde logische Trennstellen basierend auf Sprache
|
|
if language == "Python":
|
|
# Trenne bei Klassen und Top-Level-Funktionen
|
|
split_pattern = re.compile(r"^(class |def |async def )")
|
|
elif language in ["JavaScript", "TypeScript/React"]:
|
|
# Trenne bei Funktionen, Klassen, und export statements
|
|
split_pattern = re.compile(r"^(export |function |class |const \w+ = )")
|
|
else:
|
|
split_pattern = None
|
|
|
|
for i, line in enumerate(lines):
|
|
current_chunk_lines.append(line)
|
|
|
|
# Prüfe ob wir splitten sollten
|
|
should_split = False
|
|
if len(current_chunk_lines) >= MAX_CHUNK_LINES:
|
|
should_split = True
|
|
elif split_pattern and i > current_chunk_start + 100:
|
|
# Versuche an logischer Stelle zu trennen
|
|
if split_pattern.match(line) and len(current_chunk_lines) > 200:
|
|
should_split = True
|
|
|
|
if should_split or i == len(lines) - 1:
|
|
chunk = RefactorChunk(
|
|
file_path=file_path,
|
|
chunk_index=len(chunks),
|
|
total_chunks=0, # Wird später gesetzt
|
|
start_line=current_chunk_start + 1,
|
|
end_line=i + 1,
|
|
content="".join(current_chunk_lines),
|
|
language=language,
|
|
)
|
|
chunks.append(chunk)
|
|
current_chunk_start = i + 1
|
|
current_chunk_lines = []
|
|
|
|
# Setze total_chunks
|
|
for chunk in chunks:
|
|
chunk.total_chunks = len(chunks)
|
|
|
|
return chunks
|
|
|
|
def create_refactoring_prompt(self, chunk: RefactorChunk, session: RefactorSession) -> str:
|
|
"""Erstellt den Refactoring-Prompt für Qwen"""
|
|
return f"""Du bist ein erfahrener Software-Entwickler. Refaktoriere den folgenden {chunk.language}-Code.
|
|
|
|
ZIELE:
|
|
1. Teile große Funktionen in kleinere, wiederverwendbare Einheiten
|
|
2. Verbessere die Lesbarkeit durch bessere Variablennamen
|
|
3. Entferne doppelten Code (DRY-Prinzip)
|
|
4. Füge hilfreiche Kommentare hinzu (aber nicht übertreiben)
|
|
5. Behalte die gesamte Funktionalität bei!
|
|
|
|
REGELN:
|
|
- Keine neuen Dependencies hinzufügen
|
|
- Alle existierenden Exports/APIs beibehalten
|
|
- Keine Breaking Changes
|
|
- Code muss weiterhin funktionieren
|
|
|
|
DATEI: {chunk.file_path}
|
|
CHUNK: {chunk.chunk_index + 1}/{chunk.total_chunks} (Zeilen {chunk.start_line}-{chunk.end_line})
|
|
SPRACHE: {chunk.language}
|
|
ORIGINALZEILEN: {session.original_lines}
|
|
|
|
--- ORIGINAL CODE ---
|
|
{chunk.content}
|
|
--- END ORIGINAL CODE ---
|
|
|
|
Gib NUR den refaktorierten Code zurück, ohne Erklärungen oder Markdown-Blöcke.
|
|
"""
|
|
|
|
async def refactor_chunk(self, chunk: RefactorChunk, session: RefactorSession) -> bool:
|
|
"""Refaktoriert einen einzelnen Chunk via Qwen"""
|
|
chunk.status = "processing"
|
|
self.save_state()
|
|
|
|
prompt = self.create_refactoring_prompt(chunk, session)
|
|
|
|
try:
|
|
async with httpx.AsyncClient(timeout=300.0) as client:
|
|
response = await client.post(
|
|
f"{OLLAMA_URL}/api/generate",
|
|
json={
|
|
"model": MODEL_NAME,
|
|
"prompt": prompt,
|
|
"stream": False,
|
|
"options": {
|
|
"num_predict": 4096,
|
|
"temperature": 0.3,
|
|
},
|
|
},
|
|
)
|
|
|
|
if response.status_code == 200:
|
|
result = response.json()
|
|
refactored = result.get("response", "")
|
|
|
|
# Entferne eventuelle Markdown-Code-Blöcke
|
|
refactored = re.sub(r"^```\w*\n", "", refactored)
|
|
refactored = re.sub(r"\n```$", "", refactored)
|
|
|
|
chunk.refactored_content = refactored
|
|
chunk.status = "completed"
|
|
self.save_state()
|
|
return True
|
|
else:
|
|
chunk.status = "failed"
|
|
chunk.error = f"HTTP {response.status_code}: {response.text}"
|
|
self.save_state()
|
|
return False
|
|
|
|
except Exception as e:
|
|
chunk.status = "failed"
|
|
chunk.error = str(e)
|
|
self.save_state()
|
|
return False
|
|
|
|
async def refactor_file(self, file_path: str, language: str) -> RefactorSession:
|
|
"""Refaktoriert eine komplette Datei"""
|
|
print(f"\n{'=' * 60}")
|
|
print(f"Starte Refactoring: {file_path}")
|
|
print(f"{'=' * 60}")
|
|
|
|
# Erstelle Session
|
|
full_path = PROJECT_ROOT / file_path
|
|
with open(full_path) as f:
|
|
original_lines = len(f.readlines())
|
|
|
|
chunks = self.split_file_into_chunks(file_path, language)
|
|
session = RefactorSession(
|
|
file_path=file_path,
|
|
language=language,
|
|
original_lines=original_lines,
|
|
chunks=chunks,
|
|
started_at=datetime.now(),
|
|
status="processing",
|
|
)
|
|
self.sessions[file_path] = session
|
|
self.save_state()
|
|
|
|
print(f"Datei aufgeteilt in {len(chunks)} Chunks")
|
|
|
|
# Refaktoriere jeden Chunk
|
|
for i, chunk in enumerate(chunks):
|
|
print(f"\nChunk {i + 1}/{len(chunks)} (Zeilen {chunk.start_line}-{chunk.end_line})...")
|
|
success = await self.refactor_chunk(chunk, session)
|
|
if success:
|
|
print(f" ✓ Chunk {i + 1} refaktoriert")
|
|
else:
|
|
print(f" ✗ Chunk {i + 1} fehlgeschlagen: {chunk.error}")
|
|
|
|
# Prüfe ob alle Chunks erfolgreich waren
|
|
all_success = all(c.status == "completed" for c in chunks)
|
|
|
|
if all_success:
|
|
# Kombiniere refaktorierten Code
|
|
refactored_content = "\n".join(c.refactored_content for c in chunks if c.refactored_content)
|
|
|
|
# Speichere refaktorierten Code
|
|
output_dir = REFACTOR_OUTPUT_DIR / Path(file_path).parent
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
output_file = REFACTOR_OUTPUT_DIR / file_path
|
|
|
|
with open(output_file, "w") as f:
|
|
f.write(refactored_content)
|
|
|
|
session.status = "completed"
|
|
session.completed_at = datetime.now()
|
|
print(f"\n✓ Refactoring abgeschlossen: {output_file}")
|
|
else:
|
|
session.status = "partial"
|
|
failed_chunks = [c for c in chunks if c.status == "failed"]
|
|
print(f"\n⚠ Refactoring teilweise abgeschlossen. {len(failed_chunks)} Chunks fehlgeschlagen.")
|
|
|
|
self.save_state()
|
|
return session
|
|
|
|
async def run_tests(self, file_path: str) -> bool:
|
|
"""Führt Tests für die refaktorierte Datei aus"""
|
|
print(f"\nFühre Tests aus für {file_path}...")
|
|
|
|
# Bestimme Test-Kommando basierend auf Dateityp
|
|
if file_path.endswith(".py"):
|
|
# Python Tests
|
|
test_cmd = ["python", "-m", "pytest", "-v", "--tb=short"]
|
|
if "backend" in file_path:
|
|
test_cmd.append("backend/tests/")
|
|
elif file_path.endswith((".ts", ".tsx", ".js")):
|
|
# TypeScript/JavaScript Tests
|
|
if "website" in file_path:
|
|
test_cmd = ["npm", "test", "--", "--passWithNoTests"]
|
|
else:
|
|
print(" Keine Tests für diesen Dateityp konfiguriert")
|
|
return True
|
|
|
|
try:
|
|
result = subprocess.run(
|
|
test_cmd,
|
|
cwd=PROJECT_ROOT,
|
|
capture_output=True,
|
|
text=True,
|
|
timeout=300,
|
|
)
|
|
if result.returncode == 0:
|
|
print(" ✓ Tests bestanden")
|
|
return True
|
|
else:
|
|
print(f" ✗ Tests fehlgeschlagen:\n{result.stdout}\n{result.stderr}")
|
|
return False
|
|
except Exception as e:
|
|
print(f" ✗ Test-Ausführung fehlgeschlagen: {e}")
|
|
return False
|
|
|
|
def print_status(self):
|
|
"""Zeigt den aktuellen Status aller Sessions"""
|
|
print("\n" + "=" * 70)
|
|
print("QWEN REFACTORING STATUS")
|
|
print("=" * 70)
|
|
|
|
if not self.sessions:
|
|
print("Keine aktiven Sessions")
|
|
return
|
|
|
|
for path, session in self.sessions.items():
|
|
completed = sum(1 for c in session.chunks if c.status == "completed")
|
|
failed = sum(1 for c in session.chunks if c.status == "failed")
|
|
pending = sum(1 for c in session.chunks if c.status == "pending")
|
|
|
|
status_icon = {
|
|
"pending": "○",
|
|
"processing": "◐",
|
|
"completed": "●",
|
|
"partial": "◑",
|
|
}.get(session.status, "?")
|
|
|
|
print(f"\n{status_icon} {path}")
|
|
print(f" Status: {session.status}")
|
|
print(f" Chunks: {completed}/{len(session.chunks)} completed, {failed} failed, {pending} pending")
|
|
if session.started_at:
|
|
print(f" Gestartet: {session.started_at.strftime('%Y-%m-%d %H:%M')}")
|
|
if session.completed_at:
|
|
print(f" Abgeschlossen: {session.completed_at.strftime('%Y-%m-%d %H:%M')}")
|
|
if session.tests_passed is not None:
|
|
print(f" Tests: {'✓ Bestanden' if session.tests_passed else '✗ Fehlgeschlagen'}")
|
|
|
|
|
|
async def main():
|
|
parser = argparse.ArgumentParser(description="Qwen Refactoring Orchestrator")
|
|
parser.add_argument("--file", help="Einzelne Datei refaktorieren")
|
|
parser.add_argument("--all-large-files", action="store_true", help="Alle großen Dateien refaktorieren")
|
|
parser.add_argument("--status", action="store_true", help="Status anzeigen")
|
|
parser.add_argument("--check-ollama", action="store_true", help="Ollama-Status prüfen")
|
|
parser.add_argument("--run-tests", help="Tests für refaktorierte Datei ausführen")
|
|
args = parser.parse_args()
|
|
|
|
orchestrator = QwenRefactorOrchestrator()
|
|
|
|
if args.status:
|
|
orchestrator.print_status()
|
|
return
|
|
|
|
if args.check_ollama:
|
|
print("Prüfe Ollama-Status auf Mac Mini...")
|
|
status = await orchestrator.check_ollama_status()
|
|
print(f"Status: {status['status']}")
|
|
if status.get("models"):
|
|
print(f"Verfügbare Modelle: {', '.join(status['models'])}")
|
|
if status.get("qwen_available"):
|
|
print("✓ Qwen2.5:32B ist verfügbar")
|
|
else:
|
|
print("✗ Qwen2.5:32B ist NICHT verfügbar")
|
|
if status.get("error"):
|
|
print(f"Fehler: {status['error']}")
|
|
return
|
|
|
|
if args.run_tests:
|
|
success = await orchestrator.run_tests(args.run_tests)
|
|
sys.exit(0 if success else 1)
|
|
|
|
if args.file:
|
|
# Finde Sprache für die Datei
|
|
language = "Unknown"
|
|
for f, _, lang in LARGE_FILES:
|
|
if f == args.file:
|
|
language = lang
|
|
break
|
|
if language == "Unknown":
|
|
if args.file.endswith(".py"):
|
|
language = "Python"
|
|
elif args.file.endswith((".ts", ".tsx")):
|
|
language = "TypeScript/React"
|
|
elif args.file.endswith(".js"):
|
|
language = "JavaScript"
|
|
|
|
await orchestrator.refactor_file(args.file, language)
|
|
return
|
|
|
|
if args.all_large_files:
|
|
print("Refaktoriere alle großen Dateien...")
|
|
for file_path, lines, language in LARGE_FILES:
|
|
try:
|
|
await orchestrator.refactor_file(file_path, language)
|
|
except Exception as e:
|
|
print(f"Fehler bei {file_path}: {e}")
|
|
return
|
|
|
|
# Default: Status anzeigen
|
|
orchestrator.print_status()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
asyncio.run(main())
|