Auditlog: Nachladen aelterer Eintraege in 300er-Schritten (v1.1.9)
Die Auditlog-Seite zeigte bisher unveraenderlich nur die neuesten 500 Zeilen an -- alles Aeltere war komplett unsichtbar (auch wenn es noch nicht archiviert war, siehe v1.1.6). Jetzt: - Seitengroesse von 500 auf 300 reduziert (Erstladung UND jeder Nachlade- Schritt). - Neuer Button "Mehr laden (300)" (orange) laedt per AJAX genau den naechsten 300er-Block aelterer Eintraege nach (Cursor-Pagination ueber die id-Spalte) und haengt ihn ans bestehende tbody an, ohne die Seite neu zu laden. - Neuer Button "Alle laden" ruft denselben Nachlade-Schritt wiederholt auf, bis wirklich alles geladen ist (mit Fortschrittsanzeige im Button-Text), statt in einer Anfrage die komplette (potenziell sehr grosse) Tabelle auf einmal zu rendern. - Suche/Sortierung/Kategorie-Filter sowie die vier Statistik-Kacheln arbeiten transparent ueber ALLE aktuell geladenen Zeilen (nicht nur die urspruenglichen 300) -- Zeilenliste fuer die Filterung wird jetzt bei jedem Aufruf frisch aus dem DOM gelesen statt einmalig zwischengespeichert. - Zeilen-Rendering (Aktions-Label/-Icon/-Farbe) in ein gemeinsames Makro (_audit_log_macros.html) ausgelagert, das sowohl die Erstladung als auch das AJAX-Fragment (_audit_log_rows.html) nutzen -- verhindert Abweichungen zwischen beiden Darstellungen. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -1 +1 @@
|
|||||||
1.1.8
|
1.1.9
|
||||||
|
|||||||
+50
-3
@@ -7706,21 +7706,68 @@ def kea_log():
|
|||||||
return render_template("logs_kea.html", log_content=log_content)
|
return render_template("logs_kea.html", log_content=log_content)
|
||||||
|
|
||||||
|
|
||||||
|
AUDIT_LOG_PAGE_SIZE = 300
|
||||||
|
|
||||||
|
|
||||||
@app.route("/logs/aenderungen")
|
@app.route("/logs/aenderungen")
|
||||||
@login_required
|
@login_required
|
||||||
def activity_log():
|
def activity_log():
|
||||||
"""Auditlog: wer hat was geändert (Aktivieren/Deaktivieren, Anlegen,
|
"""Auditlog: wer hat was geändert (Aktivieren/Deaktivieren, Anlegen,
|
||||||
Bearbeiten, Löschen). Bewusst OHNE PoE-Neustarts — die stehen im Live-Log."""
|
Bearbeiten, Löschen). Bewusst OHNE PoE-Neustarts — die stehen im Live-Log.
|
||||||
|
Lädt anfangs nur die neuesten AUDIT_LOG_PAGE_SIZE Zeilen (Performance bei
|
||||||
|
einer ggf. noch nicht archivierten, großen Tabelle) -- ältere Zeilen
|
||||||
|
werden bei Bedarf über "Weitere 300 laden" bzw. "Alle laden" (siehe
|
||||||
|
activity_log_more()) nachgeladen, ohne die Seite neu zu laden."""
|
||||||
if not current_user.can_view_activity_log:
|
if not current_user.can_view_activity_log:
|
||||||
flash("Keine Berechtigung, das Auditlog einzusehen.", "danger")
|
flash("Keine Berechtigung, das Auditlog einzusehen.", "danger")
|
||||||
return redirect(url_for("index"))
|
return redirect(url_for("index"))
|
||||||
|
|
||||||
conn = get_db_connection()
|
conn = get_db_connection()
|
||||||
entries = conn.execute(
|
entries = conn.execute(
|
||||||
"SELECT id, ts, username, action, target, details FROM audit_log ORDER BY id DESC LIMIT 500"
|
"SELECT id, ts, username, action, target, details FROM audit_log ORDER BY id DESC LIMIT ?",
|
||||||
|
(AUDIT_LOG_PAGE_SIZE,),
|
||||||
|
).fetchall()
|
||||||
|
total_count = conn.execute("SELECT COUNT(*) AS n FROM audit_log").fetchone()["n"]
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
oldest_loaded_id = entries[-1]["id"] if entries else None
|
||||||
|
has_more = len(entries) >= AUDIT_LOG_PAGE_SIZE and total_count > len(entries)
|
||||||
|
return render_template(
|
||||||
|
"activity_log.html", entries=entries, total_count=total_count,
|
||||||
|
oldest_loaded_id=oldest_loaded_id, has_more=has_more,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/logs/aenderungen/more")
|
||||||
|
@login_required
|
||||||
|
def activity_log_more():
|
||||||
|
"""AJAX-Nachladen älterer Auditlog-Zeilen in AUDIT_LOG_PAGE_SIZE-Schritten
|
||||||
|
(Cursor-Pagination über die id-Spalte, absteigend) -- liefert reines
|
||||||
|
HTML-Zeilenfragment (dieselbe Zeilen-Darstellung wie die Erstladung, über
|
||||||
|
das gemeinsame Makro in _audit_log_macros.html) statt JSON, damit das
|
||||||
|
Anhängen im Frontend ein einfaches insertAdjacentHTML bleibt."""
|
||||||
|
if not current_user.can_view_activity_log:
|
||||||
|
return "Keine Berechtigung.", 403
|
||||||
|
try:
|
||||||
|
before_id = int(request.args.get("before_id", ""))
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
return "Ungültiger Parameter.", 400
|
||||||
|
|
||||||
|
conn = get_db_connection()
|
||||||
|
entries = conn.execute(
|
||||||
|
"SELECT id, ts, username, action, target, details FROM audit_log WHERE id < ? ORDER BY id DESC LIMIT ?",
|
||||||
|
(before_id, AUDIT_LOG_PAGE_SIZE),
|
||||||
).fetchall()
|
).fetchall()
|
||||||
conn.close()
|
conn.close()
|
||||||
return render_template("activity_log.html", entries=entries)
|
|
||||||
|
oldest_loaded_id = entries[-1]["id"] if entries else before_id
|
||||||
|
has_more = len(entries) >= AUDIT_LOG_PAGE_SIZE
|
||||||
|
html = render_template("_audit_log_rows.html", entries=entries)
|
||||||
|
resp = app.response_class(html, mimetype="text/html")
|
||||||
|
resp.headers["X-Row-Count"] = str(len(entries))
|
||||||
|
resp.headers["X-Has-More"] = "1" if has_more else "0"
|
||||||
|
resp.headers["X-Oldest-Id"] = str(oldest_loaded_id)
|
||||||
|
return resp
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
{#
|
||||||
|
Gemeinsame Auditlog-Zeilendarstellung -- von activity_log.html (Erstladung)
|
||||||
|
UND _audit_log_rows.html (AJAX-Nachladen über "Weitere 500 laden")
|
||||||
|
importiert, damit beide garantiert dieselbe Darstellung erzeugen und
|
||||||
|
action_labels/action_icons/category_of nicht an zwei Stellen gepflegt
|
||||||
|
werden müssen.
|
||||||
|
#}
|
||||||
|
{% set action_labels = {
|
||||||
|
"settings.update": "Einstellung geändert",
|
||||||
|
"device.create": "Gerät angelegt",
|
||||||
|
"device.edit": "Gerät bearbeitet",
|
||||||
|
"device.delete": "Gerät gelöscht",
|
||||||
|
"device.activate": "Gerät aktiviert",
|
||||||
|
"device.deactivate": "Gerät deaktiviert",
|
||||||
|
"switch.create": "Switch angelegt",
|
||||||
|
"switch.edit": "Switch bearbeitet",
|
||||||
|
"switch.delete": "Switch gelöscht",
|
||||||
|
"credential.create": "Zugangsdaten angelegt",
|
||||||
|
"credential.edit": "Zugangsdaten bearbeitet",
|
||||||
|
"credential.delete": "Zugangsdaten gelöscht",
|
||||||
|
"user.create": "Benutzer angelegt",
|
||||||
|
"user.edit": "Benutzer bearbeitet",
|
||||||
|
"user.delete": "Benutzer gelöscht",
|
||||||
|
"user.assign_group": "Gruppe zugewiesen",
|
||||||
|
"group.create": "Gruppe angelegt",
|
||||||
|
"group.edit": "Gruppe bearbeitet",
|
||||||
|
"group.delete": "Gruppe gelöscht",
|
||||||
|
"group.assign_admins": "Admin-Zuweisung geändert",
|
||||||
|
"profile.update": "Profil aktualisiert",
|
||||||
|
"profile.password": "Passwort geändert",
|
||||||
|
"check.run_now": "Prüfung manuell gestartet",
|
||||||
|
"fileshare.upload": "Datei(en) hochgeladen",
|
||||||
|
"fileshare.mkdir": "Ordner angelegt",
|
||||||
|
"fileshare.delete": "Datei/Ordner gelöscht",
|
||||||
|
"fileshare.rename": "Datei/Ordner umbenannt",
|
||||||
|
"fileshare.download": "Als ZIP heruntergeladen",
|
||||||
|
"auditlog.archive": "Auditlog archiviert",
|
||||||
|
"auditlog.export_delete": "Auditlog-Archiv exportiert",
|
||||||
|
} %}
|
||||||
|
{% set action_icons = {
|
||||||
|
"delete": '<path d="M3 6h18M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2m3 0l-1 14a2 2 0 01-2 2H7a2 2 0 01-2-2L4 6"/>',
|
||||||
|
"create": '<path d="M12 5v14M5 12h14"/>',
|
||||||
|
"edit": '<path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 013 3L7 19l-4 1 1-4z"/>',
|
||||||
|
"activate": '<path d="M20 6L9 17l-5-5"/>',
|
||||||
|
"deactivate": '<circle cx="12" cy="12" r="9"/><path d="M15 9l-6 6M9 9l6 6"/>',
|
||||||
|
} %}
|
||||||
|
{% set create_kinds = ("create", "upload", "mkdir") %}
|
||||||
|
|
||||||
|
{% macro category_of(kind) %}{% if kind in create_kinds %}create{% elif kind == "delete" %}delete{% else %}edit{% endif %}{% endmacro %}
|
||||||
|
|
||||||
|
{% macro audit_row(e) %}
|
||||||
|
{% set kind = e['action'].split('.')[-1] %}
|
||||||
|
{% set cat = category_of(kind)|trim %}
|
||||||
|
<tr data-category="{{ cat }}" data-sort-ts="{{ e['ts'] }}" data-sort-user="{{ e['username']|lower }}"
|
||||||
|
data-sort-action="{{ action_labels.get(e['action'], e['action'])|lower }}" data-sort-target="{{ (e['target'] or '')|lower }}">
|
||||||
|
<td class="text-dim mono" style="font-size:12.5px;">{{ e['ts'] }}</td>
|
||||||
|
<td class="cell-name">{{ e['username'] }}</td>
|
||||||
|
<td>
|
||||||
|
<span class="pill action-pill action-pill--{{ cat }}">
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">{{ (action_icons.get(kind) or action_icons['edit'])|safe }}</svg>
|
||||||
|
{{ action_labels.get(e['action'], e['action']) }}
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>{{ e['target'] or '—' }}</td>
|
||||||
|
<td class="text-dim">{{ e['details'] or '—' }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endmacro %}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{# Reines Zeilenfragment fürs AJAX-Nachladen (activity_log_more()) -- kein
|
||||||
|
umschließendes <table>/<tbody>, wird per insertAdjacentHTML direkt an das
|
||||||
|
bestehende tbody von #auditTable angehängt. #}
|
||||||
|
{% import "_audit_log_macros.html" as m %}
|
||||||
|
{% for e in entries %}{{ m.audit_row(e) }}{% endfor %}
|
||||||
@@ -1,56 +1,18 @@
|
|||||||
{% extends "base.html" %}
|
{% extends "base.html" %}
|
||||||
{% set active_page = "logs" %}
|
{% set active_page = "logs" %}
|
||||||
{% block page_title %}Auditlog{% endblock %}
|
{% block page_title %}Auditlog{% endblock %}
|
||||||
{% block page_sub %}<div class="topbar-sub">{{ entries|length }} Einträge (letzte 500)</div>{% endblock %}
|
{% block page_sub %}
|
||||||
|
<div class="topbar-sub" id="auditEntryCount">
|
||||||
|
{% if has_more %}{{ entries|length }} von {{ total_count }} Einträgen geladen{% else %}{{ entries|length }} Eintrag{{ 'e' if entries|length != 1 else '' }}{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
{% import "_audit_log_macros.html" as m %}
|
||||||
{% set action_labels = {
|
|
||||||
"settings.update": "Einstellung geändert",
|
|
||||||
"device.create": "Gerät angelegt",
|
|
||||||
"device.edit": "Gerät bearbeitet",
|
|
||||||
"device.delete": "Gerät gelöscht",
|
|
||||||
"device.activate": "Gerät aktiviert",
|
|
||||||
"device.deactivate": "Gerät deaktiviert",
|
|
||||||
"switch.create": "Switch angelegt",
|
|
||||||
"switch.edit": "Switch bearbeitet",
|
|
||||||
"switch.delete": "Switch gelöscht",
|
|
||||||
"credential.create": "Zugangsdaten angelegt",
|
|
||||||
"credential.edit": "Zugangsdaten bearbeitet",
|
|
||||||
"credential.delete": "Zugangsdaten gelöscht",
|
|
||||||
"user.create": "Benutzer angelegt",
|
|
||||||
"user.edit": "Benutzer bearbeitet",
|
|
||||||
"user.delete": "Benutzer gelöscht",
|
|
||||||
"user.assign_group": "Gruppe zugewiesen",
|
|
||||||
"group.create": "Gruppe angelegt",
|
|
||||||
"group.edit": "Gruppe bearbeitet",
|
|
||||||
"group.delete": "Gruppe gelöscht",
|
|
||||||
"group.assign_admins": "Admin-Zuweisung geändert",
|
|
||||||
"profile.update": "Profil aktualisiert",
|
|
||||||
"profile.password": "Passwort geändert",
|
|
||||||
"check.run_now": "Prüfung manuell gestartet",
|
|
||||||
"fileshare.upload": "Datei(en) hochgeladen",
|
|
||||||
"fileshare.mkdir": "Ordner angelegt",
|
|
||||||
"fileshare.delete": "Datei/Ordner gelöscht",
|
|
||||||
"fileshare.rename": "Datei/Ordner umbenannt",
|
|
||||||
"fileshare.download": "Als ZIP heruntergeladen",
|
|
||||||
"auditlog.archive": "Auditlog archiviert",
|
|
||||||
"auditlog.export_delete": "Auditlog-Archiv exportiert",
|
|
||||||
} %}
|
|
||||||
{% set action_icons = {
|
|
||||||
"delete": '<path d="M3 6h18M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2m3 0l-1 14a2 2 0 01-2 2H7a2 2 0 01-2-2L4 6"/>',
|
|
||||||
"create": '<path d="M12 5v14M5 12h14"/>',
|
|
||||||
"edit": '<path d="M12 20h9"/><path d="M16.5 3.5a2.1 2.1 0 013 3L7 19l-4 1 1-4z"/>',
|
|
||||||
"activate": '<path d="M20 6L9 17l-5-5"/>',
|
|
||||||
"deactivate": '<circle cx="12" cy="12" r="9"/><path d="M15 9l-6 6M9 9l6 6"/>',
|
|
||||||
} %}
|
|
||||||
{% set create_kinds = ("create", "upload", "mkdir") %}
|
|
||||||
|
|
||||||
{% macro category_of(kind) %}{% if kind in create_kinds %}create{% elif kind == "delete" %}delete{% else %}edit{% endif %}{% endmacro %}
|
|
||||||
|
|
||||||
{% set ns = namespace(create=0, delete=0, edit=0) %}
|
{% set ns = namespace(create=0, delete=0, edit=0) %}
|
||||||
{% for e in entries %}
|
{% for e in entries %}
|
||||||
{% set cat = category_of(e['action'].split('.')[-1])|trim %}
|
{% set cat = m.category_of(e['action'].split('.')[-1])|trim %}
|
||||||
{% if cat == "create" %}{% set ns.create = ns.create + 1 %}
|
{% if cat == "create" %}{% set ns.create = ns.create + 1 %}
|
||||||
{% elif cat == "delete" %}{% set ns.delete = ns.delete + 1 %}
|
{% elif cat == "delete" %}{% set ns.delete = ns.delete + 1 %}
|
||||||
{% else %}{% set ns.edit = ns.edit + 1 %}{% endif %}
|
{% else %}{% set ns.edit = ns.edit + 1 %}{% endif %}
|
||||||
@@ -59,19 +21,19 @@
|
|||||||
<div class="stat-row" style="margin-bottom:18px;">
|
<div class="stat-row" style="margin-bottom:18px;">
|
||||||
<div class="stat-card" data-category-filter="">
|
<div class="stat-card" data-category-filter="">
|
||||||
<div class="stat-label">Alle</div>
|
<div class="stat-label">Alle</div>
|
||||||
<div class="stat-value">{{ entries|length }}</div>
|
<div class="stat-value" id="statAll">{{ entries|length }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card" data-category-filter="create">
|
<div class="stat-card" data-category-filter="create">
|
||||||
<div class="stat-label">Hinzufügen</div>
|
<div class="stat-label">Hinzufügen</div>
|
||||||
<div class="stat-value" style="color:var(--success);">{{ ns.create }}</div>
|
<div class="stat-value" id="statCreate" style="color:var(--success);">{{ ns.create }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card" data-category-filter="edit">
|
<div class="stat-card" data-category-filter="edit">
|
||||||
<div class="stat-label">Änderungen</div>
|
<div class="stat-label">Änderungen</div>
|
||||||
<div class="stat-value" style="color:var(--accent-strong);">{{ ns.edit }}</div>
|
<div class="stat-value" id="statEdit" style="color:var(--accent-strong);">{{ ns.edit }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card" data-category-filter="delete">
|
<div class="stat-card" data-category-filter="delete">
|
||||||
<div class="stat-label">Löschungen</div>
|
<div class="stat-label">Löschungen</div>
|
||||||
<div class="stat-value" style="color:var(--danger);">{{ ns.delete }}</div>
|
<div class="stat-value" id="statDelete" style="color:var(--danger);">{{ ns.delete }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -92,29 +54,21 @@
|
|||||||
<th>Details</th>
|
<th>Details</th>
|
||||||
</tr></thead>
|
</tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for e in entries %}
|
{% for e in entries %}{{ m.audit_row(e) }}{% else %}
|
||||||
{% set kind = e['action'].split('.')[-1] %}
|
|
||||||
{% set cat = category_of(kind)|trim %}
|
|
||||||
<tr data-category="{{ cat }}" data-sort-ts="{{ e['ts'] }}" data-sort-user="{{ e['username']|lower }}"
|
|
||||||
data-sort-action="{{ action_labels.get(e['action'], e['action'])|lower }}" data-sort-target="{{ (e['target'] or '')|lower }}">
|
|
||||||
<td class="text-dim mono" style="font-size:12.5px;">{{ e['ts'] }}</td>
|
|
||||||
<td class="cell-name">{{ e['username'] }}</td>
|
|
||||||
<td>
|
|
||||||
<span class="pill action-pill action-pill--{{ cat }}">
|
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">{{ (action_icons.get(kind) or action_icons['edit'])|safe }}</svg>
|
|
||||||
{{ action_labels.get(e['action'], e['action']) }}
|
|
||||||
</span>
|
|
||||||
</td>
|
|
||||||
<td>{{ e['target'] or '—' }}</td>
|
|
||||||
<td class="text-dim">{{ e['details'] or '—' }}</td>
|
|
||||||
</tr>
|
|
||||||
{% else %}
|
|
||||||
<tr class="empty-row"><td colspan="5">Noch keine Änderungen protokolliert.</td></tr>
|
<tr class="empty-row"><td colspan="5">Noch keine Änderungen protokolliert.</td></tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<p id="auditNoResults" class="text-faint hidden" style="padding:16px; text-align:center; font-size:12.5px;">Keine Einträge für diese Auswahl.</p>
|
<p id="auditNoResults" class="text-faint hidden" style="padding:16px; text-align:center; font-size:12.5px;">Keine Einträge für diese Auswahl.</p>
|
||||||
|
<div id="auditLoadMoreWrap" class="{{ 'hidden' if not has_more }}" style="padding:16px; text-align:center; display:flex; gap:8px; justify-content:center;">
|
||||||
|
<button type="button" id="auditLoadMoreBtn" class="btn btn-primary" data-oldest-id="{{ oldest_loaded_id or '' }}">
|
||||||
|
Mehr laden (300)
|
||||||
|
</button>
|
||||||
|
<button type="button" id="auditLoadAllBtn" class="btn btn-secondary">
|
||||||
|
Alle laden
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -124,11 +78,11 @@
|
|||||||
document.addEventListener("DOMContentLoaded", function () {
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
const searchInput = document.getElementById("auditSearch");
|
const searchInput = document.getElementById("auditSearch");
|
||||||
const noResults = document.getElementById("auditNoResults");
|
const noResults = document.getElementById("auditNoResults");
|
||||||
const rows = Array.from(document.querySelectorAll("#auditTable tbody tr"));
|
|
||||||
let activeCategory = null;
|
let activeCategory = null;
|
||||||
|
|
||||||
function applyFilters() {
|
function applyFilters() {
|
||||||
const q = (searchInput ? searchInput.value : "").trim().toLowerCase();
|
const q = (searchInput ? searchInput.value : "").trim().toLowerCase();
|
||||||
|
const rows = Array.from(document.querySelectorAll("#auditTable tbody tr"));
|
||||||
let anyVisible = false;
|
let anyVisible = false;
|
||||||
rows.forEach(function (row) {
|
rows.forEach(function (row) {
|
||||||
if (row.classList.contains("empty-row")) return;
|
if (row.classList.contains("empty-row")) return;
|
||||||
@@ -156,7 +110,106 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/* ---- Nachladen älterer Einträge (Mehr laden / Alle laden) ---- */
|
||||||
|
const totalCount = {{ total_count }};
|
||||||
|
const entryCountLabel = document.getElementById("auditEntryCount");
|
||||||
|
|
||||||
|
function recomputeStats() {
|
||||||
|
const rows = Array.from(document.querySelectorAll("#auditTable tbody tr[data-category]"));
|
||||||
|
const counts = { create: 0, edit: 0, delete: 0 };
|
||||||
|
rows.forEach(function (r) { counts[r.dataset.category] = (counts[r.dataset.category] || 0) + 1; });
|
||||||
|
document.getElementById("statAll").textContent = rows.length;
|
||||||
|
document.getElementById("statCreate").textContent = counts.create;
|
||||||
|
document.getElementById("statEdit").textContent = counts.edit;
|
||||||
|
document.getElementById("statDelete").textContent = counts.delete;
|
||||||
|
if (entryCountLabel) {
|
||||||
|
entryCountLabel.textContent = rows.length < totalCount
|
||||||
|
? rows.length + " von " + totalCount + " Einträgen geladen"
|
||||||
|
: rows.length + " Eintrag" + (rows.length === 1 ? "" : "e");
|
||||||
|
}
|
||||||
|
return rows.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
const loadMoreBtn = document.getElementById("auditLoadMoreBtn");
|
||||||
|
const loadAllBtn = document.getElementById("auditLoadAllBtn");
|
||||||
|
const loadMoreWrap = document.getElementById("auditLoadMoreWrap");
|
||||||
|
const tbody = document.querySelector("#auditTable tbody");
|
||||||
|
const MORE_URL = "{{ url_for('activity_log_more') }}";
|
||||||
|
|
||||||
|
// Laedt genau einen weiteren 300er-Block nach und haengt ihn an -- von
|
||||||
|
// beiden Buttons genutzt: "Mehr laden" ruft das einmal auf, "Alle laden"
|
||||||
|
// ruft es wiederholt auf, bis der Server "keine weiteren mehr" meldet.
|
||||||
|
function loadMoreOnce() {
|
||||||
|
const oldestId = loadMoreBtn.dataset.oldestId;
|
||||||
|
if (!oldestId) return Promise.resolve({ hasMore: false });
|
||||||
|
return fetch(MORE_URL + "?before_id=" + encodeURIComponent(oldestId))
|
||||||
|
.then(function (r) {
|
||||||
|
const hasMore = r.headers.get("X-Has-More") === "1";
|
||||||
|
const newOldestId = r.headers.get("X-Oldest-Id");
|
||||||
|
return r.text().then(function (html) {
|
||||||
|
return { html: html, hasMore: hasMore, newOldestId: newOldestId };
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.then(function (result) {
|
||||||
|
const emptyRow = tbody.querySelector(".empty-row");
|
||||||
|
if (emptyRow) emptyRow.remove();
|
||||||
|
tbody.insertAdjacentHTML("beforeend", result.html);
|
||||||
|
if (result.newOldestId) loadMoreBtn.dataset.oldestId = result.newOldestId;
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loadMoreBtn) {
|
||||||
|
loadMoreBtn.addEventListener("click", function () {
|
||||||
|
loadMoreBtn.disabled = true;
|
||||||
|
if (loadAllBtn) loadAllBtn.disabled = true;
|
||||||
|
const originalText = loadMoreBtn.textContent;
|
||||||
|
loadMoreBtn.textContent = "Lädt …";
|
||||||
|
loadMoreOnce()
|
||||||
|
.then(function (result) {
|
||||||
|
if (!result.hasMore) loadMoreWrap.classList.add("hidden");
|
||||||
|
recomputeStats();
|
||||||
|
applyFilters();
|
||||||
|
})
|
||||||
|
.catch(function () { /* Button unten wird trotzdem wieder aktiviert */ })
|
||||||
|
.then(function () {
|
||||||
|
loadMoreBtn.disabled = false;
|
||||||
|
if (loadAllBtn) loadAllBtn.disabled = false;
|
||||||
|
loadMoreBtn.textContent = originalText;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loadAllBtn) {
|
||||||
|
loadAllBtn.addEventListener("click", function () {
|
||||||
|
loadMoreBtn.disabled = true;
|
||||||
|
loadAllBtn.disabled = true;
|
||||||
|
const originalText = loadAllBtn.textContent;
|
||||||
|
|
||||||
|
function step() {
|
||||||
|
loadAllBtn.textContent = "Lädt … (" + recomputeStats() + " von " + totalCount + ")";
|
||||||
|
return loadMoreOnce().then(function (result) {
|
||||||
|
return result.hasMore ? step() : null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
step()
|
||||||
|
.then(function () {
|
||||||
|
loadMoreWrap.classList.add("hidden");
|
||||||
|
})
|
||||||
|
.catch(function () { /* teilweise geladene Eintraege bleiben stehen */ })
|
||||||
|
.then(function () {
|
||||||
|
recomputeStats();
|
||||||
|
applyFilters();
|
||||||
|
loadMoreBtn.disabled = false;
|
||||||
|
loadAllBtn.disabled = false;
|
||||||
|
loadAllBtn.textContent = originalText;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
applyFilters();
|
applyFilters();
|
||||||
|
recomputeStats();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
Reference in New Issue
Block a user