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.
+
+
+
+
+
+ `;
+ 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).
+
-
-
-
📚 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.
-
-
-
+
+ 📖 Anleitung & Workflow
+ 🌱 1. Grundlagen
+ 🏗️ 2. OOP Einstieg
+ 🚀 3. Fortgeschritten
+ ⚡ Live-Playground
+ 🩺 Fehler-Dolmetscher
+ 🏆 Zertifikate
+
-
+
-
-
- 🔍
-
-
-
+
-
-