From 759e64666aee752eee8985a5dbadfaf0a8cb6843 Mon Sep 17 00:00:00 2001 From: Alex Mueller Date: Sat, 29 Aug 2026 20:50:59 +0200 Subject: [PATCH] Implementiere Online-Lernplattform: Monaco Split-Screen Web-IDE, In-Browser Pyodide Test-Runner, User Auth, Auto-Save und Lehrer-Dashboard --- .gamification.json | 2 +- .gitignore | 2 + api.php | 184 ++++++++++++ assets/app_auth.js | 245 ++++++++++++++++ assets/gamification_data.js | 2 +- assets/monaco_ide.js | 333 ++++++++++++++++++++++ index.html | 538 +++++++++++++++++++----------------- server.py | 486 ++++++++++++++++++++++++++++++++ teacher.html | 248 +++++++++++++++++ workspace.html | 372 +++++++++++++++++++++++++ 10 files changed, 2160 insertions(+), 252 deletions(-) create mode 100644 api.php create mode 100644 assets/app_auth.js create mode 100644 assets/monaco_ide.js create mode 100644 server.py create mode 100644 teacher.html create mode 100644 workspace.html diff --git a/.gamification.json b/.gamification.json index 25906f3..f4ef2b6 100644 --- a/.gamification.json +++ b/.gamification.json @@ -10,5 +10,5 @@ "streak_tage": 1, "letzter_aktiver_tag": "2026-08-29", "historie": [], - "zuletzt_aktualisiert": "2026-08-29T16:00:34.190254" + "zuletzt_aktualisiert": "2026-08-29T20:50:51.073181" } \ No newline at end of file diff --git a/.gitignore b/.gitignore index df05013..af77a24 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ htmlcov/ venv/ .venv/ .DS_Store +*.db +platform_data.db diff --git a/api.php b/api.php new file mode 100644 index 0000000..4df2130 --- /dev/null +++ b/api.php @@ -0,0 +1,184 @@ +setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + +// Init Tabellen +$pdo->exec(" +CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + email TEXT UNIQUE NOT NULL, + password_hash TEXT NOT NULL, + salt TEXT NOT NULL, + name TEXT NOT NULL, + role TEXT DEFAULT 'student', + xp INTEGER DEFAULT 0, + level INTEGER DEFAULT 1, + streak_days INTEGER DEFAULT 1, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +CREATE TABLE IF NOT EXISTS chapter_progress ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + chapter_id TEXT NOT NULL, + code_draft TEXT, + subgoals_json TEXT, + is_solved INTEGER DEFAULT 0, + solved_at TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE(user_id, chapter_id) +); +CREATE TABLE IF NOT EXISTS classrooms ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + teacher_id INTEGER NOT NULL, + name TEXT NOT NULL, + invite_code TEXT UNIQUE NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); +CREATE TABLE IF NOT EXISTS class_enrollments ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + classroom_id INTEGER NOT NULL, + user_id INTEGER NOT NULL, + joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE(classroom_id, user_id) +); +"); + +$path = $_SERVER['PATH_INFO'] ?? $_GET['action'] ?? ''; +$body = json_decode(file_get_contents('php://input'), true) ?? []; + +// Helper Auth +function get_auth_user($pdo) { + $headers = getallheaders(); + $auth = $headers['Authorization'] ?? $headers['authorization'] ?? ''; + if (strpos($auth, 'Bearer ') === 0) { + $token = substr($auth, 7); + $parts = explode('.', $token); + if (count($parts) === 2) { + $payload = json_decode(base64_decode($parts[0]), true); + if ($payload && isset($payload['uid'])) { + return $payload; + } + } + } + return null; +} + +function create_token($uid, $email, $role) { + $payload = base64_encode(json_encode(['uid' => $uid, 'email' => $email, 'role' => $role])); + $sig = hash_hmac('sha256', $payload, 'python_lernportal_secret'); + return $payload . '.' . $sig; +} + +// 1. REGISTER +if ($path === '/auth/register') { + $email = strtolower(trim($body['email'] ?? '')); + $password = $body['password'] ?? ''; + $name = trim($body['name'] ?? ''); + $role = $body['role'] ?? 'student'; + + if (!$email || !$password || !$name) { + http_response_code(400); + echo json_encode(['error' => 'Pflichtfelder fehlen']); + exit; + } + + $salt = bin2hex(random_bytes(16)); + $hash = hash_pbkdf2('sha256', $password, $salt, 100000); + + try { + $stmt = $pdo->prepare("INSERT INTO users (email, password_hash, salt, name, role) VALUES (?, ?, ?, ?, ?)"); + $stmt->execute([$email, $hash, $salt, $name, $role]); + $uid = $pdo->lastInsertId(); + $token = create_token($uid, $email, $role); + http_response_code(201); + echo json_encode(['token' => $token, 'user' => ['id' => $uid, 'email' => $email, 'name' => $name, 'role' => $role, 'xp' => 0, 'level' => 1]]); + } catch (Exception $e) { + http_response_code(409); + echo json_encode(['error' => 'E-Mail existiert bereits']); + } + exit; +} + +// 2. LOGIN +if ($path === '/auth/login') { + $email = strtolower(trim($body['email'] ?? '')); + $password = $body['password'] ?? ''; + + $stmt = $pdo->prepare("SELECT * FROM users WHERE email = ?"); + $stmt->execute([$email]); + $user = $stmt->fetch(PDO::FETCH_ASSOC); + + if ($user) { + $hash = hash_pbkdf2('sha256', $password, $user['salt'], 100000); + if (hash_equals($hash, $user['password_hash'])) { + $token = create_token($user['id'], $user['email'], $user['role']); + echo json_encode([ + 'token' => $token, + 'user' => [ + 'id' => $user['id'], + 'email' => $user['email'], + 'name' => $user['name'], + 'role' => $user['role'], + 'xp' => (int)$user['xp'], + 'level' => (int)$user['level'] + ] + ]); + exit; + } + } + http_response_code(401); + echo json_encode(['error' => 'Ungültige Anmeldedaten']); + exit; +} + +// 3. PROGRESS SAVE +if ($path === '/progress/save') { + $user = get_auth_user($pdo); + if (!$user) { http_response_code(401); echo json_encode(['error' => 'Nicht angemeldet']); exit; } + + $chapter_id = $body['chapter_id'] ?? ''; + $code_draft = $body['code_draft'] ?? ''; + + $stmt = $pdo->prepare("INSERT INTO chapter_progress (user_id, chapter_id, code_draft) VALUES (?, ?, ?) ON CONFLICT(user_id, chapter_id) DO UPDATE SET code_draft = excluded.code_draft, updated_at = CURRENT_TIMESTAMP"); + $stmt->execute([$user['uid'], $chapter_id, $code_draft]); + echo json_encode(['status' => 'saved']); + exit; +} + +// 4. PROGRESS SOLVE +if ($path === '/progress/solve') { + $user = get_auth_user($pdo); + if (!$user) { http_response_code(401); echo json_encode(['error' => 'Nicht angemeldet']); exit; } + + $chapter_id = $body['chapter_id'] ?? ''; + $xp_reward = (int)($body['xp_reward'] ?? 100); + + $stmt = $pdo->prepare("INSERT INTO chapter_progress (user_id, chapter_id, is_solved, solved_at) VALUES (?, ?, 1, CURRENT_TIMESTAMP) ON CONFLICT(user_id, chapter_id) DO UPDATE SET is_solved = 1, solved_at = CURRENT_TIMESTAMP"); + $stmt->execute([$user['uid'], $chapter_id]); + + $stmt = $pdo->prepare("UPDATE users SET xp = xp + ?, level = ((xp + ?) / 100) + 1 WHERE id = ?"); + $stmt->execute([$xp_reward, $xp_reward, $user['uid']]); + + echo json_encode(['status' => 'solved', 'earned_xp' => $xp_reward]); + exit; +} + +http_response_code(404); +echo json_encode(['error' => 'Endpunkt nicht gefunden']); diff --git a/assets/app_auth.js b/assets/app_auth.js new file mode 100644 index 0000000..76699fd --- /dev/null +++ b/assets/app_auth.js @@ -0,0 +1,245 @@ +/** + * 🔐 AUTHENTIFIZIERUNG & USER MANAGEMENT 🔐 + * ========================================== + * Verarbeitet Login, Registrierung, JWT-Tokens und Lehrer-Klassenzuweisung. + * Mit automatischem Offline-Fallback (LocalStorage), falls kein Server läuft. + */ + +(function () { + window.AUTH = { + token: localStorage.getItem("auth_token") || null, + user: JSON.parse(localStorage.getItem("auth_user") || "null"), + + async register(name, email, password, role = "student") { + try { + const res = await fetch("/api/auth/register", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name, email, password, role }) + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || "Registrierung fehlgeschlagen"); + + this.setSession(data.token, data.user); + return { success: true, user: data.user }; + } catch (err) { + // Fallback für statische Offline-Nutzung + if (err.message.includes("Failed to fetch")) { + const mockUser = { id: Date.now(), name, email, role, xp: 0, level: 1 }; + this.setSession("offline_token_" + Date.now(), mockUser); + return { success: true, user: mockUser, offline: true }; + } + return { success: false, error: err.message }; + } + }, + + async login(email, password) { + try { + const res = await fetch("/api/auth/login", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ email, password }) + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error || "Anmeldung fehlgeschlagen"); + + this.setSession(data.token, data.user); + return { success: true, user: data.user }; + } catch (err) { + if (err.message.includes("Failed to fetch")) { + const mockUser = { id: 1, name: email.split("@")[0], email, role: "student", xp: 150, level: 2 }; + this.setSession("offline_token", mockUser); + return { success: true, user: mockUser, offline: true }; + } + return { success: false, error: err.message }; + } + }, + + logout() { + this.token = null; + this.user = null; + localStorage.removeItem("auth_token"); + localStorage.removeItem("auth_user"); + window.location.reload(); + }, + + setSession(token, user) { + this.token = token; + this.user = user; + localStorage.setItem("auth_token", token); + localStorage.setItem("auth_user", JSON.stringify(user)); + this.updateUI(); + }, + + async fetchMe() { + if (!this.token) return null; + try { + const res = await fetch("/api/auth/me", { + headers: { Authorization: `Bearer ${this.token}` } + }); + if (res.ok) { + const data = await res.json(); + this.user = data.user; + localStorage.setItem("auth_user", JSON.stringify(data.user)); + this.updateUI(); + return data.user; + } + } catch (e) {} + return this.user; + }, + + updateUI() { + const userBadge = document.getElementById("nav-auth-user"); + const loginBtn = document.getElementById("nav-btn-login"); + const registerBtn = document.getElementById("nav-btn-register"); + const teacherLink = document.getElementById("nav-teacher-link"); + + if (this.user) { + if (userBadge) { + userBadge.style.display = "inline-flex"; + userBadge.innerHTML = `👤 ${this.user.name} ${this.user.xp || 0} XP`; + } + if (loginBtn) loginBtn.style.display = "none"; + if (registerBtn) { + registerBtn.innerText = "Abmelden"; + registerBtn.onclick = (e) => { + e.preventDefault(); + this.logout(); + }; + } + if (teacherLink && this.user.role === "teacher") { + teacherLink.style.display = "inline-flex"; + } + } else { + if (userBadge) userBadge.style.display = "none"; + if (loginBtn) loginBtn.style.display = "inline-flex"; + if (registerBtn) { + registerBtn.innerText = "Konto erstellen"; + registerBtn.onclick = () => window.openAuthModal("register"); + } + if (teacherLink) teacherLink.style.display = "none"; + } + } + }; + + // Auth Modal UI + window.openAuthModal = function (mode = "login") { + let modal = document.getElementById("auth-modal-overlay"); + if (!modal) { + modal = document.createElement("div"); + modal.id = "auth-modal-overlay"; + modal.className = "certificate-overlay"; + modal.innerHTML = ` +
+

🔑 Anmelden

+

Melde dich an, um deinen Code und deine XP in der Cloud zu speichern.

+ +
+ + +
+ + +
+ +
+ + +
+ + + + + + + +
+ +
+ Noch kein Konto? Jetzt registrieren → +
+
+ `; + document.body.appendChild(modal); + } + + const titleEl = document.getElementById("auth-modal-title"); + const nameField = document.getElementById("auth-field-name"); + const roleField = document.getElementById("auth-field-role"); + const submitBtn = document.getElementById("auth-submit-btn"); + const toggleLink = document.getElementById("auth-toggle-mode"); + const errorMsg = document.getElementById("auth-error-msg"); + const form = document.getElementById("auth-form"); + + let isRegister = mode === "register"; + + function updateModalMode() { + if (isRegister) { + titleEl.innerText = "✨ Neues Konto erstellen"; + nameField.style.display = "block"; + roleField.style.display = "block"; + submitBtn.innerText = "Konto registrieren"; + toggleLink.innerText = "Bereits registriert? Hier anmelden →"; + } else { + titleEl.innerText = "🔑 Anmelden"; + nameField.style.display = "none"; + roleField.style.display = "none"; + submitBtn.innerText = "Anmelden"; + toggleLink.innerText = "Noch kein Konto? Jetzt registrieren →"; + } + errorMsg.style.display = "none"; + } + + updateModalMode(); + + toggleLink.onclick = (e) => { + e.preventDefault(); + isRegister = !isRegister; + updateModalMode(); + }; + + form.onsubmit = async (e) => { + e.preventDefault(); + errorMsg.style.display = "none"; + const email = document.getElementById("auth-input-email").value.trim(); + const password = document.getElementById("auth-input-password").value; + const name = document.getElementById("auth-input-name")?.value.trim() || email.split("@")[0]; + const role = document.getElementById("auth-select-role")?.value || "student"; + + submitBtn.disabled = true; + submitBtn.innerText = "Bitte warten..."; + + let result; + if (isRegister) { + result = await window.AUTH.register(name, email, password, role); + } else { + result = await window.AUTH.login(email, password); + } + + submitBtn.disabled = false; + submitBtn.innerText = isRegister ? "Konto registrieren" : "Anmelden"; + + if (result.success) { + document.getElementById("auth-modal-overlay")?.remove(); + alert(`Willkommen zurück, ${result.user.name}! 👋`); + } else { + errorMsg.innerText = result.error || "Ein Fehler ist aufgetreten"; + errorMsg.style.display = "block"; + } + }; + }; + + document.addEventListener("DOMContentLoaded", () => { + window.AUTH.updateUI(); + window.AUTH.fetchMe(); + }); +})(); diff --git a/assets/gamification_data.js b/assets/gamification_data.js index 3825302..c2a1cbb 100644 --- a/assets/gamification_data.js +++ b/assets/gamification_data.js @@ -11,7 +11,7 @@ window.GAMIFICATION_DATA = { "streak_tage": 1, "letzter_aktiver_tag": "2026-08-29", "historie": [], - "zuletzt_aktualisiert": "2026-08-29T16:00:34.190254" + "zuletzt_aktualisiert": "2026-08-29T20:50:51.073181" }; window.GAMIFICATION_BADGES_KATALOG = { "rechenkonegin": { diff --git a/assets/monaco_ide.js b/assets/monaco_ide.js new file mode 100644 index 0000000..50e529e --- /dev/null +++ b/assets/monaco_ide.js @@ -0,0 +1,333 @@ +/** + * ⚡ MONACO WEB-IDE & PYODIDE TEST-RUNNER ENGINE ⚡ + * ================================================= + * Interaktive Split-Screen Entwicklungsumgebung mit: + * - Monaco Editor (VS Code Engine mit Autocomplete & Syntax-Highlighting) + * - In-Browser Pyodide WebAssembly Python Runner (95% Client-Execution) + * - Automatischer Test-Auswertung mit XP-Vergabe + * - Cloud Auto-Save + */ + +(function () { + let monacoEditor = null; + let pyodideInstance = null; + let pyodideLoading = false; + let currentChapterData = null; + let autoSaveTimeout = null; + + // 1. Initialisiere Monaco Editor via CDN + function initMonacoEditor(initialCode = "") { + const editorContainer = document.getElementById("monaco-editor-container"); + if (!editorContainer) return; + + if (window.monaco) { + createMonacoInstance(initialCode); + return; + } + + // Monaco CDN Loader + const loaderScript = document.createElement("script"); + loaderScript.src = "https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.45.0/min/vs/loader.min.js"; + loaderScript.onload = () => { + window.require.config({ + paths: { vs: "https://cdnjs.cloudflare.com/ajax/libs/monaco-editor/0.45.0/min/vs" } + }); + window.require(["vs/editor/editor.main"], () => { + createMonacoInstance(initialCode); + }); + }; + loaderScript.onerror = () => { + // Fallback: Einfache Textarea falls CDN offline + editorContainer.innerHTML = ``; + }; + document.head.appendChild(loaderScript); + } + + function createMonacoInstance(initialCode) { + const editorContainer = document.getElementById("monaco-editor-container"); + if (!editorContainer) return; + editorContainer.innerHTML = ""; + + monacoEditor = window.monaco.editor.create(editorContainer, { + value: initialCode, + language: "python", + theme: "vs-dark", + fontSize: 14, + fontFamily: "'JetBrains Mono', 'Fira Code', Consolas, monospace", + automaticLayout: true, + minimap: { enabled: false }, + scrollBeyondLastLine: false, + tabSize: 4, + insertSpaces: true, + lineNumbers: "on", + bracketPairColorization: { enabled: true } + }); + + // Auto-Save Trigger beim Tippen + monacoEditor.onDidChangeModelContent(() => { + triggerAutoSave(); + }); + } + + // 2. Pyodide WebAssembly Initialisierung + async function getPyodide() { + if (pyodideInstance) return pyodideInstance; + if (pyodideLoading) { + while (pyodideLoading) { + await new Promise((r) => setTimeout(r, 100)); + } + return pyodideInstance; + } + + pyodideLoading = true; + logToTerminal("⏳ Lade Python WebAssembly Engine (Pyodide)... Bitte kurz warten.\n", "info"); + + try { + if (!window.loadPyodide) { + await new Promise((resolve, reject) => { + const script = document.createElement("script"); + script.src = "https://cdn.jsdelivr.net/pyodide/v0.25.0/full/pyodide.js"; + script.onload = resolve; + script.onerror = reject; + document.head.appendChild(script); + }); + } + + pyodideInstance = await window.loadPyodide({ + stdout: (text) => logToTerminal(text + "\n", "stdout"), + stderr: (text) => logToTerminal(text + "\n", "stderr") + }); + + logToTerminal("✅ Python WebAssembly erfolgreich bereitgestellt!\n\n", "success"); + } catch (err) { + logToTerminal(`❌ Fehler beim Laden von Pyodide: ${err.message}\n`, "error"); + } finally { + pyodideLoading = false; + } + return pyodideInstance; + } + + // 3. Code-Ausführung (Run) + async function runCode() { + const code = getEditorCode(); + clearTerminal(); + logToTerminal("▶ Starte Python-Skript...\n----------------------------------------\n", "info"); + + const py = await getPyodide(); + if (!py) return; + + try { + await py.runPythonAsync(code); + logToTerminal("\n----------------------------------------\n✨ Skript erfolgreich ausgeführt!\n", "success"); + } catch (err) { + logToTerminal(`\n❌ Ausführungsfehler:\n${err.message}\n`, "error"); + suggestErrorInterpreter(err.message); + } + } + + // 4. Unittest-Ausführung (Test Runner) + async function runTests() { + if (!currentChapterData || !currentChapterData.testCode) { + logToTerminal("❌ Keine Unittests für dieses Kapitel gefunden.\n", "error"); + return; + } + + const userCode = getEditorCode(); + clearTerminal(); + logToTerminal("🧪 Führe automatisierte Unittests aus...\n----------------------------------------\n", "info"); + + const py = await getPyodide(); + if (!py) return; + + try { + // 1. Schreibe aufgabe.py in das Pyodide-Dateisystem + py.FS.writeFile("aufgabe.py", userCode); + + // 2. Führe test_aufgabe.py aus + const testRunnerScript = ` +import unittest, io, sys +from unittest import TextTestRunner + +for mod in list(sys.modules.keys()): + if mod in ('aufgabe', 'test_aufgabe'): + del sys.modules[mod] + +import aufgabe + +${currentChapterData.testCode} + +suite = unittest.TestLoader().loadTestsFromModule(sys.modules[__name__]) +stream = io.StringIO() +runner = TextTestRunner(stream=stream, verbosity=2) +result = runner.run(suite) + +output_text = stream.getvalue() +success = result.wasSuccessful() +total_runs = result.testsRun +failures = len(result.failures) +errors = len(result.errors) +(success, total_runs, failures, errors, output_text) +`; + + const [isSuccess, totalRuns, fails, errs, outputText] = await py.runPythonAsync(testRunnerScript); + + logToTerminal(outputText + "\n", "stdout"); + + if (isSuccess) { + logToTerminal(`\n🎉 HERVORRAGEND! Alle ${totalRuns} Tests erfolgreich bestanden! (+100 XP)\n`, "success"); + triggerSuccessCelebration(); + recordChapterSolved(); + } else { + logToTerminal(`\n⚠️ ${fails + errs} von ${totalRuns} Tests fehlgeschlagen. Überarbeite deinen Code!\n`, "warning"); + suggestErrorInterpreter(outputText); + } + } catch (err) { + logToTerminal(`\n❌ Fehler beim Ausführen der Testsuite:\n${err.message}\n`, "error"); + suggestErrorInterpreter(err.message); + } + } + + // 5. Hilfsfunktionen + function getEditorCode() { + if (monacoEditor) { + return monacoEditor.getValue(); + } + const fallback = document.getElementById("fallback-code-editor"); + return fallback ? fallback.value : ""; + } + + function setEditorCode(code) { + if (monacoEditor) { + monacoEditor.setValue(code); + } else { + const fallback = document.getElementById("fallback-code-editor"); + if (fallback) fallback.value = code; + } + } + + function logToTerminal(text, type = "stdout") { + const term = document.getElementById("workspace-terminal-output"); + if (!term) return; + + const span = document.createElement("span"); + span.className = `term-line term-${type}`; + span.innerText = text; + term.appendChild(span); + term.scrollTop = term.scrollHeight; + } + + function clearTerminal() { + const term = document.getElementById("workspace-terminal-output"); + if (term) term.innerHTML = ""; + } + + function triggerAutoSave() { + clearTimeout(autoSaveTimeout); + autoSaveTimeout = setTimeout(() => { + saveProgressDraft(); + }, 1500); // 1.5s nach letztem Tastenanschlag speichern + } + + async function saveProgressDraft() { + if (!currentChapterData || !currentChapterData.chapterId) return; + const code = getEditorCode(); + + // 1. Lokal in LocalStorage + localStorage.setItem(`code_draft_${currentChapterData.chapterId}`, code); + + // 2. In Backend-Cloud falls angemeldet + const token = localStorage.getItem("auth_token"); + if (token) { + try { + await fetch("/api/progress/save", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + chapter_id: currentChapterData.chapterId, + code_draft: code + }) + }); + } catch (e) {} + } + + const saveBadge = document.getElementById("editor-save-indicator"); + if (saveBadge) { + saveBadge.innerText = "💾 Gespeichert"; + saveBadge.style.opacity = "1"; + setTimeout(() => { + saveBadge.style.opacity = "0.5"; + }, 2000); + } + } + + async function recordChapterSolved() { + if (!currentChapterData || !currentChapterData.chapterId) return; + + // 1. Lokale Gamification + if (window.addXP) { + window.addXP(100, currentChapterData.chapterId); + } + + // 2. Backend Cloud + const token = localStorage.getItem("auth_token"); + if (token) { + try { + await fetch("/api/progress/solve", { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}` + }, + body: JSON.stringify({ + chapter_id: currentChapterData.chapterId, + xp_reward: 100 + }) + }); + } catch (e) {} + } + } + + function triggerSuccessCelebration() { + const banner = document.getElementById("success-modal-banner"); + if (banner) { + banner.style.display = "flex"; + } + } + + function suggestErrorInterpreter(traceback) { + const errorTab = document.getElementById("tab-btn-interpreter"); + const errorInput = document.getElementById("workspace-error-input"); + if (errorInput) { + errorInput.value = traceback; + } + if (errorTab) { + errorTab.style.borderColor = "var(--danger)"; + errorTab.innerHTML = "🩺 Fehler-Dolmetscher ⚠️"; + } + } + + // 6. Init Workspace Controller + window.initWorkspace = function (chapterData) { + currentChapterData = chapterData; + + // Lade gespeicherten Code-Draft oder Startercode + const savedDraft = localStorage.getItem(`code_draft_${chapterData.chapterId}`); + const initialCode = savedDraft || chapterData.starterCode || ""; + + initMonacoEditor(initialCode); + + // Event Listener für Buttons + document.getElementById("btn-run-code")?.addEventListener("click", runCode); + document.getElementById("btn-run-tests")?.addEventListener("click", runTests); + document.getElementById("btn-save-code")?.addEventListener("click", saveProgressDraft); + document.getElementById("btn-clear-terminal")?.addEventListener("click", clearTerminal); + document.getElementById("btn-reset-code")?.addEventListener("click", () => { + if (confirm("Möchtest du deinen Code wirklich auf den Original-Startercode zurücksetzen?")) { + setEditorCode(chapterData.starterCode || ""); + } + }); + }; +})(); diff --git a/index.html b/index.html index 2c196bd..632415f 100644 --- a/index.html +++ b/index.html @@ -9,7 +9,7 @@
-
-
+
- -
- -
- - -
-

👋 Willkommen zu deiner Python-Lernreise!

-

- Dieses interaktive Portal begleitet dich Schritt für Schritt von den allerersten Zeilen Code bis hin zu vollständigen, objektorientierten Desktop-Anwendungen. Jede Einheit bietet didaktische Alltagsanalogien, Subgoal-Schritte und automatische Unittests für sofortiges Feedback. -

- -
-
- Lehrpfad 1 -

🌱 Grundlagen der Programmierung

-

Rechnen, Variablen, Bedingungen, Schleifen, Funktionen, Listen, Dictionaries & Strings (G01–G10).

+
+
+
🐍 Python Online-Lernplattform & Web-IDE
+
+ + + + +
-
- Lehrpfad 2 -

🏗️ Einstieg in OOP & Tools

-

Klassen, __init__, Methoden, UML-Diagramme, VS Code, Git-Versionskontrolle & Tamagotchi (00–06).

-
-
- Lehrpfad 3 -

🚀 Fortgeschrittenes OOP & GUIs

-

Speicher, Dunder-Methoden, Unit Tests (TDD), Vererbung, Exceptions, JSON, Tkinter & Masterprojekt (07–16).

-
-
- -
-
📚 Lehrplan-Abstimmung:
- Alle Einheiten sind exakt auf das offizielle Unterrichtsskriptum abgestimmt. Detaillierter Leitfaden in LEHRPLAN.md. -
-
- - -
-

📖 Schritt-für-Schritt-Tutorial: So lernst du mit dem Portal

-

- Dieses Portal basiert auf dem PRIMM-Modell der Informatikdidaktik (Predict • Read • Investigate • Modify • Make): -

- -
+

Vom Einsteiger zum Software-Entwickler

+

3 didaktisch strukturierte Lehrpfade mit integrierter **Monaco Web-IDE**, In-Browser Python-Runner (Pyodide), Aufgaben-Checklisten & persistentem Entwickler-Pass.

-
-
1
-
-

📥 Lokal nutzen oder online lernen

-

Du kannst das Repository lokal klonen (git clone https://github.com/alexander-mueller/python-oop-lernportal.git) oder die Seiten direkt offline per Doppelklick auf index.html öffnen.

-
-
- -
-
2
-
-

🧩 Kapitel lesen & Parsons-Puzzle lösen (+25 XP)

-

Lies die Alltagsanalogie im Kapitel-Guide und bringe die durcheinandergeratenen Code-Zeilen im interaktiven Puzzle per Klick in die richtige Reihenfolge.

-
-
- -
-
3
-
-

💻 Startercode lösen & Tests ausführen (+100 XP)

-

Öffne aufgabe.py im Editor, bearbeite die nummerierten Teilziele und teste deine Lösung im Terminal mit python3 test_aufgabe.py.

-
-
- -
-
4
-
-

🩺 Hilfe bei Fehlern

-

Kopiere rote Traceback-Fehlermeldungen aus dem Terminal in den untenstehenden Fehler-Dolmetscher, um verständliche Lösungsschritte auf Deutsch zu erhalten.

-
-
- +
-
+ - -
- 🔍 - - -
+
- -
-
-
-

🌱 Lehrpfad 1: Grundlagen der Programmierung

-
Datentypen, Kontrollstrukturen, Funktionen und Datenstrukturen (G01–G10).
+ +
+

👋 Willkommen zu deiner Python-Lernreise!

+

+ Dieses Portal begleitet dich Schritt für Schritt von den ersten Schritten in Python bis hin zu vollständigen Desktop-Apps. Du kannst jedes Kapitel direkt in der integrierten Monaco Web-IDE im Browser programmieren und mit 1 Klick gegen automatische Tests prüfen! +

+ +
+
+ Lehrpfad 1 +

🌱 Grundlagen der Programmierung

+

Rechnen, Variablen, Bedingungen, Schleifen, Funktionen, Listen, Dictionaries & Strings (G01–G10).

+
+
+ Lehrpfad 2 +

🏗️ Einstieg in OOP & Tools

+

Klassen, __init__, Methoden, UML-Diagramme, VS Code, Git-Versionskontrolle & Tamagotchi (00–06).

+
+
+ Lehrpfad 3 +

🚀 Fortgeschrittenes OOP & GUIs

+

Speicher, Dunder-Methoden, Unit Tests (TDD), Vererbung, Exceptions, JSON, Tkinter & Masterprojekt (07–16).

+
- 10 Kapitel + +
+
📚 Lehrplan-Abstimmung:
+ Alle Einheiten sind exakt auf das offizielle Unterrichtsskriptum abgestimmt. Detaillierter Leitfaden in LEHRPLAN.md. +
+
+ + +
+

📖 Schritt-für-Schritt-Tutorial: So lernst du mit der Online-Plattform

+

+ Dieses Portal basiert auf dem PRIMM-Modell der Informatikdidaktik: +

+ +
+ +
+
1
+
+

💻 In Web-IDE öffnen

+

Klicke bei jedem Kapitel auf 'In Web-IDE öffnen', um den Monaco Code-Editor mit Startercode und Teilzielen direkt im Browser zu starten.

+
+
+ +
+
2
+
+

🧩 Parsons-Puzzle lösen (+25 XP)

+

Bringe im Tab 'Puzzle' die durcheinandergeratenen Code-Zeilen in die richtige Reihenfolge, um ein intuitives Verständnis des Codes aufzubauen.

+
+
+ +
+
3
+
+

🧪 Tests im Browser ausführen (+100 XP)

+

Klicke oben auf '🧪 Tests starten'. Die Python WebAssembly-Engine prüft deinen Code sofort und vergibt bei Erfolg XP und Trophäen.

+
+
+ +
+
4
+
+

🩺 Hilfe bei Fehlern & Auto-Save

+

Dein Code wird automatisch in deinem Konto gespeichert. Tritt ein Fehler auf, erhältst du im Fehler-Dolmetscher sofort eine verständliche Erklärung auf Deutsch.

+
+
+ +
+
+ + +
+ 🔍 + +
-
- + + +
+
+
+

🌱 Lehrpfad 1: Grundlagen der Programmierung

+
Datentypen, Kontrollstrukturen, Funktionen und Datenstrukturen (G01–G10).
+
+ 10 Kapitel +
+
+
G01 @@ -298,7 +306,8 @@
Zahlen (int, float), Grundrechenarten, Ganzzahldivision //, Modulo %, Potenzen ** & print().
- 📖 Kapitel öffnen → + 💻 In Web-IDE öffnen → + 📖 Kapitel-Guide lesen
- 📖 Kapitel öffnen → + 💻 In Web-IDE öffnen → + 📖 Kapitel-Guide lesen
- 📖 Kapitel öffnen → + 💻 In Web-IDE öffnen → + 📖 Kapitel-Guide lesen
- 📖 Kapitel öffnen → + 💻 In Web-IDE öffnen → + 📖 Kapitel-Guide lesen
- 📖 Kapitel öffnen → + 💻 In Web-IDE öffnen → + 📖 Kapitel-Guide lesen
- 📖 Kapitel öffnen → + 💻 In Web-IDE öffnen → + 📖 Kapitel-Guide lesen
- 📖 Kapitel öffnen → + 💻 In Web-IDE öffnen → + 📖 Kapitel-Guide lesen
- 📖 Kapitel öffnen → + 💻 In Web-IDE öffnen → + 📖 Kapitel-Guide lesen
- 📖 Kapitel öffnen → + 💻 In Web-IDE öffnen → + 📖 Kapitel-Guide lesen
-
-
- - -
-
-
-

🏗️ Lehrpfad 2: Einstieg in OOP & Entwickler-Tools

-
Klassen, Konstruktoren, Methoden, Komposition, VS Code, Git & Tamagotchi (00–06).
- 9 Module -
-
- +
+ + +
+
+
+

🏗️ Lehrpfad 2: Einstieg in OOP & Entwickler-Tools

+
Klassen, Konstruktoren, Methoden, Komposition, VS Code, Git & Tamagotchi (00–06).
+
+ 9 Module +
+
+
OOP 00 @@ -472,7 +490,8 @@
Typische Syntax-, Type- und Index-Fehler verstehen, Fehlermeldungen lesen und gezielt beheben.
- 📖 Kapitel öffnen → + 💻 In Web-IDE öffnen → + 📖 Kapitel-Guide lesen
- 📖 Kapitel öffnen → + 💻 In Web-IDE öffnen → + 📖 Kapitel-Guide lesen
- 📖 Kapitel öffnen → + 💻 In Web-IDE öffnen → + 📖 Kapitel-Guide lesen
- 📖 Kapitel öffnen → + 💻 In Web-IDE öffnen → + 📖 Kapitel-Guide lesen
- 📖 Kapitel öffnen → + 💻 In Web-IDE öffnen → + 📖 Kapitel-Guide lesen
- 📖 Kapitel öffnen → + 💻 In Web-IDE öffnen → + 📖 Kapitel-Guide lesen
- 📖 Kapitel öffnen → + 💻 In Web-IDE öffnen → + 📖 Kapitel-Guide lesen
- 📖 Kapitel öffnen → + 💻 In Web-IDE öffnen → + 📖 Kapitel-Guide lesen
-
-
- - -
-
-
-

🚀 Lehrpfad 3: Fortgeschrittenes OOP & GUIs

-
Dunder-Methoden, Unit Tests (TDD), Vererbung, Exceptions, JSON, Tkinter & Masterprojekt (07–16).
- 10 Module -
-
- +
+ + +
+
+
+

🚀 Lehrpfad 3: Fortgeschrittenes OOP & GUIs

+
Dunder-Methoden, Unit Tests (TDD), Vererbung, Exceptions, JSON, Tkinter & Masterprojekt (07–16).
+
+ 10 Module +
+
+
Adv 07 @@ -628,7 +655,8 @@
Veränderliche vs. unveränderliche Datentypen, is vs. ==, flache und tiefe Kopien.
- 📖 Kapitel öffnen → + 💻 In Web-IDE öffnen → + 📖 Kapitel-Guide lesen
- 📖 Kapitel öffnen → + 💻 In Web-IDE öffnen → + 📖 Kapitel-Guide lesen
- 📖 Kapitel öffnen → + 💻 In Web-IDE öffnen → + 📖 Kapitel-Guide lesen
- 📖 Kapitel öffnen → + 💻 In Web-IDE öffnen → + 📖 Kapitel-Guide lesen
- 📖 Kapitel öffnen → + 💻 In Web-IDE öffnen → + 📖 Kapitel-Guide lesen
- 📖 Kapitel öffnen → + 💻 In Web-IDE öffnen → + 📖 Kapitel-Guide lesen
- 📖 Kapitel öffnen → + 💻 In Web-IDE öffnen → + 📖 Kapitel-Guide lesen
- 📖 Kapitel öffnen → + 💻 In Web-IDE öffnen → + 📖 Kapitel-Guide lesen
- 📖 Kapitel öffnen → + 💻 In Web-IDE öffnen → + 📖 Kapitel-Guide lesen
-
-
+
+
- -
-
-
-
🐣
-
-
Entwickler-Pass
-
Code-Küken • Rang: Bronze
+ +
+
+
+
🐣
+
+
Entwickler-Pass
+
Code-Küken • Rang: Bronze
+
+
+
+
+ 1 + Level +
+
+ 0 XP + Erfahrung +
+
+ 0 / 27 + Gelöst +
+
+ 🔥 1 Tag + Streak +
-
-
- 1 - Level -
-
- 0 XP - Erfahrung -
-
- 0 / 27 - Gelöst -
-
- 🔥 1 Tag - Streak -
-
-
-
-
- Fortschritt zum nächsten Level - 0 / 100 XP +
+
+ Fortschritt zum nächsten Level + 0 / 100 XP +
+
+
+
-
-
-
-
-
+ - -
-

⚡ Interaktiver Python Live-Playground (Pyodide WebAssembly)

-

Führe beliebigen Python-Code direkt im Browser aus – ohne Installation!

- -
-
- 🐍 Python Code-Editor - -
- -
+
-
-
🖥️ Programmausgabe (Terminal):
-
Klicke oben auf '▶ Code ausführen', um das Skript zu starten...
-
- +
+
🖥️ Programmausgabe (Terminal):
+
Klicke oben auf '▶ Code ausführen', um das Skript zu starten...
+
+ - -
-

🩺 Interaktiver Python Fehler-Dolmetscher

-

Füge eine englische Fehlermeldung (Traceback) aus dem Terminal ein:

- - - + +
+

🩺 Interaktiver Python Fehler-Dolmetscher

+

Füge eine englische Fehlermeldung (Traceback) aus dem Terminal ein:

+ + + - -
+ +
- -
-

🏆 Lehrpfad-Abschluss-Zertifikate

-

Generiere offizielle, druckbare Zertifikate nach erfolgreichem Abschluss eines Lehrpfads:

+ +
+

🏆 Lehrpfad-Abschluss-Zertifikate

+

Generiere offizielle, druckbare Zertifikate nach erfolgreichem Abschluss eines Lehrpfads:

-
- - - - -
-
+
+ + + + +
+
- + @@ -908,6 +945,7 @@ print(mein_auto.hupen()) + diff --git a/server.py b/server.py new file mode 100644 index 0000000..1600411 --- /dev/null +++ b/server.py @@ -0,0 +1,486 @@ +#!/usr/bin/env python3 +""" +🐍 PYTHON LERNPLATTFORM – BACKEND SERVER 🐍 +=========================================== +Leichtgewichtiger, abhängigkeitsfreier REST-API & Static Server für die Online-Lernplattform. +- Läuft auf jedem Linux-Server / Webspace mit Standard-Python 3. +- SQLite-Datenbank (User-Accounts, Code-Drafts, XP, Klassen & Lehrer-Dashboard). +- PBKDF2-HMAC-SHA256 Passwort-Verschlüsselung & signierte Session-Tokens. +""" + +import http.server +import socketserver +import json +import sqlite3 +import hashlib +import hmac +import secrets +import time +import os +import urllib.parse +from pathlib import Path + +PORT = int(os.environ.get("PORT", 8000)) +BASE_DIR = Path(__file__).parent.resolve() +DB_FILE = BASE_DIR / "platform_data.db" +SECRET_KEY = secrets.token_hex(32) + +# ============================================================================== +# DATENBANK INITIALISIERUNG +# ============================================================================== + +def get_db(): + conn = sqlite3.connect(str(DB_FILE)) + conn.row_factory = sqlite3.Row + return conn + +def init_db(): + conn = get_db() + c = conn.cursor() + + # 1. Benutzer-Tabelle + c.execute(""" + CREATE TABLE IF NOT EXISTS users ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + email TEXT UNIQUE NOT NULL, + password_hash TEXT NOT NULL, + salt TEXT NOT NULL, + name TEXT NOT NULL, + role TEXT DEFAULT 'student', + xp INTEGER DEFAULT 0, + level INTEGER DEFAULT 1, + streak_days INTEGER DEFAULT 1, + last_active_date TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + )""") + + # 2. Kapitel-Fortschritt & Code-Drafts + c.execute(""" + CREATE TABLE IF NOT EXISTS chapter_progress ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + chapter_id TEXT NOT NULL, + code_draft TEXT, + subgoals_json TEXT, + is_solved INTEGER DEFAULT 0, + solved_at TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE(user_id, chapter_id), + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE + )""") + + # 3. Klassenräume (Lehrer-Feature) + c.execute(""" + CREATE TABLE IF NOT EXISTS classrooms ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + teacher_id INTEGER NOT NULL, + name TEXT NOT NULL, + invite_code TEXT UNIQUE NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY(teacher_id) REFERENCES users(id) ON DELETE CASCADE + )""") + + # 4. Klassen-Mitgliedschaften + c.execute(""" + CREATE TABLE IF NOT EXISTS class_enrollments ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + classroom_id INTEGER NOT NULL, + user_id INTEGER NOT NULL, + joined_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE(classroom_id, user_id), + FOREIGN KEY(classroom_id) REFERENCES classrooms(id) ON DELETE CASCADE, + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE + )""") + + # 5. Freigeschaltete Trophäen + c.execute(""" + CREATE TABLE IF NOT EXISTS achievements ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + trophy_id TEXT NOT NULL, + unlocked_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + UNIQUE(user_id, trophy_id), + FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE + )""") + + conn.commit() + conn.close() + +# ============================================================================== +# AUTHENTIFIZIERUNG & HASHING +# ============================================================================== + +def hash_password(password: str, salt: str = None) -> tuple[str, str]: + if not salt: + salt = secrets.token_hex(16) + pwd_bytes = password.encode("utf-8") + salt_bytes = salt.encode("utf-8") + key = hashlib.pbkdf2_hmac("sha256", pwd_bytes, salt_bytes, 100000) + return key.hex(), salt + +def verify_password(password: str, password_hash: str, salt: str) -> bool: + new_hash, _ = hash_password(password, salt) + return hmac.compare_digest(new_hash, password_hash) + +def generate_token(user_id: int, email: str, role: str) -> str: + payload = { + "uid": user_id, + "email": email, + "role": role, + "exp": int(time.time()) + (86400 * 30) # 30 Tage gültig + } + payload_json = json.dumps(payload) + sig = hmac.new(SECRET_KEY.encode("utf-8"), payload_json.encode("utf-8"), hashlib.sha256).hexdigest() + token = f"{payload_json}.{sig}" + return urllib.parse.quote(token) + +def verify_token(token_str: str) -> dict | None: + try: + raw_token = urllib.parse.unquote(token_str) + if "." not in raw_token: + return None + payload_json, sig = raw_token.rsplit(".", 1) + expected_sig = hmac.new(SECRET_KEY.encode("utf-8"), payload_json.encode("utf-8"), hashlib.sha256).hexdigest() + if not hmac.compare_digest(sig, expected_sig): + return None + payload = json.loads(payload_json) + if payload.get("exp", 0) < time.time(): + return None + return payload + except Exception: + return None + +# ============================================================================== +# HTTP REQUEST HANDLER +# ============================================================================== + +class PlatformRequestHandler(http.server.SimpleHTTPRequestHandler): + def __init__(self, *args, **kwargs): + super().__init__(*args, directory=str(BASE_DIR), **kwargs) + + def end_headers(self): + # CORS & Cache-Header für moderne Web-Apps + self.send_header("Access-Control-Allow-Origin", "*") + self.send_header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") + self.send_header("Access-Control-Allow-Headers", "Content-Type, Authorization") + self.send_header("Cache-Control", "no-cache, must-revalidate") + super().end_headers() + + def do_OPTIONS(self): + self.send_response(204) + self.end_headers() + + def send_json(self, data: dict, status: int = 200): + self.send_response(status) + self.send_header("Content-Type", "application/json; charset=utf-8") + self.end_headers() + self.wfile.write(json.dumps(data, ensure_ascii=False).encode("utf-8")) + + def get_auth_user(self) -> dict | None: + auth_header = self.headers.get("Authorization", "") + if auth_header.startswith("Bearer "): + token = auth_header[7:].strip() + return verify_token(token) + return None + + def read_json_body(self) -> dict: + content_length = int(self.headers.get("Content-Length", 0)) + if content_length == 0: + return {} + body = self.rfile.read(content_length) + return json.loads(body.decode("utf-8")) + + def do_GET(self): + parsed = urllib.parse.urlparse(self.path) + path = parsed.path + + # --- API ROUTES --- + if path == "/api/auth/me": + user_token = self.get_auth_user() + if not user_token: + return self.send_json({"error": "Nicht authentifiziert"}, 401) + conn = get_db() + row = conn.execute("SELECT id, email, name, role, xp, level, streak_days FROM users WHERE id = ?", (user_token["uid"],)).fetchone() + conn.close() + if not row: + return self.send_json({"error": "Benutzer nicht gefunden"}, 404) + return self.send_json({"user": dict(row)}) + + elif path.startswith("/api/progress/get"): + user_token = self.get_auth_user() + if not user_token: + return self.send_json({"error": "Nicht authentifiziert"}, 401) + query = urllib.parse.parse_qs(parsed.query) + chapter_id = query.get("chapter_id", [""])[0] + + conn = get_db() + if chapter_id: + row = conn.execute("SELECT chapter_id, code_draft, subgoals_json, is_solved, solved_at FROM chapter_progress WHERE user_id = ? AND chapter_id = ?", (user_token["uid"], chapter_id)).fetchone() + conn.close() + return self.send_json({"progress": dict(row) if row else None}) + else: + rows = conn.execute("SELECT chapter_id, is_solved, solved_at FROM chapter_progress WHERE user_id = ?", (user_token["uid"],)).fetchall() + conn.close() + return self.send_json({"all_progress": [dict(r) for r in rows]}) + + elif path.startswith("/api/classrooms/list"): + user_token = self.get_auth_user() + if not user_token: + return self.send_json({"error": "Nicht authentifiziert"}, 401) + + conn = get_db() + if user_token.get("role") == "teacher": + classes = conn.execute("SELECT id, name, invite_code, created_at FROM classrooms WHERE teacher_id = ?", (user_token["uid"],)).fetchall() + result = [] + for cl in classes: + student_count = conn.execute("SELECT COUNT(*) FROM class_enrollments WHERE classroom_id = ?", (cl["id"],)).fetchone()[0] + result.append({**dict(cl), "student_count": student_count}) + conn.close() + return self.send_json({"classrooms": result}) + else: + classes = conn.execute(""" + SELECT c.id, c.name, u.name as teacher_name, ce.joined_at + FROM class_enrollments ce + JOIN classrooms c ON ce.classroom_id = c.id + JOIN users u ON c.teacher_id = u.id + WHERE ce.user_id = ? + """, (user_token["uid"],)).fetchall() + conn.close() + return self.send_json({"classrooms": [dict(c) for c in classes]}) + + elif path.startswith("/api/classrooms/matrix"): + user_token = self.get_auth_user() + if not user_token or user_token.get("role") != "teacher": + return self.send_json({"error": "Nur für Lehrkräfte"}, 403) + + query = urllib.parse.parse_qs(parsed.query) + classroom_id = query.get("classroom_id", [""])[0] + if not classroom_id: + return self.send_json({"error": "classroom_id fehlt"}, 400) + + conn = get_db() + students = conn.execute(""" + SELECT u.id, u.name, u.email, u.xp, u.level, u.streak_days + FROM class_enrollments ce + JOIN users u ON ce.user_id = u.id + WHERE ce.classroom_id = ? + """, (classroom_id,)).fetchall() + + matrix = [] + for st in students: + solved = conn.execute("SELECT chapter_id, is_solved FROM chapter_progress WHERE user_id = ?", (st["id"],)).fetchall() + matrix.append({ + "student": dict(st), + "solved_chapters": {r["chapter_id"]: bool(r["is_solved"]) for r in solved} + }) + conn.close() + return self.send_json({"matrix": matrix}) + + elif path == "/api/stats/leaderboard": + conn = get_db() + rows = conn.execute("SELECT name, xp, level, role FROM users WHERE role = 'student' ORDER BY xp DESC LIMIT 20").fetchall() + conn.close() + return self.send_json({"leaderboard": [dict(r) for r in rows]}) + + # --- STATISCHE DATEIEN --- + return super().do_GET() + + def do_POST(self): + parsed = urllib.parse.urlparse(self.path) + path = parsed.path + + try: + body = self.read_json_body() + except Exception: + return self.send_json({"error": "Ungültiges JSON"}, 400) + + # 1. REGISTRIERUNG + if path == "/api/auth/register": + email = body.get("email", "").strip().lower() + password = body.get("password", "") + name = body.get("name", "").strip() + role = body.get("role", "student") + + if not email or not password or not name: + return self.send_json({"error": "Name, E-Mail und Passwort sind erforderlich"}, 400) + if len(password) < 6: + return self.send_json({"error": "Passwort muss mindestens 6 Zeichen lang sein"}, 400) + + pwd_hash, salt = hash_password(password) + conn = get_db() + try: + c = conn.cursor() + c.execute("INSERT INTO users (email, password_hash, salt, name, role) VALUES (?, ?, ?, ?, ?)", + (email, pwd_hash, salt, name, role)) + user_id = c.lastrowid + conn.commit() + token = generate_token(user_id, email, role) + return self.send_json({ + "message": "Erfolgreich registriert!", + "token": token, + "user": {"id": user_id, "email": email, "name": name, "role": role, "xp": 0, "level": 1} + }, 201) + except sqlite3.IntegrityError: + return self.send_json({"error": "Ein Benutzer mit dieser E-Mail existiert bereits"}, 409) + finally: + conn.close() + + # 2. LOGIN + elif path == "/api/auth/login": + email = body.get("email", "").strip().lower() + password = body.get("password", "") + + conn = get_db() + user = conn.execute("SELECT * FROM users WHERE email = ?", (email,)).fetchone() + conn.close() + + if not user or not verify_password(password, user["password_hash"], user["salt"]): + return self.send_json({"error": "Ungültige E-Mail-Adresse oder Passwort"}, 401) + + token = generate_token(user["id"], user["email"], user["role"]) + return self.send_json({ + "message": "Erfolgreich angemeldet!", + "token": token, + "user": { + "id": user["id"], + "email": user["email"], + "name": user["name"], + "role": user["role"], + "xp": user["xp"], + "level": user["level"], + "streak_days": user["streak_days"] + } + }) + + # 3. CODE SPEICHERN (AUTO-SAVE) + elif path == "/api/progress/save": + user_token = self.get_auth_user() + if not user_token: + return self.send_json({"error": "Nicht authentifiziert"}, 401) + + chapter_id = body.get("chapter_id", "") + code_draft = body.get("code_draft", "") + subgoals_json = json.dumps(body.get("subgoals", {})) + + if not chapter_id: + return self.send_json({"error": "chapter_id erforderlich"}, 400) + + conn = get_db() + conn.execute(""" + INSERT INTO chapter_progress (user_id, chapter_id, code_draft, subgoals_json, updated_at) + VALUES (?, ?, ?, ?, CURRENT_TIMESTAMP) + ON CONFLICT(user_id, chapter_id) DO UPDATE SET + code_draft = excluded.code_draft, + subgoals_json = excluded.subgoals_json, + updated_at = CURRENT_TIMESTAMP + """, (user_token["uid"], chapter_id, code_draft, subgoals_json)) + conn.commit() + conn.close() + + return self.send_json({"status": "saved", "chapter_id": chapter_id}) + + # 4. KAPITEL ALS GELÖST MARKIEREN & XP VERGEBEN + elif path == "/api/progress/solve": + user_token = self.get_auth_user() + if not user_token: + return self.send_json({"error": "Nicht authentifiziert"}, 401) + + chapter_id = body.get("chapter_id", "") + xp_reward = int(body.get("xp_reward", 100)) + + if not chapter_id: + return self.send_json({"error": "chapter_id erforderlich"}, 400) + + conn = get_db() + # Prüfe, ob das Kapitel bereits gelöst war + existing = conn.execute("SELECT is_solved FROM chapter_progress WHERE user_id = ? AND chapter_id = ?", (user_token["uid"], chapter_id)).fetchone() + + was_already_solved = existing and existing["is_solved"] == 1 + + if not was_already_solved: + conn.execute(""" + INSERT INTO chapter_progress (user_id, chapter_id, is_solved, solved_at, updated_at) + VALUES (?, ?, 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP) + ON CONFLICT(user_id, chapter_id) DO UPDATE SET + is_solved = 1, + solved_at = CURRENT_TIMESTAMP, + updated_at = CURRENT_TIMESTAMP + """, (user_token["uid"], chapter_id)) + + # XP & Level berechnen + user_row = conn.execute("SELECT xp FROM users WHERE id = ?", (user_token["uid"],)).fetchone() + new_xp = (user_row["xp"] if user_row else 0) + xp_reward + new_level = (new_xp // 100) + 1 + + conn.execute("UPDATE users SET xp = ?, level = ? WHERE id = ?", (new_xp, new_level, user_token["uid"])) + conn.commit() + conn.close() + return self.send_json({"status": "solved", "earned_xp": xp_reward, "total_xp": new_xp, "level": new_level}) + else: + conn.close() + return self.send_json({"status": "already_solved", "earned_xp": 0}) + + # 5. KLASSENRAUM ERSTELLEN (LEHRER) + elif path == "/api/classrooms/create": + user_token = self.get_auth_user() + if not user_token or user_token.get("role") != "teacher": + return self.send_json({"error": "Nur für Lehrkräfte gestattet"}, 403) + + name = body.get("name", "").strip() + if not name: + return self.send_json({"error": "Klassenname erforderlich"}, 400) + + invite_code = "PY-" + secrets.token_hex(3).upper() + conn = get_db() + c = conn.cursor() + c.execute("INSERT INTO classrooms (teacher_id, name, invite_code) VALUES (?, ?, ?)", + (user_token["uid"], name, invite_code)) + class_id = c.lastrowid + conn.commit() + conn.close() + + return self.send_json({"classroom": {"id": class_id, "name": name, "invite_code": invite_code}}, 201) + + # 6. KLASSENRAUM BEITRETEN (SCHÜLER) + elif path == "/api/classrooms/join": + user_token = self.get_auth_user() + if not user_token: + return self.send_json({"error": "Nicht authentifiziert"}, 401) + + code = body.get("invite_code", "").strip().upper() + conn = get_db() + cl = conn.execute("SELECT id, name FROM classrooms WHERE invite_code = ?", (code,)).fetchone() + if not cl: + conn.close() + return self.send_json({"error": "Ungültiger Einladungscode"}, 404) + + try: + conn.execute("INSERT INTO class_enrollments (classroom_id, user_id) VALUES (?, ?)", (cl["id"], user_token["uid"])) + conn.commit() + conn.close() + return self.send_json({"message": f"Erfolgreich Klasse '{cl['name']}' beigetreten!", "classroom": dict(cl)}) + except sqlite3.IntegrityError: + conn.close() + return self.send_json({"message": "Du bist dieser Klasse bereits beigetreten.", "classroom": dict(cl)}) + + return self.send_json({"error": "Endpunkt nicht gefunden"}, 404) + +# ============================================================================== +# SERVER START +# ============================================================================== + +def run_server(): + init_db() + handler = PlatformRequestHandler + socketserver.TCPServer.allow_reuse_address = True + with socketserver.TCPServer(("", PORT), handler) as httpd: + print(f"🚀 Python Lernplattform Server läuft auf: http://localhost:{PORT}") + print(f"📁 Root-Verzeichnis: {BASE_DIR}") + print(f"💾 SQLite-Datenbank: {DB_FILE}") + try: + httpd.serve_forever() + except KeyboardInterrupt: + print("\n🛑 Server gestoppt.") + +if __name__ == "__main__": + run_server() diff --git a/teacher.html b/teacher.html new file mode 100644 index 0000000..41adae0 --- /dev/null +++ b/teacher.html @@ -0,0 +1,248 @@ + + + + + + Lehrer-Dashboard | Klassen & Lernfortschritt + + + + + +
+ + + +
+
+
+
👨‍🏫 Dozenten- & Lehrer-Portal
+

Klassenverwaltung & Lernfortschritts-Matrix

+

Behalte den Überblick über deine Klassen, lade Schüler per Code ein und verfolge gelöste Aufgaben in Echtzeit.

+
+
+ +
+ + +
+

✨ Neue Schulklasse / Kursgruppe anlegen

+
+ + +
+
+ + +
+
+

📚 Deine aktiven Klassen

+
+ +
+
+ Lade Klassenräume... +
+
+
+ +
+
+ +
+ + + + + diff --git a/workspace.html b/workspace.html new file mode 100644 index 0000000..378be52 --- /dev/null +++ b/workspace.html @@ -0,0 +1,372 @@ + + + + + + Python Web-IDE | Interaktiver Arbeitsbereich + + + + + + +
+
+ 🏠 Dashboard + / + Lerneinheit wird geladen... + 💾 Gespeichert +
+ +
+ + + + +
+
+ + +
+ + +
+
+ + + + +
+ +
+
Lade didaktische Erklärung...
+
+ + + + + + +
+ + +
+
+
+
+ +
+
+ 🖥️ Terminal / Test-Ausgabe + +
+
Klicke oben auf '▶ Code ausführen' oder '🧪 Tests starten'...
+
+
+ +
+ + + + + + + + + + + + + + +