diff --git a/App/php/api/assessment.php b/App/php/api/assessment.php index b7aa896..1ba34db 100644 --- a/App/php/api/assessment.php +++ b/App/php/api/assessment.php @@ -29,8 +29,13 @@ $durationMs = max(0, min($rawDuration, GGS_MAX_SESSION_MS)); // Der Live-Zustand für das Lehrer-Cockpit kommt über den Heartbeat (live_sessions); // diese 30-s-Pings (phase 'running'/'started') haben früher die Tabelle zugemüllt // (~96 % der Zeilen). Nur echte Abschlüsse/Ergebnisse werden gespeichert. +// phase steht auf oberster Body-Ebene (alle Sims senden {simId, phase, …}). +// Früher wurde fälschlich $body['processLog']['phase'] gelesen → der Skip +// griff nie, und JEDER 'started'/'running'-Ping landete als leere Zeile in +// assessments (verschmutzt Ergebnisse/Tag). Robust: oberste Ebene, Fallback nested. $pl = $body['processLog'] ?? []; -$phase = (is_array($pl) && isset($pl['phase'])) ? $pl['phase'] : null; +$phase = $body['phase'] + ?? ((is_array($pl) && isset($pl['phase'])) ? $pl['phase'] : null); if ($phase === 'running' || $phase === 'started') { Response::ok(['skipped' => true]); } diff --git a/App/teacher.html b/App/teacher.html index 9961856..3a1cc46 100644 --- a/App/teacher.html +++ b/App/teacher.html @@ -888,6 +888,26 @@ var LIVE_PRIMARY_FIELDS = { {k:'time_left_s', label:'⏱ Rest', align:'center', fmt:function(v){ if (v==null) return '∞'; var c = v<=30 ? '#c07a6b' : v<=120 ? '#c9913a' : '#5a8a5e'; return ''+_fmtSeconds(v)+''; }}, {k:'airport', label:'✈ Flughafen', align:'left'}, ], + 'logistik': [ + {k:'levelId', label:'Stufe', align:'center'}, + {k:'balance', label:'Budget', fmt:function(v){ + if (v==null) return '—'; + var c = v>=5000 ? '#5a8a5e' : v>=0 ? '#c9913a' : '#c07a6b'; + return ''+Number(v).toLocaleString('de-AT')+' €'; + }}, + {k:'ordersDelivered', label:'Geliefert', align:'center', fmt:function(v, row){ + var s = row.state||{}; var t = s.totalOrders||0; var done = v||0; + var c = done>0 ? '#5a8a5e' : '#8a8a8a'; + return ''+done+''+(t?' / '+t+'':''); + }}, + {k:'ordersInFlight', label:'Unterwegs', align:'center'}, + {k:'ordersOpen', label:'Offen', align:'center'}, + {k:'ordersLate', label:'Verspätet', align:'center', fmt:function(v){ + var n = v||0; + return n>0 ? ''+n+'' : '0'; + }}, + {k:'progressPct', label:'Fortschritt', fmt:function(v){ return _liveBar(v, 0, 100, 'high'); }}, + ], // Default-Schema: zeigt einfach den state, soweit Felder benannt sind. _default: null, }; @@ -1337,10 +1357,16 @@ async function renderLive() { // Spalten-Schema: bevorzugt LIVE_PRIMARY_FIELDS, sonst Vereinigung aller state-Keys. var fields = LIVE_PRIMARY_FIELDS[modId]; if (!fields) { + // Kein kuratiertes Schema → nur sinnvolle State-Keys zeigen. + // Interne Heartbeat-Keys (__…) und Rausch-Felder NIE anzeigen. + var _hideKeys = {simId:1, timestamp:1, screen:1}; var keySet = {}; grp.rows.forEach(function(r) { var s = r.state || {}; - Object.keys(s).forEach(function(k) { keySet[k] = true; }); + Object.keys(s).forEach(function(k) { + if (k.indexOf('__') === 0 || _hideKeys[k]) return; + keySet[k] = true; + }); }); fields = Object.keys(keySet).map(function(k) { return {k:k, label:k}; }); } @@ -1430,14 +1456,39 @@ async function renderLive() { // === LIVE-ÜBERSICHT (Tages-Balkenchart pro Schüler*in) === // Kurz-Form pro Sim für Balken-Label -var LIVE_SIM_SHORT = { - heli: 'Heli', klima: 'Klima', 'klima-3d': 'Klima 3D', - busfahrt: 'Bus', fluggesellschaft: 'Flug', - sonnensystem: 'Sonne', 'eu-werkstatt': 'EU', energiemanager: 'Energie', - farmer: 'Farmer', fluss: 'Fluss', logistik: 'Log', stadt: 'Stadt', - regenwald: 'Regenw.', staustufen: 'Stau', erdbeben: 'Erdb.', - vulkan: 'Vulkan', wal: 'Wal', +// Kanonische Sim-Bezeichner: EIN Wort + 3-Buchstaben-Kürzel je App. +// Wort wo Platz ist, Kürzel in engen Balken. Zentrale Quelle — auch anderswo +// nutzbar (Tages-Balken, Kompakt-Anzeigen). Kürzel sind eindeutig. +var SIM_LABELS = { + weltkueche: { w:'Weltküche', c:'WEK' }, + klima: { w:'Klima', c:'KLI' }, + 'klima-3d': { w:'Klima 3D', c:'K3D' }, + staustufen: { w:'Staustufen', c:'STU' }, + fluss: { w:'Fluss', c:'FLU' }, + heli: { w:'Heli', c:'HEL' }, + energiemanager: { w:'Energie', c:'ENE' }, + 'eu-werkstatt': { w:'EU-Werkstatt', c:'EUW' }, + regenwald: { w:'Regenwald', c:'RGW' }, + erdbeben: { w:'Erdbeben', c:'ERD' }, + energiemix: { w:'Energiemix', c:'EMX' }, + lieferketten: { w:'Lieferketten', c:'LIF' }, + farmer: { w:'Farmer', c:'FAR' }, + busfahrt: { w:'Busfahrt', c:'BUS' }, + sonnensystem: { w:'Sonne', c:'SON' }, + tourismustal: { w:'Tourismustal', c:'TAL' }, + tourismusregion: { w:'Tourismusregion', c:'REG' }, + logistik: { w:'Logistik', c:'LOG' }, + fluggesellschaft: { w:'Flug', c:'FLG' }, + vulkan: { w:'Vulkan', c:'VUL' }, + wal: { w:'Wal', c:'WAL' }, + stadt: { w:'Stadt', c:'STD' }, + kofferdetektiv: { w:'Koffer', c:'KOF' }, + entscheidungstag: { w:'Alltag', c:'ALT' }, }; +function simWord(id){ return (SIM_LABELS[id] && SIM_LABELS[id].w) || id; } +function simCode(id){ return (SIM_LABELS[id] && SIM_LABELS[id].c) || String(id||'').slice(0,3).toUpperCase(); } +// Rückwärtskompatibel: LIVE_SIM_SHORT[id] = Wort je Sim. +var LIVE_SIM_SHORT = Object.keys(SIM_LABELS).reduce(function(o,k){ o[k]=SIM_LABELS[k].w; return o; }, {}); function _liveFmtMin(sec) { if (sec < 60) return sec + 's'; var m = Math.floor(sec/60); var s = sec%60; @@ -1998,14 +2049,16 @@ async function renderLiveSummary() { var pctL = _msToPct(sess.startMs); var pctR = _msToPct(endMs); var pctW = Math.max(0.8, pctR - pctL); // mindestens 0.8 % sichtbar - var simShort = LIVE_SIM_SHORT[sess.simId] || sess.simIcon || sess.simId; - var lbl = simShort + (sess.lessonLabel ? ' '+sess.lessonLabel : ''); + var simW = simWord(sess.simId); + var simC = simCode(sess.simId); + var lbl = simW + (sess.lessonLabel ? ' '+sess.lessonLabel : ''); var color = _liveSuccessColor(sess.success); var tip = (sess.simTitle||sess.simId) + (sess.lessonLabel?' · '+sess.lessonLabel:'') + ' · ' + _fmtHM(sess.startMs) + '–' + _fmtHM(endMs) + ' · ' + _liveFmtMin(realDurSec) + (sess.stars!=null?' · '+sess.stars+'/5★':''); - var inner = (pctW < 3) ? '' : (pctW < 8) ? simShort : lbl; + // enger Balken → 3-Buchstaben-Kürzel, mittel → Wort, breit → Wort + Lektion + var inner = (pctW < 2.5) ? '' : (pctW < 6) ? simC : (pctW < 11) ? simW : lbl; var sessKey = s.studentId + ':' + sess.simId + ':' + (sess.lessonLabel||'_') + ':' + sess.startMs; html += '