Grotti 3.2.0: Jetzt-Liste, Filament-Pause, Strom-Torte, UI-Polish.
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.
This commit is contained in:
1 parent
6e59df22e5
commit
4ddffbbeb1
68 files changed
+4348
-1215
No files matched your search
+234
-101
@@ -5,9 +5,26 @@
|
||||
<h1>Einstellungen</h1>
|
||||
<p class="tiny">Drucker, Benachrichtigungen und Energiekosten.</p>
|
||||
</div>
|
||||
<p class="settings-save-hint" id="settings-autosave-hint" aria-live="polite"></p>
|
||||
</div>
|
||||
|
||||
<section class="card">
|
||||
<div class="settings-layout">
|
||||
<nav class="settings-nav" aria-label="Abschnitte">
|
||||
<a href="#sec-browser"><svg class="ico"><use href="#i-monitor"/></svg>Browser</a>
|
||||
<a href="#sec-auto"><svg class="ico"><use href="#i-zap"/></svg>Automatik</a>
|
||||
<a href="#sec-slice" class="admin-only"><svg class="ico"><use href="#i-folder"/></svg>Slice</a>
|
||||
<a href="#sec-fqdn" class="admin-only"><svg class="ico"><use href="#i-link"/></svg>Adresse</a>
|
||||
<a href="#sec-drucker" class="admin-only"><svg class="ico"><use href="#i-printer"/></svg>Drucker</a>
|
||||
<a href="#sec-telegram" class="admin-only"><svg class="ico"><use href="#i-message"/></svg>Telegram</a>
|
||||
<a href="#sec-ntfy" class="admin-only"><svg class="ico"><use href="#i-bell"/></svg>ntfy</a>
|
||||
<a href="#sec-backup" class="admin-only"><svg class="ico"><use href="#i-hard-drive"/></svg>Backup</a>
|
||||
<a href="#sec-energie" class="admin-only"><svg class="ico"><use href="#i-zap"/></svg>Energie</a>
|
||||
<a href="#sec-users" class="admin-only"><svg class="ico"><use href="#i-users"/></svg>Benutzer</a>
|
||||
<a href="#sec-tokens"><svg class="ico"><use href="#i-key"/></svg>API-Tokens</a>
|
||||
</nav>
|
||||
<div class="settings-main">
|
||||
|
||||
<section class="card" id="sec-browser">
|
||||
<h2>Dieser Browser</h2>
|
||||
<p class="hint">Nur auf diesem Gerät / in diesem Browser. Anderer PC oder Inkognito bleibt unberührt.</p>
|
||||
<div class="grid-2 narrow-fields">
|
||||
@@ -36,20 +53,28 @@
|
||||
<option value="UTC">UTC</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="camera-mode">Kamera</label>
|
||||
<select id="camera-mode">
|
||||
<option value="snapshot">Snapshot (schonend)</option>
|
||||
<option value="stream">Stream (flüssig)</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<p class="hint">Regal: größere Bedienelemente — Shortcut <code>?shelf=1</code>. Darstellung und Zeitzone nur dieser Browser.</p>
|
||||
<p class="hint">Regal: größere Bedienelemente — Shortcut <code>?shelf=1</code>. Darstellung, Zeitzone und Kamera nur dieser Browser. Stream hält eine Verbindung je sichtbarer Kamera.</p>
|
||||
<p id="display-note" class="ok" hidden>Gespeichert — gilt sofort.</p>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<section class="card" id="sec-auto">
|
||||
<h2>Automatik</h2>
|
||||
<label class="check"><input type="checkbox" id="auto-queue"> Warteschlange „wenn frei“ und Abläufe automatisch starten</label>
|
||||
<p class="hint">Aus = keine neuen Starts. Laufende Abläufe und gesperrte Drucker bleiben unberührt.</p>
|
||||
<div class="actions"><button type="button" id="auto-save">Speichern</button></div>
|
||||
<p id="auto-note"></p>
|
||||
<label class="check"><input type="checkbox" id="auto-retry"> Automatisch einmal nochmal versuchen bei Fehler</label>
|
||||
<p class="hint">Aus = keine neuen Starts. Laufende Abläufe und gesperrte Drucker bleiben unberührt. Änderungen speichern sich selbst.</p>
|
||||
<div class="actions"><button type="button" id="auto-save" hidden>Speichern</button></div>
|
||||
<p id="auto-note" class="settings-save-hint"></p>
|
||||
</section>
|
||||
|
||||
<section class="card admin-only">
|
||||
<section class="card admin-only" id="sec-slice">
|
||||
<h2>Slice-Ordner</h2>
|
||||
<p class="hint">Ordner, den Grotti für die Bibliothek beobachtet (Orca-Export, „Ordner neu lesen“). Leer = nur manueller Upload. Standard: Ordner <code>slice</code> im Grotti-Repo.</p>
|
||||
<div class="field" style="max-width:36rem">
|
||||
@@ -57,13 +82,13 @@
|
||||
<input id="watch-dir" type="text" autocomplete="off" placeholder="D:\Projekte\Grotti\slice" spellcheck="false">
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button type="button" id="watch-save">Speichern</button>
|
||||
<button type="button" id="watch-save" hidden>Speichern</button>
|
||||
<button type="button" class="ghost" id="watch-default">Standardordner</button>
|
||||
</div>
|
||||
<p id="watch-note"></p>
|
||||
</section>
|
||||
|
||||
<section class="card admin-only">
|
||||
<section class="card admin-only" id="sec-fqdn">
|
||||
<h2>Öffentliche Adresse</h2>
|
||||
<p class="hint">FQDN bzw. Basis-URL für QR-Codes auf Spulen-Etiketten. Leer = Host der aktuellen Anfrage (z. B. localhost).</p>
|
||||
<div class="field" style="max-width:28rem">
|
||||
@@ -71,14 +96,14 @@
|
||||
<input id="fqdn" placeholder="https://grotti.example.de oder grotti.lan" autocomplete="off">
|
||||
</div>
|
||||
<p class="hint">Ohne Schema wird <code>https://</code> ergänzt. Mit Port: <code>http://10.10.1.5:8770</code>.</p>
|
||||
<div class="actions"><button type="button" id="fqdn-save">Speichern</button></div>
|
||||
<div class="actions"><button type="button" id="fqdn-save" hidden>Speichern</button></div>
|
||||
<p id="fqdn-note"></p>
|
||||
</section>
|
||||
|
||||
<section class="card admin-only">
|
||||
<section class="card admin-only" id="sec-drucker">
|
||||
<h2>Drucker</h2>
|
||||
<p class="hint">Gesperrt = kein neuer „wenn frei“-Start. Zeitfenster begrenzt Starts (z. B. Nachtstrom).</p>
|
||||
<div id="printer-list" class="printer-edit-list"></div>
|
||||
<p class="hint">Gesperrt = kein neuer „wenn frei“-Start. Zeitfenster begrenzt Starts (z. B. Nachtstrom). Tippen öffnet den Dialog.</p>
|
||||
<div id="printer-list" class="printer-edit-cards"></div>
|
||||
<form id="printer-add" class="grid-2 section-gap">
|
||||
<div class="field">
|
||||
<label for="add-name">Name</label>
|
||||
@@ -101,7 +126,7 @@
|
||||
<p id="printer-note"></p>
|
||||
</section>
|
||||
|
||||
<section class="card admin-only">
|
||||
<section class="card admin-only" id="sec-telegram">
|
||||
<h2>Telegram</h2>
|
||||
<label class="check"><input type="checkbox" id="tg-enabled"> Benachrichtigungen aktiv</label>
|
||||
<div class="grid-2" style="margin-top:0.75rem">
|
||||
@@ -124,13 +149,13 @@
|
||||
</div>
|
||||
<p class="hint">Ereignisse stehen immer unter <a href="/aktivitaet">Aktivität</a>. Die Häkchen steuern nur Telegram.</p>
|
||||
<div class="actions">
|
||||
<button type="button" id="tg-save">Speichern</button>
|
||||
<button type="button" id="tg-save" hidden>Speichern</button>
|
||||
<button type="button" class="ghost" id="tg-test">Test senden</button>
|
||||
</div>
|
||||
<p id="tg-note"></p>
|
||||
</section>
|
||||
|
||||
<section class="card admin-only">
|
||||
<section class="card admin-only" id="sec-ntfy">
|
||||
<h2>ntfy</h2>
|
||||
<label class="check"><input type="checkbox" id="ntfy-enabled"> Push über ntfy</label>
|
||||
<div class="grid-2" style="margin-top:0.75rem">
|
||||
@@ -149,13 +174,13 @@
|
||||
</div>
|
||||
<p class="hint">Gleiche Meldungen wie Telegram — unabhängig abschaltbar. Push in der ntfy-App auf dem Handy.</p>
|
||||
<div class="actions">
|
||||
<button type="button" id="ntfy-save">Speichern</button>
|
||||
<button type="button" id="ntfy-save" hidden>Speichern</button>
|
||||
<button type="button" class="ghost" id="ntfy-test">Test senden</button>
|
||||
</div>
|
||||
<p id="ntfy-note"></p>
|
||||
</section>
|
||||
|
||||
<section class="card admin-only">
|
||||
<section class="card admin-only" id="sec-backup">
|
||||
<h2>Backup</h2>
|
||||
<p class="hint">PostgreSQL-Dump und Datenordner (ohne toolpath/ und alte Backups).</p>
|
||||
<div class="actions">
|
||||
@@ -165,7 +190,7 @@
|
||||
<p id="backup-note"></p>
|
||||
</section>
|
||||
|
||||
<section class="card admin-only">
|
||||
<section class="card admin-only" id="sec-energie">
|
||||
<h2>Energie</h2>
|
||||
<p class="hint">Kosten im Protokoll. Mit Düse-/Bett-Watt und ED-Proben schätzt Grotti den Verbrauch; sonst Dauer × Grundlast. Filament braucht eine Spule.</p>
|
||||
<div class="grid-2">
|
||||
@@ -188,11 +213,11 @@
|
||||
</div>
|
||||
<p class="hint">Grundlast = Elektronik/Motion. Düse/Bett 0 = nur pauschal. Mit HT/NT gewichtet Grotti die Stromkosten nach Uhrzeit.</p>
|
||||
<p id="en-ed-hint" class="hint" hidden></p>
|
||||
<div class="actions"><button type="button" id="en-save">Speichern</button></div>
|
||||
<div class="actions"><button type="button" id="en-save" hidden>Speichern</button></div>
|
||||
<p id="en-note"></p>
|
||||
</section>
|
||||
|
||||
<section class="card admin-only">
|
||||
<section class="card admin-only" id="sec-users">
|
||||
<h2>Benutzer</h2>
|
||||
<p class="hint">Rollen: <strong>Admin</strong> (alles) und <strong>Operator</strong> (Drucker, Queue, Material — keine Benutzerverwaltung).</p>
|
||||
<div class="table-wrap">
|
||||
@@ -213,7 +238,7 @@
|
||||
<p id="user-note"></p>
|
||||
</section>
|
||||
|
||||
<section class="card">
|
||||
<section class="card" id="sec-tokens">
|
||||
<h2>API-Tokens</h2>
|
||||
<p class="hint">Für App, Home-Automation oder MCP. Klartext erscheint nur einmal bei der Erstellung. Authorization: <code>Bearer grt_…</code></p>
|
||||
<div class="table-wrap">
|
||||
@@ -234,6 +259,38 @@
|
||||
<p class="ok" id="token-once" hidden></p>
|
||||
<p class="hint" id="legacy-token-hint" hidden>In der Config steht noch ein Shared-<code>token</code> — der wird seit 3.1 nicht mehr akzeptiert. Bitte leeren und API-Tokens (<code>grt_…</code>) nutzen.</p>
|
||||
</section>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<dialog id="printer-edit-dlg" class="abraeumen-dlg">
|
||||
<form method="dialog" id="printer-edit-form" class="abraeumen-form">
|
||||
<h2 id="printer-edit-title">Drucker</h2>
|
||||
<input type="hidden" id="pe-id">
|
||||
<div class="grid-2">
|
||||
<div class="field"><label for="pe-name">Name</label><input id="pe-name" required></div>
|
||||
<div class="field"><label for="pe-host">Host / IP</label><input id="pe-host" required></div>
|
||||
<div class="field"><label for="pe-family">Familie</label>
|
||||
<select id="pe-family">
|
||||
<option value="kobra_3">Kobra 3</option>
|
||||
<option value="kobra_s1">Kobra S1</option>
|
||||
<option value="">—</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="field"><label class="check" style="margin-top:1.4rem"><input type="checkbox" id="pe-locked"> Gesperrt</label></div>
|
||||
</div>
|
||||
<label class="check"><input type="checkbox" id="pe-win-on"> Nur in Zeitfenster starten</label>
|
||||
<div class="grid-2" id="pe-win-fields">
|
||||
<div class="field"><label for="pe-win-from">Von</label><input type="time" id="pe-win-from" value="22:00"></div>
|
||||
<div class="field"><label for="pe-win-to">Bis</label><input type="time" id="pe-win-to" value="06:00"></div>
|
||||
<div class="field"><label for="pe-win-max">Max. Stunden</label><input type="number" id="pe-win-max" min="0" step="0.5" placeholder="leer = egal"></div>
|
||||
</div>
|
||||
<div class="actions">
|
||||
<button type="submit" value="ok" class="btn-primary">Speichern</button>
|
||||
<button type="button" class="ghost" id="pe-cancel">Abbrechen</button>
|
||||
<button type="button" class="btn-danger" id="pe-delete">Entfernen</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
{% endblock %}
|
||||
{% block script %}
|
||||
<script>
|
||||
@@ -261,7 +318,27 @@ function windowFrom(row) {
|
||||
};
|
||||
}
|
||||
|
||||
let printerRowsCache = [];
|
||||
|
||||
function openPrinterDialog(row) {
|
||||
const dlg = document.getElementById("printer-edit-dlg");
|
||||
const win = windowFrom(row);
|
||||
document.getElementById("printer-edit-title").textContent = row.name || "Drucker";
|
||||
document.getElementById("pe-id").value = row.id;
|
||||
document.getElementById("pe-name").value = row.name || "";
|
||||
document.getElementById("pe-host").value = row.host || "";
|
||||
document.getElementById("pe-family").value = row.family || "";
|
||||
document.getElementById("pe-locked").checked = !!row.locked;
|
||||
document.getElementById("pe-win-on").checked = win.enabled;
|
||||
document.getElementById("pe-win-from").value = win.from;
|
||||
document.getElementById("pe-win-to").value = win.to;
|
||||
document.getElementById("pe-win-max").value = win.maxHours;
|
||||
document.getElementById("pe-win-fields").hidden = !win.enabled;
|
||||
dlg.showModal();
|
||||
}
|
||||
|
||||
function renderPrinters(rows) {
|
||||
printerRowsCache = rows || [];
|
||||
const root = document.getElementById("printer-list");
|
||||
root.innerHTML = "";
|
||||
if (!rows.length) {
|
||||
@@ -271,86 +348,22 @@ function renderPrinters(rows) {
|
||||
rows.forEach((row) => {
|
||||
const win = windowFrom(row);
|
||||
const card = document.createElement("div");
|
||||
card.className = "printer-edit";
|
||||
card.className = "printer-edit-card";
|
||||
card.innerHTML = `
|
||||
<div class="grid-2">
|
||||
<div class="field"><label>Name</label><input data-f="name"></div>
|
||||
<div class="field"><label>Host / IP</label><input data-f="host"></div>
|
||||
<div class="field"><label>Familie</label><select data-f="family">
|
||||
<option value="kobra_3">Kobra 3</option>
|
||||
<option value="kobra_s1">Kobra S1</option>
|
||||
<option value="">—</option>
|
||||
</select></div>
|
||||
<div class="field"><label class="check" style="margin-top:1.4rem"><input type="checkbox" data-f="locked"> Gesperrt</label></div>
|
||||
<div class="grow">
|
||||
<strong></strong>
|
||||
<div class="tiny"></div>
|
||||
</div>
|
||||
<details class="mat-more" ${win.enabled ? "open" : ""}>
|
||||
<summary><svg class="ico caret"><use href="#i-down"/></svg><span class="grow">Start-Zeitfenster</span>
|
||||
<span class="tiny">${win.enabled ? escapeHtml(win.from) + "–" + escapeHtml(win.to) : "aus"}</span>
|
||||
</summary>
|
||||
<div class="grid-2" style="margin-top:0.6rem">
|
||||
<label class="check" style="grid-column:1/-1"><input type="checkbox" data-f="win-on"> Nur in Zeitfenster starten</label>
|
||||
<div class="field"><label>Von</label><input type="time" data-f="win-from"></div>
|
||||
<div class="field"><label>Bis</label><input type="time" data-f="win-to"></div>
|
||||
<div class="field"><label>Max. Stunden (Nachtjob)</label><input type="number" min="0" step="0.5" data-f="win-max" placeholder="leer = egal"></div>
|
||||
</div>
|
||||
</details>
|
||||
<div class="actions"></div>`;
|
||||
card.querySelector('[data-f="name"]').value = row.name || "";
|
||||
card.querySelector('[data-f="host"]').value = row.host || "";
|
||||
card.querySelector('[data-f="family"]').value = row.family || "";
|
||||
card.querySelector('[data-f="locked"]').checked = !!row.locked;
|
||||
card.querySelector('[data-f="win-on"]').checked = win.enabled;
|
||||
card.querySelector('[data-f="win-from"]').value = win.from;
|
||||
card.querySelector('[data-f="win-to"]').value = win.to;
|
||||
card.querySelector('[data-f="win-max"]').value = win.maxHours;
|
||||
const save = document.createElement("button");
|
||||
save.type = "button";
|
||||
save.textContent = "Speichern";
|
||||
save.addEventListener("click", async () => {
|
||||
const on = card.querySelector('[data-f="win-on"]').checked;
|
||||
const start_window = {};
|
||||
if (on) {
|
||||
start_window.allowed = [{
|
||||
from: card.querySelector('[data-f="win-from"]').value || "22:00",
|
||||
to: card.querySelector('[data-f="win-to"]').value || "06:00",
|
||||
}];
|
||||
const maxH = Number(card.querySelector('[data-f="win-max"]').value);
|
||||
if (maxH > 0) start_window.max_hours_overnight = maxH;
|
||||
}
|
||||
try {
|
||||
await api("/drucker/id/" + row.id, {
|
||||
method: "PATCH",
|
||||
json: {
|
||||
name: card.querySelector('[data-f="name"]').value,
|
||||
host: card.querySelector('[data-f="host"]').value,
|
||||
family: card.querySelector('[data-f="family"]').value,
|
||||
locked: card.querySelector('[data-f="locked"]').checked,
|
||||
start_window,
|
||||
},
|
||||
});
|
||||
showNote("printer-note", null, "Gespeichert: " + card.querySelector('[data-f="name"]').value);
|
||||
toast("Drucker gespeichert.");
|
||||
load();
|
||||
} catch (error) {
|
||||
showNote("printer-note", error);
|
||||
}
|
||||
});
|
||||
const del = document.createElement("button");
|
||||
del.type = "button";
|
||||
del.className = "ghost warn";
|
||||
del.textContent = "Entfernen";
|
||||
del.addEventListener("click", () => {
|
||||
armButton(del, "Wirklich entfernen?", async () => {
|
||||
try {
|
||||
await api("/drucker/id/" + row.id, { method: "DELETE" });
|
||||
showNote("printer-note", null, "Drucker entfernt.");
|
||||
load();
|
||||
} catch (error) {
|
||||
showNote("printer-note", error);
|
||||
}
|
||||
});
|
||||
});
|
||||
card.querySelector(".actions").append(save, del);
|
||||
<button type="button" class="ghost">Bearbeiten</button>`;
|
||||
card.querySelector("strong").textContent = row.name || row.host;
|
||||
card.querySelector(".tiny").textContent = [
|
||||
row.host,
|
||||
families[row.family] || row.family || "",
|
||||
row.locked ? "gesperrt" : "",
|
||||
win.enabled ? ("Fenster " + win.from + "–" + win.to) : "",
|
||||
].filter(Boolean).join(" · ");
|
||||
card.querySelector("button").addEventListener("click", () => openPrinterDialog(row));
|
||||
card.addEventListener("dblclick", () => openPrinterDialog(row));
|
||||
root.appendChild(card);
|
||||
});
|
||||
}
|
||||
@@ -453,6 +466,8 @@ async function load() {
|
||||
]);
|
||||
if (isAdmin) renderPrinters(printers);
|
||||
document.getElementById("auto-queue").checked = settings.auto_queue !== false;
|
||||
const ar = document.getElementById("auto-retry");
|
||||
if (ar) ar.checked = settings.auto_retry !== false;
|
||||
const watchDir = document.getElementById("watch-dir");
|
||||
if (watchDir) {
|
||||
watchDir.value = settings.watch_dir || "";
|
||||
@@ -551,7 +566,10 @@ document.getElementById("auto-save").addEventListener("click", async () => {
|
||||
try {
|
||||
await api("/einstellungen", {
|
||||
method: "PATCH",
|
||||
json: { auto_queue: document.getElementById("auto-queue").checked },
|
||||
json: {
|
||||
auto_queue: document.getElementById("auto-queue").checked,
|
||||
auto_retry: document.getElementById("auto-retry")?.checked !== false,
|
||||
},
|
||||
});
|
||||
showNote(
|
||||
"auto-note",
|
||||
@@ -841,6 +859,121 @@ function flashDisplayNote() {
|
||||
});
|
||||
})();
|
||||
|
||||
(function initCameraMode() {
|
||||
const sel = document.getElementById("camera-mode");
|
||||
if (!sel || typeof cameraMode !== "function") return;
|
||||
sel.value = cameraMode();
|
||||
sel.addEventListener("change", () => {
|
||||
setCameraMode(sel.value);
|
||||
flashDisplayNote();
|
||||
});
|
||||
window.addEventListener("grotti-camera-mode", () => {
|
||||
sel.value = cameraMode();
|
||||
});
|
||||
})();
|
||||
|
||||
document.getElementById("pe-win-on")?.addEventListener("change", () => {
|
||||
document.getElementById("pe-win-fields").hidden = !document.getElementById("pe-win-on").checked;
|
||||
});
|
||||
document.getElementById("pe-cancel")?.addEventListener("click", () => {
|
||||
document.getElementById("printer-edit-dlg").close();
|
||||
});
|
||||
document.getElementById("printer-edit-form")?.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
const id = document.getElementById("pe-id").value;
|
||||
const on = document.getElementById("pe-win-on").checked;
|
||||
const start_window = {};
|
||||
if (on) {
|
||||
start_window.allowed = [{
|
||||
from: document.getElementById("pe-win-from").value || "22:00",
|
||||
to: document.getElementById("pe-win-to").value || "06:00",
|
||||
}];
|
||||
const maxH = Number(document.getElementById("pe-win-max").value);
|
||||
if (maxH > 0) start_window.max_hours_overnight = maxH;
|
||||
}
|
||||
try {
|
||||
await api("/drucker/id/" + id, {
|
||||
method: "PATCH",
|
||||
json: {
|
||||
name: document.getElementById("pe-name").value,
|
||||
host: document.getElementById("pe-host").value,
|
||||
family: document.getElementById("pe-family").value,
|
||||
locked: document.getElementById("pe-locked").checked,
|
||||
start_window,
|
||||
},
|
||||
});
|
||||
document.getElementById("printer-edit-dlg").close();
|
||||
toast("Drucker gespeichert.");
|
||||
markAutosaved();
|
||||
load();
|
||||
} catch (error) {
|
||||
showNote("printer-note", error);
|
||||
}
|
||||
});
|
||||
document.getElementById("pe-delete")?.addEventListener("click", (event) => {
|
||||
const id = document.getElementById("pe-id").value;
|
||||
armButton(event.currentTarget, "Wirklich entfernen?", async () => {
|
||||
try {
|
||||
await api("/drucker/id/" + id, { method: "DELETE" });
|
||||
document.getElementById("printer-edit-dlg").close();
|
||||
toast("Drucker entfernt.");
|
||||
load();
|
||||
} catch (error) {
|
||||
showNote("printer-note", error);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function markAutosaved() {
|
||||
const el = document.getElementById("settings-autosave-hint");
|
||||
if (!el) return;
|
||||
el.textContent = "Gespeichert ✓";
|
||||
clearTimeout(markAutosaved._t);
|
||||
markAutosaved._t = setTimeout(() => { el.textContent = ""; }, 2200);
|
||||
}
|
||||
|
||||
function wireAutosave(triggerId, noteId) {
|
||||
const btn = document.getElementById(triggerId);
|
||||
if (!btn) return;
|
||||
const run = () => {
|
||||
btn.click();
|
||||
markAutosaved();
|
||||
if (noteId) {
|
||||
const note = document.getElementById(noteId);
|
||||
if (note) note.textContent = "Gespeichert ✓";
|
||||
}
|
||||
};
|
||||
let timer = 0;
|
||||
return (event) => {
|
||||
if (event && event.target && event.target.id === triggerId) return;
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(run, 450);
|
||||
};
|
||||
}
|
||||
|
||||
document.getElementById("auto-queue")?.addEventListener("change", wireAutosave("auto-save", "auto-note"));
|
||||
document.getElementById("auto-retry")?.addEventListener("change", wireAutosave("auto-save", "auto-note"));
|
||||
["watch-dir"].forEach((id) => {
|
||||
document.getElementById(id)?.addEventListener("change", wireAutosave("watch-save", "watch-note"));
|
||||
});
|
||||
document.getElementById("fqdn")?.addEventListener("change", wireAutosave("fqdn-save", "fqdn-note"));
|
||||
["tg-enabled", "tg-token", "tg-chat", "al-fertig", "al-fehler", "al-offline", "al-queue", "al-ablauf", "al-start"].forEach((id) => {
|
||||
document.getElementById(id)?.addEventListener("change", wireAutosave("tg-save", "tg-note"));
|
||||
});
|
||||
["ntfy-enabled", "ntfy-server", "ntfy-topic", "ntfy-token"].forEach((id) => {
|
||||
document.getElementById(id)?.addEventListener("change", wireAutosave("ntfy-save", "ntfy-note"));
|
||||
});
|
||||
["en-watts", "en-kwh", "en-nozzle", "en-bed", "en-dryer", "en-dryh", "en-waste", "en-util", "en-tariff", "en-ht", "en-nt", "en-nt-from", "en-nt-to"].forEach((id) => {
|
||||
document.getElementById(id)?.addEventListener("change", wireAutosave("en-save", "en-note"));
|
||||
});
|
||||
|
||||
document.querySelectorAll(".settings-nav a").forEach((a) => {
|
||||
a.addEventListener("click", () => {
|
||||
document.querySelectorAll(".settings-nav a").forEach((x) => x.classList.remove("on"));
|
||||
a.classList.add("on");
|
||||
});
|
||||
});
|
||||
|
||||
load().catch((error) => showNote("printer-note", error));
|
||||
</script>
|
||||
{% endblock %}
|
||||
Reference in new issue
Block a user