350
web/frontend/src/pages/Monitoring.tsx
Normal file
350
web/frontend/src/pages/Monitoring.tsx
Normal file
@@ -0,0 +1,350 @@
|
||||
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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user