/** * Component: Admin Dashboard Page * Documentation: documentation/admin-dashboard.md */ 'use client'; import useSWR, { mutate } from 'swr'; import Link from 'next/link'; import { authenticatedFetcher, fetchJSON } from '@/lib/utils/api'; import { MetricCard } from './components/MetricCard'; import { ActiveDownloadsTable } from './components/ActiveDownloadsTable'; import { RecentRequestsTable } from './components/RecentRequestsTable'; import { ToastProvider, useToast } from '@/components/ui/Toast'; import { ReportedIssuesSection } from './components/ReportedIssuesSection'; import { InteractiveTorrentSearchModal } from '@/components/requests/InteractiveTorrentSearchModal'; import { AudiobookDetailsModal } from '@/components/audiobooks/AudiobookDetailsModal'; import { BulkImportWizard } from '@/components/admin/BulkImportWizard'; import { TorrentResult } from '@/lib/utils/ranking-algorithm'; import { InformationCircleIcon } from '@heroicons/react/24/outline'; import { formatDistanceToNow } from 'date-fns'; import { useState } from 'react'; interface SelectedTorrentData { title?: string; indexer?: string; size?: number; format?: string; ebookFormat?: string; seeders?: number; infoUrl?: string; source?: string; protocol?: string; score?: number; } interface PendingApprovalRequest { id: string; createdAt: string; type: 'audiobook' | 'ebook'; selectedTorrent: SelectedTorrentData | null; audiobook: { title: string; author: string; coverArtUrl: string | null; audibleAsin: string | null; }; user: { id: string; plexUsername: string; avatarUrl: string | null; }; } function formatTorrentSize(bytes: number): string { const gb = bytes / (1024 ** 3); const mb = bytes / (1024 ** 2); return gb >= 1 ? `${gb.toFixed(1)} GB` : `${mb.toFixed(0)} MB`; } function LoadingSpinner() { return ( ); } interface ApprovalActionButtonsProps { isLoading: boolean; onApprove: () => void; onSearch: () => void; onDeny: () => void; } function ApprovalActionButtons({ isLoading, onApprove, onSearch, onDeny }: ApprovalActionButtonsProps) { return ( <> ); } function PendingApprovalSection({ requests }: { requests: PendingApprovalRequest[] }) { const toast = useToast(); const [loadingStates, setLoadingStates] = useState>({}); const [searchModalRequestId, setSearchModalRequestId] = useState(null); const [detailsAsin, setDetailsAsin] = useState(null); const [detailsRequestId, setDetailsRequestId] = useState(null); const searchModalRequest = searchModalRequestId ? requests.find((r) => r.id === searchModalRequestId) : null; const detailsRequest = detailsRequestId ? requests.find((r) => r.id === detailsRequestId) : null; const handleApproveRequest = async (requestId: string) => { setLoadingStates((prev) => ({ ...prev, [requestId]: true })); try { await fetchJSON(`/api/admin/requests/${requestId}/approve`, { method: 'POST', body: JSON.stringify({ action: 'approve' }), }); toast.success('Request approved'); await mutate('/api/admin/requests/pending-approval'); await mutate('/api/admin/requests/recent'); await mutate('/api/admin/metrics'); } catch (error) { console.error('[Admin] Failed to approve request:', error); toast.error( `Failed to approve request: ${error instanceof Error ? error.message : 'Unknown error'}` ); } finally { setLoadingStates((prev) => ({ ...prev, [requestId]: false })); } }; const handleDenyRequest = async (requestId: string) => { setLoadingStates((prev) => ({ ...prev, [requestId]: true })); try { await fetchJSON(`/api/admin/requests/${requestId}/approve`, { method: 'POST', body: JSON.stringify({ action: 'deny' }), }); toast.success('Request denied'); await mutate('/api/admin/requests/pending-approval'); await mutate('/api/admin/metrics'); } catch (error) { console.error('[Admin] Failed to deny request:', error); toast.error( `Failed to deny request: ${error instanceof Error ? error.message : 'Unknown error'}` ); } finally { setLoadingStates((prev) => ({ ...prev, [requestId]: false })); } }; const handleApproveWithTorrent = async (requestId: string, torrent: TorrentResult) => { await fetchJSON(`/api/admin/requests/${requestId}/approve`, { method: 'POST', body: JSON.stringify({ action: 'approve', selectedTorrent: torrent }), }); toast.success('Request approved and download started'); await mutate('/api/admin/requests/pending-approval'); await mutate('/api/admin/requests/recent'); await mutate('/api/admin/metrics'); }; return (
{/* Section Header */}

Requests Awaiting Approval

{requests.length}
{/* Requests Grid */}
{requests.map((request) => { const isLoading = loadingStates[request.id] || false; const torrent = request.selectedTorrent; const displayFormat = torrent?.format || torrent?.ebookFormat; const isAnnasArchive = torrent?.source === 'annas_archive'; return (
{/* Info Button — opens AudiobookDetailsModal */} {request.audiobook.audibleAsin && ( )} {/* Card Content */}
{/* Cover Image */}
{/* eslint-disable-next-line @next/next/no-img-element */} {request.audiobook.title} { (e.target as HTMLImageElement).src = '/placeholder_cover.svg'; }} />
{/* Book Info */}

{request.audiobook.title}

{request.type === 'ebook' && ( Ebook )}

{request.audiobook.author}

{/* User Info */}
{request.user.avatarUrl ? ( {request.user.plexUsername} ) : (
)} {request.user.plexUsername}
{/* Timestamp */}

{formatDistanceToNow(new Date(request.createdAt), { addSuffix: true })}

{/* Pre-Selected Release */} {torrent && torrent.title && (
User-Selected Release
{torrent.infoUrl ? ( {torrent.title} ) : (

{torrent.title}

)}
{isAnnasArchive ? ( Anna's Archive ) : torrent.indexer ? ( {torrent.indexer} ) : null} {torrent.size && torrent.size > 0 ? ( <> · {formatTorrentSize(torrent.size)} ) : null} {displayFormat ? ( <> · {displayFormat} ) : null} {torrent.protocol === 'usenet' ? ( <> · NZB ) : torrent.seeders !== undefined && torrent.seeders !== null ? ( <> · {torrent.seeders} seeds ) : null} {torrent.score !== undefined && torrent.score !== null ? ( <> · Score {Math.round(torrent.score)} ) : null}
)} {/* Action Buttons */}
handleApproveRequest(request.id)} onSearch={() => setSearchModalRequestId(request.id)} onDeny={() => handleDenyRequest(request.id)} />
); })}
{/* Interactive Search Modal */} {searchModalRequest && ( setSearchModalRequestId(null)} requestId={searchModalRequest.id} audiobook={{ title: searchModalRequest.audiobook.title, author: searchModalRequest.audiobook.author, }} searchMode={searchModalRequest.type === 'ebook' ? 'ebook' : 'audiobook'} onConfirm={async (torrent) => { await handleApproveWithTorrent(searchModalRequest.id, torrent); }} onSuccess={() => { setSearchModalRequestId(null); }} /> )} {/* Book Details Modal — opened via info button on each approval card */} {detailsAsin && detailsRequestId && ( { setDetailsAsin(null); setDetailsRequestId(null); }} requestStatus="awaiting_approval" requestedByUsername={detailsRequest?.user.plexUsername ?? null} adminActions={ { await handleApproveRequest(detailsRequestId); setDetailsAsin(null); setDetailsRequestId(null); }} onSearch={() => { setSearchModalRequestId(detailsRequestId); setDetailsAsin(null); setDetailsRequestId(null); }} onDeny={async () => { await handleDenyRequest(detailsRequestId); setDetailsAsin(null); setDetailsRequestId(null); }} /> } /> )}
); } function AdminDashboardContent() { const [isBulkImportOpen, setIsBulkImportOpen] = useState(false); // Fetch data with auto-refresh every 10 seconds const { data: metrics, error: metricsError } = useSWR( '/api/admin/metrics', authenticatedFetcher, { refreshInterval: 10000, } ); const { data: downloadsData, error: downloadsError } = useSWR( '/api/admin/downloads/active', authenticatedFetcher, { refreshInterval: 5000, // Refresh downloads more frequently } ); // Note: RecentRequestsTable now fetches its own data with filtering/pagination const { data: pendingApprovalData } = useSWR( '/api/admin/requests/pending-approval', authenticatedFetcher, { refreshInterval: 10000, } ); const { data: reportedIssuesData } = useSWR( '/api/admin/reported-issues', authenticatedFetcher, { refreshInterval: 10000, } ); const { data: settingsData } = useSWR( '/api/admin/settings', authenticatedFetcher, { refreshInterval: 60000, // Settings change infrequently } ); const isLoading = !metrics || !downloadsData; const hasError = metricsError || downloadsError; if (hasError) { return (

Error Loading Dashboard

{metricsError?.message || downloadsError?.message || 'Failed to load dashboard data'}

); } return (
{/* Header */}

Admin Dashboard

Monitor system health, active downloads, and recent requests

Back to Home Home
{isLoading ? (
) : ( <> {/* Metrics Grid */}
} variant="default" /> } variant={metrics.activeDownloads > 0 ? 'info' : 'default'} /> } variant="success" /> } variant={metrics.failedLast30Days > 0 ? 'error' : 'default'} /> } variant="default" /> } variant={ metrics.systemHealth.status === 'healthy' ? 'success' : metrics.systemHealth.status === 'degraded' ? 'warning' : 'error' } subtitle={ metrics.systemHealth.issues.length > 0 ? metrics.systemHealth.issues.join(', ') : 'All systems operational' } />
{/* Quick Actions */}
Settings
Users
Scheduled Jobs
System Logs
Blocklist
{/* Bulk Import Wizard Modal */} setIsBulkImportOpen(false)} /> {/* Requests Awaiting Approval */} {pendingApprovalData?.requests && pendingApprovalData.requests.length > 0 && ( )} {/* Reported Issues */} {reportedIssuesData?.issues && reportedIssuesData.issues.length > 0 && ( )} {/* Active Downloads */}

Active Downloads

{/* Request Management */}

Request Management

)}
); } export default function AdminDashboard() { return ( ); }