Files
grotti/web/templates/einstellungen.html
T

350 lines
14 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{% extends "layout.html" %}
{% block body %}
<div class="page-head">
<div>
<h1>Einstellungen</h1>
<p class="tiny">Drucker, Benachrichtigungen und Energiekosten.</p>
</div>
</div>
<section class="card">
<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="field" style="max-width:16rem">
<label for="display-mode">Anzeige</label>
<select id="display-mode">
<option value="desktop">Desktop</option>
<option value="shelf">Regal (Touch)</option>
</select>
</div>
<p class="hint">Regal: größere Bedienelemente, kompakte Navigation — ideal für ~1024×860 Touch am Regal. Shortcut: <code>?shelf=1</code></p>
<p id="display-note" class="ok" hidden>Gespeichert — gilt sofort.</p>
</section>
<section class="card">
<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>
</section>
<section class="card">
<h2>Öffentliche Adresse</h2>
<p class="hint">FQDN bzw. Basis-URL für QR-Codes auf Spulen-Etiketten. Leer = Host der aktuellen Anfrage (z.&nbsp;B. localhost).</p>
<div class="field" style="max-width:28rem">
<label for="fqdn">FQDN</label>
<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>
<p id="fqdn-note"></p>
</section>
<section class="card">
<h2>Drucker</h2>
<p class="hint">Gesperrt = kein neuer „wenn frei“-Start auf diesem Drucker.</p>
<div class="table-wrap">
<table>
<thead><tr><th>Name</th><th>Host</th><th>Familie</th><th>Gesperrt</th><th></th></tr></thead>
<tbody id="printer-rows"></tbody>
</table>
</div>
<form id="printer-add" class="grid-2" style="margin-top:1rem">
<div class="field">
<label for="add-name">Name</label>
<input id="add-name" required placeholder="z. B. Kobra 3 Werkstatt">
</div>
<div class="field">
<label for="add-host">Host / IP</label>
<input id="add-host" required placeholder="10.10.1.60">
</div>
<div class="field">
<label for="add-family">Familie</label>
<select id="add-family">
<option value="">automatisch</option>
<option value="kobra_3">Kobra 3</option>
<option value="kobra_s1">Kobra S1</option>
</select>
</div>
<div class="actions" style="align-self:end"><button type="submit">Hinzufügen</button></div>
</form>
<p id="printer-note"></p>
</section>
<section class="card">
<h2>Telegram</h2>
<label class="check"><input type="checkbox" id="tg-enabled"> Benachrichtigungen aktiv</label>
<div class="grid-2" style="margin-top:0.75rem">
<div class="field">
<label for="tg-token">Bot-Token</label>
<input id="tg-token" type="password" autocomplete="off" placeholder="123456:ABC…">
</div>
<div class="field">
<label for="tg-chat">Chat-ID</label>
<input id="tg-chat" placeholder="z. B. -100… oder User-ID">
</div>
</div>
<div class="grid-2" style="margin-top:0.75rem">
<label class="check"><input type="checkbox" id="al-fertig"> Fertig</label>
<label class="check"><input type="checkbox" id="al-fehler"> Fehler und Abbrüche</label>
<label class="check"><input type="checkbox" id="al-offline"> Offline</label>
<label class="check"><input type="checkbox" id="al-queue"> Queue (Fehler, Ablauf gestoppt)</label>
<label class="check"><input type="checkbox" id="al-ablauf"> Ablauf-Schritte</label>
<label class="check"><input type="checkbox" id="al-start"> Druckstart und Pause</label>
</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" class="ghost" id="tg-test">Test senden</button>
</div>
<p id="tg-note"></p>
</section>
<section class="card">
<h2>Energie</h2>
<p class="hint">Für Kosten im Protokoll. Filament zählt erst mit zugeordneter Spule.</p>
<div class="grid-2">
<div class="field"><label for="en-watts">Drucker W</label><input id="en-watts" type="number" step="1"></div>
<div class="field"><label for="en-kwh">€ / kWh</label><input id="en-kwh" type="number" step="0.01"></div>
<div class="field"><label for="en-dryer">Trockner W</label><input id="en-dryer" type="number" step="1"></div>
<div class="field"><label for="en-dryh">Trockenstunden</label><input id="en-dryh" type="number" step="0.1"></div>
<div class="field"><label for="en-waste">Ausschuss %</label><input id="en-waste" type="number" step="0.1"></div>
<div class="field"><label for="en-util">Auslastung %</label><input id="en-util" type="number" step="1"></div>
</div>
<div class="actions"><button type="button" id="en-save">Speichern</button></div>
<p id="en-note"></p>
</section>
{% endblock %}
{% block script %}
<script>
const families = { kobra_3: "Kobra 3", kobra_s1: "Kobra S1", "": "—" };
/* showNote aus api.js: Inline-Hinweis + Toast */
function renderPrinters(rows) {
const body = document.getElementById("printer-rows");
body.innerHTML = "";
if (!rows.length) {
body.innerHTML = '<tr><td class="empty" colspan="5">Keine Drucker.</td></tr>';
return;
}
rows.forEach((row) => {
const tr = document.createElement("tr");
tr.innerHTML = `<td><input data-f="name"></td><td><input data-f="host"></td><td><select data-f="family">
<option value="kobra_3">Kobra 3</option>
<option value="kobra_s1">Kobra S1</option>
<option value="">—</option>
</select></td><td><label class="check"><input type="checkbox" data-f="locked"></label></td><td></td>`;
tr.querySelector('[data-f="name"]').value = row.name || "";
tr.querySelector('[data-f="host"]').value = row.host || "";
tr.querySelector('[data-f="family"]').value = row.family || "";
tr.querySelector('[data-f="locked"]').checked = !!row.locked;
const save = document.createElement("button");
save.type = "button";
save.textContent = "Speichern";
save.addEventListener("click", async () => {
try {
await api("/drucker/id/" + row.id, {
method: "PATCH",
json: {
name: tr.querySelector('[data-f="name"]').value,
host: tr.querySelector('[data-f="host"]').value,
family: tr.querySelector('[data-f="family"]').value,
locked: tr.querySelector('[data-f="locked"]').checked,
},
});
showNote("printer-note", null, "Gespeichert: " + tr.querySelector('[data-f="name"]').value);
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);
}
});
});
tr.children[4].appendChild(save);
tr.children[4].appendChild(del);
body.appendChild(tr);
});
}
async function load() {
const [settings, printers] = await Promise.all([
api("/einstellungen"),
api("/drucker?live=0"),
]);
renderPrinters(printers);
document.getElementById("auto-queue").checked = settings.auto_queue !== false;
document.getElementById("fqdn").value = settings.fqdn || "";
document.getElementById("tg-enabled").checked = !!(settings.telegram && settings.telegram.enabled);
document.getElementById("tg-token").value = "";
document.getElementById("tg-token").placeholder = settings.telegram && settings.telegram.bot_token_set
? ("gesetzt: " + (settings.telegram.bot_token || "••••"))
: "Bot-Token";
document.getElementById("tg-chat").value = (settings.telegram && settings.telegram.chat_id) || "";
const alerts = settings.alerts || {};
document.getElementById("al-fertig").checked = alerts.fertig !== false;
document.getElementById("al-fehler").checked = alerts.fehler !== false;
document.getElementById("al-offline").checked = alerts.offline !== false;
document.getElementById("al-queue").checked = alerts.queue !== false;
document.getElementById("al-ablauf").checked = !!alerts.ablauf;
document.getElementById("al-start").checked = !!alerts.start;
const en = settings.energy || {};
document.getElementById("en-watts").value = en.watts ?? 120;
document.getElementById("en-kwh").value = en.euro_per_kwh ?? 0.35;
document.getElementById("en-dryer").value = en.dryer_watts ?? 0;
document.getElementById("en-dryh").value = en.dry_hours ?? 0;
document.getElementById("en-waste").value = en.waste_percent ?? 0;
document.getElementById("en-util").value = en.utilization ?? 50;
}
document.getElementById("auto-save").addEventListener("click", async () => {
try {
await api("/einstellungen", {
method: "PATCH",
json: { auto_queue: document.getElementById("auto-queue").checked },
});
showNote(
"auto-note",
null,
document.getElementById("auto-queue").checked ? "Automatik an." : "Automatik aus — nichts Neues startet."
);
} catch (error) {
showNote("auto-note", error);
}
});
document.getElementById("fqdn-save").addEventListener("click", async () => {
try {
const out = await api("/einstellungen", {
method: "PATCH",
json: { fqdn: document.getElementById("fqdn").value.trim() },
});
document.getElementById("fqdn").value = out.fqdn || "";
showNote(
"fqdn-note",
null,
out.fqdn
? "Gespeichert — Etiketten-QR: " + out.fqdn + "/material?spool=…"
: "Leer — Etiketten nutzen den Request-Host."
);
} catch (error) {
showNote("fqdn-note", error);
}
});
document.getElementById("printer-add").addEventListener("submit", async (event) => {
event.preventDefault();
try {
await api("/drucker", {
method: "POST",
json: {
name: document.getElementById("add-name").value,
host: document.getElementById("add-host").value,
family: document.getElementById("add-family").value,
},
});
document.getElementById("printer-add").reset();
showNote("printer-note", null, "Drucker angelegt.");
load();
} catch (error) {
showNote("printer-note", error);
}
});
document.getElementById("tg-save").addEventListener("click", async () => {
const token = document.getElementById("tg-token").value.trim();
const payload = {
telegram: {
enabled: document.getElementById("tg-enabled").checked,
chat_id: document.getElementById("tg-chat").value.trim(),
},
alerts: {
fertig: document.getElementById("al-fertig").checked,
fehler: document.getElementById("al-fehler").checked,
offline: document.getElementById("al-offline").checked,
queue: document.getElementById("al-queue").checked,
ablauf: document.getElementById("al-ablauf").checked,
start: document.getElementById("al-start").checked,
},
};
if (token) payload.telegram.bot_token = token;
try {
await api("/einstellungen", { method: "PATCH", json: payload });
showNote("tg-note", null, "Telegram gespeichert.");
load();
} catch (error) {
showNote("tg-note", error);
}
});
document.getElementById("tg-test").addEventListener("click", async () => {
try {
await api("/telegram/test", { method: "POST", json: {} });
showNote("tg-note", null, "Testnachricht gesendet.");
} catch (error) {
showNote("tg-note", error);
}
});
document.getElementById("en-save").addEventListener("click", async () => {
try {
await api("/einstellungen", {
method: "PATCH",
json: {
energy: {
watts: Number(document.getElementById("en-watts").value),
euro_per_kwh: Number(document.getElementById("en-kwh").value),
dryer_watts: Number(document.getElementById("en-dryer").value),
dry_hours: Number(document.getElementById("en-dryh").value),
waste_percent: Number(document.getElementById("en-waste").value),
utilization: Number(document.getElementById("en-util").value),
},
},
});
showNote("en-note", null, "Energie gespeichert.");
} catch (error) {
showNote("en-note", error);
}
});
(function initDisplayMode() {
const sel = document.getElementById("display-mode");
if (!sel) return;
try {
sel.value = localStorage.getItem("grotti-display") === "shelf" ? "shelf" : "desktop";
} catch (error) {
sel.value = "desktop";
}
sel.addEventListener("change", () => {
if (typeof setDisplayMode === "function") setDisplayMode(sel.value);
else {
try { localStorage.setItem("grotti-display", sel.value === "shelf" ? "shelf" : "desktop"); } catch (error) {}
document.documentElement.classList.toggle("shelf", sel.value === "shelf");
document.body.classList.toggle("shelf", sel.value === "shelf");
}
const note = document.getElementById("display-note");
if (note) {
note.hidden = false;
setTimeout(() => { note.hidden = true; }, 2500);
}
});
})();
load().catch((error) => showNote("printer-note", error));
</script>
{% endblock %}