index.html
Rama main · réplica de lectura del repositorio original.
<!doctype html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<title>Meistertracker</title>
<link rel="manifest" href="/manifest.json" />
<meta name="theme-color" content="#1a1a1a" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Meistertracker" />
<link rel="apple-touch-icon" href="/icon-192.png" />
<link rel="stylesheet" href="/styles.css" />
<!-- P-02: vendor libs deferred until idle. The four libs total ~660 KB raw
(~195 KB gzip) and are only used in specific flows (camera scanner,
dashboard charts, label print/preview). Loading them eagerly with
<script defer> still has them parse + compile on every page load,
even on phones that just open the dashboard. We injection-load them
after DOMContentLoaded + idle so the first paint isn't blocked.
A single _vendorLibsReady promise lets the few callers that hit
these libs await readiness — see app.js loadVendorLibs(). -->
</head>
<body>
<!-- Worktree warning banner: shown only when the server is running from
a git worktree (WORKTREE_MODE=1, surfaced via /api/health). Designed
to be impossible to miss so nobody enters real data here thinking
it's the prod instance. Hidden by default; the inline script below
flips it on if /api/health reports worktree:true. -->
<div id="worktree-banner" class="worktree-banner" role="alert" hidden>
<span class="worktree-banner-icon" aria-hidden="true">⚠</span>
<span class="worktree-banner-text">
<b>WORKTREE INSTANCE</b> — this is <b>not production</b>. Data entered here is isolated to this branch.
<span id="worktree-banner-port"></span>
</span>
</div>
<script>
// Self-contained probe so the banner appears as soon as possible,
// independent of the main app bundle's auth/init flow. /api/health
// returns the worktree flag in its public (unauthenticated) payload.
(function () {
try {
fetch('/api/health', { credentials: 'same-origin' })
.then(function (r) {
return r.ok ? r.json() : null;
})
.then(function (h) {
if (!h || !h.worktree) return;
var el = document.getElementById('worktree-banner');
if (!el) return;
var portEl = document.getElementById('worktree-banner-port');
if (portEl && window.location.port) {
portEl.textContent = ' Port ' + window.location.port + '.';
}
el.hidden = false;
document.body.classList.add('has-worktree-banner');
// Banner text wraps on narrow viewports — without this, the
// sidebar/topbar/body padding-top stay at the single-line
// height and the wrapped banner covers real content. Track
// the actual height and surface it as a CSS variable.
var syncHeight = function () {
var h = el.offsetHeight;
if (h > 0) {
document.documentElement.style.setProperty('--worktree-banner-h', h + 'px');
}
};
syncHeight();
if (window.ResizeObserver) {
new ResizeObserver(syncHeight).observe(el);
} else {
window.addEventListener('resize', syncHeight);
}
})
.catch(function () {
/* offline or auth wall — banner stays hidden, that's fine */
});
} catch (e) {
/* ignore */
}
})();
</script>
<!-- Add bags modal -->
<div class="modal-bg" id="m-addbags" role="dialog" aria-modal="true" aria-labelledby="m-addbags-title" hidden>
<div class="modal">
<h3 id="m-addbags-title" data-i18n="batchAdd.title">Add bags to batch</h3>
<!-- Phase 1: input -->
<div id="ab-phase-input">
<p id="ab-info" style="font-size: 13px; color: var(--c-text-sec); margin-bottom: 12px"></p>
<div style="display: flex; flex-direction: column; gap: 10px">
<div>
<label data-i18n="addBags.howMany">How many bags to add</label>
<input type="number" id="ab-qty" value="1" min="1" max="200" style="width: 100px" />
</div>
<!-- Körnerbrut wird gewogen, und eine Charge darf mehr als eine
Größe tragen. Ohne dieses Feld bekämen nachträglich ergänzte
Beutel stumm das Gewicht der Charge — und stünden dann mit einer
Zahl im Regal, die nicht ihre ist. Nur für Körner sichtbar
(siehe openAddBags). -->
<div id="ab-kg-wrap" style="display: none">
<label data-i18n="addBags.bagKg">kg pro Beutel</label>
<input type="text" inputmode="decimal" id="ab-kg" autocomplete="off" style="width: 100px" />
</div>
<div class="fs-meta"
id="ab-preview"
style="color: var(--c-text-sec); background: var(--c-bg); border-radius: 7px; padding: 8px 10px; display: none"
></div>
</div>
<div class="modal-btns">
<button class="btn" id="addbags-cancel-btn" data-i18n="common.cancel">Cancel</button>
<button class="btn btn-p" id="addbags-confirm-btn" data-i18n="addBags.title">Add bags</button>
</div>
</div>
<!-- Phase 2: result + print -->
<div id="ab-phase-result" style="display: none">
<p id="ab-result-info" style="font-size: 13px; color: var(--c-green-dark); margin-bottom: 12px"></p>
<div
id="ab-new-bags"
style="display: flex; flex-wrap: wrap; gap: 4px; max-height: 180px; overflow-y: auto"
></div>
<div class="modal-btns">
<button class="btn" id="ab-done-btn" data-i18n="addBags.close">Close</button>
<button class="btn btn-p" id="ab-print-btn" data-i18n="addBags.printLabels">Print labels</button>
</div>
</div>
</div>
</div>
<!-- One species of the Laborbestand: stock (read-only), target, and what to make (lbSheetRender) -->
<div class="modal-bg" id="m-lb" role="dialog" aria-modal="true" aria-labelledby="m-lb-title" hidden>
<div class="modal">
<h3 id="m-lb-title"></h3>
<label class="lb-prog">
<input type="checkbox" id="m-lb-prog" />
<span>
<b data-i18n="lb.programm">Im Programm</b>
<small data-i18n="lb.programmHelp"
>Aus: die Sorte fehlt nirgends und steht nicht in der Tagesliste. Nur offene Bestellungen zählen noch.</small
>
</span>
</label>
<p class="lb-help" data-i18n="lb.sheetHelp">
Der Bestand ist gezählt, aus Ansetzen, Verbrauchen und Rausscannen. Was nicht mehr da ist, unter Löschen
antippen. Rechts steht, was du jetzt ansetzt.
</p>
<div id="m-lb-items"></div>
<div class="modal-btns">
<button type="button" class="btn" data-lb-act="close" data-i18n="vb.close">Schließen</button>
<button type="button" class="btn btn-p" id="m-lb-make" data-lb-act="make" hidden></button>
</div>
</div>
</div>
<!-- Calendar print range modal -->
<div class="modal-bg" id="m-cal-print" role="dialog" aria-modal="true" aria-labelledby="m-cal-print-title" hidden>
<div class="modal">
<h3 id="m-cal-print-title" data-i18n="cal.printTitle">Kalender drucken</h3>
<p data-i18n="cal.printChooseRange">Wähle den Zeitraum für die Aufgabenliste:</p>
<div style="display: flex; flex-direction: column; gap: 8px; margin-top: 6px">
<button
class="btn btn-p"
id="m-cal-print-week"
style="width: 100%; justify-content: flex-start"
data-i18n="cal.printWeek"
>
Woche — Aufgabenliste der aktuellen Woche
</button>
<button
class="btn btn-p"
id="m-cal-print-month"
style="width: 100%; justify-content: flex-start"
data-i18n="cal.printMonth"
>
Monat — Aufgabenliste des aktuellen Monats
</button>
</div>
<div class="modal-btns">
<button class="btn" id="m-cal-print-cancel" data-i18n="calEntry.cancel">Abbrechen</button>
</div>
</div>
</div>
<!-- Search palette. Strg/Cmd+K from anywhere, the field in the sidebar, the
magnifier in the phone topbar. It is a .modal-bg like every other dialog,
but sits near the top of the viewport instead of centred: the list grows
downwards as you type, and a centred box would push its own first result
out from under the eye on every keystroke. -->
<div
class="modal-bg gs-bg no-print"
id="m-search"
role="dialog"
aria-modal="true"
data-i18n-aria-label="search.open"
aria-label="Suchen"
hidden
>
<div class="gs-box">
<div class="gs-field">
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
aria-hidden="true"
>
<circle cx="11" cy="11" r="7" />
<path d="m20 20-3.5-3.5" />
</svg>
<input
type="text"
id="gs-q"
autocomplete="off"
autocapitalize="off"
spellcheck="false"
role="combobox"
aria-expanded="true"
aria-controls="gs-results"
aria-autocomplete="list"
data-i18n-placeholder="search.placeholder"
placeholder="Suchen oder scannen"
/>
<button
type="button"
class="gs-esc"
id="gs-close"
data-i18n-aria-label="common.close"
aria-label="Schließen"
>
<span class="gs-esc-key">esc</span><span class="gs-esc-x" aria-hidden="true">✕</span>
</button>
</div>
<div
class="gs-results"
id="gs-results"
role="listbox"
data-i18n-aria-label="search.results"
aria-label="Treffer"
></div>
<!-- How many. The listbox itself must not be the live region — it would
read every row on every keystroke — and the hint below it is inside
.gs-foot, which a phone does not show. So: a count, out of sight,
announced when it changes. -->
<div id="gs-live" class="gs-live" role="status" aria-live="polite" aria-atomic="true"></div>
<div class="gs-foot">
<span data-i18n="search.keys">↑↓ wählen · ↵ öffnen</span>
<span id="gs-hint" class="gs-hint"></span>
</div>
</div>
</div>
<!-- Confirm modal -->
<div class="modal-bg" id="m-confirm" role="dialog" aria-modal="true" aria-labelledby="m-title" hidden>
<div class="modal">
<h3 id="m-title"></h3>
<p id="m-body"></p>
<div class="modal-btns">
<button class="btn" id="m-cancel" data-i18n="common.cancel">Cancel</button
><button class="btn btn-r" id="m-ok" data-i18n="common.confirm">Confirm</button>
</div>
</div>
</div>
<!-- Three-choice confirm modal (recurring event delete) -->
<div class="modal-bg" id="m-confirm3" role="dialog" aria-modal="true" aria-labelledby="m-title3" hidden>
<div class="modal">
<h3 id="m-title3"></h3>
<p id="m-body3"></p>
<div class="modal-btns">
<button class="btn" id="m-cancel3" data-i18n="common.cancel">Cancel</button
><button class="btn btn-p" id="m-ok3a"></button><button class="btn btn-r" id="m-ok3b"></button>
</div>
</div>
</div>
<!-- Prompt modal -->
<div class="modal-bg" id="m-prompt" role="dialog" aria-modal="true" aria-labelledby="m-pr-title" hidden>
<div class="modal">
<h3 id="m-pr-title"></h3>
<input
type="text"
id="m-pr-input"
style="
width: 100%;
margin: 8px 0;
padding: 8px;
border: 1px solid var(--c-border);
border-radius: 6px;
font-size: 14px;
"
/>
<div class="modal-btns">
<button class="btn" id="m-pr-cancel" data-i18n="common.cancel">Cancel</button
><button class="btn btn-p" id="m-pr-ok" data-i18n="common.ok">OK</button>
</div>
</div>
</div>
<!-- Beimpfen: aus einer gescannten Kultur vieles auf einmal ansetzen —
Körnerbeutel, Flüssigkulturen, Petrischalen, Spritzen, je eine Zahl.
Gefüllt von bmpRender(); geöffnet aus dem Kultur-Fenster. -->
<div
id="m-beimpfen"
style="
display: none;
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.45);
z-index: 1200;
align-items: center;
justify-content: center;
padding: 12px;
"
onclick="if (event.target === this) bmpClose();"
>
<div
class="card"
style="
max-width: 380px;
width: 100%;
margin: 0;
max-height: calc(100vh - 24px);
max-height: calc(100dvh - 24px);
overflow-y: auto;
"
>
<div class="sec" id="bmp-title">—</div>
<div id="bmp-sub" class="muted fs-meta" style="margin: 4px 0 6px"></div>
<div id="bmp-rows"></div>
<div id="bmp-anlage-wrap" class="anlage-wrap" style="margin-top: 8px"></div>
<div id="bmp-preview" class="muted fs-meta" style="margin-top: 8px"></div>
<div style="display: flex; gap: 8px; margin-top: 12px">
<button type="button" class="btn btn-p" id="bmp-go" data-bmp="go" data-i18n="bmp.go">Beimpfen</button>
<button type="button" class="btn" data-bmp="cancel" data-i18n="strains.cancel">Abbrechen</button>
</div>
</div>
</div>
<!-- Quick-create: spin a Charge or Laborarbeit straight from a Sorte recipe.
Global modal (not inside a page) so it can open from the dashboard too. -->
<div
id="ms-quick-modal"
style="
display: none;
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.45);
z-index: 1200;
align-items: center;
justify-content: center;
padding: 12px;
"
onclick="if (event.target === this) msQuickClose();"
>
<!-- Der Dialog wächst um eine Zeile je Größe, und im Querformat ist er
schon mit einer knapp. dvh mit vh davor, wie überall sonst im Blatt:
mit aufgeklappter Tastatur misst vh weiter das ganze Fenster. -->
<div
class="card"
style="
max-width: 380px;
width: 100%;
margin: 0;
max-height: calc(100vh - 24px);
max-height: calc(100dvh - 24px);
overflow-y: auto;
"
>
<div class="sec" id="ms-q-title">—</div>
<div id="ms-q-sub" class="muted fs-meta" style="margin: 4px 0 10px"></div>
<!-- One "Beimpft mit" field for every kind of inoculant — camera first,
since the barcode is on the thing in your hand. Applies to a Charge
and to new Grainspawn alike (scan the LC it came from). Steht ganz
oben: was man in der Hand hat, scannt man zuerst, und es bringt Sorte
und Stamm mit (msqInocAdd). -->
<div id="ms-q-culture-wrap" style="display: none; margin-bottom: 10px">
<label data-i18n="inoc.label">Beimpft mit (optional)</label>
<div style="display: flex; gap: 6px; align-items: center">
<button type="button" class="btn btn-sm" id="ms-q-inoc-scan" style="flex: none; white-space: nowrap">
<span aria-hidden="true">📷</span> <span data-i18n="inoc.scan">Scannen</span>
</button>
<select id="ms-q-inoc-pick" style="flex: 1; min-width: 0" onchange="inocPicked('ms-q')"></select>
</div>
<div id="ms-q-inoc-chips" style="display: flex; flex-wrap: wrap; gap: 4px; margin-top: 5px"></div>
</div>
<div id="ms-q-sorte-wrap" style="display: none; margin-bottom: 8px">
<label data-i18n="msq.pickSorte">Pilzsorte</label>
<select id="ms-q-sorte" onchange="msQuickSorteChanged()"></select>
</div>
<!-- Eine Art aus der Liste, oder mehrere aus derselben Herkunft: "Mehrere"
macht aus der Liste Knöpfe zum Antippen, und jede angetippte Art
bekommt ihre Zahlen (msqMehrereUmschalten, msqArtenRender). -->
<div id="ms-q-labtype-wrap" style="display: none; margin-bottom: 8px">
<div style="display: flex; align-items: center; justify-content: space-between; gap: 8px">
<label data-i18n="msq.labType">Art</label>
<button
type="button"
id="ms-q-mehrere"
class="btn btn-sm"
aria-pressed="false"
style="min-height: var(--tap-sm)"
data-i18n="msq.mehrere"
>
Mehrere
</button>
</div>
<div id="ms-q-arten" role="group" style="display: none; flex-wrap: wrap; gap: 6px; margin-top: 4px"></div>
<select id="ms-q-labtype" onchange="msQuickLabTypeChanged()">
<!-- Beschriftungen aus derselben Quelle wie im langen Formular:
fest eingetragener Text zeigte allen Sprachen dieselbe an.
G2G fehlt bewusst — logLabWork() weist den Typ ab, er wird
über die Scan-Leiste erfasst. -->
<option value="MC" data-i18n="lab.registerMC">Slant registrieren</option>
<option value="PD" data-i18n="lab.pdTransfer">Petrischalen-Transfer</option>
<option value="LC" data-i18n="lab.lcPrep">Flüssigkultur ansetzen</option>
<option value="SY" data-i18n="lab.syPrep">Spritzen abfüllen</option>
<option value="KB" data-i18n="lab.kb">Körnerbrut</option>
<option value="DU" data-i18n="lab.duPrep">Impfdübel beimpfen</option>
</select>
</div>
<!-- Bei mehreren Arten: je eine Anzahl für Petrischalen und Flüssigkultur.
Spritzen und Körnerbrut bringen ihre eigenen Felder mit (unten). -->
<div id="ms-q-multi" style="display: none; margin-bottom: 8px"></div>
<!-- Spritzen gibt es in 10, 20 und 50 ml, und abgefüllt wird oft mehr als
eine Größe auf einmal: je Größe eine Anzahl, ein Etiketten-Fenster für
alle (syMengenHtml). Nur eine aufgenommene Spritze ist genau eine und
wählt ihre Größe mit drei Knöpfen (msQuickRender). -->
<div id="ms-q-sy-wrap" style="display: none; margin-bottom: 8px">
<div
id="ms-q-sy-head"
style="display: none; font-weight: 600; padding-top: 8px; border-top: 1px solid var(--c-border-light)"
data-i18n="msq.art.SY"
>
Spritzen
</div>
<label data-i18n="sy.perSize">Anzahl je Größe</label>
<div id="ms-q-sy"></div>
</div>
<div id="ms-q-ml-wrap" style="display: none; margin-bottom: 8px">
<label data-i18n="sy.size">Größe</label>
<div id="ms-q-ml"></div>
</div>
<!-- Körnerbrut goes into bags of 1, 2 and 5 kg, and one Ansatz often into
more than one size: same Flüssigkultur, same Charge. So the count does
not live in the shared Anzahl stepper here — it lives per size, one
counter each, the same shape as the syringes (kbMengenHtml). Shown for
Körnerbrut in either mode — as Laborarbeit and as the Charge of a
Körner-Sorte (see msQuickRender). -->
<div id="ms-q-sizes-wrap" style="display: none; margin-bottom: 8px">
<div
id="ms-q-kb-head"
style="display: none; font-weight: 600; padding-top: 8px; border-top: 1px solid var(--c-border-light)"
data-i18n="msq.art.KB"
>
Körnerbrut
</div>
<label data-i18n="sy.perSize">Anzahl je Größe</label>
<div id="ms-q-sizes"></div>
</div>
<div class="g2" id="ms-q-grid">
<div id="ms-q-qty-wrap">
<label data-i18n="msq.qty">Anzahl</label>
<div style="display: flex; align-items: center; gap: 8px">
<button
type="button"
aria-label="weniger"
onclick="msqQtyStep(-1)"
style="
width: var(--tap);
height: var(--tap);
min-width: 44px;
min-height: 44px;
flex: none;
border-radius: 10px;
border: 1px solid var(--c-border);
background: var(--c-surface);
font-size: 22px;
cursor: pointer;
"
>
−
</button>
<input
type="number"
id="ms-q-qty"
min="1"
value="1"
inputmode="numeric"
oninput="msQuickPreview()"
style="flex: 1; text-align: center; font-size: 18px"
/>
<button
type="button"
aria-label="mehr"
onclick="msqQtyStep(1)"
style="
width: var(--tap);
height: var(--tap);
min-width: 44px;
min-height: 44px;
flex: none;
border-radius: 10px;
border: 1px solid var(--c-border);
background: var(--c-surface);
font-size: 22px;
cursor: pointer;
"
>
+
</button>
</div>
</div>
<div id="ms-q-days-wrap">
<label data-i18n="batch.incDays">Inkubationstage</label>
<input type="number" id="ms-q-days" min="1" value="14" />
</div>
<!-- Impfdübel: wie viele Dübel ein Beutel hat. Steht aufs Etikett
("ca. 100") und bleibt fürs nächste Mal stehen, wie eine Größe. -->
<div id="ms-q-stueck-wrap" style="display: none">
<label for="ms-q-stueck" data-i18n="lab.duStueck">Dübel je Beutel</label>
<input type="number" id="ms-q-stueck" min="1" inputmode="numeric" />
</div>
</div>
<!-- Der Stamm kommt aus der Liste der Sorte (Pilzsorten → Stämme), nicht
mehr als freier Text — dieselbe Linie hieß sonst jedes Mal anders.
Gefüllt von stammRender(), sobald die Sorte feststeht. -->
<div id="ms-q-stamm-wrap" class="stamm-wrap"></div>
<!-- Kept only so msQuickConfirm can hand a culture to nb-culture/gs-culture. -->
<select id="ms-q-culture" style="display: none"></select>
<!-- Angesetzt am: zu und auf heute, bis es jemand braucht (anlageRender). -->
<div id="ms-q-anlage-wrap" class="anlage-wrap"></div>
<div id="ms-q-preview" class="muted fs-meta" style="margin-top: 8px"></div>
<div style="display: flex; gap: 8px; margin-top: 12px">
<button class="btn btn-p" id="ms-q-go" onclick="msQuickConfirm()" data-i18n="msq.create">Anlegen</button>
<button class="btn" onclick="msQuickClose()" data-i18n="strains.cancel">Abbrechen</button>
</div>
</div>
</div>
<!-- Change password modal -->
<div
class="modal-bg"
id="change-pw-modal"
role="dialog"
aria-modal="true"
aria-labelledby="chpw-title-h3"
style="
display: none;
position: fixed;
inset: 0;
z-index: 9999;
background: rgba(0, 0, 0, 0.5);
align-items: center;
justify-content: center;
"
>
<div class="modal" style="max-width: 360px">
<h3 id="chpw-title-h3" data-i18n="chpw.title">Change password</h3>
<!-- Hidden username companion: lets iOS Keychain / 1Password / etc.
recognise this as a credential update for the current user, so
the new password gets saved to the right entry instead of being
treated as an unrelated form. -->
<input type="text" id="chpw-username-helper" autocomplete="username" hidden />
<label style="display: block; margin-bottom: 8px"
><span data-i18n="chpw.current">Current password</span><br /><input
type="password"
id="chpw-current"
style="width: 100%; padding: 6px; margin-top: 4px"
autocomplete="current-password"
/></label>
<label style="display: block; margin-bottom: 8px"
><span data-i18n="chpw.new">New password (min 8 chars)</span><br /><input
type="password"
id="chpw-new"
style="width: 100%; padding: 6px; margin-top: 4px"
autocomplete="new-password"
/></label>
<p id="chpw-status" style="min-height: 1.2em; margin: 8px 0"></p>
<div class="modal-btns">
<button id="btn-1" class="btn" data-i18n="common.cancel">Cancel</button>
<button id="act-2" class="btn btn-p" data-i18n="chpw.change">Change</button>
</div>
</div>
</div>
<!-- Note modal -->
<div class="modal-bg" id="m-note" role="dialog" aria-modal="true" aria-labelledby="m-note-title" hidden>
<div class="modal">
<h3 id="m-note-title" data-i18n="note.editTitle">Edit note</h3>
<textarea
id="m-note-text"
rows="4"
data-i18n-placeholder="note.placeholder"
placeholder="Type your note here..."
></textarea>
<div class="modal-btns">
<button id="cls-3" class="btn" data-i18n="common.cancel">Cancel</button
><button id="act-4" class="btn btn-p" data-i18n="common.save">Save</button>
</div>
</div>
</div>
<!-- Weekly rhythm: one theme per weekday. Rows are built in JS so the theme
list stays defined in one place instead of seven hardcoded selects. -->
<div class="modal-bg" id="m-rhythm" role="dialog" aria-modal="true" aria-labelledby="rhythm-title" hidden>
<div class="modal">
<!-- Das X oben, weil der Weg hinaus sonst unten liegt: sieben Wochentage
mit Menge, Sorte und Notiz sind höher als ein Telefonschirm, und um
abzubrechen musste man erst durch das ganze Fenster scrollen. -->
<div class="wkf-head">
<h3 id="rhythm-title" data-i18n="rhythm.title">Wochenrhythmus</h3>
<button id="rhythm-close" class="modal-close-icon" data-i18n-aria-label="common.close" aria-label="Close">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<path d="M18 6 6 18M6 6l12 12" />
</svg>
</button>
</div>
<p class="fs-meta" id="rhythm-hint" style="color: var(--c-text-muted); margin: 0 0 10px"></p>
<div id="rhythm-rows"></div>
<!-- Die Wochenmenge. Gedacht wird in "N die Woche", eingetragen werden
sieben Tageszahlen — ohne diese Zeile muss der Kopf addieren. Je
Thema getrennt, weil Substrat und Körnerbrut nicht dieselbe Einheit
sind und eine gemeinsame Summe nichts bedeuten würde. -->
<div id="rhythm-total" class="rhythm-total"></div>
<!-- Was die Anlage sonst noch regelmäßig tut. Der Rhythmus oben zählt
Produktion und kann nur, was eine Buchung hinterlässt; hier stehen
die Aufgaben, die keine hinterlassen — putzen, warten, prüfen —
mit eigenem Namen, eigenem Takt und einem Haken. Gebaut in JS, weil
die Liste wächst, und nur für Admins sichtbar. -->
<div id="rhythm-own" class="rhythm-own" hidden></div>
<!-- Ab wann gerechnet wird. Ohne das steht jede Woche vor der
Inbetriebnahme als verfehlt da, und daran ist nichts nachzuholen. -->
<div style="display: flex; align-items: center; gap: 8px; margin-top: 10px">
<button type="button" class="btn btn-sm fs-xs" data-action="rhythm-start" data-i18n="rhythm.startSet">
Ab wann zaehlen
</button>
<span class="fs-xs" id="rhythm-start-now" style="color: var(--c-text-muted)"></span>
</div>
<div class="modal-btns">
<button id="rhythm-cancel" class="btn" data-i18n="common.cancel">Cancel</button
><button id="rhythm-save" class="btn btn-p" data-i18n="common.save">Save</button>
</div>
</div>
</div>
<!-- Was ein einzelner Tag verlangt — beide Zahlen an einer Stelle.
Vorher war die wiederkehrende Zahl im Wochenrhythmus und die einmalige
in einer nackten Abfrage im Kalender, jede ohne Erwähnung der anderen.
Wer die 72 im Kalender sah, konnte nicht wissen, ob sie die Regel oder
die Ausnahme war — und dass er eben die Ausnahme setzte, stand nirgends.
Hier stehen beide untereinander, beschriftet, mit ihrer Summe. -->
<div class="modal-bg" id="m-dayqty" role="dialog" aria-modal="true" aria-labelledby="dayqty-title" hidden>
<div class="modal">
<h3 id="dayqty-title"></h3>
<p class="fs-meta dayqty-sub" id="dayqty-sub"></p>
<label class="dayqty-row" for="dayqty-plan">
<span class="dayqty-lbl">
<span data-i18n="dayqty.plan">Wochenplan</span>
<span class="fs-xs dayqty-hint" id="dayqty-plan-hint"></span>
</span>
<input type="number" min="0" step="1" id="dayqty-plan" class="dayqty-in" />
</label>
<label class="dayqty-row" for="dayqty-extra">
<span class="dayqty-lbl">
<span data-i18n="dayqty.extra">Extra nur diese Woche</span>
<span class="fs-xs dayqty-hint" id="dayqty-extra-hint"></span>
</span>
<input type="number" step="1" id="dayqty-extra" class="dayqty-in" />
</label>
<!-- Die Summe ist die Zahl, die am Ende zählt. Ohne sie muss der Leser
die beiden Felder im Kopf addieren, und genau das war die Klage. -->
<div class="fs-sm dayqty-sum" id="dayqty-sum" aria-live="polite"></div>
<div class="modal-btns">
<button id="dayqty-cancel" class="btn" data-i18n="common.cancel">Cancel</button
><button id="dayqty-save" class="btn btn-p" data-i18n="common.save">Save</button>
</div>
</div>
</div>
<!-- Calendar event modal (create/edit/move) -->
<div class="modal-bg" id="m-cal-entry" role="dialog" aria-modal="true" aria-labelledby="cal-entry-title" hidden>
<div class="modal">
<h3 id="cal-entry-title" data-i18n="calEntry.titleNew">Neuer Eintrag</h3>
<input type="hidden" id="cal-entry-id" />
<input type="hidden" id="cal-entry-mode" value="create" />
<div style="margin-bottom: 8px">
<label data-i18n="calEntry.name">Titel</label>
<input
type="text"
id="cal-entry-name"
data-i18n-placeholder="calEntry.namePhTask"
placeholder="z.B. Luftfeuchtezelt reinigen"
/>
</div>
<div class="g2" style="margin-bottom: 8px">
<div>
<label data-i18n="calEntry.category">Kategorie</label>
<select id="cal-entry-type-select">
<option value="task" data-i18n="calEntry.cat.task">Aufgabe</option>
<option value="custom" data-i18n="calEntry.cat.custom">Eigener Termin</option>
<option value="meeting" data-i18n="calEntry.cat.meeting">Meeting</option>
<option value="delivery" data-i18n="calEntry.cat.delivery">Lieferung</option>
<option value="maintenance" data-i18n="calEntry.cat.maintenance">Wartung</option>
<option value="pickup" data-i18n="calEntry.cat.pickup">Abholfenster</option>
</select>
</div>
<div id="cal-entry-prio-wrap">
<label data-i18n="calEntry.prio">Priorität</label>
<select id="cal-entry-prio">
<option value="low" data-i18n="calEntry.prio.low">Niedrig</option>
<option value="med" selected data-i18n="calEntry.prio.med">Mittel</option>
<option value="high" data-i18n="calEntry.prio.high">Hoch</option>
</select>
</div>
</div>
<div class="g2" style="margin-bottom: 8px">
<div><label data-i18n="calEntry.date">Datum</label><input type="date" id="cal-entry-date" /></div>
<div id="cal-entry-enddate-wrap" style="display: none">
<label data-i18n="calEntry.endDate">Bis (optional)</label><input type="date" id="cal-entry-end-date" />
</div>
</div>
<div id="cal-entry-allday-wrap" style="margin-bottom: 8px; display: none">
<label style="display: flex; align-items: center; gap: 6px; cursor: pointer">
<input type="checkbox" id="cal-entry-allday" checked style="width: auto" />
<span data-i18n="calEntry.allDay">Ganztägig</span>
</label>
</div>
<div class="g2" id="cal-entry-times" style="margin-bottom: 8px; display: none">
<div>
<label data-i18n="calEntry.from">Von</label
><input
type="text"
id="cal-entry-start-time"
value="09:00"
inputmode="numeric"
maxlength="5"
pattern="([01][0-9]|2[0-3]):[0-5][0-9]"
placeholder="HH:MM"
autocomplete="off"
/>
</div>
<div>
<label data-i18n="calEntry.to">Bis</label
><input
type="text"
id="cal-entry-end-time"
value="10:00"
inputmode="numeric"
maxlength="5"
pattern="([01][0-9]|2[0-3]):[0-5][0-9]"
placeholder="HH:MM"
autocomplete="off"
/>
</div>
</div>
<div class="g2" id="cal-entry-recurrence-wrap" style="margin-bottom: 8px; display: none">
<div>
<label data-i18n="calEntry.recurrence">Wiederholung</label>
<select id="cal-entry-recurrence">
<option value="" data-i18n="calEntry.rec.none">Keine</option>
<option value="daily" data-i18n="calEntry.rec.daily">Täglich</option>
<option value="weekly" data-i18n="calEntry.rec.weekly">Wöchentlich</option>
<option value="monthly" data-i18n="calEntry.rec.monthly">Monatlich</option>
</select>
</div>
<div id="cal-entry-recurrence-until-wrap" style="display: none">
<label data-i18n="calEntry.recurrenceUntil">Wiederholen bis (optional)</label>
<input type="date" id="cal-entry-recurrence-until" />
</div>
</div>
<!-- Ort und Plätze. Der Ort steht jedem Termin frei; die Plätze gibt es
nur beim Abholfenster, weil nur dort jemand von außen einen davon
belegt. -->
<div class="g2" id="cal-entry-place-wrap" style="margin-bottom: 8px; display: none">
<div>
<label data-i18n="calEntry.place">Ort</label>
<select id="cal-entry-location"></select>
</div>
<div id="cal-entry-capacity-wrap" style="display: none">
<label data-i18n="calEntry.capacity">Plätze (optional)</label>
<input
type="number"
id="cal-entry-capacity"
min="0"
step="1"
data-i18n-placeholder="calEntry.capacityPh"
placeholder="leer = unbegrenzt"
/>
</div>
</div>
<div style="margin-bottom: 8px">
<label data-i18n="calEntry.desc">Beschreibung (optional)</label>
<textarea class="fs-meta"
id="cal-entry-desc"
rows="2"
data-i18n-placeholder="calEntry.descPh"
placeholder="Details..."
style="width: 100%; padding: 6px 8px; border: 1px solid var(--c-border); border-radius: 6px; resize: vertical; font-family: inherit"
></textarea>
</div>
<div id="cal-entry-task-assign-wrap" style="margin-bottom: 8px">
<label data-i18n="calEntry.assignTo">Zuweisen an</label>
<div
id="cal-task-assignees"
style="
display: flex;
flex-wrap: wrap;
gap: 4px;
min-height: 32px;
padding: 6px 8px;
border: 1px solid var(--c-border);
border-radius: 6px;
cursor: pointer;
"
>
<span class="fs-meta" style="color: var(--c-text-muted)" data-i18n="calEntry.allClickToSelect"
>Alle (klicken um auszuwählen)</span
>
</div>
<div
id="cal-task-assignee-dropdown"
style="
display: none;
border: 1px solid var(--c-border);
border-radius: 6px;
max-height: 150px;
overflow-y: auto;
margin-top: 2px;
"
></div>
</div>
<div id="cal-entry-ev-assign-wrap" style="margin-bottom: 8px; display: none">
<label data-i18n="calEntry.assignedTo">Zugewiesen an</label>
<div
id="cal-ev-assignees"
style="
display: flex;
flex-wrap: wrap;
gap: 4px;
min-height: 32px;
padding: 6px 8px;
border: 1px solid var(--c-border);
border-radius: 6px;
cursor: pointer;
"
>
<span class="fs-meta" style="color: var(--c-text-muted)" data-i18n="calEntry.allClickToSelect"
>Alle (klicken um auszuwählen)</span
>
</div>
<div
id="cal-ev-assignee-dropdown"
style="
display: none;
border: 1px solid var(--c-border);
border-radius: 6px;
max-height: 150px;
overflow-y: auto;
margin-top: 2px;
"
></div>
</div>
<div id="cal-entry-private-wrap" style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px">
<label class="fs-xs"
style="display: flex; align-items: center; gap: 5px; cursor: pointer; margin: 0; color: var(--c-text-muted)"
>
<input type="checkbox" id="cal-entry-private" style="width: auto" />
<span data-i18n="calEntry.private">Nur für zugewiesene Person sichtbar</span>
</label>
</div>
<div class="modal-btns">
<button
class="btn btn-r"
id="cal-entry-del-btn"
style="display: none; margin-right: auto"
data-i18n="calEntry.delete"
>
Löschen
</button>
<button class="btn" id="cal-entry-cancel-btn" data-i18n="calEntry.cancel">Abbrechen</button>
<button class="btn btn-p" id="cal-entry-save-btn" data-i18n="calEntry.save">Speichern</button>
</div>
</div>
</div>
<!-- Calendar event detail (read-only) modal -->
<div class="modal-bg" id="m-cal-detail" role="dialog" aria-modal="true" aria-labelledby="cal-detail-title" hidden>
<div class="modal">
<h3 id="cal-detail-title" style="margin-bottom: 4px; word-break: break-word"></h3>
<div class="fs-meta" id="cal-detail-meta" style="color: var(--c-text-muted); margin-bottom: 12px"></div>
<div id="cal-detail-badges" style="margin-bottom: 10px; display: flex; gap: 6px; flex-wrap: wrap"></div>
<div class="fs-meta" id="cal-detail-assignee" style="margin-bottom: 10px; color: var(--c-text-sec)"></div>
<div
id="cal-detail-desc"
style="
font-size: 13px;
line-height: 1.6;
color: var(--c-text-sec);
margin-bottom: 14px;
white-space: pre-wrap;
word-break: break-word;
"
></div>
<div class="modal-btns" id="cal-detail-btns" style="flex-wrap: wrap"></div>
</div>
</div>
<!-- Batch-add modal -->
<div class="modal-bg" id="m-batchadd" role="dialog" aria-modal="true" aria-labelledby="m-batchadd-title" hidden>
<div class="modal">
<h3 id="m-batchadd-title" data-i18n="batchAddModal.title">Add entire batch to location</h3>
<p data-i18n="batchAddModal.desc">All bags will be logged as ADD in one step.</p>
<div style="display: flex; flex-direction: column; gap: 8px">
<div>
<label data-i18n="batchAddModal.batch">Batch</label
><select id="ba-batch">
<option value="" data-i18n="batchAddModal.chooseBatch">— choose batch —</option>
</select>
</div>
<div>
<label data-i18n="batchAddModal.location">Location / rack</label
><select id="ba-loc"></select>
</div>
<div class="fs-meta" id="ba-prev" style="color: var(--c-text-sec); min-height: 16px"></div>
</div>
<div class="modal-btns">
<button id="cls-7" class="btn" data-i18n="common.cancel">Cancel</button
><button id="act-8" class="btn btn-p" data-i18n="batchAddModal.addAll">Add all bags</button>
</div>
</div>
</div>
<!-- Etiketten drucken: erscheint direkt nach dem Anlegen einer Charge.
Vorher stand das als Karte am Ende der Charge-Seite — der Zonenwähler
schloss sich, und das Nächste, was anstand, lag außerhalb des Bildes. -->
<div class="modal-bg" id="m-batch-print" role="dialog" aria-modal="true" aria-labelledby="m-batch-print-title" hidden>
<div class="modal" style="max-width: 480px">
<div class="wkf-head">
<h3 id="m-batch-print-title" data-i18n="batch.printTitle">Etiketten drucken</h3>
<button id="nbp-close" class="modal-close-icon" data-i18n-aria-label="common.close" aria-label="Close">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<path d="M18 6 6 18M6 6l12 12" />
</svg>
</button>
</div>
<div id="nb-result">
<div id="nbp-head" style="font-size: 13px; color: var(--c-text-sec); margin-bottom: 8px"></div>
<div class="sec" data-i18n="batch.generatedIds">Generated bag IDs</div>
<div
id="nb-bags"
style="display: flex; flex-wrap: wrap; gap: 4px; margin-top: 6px; max-height: 220px; overflow-y: auto"
></div>
</div>
<!-- Der Druckknopf steht ganz oben und über die volle Breite: er ist der
Grund, warum der Dialog aufgeht. Die beiden Auswege darunter. -->
<div class="nbp-btns">
<button id="prt-25" class="btn btn-p" data-i18n="batch.printLabels">Print labels for this batch</button>
<div class="nbp-sub">
<button id="prt-25-opts" class="btn" data-i18n="batch.printOptions">Print options…</button>
<button id="nbp-later" class="btn" data-i18n="batch.printLater">Später</button>
</div>
</div>
</div>
</div>
<!-- Etiketten für Kulturen: erscheint direkt nach dem Anlegen von Laborarbeit,
wie m-batch-print nach einer Charge — nur ohne Zonenwahl davor, eine
Kultur hat keinen Standort. Vorher stand das als Karte im langen
Laborformular; der Schnelldialog, der das Formular nur noch als Ablage
benutzt, zeigte sie nie. -->
<div class="modal-bg" id="m-culture-print" role="dialog" aria-modal="true" aria-labelledby="m-culture-print-title" hidden>
<div class="modal" style="max-width: 480px">
<div class="wkf-head">
<h3 id="m-culture-print-title" data-i18n="batch.printTitle">Etiketten drucken</h3>
<button id="cpr-close" class="modal-close-icon" data-i18n-aria-label="common.close" aria-label="Close">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<path d="M18 6 6 18M6 6l12 12" />
</svg>
</button>
</div>
<div id="cpr-head" style="font-size: 13px; color: var(--c-text-sec); margin-bottom: 8px"></div>
<div class="sec" id="cpr-ids-title" data-i18n="lab.createdIds">Created culture IDs</div>
<div
id="cpr-ids"
style="display: flex; flex-wrap: wrap; gap: 4px; margin-top: 6px; max-height: 220px; overflow-y: auto"
></div>
<div class="nbp-btns">
<button id="prt-cpr" class="btn btn-p" data-i18n="lab.printLabelsNow">Print labels for these cultures</button>
<div class="nbp-sub">
<button id="prt-cpr-opts" class="btn" data-i18n="batch.printOptions">Print options…</button>
<button id="cpr-later" class="btn" data-i18n="batch.printLater">Später</button>
</div>
</div>
</div>
</div>
<!-- Move bag modal -->
<div class="modal-bg" id="m-locmove" role="dialog" aria-modal="true" aria-labelledby="lm-title" hidden>
<div class="modal" style="max-width: 420px">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 4px">
<h3 id="lm-title" style="font-family: monospace; font-size: 15px" data-i18n="locmove.bag">Bag</h3>
<button id="cls-9" class="btn btn-sm" data-i18n="common.close">Close</button>
</div>
<div class="fs-meta" id="lm-info" style="color: var(--c-text-muted); margin-bottom: 12px"></div>
<div class="fs-meta" style="font-weight: 600; margin-bottom: 8px" data-i18n="locmove.moveTo">Move to:</div>
<div id="lm-grid" style="display: flex; flex-wrap: wrap; gap: 6px"></div>
<div id="lm-confirm" style="display: none"></div>
<div style="border-top: 1px solid var(--c-border); margin-top: 14px; padding-top: 12px">
<button id="btn-10" class="btn btn-sm btn-r" style="width: 100%" data-i18n="locmove.removeBag">
Remove this bag
</button>
</div>
</div>
</div>
<!-- Zone picker modal — mandatory after batch creation -->
<div
class="modal-bg"
id="m-zone-pick"
role="dialog"
aria-modal="true"
aria-labelledby="zp-title"
style="display: none"
>
<div class="modal" style="max-width: 400px">
<h3 id="zp-title" data-i18n="batch.whereGo">Where do these bags go?</h3>
<p class="fs-meta" id="zp-info" style="color: var(--c-text-muted); margin-bottom: 12px"></p>
<div id="zp-zones" style="display: flex; flex-direction: column; gap: 8px"></div>
</div>
</div>
<!-- Move-batch modal — select destination for all bags in a batch -->
<div class="modal-bg" id="m-move-batch" role="dialog" aria-modal="true" aria-labelledby="mb-title" hidden>
<div class="modal" style="max-width: 380px">
<h3 id="mb-title"></h3>
<div
id="mb-zones"
style="
display: flex;
flex-direction: column;
gap: 4px;
max-height: 55vh;
overflow-y: auto;
margin-bottom: 12px;
"
></div>
<div class="modal-btns">
<button class="btn" id="mb-cancel-btn" data-i18n="nav.cancel">Abbrechen</button>
</div>
</div>
</div>
<!-- Bag-select modal — pick a subset of bags in a batch to move together -->
<div class="modal-bg" id="m-bagselect" role="dialog" aria-modal="true" aria-labelledby="bs-title" hidden>
<div class="modal" style="max-width: 520px">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px">
<h3 id="bs-title" data-i18n="bagSelect.title">Select bags to move</h3>
<button id="bs-cancel" class="btn btn-sm" data-i18n="common.close">Close</button>
</div>
<p class="fs-meta" id="bs-subtitle" style="color: var(--c-text-muted); margin-bottom: 4px"></p>
<p class="fs-xs"
id="bs-hint"
style="color: var(--c-text-muted); margin-bottom: 10px; font-style: italic"
data-i18n="bagSelect.scanHint"
>
Tap bags below or scan more QR codes to toggle them.
</p>
<div
id="bs-bags"
style="display: flex; flex-wrap: wrap; gap: 8px; max-height: 45vh; overflow-y: auto; margin-bottom: 14px"
></div>
<div style="display: flex; gap: 8px; justify-content: space-between; align-items: center; flex-wrap: wrap">
<span id="bs-count" style="font-size: 13px; color: var(--c-text-sec); font-weight: 600"></span>
<button id="bs-continue" class="btn btn-p" data-i18n="bagSelect.continueToZone">Continue → pick zone</button>
</div>
</div>
</div>
<!-- Bag info modal — opens when scanning a bag with no action set -->
<div class="modal-bg" id="m-baginfo" role="dialog" aria-modal="true" aria-labelledby="bi-title" hidden>
<div class="modal" style="max-width: 540px">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px">
<h3 id="bi-title" data-i18n="bagInfo.title">Bag info</h3>
<!-- Ein Tipp, und der Block steht im Blockbuch, mit seiner Herkunft.
Im Kopf, damit er beim Scannen in der Fruchtung nicht unter den
großen Knöpfen liegt. Nur für Blöcke, nicht für Körnerbrut. -->
<button type="button" id="bi-good" class="btn btn-sm block-good" aria-pressed="false" hidden>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round" aria-hidden="true"><path d="M12 3l2.7 5.6 6.1.9-4.4 4.3 1 6.1L12 17l-5.4 2.9 1-6.1-4.4-4.3 6.1-.9z" /></svg><span data-i18n="block.good">Good block</span>
</button>
<button id="cls-11" class="modal-close-icon" data-i18n-aria-label="common.close" aria-label="Close">
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>
</button>
</div>
<div id="bi-body"></div>
<div id="bi-actions" style="margin-top: 14px"></div>
<div id="bi-notes"></div>
</div>
</div>
<!-- Zone check (stocktake): tick off the bags you actually find in a zone,
then fix whatever the app still expects there but you didn't. -->
<!-- How a mix was made, and what came out of it. -->
<div class="modal-bg" id="m-subinfo" role="dialog" aria-modal="true" aria-labelledby="si-title" hidden>
<div class="modal" style="max-width: 560px">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px">
<h3 id="si-title">Ansatz</h3>
<button id="si-close" class="modal-close-icon" data-i18n-aria-label="common.close" aria-label="Close">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>
</button>
</div>
<div id="si-body"></div>
<div id="si-actions" style="margin-top: 14px"></div>
</div>
</div>
<div class="modal-bg" id="m-zonecheck" role="dialog" aria-modal="true" aria-labelledby="zc-title" hidden>
<div class="modal" style="max-width: 560px">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 4px">
<h3 id="zc-title">Zone prüfen</h3>
<button id="zc-close" class="modal-close-icon" data-i18n-aria-label="common.close" aria-label="Close">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<line x1="18" y1="6" x2="6" y2="18" />
<line x1="6" y1="6" x2="18" y2="18" />
</svg>
</button>
</div>
<div id="zc-count" style="font-size: 13px; color: var(--c-text-sec); margin-bottom: 10px"></div>
<div id="zc-body" style="max-height: 55vh; overflow-y: auto"></div>
<div id="zc-foot" style="display: flex; gap: 8px; flex-wrap: wrap; margin-top: 14px"></div>
</div>
</div>
<!-- Work list: findable, printable pick/walk sheet from the dashboard chips -->
<div class="modal-bg" id="m-worklist" role="dialog" aria-modal="true" aria-labelledby="wl-title" hidden>
<div class="modal" style="max-width: 560px">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px">
<h3 id="wl-title">Liste</h3>
<button id="wl-close" class="modal-close-icon" data-i18n-aria-label="common.close" aria-label="Close">
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>
</button>
</div>
<div id="wl-group" style="display: flex; gap: 8px; margin-bottom: 12px">
<button
type="button"
data-wl-group="location"
aria-pressed="true"
class="wl-group-btn"
data-i18n="worklist.byLocation"
>
Nach Ort
</button>
<button
type="button"
data-wl-group="batch"
aria-pressed="false"
class="wl-group-btn"
data-i18n="worklist.byBatch"
>
Nach Charge
</button>
</div>
<div id="wl-body" style="max-height: 60vh; overflow-y: auto"></div>
<div style="display: flex; justify-content: flex-end; margin-top: 14px">
<button id="wl-print" class="btn btn-p" data-i18n="worklist.print">Drucken</button>
</div>
</div>
</div>
<!-- Contamination report drill-down — full-size photos + delete -->
<div class="modal-bg" id="m-contam-detail" role="dialog" aria-modal="true" aria-labelledby="cd-title" hidden>
<div class="modal" style="max-width: 720px">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px">
<h3 id="cd-title" data-i18n="contam.detailTitle">Kontaminationsbericht</h3>
<button id="cls-cd" class="modal-close-icon" data-i18n-aria-label="common.close" aria-label="Close">
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>
</button>
</div>
<div id="cd-body"></div>
<div style="display: flex; gap: 8px; justify-content: space-between; margin-top: 14px; flex-wrap: wrap">
<button id="cd-delete" type="button" class="btn btn-sm btn-r" data-i18n="contam.delete">Löschen</button>
<button id="cd-close" type="button" class="btn btn-sm" data-i18n="common.close">Schließen</button>
</div>
</div>
</div>
<!-- Work-step flow modal — one shell for every click-driven Arbeitsgang.
Header, step chips, body and footer are rendered by wkRender() so the
batch picker, the harvest list, the move target and the receipt all
look and behave the same. Nothing in here depends on a scanner; the
scanner only jumps ahead a step where one is present. -->
<div class="modal-bg" id="m-work-flow" role="dialog" aria-modal="true" aria-labelledby="wkf-title" hidden>
<div class="modal" style="max-width: 620px">
<div class="wkf-head">
<button id="wkf-back" class="btn btn-sm" hidden data-i18n="work.back">Zurück</button>
<h3 id="wkf-title">—</h3>
<button id="wkf-close" class="modal-close-icon" data-i18n-aria-label="common.close" aria-label="Close">
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>
</button>
</div>
<div id="wkf-steps" class="wkf-steps"></div>
<div id="wkf-body"></div>
<div id="wkf-foot" class="wkf-foot"></div>
</div>
</div>
<!-- Contamination report modal — capture photo + tag type for a bag/batch -->
<div class="modal-bg" id="m-contam-report" role="dialog" aria-modal="true" aria-labelledby="cr-title" hidden>
<div class="modal" style="max-width: 540px">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px">
<h3 id="cr-title" data-i18n="contam.title">Kontamination melden</h3>
<button id="cls-cr" class="modal-close-icon" data-i18n-aria-label="common.close" aria-label="Close">
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>
</button>
</div>
<div
id="cr-target"
style="
font-family: monospace;
font-size: 13px;
color: var(--c-text-sec);
margin-bottom: 14px;
padding: 8px 10px;
background: var(--c-bg);
border-radius: 6px;
"
></div>
<!-- Am Regal braucht es nur das: Beutel scannen, Senden. Foto und Notiz
stehen bereit, wenn man will; Typ, Schwere und das Verschieben
liegen unter "Mehr", zugeklappt. Vorher stand alles offen da, zehn
Typen, drei Schweregrade, ein Kästchen, und das Fenster füllte den
ganzen Bildschirm, bevor man an Senden kam. -->
<div class="cr-quick">
<div id="cr-photo-tiles" class="contam-photo-tiles"></div>
<input type="file" id="cr-file-input" accept="image/*" capture="environment" multiple hidden />
<div class="fs-xs" id="cr-photo-status" style="color: var(--c-text-muted)"></div>
<input
type="text"
id="cr-notes"
maxlength="2000"
data-i18n-placeholder="contam.notesPlaceholder"
placeholder="Notiz (optional)"
/>
</div>
<details id="cr-more" class="cr-more">
<summary data-i18n="contam.more">Mehr: Typ, Schwere, nach CONTAM</summary>
<div class="cr-more-body">
<label class="cr-label" data-i18n="contam.type">Typ</label>
<div id="cr-type-grid" class="contam-type-grid"></div>
<label class="cr-label" data-i18n="contam.severity">Schwere</label>
<div class="contam-severity-row" id="cr-severity-row">
<button type="button" class="contam-sev-btn active" data-sev="minor" data-i18n="contam.sevMinor">
Klein
</button>
<button type="button" class="contam-sev-btn" data-sev="major" data-i18n="contam.sevMajor">
Größer
</button>
<button type="button" class="contam-sev-btn" data-sev="lost" data-i18n="contam.sevLost">Total</button>
</div>
<!-- Auto-MOVE: on by default, whatever the severity. When checked,
the server inserts a MOVE-to-CONTAM scan-log entry alongside
the report. -->
<label id="cr-auto-move-row" class="cr-auto-move-row">
<input type="checkbox" id="cr-auto-move" />
<span data-i18n="contam.autoMove">Bag automatisch nach CONTAM verschieben</span>
</label>
</div>
</details>
<div style="display: flex; gap: 8px; justify-content: flex-end; margin-top: 16px; flex-wrap: wrap">
<button id="cr-cancel" type="button" class="btn btn-sm" data-i18n="common.cancel">Abbrechen</button>
<button id="cr-submit" type="button" class="btn btn-sm btn-p" data-i18n="contam.submit">Senden</button>
</div>
</div>
</div>
<!-- Photo annotation modal — finger-drawing on a contamination photo
before submit. Used from the capture-modal photo tile (tap a tile
to annotate, X to remove, + tile to add). -->
<div class="modal-bg" id="m-photo-annotate" role="dialog" aria-modal="true" aria-labelledby="pa-title" hidden>
<div class="pa-shell">
<div class="pa-header">
<span class="pa-title" id="pa-title" data-i18n="contam.annotateTitle">Foto markieren</span>
<button id="pa-clear" type="button" class="btn btn-sm" data-i18n="contam.annotateClear">Löschen</button>
<button id="pa-cancel" type="button" class="btn btn-sm" data-i18n="common.cancel">Abbrechen</button>
<button id="pa-done" type="button" class="btn btn-sm btn-p" data-i18n="contam.annotateDone">Fertig</button>
</div>
<div class="pa-canvas-wrap">
<canvas id="pa-canvas"></canvas>
</div>
<div class="pa-hint" data-i18n="contam.annotateHint">
Mit dem Finger auf das Foto malen, um die Stelle zu markieren.
</div>
</div>
</div>
<!-- Camera scan modal -->
<div
class="modal-bg"
id="m-camscan"
role="dialog"
aria-modal="true"
data-i18n-aria-label="aria.cameraScanner"
aria-label="Camera scanner"
style="background: rgba(0, 0, 0, 0.85)"
hidden
>
<div class="cam-scan-wrap">
<div
id="cam-reader"
style="width: 100%; max-width: 500px; border-radius: 12px; overflow: hidden; background: #000"
></div>
<!-- Camera scan HUD: compact status shown during continuous scanning -->
<div id="cam-scan-hud" class="cam-hud">
<div id="cam-hud-toast" class="cam-hud-toast"></div>
<!-- Vorgang, Ziel, Zähler: die Sprache der gedruckten Etiketten. Wer mit
den Knöpfen darunter arbeitet, braucht sie nicht — der leuchtende
Knopf sagt, was läuft, und seine Zahl, wieviel. updateCamHud()
zeigt die Reihe nur, solange ein gedrucktes Etikett einen Vorgang
ohne Knopf gesetzt hat (ADD, MOVE_BATCH) oder ein Ziel fehlt. -->
<div class="cam-hud-chips" id="cam-hud-chips">
<span class="cam-chip" id="cam-chip-action"
><span class="cam-chip-l">Action</span> <span id="ch-action">—</span></span
>
<span class="cam-chip" id="cam-chip-from"
><span class="cam-chip-l">From</span> <span id="ch-from">—</span></span
>
<span class="cam-chip cam-chip-arrow" id="cam-chip-arrow">→</span>
<span class="cam-chip" id="cam-chip-to"
><span class="cam-chip-l">To</span> <span id="ch-to">—</span></span
>
<span class="cam-chip" id="cam-chip-count"><span id="ch-count">0</span></span>
</div>
<!-- Die fünf Vorgänge, die man in Serie macht: ein Knopf, dann Beutel
für Beutel scannen, bis der Knopf wieder aus ist. Mit Wort, nicht
nur als Zeichen — sie ändern, was der nächste Scan tut, und das
muss man lesen können, ohne es zu kennen. Vorher ging das nur über
gedruckte Etiketten (MOVE, HARVEST, REMOVE), und die hat in der
Kammer niemand dabei. -->
<div class="cam-hud-modes" id="cam-hud-modes">
<!-- Umziehen: Ziel einmal wählen, dann zieht jeder Scan seinen Beutel
dorthin. An, steht hier das Ziel statt des Worts. -->
<button
id="btn-cam-move"
class="cam-mode-btn cam-mode-move"
type="button"
aria-pressed="false"
data-i18n-aria-label="cam.moveAria"
aria-label="Umziehen: Ziel wählen, dann Beutel scannen"
>
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="M5 12h14" />
<path d="m12 5 7 7-7 7" />
</svg>
<span class="cam-mode-l" id="cam-move-idle" data-i18n="cam.move">Umziehen</span>
<span class="cam-mode-l" id="cam-move-to" hidden></span>
<!-- Wieviele Beutel in diesem Durchgang schon am Ziel sind. -->
<span class="cam-hud-badge" id="cam-move-count" hidden>0</span>
</button>
<!-- Ernten: jeder Scan öffnet das Gewicht für seinen Beutel; nach dem
Speichern geht es zurück in die Kamera, Ernten bleibt an. -->
<button
id="btn-cam-harvest"
class="cam-mode-btn cam-mode-harvest"
type="button"
aria-pressed="false"
data-i18n-aria-label="cam.harvestAria"
aria-label="Ernten: dann Beutel scannen und Gewicht eingeben"
>
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<circle cx="6" cy="6" r="3" />
<path d="M8.12 8.12 12 12" />
<path d="M20 4 8.12 15.88" />
<circle cx="6" cy="18" r="3" />
<path d="M14.8 14.8 20 20" />
</svg>
<span class="cam-mode-l" data-i18n="cam.harvest">Ernten</span>
<span class="cam-hud-badge" id="cam-harvest-count" hidden>0</span>
</button>
<!-- Reporting contamination is done standing in front of a bad bag
with the phone already in hand. The scanner could only be put
into CONTAM mode by scanning the printed CONTAM barcode, which
is on a sheet at the bench, not in the tent. Everything behind
this button already existed; it just had no door on a phone. -->
<button
id="btn-cam-contam"
class="cam-mode-btn cam-mode-contam"
type="button"
aria-pressed="false"
data-i18n-aria-label="scan.contamAria"
aria-label="Kontamination melden"
data-i18n-title="scan.contamAria"
title="Kontamination melden"
>
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="M12 9v4" />
<path d="M12 17h.01" />
<path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
</svg>
<span class="cam-mode-l" data-i18n="cam.contam">Kontamination</span>
<!-- Wieviele Beutel schon gesammelt sind. Solange die Kamera das
Bild füllt, ist diese Zahl die einzige Rückmeldung darüber —
und der Knopf, der sie trägt, ist zugleich der Weg ins
Formular. -->
<span class="cam-hud-badge" id="cam-contam-count" hidden>0</span>
</button>
<!-- Raus: jeder Scan nimmt etwas aus dem System — einen Beutel
(Körnerbrut als verbraucht), eine Kultur als verbraucht. Früher
zwei Knöpfe, Entsorgen und Verbraucht, die fast dasselbe taten.
Die Bestätigung ist dieser Knopf; ↩ nimmt den letzten zurück. -->
<button
id="btn-cam-trash"
class="cam-mode-btn cam-mode-trash"
type="button"
aria-pressed="false"
data-i18n-aria-label="cam.trashAria"
aria-label="Raus: alles, was du jetzt scannst, geht aus dem System"
>
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="M3 6h18" />
<path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" />
<path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" />
<path d="M10 11v6" />
<path d="M14 11v6" />
</svg>
<span class="cam-mode-l" data-i18n="cam.trash">Raus</span>
<span class="cam-hud-badge" id="cam-trash-count" hidden>0</span>
</button>
</div>
<div class="cam-hud-btns">
<button
id="btn-cam-torch"
class="cam-hud-btn"
data-i18n-aria-label="aria.torchToggle"
aria-label="Taschenlampe ein/aus"
hidden
>
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2" />
</svg>
</button>
<button
id="btn-flip-cam"
class="cam-hud-btn"
data-i18n-aria-label="aria.switchCamera"
aria-label="Kamera wechseln"
>
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M11 19H4a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2h5" />
<path d="M13 5h7a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2h-5" />
<circle cx="12" cy="12" r="3" />
<path d="m18 22-3-3 3-3" />
<path d="m6 2 3 3-3 3" />
</svg>
</button>
<button
id="btn-cam-undo"
class="cam-hud-btn"
data-i18n-aria-label="aria.clearLastScan"
aria-label="Letzten Scan zur\u00fccknehmen"
>
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M3 7v6h6" />
<path d="M21 17a9 9 0 0 0-9-9 9 9 0 0 0-6 2.3L3 13" />
</svg>
</button>
<button
id="btn-cam-reset"
class="cam-hud-btn"
data-i18n-aria-label="aria.clearScan"
aria-label="Scan zur\u00fccksetzen"
>
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="12" cy="12" r="10" />
<line x1="15" y1="9" x2="9" y2="15" />
<line x1="9" y1="9" x2="15" y2="15" />
</svg>
</button>
<button
id="cls-16"
class="cam-hud-btn cam-hud-done"
data-i18n-aria-label="aria.doneScanning"
aria-label="Scannen fertig"
>
<svg
width="22"
height="22"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="3"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M20 6L9 17l-5-5" />
</svg>
</button>
</div>
</div>
</div>
</div>
<!-- Sidebar -->
<aside class="sidebar no-print" id="sidebar">
<div class="sb-header">
<div class="sb-logo">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="8" r="5" />
<path d="M12 13v5M8 21h8M9 3c0 0-2-2-2-3M15 3c0 0 2-2 2-3" /></svg
><span class="sb-logo-text">Meistertracker</span>
</div>
<button class="sb-toggle" id="sb-toggle" data-i18n-aria-label="aria.toggleSidebar" aria-label="Toggle sidebar">
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
>
<path d="M3 12h18M3 6h18M3 18h18" />
</svg>
</button>
</div>
<!-- The shortcut needs a place to be found. A keystroke nobody mentions is
a feature only its author has; a field-shaped button says "type here"
and carries the shortcut on its face from the second time on.
Above .sb-nav rather than inside it, in a wrapper that repeats the
list's own horizontal padding so the two line up. Two reasons for the
wrapper: .sb-nav is the scrolling region (flex:1, overflow-y:auto) and
it is what ran out of room on a 1366x768 laptop, so a thirteenth row
in there would spend the space the last change won back and would
scroll the search out of sight with the list; and a bare child of
.sidebar is a flex item, which made this button 20px wider than every
entry under it. -->
<div class="sb-search-wrap">
<button class="sb-btn sb-search" id="sb-search" type="button" data-gs-open>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
>
<circle cx="11" cy="11" r="7" />
<path d="m20 20-3.5-3.5" /></svg
><span data-i18n="search.open">Suchen</span
><kbd class="sb-search-key" id="sb-search-key" data-i18n="search.keyCtrl" aria-hidden="true">Strg K</kbd>
</button>
</div>
<nav class="sb-nav" data-i18n-aria-label="aria.mainNav" aria-label="Main navigation" role="navigation">
<div class="sb-group-label" data-i18n="nav.work">Arbeiten</div>
<button class="sb-btn active" id="n-work" data-page="work">
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M9 6h11M9 12h11M9 18h11" />
<path d="m3.5 6 1 1 2-2" />
<path d="m3.5 12 1 1 2-2" />
<path d="m3.5 18 1 1 2-2" /></svg
><span data-i18n="nav.workSteps">Arbeitsgänge</span>
</button>
<button class="sb-btn" id="n-dash" data-page="dash">
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect x="3" y="3" width="7" height="7" rx="1" />
<rect x="14" y="3" width="7" height="7" rx="1" />
<rect x="3" y="14" width="7" height="7" rx="1" />
<rect x="14" y="14" width="7" height="7" rx="1" /></svg
><span data-i18n="nav.dashboard">Dashboard</span>
</button>
<button class="sb-btn" id="n-cal" data-page="cal">
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect x="3" y="4" width="18" height="18" rx="2" ry="2" />
<line x1="16" y1="2" x2="16" y2="6" />
<line x1="8" y1="2" x2="8" y2="6" />
<line x1="3" y1="10" x2="21" y2="10" /></svg
><span data-i18n="nav.calendar">Kalender</span>
</button>
<div class="sb-group-label" data-i18n="nav.lookup">Nachschauen</div>
<button class="sb-btn" id="n-batch" data-page="batch">
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path
d="M21 16V8a2 2 0 00-1-1.73l-7-4a2 2 0 00-2 0l-7 4A2 2 0 003 8v8a2 2 0 001 1.73l7 4a2 2 0 002 0l7-4A2 2 0 0021 16z"
/>
<polyline points="3.27 6.96 12 12.01 20.73 6.96" />
<line x1="12" y1="22.08" x2="12" y2="12" /></svg
><span data-i18n="nav.batches">Batches</span>
</button>
<button class="sb-btn" id="n-lab" data-page="lab">
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M9 3h6v7l4 9H5l4-9V3z" />
<line x1="9" y1="3" x2="15" y2="3" /></svg
><span data-i18n="nav.lab">Lab</span>
</button>
<button class="sb-btn" id="n-inv" data-page="inv">
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" /></svg
><span data-i18n="nav.inventory">Inventory</span>
</button>
<div class="sb-group-label" data-i18n="nav.tools">Tools</div>
<button class="sb-btn" id="n-zones" data-page="zones">
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect x="3" y="3" width="18" height="18" rx="2" />
<line x1="3" y1="9" x2="21" y2="9" />
<line x1="9" y1="3" x2="9" y2="21" /></svg
><span data-i18n="nav.zones">Zonen</span>
</button>
<button class="sb-btn" id="n-print" data-page="print">
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="6 9 6 2 18 2 18 9" />
<path d="M6 18H4a2 2 0 01-2-2v-5a2 2 0 012-2h16a2 2 0 012 2v5a2 2 0 01-2 2h-2" />
<rect x="6" y="14" width="12" height="8" /></svg
><span data-i18n="nav.print">Print</span>
</button>
<button class="sb-btn" id="n-strains" data-page="strains">
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="12" cy="12" r="3" />
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2z" />
<path d="M12 8v1m0 6v1m-4-4h1m6 0h1" /></svg
><span data-i18n="nav.strains">Pilzsorten</span>
</button>
<button class="sb-btn" id="n-proj" data-page="proj">
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M9 18h6" />
<path d="M10 22h4" />
<path d="M12 2a7 7 0 00-4 12.7V18h8v-3.3A7 7 0 0012 2z" /></svg
><span data-i18n="nav.projects">Vorhaben</span>
</button>
<div class="sb-group-label" data-i18n="nav.sales">Verkauf</div>
<button class="sb-btn" id="n-pickups" data-page="pickups">
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect x="3" y="4" width="18" height="17" rx="2" />
<line x1="3" y1="9" x2="21" y2="9" />
<line x1="8" y1="2" x2="8" y2="6" />
<line x1="16" y1="2" x2="16" y2="6" />
<path d="M9 15l2 2 4-4" /></svg
><span data-i18n="nav.pickups">Abholungen</span>
</button>
<!-- One entry, five views. The five that used to stand here all called
go('orders', ...) and then openStab() on the same page — sub-tabs promoted to
the top level, while Admin's thirteen stayed nested behind one. The sidebar
paid for the inconsistency: at 1366x768 it needed 778px and had 611, so Kunden
and Versand were off the screen with nothing to say they existed. The strip
below #p-orders is the list now, on both devices — see USABILITY_PLAN.md. -->
<button class="sb-btn" id="n-orders" data-page="orders">
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M22 12h-6l-2 3h-4l-2-3H2" />
<path
d="M5.45 5.11L2 12v6a2 2 0 002 2h16a2 2 0 002-2v-6l-3.45-6.89A2 2 0 0016.76 4H7.24a2 2 0 00-1.79 1.11z"
/></svg
><span data-i18n="nav.orders">Bestellungen</span>
</button>
</nav>
<!-- Admin navigation — takes the sidebar over while the Admin section is open, on a
desktop. Fourteen sub-tabs never fitted the horizontal strip there; grouped and
vertical they do. A phone reaches them through the page's own drill-down list
instead, and keeps the main navigation in the drawer — see .sb-admin-nav in
styles.css. -->
<nav class="sb-admin-nav" id="sb-admin-nav" data-i18n-aria-label="aria.adminNav" aria-label="Admin navigation">
<div class="sb-group-label" data-i18n="nav.adminSystem">System</div>
<button class="sb-btn active" id="sn-settings-server" type="button" data-sub="server" style="display: none">
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect x="2" y="2" width="20" height="8" rx="2" />
<rect x="2" y="14" width="20" height="8" rx="2" />
<line x1="6" y1="6" x2="6.01" y2="6" />
<line x1="6" y1="18" x2="6.01" y2="18" /></svg
><span data-i18n="settings.tabServer">Server</span>
</button>
<button class="sb-btn" id="sn-settings-backup" type="button" data-sub="backup">
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4" />
<polyline points="7 10 12 15 17 10" />
<line x1="12" y1="15" x2="12" y2="3" /></svg
><span data-i18n="settings.backup">Backup</span>
</button>
<button class="sb-btn" id="sn-settings-caldav" type="button" data-sub="caldav">
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect x="3" y="4" width="18" height="18" rx="2" />
<line x1="16" y1="2" x2="16" y2="6" />
<line x1="8" y1="2" x2="8" y2="6" />
<line x1="3" y1="10" x2="21" y2="10" /></svg
><span data-i18n="settings.tabCaldav">CalDAV</span>
</button>
<button class="sb-btn" id="sn-settings-duckdns" type="button" data-sub="duckdns">
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="12" cy="12" r="10" />
<line x1="2" y1="12" x2="22" y2="12" />
<path d="M12 2a15.3 15.3 0 014 10 15.3 15.3 0 01-4 10 15.3 15.3 0 01-4-10 15.3 15.3 0 014-10z" /></svg
><span data-i18n="settings.tabDuckdns">DuckDNS</span>
</button>
<button class="sb-btn" id="sn-settings-mcp" type="button" data-sub="mcp" style="display: none">
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="4 17 10 11 4 5" />
<line x1="12" y1="19" x2="20" y2="19" /></svg
><span data-i18n="settings.tabMcp">MCP</span>
</button>
<div class="sb-group-label" data-i18n="nav.adminOps">Betrieb</div>
<button class="sb-btn" id="sn-settings-log" type="button" data-sub="log">
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<line x1="8" y1="6" x2="21" y2="6" />
<line x1="8" y1="12" x2="21" y2="12" />
<line x1="8" y1="18" x2="21" y2="18" />
<line x1="3" y1="6" x2="3.01" y2="6" />
<line x1="3" y1="12" x2="3.01" y2="12" />
<line x1="3" y1="18" x2="3.01" y2="18" /></svg
><span data-i18n="settings.scanLog">Scan log</span>
</button>
<button class="sb-btn" id="sn-settings-users" type="button" data-sub="users">
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2" />
<circle cx="12" cy="7" r="4" /></svg
><span data-i18n="settings.tabUsers">Users</span>
</button>
<button class="sb-btn" id="sn-settings-pickuplocations" type="button" data-sub="pickuplocations">
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0118 0z" />
<circle cx="12" cy="10" r="3" /></svg
><span data-i18n="settings.tabPickupLocations">Abholorte</span>
</button>
<div class="sb-group-label" data-i18n="nav.adminSales">Verkauf</div>
<button class="sb-btn" id="sn-settings-channels" type="button" data-sub="channels">
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="18" cy="5" r="3" />
<circle cx="6" cy="12" r="3" />
<circle cx="18" cy="19" r="3" />
<line x1="8.6" y1="13.5" x2="15.4" y2="17.5" />
<line x1="15.4" y1="6.5" x2="8.6" y2="10.5" /></svg
><span data-i18n="settings.tabChannels">Kanäle</span>
</button>
<button class="sb-btn" id="sn-settings-versand" type="button" data-sub="versand">
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect x="1" y="3" width="15" height="13" />
<path d="M16 8h4l3 3v5h-7V8z" />
<circle cx="5.5" cy="18.5" r="2.5" />
<circle cx="18.5" cy="18.5" r="2.5" /></svg
><span data-i18n="settings.tabVersand">Versand</span>
</button>
<button class="sb-btn" id="sn-settings-harvestfeed" type="button" data-sub="harvestfeed">
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M4 11a9 9 0 019 9M4 4a16 16 0 0116 16" />
<circle cx="5" cy="19" r="1" /></svg
><span data-i18n="settings.tabHarvestFeed">Harvest feed</span>
</button>
<div class="sb-group-label" data-i18n="nav.adminDevices">Geräte</div>
<button class="sb-btn" id="sn-settings-printer" type="button" data-sub="printer">
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="6 9 6 2 18 2 18 9" />
<path d="M6 18H4a2 2 0 01-2-2v-5a2 2 0 012-2h16a2 2 0 012 2v5a2 2 0 01-2 2h-2" />
<rect x="6" y="14" width="12" height="8" /></svg
><span data-i18n="settings.tabPrinter">Drucker</span>
</button>
<button class="sb-btn" id="sn-settings-camera" type="button" data-sub="camera" style="display: none">
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M23 19a2 2 0 01-2 2H3a2 2 0 01-2-2V8a2 2 0 012-2h4l2-3h6l2 3h4a2 2 0 012 2z" />
<circle cx="12" cy="13" r="4" /></svg
><span data-i18n="settings.tabCamera">Camera</span>
</button>
</nav>
<div class="sb-footer">
<button class="sb-btn" id="n-settings" data-page="settings" style="display: none">
<svg
class="sb-ico-back"
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M19 12H5" />
<polyline points="12 19 5 12 12 5" />
</svg>
<svg
class="sb-ico-admin"
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<circle cx="12" cy="12" r="3" />
<path
d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"
/></svg
><span data-i18n="nav.admin">Admin</span>
</button>
<div class="sb-sync">
<span class="sync-dot" id="sync-dot" data-i18n-title="aria.clickToSync" title="Click to sync"></span>
<span id="sync-label" class="sb-sync-label"></span>
<select id="lang-sel" class="sb-lang-sel">
<option value="de">DE</option>
<option value="en">EN</option>
<option value="pt">PT</option>
</select>
</div>
</div>
</aside>
<!-- Mobile topbar (hamburger) -->
<div class="mobile-topbar no-print" id="mobile-topbar">
<button id="tgl-17" class="sb-toggle" data-i18n-aria-label="aria.openMenu" aria-label="Open menu">
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
>
<path d="M3 12h18M3 6h18M3 18h18" />
</svg>
</button>
<!-- The page's name, not the app's. A phone has no sidebar to read the
highlight off, and the bottom bar covers five of the twelve pages — open
the drawer, tap Abholungen, watch it close, and nothing on the screen said
where you landed. go() writes the label here from the nav entry it was
handed, so the two cannot drift; data-i18n goes with it, so a language
switch reaches it through translatePage() like any other string. -->
<div class="sb-logo">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="8" r="5" />
<path d="M12 13v5M8 21h8" /></svg
><span class="sb-logo-text" id="topbar-title" data-i18n="nav.workSteps">Arbeitsgänge</span>
</div>
<!-- The phone has no sidebar to hold the field, so the topbar holds the
magnifier instead — next to the bell, in the corner the thumb already
goes to. -->
<button
class="mobile-search"
id="topbar-search"
type="button"
data-gs-open
data-i18n-aria-label="search.open"
aria-label="Suchen"
>
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
>
<circle cx="11" cy="11" r="7" />
<path d="m20 20-3.5-3.5" />
</svg>
</button>
<button
class="mobile-bell"
id="n-notif-m"
type="button"
data-i18n-aria-label="notifications.title"
aria-label="Notifications"
>
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="22 12 16 12 14 15 10 15 8 12 2 12" />
<path
d="M5.45 5.11 2 12v6a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-6l-3.45-6.89A2 2 0 0 0 16.76 4H7.24a2 2 0 0 0-1.79 1.11z"
/>
</svg>
<span class="bell-badge" id="bell-badge-m" hidden>0</span>
</button>
<span class="sync-dot" id="sync-dot-m"></span>
</div>
<!-- Sidebar overlay for mobile -->
<div class="sb-overlay" id="sb-overlay"></div>
<!-- Mobile bottom navigation — the five most-used pages, thumb-reach zone.
Visible only on touch + narrow viewport; the sidebar remains the
single source of truth for active state and routing. -->
<nav class="bottom-nav no-print" id="bottom-nav" data-i18n-aria-label="aria.mainNav" aria-label="Main navigation">
<button class="bottom-nav-btn" id="bn-work" type="button" data-page="work">
<svg
width="22"
height="22"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M9 6h11M9 12h11M9 18h11" />
<path d="m3.5 6 1 1 2-2" />
<path d="m3.5 12 1 1 2-2" />
<path d="m3.5 18 1 1 2-2" />
</svg>
<span data-i18n="nav.workSteps">Arbeitsgänge</span>
</button>
<button class="bottom-nav-btn" id="bn-dash" type="button" data-page="dash">
<svg
width="22"
height="22"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect x="3" y="3" width="7" height="7" rx="1" />
<rect x="14" y="3" width="7" height="7" rx="1" />
<rect x="3" y="14" width="7" height="7" rx="1" />
<rect x="14" y="14" width="7" height="7" rx="1" />
</svg>
<span data-i18n="nav.dashboard">Dashboard</span>
</button>
<button class="bottom-nav-btn" id="bn-batch" type="button" data-page="batch">
<svg
width="22"
height="22"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path
d="M21 16V8a2 2 0 00-1-1.73l-7-4a2 2 0 00-2 0l-7 4A2 2 0 003 8v8a2 2 0 001 1.73l7 4a2 2 0 002 0l7-4A2 2 0 0021 16z"
/>
<polyline points="3.27 6.96 12 12.01 20.73 6.96" />
<line x1="12" y1="22.08" x2="12" y2="12" />
</svg>
<span data-i18n="nav.batches">Batches</span>
</button>
<button class="bottom-nav-btn" id="bn-lab" type="button" data-page="lab">
<svg
width="22"
height="22"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M9 3h6v7l4 9H5l4-9V3z" />
<line x1="9" y1="3" x2="15" y2="3" />
</svg>
<span data-i18n="nav.lab">Lab</span>
</button>
<!-- Bestellungen, not Kalender: orders are looked at many times a day, the
calendar rarely, and the drawer was the only way in. Kalender stays in
the drawer. Its own short label, like Arbeitsgänge: at the 13px phone
floor "Bestellungen" is 24px wider than "Kalender", and the bar had 7px
to spare at 320px (see .bottom-nav-btn). -->
<button class="bottom-nav-btn" id="bn-orders" type="button" data-page="orders">
<svg
width="22"
height="22"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M22 12h-6l-2 3h-4l-2-3H2" />
<path
d="M5.45 5.11L2 12v6a2 2 0 002 2h16a2 2 0 002-2v-6l-3.45-6.89A2 2 0 0016.76 4H7.24a2 2 0 00-1.79 1.11z"
/>
</svg>
<span data-i18n="nav.ordersBar">Aufträge</span>
</button>
</nav>
<!-- Notifications dropdown (anchored from JS; starts hidden) -->
<div
class="notif-dropdown no-print"
id="notif-dropdown"
role="dialog"
aria-modal="false"
aria-labelledby="notif-title"
hidden
>
<div class="notif-header">
<span class="notif-title" id="notif-title" data-i18n="notifications.title">Benachrichtigungen</span>
<button class="notif-mark-read" id="notif-mark-read" type="button" data-i18n="notifications.markAllRead">
Alle als gelesen markieren
</button>
</div>
<div class="notif-list" id="notif-list"></div>
<div class="notif-empty" id="notif-empty" data-i18n="notifications.empty" hidden>
Keine neuen Benachrichtigungen
</div>
</div>
<!-- Content wrapper -->
<div class="content-wrap">
<!-- Scan modal (centered, appears on scan) -->
<div id="scan-overlay" class="scan-overlay no-print">
<div class="scan-modal" id="scan-modal">
<button id="cls-18" class="scan-modal-close" data-i18n-title="common.close" title="Close">×</button>
<div class="scan-modal-header" id="scan-modal-header">
<svg
width="20"
height="20"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M3 7V5a2 2 0 0 1 2-2h2" />
<path d="M17 3h2a2 2 0 0 1 2 2v2" />
<path d="M21 17v2a2 2 0 0 1-2 2h-2" />
<path d="M7 21H5a2 2 0 0 1-2-2v-2" />
<line x1="7" y1="12" x2="17" y2="12" />
</svg>
<span data-i18n="scan.mode">Scan-Modus</span>
</div>
<div class="scan-modal-tabs" id="scan-modal-tabs" role="tablist">
<button
type="button"
class="scan-tab active"
data-scan-tab="current"
role="tab"
data-i18n="scan.tabCurrent"
>
Aktueller Scan
</button>
<button type="button" class="scan-tab" data-scan-tab="successes" role="tab">
<span data-i18n="scan.tabSuccesses">Letzte Erfolge</span>
<span class="scan-tab-count" id="scan-tab-succ-count">0</span>
</button>
<button type="button" class="scan-tab" data-scan-tab="all" role="tab" data-i18n="scan.tabAll">
Gesamt-Log
</button>
</div>
<div class="scan-modal-body" id="scan-modal-body">
<div class="scan-tab-panel active" data-scan-panel="current">
<div class="scan-modal-chips">
<div class="chip" id="chip-action">
<div class="chip-l" data-i18n="scan.action">Action</div>
<div class="chip-v" id="s-action">—</div>
</div>
<div class="chip" id="chip-from">
<div class="chip-l" data-i18n="scan.from">From</div>
<div class="chip-v" id="s-from">—</div>
</div>
<div class="chip" id="chip-to">
<div class="chip-l" data-i18n="scan.to">To</div>
<div class="chip-v" id="s-to">—</div>
</div>
<div class="chip" id="chip-count">
<div class="chip-l" data-i18n="scan.count">Count</div>
<div class="chip-v" id="s-count">0</div>
</div>
<div class="chip chip-last-scan" id="chip-last" style="display: none">
<div class="chip-l" data-i18n="scan.lastScan">Letzter Scan</div>
<div class="chip-v" id="s-last">—</div>
</div>
</div>
<div id="scan-toast" class="scan-toast-inline"></div>
<div id="scan-session-summary" class="scan-session-summary" style="display: none"></div>
</div>
<div class="scan-tab-panel" data-scan-panel="successes">
<div class="scan-successes-empty" id="scan-successes-empty" data-i18n="scan.noSuccessesYet">
Noch keine erfolgreichen Scans in dieser Sitzung.
</div>
<div class="scan-successes-list" id="scan-successes-list"></div>
</div>
<div class="scan-tab-panel" data-scan-panel="all">
<div class="scan-modal-log" id="scan-modal-log"></div>
</div>
</div>
<div class="scan-modal-btns">
<button id="set-19" class="btn-xs" data-i18n="scan.reset">Reset</button>
<button id="btn-20" class="btn-xs green" data-i18n="scan.addBatch">+ Batch</button>
<!-- Am Tisch wird mit dem Handscanner gesammelt, und dort gibt es
keine Kamera-Leiste mit dem ⚠-Knopf. Ohne diesen hier käme man
an das Formular nur über die Kamera. Beschriftung und
Sichtbarkeit setzt updateSD(). -->
<button id="btn-contam-collect" class="btn-xs btn-contam-collect" style="display: none"></button>
<button
class="btn-xs btn-end-session"
id="btn-end-session"
style="display: none"
data-i18n="scan.endSession"
>
Session beenden
</button>
<button
class="btn-xs btn-scan-cam"
id="btn-scan-cam"
data-i18n-title="scan.openCamera"
title="Kamera öffnen"
>
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<rect x="2" y="5" width="20" height="15" rx="2" />
<circle cx="12" cy="13" r="4" />
<path d="M8 5l1-2h6l1 2" />
</svg>
<span data-i18n="scan.camera">Kamera</span>
</button>
<button
class="btn-xs"
id="btn-scan-audio"
data-i18n-title="scan.audioToggle"
data-i18n-aria-label="scan.audioToggle"
title="Audio an/aus"
aria-label="Audio an/aus"
style="margin-left: auto"
>
🔊
</button>
</div>
</div>
</div>
<!-- Harvest panel -->
<div id="harvest-panel">
<div class="inner">
<div>
<div class="fs-xs"
style="color: var(--c-amber-dark); font-weight: 600; margin-bottom: 3px"
id="hp-lbl"
data-i18n="harvest.logHarvest"
>
Log harvest
</div>
<input class="fs-meta"
type="text"
id="hp-bag"
readonly
style="width: 180px; font-family: monospace; background: var(--c-amber-light)"
/>
</div>
<div>
<label class="fs-xs" style="color: var(--c-amber-dark)" data-i18n="harvest.grams">Grams</label
><input
type="text"
inputmode="decimal"
enterkeyhint="done"
id="hp-grams"
autocomplete="off"
style="width: 100px"
data-i18n-placeholder="harvest.gramsPlaceholder"
placeholder="e.g. 245"
/>
</div>
<div>
<label class="fs-xs" style="color: var(--c-amber-dark)" data-i18n="harvest.flush">Flush #</label
><input
type="text"
inputmode="numeric"
enterkeyhint="done"
id="hp-flush"
value="1"
autocomplete="off"
style="width: 65px"
/>
</div>
<div id="hp-release-wrap" style="display: none">
<label class="fs-xs"
style="color: var(--c-amber-dark)"
data-i18n="harvest.release"
data-i18n-title="harvest.releaseHint"
>Release for sale</label
><input
type="text"
inputmode="decimal"
enterkeyhint="done"
id="hp-release"
autocomplete="off"
style="width: 100px"
data-i18n-placeholder="harvest.releasePlaceholder"
data-i18n-title="harvest.releaseHint"
placeholder="grams"
/>
</div>
<div id="hp-note-wrap">
<label class="fs-xs" style="color: var(--c-amber-dark)" data-i18n="block.noteLabel">Note on block</label
><input
type="text"
enterkeyhint="done"
id="hp-note"
maxlength="2000"
autocomplete="off"
style="width: 220px"
data-i18n-placeholder="block.notePlaceholder"
placeholder="What stands out about this block …"
/>
</div>
<button type="button" id="hp-good" class="btn btn-sm block-good" aria-pressed="false">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round" aria-hidden="true"><path d="M12 3l2.7 5.6 6.1.9-4.4 4.3 1 6.1L12 17l-5.4 2.9 1-6.1-4.4-4.3 6.1-.9z" /></svg><span data-i18n="block.good">Good block</span>
</button>
<button id="act-21" class="btn btn-p btn-sm" data-i18n="harvest.logHarvest">Log harvest</button>
<button id="hp-next" class="btn btn-sm" data-i18n="harvest.logNext">Save & scan next</button>
<button id="btn-22" class="btn btn-sm" data-i18n="harvest.cancel">Cancel</button>
</div>
</div>
<div class="main">
<!-- ══ ARBEITSGÄNGE ══
The launcher: the seven things that actually get done, in the order
they happen. Every tile opens a flow that works by clicking — the
scanner stays the fast path on the tablet, but nothing here needs it. -->
<div class="page active" id="p-work">
<!-- Die sieben Arbeitsgänge zuerst. Die Tagesliste stand darüber, seit
sie vom Dashboard hierherzog — und am Telefon hieß das: die App
öffnet auf einer Liste, und die Kacheln, deretwegen man sie
aufgemacht hat, liegen unter dem Falz. Jetzt ist das Erste im
Bild das, was man gleich tut; was ansteht, folgt darunter. Die
Überschrift gilt nur für die Kacheln, und die Liste trägt ihre
eigene. -->
<!-- Nur wenn etwas nicht stimmt: Überfälliges, ein volles Zelt.
Gefüllt von renderWorkSummary(); tippen öffnet die Aufgaben. -->
<button type="button" class="wk-alert" id="wk-alert" hidden></button>
<!-- Something to produce: articles under their target or ordered and
not in the lab (renderWorkNachproduzieren); tapping opens the
Nachproduzieren card on the Labor page. -->
<button type="button" class="wk-alert wk-np" id="wk-np" hidden></button>
<!-- Admins only: today's USB stick backup — due, running, done (take
the stick out), failed, or not set up yet. Filled by
renderUsbReminder(); tapping opens Settings → Backup. -->
<button type="button" class="wk-usb" id="wk-usb" hidden></button>
<!-- Admins only, beside the line above and not instead of it: the
recovery key has not been confirmed as put away yet. -->
<button type="button" class="wk-usb wk-usb-key" id="wk-usb-key" hidden></button>
<div class="wk-head">
<div>
<div class="sec" style="margin: 0" data-i18n="work.title">Was hast du gemacht?</div>
<div class="wk-sub" data-i18n="work.sub">
Wähle den Arbeitsgang — die App fragt den Rest ab.
</div>
</div>
</div>
<div class="wk-tiles" id="wk-tiles">
<button type="button" class="wk-tile" id="wk-t-move" style="--wk-c: var(--c-blue-dark); --wk-cl: var(--c-blue-light)">
<span class="wk-tile-art" aria-hidden="true">
<svg viewBox="0 0 140 110" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="44" y="20" width="54" height="34" rx="7" fill="currentColor" opacity=".22"/>
<rect x="44" y="57" width="54" height="34" rx="7" fill="currentColor" opacity=".15"/>
<path d="M38 10v84M38 94h58" stroke="currentColor" stroke-width="5" opacity=".28" stroke-linecap="round" fill="none"/>
<circle cx="46" cy="100" r="9" fill="currentColor" opacity=".30"/>
<circle cx="46" cy="100" r="3.4" fill="currentColor" opacity=".16"/>
<path d="M110 36h22M114 52h18M118 68h14" stroke="currentColor" stroke-width="3.4" opacity=".22" stroke-linecap="round"/>
</svg>
</span>
<span class="wk-tile-t" data-i18n="work.move">Umziehen</span>
<span class="wk-tile-when" data-i18n="work.moveWhen">Inkubation → Zelt → Lager</span>
</button>
<button type="button" class="wk-tile" id="wk-t-harvest" style="--wk-c: var(--c-amber-dark); --wk-cl: var(--c-amber-light)">
<span class="wk-tile-art" aria-hidden="true">
<svg viewBox="0 0 140 110" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M30 60c0-17 15-28 32-28s30 11 30 23c0 6-6 9-15 9H43c-9 0-13-2-13-4z" fill="currentColor" opacity=".16"/>
<path d="M56 64c-3 13-7 24-3 38h18c-5-14-7-25-7-38z" fill="currentColor" opacity=".13"/>
<path d="M48 82c0-15 13-24 28-24s28 9 28 21c0 7-6 9-15 9H61c-9 0-13-2-13-6z" fill="currentColor" opacity=".30"/>
<path d="M60 88h34M65 95h25" stroke="currentColor" stroke-width="2.6" opacity=".16" stroke-linecap="round"/>
</svg>
</span>
<span class="wk-tile-t" data-i18n="work.harvest">Ernte erfassen</span>
<span class="wk-tile-when" data-i18n="work.harvestWhen">Gramm pro Beutel, nach dem Ernten</span>
</button>
<button type="button" class="wk-tile" id="wk-t-batch" style="--wk-c: var(--c-green-dark); --wk-cl: var(--c-primary-light)">
<span class="wk-tile-art" aria-hidden="true">
<svg viewBox="0 0 140 110" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M36 46h58v48a9 9 0 0 1-9 9H45a9 9 0 0 1-9-9z" fill="currentColor" opacity=".16"/>
<path d="M36 46l-8-13 14 5 11-6 11 6 14-5-8 13z" fill="currentColor" opacity=".26"/>
<circle cx="65" cy="76" r="9" fill="currentColor" opacity=".16"/>
<g transform="rotate(38 108 26)">
<rect x="92" y="10" width="34" height="26" rx="5" fill="currentColor" opacity=".22"/>
<rect x="88" y="14" width="7" height="18" rx="3" fill="currentColor" opacity=".28"/>
</g>
<g fill="currentColor" opacity=".34">
<ellipse cx="88" cy="40" rx="4.2" ry="2.6" transform="rotate(-30 88 40)"/>
<ellipse cx="82" cy="48" rx="4.2" ry="2.6" transform="rotate(20 82 48)"/>
<ellipse cx="76" cy="56" rx="4.2" ry="2.6" transform="rotate(-14 76 56)"/>
<ellipse cx="70" cy="64" rx="4.2" ry="2.6" transform="rotate(26 70 64)"/>
</g>
</svg>
</span>
<span class="wk-tile-t" data-i18n="work.tileBatch">Beutel beimpfen</span>
<span class="wk-tile-when" data-i18n="work.batchWhen">wenn Körnerbrut durchgewachsen ist</span>
</button>
<button type="button" class="wk-tile" id="wk-t-lab" style="--wk-c: var(--c-purple-dark); --wk-cl: var(--c-purple-light)">
<span class="wk-tile-art" aria-hidden="true">
<svg viewBox="0 0 140 110" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="18" y="8" width="112" height="60" rx="5" fill="currentColor" opacity=".13"/>
<rect x="27" y="17" width="94" height="36" rx="3" fill="currentColor" opacity=".09"/>
<path d="M32 24h84M32 30h84M32 36h84" stroke="currentColor" stroke-width="2" opacity=".20" stroke-linecap="round"/>
<rect x="12" y="68" width="122" height="7" rx="3" fill="currentColor" opacity=".30"/>
<rect x="26" y="75" width="6" height="30" rx="2" fill="currentColor" opacity=".18"/>
<rect x="114" y="75" width="6" height="30" rx="2" fill="currentColor" opacity=".18"/>
<ellipse cx="52" cy="63" rx="17" ry="5" fill="currentColor" opacity=".26"/>
<ellipse cx="52" cy="60" rx="17" ry="5" fill="currentColor" opacity=".16"/>
<path d="M96 40h10v10l8 14a3 3 0 0 1-3 4H91a3 3 0 0 1-3-4l8-14z" fill="currentColor" opacity=".24"/>
</svg>
</span>
<span class="wk-tile-t" data-i18n="work.lab">Labor</span>
<span class="wk-tile-when" data-i18n="work.labWhen">Slant, Petri, Flüssigkultur, Spritzen, Korn-zu-Korn</span>
</button>
<button type="button" class="wk-tile" id="wk-t-grain" style="--wk-c: var(--c-purple-dark); --wk-cl: var(--c-purple-light)">
<span class="wk-tile-art" aria-hidden="true">
<svg viewBox="0 0 140 110" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M42 32h54v56a9 9 0 0 1-9 9H51a9 9 0 0 1-9-9z" fill="currentColor" opacity=".10"/>
<rect x="48" y="23" width="42" height="11" rx="3" fill="currentColor" opacity=".17"/>
<g fill="currentColor" opacity=".30">
<ellipse cx="55" cy="92" rx="7.4" ry="4.2" transform="rotate(-18 55 92)"/>
<ellipse cx="70" cy="94" rx="7.4" ry="4.2" transform="rotate(12 70 94)"/>
<ellipse cx="85" cy="92" rx="7.4" ry="4.2" transform="rotate(-8 85 92)"/>
<ellipse cx="62" cy="83" rx="7.4" ry="4.2" transform="rotate(22 62 83)"/>
<ellipse cx="78" cy="83" rx="7.4" ry="4.2" transform="rotate(-24 78 83)"/>
<ellipse cx="70" cy="74" rx="7.4" ry="4.2" transform="rotate(6 70 74)"/>
<ellipse cx="48" cy="83" rx="6.6" ry="3.9" transform="rotate(-32 48 83)"/>
<ellipse cx="92" cy="83" rx="6.6" ry="3.9" transform="rotate(28 92 83)"/>
</g>
</svg>
</span>
<span class="wk-tile-t" data-i18n="work.grain">Körnerbrut</span>
<span class="wk-tile-when" data-i18n="work.grainWhen">wenn eine Flüssigkultur fertig ist</span>
</button>
<button type="button" class="wk-tile" id="wk-t-sub" style="--wk-c: var(--c-blue-dark); --wk-cl: var(--c-blue-light)">
<span class="wk-tile-art" aria-hidden="true">
<svg viewBox="0 0 140 110" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="20" y="20" width="100" height="52" rx="26" fill="currentColor" opacity=".15"/>
<ellipse cx="46" cy="46" rx="13" ry="26" fill="currentColor" opacity=".10"/>
<path d="M44 33l26 26M44 59l26-26" stroke="currentColor" stroke-width="3.4" opacity=".20" stroke-linecap="round"/>
<path d="M96 14a26 26 0 0 1 12 14" stroke="currentColor" stroke-width="3.4" opacity=".28" stroke-linecap="round" fill="none"/>
<path d="M110 30l-1-9 -8 5z" fill="currentColor" opacity=".28"/>
<path d="M40 72l-9 30M104 72l9 30" stroke="currentColor" stroke-width="5" opacity=".20" stroke-linecap="round"/>
<path d="M24 104h96" stroke="currentColor" stroke-width="4" opacity=".26" stroke-linecap="round"/>
</svg>
</span>
<span class="wk-tile-t" data-i18n="work.tileSubstrate">Substrat</span>
<span class="wk-tile-when" data-i18n="work.substrateWhen">bucht Pellets, Kleie und Gips vom Lager ab</span>
</button>
<button type="button" class="wk-tile" id="wk-t-contam" style="--wk-c: var(--c-red-dark); --wk-cl: var(--c-red-light)">
<span class="wk-tile-art" aria-hidden="true">
<svg viewBox="0 0 140 110" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="24" y="24" width="94" height="68" rx="8" fill="currentColor" opacity=".13"/>
<path d="M24 44h94" stroke="currentColor" stroke-width="2.4" opacity=".14"/>
<g fill="currentColor" opacity=".32">
<circle cx="52" cy="58" r="13"/>
<circle cx="67" cy="51" r="9"/>
<circle cx="62" cy="69" r="8"/>
<circle cx="96" cy="76" r="11"/>
<circle cx="84" cy="82" r="7"/>
</g>
<g fill="currentColor" opacity=".22">
<circle cx="38" cy="34" r="2.8"/><circle cx="79" cy="33" r="2.3"/>
<circle cx="104" cy="52" r="2.5"/><circle cx="42" cy="84" r="2.3"/>
</g>
</svg>
</span>
<span class="wk-tile-t" data-i18n="work.contam">Kontamination melden</span>
<span class="wk-tile-when" data-i18n="work.contamWhen">sobald du sie siehst, nicht später</span>
</button>
</div>
<!-- Die Reihenfolge der Kacheln stellt jeder selbst ein; gemerkt wird sie
je Benutzer auf diesem Gerät (applyWkTileOrder). -->
<div class="wk-order-bar">
<button type="button" class="wk-order-link" id="wk-order-btn" aria-controls="wk-order" aria-expanded="false" data-i18n="work.orderEdit">Reihenfolge ändern</button>
</div>
<div class="card wk-order" id="wk-order" hidden>
<ol class="wk-order-list" id="wk-order-list"></ol>
<div class="wk-order-actions">
<button type="button" class="btn btn-sm" id="wk-order-reset" data-i18n="work.orderReset">Standard</button>
<button type="button" class="btn btn-sm btn-p" id="wk-order-done" data-i18n="work.orderDone">Fertig</button>
</div>
</div>
<!-- Was in welchem Zelt und welcher Inkubation steht, nach Sorte und Einzugstag. Gefüllt von
renderWorkTents(); ohne solche Zonen bleibt die Karte weg. -->
<div class="card wk-tents" id="wk-tents" hidden>
<div class="sec" style="margin: 0 0 6px" data-i18n="work.tentsTitle">Wo steht was</div>
<div id="wk-tents-list"></div>
</div>
<!-- What is due, below the seven jobs that do it. This lived on the
Dashboard, and the two screens pointed at each other: a strip here read
"Weißt du nicht, was ansteht?" and sent you there, and the first thing
there was two buttons that came straight back — the same redundancy the
speed-dial FAB was deleted for. One screen answers both halves now, and
the Dashboard keeps what only it has. The list sat above the tiles at
first; on a phone that opened the app on a list and put the seven jobs
below the fold, so the halves swapped. -->
<div class="dash-top-row">
<!-- Batch tasks — what is due, under the seven jobs -->
<div class="card" id="dash-batch-tasks-card" style="padding: 12px 16px; flex: 1; min-width: 0">
<!-- No All/Urgent-only filter: the urgency sections below ARE the
filter, and "urgent only" hid the sections it duplicated. -->
<button type="button" class="wk-today" id="wk-today" aria-expanded="false" aria-controls="dash-batch-tasks">
<span class="sec" style="margin: 0" data-i18n="todo.batchTasks">Batch tasks</span>
<span class="wk-today-sum" id="wk-today-sum"></span>
<span class="wk-today-chev" aria-hidden="true">▾</span>
</button>
<div id="dash-batch-tasks"></div>
</div>
<!-- Split batches (bags of the same batch across multiple stages) used
to be its own card here. It is now the LIEGENGEBLIEBEN section of
the Batch tasks card: same data (getSplitBatches), but read as an
instruction alongside the dated work instead of a separate table. -->
</div>
<!-- Aufs Handy legen. Steht nur da, wenn der Browser die Installation
anbietet oder Safari sie von Hand verlangt — und nie, wenn die App
ohnehin schon als Symbol läuft. Zwei Texte statt einem, damit die
Sprachumschaltung beide übersetzen kann, ohne dass JavaScript
dazwischenschreibt. -->
<div class="wk-install" id="wk-install" hidden>
<span id="wk-install-why" data-i18n="work.installWhy" hidden>
Die App aufs Handy legen — dann öffnet sie mit einem Symbol, ohne Browserleiste.
</span>
<span id="wk-install-apple" data-i18n="work.installApple" hidden>
Aufs Handy legen: unten auf Teilen tippen, dann „Zum Home-Bildschirm".
</span>
<button class="btn btn-sm" id="wk-install-btn" data-i18n="work.installDo" hidden>Aufs Handy legen</button>
</div>
</div>
<!-- ══ DASHBOARD ══ -->
<div class="page" id="p-dash">
<!-- The whole page. It used to be one of two modes behind a Farm/Overview
toggle; the other mode held the day's work, which now sits on
Arbeitsgänge where the jobs it names are started. See USABILITY_PLAN.md. -->
<div id="dash-charts-section">
<!-- Top bar: Period toggle + CSV download. It wraps, which it never had to
while this whole section sat behind display:none in the Farm mode — three
period pills and a CSV button come to 382px at 375. -->
<div class="ov-toolbar">
<div class="ov-period-toggle" style="margin-bottom: 0">
<button
class="ov-period-btn active"
id="ov-p-week"
onclick="setOvPeriod('week')"
data-i18n="dash.ov.periodWeek"
>
This week
</button>
<button
class="ov-period-btn"
id="ov-p-month"
onclick="setOvPeriod('month')"
data-i18n="dash.ov.periodMonth"
>
This month
</button>
<button
class="ov-period-btn"
id="ov-p-year"
onclick="setOvPeriod('year')"
data-i18n="dash.ov.periodYear"
>
This year
</button>
</div>
<button class="btn btn-sm" onclick="exportOverviewCSV()" title="Download CSV">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4" />
<polyline points="7 10 12 15 17 10" />
<line x1="12" y1="15" x2="12" y2="3" />
</svg>
CSV
</button>
</div>
<!-- Production KPI row (adapts to period) -->
<div class="ov-kpi-group" style="margin-bottom: 10px">
<div class="ov-kpi-group-label" id="ov-period-label">This week</div>
<div class="g4" id="ov-kpi-week"></div>
</div>
<!-- Substrates KPI row -->
<div class="ov-kpi-group" style="margin-bottom: 10px">
<div class="ov-kpi-group-label" data-i18n="dash.ov.substrates">Substrates</div>
<div class="g3" id="ov-kpi-substrates"></div>
</div>
<!-- Quality & Efficiency KPI row (all-time) -->
<div class="ov-kpi-group" style="margin-bottom: 14px">
<div class="ov-kpi-group-label" data-i18n="dash.ov.qualityEff">Quality & efficiency</div>
<div class="g3" id="ov-kpi-quality"></div>
</div>
<!-- Unified charts (adapt to selected period) -->
<div class="g2" style="margin-bottom: 10px">
<div class="card">
<div class="sec" id="ov-chart-harvest-label">Harvest</div>
<canvas id="ov-chart-harvest" height="180"></canvas>
</div>
<div class="card">
<div class="sec" id="ov-chart-species-label">Harvest by species (kg)</div>
<canvas id="ov-chart-species" height="180"></canvas>
</div>
</div>
<div class="g2" style="margin-bottom: 10px">
<div class="card">
<div class="sec" id="ov-chart-substrate-label">Substrate usage</div>
<canvas id="ov-chart-substrate" height="180"></canvas>
</div>
<div class="card">
<div class="sec" id="ov-chart-bags-label">Bags created</div>
<canvas id="ov-chart-bags" height="180"></canvas>
</div>
</div>
<!-- KPI History (trend charts from daily snapshots) -->
<!-- Shown with the page. It used to be un-hidden by applyDashMode(), which
went with the Farm/Overview toggle; the empty state inside is what
handles "not enough snapshots yet". -->
<div id="ov-kpi-history">
<div style="display: flex; align-items: center; justify-content: space-between">
<div class="ov-kpi-group-label" data-i18n="dash.ov.history">KPI History</div>
<button
class="btn btn-sm fs-xs"
onclick="takeKpiSnapshot()"
title="Update today's snapshot"
>
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="23 4 23 10 17 10" />
<path d="M20.49 15a9 9 0 11-2.12-9.36L23 10" />
</svg>
Snapshot
</button>
</div>
<div
id="ov-history-empty"
class="card"
style="display: none; text-align: center; padding: 24px; color: var(--c-text-muted)"
>
<p data-i18n="dash.ov.historyNoData">No KPI history yet — snapshots are taken daily with the backup.</p>
<button class="btn" onclick="takeKpiSnapshot()" data-i18n="dash.ov.historyTakeSnapshot">
Take snapshot now
</button>
</div>
<div id="ov-history-charts" class="g2" style="margin-bottom: 10px">
<div class="card">
<div class="sec" data-i18n="dash.ov.historyHarvest">Daily harvest (kg)</div>
<canvas id="ov-history-harvest-chart" height="180"></canvas>
</div>
<div class="card">
<div class="sec" data-i18n="dash.ov.historyPipeline">Pipeline (bags by stage)</div>
<canvas id="ov-history-pipeline-chart" height="180"></canvas>
</div>
<div class="card">
<div class="sec" data-i18n="dash.ov.historyContam">Contamination rate (%)</div>
<canvas id="ov-history-contam-chart" height="180"></canvas>
</div>
<div class="card">
<div class="sec" data-i18n="dash.ov.historyStock">Stock levels (kg)</div>
<canvas id="ov-history-stock-chart" height="180"></canvas>
</div>
</div>
</div>
</div>
<!-- Floating action bar for bag selection -->
</div>
<!-- ══ BATCHES ══ -->
<div class="page" id="p-batch">
<!-- Making a Charge is not a fifth place to look. The four tabs below
show what exists — every Charge, where the bags are, the substrate
mixes, the harvests — and this is where something new starts, so it
reads as a different kind of control and sits above them, the same
way Laborarbeit does one section over.
It opens the guided flow (wkbOpen), the same one the Arbeitsgänge
tile opens: there is one way to make a Charge, not a long form
here and a wizard there. sp-batch-new stays below as that flow's
backing form — createBatch() still reads its fields — but nothing
sends a person to it any more. -->
<button class="btn btn-p btn-lead" id="btn-batch-new">
<svg
width="17"
height="17"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<line x1="12" y1="5" x2="12" y2="19" />
<line x1="5" y1="12" x2="19" y2="12" />
</svg>
<span data-i18n="batch.newBatch">New batch</span>
</button>
<div class="stabs" data-stab-home="nav.batches">
<button class="stab active" id="st-batch-list" data-i18n="batch.allBatches">All batches</button>
<!-- "Where does everything stand" used to sit on top of the batch
list, which meant this page answered the same question three
times: a stage strip counting bags, a room view counting the
same bags by rack, and a table grouping them by stage again.
The room view is a real tool — it is the only place bags can be
selected and moved in bulk — so it keeps everything it had and
gets its own tab instead of competing for the first screen. -->
<button class="stab" id="st-batch-locations" data-i18n="batch.locationsTab">Standorte</button>
<button class="stab" id="st-batch-substrate" data-i18n="sub.tab">Substrat-Ansätze</button>
<button class="stab" id="st-batch-harvest" data-i18n="batch.harvests">Harvests</button>
<button class="stab" id="st-batch-blocks" data-i18n="batch.blocksTab">Good blocks</button>
</div>
<!-- All batches -->
<div class="sp active" id="sp-batch-list">
<!-- Chargen on the Sortenliste, the same list as the Kulturbibliothek:
one row per Sorte with its bags by phase; a tap opens its Chargen,
the most due first; a tap on a Charge opens its window
(renderBatches). -->
<div class="card">
<div class="sl-head">
<div class="sec" data-i18n="ch.title">Chargen</div>
<span class="sl-count" id="ch-count"></span>
</div>
<div class="sl-tools">
<button type="button" id="ch-scan" class="btn btn-p" data-i18n="ch.scan">Charge scannen</button>
<button type="button" id="ch-archiv" class="btn" aria-pressed="false"></button>
</div>
<p class="lb-tap" data-i18n="ch.help">
Tipp auf eine Sorte, dann auf eine Charge: umziehen, Beutel, Notiz, löschen.
</p>
<div id="ch-list"></div>
</div>
</div>
<!-- Where everything currently sits, by room. Unchanged in every
respect except which tab it is on: same racks, same bag chips,
same multi-select and bulk move, same search. -->
<div class="sp" id="sp-batch-locations">
<div class="card" style="padding: 10px 14px">
<!-- Location overview — collapsible; the search sits in the body so it hides when collapsed -->
<div class="dash-locations-header">
<input
type="text"
id="status-q"
class="dash-search-inline"
data-i18n-aria-label="aria.searchDashboard"
aria-label="Search dashboard"
data-i18n-placeholder="dash.search"
placeholder="Search..."
/>
</div>
<div id="dash-locations"></div>
</div>
<!-- It has to live in the same page as the chips it acts on: .page
is display:none for every page but the open one, so while it sat
in p-dash the racks moved here and their bulk-move bar did not,
and selecting bags offered nothing to do with them. -->
<div id="loc-action-bar" class="action-bar-sticky" style="display: none"></div>
</div>
<!-- Every mix ever made, open and spent. Not front and center: this is
where you go to count them and to find the one that went wrong. -->
<div class="sp" id="sp-batch-substrate">
<!-- Mixing substrate and the list of mixes are one subject, so
they are one tab. The form used to sit in "Neue Charge",
beside the Growbags card, because mixing is step one of that
workflow — but it is also the thing this tab is about, and a
form on one tab whose results are listed on another asks the
reader to hold the connection in their head.
Its own "Offene Ansätze" list went with the move: the table
below shows the same mixes with more columns and a switch for
the spent ones.
Species-neutral — a mix is only the blend it was made to, and
any Sorte may draw from it. This is the only place raw material
leaves the shelf. -->
<div class="card form-card" id="sb-card" style="border: 1px solid var(--c-accent)">
<div class="card-head" style="--wk-c: var(--c-blue-dark); --wk-cl: var(--c-blue-light)">
<span class="card-head-art" aria-hidden="true"><svg viewBox="0 0 140 110" fill="none" xmlns="http://www.w3.org/2000/svg"><rect x="20" y="20" width="100" height="52" rx="26" fill="currentColor" opacity=".15"/><ellipse cx="46" cy="46" rx="13" ry="26" fill="currentColor" opacity=".10"/><path d="M44 33l26 26M44 59l26-26" stroke="currentColor" stroke-width="3.4" opacity=".20" stroke-linecap="round"/><path d="M96 14a26 26 0 0 1 12 14" stroke="currentColor" stroke-width="3.4" opacity=".28" stroke-linecap="round" fill="none"/><path d="M110 30l-1-9 -8 5z" fill="currentColor" opacity=".28"/><path d="M40 72l-9 30M104 72l9 30" stroke="currentColor" stroke-width="5" opacity=".20" stroke-linecap="round"/><path d="M24 104h96" stroke="currentColor" stroke-width="4" opacity=".26" stroke-linecap="round"/></svg></span>
<div class="sec" style="margin: 0" data-i18n="sub.title">Substrat ansetzen</div>
</div>
<div class="fs-meta" style="color: var(--c-text-sec); margin: 4px 0 10px" data-i18n="sub.desc">
Menge fertiges Substrat und Rezept angeben. Nur dieser Schritt bucht Pellets, Kleie und Gips vom Lager
ab — die Chargen darunter ziehen dann Kilo aus dem Ansatz.
</div>
<div class="g2">
<div>
<label data-i18n="sub.recipe">Rezept</label>
<select id="sb-recipe" onchange="sbRecipeChanged()"></select>
</div>
<div>
<label data-i18n="sub.bags">Anzahl Beutel</label>
<input type="text" id="sb-bags" value="50" inputmode="numeric" oninput="sbPreviewSoon()" />
</div>
</div>
<!-- Gezählt wird in Beuteln, nicht in Kilo: so wird abgefüllt. Die
Kilo rechnet sbTargetKg() aus Anzahl mal Größe. -->
<div style="margin-top: 8px">
<label data-i18n="batch.bagWeight">Beutelgewicht</label>
<div class="wkf-chipsrow" id="sb-bagkg-btns"></div>
</div>
<!-- Die Mischung. Aus der gewählten Mischung (Tabelle recipes)
vorbelegt und hier für diesen einen Ansatz änderbar, ohne
dass jemand dafür die gespeicherte Mischung umschreibt und
damit jeden künftigen Ansatz mit.
Pellets und Kleie sind eine Zahl in zwei Feldern: was nicht
Kleie (und Mais) ist, ist Pellets. -->
<div style="margin-top: 12px">
<div
style="display: flex; align-items: baseline; justify-content: space-between; gap: 8px; flex-wrap: wrap"
>
<label style="margin: 0" data-i18n="sub.blendTitle">Mischung</label>
<button
type="button"
id="sb-blend-reset"
class="btn-xs"
style="display: none"
onclick="sbResetBlend()"
>
<span data-i18n="sub.blendReset">Zurück zum Rezept</span>
</button>
</div>
<div class="sb-blend-row">
<div>
<label class="fs-xs"><span data-i18n="sub.pellets">Pellets</span> %</label>
<input
type="number"
id="sb-hw"
min="0"
max="100"
step="1"
inputmode="decimal"
oninput="sbBlendEdited('hw')"
/>
</div>
<div>
<label class="fs-xs"><span data-i18n="sub.bran">Weizenkleie</span> %</label>
<input
type="number"
id="sb-wb"
min="0"
max="100"
step="1"
inputmode="decimal"
oninput="sbBlendEdited('wb')"
/>
</div>
<div id="sb-corn-wrap" style="display: none">
<label class="fs-xs"><span data-i18n="sub.corn">Maismehl</span> %</label>
<input
type="number"
id="sb-corn"
min="0"
max="95"
step="1"
inputmode="decimal"
oninput="sbBlendEdited('corn')"
/>
</div>
<div>
<label class="fs-xs"><span data-i18n="sub.gypsum">Gips</span> %</label>
<input
type="number"
id="sb-gyp"
min="0"
max="20"
step="0.5"
inputmode="decimal"
oninput="sbBlendEdited('gyp')"
/>
</div>
<div>
<label class="fs-xs"><span data-i18n="sub.calcium">Calciumcarbonat</span> %</label>
<input
type="number"
id="sb-ca"
min="0"
max="20"
step="0.5"
inputmode="decimal"
oninput="sbBlendEdited('ca')"
/>
</div>
<div>
<label class="fs-xs"><span data-i18n="sub.moisture">Soll-Feuchte</span> %</label>
<input
type="number"
id="sb-rh"
min="20"
max="90"
step="0.5"
inputmode="decimal"
oninput="sbBlendEdited('rh')"
/>
</div>
<!-- Keine Zahl der Mischung: ändert die Rechnung nicht, wird
nur mit dem Ansatz gespeichert, damit sich der pH der
Chargen gegen die Sterilisierzeit lesen lässt. -->
<div>
<label class="fs-xs"><span data-i18n="sub.steri">Sterilisieren</span> min</label>
<input type="number" id="sb-steri" min="0" max="1440" step="1" inputmode="decimal" />
</div>
</div>
</div>
<!-- Wie feucht Pellets und Kleie ankommen. Gilt für alle Ansätze,
deshalb zugeklappt: es ändert sich mit der Lieferung, nicht
mit dem Ansatz. Speichern darf nur ein Admin. -->
<details id="sb-resid" style="margin-top: 10px">
<summary class="fs-meta" style="cursor: pointer; color: var(--c-text-sec)">
<span data-i18n="sub.residTitle">Restfeuchte Sackware</span>
<span id="sb-resid-sum"></span>
</summary>
<div class="sb-blend-row" style="margin-top: 6px">
<div>
<label class="fs-xs"><span data-i18n="sub.pellets">Pellets</span> %</label>
<input type="text" id="sb-resid-hw" inputmode="decimal" />
</div>
<div>
<label class="fs-xs"><span data-i18n="sub.bran">Weizenkleie</span> %</label>
<input type="text" id="sb-resid-wb" inputmode="decimal" />
</div>
<div>
<label class="fs-xs"><span data-i18n="sub.holdback">Wasser zurückhalten</span> %</label>
<input type="text" id="sb-resid-hold" inputmode="decimal" />
</div>
</div>
<button type="button" class="btn btn-sm" style="margin-top: 8px" onclick="sbSaveResidual()">
<span data-i18n="sub.residSave">Speichern</span>
</button>
</details>
<div id="sb-out" style="margin-top: 10px"></div>
<div style="margin-top: 8px">
<label data-i18n="sub.notes">Notiz</label>
<input type="text" id="sb-notes" maxlength="200" />
</div>
<div style="display: flex; gap: 8px; align-items: center; margin-top: 10px; flex-wrap: wrap">
<button id="sb-go" class="btn btn-p" onclick="createSubstrateBatchUI()" data-i18n="sub.create">
Ansatz anlegen
</button>
<span class="fs-meta" id="sb-status" style="color: var(--c-text-sec)"></span>
</div>
</div>
<div class="card">
<div style="display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 8px">
<div class="sec" data-i18n="sub.allTitle">Substrat-Ansätze</div>
<label class="fs-meta" style="color: var(--c-text-sec); display: flex; align-items: center; gap: 6px">
<input type="checkbox" id="sb-show-spent" style="width: auto" onchange="renderSubstrateTab()" />
<span data-i18n="sub.showSpent">Verbrauchte anzeigen</span>
</label>
</div>
<div class="fs-meta" id="sb-tab-summary" style="color: var(--c-text-sec); margin: 4px 0 10px"></div>
<div id="sb-tab-list"></div>
</div>
</div>
<!-- New batch -->
<div class="sp" id="sp-batch-new" data-stab-lead="batch.newBatch">
<!-- Step two. The mix is made one tab over, on Substrat-Ansätze, and
this draws kilos out of it — so a Charge cannot be started
before there is something to start it from, which is the order
the two steps have always had. -->
<div class="card form-card">
<div class="card-head" style="--wk-c: var(--c-green-dark); --wk-cl: var(--c-primary-light)">
<span class="card-head-art" aria-hidden="true"><svg viewBox="0 0 140 110" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M40 34h60v58a10 10 0 0 1-10 10H50a10 10 0 0 1-10-10z" fill="currentColor" opacity=".16"/><path d="M40 34l-9-14 15 5 12-7 12 7 15-5-9 14z" fill="currentColor" opacity=".28"/><circle cx="70" cy="64" r="11" fill="currentColor" opacity=".20"/><circle cx="70" cy="64" r="4" fill="currentColor" opacity=".26"/><path d="M50 86h40" stroke="currentColor" stroke-width="3" opacity=".18" stroke-linecap="round"/></svg></span>
<div class="sec" style="margin: 0" data-i18n="batch.createNew">Growbags Batch</div>
<button type="button" class="btn btn-sm btn-p" id="nb-guided" data-i18n="batch.guided">
Schritt für Schritt
</button>
</div>
<div class="fs-meta"
id="nb-source-banner"
style="display: none; margin-top: 6px; padding: 6px 10px; border-radius: 6px; background: #dcfce7; border-left: 4px solid #16a34a; color: #14532d"
></div>
<div style="display: flex; flex-direction: column; gap: 10px; margin-top: 4px">
<div>
<label data-i18n="batch.bagWeight">Bag weight</label>
<div style="display: flex; gap: 4px; flex-wrap: wrap; margin-top: 2px" id="nb-weight-btns">
<!-- data-kg ist die Quelle, nicht die Beschriftung: parseFloat
liest aus "3,5 kg" eine 3. -->
<button type="button" class="btn btn-sm" id="wbtn-35" data-kg="3.5">3,5 kg</button>
<button type="button" class="btn btn-sm btn-p" id="wbtn-38" data-kg="3.8">3,8 kg</button>
<button type="button" class="btn btn-sm" id="wbtn-43" data-kg="4.3">4,3 kg</button>
<button type="button" class="btn btn-sm" id="wbtn-5" data-kg="5">5 kg</button>
<button type="button" class="btn btn-sm" id="wbtn-other" data-i18n="batch.otherWeight">
anderes
</button>
</div>
<input
type="text"
inputmode="decimal"
id="nb-weight"
value="3.8"
style="margin-top: 6px; width: 100px; display: none"
autocomplete="off"
/>
</div>
<div>
<label data-i18n="strains.pilzsorte">Pilzsorte</label>
<select id="nb-strain-sel" onchange="nbStrainChanged()">
<option value="" data-i18n="strains.selectPlaceholder">— Pilzsorte wählen —</option>
</select>
<div class="fs-xs"
id="nb-no-strains-hint"
style="display: none; color: var(--c-red); margin-top: 4px"
>
<span data-i18n="strains.noStrainsHint"
>Keine Pilzsorten angelegt. Bitte zuerst eine Pilzsorte anlegen.</span
>
<button
type="button"
id="nb-create-strain-btn"
class="btn btn-sm btn-p fs-xs"
style="margin-left: 6px"
data-i18n="strains.createNow"
>
Jetzt Pilzsorte anlegen →
</button>
</div>
</div>
<div>
<label data-i18n="batch.strainLabel">Strain</label>
<input
type="text"
id="nb-strain-text"
data-i18n-placeholder="batch.strainPlaceholder"
placeholder="z.B. XY-1, Nordlicht, Beispiel-Genetik…"
autocomplete="off"
/>
<!-- Angesetzt am (JJJJ-MM-TT), leer = heute. Nur die Arbeitsgänge
setzen es — bei der Inventur für Bestand, der schon stand.
createBatch() liest und leert es als Erstes. -->
<input type="hidden" id="nb-created" value="" />
</div>
<div>
<label data-i18n="batch.fromProduct">Aus Produkt (optional)</label>
<select id="nb-product" onchange="_nbProductChanged()">
<option value="" data-i18n="batch.noProduct">— ohne Produkt —</option>
</select>
<div class="fs-xs"
id="nb-product-hint"
style="color: var(--c-text-muted); margin-top: 2px"
data-i18n="batch.fromProductHint"
>
Produkt wählen → Substrat, Kokos & Grain werden automatisch berechnet.
</div>
</div>
<!-- v67: pick a mix and this Charge is portioned out of it —
no substrate is booked, only the spawn. -->
<div style="margin-bottom: 8px">
<label data-i18n="sub.fromMix">Aus Substrat-Ansatz</label>
<select id="nb-substrate-batch" onchange="nbSubstrateChanged()"></select>
<div id="nb-substrate-info" style="display: none; margin-top: 6px"></div>
</div>
<div class="g2">
<div>
<label data-i18n="batch.qty">Qty (bags)</label
><input type="text" inputmode="numeric" enterkeyhint="next" id="nb-qty" value="10" oninput="nbSubstrateNeed()" />
</div>
<div>
<label data-i18n="batch.incDays">Incubation days</label
><input type="text" inputmode="numeric" enterkeyhint="next" id="nb-days" value="14" />
</div>
</div>
<details id="nb-sub-section" style="background: var(--c-bg); border-radius: 8px; padding: 10px">
<summary class="fs-meta" style="color: var(--c-text-sec); cursor: pointer">
<span data-i18n="batch.substrate">Substrate</span>
<!-- Filled by nbSubSum() so the composition that will actually be
deducted stays visible while this section is collapsed. -->
<span id="nb-sub-head" style="font-weight: 600; color: var(--c-text)"></span>
</summary>
<div style="margin-top: 10px; display: flex; flex-direction: column; gap: 8px">
<div class="g2">
<div>
<label data-i18n="batch.hardwood">Hardwood %</label
><input type="number" id="nb-hw" placeholder="70" min="0" max="100" />
</div>
<div>
<label data-i18n="batch.wheatBran">Wheat bran %</label
><input type="number" id="nb-wb" placeholder="30" min="0" max="100" />
</div>
</div>
<div class="g2">
<div>
<label data-i18n="batch.fieldCapacity">Field capacity %RH</label
><input type="number" id="nb-rh" placeholder="65" min="0" max="100" />
</div>
<div style="display: flex; align-items: center; gap: 8px; padding-top: 18px">
<input type="checkbox" id="nb-gyp" style="width: 22px; height: 22px; margin: 0" />
<label
style="margin: 0; font-size: 13px; color: var(--c-text); cursor: pointer"
for="nb-gyp"
data-i18n="batch.gypsumAdded"
>Gypsum added</label
>
</div>
</div>
<div class="g2">
<div>
<label data-i18n="batch.coir">Kokos / CVG %</label
><input type="number" id="nb-coir" placeholder="0" min="0" max="100" />
</div>
<div></div>
</div>
<div class="g2">
<div>
<label data-i18n="batch.grainKg">Grain kg/Beutel</label
><input type="number" id="nb-grainkg" placeholder="0" min="0" step="0.01" />
</div>
<div>
<label data-i18n="batch.grainRh">Grain Hydration %</label
><input type="number" id="nb-grainrh" placeholder="52" min="0" max="100" />
</div>
</div>
<div class="fs-xs" id="nb-subsum" style="color: var(--c-text-muted)"></div>
</div>
</details>
<!-- One field for every kind of inoculant. Cultures live in
`cultures`, grain spawn in `batches` as G-… bags, but the
worker just scans what is in their hand. Nothing is consumed
automatically — after the batch saves we ask whether it was
used up (see inocAskConsume). -->
<div>
<label data-i18n="inoc.label">Beimpft mit (optional)</label>
<div style="display: flex; gap: 6px; align-items: center">
<button
type="button"
class="btn btn-sm"
id="nb-inoc-scan"
style="flex: none; white-space: nowrap"
>
<span aria-hidden="true">📷</span> <span data-i18n="inoc.scan">Scannen</span>
</button>
<select id="nb-inoc-pick" style="flex: 1; min-width: 0" onchange="inocPicked('nb')"></select>
</div>
<div id="nb-inoc-chips" style="display: flex; flex-wrap: wrap; gap: 4px; margin-top: 5px"></div>
<div class="fs-xs" style="color: var(--c-text-muted); margin-top: 3px" data-i18n="inoc.hint">
Flüssigkultur, Petrischale, Mutterkultur oder Körnerbrut — scannen oder wählen
</div>
</div>
<!-- Kept for createBatch's sourceId and for the scan autofill;
the visible control above is what the operator uses. -->
<select id="nb-culture" style="display: none"></select>
<!-- Beim Beimpfen gemessen. Neben der Sterilisierzeit des
Ansatzes zeigt es, wie die Zeit den pH verschiebt. -->
<div>
<label data-i18n="batch.ph">pH (gemessen, optional)</label>
<input type="text" id="nb-ph" inputmode="decimal" class="w-cap" style="--w-cap: 120px" />
</div>
<div>
<label data-i18n="batch.notes">Notes (optional)</label
><textarea
id="nb-notes"
rows="2"
data-i18n-placeholder="batch.notesPlaceholder"
placeholder="Any notes..."
></textarea>
</div>
<div class="met">
<div class="met-l" data-i18n="batch.idPreview">Batch ID preview</div>
<div
id="nb-prev"
style="font-family: monospace; font-size: 13px; font-weight: 600; margin-top: 3px"
>
—
</div>
</div>
<div class="fs-meta"
id="nb-mat-preview"
style="display: none; background: var(--c-primary-light); border: 1px solid var(--c-green-border); border-radius: 8px; padding: 10px 12px; color: var(--c-green-dark); line-height: 1.8"
></div>
<button id="btn-24" class="btn btn-p" data-i18n="batch.create">Create batch</button>
</div>
</div>
</div>
<!-- Harvests -->
<div class="sp" id="sp-batch-harvest">
<div class="g3" id="harvest-metrics" style="margin-bottom: 10px"></div>
<!-- Harvest charts row -->
<div class="g2" style="margin-bottom: 10px">
<div class="card">
<div class="sec" data-i18n="harvest.totalYieldPerBatch">Total yield per batch (g)</div>
<canvas id="batch-yield-chart" height="180"></canvas>
</div>
<div class="card">
<div class="sec" data-i18n="harvest.overTime">Harvest over time (weekly, g)</div>
<canvas id="harvest-timeline-chart" height="180"></canvas>
</div>
</div>
<div class="card">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px">
<div class="sec" style="margin: 0" data-i18n="harvest.perBatchTotals">Per batch totals</div>
</div>
<div id="harvest-totals"></div>
</div>
<div class="card">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px">
<div class="sec" style="margin: 0" data-i18n="harvest.log">Harvest log</div>
<input class="fs-meta"
type="search"
enterkeyhint="search"
autocomplete="off"
id="harvest-q"
data-i18n-aria-label="aria.searchHarvests"
aria-label="Search harvests"
data-i18n-placeholder="harvest.searchBatch"
placeholder="Search batch..."
style="width: 140px; padding: 4px 8px"
/>
</div>
<div style="overflow-x: auto">
<table id="t-harvest" class="t-cards" role="table">
<thead role="rowgroup">
<tr role="row">
<th class="sortable" data-sort="time" data-i18n="th.date" role="columnheader">Date</th>
<th class="sortable" data-sort="batch" data-i18n="th.batch" role="columnheader">Batch</th>
<th class="sortable" data-sort="bag" data-i18n="th.bag" role="columnheader">Bag</th>
<th class="sortable" data-sort="species" data-i18n="th.species" role="columnheader">Species</th>
<th class="sortable" data-sort="strain" data-i18n="th.strain" role="columnheader">Strain</th>
<th class="sortable" data-sort="flush" data-i18n="th.flush" role="columnheader">Flush</th>
<th class="sortable" data-sort="grams" data-i18n="th.grams" role="columnheader">Grams</th>
</tr>
</thead>
<tbody id="harvest-body" role="rowgroup"></tbody>
</table>
</div>
</div>
</div>
<!-- Blockbuch: die als gut vermerkten Blöcke mit dem, woraus sie gemacht
waren, und darunter jeder Block mit einer Notiz. Gefüllt von
loadBlockBook(), erst beim Öffnen und dann bei jeder Synchronisierung. -->
<div class="sp" id="sp-batch-blocks">
<div class="card">
<div class="sec" data-i18n="block.summary">What the good blocks have in common</div>
<div class="fs-meta" style="color: var(--c-text-sec); margin-bottom: 8px" data-i18n="block.goodListHint">
What the good blocks were made of.
</div>
<div id="bb-summary"></div>
</div>
<div class="card">
<div class="sec" data-i18n="block.goodList">Good blocks</div>
<div id="bb-good"></div>
</div>
<div class="card">
<div class="sec" data-i18n="block.notesList">Notes on blocks</div>
<div id="bb-notes"></div>
</div>
</div>
</div>
<!-- ══ PILZSORTEN ══ -->
<div class="page" id="p-strains">
<div class="g2" style="align-items: start">
<div class="card">
<div class="sec" data-i18n="strains.manage">Pilzsorten verwalten</div>
<div style="display: flex; flex-direction: column; gap: 10px; margin-top: 8px">
<div class="g2">
<div>
<label data-i18n="strains.name">Name</label
><input
type="text"
id="ms-name"
data-i18n-placeholder="strains.namePlaceholder"
placeholder="z. B. Shiitake"
/>
</div>
<div>
<label data-i18n="strains.kuerzel">Kürzel (2-4 Zeichen)</label
><input
type="text"
id="ms-kuerzel"
data-i18n-placeholder="strains.kuerzelPlaceholder"
placeholder="z.B. LIMA"
minlength="2"
maxlength="4"
style="text-transform: uppercase; width: 80px"
/>
</div>
</div>
<div>
<label data-i18n="strains.descriptor">Genetik-Bezeichnung (z.B. Nordlicht)</label
><input
type="text"
id="ms-desc"
data-i18n-placeholder="strains.descriptorPlaceholder"
placeholder="z. B. Nordlicht, Beispiel-Genetik, …"
/>
</div>
<div style="border-top: 1px solid var(--c-border); padding-top: 10px; margin-top: 4px">
<div
style="font-size: 13px; font-weight: 600; color: var(--c-text-sec)"
data-i18n="strains.recHeader"
>
Standard-Anbau (optional)
</div>
<div class="fs-xs"
style="color: var(--c-text-muted); margin: 2px 0 8px"
data-i18n="strains.recHint"
>
Vorlage, die neue Chargen automatisch vorausfüllt. Kann leer bleiben — nur nötig, damit „+ Charge"
diese Sorte anbietet.
</div>
<select id="ms-rec-copy" onchange="msRecCopyFrom()" style="width: 100%; margin-bottom: 8px">
<option value="" data-i18n="strains.recCopyNone">— Rezept von anderer Sorte übernehmen —</option>
</select>
<label class="fs-meta" style="color: var(--c-text-muted)">
<span data-i18n="strains.recType">Anbau-Art</span>
<select id="ms-rec-type" onchange="msRecTypeChange()">
<option value="" data-i18n="strains.recNone">Kein Rezept</option>
<option value="block" data-i18n="strains.recBlock">Substrat-Block</option>
<option value="allinone" data-i18n="strains.recAllinone">All-in-One (Grain + Substrat)</option>
<option value="grain" data-i18n="strains.recGrain">Körnerbrut (Grain)</option>
</select>
</label>
<div id="ms-rec-subgroup" style="display: none; margin-top: 8px">
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px">
<label class="fs-meta" style="color: var(--c-text-muted)">
<span data-i18n="orders.p.substrate">Substrat-Art</span>
<select id="ms-rec-substrate" onchange="msRecTypeChange()">
<option value="holzkleie" data-i18n="orders.sub.holzkleie">Holz + Weizenkleie</option>
<option value="cvg" data-i18n="orders.sub.cvg">Kokos / CVG</option>
</select>
</label>
<label class="fs-meta" style="color: var(--c-text-muted)">
<span data-i18n="orders.p.bagKg">Beutel kg (nass)</span>
<input id="ms-rec-bagkg" type="number" min="0" step="0.1" value="0" oninput="msRecNeed()" />
</label>
<label class="fs-meta" style="color: var(--c-text-muted)">
<span data-i18n="orders.p.rh">Hydration %</span>
<input
id="ms-rec-rh"
type="number"
min="0"
max="100"
step="1"
value="0"
oninput="msRecNeed()"
/>
</label>
</div>
<div
id="ms-rec-holzgroup"
style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px; margin-top: 8px"
>
<label class="fs-meta" style="color: var(--c-text-muted)">
<span data-i18n="orders.p.hw">Hartholz %</span>
<input
id="ms-rec-hw"
type="number"
min="0"
max="100"
step="1"
value="75"
oninput="msRecNeed()"
/>
</label>
<label class="fs-meta" style="color: var(--c-text-muted)">
<span data-i18n="orders.p.wb">Weizenkleie %</span>
<input
id="ms-rec-wb"
type="number"
min="0"
max="100"
step="1"
value="25"
oninput="msRecNeed()"
/>
</label>
<!-- Only a few recipes use this, but it has to be enterable
or such a recipe cannot be expressed at all. -->
<label class="fs-meta" style="color: var(--c-text-muted)">
<span data-i18n="strains.recCornPct">Maismehl %</span>
<input
id="ms-rec-corn"
type="number"
min="0"
max="100"
step="1"
value="0"
oninput="msRecNeed()"
/>
</label>
</div>
<div
id="ms-rec-coirgroup"
style="display: none; grid-template-columns: 1fr; gap: 8px; margin-top: 8px"
>
<label class="fs-meta" style="color: var(--c-text-muted)">
<span data-i18n="orders.p.coir">Kokos %</span>
<input
id="ms-rec-coir"
type="number"
min="0"
max="100"
step="1"
value="100"
oninput="msRecNeed()"
/>
</label>
</div>
<label style="display: flex; align-items: center; gap: 6px; font-size: 13px; margin-top: 8px">
<input type="checkbox" id="ms-rec-gyp" style="width: auto" oninput="msRecNeed()" />
<span data-i18n="orders.p.gypsum">Gips (~1%)</span>
</label>
<!-- The two figures a bulk mix needs and a single bag does not:
how much gypsum goes in per dry kilo, and how much spawn the
finished substrate is inoculated at. -->
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 8px">
<label class="fs-meta" style="color: var(--c-text-muted)">
<span data-i18n="strains.recGypsumPct">Gips % vom Trockenmix</span>
<input id="ms-rec-gyppct" type="number" min="0" max="20" step="0.5" value="0" oninput="msRecNeed()" />
</label>
<label class="fs-meta" style="color: var(--c-text-muted)">
<span data-i18n="strains.recSpawnPct">Körnerbrut % vom Substrat</span>
<input id="ms-rec-spawn" type="number" min="0" max="20" step="0.5" value="0" oninput="msRecNeed()" />
</label>
</div>
<div class="fs-xs" id="ms-rec-ref" style="color: var(--c-text-muted); margin-top: 6px"></div>
<!-- What the lab should never be without while this Sorte is
being grown. Per Sorte because shiitake needs two months of
notice and an oyster needs a fortnight. -->
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 10px">
<label class="fs-meta" style="color: var(--c-text-muted)">
<span data-i18n="strains.minSpawn">Mindestbestand Körnerbrut (kg)</span>
<input id="ms-min-spawn" type="number" min="0" step="1" value="0" />
</label>
<label class="fs-meta" style="color: var(--c-text-muted)">
<span data-i18n="strains.minLc">Mindestbestand Flüssigkultur (Gläser)</span>
<input id="ms-min-lc" type="number" min="0" step="1" value="0" />
</label>
<!-- A slant is the long-term backup, a plate is what you
work from, a jar of LC is what goes into grain. Three
reasons to keep something, stocked in three different
numbers — so three figures, not one shared with LC. -->
<label class="fs-meta" style="color: var(--c-text-muted)">
<span data-i18n="strains.minMc">Mindestbestand Slants (Stück)</span>
<input id="ms-min-mc" type="number" min="0" step="1" value="0" />
</label>
<label class="fs-meta" style="color: var(--c-text-muted)">
<span data-i18n="strains.minPd">Mindestbestand Petrischalen (Stück)</span>
<input id="ms-min-pd" type="number" min="0" step="1" value="0" />
</label>
</div>
</div>
<div id="ms-rec-graingroup" style="display: none; margin-top: 8px">
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px">
<label class="fs-meta" style="color: var(--c-text-muted)">
<span data-i18n="orders.p.grainKg">Grain kg (nass)</span>
<input id="ms-rec-grainkg" type="number" min="0" step="0.01" value="0" oninput="msRecNeed()" />
</label>
<label class="fs-meta" style="color: var(--c-text-muted)">
<span data-i18n="orders.p.grainRh">Grain Hydration %</span>
<input
id="ms-rec-grainrh"
type="number"
min="0"
max="100"
step="1"
value="52"
oninput="msRecNeed()"
/>
</label>
</div>
</div>
<div id="ms-rec-daysrow" class="g2" style="display: none; margin-top: 8px">
<label class="fs-meta" style="color: var(--c-text-muted)">
<span data-i18n="batch.incDays">Inkubationstage</span>
<input id="ms-rec-days" type="number" min="1" step="1" value="14" />
</label>
<!-- Distinct from incubation: rec_inc_days ends at colonisation,
this is how long the bags then sit in the fruiting tent. -->
<label class="fs-meta" style="color: var(--c-text-muted)">
<span data-i18n="strains.recFruitDays">Erntetage</span>
<input id="ms-rec-fruitdays" type="number" min="0" step="1" value="0" />
</label>
</div>
<div id="ms-rec-need" class="muted fs-meta" style="margin-top: 6px"></div>
</div>
<div style="display: flex; gap: 8px">
<button class="btn btn-p" id="ms-save-btn" data-i18n="strains.save">Pilzsorte anlegen</button>
<button class="btn" id="ms-cancel-btn" style="display: none" data-i18n="strains.cancel">
Abbrechen
</button>
</div>
<input type="hidden" id="ms-edit-id" value="" />
</div>
</div>
<div class="card">
<div class="sec" data-i18n="strains.list">Vorhandene Pilzsorten</div>
<div class="fs-meta"
id="strains-hint"
style="color: var(--c-text-muted); margin-bottom: 8px"
data-i18n="strains.hint"
>
Ohne angelegte Pilzsorte können keine Chargen erstellt werden.
</div>
<div style="overflow-x: auto">
<table>
<thead>
<tr>
<!-- Which Sorten the farm is growing this season. It belongs
here rather than on a tab of its own: it is a property
of the Sorte, next to its Kürzel and its recipe, and
this is the page somebody already opens to add or
retire one. -->
<th data-i18n="strains.imProgramm">Im Programm</th>
<th data-i18n="strains.name">Name</th>
<th data-i18n="strains.kuerzel">Kürzel</th>
<th data-i18n="strains.descriptor">Genetik</th>
<th data-i18n="strains.recipe">Rezept</th>
<th data-i18n="strains.inUse">Verwendung</th>
<th></th>
</tr>
</thead>
<tbody id="strains-body"></tbody>
</table>
</div>
</div>
</div>
<!-- Stämme je Pilzsorte (v87). Eine eigene Karte statt einer Spalte in der
Sortentabelle: die scrollt am Telefon seitwärts, und diese Liste soll
man mit dem Daumen pflegen können, während das Glas in der anderen
Hand ist. Anlegen geht auch mitten im Arbeitsgang, siehe stammRender. -->
<div class="card" id="staemme-card">
<div class="sec" data-i18n="stamm.manage">Stämme</div>
<div class="fs-meta stamm-hint" data-i18n="stamm.hint">
Die Linien einer Pilzsorte, z. B. Nordlicht oder Blue Seestern 01.
</div>
<div class="stamm-add">
<select id="st-sorte" data-i18n-aria-label="strains.pilzsorte" aria-label="Pilzsorte"></select>
<input
type="text"
id="st-name"
maxlength="60"
autocomplete="off"
data-i18n-placeholder="stamm.newPh"
placeholder="Name, z. B. Nordlicht"
/>
<button type="button" class="btn btn-p" id="st-add-btn" data-i18n="stamm.add">Hinzufügen</button>
</div>
<div id="staemme-list"></div>
</div>
</div>
<!-- ══ LAB ══ -->
<div class="page" id="p-orders">
<!-- The strip is the section's own list again, on both devices: five pills on a
desk, a drill-down index in a hand. It was hidden while the five views were
top-level sidebar entries, which cost the sidebar more room than it had. -->
<div class="stabs" data-stab-home="nav.orders">
<button class="stab active" id="st-orders-pack" data-i18n="orders.packTab">Packen</button>
<button class="stab" id="st-orders-inbox" data-i18n="orders.inbox">Eingang</button>
<button class="stab" id="st-orders-tomake" data-i18n="orders.tomake">Was muss ich machen</button>
<button class="stab" id="st-orders-mapping" data-i18n="orders.mapping">Produkt-Zuordnung</button>
<button class="stab" id="st-orders-customers" data-i18n="orders.customers">Kunden</button>
<button class="stab" id="st-orders-versand" data-i18n="orders.versandTab">Versand</button>
</div>
<!-- Packen: the shop orders from Billbee, as what to pack today and what to
make first. Filled by renderOrderBoard() from /api/orders/board
(order-board.js); no customer data is in it. -->
<div class="sp active" id="sp-orders-pack">
<div id="ob-sync" class="ob-sync" role="status" style="display: none"></div>
<div class="card">
<div class="ob-head">
<div class="sec" data-i18n="orders.board.totalsTitle">Alles zusammen</div>
</div>
<div class="muted fs-meta ob-hint" data-i18n="orders.board.totalsHint">
Alle offenen Bestellungen, einmal gezählt: erst alles aus dem Regal holen, dann Bestellung für Bestellung
packen.
</div>
<div id="ob-totals"></div>
</div>
<div class="card">
<div class="ob-head">
<div class="sec" data-i18n="orders.board.packTitle">Heute packen</div>
<span id="ob-pack-count" class="ob-count"></span>
</div>
<div id="ob-pack"></div>
</div>
<div class="card">
<div class="ob-head">
<div class="sec" data-i18n="orders.board.makeTitle">Herstellen</div>
</div>
<div class="muted fs-meta ob-hint" data-i18n="orders.board.makeHint">
Wird frisch nach Bestellung gemacht. Ist eine Bestellung fertig, unten auf „Hergestellt" tippen, dann
steht sie unter Heute packen.
</div>
<div id="ob-make"></div>
</div>
<div class="card" id="ob-later-card" style="display: none">
<div class="ob-head">
<div class="sec" data-i18n="orders.board.laterTitle">Wartet auf Herstellung</div>
<span id="ob-later-count" class="ob-count"></span>
</div>
<div id="ob-later"></div>
</div>
<details class="card ob-done-list">
<summary class="ob-head">
<span class="sec" data-i18n="orders.board.doneTitle">Erledigt heute</span>
<span id="ob-done-count" class="ob-count"></span>
</summary>
<div id="ob-done"></div>
</details>
</div>
<!-- Inbox -->
<div class="sp" id="sp-orders-inbox">
<div id="orders-unmapped-banner" class="oh-banner" style="display: none">
<span
>⚠️ <strong id="orders-unmapped-count">0</strong>
<span data-i18n="orders.unmappedBanner">Positionen ohne Produktzuordnung</span></span
>
<button class="btn btn-sm" data-action="oh-goto-mapping" data-i18n="orders.mapNow">Jetzt zuordnen</button>
</div>
<div class="card">
<div
style="
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
flex-wrap: wrap;
margin-bottom: 10px;
"
>
<div style="display: flex; gap: 6px; flex-wrap: wrap">
<button class="oh-fchip active" data-action="oh-filter" data-filter="" data-i18n="orders.filterAll">
Alle
</button>
<button class="oh-fchip" data-action="oh-filter" data-filter="new" data-i18n="orders.status.new">
Neu
</button>
<button
class="oh-fchip"
data-action="oh-filter"
data-filter="in_production"
data-i18n="orders.status.in_production"
>
In Produktion
</button>
<button class="oh-fchip" data-action="oh-filter" data-filter="ready" data-i18n="orders.status.ready">
Versandbereit
</button>
</div>
<div style="display: flex; gap: 6px; flex-wrap: wrap">
<button class="btn btn-sm" data-action="oh-manual-toggle" data-i18n="orders.addManual">
+ Manuell
</button>
<button class="btn btn-sm" data-action="oh-import-csv" data-i18n="orders.importCsv">
CSV importieren
</button>
<input type="file" id="orders-csv-file" accept=".csv,text/csv" style="display: none" />
</div>
</div>
<div id="orders-manual-panel" class="oh-manual" style="display: none">
<div style="display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px">
<select id="oh-m-channel">
<option value="manual">Manuell</option>
<option value="wix">Wix</option>
<option value="etsy">Etsy</option>
<option value="ebay">eBay</option>
<option value="billbee">Billbee</option>
</select>
<input id="oh-m-oid" data-i18n-placeholder="orders.f.orderId" placeholder="Bestell-Nr." />
<input id="oh-m-shipby" type="date" />
<input id="oh-m-cust" data-i18n-placeholder="orders.f.customer" placeholder="Kunde" />
<input id="oh-m-email" data-i18n-placeholder="orders.f.email" placeholder="E-Mail" />
<input id="oh-m-sku" data-i18n-placeholder="orders.f.sku" placeholder="SKU" />
<input id="oh-m-title" data-i18n-placeholder="orders.f.item" placeholder="Artikel" />
<input id="oh-m-qty" type="number" min="1" value="1" />
<button class="btn btn-p btn-sm" data-action="oh-manual-submit" data-i18n="orders.add">
Hinzufügen
</button>
</div>
</div>
<div style="overflow-x: auto">
<table id="t-orders" class="t-cards" role="table">
<thead role="rowgroup">
<tr role="row">
<th data-i18n="orders.th.channel" role="columnheader">Kanal</th>
<th data-i18n="orders.th.order" role="columnheader">Bestellung</th>
<th data-i18n="orders.th.customer" role="columnheader">Kunde</th>
<th data-i18n="orders.th.items" role="columnheader">Artikel</th>
<th data-i18n="orders.th.shipBy" role="columnheader">Liefern bis</th>
<th data-i18n="orders.th.status" role="columnheader">Status</th>
</tr>
</thead>
<tbody id="orders-body" role="rowgroup"></tbody>
</table>
</div>
</div>
</div>
<!-- To-Make -->
<div class="sp" id="sp-orders-tomake">
<div class="card">
<div class="sec" data-i18n="orders.demandTitle">Netto-Produktionsbedarf</div>
<div class="muted fs-meta" style="margin-bottom: 8px" data-i18n="orders.demandHint">
Offene Bestellungen minus reserviert. „Start bis" = frühestes Lieferdatum minus Vorlaufzeit.
</div>
<div style="overflow-x: auto">
<table id="t-demand" class="t-cards" role="table">
<thead role="rowgroup">
<tr role="row">
<th data-i18n="orders.th.product" role="columnheader">Produkt</th>
<th data-i18n="orders.th.demand" role="columnheader">Bedarf</th>
<th data-i18n="orders.th.fromStock" role="columnheader">vom Lager</th>
<th data-i18n="orders.th.toProduce" role="columnheader">produzieren</th>
<th data-i18n="orders.th.startBy" role="columnheader">Start bis</th>
<th data-i18n="orders.th.components" role="columnheader">Komponenten</th>
</tr>
</thead>
<tbody id="demand-body" role="rowgroup"></tbody>
</table>
</div>
</div>
</div>
<!-- Mapping -->
<div class="sp" id="sp-orders-mapping">
<div class="card">
<div class="sec" data-i18n="orders.unmappedTitle">Noch zuzuordnen</div>
<div id="orders-unmapped-list"></div>
</div>
<div class="card">
<div class="sec" data-i18n="orders.fixmapTitle">Feste Zuordnungen</div>
<p class="fs-meta"
style="color: var(--c-text-muted); margin-bottom: 10px; line-height: 1.6"
data-i18n="orders.fixmapDesc"
>
Welche Artikelnummer eines Kanals zu welchem Produkt gehört. Die Liste darüber füllt sich nur aus
Bestellungen; hier lässt sich eine Nummer schon vorher zuordnen, dann löst die erste Bestellung
dieses Artikels sofort auf ein Produkt auf.
</p>
<div style="display: flex; gap: 6px; flex-wrap: wrap; align-items: center">
<select id="oh-fixmap-channel" style="width: auto">
<option value="billbee">Billbee</option>
<option value="wix">Wix</option>
<option value="etsy">Etsy</option>
<option value="ebay">eBay</option>
</select>
<input id="oh-fixmap-sku" data-i18n-placeholder="orders.f.sku" placeholder="SKU" style="width: auto" />
<select id="oh-fixmap-product" style="width: auto"></select>
<button class="btn btn-sm btn-p" data-action="oh-map-fixed" data-i18n="orders.assign">
Zuordnen
</button>
</div>
<div id="orders-fixmap-list" style="margin-top: 10px"></div>
</div>
<div class="card">
<div
style="
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
flex-wrap: wrap;
margin-bottom: 8px;
"
>
<div class="sec" style="margin: 0" data-i18n="orders.catalog">Produktkatalog</div>
<button class="btn btn-sm btn-p" data-action="oh-prod-new" data-i18n="orders.newProduct">
+ Neues Produkt
</button>
</div>
<div id="orders-prod-form" class="oh-manual" style="display: none">
<input type="hidden" id="oh-p-id" />
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 8px">
<input id="oh-p-name" data-i18n-placeholder="orders.p.name" placeholder="Produktname" />
<input id="oh-p-sku" data-i18n-placeholder="orders.p.sku" placeholder="SKU (optional)" />
<label class="fs-meta" style="color: var(--c-text-muted)">
<span data-i18n="orders.th.category">Kategorie</span>
<select id="oh-p-category">
<option value="all-in-one" data-i18n="orders.cat.allinone">All-in-One</option>
<option value="substrat" data-i18n="orders.cat.substrat">Substrat</option>
<option value="koernerbrut" data-i18n="orders.cat.koernerbrut">Körnerbrut</option>
<option value="kultur" data-i18n="orders.cat.kultur">Kultur</option>
<option value="zuchtset" data-i18n="orders.cat.zuchtset">Zuchtset</option>
<option value="zubehoer" data-i18n="orders.cat.zubehoer">Zubehör</option>
</select>
</label>
<label class="fs-meta" style="color: var(--c-text-muted)">
<span data-i18n="orders.p.prodType">Produktionsart</span>
<select id="oh-p-prodtype">
<option value="allinone" data-i18n="orders.pt.allinone">All-in-One (Grain + Substrat)</option>
<option value="block" data-i18n="orders.pt.block">Substrat-Block</option>
<option value="grain" data-i18n="orders.pt.grain">Körnerbrut (Grain)</option>
<option value="buy" data-i18n="orders.pt.buy">Zukauf / nur Lager</option>
</select>
</label>
<label class="fs-meta" style="color: var(--c-text-muted)">
<span data-i18n="orders.p.stock">Fertigbestand</span>
<input id="oh-p-stock" type="number" min="0" step="1" value="0" />
</label>
<label class="fs-meta" style="color: var(--c-text-muted)">
<span data-i18n="orders.p.lead">Vorlauf (Tage)</span>
<input id="oh-p-lead" type="number" min="0" step="1" value="0" />
</label>
</div>
<div
id="oh-p-subgroup"
style="display: none; border-top: 1px solid var(--c-border); padding-top: 8px; margin-bottom: 8px"
>
<div class="muted fs-meta" style="margin-bottom: 6px" data-i18n="orders.p.subTitle">
Substrat pro Stück
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px">
<label class="fs-meta" style="color: var(--c-text-muted)">
<span data-i18n="orders.p.substrate">Substrat-Art</span>
<select id="oh-p-substrate">
<option value="holzkleie" data-i18n="orders.sub.holzkleie">Holz + Weizenkleie</option>
<option value="cvg" data-i18n="orders.sub.cvg">Kokos / CVG</option>
</select>
</label>
<label class="fs-meta" style="color: var(--c-text-muted)">
<span data-i18n="orders.p.bagKg">Beutel kg (nass)</span>
<input id="oh-p-bagkg" type="number" min="0" step="0.1" value="0" />
</label>
<label class="fs-meta" style="color: var(--c-text-muted)">
<span data-i18n="orders.p.rh">Hydration %</span>
<input id="oh-p-rh" type="number" min="0" max="100" step="1" value="0" />
</label>
</div>
<div
id="oh-p-holzgroup"
style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 8px"
>
<label class="fs-meta" style="color: var(--c-text-muted)">
<span data-i18n="orders.p.hw">Hartholz %</span>
<input id="oh-p-hw" type="number" min="0" max="100" step="1" value="0" />
</label>
<label class="fs-meta" style="color: var(--c-text-muted)">
<span data-i18n="orders.p.wb">Weizenkleie %</span>
<input id="oh-p-wb" type="number" min="0" max="100" step="1" value="0" />
</label>
</div>
<div id="oh-p-coirgroup" style="display: none; grid-template-columns: 1fr; gap: 8px; margin-top: 8px">
<label class="fs-meta" style="color: var(--c-text-muted)">
<span data-i18n="orders.p.coir">Kokos %</span>
<input id="oh-p-coir" type="number" min="0" max="100" step="1" value="100" />
</label>
</div>
<label style="display: flex; align-items: center; gap: 6px; font-size: 13px; margin-top: 8px">
<input type="checkbox" id="oh-p-gyp" style="width: auto" />
<span data-i18n="orders.p.gypsum">Gips (~1%)</span>
</label>
</div>
<div
id="oh-p-graingroup"
style="display: none; border-top: 1px solid var(--c-border); padding-top: 8px; margin-bottom: 8px"
>
<div class="muted fs-meta" style="margin-bottom: 6px" data-i18n="orders.p.grainTitle">
Körnerbrut pro Stück
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px">
<label class="fs-meta" style="color: var(--c-text-muted)">
<span data-i18n="orders.p.grainKg">Grain kg (nass)</span>
<input id="oh-p-grainkg" type="number" min="0" step="0.01" value="0" />
</label>
<label class="fs-meta" style="color: var(--c-text-muted)">
<span data-i18n="orders.p.grainRh">Grain Hydration %</span>
<input id="oh-p-grainrh" type="number" min="0" max="100" step="1" value="52" />
</label>
</div>
</div>
<div id="oh-p-need" class="muted fs-meta" style="margin: 6px 0"></div>
<div style="display: flex; gap: 6px; margin-top: 10px">
<button class="btn btn-p btn-sm" data-action="oh-prod-save" data-i18n="common.save">Speichern</button>
<button class="btn btn-sm" data-action="oh-prod-cancel" data-i18n="common.cancel">Abbrechen</button>
<button
class="btn btn-sm"
data-action="oh-prod-delete"
id="oh-p-delete"
style="display: none; margin-left: auto; color: var(--c-red-dark)"
data-i18n="common.delete"
>
Löschen
</button>
</div>
</div>
<div style="overflow-x: auto">
<table id="t-catalog" class="t-cards" role="table">
<thead role="rowgroup">
<tr role="row">
<th data-i18n="orders.th.product" role="columnheader">Produkt</th>
<th data-i18n="orders.th.category" role="columnheader">Kategorie</th>
<th data-i18n="orders.th.sku" role="columnheader">SKU</th>
<th role="columnheader"></th>
</tr>
</thead>
<tbody id="orders-catalog-body" role="rowgroup"></tbody>
</table>
</div>
</div>
<div class="card">
<div class="sec" data-i18n="orders.rawStock">Lager — Rohstoffe</div>
<div class="muted fs-meta" style="margin-bottom: 8px" data-i18n="orders.rawStockHint">
Bestände aus dem zentralen Lager. Hier gesetzt wirkt überall (Chargen + Bedarf).
</div>
<div style="overflow-x: auto">
<table id="t-rawstock" class="t-cards" role="table">
<thead role="rowgroup">
<tr role="row">
<th data-i18n="orders.mat.name" role="columnheader">Material</th>
<th data-i18n="orders.rawStockKg" role="columnheader">Bestand (kg)</th>
<th role="columnheader"></th>
</tr>
</thead>
<tbody id="orders-rawstock-body" role="rowgroup"></tbody>
</table>
</div>
</div>
</div>
<!-- Customers -->
<div class="sp" id="sp-orders-customers">
<div class="card">
<div class="sec" data-i18n="orders.customersTitle">Kunden</div>
<div style="overflow-x: auto">
<table id="t-customers" class="t-cards" role="table">
<thead role="rowgroup">
<tr role="row">
<th data-i18n="orders.th.customer" role="columnheader">Kunde</th>
<th data-i18n="orders.th.channels" role="columnheader">Kanäle</th>
<th data-i18n="orders.th.orders" role="columnheader">Bestellungen</th>
<th data-i18n="orders.th.ltv" role="columnheader">Umsatz</th>
<th data-i18n="orders.th.last" role="columnheader">Letzte</th>
<th data-i18n="orders.th.type" role="columnheader">Status</th>
<!-- Admin-only; empty for everyone else. This is where a
marketplace account-closure request gets carried out.
The eBay deletion endpoint records such requests but
never erases on its own — it is unauthenticated by
necessity, so the irreversible part lives behind auth. -->
<th id="orders-customers-privacy-th" hidden data-i18n="orders.th.privacy" role="columnheader">Datenschutz</th>
</tr>
</thead>
<tbody id="orders-customers-body" role="rowgroup"></tbody>
</table>
</div>
</div>
</div>
<!-- Versand: buy carrier labels per order -->
<div class="sp" id="sp-orders-versand">
<div class="card">
<div class="sec" data-i18n="versand.ordersTitle">Versand — Etiketten</div>
<div class="fs-meta"
id="versand-config-warn"
style="display: none; color: var(--c-red-dark); margin-bottom: 8px"
data-i18n="versand.notConfigured"
>
Versand ist noch nicht eingerichtet — Admin → Einstellungen → Versand.
</div>
<div style="overflow-x: auto">
<table>
<thead>
<tr>
<th data-i18n="orders.th.channel">Kanal</th>
<th data-i18n="orders.th.orderId">Bestell-ID</th>
<th data-i18n="orders.th.customer">Kunde</th>
<th data-i18n="orders.th.status">Status</th>
<th></th>
</tr>
</thead>
<tbody id="versand-orders-body"></tbody>
</table>
</div>
</div>
</div>
<!-- Buy-label modal -->
<div
id="ship-modal"
style="
display: none;
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.45);
z-index: 1200;
align-items: center;
justify-content: center;
padding: 12px;
"
onclick="if (event.target === this) shipModalClose();"
>
<div class="card" style="max-width: 480px; width: 100%; margin: 0; max-height: 92vh; overflow: auto">
<div class="sec" data-i18n="versand.modalTitle">Label kaufen</div>
<div id="ship-m-sub" class="muted fs-meta" style="margin: 4px 0 10px"></div>
<input type="hidden" id="ship-m-orderid" />
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 8px">
<label class="fs-meta" style="color: var(--c-text-muted)"
><span data-i18n="versand.f.name">Name</span><input id="ship-m-name"
/></label>
<label class="fs-meta" style="color: var(--c-text-muted)"
><span data-i18n="versand.f.company">Firma</span><input id="ship-m-company"
/></label>
</div>
<div style="display: grid; grid-template-columns: 3fr 1fr; gap: 8px; margin-top: 8px">
<label class="fs-meta" style="color: var(--c-text-muted)"
><span data-i18n="versand.f.street">Straße</span><input id="ship-m-street"
/></label>
<label class="fs-meta" style="color: var(--c-text-muted)"
><span data-i18n="versand.f.house">Nr.</span><input id="ship-m-house"
/></label>
</div>
<label class="fs-meta" style="color: var(--c-text-muted); display: block; margin-top: 8px"
><span data-i18n="versand.f.address2">Zusatz</span><input id="ship-m-address2"
/></label>
<div style="display: grid; grid-template-columns: 1fr 2fr; gap: 8px; margin-top: 8px">
<label class="fs-meta" style="color: var(--c-text-muted)"
><span data-i18n="versand.f.postal">PLZ</span><input id="ship-m-postal"
/></label>
<label class="fs-meta" style="color: var(--c-text-muted)"
><span data-i18n="versand.f.city">Ort</span><input id="ship-m-city"
/></label>
</div>
<div style="display: grid; grid-template-columns: 1fr 2fr; gap: 8px; margin-top: 8px">
<label class="fs-meta" style="color: var(--c-text-muted)"
><span data-i18n="versand.f.country">Land</span
><input
id="ship-m-country"
value="DE"
maxlength="2"
style="text-transform: uppercase"
onchange="shipLoadMethods()"
/></label>
<label class="fs-meta" style="color: var(--c-text-muted)"
><span data-i18n="versand.f.phone">Telefon</span><input id="ship-m-phone"
/></label>
</div>
<div style="display: grid; grid-template-columns: 1fr 2fr; gap: 8px; margin-top: 8px">
<label class="fs-meta" style="color: var(--c-text-muted)"
><span data-i18n="versand.f.weight">Gewicht (g)</span
><input type="number" id="ship-m-weight" min="1" step="10" value="1000" onchange="shipLoadMethods()"
/></label>
<label class="fs-meta" style="color: var(--c-text-muted)"
><span data-i18n="versand.f.method">Versandart</span>
<select id="ship-m-method">
<option value="" data-i18n="versand.loadingMethods">…</option>
</select>
</label>
</div>
<div id="ship-m-result" class="muted fs-meta" style="margin-top: 10px"></div>
<div style="display: flex; gap: 8px; margin-top: 12px">
<button class="btn btn-p" data-action="oh-ship-buy" data-i18n="versand.buyBtn">Label kaufen</button>
<button class="btn" data-action="oh-ship-close" data-i18n="strains.cancel">Abbrechen</button>
</div>
</div>
</div>
</div>
<div class="page" id="p-lab">
<!-- Laborarbeit is an ACTION, not a view: it opens the quick-create
dialog instead of switching panels. Standing in the strip beside
four view-switchers it was a fifth pill that behaved unlike its
neighbours, and the only way to find that out was to press it. On
its own line, wider and taller, the difference is visible first.
It sits above the stock card because that card is reference and
this is the work: on a phone the thing you opened the section to
do should not be below a panel you came to read.
The sp-lab-work panel stays below as that dialog's backing form. -->
<button class="btn btn-p btn-lead" id="btn-lab-work">
<svg
width="17"
height="17"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2.5"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<line x1="12" y1="5" x2="12" y2="19" />
<line x1="5" y1="12" x2="19" y2="12" />
</svg>
<span data-i18n="lab.logWork">Log work</span>
</button>
<!-- What is missing, per species, first; shown only while something is
(renderNachproduzieren). Same figures as the list below. -->
<div class="card" id="np-card" hidden>
<div class="sec" data-i18n="np.title">Nachproduzieren</div>
<div id="np-rows"></div>
<div class="np-foot" data-i18n="np.foot">
Gelb ist unter dem Soll, rot eine Bestellung, die nicht rausgehen kann. Das Soll setzt man je Sorte, mit
einem Tipp auf sie unten.
</div>
</div>
<!-- The lab's stock per species: slants, liquid culture, petri, syringes,
grain spawn. Replaces the tiles per culture type and the sales stock
card (renderLabStock). -->
<div class="card" id="lb-card" hidden>
<div class="sec" data-i18n="lb.title">Laborbestand</div>
<p class="lb-tap" data-i18n="lb.tapHint">Tipp auf eine Sorte: ansetzen, Soll, Im Programm.</p>
<div id="lb-list"></div>
<div class="vb-legend">
<span data-i18n="lb.legendSoll">3/2 = da/Soll</span>
<span data-i18n="lb.legendShop">Punkt = im Shop</span>
</div>
<div class="vb-bb" id="lb-bb" hidden></div>
</div>
<div class="stabs" data-stab-home="nav.lab">
<button class="stab" id="st-lab-grain" data-i18n="lab.grain">Körnerbrut</button>
<button class="stab active" id="st-lab-cultures" data-i18n="lab.cultures">Cultures</button>
<button class="stab" id="st-lab-lineage" data-i18n="lab.lineage">Lineage</button>
<button class="stab" id="st-lab-contam" data-i18n="lab.contam">Kontaminationen</button>
</div>
<!-- Körnerbrut — grain spawn batches. Same columns as the batch
table so both lists share batchRowHtml. -->
<!-- Körnerbrut on the Sortenliste, like Chargen and the Kulturbibliothek
(renderGrainBatches). -->
<div class="sp" id="sp-lab-grain">
<div class="card">
<div class="sl-head">
<div class="sec" data-i18n="lab.grain">Körnerbrut</div>
<span class="sl-count" id="gr-count"></span>
</div>
<div class="sl-tools">
<button type="button" id="gr-scan" class="btn btn-p" data-i18n="gr.scan">Glas scannen</button>
<button type="button" id="gr-archiv" class="btn" aria-pressed="false"></button>
</div>
<p class="lb-tap" data-i18n="gr.help">Tipp auf eine Sorte, dann auf eine Charge: Beutel, Notiz, löschen.</p>
<div id="gr-list"></div>
</div>
</div>
<!-- Cultures -->
<div class="sp active" id="sp-lab-cultures">
<!-- Kulturbibliothek on the Sortenliste: one row per Sorte with what is
in stock; a tap opens its cultures by type, oldest first; a tap
on a culture opens the same sheet a scan does (renderCultures). -->
<div class="card">
<div class="sl-head">
<div class="sec" data-i18n="lab.cultureLibrary">Kulturbibliothek</div>
<span class="sl-count" id="kb-count"></span>
</div>
<div class="sl-tools">
<button type="button" id="kb-scan" class="btn btn-p" data-i18n="kb.scan">Kultur scannen</button>
<!-- Lab stocktake: scan the shelf, mark what's gone as used up -->
<button type="button" id="lab-check-btn" class="btn" data-i18n="zoneCheck.btn">Prüfen</button>
<button type="button" id="kb-archiv" class="btn" aria-pressed="false"></button>
</div>
<p class="lb-tap" id="kb-help" data-i18n="kb.help">
Tipp auf eine Sorte, dann auf eine Kultur: verkaufen, verbrauchen, drucken, löschen.
</p>
<div id="kb-list"></div>
</div>
</div>
<!-- Log work -->
<div class="sp" id="sp-lab-work" data-stab-lead="lab.logWork">
<div class="g2">
<div class="card">
<div class="sec" data-i18n="lab.logCleanRoom">Log clean room work</div>
<div style="display: flex; flex-direction: column; gap: 10px; margin-top: 4px">
<div class="g2">
<div>
<label data-i18n="lab.workType">Work type</label>
<select id="lw-type">
<option value="MC" data-i18n="lab.registerMC">Register mother culture</option>
<option value="PD" data-i18n="lab.pdTransfer">Petri dish transfer</option>
<option value="LC" data-i18n="lab.lcPrep">Liquid culture prep</option>
<option value="SY" data-i18n="lab.syPrep">Fill liquid syringes</option>
<option value="KB" data-i18n="lab.kb">Körnerbrut</option>
<option value="DU" data-i18n="lab.duPrep">Impfdübel beimpfen</option>
</select>
</div>
<div>
<label data-i18n="strains.pilzsorte">Pilzsorte</label>
<select id="lw-st" required>
<option value="" data-i18n="strains.selectPlaceholder">— Pilzsorte wählen —</option>
</select>
</div>
</div>
<div id="lw-strain-text-row" style="display: none">
<label data-i18n="batch.strainLabel">Strain</label>
<input
type="text"
id="lw-strain-text"
data-i18n-placeholder="batch.strainPlaceholder"
placeholder="z.B. XY-1, Nordlicht, Beispiel-Genetik…"
autocomplete="off"
/>
<!-- Angesetzt am, wie nb-created: leer = heute. logLabWork()
und createGrainBatch() lesen und leeren es. -->
<input type="hidden" id="lw-created" value="" />
</div>
<div id="lw-parent-row">
<label id="lw-parent-lbl" data-i18n="lab.parentCulture">Parent culture</label>
<select id="lw-parent">
<option value="" data-i18n="lab.noneNewIsolation">— none / new isolation —</option>
</select>
</div>
<div id="lw-source-row" style="display: none">
<label data-i18n="lab.source">Source (e.g. clone, spore print, wild)</label>
<input
type="text"
id="lw-source"
data-i18n-placeholder="lab.sourcePlaceholder"
placeholder="e.g. Clone from KINGS-250101-01-03"
/>
</div>
<div id="lw-ml-row" style="display: none">
<label for="lw-ml" data-i18n="sy.size">Größe</label>
<select id="lw-ml">
<option value="10">10 ml</option>
<option value="20">20 ml</option>
<option value="50">50 ml</option>
</select>
</div>
<div id="lw-qty-row">
<label id="lw-qty-lbl" data-i18n="lab.qtyMade">Quantity made</label
><input type="text" inputmode="numeric" enterkeyhint="next" id="lw-qty" value="1" />
</div>
<div>
<label data-i18n="batch.notes">Notes (optional)</label
><textarea
id="lw-notes"
rows="2"
data-i18n-placeholder="lab.observations"
placeholder="Observations, conditions..."
></textarea>
</div>
<div
id="lw-prev-box"
style="display: none; background: var(--c-bg); border-radius: 8px; padding: 8px 10px"
>
<div class="fs-micro"
style="color: var(--c-text-muted); margin-bottom: 3px"
data-i18n="lab.idsCreated"
>
IDs that will be created
</div>
<div class="fs-meta" id="lw-prev" style="font-family: monospace; line-height: 1.8"></div>
</div>
<!-- KB (Körnerbrut) fields — shown only when work type = KB -->
<div id="lw-kb-rows" style="display: none; flex-direction: column; gap: 10px">
<div id="gs-weight-lines">
<div
class="gs-wline"
style="display: flex; gap: 8px; align-items: flex-end; flex-wrap: wrap; margin-bottom: 6px"
>
<div style="flex: 1; min-width: 120px">
<label data-i18n="batch.bagWeight">Beutelgewicht</label>
<div style="display: flex; gap: 4px; flex-wrap: wrap; margin-top: 2px">
<button type="button" class="btn btn-sm gs-wbtn" data-kg="0.7">0.7</button>
<button type="button" class="btn btn-sm btn-p gs-wbtn" data-kg="1">1</button>
<button type="button" class="btn btn-sm gs-wbtn" data-kg="2">2</button>
<button type="button" class="btn btn-sm gs-wbtn" data-kg="5">5</button>
</div>
<div style="display: flex; align-items: center; gap: 4px; margin-top: 4px">
<input
type="text"
inputmode="decimal"
class="gs-line-kg"
value="1"
style="width: 70px"
autocomplete="off"
/>
<span class="fs-meta" style="color: var(--c-text-muted)">kg</span>
</div>
</div>
<div style="min-width: 80px">
<label data-i18n="batch.qty">Menge (Beutel)</label>
<input type="number" class="gs-line-qty" value="10" min="1" max="500" />
</div>
<button
type="button"
class="btn btn-sm gs-line-rm"
style="display: none; color: var(--c-red)"
title="Remove"
>
×
</button>
</div>
</div>
<button type="button" id="gs-add-line" class="btn btn-sm" style="margin-bottom: 4px">
+ Gewicht
</button>
<div class="g2">
<div>
<label data-i18n="batch.incDays">Inkubationstage</label
><input type="number" id="gs-days" value="14" min="1" max="90" />
</div>
<div>
<label data-i18n="batch.grainRh">Wassergehalt (%)</label
><input type="number" id="gs-rh" value="52" min="0" max="90" step="1" />
<div class="fs-xs"
style="color: var(--c-text-muted); margin-top: 3px"
data-i18n="batch.grainRhHint"
>
Wasseranteil nach Hydratation — nur Trockenmasse wird vom Lagerbestand abgezogen
</div>
</div>
</div>
<div>
<label data-i18n="batch.sourceCulture">Quellkultur (optional)</label
><select id="gs-culture">
<option value="" data-i18n="batch.none">— keine —</option>
</select>
<div class="fs-xs"
style="color: var(--c-text-muted); margin-top: 3px"
data-i18n="batch.sourceCultureHint"
>
Link to the PD or LC used to inoculate this batch
</div>
</div>
<div class="met">
<div class="met-l" data-i18n="lab.grainIdPreview">Körnerbrut-ID Vorschau</div>
<div
id="gs-prev"
style="font-family: monospace; font-size: 13px; font-weight: 600; margin-top: 3px"
>
—
</div>
</div>
<div class="fs-meta"
id="gs-mat-preview"
style="display: none; background: var(--c-primary-light); border: 1px solid var(--c-green-border); border-radius: 8px; padding: 10px 12px; color: var(--c-green-dark); line-height: 1.8"
></div>
</div>
<!-- No result card here any more: every path that makes bags
ends in m-batch-print and every path that makes cultures
in m-culture-print, wherever the person is standing. -->
<button id="btn-26" class="btn btn-p" data-i18n="lab.logWorkBtn">Log work</button>
</div>
</div>
<div class="card">
<div class="sec" data-i18n="lab.recentEntries">Recent lab entries</div>
<div style="overflow-x: auto">
<table>
<thead>
<tr>
<th data-i18n="th.date">Date</th>
<th data-i18n="th.type">Type</th>
<th>ID</th>
<th data-i18n="th.parent">Parent</th>
<th data-i18n="th.species">Species</th>
</tr>
</thead>
<tbody id="lab-log-body"></tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Lineage -->
<div class="sp" id="sp-lab-lineage">
<div class="card">
<div class="sec-head">
<div class="sec" data-i18n="lab.geneticLineage">Genetic lineage</div>
<select class="head-select" id="lineage-sel" style="min-width: min(220px, 100%)">
<option value="" data-i18n="lab.selectCultureBatch">— select a culture or batch —</option>
</select>
</div>
<div id="lineage-body">
<div class="empty" data-i18n="lab.selectAbove">
Select a culture or batch above to trace its lineage.
</div>
</div>
</div>
</div>
<!-- Contamination reports list -->
<div class="sp" id="sp-lab-contam">
<div class="card">
<div
style="
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
flex-wrap: wrap;
gap: 8px;
"
>
<div class="sec" style="margin: 0" data-i18n="contam.listTitle">Kontaminationsberichte</div>
<div style="display: flex; gap: 6px; flex-wrap: wrap">
<select id="cl-type-filter" data-i18n-aria-label="contam.type" aria-label="Type filter">
<option value="" data-i18n="contam.allTypes">Alle Typen</option>
</select>
<select id="cl-sev-filter" data-i18n-aria-label="contam.severity" aria-label="Severity filter">
<option value="" data-i18n="contam.allSev">Alle Schwere</option>
<option value="minor" data-i18n="contam.sevMinor">Klein</option>
<option value="major" data-i18n="contam.sevMajor">Größer</option>
<option value="lost" data-i18n="contam.sevLost">Total</option>
</select>
<select id="cl-status-filter" data-i18n-aria-label="contam.statusLabel" aria-label="Status filter">
<option value="" data-i18n="contam.statusAll">Alle Status</option>
<option value="open" data-i18n="contam.statusOpen">Offen</option>
<option value="resolved" data-i18n="contam.statusResolved">Aufgelöst</option>
</select>
<input type="date" id="cl-since-filter" data-i18n-aria-label="contam.since" aria-label="Since" />
<button id="cl-reset" type="button" class="btn btn-sm" data-i18n="contam.resetFilters">
Zurücksetzen
</button>
</div>
</div>
<div class="fs-xs" id="contam-list-count" style="color: var(--c-text-muted); margin-bottom: 8px"></div>
<div id="contam-list"></div>
</div>
</div>
</div>
<!-- ══ PRINT ══ -->
<div class="page" id="p-print">
<!-- Printer status — refreshed when this page is opened and after each print attempt -->
<div
id="printer-status-banner"
class="card"
style="
margin-bottom: 10px;
cursor: pointer;
transition:
background-color 0.2s,
border-color 0.2s;
"
title="Click to re-check"
>
<div id="printer-status-text" style="font-size: 13px; line-height: 1.5" data-i18n="print.status.checking">
Checking printer status…
</div>
<div class="fs-xs" id="printer-status-detail" style="opacity: 0.8; margin-top: 4px"></div>
</div>
<div class="stabs" data-stab-home="nav.print">
<button class="stab active" id="st-print-bags" data-i18n="print.bagLabels">Bag labels</button>
<button class="stab" id="st-print-lab" data-i18n="print.labLabels">Lab labels</button>
<button class="stab" id="st-print-ref" data-i18n="print.refBarcodes">Reference barcodes</button>
</div>
<!-- Bag labels -->
<div class="sp active" id="sp-print-bags">
<div class="card">
<div class="g2" style="margin-bottom: 12px">
<div>
<label data-i18n="print.selectBatch">Select batch</label>
<input class="fs-meta"
type="search"
enterkeyhint="search"
id="print-batch-search"
data-i18n-placeholder="print.searchBatch"
placeholder="Search batches…"
autocomplete="off"
style="margin-bottom: 4px"
/>
<select id="print-batch">
<option value="" data-i18n="print.chooseBatch">— choose batch —</option>
</select>
</div>
<div>
<label data-i18n="print.labelContent">Label content</label>
<select id="print-mode">
<option value="minimal" data-i18n="print.barcodeOnly">Nur Barcode (Barcode + ID)</option>
<option value="sorte" selected data-i18n="print.barcodeStrain">
Barcode + Sorte (+ Pilzsorte & Notizen)
</option>
<option value="full" data-i18n="print.barcodeFull">Vollständig (+ Fälligkeit)</option>
</select>
<label
style="
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: var(--c-text);
cursor: pointer;
margin: 6px 0 0;
"
><input type="checkbox" id="bag-qr" style="width: auto; margin: 0" />
<span data-i18n="print.qrCode">QR code</span></label
>
</div>
</div>
<div style="margin-bottom: 10px">
<label data-i18n="print.printRange">Print range</label>
<div style="display: flex; gap: 8px; align-items: center; flex-wrap: wrap">
<select id="print-range" style="min-width: 140px">
<option value="all" data-i18n="print.allBags">All bags</option>
<option value="range" data-i18n="print.bagRange">Bag range (from–to)</option>
</select>
<span id="bag-range-inputs" style="display: none; gap: 6px; align-items: center">
<input
type="number"
id="bag-from"
min="1"
style="width: 60px"
data-i18n-placeholder="print.bagFromPh"
placeholder="from"
/>
<span>–</span>
<input
type="number"
id="bag-to"
min="1"
style="width: 60px"
data-i18n-placeholder="print.bagToPh"
placeholder="to"
/>
</span>
</div>
</div>
<div style="display: flex; gap: 8px; flex-wrap: wrap">
<button id="prt-27" class="btn btn-p" data-i18n="print.printDirect">Print labels (direct)</button>
</div>
<p class="fs-xs" style="color: var(--c-text-muted); margin-top: 8px">
50×30mm · Code 128 / QR · GK420d 203dpi
</p>
</div>
<div class="card">
<div class="sec" data-i18n="print.preview">Preview</div>
<div id="bag-preview" style="margin-top: 8px"></div>
</div>
</div>
<!-- Lab labels -->
<div class="sp" id="sp-print-lab">
<div class="card">
<div class="g2" style="margin-bottom: 12px">
<div>
<label data-i18n="print.filterCultures">Filter cultures</label>
<select id="lab-filter" style="margin-bottom: 6px">
<option value="all" data-i18n="print.allActiveCultures">All active cultures</option>
<option value="MC" data-i18n="lab.motherCultures">Mother cultures</option>
<option value="PD" data-i18n="lab.petriDishes">Petri dishes</option>
<option value="LC" data-i18n="lab.liquidCultures">Liquid cultures</option>
<option value="today" data-i18n="print.createdToday">Created today</option>
</select>
<div
id="lab-list"
style="
max-height: 180px;
overflow-y: auto;
border: 1px solid var(--c-border);
border-radius: 7px;
padding: 6px;
"
></div>
</div>
<div>
<label data-i18n="print.labelContent">Label content</label>
<select id="lab-mode">
<option value="minimal" data-i18n="print.barcodeOnly">Nur Barcode (Barcode + ID)</option>
<option value="sorte" selected data-i18n="print.barcodeStrain">
Barcode + Sorte (+ Pilzsorte & Herkunft)
</option>
<option value="full" data-i18n="print.barcodeFull">Vollständig (+ Datum)</option>
</select>
<label
style="
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: var(--c-text);
cursor: pointer;
margin: 8px 0 0;
"
><input type="checkbox" id="lab-qr" style="width: auto; margin: 0" />
<span data-i18n="print.qrCode">QR code</span></label
>
<div style="margin-top: 12px; display: flex; gap: 8px">
<button id="prt-28" class="btn btn-p" data-i18n="print.printLabDirect">
Print labels (direct)
</button>
</div>
<p class="fs-xs" style="color: var(--c-text-muted); margin-top: 8px">
50×30mm · Code 128 / QR · 203dpi
</p>
</div>
</div>
</div>
<div class="card">
<div class="sec" data-i18n="print.preview">Preview</div>
<div id="lab-preview" style="margin-top: 8px"></div>
</div>
</div>
<!-- Reference barcodes -->
<div class="sp" id="sp-print-ref">
<div class="card">
<div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px">
<div class="sec" style="margin: 0" data-i18n="print.refBarcodesTitle">
Reference barcodes — print & hang on wall
</div>
<div style="display: flex; gap: 8px; align-items: center">
<label class="fs-meta"
style="display: flex; align-items: center; gap: 5px; cursor: pointer; color: var(--c-text); margin: 0"
>
<input type="checkbox" id="ref-qr" style="width: auto; margin: 0" />
<span data-i18n="print.qrCodes">QR codes</span>
</label>
<button id="prt-29" class="btn btn-sm btn-p" data-i18n="print.printSheet">Print sheet</button>
</div>
</div>
<p class="fs-meta" style="color: var(--c-text-sec)" data-i18n="print.scanInfo">
Scanning works on every tab — just scan from this sheet at any time.
</p>
</div>
<div id="ref-grid"></div>
</div>
</div>
<!-- ══ CALENDAR ══ -->
<div class="page" id="p-cal">
<div class="cal-toolbar">
<div class="cal-toolbar-left">
<button id="btn-33" class="cal-today-btn" data-i18n="cal.today">Today</button>
<button id="btn-34" class="cal-nav-btn">‹</button>
<button id="btn-35" class="cal-nav-btn">›</button>
<span class="cal-title" id="cal-title"></span>
</div>
<div class="cal-toolbar-right">
<select class="fs-meta"
id="cal-filter-user"
style="padding: 4px 8px; border-radius: 6px; border: 1px solid var(--c-border)"
>
<option value="" data-i18n="calEntry.assignTo.all">Alle (Betrieb)</option>
</select>
<div class="cal-view-toggle">
<button class="cal-vbtn active" id="cv-month" data-i18n="cal.monthShort">Monat</button>
<button class="cal-vbtn" id="cv-week" data-i18n="cal.weekShort">Woche</button>
<button class="cal-vbtn" id="cv-day" data-i18n="cal.dayShort">Tag</button>
</div>
<button
id="btn-cal-print"
class="cal-print-btn"
data-i18n-title="cal.printTitle"
title="Kalender drucken"
>
<svg
width="18"
height="18"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="6 9 6 2 18 2 18 9" />
<path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2" />
<rect x="6" y="14" width="12" height="8" />
</svg>
</button>
<button id="btn-cal-add" class="cal-add-btn" data-i18n="cal.newEntry">+ Neu</button>
</div>
</div>
<div class="cal-legend">
<span class="leg-batch" data-i18n="cal.legend.batches">Due dates</span>
<span class="leg-task" data-i18n="cal.legend.tasks">Tasks</span>
<span class="leg-harvest" data-i18n="cal.legend.harvests">Harvests</span>
<span class="leg-custom" data-i18n="cal.legend.custom">Custom events</span>
<span class="leg-meeting" data-i18n="cal.legend.meetings">Meetings</span>
<span class="leg-delivery" data-i18n="cal.legend.deliveries">Deliveries</span>
<span class="leg-maintenance" data-i18n="cal.legend.maintenance">Maintenance</span>
<span class="leg-import" data-i18n="cal.legend.external">External events</span>
</div>
<div id="cal-container">
<div class="cal-grid" id="cal-grid"></div>
</div>
</div>
<!-- ══ MORE ══ -->
<div class="page" id="p-settings">
<div class="stabs" data-stab-home="nav.admin">
<button class="stab active" id="st-settings-server" style="display: none" data-i18n="settings.tabServer">
Server
</button>
<button class="stab" id="st-settings-backup" data-i18n="settings.backup">Backup</button>
<button class="stab" id="st-settings-caldav" data-i18n="settings.tabCaldav">CalDAV</button>
<button class="stab" id="st-settings-duckdns" data-i18n="settings.tabDuckdns">DuckDNS</button>
<button class="stab" id="st-settings-mcp" style="display: none" data-i18n="settings.tabMcp">MCP</button>
<button class="stab" id="st-settings-log" data-i18n="settings.scanLog">Scan log</button>
<button class="stab" id="st-settings-users" data-i18n="settings.tabUsers">Users</button>
<button class="stab" id="st-settings-pickuplocations" data-i18n="settings.tabPickupLocations">
Abholorte
</button>
<button class="stab" id="st-settings-channels" data-i18n="settings.tabChannels">Kanäle</button>
<button class="stab" id="st-settings-versand" data-i18n="settings.tabVersand">Versand</button>
<button class="stab" id="st-settings-harvestfeed" data-i18n="settings.tabHarvestFeed">Harvest feed</button>
<button class="stab" id="st-settings-printer" data-i18n="settings.tabPrinter">Drucker</button>
<button class="stab" id="st-settings-camera" style="display: none" data-i18n="settings.tabCamera">
Camera
</button>
</div>
<!-- Versand (Sendcloud shipping) config -->
<div class="sp" id="sp-settings-versand">
<div class="card">
<div class="sec" data-i18n="versand.title">Versand (Sendcloud)</div>
<p class="fs-meta"
style="color: var(--c-text-muted); margin-bottom: 12px; line-height: 1.6"
data-i18n="versand.desc"
>
Versandetiketten direkt aus dem Order-Hub kaufen. API-Keys: Sendcloud → Einstellungen → Integrationen →
Sendcloud API.
</p>
<div style="display: flex; flex-direction: column; gap: 10px; max-width: 440px">
<label style="display: flex; align-items: center; gap: 6px; cursor: pointer; margin: 0">
<input type="checkbox" id="versand-enabled" style="width: auto" />
<span data-i18n="versand.enable">Versand aktivieren</span>
</label>
<div>
<label class="fs-meta"
style="font-weight: 600; color: var(--c-text-sec); display: block; margin-bottom: 4px"
data-i18n="versand.publicKey"
>Public Key</label
>
<input
type="text"
id="versand-public"
placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
style="width: 100%"
/>
</div>
<div>
<label class="fs-meta"
style="font-weight: 600; color: var(--c-text-sec); display: block; margin-bottom: 4px"
data-i18n="versand.secretKey"
>Secret Key</label
>
<input
type="password"
id="versand-secret"
data-i18n-placeholder="versand.secretPlaceholder"
placeholder="(leer lassen = unverändert)"
style="width: 100%"
/>
</div>
<div style="display: flex; gap: 12px; flex-wrap: wrap">
<div>
<label class="fs-meta"
style="font-weight: 600; color: var(--c-text-sec); display: block; margin-bottom: 4px"
data-i18n="versand.mode"
>Modus</label
>
<select id="versand-mode">
<option value="test" data-i18n="versand.modeTest">Test</option>
<option value="live" data-i18n="versand.modeLive">Live</option>
</select>
</div>
<div>
<label class="fs-meta"
style="font-weight: 600; color: var(--c-text-sec); display: block; margin-bottom: 4px"
data-i18n="versand.defWeight"
>Standardgewicht (g)</label
>
<input type="number" id="versand-weight" min="1" step="10" value="1000" style="width: 120px" />
</div>
</div>
<div style="display: flex; gap: 6px; flex-wrap: wrap; margin-top: 4px">
<button id="versand-save-btn" class="btn btn-sm btn-p" data-i18n="common.save">Speichern</button>
<button id="versand-test-btn" class="btn btn-sm" data-i18n="versand.test">Verbindung testen</button>
</div>
<div class="fs-meta"
id="versand-test-result"
style="color: var(--c-text-muted); padding: 6px 10px; background: var(--c-bg); border-radius: 7px; display: none"
></div>
</div>
</div>
</div>
<!-- Sales channels (live order sync) -->
<div class="sp" id="sp-settings-channels">
<div class="card">
<div class="sec" data-i18n="channels.billbeeTitle">Billbee</div>
<p class="fs-meta"
style="color: var(--c-text-muted); margin-bottom: 10px; line-height: 1.6"
data-i18n="channels.billbeeDesc"
>
Billbee ist kein Shop, sondern die Zentrale: von dort kommen die Bestellungen aller Kanäle auf einmal,
damit die Produktionsplanung weiß, was gebraucht wird. Meistertracker liest nur, Bestand, Rechnungen
und Versand bleiben in Billbee. API-Key bei Billbee anfragen, im Billbee-Konto die API einschalten und
dort ein API-Passwort anlegen. Sobald Billbee an ist, pausieren die Direktverbindungen zu Wix, Etsy und
eBay von selbst: sonst käme jede Bestellung doppelt an, einmal über Billbee und einmal direkt.
</p>
<div style="display: flex; flex-direction: column; gap: 10px; max-width: 440px">
<label style="display: flex; align-items: center; gap: 6px; cursor: pointer; margin: 0">
<input type="checkbox" id="billbee-enabled" style="width: auto" />
<span data-i18n="channels.enable">Aktivieren</span>
</label>
<div>
<label class="fs-meta"
style="font-weight: 600; color: var(--c-text-sec); display: block; margin-bottom: 4px"
data-i18n="channels.apiKey"
>API-Key</label
>
<input
type="password"
id="billbee-apikey"
data-i18n-placeholder="channels.secretPlaceholder"
placeholder="(leer lassen = unverändert)"
style="width: 100%"
/>
</div>
<div>
<label class="fs-meta"
style="font-weight: 600; color: var(--c-text-sec); display: block; margin-bottom: 4px"
data-i18n="channels.billbeeUser"
>Billbee-Benutzer (E-Mail)</label
>
<input type="text" id="billbee-user" style="width: 100%" />
</div>
<div>
<label class="fs-meta"
style="font-weight: 600; color: var(--c-text-sec); display: block; margin-bottom: 4px"
data-i18n="channels.billbeeApiPassword"
>API-Passwort</label
>
<input
type="password"
id="billbee-apipw"
data-i18n-placeholder="channels.secretPlaceholder"
placeholder="(leer lassen = unverändert)"
style="width: 100%"
/>
</div>
<div style="display: flex; gap: 6px; flex-wrap: wrap; margin-top: 4px">
<button id="billbee-save-btn" class="btn btn-sm btn-p" data-i18n="common.save">Speichern</button>
<button id="billbee-test-btn" class="btn btn-sm" data-i18n="channels.test">Verbindung testen</button>
<button id="billbee-sync-btn" class="btn btn-sm" data-i18n="channels.syncNow">
Jetzt synchronisieren
</button>
</div>
<div class="fs-meta"
id="billbee-status"
style="color: var(--c-text-muted); padding: 6px 10px; background: var(--c-bg); border-radius: 7px; display: none"
></div>
</div>
</div>
<!-- Syringe, petri dish and grain spawn stock → Billbee → shops (syringe-stock.js) -->
<div class="card" id="syr-card">
<div class="sec" data-i18n="syr.title">Laborbestand an Billbee</div>
<p class="fs-meta"
style="color: var(--c-text-muted); margin-bottom: 10px; line-height: 1.6"
data-i18n="syr.desc"
>
Meistertracker zählt die Flüssigkultur-Spritzen je Art und Größe, die Petrischalen je
Art (aktiv und gelagert) und die bewachsene Körnerbrut je Art und Beutelgröße, und
meldet die Zahl an Billbee, sobald sie sich ändert. Körnerbrut zählt, sobald die Charge
fällig ist und solange der Beutel im Regal steht; was noch wächst, zählt erst ab dem
Fälligkeitstag. Billbee zieht offene Bestellungen ab und gibt den Bestand an die Shops
weiter, bei denen dort der Bestandsabgleich an ist. Verschickte Spritzen und
Petrischalen als verkauft markieren, verschickte Beutel rausscannen, sonst bietet der
Shop sie nach dem Versand wieder an. Jede Größe ist im Shop ein eigener Artikel und hat
ihre eigene SKU. Gemeldet wird nur, was eine SKU hat.
</p>
<label style="display: flex; align-items: center; gap: 6px; cursor: pointer; margin: 0 0 10px">
<input type="checkbox" id="syr-enabled" style="width: auto" />
<span data-i18n="syr.enable">Spritzen, Petrischalen und Körnerbrut an Billbee melden</span>
</label>
<div id="syr-rows" style="display: flex; flex-direction: column; gap: 8px; max-width: 520px"></div>
<div style="display: flex; gap: 6px; flex-wrap: wrap; margin-top: 10px">
<button id="syr-push-btn" class="btn btn-sm" data-i18n="syr.push">Jetzt an Billbee melden</button>
</div>
<div class="fs-meta"
id="syr-status"
style="color: var(--c-text-muted); padding: 6px 10px; background: var(--c-bg); border-radius: 7px; margin-top: 8px; display: none"
></div>
</div>
<div class="card">
<div class="sec" data-i18n="channels.wixTitle">Wix</div>
<p class="fs-meta"
style="color: var(--c-text-muted); margin-bottom: 10px; line-height: 1.6"
data-i18n="channels.wixDesc"
>
Bestellungen automatisch aus Wix holen. API-Key: Wix-Dashboard → Einstellungen → API-Keys (mit
Stores/Orders-Leserecht). Site-ID steht in der Dashboard-URL.
</p>
<div style="display: flex; flex-direction: column; gap: 10px; max-width: 440px">
<label style="display: flex; align-items: center; gap: 6px; cursor: pointer; margin: 0">
<input type="checkbox" id="wix-enabled" style="width: auto" />
<span data-i18n="channels.enable">Aktivieren</span>
</label>
<div>
<label class="fs-meta"
style="font-weight: 600; color: var(--c-text-sec); display: block; margin-bottom: 4px"
data-i18n="channels.apiKey"
>API-Key</label
>
<input
type="password"
id="wix-apikey"
data-i18n-placeholder="channels.secretPlaceholder"
placeholder="(leer lassen = unverändert)"
style="width: 100%"
/>
</div>
<div>
<label class="fs-meta"
style="font-weight: 600; color: var(--c-text-sec); display: block; margin-bottom: 4px"
data-i18n="channels.siteId"
>Site-ID</label
>
<input type="text" id="wix-siteid" style="width: 100%" />
</div>
<div>
<label class="fs-meta"
style="font-weight: 600; color: var(--c-text-sec); display: block; margin-bottom: 4px"
data-i18n="channels.accountId"
>Account-ID (falls erforderlich)</label
>
<input type="text" id="wix-accountid" style="width: 100%" />
</div>
<div style="display: flex; gap: 6px; flex-wrap: wrap; margin-top: 4px">
<button id="wix-save-btn" class="btn btn-sm btn-p" data-i18n="common.save">Speichern</button>
<button id="wix-test-btn" class="btn btn-sm" data-i18n="channels.test">Verbindung testen</button>
<button id="wix-sync-btn" class="btn btn-sm" data-i18n="channels.syncNow">
Jetzt synchronisieren
</button>
</div>
<div class="fs-meta"
id="wix-status"
style="color: var(--c-text-muted); padding: 6px 10px; background: var(--c-bg); border-radius: 7px; display: none"
></div>
</div>
</div>
<div class="card">
<div class="sec" data-i18n="channels.ebayTitle">eBay</div>
<p class="fs-meta"
style="color: var(--c-text-muted); margin-bottom: 10px; line-height: 1.6"
data-i18n="channels.ebayDesc"
>
Direkte eBay-Anbindung (Sell Fulfillment). App-ID, Cert-ID und RuName aus dem eBay-Developer-Portal
eintragen, speichern, dann „Verbinden". Der Login schließt nur auf der öffentlichen Instanz
(meistertracker.com) ab.
</p>
<div style="display: flex; flex-direction: column; gap: 10px; max-width: 440px">
<label style="display: flex; align-items: center; gap: 6px; cursor: pointer; margin: 0">
<input type="checkbox" id="ebay-enabled" style="width: auto" />
<span data-i18n="channels.enable">Aktivieren</span>
</label>
<div>
<label class="fs-meta"
style="font-weight: 600; color: var(--c-text-sec); display: block; margin-bottom: 4px"
data-i18n="channels.appId"
>App-ID (Client-ID)</label
>
<input type="text" id="ebay-clientid" style="width: 100%" />
</div>
<div>
<label class="fs-meta"
style="font-weight: 600; color: var(--c-text-sec); display: block; margin-bottom: 4px"
data-i18n="channels.certId"
>Cert-ID (Client-Secret)</label
>
<input
type="password"
id="ebay-secret"
data-i18n-placeholder="channels.secretPlaceholder"
placeholder="(leer lassen = unverändert)"
style="width: 100%"
/>
</div>
<div>
<label class="fs-meta"
style="font-weight: 600; color: var(--c-text-sec); display: block; margin-bottom: 4px"
data-i18n="channels.ruName"
>RuName (Redirect-URL-Name)</label
>
<input type="text" id="ebay-siteid" style="width: 100%" />
</div>
<div>
<label class="fs-meta"
style="font-weight: 600; color: var(--c-text-sec); display: block; margin-bottom: 4px"
data-i18n="channels.ebayVerifyToken"
>Verification-Token (Kontolöschung)</label
>
<input
type="password"
id="ebay-verifytoken"
data-i18n-placeholder="channels.secretPlaceholder"
placeholder="(leer lassen = unverändert)"
style="width: 100%"
/>
<p class="fs-xs"
style="color: var(--c-text-muted); margin: 4px 0 0; line-height: 1.5"
data-i18n="channels.ebayVerifyHint"
>
Selbst ausgedachtes Geheimnis, 32–80 Zeichen (A–Z, a–z, 0–9, _ und -). Muss im eBay-Developer-Portal
unter „Marketplace account deletion" identisch eingetragen werden — zusammen mit der Endpunkt-URL
unten.
</p>
<code class="fs-xs"
id="ebay-deletion-url"
style="display: block; margin-top: 6px; padding: 5px 8px; background: var(--c-bg); border-radius: 6px; word-break: break-all"
></code>
</div>
<div style="display: flex; gap: 6px; flex-wrap: wrap; margin-top: 4px">
<button id="ebay-save-btn" class="btn btn-sm" data-i18n="common.save">Speichern</button>
<button id="ebay-connect-btn" class="btn btn-sm btn-p" data-i18n="channels.connect">Verbinden</button>
<button id="ebay-test-btn" class="btn btn-sm" data-i18n="channels.test">Verbindung testen</button>
<button id="ebay-sync-btn" class="btn btn-sm" data-i18n="channels.syncNow">
Jetzt synchronisieren
</button>
</div>
<div class="fs-meta"
id="ebay-status"
style="color: var(--c-text-muted); padding: 6px 10px; background: var(--c-bg); border-radius: 7px; display: none"
></div>
</div>
</div>
<div class="card">
<div class="sec" data-i18n="channels.etsyTitle">Etsy</div>
<p class="fs-meta"
style="color: var(--c-text-muted); margin-bottom: 10px; line-height: 1.6"
data-i18n="channels.etsyDesc"
>
Direkte Etsy-Anbindung (Open API v3). Keystring (App-API-Key) eintragen, speichern, dann „Verbinden".
Der Login schließt nur auf der öffentlichen Instanz (meistertracker.com) ab.
</p>
<div style="display: flex; flex-direction: column; gap: 10px; max-width: 440px">
<label style="display: flex; align-items: center; gap: 6px; cursor: pointer; margin: 0">
<input type="checkbox" id="etsy-enabled" style="width: auto" />
<span data-i18n="channels.enable">Aktivieren</span>
</label>
<div>
<label class="fs-meta"
style="font-weight: 600; color: var(--c-text-sec); display: block; margin-bottom: 4px"
data-i18n="channels.keystring"
>Keystring (API-Key)</label
>
<input type="text" id="etsy-clientid" style="width: 100%" />
</div>
<div style="display: flex; gap: 6px; flex-wrap: wrap; margin-top: 4px">
<button id="etsy-save-btn" class="btn btn-sm" data-i18n="common.save">Speichern</button>
<button id="etsy-connect-btn" class="btn btn-sm btn-p" data-i18n="channels.connect">Verbinden</button>
<button id="etsy-test-btn" class="btn btn-sm" data-i18n="channels.test">Verbindung testen</button>
<button id="etsy-sync-btn" class="btn btn-sm" data-i18n="channels.syncNow">
Jetzt synchronisieren
</button>
</div>
<div class="fs-meta"
id="etsy-status"
style="color: var(--c-text-muted); padding: 6px 10px; background: var(--c-bg); border-radius: 7px; display: none"
></div>
</div>
</div>
</div>
<!-- Scan log -->
<div class="sp" id="sp-settings-log">
<div class="card">
<div
style="
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 8px;
flex-wrap: wrap;
gap: 6px;
"
>
<div class="sec" style="margin: 0" data-i18n="settings.scanLog">Scan log</div>
<div style="display: flex; gap: 6px; align-items: center; flex-wrap: wrap">
<select class="fs-meta" id="log-action-filter" style="padding: 4px 6px">
<option value="" data-i18n="log.allActions">Alle Aktionen</option>
<option value="ADD">ADD</option>
<option value="MOVE">MOVE</option>
<option value="REMOVE">REMOVE</option>
<option value="HARVEST">HARVEST</option>
</select>
<input class="fs-meta"
type="date"
id="log-date-from"
style="padding: 4px 6px"
data-i18n-title="log.dateFromTitle"
title="Von Datum"
/>
<input class="fs-meta"
type="date"
id="log-date-to"
style="padding: 4px 6px"
data-i18n-title="log.dateToTitle"
title="Bis Datum"
/>
<input class="fs-meta"
type="text"
id="log-q"
data-i18n-aria-label="aria.searchLog"
aria-label="Search log"
data-i18n-placeholder="dash.search"
placeholder="Search..."
style="width: 130px; padding: 4px 8px"
/>
<button id="btn-37" class="btn btn-sm btn-r" data-i18n="settings.clear">Clear</button>
</div>
</div>
<div class="fs-xs" id="log-count" style="color: var(--c-text-muted); margin-bottom: 4px"></div>
<div style="overflow-x: auto">
<table id="t-log" class="t-cards" role="table">
<thead role="rowgroup">
<tr role="row">
<th id="tgl-38" data-i18n="settings.time" style="cursor: pointer" role="columnheader">
Time <span id="log-sort-time"></span>
</th>
<th data-i18n="settings.user" role="columnheader">User</th>
<th id="tgl-39" data-i18n="settings.action" style="cursor: pointer" role="columnheader">
Action <span id="log-sort-action"></span>
</th>
<th data-i18n="batch.batchId" role="columnheader">Batch</th>
<th data-i18n="settings.bag" role="columnheader">Bag</th>
<th data-i18n="settings.from" role="columnheader">From</th>
<th data-i18n="settings.to" role="columnheader">To</th>
<th data-i18n="batch.species" role="columnheader">Species</th>
<th role="columnheader"></th>
</tr>
</thead>
<tbody id="log-body" role="rowgroup"></tbody>
</table>
</div>
<div id="log-load-more" style="text-align: center; padding: 8px; display: none">
<button id="ctl-40" class="btn btn-sm" data-i18n="log.loadMore">Mehr laden...</button>
</div>
</div>
</div>
<!-- Backup -->
<div class="sp" id="sp-settings-backup">
<!-- The copy that leaves the building. Three parts drawn separately,
so the status, which redraws on every step of a running backup,
never wipes a half-typed key or a drive picked for setup. -->
<div class="card" id="usb-card">
<div class="sec" data-i18n="usb.title">USB stick</div>
<p class="usb-intro" data-i18n="usb.intro">
Once a day, plug in a stick that is set up here. The backup starts by itself, adds only what is
new, and is encrypted. Then take the stick out and keep it away from this PC.
</p>
<div id="usb-key" hidden></div>
<div id="usb-body"></div>
<div id="usb-setup" hidden></div>
</div>
<div class="card">
<div class="sec" data-i18n="settings.downloadBackupTitle">Download backup</div>
<p
style="font-size: 13px; color: var(--c-text-sec); margin-bottom: 12px; line-height: 1.6"
data-i18n="settings.downloadBackupDesc"
>
Download a password-encrypted full backup. Save to USB, cloud or email it to yourself.
</p>
<div style="display: flex; gap: 8px; align-items: center; flex-wrap: wrap">
<input
type="password"
id="backup-dl-pw"
autocomplete="new-password"
data-i18n-placeholder="settings.encryptPwPh"
placeholder="Encryption password"
style="flex: 1; min-width: 140px"
/>
<button id="btn-41" class="btn btn-p" data-i18n="settings.downloadBackupBtn">Download backup</button>
</div>
<div class="fs-meta" id="backup-dl-status" style="margin-top: 8px; color: var(--c-text-sec)"></div>
</div>
<div class="card">
<div class="sec" data-i18n="settings.restoreBackupTitle">Restore backup</div>
<p
style="font-size: 13px; color: var(--c-text-sec); margin-bottom: 12px; line-height: 1.6"
data-i18n-html="settings.restoreDescHtml"
>
Restore from an encrypted backup file.
<strong style="color: var(--c-red-dark)">Replaces all current data for everyone.</strong>
</p>
<div style="display: flex; gap: 8px; align-items: center; flex-wrap: wrap">
<input type="file" id="restore-file" accept=".enc" style="width: auto; flex: 1" />
</div>
<div style="display: flex; gap: 8px; align-items: center; flex-wrap: wrap; margin-top: 8px">
<input
type="password"
id="backup-restore-pw"
autocomplete="off"
data-i18n-placeholder="settings.decryptPwPh"
placeholder="Decryption password"
style="flex: 1; min-width: 140px"
/>
<button id="btn-42" class="btn btn-r" data-i18n="settings.restoreBtn">Restore</button>
</div>
<div class="fs-meta" id="backup-restore-status" style="margin-top: 8px; color: var(--c-text-sec)"></div>
</div>
<!-- Clean slate: wipe production data, keep the setup. Admin only;
writes a verified backup before deleting anything. -->
<div class="card" id="reset-card" style="border: 1px solid var(--c-red-border)">
<div class="sec" style="color: var(--c-red-dark)" data-i18n="reset.title">Start clean</div>
<p
style="font-size: 13px; color: var(--c-text-sec); margin-bottom: 12px; line-height: 1.6"
data-i18n="reset.desc"
>
Deletes the selected data so you can start over with fresh bags. Zones, racks, Sorten, recipes, users,
suppliers, thresholds and all settings are kept. A backup is saved first.
</p>
<label style="display: flex; gap: 9px; align-items: flex-start; margin-bottom: 8px; cursor: pointer">
<input type="checkbox" id="reset-growing" checked style="width: 22px; height: 22px; flex: none" />
<span style="font-size: 13px" data-i18n="reset.scopeGrowing"
>Batches, bags, scan history, harvests, cultures, contamination reports</span
>
</label>
<label style="display: flex; gap: 9px; align-items: flex-start; margin-bottom: 8px; cursor: pointer">
<input type="checkbox" id="reset-orders" style="width: 22px; height: 22px; flex: none" />
<span style="font-size: 13px" data-i18n="reset.scopeOrders">Orders, customers and shipments</span>
</label>
<label style="display: flex; gap: 9px; align-items: flex-start; margin-bottom: 8px; cursor: pointer">
<input type="checkbox" id="reset-planning" style="width: 22px; height: 22px; flex: none" />
<span style="font-size: 13px" data-i18n="reset.scopePlanning"
>Tasks, calendar, notifications, maintenance log and KPI history</span
>
</label>
<label style="display: flex; gap: 9px; align-items: flex-start; margin-bottom: 12px; cursor: pointer">
<input type="checkbox" id="reset-stock" style="width: 22px; height: 22px; flex: none" />
<span style="font-size: 13px" data-i18n="reset.scopeStock"
>Material stock set to zero and its ledger cleared, ready for a fresh count (thresholds and average
composition are kept)</span
>
</label>
<div style="display: flex; gap: 8px; align-items: center; flex-wrap: wrap">
<input
type="text"
id="reset-confirm"
autocomplete="off"
spellcheck="false"
placeholder="ALLES LOESCHEN"
style="flex: 1; min-width: 160px"
/>
<button id="reset-go" class="btn btn-r" data-i18n="reset.btn">Delete permanently</button>
</div>
<div class="fs-meta" id="reset-status" style="margin-top: 8px; color: var(--c-text-sec)"></div>
</div>
<div class="card">
<div class="sec" data-i18n="settings.syncInfo">Sync info</div>
<p class="fs-meta"
style="color: var(--c-text-muted); line-height: 1.7"
data-i18n-html="settings.syncInfoDesc"
>
All data is stored on the server — shared by all devices automatically. Changes sync every 5 seconds.
Click the green dot in the top bar to sync immediately.
</p>
</div>
</div>
<!-- Users -->
<div class="sp" id="sp-settings-users">
<div class="card">
<div class="sec" data-i18n="users.account">Account</div>
<p id="users-account" style="font-size: 13px; color: var(--c-text-sec); margin-bottom: 8px"></p>
<button id="btn-43" class="btn btn-r" style="margin-top: 4px" data-i18n="users.logout">Logout</button>
</div>
<div class="card">
<div class="sec" data-i18n="users.manage">Manage users</div>
<div id="users-table" style="margin-bottom: 12px"></div>
<div
style="
display: flex;
gap: 6px;
align-items: end;
flex-wrap: wrap;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid var(--c-border);
"
>
<div>
<label class="fs-xs" style="color: var(--c-text-muted)" data-i18n="users.username">Username</label
><input type="text" id="new-username" autocomplete="username" style="width: 120px; font-size: 13px" />
</div>
<div>
<label class="fs-xs" style="color: var(--c-text-muted)" data-i18n="users.password">Password</label
><input
type="password"
id="new-password"
autocomplete="new-password"
style="width: 120px; font-size: 13px"
/>
</div>
<div>
<label class="fs-xs" style="color: var(--c-text-muted)" data-i18n="users.role">Role</label
><select id="new-role" style="width: auto; font-size: 13px; padding: 6px">
<option value="user" data-i18n="users.roleUser">user</option>
<option value="admin" data-i18n="users.roleAdmin">admin</option>
</select>
</div>
<button id="btn-44" class="btn btn-p fs-meta" data-i18n="users.addUser">
Add user
</button>
</div>
</div>
</div>
<!-- CalDAV Calendar Sync -->
<div class="sp" id="sp-settings-caldav">
<div class="card">
<div class="sec" data-i18n="caldav.title">CalDAV calendar server</div>
<p class="fs-meta"
style="color: var(--c-text-muted); margin-bottom: 12px; line-height: 1.6"
data-i18n="caldav.desc"
>
This server includes a built-in CalDAV server. Connect any calendar app (Apple Calendar, Thunderbird,
DAVx5 on Android) to see your tasks.
</p>
<div
style="
background: var(--c-primary-light);
border: 1px solid var(--c-green-border);
border-radius: 8px;
padding: 10px 12px;
margin-bottom: 12px;
"
>
<div class="fs-xs"
style="color: var(--c-green-dark); font-weight: 600; margin-bottom: 4px"
data-i18n="caldav.urlTitle"
>
CalDAV URL (use this in your calendar app)
</div>
<div style="display: flex; align-items: center; gap: 8px">
<code class="fs-meta"
id="caldav-url-display"
style="word-break: break-all; color: var(--c-text); flex: 1"
data-i18n="common.loading"
>Loading…</code
>
<button
id="btn-45"
class="btn btn-sm fs-xs"
data-i18n-title="caldav.copyUrl"
title="Copy URL"
style="white-space: nowrap"
data-i18n="common.copy"
>
Copy
</button>
</div>
</div>
<div class="fs-meta"
style="background: var(--c-blue-light); border: 1px solid var(--c-blue-border); border-radius: 8px; padding: 10px 12px; margin-bottom: 12px; color: var(--c-blue-dark); line-height: 1.6"
data-i18n-html="caldav.loginHint"
>
<strong>Login:</strong> Your usual username. For the password, prefer an <strong>app password</strong>
(below) over your account password — the calendar app keeps it for good.
</div>
<div style="display: flex; flex-direction: column; gap: 10px">
<div style="display: flex; align-items: center; gap: 8px; flex-wrap: wrap">
<label style="display: flex; align-items: center; gap: 6px; cursor: pointer; margin: 0">
<input type="checkbox" id="caldav-enabled" style="width: auto" />
<span data-i18n="caldav.enableSync">Enable sync (push tasks to calendars)</span>
</label>
</div>
<div style="display: flex; gap: 6px; flex-wrap: wrap">
<button id="act-46" class="btn btn-sm btn-p" data-i18n="caldav.saveSettings">Save settings</button>
<button class="btn btn-sm" id="caldav-sync-btn" data-i18n="caldav.syncNow">Sync all tasks now</button>
</div>
<div class="fs-meta"
id="caldav-status"
style="color: var(--c-text-muted); padding: 6px 10px; background: var(--c-bg); border-radius: 7px; display: none"
></div>
</div>
</div>
<div class="card">
<div class="sec" data-i18n="caldav.appPwTitle">App passwords for calendar apps</div>
<div class="fs-meta" style="color: var(--c-text-sec); line-height: 1.6; margin-bottom: 10px" data-i18n="caldav.appPwDesc">
A calendar app stores whatever password you give it. Create one here per device instead of typing your account password: it opens calendars and nothing else, and you can revoke a single device without changing your login.
</div>
<div style="display: flex; gap: 6px; flex-wrap: wrap; align-items: center">
<input type="text" id="caldav-apppw-label" maxlength="60" style="flex: 1 1 180px; min-width: 140px" data-i18n-placeholder="caldav.appPwLabelPh" placeholder="Device name, e.g. iPhone" />
<button id="caldav-apppw-create" class="btn btn-sm btn-p" data-i18n="caldav.appPwCreate">Create</button>
</div>
<div class="fs-meta"
id="caldav-apppw-new"
style="display: none; margin-top: 10px; padding: 10px 12px; border: 1px solid var(--c-green-border); border-radius: 8px; background: var(--c-green-light); color: var(--c-green-dark)"
>
<div data-i18n="caldav.appPwOnce">Copy it now — it is shown this once and never again.</div>
<code id="caldav-apppw-value" style="display: block; margin-top: 6px; font-size: 16px; letter-spacing: 1px; user-select: all"></code>
</div>
<div id="caldav-apppw-list" style="margin-top: 12px"></div>
</div>
<div class="card">
<div class="sec" data-i18n="caldav.howToConnect">How to connect</div>
<div class="fs-meta" style="color: var(--c-text-sec); line-height: 1.7">
<p data-i18n-html="caldav.howThunderbird">
<strong>Thunderbird / GNOME Calendar:</strong> Add a new "CalDAV" or "network calendar" and paste the
CalDAV URL above.
</p>
<p style="margin-top: 6px" data-i18n-html="caldav.howAndroid">
<strong>Android (DAVx5):</strong> Install DAVx5 from F-Droid or Play Store. Add account with "Login
with URL" and paste the URL.
</p>
<p style="margin-top: 6px" data-i18n-html="caldav.howApple">
<strong>Apple Calendar (iPhone/Mac):</strong> Settings → Calendar → Accounts → Add CalDAV account.
Server = the URL above.
</p>
</div>
</div>
<div class="card">
<div class="sec" data-i18n="caldav.howItWorks">How it works</div>
<div class="fs-meta" style="color: var(--c-text-sec); line-height: 1.7">
<p data-i18n-html="caldav.hiwShared">
<strong>Shared calendar (<code>meisterpilze</code>):</strong> All tasks, events and batch due dates
land here. Everyone on the farm sees this calendar.
</p>
<p style="margin-top: 6px" data-i18n-html="caldav.hiwPersonal">
<strong>Personal calendars (<code>username</code>):</strong> Every user account automatically gets its
own calendar. Assigned tasks appear there as well.
</p>
<p style="margin-top: 6px" data-i18n-html="caldav.hiwVisibility">
<strong>Visibility:</strong> By default every task is visible to everyone (in the shared calendar).
Individual tasks can be marked "assignee only" — they will then only show up in the personal calendar.
</p>
<p style="margin-top: 6px" data-i18n-html="caldav.hiwTwoWay">
<strong>Two-way sync:</strong> Tasks are stored as calendar files on the server. Calendar apps read
them via CalDAV. Changes made in the calendar app are synced back.
</p>
</div>
</div>
</div>
<!-- DuckDNS -->
<div class="sp" id="sp-settings-duckdns">
<div class="card">
<div class="sec" data-i18n="duckdns.title">DuckDNS Dynamic DNS</div>
<p class="fs-meta"
style="color: var(--c-text-muted); margin-bottom: 12px; line-height: 1.6"
data-i18n="duckdns.desc"
>
Connect your server to a free DuckDNS domain for access over the internet. The public IP is updated
automatically every 5 minutes.
</p>
<div class="fs-meta"
id="duckdns-status-banner"
style="border-radius: 8px; padding: 10px 12px; margin-bottom: 12px; display: none"
></div>
<div style="display: flex; flex-direction: column; gap: 10px">
<label style="display: flex; align-items: center; gap: 6px; cursor: pointer; margin: 0">
<input type="checkbox" id="duckdns-enabled" style="width: auto" />
<span data-i18n="duckdns.enable">Enable DuckDNS</span>
</label>
<div>
<label class="fs-meta"
style="font-weight: 600; color: var(--c-text-sec); display: block; margin-bottom: 4px"
data-i18n="duckdns.subdomain"
>Subdomain</label
>
<div style="display: flex; align-items: center; gap: 4px">
<input class="w-cap"
type="text"
id="duckdns-domain"
data-i18n-placeholder="duckdns.subdomainPlaceholder"
placeholder="myserver"
style="flex: 1; --w-cap: 200px"
/>
<span style="font-size: 13px; color: var(--c-text-muted)">.duckdns.org</span>
</div>
</div>
<div>
<label class="fs-meta"
style="font-weight: 600; color: var(--c-text-sec); display: block; margin-bottom: 4px"
data-i18n="duckdns.token"
>DuckDNS token</label
>
<input class="w-cap"
type="password"
id="duckdns-token"
placeholder="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
style="width: 100%; --w-cap: 350px"
/>
<div class="fs-xs"
style="color: var(--c-text-muted); margin-top: 2px"
data-i18n-html="duckdns.tokenHint"
>
You can find the token on
<a href="https://www.duckdns.org" target="_blank" rel="noopener">duckdns.org</a> after login.
</div>
</div>
<div style="display: flex; gap: 6px; flex-wrap: wrap; margin-top: 4px">
<button id="duckdns-save-btn" class="btn btn-sm btn-p" data-i18n="common.save">Save</button>
<button id="duckdns-update-btn" class="btn btn-sm" data-i18n="duckdns.updateNow">
Update IP now
</button>
</div>
<div class="fs-meta"
id="duckdns-ip-status"
style="color: var(--c-text-muted); padding: 6px 10px; background: var(--c-bg); border-radius: 7px; display: none"
></div>
</div>
</div>
<div class="card">
<div class="sec" data-i18n="le.title">Let's Encrypt TLS certificate</div>
<p class="fs-meta"
style="color: var(--c-text-muted); margin-bottom: 12px; line-height: 1.6"
data-i18n="le.desc"
>
Free, trusted TLS certificate from Let's Encrypt for your DuckDNS domain. Replaces the self-signed
certificate. Auto-renewed before expiry.
</p>
<div class="fs-meta"
id="le-cert-status"
style="border-radius: 8px; padding: 10px 12px; margin-bottom: 12px; display: none"
></div>
<div style="display: flex; flex-direction: column; gap: 10px">
<label style="display: flex; align-items: center; gap: 6px; cursor: pointer; margin: 0">
<input type="checkbox" id="duckdns-le-enabled" style="width: auto" />
<span data-i18n="le.enableAutoRenew">Enable Let's Encrypt auto-renewal</span>
</label>
<div style="display: flex; gap: 6px; flex-wrap: wrap">
<button id="le-request-btn" class="btn btn-sm btn-p" data-i18n="le.requestNow">
Request certificate now
</button>
</div>
<div class="fs-meta"
id="le-status"
style="color: var(--c-text-muted); padding: 6px 10px; background: var(--c-bg); border-radius: 7px; display: none"
></div>
</div>
<div class="fs-xs"
style="margin-top: 12px; color: var(--c-text-muted); line-height: 1.6"
data-i18n="le.note"
>
No extra software required — works on all operating systems (Linux, macOS, Windows).
</div>
</div>
</div>
<!-- Harvest feed in three cards, and the order is the point: the feed
itself first — is it on, did the last message arrive — then what the
farm sells, which is read and rewritten by whoever keeps the shop
window, and last the address and the key, typed once and then left
alone. The goods used to sit under the plumbing: a pack size was a
scroll past a shared secret and a polling interval, so the setting
that moves with the season was the one you had to hunt for. It stays
one config with one Save, at the foot of all three. -->
<div class="sp" id="sp-settings-harvestfeed">
<div class="card">
<div class="sec" data-i18n="harvestFeed.title">Harvest feed</div>
<p class="fs-meta"
style="color: var(--c-text-muted); margin-bottom: 12px; line-height: 1.6"
data-i18n="harvestFeed.desc"
>
Push a summary of what you have harvested and what is coming to a URL you choose — your own website, a
box scheme, a chat bot. This server opens the connection; nothing can reach it unbidden. In its reply
the receiver may report pickups back, which show up under Pickups. No batch or bag ids, no customers, no
scan history.
</p>
<div class="fs-meta"
id="harvestfeed-status-banner"
style="border-radius: 8px; padding: 10px 12px; margin-bottom: 12px; display: none"
></div>
<label style="display: flex; align-items: center; gap: 6px; cursor: pointer; margin: 0">
<input type="checkbox" id="harvestfeed-enabled" style="width: auto" />
<span data-i18n="harvestFeed.enable">Enable harvest feed</span>
</label>
</div>
<div class="card">
<div class="sec" data-i18n="harvestFeed.offerTitle">What is on offer</div>
<p class="fs-meta"
style="color: var(--c-text-muted); margin-bottom: 12px; line-height: 1.6"
data-i18n="harvestFeed.offerDesc"
>
The goods rather than the connection: how long a harvest counts as fresh, how far ahead a planned one is
announced, and the portions it is handed over in. A shop builds its window out of these, and they ride
along in every message that goes out.
</p>
<div style="display: flex; flex-direction: column; gap: 10px">
<div style="display: flex; gap: 14px; flex-wrap: wrap">
<div>
<label class="fs-meta"
style="font-weight: 600; color: var(--c-text-sec); display: block; margin-bottom: 4px"
data-i18n="harvestFeed.freshDays"
>Counts as fresh for … days</label
>
<input type="number" id="harvestfeed-fresh" min="0" max="365" style="width: 90px" />
</div>
<div>
<label class="fs-meta"
style="font-weight: 600; color: var(--c-text-sec); display: block; margin-bottom: 4px"
data-i18n="harvestFeed.plannedDays"
>Look ahead … days</label
>
<input type="number" id="harvestfeed-planned" min="0" max="365" style="width: 90px" />
</div>
<div>
<label class="fs-meta"
style="font-weight: 600; color: var(--c-text-sec); display: block; margin-bottom: 4px"
data-i18n="harvestFeed.leadDays"
>Lead time … days</label
>
<input type="number" id="harvestfeed-lead" min="0" max="365" style="width: 90px" />
</div>
</div>
<label style="display: flex; align-items: center; gap: 6px; cursor: pointer; margin: 0">
<input type="checkbox" id="harvestfeed-strain" style="width: auto" />
<span data-i18n="harvestFeed.strain">Include strain names</span>
</label>
<!--
One ladder for every species, and that is the point rather
than a shortcut: portions follow the packing bench — which
trays are on the shelf, what the scale steps in — not the
mushroom in the tray. Per species this would be a field to
fill in for every new Sorte, for an answer that is the same
every time, and the first one forgotten is a species a shop
quietly offers in the wrong sizes.
Ticked boxes rather than a text field, because this is a
choice and not a sentence. The row below adds a size the list
does not offer, so a farm that packs 400 g trays is not stuck
with somebody else's ladder.
-->
<div>
<label class="fs-meta"
style="font-weight: 600; color: var(--c-text-sec); display: block; margin-bottom: 4px"
data-i18n="harvestFeed.packSizes"
>Amounts handed out</label
>
<div
id="harvestfeed-packs"
style="display: flex; gap: 10px; flex-wrap: wrap; align-items: center"
></div>
<div style="display: flex; gap: 6px; flex-wrap: wrap; align-items: center; margin-top: 6px">
<input
type="number"
id="harvestfeed-pack-new"
min="25"
max="25000"
step="5"
style="width: 110px"
data-i18n-placeholder="harvestFeed.packNewHint"
placeholder="e.g. 400"
/>
<button id="harvestfeed-pack-add" class="btn btn-sm" data-i18n="harvestFeed.packAdd">
Add size
</button>
<span class="fs-xs" id="harvestfeed-pack-msg" style="color: var(--c-red-dark)"></span>
</div>
<div class="fs-xs"
style="color: var(--c-text-muted); margin-top: 2px"
data-i18n="harvestFeed.packHint"
>
In grams, the same for every species. A shop offers these amounts and hides the ones a release no
longer covers. Tick nothing and no portions go out — then there is nothing to order produce in, and
a shop built on this feed takes no orders at all.
</div>
</div>
</div>
</div>
<div class="card">
<div class="sec" data-i18n="harvestFeed.connTitle">Where it goes</div>
<p class="fs-meta"
style="color: var(--c-text-muted); margin-bottom: 12px; line-height: 1.6"
data-i18n="harvestFeed.connDesc"
>
The address the messages are sent to and the key they are signed with — typed once and then left
alone.
</p>
<div style="display: flex; flex-direction: column; gap: 10px">
<div>
<label class="fs-meta"
style="font-weight: 600; color: var(--c-text-sec); display: block; margin-bottom: 4px"
data-i18n="harvestFeed.url"
>Receiver URL</label
>
<input class="w-cap"
type="url"
id="harvestfeed-url"
placeholder="https://example.org/harvest"
style="width: 100%; --w-cap: 480px"
/>
<div class="fs-xs"
style="color: var(--c-text-muted); margin-top: 2px"
data-i18n="harvestFeed.urlHint"
>
Must be https. http is allowed for localhost only, so you can try it against a local receiver first.
</div>
</div>
<div>
<label class="fs-meta"
style="font-weight: 600; color: var(--c-text-sec); display: block; margin-bottom: 4px"
data-i18n="harvestFeed.secret"
>Shared secret</label
>
<div style="display: flex; gap: 6px; flex-wrap: wrap; align-items: center">
<input class="w-cap" type="password" id="harvestfeed-secret" style="width: 100%; flex: 1; --w-cap: 380px" />
<button id="harvestfeed-gen-btn" class="btn btn-sm" data-i18n="harvestFeed.generate">
Generate
</button>
</div>
<div class="fs-xs