Admin: Lehrer-View Parameter-Ueberblick Schuelerleistung

Neue Referenzseite /admin-lehrer-view.html: pro Sim (14, der Reihe nach)
die 'Ergebnisse/Aktuell'-Tabelle mit Testdaten, gerendert mit der ECHTEN
Logik (LIVE_PRIMARY_FIELDS + Formatter aus teacher.html gespiegelt).
- Unter jedem Spaltentitel der State-Key (Spiel->Backend-Verkabelung).
- Spalten GRUEN, die nach Abschluss relevant bleiben (Ergebnis/Fortschritt/
  Identitaet), GRAU = nur live; je Sim Begruendungs-Liste der Grau-Spalten.
Verlinkt aus admin-sitemap.html.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-08-24 19:37:26 +02:00
parent 62e903a5ec
commit 691387c4d8
2 changed files with 622 additions and 0 deletions
+620
View File
@@ -0,0 +1,620 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GeoGraSim — Lehrer View Parameter-Überblick Schülerleistung</title>
<link rel="icon" type="image/png" href="favicon-96x96.png" sizes="96x96">
<link rel="icon" type="image/svg+xml" href="favicon.svg">
<link rel="stylesheet" href="assets/fonts/inter.css">
<style>
*{margin:0;padding:0;box-sizing:border-box}
body{font-family:'Inter',sans-serif;background:#f7f6f3;min-height:100vh;color:#2a2a2a}
.topbar{background:#fff;padding:.6rem 1.2rem;display:flex;align-items:center;gap:.8rem;border-bottom:1px solid rgba(0,0,0,.06);position:sticky;top:0;z-index:20;flex-wrap:wrap}
.topbar h1{font-size:.95rem;font-weight:800;color:#4a7c8a;flex:1;display:flex;align-items:center;gap:.5rem;min-width:220px}
.topbar h1 img{height:26px}
.btn-sm{padding:.3rem .6rem;border:1px solid rgba(0,0,0,.08);border-radius:6px;background:#fff;font-size:.68rem;font-weight:600;cursor:pointer;color:#4a4a4a;text-decoration:none;white-space:nowrap}
.btn-sm:hover{background:#dae8ec;color:#4a7c8a}
.wrap{max-width:1240px;margin:1rem auto 4rem;padding:0 1rem}
.intro{background:#fff;border-radius:12px;padding:1.1rem 1.3rem;box-shadow:0 1px 4px rgba(0,0,0,.04);margin-bottom:1.2rem}
.intro h2{font-size:1.05rem;color:#1f4b37;font-weight:800;margin-bottom:.4rem}
.intro p{font-size:.82rem;line-height:1.5;color:#4a4a4a;margin-bottom:.5rem}
.intro .wiring{display:flex;gap:.5rem;flex-wrap:wrap;margin:.6rem 0 .2rem}
.wire-chip{background:#eef3f5;border:1px solid #d5e2e7;border-radius:8px;padding:.35rem .7rem;font-size:.72rem;color:#3a5a66;font-weight:600}
.wire-chip b{color:#1f4b37}
.legend{display:flex;gap:1.2rem;flex-wrap:wrap;margin-top:.7rem;padding-top:.7rem;border-top:1px solid rgba(0,0,0,.06);font-size:.76rem;color:#4a4a4a}
.legend span{display:inline-flex;align-items:center;gap:.4rem}
.sw{width:16px;height:16px;border-radius:4px;display:inline-block;border:1px solid rgba(0,0,0,.12)}
.toc{display:flex;flex-wrap:wrap;gap:.4rem;margin-bottom:1.4rem}
.toc a{font-size:.72rem;text-decoration:none;background:#fff;border:1px solid rgba(0,0,0,.08);border-radius:20px;padding:.3rem .7rem;color:#3a5a66;font-weight:600}
.toc a:hover{background:#dae8ec}
.sim{background:#fff;border-radius:12px;box-shadow:0 1px 4px rgba(0,0,0,.04);margin-bottom:1.6rem;overflow:hidden;scroll-margin-top:70px}
.sim-head{display:flex;align-items:baseline;gap:.6rem;padding:.9rem 1.1rem .5rem;flex-wrap:wrap}
.sim-head .emoji{font-size:1.5rem;line-height:1}
.sim-head h3{font-size:1.1rem;font-weight:800;color:#1f4b37}
.sim-head .sid{font-size:.68rem;color:#8a8a8a;font-family:'Courier New',monospace}
.sim-head .count{margin-left:auto;font-size:.7rem;color:#5a8a5e;font-weight:700}
.tbl-scroll{overflow-x:auto;padding:0 1.1rem}
table.live{width:100%;border-collapse:collapse;font-size:.74rem}
table.live th{padding:.4rem .5rem;white-space:nowrap;vertical-align:bottom;border-bottom:2px solid rgba(0,0,0,.08)}
table.live th .key{display:block;font-size:.58rem;color:#9a9a9a;font-weight:500;font-family:'Courier New',monospace;margin-top:2px}
table.live th.green{background:#d9eddc}
table.live th.grey{background:#f7f6f3}
table.live td{padding:.4rem .5rem;font-variant-numeric:tabular-nums;vertical-align:top;border-bottom:1px solid rgba(0,0,0,.04)}
table.live td.green{background:#eef7f0}
.tag{display:inline-block;font-size:.55rem;font-weight:700;border-radius:4px;padding:1px 5px;margin-top:3px;letter-spacing:.03em}
.tag.g{background:#5a8a5e;color:#fff}
.tag.l{background:#e3e0d8;color:#7a7568}
.note{margin:.6rem 1.1rem 1rem;background:#faf7f0;border:1px solid #ece3cf;border-left:4px solid #d8b04a;border-radius:8px;padding:.6rem .85rem;font-size:.76rem;color:#6a5a2a}
.note b{color:#8a6a1a}
.note ul{margin:.35rem 0 0 1.1rem;padding:0}
.note li{margin:.15rem 0;line-height:1.4}
.note code{background:#efe8d5;border-radius:3px;padding:0 4px;font-size:.72rem;color:#7a5a1a}
.allgreen{margin:.6rem 1.1rem 1rem;background:#f0f7f1;border:1px solid #cfe6d2;border-left:4px solid #5a8a5e;border-radius:8px;padding:.55rem .85rem;font-size:.76rem;color:#3a6b3e}
.foot{font-size:.72rem;color:#8a8a8a;text-align:center;margin-top:2rem}
</style>
</head>
<body>
<div class="topbar">
<h1><img src="assets/img/geograsim_t.svg" alt="GeoGraSim"> Lehrer View · Parameter-Überblick Schülerleistung</h1>
<a class="btn-sm" href="admin-sitemap.html">← Admin-Übersicht</a>
<a class="btn-sm" href="admin-modules.html">Module</a>
<a class="btn-sm" href="teacher.html">Lehreransicht</a>
</div>
<div class="wrap">
<div class="intro">
<h2>Wozu diese Seite?</h2>
<p>Referenz für die <b>Verkabelung</b> jeder Simulation. Pro Sim siehst du die Tabelle aus dem Lehrer-Menüpunkt
<b>Ergebnisse&nbsp;/&nbsp;Aktuell</b> — dort erscheint jede Schüler:in mit den relevanten Leistungsdaten der laufenden Sim.
Die Tabellen hier sind <b>1:1 dieselbe Render-Logik</b> (<code>LIVE_PRIMARY_FIELDS</code> + Formatter aus <code>teacher.html</code>), nur mit <b>Testdaten</b>, damit du sie vergleichen kannst.</p>
<div class="wiring">
<span class="wire-chip"><b>Spiel</b><code>window.GGS_LIVE_STATE()</code> liefert die State-Keys</span>
<span class="wire-chip"><b>Lehrer-Backend</b> → Spalte + Formatter je Key</span>
<span class="wire-chip"><b>Schülerinfo</b> → Name · Avatar · Spielzeit</span>
</div>
<p style="font-size:.78rem;margin-top:.5rem">Unter jedem Spaltentitel steht der <b>State-Key</b> (der Parameter, den das Spiel senden muss). So ist ablesbar, welcher Wert aus dem Spiel welche Spalte speist.</p>
<div class="legend">
<span><span class="sw" style="background:#d9eddc"></span> <b>Grün = nach Abschluss noch relevant</b> — Ergebnis, Fortschritt (auch bei Abbruch) oder Identität der Aufgabe. Sollte in die Ergebnis-/Verlaufsansicht übernommen werden.</span>
<span><span class="sw" style="background:#f7f6f3"></span> <b>Grau = nur live</b> — momentaner Zustand, der sich laufend ändert oder zurücksetzt. Nach Abschluss ohne Aussagekraft.</span>
</div>
</div>
<div class="toc" id="toc"></div>
<div id="content"></div>
<p class="foot">Alle Werte sind Beispieldaten zur Ansicht. Render-Logik gespiegelt aus <code>teacher.html · renderLive()</code>. Grün/Grau ist Atlas' Vorschlag — bitte prüfen &amp; korrigieren.</p>
</div>
<script>
/* ============================================================
1) Formatter & Helfer — VERBATIM aus teacher.html gespiegelt,
damit die Zellen exakt wie in der echten Ansicht rendern.
============================================================ */
function esc(s){ var d=document.createElement('div'); d.textContent=s||''; return d.innerHTML; }
function _fmtSeconds(s){ s=Math.max(0,Math.round(s||0)); var mm=Math.floor(s/60), ss=s%60; return mm+':'+(ss<10?'0':'')+ss; }
function _fmtSince(sec){ return sec<60 ? sec+' s' : Math.floor(sec/60)+' min'; }
var LIVE_VALUE_LABELS = {
screen:{ start:'Auswahl', play:'Spielt', end:'Endscreen' },
difficulty:{ easy:'Einfach', expert:'Profi' },
stepType:{ pick:'Karte wählen', sentence:'Lückentext', multichoice:'4 Antworten', animation:'Animation' },
health:{
good:'<span style="color:#5a8a5e">● gut</span>',
ok:'<span style="color:#c9913a">● ok</span>',
struggling:'<span style="color:#c07a6b">● stockt</span>',
struggle:'<span style="color:#c07a6b">● stockt</span>',
},
};
function _liveBar(val, min, max, goodWhen, dec){
if (val==null || isNaN(val)) return '<span style="color:#9a9a9a">—</span>';
var span=(max-min)||1;
var pct=Math.max(0,Math.min(100,((val-min)/span)*100));
var c;
if (goodWhen==='low'){ c=pct<=33?'#5a8a5e':pct<=66?'#c9913a':'#c07a6b'; }
else if (goodWhen==='mid'){ c='#7a7a7a'; }
else { c=pct>=60?'#5a8a5e':pct>=30?'#c9913a':'#c07a6b'; }
var disp=(dec!=null)?Number(val).toFixed(dec):Math.round(val);
return '<span style="display:inline-flex;align-items:center;gap:.35rem;font-variant-numeric:tabular-nums">'
+ '<span aria-hidden="true" style="display:inline-block;width:42px;height:7px;border-radius:99px;background:rgba(0,0,0,.08);overflow:hidden">'
+ '<span style="display:block;height:100%;width:'+pct.toFixed(0)+'%;background:'+c+'"></span></span>'
+ '<span style="color:'+c+';font-weight:700;font-size:.8rem;min-width:1.6em;text-align:right">'+disp+'</span></span>';
}
function _liveBarColor(pct, goodWhen){
pct=Math.max(0,Math.min(100,pct||0));
if (goodWhen==='low') return pct<=33?'#5a8a5e':pct<=66?'#c9913a':'#c07a6b';
if (goodWhen==='mid') return '#9a9a9a';
return pct>=60?'#5a8a5e':pct>=30?'#c9913a':'#c07a6b';
}
function _liveMiniBar(pct, color){
pct=Math.max(0,Math.min(100,pct||0));
return '<div style="margin-top:3px;height:3px;border-radius:99px;background:rgba(0,0,0,.06);overflow:hidden">'
+ '<div style="height:100%;width:'+pct.toFixed(0)+'%;background:'+color+'"></div></div>';
}
function _liveNeedsHelp(simId, s){
if (!s || typeof s!=='object') return false;
switch (simId){
case 'heli': return (s.planAttempts||0) >= 10;
case 'klima': case 'klima-3d': return (s.budget||0) < 0;
case 'weltkueche': return (s.mistakesInDish||0) >= 12;
case 'farmer': return (s.cash==null?1000:s.cash) < 0 || ((s.attempts||0) >= 6 && s.hit_rate != null && s.hit_rate < 30);
case 'logistik': return (s.balance==null?1000:s.balance) < 0;
case 'eu-werkstatt': return (s.attemptsThisStep||0) >= 6 || (s.reshufflesThisStep||0) >= 5;
case 'energiemanager': return (s.blackouts||0) >= 2 || (s.phase==='done' && (s.balanced||0) < 4);
case 'tourismustal': return (s.money==null?1:s.money) < 0 || (s.lossStreak||0) >= 8;
case 'tourismusregion': return (s.budget==null?1:s.budget) < -400 || (s.env==null?100:s.env) < 30;
}
return false;
}
function _liveMood(simId, s){
if (!s) return null;
if (s.health) return ({good:'good',ok:'good',warning:'mid',bad:'bad',critical:'bad'})[s.health] || 'mid';
if (_liveNeedsHelp(simId, s)) return 'bad';
switch (simId){
case 'weltkueche': return (s.mistakesInDish||0)<=2?'good':(s.mistakesInDish>=8?'bad':'mid');
case 'farmer': return (s.hit_rate==null)?'mid':(s.hit_rate>=60?'good':s.hit_rate>=30?'mid':'bad');
case 'klima': case 'klima-3d': return (typeof s.score==='number')?(s.score>=65?'good':s.score>=40?'mid':'bad'):(((s.floodedPct==null?100:s.floodedPct)<25 && (s.budget||0)>=0)?'good':'mid');
case 'busfahrt': case 'fluggesellschaft': return (s.budget==null?0:s.budget)>=5000?'good':(s.budget<0?'bad':'mid');
case 'tourismustal': return (s.money==null?0:s.money)>=4000?'good':(s.money<0?'bad':'mid');
case 'heli': return (s.totalStars||0)>=4?'good':'mid';
case 'fluss': case 'kofferdetektiv': case 'sonnensystem': return s.score!=null?(s.score>=70?'good':s.score>=40?'mid':'bad'):'mid';
}
return 'mid';
}
function _liveMoodBadge(simId, s){
var m=_liveMood(simId,s);
if (!m) return '<span style="color:#8a8a8a">—</span>';
var map={ good:['#5a8a5e','Stark'], mid:['#c9913a','Solide'], bad:['#c07a6b','Viele Fehler'] };
var x=map[m]||map.mid;
return '<span style="display:inline-flex;align-items:center;gap:.3rem">'
+ '<span style="width:8px;height:8px;border-radius:50%;background:'+x[0]+';display:inline-block"></span>'
+ '<span style="font-size:.66rem;color:'+x[0]+';font-weight:700">'+x[1]+'</span></span>';
}
function _liveClicks(simId, s){
if (!s) return null;
if (typeof s.correctClicks==='number' || typeof s.wrongClicks==='number')
return { correct:s.correctClicks||0, wrong:s.wrongClicks||0 };
switch (simId){
case 'heli': if (s.planTotal) return { correct:s.planCorrect||0, wrong:Math.max(0,s.planTotal-(s.planCorrect||0)) }; break;
}
return null;
}
function _liveQualityBar(correct, wrong){
correct=Math.max(0,correct||0); wrong=Math.max(0,wrong||0);
var tot=correct+wrong;
if (!tot) return '<span style="color:#8a8a8a">—</span>';
var gp=Math.round(correct/tot*100), rp=100-gp;
var c=gp>=80?'#5a8a5e':gp>=55?'#c9913a':'#c07a6b';
return '<span title="'+correct+' richtig · '+wrong+' Fehler ('+gp+' % richtig)" style="display:inline-flex;align-items:center;gap:.35rem;font-variant-numeric:tabular-nums">'
+ '<span aria-hidden="true" style="display:inline-flex;width:56px;height:9px;border-radius:99px;overflow:hidden;background:#ece8dc">'
+ '<span style="width:'+gp+'%;background:#5a8a5e"></span><span style="width:'+rp+'%;background:#d98b7a"></span></span>'
+ '<span style="font-size:.72rem;font-weight:700;color:'+c+';min-width:2.3em;text-align:right">'+gp+' %</span></span>';
}
function _liveQualityFromState(simId, s){
var cw=_liveClicks(simId,s);
return cw ? _liveQualityBar(cw.correct,cw.wrong) : '<span style="color:#8a8a8a">—</span>';
}
function _liveFmt(v, unit, fieldDef, row){
if (fieldDef && typeof fieldDef.fmt==='function'){
try { var f=fieldDef.fmt(v,row); return (f===undefined||f===null||f==='')?'—':f; } catch(_){}
}
if (v===undefined||v===null||v==='') return '—';
if (typeof v==='object') return JSON.stringify(v).slice(0,30);
if (typeof v==='boolean') return v?'✓':'';
return v + (unit?' <span style="color:#8a8a8a;font-weight:400">'+unit+'</span>':'');
}
/* ============================================================
2) Spaltenschema — VERBATIM aus teacher.html (LIVE_PRIMARY_FIELDS)
============================================================ */
var LIVE_PRIMARY_FIELDS = {
farmer: [
{k:'year', label:'Jahr', bar:{kind:'abs',max:10,good:'mid'}, fmt:function(v){ return (v||0) + '<span style="color:#8a8a8a"> / 10</span>'; }},
{k:'market_de', label:'Markt', align:'left', fmt:function(v){ return v ? '🌍 '+esc(v) : '<span style="color:#8a8a8a">—</span>'; }},
{k:'cash', label:'Konto', bar:{kind:'rel',good:'signed'}, fmt:function(v){ if (v==null) return '<span style="color:#9a9a9a">—</span>'; var color=v<0?'#c0392b':'#5a8a5e'; return '<span style="color:'+color+';font-weight:700">'+Number(v).toLocaleString('de-AT')+' €</span>'; }},
{k:'hit_rate', label:'Trefferquote', unit:'%', bar:{kind:'abs',max:100,good:'high'}, fmt:function(v){ return (v!=null? v : 0); }},
{k:'crops_used', label:'Sorten', bar:{kind:'abs',max:10,good:'high'}, fmt:function(v){ return (v||0) + '<span style="color:#8a8a8a"> / 10</span>'; }},
{k:'countries', label:'Länder', bar:{kind:'rel',good:'high'}, fmt:function(v){ return (v!=null? v : 0); }},
{k:'fields', label:'Felder', bar:{kind:'rel',good:'mid'}, fmt:function(v){ return (v!=null? v : 0); }},
{k:'attempts', label:'Versuche',bar:{kind:'rel',good:'mid'}, fmt:function(v){ return (v!=null? v : 0); }},
{k:'co2_kg', label:'CO₂', unit:'kg', bar:{kind:'rel',good:'low'}, fmt:function(v){ if (v==null) return '<span style="color:#9a9a9a">—</span>'; return '<span style="font-variant-numeric:tabular-nums">'+Number(Math.round(v)).toLocaleString('de-AT')+'</span>'; }},
{k:'diseases', label:'Krankheiten', align:'center', bar:{kind:'abs',max:5,good:'low'}, fmt:function(v){ if (v==null) return '<span style="color:#9a9a9a">—</span>'; var color=(v>0)?'#c07a6b':'#8a8a8a'; return '<span style="color:'+color+';font-weight:'+(v>0?'700':'400')+'">'+v+'</span>'; }},
],
'tourismusregion': [
{k:'mapId', label:'Karte', align:'left', fmt:function(v){ return {talschleife:'🏞 Talschleife', seerunde:'🚣 Seerunde', serpentinen:'⛰ Serpentinen', kreuzung:'✚ Kreuzung', passstrasse:'🛣 Passstraße'}[v] || v || '—'; }},
{k:'wave', label:'Welle', fmt:function(v, row){ var s=row.state||{}; if (v===undefined) return '—'; var status=s.waveStatus==='pause'?' <span style="color:#8a8a8a">⏸ Baupause</span>':''; return v+'<span style="color:#8a8a8a"> / '+(s.waveTotal||8)+'</span>'+status; }},
{k:'season', label:'Saison', align:'center'},
{k:'budget', label:'Budget', fmt:function(v){ if (v===undefined) return '—'; var color=v<0?'#c0392b':'#5a8a5e'; return '<span style="color:'+color+';font-weight:700">'+Number(v).toLocaleString('de-AT')+' €</span>'; }},
{k:'revenue', label:'Wertsch.', unit:'€'},
{k:'avgStars', label:'Ø ★', fmt:function(v){ if (v===undefined) return '—'; var color=v>=3.5?'#5a8a5e':v>=2.5?'#c9913a':'#c07a6b'; return '<span style="color:'+color+';font-weight:700">'+v.toFixed(2)+'</span>'; }},
{k:'env', label:'Umwelt', fmt:function(v){ return _liveBar(v,0,100,'high'); }},
{k:'visitors', label:'Gäste'},
{k:'goalsDone', label:'Ziele', fmt:function(v, row){ var s=row.state||{}; if (v===undefined||!s.goalsTotal) return '—'; var color=v===s.goalsTotal?'#5a8a5e':v>=2?'#c9913a':'#8a8a8a'; return '<span style="color:'+color+';font-weight:700">'+v+'/'+s.goalsTotal+'</span>'; }},
],
'tourismustal': [
{k:'scene', label:'Szene', align:'left', fmt:function(v){ return {pionierzeit:'⛏️ 1975', boom:'📈 Boom 2000', klimawandel:'🌡️ 2035'}[v] || v || '—'; }},
{k:'season', label:'Saison', align:'center'},
{k:'year', label:'Jahr'},
{k:'money', label:'Konto', fmt:function(v){ if (v===undefined) return '—'; var color=v<0?'#c0392b':v<4000?'#c9913a':'#5a8a5e'; return '<span style="color:'+color+';font-weight:700">'+Number(v).toLocaleString('de-AT')+' €</span>'; }},
{k:'guestsToday', label:'Gäste'},
{k:'satisfaction', label:'Zufr.', fmt:function(v){ return _liveBar(v,0,100,'high'); }},
{k:'nature', label:'Natur', fmt:function(v){ return _liveBar(v,0,100,'high'); }},
{k:'goalsDone', label:'Ziele', fmt:function(v, row){ var s=row.state||{}; if (v===undefined||!s.goalsTotal) return '—'; var color=v===s.goalsTotal?'#5a8a5e':v>=2?'#c9913a':'#8a8a8a'; return '<span style="color:'+color+';font-weight:700">'+v+'/'+s.goalsTotal+'</span>'; }},
{k:'buildings', label:'Geb.'},
{k:'bottleneck', label:'Engpass', align:'left', fmt:function(v){ return v ? '<span style="color:#c07a6b">⚠️ '+v+'</span>' : '<span style="color:#8a8a8a">—</span>'; }},
],
klima: [
{k:'level', label:'Stufe'},
{k:'year', label:'Jahr'},
{k:'co2', label:'CO₂', unit:'ppm', fmt:function(v){ return _liveBar(v,280,700,'low'); }},
{k:'temp', label:'Temp', unit:'°C', fmt:function(v){ return _liveBar(v,14,20,'low',1); }},
{k:'floodedPct', label:'Geflutet', unit:'%', fmt:function(v){ if (v==null||isNaN(v)) return '<span style="color:#9a9a9a">—</span>'; var c=v<25?'#5a8a5e':v<=40?'#c9913a':'#c07a6b'; var pct=Math.max(0,Math.min(100,v)); return '<span style="display:inline-flex;align-items:center;gap:.35rem;font-variant-numeric:tabular-nums"><span aria-hidden="true" style="display:inline-block;width:42px;height:7px;border-radius:99px;background:rgba(0,0,0,.08);overflow:hidden"><span style="display:block;height:100%;width:'+pct.toFixed(0)+'%;background:'+c+'"></span></span><span style="color:'+c+';font-weight:700;font-size:.8rem;min-width:1.6em;text-align:right">'+Math.round(v)+'</span></span>'; }},
{k:'budget', label:'Budget', unit:'Mio €', fmt:function(v){ return _liveBar(v,-500,3000,'high'); }},
{k:'population', label:'Bev.', fmt:function(v, row){ var sp=(((row&&row.state)||{}).level>=3)?7000:6000; return _liveBar(v,sp*0.3,sp*1.6,'high'); }},
{k:'measures', label:'Maßn.'},
{k:'speed', label:'Speed', unit:'×'},
],
'sonnensystem': [
{k:'health', label:'Status', align:'center', fmt:function(v){ return LIVE_VALUE_LABELS.health[v] || '—'; }},
{k:'screen', label:'Modus', fmt:function(v){ return v==='play'?'📚 Aufgabe':'🚀 Erkunden'; }},
{k:'taskTitle', label:'Aufgabe', align:'left', fmt:function(v, row){ var s=row.state||{}; if (s.screen!=='play'||!v) return '<span style="color:#8a8a8a">—</span>'; return v+' <span style="color:#8a8a8a;font-weight:400">('+s.taskIndex+'/'+s.taskTotal+')</span>'; }},
{k:'completed', label:'Gelöst', fmt:function(v, row){ var s=row.state||{}; return s.taskTotal ? v+' <span style="color:#8a8a8a">/ '+s.taskTotal+'</span>' : '—'; }},
{k:'progressPct', label:'Quote', fmt:function(v){ return _liveBar(v,0,100,'high'); }},
{k:'simDate', label:'Sim-Datum', align:'center'},
{k:'achievementsUnlocked', label:'Erfolge', fmt:function(v, row){ var s=row.state||{}; if (v===undefined) return '—'; return v+' <span style="color:#8a8a8a">/ '+(s.achievementsTotal||6)+'</span>'; }},
],
'energiemanager': [
{k:'health', label:'Status', align:'center', fmt:function(v){ return LIVE_VALUE_LABELS.health[v] || '—'; }},
{k:'setTitle', label:'Set', align:'left', fmt:function(v, row){ var s=row.state||{}; var icon=s.setIcon?'<span style="margin-right:.25rem">'+s.setIcon+'</span>':''; var idxBadge=(s.setIndex&&s.setTotal)?' <span style="color:#8a8a8a;font-weight:400">'+s.setIndex+'/'+s.setTotal+'</span>':''; return v ? icon+v+idxBadge : '—'; }},
{k:'day', label:'Tag', fmt:function(v, row){ var s=row.state||{}; return s.dayTotal ? v+' <span style="color:#8a8a8a">/ '+s.dayTotal+'</span>' : '—'; }},
{k:'block', label:'Block', fmt:function(v, row){ var s=row.state||{}; if (s.phase==='blackout') return '<span style="color:#d8453a;font-weight:700">🔌 neu planen</span>'; if (s.phase==='plan') return '<span style="color:#8a8a8a">plant</span>'; return s.blockTotal ? (v+1)+' <span style="color:#8a8a8a">/ '+s.blockTotal+'</span>' : '—'; }},
{k:'blackouts', label:'🔌', align:'center', fmt:function(v){ if (!v) return '<span style="color:#8a8a8a"></span>'; var c=v>=2?'#d8453a':'#c9913a'; return '<span style="color:'+c+';font-weight:700">'+v+'</span>'; }},
{k:'balanced', label:'Tag ausg.', fmt:function(v, row){ var s=row.state||{}; if (s.phase!=='done') return '<span style="color:#8a8a8a">—</span>'; var color=v===s.blockTotal?'#5a8a5e':v>=4?'#c9913a':'#c07a6b'; return '<span style="color:'+color+';font-weight:700">'+v+'/'+s.blockTotal+'</span>'; }},
{k:'setBlocksBalanced', label:'Set ausg.', fmt:function(v, row){ var s=row.state||{}; var max=(s.setTotal&&s.dayTotal&&s.blockTotal)?s.dayTotal*s.blockTotal:0; if (!max) return '—'; var color=v>=max*0.87?'#5a8a5e':v>=max*0.5?'#c9913a':'#c07a6b'; return '<span style="color:'+color+';font-weight:700">'+v+'/'+max+'</span>'; }},
{k:'upperLake', label:'⬆ See', unit:'GWh'},
{k:'lowerLake', label:'⬇ See', unit:'GWh'},
],
'eu-werkstatt': [
{k:'health', label:'Status', align:'center', fmt:function(v){ return LIVE_VALUE_LABELS.health[v] || '—'; }},
{k:'scenarioTitle', label:'Szenario', align:'left', fmt:function(v, row){ var s=row.state||{}; var icon=s.scenarioIcon?'<span style="margin-right:.25rem">'+s.scenarioIcon+'</span>':''; return v ? icon+v : '<span style="color:#8a8a8a">Szenario-Auswahl</span>'; }},
{k:'step', label:'Schritt', fmt:function(v, row){ var s=row.state||{}; if (!s.stepsTotal) return '—'; return v+' <span style="color:#8a8a8a">/ '+s.stepsTotal+'</span>'; }},
{k:'difficulty', label:'Modus', fmt:function(v){ return LIVE_VALUE_LABELS.difficulty[v] || '—'; }},
{k:'firstTryHits', label:'Treffer'},
{k:'muelleimerCount', label:'Mülleimer', fmt:function(v){ if (v===undefined||v===null) return '—'; var color=v===0?'#5a8a5e':v>=3?'#c07a6b':'#c9913a'; return '<span style="color:'+color+';font-weight:700">'+v+'</span>'; }},
{k:'reshuffles', label:'🔄 Probier', align:'center', fmt:function(v){ if (!v) return '<span style="color:#8a8a8a"></span>'; var color=v>=5?'#c07a6b':v>=2?'#c9913a':'#5a8a5e'; return '<span style="color:'+color+';font-weight:700">'+v+'</span>'; }},
{k:'secondsElapsed', label:'Dauer', fmt:function(v){ return v ? _fmtSeconds(v) : '—'; }},
{k:'achievementsUnlocked', label:'Erfolge', fmt:function(v, row){ var s=row.state||{}; if (v===undefined||v===null) return '—'; return v+' <span style="color:#8a8a8a">/ '+(s.achievementsTotal||6)+'</span>'; }},
],
'weltkueche': [
{k:'dishName', label:'Gericht', align:'left', fmt:function(v){ return v ? '<b>'+esc(v)+'</b>' : '<span style="color:#8a8a8a">Auswahl</span>'; }},
{k:'completed', label:'Gerichte', align:'center', fmt:function(v, row){ var s=row.state||{}; var t=s.completedTotal||0; if (!t) return '<span style="color:#8a8a8a">—</span>'; var done=v||0; var c=done>=t?'#5a8a5e':'#3d5165'; return '<b style="color:'+c+'">'+done+'</b><span style="color:#8a8a8a"> / '+t+'</span>'; }},
{k:'ingSolved', label:'Zutaten akt.', fmt:function(v, row){ var s=row.state||{}; var t=s.ingTotal||0; if (!s.dishName||!t) return '<span style="color:#8a8a8a">—</span>'; var done=v||0; var pct=done/t; var c=pct>=1?'#5a8a5e':pct>=.5?'#c9913a':'#8a7a4a'; return '<span style="color:'+c+';font-weight:700">'+done+' <span style="color:#8a8a8a;font-weight:400">/ '+t+'</span></span>'; }},
{k:'__quality', label:'Qualität', align:'left', fmt:function(v, row){ return _liveQualityFromState(row.module_id, row.state); }},
{k:'mistakesInDish', label:'Fehler (akt.)', align:'center', fmt:function(v){ var n=v||0; var c=n>=12?'#c07a6b':n>=5?'#c9913a':'#5a8a5e'; return '<span style="color:'+c+';font-weight:700">'+n+'</span>'; }},
{k:'__lage', label:'Lage', align:'center', fmt:function(v, row){ return _liveMoodBadge(row.module_id, row.state); }},
],
'busfahrt': [
{k:'levelId', label:'Stufe', align:'center'},
{k:'orderIndex', label:'Auftrag', fmt:function(v, row){ var s=row.state||{}; return s.totalOrders ? (v+1)+' / '+s.totalOrders : '—'; }},
{k:'budget', label:'Budget', unit:'€', fmt:function(v){ if (v==null) return '—'; var c=v>=5000?'#5a8a5e':v>=0?'#c9913a':'#c07a6b'; return '<span style="color:'+c+';font-weight:700">'+v.toLocaleString('de-AT')+' €</span>'; }},
{k:'wonOrders', label:'Aufträge ✓'},
{k:'lastCityTitle', label:'Letzte Stadt', align:'left'},
{k:'lastMargin', label:'Letzte Marge', unit:'€'},
{k:'quizFirstTry', label:'🧠 1.-Versuch', fmt:function(v, row){ var s=row.state||{}; var t=s.orderIndex||0; return t ? v+' / '+t : '—'; }},
],
'fluggesellschaft': [
{k:'levelId', label:'Stufe', align:'center'},
{k:'orderIndex', label:'Flug', fmt:function(v, row){ var s=row.state||{}; return s.totalOrders ? (v+1)+' / '+s.totalOrders : '—'; }},
{k:'budget', label:'Budget', unit:'€', fmt:function(v){ if (v==null) return '—'; var c=v>=8000?'#5a8a5e':v>=0?'#c9913a':'#c07a6b'; return '<span style="color:'+c+';font-weight:700">'+v.toLocaleString('de-AT')+' €</span>'; }},
{k:'wonOrders', label:'Flüge ✓'},
{k:'co2Kg', label:'🌍 CO₂', fmt:function(v){ return v==null ? '—' : (v/1000).toFixed(1)+' t'; }},
{k:'lastCityTitle', label:'Letztes Ziel', align:'left'},
{k:'lastTzDiff', label:'Zeitzone', fmt:function(v){ if (v==null) return '—'; return (v>0?'+':'')+v+' h'; }},
{k:'quizFirstTry', label:'🧠 1.-Versuch', fmt:function(v, row){ var s=row.state||{}; var t=s.orderIndex||0; return t ? v+' / '+t : '—'; }},
],
'heli': [
{k:'phase', label:'Phase', align:'center', fmt:function(v){ var m={mission:'📋 Auftragswahl', plan:'🗺️ Routenplan', briefing:'📋 Briefing', start:'🛫 Start', flight:'✈️ Flug', approach:'🎯 Anflug', landing:'🛬 Landung', results:'🏁 Ende'}; return m[v] || (v ? v : '—'); }},
{k:'missionId', label:'Auftrag', align:'left', fmt:function(v, row){ var s=row.state||{}; var title=s.missionTitle||''; return v ? '<b>'+v+'</b>'+(title?' <span style="color:#8a8a8a;font-weight:400">'+title+'</span>':'') : '—'; }},
{k:'planIndex', label:'🗺️ Wegpunkt', fmt:function(v, row){ var s=row.state||{}; var t=s.planTotal||0; if (!t) return '—'; var done=(v||0); var pct=done/t; var c=pct>=1?'#5a8a5e':pct>=.5?'#c9913a':'#c07a6b'; return '<span style="color:'+c+';font-weight:700">'+done+' / '+t+'</span>'; }},
{k:'planCorrect', label:'🎯 Treffer', fmt:function(v, row){ var s=row.state||{}; var t=s.planTotal||0; if (!t) return '—'; var done=(v||0); var pct=done/t; var c=pct>=.9?'#5a8a5e':pct>=.5?'#c9913a':'#c07a6b'; return '<span style="color:'+c+';font-weight:700">'+done+' / '+t+'</span>'; }},
{k:'planAttempts', label:'❌ Fehlkl.', align:'center', fmt:function(v){ var n=v||0; var c=n<=2?'#5a8a5e':n<=8?'#c9913a':'#c07a6b'; return n===0 ? '<span style="color:#5a8a5e;font-weight:700">0</span>' : '<span style="color:'+c+';font-weight:700">'+n+'</span>'; }},
{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 ? '—' : '<span style="color:'+c+';font-weight:700">'+'★'.repeat(n)+'<span style="color:#ccc">'+'★'.repeat(5-n)+'</span></span>'; }},
{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 ? '—' : '<span style="color:'+c+';font-weight:700">'+'★'.repeat(n)+'<span style="color:#ccc">'+'★'.repeat(5-n)+'</span></span>'; }},
{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 ? '—' : '<span style="color:'+c+';font-weight:800;font-size:.82rem">'+'★'.repeat(n)+'<span style="color:#ccc">'+'★'.repeat(5-n)+'</span></span>'; }},
{k:'failed', label:'Status', align:'center', fmt:function(v, row){ var s=row.state||{}; if (v===true) return '<span style="color:#c07a6b;font-weight:700">⚠ Chefpilot</span>'; if (s.phase==='results') return '<span style="color:#5a8a5e;font-weight:700">✓ Erfolg</span>'; if (s.phase==='mission'||!s.phase){ if (s.lastResult) return s.lastFailed ? '<span style="color:#c07a6b;font-weight:600" title="Werte = letzter Einsatz">⚠ letzter Einsatz</span>' : '<span style="color:#5a8a5e;font-weight:600" title="Werte = letzter Einsatz">✓ letzter Einsatz</span>'; return '<span style="color:#8a8a8a">wählt Auftrag</span>'; } return '<span style="color:#4a7c8a;font-weight:600">🟢 unterwegs</span>'; }},
],
'fluss': [
{k:'health', label:'Status', align:'center', fmt:function(v){ return LIVE_VALUE_LABELS.health[v] || '—'; }},
{k:'levelId', label:'Stufe', align:'center'},
{k:'year', label:'Jahr', fmt:function(v, row){ var s=row.state||{}; var max=100; return s.progressPct!=null ? (v||0)+' <span style="color:#8a8a8a">/ '+max+'</span>' : (v||'—'); }},
{k:'score', label:'🏅 Score', fmt:function(v){ return _liveBar(v,0,100,'high'); }},
{k:'progressPct', label:'Fortschritt', fmt:function(v){ return _liveBar(v,0,100,'mid'); }},
{k:'budget', label:'Budget', unit:'Mio €', fmt:function(v){ if (v==null) return '—'; var c=v>=20?'#5a8a5e':v>=0?'#c9913a':'#c07a6b'; return '<span style="color:'+c+';font-weight:700">'+v+'</span>'; }},
{k:'biodiv', label:'🌳 Biodiv.', fmt:function(v){ return _liveBar(v,0,100,'high'); }},
{k:'economy', label:'💰 Wirtsch.', fmt:function(v){ return _liveBar(v,0,100,'high'); }},
{k:'flood', label:'🛡️ Hochw.-Schutz', fmt:function(v){ return _liveBar(v,0,100,'high'); }},
{k:'settlementsLost', label:'🏚️ Verlust', align:'center', fmt:function(v){ if (!v) return '<span style="color:#5a8a5e">0</span>'; var c=v>=3?'#c07a6b':'#c9913a'; return '<span style="color:'+c+';font-weight:700">'+v+'</span>'; }},
],
'kofferdetektiv': [
{k:'health', label:'Status', align:'center', fmt:function(v){ return LIVE_VALUE_LABELS.health[v] || '—'; }},
{k:'phase', label:'Phase'},
{k:'progressPct', label:'Fortschritt', fmt:function(v){ return _liveBar(v,0,100,'mid'); }},
{k:'score', label:'🏅 Score', fmt:function(v){ return _liveBar(v,0,100,'high'); }},
{k:'cases_solved', label:'Gelöst', align:'center', fmt:function(v, row){ var s=row.state||{}; var t=s.cases_total||0; return t ? v+' <span style="color:#8a8a8a">/ '+t+'</span>' : (v||'—'); }},
{k:'case_iso2', label:'🎯 Ziel', align:'left', fmt:function(v, row){ var s=row.state||{}; if (!v) return '—'; var iso=String(v).toUpperCase(); if (s.case_country) return '<span style="font-weight:600;color:#5a8a5e">'+esc(s.case_country)+'</span>'; return '<span style="font-weight:600;color:#b04a3e" title="Antwort verdeckt — Schüler:in sucht noch">'+iso+' (suchend)</span>'; }},
{k:'hints_revealed', label:'Hinweise', align:'center'},
{k:'wrong_guesses_total', label:'❌ Fehler', fmt:function(v){ return _liveBar(v,0,10,'low'); }},
{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'); }},
],
};
/* ============================================================
3) Reihenfolge, Titel/Icons + Grün-Bewertung je Sim
GREEN = Keys, die NACH Abschluss noch relevant sind
(Ergebnis · Fortschritt/Abbruch · Aufgaben-Identität).
Alles andere = nur live.
============================================================ */
var SIM_META = [
{id:'farmer', icon:'🌾', title:'Farmer'},
{id:'klima', icon:'🌍', title:'Klimawächter'},
{id:'fluss', icon:'🌊', title:'Flussmanagement'},
{id:'energiemanager', icon:'⚡', title:'Energiemanager'},
{id:'eu-werkstatt', icon:'🇪🇺', title:'EU-Werkstatt'},
{id:'weltkueche', icon:'🍽️', title:'Weltküche'},
{id:'busfahrt', icon:'🚌', title:'Busfahrt'},
{id:'fluggesellschaft', icon:'✈️', title:'Fluggesellschaft'},
{id:'heli', icon:'🚁', title:'Hubschrauber-Rettung'},
{id:'kofferdetektiv', icon:'🧳', title:'Kofferdetektiv'},
{id:'sonnensystem', icon:'🪐', title:'Sonnensystem'},
{id:'tourismustal', icon:'🏔️', title:'Tourismustal'},
{id:'tourismusregion', icon:'🗺️', title:'Tourismusregion'},
{id:'logistik', icon:'📦', title:'Logistik'},
];
var GREEN = {
farmer: ['year','cash','hit_rate','crops_used','countries','fields','attempts','co2_kg','diseases'],
klima: ['level','year','co2','temp','floodedPct','budget','population','measures'],
fluss: ['levelId','year','score','progressPct','budget','biodiv','economy','flood','settlementsLost'],
energiemanager: ['setTitle','day','blackouts','balanced','setBlocksBalanced'],
'eu-werkstatt': ['scenarioTitle','step','difficulty','firstTryHits','muelleimerCount','reshuffles','secondsElapsed','achievementsUnlocked'],
weltkueche: ['completed','__quality'],
busfahrt: ['levelId','orderIndex','budget','wonOrders','quizFirstTry'],
fluggesellschaft: ['levelId','orderIndex','budget','wonOrders','co2Kg','quizFirstTry'],
heli: ['missionId','planIndex','planCorrect','planAttempts','startScore','landingScore','totalStars','failed'],
kofferdetektiv: ['progressPct','score','cases_solved','hints_revealed','wrong_guesses_total'],
sonnensystem: ['completed','progressPct','achievementsUnlocked'],
tourismustal: ['scene','year','money','satisfaction','nature','goalsDone','buildings'],
tourismusregion: ['mapId','wave','budget','revenue','avgStars','env','goalsDone'],
logistik: ['levelId','balance','ordersDelivered','ordersLate','progressPct'],
};
// Kurzbegründung, warum ein Grau-Key nur live relevant ist.
var GREY_REASON = {
market_de:'aktueller Handelsmarkt — wechselt ständig',
season:'momentane Jahreszeit im Spiel',
visitors:'Gäste im aktuellen Moment',
guestsToday:'Gäste nur am heutigen Sim-Tag',
bottleneck:'aktueller Engpass — ändert sich laufend',
speed:'Simulationstempo (Bedien-Einstellung, keine Leistung)',
health:'Live-Ampel des Moments — das Ergebnis steckt in Score/Sternen',
screen:'was gerade offen ist (Aufgabe/Erkunden)',
taskTitle:'aktuell offene Aufgabe',
simDate:'gerade beobachtetes Datum',
block:'aktueller Zeitblock im Tag',
upperLake:'momentaner Speicherstand (oberer See)',
lowerLake:'momentaner Speicherstand (unterer See)',
dishName:'aktuell gekochtes Gericht',
ingSolved:'Zutaten im aktuellen Gericht (setzt sich je Gericht zurück)',
mistakesInDish:'Fehler nur im aktuellen Gericht (setzt sich zurück)',
__lage:'Live-Stimmung — Ergebnis steckt in Qualität/Gerichte',
lastCityTitle:'zuletzt angefahrenes Ziel',
lastMargin:'Marge des letzten Auftrags',
lastTzDiff:'Zeitzonendifferenz des letzten Ziels',
phase:'aktueller Spielabschnitt',
case_iso2:'aktuelles Zielland — bewusst verdeckt, nur live',
time_left_s:'Restzeit im laufenden Fall',
airport:'aktueller Standort',
ordersInFlight:'Aufträge gerade unterwegs',
ordersOpen:'offene Aufträge im Moment',
};
/* ============================================================
4) Testdaten je Sim (3 Schüler:innen: stark · schwach · mittel)
============================================================ */
var NOW = Date.now();
function R(name, emoji, state, o){
o = o || {};
return {
displayName:name, emoji:emoji, module_id:state.__mod, state:state,
started_at_ms: NOW - (o.sinceMin||10)*60000,
last_seen_ms: NOW - (o.seenSec!=null?o.seenSec:4)*1000,
};
}
// missionStartedAt in ms setzen (für die „Seit"-Zeile)
function withMission(state, missMin){ if (missMin!=null) state.missionStartedAt = NOW - missMin*60000; return state; }
var TEST = {
farmer: [
R('Anna','🦊', {__mod:'farmer', year:8, market_de:'Ägypten', cash:14200, hit_rate:78, crops_used:7, countries:5, fields:9, attempts:12, co2_kg:3400, diseases:1}, {sinceMin:24, seenSec:3}),
R('Ben','🐼', {__mod:'farmer', year:4, market_de:'Indien', cash:-800, hit_rate:22, crops_used:3, countries:2, fields:5, attempts:14, co2_kg:5200, diseases:3}, {sinceMin:16, seenSec:6}),
R('Clara','🦉',{__mod:'farmer', year:6, market_de:'Brasilien', cash:5100, hit_rate:54, crops_used:5, countries:4, fields:7, attempts:9, co2_kg:4100, diseases:2}, {sinceMin:19, seenSec:140}),
],
klima: [
R('Anna','🦊', {__mod:'klima', level:2, year:2068, co2:520, temp:16.8, floodedPct:18, budget:1200, population:6400, measures:11, speed:2}, {sinceMin:14}),
R('Ben','🐼', {__mod:'klima', level:1, year:2041, co2:610, temp:17.9, floodedPct:47, budget:-220, population:5200, measures:5, speed:1}, {sinceMin:9, seenSec:5}),
R('Clara','🦉',{__mod:'klima', level:3, year:2085, co2:430, temp:15.9, floodedPct:31, budget:2600, population:8100, measures:16, speed:3}, {sinceMin:22, seenSec:130}),
],
fluss: [
R('Anna','🦊', {__mod:'fluss', health:'good', levelId:'L2', year:64, progressPct:64, score:78, budget:32, biodiv:72, economy:66, flood:81, settlementsLost:0}, {sinceMin:17}),
R('Ben','🐼', {__mod:'fluss', health:'struggling', levelId:'L1', year:20, progressPct:20, score:34, budget:-4, biodiv:41, economy:38, flood:52, settlementsLost:3}, {sinceMin:8, seenSec:7}),
R('Clara','🦉',{__mod:'fluss', health:'ok', levelId:'L2', year:100, progressPct:100, score:88, budget:18, biodiv:80, economy:74, flood:90, settlementsLost:1}, {sinceMin:26, seenSec:150}),
],
energiemanager: [
R('Anna','🦊', {__mod:'energiemanager', health:'good', setTitle:'Frühling', setIcon:'🌱', setIndex:1, setTotal:6, day:3, dayTotal:3, phase:'done', block:7, blockTotal:8, blackouts:0, balanced:8, setBlocksBalanced:21, upperLake:32, lowerLake:28}, {sinceMin:15}),
R('Ben','🐼', {__mod:'energiemanager', health:'struggling', setTitle:'Sommer', setIcon:'🌞', setIndex:2, setTotal:6, day:2, dayTotal:3, phase:'blackout', block:4, blockTotal:8, blackouts:2, balanced:0, setBlocksBalanced:9, upperLake:5, lowerLake:55}, {sinceMin:11, seenSec:6}),
R('Clara','🦉',{__mod:'energiemanager', health:'ok', setTitle:'Herbst', setIcon:'🍂', setIndex:3, setTotal:6, day:1, dayTotal:3, phase:'done', block:5, blockTotal:8, blackouts:1, balanced:6, setBlocksBalanced:6, upperLake:40, lowerLake:20}, {sinceMin:20, seenSec:135}),
],
'eu-werkstatt': [
R('Anna','🦊', {__mod:'eu-werkstatt', health:'good', scenarioTitle:'Käfige abschaffen', scenarioIcon:'🐔', step:6, stepsTotal:6, difficulty:'easy', firstTryHits:6, muelleimerCount:0, reshuffles:0, secondsElapsed:214, achievementsUnlocked:4, achievementsTotal:6}, {sinceMin:6}),
R('Ben','🐼', {__mod:'eu-werkstatt', health:'struggling', scenarioTitle:'Käfige abschaffen', scenarioIcon:'🐔', step:2, stepsTotal:6, difficulty:'easy', firstTryHits:1, muelleimerCount:3, reshuffles:2, secondsElapsed:150, achievementsUnlocked:1, achievementsTotal:6, attemptsThisStep:6}, {sinceMin:5, seenSec:6}),
R('Clara','🦉',{__mod:'eu-werkstatt', health:'ok', scenarioTitle:'Ein Kabel für alle', scenarioIcon:'🔌', step:3, stepsTotal:5, difficulty:'expert', firstTryHits:2, muelleimerCount:1, reshuffles:6, secondsElapsed:330, achievementsUnlocked:2, achievementsTotal:6}, {sinceMin:9, seenSec:150}),
],
weltkueche: [
R('Anna','🦊', {__mod:'weltkueche', dishName:'Tiroler Gröstl', completed:4, completedTotal:6, ingSolved:5, ingTotal:5, correctClicks:38, wrongClicks:4, mistakesInDish:1}, {sinceMin:13}),
R('Ben','🐼', {__mod:'weltkueche', dishName:'Moussaka', completed:1, completedTotal:6, ingSolved:2, ingTotal:6, correctClicks:12, wrongClicks:9, mistakesInDish:7}, {sinceMin:7, seenSec:5}),
R('Clara','🦉',{__mod:'weltkueche', dishName:'Sushi', completed:2, completedTotal:6, ingSolved:6, ingTotal:6, correctClicks:22, wrongClicks:14, mistakesInDish:12}, {sinceMin:10, seenSec:145}),
],
busfahrt: [
R('Anna','🦊', {__mod:'busfahrt', levelId:'L2', orderIndex:8, totalOrders:12, budget:9200, wonOrders:8, lastCityTitle:'Salzburg', lastMargin:420, quizFirstTry:6}, {sinceMin:18}),
R('Ben','🐼', {__mod:'busfahrt', levelId:'L1', orderIndex:3, totalOrders:10, budget:-300, wonOrders:2, lastCityTitle:'Graz', lastMargin:-120, quizFirstTry:1}, {sinceMin:9, seenSec:6}),
R('Clara','🦉',{__mod:'busfahrt', levelId:'L2', orderIndex:5, totalOrders:12, budget:5400, wonOrders:5, lastCityTitle:'Wien', lastMargin:260, quizFirstTry:4}, {sinceMin:14, seenSec:150}),
],
fluggesellschaft: [
R('Anna','🦊', {__mod:'fluggesellschaft', levelId:'L4', orderIndex:6, totalOrders:9, budget:14200, wonOrders:6, co2Kg:8400, lastCityTitle:'Tokio', lastTzDiff:8, quizFirstTry:5}, {sinceMin:21}),
R('Ben','🐼', {__mod:'fluggesellschaft', levelId:'L4', orderIndex:2, totalOrders:9, budget:-500, wonOrders:1, co2Kg:3200, lastCityTitle:'Kairo', lastTzDiff:1, quizFirstTry:1}, {sinceMin:8, seenSec:5}),
R('Clara','🦉',{__mod:'fluggesellschaft', levelId:'L4', orderIndex:4, totalOrders:9, budget:7600, wonOrders:4, co2Kg:5600, lastCityTitle:'New York', lastTzDiff:-6, quizFirstTry:3}, {sinceMin:12, seenSec:150}),
],
heli: [
R('Anna','🦊', withMission({__mod:'heli', phase:'results', missionId:'M19', missionTitle:'Seerettung', planIndex:6, planTotal:6, planCorrect:6, planAttempts:1, startScore:5, landingScore:4, totalStars:5, failed:false}, 5), {sinceMin:12}),
R('Ben','🐼', withMission({__mod:'heli', phase:'flight', missionId:'M7', missionTitle:'Kufstein', planIndex:3, planTotal:5, planCorrect:2, planAttempts:11, startScore:3, landingScore:null, totalStars:null, failed:false}, 3), {sinceMin:8, seenSec:6}),
R('Clara','🦉',withMission({__mod:'heli', phase:'mission', missionId:'M5', missionTitle:'Bregenz', planIndex:0, planTotal:5, planCorrect:0, planAttempts:0, startScore:null, landingScore:null, totalStars:null, failed:false, lastResult:true, lastFailed:false}, 1), {sinceMin:14, seenSec:150}),
],
kofferdetektiv: [
R('Anna','🦊', {__mod:'kofferdetektiv', health:'good', phase:'suche', progressPct:60, score:82, cases_solved:6, cases_total:10, case_iso2:'IT', hints_revealed:1, wrong_guesses_total:2, time_left_s:180, airport:'Rom-Fiumicino'}, {sinceMin:16}),
R('Ben','🐼', {__mod:'kofferdetektiv', health:'ok', phase:'suche', progressPct:30, score:55, cases_solved:3, cases_total:10, case_iso2:'BR', hints_revealed:4, wrong_guesses_total:7, time_left_s:45, airport:'São Paulo-Guarulhos'}, {sinceMin:11, seenSec:6}),
R('Clara','🦉',{__mod:'kofferdetektiv', health:'struggling', phase:'briefing', progressPct:10, score:28, cases_solved:1, cases_total:10, case_iso2:'JP', hints_revealed:6, wrong_guesses_total:9, time_left_s:20, airport:'Tokio-Narita'}, {sinceMin:6, seenSec:150}),
],
sonnensystem: [
R('Anna','🦊', {__mod:'sonnensystem', health:'good', screen:'play', taskTitle:'Mondphasen', taskIndex:8, taskTotal:20, completed:8, progressPct:40, simDate:'14.03.', achievementsUnlocked:3, achievementsTotal:8}, {sinceMin:13}),
R('Ben','🐼', {__mod:'sonnensystem', health:'ok', screen:'explore', taskTitle:'', taskIndex:0, taskTotal:20, completed:14, progressPct:70, simDate:'22.09.', achievementsUnlocked:5, achievementsTotal:8}, {sinceMin:19, seenSec:5}),
R('Clara','🦉',{__mod:'sonnensystem', health:'struggling', screen:'play', taskTitle:'Sonnenfinsternis', taskIndex:3, taskTotal:20, completed:2, progressPct:10, simDate:'02.07.', achievementsUnlocked:1, achievementsTotal:8}, {sinceMin:7, seenSec:150}),
],
tourismustal: [
R('Anna','🦊', {__mod:'tourismustal', scene:'boom', season:'Sommer', year:2004, money:8200, guestsToday:180, satisfaction:74, nature:52, goalsDone:2, goalsTotal:3, buildings:6, bottleneck:''}, {sinceMin:20}),
R('Ben','🐼', {__mod:'tourismustal', scene:'klimawandel', season:'Winter', year:2037, money:-600, guestsToday:60, satisfaction:38, nature:41, goalsDone:1, goalsTotal:3, buildings:9, bottleneck:'Schnee'}, {sinceMin:10, seenSec:6}),
R('Clara','🦉',{__mod:'tourismustal', scene:'pionierzeit', season:'Sommer', year:1978, money:4300, guestsToday:120, satisfaction:63, nature:78, goalsDone:1, goalsTotal:3, buildings:3, bottleneck:''}, {sinceMin:15, seenSec:150}),
],
tourismusregion: [
R('Anna','🦊', {__mod:'tourismusregion', mapId:'talschleife', wave:8, waveTotal:8, budget:22000, revenue:180000, avgStars:3.8, env:72, visitors:540, goalsDone:3, goalsTotal:3}, {sinceMin:23}),
R('Ben','🐼', {__mod:'tourismusregion', mapId:'seerunde', wave:3, waveTotal:8, waveStatus:'pause', budget:-400, revenue:42000, avgStars:2.1, env:34, visitors:210, goalsDone:1, goalsTotal:3}, {sinceMin:9, seenSec:6}),
R('Clara','🦉',{__mod:'tourismusregion', mapId:'serpentinen', wave:5, waveTotal:8, budget:8600, revenue:96000, avgStars:2.9, env:58, visitors:330, goalsDone:2, goalsTotal:3}, {sinceMin:16, seenSec:150}),
],
logistik: [
R('Anna','🦊', {__mod:'logistik', levelId:'L1', balance:9200, ordersDelivered:8, totalOrders:12, ordersInFlight:2, ordersOpen:2, ordersLate:0, progressPct:66}, {sinceMin:18}),
R('Ben','🐼', {__mod:'logistik', levelId:'L2', balance:-300, ordersDelivered:3, totalOrders:14, ordersInFlight:3, ordersOpen:8, ordersLate:2, progressPct:21}, {sinceMin:9, seenSec:6}),
R('Clara','🦉',{__mod:'logistik', levelId:'L1', balance:5400, ordersDelivered:6, totalOrders:12, ordersInFlight:1, ordersOpen:5, ordersLate:1, progressPct:50}, {sinceMin:13, seenSec:150}),
],
};
/* ============================================================
5) Rendern — Tabelle wie renderLive(), plus Grün/Grau + Keys
============================================================ */
var _LIVE_PAUSE_SEC = 90;
function _liveIsActive(ls){ return ls.last_seen_ms && (NOW - ls.last_seen_ms) <= _LIVE_PAUSE_SEC*1000; }
function renderSim(meta){
var fields = LIVE_PRIMARY_FIELDS[meta.id] || [];
var rows = TEST[meta.id] || [];
var greenSet = {}; (GREEN[meta.id]||[]).forEach(function(k){ greenSet[k]=true; });
var greyKeys = fields.filter(function(f){ return !greenSet[f.k]; }).map(function(f){ return f.k; });
var greenCount = fields.length - greyKeys.length;
// Spalten-Maxima für relative Mini-Balken
var colMax = {};
fields.forEach(function(f){ var mx=0; rows.forEach(function(r){ var val=(r.state||{})[f.k]; if (typeof val==='number' && !isNaN(val)) mx=Math.max(mx,Math.abs(val)); }); colMax[f.k]=mx; });
var h = '<div class="sim" id="sim-'+meta.id+'">';
h += '<div class="sim-head"><span class="emoji">'+meta.icon+'</span><h3>'+esc(meta.title)+'</h3>'
+ '<span class="sid">'+meta.id+'</span>'
+ '<span class="count">'+greenCount+' grün · '+greyKeys.length+' nur live</span></div>';
h += '<div class="tbl-scroll"><table class="live"><thead><tr>';
h += '<th style="text-align:left">Schüler*in<span class="key">displayName</span></th>';
h += '<th style="text-align:left;color:#8a8a8a">Seit<span class="key">missionStartedAt / started_at</span></th>';
fields.forEach(function(f){
var align=f.align||'right';
var isG=!!greenSet[f.k];
h += '<th class="'+(isG?'green':'grey')+'" style="text-align:'+align+'">'
+ esc(f.label)+(f.unit?' <span style="color:#8a8a8a;font-weight:400;font-size:.66rem">'+f.unit+'</span>':'')
+ '<span class="key">'+esc(f.k)+'</span>'
+ '<span class="tag '+(isG?'g':'l')+'">'+(isG?'nach Abschluss':'nur live')+'</span>'
+ '</th>';
});
h += '<th style="width:1.2em"></th></tr></thead><tbody>';
rows.forEach(function(ls){
var s = ls.state||{};
var avatarHtml = '<span style="font-size:1.1rem">'+(ls.emoji||'🧑‍🎓')+'</span>';
var sinceSimBase = ls.started_at_ms||0;
var sinceMissionBase = (s.missionStartedAt)||(s.levelStartedAt)||(s.scenarioStartedAt)||0;
var sinceSim = sinceSimBase ? Math.max(0,Math.round((NOW-sinceSimBase)/1000)) : 0;
var sinceMission = sinceMissionBase ? Math.max(0,Math.round((NOW-sinceMissionBase)/1000)) : 0;
var simStr = sinceSimBase ? _fmtSince(sinceSim) : '—';
var missionStr = sinceMissionBase ? _fmtSince(sinceMission) : '—';
var lastSeen = ls.last_seen_ms ? Math.max(0,Math.round((NOW-ls.last_seen_ms)/1000)) : 0;
var paused = !_liveIsActive(ls) || !!(s.__idlePaused||s.__paused);
var dotColor = lastSeen<8?'#5a8a5e':lastSeen<20?'#e8833a':'#c07a6b';
var help = !paused && _liveNeedsHelp(ls.module_id, s);
var restBg = help ? '#fdf3ee' : (paused ? '#f6f5f2' : '');
var accent = help ? 'inset 3px 0 0 #c0574a' : (paused ? 'inset 3px 0 0 #cfcabb' : 'none');
h += '<tr style="background:'+restBg+';'+(paused?'color:#9a9a9a;':'')+'box-shadow:'+accent+'">';
h += '<td style="white-space:nowrap"><span style="display:inline-flex;align-items:center;gap:.4rem">'+(help?'<span title="stockt · braucht evtl. Hilfe">🆘</span>':(paused?'<span title="pausiert">⏸</span>':''))+avatarHtml+'<strong>'+esc(ls.displayName)+'</strong></span></td>';
h += '<td style="white-space:nowrap;line-height:1.15"><div style="font-size:.78rem;color:#3a3a3a;font-weight:600">'+missionStr+'</div><div style="font-size:.62rem;color:#8a8a8a">'+simStr+' ges.</div></td>';
fields.forEach(function(f){
var align=f.align||'right';
var isG=!!greenSet[f.k];
var cell=_liveFmt(s[f.k], f.unit, f, ls);
if (f.bar){
var raw=s[f.k];
if (typeof raw==='number' && !isNaN(raw)){
var pct=0, bcol;
if (f.bar.kind==='abs'){ pct=(raw/(f.bar.max||100))*100; bcol=_liveBarColor(pct,f.bar.good); }
else { var mx=colMax[f.k]||0; pct=mx>0?(Math.abs(raw)/mx)*100:0; bcol=(f.bar.good==='signed')?(raw<0?'#c0392b':'#5a8a5e'):_liveBarColor(pct,f.bar.good); }
cell='<div style="line-height:1.1">'+cell+'</div>'+_liveMiniBar(pct,bcol);
}
} else if (typeof s[f.k]==='number' && !isNaN(s[f.k]) && (colMax[f.k]||0)>0 && cell.indexOf('border-radius:99px')<0){
var apct=(Math.abs(s[f.k])/colMax[f.k])*100;
cell='<div style="line-height:1.1">'+cell+'</div>'+_liveMiniBar(apct,'#9aa7b0');
}
h += '<td class="'+(isG?'green':'')+'" style="text-align:'+align+'">'+cell+'</td>';
});
h += '<td style="text-align:center">'+(paused
? '<span style="font-size:.6rem;color:#9a9a9a;white-space:nowrap">⏸ '+_fmtSince(lastSeen)+'</span>'
: '<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:'+dotColor+'"></span>')+'</td>';
h += '</tr>';
});
h += '</tbody></table></div>';
// Nach-Abschluss-Hinweis
if (greyKeys.length){
h += '<div class="note"><b>Nach Abschluss NICHT relevant (nur live):</b><ul>';
greyKeys.forEach(function(k){
var lbl=(fields.filter(function(f){return f.k===k;})[0]||{}).label||k;
h += '<li><code>'+esc(k)+'</code> · '+esc(lbl)+' — '+esc(GREY_REASON[k]||'momentaner Zustand, ändert sich laufend')+'</li>';
});
h += '</ul></div>';
} else {
h += '<div class="allgreen">✓ Alle Spalten dieser Sim bleiben auch nach Abschluss relevant.</div>';
}
h += '</div>';
return h;
}
// TOC + Inhalt aufbauen
var toc = SIM_META.map(function(m){ return '<a href="#sim-'+m.id+'">'+m.icon+' '+esc(m.title)+'</a>'; }).join('');
document.getElementById('toc').innerHTML = toc;
document.getElementById('content').innerHTML = SIM_META.map(renderSim).join('');
</script>
</body>
</html>
+2
View File
@@ -90,6 +90,7 @@
<a href="admin-levels.html" class="btn-sm">Level</a>
<a href="admin-licenses.html" class="btn-sm">Lizenzen</a>
<a href="admin-accounts.html" class="btn-sm">👤 Admins</a>
<a href="admin-lehrer-view.html" class="btn-sm">📊 Lehrer-View Parameter</a>
<a href="index.html" class="btn-sm">Startseite</a>
</div>
@@ -249,6 +250,7 @@
<div class="pageitem"><a href="admin-modules.html">Module</a><span class="url">/admin-modules.html</span></div>
<div class="pageitem"><a href="admin-levels.html">Level / Waypoints</a><span class="url">/admin-levels.html</span></div>
<div class="pageitem"><a href="admin-styleguide.html">Styleguide</a><span class="url">/admin-styleguide.html</span></div>
<div class="pageitem"><a href="admin-lehrer-view.html">Lehrer-View Parameter-Überblick</a><span class="url">/admin-lehrer-view.html</span></div>
</div>
</div>
</details>