1e51ef7def
- Konzept/, didaktik_geografie/, didaktik_simulation/, v2-modules/, v2-platform/ - 12 code-workspace-Files - STATUS-*.md - viele M/D/R-Änderungen an bereits getrackten Files - .gitignore verstärkt: **/.humaninput/, **/secret_keys.txt Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
85 lines
2.6 KiB
PHP
85 lines
2.6 KiB
PHP
<?php
|
|
/**
|
|
* GET /api/teacher/live-stream?classId=42
|
|
*
|
|
* Server-Sent Events für Lehrer-Live-Dashboard.
|
|
* Pollt regelmäßig die telemetry_events_v2 nach neuen Events der Klasse
|
|
* und sendet sie als SSE.
|
|
*
|
|
* NOTE: Apache nutzt häufig prefork und blockiert lange Connections.
|
|
* Für Produktion ggf. auf Long-Polling oder Websocket umstellen.
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
require_once __DIR__ . '/../../bootstrap.php';
|
|
|
|
v2_validate_method('GET');
|
|
$auth = v2_auth_require('teacher');
|
|
$teacherId = (int) ($auth['sub'] ?? 0);
|
|
$classId = (int) ($_GET['classId'] ?? 0);
|
|
if (!$classId) {
|
|
v2_response_error(400, 'missing_classId', 'Parameter "classId" fehlt.');
|
|
}
|
|
|
|
// Lehrer-Berechtigung für die Klasse prüfen
|
|
$ok = v2_db_one("
|
|
SELECT id FROM classes WHERE id = :cid AND teacher_id = :tid
|
|
", [':cid' => $classId, ':tid' => $teacherId]);
|
|
if (!$ok) {
|
|
v2_response_error(403, 'forbidden', 'Diese Klasse gehört nicht dieser Lehrperson.');
|
|
}
|
|
|
|
// SSE-Header
|
|
header('Content-Type: text/event-stream');
|
|
header('Cache-Control: no-cache');
|
|
header('X-Accel-Buffering: no');
|
|
ob_implicit_flush(true);
|
|
@ob_end_flush();
|
|
|
|
$lastId = 0;
|
|
$startTs = time();
|
|
|
|
// Initial dump: letzte 30 Events der Klasse, falls Lehrperson mitten in Stunde reinhängt
|
|
$initial = v2_db_all("
|
|
SELECT id, student_id, module_slug, event_type, phase, phase_label, step,
|
|
current_score, score_max, time_in_phase_s, milestone_id, milestone_label,
|
|
idle_s, hint, blocker_type, ts
|
|
FROM telemetry_events_v2
|
|
WHERE class_id = :cid AND ts > NOW() - INTERVAL 5 MINUTE
|
|
ORDER BY id ASC
|
|
LIMIT 30
|
|
", [':cid' => $classId]);
|
|
foreach ($initial as $row) {
|
|
echo "id: {$row['id']}\n";
|
|
echo "event: telemetry\n";
|
|
echo 'data: ' . json_encode($row, JSON_UNESCAPED_UNICODE) . "\n\n";
|
|
$lastId = max($lastId, (int) $row['id']);
|
|
}
|
|
|
|
// Loop — max 60s, dann Client-Reconnect
|
|
while (time() - $startTs < 60) {
|
|
if (connection_aborted()) break;
|
|
|
|
$rows = v2_db_all("
|
|
SELECT id, student_id, module_slug, event_type, phase, phase_label, step,
|
|
current_score, score_max, time_in_phase_s, milestone_id, milestone_label,
|
|
idle_s, hint, blocker_type, ts
|
|
FROM telemetry_events_v2
|
|
WHERE class_id = :cid AND id > :lid
|
|
ORDER BY id ASC
|
|
LIMIT 50
|
|
", [':cid' => $classId, ':lid' => $lastId]);
|
|
|
|
foreach ($rows as $row) {
|
|
echo "id: {$row['id']}\n";
|
|
echo "event: telemetry\n";
|
|
echo 'data: ' . json_encode($row, JSON_UNESCAPED_UNICODE) . "\n\n";
|
|
$lastId = (int) $row['id'];
|
|
}
|
|
|
|
// Heartbeat-Comment damit Verbindung nicht abreißt
|
|
echo ": ping\n\n";
|
|
@flush();
|
|
sleep(2);
|
|
}
|