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