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() + '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 + '