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

+33 -20
View File
@@ -49,7 +49,7 @@
<div class="desk-facts" data-role="facts"></div>
<div data-role="flow"></div>
<div data-role="strips"></div>
<details class="desk-fold desk-steer-fold" data-fold="steer" open>
<details class="desk-fold desk-steer-fold" data-fold="steer">
<summary class="desk-fold-sum"><svg class="ico caret"><use href="#i-down"/></svg><span class="grow">Steuerung</span></summary>
<div data-role="steer"></div>
</details>
@@ -61,7 +61,7 @@
</aside>
</div>
<div class="desk-tools">
<details class="desk-fold desk-tool" data-fold="path" open>
<details class="desk-fold desk-tool" data-fold="path">
<summary class="desk-fold-sum"><svg class="ico caret"><use href="#i-down"/></svg><span class="grow">3D-Pfad</span><span class="tiny" data-role="path-caption"></span></summary>
<div class="desk-fold-body">
<p class="tiny">Ziehen dreht · Rad zoomt · Rechtsklick/Shift schwenkt · Doppelklick setzt zurück</p>
@@ -75,7 +75,7 @@
</div>
</div>
</details>
<details class="desk-fold desk-tool desk-tool-mesh" data-fold="mesh" open>
<details class="desk-fold desk-tool desk-tool-mesh" data-fold="mesh">
<summary class="desk-fold-sum"><svg class="ico caret"><use href="#i-down"/></svg><span class="grow">Bettnetz</span></summary>
<div class="desk-fold-body" data-role="mesh">
<p class="desk-empty"><svg class="ico"><use href="#i-grid"/></svg><span>Noch kein Bettnetz</span></p>
@@ -88,16 +88,15 @@
<script src="{{ url_for('static', filename='three.min.js') }}?v=1"></script>
<script src="{{ url_for('static', filename='gcode-viewer.js') }}?v=8"></script>
<script>
const labels = {
printing: "druckt", paused: "pausiert", complete: "fertig", standby: "bereit",
heating: "heizt", error: "Fehler", cancelled: "abgebrochen", offline: "offline"
};
const labels = STATE_LABELS;
const rank = { printing: 0, paused: 1, error: 2, heating: 3, complete: 4, cancelled: 5, standby: 6, offline: 7 };
const DESK_STATES = ["printing", "paused", "complete", "standby", "heating", "error", "cancelled", "offline"];
let deskHost = "";
let hideOffline = false;
let libraryFiles = [];
let wallSpools = [];
let deskViewer = null;
let deskCamFails = 0;
let deskViewerFileId = null;
let deskExcludeSelected = "";
const wallPullTried = new Set();
@@ -123,9 +122,9 @@ function syncDeskCamCollapsed() {
}
function restoreDeskFolds() {
let closed = [];
try { closed = JSON.parse(localStorage.getItem(DESK_FOLD_KEY) || "[]"); } catch (error) { closed = []; }
if (!Array.isArray(closed)) closed = [];
let closed;
try { closed = JSON.parse(localStorage.getItem(DESK_FOLD_KEY) || "null"); } catch (error) { closed = null; }
if (!Array.isArray(closed)) closed = ["steer", "path", "mesh"];
deskEl.querySelectorAll("details[data-fold]").forEach((box) => {
box.open = !closed.includes(box.dataset.fold);
box.addEventListener("toggle", () => {
@@ -193,13 +192,6 @@ function heatMapHtml(printer) {
return `<div class="mesh desk-mesh"><p>Aktuelles Netz, oben ist hinten. ${low.toFixed(2)} bis ${high.toFixed(2)} mm</p><div class="gridmesh" style="grid-template-columns:repeat(${rows[0].length},1fr)">${cells}</div><p class="tiny">${bits.join(" · ")}</p></div>`;
}
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 jobName(filename) {
return (filename || "").replace(/\.gcode$/i, "").replace(/^(\d+-)+/, "");
}
@@ -575,13 +567,14 @@ function refreshDeskCamera(printer) {
const img = deskEl.querySelector('[data-role="camera"]');
const empty = deskEl.querySelector('[data-role="cam-empty"]');
if (!img || !empty) return;
if (!printer || !printer.camera) {
if (!printer || !printer.camera || printer.has_camera === false) {
img.removeAttribute("src");
img.hidden = true;
empty.hidden = false;
empty.innerHTML = icon("camera") + "<span>Keine Kamera</span>";
setDeskCameraMode(false, "keine");
img.dataset.srcKey = "";
deskCamFails = 0;
return;
}
// Layout (no-cam) nicht bei jedem Frame-Fehler umwerfen — sonst springt die
@@ -593,6 +586,7 @@ function refreshDeskCamera(printer) {
const probe = new Image();
probe.onload = () => {
if (img.dataset.srcKey !== next) return;
deskCamFails = 0;
img.src = next;
img.hidden = false;
empty.hidden = true;
@@ -600,7 +594,16 @@ function refreshDeskCamera(printer) {
};
probe.onerror = () => {
if (img.dataset.srcKey !== next) return;
// Altes Bild behalten; nur wenn noch nie was kam, Platzhalter zeigen
deskCamFails += 1;
if (deskCamFails >= 2) {
img.removeAttribute("src");
img.hidden = true;
empty.hidden = false;
empty.innerHTML = icon("camera") + "<span>Keine Kamera</span>";
setDeskCameraMode(false, "offline");
img.dataset.srcKey = "";
return;
}
if (!img.src || img.hidden) {
empty.hidden = false;
empty.innerHTML = icon("camera") + "<span>Kamera nicht erreichbar</span>";
@@ -652,7 +655,9 @@ function patchDesk(printer) {
deskEl.dataset.host = printer.host;
deskEl.dataset.filename = printer.filename || "";
deskEl.dataset.progress = String(printer.progress || 0);
deskEl.className = "desk " + state;
deskEl.classList.remove(...DESK_STATES);
deskEl.classList.add(state);
syncDeskCamCollapsed();
const role = (name) => deskEl.querySelector(`[data-role=${name}]`);
const nameEl = role("name");
@@ -1006,6 +1011,14 @@ async function refresh() {
document.getElementById("summary").innerHTML = summaryText(all) || "keine Drucker";
document.getElementById("when").textContent = new Date().toLocaleTimeString("de-DE", { hour: "2-digit", minute: "2-digit" });
if (!all.length) {
lastFleetKey = "";
cardNodes.clear();
cardsEl.innerHTML = `<div class="empty-state">${icon("printer", "lg")}<p>Noch keine Drucker.</p><p class="tiny">Unter Einstellungen anlegen.</p><a href="/einstellungen">Zu den Einstellungen</a></div>`;
if (deskHost) closeDesk();
return;
}
const sections = ensureFleetShell(printers);
const seen = new Set();
for (const printer of printers) {