fix: Restore all files lost during destructive rebase
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>
This commit is contained in:
478
scripts/qwen_refactor_orchestrator.py
Executable file
478
scripts/qwen_refactor_orchestrator.py
Executable file
@@ -0,0 +1,478 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user