Files
Adminator 1e51ef7def Nachtrag: alle bisher untracked Ordner + hängende Änderungen mit-committen
- 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>
2026-07-08 02:27:02 +02:00

85 lines
3.1 KiB
PHP

<?php
/**
* POST /api/telemetry
* Akzeptiert Heartbeat / Milestone / Stuck / Accessibility-Blocker.
*/
declare(strict_types=1);
require_once __DIR__ . '/../../bootstrap.php';
v2_validate_method('POST');
$auth = v2_auth_require('student');
$studentId = (int) ($auth['sub'] ?? 0);
$classId = (int) ($auth['class_id'] ?? 0);
$body = v2_request_body_json_required();
v2_validate_required($body, ['type', 'moduleSlug', 'moduleVersion']);
$type = (string) $body['type'];
v2_validate_enum($type, ['heartbeat', 'milestone', 'stuck', 'accessibility-blocker'], 'type');
// Payload-Größe-Check (Spec: max 4 KB pro Event)
if (strlen(json_encode($body)) > 4096) {
v2_response_error(413, 'payload_too_large', 'Event darf max 4 KB sein.');
}
// Rate-Limit: max 1 Event/Sekunde pro Schüler*in
$lastTs = v2_db_one("
SELECT UNIX_TIMESTAMP(MAX(ts)) AS last_unix
FROM telemetry_events_v2
WHERE student_id = :sid
", [':sid' => $studentId]);
if ($lastTs && $lastTs['last_unix'] && (time() - (int) $lastTs['last_unix']) < 1) {
v2_response_error(429, 'rate_limited', 'Max 1 Event pro Sekunde.');
}
// Pro Sitzung max 2000 Events — dann Heartbeats droppen, Milestones noch akzeptieren
$count = v2_db_one("
SELECT COUNT(*) AS c
FROM telemetry_events_v2
WHERE student_id = :sid
AND module_slug = :slug
AND ts > NOW() - INTERVAL 8 HOUR
", [':sid' => $studentId, ':slug' => $body['moduleSlug']]);
if ($count && (int) $count['c'] > 2000 && $type === 'heartbeat') {
v2_response_ok(['ok' => true, 'dropped' => 'rate-cap-heartbeat']);
}
v2_db_exec("
INSERT INTO telemetry_events_v2
(student_id, class_id, module_slug, module_version, event_type,
phase, phase_label, step, current_score, score_max,
time_in_phase_s, total_time_s,
milestone_id, milestone_label, score_delta,
idle_s, last_action_label, hint, blocker_type, ts)
VALUES
(:sid, :cid, :slug, :mv, :et,
:ph, :phl, :step, :cs, :csmax,
:tip, :tt,
:mid, :ml, :sd,
:idle, :lal, :hint, :bt, :ts)
", [
':sid' => $studentId,
':cid' => $classId,
':slug' => $body['moduleSlug'],
':mv' => $body['moduleVersion'],
':et' => $type,
':ph' => $body['phase'] ?? null,
':phl' => $body['phaseLabel'] ?? null,
':step' => $body['step'] ?? null,
':cs' => isset($body['currentScore']) ? (int) $body['currentScore'] : null,
':csmax' => isset($body['scoreMax']) ? (int) $body['scoreMax'] : null,
':tip' => isset($body['timeInPhaseS']) ? (int) $body['timeInPhaseS'] : null,
':tt' => isset($body['totalTimeS']) ? (int) $body['totalTimeS'] : null,
':mid' => $body['milestoneId'] ?? null,
':ml' => $body['milestoneLabel'] ?? null,
':sd' => isset($body['scoreDelta']) ? (int) $body['scoreDelta'] : null,
':idle' => isset($body['idleS']) ? (int) $body['idleS'] : null,
':lal' => $body['lastActionLabel'] ?? null,
':hint' => $body['hint'] ?? null,
':bt' => $body['blockerType'] ?? null,
':ts' => date('Y-m-d H:i:s', strtotime($body['ts'] ?? 'now') ?: time()),
]);
v2_response_ok(['ok' => true]);