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:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}, [])
|
||||
|
||||
|
||||
@@ -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 []
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user