Atlas: Live-View-Feature + Submit-Bug-Fixes + Admin-Styleguide
- Neuer API-Endpoint /api/live (heartbeat / spectator) + DB-Tabelle live_sessions + class_modules.paused-Spalte - Plattform-Live-Client (assets/js/live-client.js): Heartbeat, Pause-Overlay, Spectator-Mode bei ?view=teacher - ggs_inject_live() in _scripts.php als Helper, in alle 11 Sim-Wrapper integriert - Lehrer-Cockpit: tabellarische Klassen-Live-Ansicht pro Sim, Pause-Toggle, blinkender Tab-Indikator wenn aktive Sessions - Submit-Bug erschlagen: progress.php hat jetzt submit_assessment- und reflection-Action; saves.php akzeptiert beide Key-Konventionen; alle Direkt-Pfad-API-Files mit require_once-Bootstrap - Avatar-Default: zufaelliger DALL-E-Avatar fuer neue Schueler:innen - Admin-Styleguide-Seite mit Bildstil, Farb-Tokens, Layout, iPad-Pattern - Klima 2D + 3D: GGS_LIVE_STATE-Hook, Klima 2D zusaetzlich skipResume fuer Klassenaufgaben-Reset - Test-Lehrer Jakob + 3 Schueler:innen lokal + auf Live angelegt - Rundbriefe an alle Sim-Instanzen + Submit-Briefe an sonnensystem, busfahrt, energiemanager + Glossar-Anfrage zu Grundriess - Status-Uebergabe in _inbox/zentrale/_status.md Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
/**
|
||||
* API: Live-Sessions (Lehrer schaut Schüler:innen live beim Spielen zu)
|
||||
*
|
||||
* POST /api/live {action:'heartbeat', module_id, state} → Schüler-Sim, alle 3-5s
|
||||
* GET /api/live?class_id=X → Lehrer: alle aktiven Sessions der Klasse
|
||||
* GET /api/live?student_id=X → Lehrer: state eines Schülers für Spectator-View
|
||||
*
|
||||
* "Aktiv" = letzter Heartbeat liegt höchstens 30 s zurück.
|
||||
*
|
||||
* Bootstrap: idempotent, falls die Datei direkt (nicht über index.php) angesprochen wird.
|
||||
*/
|
||||
require_once __DIR__ . '/../config/app.php';
|
||||
require_once __DIR__ . '/../lib/Database.php';
|
||||
require_once __DIR__ . '/../lib/Session.php';
|
||||
require_once __DIR__ . '/../lib/Response.php';
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$db = Database::get();
|
||||
|
||||
const LIVE_ACTIVE_WINDOW_SEC = 30;
|
||||
|
||||
if ($method === 'POST') {
|
||||
$sessionId = Session::requireStudent();
|
||||
$body = json_decode(file_get_contents('php://input'), true);
|
||||
$action = $body['action'] ?? 'heartbeat';
|
||||
|
||||
if ($action === 'heartbeat') {
|
||||
$moduleId = (string)($body['module_id'] ?? $body['moduleId'] ?? '');
|
||||
if ($moduleId === '' || strlen($moduleId) > 32) Response::error('module_id erforderlich');
|
||||
$state = $body['state'] ?? null;
|
||||
$stateJson = is_string($state) ? $state : json_encode($state, JSON_UNESCAPED_UNICODE);
|
||||
if ($stateJson !== null && strlen($stateJson) > 50000) {
|
||||
// Überlange States kappen (Sicherheitsnetz; State sollte schlank gehalten werden)
|
||||
$stateJson = substr($stateJson, 0, 50000);
|
||||
}
|
||||
|
||||
$session = $db->fetchOne('SELECT class_id, display_name FROM student_sessions WHERE id = ?', [$sessionId]);
|
||||
if (!$session || !$session['class_id']) Response::ok(['ok' => false, 'reason' => 'no-class']);
|
||||
$student = $db->fetchOne('SELECT id FROM students WHERE class_id = ? AND display_name = ?', [$session['class_id'], $session['display_name']]);
|
||||
if (!$student) Response::ok(['ok' => false, 'reason' => 'no-student']);
|
||||
|
||||
$studentId = (int)$student['id'];
|
||||
$classId = (int)$session['class_id'];
|
||||
|
||||
// started_at frisch setzen, wenn (a) anderes Modul oder (b) länger als 30s
|
||||
// kein Heartbeat kam (Sim wurde geschlossen und neu gestartet).
|
||||
$db->execute(
|
||||
'INSERT INTO live_sessions (student_id, class_id, module_id, state_json, started_at, last_seen)
|
||||
VALUES (?, ?, ?, ?, NOW(), NOW())
|
||||
ON DUPLICATE KEY UPDATE
|
||||
class_id = VALUES(class_id),
|
||||
module_id = VALUES(module_id),
|
||||
state_json = VALUES(state_json),
|
||||
started_at = IF(module_id <> VALUES(module_id) OR last_seen < NOW() - INTERVAL 30 SECOND, NOW(), started_at),
|
||||
last_seen = NOW()',
|
||||
[$studentId, $classId, $moduleId, $stateJson]
|
||||
);
|
||||
Response::ok(['ok' => true]);
|
||||
}
|
||||
|
||||
if ($action === 'end') {
|
||||
$session = $db->fetchOne('SELECT display_name, class_id FROM student_sessions WHERE id = ?', [$sessionId]);
|
||||
$student = $session ? $db->fetchOne('SELECT id FROM students WHERE class_id = ? AND display_name = ?', [$session['class_id'], $session['display_name']]) : null;
|
||||
if ($student) $db->execute('DELETE FROM live_sessions WHERE student_id = ?', [(int)$student['id']]);
|
||||
Response::ok();
|
||||
}
|
||||
|
||||
Response::error('Unbekannte Aktion');
|
||||
}
|
||||
|
||||
if ($method === 'GET') {
|
||||
$teacherId = Session::requireTeacher();
|
||||
|
||||
// Spectator: state eines Schülers
|
||||
if (isset($_GET['student_id'])) {
|
||||
$studentId = (int)$_GET['student_id'];
|
||||
$row = $db->fetchOne(
|
||||
'SELECT ls.*, s.display_name, s.username, s.emoji_avatar, c.id AS owner_class_id
|
||||
FROM live_sessions ls
|
||||
JOIN students s ON s.id = ls.student_id
|
||||
JOIN classes c ON c.id = ls.class_id
|
||||
WHERE ls.student_id = ? AND c.teacher_id = ?',
|
||||
[$studentId, $teacherId]
|
||||
);
|
||||
if (!$row) Response::ok(['active' => false]);
|
||||
$stale = (strtotime($row['last_seen']) < time() - LIVE_ACTIVE_WINDOW_SEC);
|
||||
Response::ok([
|
||||
'active' => !$stale,
|
||||
'student_id' => (int)$row['student_id'],
|
||||
'username' => $row['username'],
|
||||
'displayName' => $row['display_name'] ?: $row['username'],
|
||||
'emoji' => $row['emoji_avatar'] ?: '🧑🎓',
|
||||
'module_id' => $row['module_id'],
|
||||
'state' => $row['state_json'] ? json_decode($row['state_json'], true) : null,
|
||||
'started_at' => $row['started_at'],
|
||||
'last_seen' => $row['last_seen'],
|
||||
]);
|
||||
}
|
||||
|
||||
// Klassen-Liste: alle aktiven Sessions
|
||||
$classId = (int)($_GET['class_id'] ?? 0);
|
||||
if (!$classId) Response::error('class_id erforderlich');
|
||||
$class = $db->fetchOne('SELECT id FROM classes WHERE id = ? AND teacher_id = ?', [$classId, $teacherId]);
|
||||
if (!$class) Response::error('Klasse nicht gefunden', 404);
|
||||
|
||||
$rows = $db->fetchAll(
|
||||
'SELECT ls.student_id, ls.module_id, ls.state_json, ls.started_at, ls.last_seen,
|
||||
s.username, s.display_name, s.emoji_avatar,
|
||||
mi.title AS module_name, mi.icon AS module_icon, mi.play_url AS play_url
|
||||
FROM live_sessions ls
|
||||
JOIN students s ON s.id = ls.student_id
|
||||
LEFT JOIN module_info mi ON mi.module_id = ls.module_id
|
||||
WHERE ls.class_id = ?
|
||||
AND ls.last_seen > NOW() - INTERVAL ? SECOND
|
||||
ORDER BY ls.last_seen DESC',
|
||||
[$classId, LIVE_ACTIVE_WINDOW_SEC]
|
||||
);
|
||||
$list = array_map(function($r) {
|
||||
return [
|
||||
'student_id' => (int)$r['student_id'],
|
||||
'username' => $r['username'],
|
||||
'displayName' => $r['display_name'] ?: $r['username'],
|
||||
'emoji' => $r['emoji_avatar'] ?: '🧑🎓',
|
||||
'module_id' => $r['module_id'],
|
||||
'moduleName' => $r['module_name'] ?: $r['module_id'],
|
||||
'moduleIcon' => $r['module_icon'] ?: '📚',
|
||||
'playUrl' => $r['play_url'] ?: $r['module_id'],
|
||||
'state' => $r['state_json'] ? json_decode($r['state_json'], true) : null,
|
||||
'started_at' => $r['started_at'],
|
||||
'last_seen' => $r['last_seen'],
|
||||
];
|
||||
}, $rows);
|
||||
Response::ok($list);
|
||||
}
|
||||
|
||||
Response::error('Methode nicht erlaubt', 405);
|
||||
+110
-19
@@ -4,7 +4,14 @@
|
||||
* GET /api/modules?class_id=X → Modulstatus für Klasse
|
||||
* GET /api/modules?class_id=X&matrix=1 → Matrix: alle Schüler × alle Module
|
||||
* POST /api/modules {action} → Modul freigeben/sperren
|
||||
*
|
||||
* Bootstrap: idempotent, damit auch direkter /php/api/modules.php-Aufruf funktioniert
|
||||
* (live-client.js pollt von der Sim aus diesen Direkt-Pfad).
|
||||
*/
|
||||
require_once __DIR__ . '/../config/app.php';
|
||||
require_once __DIR__ . '/../lib/Database.php';
|
||||
require_once __DIR__ . '/../lib/Session.php';
|
||||
require_once __DIR__ . '/../lib/Response.php';
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$db = Database::get();
|
||||
@@ -13,7 +20,7 @@ $db = Database::get();
|
||||
// Nur aktive + beta + geplante Module anzeigen (keine archivierten)
|
||||
$ALL_MODULES = $db->fetchAll(
|
||||
"SELECT module_id AS id, title AS name, short_desc AS `desc`, icon,
|
||||
status, play_url, sort_order
|
||||
card_image, status, play_url, sort_order
|
||||
FROM module_info
|
||||
WHERE status IN ('aktiv','beta','geplant')
|
||||
ORDER BY sort_order, title"
|
||||
@@ -28,17 +35,37 @@ if ($method === 'GET') {
|
||||
// Kein Klasse = alle Module frei (Autodidakt)
|
||||
$result = [];
|
||||
foreach ($ALL_MODULES as $mod) {
|
||||
$result[] = ['id' => $mod['id'], 'name' => $mod['name'], 'icon' => $mod['icon'], 'mode' => 'free'];
|
||||
$result[] = [
|
||||
'id' => $mod['id'],
|
||||
'name' => $mod['name'],
|
||||
'desc' => $mod['desc'],
|
||||
'icon' => $mod['icon'],
|
||||
'card_image' => $mod['card_image'],
|
||||
'status' => $mod['status'],
|
||||
'play_url' => $mod['play_url'],
|
||||
'mode' => $mod['status'] === 'geplant' ? 'coming_soon' : 'free',
|
||||
];
|
||||
}
|
||||
Response::ok($result);
|
||||
}
|
||||
$classId = $session['class_id'];
|
||||
$classMap = [];
|
||||
$settings = $db->fetchAll(
|
||||
'SELECT module_id, mode, current_level, quiz_enabled, due_date
|
||||
'SELECT module_id, mode, current_level, quiz_enabled, due_date, started_at, paused
|
||||
FROM class_modules WHERE class_id = ?', [$classId]);
|
||||
foreach ($settings as $s) $classMap[$s['module_id']] = $s;
|
||||
|
||||
// Pro Sim den letzten Abschluss-Zeitpunkt des Schülers laden (assessments).
|
||||
// Wird gegen class_modules.started_at verglichen, um teacher_started-
|
||||
// Aufträge als "erledigt" zu markieren, sobald der Schüler nach dem
|
||||
// Auftrag-Start einmal abgeschlossen hat.
|
||||
$lastDone = [];
|
||||
$rows = $db->fetchAll(
|
||||
'SELECT sim_id, MAX(submitted_at) AS last_at FROM assessments WHERE session_id = ? GROUP BY sim_id',
|
||||
[$sessionId]
|
||||
);
|
||||
foreach ($rows as $r) $lastDone[$r['sim_id']] = $r['last_at'];
|
||||
|
||||
// Individuelle Overrides für diesen Schüler
|
||||
$studentRow = $db->fetchOne(
|
||||
'SELECT s.id FROM students s JOIN student_sessions ss ON ss.class_id = s.class_id AND ss.display_name = s.display_name WHERE ss.id = ?',
|
||||
@@ -58,15 +85,30 @@ if ($method === 'GET') {
|
||||
if ($filterId && $mod['id'] !== $filterId) continue;
|
||||
$cm = $classMap[$mod['id']] ?? null;
|
||||
$mode = $overrides[$mod['id']] ?? ($cm['mode'] ?? 'locked');
|
||||
// 'geplant' überschreibt jeden mode-Eintrag — Modul ist nicht spielbar.
|
||||
if ($mod['status'] === 'geplant') $mode = 'coming_soon';
|
||||
|
||||
// Lehrer-Auftrag erledigt? (assessment-submit nach class_modules.started_at)
|
||||
$assignmentCompleted = false;
|
||||
if ($mode === 'teacher_started' && $cm && !empty($cm['started_at'])) {
|
||||
$done = $lastDone[$mod['id']] ?? null;
|
||||
if ($done && $done > $cm['started_at']) $assignmentCompleted = true;
|
||||
}
|
||||
|
||||
$result[] = [
|
||||
'id' => $mod['id'],
|
||||
'name' => $mod['name'],
|
||||
'icon' => $mod['icon'],
|
||||
'play_url' => $mod['play_url'],
|
||||
'mode' => $mode,
|
||||
'forcedLevel' => ($mode === 'teacher_started' && $cm) ? (int)($cm['current_level'] ?? 1) : null,
|
||||
'quizEnabled' => (bool)($cm['quiz_enabled'] ?? 0),
|
||||
'dueDate' => $cm['due_date'] ?? null,
|
||||
'id' => $mod['id'],
|
||||
'name' => $mod['name'],
|
||||
'desc' => $mod['desc'],
|
||||
'icon' => $mod['icon'],
|
||||
'card_image' => $mod['card_image'],
|
||||
'status' => $mod['status'],
|
||||
'play_url' => $mod['play_url'],
|
||||
'mode' => $mode,
|
||||
'forcedLevel' => ($mode === 'teacher_started' && $cm) ? (int)($cm['current_level'] ?? 1) : null,
|
||||
'quizEnabled' => (bool)($cm['quiz_enabled'] ?? 0),
|
||||
'dueDate' => $cm['due_date'] ?? null,
|
||||
'paused' => (bool)($cm['paused'] ?? 0),
|
||||
'assignmentCompleted' => $assignmentCompleted,
|
||||
];
|
||||
}
|
||||
// Wenn einzelnes Modul, liefere Objekt statt Array
|
||||
@@ -83,10 +125,17 @@ if ($method === 'GET') {
|
||||
$class = $db->fetchOne('SELECT id FROM classes WHERE id = ? AND teacher_id = ?', [$classId, $teacherId]);
|
||||
if (!$class) Response::error('Klasse nicht gefunden', 404);
|
||||
|
||||
// Klassen-Defaults laden
|
||||
$settings = $db->fetchAll('SELECT module_id, mode FROM class_modules WHERE class_id = ?', [$classId]);
|
||||
// Klassen-Defaults laden (inkl. Level + Pause-Status für die Lehrer-UI)
|
||||
$settings = $db->fetchAll(
|
||||
'SELECT module_id, mode, current_level, paused, due_date, started_at FROM class_modules WHERE class_id = ?',
|
||||
[$classId]
|
||||
);
|
||||
$classMap = [];
|
||||
foreach ($settings as $s) $classMap[$s['module_id']] = $s['mode'];
|
||||
$classMeta = [];
|
||||
foreach ($settings as $s) {
|
||||
$classMap[$s['module_id']] = $s['mode'];
|
||||
$classMeta[$s['module_id']] = $s;
|
||||
}
|
||||
|
||||
// Matrix-Modus: alle Schüler × alle Module
|
||||
if (isset($_GET['matrix']) && $_GET['matrix'] === '1') {
|
||||
@@ -128,12 +177,19 @@ if ($method === 'GET') {
|
||||
// Modul-Infos mit Klassen-Defaults
|
||||
$modulesInfo = [];
|
||||
foreach ($ALL_MODULES as $mod) {
|
||||
$meta = $classMeta[$mod['id']] ?? null;
|
||||
$modulesInfo[] = [
|
||||
'id' => $mod['id'],
|
||||
'name' => $mod['name'],
|
||||
'desc' => $mod['desc'],
|
||||
'icon' => $mod['icon'],
|
||||
'classMode' => $classMap[$mod['id']] ?? 'locked',
|
||||
'card_image' => $mod['card_image'],
|
||||
'status' => $mod['status'],
|
||||
'classMode' => $classMap[$mod['id']] ?? 'locked',
|
||||
'currentLevel' => $meta ? (int)($meta['current_level'] ?? 1) : 1,
|
||||
'paused' => $meta ? (bool)($meta['paused'] ?? 0) : false,
|
||||
'dueDate' => $meta['due_date'] ?? null,
|
||||
'startedAt' => $meta['started_at'] ?? null,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -148,6 +204,7 @@ if ($method === 'GET') {
|
||||
'name' => $mod['name'],
|
||||
'desc' => $mod['desc'],
|
||||
'icon' => $mod['icon'],
|
||||
'status' => $mod['status'],
|
||||
'mode' => $classMap[$mod['id']] ?? 'locked',
|
||||
];
|
||||
}
|
||||
@@ -166,16 +223,50 @@ if ($method === 'POST') {
|
||||
$class = $db->fetchOne('SELECT id FROM classes WHERE id = ? AND teacher_id = ?', [$classId, $teacherId]);
|
||||
if (!$class) Response::error('Klasse nicht gefunden', 404);
|
||||
|
||||
// Schutz: 'geplant'-Module dürfen NICHT freigeschaltet werden — sie sind
|
||||
// im Lehrer-UI auch nur als 'In Vorbereitung' sichtbar.
|
||||
$modStatus = $db->fetchOne('SELECT status FROM module_info WHERE module_id = ?', [$moduleId]);
|
||||
if (!$modStatus || $modStatus['status'] === 'geplant' || $modStatus['status'] === 'archiv') {
|
||||
Response::error('Modul ist noch in Vorbereitung und kann nicht freigeschaltet werden.');
|
||||
}
|
||||
|
||||
// Klassen-Default setzen
|
||||
if ($action === 'set_mode') {
|
||||
$mode = $body['mode'] ?? 'locked';
|
||||
if (!in_array($mode, ['locked', 'free', 'teacher_started'])) Response::error('Ungültiger Modus');
|
||||
$level = isset($body['currentLevel']) ? max(1, min(3, (int)$body['currentLevel'])) : 1;
|
||||
$dueDate = !empty($body['dueDate']) ? $body['dueDate'] : null;
|
||||
$paused = !empty($body['paused']) ? 1 : 0;
|
||||
|
||||
$db->execute(
|
||||
'INSERT INTO class_modules (class_id, module_id, enabled, mode)
|
||||
VALUES (?, ?, 1, ?)
|
||||
ON DUPLICATE KEY UPDATE mode = VALUES(mode), enabled = 1',
|
||||
[$classId, $moduleId, $mode]
|
||||
'INSERT INTO class_modules (class_id, module_id, enabled, mode, current_level, due_date, paused)
|
||||
VALUES (?, ?, 1, ?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE mode = VALUES(mode), enabled = 1,
|
||||
current_level = VALUES(current_level), due_date = VALUES(due_date), paused = VALUES(paused)',
|
||||
[$classId, $moduleId, $mode, $level, $dueDate, $paused]
|
||||
);
|
||||
// Bei "teacher_started" started_at frisch setzen — markiert Auftragsstart
|
||||
// und macht alte assessments unter dieser started_at zu "vorher".
|
||||
if ($mode === 'teacher_started') {
|
||||
$db->execute(
|
||||
'UPDATE class_modules SET started_at = NOW() WHERE class_id = ? AND module_id = ?',
|
||||
[$classId, $moduleId]
|
||||
);
|
||||
} else {
|
||||
$db->execute(
|
||||
'UPDATE class_modules SET started_at = NULL, paused = 0 WHERE class_id = ? AND module_id = ?',
|
||||
[$classId, $moduleId]
|
||||
);
|
||||
}
|
||||
Response::ok();
|
||||
}
|
||||
|
||||
// Auftrag pausieren / fortsetzen
|
||||
if ($action === 'pause' || $action === 'resume') {
|
||||
$paused = $action === 'pause' ? 1 : 0;
|
||||
$db->execute(
|
||||
'UPDATE class_modules SET paused = ? WHERE class_id = ? AND module_id = ?',
|
||||
[$paused, $classId, $moduleId]
|
||||
);
|
||||
Response::ok();
|
||||
}
|
||||
|
||||
+126
-1
@@ -4,7 +4,15 @@
|
||||
* GET /api/progress?sim_id=X → Level/XP für ein Spiel
|
||||
* GET /api/progress?student_id=X → Alle Fortschritte (Lehrer-Ansicht)
|
||||
* POST /api/progress {action} → Level updaten, Pre/Post-Antworten speichern
|
||||
*
|
||||
* Bootstrap: idempotent, falls direkt über /php/api/progress.php aufgerufen
|
||||
* (Klima 2D + 3D rufen diese URL direkt — bisher ergab das einen Fatal Error,
|
||||
* weil Database/Session/Response erst über index.php geladen werden).
|
||||
*/
|
||||
require_once __DIR__ . '/../config/app.php';
|
||||
require_once __DIR__ . '/../lib/Database.php';
|
||||
require_once __DIR__ . '/../lib/Session.php';
|
||||
require_once __DIR__ . '/../lib/Response.php';
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$db = Database::get();
|
||||
@@ -29,13 +37,36 @@ if ($method === 'GET') {
|
||||
Response::ok(['progress' => $progress ?: ['level'=>1,'xp'=>0,'plays'=>0,'best_stars'=>0], 'answers' => $answers]);
|
||||
}
|
||||
|
||||
// Lehrer*in: alle Fortschritte eines/r Schüler*in
|
||||
// Lehrer*in: alle Fortschritte eines/r bestimmten Schüler*in
|
||||
if ($studentId && !$simId) {
|
||||
$teacherId = Session::requireTeacher();
|
||||
$all = $db->fetchAll('SELECT sim_id, level, xp, plays, best_stars FROM player_progress WHERE student_id = ? ORDER BY sim_id', [$studentId]);
|
||||
Response::ok($all);
|
||||
}
|
||||
|
||||
// Schüler*in: eigene Fortschritts-Liste (Frontend ruft mit student_id=0 oder ohne)
|
||||
if (!$simId) {
|
||||
$sessionId = Session::requireStudent();
|
||||
$session = $db->fetchOne('SELECT class_id, display_name FROM student_sessions WHERE id = ?', [$sessionId]);
|
||||
$student = $session ? $db->fetchOne('SELECT id FROM students WHERE class_id = ? AND display_name = ?', [$session['class_id'], $session['display_name']]) : null;
|
||||
if (!$student) { Response::ok([]); }
|
||||
$studentId = (int)$student['id'];
|
||||
$all = $db->fetchAll(
|
||||
'SELECT pp.sim_id, pp.level, pp.xp, pp.plays, pp.best_stars,
|
||||
mi.title, mi.icon,
|
||||
(SELECT MAX(submitted_at) FROM assessments
|
||||
WHERE session_id IN (SELECT id FROM student_sessions
|
||||
WHERE class_id = ? AND display_name = ?)
|
||||
AND sim_id = pp.sim_id) AS last_played_at
|
||||
FROM player_progress pp
|
||||
LEFT JOIN module_info mi ON mi.module_id = pp.sim_id
|
||||
WHERE pp.student_id = ?
|
||||
ORDER BY pp.xp DESC, pp.plays DESC',
|
||||
[$session['class_id'], $session['display_name'], $studentId]
|
||||
);
|
||||
Response::ok($all);
|
||||
}
|
||||
|
||||
Response::error('sim_id oder student_id erforderlich');
|
||||
}
|
||||
|
||||
@@ -102,6 +133,100 @@ if ($method === 'POST') {
|
||||
Response::ok();
|
||||
}
|
||||
|
||||
// === Sim-Abschluss speichern (Klima-Format mit nested data) ===
|
||||
// Klima 2D + 3D senden action: 'submit_assessment' mit Body
|
||||
// {sim_id, data: {level, stars, score, duration_ms, completed, results, action_log, badges_earned}}.
|
||||
// 'complete' (oben) erwartet ein anderes Format; daher hier ein eigener Handler.
|
||||
if ($action === 'submit_assessment') {
|
||||
$sessionId = Session::requireStudent();
|
||||
$simId = $body['sim_id'] ?? $body['simId'] ?? '';
|
||||
if (!$simId) Response::error('sim_id erforderlich');
|
||||
|
||||
$payload = is_array($body['data'] ?? null) ? $body['data'] : [];
|
||||
$stars = max(0, min(5, (int)($payload['stars'] ?? 0)));
|
||||
$durationMs = (int)($payload['duration_ms'] ?? $body['durationMs'] ?? 0);
|
||||
$resultsJson = json_encode($payload, JSON_UNESCAPED_UNICODE);
|
||||
|
||||
$session = $db->fetchOne('SELECT class_id, display_name FROM student_sessions WHERE id = ?', [$sessionId]);
|
||||
$student = $session ? $db->fetchOne('SELECT id FROM students WHERE class_id = ? AND display_name = ?', [$session['class_id'], $session['display_name']]) : null;
|
||||
if (!$student) Response::error('Schüler*in nicht gefunden');
|
||||
$studentId = (int)$student['id'];
|
||||
|
||||
// XP + player_progress upserten (gleiche Logik wie 'complete')
|
||||
$xpGain = $stars * 10 + ($stars >= 5 ? 5 : 0);
|
||||
$db->execute(
|
||||
'INSERT INTO player_progress (student_id, sim_id, level, xp, plays, best_stars)
|
||||
VALUES (?, ?, 1, ?, 1, ?)
|
||||
ON DUPLICATE KEY UPDATE xp = xp + VALUES(xp), plays = plays + 1, best_stars = GREATEST(best_stars, VALUES(best_stars))',
|
||||
[$studentId, $simId, $xpGain, $stars]
|
||||
);
|
||||
$progress = $db->fetchOne('SELECT xp FROM player_progress WHERE student_id = ? AND sim_id = ?', [$studentId, $simId]);
|
||||
$xp = (int)$progress['xp'];
|
||||
$newLevel = $xp < 50 ? 1 : ($xp < 150 ? 2 : ($xp < 300 ? 3 : 4));
|
||||
$db->execute('UPDATE player_progress SET level = ? WHERE student_id = ? AND sim_id = ?', [$newLevel, $studentId, $simId]);
|
||||
|
||||
// Assessment speichern (zentrale Tabelle für Lehrer-Auswertung)
|
||||
$db->execute(
|
||||
'INSERT INTO assessments (session_id, sim_id, class_id, results, duration_ms, submitted_at) VALUES (?, ?, ?, ?, ?, NOW())',
|
||||
[$sessionId, $simId, $session['class_id'], $resultsJson, $durationMs]
|
||||
);
|
||||
|
||||
// Spiegel in student_results (V2)
|
||||
$score = isset($payload['score']) ? (float)$payload['score'] : null;
|
||||
$db->execute(
|
||||
'INSERT INTO student_results (student_id, class_id, module_id, score, duration_sec, detail_json, completed_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, NOW())',
|
||||
[$studentId, $session['class_id'], $simId, $score, (int)round($durationMs/1000), $resultsJson]
|
||||
);
|
||||
|
||||
Response::ok(['xpGained' => $xpGain, 'totalXp' => $xp + $xpGain, 'level' => $newLevel, 'stars' => $stars]);
|
||||
}
|
||||
|
||||
// === Endscreen-Reflexion speichern (Multiple-Choice-Antwort am Spielende) ===
|
||||
// Sims senden {action:'reflection', data:{level, question, answer}}.
|
||||
// Speichert die Antwort an den letzten assessment-Eintrag des Schülers für
|
||||
// diese Sim — damit Lehrkräfte später nachvollziehen können, was die
|
||||
// Bearbeiter:in als wirksamste/schwierigste Entscheidung empfunden hat.
|
||||
if ($action === 'reflection') {
|
||||
$sessionId = Session::requireStudent();
|
||||
$simId = $body['sim_id'] ?? $body['simId'] ?? '';
|
||||
$data = is_array($body['data'] ?? null) ? $body['data'] : [];
|
||||
if (!$simId) Response::error('sim_id erforderlich');
|
||||
|
||||
// Letzten assessment-Eintrag dieser Schüler:in × Sim suchen.
|
||||
$row = $db->fetchOne(
|
||||
'SELECT id, reflections FROM assessments
|
||||
WHERE session_id = ? AND sim_id = ? ORDER BY submitted_at DESC LIMIT 1',
|
||||
[$sessionId, $simId]
|
||||
);
|
||||
$entry = [
|
||||
'level' => $data['level'] ?? null,
|
||||
'question' => $data['question'] ?? null,
|
||||
'answer' => $data['answer'] ?? null,
|
||||
'recorded_at'=> date('Y-m-d H:i:s'),
|
||||
];
|
||||
|
||||
if ($row) {
|
||||
$existing = $row['reflections'] ? json_decode($row['reflections'], true) : [];
|
||||
if (!is_array($existing)) $existing = [];
|
||||
$existing[] = $entry;
|
||||
$db->execute(
|
||||
'UPDATE assessments SET reflections = ? WHERE id = ?',
|
||||
[json_encode($existing, JSON_UNESCAPED_UNICODE), $row['id']]
|
||||
);
|
||||
} else {
|
||||
// Kein Assessment-Eintrag (z. B. Reflexion vor submit_assessment) →
|
||||
// schreibe einen Stub mit nur Reflexion. Wird bei späterem Submit
|
||||
// nicht überschrieben (verschiedene IDs).
|
||||
$session = $db->fetchOne('SELECT class_id FROM student_sessions WHERE id = ?', [$sessionId]);
|
||||
$db->execute(
|
||||
'INSERT INTO assessments (session_id, sim_id, class_id, reflections) VALUES (?, ?, ?, ?)',
|
||||
[$sessionId, $simId, $session['class_id'] ?? null, json_encode([$entry], JSON_UNESCAPED_UNICODE)]
|
||||
);
|
||||
}
|
||||
Response::ok();
|
||||
}
|
||||
|
||||
// === Wirkungsklammer-Ergebnis abrufen ===
|
||||
if ($action === 'wirkung') {
|
||||
$sessionId = $body['sessionId'] ?? Session::studentId();
|
||||
|
||||
+41
-3
@@ -3,7 +3,13 @@
|
||||
* API: Spielstaende speichern/laden
|
||||
* GET /api/saves?key=klimawaechter-save
|
||||
* POST /api/saves {key, data, version}
|
||||
*
|
||||
* Bootstrap: idempotent, falls direkt über /php/api/saves.php aufgerufen.
|
||||
*/
|
||||
require_once __DIR__ . '/../config/app.php';
|
||||
require_once __DIR__ . '/../lib/Database.php';
|
||||
require_once __DIR__ . '/../lib/Session.php';
|
||||
require_once __DIR__ . '/../lib/Response.php';
|
||||
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$db = Database::get();
|
||||
@@ -15,6 +21,34 @@ if ($method === 'GET') {
|
||||
$key = $_GET['key'] ?? '';
|
||||
if (!$key || strlen($key) > 100) Response::error('key fehlt oder zu lang');
|
||||
|
||||
// Optional: module_id mitgeben → erlaubt es, Saves bei aktivem
|
||||
// Lehrer-Auftrag (mode='teacher_started') zu unterdrücken, damit der
|
||||
// Schüler frisch in den Auftrag startet. Sobald der Auftrag durch ein
|
||||
// assessment-Eintrag (submitted_at > started_at) abgeschlossen ist,
|
||||
// greift wieder der normale Save-Resume.
|
||||
$moduleId = $_GET['module_id'] ?? '';
|
||||
if ($moduleId !== '' && strlen($moduleId) <= 32) {
|
||||
$session = $db->fetchOne(
|
||||
'SELECT class_id FROM student_sessions WHERE id = ?', [$sessionId]
|
||||
);
|
||||
if ($session && $session['class_id']) {
|
||||
$cm = $db->fetchOne(
|
||||
'SELECT mode, started_at FROM class_modules WHERE class_id = ? AND module_id = ?',
|
||||
[$session['class_id'], $moduleId]
|
||||
);
|
||||
if ($cm && $cm['mode'] === 'teacher_started' && $cm['started_at']) {
|
||||
$completed = $db->fetchOne(
|
||||
'SELECT 1 FROM assessments WHERE session_id = ? AND sim_id = ? AND submitted_at > ? LIMIT 1',
|
||||
[$sessionId, $moduleId, $cm['started_at']]
|
||||
);
|
||||
if (!$completed) {
|
||||
// Aktiver, noch nicht erledigter Lehrer-Auftrag → frisch starten.
|
||||
Response::json(['data' => null, 'version' => null, 'fresh' => 'teacher_started']);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$row = $db->fetchOne(
|
||||
'SELECT save_data, save_version FROM game_saves WHERE session_id = ? AND save_key = ?',
|
||||
[$sessionId, $key]
|
||||
@@ -25,10 +59,14 @@ if ($method === 'GET') {
|
||||
if ($method === 'POST') {
|
||||
$sessionId = Session::requireStudent();
|
||||
$body = json_decode(file_get_contents('php://input'), true);
|
||||
if (!$body || !isset($body['key']) || !isset($body['data'])) {
|
||||
// Klima 2D/3D senden 'save_key'/'save_data', historische Sims 'key'/'data'.
|
||||
$key = $body['key'] ?? $body['save_key'] ?? null;
|
||||
$data = $body['data'] ?? $body['save_data'] ?? null;
|
||||
if (!$body || $key === null || $data === null) {
|
||||
Response::error('key und data erforderlich');
|
||||
}
|
||||
if (strlen($body['key']) > 100 || strlen($body['data']) > 500000) {
|
||||
if (!is_string($data)) $data = json_encode($data, JSON_UNESCAPED_UNICODE);
|
||||
if (strlen($key) > 100 || strlen($data) > 500000) {
|
||||
Response::error('Payload zu gross', 413);
|
||||
}
|
||||
|
||||
@@ -36,7 +74,7 @@ if ($method === 'POST') {
|
||||
'INSERT INTO game_saves (session_id, save_key, save_data, save_version)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON DUPLICATE KEY UPDATE save_data = VALUES(save_data), save_version = VALUES(save_version)',
|
||||
[$sessionId, $body['key'], $body['data'], $body['version'] ?? 2]
|
||||
[$sessionId, $key, $data, $body['version'] ?? 2]
|
||||
);
|
||||
Response::ok();
|
||||
}
|
||||
|
||||
+124
-8
@@ -8,6 +8,17 @@
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
$db = Database::get();
|
||||
|
||||
// Zufälliger Bild-Avatar im Plattform-Bildstil (siehe assets/img/avatars/).
|
||||
// Slug-Liste deckungsgleich mit AVATAR_GROUPS in profil.html.
|
||||
function ggs_random_avatar_slug(): string {
|
||||
static $slugs = [
|
||||
'pilotin','pilot','kartografin','geologe','meteorologin','stadtplaner','forscherin','forscher','taucherin','astronaut',
|
||||
'coole-girl','coole-boy','lustige-girl','lustige-boy','nerd-girl','sportlich-boy','kuenstlerin','musiker','gaertnerin','abenteurer',
|
||||
'fuchs-explorer','eule-prof','baer-foerster','pinguin-polar','affe-tropen','delfin-meer','adler-pilot','schildkroete-w','katze-stadt','biene-natur'
|
||||
];
|
||||
return 'avatar:' . $slugs[array_rand($slugs)];
|
||||
}
|
||||
|
||||
if ($method === 'GET') {
|
||||
$teacherId = Session::requireTeacher();
|
||||
$classId = (int)($_GET['class_id'] ?? 0);
|
||||
@@ -18,7 +29,7 @@ if ($method === 'GET') {
|
||||
if (!$class) Response::error('Klasse nicht gefunden', 404);
|
||||
|
||||
$students = $db->fetchAll(
|
||||
'SELECT id, username, display_name, first_name, last_name, email, emoji_avatar, is_anonymous, easy_language, created_at, last_login FROM students WHERE class_id = ? AND deleted_at IS NULL ORDER BY username',
|
||||
'SELECT id, username, display_name, first_name, last_name, email, emoji_avatar, is_anonymous, easy_language, password_plaintext, created_at, last_login FROM students WHERE class_id = ? AND deleted_at IS NULL ORDER BY username',
|
||||
[$classId]
|
||||
);
|
||||
|
||||
@@ -66,11 +77,52 @@ if ($method === 'POST') {
|
||||
if ($existing) Response::error('Benutzername in dieser Klasse bereits vergeben');
|
||||
|
||||
$hash = password_hash($password, PASSWORD_DEFAULT);
|
||||
$avatar = ggs_random_avatar_slug();
|
||||
$db->execute(
|
||||
'INSERT INTO students (class_id, username, password, display_name, first_name, last_name, email, is_anonymous) VALUES (?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
[$classId, $username, $hash, $displayName ?: null, $firstName ?: null, $lastName ?: null, $email ?: null, $isAnonymous ? 1 : 0]
|
||||
'INSERT INTO students (class_id, username, password, password_plaintext, display_name, first_name, last_name, email, is_anonymous, emoji_avatar) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
[$classId, $username, $hash, $password, $displayName ?: null, $firstName ?: null, $lastName ?: null, $email ?: null, $isAnonymous ? 1 : 0, $avatar]
|
||||
);
|
||||
Response::ok(['id' => (int)$db->lastInsertId()]);
|
||||
Response::ok(['id' => (int)$db->lastInsertId(), 'emoji_avatar' => $avatar]);
|
||||
}
|
||||
|
||||
// === CSV-IMPORT — pro Zeile einen Schüler ===
|
||||
if ($action === 'create_csv') {
|
||||
$classId = (int)($body['classId'] ?? 0);
|
||||
$rows = $body['rows'] ?? [];
|
||||
if (!is_array($rows) || !$rows) Response::error('keine Zeilen');
|
||||
|
||||
$class = $db->fetchOne('SELECT id FROM classes WHERE id = ? AND teacher_id = ?', [$classId, $teacherId]);
|
||||
if (!$class) Response::error('Klasse nicht gefunden', 404);
|
||||
|
||||
$created = [];
|
||||
$skipped = [];
|
||||
foreach ($rows as $r) {
|
||||
$username = mb_substr(trim($r['username'] ?? ''), 0, 64);
|
||||
$password = trim($r['password'] ?? '');
|
||||
$displayName = mb_substr(trim($r['display_name'] ?? ''), 0, 128);
|
||||
$firstName = mb_substr(trim($r['first_name'] ?? ''), 0, 64);
|
||||
$lastName = mb_substr(trim($r['last_name'] ?? ''), 0, 64);
|
||||
$email = trim($r['email'] ?? '');
|
||||
$easy = !empty($r['easy']) && $r['easy'] !== '0';
|
||||
if (!$username) { $skipped[] = ['row' => $r, 'reason' => 'kein username']; continue; }
|
||||
if ($email && !filter_var($email, FILTER_VALIDATE_EMAIL)) { $skipped[] = ['row' => $r, 'reason' => 'ungueltige E-Mail']; continue; }
|
||||
$existing = $db->fetchOne('SELECT id FROM students WHERE class_id = ? AND username = ?', [$classId, $username]);
|
||||
if ($existing) { $skipped[] = ['row' => $r, 'reason' => 'username existiert bereits']; continue; }
|
||||
|
||||
// Passwort: angegeben oder 4-Ziffern-Generat
|
||||
if (!$password) $password = str_pad((string)random_int(1000, 9999), 4, '0', STR_PAD_LEFT);
|
||||
if (strlen($password) < 4) { $skipped[] = ['row' => $r, 'reason' => 'Passwort zu kurz']; continue; }
|
||||
$hash = password_hash($password, PASSWORD_DEFAULT);
|
||||
|
||||
$isAnonymous = !$firstName && !$lastName;
|
||||
$avatar = ggs_random_avatar_slug();
|
||||
$db->execute(
|
||||
'INSERT INTO students (class_id, username, password, password_plaintext, display_name, first_name, last_name, email, is_anonymous, easy_language, emoji_avatar) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
|
||||
[$classId, $username, $hash, $password, $displayName ?: $username, $firstName ?: null, $lastName ?: null, $email ?: null, $isAnonymous ? 1 : 0, $easy ? 1 : 0, $avatar]
|
||||
);
|
||||
$created[] = ['username' => $username, 'password' => $password, 'displayName' => $displayName ?: $username, 'id' => (int)$db->lastInsertId(), 'emoji_avatar' => $avatar];
|
||||
}
|
||||
Response::ok(['created' => $created, 'skipped' => $skipped]);
|
||||
}
|
||||
|
||||
// === MEHRERE SCHUELER AUF EINMAL ERSTELLEN ===
|
||||
@@ -92,11 +144,12 @@ if ($method === 'POST') {
|
||||
$existing = $db->fetchOne('SELECT id FROM students WHERE class_id = ? AND username = ?', [$classId, $username]);
|
||||
if ($existing) continue; // Ueberspringen wenn schon vorhanden
|
||||
|
||||
$avatar = ggs_random_avatar_slug();
|
||||
$db->execute(
|
||||
'INSERT INTO students (class_id, username, password, display_name, is_anonymous) VALUES (?, ?, ?, ?, 1)',
|
||||
[$classId, $username, $hash, $username]
|
||||
'INSERT INTO students (class_id, username, password, password_plaintext, display_name, is_anonymous, emoji_avatar) VALUES (?, ?, ?, ?, ?, 1, ?)',
|
||||
[$classId, $username, $hash, $pw, $username, $avatar]
|
||||
);
|
||||
$created[] = ['username' => $username, 'password' => $pw, 'id' => (int)$db->lastInsertId()];
|
||||
$created[] = ['username' => $username, 'password' => $pw, 'id' => (int)$db->lastInsertId(), 'emoji_avatar' => $avatar];
|
||||
}
|
||||
Response::ok(['created' => $created]);
|
||||
}
|
||||
@@ -136,7 +189,10 @@ if ($method === 'POST') {
|
||||
$db->execute('UPDATE students SET emoji_avatar = ? WHERE id = ?', [$emojiAvatar, $studentId]);
|
||||
}
|
||||
if ($newPassword && strlen($newPassword) >= 4) {
|
||||
$db->execute('UPDATE students SET password = ? WHERE id = ?', [password_hash($newPassword, PASSWORD_DEFAULT), $studentId]);
|
||||
$db->execute(
|
||||
'UPDATE students SET password = ?, password_plaintext = ? WHERE id = ?',
|
||||
[password_hash($newPassword, PASSWORD_DEFAULT), $newPassword, $studentId]
|
||||
);
|
||||
}
|
||||
// Leichte Sprache (nur setzen wenn Feld mitgeschickt)
|
||||
if (array_key_exists('easyLanguage', $body)) {
|
||||
@@ -146,6 +202,66 @@ if ($method === 'POST') {
|
||||
Response::ok();
|
||||
}
|
||||
|
||||
// === PASSWORT EINES SCHUELERS ZURUECKSETZEN ===
|
||||
if ($action === 'reset_password') {
|
||||
$studentId = (int)($body['studentId'] ?? 0);
|
||||
$student = $db->fetchOne(
|
||||
'SELECT s.id FROM students s JOIN classes c ON c.id = s.class_id WHERE s.id = ? AND c.teacher_id = ?',
|
||||
[$studentId, $teacherId]
|
||||
);
|
||||
if (!$student) Response::error('Schüler nicht gefunden', 404);
|
||||
|
||||
$pw = str_pad((string)random_int(1000, 9999), 4, '0', STR_PAD_LEFT);
|
||||
$hash = password_hash($pw, PASSWORD_DEFAULT);
|
||||
$db->execute(
|
||||
'UPDATE students SET password = ?, password_plaintext = ? WHERE id = ?',
|
||||
[$hash, $pw, $studentId]
|
||||
);
|
||||
Response::ok(['password' => $pw]);
|
||||
}
|
||||
|
||||
// === PASSWOERTER EINER AUSWAHL (oder alle der Klasse) ZURUECKSETZEN ===
|
||||
if ($action === 'reset_class_passwords') {
|
||||
$classId = (int)($body['classId'] ?? 0);
|
||||
$studentIds = $body['studentIds'] ?? null; // optional: array von ids
|
||||
$class = $db->fetchOne('SELECT id FROM classes WHERE id = ? AND teacher_id = ?', [$classId, $teacherId]);
|
||||
if (!$class) Response::error('Klasse nicht gefunden', 404);
|
||||
|
||||
if (is_array($studentIds) && $studentIds) {
|
||||
$studentIds = array_values(array_filter(array_map('intval', $studentIds)));
|
||||
if (!$studentIds) Response::error('Keine gültigen IDs');
|
||||
$placeholders = implode(',', array_fill(0, count($studentIds), '?'));
|
||||
$params = array_merge([$classId], $studentIds);
|
||||
$students = $db->fetchAll(
|
||||
"SELECT id, username, display_name, emoji_avatar FROM students
|
||||
WHERE class_id = ? AND deleted_at IS NULL AND id IN ($placeholders) ORDER BY username",
|
||||
$params
|
||||
);
|
||||
} else {
|
||||
$students = $db->fetchAll(
|
||||
'SELECT id, username, display_name, emoji_avatar FROM students WHERE class_id = ? AND deleted_at IS NULL ORDER BY username',
|
||||
[$classId]
|
||||
);
|
||||
}
|
||||
$reset = [];
|
||||
foreach ($students as $s) {
|
||||
$pw = str_pad((string)random_int(1000, 9999), 4, '0', STR_PAD_LEFT);
|
||||
$hash = password_hash($pw, PASSWORD_DEFAULT);
|
||||
$db->execute(
|
||||
'UPDATE students SET password = ?, password_plaintext = ? WHERE id = ?',
|
||||
[$hash, $pw, $s['id']]
|
||||
);
|
||||
$reset[] = [
|
||||
'id' => (int)$s['id'],
|
||||
'username' => $s['username'],
|
||||
'displayName' => $s['display_name'] ?: $s['username'],
|
||||
'emoji' => $s['emoji_avatar'] ?: '🧑🎓',
|
||||
'password' => $pw,
|
||||
];
|
||||
}
|
||||
Response::ok(['reset' => $reset]);
|
||||
}
|
||||
|
||||
// === SCHUELER LOESCHEN ===
|
||||
if ($action === 'delete') {
|
||||
$studentId = (int)($body['studentId'] ?? 0);
|
||||
|
||||
@@ -42,6 +42,40 @@ function injectSessionContext(): void {
|
||||
echo '<script>window.__GGS__ = ' . json_encode($ctx, JSON_UNESCAPED_UNICODE) . ';</script>' . "\n";
|
||||
}
|
||||
|
||||
/**
|
||||
* Plattform-Live-Client in eine Sim-Render-HTML einfügen.
|
||||
*
|
||||
* Stellt sicher, dass `window.__GGS__` mit `sessionId`, `simId`, `baseUrl`,
|
||||
* `basePath`, `apiUrl` belegt ist (Sim-eigene Werte gewinnen) und lädt
|
||||
* danach `assets/js/live-client.js`. Der Live-Client kümmert sich dann um
|
||||
* Heartbeat / Pause-Polling / Spectator-Mode.
|
||||
*
|
||||
* In jedem Sim-Wrapper kurz vor `echo $html` aufrufen:
|
||||
* $html = ggs_inject_live($html, 'logistik');
|
||||
*/
|
||||
function ggs_inject_live(string $html, string $simId): string {
|
||||
$bp = BASE_PATH;
|
||||
$bu = BASE_URL;
|
||||
$sid = Session::studentId() ?? '';
|
||||
$ctx = [
|
||||
'sessionId' => $sid,
|
||||
'simId' => $simId,
|
||||
'baseUrl' => $bu,
|
||||
'basePath' => $bp,
|
||||
'apiUrl' => $bu . '/php/api',
|
||||
];
|
||||
$ctxJson = json_encode($ctx, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
// Sim-eigene __GGS__-Felder gewinnen — wir ergänzen nur Plattform-Felder.
|
||||
$bootstrap = '<script>window.__GGS__=Object.assign(' . $ctxJson . ',window.__GGS__||{});</script>';
|
||||
$tag = '<script src="' . htmlspecialchars($bp . '/assets/js/live-client.js', ENT_QUOTES) . '" defer></script>';
|
||||
$injection = $bootstrap . "\n" . $tag . "\n";
|
||||
|
||||
// Sicheres Inject: vor dem letzten </body>. Falls keines, einfach anhängen.
|
||||
$pos = strrpos($html, '</body>');
|
||||
if ($pos === false) return $html . "\n" . $injection;
|
||||
return substr($html, 0, $pos) . $injection . substr($html, $pos);
|
||||
}
|
||||
|
||||
/** Seite rendern: HTML laden, Session-Kontext + Favicon injizieren, ausgeben */
|
||||
function renderPage(string $htmlFile): void {
|
||||
$html = file_get_contents(APP_ROOT . '/' . $htmlFile);
|
||||
|
||||
Reference in New Issue
Block a user