feat(audit): expose structured before/after evidence
audit_events already stored before_json/after_json, but the API and UI only ever surfaced metadata -- the audit trail could say something happened but never show what changed. Add before/after to AuditEventOut, resolve a safe entity_ref/entity_link for vehicle/booking/data-quality-issue entities (customer stays label-only; no customer detail route exists in this PoC), and render a human-readable change summary in the UI with the raw before/after/metadata JSON kept behind a <details> disclosure rather than shown by default.
This commit is contained in:
@@ -1,9 +1,25 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Link } from "react-router-dom";
|
||||
import { api } from "../api/client";
|
||||
import type { AuditEvent } from "../api/types";
|
||||
import { useAuth } from "../context/AuthContext";
|
||||
import { EmptyState, ErrorState, LoadingState, PageHeader } from "../components/PageChrome";
|
||||
|
||||
function describeChanges(before: Record<string, unknown> | null, after: Record<string, unknown> | null): string {
|
||||
if (!before && !after) return "No recorded change detail.";
|
||||
const keys = new Set([...Object.keys(before ?? {}), ...Object.keys(after ?? {})]);
|
||||
const lines: string[] = [];
|
||||
for (const key of keys) {
|
||||
const b = before?.[key];
|
||||
const a = after?.[key];
|
||||
if (JSON.stringify(b) === JSON.stringify(a)) continue;
|
||||
if (b === undefined) lines.push(`${key}: set to ${JSON.stringify(a)}`);
|
||||
else if (a === undefined) lines.push(`${key}: was ${JSON.stringify(b)}`);
|
||||
else lines.push(`${key}: ${JSON.stringify(b)} → ${JSON.stringify(a)}`);
|
||||
}
|
||||
return lines.length > 0 ? lines.join("; ") : "No field-level change detected.";
|
||||
}
|
||||
|
||||
export function Audit() {
|
||||
const { user } = useAuth();
|
||||
const [events, setEvents] = useState<AuditEvent[] | null>(null);
|
||||
@@ -60,7 +76,8 @@ export function Audit() {
|
||||
<th scope="col">Actor</th>
|
||||
<th scope="col">Action</th>
|
||||
<th scope="col">Entity</th>
|
||||
<th scope="col">Correlation</th>
|
||||
<th scope="col">Change</th>
|
||||
<th scope="col">Details</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -73,8 +90,20 @@ export function Audit() {
|
||||
</td>
|
||||
<td data-label="Actor"><strong>{e.actor_label}</strong><small className="table-subtext">{e.actor_type}</small></td>
|
||||
<td data-label="Action">{e.action.replace(/_/g, " ")}</td>
|
||||
<td data-label="Entity">{e.entity_type}</td>
|
||||
<td className="mono" data-label="Correlation"><details><summary>{e.correlation_id.slice(0, 8)}</summary><pre>{JSON.stringify(e.metadata ?? {}, null, 2)}</pre></details></td>
|
||||
<td data-label="Entity">
|
||||
{e.entity_link ? (
|
||||
<Link to={e.entity_link}>{e.entity_ref ?? e.entity_type}</Link>
|
||||
) : (
|
||||
e.entity_ref ?? e.entity_type
|
||||
)}
|
||||
</td>
|
||||
<td data-label="Change">{describeChanges(e.before, e.after)}</td>
|
||||
<td className="mono" data-label="Details">
|
||||
<details>
|
||||
<summary>{e.correlation_id.slice(0, 8)}</summary>
|
||||
<pre>{JSON.stringify({ before: e.before, after: e.after, metadata: e.metadata }, null, 2)}</pre>
|
||||
</details>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
|
||||
Reference in New Issue
Block a user