import { useState, useEffect, useCallback, Fragment } from 'react'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Badge } from '@/components/ui/badge'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'; import { ChevronLeft, ChevronRight, Search, ScrollText, RefreshCw, Download, ChevronDown } from 'lucide-react'; import { apiFetch } from '@/lib/api'; import { toast } from '@/components/ui/toast-store'; interface AuditEntry { id: number; timestamp: number; username: string; method: string; path: string; status_code: number; node_id: number | null; ip_address: string; summary: string; } export function AuditLogView() { const [entries, setEntries] = useState([]); const [total, setTotal] = useState(0); const [page, setPage] = useState(1); const [loading, setLoading] = useState(true); const [searchFilter, setSearchFilter] = useState(''); const [methodFilter, setMethodFilter] = useState('all'); const [fromDate, setFromDate] = useState(''); const [toDate, setToDate] = useState(''); const [expandedId, setExpandedId] = useState(null); const limit = 50; const buildFilterParams = useCallback(() => { const params = new URLSearchParams(); if (searchFilter) params.set('search', searchFilter); if (methodFilter !== 'all') params.set('method', methodFilter); if (fromDate) params.set('from', String(new Date(fromDate).getTime())); if (toDate) { const end = new Date(toDate); end.setHours(23, 59, 59, 999); params.set('to', String(end.getTime())); } return params; }, [searchFilter, methodFilter, fromDate, toDate]); const fetchLogs = useCallback(async () => { setLoading(true); try { const params = buildFilterParams(); params.set('page', String(page)); params.set('limit', String(limit)); const res = await apiFetch(`/audit-log?${params}`, { localOnly: true }); if (res.ok) { const data = await res.json(); setEntries(data.entries); setTotal(data.total); } } catch { // Silently fail - non-critical view } finally { setLoading(false); } }, [page, buildFilterParams]); useEffect(() => { fetchLogs(); }, [fetchLogs]); const totalPages = Math.max(1, Math.ceil(total / limit)); const methodBadgeVariant = (method: string): 'default' | 'secondary' | 'destructive' | 'outline' => { switch (method) { case 'POST': return 'default'; case 'PUT': case 'PATCH': return 'secondary'; case 'DELETE': return 'destructive'; default: return 'outline'; } }; const statusColor = (code: number): string => { if (code >= 200 && code < 300) return 'text-success'; if (code >= 400 && code < 500) return 'text-yellow-500'; if (code >= 500) return 'text-red-500'; return 'text-muted-foreground'; }; const handleExport = async (format: 'csv' | 'json') => { try { const params = buildFilterParams(); params.set('format', format); const res = await apiFetch(`/audit-log/export?${params}`, { localOnly: true }); if (!res.ok) { const err = await res.json().catch(() => ({})); toast.error(err?.error || err?.message || 'Export failed.'); return; } const blob = await res.blob(); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `audit-log-${new Date().toISOString().slice(0, 10)}.${format === 'csv' ? 'csv' : 'json'}`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); } catch { toast.error('Export failed.'); } }; return (
Audit Log
handleExport('csv')}>Export as CSV handleExport('json')}>Export as JSON

Track all mutating actions across your Sencho instance. {total > 0 && `${total} total entries.`}

{/* Filters */}
{ setSearchFilter(e.target.value); setPage(1); }} className="pl-8" />
{ setFromDate(e.target.value); setPage(1); }} className="w-[150px]" placeholder="From" /> { setToDate(e.target.value); setPage(1); }} className="w-[150px]" placeholder="To" />
{/* Table */}
Timestamp User Method Action Status Node {loading && entries.length === 0 ? ( Loading... ) : entries.length === 0 ? ( No audit log entries found. ) : ( entries.map((entry) => ( setExpandedId(expandedId === entry.id ? null : entry.id)} > {new Date(entry.timestamp).toLocaleString()} {entry.username} {entry.method} {entry.summary} {entry.status_code} {entry.node_id ?? '-'} {expandedId === entry.id && (
Request Path {entry.path}
IP Address {entry.ip_address || '-'}
Node ID {entry.node_id ?? 'Local'}
Entry ID #{entry.id}
)}
)) )}
{/* Pagination */} {totalPages > 1 && (

Page {page} of {totalPages}

)}
); }