Plugin audit-log: Protokolltabelle, öffentliche API und Admin-Ansicht
- Eigene Migration 0001_audit_eintraege (Tabelle audit_eintraege: Akteur,
Aktion, Objekttyp/-ID, Alt/Neu + Details als JSON, optionale IP, Zeitstempel)
- Öffentliche Plugin-API: await log(actor, action, objekt_typ, objekt_id,
details, ip_adresse) bzw. log_sync() für synchrone Routen; andere Plugins
holen die API über request.app.state.registry.get("audit-log")
- Admin-Ansicht /audit-log (nur Admin): Filter nach Benutzer, Aktionstyp und
Zeitraum, Paginierung (25/Seite), deutsche UI, Alt/Neu-JSON-Darstellung
- Tests: Logging-Funktion (Actor-Varianten, alt/neu-Extraktion), Migration,
Filter-Querys, Paginierung, Nur-Admin-Zugriff (13 neue Tests)
- Loader-Test angepasst: voll implementierte Plugins tragen keinen
Platzhalter-Text mehr
- AGENTS.md: Status aktualisiert
Verifiziert: HEAD + diese Änderungen = 58 Tests grün (uv run pytest)
This commit is contained in:
@@ -1,10 +1,132 @@
|
||||
{% extends "base.html" %}
|
||||
{% block titel %}{{ titel }} — Spiele-Redaktion{% endblock %}
|
||||
{% block titel %}{{ titel }} — {{ app_name }}{% endblock %}
|
||||
{% block inhalt %}
|
||||
<h1 class="text-2xl font-bold mb-2">{{ titel }}</h1>
|
||||
<p class="text-slate-600 max-w-2xl">
|
||||
Plugin <code class="bg-slate-200 rounded px-1 py-0.5 text-sm">{{ name }}</code>
|
||||
in Version {{ version }} ist geladen.
|
||||
Diese Seite ist ein Platzhalter — die Funktion wird in einer späteren Phase implementiert.
|
||||
<h1 class="text-2xl font-bold mb-1">{{ titel }}</h1>
|
||||
<p class="text-slate-600 mb-4 text-sm">
|
||||
Protokoll aller relevanten Ereignisse — nur für Administratoren sichtbar.
|
||||
</p>
|
||||
|
||||
{% if fehler %}
|
||||
{% for meldung in fehler %}
|
||||
<div class="mb-3 rounded-lg border border-amber-300 bg-amber-50 text-amber-800 px-4 py-2 text-sm">
|
||||
{{ meldung }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<form method="get" action="/audit-log" class="bg-white rounded-xl border border-slate-200 p-4 mb-4">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-5 gap-3 items-end">
|
||||
<label class="block text-sm">
|
||||
<span class="text-slate-500 block mb-1">Benutzer</span>
|
||||
<select name="benutzer"
|
||||
class="w-full border border-slate-300 rounded-lg px-2 py-2 bg-white">
|
||||
<option value="">Alle</option>
|
||||
{% for name in benutzer_auswahl %}
|
||||
<option value="{{ name }}" {% if name == filter_benutzer %}selected{% endif %}>{{ name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<label class="block text-sm">
|
||||
<span class="text-slate-500 block mb-1">Aktionstyp</span>
|
||||
<select name="aktion"
|
||||
class="w-full border border-slate-300 rounded-lg px-2 py-2 bg-white">
|
||||
<option value="">Alle</option>
|
||||
{% for a in aktionen_auswahl %}
|
||||
<option value="{{ a }}" {% if a == filter_aktion %}selected{% endif %}>
|
||||
{{ aktionen_anzeige[a] or a }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</label>
|
||||
<label class="block text-sm">
|
||||
<span class="text-slate-500 block mb-1">Von</span>
|
||||
<input type="date" name="von" value="{{ filter_von }}"
|
||||
class="w-full border border-slate-300 rounded-lg px-2 py-2">
|
||||
</label>
|
||||
<label class="block text-sm">
|
||||
<span class="text-slate-500 block mb-1">Bis</span>
|
||||
<input type="date" name="bis" value="{{ filter_bis }}"
|
||||
class="w-full border border-slate-300 rounded-lg px-2 py-2">
|
||||
</label>
|
||||
<div class="flex gap-2">
|
||||
<button type="submit"
|
||||
class="bg-emerald-600 hover:bg-emerald-700 text-white rounded-lg px-4 py-2 text-sm font-medium">
|
||||
Filtern
|
||||
</button>
|
||||
<a href="/audit-log"
|
||||
class="border border-slate-300 hover:bg-slate-50 rounded-lg px-4 py-2 text-sm text-slate-600">
|
||||
Zurücksetzen
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div class="bg-white rounded-xl border border-slate-200 overflow-x-auto">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-slate-50 text-left text-slate-500">
|
||||
<tr>
|
||||
<th class="px-4 py-2 font-medium whitespace-nowrap">Zeitpunkt</th>
|
||||
<th class="px-4 py-2 font-medium">Benutzer</th>
|
||||
<th class="px-4 py-2 font-medium">Aktion</th>
|
||||
<th class="px-4 py-2 font-medium">Objekt</th>
|
||||
<th class="px-4 py-2 font-medium">Änderung / Details</th>
|
||||
<th class="px-4 py-2 font-medium">IP</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-slate-100">
|
||||
{% for e in eintraege %}
|
||||
<tr class="align-top">
|
||||
<td class="px-4 py-2 text-slate-500 whitespace-nowrap">
|
||||
{{ e.erstellt_am.strftime('%d.%m.%Y %H:%M:%S') if e.erstellt_am else '–' }}
|
||||
</td>
|
||||
<td class="px-4 py-2 font-medium">{{ e.actor_name }}</td>
|
||||
<td class="px-4 py-2">{{ aktionen_anzeige[e.action] or e.action }}</td>
|
||||
<td class="px-4 py-2">
|
||||
{{ e.objekt_typ }}{% if e.objekt_id %} #{{ e.objekt_id }}{% endif %}
|
||||
</td>
|
||||
<td class="px-4 py-2 max-w-md">
|
||||
{% if e.alt or e.neu %}
|
||||
{% if e.alt %}<div class="mb-1"><span class="text-slate-400 text-xs">Alt:</span>
|
||||
<pre class="text-xs bg-slate-50 rounded p-1 overflow-x-auto whitespace-pre-wrap">{{ e.alt | json_schoen }}</pre></div>{% endif %}
|
||||
{% if e.neu %}<div><span class="text-slate-400 text-xs">Neu:</span>
|
||||
<pre class="text-xs bg-emerald-50 rounded p-1 overflow-x-auto whitespace-pre-wrap">{{ e.neu | json_schoen }}</pre></div>{% endif %}
|
||||
{% elif e.details %}
|
||||
<pre class="text-xs bg-slate-50 rounded p-1 overflow-x-auto whitespace-pre-wrap">{{ e.details | json_schoen }}</pre>
|
||||
{% else %}
|
||||
<span class="text-slate-400">–</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="px-4 py-2 text-slate-500">{{ e.ip_adresse or '–' }}</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="6" class="px-4 py-8 text-center text-slate-500">
|
||||
Keine Einträge gefunden.
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between mt-3 text-sm text-slate-600">
|
||||
<p>
|
||||
{% if gesamt %}
|
||||
Zeige {{ start }}–{{ ende }} von {{ gesamt }} Einträgen
|
||||
{% else %}
|
||||
0 Einträge
|
||||
{% endif %}
|
||||
</p>
|
||||
{% if gesamt_seiten > 1 %}
|
||||
<div class="flex items-center gap-2">
|
||||
{% if seite > 1 %}
|
||||
<a href="{{ url_zurueck }}" class="border border-slate-300 hover:bg-slate-50 rounded-lg px-3 py-1">← Zurück</a>
|
||||
{% endif %}
|
||||
<span>Seite {{ seite }} von {{ gesamt_seiten }}</span>
|
||||
{% if seite < gesamt_seiten %}
|
||||
<a href="{{ url_weiter }}" class="border border-slate-300 hover:bg-slate-50 rounded-lg px-3 py-1">Weiter →</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
||||
Reference in New Issue
Block a user