Grotti 2.10.0: Orca-Senden, Slice-Ordner, ED-Strom, Hygiene.

Slice-Watch mit Ordnern/Filtern, Orca-Nachbearbeitung, Cockpit-Verlauf
mit ED, Zeitzone im Browser. Tasten-Grenze und README-API angeglichen,
toter Code entfernt, Requirements gepinnt.
This commit is contained in:
Dominic Gundel committed 2026-10-18 00:41:15 +02:00
1 parent efd0801cd1
commit 9e22cebb81
48 files changed
+2259 -422

No files matched your search

+12 -32
View File
@@ -29,33 +29,6 @@ function sourceLabel(source, estimate) {
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";
@@ -66,7 +39,14 @@ function render(data) {
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 energy = data.energy || {};
const energyEuro = energy.cost != null ? Number(energy.cost) : Number(run.energy_cost || 0);
const cost = Number(run.filament_cost || 0) + energyEuro;
const energyHint = energy.source === "ed"
? (Number(energy.kwh) || 0).toLocaleString("de-DE", { maximumFractionDigits: 2 }) + " kWh aus ED"
: (energy.kwh != null
? (Number(energy.kwh) || 0).toLocaleString("de-DE", { maximumFractionDigits: 2 }) + " kWh pauschal"
: "");
const spoolRows = (data.spools || []).map((s) => `
<tr>
@@ -79,7 +59,7 @@ function render(data) {
</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>`
`<li><span class="tiny">${escapeHtml(formatDateTime(e.created_at))}</span> ${escapeHtml(e.message || e.kind)}</li>`
).join("") || "<li class=\"tiny\">Keine Ereignisse.</li>";
const labels = (data.labels || []).filter((l) => !l.excluded);
@@ -96,11 +76,11 @@ function render(data) {
<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 class="tiny">${escapeHtml(formatDateRange(run.started_at, 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>
<p>Kosten <b>${euro(cost)}</b> (Filament ${euro(run.filament_cost)} · Strom ${euro(energyEuro)}${energyHint ? " · " + energyHint : ""})</p>
${run.message ? "<p class=\"msg\">" + escapeHtml(run.message) + "</p>" : ""}
${data.project ? "<p class=\"tiny\">Projekt: " + escapeHtml(data.project.name) + "</p>" : ""}
</div>
@@ -115,7 +95,7 @@ function render(data) {
</div>
<div class="card">
<h2>Verlauf</h2>
${chart(data.samples)}
${tempChartHtml(data.samples)}
</div>
<div class="card grid-2">
<div>