Files

357 lines
14 KiB
HTML

{% extends "layout.html" %}
{% block body %}
<div class="page-head">
<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>
</div>
</div>
<div class="toolbar page-tools">
<select id="run-filter-printer" title="Drucker">
<option value="">Alle Drucker</option>
</select>
<select id="run-filter-verdict" title="Urteil">
<option value="">Alle Urteile</option>
<option value="offen">offen</option>
<option value="gut">gut</option>
<option value="ausschuss">Ausschuss</option>
</select>
<label class="check"><input type="checkbox" id="run-filter-spool"> Spule fehlt</label>
</div>
<p id="run-note"></p>
<div class="card table-wrap">
<table class="proto-table">
<thead><tr><th>Drucker</th><th>Datei</th><th>Zustand</th><th>Dauer</th><th>Ist</th><th>Kosten</th><th>Urteil</th><th>Spule</th><th></th></tr></thead>
<tbody id="run-rows"></tbody>
</table>
<div class="pager" id="run-pager" hidden></div>
</div>
{% endblock %}
{% block script %}
<script>
const states = {
completed: "fertig", cancelled: "abgebrochen", error: "Fehler",
in_progress: "druckt", interrupted: "unterbrochen",
};
let runs = [];
let spools = [];
let runPage = 1;
function fillPrinterFilter() {
const select = document.getElementById("run-filter-printer");
const current = select.value;
const names = [...new Set(runs.map((run) => run.printer_name).filter(Boolean))].sort((a, b) => a.localeCompare(b, "de"));
select.innerHTML = '<option value="">Alle Drucker</option>';
names.forEach((name) => {
const option = document.createElement("option");
option.value = name;
option.textContent = name;
select.appendChild(option);
});
if ([...select.options].some((o) => o.value === current)) select.value = current;
}
function filteredRuns() {
const printer = document.getElementById("run-filter-printer").value;
const verdict = document.getElementById("run-filter-verdict").value;
const needSpool = document.getElementById("run-filter-spool").checked;
return runs.filter((run) => {
if (printer && run.printer_name !== printer) return false;
if (verdict === "offen" && run.verdict) return false;
if (verdict && verdict !== "offen" && run.verdict !== verdict) return false;
if (needSpool && !(run.needs_spool || (!run.deducted && run.weight_g > 0))) return false;
return true;
});
}
function shortRunName(filename) {
let name = (filename || "").split("/").pop() || "";
if (!name) return "";
name = name.replace(/^\d{8}-\d{6}-/, "");
name = name.replace(/^\d{4}-\d{4}-/, "");
name = name.replace(/\.gcode$/i, "");
name = name.replace(/_\d+(?:\.\d+)?_(?:\d+h)?(?:\d+m)?(?:\d+s)?$/i, "");
return name || (filename || "").split("/").pop() || "";
}
function render() {
fillPrinterFilter();
const body = document.getElementById("run-rows");
body.innerHTML = "";
const shown = filteredRuns();
if (!runs.length) {
body.innerHTML = '<tr><td class="empty" colspan="9"><div class="empty-state">' + icon("list") + '<p>Noch kein Lauf.</p><p class="tiny">„Läufe holen“ liest die Drucker aus.</p></div></td></tr>';
renderPager(document.getElementById("run-pager"), null, () => {});
return;
}
if (!shown.length) {
body.innerHTML = '<tr><td class="empty" colspan="9"><div class="empty-state">' + icon("filter") + '<p>Kein Lauf passt zum Filter.</p></div></td></tr>';
renderPager(document.getElementById("run-pager"), null, () => {});
return;
}
const paged = pageSlice(shown, runPage, DEFAULT_PAGE_SIZE);
runPage = paged.page;
paged.items.forEach((run) => {
const tr = document.createElement("tr");
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";
const fileCell = tr.children[1];
fileCell.className = "proto-file";
const fullName = (run.filename || "").split("/").pop() || ("#" + run.id);
const link = document.createElement("a");
link.href = "/protokoll/" + run.id;
link.className = "cell-file";
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";
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;
tr.children[4].textContent = used ? grams(used) : "";
if (used && planned && Math.abs(used - planned) > 0.5) {
tr.children[4].title = "Soll " + grams(planned);
}
if (run.weight_source === "estimate" || (run.weight_source === "measured" && false)) {
tr.children[4].textContent += run.weight_source === "estimate" ? " ≈" : "";
}
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;
option.textContent = label;
verdict.appendChild(option);
});
verdict.value = run.verdict || "";
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");
const blank = document.createElement("option");
blank.value = "";
blank.textContent = run.needs_spool ? "Spule wählen" : "keine";
select.appendChild(blank);
const mine = spools.filter((s) => s.printer_host === run.printer_host);
const others = spools.filter((s) => s.printer_host !== run.printer_host);
const addOpt = (spool, prefix) => {
const option = document.createElement("option");
option.value = spool.id;
option.textContent = (prefix ? prefix + " " : "") + spool.name;
select.appendChild(option);
};
mine.forEach((s) => addOpt(s, ""));
if (mine.length && others.length) {
const sep = document.createElement("option");
sep.disabled = true;
sep.textContent = "— andere —";
select.appendChild(sep);
}
others.forEach((s) => addOpt(s, s.printer_host ? "·" : "Regal ·"));
if (run.spool_id) select.value = String(run.spool_id);
select.addEventListener("change", async () => {
if (!select.value) return;
try {
await api("/laeufe/" + run.id, { method: "PATCH", json: { spool_id: Number(select.value) } });
load();
} catch (error) {
showNote("run-note", error);
}
});
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].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";
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";
again.textContent = "Nochmal";
again.title = "Denselben Druck in „wenn frei“ einreihen";
again.addEventListener("click", () => {
armButton(again, "Nochmal einreihen?", async () => {
again.disabled = true;
try {
let aceMap = [];
let bedLevel = false;
try {
const last = await api(
"/senden-zuletzt?host=" + encodeURIComponent(run.printer_host || "")
+ "&file_id=" + encodeURIComponent(run.file_id)
);
if (last) {
aceMap = last.ace_map || [];
bedLevel = !!last.bed_leveling;
}
} catch (error) { /* ohne Map weiter */ }
const payload = await api("/senden", {
method: "POST",
json: {
file_id: run.file_id,
hosts: run.printer_host ? [run.printer_host] : [],
spool_id: run.spool_id || null,
confirm: true,
when_free: true,
ace_maps: run.printer_host && aceMap.length
? { [run.printer_host]: aceMap }
: {},
bed_leveling: bedLevel,
},
});
toast((payload.count || (payload.jobs || []).length || 1) + " in Warteschlange „wenn frei“.");
if (typeof refreshPrintQueue === "function") refreshPrintQueue();
} catch (error) {
toast(error.message, "bad");
} finally {
again.disabled = false;
}
});
});
actions.appendChild(again);
}
body.appendChild(tr);
});
renderPager(document.getElementById("run-pager"), paged, (p) => {
runPage = p;
render();
});
}
async function load() {
[runs, spools] = await Promise.all([api("/laeufe?limit=500"), api("/spulen")]);
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(); });
document.getElementById(id).addEventListener("input", () => { runPage = 1; render(); });
});
document.getElementById("sync").addEventListener("click", async () => {
const button = document.getElementById("sync");
button.disabled = true;
try {
const result = await api("/laeufe/abgleich", { method: "POST" });
document.getElementById("run-note").textContent = result.gelesen + " Läufe, " + result.abgezogen + " abgezogen.";
document.getElementById("run-note").className = "ok";
await load();
} catch (error) {
showNote("run-note", error);
} finally {
button.disabled = false;
}
});
load().catch((error) => showNote("run-note", error));
</script>
{% endblock %}