feat(audit): add Audit Log page — read-only table of tenant audit entries

This commit is contained in:
admin-valentin 2026-07-31 17:12:05 +00:00
parent 643fe527a2
commit 6ee566143b

View file

@ -0,0 +1,102 @@
'use client';
import { useQuery } from '@tanstack/react-query';
import { apiFetch, type AuditLogEntry } from '../../../../lib/api';
import { useSession } from '../../../../components/session-provider';
function formatAction(action: string): string {
return action.replace(/\./g, ' · ').replace(/_/g, ' ');
}
function severityColor(action: string): string {
if (action.includes('deleted') || action.includes('removed')) return 'text-signal-danger';
if (action.includes('created') || action.includes('added')) return 'text-signal-ok';
if (action.includes('updated') || action.includes('changed') || action.includes('selected'))
return 'text-signal-warn';
return 'text-ink-faint';
}
export default function AuditLogPage() {
const { activeTenant } = useSession();
const tenantId = activeTenant?.tenantId ?? '';
const { data: entries = [], isLoading } = useQuery({
queryKey: ['audit-log', tenantId],
queryFn: () => apiFetch<AuditLogEntry[]>('/v1/audit-log?limit=200', { tenantId }),
enabled: Boolean(tenantId),
staleTime: 30_000,
});
return (
<div className="max-w-4xl">
<div className="mb-6">
<h1 className="font-display text-2xl font-semibold text-ink">Audit Log</h1>
<p className="text-sm text-ink-faint">
Toate operațiunile materiale din workspace imutabil, doar citire.
{entries.length > 0 && ` · Ultimele ${entries.length} înregistrări.`}
</p>
</div>
{isLoading && <p className="text-sm text-ink-faint">Se încarcă</p>}
{!isLoading && entries.length === 0 && (
<p className="card p-6 text-sm text-ink-faint">
Nicio înregistrare de audit. Jurnalul se completează automat cu fiecare operațiune.
</p>
)}
{entries.length > 0 && (
<div className="card overflow-hidden">
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-ink-line bg-paper-sunken text-left">
<th className="px-4 py-2.5 text-[11px] font-semibold uppercase tracking-wide text-ink-faint">
Data
</th>
<th className="px-4 py-2.5 text-[11px] font-semibold uppercase tracking-wide text-ink-faint">
Acțiune
</th>
<th className="px-4 py-2.5 text-[11px] font-semibold uppercase tracking-wide text-ink-faint">
Resursă
</th>
<th className="px-4 py-2.5 text-[11px] font-semibold uppercase tracking-wide text-ink-faint">
Actor
</th>
</tr>
</thead>
<tbody>
{entries.map((entry, i) => (
<tr
key={entry.id}
className={`border-b border-ink-line last:border-0 ${
i % 2 === 0 ? '' : 'bg-paper-sunken/40'
}`}
>
<td className="whitespace-nowrap px-4 py-2.5 text-xs text-ink-faint">
{new Date(entry.createdAt).toLocaleString('ro-RO', {
day: '2-digit',
month: '2-digit',
hour: '2-digit',
minute: '2-digit',
})}
</td>
<td className={`px-4 py-2.5 font-medium ${severityColor(entry.action)}`}>
{formatAction(entry.action)}
</td>
<td className="px-4 py-2.5 font-mono text-xs text-ink-soft">
{entry.resource}
</td>
<td className="px-4 py-2.5 font-mono text-xs text-ink-faint">
{entry.actorId ? entry.actorId.slice(0, 8) + '…' : entry.actorType}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
</div>
);
}