'use client' /** * University Crawler Control Panel * * Admin interface for managing the multi-phase university crawling system. * Allows starting/stopping the orchestrator, adding universities to the queue, * and monitoring crawl progress. */ import { useState, useEffect, useCallback } from 'react' import AdminLayout from '@/components/admin/AdminLayout' // Types matching the Go backend interface CrawlQueueItem { id: string university_id: string university_name: string university_short: string queue_position: number | null priority: number current_phase: CrawlPhase discovery_completed: boolean discovery_completed_at?: string professors_completed: boolean professors_completed_at?: string all_staff_completed: boolean all_staff_completed_at?: string publications_completed: boolean publications_completed_at?: string discovery_count: number professors_count: number staff_count: number publications_count: number retry_count: number max_retries: number last_error?: string started_at?: string completed_at?: string progress_percent: number created_at: string updated_at: string } type CrawlPhase = 'pending' | 'discovery' | 'professors' | 'all_staff' | 'publications' | 'completed' | 'failed' | 'paused' interface OrchestratorStatus { is_running: boolean current_university?: CrawlQueueItem current_phase: CrawlPhase queue_length: number completed_today: number total_processed: number last_activity?: string } interface University { id: string name: string short_name?: string url: string state?: string uni_type?: string } // Use local API proxy to avoid CORS issues and keep API keys server-side const API_BASE = '/api/admin/uni-crawler' // Phase display configuration const phaseConfig: Record = { pending: { label: 'Wartend', color: 'bg-gray-100 text-gray-700', icon: 'clock' }, discovery: { label: 'Discovery', color: 'bg-blue-100 text-blue-700', icon: 'search' }, professors: { label: 'Professoren', color: 'bg-indigo-100 text-indigo-700', icon: 'user' }, all_staff: { label: 'Alle Mitarbeiter', color: 'bg-purple-100 text-purple-700', icon: 'users' }, publications: { label: 'Publikationen', color: 'bg-orange-100 text-orange-700', icon: 'book' }, completed: { label: 'Abgeschlossen', color: 'bg-green-100 text-green-700', icon: 'check' }, failed: { label: 'Fehlgeschlagen', color: 'bg-red-100 text-red-700', icon: 'x' }, paused: { label: 'Pausiert', color: 'bg-yellow-100 text-yellow-700', icon: 'pause' } } export default function UniCrawlerPage() { const [status, setStatus] = useState(null) const [queue, setQueue] = useState([]) const [universities, setUniversities] = useState([]) const [loading, setLoading] = useState(false) const [error, setError] = useState(null) const [success, setSuccess] = useState(null) // Add to queue form const [selectedUniversity, setSelectedUniversity] = useState('') const [priority, setPriority] = useState(5) // Fetch status const fetchStatus = useCallback(async () => { try { const res = await fetch(`${API_BASE}?action=status`) if (res.ok) { const data = await res.json() setStatus(data) } } catch (err) { console.error('Failed to fetch status:', err) } }, []) // Fetch queue const fetchQueue = useCallback(async () => { try { const res = await fetch(`${API_BASE}?action=queue`) if (res.ok) { const data = await res.json() setQueue(data.queue || []) } } catch (err) { console.error('Failed to fetch queue:', err) } }, []) // Fetch universities (for dropdown) const fetchUniversities = useCallback(async () => { try { const res = await fetch(`${API_BASE}?action=universities`) if (res.ok) { const data = await res.json() // Handle null/undefined universities array from API const unis = data.universities ?? data ?? [] setUniversities(Array.isArray(unis) ? unis : []) } } catch (err) { console.error('Failed to fetch universities:', err) } }, []) // Initial load and polling useEffect(() => { fetchStatus() fetchQueue() fetchUniversities() // Poll every 5 seconds const interval = setInterval(() => { fetchStatus() fetchQueue() }, 5000) return () => clearInterval(interval) }, [fetchStatus, fetchQueue, fetchUniversities]) // Start orchestrator const handleStart = async () => { setLoading(true) setError(null) try { const res = await fetch(`${API_BASE}?action=start`, { method: 'POST' }) if (res.ok) { setSuccess('Orchestrator gestartet') fetchStatus() } else { const data = await res.json() setError(data.error || 'Start fehlgeschlagen') } } catch (err) { setError('Verbindungsfehler') } finally { setLoading(false) } } // Stop orchestrator const handleStop = async () => { setLoading(true) setError(null) try { const res = await fetch(`${API_BASE}?action=stop`, { method: 'POST' }) if (res.ok) { setSuccess('Orchestrator gestoppt') fetchStatus() } else { const data = await res.json() setError(data.error || 'Stop fehlgeschlagen') } } catch (err) { setError('Verbindungsfehler') } finally { setLoading(false) } } // Add university to queue const handleAddToQueue = async (e: React.FormEvent) => { e.preventDefault() if (!selectedUniversity) return setLoading(true) setError(null) try { const res = await fetch(`${API_BASE}?action=queue`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ university_id: selectedUniversity, priority: priority, initiated_by: 'admin-ui' }) }) if (res.ok) { setSuccess('Universitaet zur Queue hinzugefuegt') setSelectedUniversity('') setPriority(5) fetchQueue() } else { const data = await res.json() setError(data.error || 'Hinzufuegen fehlgeschlagen') } } catch (err) { setError('Verbindungsfehler') } finally { setLoading(false) } } // Remove from queue const handleRemove = async (universityId: string) => { if (!confirm('Wirklich aus der Queue entfernen?')) return try { const res = await fetch(`${API_BASE}?university_id=${universityId}`, { method: 'DELETE' }) if (res.ok) { fetchQueue() } } catch (err) { console.error('Remove failed:', err) } } // Pause/Resume const handlePause = async (universityId: string) => { try { const res = await fetch(`${API_BASE}?action=pause&university_id=${universityId}`, { method: 'POST' }) if (res.ok) { fetchQueue() } } catch (err) { console.error('Pause failed:', err) } } const handleResume = async (universityId: string) => { try { const res = await fetch(`${API_BASE}?action=resume&university_id=${universityId}`, { method: 'POST' }) if (res.ok) { fetchQueue() } } catch (err) { console.error('Resume failed:', err) } } // Clear messages after 5 seconds useEffect(() => { if (success) { const timer = setTimeout(() => setSuccess(null), 5000) return () => clearTimeout(timer) } }, [success]) useEffect(() => { if (error) { const timer = setTimeout(() => setError(null), 5000) return () => clearTimeout(timer) } }, [error]) return ( {/* Messages */} {error && (
{error}
)} {success && (
{success}
)}
{/* Status Card */}

Orchestrator Status

{/* Status Indicator */}
{status?.is_running ? 'Laeuft' : 'Gestoppt'}
{/* Control Buttons */}
{/* Stats */}
In Queue: {status?.queue_length || 0}
Heute abgeschlossen: {status?.completed_today || 0}
Gesamt verarbeitet: {status?.total_processed || 0}
{status?.last_activity && (
Letzte Aktivitaet: {new Date(status.last_activity).toLocaleTimeString('de-DE')}
)}
{/* Current University */} {status?.current_university && (

Aktuelle Verarbeitung

{status.current_university.university_name}

{phaseConfig[status.current_phase].label} {status.current_university.progress_percent}%
)}
{/* Add to Queue Form */}

Zur Queue hinzufuegen

setPriority(Number(e.target.value))} className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500" />

Hoeher = dringender

{/* Queue List */}

Crawl Queue ({queue.length})

{queue.length === 0 ? (
Queue ist leer. Fuege Universitaeten hinzu, um das Crawling zu starten.
) : (
{queue.map((item) => (
{/* Header */}
#{item.queue_position || '-'}

{item.university_name}

({item.university_short})
{/* Phase Badge */}
{phaseConfig[item.current_phase].label} Prioritaet: {item.priority} {item.retry_count > 0 && ( Versuch {item.retry_count}/{item.max_retries} )}
{/* Progress Bar */}
{item.progress_percent}% {item.discovery_count} Disc / {item.professors_count} Prof / {item.staff_count} Staff / {item.publications_count} Pub
{/* Phase Checkmarks */}
{item.discovery_completed ? 'Discovery' : 'Discovery'} {item.professors_completed ? 'Professoren' : 'Professoren'} {item.all_staff_completed ? 'Mitarbeiter' : 'Mitarbeiter'} {item.publications_completed ? 'Publikationen' : 'Publikationen'}
{/* Error Message */} {item.last_error && (

{item.last_error}

)}
{/* Actions */}
{item.current_phase === 'paused' ? ( ) : item.current_phase !== 'completed' && item.current_phase !== 'failed' ? ( ) : null}
))}
)}
) }