- DeviceDiscovery: auto device detection with type and status - LogsPage: comprehensive logs view with filters and stats - RipTargetModal: target directory selection wizard - ConfirmDialog: generic confirmation dialog component - LiveLogSection: real-time job progress and logs - Update App.tsx to include new pages
This commit is contained in:
@@ -2,6 +2,8 @@ import { useEffect, useState } from 'react'
|
||||
import axios from 'axios'
|
||||
import { Clock, Activity, HardDrive, BarChart, AlertCircle, CheckCircle, Disc, Play, Pause, SkipForward } from 'lucide-react'
|
||||
import { useDarkMode } from '../context/ThemeContext'
|
||||
import DeviceDiscovery from '../components/DeviceDiscovery'
|
||||
import LiveLogSection from '../components/LiveLogSection'
|
||||
|
||||
interface Job {
|
||||
id: string
|
||||
@@ -182,6 +184,12 @@ export default function Dashboard() {
|
||||
<p className={`text-sm ${theme === 'dark' ? 'text-slate-400' : 'text-slate-500'}`}>Übersicht über alle Ripping-Jobs und Geräte</p>
|
||||
</div>
|
||||
|
||||
{/* Device Discovery Section */}
|
||||
<DeviceDiscovery />
|
||||
|
||||
{/* Live Log Section */}
|
||||
<LiveLogSection />
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 transition-colors duration-300">
|
||||
<StatCard
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import { Terminal, Download, AlertCircle, CheckCircle, Clock, RefreshCw } from 'lucide-react'
|
||||
import { useDarkMode } from '../context/ThemeContext'
|
||||
|
||||
interface LogEntry {
|
||||
id: string
|
||||
timestamp: string
|
||||
level: 'info' | 'warning' | 'error' | 'success'
|
||||
message: string
|
||||
source: string
|
||||
details?: string
|
||||
}
|
||||
|
||||
export default function LogsPage() {
|
||||
const [logs, setLogs] = useState<LogEntry[]>([])
|
||||
const [filter, setFilter] = useState<string>('all')
|
||||
const [loading, setLoading] = useState(true)
|
||||
const { theme } = useDarkMode()
|
||||
|
||||
const refreshLogs = async () => {
|
||||
try {
|
||||
// TODO: API endpoint für Logs implementieren
|
||||
// const response = await axios.get(`${API_URL}/logs`)
|
||||
// setLogs(response.data)
|
||||
|
||||
// Mock-Daten für Demo
|
||||
const mockLogs: LogEntry[] = [
|
||||
{ id: '1', timestamp: '2024-01-15 14:30:22', level: 'info', source: 'udev-daemon', message: 'Disc-Einwurf erkannt' },
|
||||
{ id: '2', timestamp: '2024-01-15 14:30:25', level: 'info', source: 'worker', message: 'Job erstellt: DVD, Device: /dev/disc/PLEXTOR_BD_XD' },
|
||||
{ id: '3', timestamp: '2024-01-15 14:30:30', level: 'success', source: 'prescan', message: 'Pre-Scan abgeschlossen, Confidence: 0.92' },
|
||||
{ id: '4', timestamp: '2024-01-15 14:30:35', level: 'info', source: 'tmdb', message: 'Metadaten lookup erfolgreich: "The Matrix"' },
|
||||
{ id: '5', timestamp: '2024-01-15 14:31:00', level: 'info', source: 'ripping', message: 'Starte MakeMKV-Ripping...' },
|
||||
{ id: '6', timestamp: '2024-01-15 14:35:22', level: 'warning', source: 'ripping', message: 'Kapitel 5 übersprungen (Trailer)' },
|
||||
{ id: '7', timestamp: '2024-01-15 14:40:15', level: 'success', source: 'ripping', message: 'Ripping abgeschlossen, Dateigröße: 4.2 GB' },
|
||||
{ id: '8', timestamp: '2024-01-15 14:40:20', level: 'info', source: 'jellyfin', message: 'NFO-Datei generiert' },
|
||||
{ id: '9', timestamp: '2024-01-15 14:40:25', level: 'info', source: 'jellyfin', message: 'Poster heruntergeladen' },
|
||||
{ id: '10', timestamp: '2024-01-15 14:40:30', level: 'success', source: 'worker', message: 'Job abgeschlossen' },
|
||||
]
|
||||
setLogs(mockLogs)
|
||||
} catch (error) {
|
||||
console.error('Fehler beim Laden der Logs:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
refreshLogs()
|
||||
const interval = setInterval(refreshLogs, 10000)
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
const getLevelColor = (level: string) => {
|
||||
switch (level) {
|
||||
case 'error': return theme === 'dark' ? 'bg-rose-900/30 text-rose-400 border-rose-700/20' : 'bg-rose-100 text-rose-700 border-rose-600/20'
|
||||
case 'warning': return theme === 'dark' ? 'bg-amber-900/30 text-amber-400 border-amber-700/20' : 'bg-amber-100 text-amber-700 border-amber-600/20'
|
||||
case 'success': return theme === 'dark' ? 'bg-emerald-900/30 text-emerald-400 border-emerald-700/20' : 'bg-emerald-100 text-emerald-700 border-emerald-600/20'
|
||||
default: return theme === 'dark' ? 'bg-blue-900/30 text-blue-400 border-blue-700/20' : 'bg-blue-100 text-blue-700 border-blue-600/20'
|
||||
}
|
||||
}
|
||||
|
||||
const getLevelIcon = (level: string) => {
|
||||
switch (level) {
|
||||
case 'error': return <AlertCircle size={16} />
|
||||
case 'warning': return <AlertCircle size={16} />
|
||||
case 'success': return <CheckCircle size={16} />
|
||||
default: return <Terminal size={16} />
|
||||
}
|
||||
}
|
||||
|
||||
const getLevelLabel = (level: string) => {
|
||||
switch (level) {
|
||||
case 'error': return 'Fehler'
|
||||
case 'warning': return 'Warnung'
|
||||
case 'success': return 'Erfolg'
|
||||
default: return 'Info'
|
||||
}
|
||||
}
|
||||
|
||||
const filteredLogs = filter === 'all'
|
||||
? logs
|
||||
: logs.filter(log => log.level === filter || log.source === filter)
|
||||
|
||||
const stats = {
|
||||
info: logs.filter(l => l.level === 'info').length,
|
||||
warning: logs.filter(l => l.level === 'warning').length,
|
||||
error: logs.filter(l => l.level === 'error').length,
|
||||
success: logs.filter(l => l.level === 'success').length,
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6 transition-colors duration-300">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col gap-2 transition-colors duration-300">
|
||||
<h1 className="text-3xl font-bold dark:text-slate-100 text-slate-900">Logs</h1>
|
||||
<p className={`text-sm ${theme === 'dark' ? 'text-slate-400' : 'text-slate-500'}`}>Alle relevanten Log-Informationen des Ripping-Systems</p>
|
||||
</div>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 transition-colors duration-300">
|
||||
<div className={`rounded-lg p-4 border transition-colors duration-300 ${theme === 'dark' ? 'bg-slate-800 border-slate-700' : 'bg-white border-slate-200'}`}>
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="p-2 rounded-lg bg-blue-500/10 text-blue-500">
|
||||
<Terminal size={18} />
|
||||
</div>
|
||||
<span className={`text-sm font-medium ${theme === 'dark' ? 'text-slate-400' : 'text-slate-500'}`}>Info</span>
|
||||
</div>
|
||||
<p className={`text-2xl font-bold ${theme === 'dark' ? 'text-slate-100' : 'text-slate-900'}`}>{stats.info}</p>
|
||||
</div>
|
||||
<div className={`rounded-lg p-4 border transition-colors duration-300 ${theme === 'dark' ? 'bg-slate-800 border-slate-700' : 'bg-white border-slate-200'}`}>
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="p-2 rounded-lg bg-amber-500/10 text-amber-500">
|
||||
<AlertCircle size={18} />
|
||||
</div>
|
||||
<span className={`text-sm font-medium ${theme === 'dark' ? 'text-slate-400' : 'text-slate-500'}`}>Warnung</span>
|
||||
</div>
|
||||
<p className={`text-2xl font-bold ${theme === 'dark' ? 'text-slate-100' : 'text-slate-900'}`}>{stats.warning}</p>
|
||||
</div>
|
||||
<div className={`rounded-lg p-4 border transition-colors duration-300 ${theme === 'dark' ? 'bg-slate-800 border-slate-700' : 'bg-white border-slate-200'}`}>
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="p-2 rounded-lg bg-emerald-500/10 text-emerald-500">
|
||||
<CheckCircle size={18} />
|
||||
</div>
|
||||
<span className={`text-sm font-medium ${theme === 'dark' ? 'text-slate-400' : 'text-slate-500'}`}>Erfolg</span>
|
||||
</div>
|
||||
<p className={`text-2xl font-bold ${theme === 'dark' ? 'text-slate-100' : 'text-slate-900'}`}>{stats.success}</p>
|
||||
</div>
|
||||
<div className={`rounded-lg p-4 border transition-colors duration-300 ${theme === 'dark' ? 'bg-slate-800 border-slate-700' : 'bg-white border-slate-200'}`}>
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<div className="p-2 rounded-lg bg-rose-500/10 text-rose-500">
|
||||
<AlertCircle size={18} />
|
||||
</div>
|
||||
<span className={`text-sm font-medium ${theme === 'dark' ? 'text-slate-400' : 'text-slate-500'}`}>Fehler</span>
|
||||
</div>
|
||||
<p className={`text-2xl font-bold ${theme === 'dark' ? 'text-slate-100' : 'text-slate-900'}`}>{stats.error}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<div className={`rounded-lg p-4 border transition-colors duration-300 ${theme === 'dark' ? 'bg-slate-800 border-slate-700' : 'bg-white border-slate-200'}`}>
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<span className={`text-sm font-medium ${theme === 'dark' ? 'text-slate-400' : 'text-slate-500'}`}>Filter:</span>
|
||||
<button
|
||||
onClick={() => setFilter('all')}
|
||||
className={`px-3 py-1.5 rounded-full text-xs font-medium transition-colors ${
|
||||
filter === 'all'
|
||||
? 'bg-indigo-600 text-white'
|
||||
: theme === 'dark' ? 'bg-slate-700 text-slate-300 hover:bg-slate-600' : 'bg-slate-100 text-slate-600 hover:bg-slate-200'
|
||||
}`}
|
||||
>
|
||||
Alle
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setFilter('info')}
|
||||
className={`px-3 py-1.5 rounded-full text-xs font-medium transition-colors ${
|
||||
filter === 'info'
|
||||
? 'bg-blue-600 text-white'
|
||||
: theme === 'dark' ? 'bg-slate-700 text-slate-300 hover:bg-slate-600' : 'bg-slate-100 text-slate-600 hover:bg-slate-200'
|
||||
}`}
|
||||
>
|
||||
Info
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setFilter('success')}
|
||||
className={`px-3 py-1.5 rounded-full text-xs font-medium transition-colors ${
|
||||
filter === 'success'
|
||||
? 'bg-emerald-600 text-white'
|
||||
: theme === 'dark' ? 'bg-slate-700 text-slate-300 hover:bg-slate-600' : 'bg-slate-100 text-slate-600 hover:bg-slate-200'
|
||||
}`}
|
||||
>
|
||||
Erfolg
|
||||
</button>
|
||||
<div className="flex-1" />
|
||||
<button
|
||||
onClick={refreshLogs}
|
||||
className={`flex items-center gap-2 px-3 py-1.5 rounded-lg text-xs font-medium transition-colors ${theme === 'dark' ? 'bg-slate-700 text-slate-300 hover:bg-slate-600' : 'bg-slate-100 text-slate-600 hover:bg-slate-200'}`}
|
||||
>
|
||||
<RefreshCw size={14} className={loading ? 'animate-spin' : ''} />
|
||||
Neu laden
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Logs List */}
|
||||
<div className={`rounded-lg border overflow-hidden transition-colors duration-300 ${theme === 'dark' ? 'bg-slate-800 border-slate-700' : 'bg-white border-slate-200'}`}>
|
||||
<div className="max-h-[600px] overflow-y-auto">
|
||||
{filteredLogs.length === 0 ? (
|
||||
<div className="text-center py-12 transition-colors duration-300">
|
||||
<div className={`mx-auto w-16 h-16 rounded-full flex items-center justify-center mb-4 ${theme === 'dark' ? 'bg-slate-700' : 'bg-slate-100'}`}>
|
||||
<Terminal className={theme === 'dark' ? 'text-slate-400' : 'text-slate-400'} size={32} />
|
||||
</div>
|
||||
<p className={`text-sm ${theme === 'dark' ? 'text-slate-400' : 'text-slate-500'}`}>Keine Logs gefunden</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y divide-slate-200 dark:divide-slate-700">
|
||||
{filteredLogs.map(log => (
|
||||
<div key={log.id} className={`p-4 transition-colors duration-300 hover:${theme === 'dark' ? 'bg-slate-700/50' : 'bg-slate-50'}`}>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className={`mt-1 p-1.5 rounded ${getLevelColor(log.level)}`}>
|
||||
{getLevelIcon(log.level)}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1 flex-wrap">
|
||||
<span className={`text-xs font-mono px-1.5 py-0.5 rounded ${theme === 'dark' ? 'bg-slate-700 text-slate-400' : 'bg-slate-100 text-slate-600'}`}>
|
||||
{log.source}
|
||||
</span>
|
||||
<span className={`text-xs font-medium ${theme === 'dark' ? 'text-slate-400' : 'text-slate-500'}`}>
|
||||
{log.timestamp}
|
||||
</span>
|
||||
<span className={`text-xs font-medium px-2 py-0.5 rounded-full ${getLevelColor(log.level)}`}>
|
||||
{getLevelLabel(log.level)}
|
||||
</span>
|
||||
</div>
|
||||
<p className={`text-sm font-medium ${theme === 'dark' ? 'text-slate-100' : 'text-slate-900'}`}>
|
||||
{log.message}
|
||||
</p>
|
||||
{log.details && (
|
||||
<p className={`text-xs mt-1 font-mono ${theme === 'dark' ? 'text-slate-500' : 'text-slate-500'}`}>
|
||||
{log.details}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user