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
+62 -19
View File
@@ -740,16 +740,10 @@ def get_device_status(devices):
# Dashboard
# ---------------------------------------------------------------------------
@app.route("/")
def index():
"""
Dashboard als Kachel-Ansicht, in drei Abschnitten: Offline, Online,
Deaktiviert (jeweils alphabetisch) — jeweils nur gerendert, wenn nicht
leer. Ohne Login: keine deaktivierten Geräte, nur Online/Offline/Gesamt-
Statistik, keine Interaktion. Eingeloggt: alle Geräte, volle
Statistik-Kacheln, Klick-Details, manueller PoE-Neustart bzw. bei
deaktivierten Geräten eine Aktivieren-Option.
"""
def _dashboard_context():
"""Gemeinsam von index() und dem AJAX-Partial /dashboard/tiles genutzt,
damit das Live-Update des Dashboards (ohne vollen Seiten-Reload) exakt
dieselben Daten liefert wie der initiale Seitenaufruf."""
conn = get_db_connection()
all_devices = conn.execute(
"SELECT mac, name, rpi_ip, switch_hostname, port, is_active FROM devices ORDER BY name ASC"
@@ -781,8 +775,7 @@ def index():
last_run_epoch_ms = int(last_run_at.timestamp() * 1000) if last_run_at else None
return render_template(
"index.html",
return dict(
offline_devices=offline_devices, online_devices=online_devices, disabled_devices=disabled_devices,
device_count=len(visible_devices),
status=status, last_seen=last_seen,
@@ -791,6 +784,54 @@ def index():
)
@app.route("/")
def index():
"""
Dashboard als Kachel-Ansicht, in drei Abschnitten: Offline, Online,
Deaktiviert (jeweils alphabetisch) — jeweils nur gerendert, wenn nicht
leer. Ohne Login: keine deaktivierten Geräte, nur Online/Offline/Gesamt-
Statistik, keine Interaktion. Eingeloggt: alle Geräte, volle
Statistik-Kacheln, Klick-Details, manueller PoE-Neustart bzw. bei
deaktivierten Geräten eine Aktivieren-Option.
"""
return render_template("index.html", **_dashboard_context())
@app.route("/dashboard/tiles")
def dashboard_tiles():
"""AJAX-Partial für das Live-Update der Dashboard-Kacheln (Status,
Statistik) ohne vollen Seiten-Reload — liefert dasselbe Fragment, das
index.html beim initialen Aufruf einbindet (siehe _dashboard_tiles.html)."""
return render_template("_dashboard_tiles.html", **_dashboard_context())
def _restart_check_service():
"""Startet rpi-check.service (poe_wrapper.py -> poe.sh) neu, damit ein
geändertes Prüfintervall sofort greift (poe.sh liest SLEEP nur einmal
beim Start aus der DB) — läuft dabei automatisch auch ein sofortiger
Prüf-Durchlauf mit frischem Logfile an. Schlägt in Umgebungen ohne
systemd (z.B. Container) lautlos fehl, statt die Anfrage abzubrechen."""
try:
subprocess.run(["systemctl", "restart", "rpi-check.service"], check=False)
except FileNotFoundError:
pass # z.B. lokale Entwicklungsumgebung ohne systemd
@app.route("/check/run-now", methods=["POST"])
@login_required
def run_check_now():
"""Manueller 'Jetzt prüfen'-Trigger (Zahnrad-Icon neben dem Prüf-Timer):
startet denselben Service neu wie eine Intervalländerung — das erzeugt
sofort ein frisches Logfile mit einem neuen Durchlauf, der Countdown
'Nächste Prüfung' setzt sich dadurch automatisch auf den manuell
ausgelösten Zeitpunkt zurück."""
if not current_user.is_admin:
return {"success": False, "msg": "Keine Berechtigung."}, 403
_restart_check_service()
log_action("check.run_now", "Prüfintervall")
return {"success": True, "msg": "Prüfung gestartet."}
# ---------------------------------------------------------------------------
# Settings
# ---------------------------------------------------------------------------
@@ -809,11 +850,7 @@ def settings():
set_setting("interval", new_interval)
set_setting("check_interval", new_interval * 60)
log_action("settings.update", "Prüfintervall", f"{new_interval} Minuten")
try:
subprocess.run(["systemctl", "restart", "rpi-check.service"], check=False)
except FileNotFoundError:
pass # z.B. lokale Entwicklungsumgebung ohne systemd
_restart_check_service()
flash(f"Intervall auf {new_interval} Minuten gesetzt und Service neu gestartet!", "success")
return redirect(url_for("settings"))
@@ -1594,9 +1631,15 @@ def get_log():
return "Keine Logfiles gefunden."
try:
with open(latest_log, "r") as f:
return f.read()
content = f.read()
except Exception as e:
return f"Fehler beim Lesen des Logs: {e}"
content = f"Fehler beim Lesen des Logs: {e}"
# Dateiname als Header mitschicken, damit die Anzeige auch nach einem
# Service-Neustart (neues Logfile, z.B. durch "Jetzt prüfen") ohne
# Seiten-Reload aktuell bleibt.
response = app.response_class(content, mimetype="text/plain")
response.headers["X-Log-Name"] = os.path.basename(latest_log)
return response
@app.route("/logs")