'use client' export interface AuditLogRow { id: number | string action: string created_at: string details: Record | null ip_address?: string | null slide_id?: string | null investor_email?: string | null investor_name?: string | null target_investor_email?: string | null target_investor_name?: string | null admin_email?: string | null admin_name?: string | null } interface AuditLogTableProps { rows: AuditLogRow[] showActor?: boolean } const ACTION_COLORS: Record = { login_success: 'text-green-400 bg-green-500/10', login_failed: 'text-rose-400 bg-rose-500/10', admin_login_success: 'text-green-400 bg-green-500/10', admin_login_failed: 'text-rose-400 bg-rose-500/10', admin_logout: 'text-white/40 bg-white/[0.04]', logout: 'text-white/40 bg-white/[0.04]', slide_viewed: 'text-indigo-400 bg-indigo-500/10', assumption_changed: 'text-amber-400 bg-amber-500/10', assumption_edited: 'text-amber-400 bg-amber-500/10', scenario_edited: 'text-amber-400 bg-amber-500/10', investor_invited: 'text-purple-400 bg-purple-500/10', magic_link_resent: 'text-purple-400 bg-purple-500/10', investor_revoked: 'text-rose-400 bg-rose-500/10', investor_edited: 'text-blue-400 bg-blue-500/10', admin_created: 'text-green-400 bg-green-500/10', admin_edited: 'text-blue-400 bg-blue-500/10', admin_deactivated: 'text-rose-400 bg-rose-500/10', new_ip_detected: 'text-amber-400 bg-amber-500/10', } function actorLabel(row: AuditLogRow): { label: string; sub: string; kind: 'admin' | 'investor' | 'system' } { if (row.admin_email) { return { label: row.admin_name || row.admin_email, sub: row.admin_email, kind: 'admin' } } if (row.investor_email) { return { label: row.investor_name || row.investor_email, sub: row.investor_email, kind: 'investor' } } return { label: 'system', sub: '', kind: 'system' } } function targetLabel(row: AuditLogRow): string | null { if (row.target_investor_email) { return row.target_investor_name ? `${row.target_investor_name} <${row.target_investor_email}>` : row.target_investor_email } return null } function formatDate(iso: string): string { return new Date(iso).toLocaleString() } function summarizeDetails(action: string, details: Record | null): string { if (!details) return '' if (action === 'slide_viewed' && details.slide_id) return String(details.slide_id) if (action === 'assumption_edited' || action === 'scenario_edited') { const before = details.before as Record | undefined const after = details.after as Record | undefined if (before && after) { const keys = Object.keys(after).filter(k => JSON.stringify(before[k]) !== JSON.stringify(after[k])) return keys.map(k => `${k}: ${JSON.stringify(before[k])} → ${JSON.stringify(after[k])}`).join(', ') } } if (action === 'investor_invited' || action === 'magic_link_resent') { return String(details.email || '') } if (action === 'investor_edited') { const before = details.before as Record | undefined const after = details.after as Record | undefined if (before && after) { const keys = Object.keys(after).filter(k => before[k] !== after[k]) return keys.map(k => `${k}: "${before[k] || ''}" → "${after[k] || ''}"`).join(', ') } } return JSON.stringify(details).slice(0, 80) } export default function AuditLogTable({ rows, showActor = true }: AuditLogTableProps) { if (rows.length === 0) { return
No audit events
} return (
{showActor && } {rows.map((row) => { const actor = actorLabel(row) const target = targetLabel(row) const summary = summarizeDetails(row.action, row.details) const colorClass = ACTION_COLORS[row.action] || 'text-white/60 bg-white/[0.04]' return ( {showActor && ( )} ) })}
WhenActorAction Target / Details IP
{formatDate(row.created_at)}
{actor.kind}
{actor.label}
{row.action} {target &&
→ {target}
} {summary &&
{summary}
}
{row.ip_address || '—'}
) }