diff --git a/README.md b/README.md index caba770..214745d 100644 --- a/README.md +++ b/README.md @@ -29,8 +29,14 @@ Die App ermöglicht: Geräten bietet das Detail-Popup stattdessen direkt eine Aktivieren-Option - Einstellbares Prüfintervall (steuert den Hintergrund-Check-Dienst) — der Countdown „Nächste Prüfung“ wird aus dem tatsächlichen letzten - Log-Eintrag berechnet, nicht bei jedem Seitenaufruf neu gestartet + Log-Eintrag berechnet, nicht bei jedem Seitenaufruf neu gestartet, und + läuft **global in der Topbar auf jeder Seite** (nicht nur im Dashboard) - Live-Log-Ansicht mit farblicher Hervorhebung (online/offline) +- **Einheitliches Seiten-Layout**: die Topbar zeigt rechts ausschließlich + den „Nächste Prüfung“-Timer; Anlegen-Buttons und eine kurze Beschreibung + sitzen stattdessen direkt über der jeweiligen Tabelle. Die Listen + (Geräte, Switche, Zugangsdaten, Benutzer, Gruppen) sind per Klick auf die + Spaltenüberschrift sortierbar - **Rechtesystem mit Gruppen**: Admins dürfen immer alles (als Gruppe „Admin“ überall mit aufgeführt); normalen Benutzern lassen sich über Gruppen einzelne Verwaltungsrechte gezielt zuweisen, inkl. reiner Ansichtsrechte diff --git a/srv/poe_manager/app.py b/srv/poe_manager/app.py index 095d765..3d5ca48 100644 --- a/srv/poe_manager/app.py +++ b/srv/poe_manager/app.py @@ -651,6 +651,40 @@ def _format_ts(ts_str: str) -> str: return ts_str +def get_last_run_at(): + """Zeitpunkt des letzten Prüf-Durchlaufs (jüngste Zeile im aktuellsten + Logfile) — Basis für den globalen 'Nächste Prüfung'-Countdown in der + Topbar, unabhängig von einer bestimmten Geräteliste.""" + latest_log = _latest_log_file() + if not latest_log: + return None + latest_ts_str = None + with open(latest_log, "r") as f: + for line in f: + m = _LOG_LINE_RE.match(line.strip()) + if m and (latest_ts_str is None or m.group(1) > latest_ts_str): + latest_ts_str = m.group(1) + if not latest_ts_str: + return None + try: + return datetime.strptime(latest_ts_str, "%Y-%m-%d %H:%M:%S") + except ValueError: + return None + + +@app.context_processor +def inject_check_timer(): + """Stellt den 'Nächste Prüfung'-Countdown global für die Topbar bereit + (auf jeder Seite sichtbar, siehe base.html) — unabhängig davon, ob die + jeweilige Route selbst etwas mit Geräten/Logs zu tun hat.""" + interval = int(get_setting("interval", 5)) + last_run_at = get_last_run_at() + return { + "global_check_interval": interval, + "global_last_run_epoch_ms": int(last_run_at.timestamp() * 1000) if last_run_at else None, + } + + def get_device_status(devices): """ Ermittelt aus dem aktuellsten Logfile in einem Durchlauf je Gerät: diff --git a/srv/poe_manager/static/css/style.css b/srv/poe_manager/static/css/style.css index 6e4b93c..64523d9 100644 --- a/srv/poe_manager/static/css/style.css +++ b/srv/poe_manager/static/css/style.css @@ -671,6 +671,22 @@ table.data-table { font-size: 13.5px; } +.data-table thead th.sortable-col { + cursor: pointer; + user-select: none; + white-space: nowrap; +} +.data-table thead th.sortable-col:hover { color: var(--text); } +.data-table thead th.sortable-col::after { + content: "⇅"; + display: inline-block; + margin-left: 5px; + font-size: 10px; + opacity: 0.35; +} +.data-table thead th.sortable-col.sort-asc::after { content: "▲"; opacity: 0.85; } +.data-table thead th.sortable-col.sort-desc::after { content: "▼"; opacity: 0.85; } + .data-table thead th { text-align: left; font-size: 11px; diff --git a/srv/poe_manager/static/js/app.js b/srv/poe_manager/static/js/app.js index f384a5f..6ba24a3 100644 --- a/srv/poe_manager/static/js/app.js +++ b/srv/poe_manager/static/js/app.js @@ -215,6 +215,81 @@ }); } + /* ---------------- Globaler "Nächste Prüfung"-Timer (Topbar, jede Seite) ---------------- */ + + function initCheckTimer() { + 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; } + + function update() { + let remainingMs = intervalMs; + if (!isNaN(lastRunMs) && lastRunMs > 0) { + const elapsed = Date.now() - lastRunMs; + remainingMs = intervalMs - (((elapsed % intervalMs) + intervalMs) % intervalMs); + } + timerEl.textContent = Math.max(0, Math.ceil(remainingMs / 1000)); + } + update(); + setInterval(update, 1000); + } + + /* ---------------- Sortierbare Tabellen ---------------- */ + /* + * Klick auf