8ffc43d7b2
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>
252 lines
22 KiB
HTML
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 & 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 > 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>
|