f2da1ad525
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>
148 lines
5.6 KiB
TypeScript
148 lines
5.6 KiB
TypeScript
import { useState, useEffect } from 'react'
|
|
import { Terminal, Play, CheckCircle, AlertCircle } from 'lucide-react'
|
|
import { api } from '../lib/api'
|
|
import { useDarkMode } from '../context/ThemeContext'
|
|
|
|
interface LiveLogEntry {
|
|
id: string
|
|
timestamp: string
|
|
level: 'info' | 'warning' | 'error' | 'success'
|
|
message: string
|
|
source: string
|
|
}
|
|
|
|
export default function LiveLogSection() {
|
|
const [logs, setLogs] = useState<LiveLogEntry[]>([])
|
|
const [currentJob, setCurrentJob] = useState<any>(null)
|
|
const { theme } = useDarkMode()
|
|
|
|
useEffect(() => {
|
|
const fetchJobs = async () => {
|
|
try {
|
|
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'))
|
|
} catch (error) {
|
|
console.error('Fehler beim Laden der Jobs:', error)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
fetchJobs()
|
|
fetchLogs()
|
|
const jobInterval = setInterval(fetchJobs, 5000)
|
|
const logInterval = setInterval(fetchLogs, 5000)
|
|
|
|
return () => {
|
|
clearInterval(jobInterval)
|
|
clearInterval(logInterval)
|
|
}
|
|
}, [])
|
|
|
|
const getStatusColor = (level: string) => {
|
|
switch (level) {
|
|
case 'error': return 'text-rose-500'
|
|
case 'warning': return 'text-amber-500'
|
|
case 'success': return 'text-emerald-500'
|
|
default: return 'text-blue-500'
|
|
}
|
|
}
|
|
|
|
const getStatusIcon = (level: string) => {
|
|
switch (level) {
|
|
case 'error': return <AlertCircle size={16} />
|
|
case 'warning': return <AlertCircle size={16} />
|
|
case 'success': return <CheckCircle size={16} />
|
|
default: return <Terminal size={16} />
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className={`rounded-xl shadow-sm border overflow-hidden transition-colors duration-300 ${theme === 'dark' ? 'bg-slate-800 border-slate-700' : 'bg-white border-slate-200'}`}>
|
|
<div className={`px-6 py-4 border-b transition-colors duration-300 ${theme === 'dark' ? 'border-slate-700' : 'border-slate-200'} flex items-center justify-between`}>
|
|
<div>
|
|
<h2 className={`text-lg font-semibold ${theme === 'dark' ? 'text-slate-100' : 'text-slate-900'}`}>Live-Log</h2>
|
|
<p className={`text-sm ${theme === 'dark' ? 'text-slate-400' : 'text-slate-500'}`}>
|
|
{currentJob
|
|
? `Aktiver Job: ${currentJob.type.toUpperCase()} (${currentJob.progress}%)`
|
|
: 'Kein aktiver Job'}
|
|
</p>
|
|
</div>
|
|
{currentJob && (
|
|
<div className="flex items-center gap-2">
|
|
<div className={`px-3 py-1 rounded-full text-xs font-medium flex items-center gap-2 ${
|
|
currentJob.progress === 100
|
|
? theme === 'dark' ? 'bg-emerald-900/30 text-emerald-400' : 'bg-emerald-100 text-emerald-700'
|
|
: theme === 'dark' ? 'bg-blue-900/30 text-blue-400' : 'bg-blue-100 text-blue-700'
|
|
}`}>
|
|
{currentJob.progress === 100 ? <CheckCircle size={12} /> : <Play size={12} />}
|
|
{currentJob.progress}%
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
<div className={`max-h-[400px] overflow-y-auto p-4 space-y-2 transition-colors duration-300 font-mono text-xs ${
|
|
theme === 'dark' ? 'bg-slate-900' : 'bg-slate-50'
|
|
}`}>
|
|
{logs.length === 0 ? (
|
|
<div className="text-center py-8 text-slate-500">
|
|
<Terminal size={32} className="mx-auto mb-2 opacity-50" />
|
|
<p>Warte auf Log-Einträge...</p>
|
|
</div>
|
|
) : (
|
|
logs.map((log) => (
|
|
<div key={log.id} className={`flex items-start gap-2 ${theme === 'dark' ? 'text-slate-300' : 'text-slate-700'}`}>
|
|
<span className="text-slate-500 whitespace-nowrap">
|
|
{new Date(log.timestamp).toLocaleTimeString('de-DE', { hour12: false })}
|
|
</span>
|
|
<span className={`font-bold ${getStatusColor(log.level)}`}>
|
|
[{log.source}]
|
|
</span>
|
|
<span className="flex-1">{log.message}</span>
|
|
<span className={getStatusColor(log.level)}>
|
|
{getStatusIcon(log.level)}
|
|
</span>
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
|
|
{/* Quick Stats */}
|
|
<div className={`px-6 py-3 border-t transition-colors duration-300 ${theme === 'dark' ? 'border-slate-700' : 'border-slate-200'} grid grid-cols-3 gap-4`}>
|
|
{currentJob && (
|
|
<>
|
|
<div>
|
|
<p className={`text-xs ${theme === 'dark' ? 'text-slate-500' : 'text-slate-400'}`}>Fortschritt</p>
|
|
<p className={`text-lg font-bold ${theme === 'dark' ? 'text-slate-100' : 'text-slate-900'}`}>
|
|
{currentJob.progress}%
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<p className={`text-xs ${theme === 'dark' ? 'text-slate-500' : 'text-slate-400'}`}>Gerät</p>
|
|
<p className={`text-sm font-medium ${theme === 'dark' ? 'text-slate-300' : 'text-slate-700'}`}>
|
|
{currentJob.device || 'Unbekannt'}
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<p className={`text-xs ${theme === 'dark' ? 'text-slate-500' : 'text-slate-400'}`}>Typ</p>
|
|
<p className={`text-sm font-medium ${theme === 'dark' ? 'text-slate-300' : 'text-slate-700'}`}>
|
|
{currentJob.type?.toUpperCase() || 'Unbekannt'}
|
|
</p>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|