Grotti 2.0.0: eigenes Repo, Umbenennung von Farm.
This commit is contained in:
commit
b2e6443c68
69 files changed
+18719
No files matched your search
@@ -0,0 +1,349 @@
|
||||
{% 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. 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 %}
|
||||
Reference in new issue
Block a user