diff --git a/App/teacher.html b/App/teacher.html index 872a1af..a12a5f7 100644 --- a/App/teacher.html +++ b/App/teacher.html @@ -667,6 +667,16 @@ function _pctBar(pct) { + '
' + ''; } +// Größere %+Balken-Plakette für Run-Karten (Ersatz für Sterne) — Balken statt +// Sterne, weil auch für Farbenblinde am Füllstand unterscheidbar. +function _pctBadge(pct) { + if (pct == null || isNaN(pct)) return ''; + pct = Math.max(0, Math.min(100, Math.round(pct))); + var c = _pctColor(pct); + return '' + + '' + + ''+pct+' %'; +} // Das EINE Leistungs-% (0–100) je Sim aus dem Live-State. Sims mit eigenem // 0–100-Score nutzen ihn direkt; sonst aus den vorhandenen Kennzahlen abgeleitet. function _leistungPct(simId, s) { @@ -944,19 +954,13 @@ var LIVE_PRIMARY_FIELDS = { : ''+n+''; }}, {k:'startScore', label:'🛫 Start', fmt:function(v){ - var n = Math.max(0, Math.min(5, v||0)); - var c = n>=4 ? '#5a8a5e' : n>=2 ? '#c9913a' : '#c07a6b'; - return v==null ? '—' : ''+'★'.repeat(n)+''+'★'.repeat(5-n)+''; + return v==null ? '' : _pctBar(Math.round(Math.max(0,Math.min(5,v))/5*100)); }}, {k:'landingScore', label:'🛬 Landung', fmt:function(v){ - var n = Math.max(0, Math.min(5, v||0)); - var c = n>=4 ? '#5a8a5e' : n>=2 ? '#c9913a' : '#c07a6b'; - return v==null ? '—' : ''+'★'.repeat(n)+''+'★'.repeat(5-n)+''; + return v==null ? '' : _pctBar(Math.round(Math.max(0,Math.min(5,v))/5*100)); }}, {k:'totalStars', label:'⭐ Gesamt', fmt:function(v){ - var n = Math.max(0, Math.min(5, v||0)); - var c = n>=4 ? '#5a8a5e' : n>=2 ? '#c9913a' : '#c07a6b'; - return v==null ? '—' : ''+'★'.repeat(n)+''+'★'.repeat(5-n)+''; + return v==null ? '' : _pctBar(Math.round(Math.max(0,Math.min(5,v))/5*100)); }}, {k:'failed', label:'Status', align:'center', fmt:function(v, row){ var s = row.state||{}; @@ -2536,7 +2540,7 @@ async function openLiveSessionDetail(sessionKey) { var dur = _liveFmtMin(sess.durationSec||0); var stars = sess.stars; var starsHtml = stars != null - ? ''+'★'.repeat(stars)+''+'★'.repeat(5-stars)+'' + ? _pctBadge(Math.round(Math.max(0,Math.min(5,stars))/5*100)) : 'noch keine Wertung'; var statusLabels = {good:'✓ gut', mid:'⚠ mittel', @@ -2578,8 +2582,8 @@ async function openLiveSessionDetail(sessionKey) { if (nested.missionTitle) rows.push(['Auftrag', nested.missionTitle]); if (nested.planCorrect != null) rows.push(['🗺️ Treffer', nested.planCorrect + ' / ' + (nested.planTotal||'?')]); if (nested.planPct != null) rows.push(['🎯 Genauigkeit', nested.planPct + ' %']); - if (nested.startScore != null) rows.push(['🛫 Start', '★'.repeat(nested.startScore||0) + '☆'.repeat(5-(nested.startScore||0))]); - if (nested.landingScore != null)rows.push(['🛬 Landung', '★'.repeat(nested.landingScore||0) + '☆'.repeat(5-(nested.landingScore||0))]); + if (nested.startScore != null) rows.push(['🛫 Start', Math.round((nested.startScore||0)/5*100) + ' %']); + if (nested.landingScore != null)rows.push(['🛬 Landung', Math.round((nested.landingScore||0)/5*100) + ' %']); if (nested.planTakeover) rows.push(['Routenplanung', 'Chefpilot übernahm']); if (nested.failed) rows.push(['Mission', 'gescheitert'+(nested.failReason?': '+esc(nested.failReason):'')]); } @@ -2844,8 +2848,8 @@ function startLiveStatusPoll() { // === INTERVENTIONS-ÜBERSICHT (neu, oben im Übersicht-Tab) === var IV_CSS = '' -+ '.iv-wrap{--ivg:#3e9b63;--ivli:#7aa03a;--ivy:#dcae35;--ivo:#db8a33;--ivr:#cc4a37;' -+ '--ivns:#5b7fa6;--ivnm:#3f8f8f;--ivnw:#8f6f9c;--ivngd:#6a72b0;--ivni:#8c857c;' ++ '.iv-wrap{--ivg:#1f7a3d;--ivli:#7cb342;--ivy:#f9c33b;--ivo:#ef8b2c;--ivr:#d8453a;' ++ '--ivns:#b04a3e;--ivnm:#155e2a;--ivnw:#d98b2c;--ivngd:#6a72b0;--ivni:#8c857c;' + '--ivtrack:rgba(41,52,46,.13);--ivline:rgba(0,0,0,.08);--ivsoft:#f5f3ec;--ivmut:#8a8a8a;font-size:14px}' + '.iv-wrap *{box-sizing:border-box}' + '.iv-h1{font-size:1.15rem;font-weight:800;margin:0 0 14px;color:#3d4a44}' @@ -2907,7 +2911,7 @@ var IV_CSS = '' + '@media (prefers-reduced-motion:reduce){.iv-dotA{animation:none!important}}'; function ivInjectCss(){ if(!document.getElementById('iv-css')){ var s=document.createElement('style'); s.id='iv-css'; s.textContent=IV_CSS; document.head.appendChild(s); } } -function ivAmp(v){ return v<38?'iv-r':v<50?'iv-o':v<62?'iv-y':v<75?'iv-li':'iv-g'; } +function ivAmp(v){ return v<50?'iv-r':v<60?'iv-o':v<80?'iv-y':v<90?'iv-li':'iv-g'; } // = _pctColor-Grenzen function ivErf(v){ if (v==null||isNaN(v)) return ''; v=Math.round(v); @@ -2937,16 +2941,18 @@ var IV_ICON={ good:'', idle:'', calm:''}; -var IV_META={support:['Fördern','zuletzt schwächere Läufe','--ivns'],more:['Fordern','zuletzt sicher, kaum gefordert','--ivnm'], - work:['Begleiten','zuletzt viel gearbeitet, wenig Ertrag','--ivnw'],good:['Bestätigen','zuletzt besser geworden','--ivngd'], - calm:['Läuft gut','solide unterwegs, keine Auffälligkeiten','--ivg'],idle:['Nicht aktiv','länger nicht mehr dran (oder noch nie)','--ivni']}; +var IV_META={support:['Fördern','schwach (< 50 %) — braucht Hilfe','--ivns'],more:['Fordern','kann alles (≥ 90 %) — mehr herausfordern','--ivnm'], + work:['Begleiten','schwach, aber fleißiger als der Klassenschnitt','--ivnw'],good:['Bestätigen','zuletzt besser geworden','--ivngd'], + calm:['Läuft gut','solide unterwegs (50–89 %)','--ivg'],idle:['Nicht aktiv','länger nicht mehr dran (oder noch nie)','--ivni']}; var IV_WEEK_CAP = 10800; // eine Woche zählt max. 3 h aktive Zeit (Ausreißer-Schutz) // Rendert die 5 Triage-Karten (support/more/work/good/idle) aus fertigen Gruppen. // Wiederverwendet für „HEUTE" und „insgesamt". todayMode passt Untertitel/Tooltips an. function _ivTriageCards(groups, maxSec, todayMode){ var h='
'; - ['support','more','work','good','calm','idle'].forEach(function(key){ + // 4 Handlungs-Felder (aus dem Leistungs-% + Fleiß) + „Nicht aktiv". „Bestätigen" + // entfällt — geht in „Läuft gut" auf. + ['support','work','calm','more','idle'].forEach(function(key){ var rows=groups[key], m=IV_META[key]; var sub=(todayMode && key==='idle')?'heute noch nicht dran':m[1]; h+='
'+IV_ICON[key]+'

'+m[0]+'

'+sub+'

'+rows.length+'
'; @@ -2968,14 +2974,15 @@ function _ivTriageCards(groups, maxSec, todayMode){ // Gesehene, kein Allzeit-Rückblick): recentErfolg = Ø letzte 3 Läufe, daysSince = // Tage seit letztem Lauf, recentPlays = Läufe der letzten 14 Tage, rising = // zuletzt besser als der bisherige Schnitt. -function ivTriage(recErf, daysSince, recentPlays, rising){ - if (daysSince == null || daysSince > 14) return 'idle'; // länger nicht dran / nie - if (recErf == null) return 'idle'; - if (recErf < 48 && recentPlays >= 5) return 'work'; // zuletzt viel, wenig Ertrag - if (recErf < 48) return 'support'; // zuletzt schwach - if (recErf >= 65 && recentPlays <= 2) return 'more'; // zuletzt sicher, kaum gefordert - if (rising) return 'good'; // zuletzt besser geworden - return null; // läuft rund +// Triage aus dem Leistungs-% (0–100) × Fleiß-im-Klassenvergleich. +// ≥90 → Fordern (kann alles) · 50–89 → Läuft gut · <50 & fleißig → Begleiten · +// <50 & nicht fleißig → Fördern. Länger als 14 Tage weg / kein Wert → Nicht aktiv. +function ivTriage(erf, daysSince, fleissig){ + if (daysSince == null || daysSince > 14) return 'idle'; + if (erf == null) return 'idle'; + if (erf >= 90) return 'more'; // Fordern + if (erf >= 50) return 'calm'; // Läuft gut + return fleissig ? 'work' : 'support'; // Begleiten : Fördern } function ivZuletzt(days){ if (days == null) return 'nie'; @@ -3012,6 +3019,22 @@ function ivBuildOverview(cockpit, weeks, studentsCount, matrix, todaySummary, to var playsById={}; ((matrix&&matrix.students)||[]).forEach(function(s){ playsById[s.id]=(matrix.totals[s.id]||{}).plays||0; }); + // Fleiß-Referenz (Klassenschnitt): aktive Zeit (Wochen-Buckets) + Durchgänge. + var _secTot={}, _secSum=0, _secCnt=0, _playSum=0, _playCnt=0; + cs.forEach(function(s){ + var sid=s.studentId||s.id; + var w=wById[sid]; var tsec=w?w.buckets.reduce(function(a,b){return a+Math.min(IV_WEEK_CAP,b.sum||0);},0):0; + _secTot[sid]=tsec; if(tsec>0){_secSum+=tsec;_secCnt++;} + var pl=playsById[sid]||0; if(pl>0){_playSum+=pl;_playCnt++;} + }); + var _avgSec=_secCnt?_secSum/_secCnt:1, _avgPlay=_playCnt?_playSum/_playCnt:1; + // „fleißig" = kombiniert (Zeit + Durchgänge) mindestens Klassenschnitt. + function _fleissig(sid, plays){ + var t=(_secTot[sid]||0)/Math.max(1,_avgSec); + var p=(plays!=null?plays:(playsById[sid]||0))/Math.max(1,_avgPlay); + return (0.5*t + 0.5*p) >= 1; + } + var groups={support:[],more:[],work:[],good:[],calm:[],idle:[]}, erfSum=0,erfN=0, weekTotals=[]; cs.forEach(function(s){ var sid=s.studentId||s.id; @@ -3025,7 +3048,7 @@ function ivBuildOverview(cockpit, weeks, studentsCount, matrix, todaySummary, to var rising=(recErf!=null && s.earlyErfolg!=null && recErf > s.earlyErfolg+10); // letzte 3 besser als frühere if(recErf!=null){ erfSum+=recErf; erfN++; } weekTotals.push(secs); - var g=ivTriage(recErf, days, rplays, rising); + var g=ivTriage(recErf, days, _fleissig(sid, playsById[sid])); var row={sid:sid, name:s.displayName, erf:recErf, status:s.status, secs:secs, days:days}; if(g) groups[g].push(row); else groups.calm.push(row); }); @@ -3100,15 +3123,12 @@ function ivBuildOverview(cockpit, weeks, studentsCount, matrix, todaySummary, to var tErf=todayErfById[sid]; var w2=wById[sid], secs2=w2?w2.buckets.map(function(b){return Math.min(IV_WEEK_CAP, b.sum||0);}):[]; while(secs2.length<10) secs2.unshift(0); - // Totalversagen heute (ein Lauf < 40) → immer Fördern, egal wie der Schnitt ist. - var g2 = !played ? 'idle' - : (todayMinById[sid]!=null && todayMinById[sid] < 40) ? 'support' - : ivTriage(tErf, 0, todayPlaysById[sid], false); + var g2 = !played ? 'idle' : ivTriage(tErf, 0, _fleissig(sid, todayPlaysById[sid])); var row2={sid:sid, name:s.displayName, erf:played?tErf:null, status:s.status, secs:secs2, days:played?0:(s.daysSince!=null?s.daysSince:null)}; if(g2) heuteGroups[g2].push(row2); else heuteGroups.calm.push(row2); }); var heutePlayed=Object.keys(todayPlaysById).length; - var need=groups.support.length+groups.more.length+groups.work.length+groups.good.length+groups.idle.length; + var need=groups.support.length+groups.work.length+groups.more.length+groups.idle.length; // 1) HEUTE (oben) — genau dieselbe Darstellung, nur auf heute gefiltert h+='
Empfohlene Aktivität für dich — HEUTE'+heutePlayed+' heute aktiv
'; @@ -3170,7 +3190,7 @@ function ivAvatar(emoji){ } return ''+(emoji||'🧑‍🎓')+''; } -function ivStars(n){ return (typeof ggsStarsHtml==='function') ? ggsStarsHtml(n) : ('★'.repeat(Math.max(0,Math.min(5,Math.round(n)))) ); } +function ivStars(n){ return _pctBar(Math.round(Math.max(0,Math.min(5,n||0))/5*100)); } async function ivOpenStudent(sid){ ivInjectCss(); var d = await api('live?student_runs='+sid); @@ -3381,7 +3401,7 @@ function ivTodayAvatar(emoji, px) { function ivBuildTodaySection(today, modById) { var tt = (today && today.topToday) || []; var prog = (today && today.progress) || []; - var starsHtml = (typeof ggsStarsHtml === 'function') ? ggsStarsHtml : function(n){ return '★'.repeat(Math.max(0,Math.min(5,n))); }; + var starsHtml = function(n){ return _pctBadge(Math.round(Math.max(0,Math.min(5,n||0))/5*100)); }; // Heute neu erreichte Abzeichen: Bedingung jetzt erfüllt, vor heute nicht. var newBadges = []; @@ -4689,7 +4709,7 @@ function renderModDetail() { stu.forEach(function(s) { var stars = s.best_stars || 0; - var starStr = '★'.repeat(stars) + ''+'★'.repeat(5-stars)+''; + var starStr = _pctBar(Math.round(Math.max(0,Math.min(5,stars))/5*100)); var lastDate = s.last_played ? new Date(s.last_played.replace(' ','T')+'Z').toLocaleDateString('de-AT', {day:'2-digit',month:'2-digit'}) : '—'; var dur = s.total_duration_ms ? Math.round(s.total_duration_ms/60000)+' min' : '—'; var avatar = (typeof s.emoji === 'string' && s.emoji.indexOf('avatar:') === 0) @@ -4948,7 +4968,7 @@ function renderSonnensystemSessionTeacher(sess) { } var quote = total > 0 ? Math.round(done/total*100) : 0; var starsHtml = sess.complete - ? ''+'★'.repeat(stars)+''+'★'.repeat(5-stars)+'' + ? _pctBadge(quote) : '⏸ läuft / abgebrochen'; var statusEmoji = sess.complete ? '🪐' : '⏳'; var statusLabel = sess.complete ? 'Aufgaben-Runde abgeschlossen' : 'Teil-Durchlauf (nicht abgeschlossen)'; @@ -5033,7 +5053,7 @@ function renderRunsLineChart(simId, runs) { } function fmtMetric(m, v){ if (v === null) return '–'; - if (m.type === 'stars' || m.type === 'starsSmall') return Math.round(v) + '★'; + if (m.type === 'stars' || m.type === 'starsSmall') return _pctBar(Math.round(Math.max(0,Math.min(5,v))/5*100)); if (m.type === 'pct' || m.type === 'pctInv' || m.type === 'ratio') return Math.round(v) + ' %'; if (m.type === 'temp') return (Math.round(v*10)/10).toLocaleString('de-AT') + ' °C'; var n = Math.abs(v) >= 100 ? Math.round(v) : Math.round(v * 10) / 10; @@ -5136,7 +5156,7 @@ function renderRunDetailCard(simId, run, idx) { function renderSonnensystemRun(dt, dur, r, d) { var stars = Math.max(0, Math.min(5, r.stars || (d.stars||0))); - var starsHtml = ''+'★'.repeat(stars)+''+'★'.repeat(5-stars)+''; + var starsHtml = _pctBadge(Math.round(stars/5*100)); var done = d.completed || 0; var total = d.total || 0; var quote = d.scoreQuote != null ? d.scoreQuote : (total > 0 ? Math.round(done/total*100) : 0); @@ -5196,7 +5216,7 @@ function renderEnergiemanagerRun(dt, dur, r, d) { + '
'; } var stars = Math.max(0, Math.min(5, r.stars || 0)); - var starsHtml = ''+'★'.repeat(stars)+''+'★'.repeat(5-stars)+''; + var starsHtml = _pctBadge(Math.round(stars/5*100)); var setIcon = d.setIcon || '⚡'; var setTitle = d.setTitle || d.setId || '?'; var blocksB = d.blocksBalanced || 0; @@ -5238,7 +5258,7 @@ function renderEnergiemanagerRun(dt, dur, r, d) { function renderEuWerkstattRun(dt, dur, r, d) { var stars = Math.max(0, Math.min(5, r.stars || 0)); - var starsHtml = ''+'★'.repeat(stars)+''+'★'.repeat(5-stars)+''; + var starsHtml = _pctBadge(Math.round(stars/5*100)); var endTypeLabel = ({success:'🏆 Gewonnen', mixed:'⚖️ Teilweise', rejected:'❌ Verloren'})[d.endType] || '—'; var endTypeColor = ({success:'#5a8a5e', mixed:'#c9913a', rejected:'#c07a6b'})[d.endType] || '#8a8a8a'; var diffLabel = d.difficulty === 'expert' ? '🦉 Profi' : '🌱 Einfach'; @@ -5285,8 +5305,8 @@ function renderEuWerkstattRun(dt, dur, r, d) { } function renderBusfahrtRun(dt, dur, r, d) { - var stars = Math.max(0, Math.min(5, r.stars || 0)); - var starsHtml = ''+'★'.repeat(stars)+''+'★'.repeat(5-stars)+''; + var stars = Math.max(0, Math.min(3, r.stars || 0)); // Busfahrt: 0–3-Skala + var starsHtml = _pctBadge(Math.round(stars/3*100)); var lvl = (d.level || 'l1').toString().toUpperCase(); var budget = d.results && d.results.budget != null ? d.results.budget : (d.budget != null ? d.budget : 0); var earnings = d.results && d.results.earnings != null ? d.results.earnings : (d.earnings != null ? d.earnings : 0); @@ -5314,7 +5334,7 @@ function renderHeliRun(dt, dur, r, d) { // Heli liefert results direkt im d (siehe submitHeliMission in game.html) var res = d.results || d; var stars = Math.max(0, Math.min(5, r.stars || (d.stars || 0))); - var starsHtml = ''+'★'.repeat(stars)+''+'★'.repeat(5-stars)+''; + var starsHtml = _pctBadge(Math.round(stars/5*100)); var failed = !!res.failed; var statusIcon = failed ? '🧑‍✈️' : '🚁'; var statusLbl = failed ? 'Chefpilot übernahm' : 'Einsatz erfolgreich'; @@ -5344,8 +5364,8 @@ function renderHeliRun(dt, dur, r, d) { + '
' + '
' + '🗺️ Navigation: '+planCorrect+'/'+planTotal+' ('+planPct+'%)' - + '🛫 Start: '+'★'.repeat(startScore)+'' - + '🛬 Landung: '+'★'.repeat(landingScore)+'' + + '🛫 Start: '+Math.round((startScore||0)/5*100)+' %' + + '🛬 Landung: '+Math.round((landingScore||0)/5*100)+' %' + (heliName ? ''+esc(heliName)+'' : '') + '
' + (takeoverNotes.length ? '
⚠ '+takeoverNotes.join(' · ')+'
' : '') @@ -5354,8 +5374,8 @@ function renderHeliRun(dt, dur, r, d) { } function renderFluggesellschaftRun(dt, dur, r, d) { - var stars = Math.max(0, Math.min(5, r.stars || 0)); - var starsHtml = ''+'★'.repeat(stars)+''+'★'.repeat(5-stars)+''; + var stars = Math.max(0, Math.min(3, r.stars || 0)); // Fluggesellschaft: 0–3-Skala + var starsHtml = _pctBadge(Math.round(stars/3*100)); var lvl = (d.level || 'l1').toString().toUpperCase(); var res = d.results || d; var budget = res.budget != null ? res.budget : 0;