Gesamt/Fleiss: laufende Sessions als blinkende Saeule im aktuellen Bucket

Statt separatem Top-Streifen (rueckgebaut): results.php view=fleiss spielt
laufende live_sessions in den aktuellen Bucket (Stunde/Wochentag/Tag/Monat) ein,
markiert ihn `live`. renderFleiss laesst diese Saeule blinken (roter Rahmen).
So sieht man Bens laufendes Spiel direkt bei "Fleiss pro Schueler*in".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-22 15:02:19 +02:00
parent ad8c6b7d7e
commit 64a9a62157
2 changed files with 49 additions and 30 deletions
+43
View File
@@ -369,6 +369,16 @@ if ($view === 'fleiss') {
default: Response::error('range muss day|week|weeks|month|year sein');
}
// Aktueller Bucket-Index (für LAUFENDE, noch nicht abgegebene Sessions).
$nowBucketIdx = -1;
switch ($range) {
case 'day': $nowBucketIdx = (int)date('G'); break; // Stunde 023
case 'week': $nowBucketIdx = (int)date('N') - 1; break; // Mo=0 … So=6
case 'month': $nowBucketIdx = (int)date('j') - 1; break; // Tag 131
case 'year': $nowBucketIdx = (int)date('n') - 1; break; // Monat 112
case 'weeks': $nowBucketIdx = isset($bmap) ? ($bmap[(int)date('oW')] ?? -1) : -1; break;
}
// Optional Modul-Filter
$params = [$classId];
$modSql = '';
@@ -463,6 +473,39 @@ if ($view === 'fleiss') {
$moduleAgg[$sim]['totalSec'] += $sec;
}
// 5b) LAUFENDE Sessions (noch kein Assessment abgegeben) in den aktuellen
// Bucket einspielen — als „live" markiert, damit die Säule dort blinkt.
if ($nowBucketIdx >= 0) {
$liveRows = $db->fetchAll(
"SELECT ls.student_id, ls.module_id,
UNIX_TIMESTAMP(ls.started_at) AS started_ts,
UNIX_TIMESTAMP(ls.last_seen) AS last_seen_ts
FROM live_sessions ls
WHERE ls.class_id = ? AND ls.last_seen > NOW() - INTERVAL 90 SECOND",
[$classId]
);
foreach ($liveRows as $lr) {
$sid = (int)$lr['student_id'];
if (!isset($byStudent[$sid])) continue;
$sim = $lr['module_id'];
if ($moduleId !== 'ALL' && $moduleId !== '' && $sim !== $moduleId) continue;
$sec = max(0, (int)$lr['last_seen_ts'] - (int)$lr['started_ts']);
$b = &$byStudent[$sid]['buckets'][$nowBucketIdx];
$b['sum'] += $sec;
$byMod = (array)$b['byModule'];
$byMod[$sim] = ($byMod[$sim] ?? 0) + $sec;
$b['byModule'] = $byMod;
$b['live'] = true; // Client lässt diese Säule blinken
$b['liveModule'] = $sim;
unset($b);
$byStudent[$sid]['totalSec'] += $sec;
if (!isset($moduleAgg[$sim])) {
$info = $moduleMap[$sim] ?? null;
$moduleAgg[$sim] = ['id'=>$sim,'name'=>$info?$info['name']:$sim,'icon'=>$info?$info['icon']:'📘','color'=>$info?$info['color']:'#888','totalSec'=>0];
}
}
}
// 6) Global-Max-Bucket-Sec für klassenweite Säulen-Skalierung
foreach ($byStudent as $sid => &$st) {
foreach ($st['buckets'] as $b) {
+6 -30
View File
@@ -3758,13 +3758,11 @@ async function loadResults() {
host.innerHTML = '<p style="font-size:.78rem;color:#8a8a8a;padding:1rem">Lade Ergebnisse…</p>';
// Drei parallele API-Calls: Fleiß (NEU oben) + Matrix + Activity (alt)
var [fleiss, matrix, activity, live] = await Promise.all([
var [fleiss, matrix, activity] = await Promise.all([
api('results?class_id=' + currentClassId + '&view=fleiss&range=' + _resultsState.fleissRange + '&module_id=' + encodeURIComponent(_resultsState.fleissModule)),
api('results?class_id=' + currentClassId + '&view=matrix'),
api('results?class_id=' + currentClassId + '&view=activity&range=' + _resultsState.range + '&groupBy=' + (_resultsState.groupBy || 'module')),
api('live?class_id=' + currentClassId),
]);
_resultsState.live = Array.isArray(live) ? live : [];
if (matrix.error) { host.innerHTML = '<p style="color:#c07a6b">'+esc(matrix.error)+'</p>'; return; }
if (activity.error) { host.innerHTML = '<p style="color:#c07a6b">'+esc(activity.error)+'</p>'; return; }
@@ -3779,41 +3777,16 @@ async function loadResults() {
function renderResults() {
var host = document.getElementById(_resultsHost) || document.getElementById('tab-results');
host.innerHTML = ''
+ '<div id="res-live"></div>'
+ '<div id="res-fleiss"></div>'
+ '<div id="res-matrix"></div>'
+ '<div id="res-activity"></div>'
+ '<div id="res-modules"></div>';
renderResultsLive();
renderFleiss();
renderMatrix();
renderActivity();
renderModuleCards();
}
// „Jetzt live"-Streifen ganz oben in Gesamt: laufende Spiele (aus live_sessions),
// als langsam blinkende Blöcke — sonst fehlen sie hier (Fleiß baut nur auf fertigen Läufen).
function renderResultsLive() {
var host = document.getElementById('res-live'); if (!host) return;
var live = (_resultsState.live || []).filter(function(ls){ return ls.last_seen_ms && (Date.now() - ls.last_seen_ms) < 90000; });
if (!live.length) { host.innerHTML = ''; return; }
var html = '<div class="card" style="border-color:#e0b3aa">'
+ '<div class="card-h" style="color:#b03a2a"><span style="display:inline-block;width:9px;height:9px;border-radius:50%;background:#c0392b;margin-right:.45rem;animation:_tagblink 1.4s ease-in-out infinite"></span>Jetzt live · '+live.length+'</div>'
+ '<div style="display:flex;flex-wrap:wrap;gap:.5rem">';
live.forEach(function(ls){
var el = ls.started_at_ms ? Math.max(0, Math.round((Date.now() - ls.started_at_ms)/60000)) : 0;
var av = (typeof ls.emoji === 'string' && ls.emoji.indexOf('avatar:') === 0)
? '<img src="assets/img/avatars/thumbs/avatar-'+ls.emoji.substring(7)+'.webp" alt="" style="width:1.4em;height:1.4em;border-radius:50%;object-fit:cover">'
: '<span style="font-size:1.05rem">'+(ls.emoji || '🧑‍🎓')+'</span>';
html += '<button onclick="openLiveSpectator('+ls.student_id+',\''+ls.module_id+'\')" title="Live zuschauen" '
+ 'style="animation:_tagblink 1.9s ease-in-out infinite;cursor:pointer;background:#fdeae4;border:1px solid #eabfb4;border-radius:10px;padding:.4rem .7rem;display:inline-flex;align-items:center;gap:.4rem;font-size:.76rem">'
+ av + '<b>'+esc(ls.displayName)+'</b>'
+ '<span style="color:#8a5a52">'+esc((ls.moduleIcon||'📚')+' '+(ls.moduleName||ls.module_id))+' · '+el+' min</span></button>';
});
html += '</div></div>';
host.innerHTML = html;
}
// ---------- Sektion 0 (NEU oben): Fleiß pro Schüler:in ----------
// Layout: Matrix mit Zeilen=Schüler:innen, Spalten=Zeitbuckets (MoSo
// bei Woche, 24 Stunden bei Tag etc.). Pro Zelle eine vertikale Säule;
@@ -3971,8 +3944,11 @@ function renderFleiss() {
var col = modColor[modId] || '#888';
stackHtml += '<div style="background:'+col+';height:'+segPct.toFixed(1)+'%;min-height:1px"></div>';
});
html += '<div style="height:'+maxBarHeight+'px;display:flex;align-items:flex-end;justify-content:center" title="'+title+'">';
html += '<div style="width:65%;height:'+hPx+'px;display:flex;flex-direction:column-reverse;border-radius:2px;overflow:hidden;box-shadow:0 0 0 1px rgba(0,0,0,.05)">' + stackHtml + '</div>';
// Laufende Session in diesem Bucket → Säule blinkt + roter Rahmen.
var liveB = b.live ? 'animation:_tagblink 1.7s ease-in-out infinite;box-shadow:0 0 0 1.5px #c0392b;' : 'box-shadow:0 0 0 1px rgba(0,0,0,.05);';
var cellTitle = b.live ? (title + '\n▶ läuft gerade') : title;
html += '<div style="height:'+maxBarHeight+'px;display:flex;align-items:flex-end;justify-content:center" title="'+cellTitle+'">';
html += '<div style="width:65%;height:'+hPx+'px;display:flex;flex-direction:column-reverse;border-radius:2px;overflow:hidden;'+liveB+'">' + stackHtml + '</div>';
html += '</div>';
});