Speicherziel-Wahl: Rip-Ziel pro Job frei waehlbar (Etappe 16 / Task 16)
Ampel / ampel (push) Successful in 28s
Ampel / ampel (push) Successful in 28s
- POST /jobs nimmt target_dir (validiert unter /app/media, .. fliegt raus) - GET /storage-targets: Verzeichnisse unter /app/media inkl. Mount-Flag und freiem Platz — NFS/SMB-Shares unter /srv/rippy/media erscheinen dank rslave-Bind automatisch - jobs.target_dir (Mini-Migration via ADD COLUMN IF NOT EXISTS) - Worker: rip_disc(target_dir) mit eigener Validierung; CD/Video/ Transcode-Pfade legen im gewaehlten Ziel ab - UI: "Rippen starten" oeffnet den Ziel-Dialog (Filme/Serien/Musik oder eigener Pfad); Modal-Bugfix: customPath wurde still ignoriert Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@ import { useState, useEffect } from 'react'
|
||||
import { HardDrive, Disc, AlertCircle, CheckCircle, RefreshCw } from 'lucide-react'
|
||||
import { api } from '../lib/api'
|
||||
import { useDarkMode } from '../context/ThemeContext'
|
||||
import RipTargetModal from './RipTargetModal'
|
||||
|
||||
interface Device {
|
||||
id: string
|
||||
@@ -19,6 +20,7 @@ export default function DeviceDiscovery() {
|
||||
const [expandedId, setExpandedId] = useState<string | null>(null)
|
||||
const [actionFeedback, setActionFeedback] = useState<string | null>(null)
|
||||
const [actionBusy, setActionBusy] = useState(false)
|
||||
const [modalDevice, setModalDevice] = useState<Device | null>(null)
|
||||
const { theme } = useDarkMode()
|
||||
|
||||
const refreshDevices = async () => {
|
||||
@@ -32,11 +34,14 @@ export default function DeviceDiscovery() {
|
||||
}
|
||||
}
|
||||
|
||||
const startRip = async (device: Device) => {
|
||||
const startRip = async (device: Device, targetDir?: string) => {
|
||||
setActionBusy(true)
|
||||
setActionFeedback(null)
|
||||
try {
|
||||
const response = await api.post('/jobs', { device_path: device.path })
|
||||
const response = await api.post('/jobs', {
|
||||
device_path: device.path,
|
||||
...(targetDir ? { target_dir: targetDir } : {}),
|
||||
})
|
||||
setActionFeedback(`✓ Job angelegt (${response.data.id.slice(0, 8)}…) — Fortschritt im Dashboard`)
|
||||
} catch (error: any) {
|
||||
setActionFeedback(`✗ ${error?.response?.data?.detail || 'Job konnte nicht angelegt werden'}`)
|
||||
@@ -204,7 +209,7 @@ export default function DeviceDiscovery() {
|
||||
|
||||
<div className="flex items-center gap-3 pt-1">
|
||||
<button
|
||||
onClick={() => startRip(device)}
|
||||
onClick={() => setModalDevice(device)}
|
||||
disabled={actionBusy || device.status !== 'ready'}
|
||||
className="px-4 py-2 rounded-lg bg-indigo-600 text-white text-sm font-medium hover:bg-indigo-700 transition-colors disabled:opacity-40 disabled:cursor-not-allowed"
|
||||
>
|
||||
@@ -231,6 +236,18 @@ export default function DeviceDiscovery() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Ziel-Auswahl vor dem Rip-Start (Filme/Serien/Musik oder eigener
|
||||
Pfad — dort eingehängte Shares sind direkt wählbar) */}
|
||||
<RipTargetModal
|
||||
isOpen={modalDevice !== null}
|
||||
onClose={() => setModalDevice(null)}
|
||||
onSave={(target) => {
|
||||
if (modalDevice) {
|
||||
startRip(modalDevice, target.path)
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -31,7 +31,8 @@ export default function RipTargetModal({ isOpen, onClose, onSave }: RipTargetMod
|
||||
const handleSave = () => {
|
||||
const target = targets.find(t => t.type === selectedType)
|
||||
if (target) {
|
||||
onSave(target)
|
||||
// Eigener Pfad gewinnt — vorher wurde customPath stillschweigend ignoriert
|
||||
onSave(customPath ? { ...target, path: customPath } : target)
|
||||
}
|
||||
onClose()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user