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+='
'+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) {
+ '