Grotti 2.10.0: Orca-Senden, Slice-Ordner, ED-Strom, Hygiene.

Slice-Watch mit Ordnern/Filtern, Orca-Nachbearbeitung, Cockpit-Verlauf
mit ED, Zeitzone im Browser. Tasten-Grenze und README-API angeglichen,
toter Code entfernt, Requirements gepinnt.
This commit is contained in:
Dominic Gundel committed 2026-10-18 00:41:15 +02:00
1 parent efd0801cd1
commit 9e22cebb81
48 files changed
+2259 -422

No files matched your search

+55 -26
View File
@@ -9,7 +9,7 @@
<link rel="icon" type="image/png" sizes="32x32" href="{{ url_for('static', filename='brand/favicon-32.png') }}">
<link rel="icon" type="image/png" sizes="512x512" href="{{ url_for('static', filename='brand/favicon.png') }}">
<link rel="apple-touch-icon" href="{{ url_for('static', filename='brand/apple-touch-icon.png') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='grotti.css') }}?v=45">
<link rel="stylesheet" href="{{ url_for('static', filename='grotti.css') }}?v=52">
<script>
(function () {
try {
@@ -157,7 +157,7 @@
<a href="/warteschlange" class="{{ 'on' if page == 'warteschlange' else '' }} nav-shelf"><svg class="ico"><use href="#i-queue"/></svg><span>Warteschlange</span></a>
<a href="/aktivitaet" class="{{ 'on' if page == 'aktivitaet' else '' }} nav-desk"><svg class="ico"><use href="#i-activity"/></svg><span>Aktivität</span></a>
</nav>
<script src="{{ url_for('static', filename='api.js') }}?v=24"></script>
<script src="{{ url_for('static', filename='api.js') }}?v=26"></script>
<script>
document.getElementById("logout-btn")?.addEventListener("click", async () => {
try { await api("/auth/logout", { method: "POST", json: {} }); } catch (e) {}
@@ -241,21 +241,44 @@ async function cancelQueueItem(job, button) {
}
window.cancelQueueItem = cancelQueueItem;
function runItemHtml(job) {
const rep = repeatText(job);
const steps = job.is_ablauf ? stepsHtml(job.step_labels, job.display_step_index != null ? job.display_step_index : job.step_index, true) : "";
return `<li class="queue-item running">
<div class="queue-row">
<a class="queue-link grow" href="/#${encodeURIComponent(job.host || "")}"><span class="queue-name">${escapeHtml(queueTarget(job))}</span></a>
${rep ? `<span class="queue-sub">${escapeHtml(rep)}</span>` : ""}
<span class="queue-tools"><button type="button" class="icon-btn danger" data-cancel-q="${job.id}" title="Abbrechen">${icon("x")}</button></span>
</div>
${steps}
<span class="queue-msg">${escapeHtml(phaseText(job))}</span>
<span class="queue-sub">${escapeHtml(jobLabel(job.filename))}</span>
function ablaufStepFrac(job) {
if (!job || !job.is_ablauf || !job.step_labels || !job.step_labels.length) return "";
const idx = Number(job.display_step_index != null ? job.display_step_index : job.step_index) || 0;
return (idx + 1) + "/" + job.step_labels.length;
}
function runLineHtml({ name, progress, remain, filename, tipExtra, cancelId }) {
const pct = Math.max(0, Math.min(100, Math.round((Number(progress) || 0) * 100)));
const finish = remain > 0 ? finishClock(remain) : "";
const bits = [name];
if (pct > 0 || remain > 0) bits.push(pct + " %");
if (finish) bits.push(finish);
if (tipExtra) bits.push(tipExtra);
const tip = [
filename ? jobLabel(filename) : "",
remain > 0 ? "noch " + hours(remain) : "",
tipExtra ? "Schritt " + tipExtra : "",
].filter(Boolean).join(" · ");
return `<li class="queue-item running run-line" style="--pct:${pct}">
<a class="queue-link run-line-link" href="/warteschlange" title="${escapeHtml(tip || "Zur Warteschlange")}">
<span class="run-line-fill" aria-hidden="true"></span>
<span class="run-line-text">${escapeHtml(bits.join(" · "))}</span>
</a>
${cancelId != null ? `<span class="queue-tools"><button type="button" class="icon-btn danger" data-cancel-q="${cancelId}" title="Abbrechen">${icon("x")}</button></span>` : ""}
</li>`;
}
function runItemHtml(job) {
return runLineHtml({
name: queueTarget(job),
progress: job.progress,
remain: job.remain,
filename: job.filename,
tipExtra: ablaufStepFrac(job),
cancelId: job.id,
});
}
function waitItemHtml(job, index, total) {
const isError = job.status === "error";
const label = job.is_ablauf ? "Ablauf · " + jobLabel(job.filename) : jobLabel(job.filename);
@@ -276,17 +299,12 @@ function waitItemHtml(job, index, total) {
}
function directItemHtml(p) {
const pct = Math.round((Number(p.progress) || 0) * 100);
const eta = p.remain > 0 ? "noch " + hours(p.remain) + " · fertig " + finishClock(p.remain) : "";
return `<li class="queue-item running">
<div class="queue-row">
<a class="queue-link grow" href="/#${encodeURIComponent(p.host)}"><span class="queue-name">${escapeHtml(p.name || p.host)}</span></a>
<span class="queue-sub">${pct} %</span>
</div>
<span class="bar slim live"><span style="width:${pct}%"></span></span>
<span class="queue-msg">${escapeHtml(STATE_LABELS[p.state] || p.state)}${eta ? " · " + escapeHtml(eta) : ""}</span>
<span class="queue-sub">${escapeHtml(jobLabel(p.filename))}</span>
</li>`;
return runLineHtml({
name: p.name || p.host,
progress: p.progress,
remain: p.remain,
filename: p.filename,
});
}
let queueCache = [];
@@ -322,7 +340,18 @@ async function refreshPrintQueue() {
[jobs, active] = await Promise.all([api("/druck-queue"), api("/laeuft").catch(() => [])]);
} catch (error) { return; }
queueCache = jobs || [];
const running = queueCache.filter((job) => job.status === "running");
const liveByHost = Object.create(null);
(active || []).forEach((p) => { if (p.host) liveByHost[p.host] = p; });
const running = queueCache.filter((job) => job.status === "running").map((job) => {
const live = liveByHost[job.host];
if (!live) return job;
return {
...job,
progress: live.progress,
remain: live.remain,
filename: stickyLiveFilename(job.host, live.filename || job.filename, live.state) || job.filename,
};
});
const queuedHosts = new Set(running.map((job) => job.host));
const direct = (active || []).filter((p) => !queuedHosts.has(p.host)).map((p) => ({
...p,