Break 839-line page.tsx into _types.ts, _components/SourcesTab.tsx, JobsTab.tsx, DocumentsTab.tsx, ReportTab.tsx, and ComplianceRing.tsx. page.tsx is now 56 LOC (wiring only). No behavior changes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
176 lines
7.1 KiB
TypeScript
176 lines
7.1 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useEffect, useCallback } from 'react'
|
|
import { CrawlJob, CrawlSource, api } from '../_types'
|
|
|
|
export function JobsTab() {
|
|
const [jobs, setJobs] = useState<CrawlJob[]>([])
|
|
const [sources, setSources] = useState<CrawlSource[]>([])
|
|
const [selectedSource, setSelectedSource] = useState('')
|
|
const [jobType, setJobType] = useState<'full' | 'delta'>('full')
|
|
const [loading, setLoading] = useState(true)
|
|
|
|
const loadData = useCallback(async () => {
|
|
setLoading(true)
|
|
try {
|
|
const [j, s] = await Promise.all([api('jobs'), api('sources')])
|
|
setJobs(j || [])
|
|
setSources(s || [])
|
|
if (!selectedSource && s?.length > 0) setSelectedSource(s[0].id)
|
|
} catch { /* ignore */ }
|
|
setLoading(false)
|
|
}, [selectedSource])
|
|
|
|
useEffect(() => { loadData() }, [loadData])
|
|
|
|
// Auto-refresh running jobs
|
|
useEffect(() => {
|
|
const hasRunning = jobs.some(j => j.status === 'running' || j.status === 'pending')
|
|
if (!hasRunning) return
|
|
const interval = setInterval(loadData, 3000)
|
|
return () => clearInterval(interval)
|
|
}, [jobs, loadData])
|
|
|
|
const handleTrigger = async () => {
|
|
if (!selectedSource) return
|
|
await api('jobs', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ source_id: selectedSource, job_type: jobType }),
|
|
})
|
|
loadData()
|
|
}
|
|
|
|
const handleCancel = async (id: string) => {
|
|
await api(`jobs/${id}/cancel`, { method: 'POST' })
|
|
loadData()
|
|
}
|
|
|
|
const statusColor = (s: string) => {
|
|
switch (s) {
|
|
case 'completed': return 'bg-green-100 text-green-700'
|
|
case 'running': return 'bg-blue-100 text-blue-700'
|
|
case 'pending': return 'bg-yellow-100 text-yellow-700'
|
|
case 'failed': return 'bg-red-100 text-red-700'
|
|
case 'cancelled': return 'bg-gray-100 text-gray-600'
|
|
default: return 'bg-gray-100 text-gray-700'
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
{/* Trigger form */}
|
|
<div className="bg-white rounded-xl border border-gray-200 p-6">
|
|
<h3 className="font-medium text-gray-900 mb-4">Neuen Crawl starten</h3>
|
|
<div className="flex gap-4 items-end">
|
|
<div className="flex-1">
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Quelle</label>
|
|
<select
|
|
value={selectedSource}
|
|
onChange={e => setSelectedSource(e.target.value)}
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm"
|
|
>
|
|
{sources.map(s => <option key={s.id} value={s.id}>{s.name}</option>)}
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Typ</label>
|
|
<select
|
|
value={jobType}
|
|
onChange={e => setJobType(e.target.value as 'full' | 'delta')}
|
|
className="px-3 py-2 border border-gray-300 rounded-lg text-sm"
|
|
>
|
|
<option value="full">Voll-Scan</option>
|
|
<option value="delta">Delta-Scan</option>
|
|
</select>
|
|
</div>
|
|
<button
|
|
onClick={handleTrigger}
|
|
disabled={!selectedSource}
|
|
className="px-6 py-2 bg-purple-600 text-white text-sm font-medium rounded-lg hover:bg-purple-700 disabled:opacity-50"
|
|
>
|
|
Crawl starten
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Job list */}
|
|
{loading ? (
|
|
<div className="text-center py-8 text-gray-500">Laden...</div>
|
|
) : jobs.length === 0 ? (
|
|
<div className="text-center py-12 text-gray-500 bg-white rounded-xl border border-gray-200">
|
|
Noch keine Crawl-Jobs ausgefuehrt.
|
|
</div>
|
|
) : (
|
|
<div className="space-y-3">
|
|
{jobs.map(job => (
|
|
<div key={job.id} className="bg-white rounded-xl border border-gray-200 p-5">
|
|
<div className="flex items-center justify-between mb-3">
|
|
<div className="flex items-center gap-3">
|
|
<span className={`px-2 py-1 text-xs font-medium rounded ${statusColor(job.status)}`}>
|
|
{job.status}
|
|
</span>
|
|
<span className="text-sm font-medium text-gray-900">{job.source_name || 'Quelle'}</span>
|
|
<span className="text-xs text-gray-400">{job.job_type === 'delta' ? 'Delta' : 'Voll'}</span>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
{(job.status === 'running' || job.status === 'pending') && (
|
|
<button onClick={() => handleCancel(job.id)} className="text-xs text-red-600 hover:text-red-800">
|
|
Abbrechen
|
|
</button>
|
|
)}
|
|
<span className="text-xs text-gray-400">
|
|
{new Date(job.created_at).toLocaleString('de-DE')}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Progress */}
|
|
{job.status === 'running' && job.files_found > 0 && (
|
|
<div className="mb-3">
|
|
<div className="w-full h-2 bg-gray-200 rounded-full overflow-hidden">
|
|
<div
|
|
className="h-full bg-purple-600 rounded-full transition-all"
|
|
style={{ width: `${(job.files_processed / job.files_found) * 100}%` }}
|
|
/>
|
|
</div>
|
|
<div className="text-xs text-gray-500 mt-1">
|
|
{job.files_processed} / {job.files_found} Dateien verarbeitet
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Stats */}
|
|
<div className="grid grid-cols-6 gap-2 text-center">
|
|
<div className="bg-gray-50 rounded-lg p-2">
|
|
<div className="text-lg font-bold text-gray-900">{job.files_found}</div>
|
|
<div className="text-xs text-gray-500">Gefunden</div>
|
|
</div>
|
|
<div className="bg-gray-50 rounded-lg p-2">
|
|
<div className="text-lg font-bold text-gray-900">{job.files_processed}</div>
|
|
<div className="text-xs text-gray-500">Verarbeitet</div>
|
|
</div>
|
|
<div className="bg-green-50 rounded-lg p-2">
|
|
<div className="text-lg font-bold text-green-700">{job.files_new}</div>
|
|
<div className="text-xs text-green-600">Neu</div>
|
|
</div>
|
|
<div className="bg-blue-50 rounded-lg p-2">
|
|
<div className="text-lg font-bold text-blue-700">{job.files_changed}</div>
|
|
<div className="text-xs text-blue-600">Geaendert</div>
|
|
</div>
|
|
<div className="bg-gray-50 rounded-lg p-2">
|
|
<div className="text-lg font-bold text-gray-500">{job.files_skipped}</div>
|
|
<div className="text-xs text-gray-500">Uebersprungen</div>
|
|
</div>
|
|
<div className="bg-red-50 rounded-lg p-2">
|
|
<div className="text-lg font-bold text-red-700">{job.files_error}</div>
|
|
<div className="text-xs text-red-600">Fehler</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|