Grotti 3.2.0: Jetzt-Liste, Filament-Pause, Strom-Torte, UI-Polish.

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.
This commit is contained in:
Dominic Gundel committed 2026-10-18 14:58:32 +02:00
1 parent 6e59df22e5
commit 4ddffbbeb1
68 files changed
+4348 -1215

No files matched your search

+69 -1
View File
@@ -29,6 +29,72 @@ function sourceLabel(source, estimate) {
return source || "";
}
const ENERGY_PART_COLORS = {
base: "#94a3b8",
nozzle: "#ea580c",
bed: "#2563eb",
dryer: "#16a34a",
};
function kwhLabel(value) {
return (Number(value) || 0).toLocaleString("de-DE", { maximumFractionDigits: 3 }) + " kWh";
}
function energyPieSvg(parts) {
const items = (parts || []).filter((p) => Number(p.kwh) > 0);
const total = items.reduce((sum, p) => sum + Number(p.kwh || 0), 0);
if (!items.length || total <= 0) return "";
const cx = 42;
const cy = 42;
const r = 36;
let angle = -Math.PI / 2;
const slices = items.map((p) => {
const share = Number(p.kwh) / total;
const sweep = share * Math.PI * 2;
const x1 = cx + r * Math.cos(angle);
const y1 = cy + r * Math.sin(angle);
angle += sweep;
const x2 = cx + r * Math.cos(angle);
const y2 = cy + r * Math.sin(angle);
const large = sweep > Math.PI ? 1 : 0;
const color = ENERGY_PART_COLORS[p.key] || "#64748b";
if (share >= 0.999) {
return `<circle cx="${cx}" cy="${cy}" r="${r}" fill="${color}"/>`;
}
return `<path d="M ${cx} ${cy} L ${x1.toFixed(2)} ${y1.toFixed(2)} A ${r} ${r} 0 ${large} 1 ${x2.toFixed(2)} ${y2.toFixed(2)} Z" fill="${color}"/>`;
}).join("");
return `<svg class="energy-pie" viewBox="0 0 84 84" role="img" aria-label="Stromanteile">${slices}</svg>`;
}
function energyBreakdownHtml(energy) {
const parts = energy.parts || [];
if (!parts.length) return "";
const src = energy.source === "ed" ? "aus ED" : "pauschal";
const rate = energy.euro_per_kwh != null
? Number(energy.euro_per_kwh).toLocaleString("de-DE", { style: "currency", currency: "EUR", maximumFractionDigits: 3 }) + "/kWh"
: "";
const rows = parts.map((p) => {
const color = ENERGY_PART_COLORS[p.key] || "#64748b";
return `<tr>
<td><span class="energy-swatch" style="background:${color}"></span>${escapeHtml(p.label)}</td>
<td class="num">${kwhLabel(p.kwh)}</td>
<td class="num">${euro(p.cost)}</td>
</tr>`;
}).join("");
return `<div class="energy-break">
<div class="energy-break-chart">${energyPieSvg(parts)}</div>
<div class="energy-break-table">
<div class="lib-head"><h3>Strom</h3><span class="tiny">${escapeHtml(src)}${rate ? " · " + rate : ""}</span></div>
<div class="table-wrap"><table>
<thead><tr><th>Anteil</th><th class="num">kWh</th><th class="num">€</th></tr></thead>
<tbody>${rows}
<tr class="energy-total"><td>Summe</td><td class="num">${kwhLabel(energy.kwh)}</td><td class="num">${euro(energy.cost)}</td></tr>
</tbody>
</table></div>
</div>
</div>`;
}
function render(data) {
const run = data.run;
const name = (run.filename || "").split("/").pop() || "ohne Datei";
@@ -80,10 +146,12 @@ function render(data) {
<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(energyEuro)}${energyHint ? " · " + energyHint : ""})</p>
<p>Kosten <b>${euro(cost)}</b> (Filament ${euro(run.filament_cost)} · Strom ${euro(energyEuro)}${energyHint ? " · " + energyHint : ""})${run.cost_per_part != null ? " · <b>" + euro(run.cost_per_part) + "</b>/Teil" : ""}</p>
${run.pieces_ok || run.pieces_bad ? `<p>Stück <b>${run.pieces_ok || 0}</b> gut${run.pieces_bad ? " · " + run.pieces_bad + " schlecht" : ""}</p>` : ""}
${run.message ? "<p class=\"msg\">" + escapeHtml(run.message) + "</p>" : ""}
${data.project ? "<p class=\"tiny\">Projekt: " + escapeHtml(data.project.name) + "</p>" : ""}
</div>
${energyBreakdownHtml(energy)}
</div>
<div class="card">
<h2>Spulen</h2>