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);