UI-Fixes (Button-Wrap, Suchfeld-Icon, Log-Farben, Nav-Persistenz), manueller Check-Trigger, echter Live-Refresh

- section-head: Button ("+ Neue Zugangsdaten" etc.) rutscht bei langer
  Beschreibung nicht mehr in die nächste Zeile — bleibt immer rechts oben,
  der Beschreibungstext wickelt stattdessen innerhalb seiner eigenen Spalte.
- Suchfeld-Icon überlappte den Platzhaltertext: eine spätere, generische
  Input-Regel mit gleicher Spezifität hat die padding-left-Regel des
  Lupe-Icons überschrieben (reine Reihenfolge-im-Stylesheet-Sache) — Selektor
  spezifischer gemacht.
- Live-Log: Neustart-bezogene Zeilen (manueller Neustart, automatischer
  PoE-Restart bei Ausfall) werden jetzt orange markiert statt farblos.
- Aufgeklappte Nav-Gruppen bleiben über Seitenwechsel hinweg erhalten
  (localStorage), zusätzlich zur automatischen Aufklappung der Gruppe der
  aktuell aktiven Seite.
- Datei-Auswahl (Import) im Dark Mode gestylt (Text + Button), inkl.
  color-scheme-bewusster Browser-Widgets.
- Manueller "Jetzt prüfen"-Trigger (Icon neben dem globalen Timer, nur
  Admins): startet rpi-check.service neu -> frisches Logfile + sofortiger
  Durchlauf, Countdown synchronisiert sich auf den manuellen Zeitpunkt.
- Dashboard und Live-Log aktualisieren sich jetzt per AJAX im Hintergrund
  (kein voller Seiten-Reload mehr): neue Route /dashboard/tiles liefert das
  Kachel-Fragment (_dashboard_tiles.html, von index.html eingebunden und vom
  Live-Update nachgeladen), get_log() liefert den aktuellen Dateinamen als
  Header mit, damit die Anzeige nach einem Service-Neustart aktuell bleibt.
- Root-Cause für "Intervall bleibt immer bei 300s": in der WSL-Testumgebung
  lief poe_wrapper.py nur als manuell gestarteter Hintergrundprozess statt
  als echter rpi-check.service — "systemctl restart" lief dadurch ins Leere.
  Echten systemd-Service in WSL eingerichtet und Intervalländerung end-to-end
  verifiziert (Log-Zeilenabstand folgt jetzt dem neu gesetzten Intervall).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-10 15:53:15 +02:00
co-authored by Claude Sonnet 5
parent 0fcb6c94cb
commit c4f682b5de
9 changed files with 438 additions and 198 deletions
+75 -5
View File
@@ -206,11 +206,39 @@
} catch (e) { /* noop */ }
}
/* Nav-Gruppen mit Untermenü (z.B. "Logs") auf-/zuklappen */
/* Nav-Gruppen mit Untermenü (z.B. "Logs") auf-/zuklappen — der manuell
aufgeklappte Zustand bleibt über Seitenwechsel hinweg erhalten
(localStorage), zusätzlich zur automatischen Aufklappung der Gruppe
der aktuell aktiven Seite (server-seitig via "expanded"-Klasse). */
const NAV_EXPANDED_KEY = "poe-nav-expanded";
function getExpandedNavGroups() {
try {
return new Set(JSON.parse(localStorage.getItem(NAV_EXPANDED_KEY) || "[]"));
} catch (e) {
return new Set();
}
}
function saveExpandedNavGroups(set) {
localStorage.setItem(NAV_EXPANDED_KEY, JSON.stringify(Array.from(set)));
}
function initNavGroups() {
const expanded = getExpandedNavGroups();
document.querySelectorAll("[data-nav-group]").forEach((group) => {
const key = group.dataset.navGroupKey;
if (key && expanded.has(key)) group.classList.add("expanded");
});
document.querySelectorAll("[data-nav-group-toggle]").forEach((btn) => {
btn.addEventListener("click", () => {
btn.closest("[data-nav-group]").classList.toggle("expanded");
const group = btn.closest("[data-nav-group]");
const isExpanded = group.classList.toggle("expanded");
const key = group.dataset.navGroupKey;
if (!key) return;
const set = getExpandedNavGroups();
if (isExpanded) set.add(key); else set.delete(key);
saveExpandedNavGroups(set);
});
});
}
@@ -221,11 +249,16 @@
const pill = document.getElementById("global-timer-pill");
const timerEl = document.getElementById("global-timer");
if (!pill || !timerEl) return;
const lastRunMs = parseInt(pill.dataset.lastRunMs, 10);
const intervalMs = parseInt(pill.dataset.intervalMs, 10);
if (!intervalMs) { timerEl.textContent = "--"; return; }
// lastRunMs/intervalMs werden bei JEDEM Tick frisch aus dem DOM gelesen
// (nicht einmalig gecacht) — so kann z.B. das Dashboard nach einem
// Live-Update (siehe dashboard-tiles.js) oder ein manueller "Jetzt
// prüfen"-Klick das data-last-run-ms-Attribut aktualisieren und der
// global laufende Countdown übernimmt das automatisch.
function update() {
const lastRunMs = parseInt(pill.dataset.lastRunMs, 10);
const intervalMs = parseInt(pill.dataset.intervalMs, 10);
if (!intervalMs) { timerEl.textContent = "--"; return; }
let remainingMs = intervalMs;
if (!isNaN(lastRunMs) && lastRunMs > 0) {
const elapsed = Date.now() - lastRunMs;
@@ -237,6 +270,42 @@
setInterval(update, 1000);
}
/* "Jetzt prüfen"-Button neben dem globalen Timer (nur Admins): startet
rpi-check.service neu (frisches Logfile + sofortiger Durchlauf) und
meldet das per Custom Event, damit Dashboard/Live-Log sich selbst
aktualisieren können, ohne dass app.js die Seiteninhalte kennen muss. */
function initRunCheckNow() {
const btn = document.getElementById("run-check-now");
if (!btn) return;
btn.addEventListener("click", () => {
btn.disabled = true;
btn.classList.add("spinning");
fetch("/check/run-now", { method: "POST" })
.then((r) => r.json())
.then((data) => {
if (!data.success) {
window.showToast(data.msg || "Fehler beim Starten der Prüfung.", "danger");
btn.disabled = false;
btn.classList.remove("spinning");
return;
}
window.showToast(data.msg || "Prüfung gestartet.", "success");
// Der Service braucht einen Moment, um neu zu starten und den
// ersten Durchlauf ins frische Logfile zu schreiben.
setTimeout(() => {
document.dispatchEvent(new CustomEvent("poe:check-triggered"));
btn.disabled = false;
btn.classList.remove("spinning");
}, 1500);
})
.catch(() => {
window.showToast("Fehler beim Starten der Prüfung.", "danger");
btn.disabled = false;
btn.classList.remove("spinning");
});
});
}
/* ---------------- Sortierbare Tabellen ---------------- */
/*
* Klick auf <th data-sort-key="..."> sortiert die Tabelle. Zwei Modi:
@@ -300,6 +369,7 @@
initFlashedMessages();
initNavGroups();
initCheckTimer();
initRunCheckNow();
initSortableTables();
document.querySelectorAll("[data-theme-toggle]").forEach((btn) => btn.addEventListener("click", toggleTheme));
});