Live: weltkueche-Spalten kuratiert; pausierte Sessions bleiben sichtbar

- LIVE_PRIMARY_FIELDS.weltkueche: Gericht/Herkunft/Zutaten/Gerichte/Fehlklicks
  (statt Rohdaten-Dump aller State-Keys) — 5 Spalten, kein Quer-Scroll
- Idle/Pause: live.php Fenster 30s->2h (LIVE_IDLE_WINDOW_SEC); Aktuell zeigt
  laenger inaktive Sessions als "pausiert" (⏸, grau) statt sie zu verlieren;
  braucht-Hilfe + aktiv-Zaehler nur fuer wirklich aktive Sessions

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-22 00:43:40 +02:00
parent 07010f2302
commit ebdd1ce438
2 changed files with 50 additions and 11 deletions
+5 -1
View File
@@ -19,6 +19,10 @@ $method = $_SERVER['REQUEST_METHOD'];
$db = Database::get();
const LIVE_ACTIVE_WINDOW_SEC = 30;
// Pausierte Sessions (Tab weg / lange idle) bleiben so lange in der Live-Liste
// sichtbar — der/die Lernende kann später weitermachen, die Lehrkraft sieht die
// Session als „pausiert" statt sie fälschlich als beendet zu verlieren.
const LIVE_IDLE_WINDOW_SEC = 7200; // 2 h
if ($method === 'POST') {
$sessionId = Session::requireStudent();
@@ -862,7 +866,7 @@ if ($method === 'GET') {
WHERE ls.class_id = ?
AND ls.last_seen > NOW() - INTERVAL ? SECOND
ORDER BY ls.last_seen DESC',
[$classId, LIVE_ACTIVE_WINDOW_SEC]
[$classId, LIVE_IDLE_WINDOW_SEC]
);
$list = array_map(function($r) {
return [
+45 -10
View File
@@ -716,6 +716,30 @@ var LIVE_PRIMARY_FIELDS = {
return v + ' <span style="color:#8a8a8a">/ ' + (s.achievementsTotal||6) + '</span>';
}},
],
'weltkueche': [
{k:'dishName', label:'Gericht', align:'left', fmt:function(v){
return v ? '<b>'+esc(v)+'</b>' : '<span style="color:#8a8a8a">Auswahl</span>';
}},
{k:'dishOrigin', label:'Herkunft', align:'left', fmt:function(v){
return v ? esc(v) : '<span style="color:#8a8a8a"></span>';
}},
{k:'ingSolved', label:'Zutaten', fmt:function(v, row){
var s = row.state||{}; var t = s.ingTotal||0;
if (!s.dishName || !t) return '<span style="color:#8a8a8a"></span>';
var done = v||0; var pct = done/t; var c = pct>=1 ? '#5a8a5e' : pct>=.5 ? '#c9913a' : '#8a7a4a';
return '<span style="color:'+c+';font-weight:700">'+done+' <span style="color:#8a8a8a;font-weight:400">/ '+t+'</span></span>';
}},
{k:'completed', label:'Gerichte', fmt:function(v, row){
var s = row.state||{}; var t = s.completedTotal||0;
if (!t) return '—';
var done = v||0; var pct = done/t; var c = pct>=1 ? '#5a8a5e' : pct>=.5 ? '#c9913a' : '#8a8a8a';
return '<span style="color:'+c+';font-weight:700">'+done+' <span style="color:#8a8a8a;font-weight:400">/ '+t+'</span></span>';
}},
{k:'mistakesInDish', label:'Fehlklicks', align:'center', fmt:function(v){
var n = v||0; var c = n>=12 ? '#c07a6b' : n>=5 ? '#c9913a' : '#5a8a5e';
return '<span style="color:'+c+';font-weight:700">'+n+'</span>';
}},
],
'busfahrt': [
{k:'levelId', label:'Stufe', align:'center'},
{k:'orderIndex', label:'Auftrag', fmt:function(v, row){ var s = row.state||{}; return s.totalOrders ? (v+1)+' / '+s.totalOrders : '—'; }},
@@ -1136,21 +1160,27 @@ function _liveNeedsHelp(simId, s) {
return false;
}
var _liveReflCache = '', _liveReflAt = 0;
// Aktiv = Lebenszeichen in den letzten 90 s. Ältere Sessions bleiben sichtbar,
// aber als „pausiert" (Lernende:r macht Pause, kann später weitermachen).
var _LIVE_PAUSE_SEC = 90;
function _liveIsActive(ls) {
return ls.last_seen_ms && (Date.now() - ls.last_seen_ms) <= _LIVE_PAUSE_SEC * 1000;
}
async function renderLive() {
var sessions = await api('live?class_id=' + currentClassId);
if (sessions.error) { document.getElementById('tab-live').innerHTML = '<p style="color:#c07a6b">'+esc(sessions.error)+'</p>'; return; }
var host = document.getElementById('tab-live');
var html = _liveToggleHtml() + '<div class="card"><div class="card-h">Live · Klassen-Vergleich</div>';
html += '<p style="font-size:.7rem;color:#8a8a8a;margin-bottom:.6rem">Aktualisiert sich automatisch alle 4 Sekunden. Aktiv = letztes Lebenszeichen ≤ 30 s. Klick auf eine Zeile öffnet die Detailansicht.</p>';
html += '<p style="font-size:.7rem;color:#8a8a8a;margin-bottom:.6rem">Aktualisiert sich alle 4 Sekunden. Aktiv = Lebenszeichen ≤ 90 s · pausierte Sessions (⏸) bleiben sichtbar, bis der/die Lernende weitermacht. Klick auf eine Zeile öffnet die Detailansicht.</p>';
if (!sessions.length) {
html += '<p style="font-size:.78rem;color:#8a8a8a">Gerade niemand online — sobald eine Schülerin oder ein Schüler eine Simulation startet, taucht sie hier auf.</p>';
host.innerHTML = html + '</div>';
return;
}
// „Braucht Hilfe"-Banner (live)
var helpList = sessions.filter(function(ls){ return _liveNeedsHelp(ls.module_id, ls.state); });
// „Braucht Hilfe"-Banner (live) — nur aktuell aktive Sessions
var helpList = sessions.filter(function(ls){ return _liveIsActive(ls) && _liveNeedsHelp(ls.module_id, ls.state); });
if (helpList.length) {
html += '<div style="background:#fdf3ee;border:1px solid #e6c3b4;border-left:4px solid #c0574a;border-radius:8px;padding:.5rem .8rem;margin-bottom:.7rem;font-size:.76rem;color:#8a3a2a">'
+ '🆘 <b>'+helpList.length+' brauchen evtl. Hilfe</b> — '
@@ -1184,7 +1214,8 @@ async function renderLive() {
html += '<div style="display:flex;align-items:center;gap:.4rem;margin:.5rem 0 .35rem;font-size:.85rem;font-weight:700;color:#1f4b37">'
+ '<span style="font-size:1.1rem">'+(meta.moduleIcon||'📚')+'</span>'
+ '<span>'+esc(meta.moduleName||modId)+'</span>'
+ '<span style="font-size:.62rem;font-weight:500;color:#8a8a8a">· '+grp.rows.length+' aktiv</span>'
+ (function(){ var a=grp.rows.filter(_liveIsActive).length, p=grp.rows.length-a;
return '<span style="font-size:.62rem;font-weight:500;color:#8a8a8a">· '+a+' aktiv'+(p?' · '+p+' pausiert':'')+'</span>'; })()
+ '</div>';
if (!anyState) {
@@ -1221,20 +1252,24 @@ async function renderLive() {
var simStr = sinceSimBase ? _fmtSince(sinceSim) : '—';
var missionStr = sinceMissionBase ? _fmtSince(sinceMission) : '—';
var lastSeen = ls.last_seen_ms ? Math.max(0, Math.round((Date.now() - ls.last_seen_ms)/1000)) : 0;
var paused = !_liveIsActive(ls);
var dotColor = lastSeen < 8 ? '#5a8a5e' : lastSeen < 20 ? '#e8833a' : '#c07a6b';
var s = ls.state || {};
var help = _liveNeedsHelp(ls.module_id, s);
var restBg = help ? '#fdf3ee' : '';
var hoverBg = help ? '#f9e7e0' : '#f7f6f3';
var help = !paused && _liveNeedsHelp(ls.module_id, s);
var restBg = help ? '#fdf3ee' : (paused ? '#f6f5f2' : '');
var hoverBg = help ? '#f9e7e0' : '#f0efe9';
var accent = help ? 'inset 3px 0 0 #c0574a' : (paused ? 'inset 3px 0 0 #cfcabb' : 'none');
html += '<tr style="border-bottom:1px solid rgba(0,0,0,.04);cursor:pointer;background:'+restBg+';box-shadow:'+(help?'inset 3px 0 0 #c0574a':'none')+'" onclick="openLiveSpectator('+ls.student_id+',\''+ls.module_id+'\')" onmouseover="this.style.background=\''+hoverBg+'\'" onmouseout="this.style.background=\''+restBg+'\'">';
html += '<td style="padding:.4rem .5rem;white-space:nowrap"><span style="display:inline-flex;align-items:center;gap:.4rem">'+(help?'<span title="stockt · braucht evtl. Hilfe">🆘</span>':'')+avatarHtml+'<strong>'+esc(ls.displayName)+'</strong></span></td>';
html += '<tr style="border-bottom:1px solid rgba(0,0,0,.04);cursor:pointer;background:'+restBg+';'+(paused?'color:#9a9a9a;':'')+'box-shadow:'+accent+'" onclick="openLiveSpectator('+ls.student_id+',\''+ls.module_id+'\')" onmouseover="this.style.background=\''+hoverBg+'\'" onmouseout="this.style.background=\''+restBg+'\'">';
html += '<td style="padding:.4rem .5rem;white-space:nowrap"><span style="display:inline-flex;align-items:center;gap:.4rem">'+(help?'<span title="stockt · braucht evtl. Hilfe">🆘</span>':(paused?'<span title="pausiert — kann später weitermachen"></span>':''))+avatarHtml+'<strong>'+esc(ls.displayName)+'</strong></span></td>';
html += '<td style="padding:.4rem .5rem;white-space:nowrap;line-height:1.15" title="Aktuelle Mission/Lektion · Gesamte Sim-Session"><div style="font-size:.78rem;color:#3a3a3a;font-weight:600">'+missionStr+'</div><div style="font-size:.62rem;color:#8a8a8a">'+simStr+' ges.</div></td>';
fields.forEach(function(f) {
var align = f.align || 'right';
html += '<td style="text-align:'+align+';padding:.4rem .5rem;font-variant-numeric:tabular-nums">'+_liveFmt(s[f.k], f.unit, f, ls)+'</td>';
});
html += '<td style="text-align:center"><span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:'+dotColor+'" title="Letztes Lebenszeichen vor '+lastSeen+' s"></span></td>';
html += '<td style="text-align:center">'+(paused
? '<span style="font-size:.6rem;color:#9a9a9a;white-space:nowrap" title="seit '+_fmtSince(lastSeen)+' keine Aktivität — Session bleibt offen">⏸ '+_fmtSince(lastSeen)+'</span>'
: '<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:'+dotColor+'" title="Letztes Lebenszeichen vor '+lastSeen+' s"></span>')+'</td>';
html += '</tr>';
});
html += '</tbody></table></div></div>';