Grotti 3.6.0: Lauf-Identität nach Filament-Pause, robustere Queue, Projekte/Aufträge list-first und API-Polish.

This commit is contained in:
Dominic Gundel committed 2026-10-25 08:24:24 +01:00
1 parent d1eb87ba25
commit 2b1e36634a
53 files changed
+4254 -636

No files matched your search

+88 -7
View File
@@ -4,6 +4,10 @@
<div>
<h1>Protokoll</h1>
<p class="tiny">Abgeschlossene und laufende Drucke mit Dauer, Material und Kosten.</p>
<details class="tiny proto-help">
<summary>Filament-Pause / doppelte Zeilen</summary>
<p>Nach Runout kann der Drucker kurz zwei History-Zeilen erzeugen. „Läufe holen“ legt sie zusammen; sonst „Verbinden“.</p>
</details>
</div>
<div class="page-head-side">
<button type="button" id="sync"><svg class="ico"><use href="#i-refresh"/></svg>Läufe holen</button>
@@ -96,7 +100,6 @@ function render() {
runPage = paged.page;
paged.items.forEach((run) => {
const tr = document.createElement("tr");
tr.className = "pick";
tr.innerHTML = "<td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td></td>";
tr.children[0].textContent = run.printer_name;
tr.children[0].className = "proto-printer";
@@ -109,19 +112,54 @@ function render() {
link.title = fullName;
link.textContent = shortRunName(run.filename) || fullName;
fileCell.appendChild(link);
if (run.file_id) {
const fileLink = document.createElement("a");
fileLink.href = "/dateien?file=" + encodeURIComponent(run.file_id);
fileLink.className = "tiny";
fileLink.textContent = "Datei";
fileLink.style.marginLeft = "0.35rem";
fileCell.appendChild(fileLink);
}
if (run.project_id) {
const projLink = document.createElement("a");
projLink.href = "/projekte#" + encodeURIComponent(run.project_id);
projLink.className = "tiny";
projLink.textContent = "Projekt";
projLink.style.marginLeft = "0.35rem";
fileCell.appendChild(projLink);
}
if (run.chargen_nr) {
const code = document.createElement("div");
code.className = "mono-badge";
code.textContent = run.chargen_nr;
fileCell.appendChild(code);
}
if ((run.spool_names || []).length > 1 || (run.segment_count || 0) > 1) {
const swap = document.createElement("div");
swap.className = "tiny warn-text";
swap.textContent = "Rollenwechsel: " + (run.spool_names || []).join(" → ");
fileCell.appendChild(swap);
}
if (run.merge_with_id) {
const hint = document.createElement("div");
hint.className = "tiny warn-text";
hint.textContent = "Vermutlich Fortsetzung von/zu #" + run.merge_with_id;
fileCell.appendChild(hint);
}
const stateKey = run.state === "in_progress" || run.state === "printing" ? "printing"
: run.state === "paused" ? "paused"
: run.state === "completed" ? "complete"
: run.state === "error" ? "error"
: run.state === "cancelled" || run.state === "interrupted" ? "cancelled"
: "idle";
tr.children[2].innerHTML = chipHtml(stateKey, states[run.state] || run.state);
tr.children[3].textContent = hours(run.duration_s);
let stateLabel = states[run.state] || run.state;
if (run.state === "cancelled" && run.merge_with_id) stateLabel = "Teilabbruch (Runout?)";
tr.children[2].innerHTML = chipHtml(stateKey, stateLabel);
let durationText = hours(run.duration_s);
if (run.pause_s && run.pause_s > 60) {
durationText += " (Pause " + hours(run.pause_s) + ")";
}
tr.children[3].textContent = durationText;
tr.children[3].className = "num";
const used = Number(run.weight_used_g) || Number(run.weight_g) || 0;
const planned = Number(run.weight_planned_g) || Number(run.weight_g) || 0;
@@ -134,7 +172,11 @@ function render() {
}
const cost = Number(run.filament_cost || 0) + Number(run.energy_cost || 0);
tr.children[5].textContent = cost ? euro(cost) : "";
const verdictLabel = document.createElement("label");
verdictLabel.className = "sr-only";
verdictLabel.textContent = "Urteil";
const verdict = document.createElement("select");
verdict.setAttribute("aria-label", "Urteil");
[["", "offen"], ["gut", "gut"], ["ausschuss", "Ausschuss"]].forEach(([value, label]) => {
const option = document.createElement("option");
option.value = value;
@@ -145,10 +187,12 @@ function render() {
verdict.addEventListener("change", async () => {
try {
await api("/laeufe/" + run.id, { method: "PATCH", json: { verdict: verdict.value } });
toast("Urteil gespeichert.");
} catch (error) {
showNote("run-note", error);
}
});
tr.children[6].appendChild(verdictLabel);
tr.children[6].appendChild(verdict);
if (run.needs_spool || !run.deducted) {
const select = document.createElement("select");
@@ -183,14 +227,49 @@ function render() {
}
});
tr.children[7].appendChild(select);
} else if ((run.spool_names || []).length > 1) {
tr.children[7].className = "proto-spool";
tr.children[7].textContent = run.spool_names.join(" → ");
tr.children[7].title = run.spool_names.join(" → ");
} else {
const spool = spools.find((item) => item.id === run.spool_id);
tr.children[7].textContent = spool ? spool.name : "abgezogen";
tr.children[7].title = spool ? spool.name : "";
tr.children[7].className = "proto-spool";
if (spool) {
tr.children[7].innerHTML = linkTo("spule", spool.id, spool.name);
tr.children[7].title = spool.name;
} else if ((run.spool_names || []).length) {
tr.children[7].textContent = run.spool_names[0];
} else {
tr.children[7].textContent = "abgezogen";
}
}
if (!tr.children[7].className) tr.children[7].className = "proto-spool";
if (run.file_id) {
const actions = tr.children[8];
actions.className = "proto-actions";
if (run.merge_with_id) {
const mergeBtn = document.createElement("button");
mergeBtn.type = "button";
mergeBtn.className = "ghost tiny-btn";
mergeBtn.textContent = "Verbinden";
mergeBtn.title = "Mit Lauf #" + run.merge_with_id + " verbinden";
mergeBtn.addEventListener("click", () => {
armButton(mergeBtn, "Mit #" + run.merge_with_id + " verbinden?", async () => {
try {
await api("/laeufe/" + run.id + "/verbinden", {
method: "POST",
json: { other_id: run.merge_with_id, confirm: true },
});
toast("Läufe verbunden.");
await load();
} catch (error) {
toast(error.message, "bad");
}
});
});
actions.appendChild(mergeBtn);
}
const running = ["in_progress", "printing", "paused"].includes(run.state);
if (run.file_id && !running) {
const again = document.createElement("button");
again.type = "button";
again.className = "ghost tiny-btn";
@@ -235,7 +314,7 @@ function render() {
}
});
});
tr.children[8].appendChild(again);
actions.appendChild(again);
}
body.appendChild(tr);
});
@@ -250,6 +329,8 @@ async function load() {
runPage = 1;
render();
}
window.onDruckerLive = () => { load().catch(() => {}); };
window.onQueueChanged = () => { load().catch(() => {}); };
["run-filter-printer", "run-filter-verdict", "run-filter-spool"].forEach((id) => {
document.getElementById(id).addEventListener("change", () => { runPage = 1; render(); });