From bf80f9dfd48dfc37a966ad243d5d64af43a8452c Mon Sep 17 00:00:00 2001 From: Hitonabi Date: Fri, 24 Jul 2026 16:43:10 +0200 Subject: [PATCH] =?UTF-8?q?refactor(ui):=20Current=20Job,=20Ripping=20Queu?= =?UTF-8?q?e=20und=20Job-Historie=20in=20ein=20einziges=20Master-Panel=20b?= =?UTF-8?q?=C3=BCndig=20unter=20Laufwerke=20zusammengef=C3=BChrt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker/ui/src/pages/Dashboard.tsx | 538 ++++++++++++++---------------- 1 file changed, 258 insertions(+), 280 deletions(-) diff --git a/docker/ui/src/pages/Dashboard.tsx b/docker/ui/src/pages/Dashboard.tsx index 31c257b..5c5cd90 100644 --- a/docker/ui/src/pages/Dashboard.tsx +++ b/docker/ui/src/pages/Dashboard.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react' -import { Cpu, HardDrive, Disc, Download, Trash2, Activity, Film, Server, RotateCcw, FileSpreadsheet } from 'lucide-react' +import { Cpu, HardDrive, Disc, Download, Trash2, Activity, Film, Server, RotateCcw, FileSpreadsheet, Play, Layers, Clock, AlertCircle } from 'lucide-react' import { api } from '../lib/api' import { useToast } from '../context/ToastContext' import { Card, CardHeader, CardTitle, CardContent } from '../components/ui/Card' @@ -60,7 +60,7 @@ export default function Dashboard() { const [loading, setLoading] = useState(true) const [detailJobId, setDetailJobId] = useState(null) const [aufraeumenOffen, setAufraeumenOffen] = useState(false) - const [tableFilter, setTableFilter] = useState<'all' | 'active' | 'completed' | 'failed'>('all') + const [activeTab, setActiveTab] = useState<'all' | 'active' | 'queue' | 'completed' | 'failed'>('all') const [sparklinePoints, setSparklinePoints] = useState([20, 35, 15, 45, 30, 60, 40, 75, 50, 65]) const { toast } = useToast() @@ -111,6 +111,7 @@ export default function Dashboard() { 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') + const failedJobs = jobs.filter(j => j.status === 'failed') const mainPlatz = systemInfo?.plaetze?.[0] const freiGb = mainPlatz ? mainPlatz.frei_gb : 0 @@ -125,10 +126,11 @@ export default function Dashboard() { return `${idx === 0 ? 'M' : 'L'} ${x} ${y}` }).join(' ') - const filteredTableJobs = jobs.filter(j => { - if (tableFilter === 'active') return j.status === 'processing' || j.status === 'transcoding' || j.status === 'pending' - if (tableFilter === 'completed') return j.status === 'completed' - if (tableFilter === 'failed') return j.status === 'failed' + const filteredJobs = jobs.filter(j => { + if (activeTab === 'active') return j.status === 'processing' || j.status === 'transcoding' + if (activeTab === 'queue') return j.status === 'pending' + if (activeTab === 'completed') return j.status === 'completed' + if (activeTab === 'failed') return j.status === 'failed' return true }) @@ -143,146 +145,23 @@ export default function Dashboard() { return (
- {/* 1. GANZ OBEN: Laufwerke & Erkannte Disc im Laufwerk */} + {/* 1. GANZ OBEN: Laufwerke & Erkannte Disc im Laufwerk (Akira Auto Pre-Scan Banner) */} - {/* 2. MAIN 2-COLUMN GRID LAYOUT (BÜNDIGER UNTERER ABSCHLUSS ZWISCHEN LINKS UND RECHTS) */} -
+ {/* 2. SYSTEM STATUS & ZULETZT GERIPPT HIGHLIGHT LEISTE */} +
- {/* LEFT COLUMN (lg:col-span-7) - flex flex-col justify-between fuer buendigen Abschluss */} -
- - {/* 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 (flex-1 flex flex-col justify-between fuer exakt buendigen Abschluss mit Recently Ripped) */} - - - Ripping Queue ({queueJobs.length}) - - - {queueJobs.length > 0 ? ( -
- {queueJobs.map((j, idx) => ( -
-
- {posterUrl(j.meta) ? ( - - ) : ( -
- -
- )} - {idx + 2}. - {j.title || 'Unbekannt'} - -
- - Queue {idx + 1} - -
- ))} -
- ) : ( - /* SC1 Styled Queue List slots (buendiger voller Abschluss) */ -
-
-
- 2. - Bereit für nächste Disc... -
- Queue 1 -
-
-
- 3. - Bereit für nächste Disc... -
- Queue 2 -
-
-
- 4. - Bereit für nächste Disc... -
- Queue 3 -
-
- )} -
-
- -
- - {/* RIGHT COLUMN (lg:col-span-5) - flex flex-col justify-between */} -
- - {/* Server Status Card */} - + {/* Left: Server Status Card */} +
+ Server Status (Echte Live-Daten) - -
+ +
@@ -321,7 +200,7 @@ export default function Dashboard() {
-
+
+
- {/* Recently Ripped Card */} - + {/* Right: Recently Ripped Posters */} +
+ - Recently Ripped ({completedJobs.length}) + 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 ( @@ -385,34 +266,81 @@ export default function Dashboard() { )} -
- {/* 3. VOLLSTÄNDIGE JOB-TABELLE & MEDIEN-BIBLIOTHEK */} - - + {/* 3. DAS EINE ZUSAMMENGEFASSTE MASTER-PANEL: "Ripping Operations & Medien-Manager" */} + +
- Job-Historie & Medien-Bibliothek - ({filteredTableJobs.length} Einträge) + + + Ripping Operations & Medien-Manager + + ({jobs.length} Jobs gesamt)
+ {/* Master Filter Tabs */}
- {(['all', 'active', 'completed', 'failed'] as const).map((f) => ( + + + + + + + + + {failedJobs.length > 0 && ( - ))} + )}
{jobs.length > 0 && ( @@ -422,138 +350,188 @@ export default function Dashboard() { title="Job-Historie als CSV herunterladen" className="inline-flex items-center gap-1.5 text-xs font-medium px-3 py-1.5 rounded-xl border border-slate-700 bg-slate-800/80 text-slate-300 hover:bg-slate-700 transition-colors" > - - CSV Export + + CSV )} - {jobs.some(j => j.status === 'completed' || j.status === 'failed') && ( - )}
- {filteredTableJobs.length === 0 ? ( -
- Keine Jobs für diesen Filter vorhanden. -
- ) : ( -
- - - - - - - - - - - - - - {filteredTableJobs.map(job => { - const poster = posterUrl(job.meta) - return ( - - - + + ) + })} + +
TypTitel & BeschreibungStatusLaufwerkFortschrittStartzeitAktionen
- - -
- {poster ? ( - - ) : ( -
- + + {/* Top Banner inside Master Panel when a Job is Ripping Live */} + {aktiverJob && (activeTab === 'all' || activeTab === 'active') && ( +
+
+
+ {posterUrl(aktiverJob.meta) ? ( + + ) : ( +
+ +
+ )} + +
+
+ + AKTIVER RIP IN ARBEIT + + + +
+

{aktiverJob.title || 'Unbekannter Titel'}

+

+ Laufwerk: {aktiverJob.device} · {aktiverJob.status === 'transcoding' ? 'HandBrake H.265' : 'MakeMKV Lossless'} +

+
+
+ +
+
+
{aktiverJob.progress}%
+
+
+
+
+ + +
+
+
+ )} + + {/* Master Unified Table */} + {filteredJobs.length === 0 ? ( +
+
+ +
+

Keine Jobs in diesem Tab vorhanden

+

Lege eine Disc ein, um einen Ripping-Prozess zu starten.

+
+ ) : ( +
+ + + + + + + + + + + + + + {filteredJobs.map(job => { + const poster = posterUrl(job.meta) + return ( + + + + + + + + - - - - - - - ) - })} - -
TypTitel & InhaltsangabeStatus & PhaseLaufwerkFortschrittStartzeitAktionen
+ + +
+ {poster ? ( + + ) : ( +
+ +
+ )} +
+ + {job.meta?.overview && ( +

+ {job.meta.overview} +

+ )}
- )} -
- - {job.meta?.overview && ( -

- {job.meta.overview} -

+
+
+ + + {job.device} + +
+
+ {job.progress}% +
+
+
+
+
+
+ {new Date(job.startTime).toLocaleString('de-DE')} + +
+ + + {job.status === 'completed' && ( + + )} + + {job.status === 'failed' && job.can_retry && ( + + )} + + {(job.status === 'completed' || job.status === 'failed') && ( + )}
- -
- - - {job.device} - -
-
- {job.progress}% -
-
-
-
-
-
- {new Date(job.startTime).toLocaleString('de-DE')} - -
- - - {job.status === 'completed' && ( - - )} - - {job.status === 'failed' && job.can_retry && ( - - )} - - {(job.status === 'completed' || job.status === 'failed') && ( - - )} -
-
-
- )} +
+
+ )} +
{/* 4. Live-Log Section */}