Grotti 3.2.0: Jetzt-Liste, Filament-Pause, Strom-Torte, UI-Polish.

Jetzt/Topbar und Material-Routing, Pause bei Filament-Ende, Druckbericht mit Stromanteilen, Design- und Kamera-Stream-Polish; Bedarf-Alert-Spam und Platte-belegt-Bugs behoben.
This commit is contained in:
Dominic Gundel committed 2026-10-18 14:58:32 +02:00
1 parent 6e59df22e5
commit 4ddffbbeb1
68 files changed
+4348 -1215

No files matched your search

+83 -47
View File
@@ -15,7 +15,7 @@
<section class="desk no-cam" id="desk" hidden data-host="">
<header class="desk-bar">
<div class="desk-bar-top">
<button type="button" class="ghost desk-back" id="desk-back">← Drucker</button>
<button type="button" class="ghost desk-back" id="desk-back"><svg class="ico"><use href="#i-chevron-left"/></svg>Drucker</button>
<div class="desk-title">
<h2 data-role="name"></h2>
<span data-role="badge"></span>
@@ -27,7 +27,7 @@
</header>
<div class="desk-stage">
<details class="desk-fold desk-cam-fold" data-fold="cam" open>
<summary class="desk-fold-sum"><svg class="ico caret"><use href="#i-down"/></svg><span class="grow">Kamera</span><span class="tiny" data-role="cam-sum"></span></summary>
<summary class="desk-fold-sum"><svg class="ico caret"><use href="#i-down"/></svg><svg class="ico title-ico"><use href="#i-camera"/></svg><span class="grow">Kamera</span><span class="tiny" data-role="cam-sum"></span></summary>
<div class="desk-cam" data-role="cam-stage">
<img class="desk-cam-img" data-role="camera" alt="Kamera" hidden>
<p class="desk-cam-empty" data-role="cam-empty"><svg class="ico"><use href="#i-camera"/></svg><span>Keine Kamera</span></p>
@@ -49,45 +49,51 @@
<div class="desk-facts" data-role="facts"></div>
<div data-role="flow"></div>
<div data-role="strips"></div>
<details class="desk-fold desk-steer-fold" data-fold="steer">
<summary class="desk-fold-sum"><svg class="ico caret"><use href="#i-down"/></svg><span class="grow">Steuerung</span></summary>
<div data-role="steer"></div>
</details>
</div>
<div class="desk-foot">
<div class="card-actions" data-role="controls"></div>
<button type="button" class="warn" data-role="estop" data-steer-go="emergency_stop" hidden>E-Stop</button>
<button type="button" class="btn-danger" data-role="estop" data-steer-go="emergency_stop" hidden>E-Stop</button>
</div>
</aside>
</div>
<div class="desk-tools">
<details class="desk-fold desk-tool" data-fold="verlauf" open>
<summary class="desk-fold-sum"><svg class="ico caret"><use href="#i-down"/></svg><span class="grow">Verlauf</span><span class="tiny" data-role="verlauf-caption"></span></summary>
<div class="desk-fold-body desk-verlauf" data-role="verlauf">
<p class="tiny">Lade Verlauf …</p>
</div>
</details>
<details class="desk-fold desk-tool" data-fold="path">
<summary class="desk-fold-sum"><svg class="ico caret"><use href="#i-down"/></svg><span class="grow">3D-Pfad</span><span class="tiny" data-role="path-caption"></span></summary>
<div class="desk-fold-body">
<p class="tiny">Ziehen dreht · Rad zoomt · Rechtsklick/Shift schwenkt · Doppelklick setzt zurück</p>
<canvas class="gcode-canvas desk-canvas" data-role="path-canvas"></canvas>
<div class="exclude-panel" data-role="exclude-panel" hidden>
<div class="exclude-head">
<span class="tiny grow">Objekte ausschließen</span>
<span class="tiny" data-role="exclude-hint">Tippen wählt · zwei Klicks schließen aus</span>
<details class="desk-fold desk-technik" data-fold="technik">
<summary class="desk-fold-sum"><svg class="ico caret"><use href="#i-down"/></svg><svg class="ico title-ico"><use href="#i-sliders"/></svg><span class="grow">Technik</span><span class="tiny">Steuerung, Verlauf, 3D, Bettnetz</span></summary>
<div class="desk-technik-body">
<div data-role="tech-facts" class="desk-facts desk-tech-facts"></div>
<details class="desk-fold desk-steer-fold" data-fold="steer" open>
<summary class="desk-fold-sum"><svg class="ico caret"><use href="#i-down"/></svg><svg class="ico title-ico"><use href="#i-gauge"/></svg><span class="grow">Steuerung</span></summary>
<div data-role="steer"></div>
</details>
<div class="desk-tools">
<details class="desk-fold desk-tool" data-fold="verlauf" open>
<summary class="desk-fold-sum"><svg class="ico caret"><use href="#i-down"/></svg><svg class="ico title-ico"><use href="#i-activity"/></svg><span class="grow">Verlauf</span><span class="tiny" data-role="verlauf-caption"></span></summary>
<div class="desk-fold-body desk-verlauf" data-role="verlauf">
<p class="tiny">Lade Verlauf …</p>
</div>
<div class="exclude-list" data-role="exclude-list"></div>
</div>
</details>
<details class="desk-fold desk-tool" data-fold="path">
<summary class="desk-fold-sum"><svg class="ico caret"><use href="#i-down"/></svg><svg class="ico title-ico"><use href="#i-cube"/></svg><span class="grow">3D-Pfad</span><span class="tiny" data-role="path-caption"></span></summary>
<div class="desk-fold-body">
<p class="tiny">Ziehen dreht · Rad zoomt · Rechtsklick/Shift schwenkt · Doppelklick setzt zurück</p>
<canvas class="gcode-canvas desk-canvas" data-role="path-canvas"></canvas>
<div class="exclude-panel" data-role="exclude-panel" hidden>
<div class="exclude-head">
<span class="tiny grow">Objekte ausschließen</span>
<span class="tiny" data-role="exclude-hint">Tippen wählt · zwei Klicks schließen aus</span>
</div>
<div class="exclude-list" data-role="exclude-list"></div>
</div>
</div>
</details>
<details class="desk-fold desk-tool desk-tool-mesh" data-fold="mesh">
<summary class="desk-fold-sum"><svg class="ico caret"><use href="#i-down"/></svg><svg class="ico title-ico"><use href="#i-grid"/></svg><span class="grow">Bettnetz</span></summary>
<div class="desk-fold-body" data-role="mesh">
<p class="desk-empty"><svg class="ico"><use href="#i-grid"/></svg><span>Noch kein Bettnetz</span></p>
</div>
</details>
</div>
</details>
<details class="desk-fold desk-tool desk-tool-mesh" data-fold="mesh">
<summary class="desk-fold-sum"><svg class="ico caret"><use href="#i-down"/></svg><span class="grow">Bettnetz</span></summary>
<div class="desk-fold-body" data-role="mesh">
<p class="desk-empty"><svg class="ico"><use href="#i-grid"/></svg><span>Noch kein Bettnetz</span></p>
</div>
</details>
</div>
</div>
</details>
</section>
{% endblock %}
{% block script %}
@@ -143,7 +149,7 @@ function syncDeskCamCollapsed() {
function restoreDeskFolds() {
let closed;
try { closed = JSON.parse(localStorage.getItem(DESK_FOLD_KEY) || "null"); } catch (error) { closed = null; }
if (!Array.isArray(closed)) closed = ["steer", "path", "mesh"];
if (!Array.isArray(closed)) closed = ["technik", "path", "mesh"];
deskEl.querySelectorAll("details[data-fold]").forEach((box) => {
box.open = !closed.includes(box.dataset.fold);
box.addEventListener("toggle", () => {
@@ -176,7 +182,11 @@ function setDeskCameraMode(live, note) {
}
syncDeskCamCollapsed();
const sum = deskEl.querySelector('[data-role="cam-sum"]');
if (sum) sum.textContent = deskCamLive ? "live" : (note || "keine");
if (sum) {
if (!deskCamLive) sum.textContent = note || "keine";
else if (note === "stream") sum.textContent = "stream";
else sum.textContent = "live";
}
}
function heatColor(value, low, high) {
@@ -501,7 +511,9 @@ function heroHtml(printer, state) {
}
if (state === "complete") {
const took = hours(printer.elapsed);
return `<div class="hero idle"><span class="hero-main">fertig</span>${took ? `<span class="hero-side">nach ${escapeHtml(took)}</span>` : ""}</div>`;
const last = jobLabel(printer.filename || "");
const side = [last ? "zuletzt " + last : "", took].filter(Boolean).join(", ");
return `<div class="hero idle"><span class="hero-main">bereit</span>${side ? `<span class="hero-side">${escapeHtml(side)}</span>` : ""}</div>`;
}
if (state === "cancelled") {
return `<div class="hero idle"><span class="hero-main">abgebrochen</span></div>`;
@@ -539,10 +551,6 @@ function patchBar(box, printer, state) {
}
function factsHtml(printer) {
const part = fanPct(printer, "part") || fanPct(printer);
const chamberFan = fanPct(printer, "chamber");
const auxFan = fanPct(printer, "aux");
const speed = speedPct(printer);
const loaded = wallSpools.filter((s) => s.printer_host === printer.host);
const spoolLine = loaded.length
? "<span>Spule <b>" + escapeHtml(loaded.map((s) => (s.ace_slot ? s.ace_slot + ": " : "") + s.name + " (" + grams(s.remaining_g) + ")").join(", ")) + "</b></span>"
@@ -552,13 +560,22 @@ function factsHtml(printer) {
${spoolLine}
${printer.material ? "<span>Material <b>" + escapeHtml(String(printer.material).split(";")[0]) + "</b></span>" : ""}
${printer.weight ? "<span>Gewicht <b>" + grams(printer.weight) + "</b></span>" : ""}
${printer.message && printer.ok === false ? `<p class="msg">${escapeHtml(printer.message)}</p>` : ""}
`;
}
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>
${printer.message && printer.ok === false ? `<p class="msg">${escapeHtml(printer.message)}</p>` : ""}
`;
}
@@ -774,19 +791,19 @@ function refreshDeskCamera(printer) {
// Layout (no-cam) nicht bei jedem Frame-Fehler umwerfen — sonst springt die
// ganze Bühne inkl. 3D-Viewer alle paar Sekunden.
img.alt = "Kamera " + (printer.name || printer.host);
const next = withToken(printer.camera) + "&t=" + Math.floor(Date.now() / 3000);
const streaming = typeof cameraMode === "function" && cameraMode() === "stream";
const next = typeof cameraUrl === "function" ? cameraUrl(printer, streaming ? 0 : 3000) : (withToken(printer.camera) + "&t=" + Math.floor(Date.now() / 3000));
if (img.dataset.srcKey === next) return;
img.dataset.srcKey = next;
const probe = new Image();
probe.onload = () => {
const onOk = () => {
if (img.dataset.srcKey !== next) return;
deskCamFails = 0;
img.src = next;
img.hidden = false;
empty.hidden = true;
setDeskCameraMode(true, "live");
setDeskCameraMode(true, streaming ? "stream" : "live");
};
probe.onerror = () => {
const onFail = () => {
if (img.dataset.srcKey !== next) return;
deskCamFails += 1;
if (deskCamFails >= 2) {
@@ -803,8 +820,17 @@ function refreshDeskCamera(printer) {
empty.innerHTML = icon("camera") + "<span>Kamera nicht erreichbar</span>";
}
const sum = deskEl.querySelector('[data-role="cam-sum"]');
if (sum && deskCamLive) sum.textContent = "live · kurz weg";
if (sum && deskCamLive) sum.textContent = streaming ? "stream · kurz weg" : "live · kurz weg";
};
if (streaming) {
img.onload = onOk;
img.onerror = onFail;
img.src = next;
return;
}
const probe = new Image();
probe.onload = onOk;
probe.onerror = onFail;
probe.src = next;
}
@@ -816,9 +842,18 @@ function startCameraTimer() {
if (printer) refreshDeskCamera(printer);
};
tick();
// Stream braucht keinen Poll — MJPEG läuft im <img>.
if (typeof cameraMode === "function" && cameraMode() === "stream") return;
cameraTimer = setInterval(tick, 3000);
}
window.addEventListener("grotti-camera-mode", () => {
if (!deskHost) return;
const img = deskEl.querySelector('[data-role="camera"]');
if (img) img.dataset.srcKey = "";
startCameraTimer();
});
function stopCameraTimer() {
if (cameraTimer) {
clearInterval(cameraTimer);
@@ -890,6 +925,7 @@ function patchDesk(printer) {
}
setHtml(role("swatches"), swatchesHtml(printer));
setHtml(role("facts"), factsHtml(printer));
setHtml(role("tech-facts"), techFactsHtml(printer));
setHtml(role("flow"), flowHtml(printer));
setHtml(role("strips"), stripsHtml(printer, state));
setHtml(role("controls"), actionsHtml(state, printer));