feat: Download-Knopf fuer fertige Rips — Dateien direkt im Browser statt scp
Ampel / ampel (push) Successful in 29s

- GET /jobs/{id}/files: Dateiliste aus job.output_path (Name + Groesse)
- GET /jobs/{id}/files/{name}: FileResponse-Stream; Validierung strikt —
  output_path muss unter /app/media liegen, nackter Dateiname (kein
  Slash/.., kein Dotfile), realpath-Check gegen Symlink-Ausbrueche.
  Mit Test (test_dateiname_validierung_blockt_pfad_tricks).
- UI: 'Download'-Knopf in der Aktion-Spalte bei fertigen Jobs; die
  Dateiliste mit Groessen + Download-Links lebt im Job-Detail-Popup
  (ein Dropdown wuerde im overflow-x-auto-Tabellencontainer clippen).
- nginx: proxy_buffering off + proxy_read_timeout 3600s waren fuer SSE
  schon gesetzt — grosse Downloads brauchen keine Aenderung.

Wunsch aus der Uebernahme-Session (Commander-Sammelliste 24.07.).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-07-24 09:01:38 +02:00
parent 5d9be4d046
commit ab75134931
6 changed files with 149 additions and 4 deletions
+11 -1
View File
@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react'
import { Clock, Activity, AlertCircle, CheckCircle, Disc } from 'lucide-react'
import { Clock, Activity, AlertCircle, CheckCircle, Disc, Download } from 'lucide-react'
import { api } from '../lib/api'
import { useDarkMode } from '../context/ThemeContext'
import DeviceDiscovery from '../components/DeviceDiscovery'
@@ -304,6 +304,16 @@ export default function Dashboard() {
{new Date(job.startTime).toLocaleString('de-DE')}
</td>
<td className="px-6 py-4 whitespace-nowrap">
{job.status === 'completed' && (
<button
onClick={() => setDetailJobId(job.id)}
title="Fertige Dateien herunterladen (öffnet die Job-Details)"
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-emerald-300 hover:bg-slate-600' : 'bg-slate-100 text-emerald-700 hover:bg-slate-200'}`}
>
<Download size={13} />
Download
</button>
)}
{job.status === 'failed' && (
<button
onClick={() => api.post(`/jobs/${job.id}/retry-transcode`).catch(() => {})}