'use client'; import { useEffect, useMemo, useState } from 'react'; import { type AuditTrailEntry, seedAuditTrailEntries } from '../lib/audit-trail'; function formatDate(value: string) { const date = new Date(value); return { day: date.toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }), time: date.toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false }), }; } function downloadCsv(rows: AuditTrailEntry[]) { const header = ['Timestamp', 'Admin User', 'Action Type', 'Module', 'IP Address', 'Severity', 'Details']; const csvRows = rows.map((row) => [ row.timestamp, row.adminUser, row.actionType, row.module, row.ipAddress, row.severity, row.details, ] .map((cell) => `"${String(cell).replaceAll('"', '""')}"`) .join(','), ); const blob = new Blob([[header.join(','), ...csvRows].join('\n')], { type: 'text/csv;charset=utf-8;' }); const url = URL.createObjectURL(blob); const anchor = document.createElement('a'); anchor.href = url; anchor.download = 'audit-trail-export.csv'; anchor.click(); URL.revokeObjectURL(url); } type Props = { initialEntries: AuditTrailEntry[]; initialTotal?: number; initialPage?: number; initialPageSize?: number; initialTotalPages?: number; }; const PAGE_SIZE_OPTIONS = [10, 25, 50, 100]; function buildVisiblePages(page: number, totalPages: number) { if (totalPages <= 5) { return Array.from({ length: totalPages }, (_, index) => index + 1); } const start = Math.max(1, Math.min(page - 2, totalPages - 4)); return Array.from({ length: 5 }, (_, index) => start + index); } export function AuditTrailBoard({ initialEntries, initialTotal, initialPage, initialPageSize, initialTotalPages, }: Props) { const [allEntries] = useState(initialEntries.length > 0 ? initialEntries : seedAuditTrailEntries); const [entries, setEntries] = useState(initialEntries.length > 0 ? initialEntries : seedAuditTrailEntries); const [total, setTotal] = useState(initialTotal ?? initialEntries.length); const [page, setPage] = useState(initialPage ?? 1); const [pageSize, setPageSize] = useState(initialPageSize ?? 50); const [totalPages, setTotalPages] = useState(initialTotalPages ?? 1); const [range, setRange] = useState('7d'); const [adminUser, setAdminUser] = useState('all'); const [actionType, setActionType] = useState('all'); const [moduleName, setModuleName] = useState('all'); const [search, setSearch] = useState(''); const [selectedEntryId, setSelectedEntryId] = useState(null); const [isLoading, setIsLoading] = useState(false); useEffect(() => { setSelectedEntryId(initialEntries[0]?.id ?? seedAuditTrailEntries[0]?.id ?? null); }, [initialEntries]); useEffect(() => { const controller = new AbortController(); const timeout = window.setTimeout(async () => { try { setIsLoading(true); const params = new URLSearchParams(); params.set('page', String(page)); params.set('limit', String(pageSize)); if (range !== 'all') params.set('range', range); if (adminUser !== 'all') params.set('user', adminUser); if (actionType !== 'all') params.set('actionType', actionType); if (moduleName !== 'all') params.set('module', moduleName); if (search.trim()) params.set('search', search.trim()); const response = await fetch(`/api/audit-trail?${params.toString()}`, { method: 'GET', signal: controller.signal, cache: 'no-store', }); if (!response.ok) { return; } const payload = (await response.json()) as { items: Array<{ id: string; actorName: string; actionType: string; module: string; ipAddress: string | null; severity: 'default' | 'alert'; details: string; createdAt: string; }>; total: number; page: number; pageSize: number; totalPages: number; }; const normalized = payload.items.map((entry) => ({ id: entry.id, timestamp: entry.createdAt, adminUser: entry.actorName, actionType: entry.actionType, module: entry.module, ipAddress: entry.ipAddress || '-', severity: entry.severity, details: entry.details, })); setEntries(normalized); setTotal(payload.total); setPage(payload.page); setPageSize(payload.pageSize); setTotalPages(payload.totalPages); } catch (error) { if ((error as Error).name !== 'AbortError') { console.error(error); } } finally { setIsLoading(false); } }, 250); return () => { controller.abort(); window.clearTimeout(timeout); }; }, [actionType, adminUser, moduleName, page, pageSize, range, search]); const selectedEntry = entries.find((entry) => entry.id === selectedEntryId) ?? entries[0] ?? null; useEffect(() => { if (!selectedEntryId && entries[0]?.id) { setSelectedEntryId(entries[0].id); return; } if (selectedEntryId && !entries.some((entry) => entry.id === selectedEntryId)) { setSelectedEntryId(entries[0]?.id ?? null); } }, [entries, selectedEntryId]); const users = Array.from(new Set(allEntries.map((entry) => entry.adminUser))); const actions = Array.from(new Set(allEntries.map((entry) => entry.actionType))); const modules = Array.from(new Set(allEntries.map((entry) => entry.module))); const alertsCount = allEntries.filter((entry) => entry.severity === 'alert').length; const mostActiveAdmin = users .map((user) => ({ user, count: allEntries.filter((entry) => entry.adminUser === user).length, })) .sort((a, b) => b.count - a.count)[0] ?? { user: 'Admin User', count: 0 }; const visiblePages = useMemo(() => buildVisiblePages(page, totalPages), [page, totalPages]); const pageStart = entries.length === 0 ? 0 : (page - 1) * pageSize + 1; const pageEnd = entries.length === 0 ? 0 : (page - 1) * pageSize + entries.length; return ( <>

Settings

Audit Trail

Monitor administrative actions, role changes, and system modifications from one place.

download Export Server CSV
Total Actions (Last 24H) history
{entries.length.toLocaleString('en-US')} 12.5%
Security Alerts security
{alertsCount.toString().padStart(2, '0')} Critical

Failed login bursts and suspicious access activity are surfaced here.

Most Active Admin person
{mostActiveAdmin.user.slice(0, 1)}
{mostActiveAdmin.user} {mostActiveAdmin.count} actions performed
filter_list Filters
{isLoading ?
: null} {entries.map((entry) => { const stamp = formatDate(entry.timestamp); const isSelected = entry.id === selectedEntry?.id; return ( ); })}
Timestamp Admin User Action Type Module IP Address Actions
{stamp.day} {stamp.time}
{entry.adminUser.slice(0, 1)}
{entry.adminUser}
{entry.actionType} {entry.module} {entry.ipAddress}
Showing {pageStart} to {pageEnd} of {total} results
{visiblePages.map((pageNumber) => ( ))} {visiblePages[visiblePages.length - 1] < totalPages ? ... : null} {visiblePages[visiblePages.length - 1] < totalPages ? ( ) : null}
); }