294 lines
12 KiB
HTML
294 lines
12 KiB
HTML
{% extends "layout.html" %}
|
|
{% block body %}
|
|
<div class="page-head">
|
|
<div>
|
|
<h1>Warteschlange</h1>
|
|
<p class="tiny">Wartende und laufende Drucke. Direktstarts erscheinen unter Läuft. Priorität mit den Pfeilen ändern.</p>
|
|
</div>
|
|
<div class="page-head-side">
|
|
<a class="ghost" id="q-bedarf" href="/material#bedarf" hidden></a>
|
|
<button type="button" class="ghost" id="q-refresh"><svg class="ico"><use href="#i-refresh"/></svg>Neu laden</button>
|
|
</div>
|
|
</div>
|
|
<div class="toolbar" id="q-filter" role="group" aria-label="Status">
|
|
<div class="fchips">
|
|
<button type="button" class="fchip on" data-status="">Alle aktiv</button>
|
|
<button type="button" class="fchip" data-status="running"><svg class="ico"><use href="#i-play"/></svg>Läuft</button>
|
|
<button type="button" class="fchip" data-status="waiting"><svg class="ico"><use href="#i-clock"/></svg>Wartet</button>
|
|
<button type="button" class="fchip" data-status="error"><svg class="ico"><use href="#i-alert"/></svg>Fehler</button>
|
|
<button type="button" class="fchip" data-status="history"><svg class="ico"><use href="#i-history"/></svg>Historie</button>
|
|
</div>
|
|
<div class="tools-end"><span id="q-note" class="tiny"></span></div>
|
|
</div>
|
|
<div class="card table-wrap">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Status</th>
|
|
<th>Datei</th>
|
|
<th>Ziel</th>
|
|
<th>Schritte</th>
|
|
<th>Meldung</th>
|
|
<th></th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="q-rows"></tbody>
|
|
</table>
|
|
<div class="pager" id="q-pager" hidden></div>
|
|
</div>
|
|
{% endblock %}
|
|
{% block script %}
|
|
<script>
|
|
const STATUS_LABEL = {
|
|
running: "läuft", waiting: "wartet", error: "Fehler",
|
|
done: "fertig", cancelled: "abgebrochen", dismissed: "verworfen",
|
|
};
|
|
let jobs = [];
|
|
let liveDirect = [];
|
|
let filter = "";
|
|
let qPage = 1;
|
|
|
|
function targetOf(job) {
|
|
return job.printer_name || job.host || (job.family_label ? "nächster " + job.family_label : "nächster freier");
|
|
}
|
|
|
|
function directRows() {
|
|
return liveDirect.map((p) => {
|
|
const pct = Math.round((Number(p.progress) || 0) * 100);
|
|
const eta = p.remain > 0 ? "noch " + hours(p.remain) + " · fertig " + finishClock(p.remain) : "";
|
|
const state = STATE_LABELS[p.state] || p.state || "läuft";
|
|
return {
|
|
id: "live:" + p.host,
|
|
status: "running",
|
|
direct: true,
|
|
host: p.host,
|
|
printer_name: p.name || p.host,
|
|
filename: p.filename || "",
|
|
message: state + (pct ? " · " + pct + " %" : "") + (eta ? " · " + eta : ""),
|
|
progress: pct,
|
|
};
|
|
});
|
|
}
|
|
|
|
function shownJobs() {
|
|
const direct = directRows();
|
|
if (filter === "history") {
|
|
return jobs.filter((j) => ["done", "cancelled", "dismissed"].includes(j.status));
|
|
}
|
|
if (filter === "running") {
|
|
return jobs.filter((j) => j.status === "running").concat(direct);
|
|
}
|
|
if (filter) return jobs.filter((j) => j.status === filter);
|
|
return jobs.filter((j) => ["running", "waiting", "error"].includes(j.status)).concat(direct);
|
|
}
|
|
|
|
function render() {
|
|
document.querySelectorAll("#q-filter .fchip").forEach((b) => {
|
|
b.classList.toggle("on", (b.dataset.status || "") === filter);
|
|
});
|
|
const body = document.getElementById("q-rows");
|
|
const shown = shownJobs();
|
|
const pager = document.getElementById("q-pager");
|
|
if (!shown.length) {
|
|
const hint = jobs.length
|
|
? "Nichts in dieser Ansicht."
|
|
: (liveDirect.length
|
|
? "Keine Einträge in der Warteschlange — laufende Direktstarts unter „Läuft“."
|
|
: "Noch kein Eintrag.");
|
|
const cta = (!jobs.length && !liveDirect.length)
|
|
? '<p><a class="btn-primary" href="/dateien">' + icon("send") + ' Datei senden</a></p>'
|
|
: "";
|
|
body.innerHTML = '<tr><td class="empty" colspan="6"><div class="empty-state">' + icon("queue") + '<p>' + hint + '</p>' + cta + '</div></td></tr>';
|
|
document.getElementById("q-note").textContent = "";
|
|
renderPager(pager, null, () => {});
|
|
return;
|
|
}
|
|
const waiting = jobs.filter((j) => j.status === "waiting");
|
|
const paged = pageSlice(shown, qPage, DEFAULT_PAGE_SIZE);
|
|
qPage = paged.page;
|
|
body.innerHTML = paged.items.map((job) => {
|
|
const st = job.status;
|
|
if (job.direct) {
|
|
return `<tr data-direct="${escapeHtml(job.host)}">
|
|
<td>${chipHtml("printing", "läuft")}</td>
|
|
<td>${job.file_id ? `<a class="cell-file" href="/dateien?file=${encodeURIComponent(job.file_id)}" title="${escapeHtml(job.filename || "")}">${escapeHtml(jobLabel(job.filename))}</a>` : `<span class="cell-file" title="${escapeHtml(job.filename || "")}">${escapeHtml(jobLabel(job.filename))}</span>`}</td>
|
|
<td><a href="/#${encodeURIComponent(job.host)}">${escapeHtml(targetOf(job))}</a></td>
|
|
<td>—</td>
|
|
<td class="tiny">${escapeHtml(job.message || "")}</td>
|
|
<td class="actions-cell"><a class="icon-btn" href="/#${encodeURIComponent(job.host)}" title="Zur Wand" aria-label="Zur Wand">${icon("printer")}</a></td>
|
|
</tr>`;
|
|
}
|
|
const waitIdx = waiting.findIndex((j) => j.id === job.id);
|
|
const steps = job.is_ablauf
|
|
? stepsHtml(job.step_labels, job.display_step_index != null ? job.display_step_index : job.step_index, true)
|
|
: "";
|
|
const rep = repeatText(job);
|
|
const tools = [];
|
|
if (st === "waiting" && waitIdx > 0) {
|
|
tools.push(`<button type="button" class="icon-btn" data-prio="${job.id}" data-dir="-1" title="Höher" aria-label="Höher">${icon("up")}</button>`);
|
|
}
|
|
if (st === "waiting" && waitIdx >= 0 && waitIdx < waiting.length - 1) {
|
|
tools.push(`<button type="button" class="icon-btn" data-prio="${job.id}" data-dir="1" title="Tiefer" aria-label="Tiefer">${icon("down")}</button>`);
|
|
}
|
|
if (st === "error" && job.is_ablauf) {
|
|
tools.push(`<button type="button" class="ghost tiny-btn with-label" data-resume="${job.id}" title="Fortsetzen" aria-label="Fortsetzen">${icon("play")}<span class="act-label">Fortsetzen</span></button>`);
|
|
tools.push(`<button type="button" class="ghost tiny-btn with-label danger" data-dismiss="${job.id}" title="Verwerfen" aria-label="Verwerfen">${icon("x")}<span class="act-label">Verwerfen</span></button>`);
|
|
} else if (st === "error") {
|
|
if (job.host) {
|
|
tools.push(`<a class="ghost tiny-btn with-label" href="/#${encodeURIComponent(job.host)}" title="Zur Wand" aria-label="Zur Wand">${icon("printer")}<span class="act-label">Wand</span></a>`);
|
|
}
|
|
if (job.file_id) {
|
|
tools.push(`<button type="button" class="ghost tiny-btn with-label" data-requeue="${job.id}" title="Erneut einreihen" aria-label="Erneut einreihen">${icon("refresh")}<span class="act-label">Nochmal</span></button>`);
|
|
}
|
|
tools.push(`<button type="button" class="ghost tiny-btn with-label danger" data-dismiss="${job.id}" title="Verwerfen" aria-label="Verwerfen">${icon("x")}<span class="act-label">Verwerfen</span></button>`);
|
|
} else if (st === "waiting" || st === "running") {
|
|
tools.push(`<button type="button" class="ghost tiny-btn with-label danger" data-cancel="${job.id}" title="Abbrechen" aria-label="Abbrechen">${icon("x")}<span class="act-label">Abbrechen</span></button>`);
|
|
}
|
|
const phase = String(job.phase || "").toLowerCase();
|
|
const starting = st === "running" && (phase === "starting" || phase === "await_print");
|
|
const chipState = starting ? "heating" : st === "running" ? "printing" : st === "waiting" ? "waiting" : st === "error" ? "error" : st === "done" ? "done" : st === "cancelled" || st === "dismissed" ? "dismissed" : "complete";
|
|
const chipLabel = starting ? "startet" : (STATUS_LABEL[st] || st);
|
|
return `<tr data-id="${job.id}">
|
|
<td>${chipHtml(chipState, chipLabel)}</td>
|
|
<td>${job.file_id ? `<a class="cell-file" href="/dateien?file=${encodeURIComponent(job.file_id)}" title="${escapeHtml(job.filename || "")}">${escapeHtml(jobLabel(job.filename))}</a>` : `<span class="cell-file" title="${escapeHtml(job.filename || "")}">${escapeHtml(jobLabel(job.filename))}</span>`}${rep ? " · " + escapeHtml(rep) : ""}</td>
|
|
<td>${job.host ? `<a href="/#${encodeURIComponent(job.host)}">${escapeHtml(targetOf(job))}</a>` : escapeHtml(targetOf(job))}</td>
|
|
<td>${steps || "—"}</td>
|
|
<td class="tiny">${escapeHtml(job.message || "")}</td>
|
|
<td class="actions-cell">${tools.join("")}</td>
|
|
</tr>`;
|
|
}).join("");
|
|
document.getElementById("q-note").textContent = shown.length === 1 ? "1 Eintrag" : shown.length + " Einträge";
|
|
renderPager(pager, paged, (p) => {
|
|
qPage = p;
|
|
render();
|
|
});
|
|
}
|
|
|
|
async function load() {
|
|
try {
|
|
const [queue, active] = await Promise.all([
|
|
api("/druck-queue?limit=200"),
|
|
api("/laeuft").catch(() => []),
|
|
]);
|
|
jobs = queue || [];
|
|
const queuedHosts = new Set(jobs.filter((j) => j.status === "running").map((j) => j.host));
|
|
liveDirect = (active || []).filter((p) => p.host && !queuedHosts.has(p.host));
|
|
qPage = 1;
|
|
render();
|
|
} catch (error) {
|
|
document.getElementById("q-rows").innerHTML =
|
|
`<tr><td class="empty" colspan="6">${escapeHtml(error.message)}</td></tr>`;
|
|
}
|
|
}
|
|
|
|
document.getElementById("q-filter").addEventListener("click", (event) => {
|
|
const btn = event.target.closest("[data-status]");
|
|
if (!btn) return;
|
|
filter = btn.dataset.status || "";
|
|
qPage = 1;
|
|
render();
|
|
});
|
|
document.getElementById("q-refresh").addEventListener("click", load);
|
|
|
|
document.getElementById("q-rows").addEventListener("click", async (event) => {
|
|
const cancel = event.target.closest("[data-cancel]");
|
|
if (cancel) {
|
|
const job = jobs.find((j) => j.id === Number(cancel.dataset.cancel));
|
|
if (job) await window.cancelQueueItem(job, cancel);
|
|
load();
|
|
return;
|
|
}
|
|
const resume = event.target.closest("[data-resume]");
|
|
if (resume) {
|
|
armButton(resume, "Fortsetzen?", async () => {
|
|
try {
|
|
await api("/druck-queue/" + resume.dataset.resume, {
|
|
method: "PATCH", json: { status: "running", resume: true },
|
|
});
|
|
toast("Ablauf geht weiter.");
|
|
load();
|
|
if (typeof refreshPrintQueue === "function") refreshPrintQueue();
|
|
} catch (error) { toast(error.message, "bad"); }
|
|
});
|
|
return;
|
|
}
|
|
const dismiss = event.target.closest("[data-dismiss]");
|
|
if (dismiss) {
|
|
armButton(dismiss, "Verwerfen?", async () => {
|
|
try {
|
|
await api("/druck-queue/" + dismiss.dataset.dismiss, {
|
|
method: "PATCH", json: { status: "dismissed" },
|
|
});
|
|
load();
|
|
if (typeof refreshPrintQueue === "function") refreshPrintQueue();
|
|
} catch (error) { toast(error.message, "bad"); }
|
|
});
|
|
return;
|
|
}
|
|
const requeue = event.target.closest("[data-requeue]");
|
|
if (requeue) {
|
|
const job = jobs.find((j) => j.id === Number(requeue.dataset.requeue));
|
|
if (!job || !job.file_id) return;
|
|
armButton(requeue, "Erneut einreihen?", async () => {
|
|
try {
|
|
await api("/druck-queue/" + job.id, {
|
|
method: "PATCH", json: { status: "waiting", undo: true },
|
|
});
|
|
toast("Wieder in der Warteschlange.");
|
|
load();
|
|
if (typeof refreshPrintQueue === "function") refreshPrintQueue();
|
|
} catch (error) {
|
|
try {
|
|
await api("/senden", {
|
|
method: "POST",
|
|
json: {
|
|
file_id: job.file_id,
|
|
hosts: job.host ? [job.host] : [],
|
|
confirm: true,
|
|
when_free: true,
|
|
ace_maps: job.host && (job.ace_map || []).length ? { [job.host]: job.ace_map } : {},
|
|
bed_leveling: !!job.bed_leveling,
|
|
},
|
|
});
|
|
toast("Erneut eingereiht.");
|
|
load();
|
|
} catch (err2) { toast(err2.message, "bad"); }
|
|
}
|
|
});
|
|
return;
|
|
}
|
|
const prio = event.target.closest("[data-prio]");
|
|
if (prio) {
|
|
const id = Number(prio.dataset.prio);
|
|
const dir = Number(prio.dataset.dir);
|
|
const waiting = jobs.filter((j) => j.status === "waiting");
|
|
const idx = waiting.findIndex((j) => j.id === id);
|
|
const other = waiting[idx + dir];
|
|
if (!other) return;
|
|
try {
|
|
await api("/druck-queue/" + id, {
|
|
method: "PATCH", json: { priority: Number(other.priority) + dir },
|
|
});
|
|
load();
|
|
if (typeof refreshPrintQueue === "function") refreshPrintQueue();
|
|
} catch (error) { toast(error.message, "bad"); }
|
|
}
|
|
});
|
|
|
|
window.onQueueChanged = load;
|
|
window.onBedarfChanged = (data) => {
|
|
const link = document.getElementById("q-bedarf");
|
|
if (!link) return;
|
|
const n = (data.shortfall || []).length;
|
|
if (!n) {
|
|
link.hidden = true;
|
|
return;
|
|
}
|
|
link.hidden = false;
|
|
link.textContent = n + " Fehlmenge" + (n === 1 ? "" : "n");
|
|
};
|
|
load();
|
|
if (typeof refreshBedarf === "function") refreshBedarf();
|
|
</script>
|
|
{% endblock %}
|