Lehreransicht aufräumen: Sim-Labels + Logistik-Spalten + assessment-Skip-Fix

- SIM_LABELS: EIN Wort + eindeutiges 3-Buchstaben-Kürzel je App (zentrale
  Quelle simWord()/simCode()). Tages-Balken nutzt jetzt Kürzel (eng) → Wort
  (mittel) → Wort+Lektion (breit) statt kryptischem "Log L1".
- LIVE_PRIMARY_FIELDS['logistik'] ergänzt (Stufe · Budget · Geliefert/Gesamt ·
  Unterwegs · Offen · Verspätet · Fortschritt) — vorher Roh-Dump aller Keys.
- Aktuell-Fallback (uncurated Sims): interne __-Heartbeat-Keys + simId/
  timestamp/screen ausgeblendet, nicht mehr als Spalten.
- assessment.php: Skip-Bug gefixt — las phase aus $body['processLog']['phase'],
  gesendet wird $body['phase']. Dadurch landeten alle 'started'/'running'-Pings
  als LEERE Zeilen in assessments und verschmutzten Ergebnisse/Tag. Jetzt
  greift der Skip; nur echte 'completed'-Abschlüsse werden gespeichert.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-19 15:05:42 +02:00
parent 37f6238d05
commit c1f90e255d
2 changed files with 70 additions and 12 deletions
+6 -1
View File
@@ -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]);
}
+64 -11
View File
@@ -888,6 +888,26 @@ var LIVE_PRIMARY_FIELDS = {
{k:'time_left_s', label:'⏱ Rest', align:'center', fmt:function(v){ if (v==null) return '<span style="color:#8a8a8a"></span>'; var c = v<=30 ? '#c07a6b' : v<=120 ? '#c9913a' : '#5a8a5e'; return '<span style="color:'+c+';font-weight:700">'+_fmtSeconds(v)+'</span>'; }},
{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 '<span style="color:'+c+';font-weight:700">'+Number(v).toLocaleString('de-AT')+' €</span>';
}},
{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 '<b style="color:'+c+'">'+done+'</b>'+(t?'<span style="color:#8a8a8a"> / '+t+'</span>':'');
}},
{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 ? '<span style="color:#c07a6b;font-weight:700">'+n+'</span>' : '<span style="color:#5a8a5e">0</span>';
}},
{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 += '<div onclick="openLiveSessionDetail(\''+sessKey+'\')" '
+ 'title="'+esc(tip)+'" '