Bedarf in Sidebar/Material, Drucker-Zeitfenster und Tarif in Einstellungen, helles Theme; Legacy-Token und /api/drucker entfernt; SSE/SW-Fixes gegen hängende Navigation in Chrome.
231 lines
8.8 KiB
HTML
231 lines
8.8 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">
|
|
<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="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;
|
|
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 %}
|