Grotti 3.1.0: Bedarf-UI, Zeitfenster, HT/NT, Theme, Chrome-Fixes.

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.
This commit is contained in:
Dominic Gundel committed 2026-10-18 02:20:12 +02:00
1 parent 8e88efc094
commit 6e59df22e5
23 files changed
+741 -171

No files matched your search

+70 -23
View File
@@ -5,7 +5,11 @@
<h1>Wartung</h1>
<p class="tiny">Laufstunden und Filament je Drucker — Verschleißteile im Blick.</p>
</div>
<div class="page-head-side">
<button type="button" class="ghost" id="wartung-refresh"><svg class="ico"><use href="#i-refresh"/></svg>Neu laden</button>
</div>
</div>
<div id="wartung-summary" class="kpi-row" hidden></div>
<div id="wartung-root"></div>
<p id="wartung-note"></p>
{% endblock %}
@@ -19,38 +23,77 @@ function partRow(part) {
<td class="num">${Math.round(part.meters_since || 0).toLocaleString("de-DE")} mm</td>
<td class="num">${part.life_hours ? part.life_hours + " h" : "—"}</td>
<td class="num">${part.life_meters ? Math.round(part.life_meters) + " mm" : "—"}</td>
<td><button type="button" class="ghost sm" data-reset="${part.id}">Zurücksetzen</button></td>
<td><button type="button" class="ghost tiny-btn" data-reset="${part.id}">Zurücksetzen</button></td>
</tr>`;
}
function printerCard(block) {
const c = block.counters || {};
const overdue = (block.parts || []).filter((p) => p.overdue_hours || p.overdue_meters).length;
const parts = (block.parts || []).map(partRow).join("")
|| '<tr><td colspan="6" class="empty">Noch keine Teile.</td></tr>';
return `<section class="card">
<h2>${escapeHtml(block.name || block.host)}</h2>
|| '<tr><td colspan="6" class="empty">Noch keine Teile — unten anlegen.</td></tr>';
return `<section class="card wartung-card">
<div class="lib-head">
<h2>${escapeHtml(block.name || block.host)}</h2>
${overdue ? chipHtml("error", overdue + " fällig") : chipHtml("idle", "ok")}
</div>
<p class="hint">${escapeHtml(block.host)} · ${(c.hours || 0).toLocaleString("de-DE", { maximumFractionDigits: 1 })} h gesamt · ${Math.round(c.meters || 0).toLocaleString("de-DE")} mm · ${c.runs || 0} Läufe</p>
<div class="table-wrap"><table>
<thead><tr><th>Teil</th><th>Seit Reset</th><th>Seit Reset</th><th>Intervall h</th><th>Intervall mm</th><th></th></tr></thead>
<thead><tr><th>Teil</th><th>Seit Reset</th><th>Filament</th><th>Intervall h</th><th>Intervall mm</th><th></th></tr></thead>
<tbody>${parts}</tbody>
</table></div>
<form class="grid-2 wartung-add" data-host="${escapeHtml(block.host)}" style="margin-top:0.75rem">
<div class="field"><label>Schlüssel</label><input name="part_key" required placeholder="nozzle"></div>
<div class="field"><label>Bezeichnung</label><input name="label" placeholder="Düse 0.4"></div>
<div class="field"><label>Intervall h</label><input name="life_hours" type="number" step="1" min="0" value="0"></div>
<div class="field"><label>Intervall mm</label><input name="life_meters" type="number" step="1000" min="0" value="0"></div>
<div class="field" style="grid-column:1/-1"><label>Notiz</label><input name="note"></div>
<div class="actions"><button type="submit">Teil anlegen</button></div>
</form>
<details class="mat-more section-gap">
<summary><svg class="ico caret"><use href="#i-down"/></svg><span class="grow">Teil anlegen</span></summary>
<form class="grid-2 wartung-add" data-host="${escapeHtml(block.host)}">
<div class="field"><label>Schlüssel</label><input name="part_key" required placeholder="nozzle"></div>
<div class="field"><label>Bezeichnung</label><input name="label" placeholder="Düse 0.4"></div>
<div class="field"><label>Intervall h</label><input name="life_hours" type="number" step="1" min="0" value="0"></div>
<div class="field"><label>Intervall mm</label><input name="life_meters" type="number" step="1000" min="0" value="0"></div>
<div class="field" style="grid-column:1/-1"><label>Notiz</label><input name="note"></div>
<div class="actions"><button type="submit">Teil anlegen</button></div>
</form>
</details>
</section>`;
}
function renderSummary(data) {
const box = document.getElementById("wartung-summary");
if (!data.length) {
box.hidden = true;
box.innerHTML = "";
return;
}
let hours = 0;
let meters = 0;
let overdue = 0;
data.forEach((block) => {
hours += Number((block.counters || {}).hours) || 0;
meters += Number((block.counters || {}).meters) || 0;
overdue += (block.parts || []).filter((p) => p.overdue_hours || p.overdue_meters).length;
});
box.hidden = false;
box.innerHTML = `
<div class="kpi"><div class="kpi-val">${data.length}</div><div class="kpi-label">Drucker</div></div>
<div class="kpi"><div class="kpi-val">${hours.toLocaleString("de-DE", { maximumFractionDigits: 0 })} h</div><div class="kpi-label">Laufzeit</div></div>
<div class="kpi"><div class="kpi-val">${Math.round(meters / 1000).toLocaleString("de-DE")} m</div><div class="kpi-label">Filament</div></div>
<div class="kpi"><div class="kpi-val">${overdue}</div><div class="kpi-label">Fällig</div></div>`;
}
async function load() {
const data = await api("/wartung");
renderSummary(data || []);
document.getElementById("wartung-root").innerHTML = (data || []).map(printerCard).join("")
|| '<p class="empty">Keine Drucker angelegt.</p>';
|| `<div class="empty-state">
<svg class="ico"><use href="#i-nozzle"/></svg>
<p>Keine Drucker angelegt.</p>
<a class="ghost" href="/einstellungen">Einstellungen →</a>
</div>`;
}
document.getElementById("wartung-refresh")?.addEventListener("click", () => {
load().catch((error) => showNote("wartung-note", error));
});
document.getElementById("wartung-root").addEventListener("submit", async (event) => {
const form = event.target.closest("form.wartung-add");
if (!form) return;
@@ -70,24 +113,28 @@ document.getElementById("wartung-root").addEventListener("submit", async (event)
});
form.reset();
showNote("wartung-note", null, "Teil gespeichert.");
toast("Wartungsteil angelegt.");
load();
} catch (error) {
showNote("wartung-note", error);
}
});
document.getElementById("wartung-root").addEventListener("click", async (event) => {
document.getElementById("wartung-root").addEventListener("click", (event) => {
const btn = event.target.closest("[data-reset]");
if (!btn) return;
try {
await api("/wartung/teile/" + btn.dataset.reset + "/reset", { method: "POST", json: {} });
showNote("wartung-note", null, "Teil zurückgesetzt.");
load();
} catch (error) {
showNote("wartung-note", error);
}
armButton(btn, "Wirklich zurücksetzen?", async () => {
try {
await api("/wartung/teile/" + btn.dataset.reset + "/reset", { method: "POST", json: {} });
showNote("wartung-note", null, "Teil zurückgesetzt.");
toast("Zähler zurückgesetzt.");
load();
} catch (error) {
showNote("wartung-note", error);
}
});
});
load();
load().catch((error) => showNote("wartung-note", error));
</script>
{% endblock %}