import { NextRequest, NextResponse } from 'next/server' const UNITY_BRIDGE_URL = process.env.UNITY_BRIDGE_URL || 'http://localhost:8090' const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000' // Backend type for routing type BackendType = 'unity' | 'python' interface EndpointConfig { path: string backend: BackendType timeout?: number } // Python Backend endpoints for Unit System const pythonEndpoints: Record = { // Unit definitions 'units-list': { path: '/api/units/definitions', backend: 'python' }, 'units-health': { path: '/api/units/health', backend: 'python' }, // Analytics 'analytics-overview': { path: '/api/analytics/dashboard/overview', backend: 'python' }, 'analytics-misconceptions': { path: '/api/analytics/misconceptions', backend: 'python' }, // Teacher dashboard 'teacher-dashboard': { path: '/api/teacher/dashboard', backend: 'python' }, 'teacher-units': { path: '/api/teacher/units/available', backend: 'python' }, } // Streaming types interface StreamStatusResponse { is_streaming: boolean frame_count: number width: number height: number quality: number uptime_seconds: string } interface StreamFrameResponse { success: boolean frame_count?: number width?: number height?: number data?: string // Base64 encoded JPEG message?: string } // Type definitions interface BridgeStatus { status: string unity_version: string project: string scene: string is_playing: boolean is_compiling: boolean errors: number warnings: number } interface LogEntry { time: string type: string message: string frame: number stack?: string } interface LogsResponse { count: number total_errors: number total_warnings: number total_info: number logs: LogEntry[] } interface DiagnosticEntry { category: string severity: 'ok' | 'warning' | 'error' message: string } interface DiagnoseResponse { diagnostics: DiagnosticEntry[] errors: number warnings: number } /** * GET /api/admin/unity-bridge * * Proxy requests to the Unity AI Bridge running in the Unity Editor. * * Query Parameters: * - action: The endpoint to call (status, compile, logs, errors, warnings, scene, play, stop, quicksetup) * - limit: For logs, the number of entries to return (default: 50) * - name: For object endpoint, the name of the GameObject */ export async function GET(request: NextRequest) { const searchParams = request.nextUrl.searchParams const action = searchParams.get('action') || 'status' const limit = searchParams.get('limit') || '50' const objectName = searchParams.get('name') const unitId = searchParams.get('unit_id') const timeRange = searchParams.get('time_range') || 'month' // Check if this is a Python backend action if (pythonEndpoints[action]) { return handlePythonBackend(action, searchParams) } // Dynamic Python backend actions (with parameters) if (action === 'units-get' && unitId) { return fetchFromPython(`/api/units/definitions/${unitId}`) } if (action === 'analytics-learning-gain' && unitId) { return fetchFromPython(`/api/analytics/learning-gain/${unitId}?time_range=${timeRange}`) } if (action === 'analytics-stops' && unitId) { return fetchFromPython(`/api/analytics/unit/${unitId}/stops?time_range=${timeRange}`) } if (action === 'content-h5p' && unitId) { return fetchFromPython(`/api/units/content/${unitId}/h5p`) } if (action === 'content-worksheet' && unitId) { return fetchFromPython(`/api/units/content/${unitId}/worksheet`) } if (action === 'content-pdf' && unitId) { return fetchPdfFromPython(`/api/units/content/${unitId}/worksheet.pdf`) } // Map actions to Unity Bridge endpoints const endpointMap: Record = { status: '/status', compile: '/compile', logs: `/logs?limit=${limit}`, errors: '/logs/errors', warnings: '/logs/warnings', scene: '/scene', selection: '/selection', play: '/play', stop: '/stop', quicksetup: '/quicksetup', // Streaming endpoints 'stream-start': '/stream/start', 'stream-stop': '/stream/stop', 'stream-frame': '/stream/frame', 'stream-status': '/stream/status', } // Handle screenshot endpoint - returns binary image data if (action === 'screenshot') { try { const response = await fetch(`${UNITY_BRIDGE_URL}/screenshot`, { headers: { 'Accept': 'image/jpeg' }, signal: AbortSignal.timeout(5000), }) if (!response.ok) { return NextResponse.json( { error: `Screenshot failed with status ${response.status}` }, { status: response.status } ) } const imageBuffer = await response.arrayBuffer() return new NextResponse(imageBuffer, { headers: { 'Content-Type': 'image/jpeg', 'Cache-Control': 'no-cache, no-store, must-revalidate', }, }) } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error' return NextResponse.json( { error: 'Screenshot fehlgeschlagen', details: errorMessage, offline: true }, { status: 503 } ) } } // Handle object endpoint separately (needs name parameter) let endpoint: string if (action === 'object' && objectName) { endpoint = `/object/${encodeURIComponent(objectName)}` } else { endpoint = endpointMap[action] } if (!endpoint) { return NextResponse.json( { error: 'Unknown action', validActions: Object.keys(endpointMap) }, { status: 400 } ) } try { const response = await fetch(`${UNITY_BRIDGE_URL}${endpoint}`, { headers: { 'Accept': 'application/json' }, // Short timeout since it's localhost signal: AbortSignal.timeout(3000), }) if (!response.ok) { return NextResponse.json( { error: `Unity Bridge returned ${response.status}`, offline: false }, { status: response.status } ) } const data = await response.json() return NextResponse.json(data) } catch (error) { // Distinguish between timeout and connection refused const errorMessage = error instanceof Error ? error.message : 'Unknown error' const isTimeout = errorMessage.includes('timeout') || errorMessage.includes('aborted') return NextResponse.json( { error: isTimeout ? 'Unity Bridge timed out - Unity might be busy' : 'Unity Bridge nicht erreichbar - Server in Unity starten', offline: true, details: errorMessage, }, { status: 503 } ) } } /** * POST /api/admin/unity-bridge * * Execute commands on the Unity Bridge. * * Query Parameters: * - action: The command to execute (diagnose, execute, clear-logs) * * For execute action, the body should contain: * - action: The execute action (select, setactive, delete, create, menu, log) * - name: GameObject name (for select, setactive, delete) * - active: true/false (for setactive) * - type: Object type (for create: cube, sphere, empty, etc.) * - path: Menu path (for menu action) * - message: Log message (for log action) */ export async function POST(request: NextRequest) { const searchParams = request.nextUrl.searchParams const action = searchParams.get('action') if (action === 'diagnose') { try { const response = await fetch(`${UNITY_BRIDGE_URL}/diagnose`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}', // Diagnose can take longer signal: AbortSignal.timeout(10000), }) if (!response.ok) { return NextResponse.json( { error: `Diagnose failed with status ${response.status}` }, { status: response.status } ) } const data: DiagnoseResponse = await response.json() return NextResponse.json(data) } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error' return NextResponse.json( { error: 'Diagnose fehlgeschlagen', details: errorMessage }, { status: 503 } ) } } if (action === 'execute') { let body: Record try { body = await request.json() } catch { return NextResponse.json( { error: 'Invalid JSON body' }, { status: 400 } ) } // Validate required fields based on execute action const executeAction = body.action as string if (!executeAction) { return NextResponse.json( { error: 'Missing "action" field in body' }, { status: 400 } ) } try { const response = await fetch(`${UNITY_BRIDGE_URL}/execute`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), signal: AbortSignal.timeout(5000), }) if (!response.ok) { const errorData = await response.text() return NextResponse.json( { error: `Execute failed: ${errorData}` }, { status: response.status } ) } const data = await response.json() return NextResponse.json(data) } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error' return NextResponse.json( { error: 'Execute fehlgeschlagen', details: errorMessage }, { status: 503 } ) } } if (action === 'clear-logs') { try { const response = await fetch(`${UNITY_BRIDGE_URL}/logs/clear`, { method: 'GET', // Unity Bridge uses GET for clear signal: AbortSignal.timeout(3000), }) const data = await response.json() return NextResponse.json(data) } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error' return NextResponse.json( { error: 'Clear logs fehlgeschlagen', details: errorMessage }, { status: 503 } ) } } return NextResponse.json( { error: 'Unknown POST action', validActions: ['diagnose', 'execute', 'clear-logs'] }, { status: 400 } ) } // ======================================== // Python Backend Helper Functions // ======================================== /** * Handle requests to Python backend endpoints */ async function handlePythonBackend( action: string, searchParams: URLSearchParams ): Promise { const config = pythonEndpoints[action] if (!config) { return NextResponse.json( { error: 'Unknown Python backend action' }, { status: 400 } ) } // Build query string from search params (excluding action) const queryParams = new URLSearchParams() searchParams.forEach((value, key) => { if (key !== 'action') { queryParams.set(key, value) } }) const queryString = queryParams.toString() const url = queryString ? `${config.path}?${queryString}` : config.path return fetchFromPython(url, config.timeout) } /** * Fetch JSON from Python backend */ async function fetchFromPython( path: string, timeout: number = 5000 ): Promise { try { const response = await fetch(`${BACKEND_URL}${path}`, { headers: { 'Accept': 'application/json' }, signal: AbortSignal.timeout(timeout), }) if (!response.ok) { const errorText = await response.text() return NextResponse.json( { error: `Backend returned ${response.status}`, details: errorText }, { status: response.status } ) } const data = await response.json() return NextResponse.json(data) } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error' const isTimeout = errorMessage.includes('timeout') || errorMessage.includes('aborted') return NextResponse.json( { error: isTimeout ? 'Backend timed out' : 'Backend nicht erreichbar - Server starten mit: cd backend && python main.py', offline: true, details: errorMessage, }, { status: 503 } ) } } /** * Fetch PDF from Python backend */ async function fetchPdfFromPython(path: string): Promise { try { const response = await fetch(`${BACKEND_URL}${path}`, { headers: { 'Accept': 'application/pdf' }, signal: AbortSignal.timeout(15000), // PDF generation can take longer }) if (!response.ok) { return NextResponse.json( { error: `PDF generation failed with status ${response.status}` }, { status: response.status } ) } const pdfBuffer = await response.arrayBuffer() return new NextResponse(pdfBuffer, { headers: { 'Content-Type': 'application/pdf', 'Content-Disposition': 'attachment; filename="worksheet.pdf"', }, }) } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error' return NextResponse.json( { error: 'PDF generation fehlgeschlagen', details: errorMessage }, { status: 503 } ) } }