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