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:
claude-dev
2026-05-09 03:19:32 +00:00
Ursprung 2001815e19
Commit 6b70a7195e
4 geänderte Dateien mit 131 neuen und 0 gelöschten Zeilen

Datei anzeigen

@@ -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&auml;ge f&uuml;r diese Quelle.</div>';
return;
}
c.innerHTML = items.map(e => {
const meta = `${formatDateTime(e.ts)} &middot; ${esc(e.admin_username || "-")} &middot; ${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}`;