UI: echter Build hinter nginx, relative API-Pfade, Logs-Seite ehrlich

Vorher lief der Vite-ENTWICKLUNGSSERVER als Produktion, und vier Dateien
riefen hartkodiert http://localhost:8000 auf — vom PC aus fragte der Browser
damit den PC selbst nach Laufwerken (deshalb blieb das UI immer leer);
Settings nutzte ein nie gesetztes VITE_API_URL. Die Logs-Seite zeigte fest
einprogrammierte Fantasie-Einträge ("The Matrix gerippt").

- lib/api.ts: axios-Instanz mit baseURL /api; nginx proxied /api → api:8000
  (ein Origin, kein CORS), SSE-tauglich (proxy_buffering off)
- Dockerfile: Vite-Build → nginx:alpine statt npm run dev
- Logs + LiveLog von echtem /logs-Endpoint; MetadataPreview: echte Laufwerke
  aus /devices statt erfundener /dev/bluray-Optionen, startet echten Job

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-07-23 15:02:39 +02:00
parent eee3a83d0e
commit f2da1ad525
8 changed files with 111 additions and 61 deletions
+2 -4
View File
@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react'
import axios from 'axios'
import { HardDrive, Disc, AlertCircle, CheckCircle, RefreshCw } from 'lucide-react'
import { api } from '../lib/api'
import { useDarkMode } from '../context/ThemeContext'
interface Device {
@@ -13,8 +13,6 @@ interface Device {
model?: string
}
const API_URL = 'http://localhost:8000'
export default function DeviceDiscovery() {
const [devices, setDevices] = useState<Device[]>([])
const [loading, setLoading] = useState(true)
@@ -22,7 +20,7 @@ export default function DeviceDiscovery() {
const refreshDevices = async () => {
try {
const response = await axios.get(`${API_URL}/devices`)
const response = await api.get('/devices')
setDevices(response.data)
} catch (error) {
console.error('Fehler beim Laden der Geräte:', error)
+16 -10
View File
@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react'
import axios from 'axios'
import { Terminal, Disc, Play, CheckCircle, AlertCircle, Clock, Download } from 'lucide-react'
import { Terminal, Play, CheckCircle, AlertCircle } from 'lucide-react'
import { api } from '../lib/api'
import { useDarkMode } from '../context/ThemeContext'
interface LiveLogEntry {
@@ -16,12 +16,10 @@ export default function LiveLogSection() {
const [currentJob, setCurrentJob] = useState<any>(null)
const { theme } = useDarkMode()
const API_URL = 'http://localhost:8000'
useEffect(() => {
const fetchJobs = async () => {
try {
const response = await axios.get(`${API_URL}/jobs`)
const response = await api.get('/jobs')
const jobs = response.data
const current = jobs.find((j: any) => j.status === 'processing')
setCurrentJob(current || jobs.find((j: any) => j.status === 'pending'))
@@ -30,15 +28,23 @@ export default function LiveLogSection() {
}
}
fetchJobs()
const jobInterval = setInterval(fetchJobs, 5000)
const fetchLogs = async () => {
try {
const response = await api.get('/logs', { params: { limit: 50 } })
setLogs(response.data)
} catch (error) {
console.error('Fehler beim Laden der Logs:', error)
}
}
// TODO: WebSocket/SSE für Echtzeit-Logs implementieren
// const logInterval = setInterval(fetchLogs, 1000)
fetchJobs()
fetchLogs()
const jobInterval = setInterval(fetchJobs, 5000)
const logInterval = setInterval(fetchLogs, 5000)
return () => {
clearInterval(jobInterval)
// clearInterval(logInterval)
clearInterval(logInterval)
}
}, [])
+3 -5
View File
@@ -1,6 +1,6 @@
import { useEffect, useState } from 'react'
import axios from 'axios'
import { Clock, Activity, HardDrive, BarChart, AlertCircle, CheckCircle, Disc, Play, Pause, SkipForward } from 'lucide-react'
import { api } from '../lib/api'
import { useDarkMode } from '../context/ThemeContext'
import DeviceDiscovery from '../components/DeviceDiscovery'
import LiveLogSection from '../components/LiveLogSection'
@@ -24,11 +24,9 @@ interface Device {
status: 'empty' | 'ready' | 'ripping'
}
const API_URL = 'http://localhost:8000'
async function fetchJobs(): Promise<Job[]> {
try {
const response = await axios.get(`${API_URL}/jobs`)
const response = await api.get('/jobs')
return response.data
} catch {
return []
@@ -37,7 +35,7 @@ async function fetchJobs(): Promise<Job[]> {
async function fetchDevices(): Promise<Device[]> {
try {
const response = await axios.get(`${API_URL}/devices`)
const response = await api.get('/devices')
return response.data
} catch {
return []
+6 -19
View File
@@ -1,5 +1,6 @@
import { useState, useEffect } from 'react'
import { Terminal, Download, AlertCircle, CheckCircle, Clock, RefreshCw } from 'lucide-react'
import { Terminal, AlertCircle, CheckCircle, RefreshCw } from 'lucide-react'
import { api } from '../lib/api'
import { useDarkMode } from '../context/ThemeContext'
interface LogEntry {
@@ -18,25 +19,11 @@ export default function LogsPage() {
const { theme } = useDarkMode()
const refreshLogs = async () => {
// Echte Ereignisse aus der Datenbank — die Mock-Einträge („The Matrix
// gerippt") sind raus. Was hier steht, ist passiert; sonst ist es leer.
try {
// TODO: API endpoint für Logs implementieren
// const response = await axios.get(`${API_URL}/logs`)
// setLogs(response.data)
// Mock-Daten für Demo
const mockLogs: LogEntry[] = [
{ id: '1', timestamp: '2024-01-15 14:30:22', level: 'info', source: 'udev-daemon', message: 'Disc-Einwurf erkannt' },
{ id: '2', timestamp: '2024-01-15 14:30:25', level: 'info', source: 'worker', message: 'Job erstellt: DVD, Device: /dev/disc/PLEXTOR_BD_XD' },
{ id: '3', timestamp: '2024-01-15 14:30:30', level: 'success', source: 'prescan', message: 'Pre-Scan abgeschlossen, Confidence: 0.92' },
{ id: '4', timestamp: '2024-01-15 14:30:35', level: 'info', source: 'tmdb', message: 'Metadaten lookup erfolgreich: "The Matrix"' },
{ id: '5', timestamp: '2024-01-15 14:31:00', level: 'info', source: 'ripping', message: 'Starte MakeMKV-Ripping...' },
{ id: '6', timestamp: '2024-01-15 14:35:22', level: 'warning', source: 'ripping', message: 'Kapitel 5 übersprungen (Trailer)' },
{ id: '7', timestamp: '2024-01-15 14:40:15', level: 'success', source: 'ripping', message: 'Ripping abgeschlossen, Dateigröße: 4.2 GB' },
{ id: '8', timestamp: '2024-01-15 14:40:20', level: 'info', source: 'jellyfin', message: 'NFO-Datei generiert' },
{ id: '9', timestamp: '2024-01-15 14:40:25', level: 'info', source: 'jellyfin', message: 'Poster heruntergeladen' },
{ id: '10', timestamp: '2024-01-15 14:40:30', level: 'success', source: 'worker', message: 'Job abgeschlossen' },
]
setLogs(mockLogs)
const response = await api.get('/logs', { params: { limit: 200 } })
setLogs(response.data)
} catch (error) {
console.error('Fehler beim Laden der Logs:', error)
} finally {
+40 -12
View File
@@ -1,10 +1,14 @@
import { useState, useEffect } from 'react'
import axios from 'axios'
import { BookOpen, Music, Film, AlertCircle, CheckCircle, Search, Loader2 } from 'lucide-react'
import type { LucideIcon } from 'lucide-react'
import { api } from '../lib/api'
import { useDarkMode } from '../context/ThemeContext'
const API_URL = 'http://localhost:8000'
interface DeviceOption {
path: string
name: string
status: string
}
interface Metadata {
type: 'movie' | 'tv' | 'music' | 'unknown'
@@ -35,12 +39,32 @@ interface PrescanResult {
export default function MetadataPreview() {
const [device, setDevice] = useState<string>('')
const [devices, setDevices] = useState<DeviceOption[]>([])
const [result, setResult] = useState<PrescanResult | null>(null)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [confirming, setConfirming] = useState(false)
const { theme } = useDarkMode()
// Echte Laufwerke laden — vorher standen hier erfundene Festwerte
// (/dev/dvd, /dev/bluray), die es im Container nie gab.
useEffect(() => {
const loadDevices = async () => {
try {
const response = await api.get('/devices')
setDevices(response.data)
if (response.data.length === 1) {
setDevice(response.data[0].path)
}
} catch {
setDevices([])
}
}
loadDevices()
const interval = setInterval(loadDevices, 5000)
return () => clearInterval(interval)
}, [])
const handleScan = async () => {
if (!device) {
setError('Bitte wählen Sie ein Gerät aus.')
@@ -51,7 +75,7 @@ export default function MetadataPreview() {
setError(null)
try {
const response = await axios.post(`${API_URL}/prescan`, { device_path: device })
const response = await api.post('/prescan', { device_path: device })
setResult(response.data)
} catch (err) {
setError('Pre-Scan fehlgeschlagen.')
@@ -66,16 +90,16 @@ export default function MetadataPreview() {
setConfirming(true)
try {
await axios.post(`${API_URL}/metadata/confirm`, {
await api.post('/metadata/confirm', {
title: result.title,
year: result.year,
metadata: result.metadata
})
// Job erstellen
await axios.post(`${API_URL}/jobs`, {
device: device,
type: result.disc_type
// Job erstellen — POST /jobs existiert seit 23.07. wirklich
await api.post('/jobs', {
device_path: device,
title: result.title
})
} catch (err) {
setError('Bestätigung fehlgeschlagen.')
@@ -119,10 +143,14 @@ export default function MetadataPreview() {
onChange={(e) => setDevice(e.target.value)}
className={`flex-1 px-4 py-2 border rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent ${theme === 'dark' ? 'bg-slate-700 border-slate-600 text-slate-100' : 'bg-white border-gray-300 text-slate-900'}`}
>
<option value="" className={theme === 'dark' ? 'bg-slate-800 text-slate-100' : 'bg-white text-slate-900'}>Gerät auswählen...</option>
<option value="/dev/cdrom" className={theme === 'dark' ? 'bg-slate-800 text-slate-100' : 'bg-white text-slate-900'}>CD-ROM (/dev/cdrom)</option>
<option value="/dev/dvd" className={theme === 'dark' ? 'bg-slate-800 text-slate-100' : 'bg-white text-slate-900'}>DVD (/dev/dvd)</option>
<option value="/dev/bluray" className={theme === 'dark' ? 'bg-slate-800 text-slate-100' : 'bg-white text-slate-900'}>Blu-ray (/dev/bluray)</option>
<option value="" className={theme === 'dark' ? 'bg-slate-800 text-slate-100' : 'bg-white text-slate-900'}>
{devices.length === 0 ? 'Kein Laufwerk gefunden' : 'Gerät auswählen...'}
</option>
{devices.map((d) => (
<option key={d.path} value={d.path} className={theme === 'dark' ? 'bg-slate-800 text-slate-100' : 'bg-white text-slate-900'}>
{d.name} ({d.path}){d.status === 'empty' ? ' — keine Disc' : ''}
</option>
))}
</select>
<button
+5 -4
View File
@@ -1,6 +1,6 @@
import { useState, useEffect } from 'react'
import axios from 'axios'
import { Settings, Save, Disc, Cpu, Database, Globe, AlertCircle, CheckCircle } from 'lucide-react'
import { api } from '../lib/api'
import { useDarkMode } from '../context/ThemeContext'
interface SettingsState {
@@ -39,8 +39,9 @@ export default function SettingsPage() {
useEffect(() => {
const loadSettings = async () => {
try {
const response = await axios.get(`${import.meta.env.VITE_API_URL}/settings`)
setSettings(response.data)
const response = await api.get('/settings')
// Leeres Objekt von der API = noch nichts gespeichert → Defaults behalten
setSettings({ ...defaultSettings, ...response.data })
} catch {
setSettings(defaultSettings)
} finally {
@@ -58,7 +59,7 @@ export default function SettingsPage() {
const handleSave = async () => {
setSaving(true)
try {
await axios.post(`${import.meta.env.VITE_API_URL}/settings`, settings)
await api.post('/settings', settings)
setSaved(true)
setTimeout(() => setSaved(false), 3000)
} finally {