Grotti 3.0.0: Aufträge, Wartung, SSE, Ausschuss-Dialog, UI-Umbau.
Migrationen 003–009, Kundaufträge mit Lieferschein, Abräumen mit Urteil, ntfy/Backup/Zeitfenster, Live-Stream und Mobile-Mehr-Menü.
This commit is contained in:
1 parent
0236278929
commit
8e88efc094
45 files changed
+3121
-122
No files matched your search
@@ -126,6 +126,38 @@
|
||||
<p id="tg-note"></p>
|
||||
</section>
|
||||
|
||||
<section class="card admin-only">
|
||||
<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">
|
||||
<div class="field">
|
||||
<label for="ntfy-server">Server</label>
|
||||
<input id="ntfy-server" placeholder="https://ntfy.sh" autocomplete="off">
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="ntfy-topic">Topic</label>
|
||||
<input id="ntfy-topic" placeholder="grotti-werkstatt" autocomplete="off">
|
||||
</div>
|
||||
<div class="field" style="grid-column:1/-1">
|
||||
<label for="ntfy-token">Token (optional)</label>
|
||||
<input id="ntfy-token" type="password" autocomplete="off" placeholder="Bearer-Token">
|
||||
</div>
|
||||
</div>
|
||||
<p class="hint">Gleiche Meldungen wie Telegram — unabhängig abschaltbar.</p>
|
||||
<div class="actions"><button type="button" id="ntfy-save">Speichern</button></div>
|
||||
<p id="ntfy-note"></p>
|
||||
</section>
|
||||
|
||||
<section class="card admin-only">
|
||||
<h2>Backup</h2>
|
||||
<p class="hint">PostgreSQL-Dump und Datenordner (ohne toolpath/ und alte Backups).</p>
|
||||
<div class="actions">
|
||||
<button type="button" id="backup-now">Backup jetzt</button>
|
||||
</div>
|
||||
<ul id="backup-list" class="tiny"></ul>
|
||||
<p id="backup-note"></p>
|
||||
</section>
|
||||
|
||||
<section class="card admin-only">
|
||||
<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>
|
||||
@@ -381,6 +413,14 @@ async function load() {
|
||||
document.getElementById("al-queue").checked = alerts.queue !== false;
|
||||
document.getElementById("al-ablauf").checked = !!alerts.ablauf;
|
||||
document.getElementById("al-start").checked = !!alerts.start;
|
||||
const ntfy = settings.ntfy || {};
|
||||
document.getElementById("ntfy-enabled").checked = !!ntfy.enabled;
|
||||
document.getElementById("ntfy-server").value = ntfy.server || "https://ntfy.sh";
|
||||
document.getElementById("ntfy-topic").value = ntfy.topic || "";
|
||||
document.getElementById("ntfy-token").value = "";
|
||||
document.getElementById("ntfy-token").placeholder = ntfy.token_set
|
||||
? ("gesetzt: " + (ntfy.token || "••••"))
|
||||
: "optional";
|
||||
const en = settings.energy || {};
|
||||
document.getElementById("en-watts").value = en.watts ?? 40;
|
||||
document.getElementById("en-nozzle").value = en.nozzle_watts ?? 60;
|
||||
@@ -394,7 +434,7 @@ async function load() {
|
||||
}
|
||||
const legacy = document.getElementById("legacy-token-hint");
|
||||
if (legacy) legacy.hidden = !settings.legacy_token_set;
|
||||
await Promise.all([loadUsers(), loadTokens()]);
|
||||
await Promise.all([loadUsers(), loadTokens(), loadBackups()]);
|
||||
}
|
||||
|
||||
document.getElementById("user-add")?.addEventListener("submit", async (event) => {
|
||||
@@ -542,6 +582,49 @@ document.getElementById("tg-save").addEventListener("click", async () => {
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("ntfy-save")?.addEventListener("click", async () => {
|
||||
const token = document.getElementById("ntfy-token").value.trim();
|
||||
const payload = {
|
||||
ntfy: {
|
||||
enabled: document.getElementById("ntfy-enabled").checked,
|
||||
server: document.getElementById("ntfy-server").value.trim() || "https://ntfy.sh",
|
||||
topic: document.getElementById("ntfy-topic").value.trim(),
|
||||
},
|
||||
};
|
||||
if (token) payload.ntfy.token = token;
|
||||
try {
|
||||
await api("/einstellungen", { method: "PATCH", json: payload });
|
||||
showNote("ntfy-note", null, "ntfy gespeichert.");
|
||||
load();
|
||||
} catch (error) {
|
||||
showNote("ntfy-note", error);
|
||||
}
|
||||
});
|
||||
|
||||
async function loadBackups() {
|
||||
const list = document.getElementById("backup-list");
|
||||
if (!list) return;
|
||||
try {
|
||||
const out = await api("/backup");
|
||||
list.innerHTML = (out.items || []).slice(0, 8).map((item) => {
|
||||
const mb = ((item.size || 0) / (1024 * 1024)).toLocaleString("de-DE", { maximumFractionDigits: 1 });
|
||||
return `<li><a href="/backup/${encodeURIComponent(item.name)}" download>${escapeHtml(item.name)}</a> · ${mb} MB</li>`;
|
||||
}).join("") || "<li>Noch kein Backup.</li>";
|
||||
} catch (error) {
|
||||
list.innerHTML = "<li>Liste nicht verfügbar.</li>";
|
||||
}
|
||||
}
|
||||
|
||||
document.getElementById("backup-now")?.addEventListener("click", async () => {
|
||||
try {
|
||||
const out = await api("/backup", { method: "POST", json: {} });
|
||||
showNote("backup-note", null, "Backup erstellt: " + (out.name || ""));
|
||||
loadBackups();
|
||||
} catch (error) {
|
||||
showNote("backup-note", error);
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById("tg-test").addEventListener("click", async () => {
|
||||
try {
|
||||
await api("/telegram/test", { method: "POST", json: {} });
|
||||
|
||||
Reference in new issue
Block a user