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>
355 lines
10 KiB
TypeScript
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 }
|
|
)
|
|
}
|
|
}
|