Files
geograsim/App/admin-weltkueche.html
T
Adminator 8ffc43d7b2 Weltkueche: Maengelliste abgearbeitet (14 Zutaten-Fixes) + Guanciale neu
Zutaten, die im Rezept nicht vorkamen/falsch waren, korrigiert:
- raus: Apfelstrudel(zitrone,essig), Sauerbraten(mehl), Pierogi(milch),
  Gulasch(tomaten), Guacamole(olivenoel), Groestl(kuemmel)
- ersetzt: Kirschtorte/Zuercher/Quiche milch->sahne, Pizza hartweizen->mehl
  (Weichweizen; hartweizen bleibt bei Carbonara=korrekt Durum), Gulasch
  butter->oel, Curry+PadThai olivenoel->oel, Carbonara speck->GUANCIALE (neu).
- guanciale (NEU, production-matrix): feste Herkunft Italien + Distraktoren.
Admin-Maengelliste als erledigt markiert; offen bleiben mediterrane Kraeuter
+ Marillenknoedel-Teig (Thomas-Entscheidung).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-22 01:17:01 +02:00

252 lines
22 KiB
HTML

<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>GeoGraSim — Weltküche-Herkunftsübersicht (Admin)</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}
.topbar h1{font-size:.95rem;font-weight:800;color:#4a7c8a;flex:1;display:flex;align-items:center;gap:.5rem}
.topbar h1 img{height:28px}
.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}
.btn-sm:hover{background:#dae8ec;color:#4a7c8a}
.wrap{max-width:1100px;margin:1rem auto;padding:0 1rem}
.lead{font-size:.82rem;color:#6a6a6a;margin-bottom:.8rem;line-height:1.5}
.lead b{color:#3a3a3a}
.filters{display:flex;gap:.4rem;flex-wrap:wrap;margin-bottom:1rem;align-items:center}
.filters input{padding:.4rem .6rem;border:1.5px solid rgba(0,0,0,.12);border-radius:7px;font-size:.8rem;font-family:inherit;min-width:220px}
.chip{padding:.3rem .7rem;border-radius:99px;border:1.5px solid rgba(0,0,0,.12);background:#fff;font-size:.72rem;font-weight:600;cursor:pointer;color:#4a4a4a}
.chip.on{background:#4a7c8a;color:#fff;border-color:#4a7c8a}
.dish{background:#fff;border-radius:12px;box-shadow:0 1px 4px rgba(0,0,0,.04);border:1px solid rgba(0,0,0,.05);margin-bottom:1rem;overflow:hidden}
.dish-h{padding:.7rem 1rem;background:#f0f5f6;border-bottom:1px solid rgba(0,0,0,.05);display:flex;align-items:center;gap:.6rem;flex-wrap:wrap}
.dish-h .nm{font-size:1rem;font-weight:800;color:#2a4a52}
.dish-h .heimat{font-size:.72rem;color:#6a6a6a}
.dish-h .cat{font-size:.6rem;text-transform:uppercase;letter-spacing:.05em;background:#e8e4d8;color:#7a6540;padding:.1rem .5rem;border-radius:99px;font-weight:700}
table{width:100%;border-collapse:collapse;font-size:.78rem}
th{text-align:left;padding:.45rem .7rem;font-size:.62rem;text-transform:uppercase;letter-spacing:.04em;color:#8a8a8a;background:#fafaf7;border-bottom:1px solid rgba(0,0,0,.05)}
td{padding:.5rem .7rem;border-bottom:1px solid rgba(0,0,0,.04);vertical-align:top}
tr:last-child td{border-bottom:none}
.ing-nm{font-weight:700;color:#3a3a3a;white-space:nowrap}
.ing-nm small{display:block;color:#9a9a9a;font-weight:400;font-size:.66rem}
.cty{display:inline-flex;align-items:center;gap:.25rem;padding:.12rem .45rem;border-radius:99px;font-size:.72rem;font-weight:600;margin:.1rem .2rem .1rem 0;white-space:nowrap}
.cty.ok{background:#dceadd;color:#2f6b3a;border:1px solid #b6d8bd}
.cty.dist{background:#f6e2dc;color:#a3472f;border:1px solid #e6c3b4}
.cty.other{background:#eee;color:#7a7a7a;border:1px solid #ddd}
.cty .g{opacity:.6;font-weight:400;font-size:.64rem}
.warn{background:#fff5e6;border-left:3px solid #e0a020;padding:.35rem .6rem;border-radius:5px;font-size:.72rem;color:#8a6510;margin-top:.35rem}
.colhead{font-size:.6rem;color:#8a8a8a;text-transform:uppercase;letter-spacing:.04em;margin-bottom:.2rem}
.legend{font-size:.72rem;color:#6a6a6a;margin:.5rem 0 1rem;display:flex;gap:1rem;flex-wrap:wrap}
#status{padding:2rem;text-align:center;color:#8a8a8a}
</style>
</head>
<body>
<div class="topbar">
<h1><img src="assets/img/geograsim_t.svg" alt="GeoGraSim"> Weltküche — Herkunftsübersicht (Admin)</h1>
<a href="admin-licenses.html" class="btn-sm">← Admin</a>
<a href="admin.html" class="btn-sm">Logout</a>
</div>
<div class="wrap">
<p class="lead">QA-Werkzeug: spiegelt die <b>Spiel-Logik</b> (Heimat + Handels-Gravitation Menge÷Entfernung, Markt-Tags). Pro Gericht &amp; Zutat siehst du, welche Länder das Spiel als <b>richtig</b> akzeptiert und welche als <b>Distraktoren</b> im Topf sind. Ziel: kein „Warum Frankreich?", keine „Bananen aus Deutschland".</p>
<div class="legend">
<span><span class="cty ok">🟢 richtig</span> akzeptierte Herkunft</span>
<span><span class="cty dist">🔴 Distraktor</span> falsch (fern/klimafremd/kein Lieferant)</span>
<span><span class="cty other">⚪ nicht angeboten</span> Produzent, aber nicht im Spiel</span>
<span><b>g</b> = Relevanz (Menge÷Entfernung zur Heimat)</span>
</div>
<div class="filters">
<input id="q" placeholder="Suche Gericht oder Zutat…" oninput="render()">
<span class="chip on" data-cat="" onclick="setCat(this,'')">Alle</span>
<span class="chip" data-cat="regional-at" onclick="setCat(this,'regional-at')">AT-regional</span>
<span class="chip" data-cat="dach" onclick="setCat(this,'dach')">DACH</span>
<span class="chip" data-cat="europa" onclick="setCat(this,'europa')">Europa</span>
<span class="chip" data-cat="exotisch" onclick="setCat(this,'exotisch')">Exotisch</span>
<label style="font-size:.72rem;color:#6a6a6a;margin-left:.5rem"><input type="checkbox" id="onlyWarn" onchange="render()"> nur Auffällige ⚠</label>
<label style="font-size:.72rem;color:#6a6a6a;margin-left:.5rem">🛒 Land des Users:
<select id="userCty" onchange="setUser(this.value)" style="font-size:.72rem;padding:.15rem .3rem">
<option value="oesterreich">🇦🇹 Österreich</option>
<option value="deutschland">🇩🇪 Deutschland</option>
<option value="schweiz">🇨🇭 Schweiz</option>
</select></label>
</div>
<details class="maengel" open style="margin:1rem 0;border:1px solid #d9b38c;border-radius:8px;background:#fff9f0;overflow:hidden">
<summary style="cursor:pointer;padding:.7rem 1rem;font-weight:700;font-size:.95rem;color:#8a5a2b;background:#fdf2e2">🔧 Mängelliste — Zutaten vs. Rezept <span style="font-weight:400;color:#9a7a5a">(Tabelle ✅ erledigt 2026-08-22 · offen: mediterrane Kräuter + Marillenknödel)</span></summary>
<div style="padding:.5rem 1rem 1rem">
<p style="font-size:.82rem;color:#2e6b34;margin:.3rem 0 .8rem;background:#e9f5ea;border-left:3px solid #4a7c4e;padding:.4rem .6rem"><b>✅ Erledigt (14 Fixes, Commit siehe git):</b> die Tabelle unten ist abgearbeitet — Zutaten raus/ersetzt, Guanciale neu für Carbonara. <b>Offen bleiben nur</b> die mediterranen Kräuter + Marillenknödel-Teig (weiter unten, deine Entscheidung).</p>
<p style="font-size:.8rem;color:#7a6a55;margin:.3rem 0 .8rem">Zutaten, die im <b>Rezepttext nicht vorkamen</b> oder <b>falsch</b> waren. „Vorschlag" = was stattdessen stimmt (umgesetzt).</p>
<table style="width:100%;border-collapse:collapse;font-size:.82rem">
<thead><tr style="text-align:left;color:#8a5a2b;border-bottom:2px solid #e6cba3">
<th style="padding:.35rem .5rem">Gericht</th><th style="padding:.35rem .5rem">Zutat</th><th style="padding:.35rem .5rem">Befund</th><th style="padding:.35rem .5rem">Vorschlag</th></tr></thead>
<tbody style="color:#4a4038">
<tr style="border-bottom:1px solid #f0e2cc"><td style="padding:.35rem .5rem">Apfelstrudel</td><td style="padding:.35rem .5rem"><b>zitrone, essig</b></td><td style="padding:.35rem .5rem">kommen im Rezept nicht vor</td><td style="padding:.35rem .5rem">beide raus</td></tr>
<tr style="border-bottom:1px solid #f0e2cc"><td style="padding:.35rem .5rem">Rheinischer Sauerbraten</td><td style="padding:.35rem .5rem"><b>mehl</b></td><td style="padding:.35rem .5rem">Sauce bindet mit Lebkuchen, kein Mehl</td><td style="padding:.35rem .5rem">raus</td></tr>
<tr style="border-bottom:1px solid #f0e2cc"><td style="padding:.35rem .5rem">Schwarzwälder Kirschtorte</td><td style="padding:.35rem .5rem"><b>milch</b></td><td style="padding:.35rem .5rem">Rezept nutzt Schlagobers, keine Milch</td><td style="padding:.35rem .5rem">→ sahne (Pantry)</td></tr>
<tr style="border-bottom:1px solid #f0e2cc"><td style="padding:.35rem .5rem">Zürcher Geschnetzeltes</td><td style="padding:.35rem .5rem"><b>milch</b></td><td style="padding:.35rem .5rem">Rezept nutzt Rahm</td><td style="padding:.35rem .5rem">→ sahne (Pantry)</td></tr>
<tr style="border-bottom:1px solid #f0e2cc"><td style="padding:.35rem .5rem">Quiche Lorraine</td><td style="padding:.35rem .5rem"><b>milch</b></td><td style="padding:.35rem .5rem">Rezept nutzt Sahne + Crème fraîche</td><td style="padding:.35rem .5rem">→ sahne (Pantry)</td></tr>
<tr style="border-bottom:1px solid #f0e2cc"><td style="padding:.35rem .5rem">Pierogi Ruskie</td><td style="padding:.35rem .5rem"><b>milch</b></td><td style="padding:.35rem .5rem">Teig mit Wasser, keine Milch</td><td style="padding:.35rem .5rem">raus</td></tr>
<tr style="border-bottom:1px solid #f0e2cc"><td style="padding:.35rem .5rem">Pizza Margherita</td><td style="padding:.35rem .5rem"><b>hartweizen</b></td><td style="padding:.35rem .5rem">Pizza nutzt Tipo-00 (Weichweizen), kein Durum</td><td style="padding:.35rem .5rem">→ weiches Weizenmehl</td></tr>
<tr style="border-bottom:1px solid #f0e2cc"><td style="padding:.35rem .5rem">Spaghetti Carbonara</td><td style="padding:.35rem .5rem"><b>speck</b></td><td style="padding:.35rem .5rem">Rezept sagt ausdrücklich Guanciale, NICHT Speck</td><td style="padding:.35rem .5rem">→ guanciale</td></tr>
<tr style="border-bottom:1px solid #f0e2cc"><td style="padding:.35rem .5rem">Ungarisches Gulasch</td><td style="padding:.35rem .5rem"><b>tomaten</b></td><td style="padding:.35rem .5rem">Gulyás ist paprika-basiert, keine Tomaten im Rezept</td><td style="padding:.35rem .5rem">raus</td></tr>
<tr style="border-bottom:1px solid #f0e2cc"><td style="padding:.35rem .5rem">Ungarisches Gulasch</td><td style="padding:.35rem .5rem"><b>butter</b></td><td style="padding:.35rem .5rem">Rezept nutzt Schweineschmalz</td><td style="padding:.35rem .5rem">→ öl/schmalz</td></tr>
<tr style="border-bottom:1px solid #f0e2cc"><td style="padding:.35rem .5rem">Gemüse-Curry</td><td style="padding:.35rem .5rem"><b>olivenoel</b></td><td style="padding:.35rem .5rem">Curry nutzt neutrales Öl, kein Olivenöl</td><td style="padding:.35rem .5rem">→ oel (neutral)</td></tr>
<tr style="border-bottom:1px solid #f0e2cc"><td style="padding:.35rem .5rem">Pad Thai</td><td style="padding:.35rem .5rem"><b>olivenoel</b></td><td style="padding:.35rem .5rem">Wok-Gericht, neutrales/Erdnussöl</td><td style="padding:.35rem .5rem">→ oel (neutral)</td></tr>
<tr style="border-bottom:1px solid #f0e2cc"><td style="padding:.35rem .5rem">Guacamole</td><td style="padding:.35rem .5rem"><b>olivenoel</b></td><td style="padding:.35rem .5rem">Guacamole hat kein Öl im Rezept</td><td style="padding:.35rem .5rem">raus</td></tr>
<tr style="border-bottom:1px solid #f0e2cc"><td style="padding:.35rem .5rem">Tiroler Gröstl</td><td style="padding:.35rem .5rem"><b>kuemmel</b></td><td style="padding:.35rem .5rem">kommt im Rezept nicht vor (nur Majoran)</td><td style="padding:.35rem .5rem">raus oder ins Rezept</td></tr>
</tbody>
</table>
<p style="font-size:.8rem;color:#7a6a55;margin:.9rem 0 .3rem"><b>🌿 Mediterrane Kräuter (<code>kraeuter</code>)</b> — Rezept nutzt keine mediterranen Kräuter (deine Entscheidung, wie bei Maultaschen/Sauerbraten):</p>
<p style="font-size:.8rem;color:#5a5048;margin:.2rem 0 .3rem">Zürcher Geschnetzeltes (nur Petersilie) · Pierogi Ruskie (nur Schnittlauch) · Ungarisches Gulasch (Kümmel/Lorbeer) · Moussaka (Zimt/Lorbeer) · Falafel (Petersilie/Koriander, nicht „mediterran")</p>
<p style="font-size:.8rem;color:#7a6a55;margin:.9rem 0 .3rem"><b>📝 Kleinere Rezept-Themen:</b></p>
<p style="font-size:.8rem;color:#5a5048;margin:.2rem 0 .3rem">Wachauer Marillenknödel — Rezept nutzt noch <b>Erdäpfel</b> trotz gewünschtem reinem Topfenteig · Steirisches Wurzelfleisch — Pfeffer nur implizit · Tiroler Gröstl / Brezel — Butter vs. Butterschmalz/Frischkäse (kosmetisch)</p>
</div>
</details>
<div id="status">Lade Daten…</div>
<div id="list"></div>
</div>
<script>
var BASE = location.pathname.replace(/\/admin-weltkueche\.html$/, '');
var D = 'sims/weltkueche/assets/data/';
var C = {}, PM = {}, DISHES = [];
var _cat = '';
var WK_USER = 'oesterreich'; // Teil B: Land des Users (Consumer-Anker) — via Umschalter
function setUser(v){ WK_USER = v || 'oesterreich'; render(); }
// ---- Spiel-Logik gespiegelt (siehe game.html buildTaskFor) ----
function mrank(m){ return m === 'eu' ? 0 : (m === 'world-far' || m === 'local') ? 2 : 1; }
function wkDist(a, b){
var A = C[a], B = C[b];
if (!A || !B || A.lat == null || B.lat == null) return 3000;
var R = 6371, toR = Math.PI/180;
var dLat = (B.lat-A.lat)*toR, dLng = (B.lng-A.lng)*toR;
var s = Math.pow(Math.sin(dLat/2),2) + Math.cos(A.lat*toR)*Math.cos(B.lat*toR)*Math.pow(Math.sin(dLng/2),2);
return Math.max(60, 2*R*Math.asin(Math.min(1, Math.sqrt(s))));
}
// Klassifiziert alle Produzenten einer Zutat für ein Gericht.
// Gibt { correct:[keys], distractors:[keys], branch } zurück (Pools, ohne Zufalls-Ziehung).
function classify(ingKey, heimat, isSig){
var ing = PM[ingKey]; if (!ing) return null;
var producers = ing.producers || {};
var keys = Object.keys(producers).filter(function(c){ return C[c]; });
// ── FEST VERDRAHTETE HERKUNFT: feste Länderliste + feste Distraktoren.
if (ing.correct && ing.correct.length) {
var fc = ing.correct.filter(function(c){ return C[c]; });
var fd = (ing.distractors||[]).filter(function(c){ return C[c] && fc.indexOf(c)<0; });
return { correct: fc, distractors: fd, branch:'fest', grav:function(){return 0;} };
}
// ── CONSUMER-REGEL (Teil B): Grundzutat, im Land des Users heimisch → kommt aus
// dem Land des Users (unabhängig vom Gericht-Land). Signaturzutaten ausgenommen.
if (!isSig && ing.heimisch && ing.heimisch.indexOf(WK_USER) >= 0 && C[WK_USER]) {
var cmarket = (ing.market||[]).filter(function(c){ return C[c] && c !== WK_USER; });
var ccorrect = [WK_USER].concat(cmarket).slice(0,3);
if (ccorrect.length < 2){ for (var hi=0; hi<(ing.heimisch||[]).length; hi++){ var hh=ing.heimisch[hi]; if(C[hh]&&ccorrect.indexOf(hh)<0){ccorrect.push(hh);break;} } }
var cdistr;
if (ing.distractors && ing.distractors.length){ cdistr = ing.distractors.filter(function(c){ return C[c] && ccorrect.indexOf(c)<0; }); }
else { var cfar = keys.filter(function(c){ var mm=producers[c].market; return (mm==='world-far'||mm==='local') && ccorrect.indexOf(c)<0; }).sort(function(a,b){ return (producers[b].t||0)-(producers[a].t||0); });
var cclim = (ing.climate_possible||[]).filter(function(c){ return C[c] && ccorrect.indexOf(c)<0; }); cdistr = cfar.concat(cclim); }
return { correct: ccorrect, distractors: cdistr, branch:'consumer', grav:function(){return 0;} };
}
// Heimat-Zweig: Heimat erzeugt die Zutat selbst
if (heimat && C[heimat] && producers[heimat]) {
var grav = function(c){ return (producers[c].t || 0) / wkDist(heimat, c); };
var realistic = keys.filter(function(c){ return c !== heimat && mrank(producers[c].market) < 2; })
.sort(function(a,b){ return (mrank(producers[a].market)-mrank(producers[b].market)) || (grav(b)-grav(a)); });
// Kuratierte Zweitlieferanten (correct_for) übersteuern die Gravitation — wie im Spiel.
var curated = ((ing.correct_for && ing.correct_for[heimat]) || []).filter(function(c){ return C[c] && c !== heimat; });
var correct = curated.length ? [heimat].concat(curated) : [heimat];
if (!curated.length && realistic[0]) correct.push(realistic[0]);
if (correct.length < 2){ var any = keys.filter(function(c){ return c!==heimat && correct.indexOf(c)<0; })
.sort(function(a,b){ return (producers[b].t||0)-(producers[a].t||0); }); if(any[0]) correct.push(any[0]); }
var farP = keys.filter(function(c){ return correct.indexOf(c)<0 && mrank(producers[c].market)===2; });
var climate = (ing.climate_possible||[]).filter(function(c){ return C[c] && correct.indexOf(c)<0; });
return { correct: correct, distractors: farP.concat(climate), branch:'heimat', grav: grav };
}
// Normal-Zweig (exotisch / Heimat kein Produzent)
var TIER = { eu:0, global:1, 'world-far':2 };
var pool = keys.filter(function(c){ var m = producers[c].market || 'global'; return m==='eu'||m==='global'||m==='world-far'; })
.sort(function(a,b){ var ma=producers[a].market||'global', mb=producers[b].market||'global';
return ((TIER[ma]!=null?TIER[ma]:2)-(TIER[mb]!=null?TIER[mb]:2)) || ((producers[b].t||0)-(producers[a].t||0)); });
var correct2 = pool.slice(0, Math.min(2, pool.length));
var climate2 = (ing.climate_possible||[]).filter(function(c){ return C[c] && correct2.indexOf(c)<0; });
var localFar = keys.filter(function(c){ var m=producers[c].market; return (m==='local'||m==='world-far') && correct2.indexOf(c)<0; });
return { correct: correct2, distractors: localFar.concat(climate2), branch:'welt', grav: function(c){ return producers[c].t||0; } };
}
function fmtT(t){ if(!t) return ''; if(t>=1e6) return (t/1e6).toFixed(t>=1e7?0:1)+' Mio t'; if(t>=1e3) return Math.round(t/1e3)+' Tsd t'; return t+' t'; }
function ctyChip(key, cls, ing, heimat, showG){
var c = C[key]; if(!c) return '';
var g = '';
if (showG && heimat && C[heimat] && key!==heimat){
var t = (PM[ing].producers[key]||{}).t||0; var d = Math.round(wkDist(heimat,key));
g = ' <span class="g">'+Math.round(t/Math.max(1,d))+'</span>';
}
return '<span class="cty '+cls+'" title="'+(c.n)+(PM[ing].producers[key]?(' · '+fmtT(PM[ing].producers[key].t)+' · Markt: '+(PM[ing].producers[key].market||'global')):'')+(heimat&&key!==heimat?(' · '+Math.round(wkDist(heimat,key))+' km'):'')+'">'+c.f+' '+c.n+g+'</span>';
}
function render(){
var q = (document.getElementById('q').value||'').toLowerCase().trim();
var onlyWarn = document.getElementById('onlyWarn').checked;
var host = document.getElementById('list'); var html = '';
var shown = 0;
DISHES.forEach(function(d){
if (_cat && d.category !== _cat) return;
var heimat = d.heimat || null;
var ings = (d.ings||[]).filter(function(k){ return PM[k]; });
// Filter Suche
var matchDish = !q || (d.name||'').toLowerCase().indexOf(q)>=0 || ings.some(function(k){ return (PM[k].nm||k).toLowerCase().indexOf(q)>=0; });
if (!matchDish) return;
var rows = ''; var dishHasWarn = false;
ings.forEach(function(k){
if (q && (d.name||'').toLowerCase().indexOf(q)<0 && (PM[k].nm||k).toLowerCase().indexOf(q)<0) return;
var cl = classify(k, heimat, !!(d.signature && d.signature[k])); if(!cl) return;
// Auffällig NUR im Heimat-Zweig: die Zutat ist lokal erzeugbar (Heimat baut
// sie an), trotzdem ist ein „richtiges" Land > 1500 km weg → echter Ausreißer.
// Welt-Zutaten (Pfeffer aus Vietnam etc.) sind zu Recht fern → keine Warnung.
var warn = '';
if (heimat && C[heimat] && cl.branch === 'heimat'){
var farCorrect = cl.correct.filter(function(c){ return c!==heimat && wkDist(heimat,c) > 1500; });
if (farCorrect.length){ warn = '<div class="warn">⚠ „richtig" trotz &gt; 1500 km von '+C[heimat].n+': '+farCorrect.map(function(c){return C[c].n+' ('+Math.round(wkDist(heimat,c))+' km)';}).join(', ')+'</div>'; dishHasWarn=true; }
}
if (onlyWarn && !warn) return;
var okChips = cl.correct.map(function(c){ return ctyChip(c,'ok',k,heimat,true); }).join('');
var distChips = cl.distractors.slice(0,6).map(function(c){ return ctyChip(c,'dist',k,heimat,false); }).join('');
rows += '<tr><td class="ing-nm">'+(PM[k].ic||'')+' '+(PM[k].nm||k)+'<small>'+(cl.branch==='fest'?'📌 Feste Herkunft':(cl.branch==='consumer'?'🛒 Consumer-Regel (User-Land)':(cl.branch==='heimat'?'Heimat-Regel':'Welt-Zutat')))+'</small></td>'
+ '<td><div class="colhead">richtig</div>'+(okChips||'—')+warn+'</td>'
+ '<td><div class="colhead">Distraktoren (Pool)</div>'+(distChips||'—')+'</td></tr>';
});
if (!rows) return;
if (onlyWarn && !dishHasWarn) return;
shown++;
html += '<div class="dish"><div class="dish-h"><span class="nm">'+(d.name||d.id)+'</span>'
+ (heimat&&C[heimat]?'<span class="heimat">Heimat: '+C[heimat].f+' '+C[heimat].n+'</span>':'<span class="heimat">ohne Heimat (Welt-Gericht)</span>')
+ (d.category?'<span class="cat">'+d.category+'</span>':'')+'</div>'
+ '<table><thead><tr><th style="width:26%">Zutat</th><th style="width:37%">🟢 Richtig</th><th style="width:37%">🔴 Distraktoren</th></tr></thead><tbody>'+rows+'</tbody></table></div>';
});
document.getElementById('status').style.display = 'none';
host.innerHTML = html || '<div id="status" style="display:block">Keine Treffer.</div>';
}
function setCat(el, cat){ _cat = cat; document.querySelectorAll('.chip').forEach(function(c){ c.classList.toggle('on', c===el); }); render(); }
// Auth + Daten laden
fetch(BASE + '/api/admin', {method:'POST', headers:{'Content-Type':'application/json'}, credentials:'same-origin', body:JSON.stringify({action:'status'})})
.then(function(r){return r.json();})
.then(function(r){
if (!r.authenticated){ location.href = 'admin.html'; return; }
var _bust = '?v=' + Date.now(); // Cache-Busting: Admin-QA zeigt IMMER die frischen Daten, nie eine gecachte JSON
return Promise.all([
fetch(D+'countries-full.json'+_bust, {cache:'no-store'}).then(function(r){return r.json();}),
fetch(D+'production-matrix.json'+_bust, {cache:'no-store'}).then(function(r){return r.json();}),
fetch(D+'dishes.json'+_bust, {cache:'no-store'}).then(function(r){return r.json();}),
]).then(function(a){
C = a[0]; PM = a[1];
DISHES = Object.keys(a[2]).filter(function(k){return k[0]!=='_';}).map(function(id){ return Object.assign({id:id}, a[2][id]); });
render();
});
})
.catch(function(e){ document.getElementById('status').textContent = 'Fehler: '+e.message; });
</script>
</body>
</html>