import { useEffect, useState } from 'react' import { Clock, Activity, AlertCircle, CheckCircle, Disc, Download, FileSpreadsheet, HardDrive, Trash2, LayoutGrid, List, RotateCcw } from 'lucide-react' import { api } from '../lib/api' import { useToast } from '../context/ToastContext' import { PageHeader } from '../components/ui/PageHeader' import { Card, CardHeader, CardTitle, CardContent } from '../components/ui/Card' import { Button } from '../components/ui/Button' import { StatusBadge, TypeBadge } from '../components/ui/Badge' import ConfirmDialog from '../components/ConfirmDialog' import DeviceDiscovery from '../components/DeviceDiscovery' import JobDetailModal from '../components/JobDetailModal' import LiveLogSection from '../components/LiveLogSection' import { HeroCinemaBanner } from '../components/HeroCinemaBanner' import { RadialProgress } from '../components/ui/RadialProgress' import { ENCODER_BADGES } from '../lib/design' interface JobMeta { year?: number overview?: string poster_path?: string genres?: string[] runtime?: number type?: string source?: string } interface Job { id: string type: 'cd' | 'dvd' | 'bluray' | 'uhd' status: 'pending' | 'processing' | 'transcoding' | 'canceling' | 'completed' | 'failed' device: string startTime: string endTime?: string progress: number title?: string can_retry?: boolean meta?: JobMeta | null } function posterUrl(meta?: JobMeta | null): string | null { const p = meta?.poster_path if (!p) return null return p.startsWith('http') ? p : `https://image.tmdb.org/t/p/w342${p}` } async function fetchJobs(): Promise { try { const response = await api.get('/jobs') return response.data } catch { return [] } } export default function Dashboard() { const [jobs, setJobs] = useState([]) const [workers, setWorkers] = useState([]) const [plaetze, setPlaetze] = useState<{ name: string, frei_gb: number, gesamt_gb: number }[]>([]) const [loading, setLoading] = useState(true) const [detailJobId, setDetailJobId] = useState(null) const [aufraeumenOffen, setAufraeumenOffen] = useState(false) const [viewMode, setViewMode] = useState<'grid' | 'table'>('grid') const { toast } = useToast() const jobLoeschen = async (job: Job) => { try { await api.delete(`/jobs/${job.id}`) toast('success', `„${job.title || job.id.slice(0, 8)}" aus der Liste entfernt`) setJobs(await fetchJobs()) } catch (e: any) { toast('error', e?.response?.data?.detail || 'Entfernen fehlgeschlagen') } } const erledigteAufraeumen = async () => { setAufraeumenOffen(false) try { const r = await api.delete('/jobs') toast('success', `${r.data.deleted} erledigte Jobs entfernt — Dateien bleiben liegen`) setJobs(await fetchJobs()) } catch { toast('error', 'Aufräumen fehlgeschlagen') } } useEffect(() => { const loadData = async () => { try { setJobs(await fetchJobs()) } finally { setLoading(false) } } loadData() api.get('/capabilities').then(r => setWorkers(r.data.workers || [])).catch(() => {}) api.get('/system/info').then(r => setPlaetze(r.data.plaetze || [])).catch(() => {}) const interval = setInterval(loadData, 5000) return () => clearInterval(interval) }, []) const aktiverJob = jobs.find(j => j.status === 'processing' || j.status === 'transcoding') const stats = { pending: jobs.filter(j => j.status === 'pending').length, processing: jobs.filter(j => j.status === 'processing').length, completed: jobs.filter(j => j.status === 'completed').length, failed: jobs.filter(j => j.status === 'failed').length, } if (loading) { return (
) } return (
{/* Hero Cinema Banner (3D Spinning Disc, Live Progress & Showcase) */} setDetailJobId(id)} onCancelClick={id => api.post(`/jobs/${id}/cancel`).catch(() => {})} totalJobsCount={jobs.length} completedCount={stats.completed} /> {/* System Telemetrie Leiste */}
Encoder: {workers.length === 0 ? ( kein Worker ) : ( workers.flatMap(w => w.encoders).filter((e, i, a) => a.indexOf(e) === i).map(e => ( {ENCODER_BADGES[e]?.label || e} )) )}
Aktiv: {aktiverJob ? ( {aktiverJob.status === 'transcoding' ? 'Komprimieren' : 'Rippen'} · {aktiverJob.progress} % ) : ( kein Job )}
{plaetze.length > 0 && (
Frei: {plaetze.map(p => ( {p.name.startsWith('Media') ? 'Media' : 'Arbeit'} {p.frei_gb} GB ))}
)}
{/* Laufwerke & Disc-Erkennung */} {/* Stats Cards */}
{/* Recent Jobs: Choice between Visual Poster Grid & Classic Table */}
Neueste Jobs ({jobs.length} insgesamt)
{/* View Mode Toggle Button */}
{jobs.length > 0 && ( CSV )} {jobs.some(j => j.status === 'completed' || j.status === 'failed') && ( )}
{jobs.length === 0 ? (
Noch keine Jobs

Lege eine CD, DVD oder Blu-ray ein, um zu starten

) : viewMode === 'grid' ? ( /* Visual Poster Cards Grid View */
{jobs.slice(0, 9).map(job => { const poster = posterUrl(job.meta) return (
{poster ? ( {job.title ) : (
)}

Laufwerk: {job.device}

{new Date(job.startTime).toLocaleTimeString('de-DE')}
{job.status === 'completed' && ( )} {job.status === 'failed' && job.can_retry && ( )} {(job.status === 'completed' || job.status === 'failed') && ( )} {(job.status === 'pending' || job.status === 'processing' || job.status === 'transcoding') && ( )}
) })}
) : ( /* Table View */
{jobs.slice(0, 10).map(job => ( ))}
Typ Titel Status Gerät Fortschritt Startzeit Aktion
{job.device} {new Date(job.startTime).toLocaleString('de-DE')} {job.status === 'completed' && ( )} {(job.status === 'completed' || job.status === 'failed') && ( )}
)}
setDetailJobId(null)} /> setAufraeumenOffen(false)} onConfirm={erledigteAufraeumen} title="Job-Liste aufräumen?" message="Alle fertigen und fehlgeschlagenen Jobs verschwinden aus der Liste. Die gerippten Dateien bleiben selbstverständlich liegen." confirmText="Aufräumen" type="warning" />
) } function StatCard({ icon: Icon, label, value, badgeStyle }: { icon: any, label: string, value: string, badgeStyle: string }) { return (

{label}

{value}

) }