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 sortiert die Tabelle. Zwei Modi: + * - Normale Tabellen: sortiert -Zeilen innerhalb des einzigen + * anhand von data-sort- auf der jeweiligen . + * - Akkordeon-Tabellen (mehrere , z.B. Gruppen mit Detail-Zeile): + * sortiert ganze -Blöcke anhand von data-sort- auf dem + * jeweiligen , damit Haupt- und Detail-Zeile zusammenbleiben. + * (z.B. die virtuelle "Admin"-Zeile) bleibt + * dabei immer an ihrer Position. + */ + function compareSortValues(a, b, asc) { + a = (a === null || a === undefined) ? "" : String(a); + b = (b === null || b === undefined) ? "" : String(b); + const na = parseFloat(a), nb = parseFloat(b); + const bothNumeric = a !== "" && b !== "" && !isNaN(na) && !isNaN(nb); + const cmp = bothNumeric ? (na - nb) : a.toLowerCase().localeCompare(b.toLowerCase(), "de"); + return asc ? cmp : -cmp; + } + + function sortTable(table, th) { + const key = th.dataset.sortKey; + const asc = th.dataset.sortDir !== "asc"; + table.querySelectorAll("thead th[data-sort-key]").forEach((h) => { + delete h.dataset.sortDir; + h.classList.remove("sort-asc", "sort-desc"); + }); + th.dataset.sortDir = asc ? "asc" : "desc"; + th.classList.add(asc ? "sort-asc" : "sort-desc"); + + if (table.tBodies.length > 1) { + const movable = Array.prototype.filter.call(table.tBodies, (tb) => !tb.hasAttribute("data-sort-pinned")); + movable.sort((a, b) => compareSortValues( + a.getAttribute("data-sort-" + key), b.getAttribute("data-sort-" + key), asc + )); + movable.forEach((tb) => table.appendChild(tb)); + } else { + const tbody = table.tBodies[0]; + const rows = Array.prototype.filter.call(tbody.rows, (r) => !r.classList.contains("empty-row")); + rows.sort((a, b) => compareSortValues( + a.getAttribute("data-sort-" + key), b.getAttribute("data-sort-" + key), asc + )); + rows.forEach((r) => tbody.appendChild(r)); + } + } + + function initSortableTables() { + document.querySelectorAll("table[data-sortable] thead th[data-sort-key]").forEach((th) => { + th.classList.add("sortable-col"); + th.addEventListener("click", () => sortTable(th.closest("table"), th)); + }); + } + /* ---------------- Init ---------------- */ document.addEventListener("DOMContentLoaded", function () { @@ -224,6 +299,8 @@ initConfirmables(); initFlashedMessages(); initNavGroups(); + initCheckTimer(); + initSortableTables(); document.querySelectorAll("[data-theme-toggle]").forEach((btn) => btn.addEventListener("click", toggleTheme)); }); diff --git a/srv/poe_manager/templates/base.html b/srv/poe_manager/templates/base.html index 121f0c6..b58da40 100644 --- a/srv/poe_manager/templates/base.html +++ b/srv/poe_manager/templates/base.html @@ -112,7 +112,11 @@
- {% block topbar_right %}{% endblock %} + + Nächste Prüfung in --s + {% if not current_user.is_authenticated %} diff --git a/srv/poe_manager/templates/credentials.html b/srv/poe_manager/templates/credentials.html index 85c2dc9..053fc30 100644 --- a/srv/poe_manager/templates/credentials.html +++ b/srv/poe_manager/templates/credentials.html @@ -5,29 +5,37 @@ {% set can_delete = current_user.has_permission('switches.delete') %} {% block page_title %}Zugangsdaten{% endblock %} {% block page_sub %}
{{ credentials|length }} Zugangsdaten
{% endblock %} -{% block topbar_right %} -{% if can_create %} - -{% endif %} -{% endblock %} {% block content %} -

- Zugangsdaten können mehreren Switchen gleichzeitig zugeordnet werden. Beim Anlegen eines - Switches lassen sich bestehende Zugangsdaten auswählen oder direkt neue hinterlegen. -

+
+
+

Zugangsdaten

+
+ Zugangsdaten können mehreren Switchen gleichzeitig zugeordnet werden. Beim Anlegen eines + Switches lassen sich bestehende Zugangsdaten auswählen oder direkt neue hinterlegen. +
+
+ {% if can_create %} + + {% endif %} +
- - +
NameUsernameVerwendet vonAktionen
+ + + + + + {% for c in credentials %} - + diff --git a/srv/poe_manager/templates/devices.html b/srv/poe_manager/templates/devices.html index 14a3c77..411c240 100644 --- a/srv/poe_manager/templates/devices.html +++ b/srv/poe_manager/templates/devices.html @@ -8,17 +8,22 @@ {% set col_count = 5 + (1 if can_toggle else 0) + (1 if show_actions_col else 0) %} {% block page_title %}Clients{% endblock %} {% block page_sub %}
{{ devices|length }} Geräte
{% endblock %} -{% block topbar_right %} -{% if can_create %} - -{% endif %} -{% endblock %} {% block content %} +
+
+

Geräte

+
PoE-Geräte mit IP, MAC-Adresse und zugeordnetem Switch-Port für die automatische Erreichbarkeitsprüfung.
+
+ {% if can_create %} + + {% endif %} +
+
@@ -27,21 +32,21 @@
-
NameUsernameVerwendet vonAktionen
{{ c['name'] }} {{ c['username'] }} {{ c['switch_count'] }} Switch{{ 'e' if c['switch_count'] != 1 else '' }}
+
- - - - - + + + + + {% if can_toggle %}{% endif %} {% if show_actions_col %}{% endif %} {% for d in devices %} - + diff --git a/srv/poe_manager/templates/groups.html b/srv/poe_manager/templates/groups.html index 9498490..aa72f24 100644 --- a/srv/poe_manager/templates/groups.html +++ b/srv/poe_manager/templates/groups.html @@ -2,28 +2,37 @@ {% set active_page = "groups" %} {% block page_title %}Gruppen{% endblock %} {% block page_sub %}
{{ groups|length + 1 }} Gruppen · Rechteverwaltung
{% endblock %} -{% block topbar_right %} - -{% endblock %} {% block content %} -

- Über Gruppen lassen sich einzelne Verwaltungsrechte für Devices und Switches gezielt vergeben. - Ein Benutzer kann mehreren Gruppen angehören — die Rechte addieren sich. Auf „Rechte“ klicken, - um eine Gruppe aufzuklappen und die Berechtigungen im Detail zu sehen bzw. zu ändern. -

+
+
+

Gruppen

+
+ Über Gruppen lassen sich einzelne Verwaltungsrechte für Devices und Switches gezielt vergeben. + Ein Benutzer kann mehreren Gruppen angehören — die Rechte addieren sich. Auf „Rechte“ klicken, + um eine Gruppe aufzuklappen und die Berechtigungen im Detail zu sehen bzw. zu ändern. +
+
+ +
-
HostnameIP-AdresseMAC-AdresseSwitchPortHostnameIP-AdresseMAC-AdresseSwitchPortStatusAktionen
{{ d['name'] }} {{ d['rpi_ip'] }} {{ d['mac'] }}
- - - +
GruppeMitgliederAktionen
+ + + + + + + + @@ -58,8 +67,12 @@

Admins dürfen immer alles — diese Rechte sind fest und nicht änderbar.

+ - {% for g in groups %} + {% for g in groups %} + + - {% else %} - - {% endfor %} + {% else %} + + + + {% endfor %}
GruppeMitgliederAktionen
Admin Systemrolle {{ admin_virtual_group.member_names|length }}
{{ g.name }} @@ -116,10 +129,12 @@
Noch keine weiteren Gruppen angelegt.
Noch keine weiteren Gruppen angelegt.
diff --git a/srv/poe_manager/templates/index.html b/srv/poe_manager/templates/index.html index 090a745..70c9dbd 100644 --- a/srv/poe_manager/templates/index.html +++ b/srv/poe_manager/templates/index.html @@ -2,15 +2,13 @@ {% set active_page = "index" %} {% block page_title %}Dashboard{% endblock %} {% block page_sub %}
{{ device_count }} Geräte{{ " im Bestand" if current_user.is_authenticated else "" }}
{% endblock %} -{% block topbar_right %} -
+ +{% block content %} + +
-Nächste Prüfung in --s -{% endblock %} - -{% block content %}
@@ -169,28 +167,27 @@ {% block scripts %} {% endblock %} diff --git a/srv/poe_manager/templates/switches.html b/srv/poe_manager/templates/switches.html index f51ba93..39bf753 100644 --- a/srv/poe_manager/templates/switches.html +++ b/srv/poe_manager/templates/switches.html @@ -8,17 +8,22 @@ {% set can_delete = current_user.has_permission('switches.delete') %} {% block page_title %}Switche{% endblock %} {% block page_sub %}
{{ switches|length }} Switche
{% endblock %} -{% block topbar_right %} -{% if can_create %} - -{% endif %} -{% endblock %} {% block content %} +
+
+

Switche

+
Aruba-Switche mit ihren Zugangsdaten, über die der automatische PoE-Neustart bei Ausfällen sowie der Verbindungstest laufen.
+
+ {% if can_create %} + + {% endif %} +
+
@@ -27,13 +32,18 @@
- +
- + + + + + + {% for s in switches %} - +
HostnameIP-AdresseZugangsdatenAktionen
HostnameIP-AdresseZugangsdatenAktionen
{{ s['hostname'] }} {{ s['ip'] }} diff --git a/srv/poe_manager/templates/users.html b/srv/poe_manager/templates/users.html index 0aaa871..486aad0 100644 --- a/srv/poe_manager/templates/users.html +++ b/srv/poe_manager/templates/users.html @@ -2,26 +2,36 @@ {% set active_page = "users" %} {% block page_title %}Benutzer{% endblock %} {% block page_sub %}
{{ users|length }} Benutzer
{% endblock %} -{% block topbar_right %} - -{% endblock %} {% block content %} +
+
+

Benutzer

+
Benutzerkonten mit Zugriff auf den PoE Manager. Die Gruppe bestimmt, welche Verwaltungsrechte ein Benutzer hat.
+
+ +
+
- - +
UsernameNameGruppeAktionen
+ + + + + + {% for u in users %} - + {% set full_name = [u['first_name'], u['last_name']]|select|join(' ') %} + {% set group_label = 'Admin' if u['is_admin'] else (u['group_names'] or '') %} + - +
UsernameNameGruppeAktionen
{{ u['username'] }} - {% if u['first_name'] or u['last_name'] %}{{ [u['first_name'], u['last_name']]|select|join(' ') }}{% else %}—{% endif %} - {{ full_name or '—' }} {% if u['is_admin'] %} Admin