Files
grotti/web/templates/warteschlange.html
T

219 lines
8.4 KiB
HTML

{% extends "layout.html" %}
{% block body %}
<div class="page-head">
<div>
<h1>Warteschlange</h1>
<p class="tiny">Wartende und laufende Aufträge. Direktstarts erscheinen unter Läuft. Priorität mit den Pfeilen ändern.</p>
</div>
<div class="page-head-side">
<button type="button" class="ghost" id="q-refresh"><svg class="ico"><use href="#i-refresh"/></svg>Neu laden</button>
</div>
</div>
<div class="filter-bar" id="q-filter" role="group" aria-label="Status">
<button type="button" class="fchip on" data-status="">Alle aktiv</button>
<button type="button" class="fchip" data-status="running">Läuft</button>
<button type="button" class="fchip" data-status="waiting">Wartet</button>
<button type="button" class="fchip" data-status="error">Fehler</button>
<button type="button" class="fchip" data-status="history">Historie</button>
</div>
<p id="q-note" class="tiny"></p>
<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>
{% 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 = "";
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();
if (!shown.length) {
const hint = jobs.length
? "Nichts in dieser Ansicht."
: (liveDirect.length ? "Keine Queue-Aufträge — laufende Direktstarts unter „Läuft“." : "Noch kein Eintrag.");
body.innerHTML = '<tr><td class="empty" colspan="6"><div class="empty-state">' + icon("queue") + '<p>' + hint + '</p></div></td></tr>';
document.getElementById("q-note").textContent = "";
return;
}
const waiting = jobs.filter((j) => j.status === "waiting");
body.innerHTML = shown.map((job) => {
const st = job.status;
if (job.direct) {
return `<tr data-direct="${escapeHtml(job.host)}">
<td>${chipHtml("printing", "läuft")}</td>
<td><span 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="queue-tools"><a class="icon-btn" href="/#${encodeURIComponent(job.host)}" title="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">${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">${icon("down")}</button>`);
}
if (st === "error" && job.is_ablauf) {
tools.push(`<button type="button" class="icon-btn" data-resume="${job.id}" title="Fortsetzen">${icon("play")}</button>`);
tools.push(`<button type="button" class="icon-btn danger" data-dismiss="${job.id}" title="Verwerfen">${icon("x")}</button>`);
} else if (st === "waiting" || st === "running") {
tools.push(`<button type="button" class="icon-btn danger" data-cancel="${job.id}" title="Abbrechen">${icon("x")}</button>`);
}
return `<tr data-id="${job.id}">
<td>${chipHtml(st === "running" ? "printing" : st === "waiting" ? "waiting" : st === "error" ? "error" : "complete", STATUS_LABEL[st] || st)}</td>
<td><span 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="queue-tools">${tools.join("")}</td>
</tr>`;
}).join("");
document.getElementById("q-note").textContent = shown.length === 1 ? "1 Eintrag" : shown.length + " Einträge";
}
async function load() {
try {
const [queue, active] = await Promise.all([
api("/druck-queue?limit=120"),
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));
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 || "";
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 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;
load();
setInterval(load, 5000);
</script>
{% endblock %}