Korrektur-Flow + Freigaben-Auswahl + Schreibtest + Schnellwahl-Klartext
Ampel / ampel (push) Failing after 52s

Commander-Befunde (23.07. spaet):
- MANUELLE KORREKTUR (KONZEPT Schritt 5 endlich komplett): Metadaten-Seite
  hat jetzt Titelsuche ueber ALLE Quellen (GET /metadata/search) mit
  Poster-Kacheln; ein Klick uebernimmt den Treffer (POST /metadata/override)
  fuer Karte, Jobs UND merkt ihn sich 30 Tage per Disc-Fingerabdruck —
  dieselbe Disc wird beim naechsten Einlegen sofort richtig erkannt
- PC/NAS per Klick: SMB-Freigaben eines Rechners auflisten
  (GET /storage-mounts/shares via smbclient) statt //host/share raten
- Berechtigungs-PRUEFUNG beim Einhaengen: Schreibtest, Warnung bei
  nur-lesbaren Zielen (API + UI + Log)
- "Standard-Unterordner" verstaendlich gemacht: heisst jetzt "Schnellwahl
  beim Rippen" mit Erklaertext; kryptisches Basis-Feld raus
- Preview zeigt Poster; Fingerprint fuer Cache/Override vereinheitlicht
  (billiges Label+Groesse statt Mount-Titel)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-07-23 21:34:34 +02:00
parent 37711f87c1
commit 6b7d391737
9 changed files with 440 additions and 48 deletions
+68 -13
View File
@@ -25,8 +25,12 @@ export default function StorageMounts() {
const [mounts, setMounts] = useState<Mount[]>([])
const [targets, setTargets] = useState<Target[]>([])
const [name, setName] = useState('')
const [typ, setTyp] = useState<'nfs' | 'cifs'>('nfs')
const [typ, setTyp] = useState<'nfs' | 'cifs'>('cifs')
const [source, setSource] = useState('')
const [host, setHost] = useState('')
const [shares, setShares] = useState<string[]>([])
const [selectedShare, setSelectedShare] = useState('')
const [sharesBusy, setSharesBusy] = useState(false)
const [username, setUsername] = useState('')
const [password, setPassword] = useState('')
const [busy, setBusy] = useState(false)
@@ -75,16 +79,43 @@ export default function StorageMounts() {
useEffect(() => { laden(); browsen('/app/media') }, [])
const freigabenAuflisten = async () => {
if (!host) return
setSharesBusy(true)
setFeedback(null)
try {
const r = await api.get('/storage-mounts/shares', {
params: { host, ...(username ? { username, password } : {}) },
})
setShares(r.data.shares)
if (r.data.shares.length === 0) {
setFeedback('✗ Keine Freigaben gefunden — Rechner erreichbar und Freigaben aktiv?')
} else {
setSelectedShare(r.data.shares[0])
}
} catch (e: any) {
setFeedback(`${e?.response?.data?.detail || 'Freigaben-Abfrage fehlgeschlagen'}`)
setShares([])
} finally {
setSharesBusy(false)
}
}
const hinzufuegen = async () => {
setBusy(true)
setFeedback(null)
const quelle = typ === 'cifs' && host && selectedShare ? `//${host}/${selectedShare}` : source
try {
await api.post('/storage-mounts', {
name, type: typ, source,
const r = await api.post('/storage-mounts', {
name, type: typ, source: quelle,
...(username ? { username, password } : {}),
})
setFeedback(`✓ „${name}" eingehängt — ab sofort als Rip-Ziel wählbar`)
setName(''); setSource(''); setUsername(''); setPassword('')
setFeedback(
r.data.writable
? `✓ „${name}" eingehängt — Schreibtest OK, ab sofort als Rip-Ziel wählbar`
: `⚠️ „${name}" eingehängt, aber NUR LESBAR — Freigabe-Rechte prüfen!`
)
setName(''); setSource(''); setHost(''); setShares([]); setSelectedShare(''); setUsername(''); setPassword('')
laden()
} catch (e: any) {
setFeedback(`${e?.response?.data?.detail || 'Einhängen fehlgeschlagen'}`)
@@ -205,14 +236,38 @@ export default function StorageMounts() {
<option value="cifs">SMB/CIFS (Windows-Freigabe, NAS)</option>
</select>
</div>
<div className="md:col-span-2">
<label className={`block text-xs font-medium mb-1 ${theme === 'dark' ? 'text-slate-400' : 'text-slate-500'}`}>
Quelle {typ === 'nfs' ? '(host:/export/pfad)' : '(//host/freigabe)'}
</label>
<input value={source} onChange={e => setSource(e.target.value)} className={feld} placeholder={typ === 'nfs' ? '192.168.1.10:/volume1/filme' : '//192.168.1.10/filme'} />
</div>
{typ === 'cifs' && (
{typ === 'nfs' ? (
<div className="md:col-span-2">
<label className={`block text-xs font-medium mb-1 ${theme === 'dark' ? 'text-slate-400' : 'text-slate-500'}`}>
Quelle (host:/export/pfad)
</label>
<input value={source} onChange={e => setSource(e.target.value)} className={feld} placeholder="192.168.1.10:/volume1/filme" />
</div>
) : (
<>
<div>
<label className={`block text-xs font-medium mb-1 ${theme === 'dark' ? 'text-slate-400' : 'text-slate-500'}`}>
Rechner (dein PC oder NAS Name oder IP)
</label>
<input value={host} onChange={e => setHost(e.target.value)} className={feld} placeholder="192.168.178.20 oder TOBIS-PC" />
</div>
<div className="flex items-end">
<button
onClick={freigabenAuflisten}
disabled={!host || sharesBusy}
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors disabled:opacity-40 ${theme === 'dark' ? 'bg-slate-700 text-indigo-300 hover:bg-slate-600' : 'bg-slate-200 text-indigo-700 hover:bg-slate-300'}`}
>
{sharesBusy ? 'Suche…' : 'Freigaben auflisten'}
</button>
</div>
{shares.length > 0 && (
<div className="md:col-span-2">
<label className={`block text-xs font-medium mb-1 ${theme === 'dark' ? 'text-slate-400' : 'text-slate-500'}`}>Freigabe wählen</label>
<select value={selectedShare} onChange={e => setSelectedShare(e.target.value)} className={feld}>
{shares.map(s => <option key={s} value={s}>{s}</option>)}
</select>
</div>
)}
<div>
<label className={`block text-xs font-medium mb-1 ${theme === 'dark' ? 'text-slate-400' : 'text-slate-500'}`}>Benutzer (leer = Gast)</label>
<input value={username} onChange={e => setUsername(e.target.value)} className={feld} autoComplete="off" />
@@ -226,7 +281,7 @@ export default function StorageMounts() {
</div>
<button
onClick={hinzufuegen}
disabled={busy || !name || !source}
disabled={busy || !name || (typ === 'nfs' ? !source : !(host && selectedShare))}
className="mt-4 px-4 py-2 rounded-lg bg-indigo-600 text-white text-sm font-medium hover:bg-indigo-700 transition-colors disabled:opacity-40"
>
{busy ? 'Hänge ein…' : 'Einhängen & speichern'}