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>
This commit is contained in:
354
website/app/api/admin/edu-search/route.ts
Normal file
354
website/app/api/admin/edu-search/route.ts
Normal file
@@ -0,0 +1,354 @@
|
||||
/**
|
||||
* 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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user