/** Live-UI: Verbindungsbanner, Poller, gemeinsame Zustands-Labels. */ const RUN_STATES = { completed: "fertig", cancelled: "abgebrochen", error: "Fehler", in_progress: "druckt", printing: "druckt", paused: "pausiert", interrupted: "unterbrochen", standby: "bereit", heating: "heizt", complete: "fertig", offline: "offline", waiting: "wartet", running: "läuft", }; let connBannerOk = true; let connBannerTimer = 0; function setConnectionBanner(ok, text) { let el = document.getElementById("conn-banner"); if (!el) { el = document.createElement("div"); el.id = "conn-banner"; el.className = "conn-banner"; el.setAttribute("role", "status"); el.hidden = true; document.body.prepend(el); } clearTimeout(connBannerTimer); if (ok) { const wasBad = !connBannerOk; connBannerOk = true; if (wasBad && text) { el.textContent = text; el.className = "conn-banner ok"; el.hidden = false; connBannerTimer = setTimeout(() => { el.hidden = true; el.textContent = ""; }, 2800); return; } el.hidden = true; el.textContent = ""; el.className = "conn-banner"; return; } connBannerOk = false; el.textContent = text || "Keine Verbindung zum Server."; el.className = "conn-banner bad"; el.hidden = false; } /** * @param {() => void | Promise} fn * @param {number} intervalMs * @returns {{ start: () => void, stop: () => void }} */ function createPoller(fn, intervalMs) { const base = Math.max(1000, Number(intervalMs) || 5000); const maxMs = 60000; let timer = 0; let busy = false; let delay = base; let fails = 0; let running = false; function schedule() { if (!running) return; clearTimeout(timer); timer = setTimeout(tick, delay); } async function tick() { if (!running || document.hidden || busy) { schedule(); return; } busy = true; try { await fn(); delay = base; fails = 0; if (!connBannerOk) setConnectionBanner(true, "Wieder verbunden."); else setConnectionBanner(true, ""); } catch (error) { fails += 1; delay = Math.min(maxMs, Math.max(base, delay * 2)); if (fails >= 2) { setConnectionBanner(false, (error && error.message) || "Daten konnten nicht geladen werden."); } } finally { busy = false; schedule(); } } function onVis() { if (!document.hidden && running) tick(); } return { start() { if (running) return; running = true; document.addEventListener("visibilitychange", onVis); tick(); }, stop() { running = false; document.removeEventListener("visibilitychange", onVis); clearTimeout(timer); timer = 0; }, }; } const VERDICT_REASON_LABELS = { haftung: "Haftung", warping: "Warping", spaghetti: "Spaghetti", layer_shift: "Layer-Shift", verstopfung: "Verstopfung", farbe: "Farbe", sonstiges: "Sonstiges", }; /** * Dialog vor „Platte geräumt“: Urteil, optional Grund und Stückzahlen. * @returns {Promise} Payload für /abraeumen oder null bei Abbruch */ function promptAbraeumen(hostLabel) { return new Promise((resolve) => { const existing = document.getElementById("abraeumen-dlg"); if (existing) existing.remove(); const dlg = document.createElement("dialog"); dlg.id = "abraeumen-dlg"; dlg.className = "abraeumen-dlg"; const reasons = Object.entries(VERDICT_REASON_LABELS) .map(([k, label]) => ``) .join(""); dlg.innerHTML = `

Platte geräumt

${escapeHtml(hostLabel || "Drucker")}

`; document.body.appendChild(dlg); const verdict = dlg.querySelector("#ab-verdict"); const reasonWrap = dlg.querySelector("#ab-reason-wrap"); const pieces = dlg.querySelector("#ab-pieces"); function sync() { const scrap = verdict.value === "ausschuss"; reasonWrap.hidden = !scrap; pieces.hidden = !scrap; } verdict.addEventListener("change", sync); sync(); dlg.querySelector("#ab-cancel").addEventListener("click", () => { dlg.close(); dlg.remove(); resolve(null); }); dlg.addEventListener("close", () => { if (dlg.returnValue !== "ok") { dlg.remove(); resolve(null); return; } const payload = { confirm: true, verdict: verdict.value, verdict_reason: verdict.value === "ausschuss" ? dlg.querySelector("#ab-reason").value : "", pieces_ok: Number(dlg.querySelector("#ab-ok").value) || 0, pieces_bad: Number(dlg.querySelector("#ab-bad").value) || 0, reprint: !!dlg.querySelector("#ab-reprint").checked, }; dlg.remove(); resolve(payload); }); dlg.showModal(); }); } /** * Toast mit Rückgängig für umkehrbare Aktionen. * @param {string} message * @param {() => void | Promise} onUndo * @param {number} [ttlMs] */ function undoToast(message, onUndo, ttlMs) { let host = document.querySelector(".toast-host"); if (!host) { host = document.createElement("div"); host.className = "toast-host"; host.setAttribute("role", "status"); host.setAttribute("aria-live", "polite"); document.body.appendChild(host); } const node = document.createElement("div"); node.className = "toast good undo-toast"; node.innerHTML = '
'; node.querySelector("div").textContent = String(message ?? ""); let undone = false; const btn = node.querySelector("button"); btn.addEventListener("click", async () => { if (undone) return; undone = true; node.remove(); try { await onUndo(); } catch (error) { toast(error.message || String(error), "bad"); } }); host.appendChild(node); setTimeout(() => { if (undone) return; node.classList.add("out"); setTimeout(() => node.remove(), 260); }, ttlMs || 6000); } /** Globale Suche Ctrl+K / Cmd+K. */ function openCommandPalette() { const existing = document.getElementById("cmdk-dlg"); if (existing) { existing.showModal(); existing.querySelector("input")?.focus(); return; } const dlg = document.createElement("dialog"); dlg.id = "cmdk-dlg"; dlg.className = "cmdk-dlg"; dlg.innerHTML = `
    `; document.body.appendChild(dlg); const input = dlg.querySelector("input"); const list = dlg.querySelector(".cmdk-list"); let items = []; let active = 0; let timer = 0; function paint() { list.innerHTML = items.map((item, i) => `
  • ${escapeHtml(item.title)} ${escapeHtml(item.subtitle || "")}
  • ` ).join("") || '
  • Nichts gefunden.
  • '; } async function search(q) { q = (q || "").trim(); if (!q) { items = []; paint(); return; } try { const data = await api("/suche?q=" + encodeURIComponent(q)); items = data.items || []; } catch (error) { items = []; } active = 0; paint(); } function go(item) { if (!item || !item.href) return; dlg.close(); location.href = item.href; } input.addEventListener("input", () => { clearTimeout(timer); timer = setTimeout(() => search(input.value), 180); }); input.addEventListener("keydown", (event) => { if (event.key === "ArrowDown") { event.preventDefault(); active = Math.min(items.length - 1, active + 1); paint(); } else if (event.key === "ArrowUp") { event.preventDefault(); active = Math.max(0, active - 1); paint(); } else if (event.key === "Enter") { event.preventDefault(); go(items[active]); } else if (event.key === "Escape") { dlg.close(); } }); list.addEventListener("click", (event) => { const li = event.target.closest("[data-i]"); if (!li) return; go(items[Number(li.dataset.i)]); }); dlg.addEventListener("close", () => { /* bleibt im DOM */ }); dlg.showModal(); input.focus(); } document.addEventListener("keydown", (event) => { if ((event.ctrlKey || event.metaKey) && (event.key === "k" || event.key === "K")) { event.preventDefault(); openCommandPalette(); } }); window.undoToast = undoToast; window.openCommandPalette = openCommandPalette; window.RUN_STATES = RUN_STATES; window.setConnectionBanner = setConnectionBanner; window.createPoller = createPoller; window.promptAbraeumen = promptAbraeumen; window.VERDICT_REASON_LABELS = VERDICT_REASON_LABELS; /** Listen-Pagination (clientseitig auf gefiltertem Array). */ const DEFAULT_PAGE_SIZE = 40; function pageSlice(items, page, pageSize) { const list = Array.isArray(items) ? items : []; const size = Math.max(1, Number(pageSize) || DEFAULT_PAGE_SIZE); const total = list.length; const pages = Math.max(1, Math.ceil(total / size) || 1); let p = Math.max(1, parseInt(page, 10) || 1); if (p > pages) p = pages; const start = (p - 1) * size; return { items: list.slice(start, start + size), page: p, pages, total, pageSize: size, from: total ? start + 1 : 0, to: Math.min(start + size, total), }; } function renderPager(el, state, onPage) { if (!el) return; const s = state || pageSlice([], 1); if (s.total <= s.pageSize) { el.hidden = true; el.innerHTML = ""; return; } el.hidden = false; el.className = "pager"; el.innerHTML = '" + '' + s.from + "–" + s.to + " von " + s.total + "" + '"; el.querySelector(".pager-prev").onclick = () => onPage(s.page - 1); el.querySelector(".pager-next").onclick = () => onPage(s.page + 1); } window.DEFAULT_PAGE_SIZE = DEFAULT_PAGE_SIZE; window.pageSlice = pageSlice; window.renderPager = renderPager;