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.
249 lines
9.7 KiB
HTML
249 lines
9.7 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>
|
|
</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>
|
|
<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>
|
|
{% endblock %}
|
|
{% block script %}
|
|
<script>
|
|
const states = {
|
|
completed: "fertig", cancelled: "abgebrochen", error: "Fehler",
|
|
in_progress: "druckt", interrupted: "unterbrochen",
|
|
};
|
|
let runs = [];
|
|
let spools = [];
|
|
|
|
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 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>';
|
|
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>';
|
|
return;
|
|
}
|
|
shown.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;
|
|
const fileCell = tr.children[1];
|
|
const link = document.createElement("a");
|
|
link.href = "/protokoll/" + run.id;
|
|
link.className = "cell-file";
|
|
link.title = (run.filename || "").split("/").pop() || ("#" + run.id);
|
|
link.textContent = (run.filename || "").split("/").pop() || ("#" + run.id);
|
|
fileCell.appendChild(link);
|
|
if (run.chargen_nr) {
|
|
const code = document.createElement("div");
|
|
code.className = "mono-badge";
|
|
code.textContent = run.chargen_nr;
|
|
fileCell.appendChild(code);
|
|
}
|
|
const stateKey = run.state === "in_progress" || run.state === "printing" ? "printing"
|
|
: 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);
|
|
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 verdict = document.createElement("select");
|
|
[["", "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 } });
|
|
} catch (error) {
|
|
showNote("run-note", error);
|
|
}
|
|
});
|
|
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 {
|
|
const spool = spools.find((item) => item.id === run.spool_id);
|
|
tr.children[7].textContent = spool ? spool.name : "abgezogen";
|
|
}
|
|
if (run.file_id) {
|
|
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;
|
|
}
|
|
});
|
|
});
|
|
tr.children[8].appendChild(again);
|
|
}
|
|
body.appendChild(tr);
|
|
});
|
|
}
|
|
|
|
async function load() {
|
|
[runs, spools] = await Promise.all([api("/laeufe"), api("/spulen")]);
|
|
render();
|
|
}
|
|
|
|
["run-filter-printer", "run-filter-verdict", "run-filter-spool"].forEach((id) => {
|
|
document.getElementById(id).addEventListener("change", render);
|
|
document.getElementById(id).addEventListener("input", 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 %}
|