/** * Component: Admin System Logs Page * Documentation: documentation/admin-dashboard.md */ 'use client'; import { useState } from 'react'; import useSWR from 'swr'; import Link from 'next/link'; import { authenticatedFetcher } from '@/lib/utils/api'; interface JobEvent { id: string; level: string; context: string; message: string; metadata: any; createdAt: string; } interface Log { id: string; bullJobId: string | null; type: string; status: string; priority: number; attempts: number; maxAttempts: number; errorMessage: string | null; startedAt: string | null; completedAt: string | null; createdAt: string; updatedAt: string; result: any; events: JobEvent[]; request: { id: string; audiobook: { title: string; author: string; } | null; user: { plexUsername: string; }; } | null; } interface LogsData { logs: Log[]; pagination: { page: number; limit: number; total: number; totalPages: number; }; } function StatusBadge({ status }: { status: string }) { const config: Record = { completed: { dot: 'bg-emerald-500', text: 'text-emerald-700 dark:text-emerald-400', bg: 'bg-emerald-500/10' }, failed: { dot: 'bg-red-500', text: 'text-red-700 dark:text-red-400', bg: 'bg-red-500/10' }, active: { dot: 'bg-blue-500', text: 'text-blue-700 dark:text-blue-400', bg: 'bg-blue-500/10' }, pending: { dot: 'bg-amber-500', text: 'text-amber-700 dark:text-amber-400', bg: 'bg-amber-500/10' }, delayed: { dot: 'bg-orange-500', text: 'text-orange-700 dark:text-orange-400', bg: 'bg-orange-500/10' }, stuck: { dot: 'bg-purple-500', text: 'text-purple-700 dark:text-purple-400', bg: 'bg-purple-500/10' }, }; const c = config[status] ?? { dot: 'bg-gray-400', text: 'text-gray-600 dark:text-gray-400', bg: 'bg-gray-500/10' }; return ( {status.charAt(0).toUpperCase() + status.slice(1)} ); } function LogDetails({ log }: { log: Log }) { return (
{log.bullJobId && (
Bull Job ID: {log.bullJobId}
)} {log.events.length > 0 && (

Event Log

{log.events.map((event) => { const timestamp = new Date(event.createdAt).toISOString().split('T')[1].split('.')[0]; const levelColor = event.level === 'error' ? 'text-red-400' : event.level === 'warn' ? 'text-amber-400' : 'text-emerald-400'; return (
[{event.context}] {' '} {event.message} {timestamp} {event.metadata && Object.keys(event.metadata).length > 0 && (
                      {JSON.stringify(event.metadata, null, 2)}
                    
)}
); })}
)} {log.result && Object.keys(log.result).length > 0 && (

Job Result

            {JSON.stringify(log.result, null, 2)}
          
)} {log.errorMessage && (

Error

{log.errorMessage}
)}
); } function formatDuration(startedAt: string | null, completedAt: string | null) { if (!startedAt) return 'N/A'; if (!completedAt) return 'Running…'; const durationMs = new Date(completedAt).getTime() - new Date(startedAt).getTime(); const seconds = Math.floor(durationMs / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); if (hours > 0) return `${hours}h ${minutes % 60}m`; if (minutes > 0) return `${minutes}m ${seconds % 60}s`; return `${seconds}s`; } function formatType(type: string) { return type.replace(/_/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase()); } function formatDateShort(dateStr: string) { const d = new Date(dateStr); const now = new Date(); const isToday = d.toDateString() === now.toDateString(); if (isToday) { return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' }); } return d.toLocaleDateString([], { month: 'short', day: 'numeric' }) + ' ' + d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); } export default function AdminLogsPage() { const [page, setPage] = useState(1); const [statusFilter, setStatusFilter] = useState('all'); const [typeFilter, setTypeFilter] = useState('all'); const [expandedLog, setExpandedLog] = useState(null); const { data, error } = useSWR( `/api/admin/logs?page=${page}&limit=50&status=${statusFilter}&type=${typeFilter}`, authenticatedFetcher, { refreshInterval: 10000 } ); const isLoading = !data && !error; if (isLoading) { return (
); } if (error) { return (

Error Loading Logs

{error?.message || 'Failed to load system logs'}

); } const logs = data?.logs || []; const pagination = data?.pagination; const hasDetails = (log: Log) => log.events.length > 0 || !!log.errorMessage || !!log.bullJobId || (log.result && Object.keys(log.result).length > 0); return (
{/* Header — stacks on mobile, row on sm+ */}

System Logs

View background jobs and system activity

Back to Dashboard
{/* Filters — full-width stacked on mobile */}
{/* Mobile card list — hidden on sm+ */}
{logs.map((log) => (
{/* Card header */}
{formatType(log.type)}
{/* Related item */} {log.request?.audiobook ? (
{log.request.audiobook.title}
by {log.request.audiobook.author} · {log.request.user.plexUsername}
) : (
System job
)} {/* Meta row */}
{formatDateShort(log.createdAt)} Duration: {formatDuration(log.startedAt, log.completedAt)} Attempts: {log.attempts}/{log.maxAttempts}
{/* Expandable details */} {hasDetails(log) && ( <> {expandedLog === log.id && (
)} )}
))} {logs.length === 0 && (

No logs found

)}
{/* Desktop table — hidden on mobile */}
{logs.map((log) => ( <> {expandedLog === log.id && ( )} ))}
Time Type Status Related Item Duration Attempts Actions
{new Date(log.createdAt).toLocaleString()}
{formatType(log.type)}
{log.request?.audiobook ? (
{log.request.audiobook.title}
by {log.request.audiobook.author}
User: {log.request.user.plexUsername}
) : ( System job )}
{formatDuration(log.startedAt, log.completedAt)} {log.attempts}/{log.maxAttempts} {hasDetails(log) && ( )}
{logs.length === 0 && (

No logs found

)}
{/* Pagination */} {pagination && pagination.totalPages > 1 && (
Page {pagination.page} of {pagination.totalPages} ({pagination.total} total logs)
)} {/* Info Box */}

About System Logs

  • • Logs are automatically refreshed every 10 seconds
  • • Tap "Show Details" to view event logs, job results, and errors
  • • Event logs show all internal operations with timestamps
  • • Jobs are retried automatically based on their max attempts setting
  • • Use filters to find specific job types or statuses
); }