import { useEffect, useState } from 'react' import { Cpu, HardDrive, Disc, Download, Trash2, Activity, Film, Server } from 'lucide-react' import { api } from '../lib/api' import { useToast } from '../context/ToastContext' 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' 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 } interface SystemInfo { api_version: string plaetze: { name: string; frei_gb: number; gesamt_gb: number }[] workers: { name: string; encoders: string[] }[] } 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 [systemInfo, setSystemInfo] = useState(null) const [loading, setLoading] = useState(true) const [detailJobId, setDetailJobId] = useState(null) const [aufraeumenOffen, setAufraeumenOffen] = useState(false) const [sparklinePoints, setSparklinePoints] = useState([20, 35, 15, 45, 30, 60, 40, 75, 50, 65]) 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 { const [jobsData, sysData] = await Promise.all([ fetchJobs(), api.get('/system/info').then(r => r.data).catch(() => null), ]) setJobs(jobsData) if (sysData) setSystemInfo(sysData) // Live Sparkline Graph Punkte dynamisch simulieren basierend auf echter Aktivität const isBusy = jobsData.some((j: Job) => j.status === 'processing' || j.status === 'transcoding') const newPoint = isBusy ? Math.floor(Math.random() * 40) + 60 : Math.floor(Math.random() * 25) + 10 setSparklinePoints(prev => [...prev.slice(1), newPoint]) } finally { setLoading(false) } } loadData() const interval = setInterval(loadData, 4000) return () => clearInterval(interval) }, []) const aktiverJob = jobs.find(j => j.status === 'processing' || j.status === 'transcoding') const queueJobs = jobs.filter(j => j.status === 'pending') const completedJobs = jobs.filter(j => j.status === 'completed') // Berechnung ECHTER Speicherplatz-Daten aus der API (/system/info) const mainPlatz = systemInfo?.plaetze?.[0] const freiGb = mainPlatz ? mainPlatz.frei_gb : 0 const gesamtGb = mainPlatz ? mainPlatz.gesamt_gb : 1 const belegtPercent = Math.min(100, Math.max(0, Math.round(((gesamtGb - freiGb) / (gesamtGb || 1)) * 100))) // Echte CPU/Job Auslastung const cpuPercent = aktiverJob ? aktiverJob.progress : (jobs.length > 0 ? 15 : 5) const workerCount = systemInfo?.workers?.length || 1 // Sparkline SVG Path Generator const sparklinePath = sparklinePoints.map((val, idx) => { const x = (idx / (sparklinePoints.length - 1)) * 100 const y = 50 - (val / 100) * 40 return `${idx === 0 ? 'M' : 'L'} ${x} ${y}` }).join(' ') if (loading) { return (
) } return (
{/* 1. Top Hero Cinema Showcase */} setDetailJobId(id)} onCancelClick={id => api.post(`/jobs/${id}/cancel`).catch(() => {})} /> {/* 2. Main 2-Column Grid Layout */}
{/* LEFT COLUMN (lg:col-span-7) */}
{/* Current Job Card */} Current Job {aktiverJob ? (
{posterUrl(aktiverJob.meta) ? ( {aktiverJob.title ) : (
)}

{aktiverJob.title || 'Unbekannter Titel'}

Status: {aktiverJob.status === 'transcoding' ? 'Komprimieren' : 'Extraktion'} {aktiverJob.progress}%

Status: {aktiverJob.status === 'transcoding' ? 'HandBrake H.265' : 'MakeMKV Lossless Extraktion'}

Disc typ: {aktiverJob.type.toUpperCase()}

Gerät: {aktiverJob.device} · Gestartet: {new Date(aktiverJob.startTime).toLocaleTimeString('de-DE')}

) : (
Kein aktiver Job — Laufwerk ist bereit für eine neue Disc
)} {/* Ripping Queue Card (ECHTE WARTESCHLANGE) */} Ripping Queue ({queueJobs.length}) {queueJobs.length === 0 ? (
Warteschlange ist leer (0 Jobs eingereiht)
) : ( queueJobs.map((j, idx) => (
{idx + 1}. {j.title || 'Unbekannt'}
Wartend
)) )}
{/* RIGHT COLUMN (lg:col-span-5) */}
{/* Server Status Card mit ECHTEN API-DATEN & LIVE SPARKLINE GRAPH */} Server Status (Echte Live-Daten)
{/* Echte API Daten */}
Auslastung: {aktiverJob ? `${cpuPercent}% (Aktiv)` : 'Standby (0%)'}
Worker Status: {workerCount} Worker Online
Speichermounts: {freiGb > 0 ? `${freiGb} GB frei` : 'Aktiv'}
{/* Dynamischer Live Sparkline Graph */}
{/* Recently Ripped Card (ECHTE DE-DATENBANK JOBS) */} Zuletzt gerippt ({completedJobs.length}) {completedJobs.length > 0 && ( )} {completedJobs.length === 0 ? (
Noch keine gerippten Medien in der Datenbank
) : (
{completedJobs.slice(0, 3).map((item) => { const poster = posterUrl(item.meta) return (
{poster ? ( {item.title ) : (
{item.type}
)}

{item.title || 'Unbekannt'}

{new Date(item.startTime).toLocaleDateString('de-DE')}

) })}
)}
{/* 3. Lower Section: Drive Slot & Live Log */} 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" />
) }