Grotti 2.2.0: Auth, API-Tokens, MCP und Doku-Trennung.

This commit is contained in:
Dominic Gundel committed 2026-10-15 10:53:12 +02:00
1 parent b20f8a7779
commit 5a3716b26a
65 files changed
+2212 -198

No files matched your search

+28 -24
View File
@@ -11,23 +11,12 @@
{% endblock %}
{% block script %}
<script>
const labels = {
printing: "druckt", paused: "pausiert", complete: "fertig", standby: "bereit",
heating: "heizt", error: "Fehler", cancelled: "abgebrochen", offline: "offline"
};
const tiles = new Map();
function shownState(printer) {
const state = printer.state || "offline";
const hot = (printer.nozzle_target || 0) > 40 || (printer.bed_target || 0) > 30;
if ((state === "standby" || state === "complete") && hot) return "heating";
return state;
}
function metaLine(printer) {
const state = shownState(printer);
const busy = state === "printing" || state === "paused";
const bits = [labels[state] || state];
const bits = [STATE_LABELS[state] || state];
if (busy) {
const pct = Math.round((printer.progress || 0) * 100);
bits.push(pct + " %");
@@ -52,8 +41,11 @@ function ensureTile(printer) {
<span class="host"></span>
</figcaption>
<p class="cam-meta tiny"></p>
<img alt="" data-role="cam">
<a class="cam-url" href="#"></a>`;
<div class="cam-stage">
<img alt="" data-role="cam" hidden>
<p class="cam-empty" data-role="cam-empty">${icon("camera")}<span>Keine Kamera</span></p>
</div>
<a class="ghost cam-open" href="/#${encodeURIComponent(printer.host)}">Cockpit</a>`;
tiles.set(printer.host, tile);
document.getElementById("grid").appendChild(tile);
return tile;
@@ -65,10 +57,10 @@ async function refresh() {
const grid = document.getElementById("grid");
if (!printers.length) {
tiles.clear();
grid.innerHTML = '<p class="empty">Keine Drucker eingetragen.</p>';
grid.innerHTML = `<div class="empty-state">${icon("camera", "lg")}<p>Noch keine Drucker.</p><a href="/einstellungen">Zu den Einstellungen</a></div>`;
return;
}
if (grid.querySelector(".empty")) grid.innerHTML = "";
if (grid.querySelector(".empty-state")) grid.innerHTML = "";
const seen = new Set();
for (const printer of printers) {
seen.add(printer.host);
@@ -78,16 +70,28 @@ async function refresh() {
tile.querySelector(".cam-name").textContent = printer.name;
tile.querySelector(".host").textContent = printer.host;
tile.querySelector(".cam-meta").textContent = metaLine(printer);
const path = "/api/v1/kamera?host=" + encodeURIComponent(printer.host);
const shown = "/api/v1/kamera?host=" + printer.host;
tile.querySelector(".cam-open").href = "/#" + encodeURIComponent(printer.host);
const img = tile.querySelector('[data-role="cam"]');
const empty = tile.querySelector('[data-role="cam-empty"]');
if (!printer.camera || printer.has_camera === false) {
img.removeAttribute("src");
img.hidden = true;
empty.hidden = false;
empty.innerHTML = icon("camera") + "<span>Keine Kamera</span>";
continue;
}
img.alt = "Kamera " + printer.name;
img.src = withToken(path) + "&t=" + Math.floor(Date.now() / 2000);
img.classList.remove("broken");
img.onerror = () => img.classList.add("broken");
const link = tile.querySelector(".cam-url");
link.href = withToken(path);
link.textContent = shown;
const next = withToken(printer.camera) + "&t=" + Math.floor(Date.now() / 2000);
img.onerror = () => {
img.hidden = true;
empty.hidden = false;
empty.innerHTML = icon("camera") + "<span>Keine Kamera</span>";
};
img.onload = () => {
img.hidden = false;
empty.hidden = true;
};
img.src = next;
}
for (const [host, tile] of [...tiles.entries()]) {
if (!seen.has(host)) {