diff --git a/App/php/api/live.php b/App/php/api/live.php index de5633b..e31c405 100644 --- a/App/php/api/live.php +++ b/App/php/api/live.php @@ -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 [ diff --git a/App/teacher.html b/App/teacher.html index a8355a4..4e6497f 100644 --- a/App/teacher.html +++ b/App/teacher.html @@ -716,6 +716,30 @@ var LIVE_PRIMARY_FIELDS = { return v + ' / ' + (s.achievementsTotal||6) + ''; }}, ], + 'weltkueche': [ + {k:'dishName', label:'Gericht', align:'left', fmt:function(v){ + return v ? ''+esc(v)+'' : 'Auswahl'; + }}, + {k:'dishOrigin', label:'Herkunft', align:'left', fmt:function(v){ + return v ? esc(v) : ''; + }}, + {k:'ingSolved', label:'Zutaten', fmt:function(v, row){ + var s = row.state||{}; var t = s.ingTotal||0; + if (!s.dishName || !t) return ''; + var done = v||0; var pct = done/t; var c = pct>=1 ? '#5a8a5e' : pct>=.5 ? '#c9913a' : '#8a7a4a'; + return ''+done+' / '+t+''; + }}, + {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 ''+done+' / '+t+''; + }}, + {k:'mistakesInDish', label:'Fehlklicks', align:'center', fmt:function(v){ + var n = v||0; var c = n>=12 ? '#c07a6b' : n>=5 ? '#c9913a' : '#5a8a5e'; + return ''+n+''; + }}, + ], '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 = '

'+esc(sessions.error)+'

'; return; } var host = document.getElementById('tab-live'); var html = _liveToggleHtml() + '
Live · Klassen-Vergleich
'; - html += '

Aktualisiert sich automatisch alle 4 Sekunden. Aktiv = letztes Lebenszeichen ≤ 30 s. Klick auf eine Zeile öffnet die Detailansicht.

'; + html += '

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.

'; if (!sessions.length) { html += '

Gerade niemand online — sobald eine Schülerin oder ein Schüler eine Simulation startet, taucht sie hier auf.

'; host.innerHTML = html + '
'; 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 += '
' + '🆘 '+helpList.length+' brauchen evtl. Hilfe — ' @@ -1184,7 +1214,8 @@ async function renderLive() { html += '
' + ''+(meta.moduleIcon||'📚')+'' + ''+esc(meta.moduleName||modId)+'' - + '· '+grp.rows.length+' aktiv' + + (function(){ var a=grp.rows.filter(_liveIsActive).length, p=grp.rows.length-a; + return '· '+a+' aktiv'+(p?' · '+p+' pausiert':'')+''; })() + '
'; 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 += ''; - html += ''+(help?'🆘':'')+avatarHtml+''+esc(ls.displayName)+''; + html += ''; + html += ''+(help?'🆘':(paused?'':''))+avatarHtml+''+esc(ls.displayName)+''; html += '
'+missionStr+'
'+simStr+' ges.
'; fields.forEach(function(f) { var align = f.align || 'right'; html += ''+_liveFmt(s[f.k], f.unit, f, ls)+''; }); - html += ''; + html += ''+(paused + ? '⏸ '+_fmtSince(lastSeen)+'' + : '')+''; html += ''; }); html += '
';