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:
@@ -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
@@ -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>';
|
||||
|
||||
Reference in New Issue
Block a user