From 64a9a62157c94c708ef981bab67ac57503eb9e27 Mon Sep 17 00:00:00 2001 From: Thomas Date: Sat, 22 Aug 2026 15:02:19 +0200 Subject: [PATCH] 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 --- App/php/api/results.php | 43 +++++++++++++++++++++++++++++++++++++++++ App/teacher.html | 36 ++++++---------------------------- 2 files changed, 49 insertions(+), 30 deletions(-) diff --git a/App/php/api/results.php b/App/php/api/results.php index 6b81987..88ca3d2 100644 --- a/App/php/api/results.php +++ b/App/php/api/results.php @@ -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 0–23 + case 'week': $nowBucketIdx = (int)date('N') - 1; break; // Mo=0 … So=6 + case 'month': $nowBucketIdx = (int)date('j') - 1; break; // Tag 1–31 + case 'year': $nowBucketIdx = (int)date('n') - 1; break; // Monat 1–12 + 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) { diff --git a/App/teacher.html b/App/teacher.html index 2c2cd4e..c27f948 100644 --- a/App/teacher.html +++ b/App/teacher.html @@ -3758,13 +3758,11 @@ async function loadResults() { host.innerHTML = '

Lade Ergebnisse…

'; // 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 = '

'+esc(matrix.error)+'

'; return; } if (activity.error) { host.innerHTML = '

'+esc(activity.error)+'

'; return; } @@ -3779,41 +3777,16 @@ async function loadResults() { function renderResults() { var host = document.getElementById(_resultsHost) || document.getElementById('tab-results'); host.innerHTML = '' - + '
' + '
' + '
' + '
' + '
'; - 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 = '
' - + '
Jetzt live · '+live.length+'
' - + '
'; - 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) - ? '' - : ''+(ls.emoji || '🧑‍🎓')+''; - html += ''; - }); - html += '
'; - host.innerHTML = html; -} - // ---------- Sektion 0 (NEU oben): Fleiß pro Schüler:in ---------- // Layout: Matrix mit Zeilen=Schüler:innen, Spalten=Zeitbuckets (Mo–So // 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 += '
'; }); - html += '
'; - html += '
' + stackHtml + '
'; + // 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 += '
'; + html += '
' + stackHtml + '
'; html += '
'; });