Beim Export archivierter Auditlog-Tage (log_action("auditlog.export_delete", ...))
stand faelschlich die komplette Dateiliste im Feld "Ziel" (dadurch wurde die
Spalte in der Auditlog-Tabelle wieder unschoen breit), statt wie bei allen
anderen Aktionen ueblich einen kurzen Ressourcennamen im Ziel und die
ausfuehrliche Beschreibung in Details zu haben. Jetzt: Ziel = "Auditlog",
Details = Anzahl + exakte Dateinamen. Gleiche Korrektur fuer den
automatischen Archivierungslauf selbst (auditlog.archive). "Ziel"-Spalte
zusaetzlich mit white-space:nowrap gegen Umbrueche bei laengeren Werten
abgesichert.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
163 lines
7.1 KiB
HTML
163 lines
7.1 KiB
HTML
{% extends "base.html" %}
|
|
{% set active_page = "logs" %}
|
|
{% block page_title %}Auditlog{% endblock %}
|
|
{% block page_sub %}<div class="topbar-sub">{{ entries|length }} Einträge (letzte 500)</div>{% endblock %}
|
|
|
|
{% block content %}
|
|
|
|
{% 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) %}
|
|
{% for e in entries %}
|
|
{% set cat = category_of(e['action'].split('.')[-1])|trim %}
|
|
{% if cat == "create" %}{% set ns.create = ns.create + 1 %}
|
|
{% elif cat == "delete" %}{% set ns.delete = ns.delete + 1 %}
|
|
{% else %}{% set ns.edit = ns.edit + 1 %}{% endif %}
|
|
{% endfor %}
|
|
|
|
<div class="stat-row" style="margin-bottom:18px;">
|
|
<div class="stat-card" data-category-filter="">
|
|
<div class="stat-label">Alle</div>
|
|
<div class="stat-value">{{ entries|length }}</div>
|
|
</div>
|
|
<div class="stat-card" data-category-filter="create">
|
|
<div class="stat-label">Hinzufügen</div>
|
|
<div class="stat-value" style="color:var(--success);">{{ ns.create }}</div>
|
|
</div>
|
|
<div class="stat-card" data-category-filter="edit">
|
|
<div class="stat-label">Änderungen</div>
|
|
<div class="stat-value" style="color:var(--accent-strong);">{{ ns.edit }}</div>
|
|
</div>
|
|
<div class="stat-card" data-category-filter="delete">
|
|
<div class="stat-label">Löschungen</div>
|
|
<div class="stat-value" style="color:var(--danger);">{{ ns.delete }}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="table-wrap">
|
|
<div class="table-toolbar">
|
|
<div class="search-input">
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg>
|
|
<input type="text" id="auditSearch" placeholder="Benutzer, Aktion, Ziel oder Details durchsuchen...">
|
|
</div>
|
|
</div>
|
|
<div style="overflow-x:auto;">
|
|
<table class="data-table" id="auditTable" data-sortable>
|
|
<thead><tr>
|
|
<th data-sort-key="ts" style="width:1%; white-space:nowrap;">Zeitpunkt</th>
|
|
<th data-sort-key="user" style="width:1%; white-space:nowrap;">Benutzer</th>
|
|
<th data-sort-key="action" style="width:1%; white-space:nowrap;">Aktion</th>
|
|
<th data-sort-key="target" style="width:1%; white-space:nowrap;">Ziel</th>
|
|
<th>Details</th>
|
|
</tr></thead>
|
|
<tbody>
|
|
{% for e in entries %}
|
|
{% 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 style="white-space:nowrap;">{{ 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>
|
|
{% endfor %}
|
|
</tbody>
|
|
</table>
|
|
</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>
|
|
</div>
|
|
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
document.addEventListener("DOMContentLoaded", function () {
|
|
const searchInput = document.getElementById("auditSearch");
|
|
const noResults = document.getElementById("auditNoResults");
|
|
const rows = Array.from(document.querySelectorAll("#auditTable tbody tr"));
|
|
let activeCategory = null;
|
|
|
|
function applyFilters() {
|
|
const q = (searchInput ? searchInput.value : "").trim().toLowerCase();
|
|
let anyVisible = false;
|
|
rows.forEach(function (row) {
|
|
if (row.classList.contains("empty-row")) return;
|
|
const categoryMatches = !activeCategory || row.dataset.category === activeCategory;
|
|
const textMatches = !q || row.innerText.toLowerCase().includes(q);
|
|
const match = categoryMatches && textMatches;
|
|
row.style.display = match ? "" : "none";
|
|
if (match) anyVisible = true;
|
|
});
|
|
if (noResults) noResults.classList.toggle("hidden", anyVisible);
|
|
document.querySelectorAll(".stat-card[data-category-filter]").forEach(function (card) {
|
|
const isTotal = card.dataset.categoryFilter === "";
|
|
const active = activeCategory === null ? isTotal : card.dataset.categoryFilter === activeCategory;
|
|
card.classList.toggle("active", active);
|
|
});
|
|
}
|
|
|
|
if (searchInput) searchInput.addEventListener("input", applyFilters);
|
|
|
|
document.querySelectorAll(".stat-card[data-category-filter]").forEach(function (card) {
|
|
card.addEventListener("click", function () {
|
|
const key = this.dataset.categoryFilter;
|
|
activeCategory = (!key || activeCategory === key) ? null : key;
|
|
applyFilters();
|
|
});
|
|
});
|
|
|
|
applyFilters();
|
|
});
|
|
</script>
|
|
{% endblock %}
|