Phase 5: Audit-Spur pro Quelle (ausklappbares Modal)
Backend
- routers/audit.py: GET /api/audit-log nimmt jetzt resource_id als Filter
(zusätzlich zu resource_type, action, admin_id, from_ts/to_ts).
Frontend
- dashboard.html: modalAudit (Modal) für die Audit-Spur einer Ressource.
- style.css: audit-entry Styles (action-Badge mit Farbcode pro Action-Typ,
Diff als <details>-Block mit JSON-Pre).
- sources.js:
- showSourceAudit(id, name) öffnet Modal, lädt /audit-log?resource_type=source&resource_id=...
- renderAuditEntries: pro Eintrag Action-Badge + Meta (ts/admin/ip) +
optional ausklappbarer Diff (before/after-JSON)
- formatDateTime Helper
- Audit-Button in der Aktionen-Spalte der Grundquellen-Tabelle
Dieser Commit ist enthalten in:
@@ -63,6 +63,56 @@ async function loadGlobalSources() {
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function showSourceAudit(sourceId, sourceName) {
|
||||
document.getElementById("auditTitle").textContent = `Audit-Spur: ${sourceName}`;
|
||||
document.getElementById("auditContent").innerHTML = '<div class="text-muted">Lade...</div>';
|
||||
openModal("modalAudit");
|
||||
try {
|
||||
const res = await API.get(`/api/audit-log?resource_type=source&resource_id=${sourceId}&limit=50`);
|
||||
renderAuditEntries(res.items || []);
|
||||
} catch (err) {
|
||||
document.getElementById("auditContent").innerHTML =
|
||||
`<div class="text-danger">Audit konnte nicht geladen werden: ${esc(err.message || String(err))}</div>`;
|
||||
}
|
||||
}
|
||||
|
||||
function renderAuditEntries(items) {
|
||||
const c = document.getElementById("auditContent");
|
||||
if (!items.length) {
|
||||
c.innerHTML = '<div class="text-muted">Keine Audit-Einträge für diese Quelle.</div>';
|
||||
return;
|
||||
}
|
||||
c.innerHTML = items.map(e => {
|
||||
const meta = `${formatDateTime(e.ts)} · ${esc(e.admin_username || "-")} · ${esc(e.ip || "-")}`;
|
||||
const hasDiff = (e.before && Object.keys(e.before).length) || (e.after && Object.keys(e.after).length);
|
||||
const diffPayload = JSON.stringify({ before: e.before, after: e.after }, null, 2);
|
||||
return `
|
||||
<div class="audit-entry">
|
||||
<div class="audit-entry-head">
|
||||
<span class="audit-entry-action audit-action-${esc(e.action)}">${esc(e.action)}</span>
|
||||
<span class="audit-entry-meta">${meta}</span>
|
||||
</div>
|
||||
${hasDiff ? `<details class="audit-entry-detail">
|
||||
<summary>Diff anzeigen</summary>
|
||||
<pre>${esc(diffPayload)}</pre>
|
||||
</details>` : ""}
|
||||
</div>
|
||||
`;
|
||||
}).join("");
|
||||
}
|
||||
|
||||
function formatDateTime(iso) {
|
||||
if (!iso) return "-";
|
||||
try {
|
||||
const d = new Date(iso);
|
||||
return d.toLocaleString("de-DE", {
|
||||
day: "2-digit", month: "2-digit", year: "numeric",
|
||||
hour: "2-digit", minute: "2-digit",
|
||||
});
|
||||
} catch { return iso; }
|
||||
}
|
||||
|
||||
function renderGlobalStats(stats) {
|
||||
const bar = document.getElementById("globalStatsBar");
|
||||
if (!bar) return;
|
||||
@@ -131,6 +181,7 @@ function renderGlobalSources(sources) {
|
||||
<td><span class="badge badge-${s.status === "active" ? "active" : "inactive"}">${s.status === "active" ? "Aktiv" : "Inaktiv"}</span></td>
|
||||
<td>
|
||||
<button class="btn btn-secondary btn-small" onclick="editGlobalSource(${s.id})">Bearbeiten</button>
|
||||
<button class="btn btn-secondary btn-small" onclick="showSourceAudit(${s.id}, '${esc(s.name)}')">Audit</button>
|
||||
<button class="btn btn-danger btn-small" onclick="confirmDeleteGlobalSource(${s.id}, '${esc(s.name)}')">Löschen</button>
|
||||
</td>
|
||||
</tr>${notesRow}`;
|
||||
|
||||
In neuem Issue referenzieren
Einen Benutzer sperren