${thumbHtml(file)}
${escapeHtml(file.name)} ${alwaysMeta(file, project)}
@@ -882,17 +951,25 @@ function updateSendButton() {
let note = "";
if (!file) problem = "Erst eine Datei wählen.";
else if (file.kind === "3mf") problem = "Die 3MF hat keinen G-code. Mit G-code slicen.";
- if (sel.mode === "now") {
+ if (sel.mode === "this") {
if (!problem && !chosen.length) problem = "Einen oder mehrere Drucker wählen.";
- const busy = chosen.filter((p) => !isFree(p));
- if (!problem && busy.length) note = namesText(busy) + " " + (busy.length === 1 ? "ist" : "sind") + " nicht frei. „Wenn frei“ reiht stattdessen ein.";
- text = chosen.length ? "Jetzt starten auf " + namesText(chosen) : "Jetzt starten";
- } else if (sel.mode === "free") {
- text = chosen.length ? "Einreihen für " + namesText(chosen) : "Einreihen: nächster freier " + familyOfSelection();
- note = chosen.length ? "Startet von selbst, sobald der Drucker bereit und die Platte frei ist." : "Ohne Drucker: der nächste freie passende übernimmt.";
+ const ready = chosen.filter(canStartNow);
+ const later = chosen.filter((p) => !canStartNow(p));
+ if (ready.length && !later.length) {
+ text = "Jetzt starten auf " + namesText(ready);
+ note = "";
+ } else if (!ready.length && later.length) {
+ text = "Einreihen für " + namesText(later);
+ note = "Startet, sobald der Drucker bereit und die Platte frei ist.";
+ } else if (ready.length && later.length) {
+ text = "Starten / einreihen auf " + namesText(chosen);
+ note = namesText(ready) + " sofort, " + namesText(later) + " in die Warteschlange.";
+ } else {
+ text = "Starten";
+ }
} else {
- text = "Einreihen: nächster freier " + familyOfSelection();
- note = "Grotti nimmt den ersten freien Drucker mit passender Maschine.";
+ text = "Einreihen: nächster passender " + familyOfSelection();
+ note = "Grotti wählt den nächsten freien Drucker mit passender Sorte (z. B. PLA Meta ≠ Matte).";
}
const mismatch = chosen.filter((p) => familyMismatch(p, file));
if (!problem && mismatch.length && !document.getElementById("send-override").checked) {
@@ -902,7 +979,7 @@ function updateSendButton() {
button.disabled = !!problem || !!sendPoll;
hint.textContent = problem || note;
hint.classList.toggle("bad", !!problem);
- targetHint.textContent = sel.mode === "next" && sel.tab === "send" ? "Ziel wählt Grotti." : chosen.length > 1 ? "Je Drucker ein eigener Auftrag." : "";
+ targetHint.textContent = sel.mode === "next" && sel.tab === "send" ? "Ziel wählt Grotti nach Sorte." : chosen.length > 1 ? "Je Drucker ein eigener Auftrag." : "";
}
function updateAblaufButton() {
@@ -971,7 +1048,7 @@ function selectFile(id) {
}
document.querySelectorAll("#lib-list .lib-item").forEach((n) => n.classList.toggle("on", n.dataset.file === String(id)));
refreshFlow();
- showToolpath(file, 0);
+ scheduleToolpath(file, 0, 0);
if (window.matchMedia("(max-width: 1080px)").matches) {
document.querySelector(".send-panel").scrollIntoView({ behavior: "smooth", block: "start" });
}
@@ -1015,19 +1092,72 @@ async function load() {
renderLibrary();
renderTabs();
refreshFlow();
- const file = selectedFile();
- if (file) showToolpath(file, 0);
- // Deep-Link von Orca /orca_senden: Senden in den Blick, Optionen offen für ACE
- if (new URLSearchParams(location.search).get("file") && file) {
+ let file = selectedFile();
+ // Orca/Anycubic: Metadaten oft ohne paint_index → ACE zeigt T0/T1 statt T1/T10
+ if (file && file.id && filamentsNeedPaintIndex(file.filaments)) {
+ try {
+ const fresh = await api("/dateien/" + file.id + "/meta", { method: "POST" });
+ if (fresh && fresh.id) {
+ const idx = files.findIndex((row) => String(row.id) === String(fresh.id));
+ if (idx >= 0) files[idx] = fresh;
+ else files.unshift(fresh);
+ file = selectedFile();
+ renderLibrary();
+ refreshFlow();
+ }
+ } catch (error) { /* manuell: Ordner neu lesen */ }
+ }
+ const q = new URLSearchParams(location.search);
+ const deepLink = !!(q.get("file") || q.get("orca"));
+ // Deep-Link / Orca-App: ACE/Senden zuerst — 3D-Pfad friert sonst den Tab ein
+ if (file) scheduleToolpath(file, 0, deepLink ? 900 : 0);
+ if (deepLink) {
sel.tab = "send";
renderTabs();
const opts = document.getElementById("options");
if (opts) opts.open = true;
const panel = document.querySelector(".send-panel");
if (panel) panel.scrollIntoView({ behavior: "smooth", block: "start" });
+ if (file) lastOrcaFocusId = String(file.id);
+ if (q.get("orca")) pullOrcaFocus();
}
}
+let lastOrcaFocusId = null;
+
+async function pullOrcaFocus() {
+ try {
+ const data = await api("/orca-fokus");
+ if (!data || data.file_id == null) return;
+ const id = String(data.file_id);
+ const age = Date.now() / 1000 - Number(data.ts || 0);
+ if (age > 180) return;
+ const already = id === String(sel.fileId) && selectedFile();
+ if (id === lastOrcaFocusId && already) return;
+ const switched = lastOrcaFocusId != null && id !== lastOrcaFocusId;
+ lastOrcaFocusId = id;
+ if (already) return;
+ let file = files.find((row) => String(row.id) === id);
+ if (!file) {
+ files = await api("/dateien");
+ renderLibrary();
+ file = files.find((row) => String(row.id) === id);
+ }
+ if (file) {
+ selectFile(file.id);
+ sel.tab = "send";
+ renderTabs();
+ if (switched) toast("Neue Slice-Datei ausgewählt.", "good");
+ }
+ } catch (error) { /* nächster Tick */ }
+}
+
+function filamentsNeedPaintIndex(filaments) {
+ const rows = filaments || [];
+ if (rows.length < 2) return false;
+ return rows.some((item) => !item || item.paint_index == null);
+}
+
async function refreshPrinters() {
if (document.hidden) return;
try {
@@ -1105,32 +1235,36 @@ async function watchJobs(ids) {
await tick();
}
+async function postSenden(hosts, whenFree) {
+ return api("/senden", {
+ method: "POST",
+ json: {
+ file_id: Number(sel.fileId),
+ hosts,
+ confirm: true,
+ override_machine: document.getElementById("send-override").checked,
+ bed_leveling: document.getElementById("send-bed-level").checked,
+ ace_maps: collectAceMaps(),
+ when_free: whenFree,
+ },
+ });
+}
+
async function doSend() {
const button = document.getElementById("send-go");
- const whenFree = sel.mode !== "now";
button.disabled = true;
document.getElementById("send-note").textContent = "";
try {
- const payload = await api("/senden", {
- method: "POST",
- json: {
- file_id: Number(sel.fileId),
- hosts: selectedHosts(),
- confirm: true,
- override_machine: document.getElementById("send-override").checked,
- bed_leveling: document.getElementById("send-bed-level").checked,
- ace_maps: collectAceMaps(),
- when_free: whenFree,
- },
- });
const projectId = document.getElementById("send-project")?.value;
if (projectId && sel.fileId) {
try {
await api("/dateien/" + sel.fileId, { method: "PATCH", json: { project_id: Number(projectId) } });
} catch (error) { /* Projektbindung optional */ }
}
- const jobs = payload.jobs || [];
- if (whenFree || payload.mode === "when_free") {
+
+ if (sel.mode === "next") {
+ const payload = await postSenden([], true);
+ const jobs = payload.jobs || [];
document.getElementById("send-progress").hidden = true;
toast((jobs.length > 1 ? jobs.length + " Aufträge" : "Auftrag") + " eingereiht. Steht unter „Wartet“.", "good");
updateSendButton();
@@ -1138,9 +1272,35 @@ async function doSend() {
refreshPrinters();
return;
}
- showJobsProgress(jobs);
+
+ const chosen = selectedPrinters();
+ const ready = chosen.filter(canStartNow).map((p) => p.host);
+ const later = chosen.filter((p) => !canStartNow(p)).map((p) => p.host);
+ let queued = 0;
+ let nowJobs = [];
+ if (later.length) {
+ const payload = await postSenden(later, true);
+ queued += (payload.jobs || []).length;
+ }
+ if (ready.length) {
+ const payload = await postSenden(ready, false);
+ nowJobs = payload.jobs || [];
+ }
+ if (queued && !nowJobs.length) {
+ document.getElementById("send-progress").hidden = true;
+ toast((queued > 1 ? queued + " Aufträge" : "Auftrag") + " eingereiht. Steht unter „Wartet“.", "good");
+ updateSendButton();
+ if (typeof refreshPrintQueue === "function") refreshPrintQueue();
+ refreshPrinters();
+ return;
+ }
+ if (queued && nowJobs.length) {
+ toast(queued + " eingereiht, " + nowJobs.length + " startet jetzt.", "good");
+ if (typeof refreshPrintQueue === "function") refreshPrintQueue();
+ }
+ showJobsProgress(nowJobs);
if (typeof refreshSendQueue === "function") refreshSendQueue();
- await watchJobs(jobs.map((job) => job.id));
+ await watchJobs(nowJobs.map((job) => job.id));
} catch (error) {
updateSendButton();
toast(error.message || String(error), "bad");
@@ -1445,7 +1605,7 @@ document.getElementById("targets").addEventListener("click", (event) => {
toast("Abläufe gehen nur auf Kobra 3.", "bad");
return;
}
- if (sel.tab === "send" && sel.mode === "next") sel.mode = "free";
+ if (sel.tab === "send" && sel.mode === "next") sel.mode = "this";
if (sel.hosts.has(host)) sel.hosts.delete(host);
else sel.hosts.add(host);
saveSendMemory();
@@ -1479,19 +1639,11 @@ document.getElementById("send-bed-level").addEventListener("change", updateOptio
document.getElementById("send-override").addEventListener("change", () => { refreshFlow(); });
document.getElementById("send-project")?.addEventListener("change", updateOptionsSummary);
-document.getElementById("send-go").addEventListener("click", (event) => {
- const button = event.currentTarget;
- const warn = spoolWarnText();
- if (sel.mode === "now") {
- armButton(button, warn ? "Warnung — trotzdem starten?" : "Wirklich starten? Nochmal klicken", doSend);
- } else if (warn) {
- armButton(button, "Warnung — trotzdem einreihen?", doSend);
- } else {
- doSend();
- }
+document.getElementById("send-go").addEventListener("click", () => {
+ doSend();
});
-document.getElementById("ablauf-go").addEventListener("click", (event) => {
- armButton(event.currentTarget, "Ablauf wirklich einreihen? Nochmal klicken", doAblauf);
+document.getElementById("ablauf-go").addEventListener("click", () => {
+ doAblauf();
});
document.getElementById("ablauf-add-print").addEventListener("click", () => addStep(defaultPrintStep()));
@@ -1591,5 +1743,6 @@ loadSendMemory();
loadAblaufMemory();
load().catch((error) => toast(error.message || String(error), "bad"));
setInterval(refreshPrinters, 5000);
+setInterval(pullOrcaFocus, 2000);
{% endblock %}
diff --git a/web/templates/doku.html b/web/templates/doku.html
index 568d2cc..fe2e384 100644
--- a/web/templates/doku.html
+++ b/web/templates/doku.html
@@ -13,15 +13,14 @@
Datei
Drucker
- Jetzt · Wenn frei · Nächster freier
+ Dieser Drucker · Nächster passender
Druck
Platte räumen
Datei wählen. Unter Dateien links in der Bibliothek. Der Slice-Ordner wird beobachtet, Hochladen geht per Knopf oder Ziehen.
Drucker wählen. Die Kacheln zeigen live, was jeder Drucker gerade tut. Passt die Maschine nicht zur Datei, ist die Kachel blass.
- Wann. Jetzt startet sofort. Wenn frei reiht für diesen Drucker ein. Nächster freier lässt Grotti den ersten freien passenden Drucker nehmen.
- Starten. „Jetzt“ will zwei Klicks. Der zweite ist die Bestätigung.
+ Start. Dieser Drucker : frei → sofort, sonst Warteschlange. Nächster passender : Grotti wählt nach Maschine und Sorte (PLA Meta ≠ Matte).
Während des Drucks zeigt die Wand Fortschritt, Fertig-Uhrzeit und Temperaturen. Ein Klick auf die Karte öffnet das Cockpit mit Kamera (falls vorhanden), Steuerung, Bettnetz und 3D-Pfad. Die Seitenleiste zeigt immer, was läuft und was wartet.
Danach ist die Platte belegt. Ein wartender Auftrag startet erst, wenn auf der Karte Platte geräumt gedrückt wurde, oder ein Ablauf sie selbst freischiebt.
@@ -49,7 +48,8 @@
Auftrag Ein Eintrag in der Warteschlange: eine Datei für einen Drucker oder für den nächsten freien.
Ablauf Auftrag mit mehreren Schritten und Wiederholungen. Nur Kobra 3.
- Wenn frei Startet, sobald der Drucker bereit und die Platte geräumt ist.
+ Dieser Drucker Frei und Platte klar → sofort; sonst Warteschlange für genau diesen Drucker.
+ Nächster passender Grotti claimt den nächsten freien Drucker mit passender Katalog-Sorte und Restfilament.
Platte belegt Nach jedem Druck gesetzt. Blockiert den nächsten Auftrag auf diesem Drucker.
Alert Wichtiges Ereignis (Fertig, Fehler, Abbruch, offline). Steht in der Seitenleiste und geht auf Wunsch an Telegram.
Ereignis Jeder Eintrag unter Aktivität. Ablauf-Schritte und Starts sind Ereignisse, aber keine Alerts.
diff --git a/web/templates/layout.html b/web/templates/layout.html
index bbf9dbc..e497152 100644
--- a/web/templates/layout.html
+++ b/web/templates/layout.html
@@ -9,7 +9,7 @@
-
+
+{% endblock %}
diff --git a/web/templates/protokoll.html b/web/templates/protokoll.html
index 5a73a81..3e633ab 100644
--- a/web/templates/protokoll.html
+++ b/web/templates/protokoll.html
@@ -23,7 +23,7 @@