Grotti 2.0.0: eigenes Repo, Umbenennung von Farm.

This commit is contained in:
Dominic Gundel committed 2026-10-15 08:45:56 +02:00
commit b2e6443c68
69 files changed
+18719

No files matched your search

+183
View File
@@ -0,0 +1,183 @@
{% 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="filter-bar" id="f-range" role="group" aria-label="Zeitraum">
<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 class="filter-bar" id="f-kind" role="group" aria-label="Art"></div>
<div class="filter-bar" 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 %}