UX-Runde: echte Disc-Titel via BD-Metadaten, Preview-Crash, Dashboard, Ziele
Ampel / ampel (push) Failing after 38s

Commander-Befunde 23.07. abends:
1. Metadaten "klappen nicht": Wurzel = kryptisches Volume-Label + kein
   TMDB-Key. Jetzt: API mountet die Disc read-only (CAP_SYS_ADMIN ist da)
   und liest den KLARTEXT-Titel aus BDMV/META/DL/bdmt_*.xml; dazu
   progressive Suchdegradation (Titel-Varianten) und OMDb-Unscharf-Suche
   (s= + imdbID-Nachladen) — mit dem vorhandenen OMDb-Key gibt es damit
   Titel/Jahr/Poster auch ohne TMDB
2. Weisses Fenster beim Pre-Scan: Lucide-Icons sind forwardRef —
   Direktaufruf getIcon(...)({size}) crashte die Seite; als JSX gerendert
3. Dashboard sortiert: System-Leiste (Encoder + aktiver Job) oben,
   Disc/Geraete -> Stats -> Jobs, Live-Log ans Ende
4. Ziel-Dialog mit ORDNER-BROWSER (GET /browse, nur unter /app/media);
   Schnellwahl nutzt die Settings-Unterordner statt Hartkodierung;
   Tab "Verzeichnisse" in "Speicherziele" aufgegangen (redundant)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-07-23 21:07:55 +02:00
parent 3d384c1e86
commit 90eb17d407
7 changed files with 348 additions and 163 deletions
+48 -4
View File
@@ -120,8 +120,21 @@ function ProgressBar({ progress }: { progress: number }) {
)
}
interface WorkerInfo {
name: string
encoders: string[]
}
const ENCODER_KURZ: Record<string, string> = {
'cpu-x264': 'H.264 (CPU)',
'cpu-x265': 'H.265 (CPU)',
'vaapi': 'VAAPI ⚡',
'nvenc': 'NVENC ⚡',
}
export default function Dashboard() {
const [jobs, setJobs] = useState<Job[]>([])
const [workers, setWorkers] = useState<WorkerInfo[]>([])
const [loading, setLoading] = useState(true)
const { theme } = useDarkMode()
@@ -135,10 +148,13 @@ export default function Dashboard() {
}
loadData()
api.get('/capabilities').then(r => setWorkers(r.data.workers || [])).catch(() => {})
const interval = setInterval(loadData, 5000)
return () => clearInterval(interval)
}, [])
const aktiverJob = jobs.find(j => j.status === 'processing' || j.status === 'transcoding')
const stats = {
pending: jobs.filter(j => j.status === 'pending').length,
processing: jobs.filter(j => j.status === 'processing').length,
@@ -162,11 +178,36 @@ export default function Dashboard() {
<p className={`text-sm ${theme === 'dark' ? 'text-slate-400' : 'text-slate-500'}`}>Übersicht über alle Ripping-Jobs und Geräte</p>
</div>
{/* Device Discovery Section */}
<DeviceDiscovery />
{/* System-Leiste: Encoder + aktiver Job auf einen Blick */}
<div className={`flex flex-wrap items-center gap-x-6 gap-y-2 rounded-xl border px-5 py-3 text-sm ${theme === 'dark' ? 'bg-slate-800 border-slate-700' : 'bg-white border-slate-200'}`}>
<div className="flex items-center gap-2">
<Activity size={16} className="text-indigo-500" />
<span className={theme === 'dark' ? 'text-slate-400' : 'text-slate-500'}>Encoder:</span>
{workers.length === 0 ? (
<span className={theme === 'dark' ? 'text-slate-500' : 'text-slate-400'}>kein Worker</span>
) : (
workers.flatMap(w => w.encoders).filter((e, i, a) => a.indexOf(e) === i).map(e => (
<span key={e} className={`text-xs px-2 py-0.5 rounded ${e.startsWith('cpu') ? (theme === 'dark' ? 'bg-slate-700 text-slate-300' : 'bg-slate-100 text-slate-600') : 'bg-emerald-600/20 text-emerald-500 font-medium'}`}>
{ENCODER_KURZ[e] || e}
</span>
))
)}
</div>
<div className="flex items-center gap-2">
<Clock size={16} className="text-indigo-500" />
<span className={theme === 'dark' ? 'text-slate-400' : 'text-slate-500'}>Aktiv:</span>
{aktiverJob ? (
<span className={theme === 'dark' ? 'text-slate-200' : 'text-slate-700'}>
{aktiverJob.status === 'transcoding' ? 'Komprimieren' : 'Rippen'} · {aktiverJob.progress} %
</span>
) : (
<span className={theme === 'dark' ? 'text-slate-500' : 'text-slate-400'}>kein Job</span>
)}
</div>
</div>
{/* Live Log Section */}
<LiveLogSection />
{/* Laufwerke + erkannte Disc */}
<DeviceDiscovery />
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 transition-colors duration-300">
@@ -271,6 +312,9 @@ export default function Dashboard() {
</table>
</div>
</div>
{/* Live-Log ganz unten — Detail-Ansicht, nicht Haupt-Info */}
<LiveLogSection />
</div>
)
}