Grotti 2.0.0: eigenes Repo, Umbenennung von Farm.

This commit is contained in:
Dominic Gundel committed 2026-10-15 08:45:56 +02:00
commit b2e6443c68
69 files changed
+18719

No files matched your search

+221
View File
@@ -0,0 +1,221 @@
{% 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="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>Gramm</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.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[1].textContent = (run.filename || "").split("/").pop();
tr.children[2].textContent = states[run.state] || run.state;
tr.children[3].textContent = hours(run.duration_s);
tr.children[4].textContent = run.weight_g ? grams(run.weight_g) : "";
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 %}