Grotti 3.3.0: Projekte mit Ordner, Datei-Löschen/Verschieben, Material-Archiv, UI-Polish.

This commit is contained in:
Dominic Gundel committed 2026-10-18 20:10:32 +02:00
1 parent 4ddffbbeb1
commit 67a89d1794
45 files changed
+2856 -320

No files matched your search

+121 -32
View File
@@ -340,6 +340,14 @@ function flowHtml(printer) {
return bits.join("");
}
function filamentPauseHint(printer) {
if (!printer) return "";
if (printer.filament_empty) return printer.pause_reason || printer.message || "Filament leer";
const text = String(printer.pause_reason || printer.message || "");
if (/filament|spule|material.?leer|gerissen|runout|10107/i.test(text)) return text;
return "";
}
function stripsHtml(printer, state) {
const bits = [];
const busy = state === "printing" || state === "paused" || state === "heating";
@@ -348,6 +356,14 @@ function stripsHtml(printer, state) {
if (state === "error") {
bits.push(`<div class="strip bad">${icon("alert")}<span class="grow">${escapeHtml(printer.message || "Drucker meldet einen Fehler.")}</span><button type="button" class="warn" data-cmd="clear_error">Fehler löschen</button></div>`);
}
const filamentHint = state === "paused" ? filamentPauseHint(printer) : "";
if (filamentHint) {
bits.push(`<div class="strip warn filament-swap">
<div class="strip-head">${icon("alert")}<span class="grow"><b>Neue Rolle in Material dem Drucker zuweisen</b></span></div>
<span class="strip-msg">${escapeHtml(filamentHint)} — sonst zählt der Verbrauch weiter zur alten Spule. Ort = dieser Drucker, speichern, dann Weiter.</span>
<div class="strip-actions"><a class="ghost" href="/material">Zu Material</a></div>
</div>`);
}
const failed = !queueBusy && printer.queue_failed;
if (failed) {
const step = failed.step_label_current || "Schritt " + ((Number(failed.step_index) || 0) + 1);
@@ -417,7 +433,11 @@ function aceSlotLabel(slot) {
function aceSteerHtml(printer) {
const dryers = printer.dryers || [];
if (!dryers.length) return "";
const loaded = wallSpools
.filter((s) => s.printer_host === printer.host && s.ace_slot)
.slice()
.sort((a, b) => Number(a.ace_slot) - Number(b.ace_slot));
if (!dryers.length && !loaded.length) return "";
const bits = [];
dryers.forEach((dryer) => {
const id = Number(dryer.id);
@@ -443,10 +463,6 @@ function aceSteerHtml(printer) {
</div>
</div>`);
});
const loaded = wallSpools
.filter((s) => s.printer_host === printer.host && s.ace_slot)
.slice()
.sort((a, b) => Number(a.ace_slot) - Number(b.ace_slot));
if (loaded.length) {
bits.push(`<div class="steer-ace-slots"><div class="steer-ace-head"><b>Plätze</b><span class="tiny">Sorte und Farbe aus Grotti</span></div>`);
loaded.forEach((spool) => {
@@ -472,16 +488,22 @@ function steerPanelHtml(printer) {
const fan = typeof printer.fan === "number" ? Math.round(printer.fan * 100) : 0;
const aceKey = aceSteerKey(printer);
return `<div class="steer" data-steer-host="${escapeHtml(printer.host)}" data-ace-key="${escapeHtml(aceKey)}">
<div class="steer-row">
<label>${icon("nozzle")} Düse <input type="number" data-steer="nozzle" data-steer-action="nozzle" min="0" max="350" value="${escapeHtml(String(n))}"></label>
<label>${icon("bed")} Bett <input type="number" data-steer="bed" data-steer-action="bed" min="0" max="150" value="${escapeHtml(String(b))}"></label>
<button type="button" data-steer-go="temps">Temp</button>
<button type="button" class="ghost" data-steer-go="heaters_off" title="Heizung und Lüfter aus">Aus</button>
<div class="steer-group">
<div class="steer-group-label">Heizung</div>
<div class="steer-row">
<label>${icon("nozzle")} Düse <input type="number" data-steer="nozzle" data-steer-action="nozzle" min="0" max="350" value="${escapeHtml(String(n))}"></label>
<label>${icon("bed")} Bett <input type="number" data-steer="bed" data-steer-action="bed" min="0" max="150" value="${escapeHtml(String(b))}"></label>
<button type="button" data-steer-go="temps">Temp</button>
<button type="button" class="ghost" data-steer-go="heaters_off" title="Heizung und Lüfter aus">Aus</button>
</div>
</div>
<div class="steer-row">
<label>Speed % <input type="number" data-steer="speed" data-steer-action="speed" min="10" max="200" value="${escapeHtml(String(sp))}"></label>
<label>Flow % <input type="number" data-steer="flow" data-steer-action="flow" min="50" max="150" value="${escapeHtml(String(flow))}"></label>
<label>${icon("fan")} Fan % <input type="number" data-steer="fan" data-steer-action="fan" min="0" max="100" value="${escapeHtml(String(fan))}"></label>
<div class="steer-group">
<div class="steer-group-label">Druck</div>
<div class="steer-row">
<label>Speed % <input type="number" data-steer="speed" data-steer-action="speed" min="10" max="200" value="${escapeHtml(String(sp))}"></label>
<label>Flow % <input type="number" data-steer="flow" data-steer-action="flow" min="50" max="150" value="${escapeHtml(String(flow))}"></label>
<label>${icon("fan")} Fan % <input type="number" data-steer="fan" data-steer-action="fan" min="0" max="100" value="${escapeHtml(String(fan))}"></label>
</div>
</div>
${aceSteerHtml(printer)}
<p class="tiny">Enter im Feld setzt nur diesen Wert.</p>
@@ -564,19 +586,24 @@ function factsHtml(printer) {
`;
}
function factChip(label, value, ico) {
if (!value && value !== 0) return "";
return `<span class="fact-chip">${ico || ""}<span class="fact-chip-label">${escapeHtml(label)}</span><b>${escapeHtml(String(value))}</b></span>`;
}
function techFactsHtml(printer) {
const part = fanPct(printer, "part") || fanPct(printer);
const chamberFan = fanPct(printer, "chamber");
const auxFan = fanPct(printer, "aux");
const speed = speedPct(printer);
return `
${part ? "<span class=\"fact-ico\">" + icon("fan") + " Teil <b>" + part + "</b></span>" : ""}
${chamberFan ? "<span class=\"fact-ico\">" + icon("fan-chamber") + " Bauraum <b>" + chamberFan + "</b></span>" : ""}
${auxFan ? "<span class=\"fact-ico\">" + icon("fan-aux") + " Aux <b>" + auxFan + "</b></span>" : ""}
${speed ? "<span>Speed <b>" + speed + "</b></span>" : ""}
${anycubicTaskLine(printer)}
<span>Host <b>${escapeHtml(printer.host)}</b></span>
`;
return [
part ? factChip("Teil", part, icon("fan")) : "",
chamberFan ? factChip("Bauraum", chamberFan, icon("fan-chamber")) : "",
auxFan ? factChip("Aux", auxFan, icon("fan-aux")) : "",
speed ? factChip("Speed", speed) : "",
anycubicTaskLine(printer),
factChip("Host", printer.host),
].filter(Boolean).join("");
}
function anycubicTaskLine(printer) {
@@ -584,13 +611,11 @@ function anycubicTaskLine(printer) {
if (!task || typeof task !== "object") return "";
const state = String(task.state || "").trim();
const tid = task.task_id != null && String(task.task_id).trim() ? String(task.task_id) : "";
const file = task.filename ? String(task.filename) : "";
if (!state && !tid && !file) return "";
if (!state && !tid) return "";
const bits = [];
if (state) bits.push(state);
if (tid) bits.push("task " + tid);
if (file) bits.push(file);
return "<span>Anycubic <b>" + escapeHtml(bits.join(" · ")) + "</b></span>";
if (tid) bits.push("#" + tid);
return factChip("Anycubic", bits.join(" · "));
}
function jobLineVisible(printer, state, filename) {
@@ -626,12 +651,15 @@ function buildCard(printer) {
article.dataset.needsClear = needsClear(printer) ? "1" : "";
article.innerHTML = `
<div class="machine-head">
<div class="machine-title">
<h2></h2>
${badge}
<span data-role="chip"></span>
<div class="machine-head-main">
<div class="machine-title">
<h2></h2>
${badge}
<span data-role="chip"></span>
</div>
<div data-role="hero"></div>
</div>
<div data-role="hero"></div>
<img class="wall-cam-thumb" data-role="wall-cam" alt="" hidden>
</div>
<div class="machine-body">
<div data-role="bar"></div>
@@ -655,6 +683,59 @@ function buildCard(printer) {
return article;
}
/** Wand-Thumb immer Snapshot — nie Stream, unabhängig von der Browser-Einstellung. */
function wallCamSnapshotUrl(printer) {
if (!printer || !printer.camera || printer.has_camera === false) return "";
return withToken(printer.camera) + "&t=" + Math.floor(Date.now() / 3000);
}
function patchWallCam(article, printer) {
const img = article.querySelector('[data-role="wall-cam"]');
const head = article.querySelector(".machine-head");
if (!img) return;
const next = wallCamSnapshotUrl(printer);
if (!next) {
img.hidden = true;
img.removeAttribute("src");
img.dataset.srcKey = "";
img.dataset.pending = "";
if (head) head.classList.remove("has-cam");
return;
}
img.alt = "Kamera " + (printer.name || printer.host || "");
if (img.dataset.srcKey === next) {
img.hidden = false;
if (head) head.classList.add("has-cam");
return;
}
img.dataset.pending = next;
const probe = new Image();
probe.onload = () => {
if (img.dataset.pending !== next) return;
img.dataset.srcKey = next;
img.dataset.pending = "";
img.src = next;
img.hidden = false;
if (head) head.classList.add("has-cam");
};
probe.onerror = () => {
if (img.dataset.pending !== next) return;
img.dataset.pending = "";
img.hidden = true;
img.removeAttribute("src");
img.dataset.srcKey = "";
if (head) head.classList.remove("has-cam");
};
probe.src = next;
}
function refreshWallCams() {
for (const [host, article] of cardNodes.entries()) {
const printer = printerCache.get(host);
if (printer) patchWallCam(article, printer);
}
}
function steerHasFocus(root) {
const active = document.activeElement;
return !!(active && root.contains(active) && active.matches("input, select, textarea, button"));
@@ -772,6 +853,7 @@ function patchCard(article, printer) {
setHtml(role("flow"), flowHtml(printer));
setHtml(role("strips"), stripsHtml(printer, state));
setHtml(role("controls"), actionsHtml(state, printer));
patchWallCam(article, printer);
}
function refreshDeskCamera(printer) {
@@ -1618,6 +1700,12 @@ async function handleWallClick(event) {
}
button.disabled = true;
try {
if (button.dataset.cmd === "fortsetzen") {
const card = printerCache.get(host);
if (card && filamentPauseHint(card)) {
toast("Neue Spule unter Material dem Drucker zuweisen — sonst bleibt der Abzug bei der alten Rolle.");
}
}
await api("/drucker/" + encodeURIComponent(host) + "/" + button.dataset.cmd, { method: "POST", json: {} });
showNote("note", null);
refresh();
@@ -1665,6 +1753,7 @@ deskEl.addEventListener("keydown", async (event) => {
refresh().catch((error) => showNote("note", error));
createPoller(() => refresh().catch((error) => showNote("note", error)), 12000).start();
setInterval(refreshWallCams, 3000);
setInterval(() => {
api("/dateien").then((rows) => { libraryFiles = rows || []; }).catch(() => {});
api("/spulen").then((rows) => { wallSpools = rows || []; }).catch(() => {});