UI-Feinschliff: Settings-Tabs funktionieren, Details-Knopf tut etwas
Ampel / ampel (push) Successful in 33s
Ampel / ampel (push) Successful in 33s
- Settings: Tabs waren reine Dekoration (kein onClick, isActive fest) — jetzt echte Tab-Navigation (Ripping/Verzeichnisse/APIs/Benachrichtigungen) + OMDb-Key-Feld im APIs-Tab - Laufwerk-Details: aufklappbares Panel (Pfad/Modell/Seriennummer/Disc) mit "Rippen starten" (POST /jobs) und "Auswerfen" (Eject-Endpoint) - Placebos raus: 4 tote Schnellaktionen, doppelte Geraete-Sektion mit zweitem totem Details-Knopf, toter Abmelden-Knopf Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,9 @@ interface Device {
|
||||
export default function DeviceDiscovery() {
|
||||
const [devices, setDevices] = useState<Device[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [expandedId, setExpandedId] = useState<string | null>(null)
|
||||
const [actionFeedback, setActionFeedback] = useState<string | null>(null)
|
||||
const [actionBusy, setActionBusy] = useState(false)
|
||||
const { theme } = useDarkMode()
|
||||
|
||||
const refreshDevices = async () => {
|
||||
@@ -29,6 +32,33 @@ export default function DeviceDiscovery() {
|
||||
}
|
||||
}
|
||||
|
||||
const startRip = async (device: Device) => {
|
||||
setActionBusy(true)
|
||||
setActionFeedback(null)
|
||||
try {
|
||||
const response = await api.post('/jobs', { device_path: device.path })
|
||||
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'}`)
|
||||
} finally {
|
||||
setActionBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
const ejectDisc = async (device: Device) => {
|
||||
setActionBusy(true)
|
||||
setActionFeedback(null)
|
||||
try {
|
||||
await api.post(`/devices/${device.id}/eject`)
|
||||
setActionFeedback('✓ Disc ausgeworfen')
|
||||
refreshDevices()
|
||||
} catch (error: any) {
|
||||
setActionFeedback(`✗ ${error?.response?.data?.detail || 'Auswurf fehlgeschlagen'}`)
|
||||
} finally {
|
||||
setActionBusy(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
refreshDevices()
|
||||
const interval = setInterval(refreshDevices, 5000)
|
||||
@@ -146,10 +176,56 @@ export default function DeviceDiscovery() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button className={`text-sm font-medium transition-colors ${theme === 'dark' ? 'text-indigo-400 hover:text-indigo-300' : 'text-indigo-600 hover:text-indigo-700'}`}>
|
||||
Details
|
||||
<button
|
||||
onClick={() => {
|
||||
setExpandedId(expandedId === device.id ? null : device.id)
|
||||
setActionFeedback(null)
|
||||
}}
|
||||
className={`text-sm font-medium transition-colors ${theme === 'dark' ? 'text-indigo-400 hover:text-indigo-300' : 'text-indigo-600 hover:text-indigo-700'}`}
|
||||
>
|
||||
{expandedId === device.id ? 'Schließen' : 'Details'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{expandedId === device.id && (
|
||||
<div className={`mt-4 pt-4 border-t space-y-3 ${theme === 'dark' ? 'border-slate-700' : 'border-slate-200'}`}>
|
||||
<div className="grid grid-cols-2 gap-x-6 gap-y-1 text-sm">
|
||||
<span className={theme === 'dark' ? 'text-slate-500' : 'text-slate-400'}>Gerätepfad</span>
|
||||
<span className={`font-mono ${theme === 'dark' ? 'text-slate-300' : 'text-slate-700'}`}>{device.path}</span>
|
||||
<span className={theme === 'dark' ? 'text-slate-500' : 'text-slate-400'}>Modell</span>
|
||||
<span className={theme === 'dark' ? 'text-slate-300' : 'text-slate-700'}>{device.model || 'unbekannt'}</span>
|
||||
<span className={theme === 'dark' ? 'text-slate-500' : 'text-slate-400'}>Seriennummer</span>
|
||||
<span className={`font-mono text-xs ${theme === 'dark' ? 'text-slate-300' : 'text-slate-700'}`}>{device.serial || '—'}</span>
|
||||
<span className={theme === 'dark' ? 'text-slate-500' : 'text-slate-400'}>Disc</span>
|
||||
<span className={theme === 'dark' ? 'text-slate-300' : 'text-slate-700'}>
|
||||
{device.status === 'ready' ? `eingelegt (${device.type.toUpperCase()})` : 'keine'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 pt-1">
|
||||
<button
|
||||
onClick={() => startRip(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"
|
||||
>
|
||||
Rippen starten
|
||||
</button>
|
||||
<button
|
||||
onClick={() => ejectDisc(device)}
|
||||
disabled={actionBusy || device.status !== 'ready'}
|
||||
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors disabled:opacity-40 disabled:cursor-not-allowed ${theme === 'dark' ? 'bg-slate-700 text-slate-200 hover:bg-slate-600' : 'bg-slate-200 text-slate-700 hover:bg-slate-300'}`}
|
||||
>
|
||||
Auswerfen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{actionFeedback && (
|
||||
<p className={`text-sm ${actionFeedback.startsWith('✓') ? 'text-emerald-500' : 'text-rose-500'}`}>
|
||||
{actionFeedback}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user