Grotti 3.7.0: Treiber für Kobra 4 und Marlin, smartere Dateien-Auswahl und einstellbare Etiketten.
Senden und Abbruch laufen über den Treiber, Orca-Doppelzeilen und tote Ideen-API fallen weg, Drucker folgen der Datei, Etikettenformat liegt in den Einstellungen.
This commit is contained in:
1 parent
5081212212
commit
db6a85a703
54 files changed
+3379
-384
No files matched your search
@@ -14,7 +14,9 @@
|
||||
<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-etikett" class="admin-only"><svg class="ico"><use href="#i-tag"/></svg>Etiketten</a>
|
||||
<a href="#sec-drucker" class="admin-only"><svg class="ico"><use href="#i-printer"/></svg>Drucker</a>
|
||||
<a href="#sec-meldungen" class="admin-only"><svg class="ico"><use href="#i-alert"/></svg>Meldungen</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>
|
||||
@@ -100,9 +102,34 @@
|
||||
<p id="fqdn-note"></p>
|
||||
</section>
|
||||
|
||||
<section class="card admin-only" id="sec-etikett">
|
||||
<h2>Etiketten</h2>
|
||||
<p class="hint">Papierformat für Spulen- und Drucketiketten (Browser-Druck). Standard 70×40 mm. Der QR schrumpft auf flacheren Rollen mit.</p>
|
||||
<div class="field" style="max-width:16rem">
|
||||
<label for="etikett-preset">Voreinstellung</label>
|
||||
<select id="etikett-preset">
|
||||
<option value="">Eigene Maße</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="grid-2" style="max-width:20rem">
|
||||
<div class="field">
|
||||
<label for="etikett-w">Breite (mm)</label>
|
||||
<input id="etikett-w" type="number" min="20" max="200" step="0.1" value="70">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="etikett-h">Höhe (mm)</label>
|
||||
<input id="etikett-h" type="number" min="15" max="200" step="0.1" value="40">
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions"><button type="button" id="etikett-save" hidden>Speichern</button></div>
|
||||
<p id="etikett-note"></p>
|
||||
</section>
|
||||
|
||||
<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). Tippen öffnet den Dialog.</p>
|
||||
<p class="hint">USB (Anycubic i3, Tenlog): Schnittstelle statt IP, zum Beispiel <code>COM5</code> oder <code>COM5@250000</code>. Grotti startet den USB-Dienst dazu selbst und lässt ihn bei einem Neustart weiterlaufen.</p>
|
||||
<p class="hint" id="marlin-line" hidden></p>
|
||||
<div id="printer-list" class="printer-edit-cards"></div>
|
||||
<form id="printer-add" class="grid-2 section-gap">
|
||||
<div class="field">
|
||||
@@ -110,7 +137,7 @@
|
||||
<input id="add-name" required placeholder="z. B. Kobra 3 Werkstatt">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="add-host">Host / IP</label>
|
||||
<label for="add-host" id="add-host-label">Host / IP</label>
|
||||
<input id="add-host" required placeholder="10.10.1.60">
|
||||
</div>
|
||||
<div class="field">
|
||||
@@ -119,6 +146,9 @@
|
||||
<option value="">automatisch</option>
|
||||
<option value="kobra_3">Kobra 3</option>
|
||||
<option value="kobra_s1">Kobra S1</option>
|
||||
<option value="kobra_4">Kobra 4</option>
|
||||
<option value="anycubic_i3">Anycubic i3</option>
|
||||
<option value="tenlog">Tenlog</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="actions" style="align-self:end"><button type="submit">Hinzufügen</button></div>
|
||||
@@ -126,6 +156,13 @@
|
||||
<p id="printer-note"></p>
|
||||
</section>
|
||||
|
||||
<section class="card admin-only" id="sec-meldungen">
|
||||
<h2>Problem-Meldungen</h2>
|
||||
<p class="hint">Kopien von „Problem melden“. Offene Einträge hier erledigen. Issues landen beim Entwickler über den Empfänger auf git.gundel.dev.</p>
|
||||
<div id="meldung-list" class="printer-edit-cards"></div>
|
||||
<p id="meldung-admin-note" class="tiny"></p>
|
||||
</section>
|
||||
|
||||
<section class="card admin-only" id="sec-telegram">
|
||||
<h2>Telegram</h2>
|
||||
<label class="check"><input type="checkbox" id="tg-enabled"> Benachrichtigungen aktiv</label>
|
||||
@@ -272,11 +309,14 @@
|
||||
<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-host" id="pe-host-label">Host / IP</label><input id="pe-host" required placeholder="10.10.1.60"></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="kobra_4">Kobra 4</option>
|
||||
<option value="anycubic_i3">Anycubic i3</option>
|
||||
<option value="tenlog">Tenlog</option>
|
||||
<option value="">—</option>
|
||||
</select>
|
||||
</div>
|
||||
@@ -299,7 +339,18 @@
|
||||
{% block script %}
|
||||
<script src="{{ url_for('static', filename='qrcode.min.js') }}?v=1"></script>
|
||||
<script>
|
||||
const families = { kobra_3: "Kobra 3", kobra_s1: "Kobra S1", "": "—" };
|
||||
const families = {
|
||||
kobra_3: "Kobra 3", kobra_s1: "Kobra S1", kobra_4: "Kobra 4",
|
||||
anycubic_i3: "Anycubic i3", tenlog: "Tenlog", "": "—",
|
||||
};
|
||||
const USB_FAMILIES = new Set(["anycubic_i3", "tenlog"]);
|
||||
|
||||
function syncHostField(family, labelEl, inputEl, nameHint) {
|
||||
const usb = USB_FAMILIES.has(family)
|
||||
|| (!family && /tenlog|i3/i.test(nameHint || ""));
|
||||
if (labelEl) labelEl.textContent = usb ? "Schnittstelle" : "Host / IP";
|
||||
if (inputEl) inputEl.placeholder = usb ? "COM5 oder COM5@250000" : "10.10.1.60";
|
||||
}
|
||||
const isAdmin = document.querySelector('meta[name="user-role"]')?.content === "admin";
|
||||
document.querySelectorAll(".admin-only").forEach((el) => { el.hidden = !isAdmin; });
|
||||
if (!isAdmin) {
|
||||
@@ -333,6 +384,7 @@ function openPrinterDialog(row) {
|
||||
document.getElementById("pe-name").value = row.name || "";
|
||||
document.getElementById("pe-host").value = row.host || "";
|
||||
document.getElementById("pe-family").value = row.family || "";
|
||||
syncHostField(row.family || "", document.getElementById("pe-host-label"), document.getElementById("pe-host"), row.name || "");
|
||||
document.getElementById("pe-locked").checked = !!row.locked;
|
||||
document.getElementById("pe-win-on").checked = win.enabled;
|
||||
document.getElementById("pe-win-from").value = win.from;
|
||||
@@ -455,6 +507,62 @@ function renderTokens(rows) {
|
||||
});
|
||||
}
|
||||
|
||||
async function loadMeldungen() {
|
||||
const root = document.getElementById("meldung-list");
|
||||
const note = document.getElementById("meldung-admin-note");
|
||||
if (!root || !isAdmin) return;
|
||||
try {
|
||||
const rows = await api("/meldungen?status=offen");
|
||||
if (!rows.length) {
|
||||
root.innerHTML = '<p class="tiny">Keine offenen Meldungen.</p>';
|
||||
return;
|
||||
}
|
||||
root.innerHTML = rows.map((row) => `
|
||||
<article class="printer-edit-card">
|
||||
<div class="grow">
|
||||
<strong>#${row.id}</strong>
|
||||
<span class="tiny">${escapeHtml((row.created_at || "").replace("T", " ").slice(0, 16))} · ${escapeHtml(row.username || "—")} · ${escapeHtml(row.page || "")}</span>
|
||||
<p>${escapeHtml(row.text || "")}</p>
|
||||
<p class="tiny">${row.relay_ok ? "Beim Entwickler angekommen" : ("Lokal: " + escapeHtml(row.relay_message || "Empfänger fehlte"))}</p>
|
||||
</div>
|
||||
<button type="button" class="ghost" data-meldung-done="${row.id}">Erledigt</button>
|
||||
</article>`).join("");
|
||||
root.querySelectorAll("[data-meldung-done]").forEach((button) => {
|
||||
button.addEventListener("click", async () => {
|
||||
button.disabled = true;
|
||||
try {
|
||||
await api("/meldungen/" + button.dataset.meldungDone + "/erledigt", { method: "POST", json: {} });
|
||||
loadMeldungen();
|
||||
} catch (error) {
|
||||
if (note) note.textContent = error.message || String(error);
|
||||
}
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
if (note) note.textContent = error.message || String(error);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadMarlin(printers) {
|
||||
const line = document.getElementById("marlin-line");
|
||||
if (!line) return;
|
||||
const usb = (printers || []).some((row) => row.family === "tenlog" || row.family === "anycubic_i3");
|
||||
if (!usb) {
|
||||
line.hidden = true;
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const status = await api("/marlin-dienst");
|
||||
line.hidden = false;
|
||||
line.textContent = status.running
|
||||
? "USB-Dienst läuft."
|
||||
: (status.message || "USB-Dienst ist aus.");
|
||||
} catch (err) {
|
||||
line.hidden = false;
|
||||
line.textContent = "USB-Dienst: " + (err.message || "keine Antwort");
|
||||
}
|
||||
}
|
||||
|
||||
async function loadUsers() {
|
||||
if (!isAdmin) return;
|
||||
renderUsers(await api("/users"));
|
||||
@@ -469,7 +577,11 @@ async function load() {
|
||||
api("/einstellungen"),
|
||||
api("/drucker?live=0"),
|
||||
]);
|
||||
if (isAdmin) renderPrinters(printers);
|
||||
if (isAdmin) {
|
||||
renderPrinters(printers);
|
||||
loadMarlin(printers);
|
||||
loadMeldungen();
|
||||
}
|
||||
document.getElementById("auto-queue").checked = settings.auto_queue !== false;
|
||||
const ar = document.getElementById("auto-retry");
|
||||
if (ar) ar.checked = settings.auto_retry !== false;
|
||||
@@ -480,6 +592,7 @@ async function load() {
|
||||
}
|
||||
const fqdn = document.getElementById("fqdn");
|
||||
if (fqdn) fqdn.value = settings.fqdn || "";
|
||||
fillEtikett(settings.etikett || {});
|
||||
const tgEnabled = document.getElementById("tg-enabled");
|
||||
if (tgEnabled) {
|
||||
tgEnabled.checked = !!(settings.telegram && settings.telegram.enabled);
|
||||
@@ -643,6 +756,109 @@ document.getElementById("fqdn-save").addEventListener("click", async () => {
|
||||
}
|
||||
});
|
||||
|
||||
let etikettPresets = [];
|
||||
|
||||
function fillEtikett(etikett) {
|
||||
const w = document.getElementById("etikett-w");
|
||||
const h = document.getElementById("etikett-h");
|
||||
const preset = document.getElementById("etikett-preset");
|
||||
if (!w || !h || !preset) return;
|
||||
etikettPresets = Array.isArray(etikett.presets) ? etikett.presets : etikettPresets;
|
||||
const width = Number(etikett.width_mm) || 70;
|
||||
const height = Number(etikett.height_mm) || 40;
|
||||
w.value = String(width);
|
||||
h.value = String(height);
|
||||
const current = preset.value;
|
||||
preset.innerHTML = '<option value="">Eigene Maße</option>';
|
||||
etikettPresets.forEach((row) => {
|
||||
const option = document.createElement("option");
|
||||
option.value = row.id;
|
||||
option.textContent = row.label;
|
||||
option.dataset.w = String(row.width_mm);
|
||||
option.dataset.h = String(row.height_mm);
|
||||
preset.appendChild(option);
|
||||
});
|
||||
const hit = etikettPresets.find(
|
||||
(row) => Number(row.width_mm) === width && Number(row.height_mm) === height
|
||||
);
|
||||
preset.value = hit ? hit.id : (current && [...preset.options].some((o) => o.value === current) ? current : "");
|
||||
}
|
||||
|
||||
function syncEtikettPresetFromFields() {
|
||||
const preset = document.getElementById("etikett-preset");
|
||||
const width = Number(document.getElementById("etikett-w").value);
|
||||
const height = Number(document.getElementById("etikett-h").value);
|
||||
const hit = etikettPresets.find(
|
||||
(row) => Number(row.width_mm) === width && Number(row.height_mm) === height
|
||||
);
|
||||
if (preset) preset.value = hit ? hit.id : "";
|
||||
}
|
||||
|
||||
document.getElementById("etikett-preset")?.addEventListener("change", () => {
|
||||
const preset = document.getElementById("etikett-preset");
|
||||
const option = preset.selectedOptions[0];
|
||||
if (!option || !option.value) return;
|
||||
document.getElementById("etikett-w").value = option.dataset.w || "70";
|
||||
document.getElementById("etikett-h").value = option.dataset.h || "40";
|
||||
document.getElementById("etikett-save")?.click();
|
||||
});
|
||||
|
||||
["etikett-w", "etikett-h"].forEach((id) => {
|
||||
document.getElementById(id)?.addEventListener("change", () => {
|
||||
syncEtikettPresetFromFields();
|
||||
});
|
||||
});
|
||||
|
||||
document.getElementById("etikett-save").addEventListener("click", async () => {
|
||||
try {
|
||||
const out = await api("/einstellungen", {
|
||||
method: "PATCH",
|
||||
json: {
|
||||
etikett: {
|
||||
width_mm: Number(document.getElementById("etikett-w").value) || 70,
|
||||
height_mm: Number(document.getElementById("etikett-h").value) || 40,
|
||||
},
|
||||
},
|
||||
});
|
||||
fillEtikett(out.etikett || {});
|
||||
const e = out.etikett || {};
|
||||
showNote(
|
||||
"etikett-note",
|
||||
null,
|
||||
"Gespeichert — " + (e.width_mm || 70) + " × " + (e.height_mm || 40) + " mm (QR " + (e.qr_mm || 20) + " mm)."
|
||||
);
|
||||
} catch (error) {
|
||||
showNote("etikett-note", error);
|
||||
}
|
||||
});
|
||||
|
||||
function syncAddHost() {
|
||||
syncHostField(
|
||||
document.getElementById("add-family").value,
|
||||
document.getElementById("add-host-label"),
|
||||
document.getElementById("add-host"),
|
||||
document.getElementById("add-name").value,
|
||||
);
|
||||
}
|
||||
document.getElementById("add-family")?.addEventListener("change", syncAddHost);
|
||||
document.getElementById("add-name")?.addEventListener("input", syncAddHost);
|
||||
document.getElementById("pe-family")?.addEventListener("change", () => {
|
||||
syncHostField(
|
||||
document.getElementById("pe-family").value,
|
||||
document.getElementById("pe-host-label"),
|
||||
document.getElementById("pe-host"),
|
||||
document.getElementById("pe-name").value,
|
||||
);
|
||||
});
|
||||
document.getElementById("pe-name")?.addEventListener("input", () => {
|
||||
syncHostField(
|
||||
document.getElementById("pe-family").value,
|
||||
document.getElementById("pe-host-label"),
|
||||
document.getElementById("pe-host"),
|
||||
document.getElementById("pe-name").value,
|
||||
);
|
||||
});
|
||||
|
||||
document.getElementById("printer-add").addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
try {
|
||||
@@ -976,6 +1192,9 @@ document.getElementById("auto-retry")?.addEventListener("change", wireAutosave("
|
||||
document.getElementById(id)?.addEventListener("change", wireAutosave("watch-save", "watch-note"));
|
||||
});
|
||||
document.getElementById("fqdn")?.addEventListener("change", wireAutosave("fqdn-save", "fqdn-note"));
|
||||
["etikett-w", "etikett-h"].forEach((id) => {
|
||||
document.getElementById(id)?.addEventListener("change", wireAutosave("etikett-save", "etikett-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"));
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user