fix: recalibrate Neapolitan yeast for v1.1

This commit is contained in:
root
2026-07-26 08:52:58 +00:00
parent c0640068fc
commit 768102473c
3 changed files with 92 additions and 9 deletions
+2
View File
@@ -2,6 +2,8 @@
Pizzateig-Kalkulator als statische Single-Page-App. Pizzateig-Kalkulator als statische Single-Page-App.
Aktuelle Version: **v1.1**
- Bäckerprozent - Bäckerprozent
- Mehl-Datenbank - Mehl-Datenbank
- Vorteig - Vorteig
+12 -9
View File
@@ -3,7 +3,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"> <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
<title>TEIG//TERMINAL — Pizzateig-Kalkulator</title> <title>TEIG//TERMINAL v1.1 — Pizzateig-Kalkulator</title>
<meta name="description" content="Pizzateig-Rechner mit Bäckerprozent, Mehl-Datenbank, Vorteig und Rückwärts-Zeitplan."> <meta name="description" content="Pizzateig-Rechner mit Bäckerprozent, Mehl-Datenbank, Vorteig und Rückwärts-Zeitplan.">
<meta name="color-scheme" content="dark"> <meta name="color-scheme" content="dark">
<style> <style>
@@ -304,7 +304,7 @@ footer b{color:var(--txt)}
<header> <header>
<h1 class="brand">TEIG<em>//</em>TERMINAL</h1> <h1 class="brand">TEIG<em>//</em>TERMINAL</h1>
<p class="tag">Pizzateig-Kalkulator · Bäckerprozent · Vorteig · Rückwärts-Zeitplan</p> <p class="tag">Pizzateig-Kalkulator · Bäckerprozent · Vorteig · Rückwärts-Zeitplan</p>
<div class="status"><span class="dot"></span><span id="statusline">System bereit</span></div> <div class="status"><span class="dot"></span><span id="statusline">System bereit · v1.1</span></div>
</header> </header>
<div class="quick-summary noprint" id="quickSummary" aria-label="Kurzüberblick"></div> <div class="quick-summary noprint" id="quickSummary" aria-label="Kurzüberblick"></div>
@@ -553,10 +553,10 @@ const FLOURS = [
{g:"Spezial", id:"custom", n:"Eigenes Mehl / Blend", w:"—", pr:0, hyd:[50,90], reife:[0,120], vt:true, note:"Keine Prüfung — du weißt, was du tust."} {g:"Spezial", id:"custom", n:"Eigenes Mehl / Blend", w:"—", pr:0, hyd:[50,90], reife:[0,120], vt:true, note:"Keine Prüfung — du weißt, was du tust."}
]; ];
/* ---------- Stile (tf = Teigdichte in g/cm²) ---------- */ /* ---------- Stile (tf = Teigdichte in g/cm², yeastK = Frischhefe-Referenz) ---------- */
const STYLES = { const STYLES = {
napo: {n:"Napoletana", ball:250, hyd:62, salt:2.8, oil:0, sugar:0, tf:0.36, mode:"w"}, napo: {n:"Napoletana", ball:250, hyd:62, salt:2.8, oil:0, sugar:0, tf:0.36, mode:"w", yeastK:1.5},
contemp: {n:"Contemporanea", ball:280, hyd:70, salt:2.8, oil:0, sugar:0, tf:0.39, mode:"w"}, contemp: {n:"Contemporanea", ball:280, hyd:70, salt:2.8, oil:0, sugar:0, tf:0.39, mode:"w", yeastK:1.7},
ny: {n:"New York", ball:300, hyd:63, salt:2.0, oil:2.5, sugar:1.5, tf:0.31, mode:"w"}, ny: {n:"New York", ball:300, hyd:63, salt:2.0, oil:2.5, sugar:1.5, tf:0.31, mode:"w"},
tonda: {n:"Römisch tonda", ball:200, hyd:57, salt:2.2, oil:4, sugar:0, tf:0.26, mode:"w"}, tonda: {n:"Römisch tonda", ball:200, hyd:57, salt:2.2, oil:4, sugar:0, tf:0.26, mode:"w"},
teglia: {n:"Teglia / Blech", ball:800, hyd:78, salt:2.2, oil:2, sugar:0, tf:0.55, mode:"p"}, teglia: {n:"Teglia / Blech", ball:800, hyd:78, salt:2.2, oil:2, sugar:0, tf:0.55, mode:"p"},
@@ -582,7 +582,7 @@ const SEQ = {
warmcold: {n:"Warm → Kalt", fridge:true, warmcold: {n:"Warm → Kalt", fridge:true,
note:"Hybrid: erst Stockgare bei Raumtemperatur, dann in den Kühlschrank, kurz vor dem Backen aufwärmen. Sehr verbreitet, braucht wenig Hefe — aber der Teig kühlt langsam durch und gärt im Kühlschrank wärmer als gedacht."}, note:"Hybrid: erst Stockgare bei Raumtemperatur, dann in den Kühlschrank, kurz vor dem Backen aufwärmen. Sehr verbreitet, braucht wenig Hefe — aber der Teig kühlt langsam durch und gärt im Kühlschrank wärmer als gedacht."},
coldwarm: {n:"Kalt → Warm", fridge:true, coldwarm: {n:"Kalt → Warm", fridge:true,
note:"Lehmann-Methode: direkt nach dem Kneten kalt stellen, die Gare passiert im Kühlschrank, die warme Zeit danach dient dem Handling. Am besten planbar, braucht deutlich mehr Hefe."} note:"Direkt nach dem Kneten kalt stellen, anschließend in warmer Stückgare ausreifen lassen. Gut planbar und besonders für längere Führungen geeignet; die warme Endgare macht die Ballen sichtbar aktiv und dehnbar."}
}; };
const STAGLIO = { const STAGLIO = {
before:"Ballen vor der Kälte", before:"Ballen vor der Kälte",
@@ -670,7 +670,7 @@ function calc(s){
const pfFrac= hasPf ? s.pfPct/100 : 0; const pfFrac= hasPf ? s.pfPct/100 : 0;
// --- Triebmittel in % vom Mehl --- // --- Triebmittel in % vom Mehl ---
let fresh = 6/U; // Frischhefe-Referenz let fresh = (st.yeastK ?? 6)/U; // stilbezogene Frischhefe-Referenz
fresh *= 1 + Math.max(0, s.salt-2.5)*0.06; // Salz bremst fresh *= 1 + Math.max(0, s.salt-2.5)*0.06; // Salz bremst
if (hasPf) fresh *= (1 - 0.8*pfFrac); // Vorteig bringt schon Trieb if (hasPf) fresh *= (1 - 0.8*pfFrac); // Vorteig bringt schon Trieb
fresh = clamp(fresh, 0.01, 6); fresh = clamp(fresh, 0.01, 6);
@@ -873,7 +873,7 @@ function applySimpleFermentation(){
S.seq = "coldwarm"; S.seq = "coldwarm";
S.staglio = "after"; S.staglio = "after";
S.roomH = doughH <= 18 ? 2 : 3; S.roomH = 4;
S.fridgeH = Math.max(4, Math.round(doughH - S.roomH)); S.fridgeH = Math.max(4, Math.round(doughH - S.roomH));
const total = S.roomH + S.fridgeH; const total = S.roomH + S.fridgeH;
S.plan = total <= 18 ? "night" : total <= 30 ? "d24" : total <= 54 ? "d48" : "d72"; S.plan = total <= 18 ? "night" : total <= 30 ? "d24" : total <= 54 ? "d48" : "d72";
@@ -1003,6 +1003,8 @@ function renderWarnings(f,r){
if (r.hasPf && !f.vt) if (r.hasPf && !f.vt)
w.push(["",`${f.n} ist für Vorteige eher schwach. Poolish und Biga verlangen mehr Backstärke.`]); w.push(["",`${f.n} ist für Vorteige eher schwach. Poolish und Biga verlangen mehr Backstärke.`]);
} }
if ((S.style==="napo" || S.style==="contemp") && S.seq==="room" && S.roomH<4)
w.push(["",`<b>Schnellteig:</b> Unter 4 h braucht neapolitanischer Teig vergleichsweise viel Hefe und entwickelt weniger Aroma. Wenn möglich, mindestens 48 h einplanen.`]);
if (S.fridgeT<4) w.push(["",`Unter 4 °C stellt die Hefe die Arbeit fast ein. 56 °C sind für die Kühlschrankgare der bessere Bereich.`]); if (S.fridgeT<4) w.push(["",`Unter 4 °C stellt die Hefe die Arbeit fast ein. 56 °C sind für die Kühlschrankgare der bessere Bereich.`]);
if (S.seq==="warmcold" && S.staglio==="after" && S.fridgeH>0 && r.tcCold > S.fridgeH*0.4) if (S.seq==="warmcold" && S.staglio==="after" && S.fridgeH>0 && r.tcCold > S.fridgeH*0.4)
w.push(["",`Die Masse braucht rund ${Math.round(r.tcCold)} h zum Durchkühlen — bei ${S.fridgeH} h Kaltgare gärt der Kern also die halbe Zeit deutlich wärmer als ${n1(S.fridgeT)} °C. Teig flach drücken oder auf mehrere Behälter aufteilen, sonst wird das Ergebnis schwer wiederholbar.`]); w.push(["",`Die Masse braucht rund ${Math.round(r.tcCold)} h zum Durchkühlen — bei ${S.fridgeH} h Kaltgare gärt der Kern also die halbe Zeit deutlich wärmer als ${n1(S.fridgeT)} °C. Teig flach drücken oder auf mehrere Behälter aufteilen, sonst wird das Ergebnis schwer wiederholbar.`]);
@@ -1010,7 +1012,8 @@ function renderWarnings(f,r){
w.push(["",`Bei der Lehmann-Methode ist die Gare im Kühlschrank abgeschlossen. ${n1(S.roomH)} h warm danach sind viel — der Teig übergärt leicht. 0,5 bis 2 h reichen, um ihn dehnbar zu bekommen.`]); w.push(["",`Bei der Lehmann-Methode ist die Gare im Kühlschrank abgeschlossen. ${n1(S.roomH)} h warm danach sind viel — der Teig übergärt leicht. 0,5 bis 2 h reichen, um ihn dehnbar zu bekommen.`]);
if (S.seq==="coldwarm" && S.roomH<0.5) if (S.seq==="coldwarm" && S.roomH<0.5)
w.push(["",`Ohne Aufwärmzeit ist der Teig sehr elastisch und wehrt sich beim Öffnen. Ab etwa 10 °C Kerntemperatur wird er dehnbar.`]); w.push(["",`Ohne Aufwärmzeit ist der Teig sehr elastisch und wehrt sich beim Öffnen. Ab etwa 10 °C Kerntemperatur wird er dehnbar.`]);
if (!r.isSour && r.yPct<0.03) w.push(["",`Die Hefemenge ist rechnerisch winzig. Ohne Feinwaage mit 0,01 g Auflösung wird das Raten — kürzere Gare oder Vorteig sind praktikabler.`]); const yeastG = r.hasPf ? r.pf.yeast+r.main.yeast : r.main.yeast;
if (!r.isSour && yeastG<0.5) w.push(["",`Nur ${yeastG.toFixed(2).replace(".",",")} g Hefe: Für reproduzierbare Ergebnisse eine Feinwaage mit 0,01 g Auflösung verwenden oder die Hefe in Wasser lösen und anteilig dosieren.`]);
if (!r.isSour && r.yPct>3) w.push(["",`Sehr viel Hefe. Der Teig geht schnell, aber flach im Geschmack. Mehr Zeit gibt mehr Aroma.`]); if (!r.isSour && r.yPct>3) w.push(["",`Sehr viel Hefe. Der Teig geht schnell, aber flach im Geschmack. Mehr Zeit gibt mehr Aroma.`]);
if (r.hasPf && r.main.water<0) if (r.hasPf && r.main.water<0)
w.push(["bad",`Der Vorteig enthält mehr Wasser als das ganze Rezept hergibt. Vorteig-Anteil senken oder Hydration erhöhen.`]); w.push(["bad",`Der Vorteig enthält mehr Wasser als das ganze Rezept hergibt. Vorteig-Anteil senken oder Hydration erhöhen.`]);
+78
View File
@@ -0,0 +1,78 @@
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const vm = require('node:vm');
const html = fs.readFileSync(path.join(__dirname, '..', 'index.html'), 'utf8');
function between(start, end) {
const from = html.indexOf(start);
const to = html.indexOf(end, from);
assert.ok(from >= 0 && to > from, `Quellbereich fehlt: ${start}${end}`);
return html.slice(from, to);
}
const core = between('const STYLES =', '/* ============================================================\n ZEITPLAN');
const simple = between('function applySimpleFermentation()', 'function initUI()');
const warnings = between('function renderWarnings(', 'function renderRecipe(');
const fixedNow = 1_700_000_000_000;
class FixedDate extends Date {
static now() { return fixedNow; }
}
const warningHost = { innerHTML: '' };
const sandbox = {
Date: FixedDate,
console,
$: id => id === 'warns' ? warningHost : {},
pct: x => `${x} %`,
n1: x => String(x),
};
vm.createContext(sandbox);
vm.runInContext(`${core}\n${simple}\n${warnings}\nglobalThis.api={STYLES,DEFAULT,calc,applySimpleFermentation,renderWarnings,setState:s=>S=s,getState:()=>S};`, sandbox);
const { STYLES, DEFAULT, calc, applySimpleFermentation, renderWarnings, setState, getState } = sandbox.api;
function state(overrides = {}) {
return { ...DEFAULT, pf: 'none', yeast: 'fresh', balls: 4, ball: 250, mode: 'w', ...overrides };
}
// Napoletana und Contemporanea werden bewusst niedriger kalibriert.
assert.equal(STYLES.napo.yeastK, 1.5);
assert.equal(STYLES.contemp.yeastK, 1.7);
// 8 h warme Napoletana bei 20 °C: rund 0,19 % Frischhefe statt bisher 0,76 %.
const napo8 = calc(state({ style: 'napo', seq: 'room', roomH: 8, roomT: 20, fridgeH: 0 }));
assert.ok(Math.abs(napo8.yPct - 0.190875) < 1e-9, `unerwartet: ${napo8.yPct}%`);
assert.ok(napo8.main.yeast > 1.1 && napo8.main.yeast < 1.2, `unerwartet: ${napo8.main.yeast} g`);
// Nicht neu kalibrierte Stile behalten den bisherigen Referenzfaktor 6.
const ny8 = calc(state({ style: 'ny', salt: 2, seq: 'room', roomH: 8, roomT: 20, fridgeH: 0 }));
assert.equal(ny8.yPct, 0.75);
// Lange Führung im Einfach-Modus endet mit 4 h warmer Stückgare.
setState(state({ uxMode: 'simple', bake: fixedNow + 24.75 * 3600e3 }));
applySimpleFermentation();
assert.equal(getState().seq, 'coldwarm');
assert.equal(getState().roomH, 4);
assert.equal(getState().fridgeH, 20);
setState(state({ uxMode: 'simple', bake: fixedNow + 48.75 * 3600e3 }));
applySimpleFermentation();
assert.equal(getState().roomH, 4);
assert.equal(getState().fridgeH, 44);
// Unter 4 h wird Napoletana ausdrücklich als Schnellteig gekennzeichnet.
const napo3State = state({ style: 'napo', seq: 'room', roomH: 3, roomT: 20, fridgeH: 0 });
setState(napo3State);
const napo3 = calc(napo3State);
renderWarnings({ n: 'Testmehl', reife: [0, 120], hyd: [50, 90], vt: true }, napo3);
assert.match(warningHost.innerHTML, /Schnellteig/);
// Kleine absolute Hefemengen nennen die Feinwaage, auch wenn der Prozentwert über 0,03 % liegt.
const napo24State = state({ style: 'napo', seq: 'room', roomH: 24, roomT: 20, fridgeH: 0 });
setState(napo24State);
const napo24 = calc(napo24State);
assert.ok(napo24.main.yeast < 0.5 && napo24.yPct > 0.03);
renderWarnings({ n: 'Testmehl', reife: [0, 120], hyd: [50, 90], vt: true }, napo24);
assert.match(warningHost.innerHTML, /Feinwaage/);
console.log('yeast calibration regression tests: OK');