diff --git a/App/php/api/live.php b/App/php/api/live.php
index ae5a023..3eabee6 100644
--- a/App/php/api/live.php
+++ b/App/php/api/live.php
@@ -54,9 +54,12 @@ if ($method === 'POST') {
VALUES (?, ?, ?, ?, NOW(), NOW())
ON DUPLICATE KEY UPDATE
class_id = VALUES(class_id),
+ -- started_at ZUERST prüfen: MySQL wertet die Zuweisungen links→rechts
+ -- aus, daher muss dieser Check VOR module_id stehen, sonst ist
+ -- module_id schon der neue Wert und der Modulwechsel wird nie erkannt.
+ started_at = IF(module_id <> VALUES(module_id) OR last_seen < NOW() - INTERVAL 30 SECOND, NOW(), started_at),
module_id = VALUES(module_id),
state_json = VALUES(state_json),
- started_at = IF(module_id <> VALUES(module_id) OR last_seen < NOW() - INTERVAL 30 SECOND, NOW(), started_at),
last_seen = NOW()',
[$studentId, $classId, $moduleId, $stateJson]
);
diff --git a/App/php/api/results.php b/App/php/api/results.php
index 88ca3d2..ccfa6dc 100644
--- a/App/php/api/results.php
+++ b/App/php/api/results.php
@@ -309,7 +309,27 @@ if ($view === 'today') {
];
}
- Response::ok(['date'=>$today, 'topToday'=>array_slice($todayRuns, 0, 6), 'progress'=>$progress]);
+ // Pro-Schüler-Aggregat der HEUTIGEN Läufe (aus student_results — zuverlässig
+ // mit student_id, unabhängig vom assessments/student_sessions-Join). Basis für
+ // die HEUTE-Empfehlung, damit ALLE heutigen Leistungen zählen (nicht nur die,
+ // die es in die assessments-Tabelle geschafft haben).
+ $todayScores = []; // sid => ['plays'=>n, 'avgScore'=>0-100, 'minScore'=>..]
+ foreach ($rows as $r) {
+ if (substr($r['completed_at'], 0, 10) !== $today) continue;
+ $sid = (int)$r['student_id'];
+ $sc = max(0, min(100, (float)$r['score']));
+ if (!isset($todayScores[$sid])) $todayScores[$sid] = ['plays'=>0, '_sum'=>0.0, 'minScore'=>100];
+ $todayScores[$sid]['plays']++;
+ $todayScores[$sid]['_sum'] += $sc;
+ if ($sc < $todayScores[$sid]['minScore']) $todayScores[$sid]['minScore'] = (int)round($sc);
+ }
+ foreach ($todayScores as $sid => &$t) {
+ $t['avgScore'] = (int)round($t['_sum'] / max(1, $t['plays']));
+ unset($t['_sum']);
+ }
+ unset($t);
+
+ Response::ok(['date'=>$today, 'topToday'=>array_slice($todayRuns, 0, 6), 'progress'=>$progress, 'todayScores'=>$todayScores]);
}
// =============================================================================
diff --git a/App/teacher.html b/App/teacher.html
index c263d71..ddd35d3 100644
--- a/App/teacher.html
+++ b/App/teacher.html
@@ -2827,7 +2827,7 @@ function _todayHelpReason(simId, st){
}
return 'stockt · braucht evtl. Hilfe';
}
-function ivBuildOverview(cockpit, weeks, studentsCount, matrix, todaySummary){
+function ivBuildOverview(cockpit, weeks, studentsCount, matrix, todaySummary, today){
ivInjectCss();
var cs = (cockpit && cockpit.students) || [];
var sc = (cockpit && cockpit.statusCounts) || {};
@@ -2895,15 +2895,30 @@ function ivBuildOverview(cockpit, weeks, studentsCount, matrix, todaySummary){
h+='
Simulationen'+simTotal+'
Durchläufe insgesamt ';
h+='';
// Triage
- // === HEUTE-Triage: gleiche Logik, aber nur auf die HEUTIGEN Läufe bezogen ===
- var todayErfById={}, todayPlaysById={};
+ // === HEUTE-Triage: ALLE heutigen Läufe. Quelle 1 = student_results (fertige
+ // Läufe, zuverlässig mit student_id); Quelle 2 = Summary NUR für laufende
+ // Sessions (die stehen noch nicht in student_results) → kein Doppelzählen. ===
+ var _tScore={}, _tPlays={}, _tMin={};
+ var _tsrc=(today&&today.todayScores)||{};
+ Object.keys(_tsrc).forEach(function(sid){
+ var t=_tsrc[sid]; _tPlays[sid]=(_tPlays[sid]||0)+(t.plays||0);
+ _tScore[sid]=(_tScore[sid]||0)+(t.avgScore||0)*(t.plays||0);
+ if(t.minScore!=null) _tMin[sid]=Math.min(_tMin[sid]==null?100:_tMin[sid], t.minScore);
+ });
((todaySummary&&todaySummary.students)||[]).forEach(function(su){
- var sess=(su.sessions||[]).filter(function(x){ return x.startMs; });
- if(!sess.length) return;
- var sco=[];
- sess.forEach(function(x){ var v=(x.score!=null)?x.score:(x.stars!=null?Math.round(x.stars/5*100):null); if(v!=null) sco.push(v); });
- todayPlaysById[su.studentId]=sess.length;
- todayErfById[su.studentId]= sco.length? Math.round(sco.reduce(function(a,b){return a+b;},0)/sco.length) : null;
+ (su.sessions||[]).forEach(function(x){
+ if(!x.startMs || !(x.isLive||x.success==='running')) return; // nur laufende
+ var v=(x.score!=null)?x.score:(x.stars!=null?Math.round(x.stars/5*100):null); if(v==null) return;
+ var sid=su.studentId;
+ _tPlays[sid]=(_tPlays[sid]||0)+1; _tScore[sid]=(_tScore[sid]||0)+v;
+ _tMin[sid]=Math.min(_tMin[sid]==null?100:_tMin[sid], v);
+ });
+ });
+ var todayErfById={}, todayPlaysById={}, todayMinById={};
+ Object.keys(_tPlays).forEach(function(sid){
+ todayPlaysById[sid]=_tPlays[sid];
+ todayErfById[sid]=Math.round(_tScore[sid]/Math.max(1,_tPlays[sid]));
+ todayMinById[sid]=_tMin[sid];
});
var heuteGroups={support:[],more:[],work:[],good:[],calm:[],idle:[]};
cs.forEach(function(s){
@@ -2912,7 +2927,10 @@ function ivBuildOverview(cockpit, weeks, studentsCount, matrix, todaySummary){
var tErf=todayErfById[sid];
var w2=wById[sid], secs2=w2?w2.buckets.map(function(b){return Math.min(IV_WEEK_CAP, b.sum||0);}):[];
while(secs2.length<10) secs2.unshift(0);
- var g2 = !played ? 'idle' : ivTriage(tErf, 0, todayPlaysById[sid], false);
+ // Totalversagen heute (ein Lauf < 40) → immer Fördern, egal wie der Schnitt ist.
+ var g2 = !played ? 'idle'
+ : (todayMinById[sid]!=null && todayMinById[sid] < 40) ? 'support'
+ : ivTriage(tErf, 0, todayPlaysById[sid], false);
var row2={sid:sid, name:s.displayName, erf:played?tErf:null, status:s.status, secs:secs2, days:played?0:(s.daysSince!=null?s.daysSince:null)};
if(g2) heuteGroups[g2].push(row2); else heuteGroups.calm.push(row2);
});
@@ -3302,7 +3320,7 @@ async function loadOverview() {
var html = '';
// === NEU: Interventions-Übersicht (Erfolg · Aktiv · Verlauf + Triage) ===
- html += ivBuildOverview(cockpit, weeks, students.length, matrix, todaySum);
+ html += ivBuildOverview(cockpit, weeks, students.length, matrix, todaySum, today);
// === Sektion B: Aktive Klassen-Simulationen ===
var active = (assignments || []).filter(function(a){return a.active;});