Grotti 3.6.0: Lauf-Identität nach Filament-Pause, robustere Queue, Projekte/Aufträge list-first und API-Polish.

This commit is contained in:
Dominic Gundel committed 2026-10-25 08:24:24 +01:00
1 parent d1eb87ba25
commit 2b1e36634a
53 files changed
+4254 -636

No files matched your search

+194 -3
View File
@@ -49,6 +49,10 @@
<div class="desk-facts" data-role="facts"></div>
<div data-role="flow"></div>
<div data-role="strips"></div>
<div class="desk-next" data-role="next" hidden>
<div class="desk-next-head"><svg class="ico"><use href="#i-play"/></svg><span class="grow">Als Nächstes</span></div>
<div data-role="next-body"></div>
</div>
</div>
<div class="desk-foot">
<div class="card-actions" data-role="controls"></div>
@@ -367,10 +371,15 @@ function stripsHtml(printer, state) {
}
const filamentHint = state === "paused" ? filamentPauseHint(printer) : "";
if (filamentHint) {
const slot = (printer.slots || []).find((s) => s && (s.empty || s.runout)) || (printer.slots || [])[0];
const slotN = slot && (slot.index != null ? Number(slot.index) + 1 : slot.slot);
const matHref = "/material?host=" + encodeURIComponent(printer.host || "")
+ (slotN ? "&slot=" + encodeURIComponent(slotN) : "");
const slotTxt = slotN ? "ACE Platz " + slotN + ": " : "";
bits.push(`<div class="strip warn filament-swap">
<div class="strip-head">${icon("alert")}<span class="grow"><b>Neue Rolle in Material dem Drucker zuweisen</b></span></div>
<span class="strip-msg">${escapeHtml(filamentHint)} — sonst zählt der Verbrauch weiter zur alten Spule. Ort = dieser Drucker, speichern, dann Weiter.</span>
<div class="strip-actions"><a class="ghost" href="/material">Zu Material</a></div>
<div class="strip-head">${icon("alert")}<span class="grow"><b>${slotTxt}Neue Rolle in Material zuweisen</b></span></div>
<span class="strip-msg">${escapeHtml(filamentHint)} — 1) Rolle dem Drucker zuweisen, 2) speichern, 3) Weiter. Verbrauch bleibt dieselbe Charge.</span>
<div class="strip-actions"><a class="ghost" href="${matHref}">Zu Material</a></div>
</div>`);
}
const failed = !queueBusy && printer.queue_failed;
@@ -1065,6 +1074,156 @@ function patchDesk(printer) {
// Kamera nur über Timer aktualisieren — sonst doppelter Reload + Layout-Ruckler
syncDeskViewer(printer).catch(() => {});
refreshDeskVerlauf(false);
refreshDeskNext(printer, state);
}
let deskNextAt = 0;
let deskNextHost = "";
let deskNextKey = "";
function deskCanOfferStart(printer, state) {
if (!printer) return false;
if (["printing", "paused", "heating", "offline", "error"].includes(state)) return false;
if (printer.queue_running || startingJob(printer)) return false;
if (needsClear(printer)) return false;
return true;
}
function deskMaterialTypes(printer) {
const types = new Set();
(printer.spools || []).forEach((s) => {
if (s && s.material_type) types.add(String(s.material_type).toLowerCase());
});
(printer.ace_slots || printer.slots || []).forEach((s) => {
const t = (s && (s.type || s.material_type)) || "";
if (t) types.add(String(t).toLowerCase());
});
return types;
}
function fileMatchesPrinter(file, printer) {
if (!file) return false;
const family = printer.family || "";
if (file.printer_family && family && file.printer_family !== family) return false;
const types = deskMaterialTypes(printer);
if (!types.size) return true;
const mats = (file.filaments || []).map((f) => String(f.type || f.material_type || "").toLowerCase()).filter(Boolean);
if (!mats.length) return true;
return mats.some((m) => [...types].some((t) => t.includes(m) || m.includes(t)));
}
async function refreshDeskNext(printer, state, force) {
const box = deskEl.querySelector('[data-role="next"]');
const body = deskEl.querySelector('[data-role="next-body"]');
if (!box || !body) return;
if (!deskCanOfferStart(printer, state)) {
box.hidden = true;
body.innerHTML = "";
deskNextKey = "";
return;
}
const host = printer.host;
const key = host + "|" + (printer.queue_waiting || 0) + "|" + (printer.filename || "");
const now = Date.now();
if (!force && deskNextHost === host && deskNextKey === key && now - deskNextAt < 25000) {
box.hidden = false;
return;
}
deskNextAt = now;
deskNextHost = host;
deskNextKey = key;
box.hidden = false;
body.innerHTML = '<p class="tiny">Lade Vorschläge …</p>';
try {
const [last, queue, files] = await Promise.all([
api("/senden-zuletzt?host=" + encodeURIComponent(host)).catch(() => null),
api("/druck-queue?host=" + encodeURIComponent(host) + "&status=waiting&limit=5").catch(() => []),
api("/dateien?family=" + encodeURIComponent(printer.family || "") + "&limit=40").catch(() => []),
]);
if (deskHost !== host) return;
const bits = [];
if (last && last.file_id) {
const name = jobLabel(last.filename) || ("#" + last.file_id);
bits.push(`<div class="desk-next-row">
<div class="grow"><b>Nochmal</b><span class="tiny">${escapeHtml(name)}</span></div>
<button type="button" class="btn-primary tiny-btn" data-next-again="${last.file_id}">Start</button>
<a class="ghost tiny-btn" href="/dateien?host=${encodeURIComponent(host)}&file=${encodeURIComponent(last.file_id)}">Anpassen</a>
</div>`);
}
const waiting = (Array.isArray(queue) ? queue : []).filter((j) => j.status === "waiting");
if (waiting.length) {
const job = waiting[0];
bits.push(`<div class="desk-next-row">
<div class="grow"><b>Warteschlange</b><span class="tiny">${escapeHtml(jobLabel(job.filename) || "Job")}</span></div>
<a class="ghost tiny-btn" href="/warteschlange">Zur Queue</a>
</div>`);
}
const matched = (Array.isArray(files) ? files : [])
.filter((f) => fileMatchesPrinter(f, printer))
.slice(0, 5);
if (matched.length) {
bits.push('<div class="desk-next-files"><span class="tiny">Passende Dateien</span>' + matched.map((f) => {
const mats = [...new Set((f.filaments || []).map((x) => x.type || x.material_type).filter(Boolean))].join(" / ");
return `<div class="desk-next-row">
<div class="grow"><span>${escapeHtml(jobLabel(f.name) || f.name)}</span><span class="tiny">${escapeHtml(mats || "")}</span></div>
<button type="button" class="ghost tiny-btn" data-next-start="${f.id}">Start</button>
<a class="ghost tiny-btn" href="/dateien?host=${encodeURIComponent(host)}&file=${encodeURIComponent(f.id)}">Anpassen</a>
</div>`;
}).join("") + "</div>");
}
if (!bits.length) {
body.innerHTML = `<p class="tiny">Keine Vorschläge. <a href="/dateien?host=${encodeURIComponent(host)}">Datei wählen →</a></p>`;
} else {
body.innerHTML = bits.join("");
}
} catch (error) {
if (deskHost !== host) return;
body.innerHTML = '<p class="tiny msg">' + escapeHtml(error.message || "Vorschläge fehlen") + "</p>";
}
}
async function deskStartFile(fileId, fromLast) {
if (!deskHost || !fileId) return;
const host = deskHost;
let aceMap = [];
let bedLevel = false;
let spoolId = null;
try {
const last = await api(
"/senden-zuletzt?host=" + encodeURIComponent(host)
+ "&file_id=" + encodeURIComponent(fileId)
);
if (last) {
aceMap = last.ace_map || [];
bedLevel = !!last.bed_leveling;
spoolId = last.spool_id || null;
}
} catch (error) { /* Defaults */ }
if (fromLast && !aceMap.length) {
try {
const lastHost = await api("/senden-zuletzt?host=" + encodeURIComponent(host));
if (lastHost && String(lastHost.file_id) === String(fileId)) {
aceMap = lastHost.ace_map || [];
bedLevel = !!lastHost.bed_leveling;
spoolId = lastHost.spool_id || null;
}
} catch (error) { /* ignore */ }
}
const printer = printerCache.get(host);
const canNow = printer && deskCanOfferStart(printer, shownState(printer));
const payload = {
file_id: Number(fileId),
hosts: [host],
confirm: true,
when_free: !canNow,
bed_leveling: bedLevel,
};
if (spoolId) payload.spool_id = spoolId;
if (aceMap.length) payload.ace_maps = { [host]: aceMap };
await api("/senden", { method: "POST", json: payload });
toast(canNow ? "Druck startet…" : "In die Warteschlange.", "good");
deskNextAt = 0;
if (printer) refreshDeskNext(printer, shownState(printer), true);
}
async function refreshDeskVerlauf(force) {
@@ -1102,8 +1261,12 @@ async function refreshDeskVerlauf(force) {
kwh,
].filter(Boolean);
if (caption) caption.textContent = bits.join(" · ");
const fileBit = run.file_id
? `<a class="ghost tiny-btn" href="/dateien?file=${encodeURIComponent(run.file_id)}">Datei</a>`
: "";
const head = `<div class="desk-verlauf-head">
<span class="tiny grow">${escapeHtml(jobLabel(run.filename) || "Druck")}${run.chargen_nr ? " · " + escapeHtml(run.chargen_nr) : ""}</span>
${fileBit}
<a class="ghost tiny-btn" href="/protokoll/${run.id}">Bericht →</a>
</div>`;
const html = head + tempChartHtml(data.samples || []);
@@ -1515,6 +1678,34 @@ window.addEventListener("keydown", (event) => {
});
async function handleWallClick(event) {
const nextAgain = event.target.closest("[data-next-again]");
if (nextAgain) {
armButton(nextAgain, "Nochmal starten?", async () => {
nextAgain.disabled = true;
try {
await deskStartFile(nextAgain.dataset.nextAgain, true);
} catch (error) {
toast(error.message || String(error), "bad");
} finally {
nextAgain.disabled = false;
}
});
return;
}
const nextStart = event.target.closest("[data-next-start]");
if (nextStart) {
armButton(nextStart, "Starten?", async () => {
nextStart.disabled = true;
try {
await deskStartFile(nextStart.dataset.nextStart, false);
} catch (error) {
toast(error.message || String(error), "bad");
} finally {
nextStart.disabled = false;
}
});
return;
}
const pill = event.target.closest("[data-desk-host]");
if (pill) {
openDesk(pill.dataset.deskHost);