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:
1 parent
6e59df22e5
commit
4ddffbbeb1
68 files changed
+4348
-1215
No files matched your search
+83
-47
@@ -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));
|
||||
|
||||
Reference in new issue
Block a user