diff --git a/App/php/api/results.php b/App/php/api/results.php
index 074a176..9f20b65 100644
--- a/App/php/api/results.php
+++ b/App/php/api/results.php
@@ -246,6 +246,72 @@ if ($view === 'matrix') {
]);
}
+// =============================================================================
+// VIEW: TODAY → Top-Leistungen von HEUTE + heute neu vergebene Abzeichen
+// =============================================================================
+// Liefert die heutigen finalen Läufe (topToday) sowie pro Schüler:in×Sim, die
+// heute aktiv war, den Progress-Stand VOR heute und JETZT (aus student_results
+// rekonstruiert). Die Badge-Auswertung passiert clientseitig mit badges.js,
+// damit die Abzeichen-Definition eine einzige Quelle bleibt.
+if ($view === 'today') {
+ $rows = $db->fetchAll(
+ "SELECT sr.student_id, sr.module_id, sr.score, sr.detail_json, sr.completed_at,
+ s.display_name, s.username, s.emoji_avatar
+ FROM student_results sr
+ JOIN students s ON s.id = sr.student_id
+ WHERE sr.class_id = ? AND s.deleted_at IS NULL
+ ORDER BY sr.completed_at",
+ [$classId]
+ );
+ $starsOf = function ($score, $dj) {
+ $d = json_decode($dj, true);
+ if (is_array($d)) {
+ if (isset($d['stars'])) return (int)$d['stars'];
+ if (isset($d['sterneGesamt'])) return (int)$d['sterneGesamt'];
+ }
+ return (int)round(((float)$score) / 20);
+ };
+ $levelFromXp = function ($xp) { return $xp < 50 ? 1 : ($xp < 150 ? 2 : ($xp < 300 ? 3 : 4)); };
+ $today = date('Y-m-d');
+
+ $agg = []; // "sid|sim" => Aggregat vorher/jetzt
+ $todayRuns = [];
+ foreach ($rows as $r) {
+ $sid = (int)$r['student_id'];
+ $sim = $r['module_id'];
+ $st = $starsOf($r['score'], $r['detail_json']);
+ $xp = $st * 10 + ($st >= 5 ? 5 : 0);
+ $isToday = substr($r['completed_at'], 0, 10) === $today;
+ $k = "$sid|$sim";
+ if (!isset($agg[$k])) $agg[$k] = [
+ 'sid'=>$sid, 'sim'=>$sim, 'name'=>$r['display_name'] ?: $r['username'], 'emoji'=>$r['emoji_avatar'],
+ 'pB'=>0,'bB'=>0,'xB'=>0, 'pN'=>0,'bN'=>0,'xN'=>0, 'today'=>false,
+ ];
+ $agg[$k]['pN']++; $agg[$k]['bN'] = max($agg[$k]['bN'], $st); $agg[$k]['xN'] += $xp;
+ if ($isToday) {
+ $agg[$k]['today'] = true;
+ $todayRuns[] = ['studentId'=>$sid, 'displayName'=>$r['display_name'] ?: $r['username'],
+ 'emoji'=>$r['emoji_avatar'] ?: '🧑🎓', 'moduleId'=>$sim,
+ 'score'=>round((float)$r['score']), 'stars'=>$st, 'at'=>$r['completed_at']];
+ } else {
+ $agg[$k]['pB']++; $agg[$k]['bB'] = max($agg[$k]['bB'], $st); $agg[$k]['xB'] += $xp;
+ }
+ }
+ usort($todayRuns, function ($a, $b) { return $b['score'] <=> $a['score']; });
+
+ $progress = [];
+ foreach ($agg as $a) {
+ if (!$a['today']) continue;
+ $progress[] = [
+ 'studentId'=>$a['sid'], 'displayName'=>$a['name'], 'emoji'=>$a['emoji'] ?: '🧑🎓', 'simId'=>$a['sim'],
+ 'before'=>['plays'=>$a['pB'], 'best_stars'=>$a['bB'], 'level'=>$levelFromXp($a['xB'])],
+ 'now' =>['plays'=>$a['pN'], 'best_stars'=>$a['bN'], 'level'=>$levelFromXp($a['xN'])],
+ ];
+ }
+
+ Response::ok(['date'=>$today, 'topToday'=>array_slice($todayRuns, 0, 6), 'progress'=>$progress]);
+}
+
// =============================================================================
// VIEW: FLEISS (Lernzeit pro Schüler*in, optional Modul-Filter)
// =============================================================================
diff --git a/App/teacher.html b/App/teacher.html
index c8deb26..6ef5a56 100644
--- a/App/teacher.html
+++ b/App/teacher.html
@@ -2367,17 +2367,82 @@ async function ivOpenStudent(sid){
// === ÜBERSICHT ===
// Lehrer-Übersicht: Stat-Bar + aktive Aufgaben + Top-Performer + Aktivitäts-Sparkline + Modul-Cards.
// Drei parallele API-Calls (matrix + activity-week + assignments) — alle existing.
+// Avatar-Bild oder Emoji, gegebene Pixelgröße
+function ivTodayAvatar(emoji, px) {
+ if (typeof emoji === 'string' && emoji.indexOf('avatar:') === 0)
+ return '
';
+ return ''+(emoji||'🧑🎓')+'';
+}
+// Sektion: Top Performers heute + heute neu vergebene Abzeichen
+function ivBuildTodaySection(today, modById) {
+ var tt = (today && today.topToday) || [];
+ var prog = (today && today.progress) || [];
+ var starsHtml = (typeof ggsStarsHtml === 'function') ? ggsStarsHtml : function(n){ return '★'.repeat(Math.max(0,Math.min(5,n))); };
+
+ // Heute neu erreichte Abzeichen: Bedingung jetzt erfüllt, vor heute nicht.
+ var newBadges = [];
+ if (typeof ggsSimBadges === 'function') {
+ prog.forEach(function(p) {
+ ggsSimBadges(p.simId).forEach(function(bd) {
+ var okNow = false, okBefore = false;
+ try { okNow = bd.cond(p.now); okBefore = bd.cond(p.before); } catch (e) {}
+ if (okNow && !okBefore) newBadges.push({ p: p, b: bd });
+ });
+ });
+ }
+
+ var html = '
Heute wurden noch keine Durchgänge abgeschlossen.
'; + } else { + var medals = ['🥇','🥈','🥉']; + html += 'Lade…
'; - var [matrix, activity, assignments, students, cockpit, weeks] = await Promise.all([ + var [matrix, activity, assignments, students, cockpit, weeks, today] = await Promise.all([ api('results?class_id=' + currentClassId + '&view=matrix'), api('results?class_id=' + currentClassId + '&view=activity&range=week'), api('assignments?class_id=' + currentClassId), api('students?class_id=' + currentClassId), api('live?class_id=' + currentClassId + '&cockpit=1'), api('results?class_id=' + currentClassId + '&view=fleiss&range=weeks'), + api('results?class_id=' + currentClassId + '&view=today'), ]); if (matrix.error) { host.innerHTML = ''+esc(matrix.error)+'
'; return; } if (activity.error) activity = {series:[], buckets:[]}; @@ -2385,6 +2450,7 @@ async function loadOverview() { if (students.error) students = []; if (!cockpit || cockpit.error) cockpit = {students:[], statusCounts:{}}; if (!weeks || weeks.error) weeks = {students:[], globalMaxBucketSec:0}; + if (!today || today.error) today = {topToday:[], progress:[]}; // === Top 3 Performer aus Matrix-Cells ableiten === var topRuns = []; @@ -2440,29 +2506,8 @@ async function loadOverview() { html += ''; } - // === Sektion C: Top-Performer (3 Karten) === - if (top.length > 0) { - html += '