diff --git a/docker/ui/src/components/HeroCinemaBanner.tsx b/docker/ui/src/components/HeroCinemaBanner.tsx
index 86d8b1b..d73fb55 100644
--- a/docker/ui/src/components/HeroCinemaBanner.tsx
+++ b/docker/ui/src/components/HeroCinemaBanner.tsx
@@ -1,8 +1,18 @@
-import { Disc, Play, Sparkles, Film, Shield, Zap, CheckCircle } from 'lucide-react'
+import { Disc, Play, Sparkles, Film, Shield, Zap, CheckCircle, Download, FileSpreadsheet } from 'lucide-react'
import { RadialProgress } from './ui/RadialProgress'
import { TypeBadge, StatusBadge } from './ui/Badge'
import { Button } from './ui/Button'
+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'
@@ -13,10 +23,18 @@ interface Job {
progress: number
title?: string
can_retry?: boolean
+ meta?: JobMeta | null
+}
+
+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/w500${p}`
}
interface HeroCinemaBannerProps {
aktiverJob?: Job | null
+ latestJob?: Job | null
onDetailClick?: (jobId: string) => void
onCancelClick?: (jobId: string) => void
totalJobsCount: number
@@ -25,68 +43,74 @@ interface HeroCinemaBannerProps {
export function HeroCinemaBanner({
aktiverJob,
+ latestJob,
onDetailClick,
onCancelClick,
totalJobsCount,
completedCount,
}: HeroCinemaBannerProps) {
+ // Fall 1: Ein Job rippt oder komprimiert gerade live
if (aktiverJob) {
const isTranscoding = aktiverJob.status === 'transcoding'
const statusText = isTranscoding ? 'HandBrake Kompression' : 'MakeMKV Extraktion'
+ const poster = posterUrl(aktiverJob.meta)
return (
-
- {/* Background Ambient Glow Effects */}
-
-
+
+
+
- {/* Left: 3D Metallic Disc Graphic */}
+ {/* 3D Spinning Disc Graphic */}
-
-
-
+ {poster ? (
+
+

+
+
+
-
-
- NOW RIPPING
-
+ ) : (
+
+ )}
+
+ NOW RIPPING
+
-
- {statusText}
-
{aktiverJob.title || 'Aktiver Ripping-Prozess'}
-
- Laufwerk: {aktiverJob.device}
+
+ Laufwerk: {aktiverJob.device}
•
- Gestartet: {new Date(aktiverJob.startTime).toLocaleTimeString('de-DE')}
+ {statusText}
- {/* Quality & Audio Badges Mockup */}
-
-
HDR10+
-
DOLBY ATMOS
-
LOSSLESS MKV
+
+ 4K / HD READY
+ LOSSLESS MKV
+ AUTO-METADATA
- {/* Right: Radial Progress Ring & Controls */}
-
+
-
+
)
diff --git a/docker/ui/src/pages/Dashboard.tsx b/docker/ui/src/pages/Dashboard.tsx
index 587418d..b49d0ec 100644
--- a/docker/ui/src/pages/Dashboard.tsx
+++ b/docker/ui/src/pages/Dashboard.tsx
@@ -100,6 +100,7 @@ export default function Dashboard() {
}, [])
const aktiverJob = jobs.find(j => j.status === 'processing' || j.status === 'transcoding')
+ const latestJob = jobs.find(j => j.status === 'completed') || jobs[0] || null
const stats = {
pending: jobs.filter(j => j.status === 'pending').length,
@@ -123,9 +124,10 @@ export default function Dashboard() {
subtitle="Dein Heimkino-Control-Center für Discs, Rips & Medien-Server"
/>
- {/* Hero Cinema Banner (3D Spinning Disc, Live Progress & Showcase) */}
+ {/* Hero Cinema Banner (Aktiver Rip ODER zuletzt geripptes Medium Showcase) */}
setDetailJobId(id)}
onCancelClick={id => api.post(`/jobs/${id}/cancel`).catch(() => {})}
totalJobsCount={jobs.length}
@@ -133,19 +135,19 @@ export default function Dashboard() {
/>
{/* System Telemetrie Leiste */}
-
+
-
-
Encoder:
+
+
Encoder:
{workers.length === 0 ? (
-
kein Worker
+
kein Worker
) : (
workers.flatMap(w => w.encoders).filter((e, i, a) => a.indexOf(e) === i).map(e => (
{ENCODER_BADGES[e]?.label || e}
@@ -154,29 +156,29 @@ export default function Dashboard() {
-
- Aktiv:
+
+ Aktiv:
{aktiverJob ? (
-
+
{aktiverJob.status === 'transcoding' ? 'Komprimieren' : 'Rippen'} · {aktiverJob.progress} %
) : (
- kein Job
+ kein Job
)}
{plaetze.length > 0 && (
-
- Frei:
+
+ Frei:
{plaetze.map(p => (
{p.name.startsWith('Media') ? 'Media' : 'Arbeit'} {p.frei_gb} GB
@@ -196,45 +198,45 @@ export default function Dashboard() {
icon={Clock}
label="Warteschlange"
value={stats.pending.toString()}
- badgeStyle="bg-amber-500/15 text-amber-500 border-amber-500/20"
+ badgeStyle="bg-amber-500/20 text-amber-400 border-amber-500/30 shadow-[0_0_15px_rgba(245,158,11,0.2)]"
/>
{/* Recent Jobs: Choice between Visual Poster Grid & Classic Table */}
-
+
Neueste Jobs
- ({jobs.length} insgesamt)
+ ({jobs.length} insgesamt)
{/* View Mode Toggle Button */}
-
+
setViewMode('grid')}
className={`p-1.5 rounded-lg text-xs font-medium transition-all ${
viewMode === 'grid'
- ? 'bg-amber-500 text-slate-950 shadow-sm font-bold'
- : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-200'
+ ? 'bg-amber-500 text-slate-950 shadow-md font-bold'
+ : 'text-slate-400 hover:text-slate-200'
}`}
title="Visuelle Poster-Kacheln"
>
@@ -244,8 +246,8 @@ export default function Dashboard() {
onClick={() => setViewMode('table')}
className={`p-1.5 rounded-lg text-xs font-medium transition-all ${
viewMode === 'table'
- ? 'bg-amber-500 text-slate-950 shadow-sm font-bold'
- : 'text-slate-500 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-200'
+ ? 'bg-amber-500 text-slate-950 shadow-md font-bold'
+ : 'text-slate-400 hover:text-slate-200'
}`}
title="Kompakte Liste"
>
@@ -258,7 +260,7 @@ export default function Dashboard() {
href="/api/jobs/export"
download
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-200 dark:border-slate-700 bg-slate-100 dark:bg-slate-800 text-slate-700 dark:text-slate-300 hover:bg-slate-200 dark:hover:bg-slate-700 transition-colors"
+ 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
@@ -280,11 +282,11 @@ export default function Dashboard() {
{jobs.length === 0 ? (
-
+
-
Noch keine Jobs
-
Lege eine CD, DVD oder Blu-ray ein, um zu starten
+
Noch keine Jobs
+
Lege eine CD, DVD oder Blu-ray ein, um zu starten
) : viewMode === 'grid' ? (
/* Visual Poster Cards Grid View */
@@ -294,31 +296,32 @@ export default function Dashboard() {
return (
{poster ? (
-

+

) : (
-
-
+
+
+ {job.type}
)}
-
+
setDetailJobId(job.id)}
- className="text-base font-bold text-left text-slate-900 dark:text-slate-100 hover:text-amber-500 dark:hover:text-amber-400 transition-colors line-clamp-2"
+ className="text-base font-bold text-left text-slate-100 hover:text-amber-400 transition-colors line-clamp-2"
>
{job.title || 'Unbekannter Titel'}
-
- Laufwerk: {job.device}
+
+ {job.device}
@@ -331,7 +334,7 @@ export default function Dashboard() {
-
+
setDetailJobId(job.id)}>
Details
@@ -379,27 +382,27 @@ export default function Dashboard() {
/* Table View */
-
+
- | Typ |
- Titel |
- Status |
- Gerät |
- Fortschritt |
- Startzeit |
- Aktion |
+ Typ |
+ Titel |
+ Status |
+ Gerät |
+ Fortschritt |
+ Startzeit |
+ Aktion |
-
+
{jobs.slice(0, 10).map(job => (
-
+
|
|
setDetailJobId(job.id)}
- className="text-sm font-medium text-left hover:underline text-slate-900 dark:text-slate-100 hover:text-amber-500 dark:hover:text-amber-400 transition-colors"
+ className="text-sm font-bold text-left hover:underline text-slate-100 hover:text-amber-400 transition-colors"
>
{job.title || 'Unbekannt'}
@@ -407,19 +410,19 @@ export default function Dashboard() {
|
|
-
+ |
{job.device}
|
|
-
+ |
{new Date(job.startTime).toLocaleString('de-DE')}
|
{job.status === 'completed' && (
setDetailJobId(job.id)}>
- Download
+ Download
)}
{(job.status === 'completed' || job.status === 'failed') && (
@@ -458,14 +461,14 @@ export default function Dashboard() {
function StatCard({ icon: Icon, label, value, badgeStyle }: { icon: any, label: string, value: string, badgeStyle: string }) {
return (
-
+
-
+
- {label}
- {value}
+ {label}
+ {value}
|