Grotti 3.1.0: Bedarf-UI, Zeitfenster, HT/NT, Theme, Chrome-Fixes.
Bedarf in Sidebar/Material, Drucker-Zeitfenster und Tarif in Einstellungen, helles Theme; Legacy-Token und /api/drucker entfernt; SSE/SW-Fixes gegen hängende Navigation in Chrome.
This commit is contained in:
1 parent
8e88efc094
commit
6e59df22e5
23 files changed
+741
-171
No files matched your search
+111
-16
@@ -9,11 +9,27 @@
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="{{ url_for('static', filename='brand/favicon-32.png') }}">
|
||||
<link rel="icon" type="image/png" sizes="512x512" href="{{ url_for('static', filename='brand/favicon.png') }}">
|
||||
<link rel="apple-touch-icon" href="{{ url_for('static', filename='brand/apple-touch-icon.png') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='grotti.css') }}?v=54">
|
||||
<link rel="stylesheet" href="{{ url_for('static', filename='grotti.css') }}?v=56">
|
||||
<link rel="manifest" href="{{ url_for('static', filename='manifest.webmanifest') }}">
|
||||
<meta name="theme-color" content="#0a0a0c">
|
||||
<meta name="theme-color" content="#0a0a0c" id="theme-color-meta">
|
||||
<script>
|
||||
(function () {
|
||||
function applyThemeEarly(mode) {
|
||||
var light = mode === "light";
|
||||
if (mode === "system") {
|
||||
try { light = window.matchMedia("(prefers-color-scheme: light)").matches; } catch (e) { light = false; }
|
||||
}
|
||||
document.documentElement.dataset.theme = light ? "light" : "dark";
|
||||
document.documentElement.style.colorScheme = light ? "light" : "dark";
|
||||
var meta = document.getElementById("theme-color-meta");
|
||||
if (meta) meta.content = light ? "#f4f4f6" : "#0a0a0c";
|
||||
}
|
||||
window.applyTheme = function (mode) {
|
||||
try { if (mode) localStorage.setItem("grotti-theme", mode); } catch (e) {}
|
||||
var m = mode;
|
||||
try { m = m || localStorage.getItem("grotti-theme") || "dark"; } catch (e) { m = "dark"; }
|
||||
applyThemeEarly(m);
|
||||
};
|
||||
try {
|
||||
if (!localStorage.getItem("grotti-display") && localStorage.getItem("farm-display")) {
|
||||
localStorage.setItem("grotti-display", localStorage.getItem("farm-display"));
|
||||
@@ -35,6 +51,14 @@
|
||||
if (nav === "1" || (nav == null && localStorage.getItem("grotti-display") === "shelf")) {
|
||||
document.documentElement.classList.add("nav-collapsed");
|
||||
}
|
||||
applyThemeEarly(localStorage.getItem("grotti-theme") || "dark");
|
||||
} catch (e) {}
|
||||
try {
|
||||
window.matchMedia("(prefers-color-scheme: light)").addEventListener("change", function () {
|
||||
var mode = "dark";
|
||||
try { mode = localStorage.getItem("grotti-theme") || "dark"; } catch (err) {}
|
||||
if (mode === "system") applyThemeEarly("system");
|
||||
});
|
||||
} catch (e) {}
|
||||
})();
|
||||
</script>
|
||||
@@ -132,6 +156,11 @@
|
||||
<ul class="queue-list" id="alert-list"></ul>
|
||||
<div class="queue-foot"><button type="button" class="ghost tiny-btn" id="events-clear">gelesen</button> <a href="/aktivitaet">alle →</a></div>
|
||||
</details>
|
||||
<details class="queue-box" data-box="bedarf" id="bedarf-box" open hidden>
|
||||
<summary class="queue-title"><svg class="ico caret"><use href="#i-down"/></svg><span class="grow">Bedarf</span><span class="count bad" id="bedarf-count">0</span></summary>
|
||||
<ul class="queue-list" id="bedarf-list"></ul>
|
||||
<div class="queue-foot"><a href="/material#bedarf">Material →</a></div>
|
||||
</details>
|
||||
</div>
|
||||
<nav class="nav-secondary" aria-label="Weitere Seiten">
|
||||
<a href="/einstellungen" class="{{ 'on' if page == 'einstellungen' else '' }}" title="Einstellungen"><svg class="ico"><use href="#i-settings"/></svg><span>Einstellungen</span></a>
|
||||
@@ -171,10 +200,10 @@
|
||||
<a href="/einstellungen">Einstellungen</a>
|
||||
<a href="/doku">Doku</a>
|
||||
</nav>
|
||||
<form method="dialog"><button class="ghost">Schließen</button></form>
|
||||
<form method="dialog"><button class="ghost" value="cancel">Schließen</button></form>
|
||||
</dialog>
|
||||
<script src="{{ url_for('static', filename='api.js') }}?v=28"></script>
|
||||
<script src="{{ url_for('static', filename='ui.js') }}?v=2"></script>
|
||||
<script src="{{ url_for('static', filename='api.js') }}?v=29"></script>
|
||||
<script src="{{ url_for('static', filename='ui.js') }}?v=3"></script>
|
||||
<script>
|
||||
document.getElementById("logout-btn")?.addEventListener("click", async () => {
|
||||
try { await api("/auth/logout", { method: "POST", json: {} }); } catch (e) {}
|
||||
@@ -477,6 +506,25 @@ async function refreshEvents() {
|
||||
</li>`).join("");
|
||||
}
|
||||
|
||||
async function refreshBedarf() {
|
||||
let data;
|
||||
try { data = await api("/bedarf"); } catch (error) { return; }
|
||||
const shortfall = data.shortfall || [];
|
||||
const box = document.getElementById("bedarf-box");
|
||||
if (!box) return;
|
||||
box.hidden = !shortfall.length;
|
||||
document.getElementById("bedarf-count").textContent = shortfall.length;
|
||||
document.getElementById("bedarf-list").innerHTML = shortfall.slice(0, 5).map((item) => {
|
||||
const label = [item.material_type, item.color].filter(Boolean).join(" · ") || "Filament";
|
||||
return `<li class="queue-item">
|
||||
<div class="queue-row"><span class="grow"><span class="queue-name">${escapeHtml(label)}</span></span>
|
||||
<span class="queue-sub">${grams(item.shortfall_g)} fehlen</span></div>
|
||||
<span class="queue-msg">${grams(item.weight_g)} Queue · ${grams(item.remaining_g)} Rest</span>
|
||||
</li>`;
|
||||
}).join("");
|
||||
if (typeof window.onBedarfChanged === "function") window.onBedarfChanged(data);
|
||||
}
|
||||
|
||||
async function refreshTasks() {
|
||||
let items;
|
||||
try { items = await api("/aufgaben"); } catch (error) { return; }
|
||||
@@ -508,11 +556,29 @@ async function refreshTasks() {
|
||||
};
|
||||
});
|
||||
}
|
||||
document.getElementById("mobile-more")?.addEventListener("click", () => {
|
||||
document.getElementById("mobile-more-sheet")?.showModal();
|
||||
});
|
||||
(function mobileMoreSheet() {
|
||||
const sheet = document.getElementById("mobile-more-sheet");
|
||||
const btn = document.getElementById("mobile-more");
|
||||
if (!sheet || !btn) return;
|
||||
btn.addEventListener("click", () => {
|
||||
if (typeof sheet.showModal === "function") sheet.showModal();
|
||||
});
|
||||
// Modal schließt und blockiert sonst die ganze App (Chrome).
|
||||
sheet.addEventListener("click", (event) => {
|
||||
const link = event.target.closest("a[href]");
|
||||
if (!link) return;
|
||||
event.preventDefault();
|
||||
const href = link.getAttribute("href");
|
||||
sheet.close();
|
||||
if (href) location.href = href;
|
||||
});
|
||||
sheet.addEventListener("close", () => {
|
||||
btn.setAttribute("aria-expanded", "false");
|
||||
});
|
||||
btn.addEventListener("click", () => btn.setAttribute("aria-expanded", "true"));
|
||||
})();
|
||||
if ("serviceWorker" in navigator) {
|
||||
navigator.serviceWorker.register("/static/sw.js").catch(() => {});
|
||||
navigator.serviceWorker.register("/static/sw.js", { updateViaCache: "none" }).catch(() => {});
|
||||
}
|
||||
|
||||
document.getElementById("events-clear").addEventListener("click", async (event) => {
|
||||
@@ -524,15 +590,21 @@ document.getElementById("events-clear").addEventListener("click", async (event)
|
||||
|
||||
window.refreshPrintQueue = refreshPrintQueue;
|
||||
window.refreshSendQueue = refreshSendQueue;
|
||||
window.refreshBedarf = refreshBedarf;
|
||||
refreshSendQueue();
|
||||
refreshPrintQueue();
|
||||
refreshEvents();
|
||||
refreshTasks();
|
||||
refreshBedarf();
|
||||
window.refreshTasks = refreshTasks;
|
||||
|
||||
(function liveUpdates() {
|
||||
let streamLive = false;
|
||||
const sseKinds = {
|
||||
queue: () => refreshPrintQueue(),
|
||||
queue: () => {
|
||||
refreshPrintQueue();
|
||||
refreshBedarf();
|
||||
},
|
||||
senden: () => refreshSendQueue(),
|
||||
ereignis: () => refreshEvents(),
|
||||
aufgaben: () => refreshTasks(),
|
||||
@@ -543,23 +615,37 @@ window.refreshTasks = refreshTasks;
|
||||
},
|
||||
};
|
||||
|
||||
function whenStreamDown(fn) {
|
||||
return async () => {
|
||||
if (streamLive) return;
|
||||
await fn();
|
||||
};
|
||||
}
|
||||
|
||||
if (typeof EventSource !== "undefined") {
|
||||
let es;
|
||||
let retryMs = 2000;
|
||||
const retryMax = 60000;
|
||||
let hadStream = false;
|
||||
let retryTimer = 0;
|
||||
|
||||
function connectStream() {
|
||||
if (es) {
|
||||
try { es.close(); } catch (e) {}
|
||||
}
|
||||
es = new EventSource("/api/v1/stream", { withCredentials: true });
|
||||
es.addEventListener("open", () => {
|
||||
streamLive = true;
|
||||
retryMs = 2000;
|
||||
if (hadStream) setConnectionBanner(true, "Live-Verbindung wieder da.");
|
||||
hadStream = true;
|
||||
});
|
||||
es.addEventListener("error", () => {
|
||||
es.close();
|
||||
streamLive = false;
|
||||
try { es.close(); } catch (e) {}
|
||||
setConnectionBanner(false, "Live-Verbindung unterbrochen — Fallback-Polling läuft.");
|
||||
setTimeout(connectStream, retryMs);
|
||||
clearTimeout(retryTimer);
|
||||
retryTimer = setTimeout(connectStream, retryMs);
|
||||
retryMs = Math.min(retryMax, retryMs * 2);
|
||||
});
|
||||
Object.keys(sseKinds).forEach((kind) => {
|
||||
@@ -568,12 +654,21 @@ window.refreshTasks = refreshTasks;
|
||||
es.addEventListener("ping", () => {});
|
||||
}
|
||||
connectStream();
|
||||
window.addEventListener("pagehide", () => {
|
||||
streamLive = false;
|
||||
clearTimeout(retryTimer);
|
||||
if (es) {
|
||||
try { es.close(); } catch (e) {}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
createPoller(refreshPrintQueue, 8000).start();
|
||||
createPoller(refreshSendQueue, 4000).start();
|
||||
createPoller(refreshEvents, 12000).start();
|
||||
createPoller(refreshTasks, 12000).start();
|
||||
// Poller nur als Fallback, wenn SSE tot ist — sonst staut Chrome die Verbindungen.
|
||||
createPoller(whenStreamDown(refreshPrintQueue), 8000).start();
|
||||
createPoller(whenStreamDown(refreshSendQueue), 4000).start();
|
||||
createPoller(whenStreamDown(refreshEvents), 12000).start();
|
||||
createPoller(whenStreamDown(refreshTasks), 12000).start();
|
||||
createPoller(whenStreamDown(refreshBedarf), 30000).start();
|
||||
})();
|
||||
</script>
|
||||
{% block script %}{% endblock %}
|
||||
|
||||
Reference in new issue
Block a user