Implemented audit logging

This commit is contained in:
2026-10-22 12:31:37 +02:00
parent 92891438f6
commit 4dd0899209
12 changed files with 3477 additions and 90 deletions
@@ -20,6 +20,8 @@ import {
Award as AwardIcon,
FileText,
AlertCircle,
AlertTriangle,
AlertOctagon,
Layers,
Activity,
Calendar,
@@ -370,8 +372,14 @@ export const SettingsPage: React.FC = () => {
const [auditSearchTerm, setAuditSearchTerm] = useState('');
const [auditEntityFilter, setAuditEntityFilter] = useState<string>('all');
const [auditActionFilter, setAuditActionFilter] = useState<string>('all');
const [auditTypeFilter, setAuditTypeFilter] = useState<'all' | 'audits' | 'exceptions'>('all');
const [auditPage, setAuditPage] = useState(1);
const [auditPageSize, setAuditPageSize] = useState(10);
const [selectedAuditLog, setSelectedAuditLog] = useState<AuditLog | null>(null);
const [isAuditDetailModalOpen, setIsAuditDetailModalOpen] = useState(false);
const [selectedExceptionLog, setSelectedExceptionLog] = useState<AuditLog | null>(null);
const [isExceptionDetailModalOpen, setIsExceptionDetailModalOpen] = useState(false);
const [copiedExceptionTrace, setCopiedExceptionTrace] = useState(false);
const [copiedAuditPayload, setCopiedAuditPayload] = useState(false);
const [expandedAuditLogIds, setExpandedAuditLogIds] = useState<Record<string | number, boolean>>({});
const [copiedUserId, setCopiedUserId] = useState<string | number | null>(null);
@@ -402,11 +410,14 @@ export const SettingsPage: React.FC = () => {
isFetching: isFetchingAuditLogs,
} = useQuery({
queryKey: ['audit-logs'],
queryFn: () => getAuditLogs(1, 100),
queryFn: () => getAuditLogs(1, 500),
});
const isLogException = (log: AuditLog) => Boolean(log.error || log.isError);
const auditExportColumns: ExportColumn<AuditLog>[] = [
{ header: 'Event ID', key: 'id' },
{ header: 'Event Type', key: 'error', format: (val, row) => (val || row?.isError ? 'EXCEPTION' : 'AUDIT') },
{ header: 'Timestamp', key: 'timestamp', format: (val) => formatDate(val) },
{ header: 'Action', key: 'action', format: (val) => val || '—' },
{ header: 'Entity Name', key: 'entityName', format: (val) => val || '—' },
@@ -421,10 +432,11 @@ export const SettingsPage: React.FC = () => {
},
{ header: 'Tenant ID', key: 'tenantId', format: (val) => (val ? `Tenant #${val}` : '—') },
{
header: 'Changes',
header: 'Changes / Arguments',
key: 'changes',
format: (val) => (Array.isArray(val) ? val.join('; ') : val || '—'),
},
{ header: 'Error Details', key: 'error', format: (val) => val || '—' },
];
const uniqueAuditEntities = Array.from(
@@ -435,6 +447,13 @@ export const SettingsPage: React.FC = () => {
);
const filteredAuditLogs = auditLogs.filter((log) => {
const isEx = isLogException(log);
if (auditTypeFilter === 'audits' && isEx) {
return false;
}
if (auditTypeFilter === 'exceptions' && !isEx) {
return false;
}
if (auditEntityFilter !== 'all' && (log.entityName || '').toLowerCase() !== auditEntityFilter.toLowerCase()) {
return false;
}
@@ -449,6 +468,7 @@ export const SettingsPage: React.FC = () => {
const entityId = String(log.entityId || '').toLowerCase();
const performedBy = (log.performedBy || '').toLowerCase();
const tenant = String(log.tenantId || '').toLowerCase();
const err = (log.error || '').toLowerCase();
const changes = Array.isArray(log.changes) ? log.changes.join(' ').toLowerCase() : '';
return (
id.includes(q) ||
@@ -457,6 +477,7 @@ export const SettingsPage: React.FC = () => {
entityId.includes(q) ||
performedBy.includes(q) ||
tenant.includes(q) ||
err.includes(q) ||
changes.includes(q)
);
});
@@ -468,6 +489,13 @@ export const SettingsPage: React.FC = () => {
handleSort: handleAuditSort,
} = useTableSort(filteredAuditLogs, 'timestamp', 'desc');
const totalAuditRecords = sortedAuditLogs.length;
const totalAuditPages = Math.max(1, Math.ceil(totalAuditRecords / auditPageSize));
const paginatedAuditLogs = React.useMemo(() => {
const start = (auditPage - 1) * auditPageSize;
return sortedAuditLogs.slice(start, start + auditPageSize);
}, [sortedAuditLogs, auditPage, auditPageSize]);
const userExportColumns: ExportColumn<User>[] = [
{ header: 'User Email', key: 'email', format: (val, u) => val || getUserEmail(u.id) || '—' },
{ header: 'User Identifier', key: 'id' },
@@ -1532,7 +1560,10 @@ export const SettingsPage: React.FC = () => {
<input
type="text"
value={auditSearchTerm}
onChange={(e) => setAuditSearchTerm(e.target.value)}
onChange={(e) => {
setAuditSearchTerm(e.target.value);
setAuditPage(1);
}}
placeholder="Search audit trail by action, entity name, ID, performed by UUID..."
className="w-full pl-9 pr-3 py-2 text-xs bg-[#090d16] border border-slate-700/80 rounded-xl text-white placeholder-slate-500 focus:outline-none focus:ring-1 focus:ring-blue-500"
/>
@@ -1543,7 +1574,10 @@ export const SettingsPage: React.FC = () => {
<span className="text-xs font-semibold text-slate-400 whitespace-nowrap">Entity:</span>
<select
value={auditEntityFilter}
onChange={(e) => setAuditEntityFilter(e.target.value)}
onChange={(e) => {
setAuditEntityFilter(e.target.value);
setAuditPage(1);
}}
className="text-xs bg-[#090d16] border border-slate-700/80 rounded-xl px-2.5 py-1.5 text-white focus:outline-none focus:ring-1 focus:ring-blue-500 [&>option]:bg-slate-900 [&>option]:text-white cursor-pointer"
>
<option value="all">All Entities</option>
@@ -1568,7 +1602,10 @@ export const SettingsPage: React.FC = () => {
<span className="text-xs font-semibold text-slate-400 whitespace-nowrap">Action:</span>
<select
value={auditActionFilter}
onChange={(e) => setAuditActionFilter(e.target.value)}
onChange={(e) => {
setAuditActionFilter(e.target.value);
setAuditPage(1);
}}
className="text-xs bg-[#090d16] border border-slate-700/80 rounded-xl px-2.5 py-1.5 text-white focus:outline-none focus:ring-1 focus:ring-blue-500 [&>option]:bg-slate-900 [&>option]:text-white cursor-pointer"
>
<option value="all">All Actions</option>
@@ -1580,13 +1617,31 @@ export const SettingsPage: React.FC = () => {
</select>
</div>
{(auditSearchTerm || auditEntityFilter !== 'all' || auditActionFilter !== 'all') && (
<div className="flex items-center gap-1.5">
<span className="text-xs font-semibold text-slate-400 whitespace-nowrap">Type:</span>
<select
value={auditTypeFilter}
onChange={(e) => {
setAuditTypeFilter(e.target.value as 'all' | 'audits' | 'exceptions');
setAuditPage(1);
}}
className="text-xs bg-[#090d16] border border-slate-700/80 rounded-xl px-2.5 py-1.5 text-white focus:outline-none focus:ring-1 focus:ring-blue-500 [&>option]:bg-slate-900 [&>option]:text-white cursor-pointer"
>
<option value="all">All Types ({auditLogs.length})</option>
<option value="audits">Audits Only ({auditLogs.filter((l) => !l.error && !l.isError).length})</option>
<option value="exceptions">Exceptions Only ({auditLogs.filter((l) => Boolean(l.error || l.isError)).length})</option>
</select>
</div>
{(auditSearchTerm || auditEntityFilter !== 'all' || auditActionFilter !== 'all' || auditTypeFilter !== 'all') && (
<button
type="button"
onClick={() => {
setAuditSearchTerm('');
setAuditEntityFilter('all');
setAuditActionFilter('all');
setAuditTypeFilter('all');
setAuditPage(1);
}}
className="px-2.5 py-1.5 text-xs text-slate-400 hover:text-white bg-slate-800 hover:bg-slate-700 border border-slate-700/80 rounded-xl transition cursor-pointer"
>
@@ -1624,12 +1679,18 @@ export const SettingsPage: React.FC = () => {
<div className="p-4 border-b border-slate-800/80 flex items-center justify-between">
<div className="flex items-center gap-2">
<History className="w-4 h-4 text-blue-400" />
<h3 className="text-sm font-semibold text-white">
Audit Log Ledger ({filteredAuditLogs.length} of {auditLogs.length})
<h3 className="text-sm font-semibold text-white flex items-center gap-2">
<span>Audit Log Ledger ({filteredAuditLogs.length} of {auditLogs.length})</span>
{auditLogs.some((l) => Boolean(l.error || l.isError)) && (
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[10px] font-bold bg-rose-500/10 text-rose-400 border border-rose-500/20">
<AlertTriangle className="w-3 h-3 text-rose-400" />
{auditLogs.filter((l) => Boolean(l.error || l.isError)).length} Exceptions
</span>
)}
</h3>
<HelpTooltip
title="Audit Trail"
content="Authoritative record of state changes and system activities."
content="Authoritative record of state changes, exceptions, and system activities."
size="sm"
placement="right"
/>
@@ -1671,13 +1732,14 @@ export const SettingsPage: React.FC = () => {
<tr>
<td colSpan={6} className="py-12 text-center text-slate-500">
<History className="w-8 h-8 text-slate-600 mx-auto mb-2" />
{auditSearchTerm || auditEntityFilter !== 'all' || auditActionFilter !== 'all'
{auditSearchTerm || auditEntityFilter !== 'all' || auditActionFilter !== 'all' || auditTypeFilter !== 'all'
? 'No audit log entries match your filter criteria.'
: 'No audit records found.'}
</td>
</tr>
) : (
sortedAuditLogs.map((log) => {
paginatedAuditLogs.map((log) => {
const isException = Boolean(log.error || log.isError);
const badge = getActionBadge(log.action);
const isExpanded = !!expandedAuditLogIds[log.id];
const changesCount = Array.isArray(log.changes) ? log.changes.length : 0;
@@ -1689,8 +1751,14 @@ export const SettingsPage: React.FC = () => {
<React.Fragment key={log.id}>
<tr
onClick={() => toggleExpandAuditLog(log.id)}
className={`hover:bg-slate-800/40 transition-colors cursor-pointer ${
isExpanded ? 'bg-slate-800/30' : ''
className={`transition-colors cursor-pointer border-l-4 ${
isException
? isExpanded
? 'border-l-rose-500 bg-rose-950/25 hover:bg-rose-950/35'
: 'border-l-rose-500 bg-rose-950/15 hover:bg-rose-950/25'
: isExpanded
? 'border-l-blue-500 bg-slate-800/40 hover:bg-slate-800/50'
: 'border-l-transparent hover:bg-slate-800/40'
}`}
>
<td className="py-3.5 px-3 text-center">
@@ -1705,7 +1773,13 @@ export const SettingsPage: React.FC = () => {
>
<ChevronRight
className={`w-3.5 h-3.5 transition-transform duration-200 ${
isExpanded ? 'rotate-90 text-blue-400' : 'text-slate-500'
isExpanded
? isException
? 'rotate-90 text-rose-400'
: 'rotate-90 text-blue-400'
: isException
? 'text-rose-400/80'
: 'text-slate-500'
}`}
/>
</button>
@@ -1714,12 +1788,33 @@ export const SettingsPage: React.FC = () => {
{formatDate(log.timestamp)}
</td>
<td className="py-3.5 px-4 font-mono font-bold text-white whitespace-nowrap">
#{log.id}
<div className="flex items-center gap-1.5">
<span>#{log.id}</span>
{isException && (
<span
className="inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[9px] font-bold uppercase tracking-wider bg-rose-950 text-rose-300 border border-rose-700/60 shadow-xs"
title="Exception captured"
>
<AlertTriangle className="w-2.5 h-2.5 text-rose-400 shrink-0" />
Exception
</span>
)}
</div>
</td>
<td className="py-3.5 px-4 whitespace-nowrap">
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-semibold border ${badge.className}`}>
{badge.label}
</span>
{isException ? (
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[10px] font-semibold border bg-rose-500/10 text-rose-400 border-rose-500/30">
<AlertCircle className="w-3 h-3 text-rose-400 shrink-0" />
<span>{log.action || 'EXCEPTION'}</span>
<span className="ml-0.5 px-1 py-0.2 rounded bg-rose-500/20 text-[9px] font-bold text-rose-300 uppercase">
FAIL
</span>
</span>
) : (
<span className={`inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-semibold border ${badge.className}`}>
{badge.label}
</span>
)}
</td>
<td
className="py-3.5 px-4"
@@ -1749,18 +1844,34 @@ export const SettingsPage: React.FC = () => {
</div>
</td>
<td className="py-3.5 px-5 text-right whitespace-nowrap">
<button
type="button"
onClick={(e) => {
e.stopPropagation();
setSelectedAuditLog(log);
setIsAuditDetailModalOpen(true);
}}
className="inline-flex items-center gap-1 px-2.5 py-1 text-[11px] font-semibold text-slate-300 bg-slate-800 hover:bg-slate-700 border border-slate-700 rounded-lg transition cursor-pointer"
>
<Eye className="w-3 h-3 text-blue-400" />
Inspect
</button>
{isException ? (
<button
type="button"
onClick={(e) => {
e.stopPropagation();
setSelectedExceptionLog(log);
setIsExceptionDetailModalOpen(true);
}}
className="inline-flex items-center gap-1 px-2.5 py-1 text-[11px] font-semibold text-rose-300 bg-rose-500/10 hover:bg-rose-500/20 border border-rose-500/30 rounded-lg transition cursor-pointer"
title="Inspect exception details"
>
<AlertTriangle className="w-3 h-3 text-rose-400" />
Inspect Error
</button>
) : (
<button
type="button"
onClick={(e) => {
e.stopPropagation();
setSelectedAuditLog(log);
setIsAuditDetailModalOpen(true);
}}
className="inline-flex items-center gap-1 px-2.5 py-1 text-[11px] font-semibold text-slate-300 bg-slate-800 hover:bg-slate-700 border border-slate-700 rounded-lg transition cursor-pointer"
>
<Eye className="w-3 h-3 text-blue-400" />
Inspect
</button>
)}
</td>
</tr>
@@ -1771,11 +1882,21 @@ export const SettingsPage: React.FC = () => {
<div className="rounded-xl border border-slate-800/90 bg-[#070a12]/90 p-4 space-y-3 shadow-inner">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-2 border-b border-slate-800/80 pb-3 flex-wrap">
<div className="flex items-center gap-2">
<Layers className="w-4 h-4 text-blue-400 shrink-0" />
{isException ? (
<AlertOctagon className="w-4 h-4 text-rose-400 shrink-0" />
) : (
<Layers className="w-4 h-4 text-blue-400 shrink-0" />
)}
<h4 className="text-xs font-bold text-white uppercase tracking-wider">
Recorded Changes Content (Event #{log.id})
{isException
? `Captured Exception & Invocation Payload (Event #${log.id})`
: `Recorded Changes Content (Event #${log.id})`}
</h4>
<span className="text-[10px] font-mono px-2 py-0.5 rounded-full bg-blue-500/10 text-blue-400 border border-blue-500/20 font-semibold shrink-0">
<span className={`text-[10px] font-mono px-2 py-0.5 rounded-full font-semibold shrink-0 ${
isException
? 'bg-rose-500/10 text-rose-400 border border-rose-500/20'
: 'bg-blue-500/10 text-blue-400 border border-blue-500/20'
}`}>
{changesCount} {changesCount === 1 ? 'Entry' : 'Entries'}
</span>
</div>
@@ -1804,6 +1925,32 @@ export const SettingsPage: React.FC = () => {
</div>
</div>
{/* Exception Diagnostic Banner inside expanded shelf */}
{log.error && (
<div className="p-3 bg-rose-950/40 border border-rose-800/60 rounded-xl space-y-1.5">
<div className="flex items-center justify-between">
<span className="text-[11px] font-bold text-rose-300 uppercase tracking-wider flex items-center gap-1.5">
<AlertCircle className="w-3.5 h-3.5 text-rose-400" />
Captured Exception Diagnostic Trace
</span>
<button
type="button"
onClick={(e) => {
e.stopPropagation();
navigator.clipboard.writeText(log.error || '');
success('Copied', 'Exception details copied to clipboard.');
}}
className="text-[10px] text-rose-300 hover:text-white bg-rose-900/50 hover:bg-rose-900 px-2 py-0.5 rounded border border-rose-700/60 transition cursor-pointer flex items-center gap-1"
>
<Copy className="w-3 h-3" /> Copy Details
</button>
</div>
<pre className="text-xs font-mono text-rose-200 whitespace-pre-wrap break-all bg-black/40 p-2.5 rounded-lg border border-rose-900/40 max-h-40 overflow-y-auto leading-relaxed">
{log.error}
</pre>
</div>
)}
{/* Changes List */}
{Array.isArray(log.changes) && log.changes.length > 0 ? (
<div className="space-y-2">
@@ -1823,7 +1970,7 @@ export const SettingsPage: React.FC = () => {
</div>
) : (
<div className="p-4 rounded-xl bg-slate-900/50 border border-slate-800/80 text-center text-slate-400 text-xs font-mono">
No itemized field changes content recorded for this event.
No itemized field changes or invocation arguments recorded for this event.
</div>
)}
</div>
@@ -1837,6 +1984,64 @@ export const SettingsPage: React.FC = () => {
</tbody>
</table>
</div>
{/* Pagination Controls */}
{totalAuditRecords > 0 && (
<div className="flex flex-col sm:flex-row items-center justify-between gap-3 px-5 py-3 border-t border-slate-800/80 bg-slate-900/40 text-xs text-slate-400">
<div className="flex items-center gap-2">
<span>
Showing <strong className="text-white font-mono">{Math.min((auditPage - 1) * auditPageSize + 1, totalAuditRecords)}</strong> to{' '}
<strong className="text-white font-mono">{Math.min(auditPage * auditPageSize, totalAuditRecords)}</strong> of{' '}
<strong className="text-white font-mono">{totalAuditRecords}</strong> records
</span>
</div>
<div className="flex items-center gap-3">
<div className="flex items-center gap-1.5">
<span className="text-slate-400">Per page:</span>
<select
value={auditPageSize}
onChange={(e) => {
setAuditPageSize(Number(e.target.value));
setAuditPage(1);
}}
className="bg-slate-900 border border-slate-700/80 rounded-lg px-2 py-1 text-xs text-white focus:outline-none focus:ring-1 focus:ring-blue-500 cursor-pointer"
>
<option value={10}>10</option>
<option value={25}>25</option>
<option value={50}>50</option>
<option value={100}>100</option>
</select>
</div>
<div className="flex items-center gap-1">
<button
type="button"
onClick={() => setAuditPage((p) => Math.max(1, p - 1))}
disabled={auditPage <= 1}
className="p-1 rounded-lg border border-slate-800 bg-slate-900 text-slate-300 hover:bg-slate-800 disabled:opacity-40 disabled:cursor-not-allowed transition-colors cursor-pointer"
title="Previous page"
>
<ChevronLeft className="w-4 h-4" />
</button>
<span className="px-2 font-mono text-slate-300">
{auditPage} / {totalAuditPages}
</span>
<button
type="button"
onClick={() => setAuditPage((p) => Math.min(totalAuditPages, p + 1))}
disabled={auditPage >= totalAuditPages}
className="p-1 rounded-lg border border-slate-800 bg-slate-900 text-slate-300 hover:bg-slate-800 disabled:opacity-40 disabled:cursor-not-allowed transition-colors cursor-pointer"
title="Next page"
>
<ChevronRight className="w-4 h-4" />
</button>
</div>
</div>
</div>
)}
</div>
</div>
)}
@@ -1990,7 +2195,195 @@ export const SettingsPage: React.FC = () => {
</Modal>
{/* ============================================================== */}
{/* Modal: Dispatch Invitation */}
{/* Modal: Exception Detail Inspection Modal */}
{/* ============================================================== */}
<Modal
isOpen={isExceptionDetailModalOpen}
onClose={() => {
setIsExceptionDetailModalOpen(false);
setSelectedExceptionLog(null);
setCopiedExceptionTrace(false);
}}
title={`Captured Exception: Event #${selectedExceptionLog?.id || ''}`}
subtitle="Runtime exception diagnostics and invocation telemetry"
maxWidth="2xl"
>
{selectedExceptionLog && (
<div className="space-y-4 text-xs min-w-0">
{/* Header Alert Box */}
<div className="p-4 bg-rose-950/30 border border-rose-500/30 rounded-xl space-y-3">
<div className="flex items-center justify-between border-b border-rose-900/50 pb-2.5 flex-wrap gap-2">
<div className="flex items-center gap-2">
<div className="w-8 h-8 rounded-lg bg-rose-500/20 border border-rose-500/30 flex items-center justify-center text-rose-400 shrink-0">
<AlertTriangle className="w-4 h-4" />
</div>
<div>
<span className="text-white font-bold text-sm block">System Exception Captured</span>
<span className="text-[11px] text-rose-300/80 font-mono">
Action: <strong className="text-white">{selectedExceptionLog.action || 'Unknown'}</strong>
</span>
</div>
</div>
<span className="inline-flex items-center gap-1 px-2.5 py-1 rounded-full text-[11px] font-bold bg-rose-500/20 text-rose-300 border border-rose-500/40">
<AlertCircle className="w-3.5 h-3.5 text-rose-400" />
FAILED EXECUTION
</span>
</div>
{/* Entity and Context Details */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 pt-1">
<div>
<span className="text-slate-400 font-semibold block text-[11px] uppercase">Entity / Target:</span>
<span className="text-white font-semibold">
{selectedExceptionLog.entityName || 'General'}{' '}
{selectedExceptionLog.entityId ? `(#${selectedExceptionLog.entityId})` : ''}
</span>
</div>
<div>
<span className="text-slate-400 font-semibold block text-[11px] uppercase">Logged Timestamp:</span>
<span className="font-mono text-slate-300">{formatDate(selectedExceptionLog.timestamp)}</span>
</div>
<div>
<span className="text-slate-400 font-semibold block text-[11px] uppercase">Triggered By:</span>
<div className="flex items-center gap-1.5 mt-0.5">
<span className="font-mono text-slate-200 text-[11px] truncate" title={selectedExceptionLog.performedBy || 'System'}>
{getUserEmail(selectedExceptionLog.performedBy) !== '—'
? getUserEmail(selectedExceptionLog.performedBy)
: (selectedExceptionLog.performedBy || 'System')}
</span>
{selectedExceptionLog.performedBy && (
<button
type="button"
onClick={(e) => handleCopyUserId(selectedExceptionLog.performedBy, e)}
className="p-1 rounded hover:bg-slate-700/80 text-slate-400 hover:text-white transition cursor-pointer"
title={`Copy User ID: ${selectedExceptionLog.performedBy}`}
>
{copiedUserId === selectedExceptionLog.performedBy ? (
<Check className="w-3.5 h-3.5 text-emerald-400" />
) : (
<Copy className="w-3.5 h-3.5 hover:text-blue-400" />
)}
</button>
)}
</div>
</div>
<div>
<span className="text-slate-400 font-semibold block text-[11px] uppercase">Tenant Scope:</span>
<span className="font-mono text-slate-300">
{selectedExceptionLog.tenantId ? `Tenant #${selectedExceptionLog.tenantId}` : '—'}
</span>
</div>
</div>
</div>
{/* Error Message & Stack Trace Section */}
<div className="space-y-2">
<div className="flex items-center justify-between">
<span className="text-slate-300 font-bold uppercase text-[11px] flex items-center gap-1.5">
<Terminal className="w-3.5 h-3.5 text-rose-400" />
Exception Diagnostic Message &amp; Trace
</span>
<button
type="button"
onClick={() => {
const textToCopy = selectedExceptionLog.error || 'No error details recorded.';
navigator.clipboard.writeText(textToCopy);
setCopiedExceptionTrace(true);
success('Exception Copied', 'Exception trace copied to clipboard.');
setTimeout(() => setCopiedExceptionTrace(false), 2000);
}}
className="inline-flex items-center gap-1 px-2.5 py-1 rounded-lg bg-slate-800 hover:bg-slate-700 text-slate-300 hover:text-white border border-slate-700 text-[11px] transition cursor-pointer"
>
{copiedExceptionTrace ? (
<>
<Check className="w-3 h-3 text-emerald-400" />
<span>Copied</span>
</>
) : (
<>
<Copy className="w-3 h-3 text-blue-400" />
<span>Copy Trace</span>
</>
)}
</button>
</div>
<div className="bg-[#050810] border border-rose-900/50 rounded-xl p-4 overflow-hidden shadow-inner">
<pre className="font-mono text-rose-400 text-xs whitespace-pre-wrap break-all max-h-72 overflow-y-auto leading-relaxed selection:bg-rose-900 selection:text-white">
{selectedExceptionLog.error || 'No stack trace or error message captured.'}
</pre>
</div>
</div>
{/* Invocation Arguments / Parameters */}
<div className="space-y-2">
<span className="text-slate-300 font-bold uppercase text-[11px] flex items-center gap-1.5">
<Layers className="w-3.5 h-3.5 text-blue-400" />
Invocation Arguments &amp; Parameters
</span>
{Array.isArray(selectedExceptionLog.changes) && selectedExceptionLog.changes.length > 0 ? (
<div className="space-y-1.5 max-h-48 overflow-y-auto pr-1">
{selectedExceptionLog.changes.map((param, idx) => (
<div
key={idx}
className="flex items-start gap-2.5 p-2.5 rounded-lg bg-slate-900/90 border border-slate-800 text-xs font-mono"
>
<span className="px-1.5 py-0.5 rounded bg-blue-500/10 text-blue-400 border border-blue-500/20 text-[10px] font-bold shrink-0 mt-0.5">
#{idx + 1}
</span>
<span className="break-all text-slate-300 flex-1 leading-relaxed">
{param}
</span>
</div>
))}
</div>
) : (
<div className="p-3 rounded-lg bg-slate-900 border border-slate-800 text-center text-slate-500 font-mono text-[11px]">
No parameters or arguments recorded for this invocation.
</div>
)}
</div>
{/* Raw JSON Data Viewer */}
<div className="border-t border-slate-800/80 pt-2.5 space-y-1.5 min-w-0">
<div className="flex items-center justify-between">
<span className="text-slate-400 font-semibold text-[11px] uppercase">
Raw Exception Record (JSON):
</span>
<button
type="button"
onClick={() => {
navigator.clipboard.writeText(JSON.stringify(selectedExceptionLog, null, 2));
success('Copied', 'JSON payload copied to clipboard.');
}}
className="inline-flex items-center gap-1 text-[10px] font-medium text-slate-400 hover:text-white bg-slate-800 px-2 py-1 rounded transition cursor-pointer"
>
<Copy className="w-3 h-3" />
<span>Copy JSON</span>
</button>
</div>
<pre className="p-3 rounded-lg bg-[#070a12] border border-slate-800/80 text-rose-300/90 font-mono text-[10.5px] overflow-x-auto whitespace-pre-wrap break-all max-h-40 min-w-0">
{JSON.stringify(selectedExceptionLog, null, 2)}
</pre>
</div>
{/* Modal Footer */}
<div className="flex items-center justify-end pt-3 border-t border-slate-800 gap-2">
<button
type="button"
onClick={() => {
setIsExceptionDetailModalOpen(false);
setSelectedExceptionLog(null);
}}
className="px-4 py-2 text-xs font-semibold text-slate-300 bg-slate-800 hover:bg-slate-700 border border-slate-700 rounded-xl transition cursor-pointer"
>
Close
</button>
</div>
</div>
)}
</Modal>
{/* ============================================================== */}
<Modal
isOpen={isSendInviteModalOpen}
@@ -346,6 +346,8 @@ export interface AuditLog {
changes?: string[] | null;
performedBy: string;
timestamp: string;
error?: string | null;
isError?: boolean;
}
export interface UserFilterRequest {