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();
|
$db = Database::get();
|
||||||
|
|
||||||
const LIVE_ACTIVE_WINDOW_SEC = 30;
|
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') {
|
if ($method === 'POST') {
|
||||||
$sessionId = Session::requireStudent();
|
$sessionId = Session::requireStudent();
|
||||||
@@ -862,7 +866,7 @@ if ($method === 'GET') {
|
|||||||
WHERE ls.class_id = ?
|
WHERE ls.class_id = ?
|
||||||
AND ls.last_seen > NOW() - INTERVAL ? SECOND
|
AND ls.last_seen > NOW() - INTERVAL ? SECOND
|
||||||
ORDER BY ls.last_seen DESC',
|
ORDER BY ls.last_seen DESC',
|
||||||
[$classId, LIVE_ACTIVE_WINDOW_SEC]
|
[$classId, LIVE_IDLE_WINDOW_SEC]
|
||||||
);
|
);
|
||||||
$list = array_map(function($r) {
|
$list = array_map(function($r) {
|
||||||
return [
|
return [
|
||||||
|
|||||||
+45
-10
@@ -716,6 +716,30 @@ var LIVE_PRIMARY_FIELDS = {
|
|||||||
return v + ' <span style="color:#8a8a8a">/ ' + (s.achievementsTotal||6) + '</span>';
|
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': [
|
'busfahrt': [
|
||||||
{k:'levelId', label:'Stufe', align:'center'},
|
{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 : '—'; }},
|
{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;
|
return false;
|
||||||
}
|
}
|
||||||
var _liveReflCache = '', _liveReflAt = 0;
|
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() {
|
async function renderLive() {
|
||||||
var sessions = await api('live?class_id=' + currentClassId);
|
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; }
|
if (sessions.error) { document.getElementById('tab-live').innerHTML = '<p style="color:#c07a6b">'+esc(sessions.error)+'</p>'; return; }
|
||||||
var host = document.getElementById('tab-live');
|
var host = document.getElementById('tab-live');
|
||||||
var html = _liveToggleHtml() + '<div class="card"><div class="card-h">Live · Klassen-Vergleich</div>';
|
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) {
|
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>';
|
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>';
|
host.innerHTML = html + '</div>';
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// „Braucht Hilfe"-Banner (live)
|
// „Braucht Hilfe"-Banner (live) — nur aktuell aktive Sessions
|
||||||
var helpList = sessions.filter(function(ls){ return _liveNeedsHelp(ls.module_id, ls.state); });
|
var helpList = sessions.filter(function(ls){ return _liveIsActive(ls) && _liveNeedsHelp(ls.module_id, ls.state); });
|
||||||
if (helpList.length) {
|
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">'
|
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> — '
|
+ '🆘 <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">'
|
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 style="font-size:1.1rem">'+(meta.moduleIcon||'📚')+'</span>'
|
||||||
+ '<span>'+esc(meta.moduleName||modId)+'</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>';
|
+ '</div>';
|
||||||
|
|
||||||
if (!anyState) {
|
if (!anyState) {
|
||||||
@@ -1221,20 +1252,24 @@ async function renderLive() {
|
|||||||
var simStr = sinceSimBase ? _fmtSince(sinceSim) : '—';
|
var simStr = sinceSimBase ? _fmtSince(sinceSim) : '—';
|
||||||
var missionStr = sinceMissionBase ? _fmtSince(sinceMission) : '—';
|
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 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 dotColor = lastSeen < 8 ? '#5a8a5e' : lastSeen < 20 ? '#e8833a' : '#c07a6b';
|
||||||
var s = ls.state || {};
|
var s = ls.state || {};
|
||||||
var help = _liveNeedsHelp(ls.module_id, s);
|
var help = !paused && _liveNeedsHelp(ls.module_id, s);
|
||||||
var restBg = help ? '#fdf3ee' : '';
|
var restBg = help ? '#fdf3ee' : (paused ? '#f6f5f2' : '');
|
||||||
var hoverBg = help ? '#f9e7e0' : '#f7f6f3';
|
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 += '<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>':'')+avatarHtml+'<strong>'+esc(ls.displayName)+'</strong></span></td>';
|
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>';
|
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) {
|
fields.forEach(function(f) {
|
||||||
var align = f.align || 'right';
|
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:'+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 += '</tr>';
|
||||||
});
|
});
|
||||||
html += '</tbody></table></div></div>';
|
html += '</tbody></table></div></div>';
|
||||||
|
|||||||
Reference in New Issue
Block a user