Jetzt/Topbar und Material-Routing, Pause bei Filament-Ende, Druckbericht mit Stromanteilen, Design- und Kamera-Stream-Polish; Bedarf-Alert-Spam und Platte-belegt-Bugs behoben.
186 lines
7.0 KiB
HTML
186 lines
7.0 KiB
HTML
{% extends "layout.html" %}
|
|
{% block body %}
|
|
<div class="page-head">
|
|
<div>
|
|
<h1>Aktivität</h1>
|
|
<p class="tiny">Ereignisse von Grotti und den Druckern. Neueste oben.</p>
|
|
</div>
|
|
<div class="page-head-side">
|
|
<button type="button" class="ghost" id="tl-refresh"><svg class="ico"><use href="#i-refresh"/></svg>Neu laden</button>
|
|
<button type="button" id="tl-read"><svg class="ico"><use href="#i-check"/></svg>Alle gelesen</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="toolbar" id="f-range" role="group" aria-label="Zeitraum">
|
|
<div class="fchips">
|
|
<button type="button" class="fchip on" data-range="day">Heute</button>
|
|
<button type="button" class="fchip" data-range="week">7 Tage</button>
|
|
<button type="button" class="fchip" data-range="all">Alle</button>
|
|
</div>
|
|
</div>
|
|
<div class="toolbar" id="f-kind" role="group" aria-label="Art"></div>
|
|
<div class="toolbar" id="f-host" role="group" aria-label="Drucker"></div>
|
|
|
|
<p id="tl-note" class="tiny"></p>
|
|
<div class="timeline" id="timeline">
|
|
<div class="skeleton" style="height:4rem"></div>
|
|
<div class="skeleton" style="height:4rem"></div>
|
|
</div>
|
|
{% endblock %}
|
|
{% block script %}
|
|
<script>
|
|
const KINDS = [
|
|
["queue", "Warteschlange"],
|
|
["ablauf", "Ablauf"],
|
|
["start", "Start / Pause"],
|
|
["fertig", "Fertig"],
|
|
["fehler", "Fehler"],
|
|
["abbruch", "Abbruch"],
|
|
["offline", "Offline"],
|
|
["spule", "Spule"],
|
|
];
|
|
const KIND_LABEL = Object.fromEntries(KINDS);
|
|
const params = new URLSearchParams(location.search);
|
|
const filter = {
|
|
range: params.get("range") || (params.get("host") ? "week" : "day"),
|
|
kinds: new Set((params.get("kind") || "").split(",").filter(Boolean)),
|
|
host: params.get("host") || "",
|
|
};
|
|
let printers = [];
|
|
|
|
function syncUrl() {
|
|
const q = new URLSearchParams();
|
|
if (filter.range !== "day") q.set("range", filter.range);
|
|
if (filter.kinds.size) q.set("kind", [...filter.kinds].join(","));
|
|
if (filter.host) q.set("host", filter.host);
|
|
const s = q.toString();
|
|
history.replaceState(null, "", s ? "?" + s : location.pathname);
|
|
}
|
|
|
|
function renderFilters() {
|
|
document.querySelectorAll("#f-range .fchip").forEach((b) => b.classList.toggle("on", b.dataset.range === filter.range));
|
|
document.getElementById("f-kind").innerHTML =
|
|
`<button type="button" class="fchip ${filter.kinds.size ? "" : "on"}" data-kind="">Alle Arten</button>` +
|
|
KINDS.map(([k, label]) =>
|
|
`<button type="button" class="fchip ${filter.kinds.has(k) ? "on" : ""}" data-kind="${k}"><span class="tl-dot ${k}"></span>${label}</button>`
|
|
).join("");
|
|
document.getElementById("f-host").innerHTML =
|
|
`<button type="button" class="fchip ${filter.host ? "" : "on"}" data-host="">Alle Drucker</button>` +
|
|
printers.map((p) =>
|
|
`<button type="button" class="fchip ${filter.host === p.host ? "on" : ""}" data-host="${escapeHtml(p.host)}">${escapeHtml(p.name)}</button>`
|
|
).join("");
|
|
}
|
|
|
|
function sinceIso() {
|
|
if (filter.range === "all") return "";
|
|
const d = new Date();
|
|
if (filter.range === "day") d.setHours(0, 0, 0, 0);
|
|
else d.setDate(d.getDate() - 7);
|
|
return d.toISOString();
|
|
}
|
|
|
|
function dayLabel(date) {
|
|
const today = new Date(); today.setHours(0, 0, 0, 0);
|
|
const that = new Date(date); that.setHours(0, 0, 0, 0);
|
|
const diff = Math.round((today - that) / 86400000);
|
|
if (diff === 0) return "Heute";
|
|
if (diff === 1) return "Gestern";
|
|
return that.toLocaleDateString("de-DE", { weekday: "long", day: "numeric", month: "long" });
|
|
}
|
|
|
|
function itemHtml(ev) {
|
|
const t = new Date(ev.created_at);
|
|
const time = t.toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" });
|
|
let msg = ev.message || "";
|
|
if (ev.printer && msg.startsWith(ev.printer)) msg = msg.slice(ev.printer.length).replace(/^[\s:·—-]+/, "");
|
|
const who = ev.printer
|
|
? `<a href="/#${encodeURIComponent(ev.host)}"><b>${escapeHtml(ev.printer)}</b></a>`
|
|
: `<b>Grotti</b>`;
|
|
const file = ev.filename && !msg.includes(ev.filename)
|
|
? `<div class="tl-file">${icon("file", "ico-sm")} ${escapeHtml(ev.filename)}</div>` : "";
|
|
return `<li class="tl-item ${ev.unread ? "unread" : ""}">
|
|
<span class="tl-time" title="${escapeHtml(t.toLocaleString("de-DE"))}">${time}</span>
|
|
<span class="tl-dot ${escapeHtml(ev.kind)}"></span>
|
|
<div class="tl-body">
|
|
<div class="tl-head">${who}<span class="tl-kind">${escapeHtml(KIND_LABEL[ev.kind] || ev.kind)}</span></div>
|
|
<div class="tl-msg">${escapeHtml(msg)}</div>${file}
|
|
</div>
|
|
</li>`;
|
|
}
|
|
|
|
function render(items) {
|
|
const box = document.getElementById("timeline");
|
|
const note = document.getElementById("tl-note");
|
|
if (!items.length) {
|
|
box.innerHTML = `<div class="card empty-state">${icon("activity")}<p>Keine Einträge${filter.range === "day" ? " heute" : ""}.</p>${
|
|
filter.range !== "all" ? `<button type="button" class="ghost" data-range-jump="all">Alle anzeigen</button>` : ""}</div>`;
|
|
note.textContent = "";
|
|
return;
|
|
}
|
|
const groups = [];
|
|
for (const ev of items) {
|
|
const label = dayLabel(ev.created_at);
|
|
if (!groups.length || groups[groups.length - 1].label !== label) groups.push({ label, items: [] });
|
|
groups[groups.length - 1].items.push(ev);
|
|
}
|
|
box.innerHTML = groups.map((g) =>
|
|
`<section class="tl-day"><h3>${escapeHtml(g.label)} · ${g.items.length}</h3><ol class="tl-list">${g.items.map(itemHtml).join("")}</ol></section>`
|
|
).join("");
|
|
note.textContent = items.length >= 300 ? "Die letzten 300 Einträge. Zeitraum oder Filter enger stellen für ältere." : "";
|
|
}
|
|
|
|
async function load() {
|
|
const q = new URLSearchParams({ limit: "300" });
|
|
const since = sinceIso();
|
|
if (since) q.set("since", since);
|
|
if (filter.kinds.size) q.set("kind", [...filter.kinds].join(","));
|
|
if (filter.host) q.set("host", filter.host);
|
|
try {
|
|
const data = await api("/ereignisse?" + q.toString());
|
|
render(data.items || []);
|
|
} catch (err) {
|
|
document.getElementById("timeline").innerHTML = `<div class="strip bad">${icon("alert")}<span>${escapeHtml(err.message)}</span></div>`;
|
|
}
|
|
}
|
|
|
|
document.addEventListener("click", (e) => {
|
|
const range = e.target.closest("[data-range], [data-range-jump]");
|
|
if (range) {
|
|
filter.range = range.dataset.range || range.dataset.rangeJump;
|
|
} else if (e.target.closest("#f-kind [data-kind]")) {
|
|
const k = e.target.closest("[data-kind]").dataset.kind;
|
|
if (!k) filter.kinds.clear();
|
|
else if (filter.kinds.has(k)) filter.kinds.delete(k);
|
|
else filter.kinds.add(k);
|
|
} else if (e.target.closest("#f-host [data-host]")) {
|
|
filter.host = e.target.closest("[data-host]").dataset.host;
|
|
} else {
|
|
return;
|
|
}
|
|
syncUrl();
|
|
renderFilters();
|
|
load();
|
|
});
|
|
|
|
document.getElementById("tl-refresh").addEventListener("click", load);
|
|
document.getElementById("tl-read").addEventListener("click", async () => {
|
|
await api("/ereignisse/gelesen", { method: "POST", json: {} });
|
|
toast("Alles als gelesen markiert.", "good");
|
|
if (typeof refreshEvents === "function") refreshEvents();
|
|
load();
|
|
});
|
|
window.onEventsRead = load;
|
|
|
|
(async () => {
|
|
try {
|
|
printers = await api("/drucker?live=0");
|
|
} catch (_) {
|
|
printers = [];
|
|
}
|
|
renderFilters();
|
|
load();
|
|
setInterval(load, 15000);
|
|
})();
|
|
</script>
|
|
{% endblock %}
|