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
3.1 KiB
PHP
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]);
|