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 { 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 { api } from '../lib/api'
|
||||||
import { useToast } from '../context/ToastContext'
|
import { useToast } from '../context/ToastContext'
|
||||||
import { Card, CardHeader, CardTitle, CardContent } from '../components/ui/Card'
|
import { Card, CardHeader, CardTitle, CardContent } from '../components/ui/Card'
|
||||||
@@ -145,132 +145,10 @@ export default function Dashboard() {
|
|||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<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 />
|
<DeviceDiscovery />
|
||||||
|
|
||||||
{/* 2. SYSTEM STATUS & ZULETZT GERIPPT HIGHLIGHT LEISTE */}
|
{/* POS 2: RIPPING OPERATIONS & MEDIEN-MANAGER (DIREKT UNTER LAUFWERKE!) */}
|
||||||
<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" */}
|
|
||||||
<Card className="border border-amber-500/30 bg-[#0f1422] shadow-2xl overflow-hidden">
|
<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">
|
<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">
|
<div className="flex items-center gap-3">
|
||||||
@@ -364,7 +242,7 @@ export default function Dashboard() {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<CardContent className="p-0">
|
<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') && (
|
{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="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">
|
<div className="flex flex-col sm:flex-row items-center justify-between gap-6">
|
||||||
@@ -410,7 +288,7 @@ export default function Dashboard() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Master Unified Table */}
|
{/* Unified Jobs Table */}
|
||||||
{filteredJobs.length === 0 ? (
|
{filteredJobs.length === 0 ? (
|
||||||
<div className="text-center py-16">
|
<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">
|
<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>
|
</CardContent>
|
||||||
</Card>
|
</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 />
|
<LiveLogSection />
|
||||||
|
|
||||||
<JobDetailModal jobId={detailJobId} onClose={() => setDetailJobId(null)} />
|
<JobDetailModal jobId={detailJobId} onClose={() => setDetailJobId(null)} />
|
||||||
|
|||||||
Reference in New Issue
Block a user