Files
media-downloader/web/frontend/src/pages/Monitoring.tsx
Todd 0d7b2b1aab Initial commit
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-29 22:42:55 -04:00

351 lines
15 KiB
TypeScript

import { useState } from 'react'
import { useQuery } from '@tanstack/react-query'
import { useBreadcrumb } from '../hooks/useBreadcrumb'
import { breadcrumbConfig } from '../config/breadcrumbConfig'
import {
Activity,
AlertCircle,
CheckCircle,
Clock,
TrendingUp,
RefreshCw,
Filter,
Gauge,
} from 'lucide-react'
import { api } from '../lib/api'
import { formatRelativeTime } from '../lib/utils'
export default function Monitoring() {
useBreadcrumb(breadcrumbConfig['/monitoring'])
const [timeWindow, setTimeWindow] = useState(24)
const [selectedDownloader, setSelectedDownloader] = useState<string | null>(null)
const [historyLimit, setHistoryLimit] = useState(100)
const { data: statusData, isLoading: statusLoading, refetch: refetchStatus } = useQuery({
queryKey: ['monitoring', 'status', timeWindow],
queryFn: () => api.getMonitoringStatus(timeWindow),
refetchInterval: 30000, // Refresh every 30 seconds
})
const { data: historyData, isLoading: historyLoading, refetch: refetchHistory } = useQuery({
queryKey: ['monitoring', 'history', selectedDownloader, historyLimit],
queryFn: () => api.getMonitoringHistory(selectedDownloader, historyLimit),
refetchInterval: 60000, // Refresh every minute
})
const downloaders = statusData?.downloaders || []
const history = historyData?.history || []
// Calculate overall stats
const totalAttempts = downloaders.reduce((sum, d) => sum + d.total_attempts, 0)
const totalSuccessful = downloaders.reduce((sum, d) => sum + d.successful, 0)
const totalFailed = downloaders.reduce((sum, d) => sum + d.failed, 0)
const overallSuccessRate = totalAttempts > 0 ? (totalSuccessful / totalAttempts * 100).toFixed(1) : 0
// Get downloader display name
const getDownloaderName = (downloader: string) => {
const names: Record<string, string> = {
fastdl: 'FastDL',
imginn: 'ImgInn',
toolzu: 'Toolzu',
instagram: 'Instagram',
snapchat: 'Snapchat',
tiktok: 'TikTok',
forums: 'Forums',
coppermine: 'Coppermine'
}
return names[downloader] || downloader
}
// Get status color
const getStatusColor = (successRate: number) => {
if (successRate >= 80) return 'text-green-600 dark:text-green-400'
if (successRate >= 50) return 'text-yellow-600 dark:text-yellow-400'
return 'text-red-600 dark:text-red-400'
}
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl sm:text-3xl font-bold flex items-center gap-2 sm:gap-3">
<Gauge className="w-8 h-8 text-orange-500" />
Downloader Monitoring
</h1>
<p className="text-slate-600 dark:text-slate-400 mt-1">
Track downloader health and performance
</p>
</div>
<div className="flex items-center space-x-3">
<select
value={timeWindow}
onChange={(e) => setTimeWindow(Number(e.target.value))}
className="px-3 py-2 border border-slate-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-700 text-slate-900 dark:text-slate-100"
>
<option value={1}>Last Hour</option>
<option value={6}>Last 6 Hours</option>
<option value={24}>Last 24 Hours</option>
<option value={72}>Last 3 Days</option>
<option value={168}>Last Week</option>
</select>
<button
onClick={() => {
refetchStatus()
refetchHistory()
}}
className="p-2 bg-blue-600 hover:bg-blue-700 text-white rounded-lg transition-colors btn-hover-lift"
title="Refresh"
>
<RefreshCw className="w-5 h-5" />
</button>
</div>
</div>
{/* Overall Stats */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="card-glass-hover rounded-xl p-5 border stat-card-blue shadow-blue-glow">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">Total Attempts</p>
<p className="text-2xl font-bold text-foreground mt-1 animate-count-up">{totalAttempts}</p>
</div>
<div className="p-3 rounded-xl bg-blue-500/20">
<Activity className="w-6 h-6 text-blue-600 dark:text-blue-400" />
</div>
</div>
</div>
<div className="card-glass-hover rounded-xl p-5 border stat-card-green shadow-green-glow">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">Successful</p>
<p className="text-2xl font-bold text-emerald-600 dark:text-emerald-400 mt-1">{totalSuccessful}</p>
</div>
<div className="p-3 rounded-xl bg-emerald-500/20">
<CheckCircle className="w-6 h-6 text-emerald-600 dark:text-emerald-400" />
</div>
</div>
</div>
<div className="card-glass-hover rounded-xl p-5 border stat-card-red shadow-red-glow">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">Failed</p>
<p className="text-2xl font-bold text-red-600 dark:text-red-400 mt-1">{totalFailed}</p>
</div>
<div className="p-3 rounded-xl bg-red-500/20">
<AlertCircle className="w-6 h-6 text-red-600 dark:text-red-400" />
</div>
</div>
</div>
<div className="card-glass-hover rounded-xl p-5 border stat-card-purple shadow-purple-glow">
<div className="flex items-center justify-between">
<div>
<p className="text-sm text-muted-foreground">Success Rate</p>
<p className={`text-2xl font-bold mt-1 ${getStatusColor(Number(overallSuccessRate))}`}>
{overallSuccessRate}%
</p>
</div>
<div className="p-3 rounded-xl bg-violet-500/20">
<TrendingUp className={`w-6 h-6 ${getStatusColor(Number(overallSuccessRate))}`} />
</div>
</div>
</div>
</div>
{/* Downloader Status Cards */}
<div>
<h2 className="text-xl font-semibold text-foreground mb-4">Downloader Status</h2>
{statusLoading ? (
<div className="text-center py-8 text-muted-foreground">Loading...</div>
) : downloaders.length === 0 ? (
<div className="card-glass-hover rounded-xl p-8 text-center">
<Activity className="w-12 h-12 text-muted-foreground mx-auto mb-3" />
<p className="text-muted-foreground">No monitoring data available yet</p>
<p className="text-sm text-muted-foreground mt-1">
Data will appear after downloaders run
</p>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{downloaders.map((downloader) => (
<div
key={downloader.downloader}
className={`card-glass-hover rounded-xl p-5 cursor-pointer ${
selectedDownloader === downloader.downloader
? 'ring-2 ring-primary'
: ''
}`}
onClick={() => setSelectedDownloader(
selectedDownloader === downloader.downloader ? null : downloader.downloader
)}
>
<div className="flex items-center justify-between mb-3">
<h3 className="font-semibold text-foreground">
{getDownloaderName(downloader.downloader)}
</h3>
<span className={`text-2xl font-bold ${getStatusColor(downloader.success_rate)}`}>
{downloader.success_rate}%
</span>
</div>
<div className="space-y-2 text-sm">
<div className="flex items-center justify-between">
<span className="text-muted-foreground">Attempts</span>
<span className="font-medium text-foreground">{downloader.total_attempts}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-muted-foreground">Successful</span>
<span className="font-medium text-green-600 dark:text-green-400">{downloader.successful}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-muted-foreground">Failed</span>
<span className="font-medium text-red-600 dark:text-red-400">{downloader.failed}</span>
</div>
<div className="flex items-center justify-between">
<span className="text-muted-foreground">Files</span>
<span className="font-medium text-foreground">{downloader.total_files}</span>
</div>
</div>
<div className="mt-4 pt-4 border-t border-border">
<div className="flex items-center space-x-2 text-xs">
<Clock className="w-3 h-3 text-muted-foreground" />
<span className="text-muted-foreground">
Last: {downloader.last_attempt ? formatRelativeTime(downloader.last_attempt) : 'Never'}
</span>
</div>
{downloader.last_success && (
<div className="flex items-center space-x-2 text-xs mt-1">
<CheckCircle className="w-3 h-3 text-green-600" />
<span className="text-muted-foreground">
Success: {formatRelativeTime(downloader.last_success)}
</span>
</div>
)}
</div>
</div>
))}
</div>
)}
</div>
{/* History Table */}
<div>
<div className="flex items-center justify-between mb-4">
<h2 className="text-xl font-semibold text-foreground">Download History</h2>
<div className="flex items-center space-x-3">
{selectedDownloader && (
<button
onClick={() => setSelectedDownloader(null)}
className="text-sm text-blue-600 hover:text-blue-700 dark:text-blue-400 flex items-center space-x-1"
>
<Filter className="w-4 h-4" />
<span>Clear Filter ({getDownloaderName(selectedDownloader)})</span>
</button>
)}
<select
value={historyLimit}
onChange={(e) => setHistoryLimit(Number(e.target.value))}
className="px-3 py-1 text-sm border border-slate-300 dark:border-slate-600 rounded-lg bg-white dark:bg-slate-700 text-slate-900 dark:text-slate-100"
>
<option value={50}>Last 50</option>
<option value={100}>Last 100</option>
<option value={200}>Last 200</option>
<option value={500}>Last 500</option>
</select>
</div>
</div>
<div className="card-glass-hover rounded-xl overflow-hidden">
{historyLoading ? (
<div className="text-center py-8 text-muted-foreground">Loading history...</div>
) : history.length === 0 ? (
<div className="text-center py-8 text-muted-foreground">
No history available
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full">
<thead className="bg-secondary/50">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">
Time
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">
Downloader
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">
Username
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">
Status
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">
Files
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">
Error
</th>
<th className="px-4 py-3 text-left text-xs font-medium text-muted-foreground uppercase tracking-wider">
Alert
</th>
</tr>
</thead>
<tbody className="divide-y divide-border">
{history.map((entry) => (
<tr
key={entry.id}
className="hover:bg-secondary/50 transition-colors"
>
<td className="px-4 py-3 whitespace-nowrap text-sm text-foreground">
{formatRelativeTime(entry.timestamp)}
</td>
<td className="px-4 py-3 whitespace-nowrap">
<span className="px-2 py-1 text-xs font-medium rounded-full bg-primary/10 text-primary">
{getDownloaderName(entry.downloader)}
</span>
</td>
<td className="px-4 py-3 whitespace-nowrap text-sm text-foreground">
{entry.username}
</td>
<td className="px-4 py-3 whitespace-nowrap">
{entry.success ? (
<span className="flex items-center space-x-1 text-green-600 dark:text-green-400">
<CheckCircle className="w-4 h-4" />
<span className="text-sm font-medium">Success</span>
</span>
) : (
<span className="flex items-center space-x-1 text-red-600 dark:text-red-400">
<AlertCircle className="w-4 h-4" />
<span className="text-sm font-medium">Failed</span>
</span>
)}
</td>
<td className="px-4 py-3 whitespace-nowrap text-sm text-foreground">
{entry.file_count}
</td>
<td className="px-4 py-3 text-sm text-muted-foreground max-w-xs truncate">
{entry.error_message || '-'}
</td>
<td className="px-4 py-3 whitespace-nowrap">
{entry.alert_sent && (
<span className="px-2 py-1 text-xs font-medium rounded-full bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200">
Alerted
</span>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div>
</div>
)
}