From e18315814cf2e1dff2be551c10aef90e4571288b Mon Sep 17 00:00:00 2001 From: Hitonabi Date: Fri, 24 Jul 2026 16:36:12 +0200 Subject: [PATCH] feat(ui): 5-Kachel-Banner ohne schwarze Loecher gefuellt, Vollstaendige Job-Tabelle mit TMDB-Beschreibungen wiederhergestellt --- docker/ui/src/components/HeroCinemaBanner.tsx | 134 ++++++----- docker/ui/src/pages/Dashboard.tsx | 224 ++++++++++++++++-- 2 files changed, 273 insertions(+), 85 deletions(-) diff --git a/docker/ui/src/components/HeroCinemaBanner.tsx b/docker/ui/src/components/HeroCinemaBanner.tsx index 711287a..71a1a75 100644 --- a/docker/ui/src/components/HeroCinemaBanner.tsx +++ b/docker/ui/src/components/HeroCinemaBanner.tsx @@ -1,4 +1,4 @@ -import { Disc, Film, Sparkles, CheckCircle, Shield, Zap } from 'lucide-react' +import { Disc, Sparkles, CheckCircle, Shield, Zap, Tv, HardDrive } from 'lucide-react' import { RadialProgress } from './ui/RadialProgress' import { TypeBadge, StatusBadge } from './ui/Badge' import { Button } from './ui/Button' @@ -45,14 +45,14 @@ export function HeroCinemaBanner({ onDetailClick, onCancelClick, }: HeroCinemaBannerProps) { - // Fall 1: Aktiver Job (Rippt oder Komprimiert) + // Fall 1: Aktiver Job (Rippt oder Komprimiert live) if (aktiverJob) { const isTranscoding = aktiverJob.status === 'transcoding' const statusText = isTranscoding ? 'HandBrake Kompression' : 'MakeMKV Extraktion' const poster = posterUrl(aktiverJob.meta) return ( -
+
@@ -108,70 +108,24 @@ export function HeroCinemaBanner({ ) } - // Fall 2: Keine echten gerippten Medien vorhanden -> Standby Hero Banner (Keine Fake-Filme!) - if (completedJobs.length === 0) { - return ( -
-
-
-
-
- -
-
- -
-
- - READY FOR DISC - - System Status: Bereit -
-

- Rippy Cinema Studio -

-

- Automatische Disc-Erkennung aktiv. Lege eine 4K UHD, Blu-ray, DVD oder CD in dein Laufwerk ein. -

-
-
- -
-
- - Gerippte Discs: 0 -
-
-
- - MakeMKV 1:1 Lossless -
-
-
-
- ) - } - - // Fall 3: Echte gerippte Titel vorhanden -> Zeige ECHTE gerippte Discs in der Galerie (Maximal 5 echte Jobs) - const displayJobs = completedJobs.slice(0, 5) + // Fall 2: Kein aktiver Job -> 5-Panel Kachel-Galerie VOLLSTÄNDIG ausfüllen (Zero "Löcher"!) + // Wir füllen freie Slots mit informativen Cinema-Engine-Showcase-Kacheln auf! + const realMovieTiles = completedJobs.slice(0, 4) return ( -
-
-

- ECHTE GERIPPTE MEDIEN ({completedJobs.length}) -

- Aus deiner Rippy-Datenbank -
+
+ {/* Top Subtle Amber Line */} +
-
- {displayJobs.map((job) => { +
+ {/* Echte Medien-Kacheln aus der API */} + {realMovieTiles.map((job) => { const poster = posterUrl(job.meta) return (
onDetailClick && onDetailClick(job.id)} - className="relative rounded-2xl overflow-hidden group cursor-pointer border border-slate-800 hover:border-amber-500/50 transition-all bg-slate-900 aspect-[2/3] flex flex-col justify-end p-3" + className="relative rounded-2xl overflow-hidden group cursor-pointer border border-slate-800 hover:border-amber-500/50 transition-all bg-slate-900 flex flex-col justify-end p-3" > {poster ? ( ) })} + + {/* Center / Showcase Feature Tile: 3D Spinning Optical Disc */} + {realMovieTiles.length < 5 && ( +
+
+
+ +
+
+
+ DISC ENGINE +
+
+ )} + + {/* Dynamic Engine Tile: Drive Status */} + {realMovieTiles.length < 4 && ( +
+
+ DRIVE READY +
+
+

Automatische Erkennung

+

Lege eine Disc ein — Rippy rippt sofort los.

+
+
+ 4K UHD · BLU-RAY · DVD · CD +
+
+ )} + + {/* Dynamic Engine Tile: Media-Server Sync */} + {realMovieTiles.length < 3 && ( +
+
+ MEDIA SERVER +
+
+

Auto-NFO & Poster

+

Jellyfin, Plex & Kodi kompatibel.

+
+
+ Auto-Scan nach Rip +
+
+ )} + + {/* Dynamic Engine Tile: Lossless Assurance */} + {realMovieTiles.length < 2 && ( +
+
+ MakeMKV +
+
+

Lossless 1:1

+

Alle Tonspuren & Subtitles erhalten.

+
+
+ x265 HandBrake Kompression +
+
+ )}
) diff --git a/docker/ui/src/pages/Dashboard.tsx b/docker/ui/src/pages/Dashboard.tsx index 6f33bda..7e807e8 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 } from 'lucide-react' +import { Cpu, HardDrive, Disc, Download, Trash2, Activity, Film, Server, RotateCcw, FileSpreadsheet, List, LayoutGrid } from 'lucide-react' import { api } from '../lib/api' import { useToast } from '../context/ToastContext' import { Card, CardHeader, CardTitle, CardContent } from '../components/ui/Card' @@ -10,6 +10,7 @@ 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' interface JobMeta { year?: number @@ -61,6 +62,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 [sparklinePoints, setSparklinePoints] = useState([20, 35, 15, 45, 30, 60, 40, 75, 50, 65]) const { toast } = useToast() @@ -95,7 +97,6 @@ export default function Dashboard() { 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]) @@ -113,23 +114,26 @@ export default function Dashboard() { 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(' ') + 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' + return true + }) + if (loading) { return (
@@ -141,7 +145,7 @@ export default function Dashboard() { return (
- {/* 1. Top Hero Cinema Showcase */} + {/* 1. Top Hero Cinema Showcase (5 Kacheln vollständig ausgefüllt) */} api.post(`/jobs/${id}/cancel`).catch(() => {})} /> - {/* 2. Main 2-Column Grid Layout */} + {/* 2. Main 2-Column Grid Layout (SC1 Mockup Bereich) */}
{/* LEFT COLUMN (lg:col-span-7) */} @@ -214,27 +218,29 @@ export default function Dashboard() { - {/* Ripping Queue Card (ECHTE WARTESCHLANGE) */} + {/* Ripping Queue Card */} Ripping Queue ({queueJobs.length}) - + {queueJobs.length === 0 ? ( -
+
Warteschlange ist leer (0 Jobs eingereiht)
) : ( - queueJobs.map((j, idx) => ( -
-
- {idx + 1}. - {j.title || 'Unbekannt'} - +
+ {queueJobs.map((j, idx) => ( +
+
+ {idx + 1}. + {j.title || 'Unbekannt'} + +
+ Wartend
- Wartend -
- )) + ))} +
)} @@ -244,7 +250,7 @@ export default function Dashboard() { {/* RIGHT COLUMN (lg:col-span-5) */}
- {/* Server Status Card mit ECHTEN API-DATEN & LIVE SPARKLINE GRAPH */} + {/* Server Status Card */} @@ -254,8 +260,6 @@ export default function Dashboard() {
- - {/* Echte API Daten */}
@@ -294,7 +298,6 @@ export default function Dashboard() {
- {/* Dynamischer Live Sparkline Graph */}
- {/* Recently Ripped Card (ECHTE DE-DATENBANK JOBS) */} + {/* Recently Ripped Card */} Zuletzt gerippt ({completedJobs.length}) @@ -364,8 +367,177 @@ export default function Dashboard() {
- {/* 3. Lower Section: Drive Slot & Live Log */} + {/* 3. Laufwerke & Disc-Erkennung (Device Discovery Slot) */} + + {/* 4. VOLLSTÄNDIGE JOB-TABELLE & MEDIEN-BIBLIOTHEK (AUS DEM ALTEN DESIGN + METADATEN/BESCHREIBUNGEN) */} + + +
+ Job-Historie & Medien-Bibliothek + ({filteredTableJobs.length} Einträge) +
+ +
+ {/* Filter Pills */} +
+ {(['all', 'active', 'completed', 'failed'] as const).map((f) => ( + + ))} +
+ + {jobs.length > 0 && ( + + + CSV Export + + )} + {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 ? ( + + ) : ( +
+ +
+ )} +
+ + {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') && ( + + )} +
+
+
+ )} +
+ + {/* 5. Live-Log Section */} setDetailJobId(null)} />