const TOKEN = document.querySelector('meta[name="token"]')?.content || ""; const STATE_LABELS = { printing: "druckt", paused: "pausiert", complete: "fertig", standby: "bereit", heating: "heizt", error: "Fehler", cancelled: "abgebrochen", offline: "offline", waiting: "wartet", running: "läuft", }; function escapeHtml(value) { return String(value ?? "") .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """); } async function api(path, options = {}) { const headers = new Headers(options.headers || {}); if (TOKEN) headers.set("Authorization", "Bearer " + TOKEN); let body = options.body; if (options.json !== undefined) { headers.set("Content-Type", "application/json"); body = JSON.stringify(options.json); } const response = await fetch("/api/v1" + path, { method: options.method || "GET", headers, body, cache: "no-store", }); if (!response.ok) { let message = response.statusText; try { const payload = await response.json(); message = payload.error || message; } catch (error) { message = response.statusText; } throw new Error(message); } if (response.status === 204) return null; const type = response.headers.get("content-type") || ""; if (type.includes("json")) return response.json(); return response; } function withToken(url) { if (!url || !TOKEN) return url; return url + (url.includes("?") ? "&" : "?") + "token=" + encodeURIComponent(TOKEN); } function euro(value) { return Number(value || 0).toLocaleString("de-DE", { style: "currency", currency: "EUR" }); } function grams(value) { return Number(value || 0).toLocaleString("de-DE", { maximumFractionDigits: 0 }) + " g"; } function hours(seconds) { seconds = Math.round(Number(seconds) || 0); if (seconds <= 0) return ""; const h = Math.floor(seconds / 3600); const m = Math.floor((seconds % 3600) / 60); return h > 0 ? h + " h " + String(m).padStart(2, "0") + " min" : m + " min"; } function finishClock(remainSeconds) { const sec = Number(remainSeconds) || 0; if (sec <= 0) return ""; const at = new Date(Date.now() + sec * 1000); return at.toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" }); } function clockOf(iso) { if (!iso) return ""; const at = new Date(iso); if (Number.isNaN(at.getTime())) return ""; return at.toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" }); } function showNote(id, error, ok) { const node = document.getElementById(id); if (node) { if (error) { node.textContent = error.message || String(error); node.className = "msg"; } else { node.textContent = ok || ""; node.className = ok ? "ok" : "ok"; } } if (error) toast(error.message || String(error), "bad"); else if (ok) toast(ok, "good"); } function icon(name, cls) { return ``; } function chipHtml(state, label, small) { const text = label || STATE_LABELS[state] || state || ""; return `${escapeHtml(text)}`; } function stepsHtml(labels, index, compact) { if (!labels || !labels.length) return ""; const parts = labels.map((label, i) => { const cls = i < index ? "done" : i === index ? "now" : ""; return `${escapeHtml(label)}`; }); return `${parts.join('')}`; } function repeatText(job) { const total = Math.max(1, Number(job.repeat_total) || 1); if (total <= 1) return ""; return Math.min(total, (Number(job.repeat_done) || 0) + 1) + "/" + total; } function phaseText(job) { const message = (job && job.message) || ""; if (!job || !job.is_ablauf || job.status !== "running") return message; const parts = message.split(" · "); return parts.length > 1 ? parts.slice(-1)[0] : message; } function jobLabel(filename) { return (filename || "").replace(/\.gcode(\.3mf)?$/i, "").replace(/\.3mf$/i, "").replace(/^(\d+-)+/, ""); } function toast(message, kind) { 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 " + (kind || "good"); node.innerHTML = icon(kind === "bad" ? "alert" : "check", "lg") + "
"; node.querySelector("div").textContent = message; host.appendChild(node); const ttl = kind === "bad" ? 7000 : 4000; setTimeout(() => { node.classList.add("out"); setTimeout(() => node.remove(), 260); }, ttl); } function armButton(button, armedText, onConfirm, timeoutMs) { if (button.dataset.armed === "1") { clearTimeout(Number(button.dataset.armTimer || 0)); button.dataset.armed = ""; button.classList.remove("confirm-armed"); if (button.dataset.idleHtml) button.innerHTML = button.dataset.idleHtml; onConfirm(); return; } button.dataset.idleHtml = button.innerHTML; button.dataset.armed = "1"; button.classList.add("confirm-armed"); button.textContent = armedText; const timer = setTimeout(() => { button.dataset.armed = ""; button.classList.remove("confirm-armed"); if (button.dataset.idleHtml) button.innerHTML = button.dataset.idleHtml; }, timeoutMs || 5000); button.dataset.armTimer = String(timer); }