fix(ui,transcode): Dialog-Falle, Arbeitsverzeichnis waehlbar, Original-Aufheben entschaerft
Ampel / ampel (push) Successful in 28s

Drei Befunde aus dem ersten echten UHD-Durchlauf (Akira). Der Rip und die
Kompression liefen sauber durch - danach hat "Original behalten" die Platte
vollgeschrieben und den fertigen Job als fehlgeschlagen markiert.

1. ORIGINAL-AUFHEBEN DARF DEN JOB NICHT MEHR TOETEN (tasks.py)
   Vorher stand dort ein nacktes shutil.move(raw_dir, ziel). Arbeits-
   verzeichnis (/app/temp, Docker-Volume) und Ziel (/app/media, Bind-Mount)
   sind VERSCHIEDENE Dateisysteme - os.rename scheitert mit EXDEV, shutil.move
   faellt auf Kopieren zurueck. Ergebnis am 25.07.: 74-GB-Vollkopie auf
   dieselbe Platte, Abbruch bei 41 GB mit ENOSPC, Platte 100 % voll, Worker-
   Container startete nicht mehr ("failed to mount: no space left on device"),
   und der Job galt als FEHLGESCHLAGEN - obwohl die komprimierte Datei
   (4,8 GB) fertig und in Ordnung war. Der Nutzer sah nur eine leere Queue.
   Jetzt: _original_aufheben() prueft erst, ob ueberhaupt kopiert werden muss
   (gleiches Dateisystem -> reines Umhaengen), prueft sonst den freien Platz
   VORHER, faengt jeden OSError ab, raeumt eine halbe Kopie weg und meldet das
   als WARNUNG. Der Job bleibt erfolgreich, die Roh-Datei bleibt liegen.
   Zwei Tests decken beide Wege ab.

2. ARBEITSVERZEICHNIS IST JETZT WAEHLBAR (Settings.tsx)
   Es war ein freies Textfeld - man musste den Container-Pfad (/app/media/...)
   KENNEN, um eine Netzwerk-Freigabe zu treffen. Genau daran ist es
   gescheitert, weshalb der 74-GB-Rohschnitt ueberhaupt erst auf der VM-Platte
   landete. Jetzt eine Auswahl aus /storage-targets (dieselbe Liste wie bei
   den Speicherzielen), inklusive Kennzeichnung als Netzwerk-Freigabe und
   freiem Platz je Ziel.

3. DIALOGE KLEBTEN IM PANEL (ui/Modal.tsx)
   "Rippen starten" im Laufwerke-Tab oeffnete den Dialog INNERHALB des
   Bereichs, teils abgeschnitten. Ursache: .glass-panel in index.css setzt
   backdrop-filter: blur(16px), und ein Element mit backdrop-filter wird zum
   Bezugsrahmen fuer position: fixed seiner Nachfahren - das Modal war damit
   an der Card ausgerichtet statt am Fenster. Modal rendert jetzt per
   createPortal an document.body. Behebt es fuer ALLE Dialoge auf einmal.

Aufgeraeumt: die abgebrochene 41-GB-Teilkopie unter
"/app/media/movies/Akira (1988)/original/" geloescht (nachweislich
unvollstaendig - 41 GB gegen 79,6 GB Quelle, Quelle intakt). Platte wieder
bei 78 %, Container laufen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-07-25 19:48:10 +02:00
parent e84afc1718
commit c065967f4d
4 changed files with 168 additions and 10 deletions
+12 -2
View File
@@ -1,4 +1,5 @@
import { ReactNode, useEffect } from 'react'
import { createPortal } from 'react-dom'
import { X } from 'lucide-react'
interface ModalProps {
@@ -35,7 +36,15 @@ export function Modal({ isOpen, onClose, title, children, maxWidth = 'lg' }: Mod
'4xl': 'max-w-4xl',
}
return (
// WICHTIG (Befund 25.07.2026): Der Dialog wird per Portal direkt an
// document.body gehängt statt dort zu bleiben, wo er im Baum steht.
// Grund: `.glass-panel` in index.css setzt `backdrop-filter: blur(16px)`,
// und ein Element mit backdrop-filter wird zum Bezugsrahmen für
// `position: fixed` seiner Nachfahren. Ein Modal INNERHALB einer Card war
// damit nicht mehr am Fenster ausgerichtet, sondern an der Card — es klebte
// im Panel und wurde am Rand abgeschnitten (gemeldet für „Rippen starten"
// im Laufwerke-Tab). Das Portal löst das für ALLE Dialoge auf einmal.
return createPortal(
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 overflow-y-auto">
{/* Glass Backdrop */}
<div
@@ -61,6 +70,7 @@ export function Modal({ isOpen, onClose, title, children, maxWidth = 'lg' }: Mod
{children}
</div>
</div>
</div>
</div>,
document.body,
)
}
+38 -5
View File
@@ -86,6 +86,14 @@ interface KeydbStatus {
geaendert: string
}
// Ein Ablageziel aus GET /storage-targets (Feldnamen exakt wie die API liefert).
interface StorageZiel {
name: string
path: string
is_mount: boolean
free_gb: number | null
}
// Antwort von GET/POST /system/keystore — MakeMKVs eigener Schluesselvorrat.
interface KeystoreStatus {
vorhanden: boolean
@@ -148,6 +156,9 @@ export default function SettingsPage() {
const [keydbBusy, setKeydbBusy] = useState(false)
const [dumps, setDumps] = useState<AacsDump[]>([])
const keydbInput = useRef<HTMLInputElement>(null)
// Ablageziele unter /app/media inkl. eingehängter Freigaben — speist die
// Auswahl des Arbeitsverzeichnisses (vorher musste man den Pfad tippen).
const [ziele, setZiele] = useState<StorageZiel[]>([])
const [keystore, setKeystore] = useState<KeystoreStatus | null>(null)
const [keystoreBusy, setKeystoreBusy] = useState(false)
const keystoreInput = useRef<HTMLInputElement>(null)
@@ -265,6 +276,9 @@ export default function SettingsPage() {
api.get('/system/keystore')
.then(r => setKeystore(r.data))
.catch(() => setKeystore(null))
api.get('/storage-targets')
.then(r => setZiele(Array.isArray(r.data) ? r.data : []))
.catch(() => setZiele([]))
api.get('/system/aacs-dumps')
.then(r => setDumps(r.data?.dumps || []))
.catch(() => setDumps([]))
@@ -613,14 +627,33 @@ export default function SettingsPage() {
/>
</div>
<Input
label="Arbeitsverzeichnis für Roh-Rips (optional)"
{/*
Auswahl statt Eintippen (Befund 25.07.2026): Das Feld war ein
freies Textfeld — man musste den Container-Pfad KENNEN
(/app/media/…), um eine Netzwerk-Freigabe zu treffen. Genau
daran ist es gescheitert, und der 74-GB-Rohschnitt landete auf
der VM-Platte, bis sie voll war. Die Ziele kommen aus
/storage-targets — dieselbe Liste wie bei den Speicherzielen,
inklusive freiem Platz.
*/}
<Select
label="Arbeitsverzeichnis für Roh-Rips"
value={settings.workDir}
onChange={(e) => handleChange('workDir', e.target.value)}
placeholder="leer = /app/temp (Container-Platte)"
/>
>
<option value="">Container-Platte (Standard) klein, nur für DVD/Blu-ray</option>
{ziele.map(z => (
<option key={z.path} value={z.path}>
{z.name}{z.is_mount ? ' (Netzwerk-Freigabe)' : ''}
{z.free_gb != null ? `${z.free_gb} GB frei` : ''}
</option>
))}
</Select>
<p className="text-xs text-slate-500 dark:text-slate-400">
Wichtig für 4K-UHD: Rohdaten sind bis 100 GB groß.
<strong className="text-slate-700 dark:text-slate-300">Wichtig für 4K-UHD:</strong> Rohdaten
sind bis 100 GB groß und passen selten auf die Container-Platte. Lege das Arbeitsverzeichnis
auf eine Freigabe mit genug Platz am besten auf dieselbe, auf der auch die fertigen Dateien
landen. Dann muss Rippy die Rohdatei am Ende nur umhängen statt sie zu kopieren.
{systemInfo?.plaetze?.length ? (
<> Aktuell frei: {systemInfo.plaetze.map(p => `${p.name}: ${p.frei_gb} GB`).join(' · ')}</>
) : null}