This repository has been archived on 2026-02-15. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
breakpilot-pwa/website/app/api/admin/edu-search/route.ts
Benjamin Admin bfdaf63ba9 fix: Restore all files lost during destructive rebase
A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.

This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).

Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 09:51:32 +01:00

355 lines
10 KiB
TypeScript

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