Files
grotti/web/static/api.js
T

193 lines
6.3 KiB
JavaScript

const TOKEN = ""; // Legacy: Auth über Session-Cookie oder Bearer von außen
const CSRF = document.querySelector('meta[name="csrf"]')?.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("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;");
}
async function api(path, options = {}) {
const headers = new Headers(options.headers || {});
if (TOKEN) headers.set("Authorization", "Bearer " + TOKEN);
const method = (options.method || "GET").toUpperCase();
if (CSRF && method !== "GET" && method !== "HEAD") {
headers.set("X-CSRF-Token", CSRF);
}
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,
headers,
body,
cache: "no-store",
credentials: "same-origin",
});
if (response.status === 401) {
const here = location.pathname + location.search;
if (!here.startsWith("/login") && !here.startsWith("/setup")) {
location.href = "/login?next=" + encodeURIComponent(here);
}
throw new Error("Anmeldung nötig.");
}
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) {
// Session-Cookie reicht für same-origin Kameras; kein Token in der URL.
return url;
}
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 `<svg class="ico${cls ? " " + cls : ""}" aria-hidden="true"><use href="#i-${name}"/></svg>`;
}
function chipHtml(state, label, small) {
const text = label || STATE_LABELS[state] || state || "";
return `<span class="chip ${escapeHtml(state || "")}${small ? " sm" : ""}">${escapeHtml(text)}</span>`;
}
function shownState(printer) {
const state = (printer && printer.state) || "offline";
const hot = ((printer && printer.nozzle_target) || 0) > 40 || ((printer && printer.bed_target) || 0) > 30;
if ((state === "standby" || state === "complete") && hot) return "heating";
return state;
}
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 `<span class="step ${cls}">${escapeHtml(label)}</span>`;
});
return `<span class="steps${compact ? " compact" : ""}">${parts.join('<span class="step-sep"></span>')}</span>`;
}
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") + "<div></div>";
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);
}