Tag-Ansicht: echte Spieldauer + Ampelfarbe nach Erfolg + Sim-Symbol
Bug: fertig gespielte Laeufe (Farmer-Endbericht = 1 Assessment) erschienen als kurzer "Abbruch"-Balken (Dauer 0). Fix: - live.php summary: nutzt duration_ms -> Balken decken die echte Spielzeit ab und fluchten am Start; kein Falsch-Abbruch mehr. Neues Feld `score` (0-100) je Session (Score/Sterne); laufende Farmer-Sessions bekommen hit_rate als Live-Score. - teacher.html Tag-Balken: einheitliche Hoehe (Hoehe raus), Farbe = Ampel nach Erfolg (gruen->rot), App-Farbe ersetzt durch Sim-Symbol; Klick = Details. Legende angepasst. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -141,6 +141,14 @@ if ($method === 'GET') {
|
||||
if (($pl['phase'] ?? '') === 'completed') return true;
|
||||
return null;
|
||||
};
|
||||
// Erfolgs-Score 0–100 (für die Ampelfarbe der Tages-Balken). Sterne → %,
|
||||
// sonst der Sim-Score (Farmer/Weltküche liefern `score`).
|
||||
$extractScore = function($row) {
|
||||
$r = $row['results'] ? json_decode($row['results'], true) : [];
|
||||
foreach ([$r['score'] ?? null, $r['results']['score'] ?? null] as $c) if (is_numeric($c)) return max(0, min(100, (int)round($c)));
|
||||
if (isset($r['stars']) && is_numeric($r['stars'])) return (int)round($r['stars'] / 5 * 100);
|
||||
return null;
|
||||
};
|
||||
|
||||
// Gruppieren: student_id × sim_id × lesson_key → eine ODER MEHRERE Sessions.
|
||||
// Wichtig: bei Lücke > SESSION_GAP_SEC zwischen aufeinanderfolgenden Submits
|
||||
@@ -197,6 +205,8 @@ if ($method === 'GET') {
|
||||
'startMs' => $ts * 1000,
|
||||
'endMs' => $ts * 1000,
|
||||
'stars' => null,
|
||||
'score' => null,
|
||||
'durationMs' => 0,
|
||||
'completed' => null,
|
||||
'rowCount' => 0,
|
||||
];
|
||||
@@ -205,6 +215,10 @@ if ($method === 'GET') {
|
||||
$currentSess['rowCount']++;
|
||||
$st = $extractStars($row);
|
||||
if ($st !== null && ($currentSess['stars'] === null || $st > $currentSess['stars'])) $currentSess['stars'] = $st;
|
||||
$scv = $extractScore($row);
|
||||
if ($scv !== null && ($currentSess['score'] === null || $scv > $currentSess['score'])) $currentSess['score'] = $scv;
|
||||
$dmv = (int)($row['duration_ms'] ?? 0);
|
||||
if ($dmv > $currentSess['durationMs']) $currentSess['durationMs'] = $dmv;
|
||||
$cp = $extractCompleted($row);
|
||||
if ($cp !== null) $currentSess['completed'] = $cp;
|
||||
$prevTs = $ts;
|
||||
@@ -227,6 +241,14 @@ if ($method === 'GET') {
|
||||
];
|
||||
}
|
||||
$durSec = max(0, intval(($s['endMs'] - $s['startMs']) / 1000));
|
||||
// Echte Spieldauer aus duration_ms — ein einzelnes Assessment (z. B. der
|
||||
// Farmer-Endbericht) hätte sonst Dauer ~0 und würde faelschlich als
|
||||
// „Abbruch" gewertet. Startzeit entsprechend zurückdatieren, damit der
|
||||
// Balken die ganze Spielzeit abdeckt (und gleich startende Läufe fluchten).
|
||||
if (!empty($s['durationMs']) && $s['durationMs'] > $durSec * 1000) {
|
||||
$durSec = intval($s['durationMs'] / 1000);
|
||||
$s['startMs'] = $s['endMs'] - $s['durationMs'];
|
||||
}
|
||||
// Success-Klassifikation
|
||||
$success = 'unknown';
|
||||
if ($s['completed'] === false) $success = 'aborted';
|
||||
@@ -234,6 +256,8 @@ if ($method === 'GET') {
|
||||
if ($s['stars'] >= 4) $success = 'good';
|
||||
elseif ($s['stars'] >= 2) $success = 'mid';
|
||||
else $success = 'bad';
|
||||
} elseif ($s['score'] !== null) {
|
||||
$success = $s['score'] >= 65 ? 'good' : ($s['score'] >= 40 ? 'mid' : 'bad');
|
||||
} elseif ($s['completed'] === true) $success = 'good';
|
||||
elseif ($durSec < 60) $success = 'aborted';
|
||||
else $success = 'running';
|
||||
@@ -247,6 +271,7 @@ if ($method === 'GET') {
|
||||
'endMs' => $s['endMs'],
|
||||
'durationSec' => $durSec,
|
||||
'stars' => $s['stars'],
|
||||
'score' => $s['score'],
|
||||
'completed' => $s['completed'],
|
||||
'success' => $success,
|
||||
'rowCount' => $s['rowCount'],
|
||||
@@ -314,6 +339,8 @@ if ($method === 'GET') {
|
||||
$startMs = ((int)$lr['started_ts']) * 1000;
|
||||
$endMs = ((int)$lr['last_seen_ts']) * 1000;
|
||||
$durSec = max(0, intval(($endMs - $startMs) / 1000));
|
||||
// Live-Erfolgs-Score für die Ampelfarbe: Farmer = aktuelle Trefferquote.
|
||||
$liveScore = ($simId === 'farmer' && isset($state['hit_rate'])) ? max(0, min(100, (int)$state['hit_rate'])) : null;
|
||||
$byStudent[$sid]['sessions'][] = [
|
||||
'simId' => $simId,
|
||||
'simTitle' => $mi['title'] ?? $simId,
|
||||
@@ -324,6 +351,7 @@ if ($method === 'GET') {
|
||||
'endMs' => $endMs,
|
||||
'durationSec' => $durSec,
|
||||
'stars' => null,
|
||||
'score' => $liveScore,
|
||||
'completed' => null,
|
||||
'success' => 'running',
|
||||
'rowCount' => 1,
|
||||
|
||||
+41
-27
@@ -1574,6 +1574,11 @@ function _liveSuccessColor(success) {
|
||||
unknown: '#7a8a9c',
|
||||
})[success] || '#7a8a9c';
|
||||
}
|
||||
// Ampelfarbe für den Tages-Erfolg (0–100): grün → hellgrün → gelb → orange → rot.
|
||||
function _tagTraffic(v) {
|
||||
v = Math.max(0, Math.min(100, v || 0));
|
||||
return v >= 80 ? '#1f7a3d' : v >= 65 ? '#7cb342' : v >= 50 ? '#f9c33b' : v >= 35 ? '#ef8b2c' : '#d8453a';
|
||||
}
|
||||
function _liveSuccessIcon(stars, success) {
|
||||
if (success === 'aborted') return '✗';
|
||||
if (success === 'running') return '⏳';
|
||||
@@ -2119,38 +2124,40 @@ 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 simW = simWord(sess.simId), simC = simCode(sess.simId);
|
||||
var col = simColor(sess.simId); // eine Farbe je Sim
|
||||
var stars = (sess.stars != null) ? Math.max(0, Math.min(5, sess.stars)) : null;
|
||||
var icon = sess.simIcon || '📚';
|
||||
var simName = sess.simTitle || simWord(sess.simId);
|
||||
// Erfolg 0–100 (Score, sonst Sterne → %). Bestimmt die AMPELFARBE.
|
||||
var score = (sess.score != null) ? sess.score
|
||||
: (sess.stars != null ? Math.round(Math.max(0,Math.min(5,sess.stars))/5*100) : null);
|
||||
var aborted = sess.success === 'aborted';
|
||||
var completed = !aborted && stars != null; // fertig gespielt
|
||||
var inProgress = !aborted && stars == null; // noch dran / kein Ergebnis
|
||||
// Balkenhöhe = Maß für den Erfolg (mehr Sterne → höher). Abbruch/läuft: flach.
|
||||
var barH = completed ? (9 + Math.round(stars / 5 * 15)) : (aborted ? 9 : 12);
|
||||
// Rahmen kodiert den Zustand: fertig = geschlossen · noch dran = rechts offen ·
|
||||
// Abbruch = gestrichelt (+ ✖ am Ende).
|
||||
var border = completed ? 'border:1.5px solid rgba(20,20,20,.45);'
|
||||
: inProgress ? 'border:1.5px solid rgba(20,20,20,.4);border-right:0;'
|
||||
: 'border:1.5px dashed rgba(20,20,20,.45);';
|
||||
var stateWord = completed ? 'fertig' : aborted ? 'abgebrochen' : 'läuft noch';
|
||||
var inProgress = !aborted && (sess.isLive || sess.success === 'running');
|
||||
// Farbe = Erfolg (Ampel). Kein Wert → neutral grau. Abbruch → rot-gestrichelt.
|
||||
var col = aborted ? '#c0392b' : (score != null ? _tagTraffic(score) : '#aab2b8');
|
||||
var barH = 18; // EINHEITLICHE Höhe — der Erfolg steckt in der Farbe, nicht in der Höhe
|
||||
// Rahmen kodiert den Zustand: fertig = geschlossen · läuft = rechts gepunktet · Abbruch = gestrichelt
|
||||
var border = aborted ? 'border:1.5px dashed rgba(255,255,255,.7);'
|
||||
: inProgress ? 'border:1.5px solid rgba(255,255,255,.55);border-right:2px dotted #fff;'
|
||||
: 'border:1.5px solid rgba(20,20,20,.25);';
|
||||
var stateWord = inProgress ? 'läuft noch' : aborted ? 'abgebrochen' : 'fertig';
|
||||
var tip = (sess.simTitle||sess.simId) + (sess.lessonLabel?' · '+sess.lessonLabel:'')
|
||||
+ ' · ' + _fmtHM(sess.startMs) + '–' + _fmtHM(endMs)
|
||||
+ ' · ' + _liveFmtMin(realDurSec) + ' · ' + stateWord
|
||||
+ (stars!=null?' · '+stars+'/5★':'');
|
||||
// enger Balken → Kürzel, mittel → Wort, breit → Wort + Lektion
|
||||
var inner = (pctW < 2.5) ? '' : (pctW < 6) ? simC : (pctW < 11) ? simW : (simW + (sess.lessonLabel ? ' '+sess.lessonLabel : ''));
|
||||
+ (score!=null?' · Erfolg '+score+' %':'');
|
||||
// Symbol immer sichtbar; ab etwas Breite zusätzlich der Sim-Name
|
||||
var inner = (pctW < 2) ? '' : (pctW < 9 ? '<span style="font-size:.82rem">'+icon+'</span>'
|
||||
: '<span style="font-size:.82rem">'+icon+'</span> <span style="font-weight:700">'+esc(simName)+'</span>');
|
||||
var sessKey = s.studentId + ':' + sess.simId + ':' + (sess.lessonLabel||'_') + ':' + sess.startMs;
|
||||
html += '<div onclick="openLiveSessionDetail(\''+sessKey+'\')" '
|
||||
+ 'title="'+esc(tip)+'" '
|
||||
+ 'style="position:absolute;left:'+pctL+'%;width:'+pctW+'%;bottom:2px;height:'+barH+'px;'
|
||||
+ 'background:'+col+';color:#fff;border-radius:3px;'+border
|
||||
+ 'padding:0 .3rem;font-size:.6rem;font-weight:700;line-height:'+Math.max(9,barH-3)+'px;'
|
||||
+ 'style="position:absolute;left:'+pctL+'%;width:'+pctW+'%;top:5px;height:'+barH+'px;'
|
||||
+ 'background:'+col+';color:#fff;border-radius:4px;'+border
|
||||
+ 'padding:0 .35rem;font-size:.66rem;line-height:'+barH+'px;'
|
||||
+ 'white-space:nowrap;overflow:hidden;text-overflow:ellipsis;cursor:pointer;'
|
||||
+ 'text-shadow:0 1px 1px rgba(0,0,0,.35);box-shadow:0 1px 2px rgba(0,0,0,.12)">'
|
||||
+ esc(inner) + '</div>';
|
||||
+ 'text-shadow:0 1px 1px rgba(0,0,0,.3);box-shadow:0 1px 2px rgba(0,0,0,.12)">'
|
||||
+ inner + '</div>';
|
||||
if (aborted) {
|
||||
html += '<span style="position:absolute;left:'+pctR+'%;bottom:-1px;height:28px;line-height:28px;'
|
||||
+ 'font-size:.74rem;color:#b03a2a;font-weight:800;padding-left:1px;pointer-events:none">✖</span>';
|
||||
html += '<span style="position:absolute;left:'+pctR+'%;top:0;height:28px;line-height:28px;'
|
||||
+ 'font-size:.74rem;color:#b03a2a;font-weight:800;padding-left:2px;pointer-events:none">✖</span>';
|
||||
}
|
||||
});
|
||||
html += '</div>';
|
||||
@@ -2158,10 +2165,17 @@ async function renderLiveSummary() {
|
||||
html += '</div>';
|
||||
|
||||
html += '<div style="margin-top:.8rem;display:flex;gap:1rem;flex-wrap:wrap;font-size:.66rem;color:#5a5a5a;align-items:center">';
|
||||
html += '<span><b>Farbe</b> = Simulation · <b>Höhe</b> = Erfolg (höher = besser)</span>';
|
||||
html += '<span><span style="display:inline-block;width:16px;height:11px;background:#8a8a8a;border:1.5px solid rgba(20,20,20,.45);vertical-align:middle;margin-right:.25rem"></span>fertig</span>';
|
||||
html += '<span><span style="display:inline-block;width:16px;height:11px;background:#8a8a8a;border:1.5px solid rgba(20,20,20,.4);border-right:0;vertical-align:middle;margin-right:.25rem"></span>läuft noch</span>';
|
||||
html += '<span><span style="display:inline-block;width:16px;height:11px;background:#8a8a8a;border:1.5px dashed rgba(20,20,20,.45);vertical-align:middle;margin-right:.15rem"></span><span style="color:#b03a2a;font-weight:800">✖</span> Abbruch</span>';
|
||||
html += '<span style="display:inline-flex;align-items:center;gap:.3rem"><b>Farbe</b> = Erfolg:'
|
||||
+ '<span style="display:inline-flex;border-radius:2px;overflow:hidden">'
|
||||
+ '<span style="display:inline-block;width:13px;height:11px;background:#1f7a3d"></span>'
|
||||
+ '<span style="display:inline-block;width:13px;height:11px;background:#7cb342"></span>'
|
||||
+ '<span style="display:inline-block;width:13px;height:11px;background:#f9c33b"></span>'
|
||||
+ '<span style="display:inline-block;width:13px;height:11px;background:#ef8b2c"></span>'
|
||||
+ '<span style="display:inline-block;width:13px;height:11px;background:#d8453a"></span></span>'
|
||||
+ ' stark → schwach</span>';
|
||||
html += '<span><b>Symbol</b> = Simulation · <b>Klick</b> = Details</span>';
|
||||
html += '<span><span style="display:inline-block;width:16px;height:11px;background:#aab2b8;border:1.5px solid rgba(255,255,255,.55);border-right:2px dotted #fff;vertical-align:middle;margin-right:.25rem"></span>läuft noch</span>';
|
||||
html += '<span><span style="color:#b03a2a;font-weight:800">✖</span> Abbruch</span>';
|
||||
html += '</div>';
|
||||
|
||||
html += '</div>';
|
||||
|
||||
Reference in New Issue
Block a user