Praxis-Feedback-Runde: CIFS-Cap-Fix, TMDB v3-Keys, 4K-UHD-Typ, Vollautomatik, Job-Verwaltung, Worker-Namen
Ampel / ampel (push) Successful in 29s
Ampel / ampel (push) Successful in 29s
Zwei bewiesene Bug-Fixes:
- SMB-Mount 'Unable to apply new capability set': mount.cifs hebt
CAP_DAC_READ_SEARCH an, die in Dockers Default-Caps fehlt (auf der VM
reproduziert: Bounding-Set a82425fb ohne Bit 2; mit der Capability
verschwindet der Fehler). Fix: cap_add DAC_READ_SEARCH fuer den
api-Container.
- TMDB fiel still aus: Client konnte nur v4-Bearer-Tokens, der uebliche
32-Hex-v3-Key bekam 401 und die Suche lieferte nur OMDb. Jetzt beide
Key-Arten (ist_v4_token + api_key-Query-Param lt.
developer.themoviedb.org, mit Tests) — damit kommen auch die deutschen
Texte an (language=de-DE war ueberall schon gesetzt). Neu:
GET /metadata/status + 'Verbindung pruefen' in Einstellungen -> APIs
(Live-Check am Cache vorbei).
Features aus dem Commander-Feedback:
- 4K UHD als eigener Disc-Typ: classify >= 55 GiB (BD-66/BD-100; BD-50
bleibt bluray), beide detection.py + Tests, eigene Badge-Farbe in
Dashboard/Laufwerken, Prescan-Label '4K UHD'. UHD-Rip-Fehler 'Failed to
open disc' (Code 11) bekommt Klartext: LibreDrive-Firmware noetig.
- Vollautomatik (Setting autoRipStart): Disc erkannt -> Rip startet ohne
Popup in den Schnellwahl-Ordner (Serie->Serien, sonst Filme, CD->Musik).
- Job-Verwaltung: 'Neu komprimieren' nur noch mit can_retry (Rohdaten
liegen wirklich da), DELETE /jobs/{id} + 'Erledigte aufraeumen'
(Dateien bleiben immer), 'Alle herunterladen' im Job-Detail
(gestaffelte Einzel-Downloads statt Server-Zip von 40-GB-Dateien).
- Worker zuordenbar: WORKER_NAME-Env als stabiler Anzeigename (fixt auch
die Offline-Leichen nach Rebuilds), info.hostname/ip gemeldet,
Online-Abgleich ueber hostname, DELETE /workers/{name} + Papierkorb im
UI, Quelle-Anzeige an der Disc-Karte ('Quelle: TMDB - 99 % sicher').
- Ripping-Tab nach Medium gegliedert (Video/Audio-CD/Allgemein) +
Untertitel-Klartext (--all-audio/--all-subtitles bleiben komplett),
CD -> Musik-Vorauswahl im Ziel-Dialog, Ordner-Verwaltung beschriftet
und standardmaessig eingeklappt.
- Docs: SAVEPOINT v3.3, ROADMAP Etappe 14 + Ideen (nativer
Windows-Worker, deutsche OMDb-Texte via TMDB-Find), README.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,20 +1,23 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { Clock, Activity, AlertCircle, CheckCircle, Disc, Download } from 'lucide-react'
|
||||
import { Clock, Activity, AlertCircle, CheckCircle, Disc, Download, Trash2 } from 'lucide-react'
|
||||
import { api } from '../lib/api'
|
||||
import { useDarkMode } from '../context/ThemeContext'
|
||||
import { useToast } from '../context/ToastContext'
|
||||
import ConfirmDialog from '../components/ConfirmDialog'
|
||||
import DeviceDiscovery from '../components/DeviceDiscovery'
|
||||
import JobDetailModal from '../components/JobDetailModal'
|
||||
import LiveLogSection from '../components/LiveLogSection'
|
||||
|
||||
interface Job {
|
||||
id: string
|
||||
type: 'cd' | 'dvd' | 'bluray'
|
||||
type: 'cd' | 'dvd' | 'bluray' | 'uhd'
|
||||
status: 'pending' | 'processing' | 'transcoding' | 'canceling' | 'completed' | 'failed'
|
||||
device: string
|
||||
startTime: string
|
||||
endTime?: string
|
||||
progress: number
|
||||
title?: string
|
||||
can_retry?: boolean
|
||||
}
|
||||
|
||||
async function fetchJobs(): Promise<Job[]> {
|
||||
@@ -64,20 +67,29 @@ function StatusBadge({ status }: { status: string }) {
|
||||
|
||||
function TypeBadge({ type }: { type: string }) {
|
||||
const { theme } = useDarkMode()
|
||||
const colors = theme === 'dark'
|
||||
? 'bg-slate-800 text-slate-300'
|
||||
: 'bg-slate-100 text-slate-700'
|
||||
|
||||
const icons = {
|
||||
cd: Disc,
|
||||
dvd: Disc,
|
||||
bluray: Disc,
|
||||
// 4K UHD sichtbar abheben (Commander-Wunsch 24.07.) — es IST eine Blu-ray,
|
||||
// aber der Unterschied (Laufwerk! Größe!) muss auf einen Blick erkennbar sein.
|
||||
const colors: Record<string, string> = theme === 'dark'
|
||||
? {
|
||||
cd: 'bg-blue-900/30 text-blue-400',
|
||||
dvd: 'bg-purple-900/30 text-purple-400',
|
||||
bluray: 'bg-pink-900/30 text-pink-400',
|
||||
uhd: 'bg-amber-900/30 text-amber-400',
|
||||
}
|
||||
: {
|
||||
cd: 'bg-blue-100 text-blue-600',
|
||||
dvd: 'bg-purple-100 text-purple-600',
|
||||
bluray: 'bg-pink-100 text-pink-600',
|
||||
uhd: 'bg-amber-100 text-amber-700',
|
||||
}
|
||||
const labels: Record<string, string> = {
|
||||
cd: 'CD', dvd: 'DVD', bluray: 'BLU-RAY', uhd: '4K UHD',
|
||||
}
|
||||
const Icon = icons[type as keyof typeof icons] || Disc
|
||||
const fallback = theme === 'dark' ? 'bg-slate-800 text-slate-300' : 'bg-slate-100 text-slate-700'
|
||||
return (
|
||||
<span className={`inline-flex items-center gap-1.5 px-2 py-1 rounded-md text-xs font-medium ${colors}`}>
|
||||
<Icon size={14} />
|
||||
{type.toUpperCase()}
|
||||
<span className={`inline-flex items-center gap-1.5 px-2 py-1 rounded-md text-xs font-medium ${colors[type] || fallback}`}>
|
||||
<Disc size={14} />
|
||||
{labels[type] || type.toUpperCase()}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@@ -141,7 +153,30 @@ export default function Dashboard() {
|
||||
const [workers, setWorkers] = useState<WorkerInfo[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [detailJobId, setDetailJobId] = useState<string | null>(null)
|
||||
const [aufraeumenOffen, setAufraeumenOffen] = useState(false)
|
||||
const { theme } = useDarkMode()
|
||||
const { toast } = useToast()
|
||||
|
||||
const jobLoeschen = async (job: Job) => {
|
||||
try {
|
||||
await api.delete(`/jobs/${job.id}`)
|
||||
toast('success', `„${job.title || job.id.slice(0, 8)}" aus der Liste entfernt`)
|
||||
setJobs(await fetchJobs())
|
||||
} catch (e: any) {
|
||||
toast('error', e?.response?.data?.detail || 'Entfernen fehlgeschlagen')
|
||||
}
|
||||
}
|
||||
|
||||
const erledigteAufraeumen = async () => {
|
||||
setAufraeumenOffen(false)
|
||||
try {
|
||||
const r = await api.delete('/jobs')
|
||||
toast('success', `${r.data.deleted} erledigte Jobs entfernt — Dateien bleiben liegen`)
|
||||
setJobs(await fetchJobs())
|
||||
} catch {
|
||||
toast('error', 'Aufräumen fehlgeschlagen')
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const loadData = async () => {
|
||||
@@ -253,7 +288,19 @@ export default function Dashboard() {
|
||||
<div className={`rounded-xl shadow-sm border overflow-hidden transition-colors duration-300 ${theme === 'dark' ? 'bg-slate-800 border-slate-700' : 'bg-white border-slate-200'}`}>
|
||||
<div className={`px-6 py-4 border-b transition-colors duration-300 ${theme === 'dark' ? 'border-slate-700' : 'border-slate-200'} flex items-center justify-between`}>
|
||||
<h2 className={`text-lg font-semibold ${theme === 'dark' ? 'text-slate-100' : 'text-slate-900'}`}>Neueste Jobs</h2>
|
||||
<span className={`text-sm ${theme === 'dark' ? 'text-slate-400' : 'text-slate-500'}`}>{jobs.length} Jobs insgesamt</span>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className={`text-sm ${theme === 'dark' ? 'text-slate-400' : 'text-slate-500'}`}>{jobs.length} Jobs insgesamt</span>
|
||||
{jobs.some(j => j.status === 'completed' || j.status === 'failed') && (
|
||||
<button
|
||||
onClick={() => setAufraeumenOffen(true)}
|
||||
title="Alle fertigen und fehlgeschlagenen Jobs aus der Liste entfernen (Dateien bleiben)"
|
||||
className={`inline-flex items-center gap-1.5 text-xs font-medium px-3 py-1.5 rounded-lg transition-colors ${theme === 'dark' ? 'bg-slate-700 text-slate-300 hover:bg-slate-600' : 'bg-slate-100 text-slate-600 hover:bg-slate-200'}`}
|
||||
>
|
||||
<Trash2 size={13} />
|
||||
Erledigte aufräumen
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="overflow-x-auto transition-colors duration-300">
|
||||
<table className="w-full transition-colors duration-300">
|
||||
@@ -314,15 +361,30 @@ export default function Dashboard() {
|
||||
Download
|
||||
</button>
|
||||
)}
|
||||
{job.status === 'failed' && (
|
||||
{/* Nur wenn Rohdaten wirklich daliegen — an einem Job,
|
||||
der nie gerippt hat, war der Knopf Unsinn */}
|
||||
{job.status === 'failed' && job.can_retry && (
|
||||
<button
|
||||
onClick={() => api.post(`/jobs/${job.id}/retry-transcode`).catch(() => {})}
|
||||
onClick={() => {
|
||||
api.post(`/jobs/${job.id}/retry-transcode`)
|
||||
.then(() => toast('success', 'Kompression neu eingereiht'))
|
||||
.catch((e: any) => toast('error', e?.response?.data?.detail || 'Neu anstoßen fehlgeschlagen'))
|
||||
}}
|
||||
title="Kompression neu anstoßen (Rohdatei bleibt nach Fehlschlägen erhalten)"
|
||||
className={`text-xs font-medium px-3 py-1.5 rounded-lg transition-colors ${theme === 'dark' ? 'bg-slate-700 text-indigo-300 hover:bg-slate-600' : 'bg-slate-100 text-indigo-700 hover:bg-slate-200'}`}
|
||||
>
|
||||
Neu komprimieren
|
||||
</button>
|
||||
)}
|
||||
{(job.status === 'completed' || job.status === 'failed') && (
|
||||
<button
|
||||
onClick={() => jobLoeschen(job)}
|
||||
title="Job aus der Liste entfernen — fertige Dateien bleiben liegen"
|
||||
className={`ml-2 p-1.5 rounded-lg transition-colors text-rose-500 ${theme === 'dark' ? 'hover:bg-slate-700' : 'hover:bg-slate-100'}`}
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
)}
|
||||
{(job.status === 'pending' || job.status === 'processing' || job.status === 'transcoding') && (
|
||||
<button
|
||||
onClick={() => api.post(`/jobs/${job.id}/cancel`).catch(() => {})}
|
||||
@@ -346,6 +408,16 @@ export default function Dashboard() {
|
||||
|
||||
{/* Klick auf einen Job-Titel: Film-/Serien-Info + Job-Fakten */}
|
||||
<JobDetailModal jobId={detailJobId} onClose={() => setDetailJobId(null)} />
|
||||
|
||||
<ConfirmDialog
|
||||
isOpen={aufraeumenOffen}
|
||||
onClose={() => setAufraeumenOffen(false)}
|
||||
onConfirm={erledigteAufraeumen}
|
||||
title="Job-Liste aufräumen?"
|
||||
message="Alle fertigen und fehlgeschlagenen Jobs verschwinden aus der Liste. Die gerippten Dateien bleiben selbstverständlich liegen."
|
||||
confirmText="Aufräumen"
|
||||
type="warning"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user