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:
451
website/app/api/admin/unity-bridge/route.ts
Normal file
451
website/app/api/admin/unity-bridge/route.ts
Normal file
@@ -0,0 +1,451 @@
|
||||
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<string, EndpointConfig> = {
|
||||
// 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<string, string> = {
|
||||
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<string, unknown>
|
||||
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<NextResponse> {
|
||||
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<NextResponse> {
|
||||
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<NextResponse> {
|
||||
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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user