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>
133 lines
5.1 KiB
TypeScript
133 lines
5.1 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useEffect, useCallback } from 'react'
|
|
import { CrawlSource, api } from '../_types'
|
|
|
|
export function SourcesTab() {
|
|
const [sources, setSources] = useState<CrawlSource[]>([])
|
|
const [loading, setLoading] = useState(true)
|
|
const [showForm, setShowForm] = useState(false)
|
|
const [formName, setFormName] = useState('')
|
|
const [formPath, setFormPath] = useState('')
|
|
const [testResult, setTestResult] = useState<Record<string, string>>({})
|
|
|
|
const loadSources = useCallback(async () => {
|
|
setLoading(true)
|
|
try {
|
|
const data = await api('sources')
|
|
setSources(data || [])
|
|
} catch { /* ignore */ }
|
|
setLoading(false)
|
|
}, [])
|
|
|
|
useEffect(() => { loadSources() }, [loadSources])
|
|
|
|
const handleCreate = async () => {
|
|
if (!formName || !formPath) return
|
|
await api('sources', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ name: formName, path: formPath }),
|
|
})
|
|
setFormName('')
|
|
setFormPath('')
|
|
setShowForm(false)
|
|
loadSources()
|
|
}
|
|
|
|
const handleDelete = async (id: string) => {
|
|
await api(`sources/${id}`, { method: 'DELETE' })
|
|
loadSources()
|
|
}
|
|
|
|
const handleToggle = async (source: CrawlSource) => {
|
|
await api(`sources/${source.id}`, {
|
|
method: 'PUT',
|
|
body: JSON.stringify({ enabled: !source.enabled }),
|
|
})
|
|
loadSources()
|
|
}
|
|
|
|
const handleTest = async (id: string) => {
|
|
setTestResult(prev => ({ ...prev, [id]: 'testing...' }))
|
|
const result = await api(`sources/${id}/test`, { method: 'POST' })
|
|
setTestResult(prev => ({ ...prev, [id]: result?.message || 'Fehler' }))
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="flex justify-between items-center">
|
|
<h2 className="text-lg font-semibold text-gray-900">Crawl-Quellen</h2>
|
|
<button
|
|
onClick={() => setShowForm(!showForm)}
|
|
className="px-4 py-2 bg-purple-600 text-white text-sm font-medium rounded-lg hover:bg-purple-700"
|
|
>
|
|
+ Neue Quelle
|
|
</button>
|
|
</div>
|
|
|
|
{showForm && (
|
|
<div className="bg-white rounded-xl border border-gray-200 p-6 space-y-4">
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Name</label>
|
|
<input
|
|
value={formName}
|
|
onChange={e => setFormName(e.target.value)}
|
|
placeholder="z.B. Compliance-Ordner"
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500"
|
|
/>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">Pfad (relativ zu /data/crawl)</label>
|
|
<input
|
|
value={formPath}
|
|
onChange={e => setFormPath(e.target.value)}
|
|
placeholder="z.B. compliance-docs"
|
|
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500"
|
|
/>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<button onClick={handleCreate} className="px-4 py-2 bg-green-600 text-white text-sm rounded-lg hover:bg-green-700">
|
|
Erstellen
|
|
</button>
|
|
<button onClick={() => setShowForm(false)} className="px-4 py-2 text-gray-600 text-sm hover:text-gray-800">
|
|
Abbrechen
|
|
</button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{loading ? (
|
|
<div className="text-center py-12 text-gray-500">Laden...</div>
|
|
) : sources.length === 0 ? (
|
|
<div className="text-center py-12 text-gray-500 bg-white rounded-xl border border-gray-200">
|
|
<p className="text-lg font-medium">Keine Quellen konfiguriert</p>
|
|
<p className="text-sm mt-1">Erstellen Sie eine Crawl-Quelle um Dokumente zu scannen.</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-3">
|
|
{sources.map(s => (
|
|
<div key={s.id} className="bg-white rounded-xl border border-gray-200 p-5 flex items-center gap-4">
|
|
<div className={`w-3 h-3 rounded-full ${s.enabled ? 'bg-green-500' : 'bg-gray-300'}`} />
|
|
<div className="flex-1 min-w-0">
|
|
<div className="font-medium text-gray-900">{s.name}</div>
|
|
<div className="text-sm text-gray-500 truncate">{s.path}</div>
|
|
<div className="text-xs text-gray-400 mt-1">
|
|
Tiefe: {s.max_depth} | Formate: {(typeof s.file_extensions === 'string' ? JSON.parse(s.file_extensions) : s.file_extensions).join(', ')}
|
|
</div>
|
|
</div>
|
|
{testResult[s.id] && (
|
|
<span className="text-xs text-gray-500 bg-gray-50 px-2 py-1 rounded">{testResult[s.id]}</span>
|
|
)}
|
|
<button onClick={() => handleTest(s.id)} className="text-sm text-blue-600 hover:text-blue-800">Testen</button>
|
|
<button onClick={() => handleToggle(s)} className="text-sm text-gray-600 hover:text-gray-800">
|
|
{s.enabled ? 'Deaktivieren' : 'Aktivieren'}
|
|
</button>
|
|
<button onClick={() => handleDelete(s.id)} className="text-sm text-red-600 hover:text-red-800">Loeschen</button>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|