Files
breakpilot-compliance/admin-compliance/app/sdk/document-crawler/_components/JobsTab.tsx
Sharang Parnerkar e6ff76d0e1 refactor(admin): split document-crawler page.tsx into colocated components
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>
2026-04-15 08:18:59 +02:00

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>
)
}