Grotti 2.5.1: Druckbericht, Statistik, Spulenakte und Protokoll-Fixes.

This commit is contained in:
Dominic Gundel committed 2026-10-17 15:32:51 +02:00
1 parent b277ca48aa
commit efd0801cd1
30 files changed
+3393 -178

No files matched your search

+146
View File
@@ -0,0 +1,146 @@
{% extends "layout.html" %}
{% block body %}
<div class="page-head">
<div>
<h1>Druckbericht</h1>
<p class="tiny" id="bericht-sub">Lauf wird geladen …</p>
</div>
<div class="page-head-side">
<a class="ghost" href="/protokoll">← Protokoll</a>
<a class="ghost" id="etikett-druck" href="#" hidden>Etikett Druck</a>
<a class="ghost" id="etikett-objekt" href="#" hidden>Etiketten Objekte</a>
</div>
</div>
<p id="bericht-note"></p>
<div id="bericht" class="bericht"></div>
{% endblock %}
{% block script %}
<script>
const runId = Number(location.pathname.split("/").pop());
const states = {
completed: "fertig", cancelled: "abgebrochen", error: "Fehler",
in_progress: "druckt", printing: "druckt", paused: "pausiert", interrupted: "unterbrochen",
};
function sourceLabel(source, estimate) {
if (estimate) return "Schätzwert";
if (source === "measured") return "Messwert";
if (source === "estimate") return "Schätzwert";
return source || "";
}
function chart(samples) {
if (!samples || samples.length < 2) {
return '<p class="tiny">Kein Temperaturverlauf (nur für neue Drucke).</p>';
}
const w = 640, h = 180, pad = 28;
const temps = samples.flatMap((s) => [s.nozzle, s.bed, s.chamber, s.nozzle_target, s.bed_target].filter((v) => v != null));
const minT = Math.min(...temps, 0);
const maxT = Math.max(...temps, 100);
const x = (i) => pad + (i / (samples.length - 1)) * (w - pad * 2);
const y = (v) => h - pad - ((Number(v) - minT) / Math.max(1, maxT - minT)) * (h - pad * 2);
function path(key, color) {
const pts = samples.map((s, i) => s[key] == null ? null : `${x(i)},${y(s[key])}`).filter(Boolean);
if (pts.length < 2) return "";
return `<polyline fill="none" stroke="${color}" stroke-width="1.6" points="${pts.join(" ")}" />`;
}
return `<svg class="temp-chart" viewBox="0 0 ${w} ${h}" role="img" aria-label="Temperaturverlauf">
${path("nozzle_target", "#fdba74")}
${path("bed_target", "#93c5fd")}
${path("nozzle", "#ea580c")}
${path("bed", "#2563eb")}
${path("chamber", "#16a34a")}
<text x="${pad}" y="14" class="chart-label">${Math.round(maxT)} °C</text>
<text x="${pad}" y="${h - 8}" class="chart-label">${Math.round(minT)} °C</text>
</svg>
<p class="tiny chart-legend"><span class="leg nozzle">Düse</span> <span class="leg bed">Bett</span> <span class="leg chamber">Kammer</span> · gestrichelt ≈ Soll</p>`;
}
function render(data) {
const run = data.run;
const name = (run.filename || "").split("/").pop() || "ohne Datei";
document.getElementById("bericht-sub").textContent =
(run.printer_name || run.printer_host) + " · " + name
+ (run.chargen_nr ? " · " + run.chargen_nr : "");
const planned = Number(run.weight_planned_g) || 0;
const used = Number(run.weight_used_g) || 0;
const diff = Math.round(used) - Math.round(planned);
const cost = Number(run.filament_cost || 0) + Number(run.energy_cost || 0);
const spoolRows = (data.spools || []).map((s) => `
<tr>
<td>${escapeHtml(s.name)}${s.ace_slot ? " · ACE " + s.ace_slot : ""}</td>
<td>${escapeHtml(s.material_type || "")}</td>
<td class="num">${grams(s.weight_planned_g)}</td>
<td class="num">${grams(s.weight_g)}${s.is_estimate ? ' <span class="tag">Schätzwert</span>' : ""}</td>
<td class="num">${euro(s.cost)}</td>
<td class="nowrap">${escapeHtml(s.chargen_nr || "")}</td>
</tr>`).join("") || '<tr><td class="empty" colspan="6">Keine Spule zugeordnet.</td></tr>';
const events = (data.events || []).map((e) =>
`<li><span class="tiny">${escapeHtml((e.created_at || "").replace("T", " ").slice(0, 19))}</span> ${escapeHtml(e.message || e.kind)}</li>`
).join("") || "<li class=\"tiny\">Keine Ereignisse.</li>";
const labels = (data.labels || []).filter((l) => !l.excluded);
const labelBits = labels.length
? labels.map((l) => `<li id="objekt-${encodeURIComponent(l.object_name || "")}"><code>${escapeHtml(l.chargen_nr)}</code> ${escapeHtml(l.object_name || "Druck")}</li>`).join("")
: (run.chargen_nr ? `<li><code>${escapeHtml(run.chargen_nr)}</code> gesamter Druck</li>` : "<li class=\"tiny\">Noch keine Charge.</li>");
const segments = (data.segments || []).map((s) => {
const end = s.mm_end == null ? "…" : Math.round(s.mm_end);
return `<li>${escapeHtml(s.spool_name)} · ${Math.round(s.mm_start)}–${end} mm${s.ace_slot ? " · Platz " + s.ace_slot : ""}</li>`;
}).join("");
document.getElementById("bericht").innerHTML = `
<div class="card bericht-head">
<div class="bericht-meta">
<p><b>${escapeHtml(states[run.state] || run.state)}</b>${run.verdict ? " · Urteil " + escapeHtml(run.verdict) : ""}</p>
<p class="tiny">${escapeHtml(run.started_at || "—")} → ${escapeHtml(run.ended_at || "—")}</p>
<p>Dauer <b>${hours(run.duration_s)}</b>${run.estimated_s ? " · Soll " + hours(run.estimated_s) : ""}</p>
<p>Material <b>${grams(used)}</b> von ${grams(planned)}${planned ? " (" + (diff > 0 ? "+" : "") + diff + " g)" : ""}
${run.weight_source ? ' · <span class="tag">' + sourceLabel(run.weight_source) + "</span>" : ""}</p>
<p>Kosten <b>${euro(cost)}</b> (Filament ${euro(run.filament_cost)} · Strom ${euro(run.energy_cost)})</p>
${run.message ? "<p class=\"msg\">" + escapeHtml(run.message) + "</p>" : ""}
${data.project ? "<p class=\"tiny\">Projekt: " + escapeHtml(data.project.name) + "</p>" : ""}
</div>
</div>
<div class="card">
<h2>Spulen</h2>
<div class="table-wrap"><table>
<thead><tr><th>Spule</th><th>Sorte</th><th>Soll</th><th>Ist</th><th>Kosten</th><th>Charge</th></tr></thead>
<tbody>${spoolRows}</tbody>
</table></div>
${segments ? "<h3>Abschnitte</h3><ul class=\"plain\">" + segments + "</ul>" : ""}
</div>
<div class="card">
<h2>Verlauf</h2>
${chart(data.samples)}
</div>
<div class="card grid-2">
<div>
<h2>Chargen</h2>
<ul class="plain">${labelBits}</ul>
</div>
<div>
<h2>Ereignisse</h2>
<ul class="plain">${events}</ul>
</div>
</div>`;
const druck = document.getElementById("etikett-druck");
const objekt = document.getElementById("etikett-objekt");
if (run.chargen_nr) {
druck.hidden = false;
druck.href = "/etikett/lauf/" + run.id + "?mode=druck";
objekt.hidden = false;
objekt.href = "/etikett/lauf/" + run.id + "?mode=objekt&qty=1";
} else {
druck.hidden = true;
objekt.hidden = true;
}
}
api("/laeufe/" + runId).then(render).catch((error) => showNote("bericht-note", error));
</script>
{% endblock %}