Implementiere Online-Lernplattform: Monaco Split-Screen Web-IDE, In-Browser Pyodide Test-Runner, User Auth, Auto-Save und Lehrer-Dashboard

This commit is contained in:
Alex Mueller 2026-08-29 20:50:59 +02:00
parent 0f73a3619f
commit 759e64666a
10 changed files with 2160 additions and 252 deletions

View file

@ -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"
}

2
.gitignore vendored
View file

@ -8,3 +8,5 @@ htmlcov/
venv/
.venv/
.DS_Store
*.db
platform_data.db

184
api.php Normal file
View file

@ -0,0 +1,184 @@
<?php
/**
* 🐘 PHP BACKEND REST API FÜR STANDARD-WEBSPACE (Hetzner, All-Inkl, Strato, cPanel)
* ==============================================================================
* SQLite-basierte REST-API für Benutzerverwaltung, Cloud-Speichern & Lehrerklassen.
*/
header('Content-Type: application/json; charset=utf-8');
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(204);
exit;
}
$db_file = __DIR__ . '/platform_data.db';
$pdo = new PDO('sqlite:' . $db_file);
$pdo->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']);

245
assets/app_auth.js Normal file
View file

@ -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 = `<span>👤 ${this.user.name}</span> <span style="background: rgba(255,255,255,0.2); padding: 2px 8px; border-radius: 99px; font-size: 0.75rem;">${this.user.xp || 0} XP</span>`;
}
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 = `
<div class="certificate-container" style="max-width: 420px; padding: 28px;">
<h3 id="auth-modal-title" style="margin-top: 0; font-size: 1.35rem; color: var(--text-main);">🔑 Anmelden</h3>
<p id="auth-modal-sub" style="font-size: 0.88rem; color: var(--text-muted); margin-bottom: 18px;">Melde dich an, um deinen Code und deine XP in der Cloud zu speichern.</p>
<form id="auth-form" style="display: flex; flex-direction: column; gap: 12px;">
<div id="auth-field-name" style="display: none;">
<label style="font-size: 0.82rem; font-weight: 700; color: var(--text-main);">Dein Name:</label>
<input type="text" id="auth-input-name" style="width: 100%; padding: 8px 12px; border: 1px solid var(--border-color); border-radius: var(--radius-sm); font-size: 0.92rem;" placeholder="z.B. Anna Schmidt">
</div>
<div>
<label style="font-size: 0.82rem; font-weight: 700; color: var(--text-main);">E-Mail-Adresse:</label>
<input type="email" id="auth-input-email" required style="width: 100%; padding: 8px 12px; border: 1px solid var(--border-color); border-radius: var(--radius-sm); font-size: 0.92rem;" placeholder="name@schule.de">
</div>
<div>
<label style="font-size: 0.82rem; font-weight: 700; color: var(--text-main);">Passwort:</label>
<input type="password" id="auth-input-password" required minlength="6" style="width: 100%; padding: 8px 12px; border: 1px solid var(--border-color); border-radius: var(--radius-sm); font-size: 0.92rem;" placeholder="Mindestens 6 Zeichen">
</div>
<div id="auth-field-role" style="display: none;">
<label style="font-size: 0.82rem; font-weight: 700; color: var(--text-main);">Rolle:</label>
<select id="auth-select-role" style="width: 100%; padding: 8px 12px; border: 1px solid var(--border-color); border-radius: var(--radius-sm); font-size: 0.92rem;">
<option value="student">🎓 Schüler / Selbstlerner</option>
<option value="teacher">👨‍🏫 Lehrkraft / Dozent</option>
</select>
</div>
<div id="auth-error-msg" style="color: var(--danger); font-size: 0.82rem; display: none;"></div>
<button type="submit" id="auth-submit-btn" class="btn" style="margin-top: 8px; justify-content: center;">Anmelden</button>
<button type="button" class="btn btn-secondary" onclick="document.getElementById('auth-modal-overlay').remove();" style="justify-content: center;">Abbrechen</button>
</form>
<div style="margin-top: 14px; text-align: center; font-size: 0.84rem;">
<a href="#" id="auth-toggle-mode" style="color: var(--primary); text-decoration: none; font-weight: 600;">Noch kein Konto? Jetzt registrieren &rarr;</a>
</div>
</div>
`;
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 &rarr;";
} else {
titleEl.innerText = "🔑 Anmelden";
nameField.style.display = "none";
roleField.style.display = "none";
submitBtn.innerText = "Anmelden";
toggleLink.innerText = "Noch kein Konto? Jetzt registrieren &rarr;";
}
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();
});
})();

View file

@ -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": {

333
assets/monaco_ide.js Normal file
View file

@ -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 = `<textarea id="fallback-code-editor" style="width: 100%; height: 100%; background: #0b0f19; color: #f8fafc; font-family: monospace; font-size: 14px; padding: 14px; border: none; outline: none;">${initialCode}</textarea>`;
};
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 || "");
}
});
};
})();

View file

@ -9,7 +9,7 @@
<body>
<div class="app-layout">
<aside class="app-sidebar">
<aside class="app-sidebar">
<div class="sidebar-header">
<a href="./index.html" class="sidebar-brand-link">
<div class="sidebar-logo">🐍</div>
@ -176,121 +176,129 @@
</div>
</div>
</aside>
<div class="app-main">
<header>
<div class="header-container">
<div class="header-badge">🐍 Python Nachhilfe- &amp; Selbstlern-Portal</div>
<h1>Vom Einsteiger zum Software-Entwickler</h1>
<p>3 didaktisch strukturierte Lehrpfade: Prozedurale Grundlagen &bull; Objektorientierung &bull; Professionelle Werkzeuge (Git, TDD, Tkinter, Persistenz)</p>
<nav class="nav-links" style="margin-top: 18px;">
<a href="#tutorial">📖 Anleitung &amp; Workflow</a>
<a href="#lehrpfad-1">🌱 1. Grundlagen</a>
<a href="#lehrpfad-2">🏗️ 2. OOP Einstieg</a>
<a href="#lehrpfad-3">🚀 3. Fortgeschritten</a>
<a href="#python-live-playground">⚡ Live-Playground</a>
<a href="#error-interpreter-widget">🩺 Fehler-Dolmetscher</a>
<a href="#zertifikate">🏆 Zertifikate</a>
</nav>
</div>
</header>
<main class="container">
<!-- Willkommen Card -->
<section class="card">
<h2>👋 Willkommen zu deiner Python-Lernreise!</h2>
<p>
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 <strong>automatische Unittests</strong> für sofortiges Feedback.
</p>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 14px; margin: 20px 0;">
<div style="background: #f8fafc; border-left: 4px solid #059669; padding: 14px; border-radius: var(--radius-sm);">
<span class="track-badge track-1-badge">Lehrpfad 1</span>
<h4 style="margin: 4px 0; color: #065f46;">🌱 Grundlagen der Programmierung</h4>
<p style="font-size: 0.85rem; color: var(--text-muted); margin: 0;">Rechnen, Variablen, Bedingungen, Schleifen, Funktionen, Listen, Dictionaries &amp; Strings (G01–G10).</p>
<div class="header-container">
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px; margin-bottom: 12px;">
<div class="header-badge">🐍 Python Online-Lernplattform &amp; Web-IDE</div>
<div style="display: flex; gap: 8px; align-items: center;">
<span id="nav-auth-user" style="display: none; font-size: 0.85rem; font-weight: 700; color: white;"></span>
<button type="button" class="btn" id="nav-btn-login" style="padding: 5px 12px; font-size: 0.8rem; background: rgba(255,255,255,0.15);" onclick="window.openAuthModal('login')">🔑 Anmelden</button>
<button type="button" class="btn" id="nav-btn-register" style="padding: 5px 12px; font-size: 0.8rem; background: var(--primary);" onclick="window.openAuthModal('register')">Konto erstellen</button>
<a href="teacher.html" class="btn" id="nav-teacher-link" style="padding: 5px 12px; font-size: 0.8rem; background: #059669; display: none;">👨‍🏫 Lehrerbereich</a>
</div>
</div>
<div style="background: #f8fafc; border-left: 4px solid #4f46e5; padding: 14px; border-radius: var(--radius-sm);">
<span class="track-badge track-2-badge">Lehrpfad 2</span>
<h4 style="margin: 4px 0; color: #312e81;">🏗️ Einstieg in OOP &amp; Tools</h4>
<p style="font-size: 0.85rem; color: var(--text-muted); margin: 0;">Klassen, __init__, Methoden, UML-Diagramme, VS Code, Git-Versionskontrolle &amp; Tamagotchi (00–06).</p>
</div>
<div style="background: #f8fafc; border-left: 4px solid #7c3aed; padding: 14px; border-radius: var(--radius-sm);">
<span class="track-badge track-3-badge">Lehrpfad 3</span>
<h4 style="margin: 4px 0; color: #4c1d95;">🚀 Fortgeschrittenes OOP &amp; GUIs</h4>
<p style="font-size: 0.85rem; color: var(--text-muted); margin: 0;">Speicher, Dunder-Methoden, Unit Tests (TDD), Vererbung, Exceptions, JSON, Tkinter &amp; Masterprojekt (07–16).</p>
</div>
</div>
<div class="box box-tipp">
<div class="box-title">📚 Lehrplan-Abstimmung:</div>
Alle Einheiten sind exakt auf das offizielle Unterrichtsskriptum abgestimmt. Detaillierter Leitfaden in <a href="LEHRPLAN.md" style="color: var(--primary); font-weight: bold;">LEHRPLAN.md</a>.
</div>
</section>
<!-- 📖 Schritt-für-Schritt Tutorial -->
<section class="card" id="tutorial" style="border-left: 4px solid var(--primary);">
<h2>📖 Schritt-für-Schritt-Tutorial: So lernst du mit dem Portal</h2>
<p>
Dieses Portal basiert auf dem <strong>PRIMM-Modell</strong> der Informatikdidaktik (Predict &bull; Read &bull; Investigate &bull; Modify &bull; Make):
</p>
<div style="display: flex; flex-direction: column; gap: 16px; margin-top: 18px;">
<h1>Vom Einsteiger zum Software-Entwickler</h1>
<p>3 didaktisch strukturierte Lehrpfade mit integrierter **Monaco Web-IDE**, In-Browser Python-Runner (Pyodide), Aufgaben-Checklisten &amp; persistentem Entwickler-Pass.</p>
<div style="display: flex; gap: 14px; align-items: flex-start;">
<div style="background: var(--primary); color: white; width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: 800; flex-shrink: 0;">1</div>
<div>
<h4 style="margin: 0 0 4px 0; color: var(--text-main);">📥 Lokal nutzen oder online lernen</h4>
<p style="font-size: 0.9rem; color: var(--text-muted); margin: 0;">Du kannst das Repository lokal klonen (<code>git clone https://github.com/alexander-mueller/python-oop-lernportal.git</code>) oder die Seiten direkt offline per Doppelklick auf <code>index.html</code> öffnen.</p>
</div>
</div>
<div style="display: flex; gap: 14px; align-items: flex-start;">
<div style="background: var(--primary); color: white; width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: 800; flex-shrink: 0;">2</div>
<div>
<h4 style="margin: 0 0 4px 0; color: var(--text-main);">🧩 Kapitel lesen &amp; Parsons-Puzzle lösen (+25 XP)</h4>
<p style="font-size: 0.9rem; color: var(--text-muted); margin: 0;">Lies die Alltagsanalogie im Kapitel-Guide und bringe die durcheinandergeratenen Code-Zeilen im interaktiven Puzzle per Klick in die richtige Reihenfolge.</p>
</div>
</div>
<div style="display: flex; gap: 14px; align-items: flex-start;">
<div style="background: var(--primary); color: white; width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: 800; flex-shrink: 0;">3</div>
<div>
<h4 style="margin: 0 0 4px 0; color: var(--text-main);">💻 Startercode lösen &amp; Tests ausführen (+100 XP)</h4>
<p style="font-size: 0.9rem; color: var(--text-muted); margin: 0;">Öffne <code>aufgabe.py</code> im Editor, bearbeite die nummerierten Teilziele und teste deine Lösung im Terminal mit <code>python3 test_aufgabe.py</code>.</p>
</div>
</div>
<div style="display: flex; gap: 14px; align-items: flex-start;">
<div style="background: var(--primary); color: white; width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: 800; flex-shrink: 0;">4</div>
<div>
<h4 style="margin: 0 0 4px 0; color: var(--text-main);">🩺 Hilfe bei Fehlern</h4>
<p style="font-size: 0.9rem; color: var(--text-muted); margin: 0;">Kopiere rote Traceback-Fehlermeldungen aus dem Terminal in den untenstehenden <strong>Fehler-Dolmetscher</strong>, um verständliche Lösungsschritte auf Deutsch zu erhalten.</p>
</div>
</div>
<nav class="nav-links" style="margin-top: 18px;">
<a href="#tutorial">📖 Anleitung &amp; Workflow</a>
<a href="#lehrpfad-1">🌱 1. Grundlagen</a>
<a href="#lehrpfad-2">🏗️ 2. OOP Einstieg</a>
<a href="#lehrpfad-3">🚀 3. Fortgeschritten</a>
<a href="#python-live-playground">⚡ Live-Playground</a>
<a href="#error-interpreter-widget">🩺 Fehler-Dolmetscher</a>
<a href="#zertifikate">🏆 Zertifikate</a>
</nav>
</div>
</section>
</header>
<!-- 🔍 Live-Suche -->
<div class="search-bar-container">
<span style="font-size: 1.1rem;">🔍</span>
<input type="text" id="portal-search-input" class="search-input" placeholder="Kapitel oder Thema durchsuchen (z.B. Schleifen, Klassen, Git, Tkinter, Dunder, Exceptions)...">
<span id="search-results-count" style="font-size: 0.8rem; font-weight: 700; color: var(--primary); display: none;"></span>
</div>
<main class="container">
<!-- LEHRPFAD 1 -->
<section>
<div class="section-header">
<div>
<h2 id="lehrpfad-1">🌱 Lehrpfad 1: Grundlagen der Programmierung</h2>
<div class="section-desc">Datentypen, Kontrollstrukturen, Funktionen und Datenstrukturen (G01–G10).</div>
<!-- Willkommen Card -->
<section class="card">
<h2>👋 Willkommen zu deiner Python-Lernreise!</h2>
<p>
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 <strong>integrierten Monaco Web-IDE</strong> im Browser programmieren und mit 1 Klick gegen automatische Tests prüfen!
</p>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 14px; margin: 20px 0;">
<div style="background: #f8fafc; border-left: 4px solid #059669; padding: 14px; border-radius: var(--radius-sm);">
<span class="track-badge track-1-badge">Lehrpfad 1</span>
<h4 style="margin: 4px 0; color: #065f46;">🌱 Grundlagen der Programmierung</h4>
<p style="font-size: 0.85rem; color: var(--text-muted); margin: 0;">Rechnen, Variablen, Bedingungen, Schleifen, Funktionen, Listen, Dictionaries &amp; Strings (G01–G10).</p>
</div>
<div style="background: #f8fafc; border-left: 4px solid #4f46e5; padding: 14px; border-radius: var(--radius-sm);">
<span class="track-badge track-2-badge">Lehrpfad 2</span>
<h4 style="margin: 4px 0; color: #312e81;">🏗️ Einstieg in OOP &amp; Tools</h4>
<p style="font-size: 0.85rem; color: var(--text-muted); margin: 0;">Klassen, __init__, Methoden, UML-Diagramme, VS Code, Git-Versionskontrolle &amp; Tamagotchi (00–06).</p>
</div>
<div style="background: #f8fafc; border-left: 4px solid #7c3aed; padding: 14px; border-radius: var(--radius-sm);">
<span class="track-badge track-3-badge">Lehrpfad 3</span>
<h4 style="margin: 4px 0; color: #4c1d95;">🚀 Fortgeschrittenes OOP &amp; GUIs</h4>
<p style="font-size: 0.85rem; color: var(--text-muted); margin: 0;">Speicher, Dunder-Methoden, Unit Tests (TDD), Vererbung, Exceptions, JSON, Tkinter &amp; Masterprojekt (07–16).</p>
</div>
</div>
<span class="track-badge track-1-badge">10 Kapitel</span>
<div class="box box-tipp">
<div class="box-title">📚 Lehrplan-Abstimmung:</div>
Alle Einheiten sind exakt auf das offizielle Unterrichtsskriptum abgestimmt. Detaillierter Leitfaden in <a href="LEHRPLAN.md" style="color: var(--primary); font-weight: bold;">LEHRPLAN.md</a>.
</div>
</section>
<!-- 📖 Schritt-für-Schritt Tutorial -->
<section class="card" id="tutorial" style="border-left: 4px solid var(--primary);">
<h2>📖 Schritt-für-Schritt-Tutorial: So lernst du mit der Online-Plattform</h2>
<p>
Dieses Portal basiert auf dem <strong>PRIMM-Modell</strong> der Informatikdidaktik:
</p>
<div style="display: flex; flex-direction: column; gap: 16px; margin-top: 18px;">
<div style="display: flex; gap: 14px; align-items: flex-start;">
<div style="background: var(--primary); color: white; width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: 800; flex-shrink: 0;">1</div>
<div>
<h4 style="margin: 0 0 4px 0; color: var(--text-main);">💻 In Web-IDE öffnen</h4>
<p style="font-size: 0.9rem; color: var(--text-muted); margin: 0;">Klicke bei jedem Kapitel auf <strong>'In Web-IDE öffnen'</strong>, um den Monaco Code-Editor mit Startercode und Teilzielen direkt im Browser zu starten.</p>
</div>
</div>
<div style="display: flex; gap: 14px; align-items: flex-start;">
<div style="background: var(--primary); color: white; width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: 800; flex-shrink: 0;">2</div>
<div>
<h4 style="margin: 0 0 4px 0; color: var(--text-main);">🧩 Parsons-Puzzle lösen (+25 XP)</h4>
<p style="font-size: 0.9rem; color: var(--text-muted); margin: 0;">Bringe im Tab 'Puzzle' die durcheinandergeratenen Code-Zeilen in die richtige Reihenfolge, um ein intuitives Verständnis des Codes aufzubauen.</p>
</div>
</div>
<div style="display: flex; gap: 14px; align-items: flex-start;">
<div style="background: var(--primary); color: white; width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: 800; flex-shrink: 0;">3</div>
<div>
<h4 style="margin: 0 0 4px 0; color: var(--text-main);">🧪 Tests im Browser ausführen (+100 XP)</h4>
<p style="font-size: 0.9rem; color: var(--text-muted); margin: 0;">Klicke oben auf <strong>'🧪 Tests starten'</strong>. Die Python WebAssembly-Engine prüft deinen Code sofort und vergibt bei Erfolg XP und Trophäen.</p>
</div>
</div>
<div style="display: flex; gap: 14px; align-items: flex-start;">
<div style="background: var(--primary); color: white; width: 32px; height: 32px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-weight: 800; flex-shrink: 0;">4</div>
<div>
<h4 style="margin: 0 0 4px 0; color: var(--text-main);">🩺 Hilfe bei Fehlern &amp; Auto-Save</h4>
<p style="font-size: 0.9rem; color: var(--text-muted); margin: 0;">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.</p>
</div>
</div>
</div>
</section>
<!-- 🔍 Live-Suche -->
<div class="search-bar-container">
<span style="font-size: 1.1rem;">🔍</span>
<input type="text" id="portal-search-input" class="search-input" placeholder="Kapitel oder Thema durchsuchen (z.B. Schleifen, Klassen, Git, Tkinter, Dunder, Exceptions)...">
<span id="search-results-count" style="font-size: 0.8rem; font-weight: 700; color: var(--primary); display: none;"></span>
</div>
<div class="roadmap-grid">
<!-- LEHRPFAD 1 -->
<section>
<div class="section-header">
<div>
<h2 id="lehrpfad-1">🌱 Lehrpfad 1: Grundlagen der Programmierung</h2>
<div class="section-desc">Datentypen, Kontrollstrukturen, Funktionen und Datenstrukturen (G01–G10).</div>
</div>
<span class="track-badge track-1-badge">10 Kapitel</span>
</div>
<div class="roadmap-grid">
<div class="roadmap-card" style="border-left: 4px solid #059669;">
<div>
<span class="roadmap-number" style="background: #05966918; color: #059669;">G01</span>
@ -298,7 +306,8 @@
<div class="roadmap-desc">Zahlen (<code>int</code>, <code>float</code>), Grundrechenarten, Ganzzahldivision <code>//</code>, Modulo <code>%</code>, Potenzen <code>**</code> & print().</div>
</div>
<div class="card-actions">
<a href="lehrpfad_1_grundlagen/01_erste_schritte_taschenrechner/index.html" class="card-primary-btn" style="background: #059669;">📖 Kapitel öffnen &rarr;</a>
<a href="workspace.html?track=lehrpfad_1_grundlagen&chapter=01_erste_schritte_taschenrechner" class="card-primary-btn" style="background: #059669;">💻 In Web-IDE öffnen &rarr;</a>
<a href="lehrpfad_1_grundlagen/01_erste_schritte_taschenrechner/index.html" style="text-align: center; font-size: 0.8rem; color: var(--text-muted); text-decoration: none; font-weight: 600;">📖 Kapitel-Guide lesen</a>
<div class="card-sublinks">
<a href="lehrpfad_1_grundlagen/01_erste_schritte_taschenrechner/aufgabe.py" download>📄 aufgabe.py</a>
@ -314,7 +323,8 @@
<div class="roadmap-desc">Typen (<code>str</code>, <code>int</code>, <code>float</code>, <code>bool</code>), Typumwandlung (Casting) & Vermeidung typischer Typfehler.</div>
</div>
<div class="card-actions">
<a href="lehrpfad_1_grundlagen/02_variablen_und_datentypen/index.html" class="card-primary-btn" style="background: #059669;">📖 Kapitel öffnen &rarr;</a>
<a href="workspace.html?track=lehrpfad_1_grundlagen&chapter=02_variablen_und_datentypen" class="card-primary-btn" style="background: #059669;">💻 In Web-IDE öffnen &rarr;</a>
<a href="lehrpfad_1_grundlagen/02_variablen_und_datentypen/index.html" style="text-align: center; font-size: 0.8rem; color: var(--text-muted); text-decoration: none; font-weight: 600;">📖 Kapitel-Guide lesen</a>
<div class="card-sublinks">
<a href="lehrpfad_1_grundlagen/02_variablen_und_datentypen/aufgabe.py" download>📄 aufgabe.py</a>
@ -330,7 +340,8 @@
<div class="roadmap-desc">Tastatureingaben mit <code>input()</code>, Textformatierung mit modernen f-Strings und Parametern.</div>
</div>
<div class="card-actions">
<a href="lehrpfad_1_grundlagen/03_ein_und_ausgabe/index.html" class="card-primary-btn" style="background: #059669;">📖 Kapitel öffnen &rarr;</a>
<a href="workspace.html?track=lehrpfad_1_grundlagen&chapter=03_ein_und_ausgabe" class="card-primary-btn" style="background: #059669;">💻 In Web-IDE öffnen &rarr;</a>
<a href="lehrpfad_1_grundlagen/03_ein_und_ausgabe/index.html" style="text-align: center; font-size: 0.8rem; color: var(--text-muted); text-decoration: none; font-weight: 600;">📖 Kapitel-Guide lesen</a>
<div class="card-sublinks">
<a href="lehrpfad_1_grundlagen/03_ein_und_ausgabe/aufgabe.py" download>📄 aufgabe.py</a>
@ -346,7 +357,8 @@
<div class="roadmap-desc">Entscheidungen treffen mit <code>if</code>, <code>elif</code>, <code>else</code>, Vergleichsoperatoren & logischen Verknüpfungen.</div>
</div>
<div class="card-actions">
<a href="lehrpfad_1_grundlagen/04_verzweigungen_und_bedingungen/index.html" class="card-primary-btn" style="background: #059669;">📖 Kapitel öffnen &rarr;</a>
<a href="workspace.html?track=lehrpfad_1_grundlagen&chapter=04_verzweigungen_und_bedingungen" class="card-primary-btn" style="background: #059669;">💻 In Web-IDE öffnen &rarr;</a>
<a href="lehrpfad_1_grundlagen/04_verzweigungen_und_bedingungen/index.html" style="text-align: center; font-size: 0.8rem; color: var(--text-muted); text-decoration: none; font-weight: 600;">📖 Kapitel-Guide lesen</a>
<div class="card-sublinks">
<a href="lehrpfad_1_grundlagen/04_verzweigungen_und_bedingungen/aufgabe.py" download>📄 aufgabe.py</a>
@ -362,7 +374,8 @@
<div class="roadmap-desc">Zählschleifen <code>for ... in range()</code>, Bedingungsschleifen <code>while</code>, <code>break</code> & <code>continue</code>.</div>
</div>
<div class="card-actions">
<a href="lehrpfad_1_grundlagen/05_schleifen_und_wiederholungen/index.html" class="card-primary-btn" style="background: #059669;">📖 Kapitel öffnen &rarr;</a>
<a href="workspace.html?track=lehrpfad_1_grundlagen&chapter=05_schleifen_und_wiederholungen" class="card-primary-btn" style="background: #059669;">💻 In Web-IDE öffnen &rarr;</a>
<a href="lehrpfad_1_grundlagen/05_schleifen_und_wiederholungen/index.html" style="text-align: center; font-size: 0.8rem; color: var(--text-muted); text-decoration: none; font-weight: 600;">📖 Kapitel-Guide lesen</a>
<div class="card-sublinks">
<a href="lehrpfad_1_grundlagen/05_schleifen_und_wiederholungen/aufgabe.py" download>📄 aufgabe.py</a>
@ -378,7 +391,8 @@
<div class="roadmap-desc">Eigene Funktionen definieren mit <code>def</code>, <code>return</code>, Default-Parameter und Importe (<code>math</code>, <code>random</code>).</div>
</div>
<div class="card-actions">
<a href="lehrpfad_1_grundlagen/06_funktionen_und_module/index.html" class="card-primary-btn" style="background: #059669;">📖 Kapitel öffnen &rarr;</a>
<a href="workspace.html?track=lehrpfad_1_grundlagen&chapter=06_funktionen_und_module" class="card-primary-btn" style="background: #059669;">💻 In Web-IDE öffnen &rarr;</a>
<a href="lehrpfad_1_grundlagen/06_funktionen_und_module/index.html" style="text-align: center; font-size: 0.8rem; color: var(--text-muted); text-decoration: none; font-weight: 600;">📖 Kapitel-Guide lesen</a>
<div class="card-sublinks">
<a href="lehrpfad_1_grundlagen/06_funktionen_und_module/aufgabe.py" download>📄 aufgabe.py</a>
@ -394,7 +408,8 @@
<div class="roadmap-desc">Listen erstellen, Indexzugriff <code>[0]</code>, Slicing <code>[1:4]</code>, Methoden (<code>append</code>, <code>pop</code>) & Schleifen.</div>
</div>
<div class="card-actions">
<a href="lehrpfad_1_grundlagen/07_listen_und_sequenzen/index.html" class="card-primary-btn" style="background: #059669;">📖 Kapitel öffnen &rarr;</a>
<a href="workspace.html?track=lehrpfad_1_grundlagen&chapter=07_listen_und_sequenzen" class="card-primary-btn" style="background: #059669;">💻 In Web-IDE öffnen &rarr;</a>
<a href="lehrpfad_1_grundlagen/07_listen_und_sequenzen/index.html" style="text-align: center; font-size: 0.8rem; color: var(--text-muted); text-decoration: none; font-weight: 600;">📖 Kapitel-Guide lesen</a>
<div class="card-sublinks">
<a href="lehrpfad_1_grundlagen/07_listen_und_sequenzen/aufgabe.py" download>📄 aufgabe.py</a>
@ -410,7 +425,8 @@
<div class="roadmap-desc">String-Methoden (<code>split</code>, <code>join</code>, <code>strip</code>, <code>replace</code>), Groß/Klein und Zeichenanalysen.</div>
</div>
<div class="card-actions">
<a href="lehrpfad_1_grundlagen/08_textverarbeitung_und_strings/index.html" class="card-primary-btn" style="background: #059669;">📖 Kapitel öffnen &rarr;</a>
<a href="workspace.html?track=lehrpfad_1_grundlagen&chapter=08_textverarbeitung_und_strings" class="card-primary-btn" style="background: #059669;">💻 In Web-IDE öffnen &rarr;</a>
<a href="lehrpfad_1_grundlagen/08_textverarbeitung_und_strings/index.html" style="text-align: center; font-size: 0.8rem; color: var(--text-muted); text-decoration: none; font-weight: 600;">📖 Kapitel-Guide lesen</a>
<div class="card-sublinks">
<a href="lehrpfad_1_grundlagen/08_textverarbeitung_und_strings/aufgabe.py" download>📄 aufgabe.py</a>
@ -426,7 +442,8 @@
<div class="roadmap-desc">Schlüssel-Wert-Paare <code>dict</code>, sicherer Zugriff mit <code>.get()</code>, eindeutige Mengen <code>set</code> & Operationen.</div>
</div>
<div class="card-actions">
<a href="lehrpfad_1_grundlagen/09_dictionaries_und_sets/index.html" class="card-primary-btn" style="background: #059669;">📖 Kapitel öffnen &rarr;</a>
<a href="workspace.html?track=lehrpfad_1_grundlagen&chapter=09_dictionaries_und_sets" class="card-primary-btn" style="background: #059669;">💻 In Web-IDE öffnen &rarr;</a>
<a href="lehrpfad_1_grundlagen/09_dictionaries_und_sets/index.html" style="text-align: center; font-size: 0.8rem; color: var(--text-muted); text-decoration: none; font-weight: 600;">📖 Kapitel-Guide lesen</a>
<div class="card-sublinks">
<a href="lehrpfad_1_grundlagen/09_dictionaries_und_sets/aufgabe.py" download>📄 aufgabe.py</a>
@ -442,7 +459,8 @@
<div class="roadmap-desc">Kompakte List-Comprehensions, Datumsberechnungen mit <code>datetime</code> & grundlegende Algorithmen.</div>
</div>
<div class="card-actions">
<a href="lehrpfad_1_grundlagen/10_comprehensions_datum_algorithmen/index.html" class="card-primary-btn" style="background: #059669;">📖 Kapitel öffnen &rarr;</a>
<a href="workspace.html?track=lehrpfad_1_grundlagen&chapter=10_comprehensions_datum_algorithmen" class="card-primary-btn" style="background: #059669;">💻 In Web-IDE öffnen &rarr;</a>
<a href="lehrpfad_1_grundlagen/10_comprehensions_datum_algorithmen/index.html" style="text-align: center; font-size: 0.8rem; color: var(--text-muted); text-decoration: none; font-weight: 600;">📖 Kapitel-Guide lesen</a>
<div class="card-sublinks">
<a href="lehrpfad_1_grundlagen/10_comprehensions_datum_algorithmen/aufgabe.py" download>📄 aufgabe.py</a>
@ -451,20 +469,20 @@
</div>
</div>
</div>
</div>
</section>
<!-- LEHRPFAD 2 -->
<section>
<div class="section-header">
<div>
<h2 id="lehrpfad-2">🏗️ Lehrpfad 2: Einstieg in OOP &amp; Entwickler-Tools</h2>
<div class="section-desc">Klassen, Konstruktoren, Methoden, Komposition, VS Code, Git &amp; Tamagotchi (00–06).</div>
</div>
<span class="track-badge track-2-badge">9 Module</span>
</div>
<div class="roadmap-grid">
</section>
<!-- LEHRPFAD 2 -->
<section>
<div class="section-header">
<div>
<h2 id="lehrpfad-2">🏗️ Lehrpfad 2: Einstieg in OOP &amp; Entwickler-Tools</h2>
<div class="section-desc">Klassen, Konstruktoren, Methoden, Komposition, VS Code, Git &amp; Tamagotchi (00–06).</div>
</div>
<span class="track-badge track-2-badge">9 Module</span>
</div>
<div class="roadmap-grid">
<div class="roadmap-card" style="border-left: 4px solid #4f46e5;">
<div>
<span class="roadmap-number" style="background: #4f46e518; color: #4f46e5;">OOP 00</span>
@ -472,7 +490,8 @@
<div class="roadmap-desc">Typische Syntax-, Type- und Index-Fehler verstehen, Fehlermeldungen lesen und gezielt beheben.</div>
</div>
<div class="card-actions">
<a href="lehrpfad_2_oop_einstieg/00_fehlersuche_und_grundlagen/index.html" class="card-primary-btn" style="background: #4f46e5;">📖 Kapitel öffnen &rarr;</a>
<a href="workspace.html?track=lehrpfad_2_oop_einstieg&chapter=00_fehlersuche_und_grundlagen" class="card-primary-btn" style="background: #4f46e5;">💻 In Web-IDE öffnen &rarr;</a>
<a href="lehrpfad_2_oop_einstieg/00_fehlersuche_und_grundlagen/index.html" style="text-align: center; font-size: 0.8rem; color: var(--text-muted); text-decoration: none; font-weight: 600;">📖 Kapitel-Guide lesen</a>
<div class="card-sublinks">
<a href="lehrpfad_2_oop_einstieg/00_fehlersuche_und_grundlagen/aufgabe.py" download>📄 aufgabe.py</a>
@ -488,7 +507,8 @@
<div class="roadmap-desc">Objektorientierung verstehen: Vom Bauplan zum fertigen Objekt (Instanz) im Speicher.</div>
</div>
<div class="card-actions">
<a href="lehrpfad_2_oop_einstieg/01_einstieg_klassen/index.html" class="card-primary-btn" style="background: #4f46e5;">📖 Kapitel öffnen &rarr;</a>
<a href="workspace.html?track=lehrpfad_2_oop_einstieg&chapter=01_einstieg_klassen" class="card-primary-btn" style="background: #4f46e5;">💻 In Web-IDE öffnen &rarr;</a>
<a href="lehrpfad_2_oop_einstieg/01_einstieg_klassen/index.html" style="text-align: center; font-size: 0.8rem; color: var(--text-muted); text-decoration: none; font-weight: 600;">📖 Kapitel-Guide lesen</a>
<div class="card-sublinks">
<a href="lehrpfad_2_oop_einstieg/01_einstieg_klassen/aufgabe.py" download>📄 aufgabe.py</a>
@ -504,7 +524,8 @@
<div class="roadmap-desc">Objekte initialisieren, Instanzvariablen mit <code>self</code> anlegen und Parameter übergeben.</div>
</div>
<div class="card-actions">
<a href="lehrpfad_2_oop_einstieg/02_init_und_self/index.html" class="card-primary-btn" style="background: #4f46e5;">📖 Kapitel öffnen &rarr;</a>
<a href="workspace.html?track=lehrpfad_2_oop_einstieg&chapter=02_init_und_self" class="card-primary-btn" style="background: #4f46e5;">💻 In Web-IDE öffnen &rarr;</a>
<a href="lehrpfad_2_oop_einstieg/02_init_und_self/index.html" style="text-align: center; font-size: 0.8rem; color: var(--text-muted); text-decoration: none; font-weight: 600;">📖 Kapitel-Guide lesen</a>
<div class="card-sublinks">
<a href="lehrpfad_2_oop_einstieg/02_init_und_self/aufgabe.py" download>📄 aufgabe.py</a>
@ -520,7 +541,8 @@
<div class="roadmap-desc">Objekten Fähigkeiten begeben: Methoden definieren, Zustand verändern und Werte berechnen.</div>
</div>
<div class="card-actions">
<a href="lehrpfad_2_oop_einstieg/03_methoden_und_verhalten/index.html" class="card-primary-btn" style="background: #4f46e5;">📖 Kapitel öffnen &rarr;</a>
<a href="workspace.html?track=lehrpfad_2_oop_einstieg&chapter=03_methoden_und_verhalten" class="card-primary-btn" style="background: #4f46e5;">💻 In Web-IDE öffnen &rarr;</a>
<a href="lehrpfad_2_oop_einstieg/03_methoden_und_verhalten/index.html" style="text-align: center; font-size: 0.8rem; color: var(--text-muted); text-decoration: none; font-weight: 600;">📖 Kapitel-Guide lesen</a>
<div class="card-sublinks">
<a href="lehrpfad_2_oop_einstieg/03_methoden_und_verhalten/aufgabe.py" download>📄 aufgabe.py</a>
@ -536,7 +558,8 @@
<div class="roadmap-desc">Magische Dunder-Methode <code>__str__</code> für saubere Textausgaben beim Drucken mit <code>print()</code>.</div>
</div>
<div class="card-actions">
<a href="lehrpfad_2_oop_einstieg/04_str_und_darstellung/index.html" class="card-primary-btn" style="background: #4f46e5;">📖 Kapitel öffnen &rarr;</a>
<a href="workspace.html?track=lehrpfad_2_oop_einstieg&chapter=04_str_und_darstellung" class="card-primary-btn" style="background: #4f46e5;">💻 In Web-IDE öffnen &rarr;</a>
<a href="lehrpfad_2_oop_einstieg/04_str_und_darstellung/index.html" style="text-align: center; font-size: 0.8rem; color: var(--text-muted); text-decoration: none; font-weight: 600;">📖 Kapitel-Guide lesen</a>
<div class="card-sublinks">
<a href="lehrpfad_2_oop_einstieg/04_str_und_darstellung/aufgabe.py" download>📄 aufgabe.py</a>
@ -552,7 +575,8 @@
<div class="roadmap-desc">Vom Schul-IDLE zu einer modernen Entwicklerumgebung: Installation, Python-Extension & Terminal.</div>
</div>
<div class="card-actions">
<a href="lehrpfad_2_oop_einstieg/04b_umstieg_vscode/index.html" class="card-primary-btn" style="background: #4f46e5;">📖 Kapitel öffnen &rarr;</a>
<a href="workspace.html?track=lehrpfad_2_oop_einstieg&chapter=04b_umstieg_vscode" class="card-primary-btn" style="background: #4f46e5;">💻 In Web-IDE öffnen &rarr;</a>
<a href="lehrpfad_2_oop_einstieg/04b_umstieg_vscode/index.html" style="text-align: center; font-size: 0.8rem; color: var(--text-muted); text-decoration: none; font-weight: 600;">📖 Kapitel-Guide lesen</a>
<div class="card-sublinks">
<a href="lehrpfad_2_oop_einstieg/04b_umstieg_vscode/vscode_test.py" download>💻 vscode_test.py</a>
@ -567,7 +591,8 @@
<div class="roadmap-desc">Versionskontrolle: Commits, Branches, Status und kollaboratives Arbeiten im Terminal.</div>
</div>
<div class="card-actions">
<a href="lehrpfad_2_oop_einstieg/04c_git_und_versionskontrolle/index.html" class="card-primary-btn" style="background: #4f46e5;">📖 Kapitel öffnen &rarr;</a>
<a href="workspace.html?track=lehrpfad_2_oop_einstieg&chapter=04c_git_und_versionskontrolle" class="card-primary-btn" style="background: #4f46e5;">💻 In Web-IDE öffnen &rarr;</a>
<a href="lehrpfad_2_oop_einstieg/04c_git_und_versionskontrolle/index.html" style="text-align: center; font-size: 0.8rem; color: var(--text-muted); text-decoration: none; font-weight: 600;">📖 Kapitel-Guide lesen</a>
<div class="card-sublinks">
<a href="lehrpfad_2_oop_einstieg/04c_git_und_versionskontrolle/git_uebung.py" download>🌿 git_uebung.py</a>
@ -582,7 +607,8 @@
<div class="roadmap-desc">Klassen interagieren lassen: Eine Playlist besitzt mehrere Songs, ein Warenkorb mehrere Artikel.</div>
</div>
<div class="card-actions">
<a href="lehrpfad_2_oop_einstieg/05_objekte_kombinieren/index.html" class="card-primary-btn" style="background: #4f46e5;">📖 Kapitel öffnen &rarr;</a>
<a href="workspace.html?track=lehrpfad_2_oop_einstieg&chapter=05_objekte_kombinieren" class="card-primary-btn" style="background: #4f46e5;">💻 In Web-IDE öffnen &rarr;</a>
<a href="lehrpfad_2_oop_einstieg/05_objekte_kombinieren/index.html" style="text-align: center; font-size: 0.8rem; color: var(--text-muted); text-decoration: none; font-weight: 600;">📖 Kapitel-Guide lesen</a>
<div class="card-sublinks">
<a href="lehrpfad_2_oop_einstieg/05_objekte_kombinieren/aufgabe.py" download>📄 aufgabe.py</a>
@ -598,7 +624,8 @@
<div class="roadmap-desc">Das große interaktive Terminal-Haustier: Hunger, Energie, Glück, Schlafen, Füttern & Spielen.</div>
</div>
<div class="card-actions">
<a href="lehrpfad_2_oop_einstieg/06_abschlussprojekt_tamagotchi/index.html" class="card-primary-btn" style="background: #4f46e5;">📖 Kapitel öffnen &rarr;</a>
<a href="workspace.html?track=lehrpfad_2_oop_einstieg&chapter=06_abschlussprojekt_tamagotchi" class="card-primary-btn" style="background: #4f46e5;">💻 In Web-IDE öffnen &rarr;</a>
<a href="lehrpfad_2_oop_einstieg/06_abschlussprojekt_tamagotchi/index.html" style="text-align: center; font-size: 0.8rem; color: var(--text-muted); text-decoration: none; font-weight: 600;">📖 Kapitel-Guide lesen</a>
<div class="card-sublinks">
<a href="lehrpfad_2_oop_einstieg/06_abschlussprojekt_tamagotchi/aufgabe.py" download>📄 aufgabe.py</a>
@ -607,20 +634,20 @@
</div>
</div>
</div>
</div>
</section>
<!-- LEHRPFAD 3 -->
<section>
<div class="section-header">
<div>
<h2 id="lehrpfad-3">🚀 Lehrpfad 3: Fortgeschrittenes OOP &amp; GUIs</h2>
<div class="section-desc">Dunder-Methoden, Unit Tests (TDD), Vererbung, Exceptions, JSON, Tkinter &amp; Masterprojekt (07–16).</div>
</div>
<span class="track-badge track-3-badge">10 Module</span>
</div>
<div class="roadmap-grid">
</section>
<!-- LEHRPFAD 3 -->
<section>
<div class="section-header">
<div>
<h2 id="lehrpfad-3">🚀 Lehrpfad 3: Fortgeschrittenes OOP &amp; GUIs</h2>
<div class="section-desc">Dunder-Methoden, Unit Tests (TDD), Vererbung, Exceptions, JSON, Tkinter &amp; Masterprojekt (07–16).</div>
</div>
<span class="track-badge track-3-badge">10 Module</span>
</div>
<div class="roadmap-grid">
<div class="roadmap-card" style="border-left: 4px solid #7c3aed;">
<div>
<span class="roadmap-number" style="background: #7c3aed18; color: #7c3aed;">Adv 07</span>
@ -628,7 +655,8 @@
<div class="roadmap-desc">Veränderliche vs. unveränderliche Datentypen, <code>is</code> vs. <code>==</code>, flache und tiefe Kopien.</div>
</div>
<div class="card-actions">
<a href="lehrpfad_3_fortgeschrittenes_oop/07_referenzen_und_speicher/index.html" class="card-primary-btn" style="background: #7c3aed;">📖 Kapitel öffnen &rarr;</a>
<a href="workspace.html?track=lehrpfad_3_fortgeschrittenes_oop&chapter=07_referenzen_und_speicher" class="card-primary-btn" style="background: #7c3aed;">💻 In Web-IDE öffnen &rarr;</a>
<a href="lehrpfad_3_fortgeschrittenes_oop/07_referenzen_und_speicher/index.html" style="text-align: center; font-size: 0.8rem; color: var(--text-muted); text-decoration: none; font-weight: 600;">📖 Kapitel-Guide lesen</a>
<div class="card-sublinks">
<a href="lehrpfad_3_fortgeschrittenes_oop/07_referenzen_und_speicher/aufgabe.py" download>📄 aufgabe.py</a>
@ -644,7 +672,8 @@
<div class="roadmap-desc">Klassen rechen- und vergleichbar machen mit <code>__add__</code>, <code>__eq__</code>, <code>__len__</code>.</div>
</div>
<div class="card-actions">
<a href="lehrpfad_3_fortgeschrittenes_oop/08_operator_overloading_dunder/index.html" class="card-primary-btn" style="background: #7c3aed;">📖 Kapitel öffnen &rarr;</a>
<a href="workspace.html?track=lehrpfad_3_fortgeschrittenes_oop&chapter=08_operator_overloading_dunder" class="card-primary-btn" style="background: #7c3aed;">💻 In Web-IDE öffnen &rarr;</a>
<a href="lehrpfad_3_fortgeschrittenes_oop/08_operator_overloading_dunder/index.html" style="text-align: center; font-size: 0.8rem; color: var(--text-muted); text-decoration: none; font-weight: 600;">📖 Kapitel-Guide lesen</a>
<div class="card-sublinks">
<a href="lehrpfad_3_fortgeschrittenes_oop/08_operator_overloading_dunder/aufgabe.py" download>📄 aufgabe.py</a>
@ -660,7 +689,8 @@
<div class="roadmap-desc">Software-Qualität sichern: Eigene Testsuiten mit <code>unittest</code> schreiben (Test-Driven Development).</div>
</div>
<div class="card-actions">
<a href="lehrpfad_3_fortgeschrittenes_oop/09_eigene_unit_tests_schreiben/index.html" class="card-primary-btn" style="background: #7c3aed;">📖 Kapitel öffnen &rarr;</a>
<a href="workspace.html?track=lehrpfad_3_fortgeschrittenes_oop&chapter=09_eigene_unit_tests_schreiben" class="card-primary-btn" style="background: #7c3aed;">💻 In Web-IDE öffnen &rarr;</a>
<a href="lehrpfad_3_fortgeschrittenes_oop/09_eigene_unit_tests_schreiben/index.html" style="text-align: center; font-size: 0.8rem; color: var(--text-muted); text-decoration: none; font-weight: 600;">📖 Kapitel-Guide lesen</a>
<div class="card-sublinks">
<a href="lehrpfad_3_fortgeschrittenes_oop/09_eigene_unit_tests_schreiben/aufgabe.py" download>📄 aufgabe.py</a>
@ -676,7 +706,8 @@
<div class="roadmap-desc">Code wiederverwenden nach dem DRY-Prinzip: Basisklassen und abgeleitete Kindklassen.</div>
</div>
<div class="card-actions">
<a href="lehrpfad_3_fortgeschrittenes_oop/10_vererbung_und_super/index.html" class="card-primary-btn" style="background: #7c3aed;">📖 Kapitel öffnen &rarr;</a>
<a href="workspace.html?track=lehrpfad_3_fortgeschrittenes_oop&chapter=10_vererbung_und_super" class="card-primary-btn" style="background: #7c3aed;">💻 In Web-IDE öffnen &rarr;</a>
<a href="lehrpfad_3_fortgeschrittenes_oop/10_vererbung_und_super/index.html" style="text-align: center; font-size: 0.8rem; color: var(--text-muted); text-decoration: none; font-weight: 600;">📖 Kapitel-Guide lesen</a>
<div class="card-sublinks">
<a href="lehrpfad_3_fortgeschrittenes_oop/10_vererbung_und_super/aufgabe.py" download>📄 aufgabe.py</a>
@ -692,7 +723,8 @@
<div class="roadmap-desc">Vielgestaltigkeit: Gleiche Methodennamen bei unterschiedlichen Klassen (Duck Typing).</div>
</div>
<div class="card-actions">
<a href="lehrpfad_3_fortgeschrittenes_oop/11_polymorphie_und_interfaces/index.html" class="card-primary-btn" style="background: #7c3aed;">📖 Kapitel öffnen &rarr;</a>
<a href="workspace.html?track=lehrpfad_3_fortgeschrittenes_oop&chapter=11_polymorphie_und_interfaces" class="card-primary-btn" style="background: #7c3aed;">💻 In Web-IDE öffnen &rarr;</a>
<a href="lehrpfad_3_fortgeschrittenes_oop/11_polymorphie_und_interfaces/index.html" style="text-align: center; font-size: 0.8rem; color: var(--text-muted); text-decoration: none; font-weight: 600;">📖 Kapitel-Guide lesen</a>
<div class="card-sublinks">
<a href="lehrpfad_3_fortgeschrittenes_oop/11_polymorphie_und_interfaces/aufgabe.py" download>📄 aufgabe.py</a>
@ -708,7 +740,8 @@
<div class="roadmap-desc">Fehler abfangen mit <code>try/except/finally</code> und eigene Exception-Klassen definieren.</div>
</div>
<div class="card-actions">
<a href="lehrpfad_3_fortgeschrittenes_oop/12_exceptions_und_fehlerbehandlung/index.html" class="card-primary-btn" style="background: #7c3aed;">📖 Kapitel öffnen &rarr;</a>
<a href="workspace.html?track=lehrpfad_3_fortgeschrittenes_oop&chapter=12_exceptions_und_fehlerbehandlung" class="card-primary-btn" style="background: #7c3aed;">💻 In Web-IDE öffnen &rarr;</a>
<a href="lehrpfad_3_fortgeschrittenes_oop/12_exceptions_und_fehlerbehandlung/index.html" style="text-align: center; font-size: 0.8rem; color: var(--text-muted); text-decoration: none; font-weight: 600;">📖 Kapitel-Guide lesen</a>
<div class="card-sublinks">
<a href="lehrpfad_3_fortgeschrittenes_oop/12_exceptions_und_fehlerbehandlung/aufgabe.py" download>📄 aufgabe.py</a>
@ -724,7 +757,8 @@
<div class="roadmap-desc">Daten dauerhaft speichern und laden: Savegames, Konfigurationen und Tabellen-Export.</div>
</div>
<div class="card-actions">
<a href="lehrpfad_3_fortgeschrittenes_oop/13_persistenz_json_und_csv/index.html" class="card-primary-btn" style="background: #7c3aed;">📖 Kapitel öffnen &rarr;</a>
<a href="workspace.html?track=lehrpfad_3_fortgeschrittenes_oop&chapter=13_persistenz_json_und_csv" class="card-primary-btn" style="background: #7c3aed;">💻 In Web-IDE öffnen &rarr;</a>
<a href="lehrpfad_3_fortgeschrittenes_oop/13_persistenz_json_und_csv/index.html" style="text-align: center; font-size: 0.8rem; color: var(--text-muted); text-decoration: none; font-weight: 600;">📖 Kapitel-Guide lesen</a>
<div class="card-sublinks">
<a href="lehrpfad_3_fortgeschrittenes_oop/13_persistenz_json_und_csv/aufgabe.py" download>📄 aufgabe.py</a>
@ -740,7 +774,8 @@
<div class="roadmap-desc">Grafische Benutzeroberflächen: Fenster, Buttons, Eingabefelder und Event-Handler.</div>
</div>
<div class="card-actions">
<a href="lehrpfad_3_fortgeschrittenes_oop/14_gui_mit_tkinter/index.html" class="card-primary-btn" style="background: #7c3aed;">📖 Kapitel öffnen &rarr;</a>
<a href="workspace.html?track=lehrpfad_3_fortgeschrittenes_oop&chapter=14_gui_mit_tkinter" class="card-primary-btn" style="background: #7c3aed;">💻 In Web-IDE öffnen &rarr;</a>
<a href="lehrpfad_3_fortgeschrittenes_oop/14_gui_mit_tkinter/index.html" style="text-align: center; font-size: 0.8rem; color: var(--text-muted); text-decoration: none; font-weight: 600;">📖 Kapitel-Guide lesen</a>
<div class="card-sublinks">
<a href="lehrpfad_3_fortgeschrittenes_oop/14_gui_mit_tkinter/aufgabe.py" download>📄 aufgabe.py</a>
@ -756,7 +791,8 @@
<div class="roadmap-desc">Variable Argumente, Unpacking und eigene Container-Klassen mit <code>__getitem__</code>.</div>
</div>
<div class="card-actions">
<a href="lehrpfad_3_fortgeschrittenes_oop/15_parameter_und_container/index.html" class="card-primary-btn" style="background: #7c3aed;">📖 Kapitel öffnen &rarr;</a>
<a href="workspace.html?track=lehrpfad_3_fortgeschrittenes_oop&chapter=15_parameter_und_container" class="card-primary-btn" style="background: #7c3aed;">💻 In Web-IDE öffnen &rarr;</a>
<a href="lehrpfad_3_fortgeschrittenes_oop/15_parameter_und_container/index.html" style="text-align: center; font-size: 0.8rem; color: var(--text-muted); text-decoration: none; font-weight: 600;">📖 Kapitel-Guide lesen</a>
<div class="card-sublinks">
<a href="lehrpfad_3_fortgeschrittenes_oop/15_parameter_und_container/aufgabe.py" download>📄 aufgabe.py</a>
@ -772,7 +808,8 @@
<div class="roadmap-desc">Die Königsdisziplin: Eine vollständige Desktop-App mit MVC, Persistenz, Tests und GUI.</div>
</div>
<div class="card-actions">
<a href="lehrpfad_3_fortgeschrittenes_oop/16_master_abschlussprojekt/index.html" class="card-primary-btn" style="background: #7c3aed;">📖 Kapitel öffnen &rarr;</a>
<a href="workspace.html?track=lehrpfad_3_fortgeschrittenes_oop&chapter=16_master_abschlussprojekt" class="card-primary-btn" style="background: #7c3aed;">💻 In Web-IDE öffnen &rarr;</a>
<a href="lehrpfad_3_fortgeschrittenes_oop/16_master_abschlussprojekt/index.html" style="text-align: center; font-size: 0.8rem; color: var(--text-muted); text-decoration: none; font-weight: 600;">📖 Kapitel-Guide lesen</a>
<div class="card-sublinks">
<a href="lehrpfad_3_fortgeschrittenes_oop/16_master_abschlussprojekt/aufgabe.py" download>📄 aufgabe.py</a>
@ -781,70 +818,70 @@
</div>
</div>
</div>
</div>
</section>
</div>
</section>
<!-- 🎮 Gamification Dashboard -->
<section class="hud-card" id="gamification-hud">
<div class="hud-header">
<div class="hud-user">
<div class="hud-avatar" id="hud-avatar-icon">🐣</div>
<div>
<div class="hud-name" id="hud-user-name">Entwickler-Pass</div>
<div class="hud-title"><span id="hud-user-title">Code-Küken</span> &bull; <span class="hud-rank" id="hud-user-rank">Rang: Bronze</span></div>
<!-- 🎮 Gamification Dashboard -->
<section class="hud-card" id="gamification-hud">
<div class="hud-header">
<div class="hud-user">
<div class="hud-avatar" id="hud-avatar-icon">🐣</div>
<div>
<div class="hud-name" id="hud-user-name">Entwickler-Pass</div>
<div class="hud-title"><span id="hud-user-title">Code-Küken</span> &bull; <span class="hud-rank" id="hud-user-rank">Rang: Bronze</span></div>
</div>
</div>
<div class="hud-stats">
<div class="hud-stat-box">
<span class="hud-stat-value" id="hud-stat-level">1</span>
<span class="hud-stat-label">Level</span>
</div>
<div class="hud-stat-box">
<span class="hud-stat-value" id="hud-stat-xp">0 XP</span>
<span class="hud-stat-label">Erfahrung</span>
</div>
<div class="hud-stat-box">
<span class="hud-stat-value" id="hud-stat-solved">0 / 27</span>
<span class="hud-stat-label">Gelöst</span>
</div>
<div class="hud-stat-box">
<span class="hud-stat-value" id="hud-stat-streak">🔥 1 Tag</span>
<span class="hud-stat-label">Streak</span>
</div>
</div>
</div>
<div class="hud-stats">
<div class="hud-stat-box">
<span class="hud-stat-value" id="hud-stat-level">1</span>
<span class="hud-stat-label">Level</span>
</div>
<div class="hud-stat-box">
<span class="hud-stat-value" id="hud-stat-xp">0 XP</span>
<span class="hud-stat-label">Erfahrung</span>
</div>
<div class="hud-stat-box">
<span class="hud-stat-value" id="hud-stat-solved">0 / 27</span>
<span class="hud-stat-label">Gelöst</span>
</div>
<div class="hud-stat-box">
<span class="hud-stat-value" id="hud-stat-streak">🔥 1 Tag</span>
<span class="hud-stat-label">Streak</span>
</div>
</div>
</div>
<div class="hud-progress-container">
<div class="hud-progress-labels">
<span>Fortschritt zum nächsten Level</span>
<span id="hud-progress-text">0 / 100 XP</span>
<div class="hud-progress-container">
<div class="hud-progress-labels">
<span>Fortschritt zum nächsten Level</span>
<span id="hud-progress-text">0 / 100 XP</span>
</div>
<div class="hud-progress-bar-bg">
<div class="hud-progress-bar-fill" id="hud-progress-bar" style="width: 0%;"></div>
</div>
</div>
<div class="hud-progress-bar-bg">
<div class="hud-progress-bar-fill" id="hud-progress-bar" style="width: 0%;"></div>
</div>
</div>
<div class="hud-footer">
<div class="hud-cli-tip">
💻 Terminal-Befehl: <code>python3 profil.py</code> &bull; Gesamttest: <code>python3 test_all.py</code>
<div class="hud-footer">
<div class="hud-cli-tip">
💻 Terminal-Befehl: <code>python3 profil.py</code> &bull; Gesamttest: <code>python3 test_all.py</code>
</div>
<button type="button" class="hud-btn" onclick="if(window.renderGamificationHUD) window.renderGamificationHUD();">
🔄 Status aktualisieren
</button>
</div>
<button type="button" class="hud-btn" onclick="if(window.renderGamificationHUD) window.renderGamificationHUD();">
🔄 Status aktualisieren
</button>
</div>
</section>
</section>
<!-- ⚡ Live Python Playground -->
<section class="card" id="python-live-playground">
<h2>⚡ Interaktiver Python Live-Playground (Pyodide WebAssembly)</h2>
<p>Führe beliebigen Python-Code direkt im Browser aus – ohne Installation!</p>
<div class="code-container">
<div class="code-header">
<span>🐍 Python Code-Editor</span>
<button type="button" class="btn" id="btn-run-code" style="padding: 4px 12px; font-size: 0.8rem;">▶ Code ausführen</button>
</div>
<textarea id="playground-editor" style="width: 100%; height: 160px; background: #0b0f19; color: #f8fafc; font-family: var(--font-mono); font-size: 0.95rem; border: none; padding: 14px; outline: none; resize: vertical;"># Schreibe hier deinen Python-Code:
<!-- ⚡ Live Python Playground -->
<section class="card" id="python-live-playground">
<h2>⚡ Interaktiver Python Live-Playground (Pyodide WebAssembly)</h2>
<p>Führe beliebigen Python-Code direkt im Browser aus – ohne Installation!</p>
<div class="code-container">
<div class="code-header">
<span>🐍 Python Code-Editor</span>
<button type="button" class="btn" id="btn-run-code" style="padding: 4px 12px; font-size: 0.8rem;">▶ Code ausführen</button>
</div>
<textarea id="playground-editor" style="width: 100%; height: 160px; background: #0b0f19; color: #f8fafc; font-family: var(--font-mono); font-size: 0.95rem; border: none; padding: 14px; outline: none; resize: vertical;"># Schreibe hier deinen Python-Code:
class Auto:
def __init__(self, marke, ps):
self.marke = marke
@ -856,44 +893,44 @@ class Auto:
mein_auto = Auto("Porsche 911", 450)
print(mein_auto.hupen())
</textarea>
</div>
</div>
<div style="background: #060911; border-radius: var(--radius-sm); border: 1px solid var(--border-color); padding: 14px; margin-top: 10px;">
<div style="font-size: 0.8rem; font-weight: 700; color: #94a3b8; margin-bottom: 6px;">🖥️ Programmausgabe (Terminal):</div>
<pre id="playground-output" style="color: #38bdf8; min-height: 50px;">Klicke oben auf '▶ Code ausführen', um das Skript zu starten...</pre>
</div>
</section>
<div style="background: #060911; border-radius: var(--radius-sm); border: 1px solid var(--border-color); padding: 14px; margin-top: 10px;">
<div style="font-size: 0.8rem; font-weight: 700; color: #94a3b8; margin-bottom: 6px;">🖥️ Programmausgabe (Terminal):</div>
<pre id="playground-output" style="color: #38bdf8; min-height: 50px;">Klicke oben auf '▶ Code ausführen', um das Skript zu starten...</pre>
</div>
</section>
<!-- 🩺 Fehler-Dolmetscher -->
<section class="card" id="error-interpreter-widget">
<h2>🩺 Interaktiver Python Fehler-Dolmetscher</h2>
<p>Füge eine englische Fehlermeldung (Traceback) aus dem Terminal ein:</p>
<textarea id="error-input" placeholder="Beispiel: TypeError: unsupported operand type(s) for +: 'int' and 'str'" style="width: 100%; height: 90px; border: 1px solid var(--border-color); border-radius: var(--radius-sm); padding: 10px; font-family: var(--font-mono); font-size: 0.9rem; margin-bottom: 10px;"></textarea>
<button type="button" class="btn" id="btn-interpret-error">🔍 Fehler übersetzen &amp; erklären</button>
<!-- 🩺 Fehler-Dolmetscher -->
<section class="card" id="error-interpreter-widget">
<h2>🩺 Interaktiver Python Fehler-Dolmetscher</h2>
<p>Füge eine englische Fehlermeldung (Traceback) aus dem Terminal ein:</p>
<textarea id="error-input" placeholder="Beispiel: TypeError: unsupported operand type(s) for +: 'int' and 'str'" style="width: 100%; height: 90px; border: 1px solid var(--border-color); border-radius: var(--radius-sm); padding: 10px; font-family: var(--font-mono); font-size: 0.9rem; margin-bottom: 10px;"></textarea>
<button type="button" class="btn" id="btn-interpret-error">🔍 Fehler übersetzen &amp; erklären</button>
<div id="error-result" style="display: none; margin-top: 15px; padding: 15px; border-radius: var(--radius-sm); background: #f0fdf4; border: 1px solid #bbf7d0;">
<h4 id="error-name" style="margin-top: 0; color: #166534;"></h4>
<p id="error-explanation" style="color: #1e293b; margin: 8px 0;"></p>
<div style="font-weight: 700; color: #166534; margin-top: 8px;">🛠️ So behebst du das Problem:</div>
<ul id="error-fixes" style="padding-left: 20px; color: #334155; margin-top: 4px;"></ul>
</div>
</section>
<div id="error-result" style="display: none; margin-top: 15px; padding: 15px; border-radius: var(--radius-sm); background: #f0fdf4; border: 1px solid #bbf7d0;">
<h4 id="error-name" style="margin-top: 0; color: #166534;"></h4>
<p id="error-explanation" style="color: #1e293b; margin: 8px 0;"></p>
<div style="font-weight: 700; color: #166534; margin-top: 8px;">🛠️ So behebst du das Problem:</div>
<ul id="error-fixes" style="padding-left: 20px; color: #334155; margin-top: 4px;"></ul>
</div>
</section>
<!-- 🏆 Zertifikate -->
<section class="card" id="zertifikate">
<h2>🏆 Lehrpfad-Abschluss-Zertifikate</h2>
<p>Generiere offizielle, druckbare Zertifikate nach erfolgreichem Abschluss eines Lehrpfads:</p>
<!-- 🏆 Zertifikate -->
<section class="card" id="zertifikate">
<h2>🏆 Lehrpfad-Abschluss-Zertifikate</h2>
<p>Generiere offizielle, druckbare Zertifikate nach erfolgreichem Abschluss eines Lehrpfads:</p>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px; margin-top: 14px;">
<button type="button" class="btn" onclick="if(window.openCertificate) window.openCertificate('lehrpfad_1');">📜 Zertifikat Lehrpfad 1</button>
<button type="button" class="btn" onclick="if(window.openCertificate) window.openCertificate('lehrpfad_2');">📜 Zertifikat Lehrpfad 2</button>
<button type="button" class="btn" onclick="if(window.openCertificate) window.openCertificate('lehrpfad_3');">📜 Zertifikat Lehrpfad 3</button>
<button type="button" class="btn" onclick="if(window.openCertificate) window.openCertificate('master');">👑 Master-Zertifikat</button>
</div>
</section>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 12px; margin-top: 14px;">
<button type="button" class="btn" onclick="if(window.openCertificate) window.openCertificate('lehrpfad_1');">📜 Zertifikat Lehrpfad 1</button>
<button type="button" class="btn" onclick="if(window.openCertificate) window.openCertificate('lehrpfad_2');">📜 Zertifikat Lehrpfad 2</button>
<button type="button" class="btn" onclick="if(window.openCertificate) window.openCertificate('lehrpfad_3');">📜 Zertifikat Lehrpfad 3</button>
<button type="button" class="btn" onclick="if(window.openCertificate) window.openCertificate('master');">👑 Master-Zertifikat</button>
</div>
</section>
</main>
</main>
</div>
</div>
@ -908,6 +945,7 @@ print(mein_auto.hupen())
<script src="assets/parsons.js"></script>
<script src="assets/runner.js"></script>
<script src="assets/ui_enhancements.js"></script>
<script src="assets/app_auth.js"></script>
<script src="assets/sidebar.js"></script>
</body>
</html>

486
server.py Normal file
View file

@ -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()

248
teacher.html Normal file
View file

@ -0,0 +1,248 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Lehrer-Dashboard | Klassen &amp; Lernfortschritt</title>
<link rel="stylesheet" href="assets/style.css">
<style>
.class-card {
background: #ffffff;
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
padding: 20px;
margin-bottom: 20px;
box-shadow: var(--shadow-sm);
}
.class-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 12px;
}
.invite-code-box {
background: #e0e7ff;
color: #3730a3;
padding: 6px 14px;
border-radius: var(--radius-sm);
font-family: var(--font-mono);
font-weight: 800;
font-size: 1.1rem;
letter-spacing: 1px;
}
.student-table {
width: 100%;
border-collapse: collapse;
margin-top: 14px;
font-size: 0.9rem;
}
.student-table th, .student-table td {
padding: 10px 12px;
border-bottom: 1px solid #e2e8f0;
text-align: left;
}
.student-table th {
background: #f8fafc;
font-weight: 700;
color: var(--text-muted);
}
</style>
</head>
<body>
<div class="app-layout">
<aside class="app-sidebar">
<div class="sidebar-header">
<a href="index.html" class="sidebar-brand-link">
<div class="sidebar-logo">🐍</div>
<div>
<div class="sidebar-title">Python Portal</div>
<div class="sidebar-subtitle">Lehrerbereich</div>
</div>
</a>
</div>
<div class="sidebar-quick-nav">
<a href="index.html" class="sidebar-link">🏠 Schüler-Dashboard</a>
<a href="teacher.html" class="sidebar-link active">👨‍🏫 Klassenverwaltung</a>
<a href="index.html#zertifikate" class="sidebar-link">🏆 Zertifikate</a>
</div>
</aside>
<div class="app-main">
<header>
<div class="header-container">
<div class="header-badge">👨‍🏫 Dozenten- &amp; Lehrer-Portal</div>
<h1>Klassenverwaltung &amp; Lernfortschritts-Matrix</h1>
<p>Behalte den Überblick über deine Klassen, lade Schüler per Code ein und verfolge gelöste Aufgaben in Echtzeit.</p>
</div>
</header>
<main class="container">
<!-- Neue Klasse erstellen -->
<section class="card">
<h2>✨ Neue Schulklasse / Kursgruppe anlegen</h2>
<form id="create-class-form" style="display: flex; gap: 12px; flex-wrap: wrap; margin-top: 14px;">
<input type="text" id="input-class-name" required placeholder="Klassenname (z.B. Informatik 10a 2026/27)" style="flex: 1; min-width: 260px; padding: 10px 14px; border: 1px solid var(--border-color); border-radius: var(--radius-sm); font-size: 0.95rem;">
<button type="submit" class="btn" style="padding: 10px 20px;">➕ Klasse erstellen</button>
</form>
</section>
<!-- Vorhandene Klassen -->
<section>
<div class="section-header">
<h2>📚 Deine aktiven Klassen</h2>
</div>
<div id="classrooms-list-container">
<div class="card" style="text-align: center; color: var(--text-muted); padding: 40px;">
Lade Klassenräume...
</div>
</div>
</section>
</main>
</div>
</div>
<script src="assets/app_auth.js"></script>
<script>
async function loadClassrooms() {
const container = document.getElementById("classrooms-list-container");
const token = localStorage.getItem("auth_token");
if (!token) {
container.innerHTML = `
<div class="card" style="text-align: center; padding: 30px;">
<p>Du musst als <strong>Lehrkraft</strong> angemeldet sein, um Klassen zu verwalten.</p>
<button class="btn" onclick="window.openAuthModal('login')">🔑 Als Lehrer anmelden</button>
</div>
`;
return;
}
try {
const res = await fetch("/api/classrooms/list", {
headers: { Authorization: `Bearer ${token}` }
});
const data = await res.json();
const classrooms = data.classrooms || [];
if (classrooms.length === 0) {
container.innerHTML = `
<div class="card" style="text-align: center; padding: 30px; color: var(--text-muted);">
Noch keine Klassen angelegt. Erstelle oben deine erste Klasse!
</div>
`;
return;
}
container.innerHTML = classrooms.map(cl => `
<div class="class-card">
<div class="class-header">
<div>
<h3 style="margin: 0 0 4px 0; color: var(--text-main); font-size: 1.25rem;">${cl.name}</h3>
<span style="font-size: 0.85rem; color: var(--text-muted);">${cl.student_count || 0} Schüler eingeschrieben</span>
</div>
<div style="text-align: right;">
<div style="font-size: 0.72rem; color: var(--text-muted); margin-bottom: 2px;">EINLADUNGSCODE:</div>
<div class="invite-code-box">${cl.invite_code}</div>
</div>
</div>
<button class="btn btn-secondary" style="padding: 6px 12px; font-size: 0.82rem; margin-top: 10px;" onclick="loadClassMatrix(${cl.id}, '${cl.name}')">📊 Lernmatrix &amp; Fortschritt anzeigen</button>
<div id="matrix-container-${cl.id}" style="display: none; margin-top: 14px;"></div>
</div>
`).join("");
} catch (err) {
container.innerHTML = `<div class="box box-danger">Fehler beim Laden: ${err.message}</div>`;
}
}
async function loadClassMatrix(classId, className) {
const target = document.getElementById(`matrix-container-${classId}`);
if (target.style.display === "block") {
target.style.display = "none";
return;
}
target.style.display = "block";
target.innerHTML = "Lade Schüler-Fortschritt...";
const token = localStorage.getItem("auth_token");
try {
const res = await fetch(`/api/classrooms/matrix?classroom_id=${classId}`, {
headers: { Authorization: `Bearer ${token}` }
});
const data = await res.json();
const matrix = data.matrix || [];
if (matrix.length === 0) {
target.innerHTML = "<p style='color: var(--text-muted); font-size: 0.88rem;'>Noch keine Schüler in dieser Klasse beigetreten.</p>";
return;
}
target.innerHTML = `
<table class="student-table">
<thead>
<tr>
<th>Schüler</th>
<th>Level</th>
<th>XP</th>
<th>Streak</th>
<th>Gelöste Kapitel</th>
</tr>
</thead>
<tbody>
${matrix.map(m => `
<tr>
<td><strong>${m.student.name}</strong><br><span style="font-size: 0.75rem; color: #64748b;">${m.student.email}</span></td>
<td>⭐ Level ${m.student.level}</td>
<td><span style="font-weight: 700; color: #0284c7;">${m.student.xp} XP</span></td>
<td>🔥 ${m.student.streak_days} Tage</td>
<td><span style="background: #ecfdf5; color: #059669; padding: 2px 8px; border-radius: 99px; font-weight: 700; font-size: 0.8rem;">${Object.values(m.solved_chapters).filter(Boolean).length} / 27</span></td>
</tr>
`).join("")}
</tbody>
</table>
`;
} catch (e) {
target.innerHTML = `<span style="color: var(--danger);">Fehler: ${e.message}</span>`;
}
}
document.getElementById("create-class-form").onsubmit = async (e) => {
e.preventDefault();
const name = document.getElementById("input-class-name").value.trim();
const token = localStorage.getItem("auth_token");
if (!token) {
window.openAuthModal("login");
return;
}
try {
const res = await fetch("/api/classrooms/create", {
method: "POST",
headers: { "Content-Type": "application/json", Authorization: `Bearer ${token}` },
body: JSON.stringify({ name })
});
const data = await res.json();
if (!res.ok) throw new Error(data.error || "Fehler beim Erstellen");
alert(`Klasse '${name}' mit Einladungscode '${data.classroom.invite_code}' erstellt! 🎉`);
document.getElementById("input-class-name").value = "";
loadClassrooms();
} catch (err) {
alert("Fehler: " + err.message);
}
};
document.addEventListener("DOMContentLoaded", loadClassrooms);
</script>
</body>
</html>

372
workspace.html Normal file
View file

@ -0,0 +1,372 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Python Web-IDE | Interaktiver Arbeitsbereich</title>
<link rel="stylesheet" href="assets/style.css">
<style>
body {
margin: 0;
padding: 0;
overflow: hidden;
height: 100vh;
background: #090d16;
color: #f8fafc;
display: flex;
flex-direction: column;
}
/* Workspace Topbar */
.workspace-topbar {
height: 52px;
background: #0f172a;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 18px;
flex-shrink: 0;
z-index: 10;
}
.workspace-title {
font-size: 0.95rem;
font-weight: 800;
color: #ffffff;
display: flex;
align-items: center;
gap: 10px;
}
.workspace-actions {
display: flex;
align-items: center;
gap: 8px;
}
/* Workspace Split Screen Layout */
.workspace-body {
flex: 1;
display: grid;
grid-template-columns: 42% 58%;
height: calc(100vh - 52px);
overflow: hidden;
}
/* Left Pane: Theory & Tasks */
.pane-left {
background: #ffffff;
color: var(--text-main);
display: flex;
flex-direction: column;
border-right: 2px solid #e2e8f0;
height: 100%;
overflow: hidden;
}
.pane-tabs {
display: flex;
background: #f8fafc;
border-bottom: 1px solid #e2e8f0;
padding: 0 10px;
gap: 4px;
flex-shrink: 0;
}
.pane-tab-btn {
padding: 10px 14px;
border: none;
background: transparent;
font-size: 0.85rem;
font-weight: 700;
color: var(--text-muted);
cursor: pointer;
border-bottom: 2px solid transparent;
transition: all 0.15s ease;
}
.pane-tab-btn:hover {
color: var(--primary);
}
.pane-tab-btn.active {
color: var(--primary);
border-bottom-color: var(--primary);
background: #ffffff;
}
.pane-content {
flex: 1;
overflow-y: auto;
padding: 24px;
}
/* Right Pane: Code Editor & Terminal */
.pane-right {
display: flex;
flex-direction: column;
height: 100%;
background: #0b0f19;
overflow: hidden;
}
.editor-wrapper {
flex: 1;
height: 65%;
min-height: 200px;
position: relative;
background: #0b0f19;
}
#monaco-editor-container {
width: 100%;
height: 100%;
}
.terminal-wrapper {
height: 35%;
min-height: 140px;
background: #060911;
border-top: 2px solid rgba(255, 255, 255, 0.08);
display: flex;
flex-direction: column;
}
.terminal-header {
padding: 6px 14px;
background: #090d16;
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.78rem;
font-family: var(--font-mono);
color: #94a3b8;
}
#workspace-terminal-output {
flex: 1;
padding: 12px 16px;
overflow-y: auto;
font-family: var(--font-mono);
font-size: 0.88rem;
line-height: 1.5;
color: #f1f5f9;
white-space: pre-wrap;
}
.term-stdout { color: #38bdf8; }
.term-stderr, .term-error { color: #f87171; font-weight: bold; }
.term-success { color: #4ade80; font-weight: bold; }
.term-warning { color: #fbbf24; font-weight: bold; }
.term-info { color: #94a3b8; }
/* Success Modal */
.modal-celebration {
display: none;
position: fixed;
inset: 0;
background: rgba(15, 23, 42, 0.85);
backdrop-filter: blur(8px);
z-index: 9999;
align-items: center;
justify-content: center;
padding: 20px;
}
@media (max-width: 900px) {
.workspace-body {
grid-template-columns: 1fr;
grid-template-rows: 50% 50%;
}
}
</style>
</head>
<body>
<!-- Topbar -->
<div class="workspace-topbar">
<div class="workspace-title">
<a href="index.html" style="color: #94a3b8; text-decoration: none; font-size: 0.88rem;">🏠 Dashboard</a>
<span style="color: #64748b;">/</span>
<span id="workspace-chapter-name">Lerneinheit wird geladen...</span>
<span id="editor-save-indicator" style="font-size: 0.75rem; color: #10b981; opacity: 0.5; margin-left: 10px;">💾 Gespeichert</span>
</div>
<div class="workspace-actions">
<button type="button" class="btn" id="btn-run-code" style="padding: 6px 14px; font-size: 0.84rem; background: #0284c7;">▶ Code ausführen</button>
<button type="button" class="btn" id="btn-run-tests" style="padding: 6px 16px; font-size: 0.84rem; background: #10b981;">🧪 Tests starten</button>
<button type="button" class="btn btn-secondary" id="btn-show-solution" style="padding: 6px 12px; font-size: 0.84rem; background: #1e293b; color: white; border: none;">💡 Lösung</button>
<button type="button" class="btn btn-secondary" id="btn-reset-code" style="padding: 6px 10px; font-size: 0.84rem; background: transparent; color: #94a3b8; border: none;" title="Auf Startercode zurücksetzen">🔄</button>
</div>
</div>
<!-- Split Screen Body -->
<div class="workspace-body">
<!-- Left Pane -->
<div class="pane-left">
<div class="pane-tabs">
<button type="button" class="pane-tab-btn active" onclick="switchTab('theorie')">📖 Theorie &amp; Analogie</button>
<button type="button" class="pane-tab-btn" onclick="switchTab('subgoals')">🎯 Teilziele</button>
<button type="button" class="pane-tab-btn" id="tab-btn-puzzle" onclick="switchTab('puzzle')">🧩 Puzzle (+25 XP)</button>
<button type="button" class="pane-tab-btn" id="tab-btn-interpreter" onclick="switchTab('interpreter')">🩺 Dolmetscher</button>
</div>
<div class="pane-content" id="tab-content-theorie">
<div id="theory-loader" style="color: var(--text-muted);">Lade didaktische Erklärung...</div>
</div>
<div class="pane-content" id="tab-content-subgoals" style="display: none;">
<h3 style="margin-top: 0; font-size: 1.15rem; color: var(--text-main);">🎯 Nummerierte Teilziele (Subgoals)</h3>
<p style="font-size: 0.9rem; color: var(--text-muted);">Folge diesen strukturierten Schritten in <code>aufgabe.py</code>:</p>
<div id="subgoals-list-container"></div>
</div>
<div class="pane-content" id="tab-content-puzzle" style="display: none;">
<div id="puzzle-container"></div>
</div>
<div class="pane-content" id="tab-content-interpreter" style="display: none;">
<h3 style="margin-top: 0; font-size: 1.15rem; color: var(--text-main);">🩺 Python Fehler-Dolmetscher</h3>
<p style="font-size: 0.88rem; color: var(--text-muted);">Füge eine Traceback-Fehlermeldung ein:</p>
<textarea id="workspace-error-input" style="width: 100%; height: 90px; border: 1px solid var(--border-color); border-radius: var(--radius-sm); padding: 8px; font-family: var(--font-mono); font-size: 0.88rem;"></textarea>
<button type="button" class="btn" style="margin-top: 8px;" onclick="interpretWorkspaceError()">🔍 Übersetzen &amp; Erklären</button>
<div id="workspace-error-result" style="display: none; margin-top: 14px; padding: 12px; background: #f0fdf4; border-radius: var(--radius-sm); border: 1px solid #bbf7d0;"></div>
</div>
</div>
<!-- Right Pane -->
<div class="pane-right">
<div class="editor-wrapper">
<div id="monaco-editor-container"></div>
</div>
<div class="terminal-wrapper">
<div class="terminal-header">
<span>🖥️ Terminal / Test-Ausgabe</span>
<button type="button" id="btn-clear-terminal" style="background: transparent; border: none; color: #94a3b8; font-size: 0.75rem; cursor: pointer;">🧹 Leeren</button>
</div>
<div id="workspace-terminal-output">Klicke oben auf '▶ Code ausführen' oder '🧪 Tests starten'...</div>
</div>
</div>
</div>
<!-- Success Modal -->
<div class="modal-celebration" id="success-modal-banner">
<div class="certificate-container" style="max-width: 480px; text-align: center; padding: 32px;">
<div style="font-size: 3rem; margin-bottom: 10px;">🎉 🏆 🌟</div>
<h2 style="margin: 0 0 10px 0; color: var(--text-main); font-size: 1.6rem;">Kapitel gemeistert!</h2>
<p style="color: var(--text-muted); font-size: 1rem; margin-bottom: 24px;">Alle Tests wurden erfolgreich bestanden. Du hast <strong>+100 XP</strong> verdient!</p>
<div style="display: flex; gap: 10px; justify-content: center;">
<a href="index.html" class="btn btn-secondary" style="padding: 10px 18px;">Zurück zum Dashboard</a>
<button type="button" class="btn" id="btn-next-chapter" style="padding: 10px 22px;">Nächstes Kapitel &rarr;</button>
</div>
</div>
</div>
<!-- Scripts -->
<script src="assets/gamification_data.js"></script>
<script src="assets/gamification.js"></script>
<script src="assets/interpreter.js"></script>
<script src="assets/certificate.js"></script>
<script src="assets/parsons.js"></script>
<script src="assets/monaco_ide.js"></script>
<script>
// Tab Switcher
function switchTab(tabId) {
document.querySelectorAll(".pane-tab-btn").forEach(b => b.classList.remove("active"));
document.querySelectorAll(".pane-content").forEach(c => c.style.display = "none");
const targetBtn = event.target;
const targetContent = document.getElementById("tab-content-" + tabId);
if (targetBtn) targetBtn.classList.add("active");
if (targetContent) targetContent.style.display = "block";
}
// URL Parameters
const urlParams = new URLSearchParams(window.location.search);
const trackFolder = urlParams.get("track") || "lehrpfad_1_grundlagen";
const chapterFolder = urlParams.get("chapter") || "01_erste_schritte_taschenrechner";
const chapterId = `${trackFolder}/${chapterFolder}`;
// Load Chapter Files
async function loadWorkspaceData() {
try {
const [aufgabeCode, testCode, loesungCode, htmlDoc] = await Promise.all([
fetch(`${trackFolder}/${chapterFolder}/aufgabe.py`).then(r => r.text()),
fetch(`${trackFolder}/${chapterFolder}/test_aufgabe.py`).then(r => r.text()),
fetch(`${trackFolder}/${chapterFolder}/musterloesung.py`).then(r => r.text()).catch(() => ""),
fetch(`${trackFolder}/${chapterFolder}/index.html`).then(r => r.text()).catch(() => "")
]);
document.getElementById("workspace-chapter-name").innerText = chapterFolder.replace(/_/g, " ").toUpperCase();
// Extract theory content from chapter index.html
if (htmlDoc) {
const parser = new DOMParser();
const doc = parser.parseFromString(htmlDoc, "text/html");
const mainContent = doc.querySelector("main.container") || doc.body;
document.getElementById("tab-content-theorie").innerHTML = mainContent.innerHTML;
// Extract Subgoals
const subgoals = aufgabeCode.match(/# 🎯 TEILZIEL.*$/gm) || [];
const subContainer = document.getElementById("subgoals-list-container");
if (subgoals.length > 0) {
subContainer.innerHTML = subgoals.map((s, idx) => `
<div class="task-item" style="margin-bottom: 8px;">
<input type="checkbox" class="task-checkbox" id="sub_check_${idx}">
<label class="task-label" for="sub_check_${idx}"><strong>${s.replace('#', '').trim()}</strong></label>
</div>
`).join("");
} else {
subContainer.innerHTML = "<p style='color: var(--text-muted);'>Keine nummerierten Teilziele vorhanden.</p>";
}
}
// Musterlösung Button
document.getElementById("btn-show-solution")?.addEventListener("click", () => {
if (confirm("Möchtest du einen Blick in die Musterlösung werfen? Versuche zuerst, es selbst zu lösen!")) {
const w = window.open("", "_blank");
w.document.write(`<pre style="background:#0b0f19;color:#f8fafc;padding:20px;font-family:monospace;font-size:14px;">${loesungCode}</pre>`);
}
});
// Initialize Monaco IDE
window.initWorkspace({
chapterId: chapterId,
starterCode: aufgabeCode,
testCode: testCode,
solutionCode: loesungCode
});
} catch (err) {
document.getElementById("tab-content-theorie").innerHTML = `<div class="box box-danger">❌ Fehler beim Laden der Einheit: ${err.message}</div>`;
}
}
function interpretWorkspaceError() {
const errText = document.getElementById("workspace-error-input").value;
const resContainer = document.getElementById("workspace-error-result");
if (window.INTERPRETER && window.INTERPRETER.translate) {
const info = window.INTERPRETER.translate(errText);
resContainer.style.display = "block";
resContainer.innerHTML = `
<h4 style="margin-top: 0; color: #166534;">${info.name}</h4>
<p style="margin: 6px 0; color: #1e293b;">${info.explanation}</p>
<strong>Tipps:</strong>
<ul style="padding-left: 20px; margin-top: 4px;">
${info.fixes.map(f => `<li>${f}</li>`).join("")}
</ul>
`;
}
}
document.addEventListener("DOMContentLoaded", loadWorkspaceData);
</script>
</body>
</html>