feat(ui): Master-Panel 'Ripping Operations' DIREKT unter Laufwerke verschoben
Ampel / ampel (push) Successful in 28s
Ampel / ampel (push) Successful in 28s
This commit is contained in:
+128
-128
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Cpu, HardDrive, Disc, Download, Trash2, Activity, Film, Server, RotateCcw, FileSpreadsheet, Play, Layers, Clock, AlertCircle } from 'lucide-react'
|
||||
import { Cpu, HardDrive, Disc, Download, Trash2, Activity, Film, Server, RotateCcw, FileSpreadsheet, Layers } from 'lucide-react'
|
||||
import { api } from '../lib/api'
|
||||
import { useToast } from '../context/ToastContext'
|
||||
import { Card, CardHeader, CardTitle, CardContent } from '../components/ui/Card'
|
||||
@@ -145,132 +145,10 @@ export default function Dashboard() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
|
||||
{/* 1. GANZ OBEN: Laufwerke & Erkannte Disc im Laufwerk (Akira Auto Pre-Scan Banner) */}
|
||||
{/* POS 1: LAUFWERKE (Device Discovery Slot ganz oben) */}
|
||||
<DeviceDiscovery />
|
||||
|
||||
{/* 2. SYSTEM STATUS & ZULETZT GERIPPT HIGHLIGHT LEISTE */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6">
|
||||
|
||||
{/* Left: Server Status Card */}
|
||||
<div className="lg:col-span-7">
|
||||
<Card className="border border-slate-800 bg-[#0f1422] h-full flex flex-col justify-between">
|
||||
<CardHeader className="border-b border-slate-800">
|
||||
<CardTitle className="text-slate-200 flex items-center gap-2">
|
||||
<Server size={18} className="text-amber-400" />
|
||||
Server Status (Echte Live-Daten)
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-5 flex-1 flex items-center">
|
||||
<div className="flex flex-col sm:flex-row items-center justify-between gap-6 w-full">
|
||||
<div className="space-y-4 flex-1 w-full">
|
||||
<div>
|
||||
<div className="flex items-center justify-between text-xs font-mono mb-1">
|
||||
<span className="text-slate-400 flex items-center gap-1.5"><Cpu size={14} className="text-amber-400" /> Auslastung:</span>
|
||||
<span className="text-amber-400 font-bold">{aktiverJob ? `${cpuPercent}% (Aktiv)` : 'Standby (0%)'}</span>
|
||||
</div>
|
||||
<div className="w-full h-2 rounded-full bg-slate-800 overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-amber-500 rounded-full transition-all duration-500"
|
||||
style={{ width: `${aktiverJob ? Math.max(15, cpuPercent) : 5}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center justify-between text-xs font-mono mb-1">
|
||||
<span className="text-slate-400 flex items-center gap-1.5"><Activity size={14} className="text-amber-400" /> Worker Status:</span>
|
||||
<span className="text-slate-200">{workerCount} Worker Online</span>
|
||||
</div>
|
||||
<div className="w-full h-2 rounded-full bg-slate-800 overflow-hidden">
|
||||
<div className="h-full bg-emerald-500 rounded-full w-full" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center justify-between text-xs font-mono mb-1">
|
||||
<span className="text-slate-400 flex items-center gap-1.5"><HardDrive size={14} className="text-amber-400" /> Speichermounts:</span>
|
||||
<span className="text-slate-200">{freiGb > 0 ? `${freiGb} GB frei` : 'Aktiv'}</span>
|
||||
</div>
|
||||
<div className="w-full h-2 rounded-full bg-slate-800 overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-amber-500/80 rounded-full transition-all duration-500"
|
||||
style={{ width: `${Math.max(10, belegtPercent)}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full sm:w-40 h-24 flex-shrink-0 flex items-end justify-center pt-2">
|
||||
<svg viewBox="0 0 100 50" className="w-full h-full stroke-amber-400 fill-amber-500/10 transition-all duration-500">
|
||||
<path
|
||||
d={`${sparklinePath} L 100 50 L 0 50 Z`}
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Right: Recently Ripped Posters */}
|
||||
<div className="lg:col-span-5">
|
||||
<Card className="border border-slate-800 bg-[#0f1422] h-full flex flex-col justify-between">
|
||||
<CardHeader className="border-b border-slate-800 flex items-center justify-between">
|
||||
<CardTitle className="text-slate-200">Zuletzt gerippt ({completedJobs.length})</CardTitle>
|
||||
{completedJobs.length > 0 && (
|
||||
<Button variant="ghost" size="sm" onClick={() => setAufraeumenOffen(true)}>
|
||||
<Trash2 size={13} /> Aufräumen
|
||||
</Button>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className="p-4 flex-1 flex flex-col justify-center">
|
||||
{completedJobs.length === 0 ? (
|
||||
<div className="text-center py-6 text-slate-500 text-xs font-mono">
|
||||
Noch keine gerippten Medien in der Datenbank
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{completedJobs.slice(0, 3).map((item) => {
|
||||
const poster = posterUrl(item.meta)
|
||||
return (
|
||||
<div key={item.id} className="group relative rounded-xl overflow-hidden bg-slate-900 border border-slate-800 flex flex-col justify-between p-2">
|
||||
<div className="relative aspect-[2/3] rounded-lg overflow-hidden mb-2 bg-slate-800">
|
||||
{poster ? (
|
||||
<img src={poster} alt={item.title || ''} className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300" />
|
||||
) : (
|
||||
<div className="w-full h-full flex flex-col items-center justify-center text-slate-500 p-2 text-center">
|
||||
<Film size={24} className="text-amber-400 mb-1" />
|
||||
<span className="text-[9px] uppercase font-bold text-slate-400">{item.type}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-1 text-left">
|
||||
<h4 className="text-xs font-bold text-slate-100 truncate" title={item.title}>{item.title || 'Unbekannt'}</h4>
|
||||
<p className="text-[10px] text-slate-400 font-mono">{new Date(item.startTime).toLocaleDateString('de-DE')}</p>
|
||||
<Button
|
||||
variant="amber"
|
||||
size="sm"
|
||||
className="w-full text-[10px] py-1 h-auto"
|
||||
onClick={() => setDetailJobId(item.id)}
|
||||
>
|
||||
Details
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* 3. DAS EINE ZUSAMMENGEFASSTE MASTER-PANEL: "Ripping Operations & Medien-Manager" */}
|
||||
{/* POS 2: RIPPING OPERATIONS & MEDIEN-MANAGER (DIREKT UNTER LAUFWERKE!) */}
|
||||
<Card className="border border-amber-500/30 bg-[#0f1422] shadow-2xl overflow-hidden">
|
||||
<CardHeader className="border-b border-slate-800 flex flex-wrap items-center justify-between gap-4 py-4">
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -364,7 +242,7 @@ export default function Dashboard() {
|
||||
</CardHeader>
|
||||
|
||||
<CardContent className="p-0">
|
||||
{/* Top Banner inside Master Panel when a Job is Ripping Live */}
|
||||
{/* Live Rip Banner inside Panel when a Job is active */}
|
||||
{aktiverJob && (activeTab === 'all' || activeTab === 'active') && (
|
||||
<div className="p-5 border-b border-amber-500/30 bg-gradient-to-r from-amber-500/15 via-slate-900 to-slate-950">
|
||||
<div className="flex flex-col sm:flex-row items-center justify-between gap-6">
|
||||
@@ -410,7 +288,7 @@ export default function Dashboard() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Master Unified Table */}
|
||||
{/* Unified Jobs Table */}
|
||||
{filteredJobs.length === 0 ? (
|
||||
<div className="text-center py-16">
|
||||
<div className="mx-auto w-16 h-16 rounded-2xl flex items-center justify-center mb-3 bg-slate-900 text-slate-500 border border-slate-800">
|
||||
@@ -534,7 +412,129 @@ export default function Dashboard() {
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* 4. Live-Log Section */}
|
||||
{/* POS 3: SYSTEM STATUS & ZULETZT GERIPPT HIGHLIGHT LEISTE (UNTER DEM MASTER-PANEL) */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6">
|
||||
|
||||
{/* Left: Server Status Card */}
|
||||
<div className="lg:col-span-7">
|
||||
<Card className="border border-slate-800 bg-[#0f1422] h-full flex flex-col justify-between">
|
||||
<CardHeader className="border-b border-slate-800">
|
||||
<CardTitle className="text-slate-200 flex items-center gap-2">
|
||||
<Server size={18} className="text-amber-400" />
|
||||
Server Status (Echte Live-Daten)
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="p-5 flex-1 flex items-center">
|
||||
<div className="flex flex-col sm:flex-row items-center justify-between gap-6 w-full">
|
||||
<div className="space-y-4 flex-1 w-full">
|
||||
<div>
|
||||
<div className="flex items-center justify-between text-xs font-mono mb-1">
|
||||
<span className="text-slate-400 flex items-center gap-1.5"><Cpu size={14} className="text-amber-400" /> Auslastung:</span>
|
||||
<span className="text-amber-400 font-bold">{aktiverJob ? `${cpuPercent}% (Aktiv)` : 'Standby (0%)'}</span>
|
||||
</div>
|
||||
<div className="w-full h-2 rounded-full bg-slate-800 overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-amber-500 rounded-full transition-all duration-500"
|
||||
style={{ width: `${aktiverJob ? Math.max(15, cpuPercent) : 5}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center justify-between text-xs font-mono mb-1">
|
||||
<span className="text-slate-400 flex items-center gap-1.5"><Activity size={14} className="text-amber-400" /> Worker Status:</span>
|
||||
<span className="text-slate-200">{workerCount} Worker Online</span>
|
||||
</div>
|
||||
<div className="w-full h-2 rounded-full bg-slate-800 overflow-hidden">
|
||||
<div className="h-full bg-emerald-500 rounded-full w-full" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center justify-between text-xs font-mono mb-1">
|
||||
<span className="text-slate-400 flex items-center gap-1.5"><HardDrive size={14} className="text-amber-400" /> Speichermounts:</span>
|
||||
<span className="text-slate-200">{freiGb > 0 ? `${freiGb} GB frei` : 'Aktiv'}</span>
|
||||
</div>
|
||||
<div className="w-full h-2 rounded-full bg-slate-800 overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-amber-500/80 rounded-full transition-all duration-500"
|
||||
style={{ width: `${Math.max(10, belegtPercent)}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full sm:w-40 h-24 flex-shrink-0 flex items-end justify-center pt-2">
|
||||
<svg viewBox="0 0 100 50" className="w-full h-full stroke-amber-400 fill-amber-500/10 transition-all duration-500">
|
||||
<path
|
||||
d={`${sparklinePath} L 100 50 L 0 50 Z`}
|
||||
strokeWidth="2.5"
|
||||
strokeLinecap="round"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Right: Recently Ripped Posters */}
|
||||
<div className="lg:col-span-5">
|
||||
<Card className="border border-slate-800 bg-[#0f1422] h-full flex flex-col justify-between">
|
||||
<CardHeader className="border-b border-slate-800 flex items-center justify-between">
|
||||
<CardTitle className="text-slate-200">Zuletzt gerippt ({completedJobs.length})</CardTitle>
|
||||
{completedJobs.length > 0 && (
|
||||
<Button variant="ghost" size="sm" onClick={() => setAufraeumenOffen(true)}>
|
||||
<Trash2 size={13} /> Aufräumen
|
||||
</Button>
|
||||
)}
|
||||
</CardHeader>
|
||||
<CardContent className="p-4 flex-1 flex flex-col justify-center">
|
||||
{completedJobs.length === 0 ? (
|
||||
<div className="text-center py-6 text-slate-500 text-xs font-mono">
|
||||
Noch keine gerippten Medien in der Datenbank
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
{completedJobs.slice(0, 3).map((item) => {
|
||||
const poster = posterUrl(item.meta)
|
||||
return (
|
||||
<div key={item.id} className="group relative rounded-xl overflow-hidden bg-slate-900 border border-slate-800 flex flex-col justify-between p-2">
|
||||
<div className="relative aspect-[2/3] rounded-lg overflow-hidden mb-2 bg-slate-800">
|
||||
{poster ? (
|
||||
<img src={poster} alt={item.title || ''} className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300" />
|
||||
) : (
|
||||
<div className="w-full h-full flex flex-col items-center justify-center text-slate-500 p-2 text-center">
|
||||
<Film size={24} className="text-amber-400 mb-1" />
|
||||
<span className="text-[9px] uppercase font-bold text-slate-400">{item.type}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="space-y-1 text-left">
|
||||
<h4 className="text-xs font-bold text-slate-100 truncate" title={item.title}>{item.title || 'Unbekannt'}</h4>
|
||||
<p className="text-[10px] text-slate-400 font-mono">{new Date(item.startTime).toLocaleDateString('de-DE')}</p>
|
||||
<Button
|
||||
variant="amber"
|
||||
size="sm"
|
||||
className="w-full text-[10px] py-1 h-auto"
|
||||
onClick={() => setDetailJobId(item.id)}
|
||||
>
|
||||
Details
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* POS 4: Live-Log Section */}
|
||||
<LiveLogSection />
|
||||
|
||||
<JobDetailModal jobId={detailJobId} onClose={() => setDetailJobId(null)} />
|
||||
|
||||
Reference in New Issue
Block a user