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

+101 -58
View File
@@ -61,35 +61,6 @@
<input id="spool-name" required>
</div>
<div class="grid-2">
<div class="field"><label for="spool-brand">Marke</label><input id="spool-brand"></div>
<div class="field"><label for="spool-type">Typ</label><input id="spool-type" placeholder="PLA"></div>
</div>
<div class="grid-2">
<div class="field">
<label for="spool-chargen">Grotti-Charge</label>
<input id="spool-chargen" readonly placeholder="wird vergeben">
</div>
<div class="field">
<label for="spool-hersteller">Hersteller-Charge</label>
<input id="spool-hersteller" placeholder="vom Rollenetikett">
</div>
</div>
<details class="mat-color" id="color-box">
<summary>
<svg class="ico caret"><use href="#i-down"/></svg>
<span class="grow">Farbe</span>
<span class="tiny" id="color-sum">noch keine</span>
</summary>
<div class="mat-color-body">
<div id="color-picked" class="picked"></div>
<div id="color-solids" class="color-grid"></div>
<div id="color-multi" class="color-grid"></div>
<label class="mix check"><input id="color-mix" type="checkbox"> Eigene Mischung</label>
</div>
</details>
<div class="grid-2" style="margin-top:0.55rem">
<div class="field">
<label for="spool-printer">Drucker</label>
<select id="spool-printer"></select>
@@ -108,11 +79,47 @@
<option value="8">ACE 2 · Platz 4</option>
</select>
</div>
<div class="field"><label for="spool-price">€ pro kg</label><input id="spool-price" type="number" min="0" step="0.01" value="0"></div>
<div class="field"><label for="spool-left">Rest gramm</label><input id="spool-left" type="number" step="1" value="1000"></div>
<div class="field"><label for="spool-density">Dichte g/cm³</label><input id="spool-density" type="number" min="0.1" step="0.01" value="1.24"></div>
<div class="field"><label for="spool-vol">Volumen mm³/s</label><input id="spool-vol" type="number" min="0.1" step="0.1" value="12"></div>
<div class="field"><label for="spool-left">Rest gramm</label><input id="spool-left" type="number" step="0.1" value="1000"></div>
</div>
<details class="mat-color" id="color-box">
<summary>
<svg class="ico caret"><use href="#i-down"/></svg>
<span class="grow">Farbe</span>
<span class="tiny" id="color-sum">noch keine</span>
</summary>
<div class="mat-color-body">
<div id="color-picked" class="picked"></div>
<div id="color-solids" class="color-grid"></div>
<div id="color-multi" class="color-grid"></div>
<label class="mix check"><input id="color-mix" type="checkbox"> Eigene Mischung</label>
</div>
</details>
<details class="mat-more" id="spool-more">
<summary>
<svg class="ico caret"><use href="#i-down"/></svg>
<span class="grow">Weitere Angaben</span>
</summary>
<div class="mat-more-body">
<div class="grid-2">
<div class="field"><label for="spool-brand">Marke</label><input id="spool-brand"></div>
<div class="field"><label for="spool-type">Typ</label><input id="spool-type" placeholder="PLA"></div>
<div class="field">
<label for="spool-chargen">Grotti-Charge</label>
<input id="spool-chargen" readonly placeholder="wird vergeben">
</div>
<div class="field">
<label for="spool-hersteller">Hersteller-Charge</label>
<input id="spool-hersteller" placeholder="vom Rollenetikett">
</div>
<div class="field"><label for="spool-price">€ pro kg</label><input id="spool-price" type="number" min="0" step="0.01" value="0"></div>
<div class="field"><label for="spool-density">Dichte g/cm³</label><input id="spool-density" type="number" min="0.1" step="0.01" value="1.24"></div>
<div class="field"><label for="spool-vol">Volumen mm³/s</label><input id="spool-vol" type="number" min="0.1" step="0.1" value="12"></div>
</div>
</div>
</details>
<div class="actions">
<button type="submit">Merken</button>
<button type="button" class="ghost" id="spool-shelf" hidden>Ins Regal</button>
@@ -122,8 +129,8 @@
</form>
<div id="spool-akte" class="spool-akte" hidden>
<h3>Akte</h3>
<p class="tiny" id="akte-summary"></p>
<ul class="plain" id="akte-runs"></ul>
<div class="kpi-row akte-kpis" id="akte-kpis"></div>
<div class="akte-runs" id="akte-runs"></div>
</div>
<p id="spool-note"></p>
</section>
@@ -429,6 +436,35 @@ function payload() {
};
}
const akteStates = {
completed: "fertig", cancelled: "abgebrochen", error: "Fehler",
in_progress: "druckt", printing: "druckt", paused: "pausiert",
interrupted: "unterbrochen",
};
function akteWhen(iso) {
if (!iso) return "—";
const text = formatDateTime(iso, { year: undefined });
return text === "—" ? "—" : text.replace(/,\s*/, " ");
}
function akteFile(name) {
const base = (name || "").split("/").pop() || "";
if (!base) return "Druck";
if (base.length <= 42) return base;
return base.slice(0, 39) + "…";
}
function akteStatus(run) {
if (run.verdict === "ausschuss") return "Ausschuss";
if (run.verdict === "gut") return "gut";
return akteStates[run.state] || run.state || "";
}
function akteKpi(label, value) {
return '<div class="kpi"><div class="kpi-val">' + escapeHtml(value) + '</div><div class="kpi-label">' + escapeHtml(label) + "</div></div>";
}
async function loadAkte(spoolId) {
const box = document.getElementById("spool-akte");
if (!spoolId) {
@@ -442,29 +478,34 @@ async function loadAkte(spoolId) {
let leer = "—";
if (stats.empty_in_days == null) leer = "kein Verbrauch";
else if (stats.empty_in_days <= 0) leer = "leer";
else leer = "noch ~" + Math.round(stats.empty_in_days) + " Tage";
document.getElementById("akte-summary").textContent =
"Verdruckt " + grams(stats.total_g)
+ " · Ausschuss " + grams(stats.scrap_g)
+ " · " + euro(stats.total_cost)
+ " · Füllstand " + fillPct + " % · Leer-in: " + leer;
else leer = "~" + Math.round(stats.empty_in_days) + " Tage";
document.getElementById("akte-kpis").innerHTML =
akteKpi("Verdruckt", grams(stats.total_g))
+ akteKpi("Ausschuss", grams(stats.scrap_g))
+ akteKpi("Kosten", euro(stats.total_cost))
+ akteKpi("Füllstand", fillPct + " %")
+ akteKpi("Leer-in", leer);
const list = document.getElementById("akte-runs");
list.innerHTML = "";
(data.runs || []).slice(0, 12).forEach((run) => {
const li = document.createElement("li");
const a = document.createElement("a");
a.href = "/protokoll/" + run.id;
a.textContent = (run.filename || "").split("/").pop() || ("#" + run.id);
li.appendChild(a);
li.appendChild(document.createTextNode(
" · " + grams(run.spool_weight_g)
+ (run.spool_is_estimate ? " ≈" : "")
+ " · " + (run.verdict || run.state || "")
));
list.appendChild(li);
});
if (!(data.runs || []).length) {
list.innerHTML = '<li class="tiny">Noch kein Druck auf dieser Spule.</li>';
const runs = data.runs || [];
if (!runs.length) {
list.innerHTML = '<p class="tiny">Noch kein Druck auf dieser Spule.</p>';
} else {
list.innerHTML = runs.slice(0, 12).map((run) => {
const href = "/protokoll/" + run.id;
const charge = run.chargen_nr
? '<a class="tag akte-charge" href="' + href + '">' + escapeHtml(run.chargen_nr) + "</a>"
: '<span class="akte-charge tiny">—</span>';
const file = '<a class="akte-file" href="' + href + '" title="' + escapeHtml((run.filename || "").split("/").pop() || "") + '">'
+ escapeHtml(akteFile(run.filename)) + "</a>";
const gramsTxt = grams(run.spool_weight_g) + (run.spool_is_estimate ? " ≈" : "");
return '<div class="akte-run">'
+ '<span class="akte-when tiny">' + escapeHtml(akteWhen(run.started_at)) + "</span>"
+ charge
+ file
+ '<span class="akte-g">' + escapeHtml(gramsTxt) + "</span>"
+ '<span class="akte-state tiny">' + escapeHtml(akteStatus(run)) + "</span>"
+ "</div>";
}).join("");
}
box.hidden = false;
} catch (error) {
@@ -483,7 +524,9 @@ function fill(spool) {
document.getElementById("spool-printer").value = spool ? spool.printer_host : "";
document.getElementById("spool-slot").value = spool && spool.ace_slot ? String(spool.ace_slot) : "";
document.getElementById("spool-price").value = spool ? spool.price_per_kg : 0;
document.getElementById("spool-left").value = spool ? spool.remaining_g : 1000;
document.getElementById("spool-left").value = spool
? Math.round((Number(spool.remaining_g) || 0) * 10) / 10
: 1000;
document.getElementById("spool-density").value = spool ? spool.density : 1.24;
document.getElementById("spool-vol").value = spool ? spool.volumetric : 12;
document.getElementById("spool-material").value = spool && spool.material_id ? String(spool.material_id) : "";