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:
1 parent
d1eb87ba25
commit
2b1e36634a
53 files changed
+4254
-636
No files matched your search
+194
-3
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user