/** * API Proxy for edu-search-service Admin Operations * * Provides secure server-side access to edu-search-service API * with API key authentication handled server-side * * Seeds are fetched from Python Backend (port 8000) * Other operations (stats, crawl) go to Go edu-search-service (port 8084) */ import { NextRequest, NextResponse } from 'next/server' const EDU_SEARCH_URL = process.env.EDU_SEARCH_URL || 'http://localhost:8084' const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000' const EDU_SEARCH_API_KEY = process.env.EDU_SEARCH_API_KEY || '' // GET: Fetch seeds, stats, or health export async function GET(request: NextRequest) { const searchParams = request.nextUrl.searchParams const action = searchParams.get('action') try { // Seeds come from Python Backend - fetch all pages for admin view if (action === 'seeds') { const category = searchParams.get('category') const pageSize = 200 // Max allowed by backend // Fetch first page to get total count let seedsUrl = `${BACKEND_URL}/v1/edu-search/seeds?page=1&page_size=${pageSize}` if (category) { seedsUrl += `&category=${category}` } const firstResponse = await fetch(seedsUrl, { headers: { 'Content-Type': 'application/json' }, }) if (!firstResponse.ok) { throw new Error(`Backend responded with ${firstResponse.status}`) } const firstData = await firstResponse.json() let allSeeds = firstData.seeds || [] const total = firstData.total || allSeeds.length // Fetch additional pages if needed const totalPages = Math.ceil(total / pageSize) for (let page = 2; page <= totalPages; page++) { let nextUrl = `${BACKEND_URL}/v1/edu-search/seeds?page=${page}&page_size=${pageSize}` if (category) { nextUrl += `&category=${category}` } const nextResponse = await fetch(nextUrl, { headers: { 'Content-Type': 'application/json' }, }) if (nextResponse.ok) { const nextData = await nextResponse.json() allSeeds = allSeeds.concat(nextData.seeds || []) } } return NextResponse.json({ seeds: allSeeds, total: total, page: 1, page_size: total, }) } // Categories come from Python Backend if (action === 'categories') { const categoriesResponse = await fetch(`${BACKEND_URL}/v1/edu-search/categories`, { headers: { 'Content-Type': 'application/json' }, }) if (!categoriesResponse.ok) { throw new Error(`Backend responded with ${categoriesResponse.status}`) } const data = await categoriesResponse.json() // Backend returns array directly, wrap it for frontend compatibility const categories = Array.isArray(data) ? data : data.categories || [] return NextResponse.json({ categories }) } // Stats come from Python Backend if (action === 'stats') { const statsResponse = await fetch(`${BACKEND_URL}/v1/edu-search/stats`, { headers: { 'Content-Type': 'application/json' }, }) if (!statsResponse.ok) { throw new Error(`Backend responded with ${statsResponse.status}`) } const data = await statsResponse.json() return NextResponse.json(data) } // Other actions go to Go edu-search-service let endpoint = '/v1/health' switch (action) { case 'stats': endpoint = '/v1/admin/stats' break case 'health': endpoint = '/v1/health' break default: endpoint = '/v1/health' } const response = await fetch(`${EDU_SEARCH_URL}${endpoint}`, { headers: { 'Authorization': `Bearer ${EDU_SEARCH_API_KEY}`, 'Content-Type': 'application/json', }, }) if (!response.ok) { // Return mock data for development when service is not running if (action === 'stats') { return NextResponse.json({ totalDocuments: 15234, totalSeeds: 127, lastCrawlTime: new Date().toISOString(), crawlStatus: 'idle', documentsPerCategory: { federal: 4521, states: 5234, science: 1234, universities: 2345, portals: 1900, }, documentsPerDocType: { Lehrplan: 2345, Arbeitsblatt: 4567, Unterrichtsentwurf: 1234, Erlass_Verordnung: 890, Pruefung_Abitur: 567, Studie_Bericht: 345, Sonstiges: 5286, }, avgTrustScore: 0.72, }) } // Seeds are now handled by Python Backend above, this case shouldn't be reached return NextResponse.json({ status: 'mock', message: 'edu-search-service not available, using mock data', }) } const data = await response.json() return NextResponse.json(data) } catch (error) { console.error('edu-search API error:', error) // Return mock data on error if (action === 'health') { return NextResponse.json({ status: 'mock', opensearch: 'unavailable', service: 'edu-search-service', version: '0.1.0', }) } return NextResponse.json( { error: 'Failed to connect to edu-search-service' }, { status: 503 } ) } } // POST: Create seed (via Python Backend), or start crawl (via Go service) export async function POST(request: NextRequest) { const searchParams = request.nextUrl.searchParams const action = searchParams.get('action') try { const body = await request.json() // Seed creation goes to Python Backend if (action === 'seed') { const response = await fetch(`${BACKEND_URL}/v1/edu-search/seeds`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(body), }) if (!response.ok) { const errData = await response.json().catch(() => ({})) return NextResponse.json( { error: errData.detail || `Backend responded with ${response.status}` }, { status: response.status } ) } const data = await response.json() return NextResponse.json(data) } // Crawl start - calls both Legal Crawler (Python) and Go edu-search-service if (action === 'crawl') { const results: { legal?: unknown; eduSearch?: unknown } = {} // 1. Start Legal Crawler (Python Backend) - crawls Schulgesetze try { const legalResponse = await fetch(`${BACKEND_URL}/v1/legal-crawler/start`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, }) if (legalResponse.ok) { results.legal = await legalResponse.json() } else { results.legal = { status: 'error', message: `Legal crawler returned ${legalResponse.status}` } } } catch (err) { results.legal = { status: 'error', message: 'Legal crawler not available' } } // 2. Start Go edu-search-service crawler (if available) try { const eduResponse = await fetch(`${EDU_SEARCH_URL}/v1/admin/crawl/start`, { method: 'POST', headers: { 'Authorization': `Bearer ${EDU_SEARCH_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify(body), }) if (eduResponse.ok) { results.eduSearch = await eduResponse.json() } else { results.eduSearch = { status: 'skipped', message: 'Go edu-search-service not available' } } } catch (err) { results.eduSearch = { status: 'skipped', message: 'Go edu-search-service not running' } } return NextResponse.json({ status: 'started', message: 'Crawl gestartet', results, }) } // Legal Crawler Status if (action === 'legal-crawler-status') { try { const response = await fetch(`${BACKEND_URL}/v1/legal-crawler/status`, { headers: { 'Content-Type': 'application/json' }, }) if (response.ok) { const data = await response.json() return NextResponse.json(data) } } catch (err) { // Fall through to error response } return NextResponse.json({ status: 'error', message: 'Legal crawler not available' }) } return NextResponse.json({ error: 'Invalid action' }, { status: 400 }) } catch (error) { console.error('edu-search API POST error:', error) return NextResponse.json( { error: 'Failed to process request' }, { status: 500 } ) } } // PUT: Update seed (via Python Backend) export async function PUT(request: NextRequest) { const searchParams = request.nextUrl.searchParams const seedId = searchParams.get('id') if (!seedId) { return NextResponse.json({ error: 'Seed ID required' }, { status: 400 }) } try { const body = await request.json() const response = await fetch(`${BACKEND_URL}/v1/edu-search/seeds/${seedId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify(body), }) if (!response.ok) { const errData = await response.json().catch(() => ({})) return NextResponse.json( { error: errData.detail || `Backend responded with ${response.status}` }, { status: response.status } ) } const data = await response.json() return NextResponse.json(data) } catch (error) { console.error('edu-search API PUT error:', error) return NextResponse.json( { error: 'Failed to update seed' }, { status: 500 } ) } } // DELETE: Remove seed (via Python Backend) export async function DELETE(request: NextRequest) { const searchParams = request.nextUrl.searchParams const seedId = searchParams.get('id') if (!seedId) { return NextResponse.json({ error: 'Seed ID required' }, { status: 400 }) } try { const response = await fetch(`${BACKEND_URL}/v1/edu-search/seeds/${seedId}`, { method: 'DELETE', headers: { 'Content-Type': 'application/json', }, }) if (!response.ok) { const errData = await response.json().catch(() => ({})) return NextResponse.json( { error: errData.detail || `Backend responded with ${response.status}` }, { status: response.status } ) } return NextResponse.json({ deleted: true, id: seedId }) } catch (error) { console.error('edu-search API DELETE error:', error) return NextResponse.json( { error: 'Failed to delete seed' }, { status: 500 } ) } }