Initial commit: breakpilot-lehrer - Lehrer KI Platform

Services: Admin-Lehrer, Backend-Lehrer, Studio v2, Website,
Klausur-Service, School-Service, Voice-Service, Geo-Service,
BreakPilot Drive, Agent-Core

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Boenisch
2026-02-11 23:47:26 +01:00
commit 5a31f52310
1224 changed files with 425430 additions and 0 deletions

View File

@@ -0,0 +1,275 @@
import { NextRequest, NextResponse } from 'next/server'
import * as fs from 'fs/promises'
import * as path from 'path'
/**
* Individual Agent API
*
* GET - Get agent details including SOUL content
* PUT - Update agent configuration
* DELETE - Delete agent
*/
const AGENT_CORE_PATH = process.env.AGENT_CORE_PATH || '/app/agent-core'
const SOUL_PATH = path.join(AGENT_CORE_PATH, 'soul')
interface AgentDetails {
id: string
name: string
description: string
soulFile: string
soulContent: string
color: string
icon: string
status: 'running' | 'paused' | 'stopped' | 'error'
activeSessions: number
totalProcessed: number
avgResponseTime: number
errorRate: number
lastActivity: string
version: string
createdAt: string
updatedAt: string
}
// Agent metadata (in production, store in database)
const agentMetadata: Record<string, Partial<AgentDetails>> = {
'tutor-agent': {
name: 'TutorAgent',
description: 'Lernbegleitung und Fragen beantworten',
color: '#3b82f6',
icon: 'brain'
},
'grader-agent': {
name: 'GraderAgent',
description: 'Klausur-Korrektur und Bewertung',
color: '#10b981',
icon: 'bot'
},
'quality-judge': {
name: 'QualityJudge',
description: 'BQAS Qualitaetspruefung',
color: '#f59e0b',
icon: 'settings'
},
'alert-agent': {
name: 'AlertAgent',
description: 'Monitoring und Benachrichtigungen',
color: '#ef4444',
icon: 'alert'
},
'orchestrator': {
name: 'Orchestrator',
description: 'Task-Koordination und Routing',
color: '#8b5cf6',
icon: 'message'
}
}
// Read SOUL file content
async function readSoulFile(agentId: string): Promise<string | null> {
const soulFile = `${agentId}.soul.md`
const soulFilePath = path.join(SOUL_PATH, soulFile)
try {
const content = await fs.readFile(soulFilePath, 'utf-8')
return content
} catch {
return null
}
}
// Get file stats
async function getFileStats(agentId: string): Promise<{ createdAt: string; updatedAt: string } | null> {
const soulFile = `${agentId}.soul.md`
const soulFilePath = path.join(SOUL_PATH, soulFile)
try {
const stats = await fs.stat(soulFilePath)
return {
createdAt: stats.birthtime.toISOString(),
updatedAt: stats.mtime.toISOString()
}
} catch {
return null
}
}
// Mock agent stats (in production, query from Redis/PostgreSQL)
function getMockStats(agentId: string) {
const mockStats: Record<string, { activeSessions: number; totalProcessed: number; avgResponseTime: number; errorRate: number }> = {
'tutor-agent': { activeSessions: 12, totalProcessed: 1847, avgResponseTime: 234, errorRate: 0.3 },
'grader-agent': { activeSessions: 3, totalProcessed: 456, avgResponseTime: 1205, errorRate: 0.5 },
'quality-judge': { activeSessions: 8, totalProcessed: 3291, avgResponseTime: 89, errorRate: 0.1 },
'alert-agent': { activeSessions: 1, totalProcessed: 892, avgResponseTime: 45, errorRate: 0.0 },
'orchestrator': { activeSessions: 24, totalProcessed: 8934, avgResponseTime: 12, errorRate: 0.2 }
}
return mockStats[agentId] || { activeSessions: 0, totalProcessed: 0, avgResponseTime: 0, errorRate: 0 }
}
// GET - Get agent details
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ agentId: string }> }
) {
try {
const { agentId } = await params
const soulContent = await readSoulFile(agentId)
if (!soulContent) {
return NextResponse.json(
{ error: 'Agent not found' },
{ status: 404 }
)
}
const fileStats = await getFileStats(agentId)
const metadata = agentMetadata[agentId] || {}
const stats = getMockStats(agentId)
const agent: AgentDetails = {
id: agentId,
name: metadata.name || agentId.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(''),
description: metadata.description || 'Custom agent',
soulFile: `${agentId}.soul.md`,
soulContent,
color: metadata.color || '#6b7280',
icon: metadata.icon || 'bot',
status: 'running',
...stats,
lastActivity: 'just now',
version: '1.0.0',
createdAt: fileStats?.createdAt || new Date().toISOString(),
updatedAt: fileStats?.updatedAt || new Date().toISOString()
}
return NextResponse.json(agent)
} catch (error) {
console.error('Error fetching agent:', error)
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Failed to fetch agent' },
{ status: 500 }
)
}
}
// PUT - Update agent
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ agentId: string }> }
) {
try {
const { agentId } = await params
const body = await request.json()
const { soulContent, name, description, color, icon } = body
const soulFile = `${agentId}.soul.md`
const soulFilePath = path.join(SOUL_PATH, soulFile)
// Check if agent exists
try {
await fs.access(soulFilePath)
} catch {
return NextResponse.json(
{ error: 'Agent not found' },
{ status: 404 }
)
}
// Update SOUL file if content provided
if (soulContent) {
// Create backup before updating
const backupPath = path.join(SOUL_PATH, '.backups', `${agentId}-${Date.now()}.soul.md`)
try {
await fs.mkdir(path.dirname(backupPath), { recursive: true })
const currentContent = await fs.readFile(soulFilePath, 'utf-8')
await fs.writeFile(backupPath, currentContent, 'utf-8')
} catch {
// Backup failed, continue anyway
}
// Write new content
await fs.writeFile(soulFilePath, soulContent, 'utf-8')
}
// In production, update metadata in database
// For now, just return success
return NextResponse.json({
success: true,
message: `Agent ${agentId} updated successfully`,
agent: {
id: agentId,
name: name || agentMetadata[agentId]?.name || agentId,
description: description || agentMetadata[agentId]?.description || '',
soulFile,
color: color || agentMetadata[agentId]?.color || '#6b7280',
icon: icon || agentMetadata[agentId]?.icon || 'bot',
updatedAt: new Date().toISOString()
}
})
} catch (error) {
console.error('Error updating agent:', error)
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Failed to update agent' },
{ status: 500 }
)
}
}
// DELETE - Delete agent
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ agentId: string }> }
) {
try {
const { agentId } = await params
// Don't allow deleting core agents
const coreAgents = ['tutor-agent', 'grader-agent', 'quality-judge', 'alert-agent', 'orchestrator']
if (coreAgents.includes(agentId)) {
return NextResponse.json(
{ error: 'Cannot delete core system agent' },
{ status: 403 }
)
}
const soulFile = `${agentId}.soul.md`
const soulFilePath = path.join(SOUL_PATH, soulFile)
// Check if agent exists
try {
await fs.access(soulFilePath)
} catch {
return NextResponse.json(
{ error: 'Agent not found' },
{ status: 404 }
)
}
// Create backup before deleting
const backupPath = path.join(SOUL_PATH, '.deleted', `${agentId}-${Date.now()}.soul.md`)
try {
await fs.mkdir(path.dirname(backupPath), { recursive: true })
const content = await fs.readFile(soulFilePath, 'utf-8')
await fs.writeFile(backupPath, content, 'utf-8')
} catch {
// Backup failed, continue anyway
}
// Delete the file
await fs.unlink(soulFilePath)
return NextResponse.json({
success: true,
message: `Agent ${agentId} deleted successfully`
})
} catch (error) {
console.error('Error deleting agent:', error)
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Failed to delete agent' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,187 @@
import { NextRequest, NextResponse } from 'next/server'
import * as fs from 'fs/promises'
import * as path from 'path'
/**
* Agent SOUL File API
*
* GET - Get SOUL file content
* PUT - Update SOUL file content
* GET /history - Get version history
*/
const AGENT_CORE_PATH = process.env.AGENT_CORE_PATH || '/app/agent-core'
const SOUL_PATH = path.join(AGENT_CORE_PATH, 'soul')
interface SoulVersion {
version: string
timestamp: string
content: string
author: string
changes: string
}
// Read SOUL file content
async function readSoulFile(agentId: string): Promise<string | null> {
const soulFile = `${agentId}.soul.md`
const soulFilePath = path.join(SOUL_PATH, soulFile)
try {
const content = await fs.readFile(soulFilePath, 'utf-8')
return content
} catch {
return null
}
}
// Get version history from backup directory
async function getVersionHistory(agentId: string): Promise<SoulVersion[]> {
const backupDir = path.join(SOUL_PATH, '.backups')
const versions: SoulVersion[] = []
try {
const files = await fs.readdir(backupDir)
const agentBackups = files.filter(f => f.startsWith(`${agentId}-`) && f.endsWith('.soul.md'))
for (const file of agentBackups.slice(-10)) { // Last 10 versions
const filePath = path.join(backupDir, file)
const stats = await fs.stat(filePath)
const content = await fs.readFile(filePath, 'utf-8')
// Extract timestamp from filename
const match = file.match(/-(\d+)\.soul\.md$/)
const timestamp = match ? new Date(parseInt(match[1])).toISOString() : stats.mtime.toISOString()
versions.push({
version: file.replace('.soul.md', ''),
timestamp,
content,
author: 'Admin',
changes: 'Manual update'
})
}
// Sort by timestamp descending
versions.sort((a, b) => new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime())
} catch {
// No backup directory or can't read
}
return versions
}
// GET - Get SOUL file content
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ agentId: string }> }
) {
try {
const { agentId } = await params
const url = new URL(request.url)
const includeHistory = url.searchParams.get('history') === 'true'
const content = await readSoulFile(agentId)
if (!content) {
return NextResponse.json(
{ error: 'SOUL file not found' },
{ status: 404 }
)
}
const soulFile = `${agentId}.soul.md`
const soulFilePath = path.join(SOUL_PATH, soulFile)
const stats = await fs.stat(soulFilePath)
const response: {
agentId: string
soulFile: string
content: string
updatedAt: string
size: number
history?: SoulVersion[]
} = {
agentId,
soulFile,
content,
updatedAt: stats.mtime.toISOString(),
size: stats.size
}
if (includeHistory) {
response.history = await getVersionHistory(agentId)
}
return NextResponse.json(response)
} catch (error) {
console.error('Error fetching SOUL file:', error)
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Failed to fetch SOUL file' },
{ status: 500 }
)
}
}
// PUT - Update SOUL file content
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ agentId: string }> }
) {
try {
const { agentId } = await params
const body = await request.json()
const { content, author, changeDescription } = body
if (!content) {
return NextResponse.json(
{ error: 'content is required' },
{ status: 400 }
)
}
const soulFile = `${agentId}.soul.md`
const soulFilePath = path.join(SOUL_PATH, soulFile)
// Check if file exists
try {
await fs.access(soulFilePath)
} catch {
return NextResponse.json(
{ error: 'SOUL file not found' },
{ status: 404 }
)
}
// Create backup before updating
const backupDir = path.join(SOUL_PATH, '.backups')
const backupPath = path.join(backupDir, `${agentId}-${Date.now()}.soul.md`)
try {
await fs.mkdir(backupDir, { recursive: true })
const currentContent = await fs.readFile(soulFilePath, 'utf-8')
await fs.writeFile(backupPath, currentContent, 'utf-8')
} catch (backupError) {
console.warn('Failed to create backup:', backupError)
}
// Write new content
await fs.writeFile(soulFilePath, content, 'utf-8')
const stats = await fs.stat(soulFilePath)
return NextResponse.json({
success: true,
message: `SOUL file for ${agentId} updated successfully`,
agentId,
soulFile,
updatedAt: stats.mtime.toISOString(),
size: stats.size,
author: author || 'Admin',
changeDescription: changeDescription || 'Manual update'
})
} catch (error) {
console.error('Error updating SOUL file:', error)
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Failed to update SOUL file' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,282 @@
import { NextRequest, NextResponse } from 'next/server'
import * as fs from 'fs/promises'
import * as path from 'path'
/**
* Agent Management API
*
* GET - List all agents with their status and configuration
* POST - Create a new agent (with SOUL file)
*/
const AGENT_CORE_PATH = process.env.AGENT_CORE_PATH || '/app/agent-core'
const SOUL_PATH = path.join(AGENT_CORE_PATH, 'soul')
interface AgentConfig {
id: string
name: string
description: string
soulFile: string
color: string
icon: string
status: 'running' | 'paused' | 'stopped' | 'error'
activeSessions: number
totalProcessed: number
avgResponseTime: number
lastActivity: string
version: string
}
interface AgentStats {
totalSessions: number
activeSessions: number
totalMessages: number
avgLatency: number
errorRate: number
memoryUsage: number
}
// Default agent configurations
const defaultAgents: AgentConfig[] = [
{
id: 'tutor-agent',
name: 'TutorAgent',
description: 'Lernbegleitung und Fragen beantworten',
soulFile: 'tutor-agent.soul.md',
color: '#3b82f6',
icon: 'brain',
status: 'running',
activeSessions: 0,
totalProcessed: 0,
avgResponseTime: 0,
lastActivity: new Date().toISOString(),
version: '1.0.0'
},
{
id: 'grader-agent',
name: 'GraderAgent',
description: 'Klausur-Korrektur und Bewertung',
soulFile: 'grader-agent.soul.md',
color: '#10b981',
icon: 'bot',
status: 'running',
activeSessions: 0,
totalProcessed: 0,
avgResponseTime: 0,
lastActivity: new Date().toISOString(),
version: '1.0.0'
},
{
id: 'quality-judge',
name: 'QualityJudge',
description: 'BQAS Qualitaetspruefung',
soulFile: 'quality-judge.soul.md',
color: '#f59e0b',
icon: 'settings',
status: 'running',
activeSessions: 0,
totalProcessed: 0,
avgResponseTime: 0,
lastActivity: new Date().toISOString(),
version: '1.0.0'
},
{
id: 'alert-agent',
name: 'AlertAgent',
description: 'Monitoring und Benachrichtigungen',
soulFile: 'alert-agent.soul.md',
color: '#ef4444',
icon: 'alert',
status: 'running',
activeSessions: 0,
totalProcessed: 0,
avgResponseTime: 0,
lastActivity: new Date().toISOString(),
version: '1.0.0'
},
{
id: 'orchestrator',
name: 'Orchestrator',
description: 'Task-Koordination und Routing',
soulFile: 'orchestrator.soul.md',
color: '#8b5cf6',
icon: 'message',
status: 'running',
activeSessions: 0,
totalProcessed: 0,
avgResponseTime: 0,
lastActivity: new Date().toISOString(),
version: '1.0.0'
}
]
// Check if SOUL file exists
async function checkSoulFile(filename: string): Promise<boolean> {
try {
await fs.access(path.join(SOUL_PATH, filename))
return true
} catch {
return false
}
}
// Get agent status from Redis/API (mock for now)
async function getAgentStatus(agentId: string): Promise<{
status: 'running' | 'paused' | 'stopped' | 'error'
activeSessions: number
totalProcessed: number
avgResponseTime: number
lastActivity: string
}> {
// In production, query Redis/PostgreSQL for real stats
// For now, return mock data with some variation
const mockStats = {
'tutor-agent': { activeSessions: 12, totalProcessed: 1847, avgResponseTime: 234, lastActivity: '2 min ago' },
'grader-agent': { activeSessions: 3, totalProcessed: 456, avgResponseTime: 1205, lastActivity: '5 min ago' },
'quality-judge': { activeSessions: 8, totalProcessed: 3291, avgResponseTime: 89, lastActivity: '1 min ago' },
'alert-agent': { activeSessions: 1, totalProcessed: 892, avgResponseTime: 45, lastActivity: '30 sec ago' },
'orchestrator': { activeSessions: 24, totalProcessed: 8934, avgResponseTime: 12, lastActivity: 'just now' }
}
const stats = mockStats[agentId as keyof typeof mockStats] || {
activeSessions: 0,
totalProcessed: 0,
avgResponseTime: 0,
lastActivity: 'never'
}
return {
status: 'running',
...stats
}
}
// GET - List all agents
export async function GET() {
try {
const agents: AgentConfig[] = []
for (const defaultAgent of defaultAgents) {
const soulExists = await checkSoulFile(defaultAgent.soulFile)
const status = await getAgentStatus(defaultAgent.id)
agents.push({
...defaultAgent,
...status,
status: soulExists ? status.status : 'error'
})
}
// Also check for any additional SOUL files
try {
const soulFiles = await fs.readdir(SOUL_PATH)
for (const file of soulFiles) {
if (file.endsWith('.soul.md')) {
const agentId = file.replace('.soul.md', '')
if (!agents.find(a => a.id === agentId)) {
const status = await getAgentStatus(agentId)
agents.push({
id: agentId,
name: agentId.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(''),
description: 'Custom agent',
soulFile: file,
color: '#6b7280',
icon: 'bot',
version: '1.0.0',
...status
})
}
}
}
} catch {
// SOUL directory doesn't exist or isn't accessible
}
// Calculate aggregate stats
const stats: AgentStats = {
totalSessions: agents.reduce((sum, a) => sum + a.totalProcessed, 0),
activeSessions: agents.reduce((sum, a) => sum + a.activeSessions, 0),
totalMessages: agents.reduce((sum, a) => sum + a.totalProcessed, 0) * 3, // estimate
avgLatency: Math.round(agents.reduce((sum, a) => sum + a.avgResponseTime, 0) / agents.length),
errorRate: 0.8, // mock
memoryUsage: 67 // mock
}
return NextResponse.json({
agents,
stats,
timestamp: new Date().toISOString()
})
} catch (error) {
console.error('Error fetching agents:', error)
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Failed to fetch agents' },
{ status: 500 }
)
}
}
// POST - Create new agent
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { id, name, description, soulContent, color, icon } = body
if (!id || !name || !soulContent) {
return NextResponse.json(
{ error: 'id, name, and soulContent are required' },
{ status: 400 }
)
}
// Validate agent ID format
if (!/^[a-z0-9-]+$/.test(id)) {
return NextResponse.json(
{ error: 'Agent ID must contain only lowercase letters, numbers, and hyphens' },
{ status: 400 }
)
}
const soulFile = `${id}.soul.md`
const soulFilePath = path.join(SOUL_PATH, soulFile)
// Check if agent already exists
const exists = await checkSoulFile(soulFile)
if (exists) {
return NextResponse.json(
{ error: 'Agent with this ID already exists' },
{ status: 409 }
)
}
// Create SOUL file
await fs.writeFile(soulFilePath, soulContent, 'utf-8')
const newAgent: AgentConfig = {
id,
name,
description: description || '',
soulFile,
color: color || '#6b7280',
icon: icon || 'bot',
status: 'stopped',
activeSessions: 0,
totalProcessed: 0,
avgResponseTime: 0,
lastActivity: new Date().toISOString(),
version: '1.0.0'
}
return NextResponse.json({
success: true,
agent: newAgent,
message: `Agent ${name} created successfully`
}, { status: 201 })
} catch (error) {
console.error('Error creating agent:', error)
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Failed to create agent' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,240 @@
import { NextRequest, NextResponse } from 'next/server'
/**
* Agent Sessions API
*
* GET - List all active agent sessions
* POST - Create/start a new session (for testing)
*/
interface AgentSession {
id: string
agentType: string
agentId: string
userId: string
userName: string
state: 'active' | 'paused' | 'completed' | 'failed'
createdAt: string
lastActivity: string
checkpointCount: number
messagesProcessed: number
currentTask: string | null
avgResponseTime: number
}
// Mock sessions data (in production, query from PostgreSQL/Redis)
function getMockSessions(): AgentSession[] {
const now = new Date()
return [
{
id: `session-${Date.now()}-001`,
agentType: 'tutor-agent',
agentId: 'tutor-1',
userId: 'user-123',
userName: 'Max Mustermann',
state: 'active',
createdAt: new Date(now.getTime() - 75 * 60000).toISOString(),
lastActivity: new Date(now.getTime() - 37000).toISOString(),
checkpointCount: 5,
messagesProcessed: 23,
currentTask: 'Erklaere Quadratische Funktionen',
avgResponseTime: 245
},
{
id: `session-${Date.now()}-002`,
agentType: 'tutor-agent',
agentId: 'tutor-2',
userId: 'user-456',
userName: 'Anna Schmidt',
state: 'active',
createdAt: new Date(now.getTime() - 45 * 60000).toISOString(),
lastActivity: new Date(now.getTime() - 108000).toISOString(),
checkpointCount: 3,
messagesProcessed: 12,
currentTask: 'Hilfe bei Gedichtanalyse',
avgResponseTime: 312
},
{
id: `session-${Date.now()}-003`,
agentType: 'grader-agent',
agentId: 'grader-1',
userId: 'user-789',
userName: 'Frau Mueller (Lehrerin)',
state: 'active',
createdAt: new Date(now.getTime() - 105 * 60000).toISOString(),
lastActivity: new Date(now.getTime() - 180000).toISOString(),
checkpointCount: 12,
messagesProcessed: 45,
currentTask: 'Korrektur Klausur 10b - Arbeit 7/24',
avgResponseTime: 1205
},
{
id: `session-${Date.now()}-004`,
agentType: 'quality-judge',
agentId: 'judge-1',
userId: 'system',
userName: 'System (BQAS)',
state: 'active',
createdAt: new Date(now.getTime() - 465 * 60000).toISOString(),
lastActivity: new Date(now.getTime() - 1000).toISOString(),
checkpointCount: 156,
messagesProcessed: 892,
currentTask: 'Quality Check Queue Processing',
avgResponseTime: 89
},
{
id: `session-${Date.now()}-005`,
agentType: 'orchestrator',
agentId: 'orchestrator-main',
userId: 'system',
userName: 'System',
state: 'active',
createdAt: new Date(now.getTime() - 945 * 60000).toISOString(),
lastActivity: now.toISOString(),
checkpointCount: 2341,
messagesProcessed: 8934,
currentTask: 'Routing incoming requests',
avgResponseTime: 12
},
{
id: `session-${Date.now()}-006`,
agentType: 'tutor-agent',
agentId: 'tutor-3',
userId: 'user-101',
userName: 'Tim Berger',
state: 'paused',
createdAt: new Date(now.getTime() - 150 * 60000).toISOString(),
lastActivity: new Date(now.getTime() - 90 * 60000).toISOString(),
checkpointCount: 8,
messagesProcessed: 34,
currentTask: null,
avgResponseTime: 278
},
{
id: `session-${Date.now()}-007`,
agentType: 'grader-agent',
agentId: 'grader-2',
userId: 'user-202',
userName: 'Herr Weber (Lehrer)',
state: 'completed',
createdAt: new Date(now.getTime() - 345 * 60000).toISOString(),
lastActivity: new Date(now.getTime() - 225 * 60000).toISOString(),
checkpointCount: 24,
messagesProcessed: 120,
currentTask: null,
avgResponseTime: 1102
},
{
id: `session-${Date.now()}-008`,
agentType: 'alert-agent',
agentId: 'alert-1',
userId: 'system',
userName: 'System (Monitoring)',
state: 'active',
createdAt: new Date(now.getTime() - 945 * 60000).toISOString(),
lastActivity: new Date(now.getTime() - 2000).toISOString(),
checkpointCount: 48,
messagesProcessed: 256,
currentTask: 'Monitoring System Health',
avgResponseTime: 45
}
]
}
// GET - List all sessions
export async function GET(request: NextRequest) {
try {
const url = new URL(request.url)
const state = url.searchParams.get('state')
const agentType = url.searchParams.get('agentType')
const limit = parseInt(url.searchParams.get('limit') || '100')
const offset = parseInt(url.searchParams.get('offset') || '0')
let sessions = getMockSessions()
// Apply filters
if (state) {
sessions = sessions.filter(s => s.state === state)
}
if (agentType) {
sessions = sessions.filter(s => s.agentType === agentType)
}
// Calculate stats
const stats = {
total: sessions.length,
active: sessions.filter(s => s.state === 'active').length,
paused: sessions.filter(s => s.state === 'paused').length,
completed: sessions.filter(s => s.state === 'completed').length,
failed: sessions.filter(s => s.state === 'failed').length,
totalMessages: sessions.reduce((sum, s) => sum + s.messagesProcessed, 0),
avgResponseTime: Math.round(
sessions.reduce((sum, s) => sum + s.avgResponseTime, 0) / sessions.length
)
}
// Apply pagination
const paginatedSessions = sessions.slice(offset, offset + limit)
return NextResponse.json({
sessions: paginatedSessions,
stats,
pagination: {
total: sessions.length,
limit,
offset,
hasMore: offset + limit < sessions.length
},
timestamp: new Date().toISOString()
})
} catch (error) {
console.error('Error fetching sessions:', error)
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Failed to fetch sessions' },
{ status: 500 }
)
}
}
// POST - Create new session (for testing)
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { agentType, userId, userName, context } = body
if (!agentType || !userId) {
return NextResponse.json(
{ error: 'agentType and userId are required' },
{ status: 400 }
)
}
// In production, create session via agent-core SessionManager
const newSession: AgentSession = {
id: `session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
agentType,
agentId: `${agentType.replace('-agent', '')}-${Math.floor(Math.random() * 100)}`,
userId,
userName: userName || userId,
state: 'active',
createdAt: new Date().toISOString(),
lastActivity: new Date().toISOString(),
checkpointCount: 0,
messagesProcessed: 0,
currentTask: context?.task || null,
avgResponseTime: 0
}
return NextResponse.json({
success: true,
session: newSession,
message: 'Session created successfully'
}, { status: 201 })
} catch (error) {
console.error('Error creating session:', error)
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Failed to create session' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,208 @@
import { NextRequest, NextResponse } from 'next/server'
/**
* Agent Statistics API
*
* GET - Get aggregated statistics for all agents
*/
interface AgentMetric {
agentType: string
name: string
color: string
sessions: number
messagesProcessed: number
avgResponseTime: number
errorRate: number
successRate: number
trend: 'up' | 'down' | 'stable'
trendValue: number
}
interface DailyStats {
date: string
sessions: number
messages: number
errors: number
avgLatency: number
}
interface HourlyLatency {
timestamp: string
value: number
}
// Mock agent metrics
function getAgentMetrics(): AgentMetric[] {
return [
{
agentType: 'tutor-agent',
name: 'TutorAgent',
color: '#3b82f6',
sessions: 156,
messagesProcessed: 4521,
avgResponseTime: 234,
errorRate: 0.3,
successRate: 99.7,
trend: 'up',
trendValue: 12
},
{
agentType: 'grader-agent',
name: 'GraderAgent',
color: '#10b981',
sessions: 45,
messagesProcessed: 1205,
avgResponseTime: 1102,
errorRate: 0.5,
successRate: 99.5,
trend: 'stable',
trendValue: 2
},
{
agentType: 'quality-judge',
name: 'QualityJudge',
color: '#f59e0b',
sessions: 89,
messagesProcessed: 8934,
avgResponseTime: 89,
errorRate: 0.1,
successRate: 99.9,
trend: 'up',
trendValue: 8
},
{
agentType: 'alert-agent',
name: 'AlertAgent',
color: '#ef4444',
sessions: 12,
messagesProcessed: 892,
avgResponseTime: 45,
errorRate: 0.0,
successRate: 100,
trend: 'stable',
trendValue: 0
},
{
agentType: 'orchestrator',
name: 'Orchestrator',
color: '#8b5cf6',
sessions: 234,
messagesProcessed: 15420,
avgResponseTime: 12,
errorRate: 0.2,
successRate: 99.8,
trend: 'up',
trendValue: 15
}
]
}
// Generate mock daily stats for the last N days
function getDailyStats(days: number): DailyStats[] {
const stats: DailyStats[] = []
const now = new Date()
for (let i = days - 1; i >= 0; i--) {
const date = new Date(now)
date.setDate(date.getDate() - i)
stats.push({
date: date.toISOString().split('T')[0],
sessions: 400 + Math.floor(Math.random() * 150),
messages: 11000 + Math.floor(Math.random() * 5000),
errors: 8 + Math.floor(Math.random() * 12),
avgLatency: 140 + Math.floor(Math.random() * 25)
})
}
return stats
}
// Generate hourly latency data for the last 24 hours
function getHourlyLatency(): HourlyLatency[] {
const data: HourlyLatency[] = []
for (let i = 0; i < 24; i++) {
data.push({
timestamp: `${i.toString().padStart(2, '0')}:00`,
value: 100 + Math.floor(Math.random() * 100)
})
}
return data
}
// GET - Get statistics
export async function GET(request: NextRequest) {
try {
const url = new URL(request.url)
const timeRange = url.searchParams.get('range') || '7d'
// Determine days based on time range
const days = timeRange === '24h' ? 1 : timeRange === '7d' ? 7 : 30
const agentMetrics = getAgentMetrics()
const dailyStats = getDailyStats(days)
const hourlyLatency = getHourlyLatency()
// Calculate totals
const totals = {
sessions: agentMetrics.reduce((sum, m) => sum + m.sessions, 0),
messages: agentMetrics.reduce((sum, m) => sum + m.messagesProcessed, 0),
avgLatency: Math.round(
agentMetrics.reduce((sum, m) => sum + m.avgResponseTime, 0) / agentMetrics.length
),
avgErrorRate: parseFloat(
(agentMetrics.reduce((sum, m) => sum + m.errorRate, 0) / agentMetrics.length).toFixed(2)
)
}
// Calculate week totals from daily stats
const weekTotals = {
sessions: dailyStats.reduce((sum, d) => sum + d.sessions, 0),
messages: dailyStats.reduce((sum, d) => sum + d.messages, 0),
errors: dailyStats.reduce((sum, d) => sum + d.errors, 0),
avgLatency: Math.round(
dailyStats.reduce((sum, d) => sum + d.avgLatency, 0) / dailyStats.length
)
}
// Calculate trends
const trends = {
sessions: {
value: 12,
direction: 'up' as const
},
messages: {
value: 8,
direction: 'up' as const
},
latency: {
value: 5,
direction: 'down' as const // down is good for latency
},
errors: {
value: 3,
direction: 'up' as const
}
}
return NextResponse.json({
agentMetrics,
dailyStats,
hourlyLatency,
totals,
weekTotals,
trends,
timeRange,
timestamp: new Date().toISOString()
})
} catch (error) {
console.error('Error fetching statistics:', error)
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Failed to fetch statistics' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,44 @@
/**
* Complete Audit Session API Route - Proxy to Backend
*/
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
// Complete audit session (in_progress -> completed)
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ sessionId: string }> }
) {
try {
const { sessionId } = await params
const authHeader = request.headers.get('Authorization')
const response = await fetch(`${BACKEND_URL}/api/v1/compliance/audit/sessions/${sessionId}/complete`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
...(authHeader ? { 'Authorization': authHeader } : {})
},
signal: AbortSignal.timeout(30000)
})
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, details: errorText },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Complete audit session proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum Backend fehlgeschlagen' },
{ status: 503 }
)
}
}

View File

@@ -0,0 +1,66 @@
/**
* Audit Session PDF Report API Route - Proxy to Backend
*/
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
// Generate PDF report for audit session
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ sessionId: string }> }
) {
try {
const { sessionId } = await params
const authHeader = request.headers.get('Authorization')
const { searchParams } = new URL(request.url)
const language = searchParams.get('language') || 'de'
const includeSignatures = searchParams.get('include_signatures') !== 'false'
const queryParams = new URLSearchParams({
language,
include_signatures: String(includeSignatures)
})
const response = await fetch(
`${BACKEND_URL}/api/v1/compliance/audit/sessions/${sessionId}/report/pdf?${queryParams}`,
{
method: 'GET',
headers: {
...(authHeader ? { 'Authorization': authHeader } : {})
},
signal: AbortSignal.timeout(60000) // 60s timeout for PDF generation
}
)
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, details: errorText },
{ status: response.status }
)
}
// Stream the PDF response
const pdfBuffer = await response.arrayBuffer()
const contentDisposition = response.headers.get('Content-Disposition') ||
`attachment; filename=audit-report-${sessionId}.pdf`
return new NextResponse(pdfBuffer, {
status: 200,
headers: {
'Content-Type': 'application/pdf',
'Content-Disposition': contentDisposition,
'Content-Length': String(pdfBuffer.byteLength)
}
})
} catch (error) {
console.error('Audit PDF proxy error:', error)
return NextResponse.json(
{ error: 'PDF-Generierung fehlgeschlagen' },
{ status: 503 }
)
}
}

View File

@@ -0,0 +1,80 @@
/**
* Audit Session Detail API Route - Proxy to Backend
*/
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
// Get audit session detail
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ sessionId: string }> }
) {
try {
const { sessionId } = await params
const authHeader = request.headers.get('Authorization')
const response = await fetch(`${BACKEND_URL}/api/v1/compliance/audit/sessions/${sessionId}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
...(authHeader ? { 'Authorization': authHeader } : {})
},
signal: AbortSignal.timeout(30000)
})
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, details: errorText },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Audit session detail proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum Backend fehlgeschlagen' },
{ status: 503 }
)
}
}
// Delete audit session
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ sessionId: string }> }
) {
try {
const { sessionId } = await params
const authHeader = request.headers.get('Authorization')
const response = await fetch(`${BACKEND_URL}/api/v1/compliance/audit/sessions/${sessionId}`, {
method: 'DELETE',
headers: {
'Content-Type': 'application/json',
...(authHeader ? { 'Authorization': authHeader } : {})
},
signal: AbortSignal.timeout(30000)
})
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, details: errorText },
{ status: response.status }
)
}
return NextResponse.json({ success: true })
} catch (error) {
console.error('Delete audit session proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum Backend fehlgeschlagen' },
{ status: 503 }
)
}
}

View File

@@ -0,0 +1,44 @@
/**
* Start Audit Session API Route - Proxy to Backend
*/
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
// Start audit session (draft -> in_progress)
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ sessionId: string }> }
) {
try {
const { sessionId } = await params
const authHeader = request.headers.get('Authorization')
const response = await fetch(`${BACKEND_URL}/api/v1/compliance/audit/sessions/${sessionId}/start`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
...(authHeader ? { 'Authorization': authHeader } : {})
},
signal: AbortSignal.timeout(30000)
})
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, details: errorText },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Start audit session proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum Backend fehlgeschlagen' },
{ status: 503 }
)
}
}

View File

@@ -0,0 +1,82 @@
/**
* Audit Sessions API Route - Proxy to Backend
*/
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
// Get all audit sessions
export async function GET(request: NextRequest) {
try {
const authHeader = request.headers.get('Authorization')
const { searchParams } = new URL(request.url)
const status = searchParams.get('status')
const queryParams = new URLSearchParams()
if (status) queryParams.set('status', status)
const url = `${BACKEND_URL}/api/v1/compliance/audit/sessions${queryParams.toString() ? `?${queryParams}` : ''}`
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
...(authHeader ? { 'Authorization': authHeader } : {})
},
signal: AbortSignal.timeout(30000)
})
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, details: errorText },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Audit sessions proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum Backend fehlgeschlagen' },
{ status: 503 }
)
}
}
// Create new audit session
export async function POST(request: NextRequest) {
try {
const authHeader = request.headers.get('Authorization')
const body = await request.json()
const response = await fetch(`${BACKEND_URL}/api/v1/compliance/audit/sessions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(authHeader ? { 'Authorization': authHeader } : {})
},
body: JSON.stringify(body),
signal: AbortSignal.timeout(30000)
})
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, details: errorText },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Create audit session proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum Backend fehlgeschlagen' },
{ status: 503 }
)
}
}

View File

@@ -0,0 +1,210 @@
/**
* Communication Admin API Route - Stats Proxy
*
* Proxies requests to Matrix/Jitsi admin endpoints via backend
* Aggregates statistics from both services
*/
import { NextRequest, NextResponse } from 'next/server'
// Service URLs
const BACKEND_URL = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8000'
const CONSENT_SERVICE_URL = process.env.CONSENT_SERVICE_URL || 'http://localhost:8081'
const MATRIX_ADMIN_URL = process.env.MATRIX_ADMIN_URL || 'http://localhost:8448'
const JITSI_URL = process.env.JITSI_URL || 'http://localhost:8443'
// Matrix Admin Token (for Synapse Admin API)
const MATRIX_ADMIN_TOKEN = process.env.MATRIX_ADMIN_TOKEN || ''
interface MatrixStats {
total_users: number
active_users: number
total_rooms: number
active_rooms: number
messages_today: number
messages_this_week: number
status: 'online' | 'offline' | 'degraded'
}
interface JitsiStats {
active_meetings: number
total_participants: number
meetings_today: number
average_duration_minutes: number
peak_concurrent_users: number
total_minutes_today: number
status: 'online' | 'offline' | 'degraded'
}
async function fetchFromBackend(): Promise<{
matrix: MatrixStats
jitsi: JitsiStats
active_meetings: unknown[]
recent_rooms: unknown[]
} | null> {
try {
const response = await fetch(`${BACKEND_URL}/api/v1/communication/admin/stats`, {
headers: { 'Content-Type': 'application/json' },
signal: AbortSignal.timeout(5000),
})
if (response.ok) {
return await response.json()
}
} catch (error) {
console.log('Backend not reachable, trying consent service:', error)
}
return null
}
async function fetchFromConsentService(): Promise<{
matrix: MatrixStats
jitsi: JitsiStats
active_meetings: unknown[]
recent_rooms: unknown[]
} | null> {
try {
const response = await fetch(`${CONSENT_SERVICE_URL}/api/v1/communication/admin/stats`, {
headers: { 'Content-Type': 'application/json' },
signal: AbortSignal.timeout(5000),
})
if (response.ok) {
return await response.json()
}
} catch (error) {
console.log('Consent service not reachable:', error)
}
return null
}
async function fetchMatrixStats(): Promise<MatrixStats> {
try {
// Check if Matrix is reachable
const healthCheck = await fetch(`${MATRIX_ADMIN_URL}/_matrix/client/versions`, {
signal: AbortSignal.timeout(5000)
})
if (healthCheck.ok) {
// Try to get user count from admin API
if (MATRIX_ADMIN_TOKEN) {
try {
const usersResponse = await fetch(`${MATRIX_ADMIN_URL}/_synapse/admin/v2/users?limit=1`, {
headers: { 'Authorization': `Bearer ${MATRIX_ADMIN_TOKEN}` },
signal: AbortSignal.timeout(5000),
})
if (usersResponse.ok) {
const data = await usersResponse.json()
return {
total_users: data.total || 0,
active_users: 0,
total_rooms: 0,
active_rooms: 0,
messages_today: 0,
messages_this_week: 0,
status: 'online'
}
}
} catch {
// Admin API not available
}
}
return {
total_users: 0,
active_users: 0,
total_rooms: 0,
active_rooms: 0,
messages_today: 0,
messages_this_week: 0,
status: 'degraded' // Server reachable but no admin access
}
}
} catch (error) {
console.error('Matrix stats fetch error:', error)
}
return {
total_users: 0,
active_users: 0,
total_rooms: 0,
active_rooms: 0,
messages_today: 0,
messages_this_week: 0,
status: 'offline'
}
}
async function fetchJitsiStats(): Promise<JitsiStats> {
try {
// Check if Jitsi is reachable
const healthCheck = await fetch(`${JITSI_URL}/http-bind`, {
method: 'HEAD',
signal: AbortSignal.timeout(5000)
})
return {
active_meetings: 0,
total_participants: 0,
meetings_today: 0,
average_duration_minutes: 0,
peak_concurrent_users: 0,
total_minutes_today: 0,
status: healthCheck.ok ? 'online' : 'offline'
}
} catch (error) {
console.error('Jitsi stats fetch error:', error)
return {
active_meetings: 0,
total_participants: 0,
meetings_today: 0,
average_duration_minutes: 0,
peak_concurrent_users: 0,
total_minutes_today: 0,
status: 'offline'
}
}
}
export async function GET(request: NextRequest) {
try {
// Try backend first
let data = await fetchFromBackend()
// Fallback to consent service
if (!data) {
data = await fetchFromConsentService()
}
// If both fail, try direct service checks
if (!data) {
const [matrixStats, jitsiStats] = await Promise.all([
fetchMatrixStats(),
fetchJitsiStats()
])
data = {
matrix: matrixStats,
jitsi: jitsiStats,
active_meetings: [],
recent_rooms: []
}
}
return NextResponse.json({
...data,
last_updated: new Date().toISOString()
})
} catch (error) {
console.error('Communication stats error:', error)
return NextResponse.json(
{
error: 'Fehler beim Abrufen der Statistiken',
matrix: { status: 'offline', total_users: 0, active_users: 0, total_rooms: 0, active_rooms: 0, messages_today: 0, messages_this_week: 0 },
jitsi: { status: 'offline', active_meetings: 0, total_participants: 0, meetings_today: 0, average_duration_minutes: 0, peak_concurrent_users: 0, total_minutes_today: 0 },
active_meetings: [],
recent_rooms: [],
last_updated: new Date().toISOString()
},
{ status: 503 }
)
}
}

View File

@@ -0,0 +1,129 @@
import { NextRequest, NextResponse } from 'next/server'
/**
* POST /api/admin/companion/feedback
* Submit feedback (bug report, feature request, general feedback)
* Proxy to backend /api/feedback
*/
export async function POST(request: NextRequest) {
try {
const body = await request.json()
// Validate required fields
if (!body.type || !body.title || !body.description) {
return NextResponse.json(
{
success: false,
error: 'Missing required fields: type, title, description',
},
{ status: 400 }
)
}
// Validate feedback type
const validTypes = ['bug', 'feature', 'feedback']
if (!validTypes.includes(body.type)) {
return NextResponse.json(
{
success: false,
error: 'Invalid feedback type. Must be: bug, feature, or feedback',
},
{ status: 400 }
)
}
// TODO: Replace with actual backend call
// const backendUrl = process.env.BACKEND_URL || 'http://localhost:8000'
// const response = await fetch(`${backendUrl}/api/feedback`, {
// method: 'POST',
// headers: {
// 'Content-Type': 'application/json',
// // Add auth headers
// },
// body: JSON.stringify({
// type: body.type,
// title: body.title,
// description: body.description,
// screenshot: body.screenshot,
// sessionId: body.sessionId,
// metadata: {
// ...body.metadata,
// source: 'companion',
// timestamp: new Date().toISOString(),
// userAgent: request.headers.get('user-agent'),
// },
// }),
// })
//
// if (!response.ok) {
// throw new Error(`Backend responded with ${response.status}`)
// }
//
// const data = await response.json()
// return NextResponse.json(data)
// Mock response - just acknowledge the submission
const feedbackId = `fb-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
console.log('Feedback received:', {
id: feedbackId,
type: body.type,
title: body.title,
description: body.description.substring(0, 100) + '...',
hasScreenshot: !!body.screenshot,
sessionId: body.sessionId,
})
return NextResponse.json({
success: true,
message: 'Feedback submitted successfully',
data: {
feedbackId,
submittedAt: new Date().toISOString(),
},
})
} catch (error) {
console.error('Submit feedback error:', error)
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
},
{ status: 500 }
)
}
}
/**
* GET /api/admin/companion/feedback
* Get feedback history (admin only)
*/
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const type = searchParams.get('type')
const limit = parseInt(searchParams.get('limit') || '10')
// TODO: Replace with actual backend call
// Mock response - empty list for now
return NextResponse.json({
success: true,
data: {
feedback: [],
total: 0,
page: 1,
limit,
},
})
} catch (error) {
console.error('Get feedback error:', error)
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
},
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,194 @@
import { NextRequest, NextResponse } from 'next/server'
/**
* POST /api/admin/companion/lesson
* Start a new lesson session
* Proxy to backend /api/classroom/sessions
*/
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const backendUrl = process.env.BACKEND_URL || 'http://localhost:8000'
// TODO: Replace with actual backend call
// const response = await fetch(`${backendUrl}/api/classroom/sessions`, {
// method: 'POST',
// headers: {
// 'Content-Type': 'application/json',
// },
// body: JSON.stringify(body),
// })
//
// if (!response.ok) {
// throw new Error(`Backend responded with ${response.status}`)
// }
//
// const data = await response.json()
// return NextResponse.json(data)
// Mock response - create a new session
const sessionId = `session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
const mockSession = {
success: true,
data: {
sessionId,
classId: body.classId,
className: body.className || body.classId,
subject: body.subject,
topic: body.topic,
startTime: new Date().toISOString(),
phases: [
{ phase: 'einstieg', duration: 8, status: 'active', actualTime: 0 },
{ phase: 'erarbeitung', duration: 20, status: 'planned', actualTime: 0 },
{ phase: 'sicherung', duration: 10, status: 'planned', actualTime: 0 },
{ phase: 'transfer', duration: 7, status: 'planned', actualTime: 0 },
{ phase: 'reflexion', duration: 5, status: 'planned', actualTime: 0 },
],
totalPlannedDuration: 50,
currentPhaseIndex: 0,
elapsedTime: 0,
isPaused: false,
pauseDuration: 0,
overtimeMinutes: 0,
status: 'in_progress',
homeworkList: [],
materials: [],
},
}
return NextResponse.json(mockSession)
} catch (error) {
console.error('Start lesson error:', error)
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
},
{ status: 500 }
)
}
}
/**
* GET /api/admin/companion/lesson
* Get current lesson session or list of recent sessions
*/
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const sessionId = searchParams.get('sessionId')
// TODO: Replace with actual backend call
// const backendUrl = process.env.BACKEND_URL || 'http://localhost:8000'
// const url = sessionId
// ? `${backendUrl}/api/classroom/sessions/${sessionId}`
// : `${backendUrl}/api/classroom/sessions`
//
// const response = await fetch(url)
// const data = await response.json()
// return NextResponse.json(data)
// Mock response
if (sessionId) {
return NextResponse.json({
success: true,
data: null, // No active session stored on server in mock
})
}
return NextResponse.json({
success: true,
data: {
sessions: [], // Empty list for now
},
})
} catch (error) {
console.error('Get lesson error:', error)
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
},
{ status: 500 }
)
}
}
/**
* PATCH /api/admin/companion/lesson
* Update lesson session (timer state, phase changes, etc.)
*/
export async function PATCH(request: NextRequest) {
try {
const body = await request.json()
const { sessionId, ...updates } = body
if (!sessionId) {
return NextResponse.json(
{ success: false, error: 'Session ID required' },
{ status: 400 }
)
}
// TODO: Replace with actual backend call
// const backendUrl = process.env.BACKEND_URL || 'http://localhost:8000'
// const response = await fetch(`${backendUrl}/api/classroom/sessions/${sessionId}`, {
// method: 'PATCH',
// headers: { 'Content-Type': 'application/json' },
// body: JSON.stringify(updates),
// })
//
// const data = await response.json()
// return NextResponse.json(data)
// Mock response - just acknowledge the update
return NextResponse.json({
success: true,
message: 'Session updated',
})
} catch (error) {
console.error('Update lesson error:', error)
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
},
{ status: 500 }
)
}
}
/**
* DELETE /api/admin/companion/lesson
* End/delete a lesson session
*/
export async function DELETE(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const sessionId = searchParams.get('sessionId')
if (!sessionId) {
return NextResponse.json(
{ success: false, error: 'Session ID required' },
{ status: 400 }
)
}
// TODO: Replace with actual backend call
return NextResponse.json({
success: true,
message: 'Session ended',
})
} catch (error) {
console.error('End lesson error:', error)
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
},
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,102 @@
import { NextResponse } from 'next/server'
/**
* GET /api/admin/companion
* Proxy to backend /api/state/dashboard for companion dashboard data
*/
export async function GET() {
try {
const backendUrl = process.env.BACKEND_URL || 'http://localhost:8000'
// TODO: Replace with actual backend call when endpoint is available
// const response = await fetch(`${backendUrl}/api/state/dashboard`, {
// method: 'GET',
// headers: {
// 'Content-Type': 'application/json',
// },
// })
//
// if (!response.ok) {
// throw new Error(`Backend responded with ${response.status}`)
// }
//
// const data = await response.json()
// return NextResponse.json(data)
// Mock response for development
const mockData = {
success: true,
data: {
context: {
currentPhase: 'erarbeitung',
phaseDisplayName: 'Erarbeitung',
},
stats: {
classesCount: 4,
studentsCount: 96,
learningUnitsCreated: 23,
gradesEntered: 156,
},
phases: [
{ id: 'einstieg', shortName: 'E', displayName: 'Einstieg', duration: 8, status: 'completed', color: '#4A90E2' },
{ id: 'erarbeitung', shortName: 'A', displayName: 'Erarbeitung', duration: 20, status: 'active', color: '#F5A623' },
{ id: 'sicherung', shortName: 'S', displayName: 'Sicherung', duration: 10, status: 'planned', color: '#7ED321' },
{ id: 'transfer', shortName: 'T', displayName: 'Transfer', duration: 7, status: 'planned', color: '#9013FE' },
{ id: 'reflexion', shortName: 'R', displayName: 'Reflexion', duration: 5, status: 'planned', color: '#6B7280' },
],
progress: {
percentage: 65,
completed: 13,
total: 20,
},
suggestions: [
{
id: '1',
title: 'Klausuren korrigieren',
description: 'Deutsch LK - 12 unkorrigierte Arbeiten warten',
priority: 'urgent',
icon: 'ClipboardCheck',
actionTarget: '/ai/klausur-korrektur',
estimatedTime: 120,
},
{
id: '2',
title: 'Elternsprechtag vorbereiten',
description: 'Notenuebersicht fuer 8b erstellen',
priority: 'high',
icon: 'Users',
actionTarget: '/education/grades',
estimatedTime: 30,
},
],
upcomingEvents: [
{
id: 'e1',
title: 'Mathe-Test 9b',
date: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000).toISOString(),
type: 'exam',
inDays: 2,
},
{
id: 'e2',
title: 'Elternsprechtag',
date: new Date(Date.now() + 5 * 24 * 60 * 60 * 1000).toISOString(),
type: 'parent_meeting',
inDays: 5,
},
],
},
}
return NextResponse.json(mockData)
} catch (error) {
console.error('Companion dashboard error:', error)
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
},
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,137 @@
import { NextRequest, NextResponse } from 'next/server'
const DEFAULT_SETTINGS = {
defaultPhaseDurations: {
einstieg: 8,
erarbeitung: 20,
sicherung: 10,
transfer: 7,
reflexion: 5,
},
preferredLessonLength: 45,
autoAdvancePhases: true,
soundNotifications: true,
showKeyboardShortcuts: true,
highContrastMode: false,
onboardingCompleted: false,
}
/**
* GET /api/admin/companion/settings
* Get teacher settings
* Proxy to backend /api/teacher/settings
*/
export async function GET() {
try {
// TODO: Replace with actual backend call
// const backendUrl = process.env.BACKEND_URL || 'http://localhost:8000'
// const response = await fetch(`${backendUrl}/api/teacher/settings`, {
// method: 'GET',
// headers: {
// 'Content-Type': 'application/json',
// // Add auth headers
// },
// })
//
// if (!response.ok) {
// throw new Error(`Backend responded with ${response.status}`)
// }
//
// const data = await response.json()
// return NextResponse.json(data)
// Mock response - return default settings
return NextResponse.json({
success: true,
data: DEFAULT_SETTINGS,
})
} catch (error) {
console.error('Get settings error:', error)
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
},
{ status: 500 }
)
}
}
/**
* PUT /api/admin/companion/settings
* Update teacher settings
*/
export async function PUT(request: NextRequest) {
try {
const body = await request.json()
// Validate the settings structure
if (!body || typeof body !== 'object') {
return NextResponse.json(
{ success: false, error: 'Invalid settings data' },
{ status: 400 }
)
}
// TODO: Replace with actual backend call
// const backendUrl = process.env.BACKEND_URL || 'http://localhost:8000'
// const response = await fetch(`${backendUrl}/api/teacher/settings`, {
// method: 'PUT',
// headers: {
// 'Content-Type': 'application/json',
// // Add auth headers
// },
// body: JSON.stringify(body),
// })
//
// if (!response.ok) {
// throw new Error(`Backend responded with ${response.status}`)
// }
//
// const data = await response.json()
// return NextResponse.json(data)
// Mock response - just acknowledge the save
return NextResponse.json({
success: true,
message: 'Settings saved',
data: body,
})
} catch (error) {
console.error('Save settings error:', error)
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
},
{ status: 500 }
)
}
}
/**
* PATCH /api/admin/companion/settings
* Partially update teacher settings
*/
export async function PATCH(request: NextRequest) {
try {
const body = await request.json()
// TODO: Replace with actual backend call
return NextResponse.json({
success: true,
message: 'Settings updated',
data: body,
})
} catch (error) {
console.error('Update settings error:', error)
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
},
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,85 @@
/**
* Audit Sign-off API Route - Proxy to Backend
*/
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
// Sign off an item
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ sessionId: string; requirementId: string }> }
) {
try {
const { sessionId, requirementId } = await params
const body = await request.json()
const response = await fetch(
`${BACKEND_URL}/api/v1/compliance/audit/checklist/${sessionId}/items/${requirementId}/sign-off`,
{
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
signal: AbortSignal.timeout(30000)
}
)
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, details: errorText },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Audit sign-off proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum Backend fehlgeschlagen' },
{ status: 503 }
)
}
}
// Get sign-off status
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ sessionId: string; requirementId: string }> }
) {
try {
const { sessionId, requirementId } = await params
const response = await fetch(
`${BACKEND_URL}/api/v1/compliance/audit/checklist/${sessionId}/items/${requirementId}`,
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
signal: AbortSignal.timeout(30000)
}
)
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, details: errorText },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Get sign-off proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum Backend fehlgeschlagen' },
{ status: 503 }
)
}
}

View File

@@ -0,0 +1,57 @@
/**
* Audit Checklist API Route - Proxy to Backend
*/
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ sessionId: string }> }
) {
try {
const { sessionId } = await params
const { searchParams } = new URL(request.url)
const queryParams = new URLSearchParams()
const page = searchParams.get('page')
const page_size = searchParams.get('page_size')
const status_filter = searchParams.get('status_filter')
const regulation_filter = searchParams.get('regulation_filter')
const search = searchParams.get('search')
if (page) queryParams.set('page', page)
if (page_size) queryParams.set('page_size', page_size)
if (status_filter) queryParams.set('status_filter', status_filter)
if (regulation_filter) queryParams.set('regulation_filter', regulation_filter)
if (search) queryParams.set('search', search)
const url = `${BACKEND_URL}/api/v1/compliance/audit/checklist/${sessionId}${queryParams.toString() ? `?${queryParams}` : ''}`
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
signal: AbortSignal.timeout(30000)
})
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, details: errorText },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Audit checklist proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum Backend fehlgeschlagen' },
{ status: 503 }
)
}
}

View File

@@ -0,0 +1,46 @@
/**
* Control Review API Route - Proxy to Backend
*/
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ controlId: string }> }
) {
try {
const { controlId } = await params
const body = await request.json()
const response = await fetch(
`${BACKEND_URL}/api/v1/compliance/controls/${controlId}/review`,
{
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
signal: AbortSignal.timeout(30000)
}
)
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, details: errorText },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Control review proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum Backend fehlgeschlagen' },
{ status: 503 }
)
}
}

View File

@@ -0,0 +1,54 @@
/**
* Compliance Controls API Route - Proxy to Backend
*/
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const queryParams = new URLSearchParams()
// Forward query parameters
const domain = searchParams.get('domain')
const status = searchParams.get('status')
const search = searchParams.get('search')
const page = searchParams.get('page')
const page_size = searchParams.get('page_size')
if (domain) queryParams.set('domain', domain)
if (status) queryParams.set('status', status)
if (search) queryParams.set('search', search)
if (page) queryParams.set('page', page)
if (page_size) queryParams.set('page_size', page_size)
const url = `${BACKEND_URL}/api/v1/compliance/controls${queryParams.toString() ? `?${queryParams}` : ''}`
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
signal: AbortSignal.timeout(30000)
})
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, details: errorText },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Compliance controls proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum Backend fehlgeschlagen', controls: [] },
{ status: 503 }
)
}
}

View File

@@ -0,0 +1,36 @@
/**
* Compliance Executive Dashboard API Route - Proxy to Backend
*/
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
export async function GET(request: NextRequest) {
try {
const response = await fetch(`${BACKEND_URL}/api/v1/compliance/dashboard/executive`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
signal: AbortSignal.timeout(30000)
})
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, details: errorText },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Executive dashboard proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum Backend fehlgeschlagen' },
{ status: 503 }
)
}
}

View File

@@ -0,0 +1,36 @@
/**
* Compliance Dashboard API Route - Proxy to Backend
*/
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
export async function GET(request: NextRequest) {
try {
const response = await fetch(`${BACKEND_URL}/api/v1/compliance/dashboard`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
signal: AbortSignal.timeout(30000)
})
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, details: errorText },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Compliance dashboard proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum Backend fehlgeschlagen' },
{ status: 503 }
)
}
}

View File

@@ -0,0 +1,81 @@
/**
* Compliance Evidence API Route - Proxy to Backend
*/
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const queryParams = new URLSearchParams()
const control_id = searchParams.get('control_id')
const evidence_type = searchParams.get('evidence_type')
const status = searchParams.get('status')
if (control_id) queryParams.set('control_id', control_id)
if (evidence_type) queryParams.set('evidence_type', evidence_type)
if (status) queryParams.set('status', status)
const url = `${BACKEND_URL}/api/v1/compliance/evidence${queryParams.toString() ? `?${queryParams}` : ''}`
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
signal: AbortSignal.timeout(30000)
})
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, details: errorText },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Compliance evidence proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum Backend fehlgeschlagen', evidence: [] },
{ status: 503 }
)
}
}
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const response = await fetch(`${BACKEND_URL}/api/v1/compliance/evidence`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
signal: AbortSignal.timeout(30000)
})
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, details: errorText },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Create evidence proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum Backend fehlgeschlagen' },
{ status: 503 }
)
}
}

View File

@@ -0,0 +1,53 @@
/**
* Evidence Upload API Route - Proxy to Backend
*/
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
export async function POST(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const queryParams = new URLSearchParams()
const control_id = searchParams.get('control_id')
const evidence_type = searchParams.get('evidence_type')
const title = searchParams.get('title')
const description = searchParams.get('description')
if (control_id) queryParams.set('control_id', control_id)
if (evidence_type) queryParams.set('evidence_type', evidence_type)
if (title) queryParams.set('title', title)
if (description) queryParams.set('description', description)
// Forward the FormData directly
const formData = await request.formData()
const response = await fetch(
`${BACKEND_URL}/api/v1/compliance/evidence/upload?${queryParams}`,
{
method: 'POST',
body: formData,
signal: AbortSignal.timeout(60000) // 60 seconds for file upload
}
)
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, details: errorText },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Evidence upload proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum Backend fehlgeschlagen' },
{ status: 503 }
)
}
}

View File

@@ -0,0 +1,44 @@
/**
* Compliance Module Detail API Route - Proxy to Backend
*/
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ moduleId: string }> }
) {
try {
const { moduleId } = await params
const response = await fetch(
`${BACKEND_URL}/api/v1/compliance/modules/${moduleId}`,
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
signal: AbortSignal.timeout(30000)
}
)
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, details: errorText },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Module detail proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum Backend fehlgeschlagen' },
{ status: 503 }
)
}
}

View File

@@ -0,0 +1,36 @@
/**
* Compliance Modules Overview API Route - Proxy to Backend
*/
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
export async function GET(request: NextRequest) {
try {
const response = await fetch(`${BACKEND_URL}/api/v1/compliance/modules/overview`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
signal: AbortSignal.timeout(30000)
})
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, details: errorText },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Modules overview proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum Backend fehlgeschlagen' },
{ status: 503 }
)
}
}

View File

@@ -0,0 +1,51 @@
/**
* Compliance Modules API Route - Proxy to Backend
*/
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const queryParams = new URLSearchParams()
const service_type = searchParams.get('service_type')
const criticality = searchParams.get('criticality')
const processes_pii = searchParams.get('processes_pii')
const ai_components = searchParams.get('ai_components')
if (service_type) queryParams.set('service_type', service_type)
if (criticality) queryParams.set('criticality', criticality)
if (processes_pii) queryParams.set('processes_pii', processes_pii)
if (ai_components) queryParams.set('ai_components', ai_components)
const url = `${BACKEND_URL}/api/v1/compliance/modules${queryParams.toString() ? `?${queryParams}` : ''}`
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
signal: AbortSignal.timeout(30000)
})
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, details: errorText },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Compliance modules proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum Backend fehlgeschlagen', modules: [] },
{ status: 503 }
)
}
}

View File

@@ -0,0 +1,39 @@
/**
* Compliance Modules Seed API Route - Proxy to Backend
*/
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const response = await fetch(`${BACKEND_URL}/api/v1/compliance/modules/seed`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
signal: AbortSignal.timeout(60000) // 60 seconds for seeding
})
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, details: errorText },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Modules seed proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum Backend fehlgeschlagen' },
{ status: 503 }
)
}
}

View File

@@ -0,0 +1,220 @@
/**
* Compliance Regulations API Route - Proxy to Backend
*
* Returns all 21 regulations with source URLs to original documents
* Includes: GDPR, ePrivacy, TDDDG, SCC, DPF, AI Act, CRA, NIS2, EU CSA,
* Data Act, DGA, DSA, EAA, DSM, PLD, GPSR, BSI-TR-03161 (1-3), BSI C5, DORA
*/
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
export async function GET(request: NextRequest) {
try {
const response = await fetch(`${BACKEND_URL}/api/v1/compliance/regulations`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
signal: AbortSignal.timeout(30000)
})
if (!response.ok) {
// If backend doesn't have this endpoint yet, return seed data
if (response.status === 404) {
return NextResponse.json({
regulations: getStaticRegulations()
})
}
const errorText = await response.text()
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, details: errorText },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Regulations proxy error:', error)
// Return static data as fallback
return NextResponse.json({
regulations: getStaticRegulations()
})
}
}
// Static seed data with source URLs - matches regulations.py
function getStaticRegulations() {
return [
{
id: '1', code: 'GDPR', name: 'DSGVO',
full_name: 'Verordnung (EU) 2016/679 - Datenschutz-Grundverordnung',
regulation_type: 'eu_regulation',
source_url: 'https://eur-lex.europa.eu/eli/reg/2016/679/oj/eng',
description: 'Grundverordnung zum Schutz natuerlicher Personen bei der Verarbeitung personenbezogener Daten.',
is_active: true, requirement_count: 99,
},
{
id: '2', code: 'EPRIVACY', name: 'ePrivacy-Richtlinie',
full_name: 'Richtlinie 2002/58/EG',
regulation_type: 'eu_directive',
source_url: 'https://eur-lex.europa.eu/eli/dir/2002/58/oj/eng',
description: 'Datenschutz in der elektronischen Kommunikation, Cookies und Tracking.',
is_active: true, requirement_count: 25,
},
{
id: '3', code: 'TDDDG', name: 'TDDDG',
full_name: 'Telekommunikation-Digitale-Dienste-Datenschutz-Gesetz',
regulation_type: 'de_law',
source_url: 'https://www.gesetze-im-internet.de/ttdsg/',
description: 'Deutsche Umsetzung der ePrivacy-Richtlinie.',
is_active: true, requirement_count: 15,
},
{
id: '4', code: 'SCC', name: 'Standardvertragsklauseln',
full_name: 'Durchfuehrungsbeschluss (EU) 2021/914',
regulation_type: 'eu_regulation',
source_url: 'https://eur-lex.europa.eu/eli/dec_impl/2021/914/oj/eng',
description: 'Standardvertragsklauseln fuer Drittlandtransfers.',
is_active: true, requirement_count: 18,
},
{
id: '5', code: 'DPF', name: 'EU-US Data Privacy Framework',
full_name: 'Durchfuehrungsbeschluss (EU) 2023/1795',
regulation_type: 'eu_regulation',
source_url: 'https://eur-lex.europa.eu/eli/dec_impl/2023/1795/oj',
description: 'Angemessenheitsbeschluss fuer USA-Transfers.',
is_active: true, requirement_count: 12,
},
{
id: '6', code: 'AIACT', name: 'EU AI Act',
full_name: 'Verordnung (EU) 2024/1689 - KI-Verordnung',
regulation_type: 'eu_regulation',
source_url: 'https://eur-lex.europa.eu/eli/reg/2024/1689/oj/eng',
description: 'EU-Verordnung zur Regulierung von KI-Systemen nach Risikostufen.',
is_active: true, requirement_count: 85,
},
{
id: '7', code: 'CRA', name: 'Cyber Resilience Act',
full_name: 'Verordnung (EU) 2024/2847',
regulation_type: 'eu_regulation',
source_url: 'https://eur-lex.europa.eu/eli/reg/2024/2847/oj/eng',
description: 'Cybersicherheitsanforderungen, SBOM-Pflicht.',
is_active: true, requirement_count: 45,
},
{
id: '8', code: 'NIS2', name: 'NIS2-Richtlinie',
full_name: 'Richtlinie (EU) 2022/2555',
regulation_type: 'eu_directive',
source_url: 'https://eur-lex.europa.eu/eli/dir/2022/2555/oj/eng',
description: 'Cybersicherheit fuer wesentliche Einrichtungen.',
is_active: true, requirement_count: 46,
},
{
id: '9', code: 'EUCSA', name: 'EU Cybersecurity Act',
full_name: 'Verordnung (EU) 2019/881',
regulation_type: 'eu_regulation',
source_url: 'https://eur-lex.europa.eu/eli/reg/2019/881/oj/eng',
description: 'ENISA und Cybersicherheitszertifizierung.',
is_active: true, requirement_count: 35,
},
{
id: '10', code: 'DATAACT', name: 'Data Act',
full_name: 'Verordnung (EU) 2023/2854',
regulation_type: 'eu_regulation',
source_url: 'https://eur-lex.europa.eu/eli/reg/2023/2854/oj/eng',
description: 'Fairer Datenzugang, IoT-Daten, Cloud-Wechsel.',
is_active: true, requirement_count: 42,
},
{
id: '11', code: 'DGA', name: 'Data Governance Act',
full_name: 'Verordnung (EU) 2022/868',
regulation_type: 'eu_regulation',
source_url: 'https://eur-lex.europa.eu/eli/reg/2022/868/oj/eng',
description: 'Weiterverwendung oeffentlicher Daten.',
is_active: true, requirement_count: 35,
},
{
id: '12', code: 'DSA', name: 'Digital Services Act',
full_name: 'Verordnung (EU) 2022/2065',
regulation_type: 'eu_regulation',
source_url: 'https://eur-lex.europa.eu/eli/reg/2022/2065/oj/eng',
description: 'Digitale Dienste, Transparenzpflichten.',
is_active: true, requirement_count: 93,
},
{
id: '13', code: 'EAA', name: 'European Accessibility Act',
full_name: 'Richtlinie (EU) 2019/882',
regulation_type: 'eu_directive',
source_url: 'https://eur-lex.europa.eu/eli/dir/2019/882/oj/eng',
description: 'Barrierefreiheit digitaler Produkte.',
is_active: true, requirement_count: 25,
},
{
id: '14', code: 'DSM', name: 'DSM-Urheberrechtsrichtlinie',
full_name: 'Richtlinie (EU) 2019/790',
regulation_type: 'eu_directive',
source_url: 'https://eur-lex.europa.eu/eli/dir/2019/790/oj/eng',
description: 'Urheberrecht, Text- und Data-Mining.',
is_active: true, requirement_count: 22,
},
{
id: '15', code: 'PLD', name: 'Produkthaftungsrichtlinie',
full_name: 'Richtlinie (EU) 2024/2853',
regulation_type: 'eu_directive',
source_url: 'https://eur-lex.europa.eu/eli/dir/2024/2853/oj/eng',
description: 'Produkthaftung inkl. Software und KI.',
is_active: true, requirement_count: 18,
},
{
id: '16', code: 'GPSR', name: 'General Product Safety',
full_name: 'Verordnung (EU) 2023/988',
regulation_type: 'eu_regulation',
source_url: 'https://eur-lex.europa.eu/eli/reg/2023/988/oj/eng',
description: 'Allgemeine Produktsicherheit.',
is_active: true, requirement_count: 30,
},
{
id: '17', code: 'BSI-TR-03161-1', name: 'BSI-TR-03161 Teil 1',
full_name: 'BSI Technische Richtlinie - Allgemeine Anforderungen',
regulation_type: 'bsi_standard',
source_url: 'https://www.bsi.bund.de/SharedDocs/Downloads/DE/BSI/Publikationen/TechnischeRichtlinien/TR03161/BSI-TR-03161-1.html',
description: 'Allgemeine Sicherheitsanforderungen (45 Pruefaspekte).',
is_active: true, requirement_count: 45,
},
{
id: '18', code: 'BSI-TR-03161-2', name: 'BSI-TR-03161 Teil 2',
full_name: 'BSI Technische Richtlinie - Web-Anwendungen',
regulation_type: 'bsi_standard',
source_url: 'https://www.bsi.bund.de/SharedDocs/Downloads/DE/BSI/Publikationen/TechnischeRichtlinien/TR03161/BSI-TR-03161-2.html',
description: 'Web-Sicherheit (40 Pruefaspekte).',
is_active: true, requirement_count: 40,
},
{
id: '19', code: 'BSI-TR-03161-3', name: 'BSI-TR-03161 Teil 3',
full_name: 'BSI Technische Richtlinie - Hintergrundsysteme',
regulation_type: 'bsi_standard',
source_url: 'https://www.bsi.bund.de/SharedDocs/Downloads/DE/BSI/Publikationen/TechnischeRichtlinien/TR03161/BSI-TR-03161-3.html',
description: 'Backend-Sicherheit (35 Pruefaspekte).',
is_active: true, requirement_count: 35,
},
{
id: '20', code: 'BSI-C5', name: 'BSI C5',
full_name: 'Cloud Computing Compliance Criteria Catalogue',
regulation_type: 'bsi_standard',
source_url: 'https://www.bsi.bund.de/DE/Themen/Unternehmen-und-Organisationen/Informationen-und-Empfehlungen/Empfehlungen-nach-Angriffszielen/Cloud-Computing/Kriterienkatalog-C5/kriterienkatalog-c5_node.html',
description: 'Deutscher Cloud-Sicherheitsstandard mit 121 Kriterien in 17 Bereichen (OIS, SP, HR, AM, PS, OPS, COS, IDM, CRY, SIM, BCM, COM, SA, SUA, PI).',
is_active: true, requirement_count: 121,
},
{
id: '21', code: 'DORA', name: 'DORA',
full_name: 'Verordnung (EU) 2022/2554 - Digital Operational Resilience Act',
regulation_type: 'eu_regulation',
source_url: 'https://eur-lex.europa.eu/eli/reg/2022/2554/oj/deu',
description: 'EU-Verordnung fuer digitale operationale Resilienz im Finanzsektor. IKT-Risikomanagement, Incident-Reporting, Resilienztests, Drittparteienrisiko.',
is_active: true, requirement_count: 64,
},
]
}

View File

@@ -0,0 +1,109 @@
/**
* Compliance Requirements API Route - Proxy to Backend
*
* Returns requirements for a specific regulation with implementation status
*/
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams
const regulationCode = searchParams.get('regulation_code')
if (!regulationCode) {
return NextResponse.json(
{ error: 'regulation_code parameter required' },
{ status: 400 }
)
}
// Build query string for backend
const params = new URLSearchParams()
params.set('regulation_code', regulationCode)
if (searchParams.get('status')) params.set('status', searchParams.get('status')!)
if (searchParams.get('priority')) params.set('priority', searchParams.get('priority')!)
if (searchParams.get('search')) params.set('search', searchParams.get('search')!)
const response = await fetch(
`${BACKEND_URL}/api/v1/compliance/requirements?${params}`,
{
method: 'GET',
headers: { 'Content-Type': 'application/json' },
signal: AbortSignal.timeout(30000)
}
)
if (!response.ok) {
// Return static BSI data as fallback if backend not available
if (response.status === 404 && regulationCode.startsWith('BSI')) {
return NextResponse.json({
requirements: getBSIRequirements(regulationCode)
})
}
const errorText = await response.text()
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, details: errorText },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Requirements proxy error:', error)
// Return fallback data for BSI
const regulationCode = request.nextUrl.searchParams.get('regulation_code')
if (regulationCode?.startsWith('BSI')) {
return NextResponse.json({
requirements: getBSIRequirements(regulationCode)
})
}
return NextResponse.json(
{ error: 'Verbindung zum Backend fehlgeschlagen', requirements: [] },
{ status: 503 }
)
}
}
// Static BSI requirements as fallback (subset)
function getBSIRequirements(code: string) {
if (code === 'BSI-TR-03161-1') {
return [
{ id: '1', regulation_code: code, article: 'O.Purp_1', title: 'Zweckbindung', description: 'Anwendungszweck klar definiert', implementation_status: 'implemented', priority: 1, controls_count: 2 },
{ id: '2', regulation_code: code, article: 'O.Data_1', title: 'Datenminimierung', description: 'Nur notwendige Daten erheben', implementation_status: 'implemented', priority: 1, controls_count: 3 },
{ id: '3', regulation_code: code, article: 'O.Auth_1', title: 'Authentifizierung', description: 'Sichere Authentifizierungsmechanismen', implementation_status: 'verified', priority: 1, controls_count: 4 },
{ id: '4', regulation_code: code, article: 'O.Auth_2', title: 'Passwortrichtlinie', description: 'Starke Passwoerter erzwingen', implementation_status: 'implemented', priority: 1, controls_count: 2 },
{ id: '5', regulation_code: code, article: 'O.Cryp_1', title: 'TLS-Verschluesselung', description: 'TLS 1.2+ fuer Transport', implementation_status: 'verified', priority: 1, controls_count: 2 },
{ id: '6', regulation_code: code, article: 'O.Cryp_2', title: 'Encryption at Rest', description: 'Sensible Daten verschluesseln', implementation_status: 'implemented', priority: 1, controls_count: 2 },
{ id: '7', regulation_code: code, article: 'O.Priv_1', title: 'Datenschutzerklaerung', description: 'Transparente Information', implementation_status: 'verified', priority: 1, controls_count: 1 },
{ id: '8', regulation_code: code, article: 'O.Log_1', title: 'Security Logging', description: 'Sicherheitsereignisse protokollieren', implementation_status: 'in_progress', priority: 1, controls_count: 2 },
]
}
if (code === 'BSI-TR-03161-2') {
return [
{ id: '20', regulation_code: code, article: 'O.Sess_1', title: 'Session-Timeout', description: 'Automatische Sitzungsbeendigung', implementation_status: 'implemented', priority: 1, controls_count: 2 },
{ id: '21', regulation_code: code, article: 'O.Input_1', title: 'Eingabevalidierung', description: 'Alle Eingaben validieren', implementation_status: 'verified', priority: 1, controls_count: 3 },
{ id: '22', regulation_code: code, article: 'O.SQL_1', title: 'SQL-Injection Schutz', description: 'Prepared Statements', implementation_status: 'verified', priority: 1, controls_count: 2 },
{ id: '23', regulation_code: code, article: 'O.XSS_1', title: 'XSS-Schutz', description: 'Output Encoding', implementation_status: 'verified', priority: 1, controls_count: 3 },
{ id: '24', regulation_code: code, article: 'O.CSRF_1', title: 'CSRF-Schutz', description: 'Anti-CSRF Token', implementation_status: 'implemented', priority: 1, controls_count: 2 },
{ id: '25', regulation_code: code, article: 'O.Head_1', title: 'Security Headers', description: 'X-Content-Type-Options', implementation_status: 'verified', priority: 1, controls_count: 1 },
{ id: '26', regulation_code: code, article: 'O.API_1', title: 'API-Authentifizierung', description: 'JWT/OAuth', implementation_status: 'verified', priority: 1, controls_count: 2 },
{ id: '27', regulation_code: code, article: 'O.API_2', title: 'Rate Limiting', description: 'Anfragen begrenzen', implementation_status: 'implemented', priority: 1, controls_count: 1 },
]
}
if (code === 'BSI-TR-03161-3') {
return [
{ id: '40', regulation_code: code, article: 'O.Arch_1', title: 'Defense in Depth', description: 'Mehrschichtige Sicherheit', implementation_status: 'implemented', priority: 1, controls_count: 3 },
{ id: '41', regulation_code: code, article: 'O.DB_1', title: 'Datenbank-Sicherheit', description: 'DB abhaerten', implementation_status: 'implemented', priority: 1, controls_count: 2 },
{ id: '42', regulation_code: code, article: 'O.Cont_1', title: 'Container-Sicherheit', description: 'Images scannen', implementation_status: 'in_progress', priority: 1, controls_count: 2 },
{ id: '43', regulation_code: code, article: 'O.Sec_1', title: 'Secrets Management', description: 'Zentrale Secrets-Verwaltung', implementation_status: 'verified', priority: 1, controls_count: 2 },
{ id: '44', regulation_code: code, article: 'O.Mon_1', title: 'Zentrale Logs', description: 'Log-Aggregation', implementation_status: 'implemented', priority: 1, controls_count: 1 },
{ id: '45', regulation_code: code, article: 'O.CI_1', title: 'Pipeline-Sicherheit', description: 'CI/CD absichern', implementation_status: 'in_progress', priority: 1, controls_count: 2 },
{ id: '46', regulation_code: code, article: 'O.DR_1', title: 'Backup-Strategie', description: '3-2-1 Backup-Regel', implementation_status: 'implemented', priority: 1, controls_count: 1 },
]
}
return []
}

View File

@@ -0,0 +1,83 @@
/**
* Risk Update API Route - Proxy to Backend
*/
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ riskId: string }> }
) {
try {
const { riskId } = await params
const body = await request.json()
const response = await fetch(
`${BACKEND_URL}/api/v1/compliance/risks/${riskId}`,
{
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
signal: AbortSignal.timeout(30000)
}
)
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, details: errorText },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Risk update proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum Backend fehlgeschlagen' },
{ status: 503 }
)
}
}
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ riskId: string }> }
) {
try {
const { riskId } = await params
const response = await fetch(
`${BACKEND_URL}/api/v1/compliance/risks/${riskId}`,
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
signal: AbortSignal.timeout(30000)
}
)
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, details: errorText },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Get risk proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum Backend fehlgeschlagen' },
{ status: 503 }
)
}
}

View File

@@ -0,0 +1,81 @@
/**
* Compliance Risks API Route - Proxy to Backend
*/
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const queryParams = new URLSearchParams()
const level = searchParams.get('level')
const status = searchParams.get('status')
const category = searchParams.get('category')
if (level) queryParams.set('level', level)
if (status) queryParams.set('status', status)
if (category) queryParams.set('category', category)
const url = `${BACKEND_URL}/api/v1/compliance/risks${queryParams.toString() ? `?${queryParams}` : ''}`
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
signal: AbortSignal.timeout(30000)
})
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, details: errorText },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Compliance risks proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum Backend fehlgeschlagen', risks: [] },
{ status: 503 }
)
}
}
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const response = await fetch(`${BACKEND_URL}/api/v1/compliance/risks`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
signal: AbortSignal.timeout(30000)
})
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, details: errorText },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Create risk proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum Backend fehlgeschlagen' },
{ status: 503 }
)
}
}

View File

@@ -0,0 +1,39 @@
/**
* Compliance Seed API Route - Proxy to Backend
*/
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const response = await fetch(`${BACKEND_URL}/api/v1/compliance/seed`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
signal: AbortSignal.timeout(60000) // 60s timeout for seeding
})
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, details: errorText },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Compliance seed proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum Backend fehlgeschlagen' },
{ status: 503 }
)
}
}

View File

@@ -0,0 +1,48 @@
/**
* Consent Admin API Route - Audit Log Proxy
*/
import { NextRequest, NextResponse } from 'next/server'
const CONSENT_SERVICE_URL = process.env.CONSENT_SERVICE_URL || 'http://localhost:8081'
export async function GET(request: NextRequest) {
try {
const authHeader = request.headers.get('Authorization')
const limit = request.nextUrl.searchParams.get('limit') || '100'
const offset = request.nextUrl.searchParams.get('offset') || '0'
const action = request.nextUrl.searchParams.get('action') || ''
const url = new URL(`${CONSENT_SERVICE_URL}/api/v1/internal/audit-log`)
url.searchParams.set('limit', limit)
url.searchParams.set('offset', offset)
if (action) {
url.searchParams.set('action', action)
}
const response = await fetch(url.toString(), {
headers: {
'Content-Type': 'application/json',
...(authHeader ? { 'Authorization': authHeader } : {})
},
signal: AbortSignal.timeout(10000)
})
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json(
{ error: `Consent Service Error: ${response.status}`, details: errorText },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Audit log proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum Consent Service fehlgeschlagen', entries: [] },
{ status: 503 }
)
}
}

View File

@@ -0,0 +1,40 @@
/**
* Consent Admin API Route - Deadlines Proxy
*/
import { NextRequest, NextResponse } from 'next/server'
const CONSENT_SERVICE_URL = process.env.CONSENT_SERVICE_URL || 'http://localhost:8081'
// Trigger deadline processing
export async function POST(request: NextRequest) {
try {
const authHeader = request.headers.get('Authorization')
const response = await fetch(`${CONSENT_SERVICE_URL}/api/v1/admin/deadlines/process`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(authHeader ? { 'Authorization': authHeader } : {})
},
signal: AbortSignal.timeout(30000)
})
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json(
{ error: `Consent Service Error: ${response.status}`, details: errorText },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Deadlines process proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum Consent Service fehlgeschlagen' },
{ status: 503 }
)
}
}

View File

@@ -0,0 +1,80 @@
/**
* Consent Admin API Route - Document Versions Proxy
*/
import { NextRequest, NextResponse } from 'next/server'
const CONSENT_SERVICE_URL = process.env.CONSENT_SERVICE_URL || 'http://localhost:8081'
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params
const authHeader = request.headers.get('Authorization')
const response = await fetch(`${CONSENT_SERVICE_URL}/api/v1/documents/${id}/versions`, {
headers: {
'Content-Type': 'application/json',
...(authHeader ? { 'Authorization': authHeader } : {})
},
signal: AbortSignal.timeout(10000)
})
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json(
{ error: `Consent Service Error: ${response.status}`, details: errorText },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Versions proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum Consent Service fehlgeschlagen', versions: [] },
{ status: 503 }
)
}
}
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params
const authHeader = request.headers.get('Authorization')
const body = await request.json()
const response = await fetch(`${CONSENT_SERVICE_URL}/api/v1/documents/${id}/versions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(authHeader ? { 'Authorization': authHeader } : {})
},
body: JSON.stringify(body),
signal: AbortSignal.timeout(10000)
})
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json(
{ error: `Consent Service Error: ${response.status}`, details: errorText },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data, { status: 201 })
} catch (error) {
console.error('Create version proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum Consent Service fehlgeschlagen' },
{ status: 503 }
)
}
}

View File

@@ -0,0 +1,75 @@
/**
* Consent Admin API Route - Documents Proxy
*
* Proxies requests to consent service documents endpoints
*/
import { NextRequest, NextResponse } from 'next/server'
const CONSENT_SERVICE_URL = process.env.CONSENT_SERVICE_URL || 'http://localhost:8081'
export async function GET(request: NextRequest) {
try {
const authHeader = request.headers.get('Authorization')
const response = await fetch(`${CONSENT_SERVICE_URL}/api/v1/internal/documents`, {
headers: {
'Content-Type': 'application/json',
...(authHeader ? { 'Authorization': authHeader } : {})
},
signal: AbortSignal.timeout(10000)
})
if (!response.ok) {
const errorText = await response.text()
console.error('Consent service documents error:', response.status, errorText)
return NextResponse.json(
{ error: `Consent Service Error: ${response.status}`, details: errorText },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Documents proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum Consent Service fehlgeschlagen', documents: [] },
{ status: 503 }
)
}
}
export async function POST(request: NextRequest) {
try {
const authHeader = request.headers.get('Authorization')
const body = await request.json()
const response = await fetch(`${CONSENT_SERVICE_URL}/api/v1/documents`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(authHeader ? { 'Authorization': authHeader } : {})
},
body: JSON.stringify(body),
signal: AbortSignal.timeout(10000)
})
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json(
{ error: `Consent Service Error: ${response.status}`, details: errorText },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data, { status: 201 })
} catch (error) {
console.error('Create document proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum Consent Service fehlgeschlagen' },
{ status: 503 }
)
}
}

View File

@@ -0,0 +1,44 @@
/**
* Consent Admin API Route - Stats Proxy
*/
import { NextRequest, NextResponse } from 'next/server'
const CONSENT_SERVICE_URL = process.env.CONSENT_SERVICE_URL || 'http://localhost:8081'
export async function GET(request: NextRequest) {
try {
const authHeader = request.headers.get('Authorization')
const documentType = request.nextUrl.searchParams.get('document_type') || ''
const url = new URL(`${CONSENT_SERVICE_URL}/api/v1/internal/stats/consents`)
if (documentType) {
url.searchParams.set('document_type', documentType)
}
const response = await fetch(url.toString(), {
headers: {
'Content-Type': 'application/json',
...(authHeader ? { 'Authorization': authHeader } : {})
},
signal: AbortSignal.timeout(10000)
})
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json(
{ error: `Consent Service Error: ${response.status}`, details: errorText },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Stats proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum Consent Service fehlgeschlagen' },
{ status: 503 }
)
}
}

View File

@@ -0,0 +1,46 @@
/**
* Version Approval History API Route
*/
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ versionId: string }> }
) {
try {
const { versionId } = await params
const response = await fetch(
`${BACKEND_URL}/api/consent/admin/versions/${versionId}/approval-history`,
{
method: 'GET',
headers: {
...(request.headers.get('Authorization')
? { Authorization: request.headers.get('Authorization')! }
: {}),
},
signal: AbortSignal.timeout(30000),
}
)
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, details: errorText },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Approval history proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum Backend fehlgeschlagen' },
{ status: 503 }
)
}
}

View File

@@ -0,0 +1,49 @@
/**
* Approve Version API Route
*/
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ versionId: string }> }
) {
try {
const { versionId } = await params
const body = await request.json().catch(() => ({}))
const response = await fetch(
`${BACKEND_URL}/api/consent/admin/versions/${versionId}/approve`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(request.headers.get('Authorization')
? { Authorization: request.headers.get('Authorization')! }
: {}),
},
body: JSON.stringify(body),
signal: AbortSignal.timeout(30000),
}
)
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, details: errorText },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Approve proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum Backend fehlgeschlagen' },
{ status: 503 }
)
}
}

View File

@@ -0,0 +1,49 @@
/**
* Publish Version API Route
*/
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ versionId: string }> }
) {
try {
const { versionId } = await params
const body = await request.json().catch(() => ({}))
const response = await fetch(
`${BACKEND_URL}/api/consent/admin/versions/${versionId}/publish`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(request.headers.get('Authorization')
? { Authorization: request.headers.get('Authorization')! }
: {}),
},
body: JSON.stringify(body),
signal: AbortSignal.timeout(30000),
}
)
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, details: errorText },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Publish proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum Backend fehlgeschlagen' },
{ status: 503 }
)
}
}

View File

@@ -0,0 +1,49 @@
/**
* Reject Version API Route
*/
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ versionId: string }> }
) {
try {
const { versionId } = await params
const body = await request.json()
const response = await fetch(
`${BACKEND_URL}/api/consent/admin/versions/${versionId}/reject`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(request.headers.get('Authorization')
? { Authorization: request.headers.get('Authorization')! }
: {}),
},
body: JSON.stringify(body),
signal: AbortSignal.timeout(30000),
}
)
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, details: errorText },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Reject proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum Backend fehlgeschlagen' },
{ status: 503 }
)
}
}

View File

@@ -0,0 +1,87 @@
/**
* Consent Version Detail API Route - Update/Delete version
*/
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ versionId: string }> }
) {
try {
const { versionId } = await params
const body = await request.json()
const response = await fetch(
`${BACKEND_URL}/api/consent/admin/versions/${versionId}`,
{
method: 'PUT',
headers: {
'Content-Type': 'application/json',
...(request.headers.get('Authorization')
? { Authorization: request.headers.get('Authorization')! }
: {}),
},
body: JSON.stringify(body),
signal: AbortSignal.timeout(30000),
}
)
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, details: errorText },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Version update proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum Backend fehlgeschlagen' },
{ status: 503 }
)
}
}
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ versionId: string }> }
) {
try {
const { versionId } = await params
const response = await fetch(
`${BACKEND_URL}/api/consent/admin/versions/${versionId}`,
{
method: 'DELETE',
headers: {
...(request.headers.get('Authorization')
? { Authorization: request.headers.get('Authorization')! }
: {}),
},
signal: AbortSignal.timeout(30000),
}
)
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, details: errorText },
{ status: response.status }
)
}
return NextResponse.json({ success: true })
} catch (error) {
console.error('Version delete proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum Backend fehlgeschlagen' },
{ status: 503 }
)
}
}

View File

@@ -0,0 +1,47 @@
/**
* Submit Version for Review API Route
*/
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ versionId: string }> }
) {
try {
const { versionId } = await params
const response = await fetch(
`${BACKEND_URL}/api/consent/admin/versions/${versionId}/submit-review`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(request.headers.get('Authorization')
? { Authorization: request.headers.get('Authorization')! }
: {}),
},
signal: AbortSignal.timeout(30000),
}
)
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, details: errorText },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Submit review proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum Backend fehlgeschlagen' },
{ status: 503 }
)
}
}

View File

@@ -0,0 +1,42 @@
/**
* Consent Versions API Route - Create new version
*/
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const response = await fetch(`${BACKEND_URL}/api/consent/admin/versions`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(request.headers.get('Authorization')
? { Authorization: request.headers.get('Authorization')! }
: {}),
},
body: JSON.stringify(body),
signal: AbortSignal.timeout(30000),
})
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, details: errorText },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Version create proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum Backend fehlgeschlagen' },
{ status: 503 }
)
}
}

View File

@@ -0,0 +1,173 @@
import { NextResponse } from 'next/server'
/**
* Server-side health check proxy
* Checks all services via HTTP from the server to avoid mixed-content issues
*/
interface ServiceConfig {
name: string
port: number
endpoint: string
category: 'core' | 'ai' | 'database' | 'storage'
}
const SERVICES: ServiceConfig[] = [
// Core Services
{ name: 'Backend API', port: 8000, endpoint: '/health', category: 'core' },
{ name: 'Consent Service', port: 8081, endpoint: '/api/v1/health', category: 'core' },
{ name: 'Voice Service', port: 8091, endpoint: '/health', category: 'core' },
{ name: 'Klausur Service', port: 8086, endpoint: '/health', category: 'core' },
{ name: 'Mail Service (Mailpit)', port: 8025, endpoint: '/api/v1/info', category: 'core' },
{ name: 'Edu Search', port: 8088, endpoint: '/health', category: 'core' },
{ name: 'H5P Service', port: 8092, endpoint: '/health', category: 'core' },
// AI Services
{ name: 'Ollama/LLM', port: 11434, endpoint: '/api/tags', category: 'ai' },
{ name: 'Embedding Service', port: 8087, endpoint: '/health', category: 'ai' },
// Databases - checked via backend proxy
{ name: 'PostgreSQL', port: 5432, endpoint: '', category: 'database' },
{ name: 'Qdrant (Vector DB)', port: 6333, endpoint: '/collections', category: 'database' },
{ name: 'Valkey (Cache)', port: 6379, endpoint: '', category: 'database' },
// Storage
{ name: 'MinIO (S3)', port: 9000, endpoint: '/minio/health/live', category: 'storage' },
]
// Use internal Docker hostnames when running in container
const getInternalHost = (port: number): string => {
// Map ports to internal Docker service names
const serviceMap: Record<number, string> = {
8000: 'backend',
8081: 'consent-service',
8091: 'voice-service',
8086: 'klausur-service',
8025: 'mailpit',
8088: 'edu-search-service',
8092: 'h5p-service',
11434: 'ollama',
8087: 'embedding-service',
5432: 'postgres',
6333: 'qdrant',
6379: 'valkey',
9000: 'minio',
}
// In container, use Docker hostnames; otherwise use localhost
const host = process.env.BACKEND_URL ? serviceMap[port] || 'localhost' : 'localhost'
return host
}
async function checkService(service: ServiceConfig): Promise<{
name: string
port: number
category: string
status: 'online' | 'offline' | 'degraded'
responseTime: number
details?: string
}> {
const startTime = Date.now()
try {
// Special handling for PostgreSQL - check via backend
if (service.port === 5432) {
const backendHost = getInternalHost(8000)
const response = await fetch(`http://${backendHost}:8000/api/tests/db-status`, {
method: 'GET',
signal: AbortSignal.timeout(3000),
})
const responseTime = Date.now() - startTime
if (response.ok) {
const data = await response.json()
return {
...service,
status: 'online',
responseTime,
details: data.host || undefined
}
}
return { ...service, status: 'offline', responseTime }
}
// Special handling for Valkey - check via backend
if (service.port === 6379) {
const backendHost = getInternalHost(8000)
try {
const response = await fetch(`http://${backendHost}:8000/api/tests/cache-status`, {
method: 'GET',
signal: AbortSignal.timeout(3000),
})
const responseTime = Date.now() - startTime
if (response.ok) {
return { ...service, status: 'online', responseTime }
}
} catch {
// Fallback: assume online if backend is reachable (Valkey is usually bundled)
}
const responseTime = Date.now() - startTime
return { ...service, status: 'online', responseTime, details: 'via Backend' }
}
const host = getInternalHost(service.port)
const url = `http://${host}:${service.port}${service.endpoint}`
const response = await fetch(url, {
method: 'GET',
signal: AbortSignal.timeout(5000),
})
const responseTime = Date.now() - startTime
if (response.ok) {
// Special handling for Ollama
if (service.port === 11434) {
try {
const data = await response.json()
const modelCount = data.models?.length || 0
return {
...service,
status: 'online',
responseTime,
details: `${modelCount} Modell${modelCount !== 1 ? 'e' : ''} geladen`
}
} catch {
return { ...service, status: 'online', responseTime }
}
}
return { ...service, status: 'online', responseTime }
} else if (response.status >= 500) {
return { ...service, status: 'degraded', responseTime, details: `HTTP ${response.status}` }
} else {
return { ...service, status: 'offline', responseTime }
}
} catch (error) {
const responseTime = Date.now() - startTime
return {
...service,
status: 'offline',
responseTime,
details: error instanceof Error ? error.message : 'Verbindungsfehler'
}
}
}
export async function GET() {
try {
const results = await Promise.all(SERVICES.map(checkService))
return NextResponse.json({
services: results,
timestamp: new Date().toISOString(),
onlineCount: results.filter(s => s.status === 'online').length,
totalCount: results.length
})
} catch (error) {
return NextResponse.json(
{ error: 'Failed to check services', details: error instanceof Error ? error.message : 'Unknown error' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,338 @@
import { NextRequest, NextResponse } from 'next/server'
/**
* Mac Mini System Monitoring API
*
* Provides system stats and Docker container management
* Requires Docker socket mounted at /var/run/docker.sock
*/
interface ContainerInfo {
id: string
name: string
image: string
status: string
state: string
created: string
ports: string[]
cpu_percent: number
memory_usage: string
memory_limit: string
memory_percent: number
network_rx: string
network_tx: string
}
interface SystemStats {
hostname: string
platform: string
arch: string
uptime: number
cpu: {
model: string
cores: number
usage_percent: number
}
memory: {
total: string
used: string
free: string
usage_percent: number
}
disk: {
total: string
used: string
free: string
usage_percent: number
}
timestamp: string
}
interface DockerStats {
containers: ContainerInfo[]
total_containers: number
running_containers: number
stopped_containers: number
}
// Helper to format bytes
function formatBytes(bytes: number): string {
if (bytes === 0) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
}
// Helper to format uptime
function formatUptime(seconds: number): string {
const days = Math.floor(seconds / 86400)
const hours = Math.floor((seconds % 86400) / 3600)
const minutes = Math.floor((seconds % 3600) / 60)
if (days > 0) return `${days}d ${hours}h ${minutes}m`
if (hours > 0) return `${hours}h ${minutes}m`
return `${minutes}m`
}
// Get Docker stats via socket
async function getDockerStats(): Promise<DockerStats> {
const DOCKER_SOCKET = process.env.DOCKER_HOST || 'unix:///var/run/docker.sock'
try {
// Fetch container list
const containersResponse = await fetch(`${DOCKER_SOCKET.replace('unix://', 'http://localhost')}/containers/json?all=true`, {
// @ts-expect-error - Node.js fetch supports unix sockets via socketPath
socketPath: '/var/run/docker.sock',
})
if (!containersResponse.ok) {
throw new Error('Failed to fetch containers')
}
const containers = await containersResponse.json()
// Get stats for running containers
const containerInfos: ContainerInfo[] = await Promise.all(
containers.map(async (container: Record<string, unknown>) => {
const names = container.Names as string[]
const name = names?.[0]?.replace(/^\//, '') || 'unknown'
const state = container.State as string
const status = container.Status as string
const image = container.Image as string
const created = container.Created as number
const ports = container.Ports as Array<{ PrivatePort: number; PublicPort?: number; Type: string }>
let cpu_percent = 0
let memory_usage = '0 B'
let memory_limit = '0 B'
let memory_percent = 0
let network_rx = '0 B'
let network_tx = '0 B'
// Get live stats for running containers
if (state === 'running') {
try {
const statsResponse = await fetch(
`http://localhost/containers/${container.Id}/stats?stream=false`,
{
// @ts-expect-error - Node.js fetch supports unix sockets
socketPath: '/var/run/docker.sock',
}
)
if (statsResponse.ok) {
const stats = await statsResponse.json()
// Calculate CPU usage
const cpuDelta = stats.cpu_stats.cpu_usage.total_usage -
(stats.precpu_stats?.cpu_usage?.total_usage || 0)
const systemDelta = stats.cpu_stats.system_cpu_usage -
(stats.precpu_stats?.system_cpu_usage || 0)
const cpuCount = stats.cpu_stats.online_cpus || 1
if (systemDelta > 0 && cpuDelta > 0) {
cpu_percent = (cpuDelta / systemDelta) * cpuCount * 100
}
// Memory usage
const memUsage = stats.memory_stats?.usage || 0
const memLimit = stats.memory_stats?.limit || 0
memory_usage = formatBytes(memUsage)
memory_limit = formatBytes(memLimit)
memory_percent = memLimit > 0 ? (memUsage / memLimit) * 100 : 0
// Network stats
const networks = stats.networks || {}
let rxBytes = 0
let txBytes = 0
Object.values(networks).forEach((net: unknown) => {
const network = net as { rx_bytes?: number; tx_bytes?: number }
rxBytes += network.rx_bytes || 0
txBytes += network.tx_bytes || 0
})
network_rx = formatBytes(rxBytes)
network_tx = formatBytes(txBytes)
}
} catch {
// Stats not available, use defaults
}
}
return {
id: (container.Id as string).substring(0, 12),
name,
image: (image as string).split(':')[0].split('/').pop() || image,
status,
state,
created: new Date(created * 1000).toISOString(),
ports: ports?.map(p =>
p.PublicPort ? `${p.PublicPort}:${p.PrivatePort}/${p.Type}` : `${p.PrivatePort}/${p.Type}`
) || [],
cpu_percent: Math.round(cpu_percent * 100) / 100,
memory_usage,
memory_limit,
memory_percent: Math.round(memory_percent * 100) / 100,
network_rx,
network_tx,
}
})
)
// Sort by name
containerInfos.sort((a, b) => a.name.localeCompare(b.name))
return {
containers: containerInfos,
total_containers: containerInfos.length,
running_containers: containerInfos.filter(c => c.state === 'running').length,
stopped_containers: containerInfos.filter(c => c.state !== 'running').length,
}
} catch (error) {
console.error('Docker stats error:', error)
// Return empty stats if Docker socket not available
return {
containers: [],
total_containers: 0,
running_containers: 0,
stopped_containers: 0,
}
}
}
// Get system stats
async function getSystemStats(): Promise<SystemStats> {
const os = await import('os')
const cpus = os.cpus()
const totalMem = os.totalmem()
const freeMem = os.freemem()
const usedMem = totalMem - freeMem
// Calculate CPU usage from cpus
let totalIdle = 0
let totalTick = 0
cpus.forEach(cpu => {
for (const type in cpu.times) {
totalTick += cpu.times[type as keyof typeof cpu.times]
}
totalIdle += cpu.times.idle
})
const cpuUsage = 100 - (totalIdle / totalTick * 100)
// Disk stats (root partition)
let diskTotal = 0
let diskUsed = 0
let diskFree = 0
try {
const { execSync } = await import('child_process')
const dfOutput = execSync('df -k / | tail -1').toString()
const parts = dfOutput.split(/\s+/)
diskTotal = parseInt(parts[1]) * 1024
diskUsed = parseInt(parts[2]) * 1024
diskFree = parseInt(parts[3]) * 1024
} catch {
// Disk stats not available
}
return {
hostname: os.hostname(),
platform: os.platform(),
arch: os.arch(),
uptime: os.uptime(),
cpu: {
model: cpus[0]?.model || 'Unknown',
cores: cpus.length,
usage_percent: Math.round(cpuUsage * 100) / 100,
},
memory: {
total: formatBytes(totalMem),
used: formatBytes(usedMem),
free: formatBytes(freeMem),
usage_percent: Math.round((usedMem / totalMem) * 100 * 100) / 100,
},
disk: {
total: formatBytes(diskTotal),
used: formatBytes(diskUsed),
free: formatBytes(diskFree),
usage_percent: diskTotal > 0 ? Math.round((diskUsed / diskTotal) * 100 * 100) / 100 : 0,
},
timestamp: new Date().toISOString(),
}
}
// Container action (start/stop/restart)
async function containerAction(containerId: string, action: 'start' | 'stop' | 'restart'): Promise<void> {
const response = await fetch(
`http://localhost/containers/${containerId}/${action}`,
{
method: 'POST',
// @ts-expect-error - Node.js fetch supports unix sockets
socketPath: '/var/run/docker.sock',
}
)
if (!response.ok && response.status !== 304) {
const error = await response.text()
throw new Error(`Failed to ${action} container: ${error}`)
}
}
// GET - Fetch system and Docker stats
export async function GET() {
try {
const [system, docker] = await Promise.all([
getSystemStats(),
getDockerStats(),
])
return NextResponse.json({
system,
docker,
timestamp: new Date().toISOString(),
})
} catch (error) {
console.error('Mac Mini stats error:', error)
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Unknown error' },
{ status: 500 }
)
}
}
// POST - Container actions (start/stop/restart)
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { container_id, action } = body
if (!container_id || !action) {
return NextResponse.json(
{ error: 'container_id and action required' },
{ status: 400 }
)
}
if (!['start', 'stop', 'restart'].includes(action)) {
return NextResponse.json(
{ error: 'Invalid action. Use: start, stop, restart' },
{ status: 400 }
)
}
await containerAction(container_id, action)
return NextResponse.json({
success: true,
message: `Container ${action} successful`,
container_id,
action,
})
} catch (error) {
console.error('Container action error:', error)
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Action failed' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,208 @@
import { NextRequest, NextResponse } from 'next/server'
// Woodpecker API configuration
const WOODPECKER_URL = process.env.WOODPECKER_URL || 'http://woodpecker-server:8000'
const WOODPECKER_TOKEN = process.env.WOODPECKER_TOKEN || ''
export interface PipelineStep {
name: string
state: 'pending' | 'running' | 'success' | 'failure' | 'skipped'
exit_code: number
error?: string
}
export interface Pipeline {
id: number
number: number
status: 'pending' | 'running' | 'success' | 'failure' | 'error'
event: string
branch: string
commit: string
message: string
author: string
created: number
started: number
finished: number
steps: PipelineStep[]
errors?: string[]
}
export interface WoodpeckerStatusResponse {
status: 'online' | 'offline'
pipelines: Pipeline[]
lastUpdate: string
error?: string
}
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams
const repoId = searchParams.get('repo') || '1'
const limit = parseInt(searchParams.get('limit') || '10')
try {
// Fetch pipelines from Woodpecker API
const response = await fetch(
`${WOODPECKER_URL}/api/repos/${repoId}/pipelines?per_page=${limit}`,
{
headers: {
'Authorization': `Bearer ${WOODPECKER_TOKEN}`,
'Content-Type': 'application/json',
},
cache: 'no-store',
}
)
if (!response.ok) {
return NextResponse.json({
status: 'offline',
pipelines: [],
lastUpdate: new Date().toISOString(),
error: `Woodpecker API nicht erreichbar (${response.status})`
} as WoodpeckerStatusResponse)
}
const rawPipelines = await response.json()
// Transform pipelines to our format
const pipelines: Pipeline[] = rawPipelines.map((p: any) => {
// Extract errors from workflows/steps
const errors: string[] = []
const steps: PipelineStep[] = []
if (p.workflows) {
for (const workflow of p.workflows) {
if (workflow.children) {
for (const child of workflow.children) {
steps.push({
name: child.name,
state: child.state,
exit_code: child.exit_code,
error: child.error
})
if (child.state === 'failure' && child.error) {
errors.push(`${child.name}: ${child.error}`)
}
}
}
}
}
return {
id: p.id,
number: p.number,
status: p.status,
event: p.event,
branch: p.branch,
commit: p.commit?.substring(0, 7) || '',
message: p.message || '',
author: p.author,
created: p.created,
started: p.started,
finished: p.finished,
steps,
errors: errors.length > 0 ? errors : undefined
}
})
return NextResponse.json({
status: 'online',
pipelines,
lastUpdate: new Date().toISOString()
} as WoodpeckerStatusResponse)
} catch (error) {
console.error('Woodpecker API error:', error)
return NextResponse.json({
status: 'offline',
pipelines: [],
lastUpdate: new Date().toISOString(),
error: 'Fehler beim Abrufen des Woodpecker Status'
} as WoodpeckerStatusResponse)
}
}
// Trigger a new pipeline
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { repoId = '1', branch = 'main' } = body
const response = await fetch(
`${WOODPECKER_URL}/api/repos/${repoId}/pipelines`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${WOODPECKER_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ branch }),
}
)
if (!response.ok) {
return NextResponse.json(
{ error: 'Pipeline konnte nicht gestartet werden' },
{ status: 500 }
)
}
const pipeline = await response.json()
return NextResponse.json({
success: true,
pipeline: {
id: pipeline.id,
number: pipeline.number,
status: pipeline.status
}
})
} catch (error) {
console.error('Pipeline trigger error:', error)
return NextResponse.json(
{ error: 'Fehler beim Starten der Pipeline' },
{ status: 500 }
)
}
}
// Get pipeline logs
export async function PUT(request: NextRequest) {
try {
const body = await request.json()
const { repoId = '1', pipelineNumber, stepId } = body
if (!pipelineNumber || !stepId) {
return NextResponse.json(
{ error: 'pipelineNumber und stepId erforderlich' },
{ status: 400 }
)
}
const response = await fetch(
`${WOODPECKER_URL}/api/repos/${repoId}/pipelines/${pipelineNumber}/logs/${stepId}`,
{
headers: {
'Authorization': `Bearer ${WOODPECKER_TOKEN}`,
'Content-Type': 'application/json',
},
}
)
if (!response.ok) {
return NextResponse.json(
{ error: 'Logs nicht verfuegbar' },
{ status: response.status }
)
}
const logs = await response.json()
return NextResponse.json({ logs })
} catch (error) {
console.error('Pipeline logs error:', error)
return NextResponse.json(
{ error: 'Fehler beim Abrufen der Logs' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,81 @@
import { NextResponse } from 'next/server'
/**
* Server-side proxy for Mailpit API
* Avoids CORS and mixed-content issues by fetching from server
*/
// Use internal Docker hostname when running in container
const getMailpitHost = (): string => {
return process.env.BACKEND_URL ? 'mailpit' : 'localhost'
}
export async function GET() {
const host = getMailpitHost()
const mailpitUrl = `http://${host}:8025/api/v1/info`
try {
const response = await fetch(mailpitUrl, {
method: 'GET',
signal: AbortSignal.timeout(5000),
})
if (!response.ok) {
return NextResponse.json(
{ error: 'Mailpit API error', status: response.status },
{ status: response.status }
)
}
const data = await response.json()
// Transform Mailpit response to our expected format
return NextResponse.json({
stats: {
totalAccounts: 1,
activeAccounts: 1,
totalEmails: data.Messages || 0,
unreadEmails: data.Unread || 0,
totalTasks: 0,
pendingTasks: 0,
overdueTasks: 0,
aiAnalyzedCount: 0,
lastSyncTime: new Date().toISOString(),
},
accounts: [{
id: 'mailpit-dev',
email: 'dev@mailpit.local',
displayName: 'Mailpit (Development)',
imapHost: 'mailpit',
imapPort: 1143,
smtpHost: 'mailpit',
smtpPort: 1025,
status: 'active' as const,
lastSync: new Date().toISOString(),
emailCount: data.Messages || 0,
unreadCount: data.Unread || 0,
createdAt: new Date().toISOString(),
}],
syncStatus: {
running: false,
accountsInProgress: [],
lastCompleted: new Date().toISOString(),
errors: [],
},
mailpitInfo: {
version: data.Version,
databaseSize: data.DatabaseSize,
uptime: data.RuntimeStats?.Uptime,
}
})
} catch (error) {
console.error('Failed to fetch from Mailpit:', error)
return NextResponse.json(
{
error: 'Failed to connect to Mailpit',
details: error instanceof Error ? error.message : 'Unknown error'
},
{ status: 503 }
)
}
}

View File

@@ -0,0 +1,172 @@
/**
* Middleware Admin API Proxy - Catch-all route
* Proxies all /api/admin/middleware/* requests to backend
* Forwards authentication cookies for session-based auth
*/
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
function getForwardHeaders(request: NextRequest): HeadersInit {
const headers: HeadersInit = {
'Content-Type': 'application/json',
}
// Forward cookie for session auth
const cookie = request.headers.get('cookie')
if (cookie) {
headers['Cookie'] = cookie
}
// Forward authorization header if present
const auth = request.headers.get('authorization')
if (auth) {
headers['Authorization'] = auth
}
return headers
}
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ path: string[] }> }
) {
const { path } = await params
const pathStr = path.join('/')
const searchParams = request.nextUrl.searchParams.toString()
const url = `${BACKEND_URL}/api/admin/middleware/${pathStr}${searchParams ? `?${searchParams}` : ''}`
try {
const response = await fetch(url, {
method: 'GET',
headers: getForwardHeaders(request),
signal: AbortSignal.timeout(30000)
})
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, details: errorText },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Middleware API proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum Backend fehlgeschlagen' },
{ status: 503 }
)
}
}
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ path: string[] }> }
) {
const { path } = await params
const pathStr = path.join('/')
const url = `${BACKEND_URL}/api/admin/middleware/${pathStr}`
try {
const body = await request.json()
const response = await fetch(url, {
method: 'POST',
headers: getForwardHeaders(request),
body: JSON.stringify(body),
signal: AbortSignal.timeout(30000)
})
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, details: errorText },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Middleware API proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum Backend fehlgeschlagen' },
{ status: 503 }
)
}
}
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ path: string[] }> }
) {
const { path } = await params
const pathStr = path.join('/')
const url = `${BACKEND_URL}/api/admin/middleware/${pathStr}`
try {
const body = await request.json()
const response = await fetch(url, {
method: 'PUT',
headers: getForwardHeaders(request),
body: JSON.stringify(body),
signal: AbortSignal.timeout(30000)
})
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, details: errorText },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Middleware API proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum Backend fehlgeschlagen' },
{ status: 503 }
)
}
}
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ path: string[] }> }
) {
const { path } = await params
const pathStr = path.join('/')
const url = `${BACKEND_URL}/api/admin/middleware/${pathStr}`
try {
const response = await fetch(url, {
method: 'DELETE',
headers: getForwardHeaders(request),
signal: AbortSignal.timeout(30000)
})
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, details: errorText },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Middleware API proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum Backend fehlgeschlagen' },
{ status: 503 }
)
}
}

View File

@@ -0,0 +1,59 @@
/**
* Middleware Admin API Proxy - Base route
* GET /api/admin/middleware -> GET all middleware configs
* Forwards authentication cookies for session-based auth
*/
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
function getForwardHeaders(request: NextRequest): HeadersInit {
const headers: HeadersInit = {
'Content-Type': 'application/json',
}
// Forward cookie for session auth
const cookie = request.headers.get('cookie')
if (cookie) {
headers['Cookie'] = cookie
}
// Forward authorization header if present
const auth = request.headers.get('authorization')
if (auth) {
headers['Authorization'] = auth
}
return headers
}
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams.toString()
const url = `${BACKEND_URL}/api/admin/middleware${searchParams ? `?${searchParams}` : ''}`
try {
const response = await fetch(url, {
method: 'GET',
headers: getForwardHeaders(request),
signal: AbortSignal.timeout(30000)
})
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, details: errorText },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Middleware API proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum Backend fehlgeschlagen' },
{ status: 503 }
)
}
}

View File

@@ -0,0 +1,50 @@
/**
* Night Mode Execute API Route
*
* POST - Sofortige Ausführung (start/stop)
*/
import { NextRequest, NextResponse } from 'next/server'
const NIGHT_SCHEDULER_URL = process.env.NIGHT_SCHEDULER_URL || 'http://night-scheduler:8096'
export async function POST(request: NextRequest) {
try {
const body = await request.json()
if (!body.action || !['start', 'stop'].includes(body.action)) {
return NextResponse.json(
{ error: 'Aktion muss "start" oder "stop" sein' },
{ status: 400 }
)
}
const response = await fetch(`${NIGHT_SCHEDULER_URL}/api/night-mode/execute`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
})
if (!response.ok) {
const error = await response.text()
return NextResponse.json(
{ error: `Night-Scheduler Fehler: ${error}` },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Night-Mode Execute API Error:', error)
return NextResponse.json(
{
error: 'Night-Scheduler nicht erreichbar',
details: error instanceof Error ? error.message : 'Unbekannter Fehler',
},
{ status: 503 }
)
}
}

View File

@@ -0,0 +1,77 @@
/**
* Night Mode API Route
*
* Proxy für den night-scheduler Service (Port 8096)
* GET - Status abrufen
* POST - Konfiguration speichern
*/
import { NextRequest, NextResponse } from 'next/server'
const NIGHT_SCHEDULER_URL = process.env.NIGHT_SCHEDULER_URL || 'http://night-scheduler:8096'
export async function GET() {
try {
const response = await fetch(`${NIGHT_SCHEDULER_URL}/api/night-mode`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
cache: 'no-store',
})
if (!response.ok) {
const error = await response.text()
return NextResponse.json(
{ error: `Night-Scheduler Fehler: ${error}` },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Night-Mode API Error:', error)
return NextResponse.json(
{
error: 'Night-Scheduler nicht erreichbar',
details: error instanceof Error ? error.message : 'Unbekannter Fehler',
},
{ status: 503 }
)
}
}
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const response = await fetch(`${NIGHT_SCHEDULER_URL}/api/night-mode`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
})
if (!response.ok) {
const error = await response.text()
return NextResponse.json(
{ error: `Night-Scheduler Fehler: ${error}` },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Night-Mode API Error:', error)
return NextResponse.json(
{
error: 'Night-Scheduler nicht erreichbar',
details: error instanceof Error ? error.message : 'Unbekannter Fehler',
},
{ status: 503 }
)
}
}

View File

@@ -0,0 +1,41 @@
/**
* Night Mode Services API Route
*
* GET - Liste aller Services abrufen
*/
import { NextResponse } from 'next/server'
const NIGHT_SCHEDULER_URL = process.env.NIGHT_SCHEDULER_URL || 'http://night-scheduler:8096'
export async function GET() {
try {
const response = await fetch(`${NIGHT_SCHEDULER_URL}/api/night-mode/services`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
cache: 'no-store',
})
if (!response.ok) {
const error = await response.text()
return NextResponse.json(
{ error: `Night-Scheduler Fehler: ${error}` },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Night-Mode Services API Error:', error)
return NextResponse.json(
{
error: 'Night-Scheduler nicht erreichbar',
details: error instanceof Error ? error.message : 'Unbekannter Fehler',
},
{ status: 503 }
)
}
}

View File

@@ -0,0 +1,215 @@
import { NextRequest, NextResponse } from 'next/server'
// Backend URL - klausur-service
const KLAUSUR_SERVICE_URL = process.env.KLAUSUR_SERVICE_URL || 'http://localhost:8086'
// Helper to proxy requests to backend
async function proxyRequest(
endpoint: string,
method: string = 'GET',
body?: any
): Promise<Response> {
const url = `${KLAUSUR_SERVICE_URL}${endpoint}`
const options: RequestInit = {
method,
headers: {
'Content-Type': 'application/json',
},
}
if (body) {
options.body = JSON.stringify(body)
}
return fetch(url, options)
}
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams
const action = searchParams.get('action')
const jobId = searchParams.get('job_id')
const versionId = searchParams.get('version_id')
try {
let response: Response
switch (action) {
case 'jobs':
response = await proxyRequest('/api/v1/admin/training/jobs')
break
case 'job':
if (!jobId) {
return NextResponse.json({ error: 'job_id required' }, { status: 400 })
}
response = await proxyRequest(`/api/v1/admin/training/jobs/${jobId}`)
break
case 'models':
response = await proxyRequest('/api/v1/admin/training/models')
break
case 'model':
if (!versionId) {
return NextResponse.json({ error: 'version_id required' }, { status: 400 })
}
response = await proxyRequest(`/api/v1/admin/training/models/${versionId}`)
break
case 'dataset-stats':
response = await proxyRequest('/api/v1/admin/training/dataset/stats')
break
case 'status':
response = await proxyRequest('/api/v1/admin/training/status')
break
default:
return NextResponse.json(
{
error: 'Unknown action',
validActions: ['jobs', 'job', 'models', 'model', 'dataset-stats', 'status']
},
{ status: 400 }
)
}
if (!response.ok) {
const errorText = await response.text()
console.error(`Backend error: ${response.status} - ${errorText}`)
return NextResponse.json(
{ error: 'Backend error', detail: errorText },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Training API proxy error:', error)
return NextResponse.json(
{ error: 'Failed to connect to backend', detail: String(error) },
{ status: 503 }
)
}
}
export async function POST(request: NextRequest) {
const searchParams = request.nextUrl.searchParams
const action = searchParams.get('action')
const jobId = searchParams.get('job_id')
const versionId = searchParams.get('version_id')
try {
let response: Response
let body: any = null
// Parse body if present
try {
const text = await request.text()
if (text) {
body = JSON.parse(text)
}
} catch {
// No body or invalid JSON
}
switch (action) {
case 'create-job':
if (!body) {
return NextResponse.json({ error: 'Body required for job creation' }, { status: 400 })
}
response = await proxyRequest('/api/v1/admin/training/jobs', 'POST', body)
break
case 'pause':
if (!jobId) {
return NextResponse.json({ error: 'job_id required' }, { status: 400 })
}
response = await proxyRequest(`/api/v1/admin/training/jobs/${jobId}/pause`, 'POST')
break
case 'resume':
if (!jobId) {
return NextResponse.json({ error: 'job_id required' }, { status: 400 })
}
response = await proxyRequest(`/api/v1/admin/training/jobs/${jobId}/resume`, 'POST')
break
case 'cancel':
if (!jobId) {
return NextResponse.json({ error: 'job_id required' }, { status: 400 })
}
response = await proxyRequest(`/api/v1/admin/training/jobs/${jobId}/cancel`, 'POST')
break
case 'activate-model':
if (!versionId) {
return NextResponse.json({ error: 'version_id required' }, { status: 400 })
}
response = await proxyRequest(`/api/v1/admin/training/models/${versionId}/activate`, 'POST')
break
default:
return NextResponse.json(
{
error: 'Unknown action',
validActions: ['create-job', 'pause', 'resume', 'cancel', 'activate-model']
},
{ status: 400 }
)
}
if (!response.ok) {
const errorData = await response.json().catch(() => ({ detail: 'Unknown error' }))
return NextResponse.json(errorData, { status: response.status })
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Training API proxy error:', error)
return NextResponse.json(
{ error: 'Failed to connect to backend', detail: String(error) },
{ status: 503 }
)
}
}
export async function DELETE(request: NextRequest) {
const searchParams = request.nextUrl.searchParams
const jobId = searchParams.get('job_id')
const versionId = searchParams.get('version_id')
try {
let response: Response
if (jobId) {
response = await proxyRequest(`/api/v1/admin/training/jobs/${jobId}`, 'DELETE')
} else if (versionId) {
response = await proxyRequest(`/api/v1/admin/training/models/${versionId}`, 'DELETE')
} else {
return NextResponse.json(
{ error: 'Either job_id or version_id required' },
{ status: 400 }
)
}
if (!response.ok) {
const errorData = await response.json().catch(() => ({ detail: 'Unknown error' }))
return NextResponse.json(errorData, { status: response.status })
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Training API proxy error:', error)
return NextResponse.json(
{ error: 'Failed to connect to backend', detail: String(error) },
{ status: 503 }
)
}
}

View File

@@ -0,0 +1,172 @@
/**
* Alerts API Proxy - Catch-all route
* Proxies all /api/alerts/* requests to backend
* Supports: inbox, topics, rules, profile, stats, etc.
*/
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
function getForwardHeaders(request: NextRequest): HeadersInit {
const headers: HeadersInit = {
'Content-Type': 'application/json',
}
// Forward cookie for session auth
const cookie = request.headers.get('cookie')
if (cookie) {
headers['Cookie'] = cookie
}
// Forward authorization header if present
const auth = request.headers.get('authorization')
if (auth) {
headers['Authorization'] = auth
}
return headers
}
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ path: string[] }> }
) {
const { path } = await params
const pathStr = path.join('/')
const searchParams = request.nextUrl.searchParams.toString()
const url = `${BACKEND_URL}/api/alerts/${pathStr}${searchParams ? `?${searchParams}` : ''}`
try {
const response = await fetch(url, {
method: 'GET',
headers: getForwardHeaders(request),
signal: AbortSignal.timeout(30000)
})
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, details: errorText },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Alerts API proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum Backend fehlgeschlagen' },
{ status: 503 }
)
}
}
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ path: string[] }> }
) {
const { path } = await params
const pathStr = path.join('/')
const url = `${BACKEND_URL}/api/alerts/${pathStr}`
try {
const body = await request.json()
const response = await fetch(url, {
method: 'POST',
headers: getForwardHeaders(request),
body: JSON.stringify(body),
signal: AbortSignal.timeout(30000)
})
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, details: errorText },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Alerts API proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum Backend fehlgeschlagen' },
{ status: 503 }
)
}
}
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ path: string[] }> }
) {
const { path } = await params
const pathStr = path.join('/')
const url = `${BACKEND_URL}/api/alerts/${pathStr}`
try {
const body = await request.json()
const response = await fetch(url, {
method: 'PUT',
headers: getForwardHeaders(request),
body: JSON.stringify(body),
signal: AbortSignal.timeout(30000)
})
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, details: errorText },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Alerts API proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum Backend fehlgeschlagen' },
{ status: 503 }
)
}
}
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ path: string[] }> }
) {
const { path } = await params
const pathStr = path.join('/')
const url = `${BACKEND_URL}/api/alerts/${pathStr}`
try {
const response = await fetch(url, {
method: 'DELETE',
headers: getForwardHeaders(request),
signal: AbortSignal.timeout(30000)
})
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, details: errorText },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Alerts API proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum Backend fehlgeschlagen' },
{ status: 503 }
)
}
}

View File

@@ -0,0 +1,86 @@
import { NextRequest, NextResponse } from 'next/server'
/**
* API Proxy for BQAS (Voice Service)
* Forwards requests to the voice-service running on port 8091
*/
// Internal Docker network uses HTTP, nginx handles HTTPS termination externally
const VOICE_SERVICE_URL = process.env.VOICE_SERVICE_URL || 'http://voice-service:8091'
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ path: string[] }> }
) {
const { path } = await params
const pathString = path.join('/')
const searchParams = request.nextUrl.searchParams.toString()
const url = `${VOICE_SERVICE_URL}/api/v1/bqas/${pathString}${searchParams ? `?${searchParams}` : ''}`
try {
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
})
if (!response.ok) {
return NextResponse.json(
{ error: `Voice service returned ${response.status}` },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('BQAS proxy error:', error)
return NextResponse.json(
{ error: 'Failed to connect to voice service' },
{ status: 503 }
)
}
}
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ path: string[] }> }
) {
const { path } = await params
const pathString = path.join('/')
const url = `${VOICE_SERVICE_URL}/api/v1/bqas/${pathString}`
try {
let body = null
try {
body = await request.json()
} catch {
// No body is fine for some POST requests
}
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: body ? JSON.stringify(body) : undefined,
})
if (!response.ok) {
return NextResponse.json(
{ error: `Voice service returned ${response.status}` },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('BQAS proxy error:', error)
return NextResponse.json(
{ error: 'Failed to connect to voice service' },
{ status: 503 }
)
}
}

View File

@@ -0,0 +1,68 @@
/**
* Content API Route
*
* GET: Load current website content
* POST: Save changed content (Admin only)
*/
import { NextRequest, NextResponse } from 'next/server'
import { getContent, saveContent } from '@/lib/content'
import type { WebsiteContent } from '@/lib/content-types'
// GET - Load content
export async function GET() {
try {
const content = getContent()
return NextResponse.json(content)
} catch (error) {
console.error('Error loading content:', error)
return NextResponse.json(
{ error: 'Failed to load content' },
{ status: 500 }
)
}
}
// POST - Save content
export async function POST(request: NextRequest) {
try {
// Simple admin check via header or query
// In production: JWT/Session-based auth
const adminKey = request.headers.get('x-admin-key')
const expectedKey = process.env.ADMIN_API_KEY || 'breakpilot-admin-2024'
if (adminKey !== expectedKey) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
)
}
const content: WebsiteContent = await request.json()
// Validation
if (!content.hero || !content.features || !content.faq || !content.pricing) {
return NextResponse.json(
{ error: 'Invalid content structure' },
{ status: 400 }
)
}
const result = saveContent(content)
if (result.success) {
return NextResponse.json({ success: true, message: 'Content saved' })
} else {
return NextResponse.json(
{ error: result.error || 'Failed to save content' },
{ status: 500 }
)
}
} catch (error) {
console.error('Error saving content:', error)
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Failed to save content' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,100 @@
/**
* DSFA Corpus API Proxy
*
* Proxies requests to klausur-service for DSFA RAG operations.
* Endpoints: /api/v1/dsfa-rag/stats, /api/v1/dsfa-rag/sources
*/
import { NextRequest, NextResponse } from 'next/server'
const KLAUSUR_SERVICE_URL = process.env.KLAUSUR_SERVICE_URL || 'http://klausur-service:8086'
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const action = searchParams.get('action')
try {
let url = `${KLAUSUR_SERVICE_URL}/api/v1/dsfa-rag`
switch (action) {
case 'status':
url += '/stats'
break
case 'sources':
url += '/sources'
break
case 'source-detail': {
const code = searchParams.get('code')
if (!code) {
return NextResponse.json({ error: 'Missing code parameter' }, { status: 400 })
}
url += `/sources/${encodeURIComponent(code)}`
break
}
default:
return NextResponse.json({ error: 'Unknown action' }, { status: 400 })
}
const res = await fetch(url, {
headers: {
'Content-Type': 'application/json',
},
cache: 'no-store',
})
const data = await res.json()
return NextResponse.json(data, { status: res.status })
} catch (error) {
console.error('DSFA corpus proxy error:', error)
return NextResponse.json(
{ error: 'Failed to connect to klausur-service' },
{ status: 503 }
)
}
}
export async function POST(request: NextRequest) {
const { searchParams } = new URL(request.url)
const action = searchParams.get('action')
try {
let url = `${KLAUSUR_SERVICE_URL}/api/v1/dsfa-rag`
switch (action) {
case 'init': {
url += '/init'
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
})
const data = await res.json()
return NextResponse.json(data, { status: res.status })
}
case 'ingest': {
const body = await request.json()
const sourceCode = body.source_code
if (!sourceCode) {
return NextResponse.json({ error: 'Missing source_code' }, { status: 400 })
}
url += `/sources/${encodeURIComponent(sourceCode)}/ingest`
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
const data = await res.json()
return NextResponse.json(data, { status: res.status })
}
default:
return NextResponse.json({ error: 'Unknown action' }, { status: 400 })
}
} catch (error) {
console.error('DSFA corpus proxy error:', error)
return NextResponse.json(
{ error: 'Failed to connect to klausur-service' },
{ status: 503 }
)
}
}

View File

@@ -0,0 +1,246 @@
import { NextRequest, NextResponse } from 'next/server'
/**
* Abitur-Archiv API Route
* Extends abitur-docs with theme search and enhanced filtering
*/
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
// Check for theme/semantic search
const thema = searchParams.get('thema')
if (thema) {
// Use semantic search endpoint
return await handleSemanticSearch(thema, searchParams)
}
// Forward all query params to backend abitur-docs
const queryString = searchParams.toString()
const url = `${BACKEND_URL}/api/abitur-docs/${queryString ? `?${queryString}` : ''}`
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
})
if (!response.ok) {
// Return mock data for development if backend is not available
if (response.status === 404 || response.status === 502) {
return NextResponse.json(getMockDocuments(searchParams))
}
throw new Error(`Backend responded with ${response.status}`)
}
const data = await response.json()
// If backend returns empty, use mock data for demo
if (data.documents && Array.isArray(data.documents) && data.documents.length === 0 && data.total === 0) {
return NextResponse.json(getMockDocuments(searchParams))
}
// Enhance response with theme information
return NextResponse.json({
...data,
themes: extractThemes(data.documents || [])
})
} catch (error) {
console.error('Abitur-Archiv error:', error)
return NextResponse.json(getMockDocuments(new URL(request.url).searchParams))
}
}
async function handleSemanticSearch(thema: string, searchParams: URLSearchParams) {
try {
// Try to call RAG search endpoint
const url = `${BACKEND_URL}/api/rag/search`
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: thema,
collection: 'abitur_documents',
limit: parseInt(searchParams.get('limit') || '20'),
filters: {
fach: searchParams.get('fach') || undefined,
jahr: searchParams.get('jahr') ? parseInt(searchParams.get('jahr')!) : undefined,
bundesland: searchParams.get('bundesland') || undefined,
niveau: searchParams.get('niveau') || undefined,
typ: searchParams.get('typ') || undefined,
}
}),
})
if (response.ok) {
const data = await response.json()
return NextResponse.json({
documents: data.results || [],
total: data.total || 0,
page: 1,
limit: parseInt(searchParams.get('limit') || '20'),
total_pages: 1,
search_query: thema
})
}
} catch (error) {
console.log('RAG search not available, falling back to mock')
}
// Fallback to filtered mock data
return NextResponse.json(getMockDocumentsWithTheme(thema, searchParams))
}
function getMockDocuments(searchParams: URLSearchParams) {
const page = parseInt(searchParams.get('page') || '1')
const limit = parseInt(searchParams.get('limit') || '20')
const fach = searchParams.get('fach')
const jahr = searchParams.get('jahr')
const bundesland = searchParams.get('bundesland')
const niveau = searchParams.get('niveau')
const typ = searchParams.get('typ')
// Generate mock documents
const allDocs = generateMockDocs()
// Apply filters
let filtered = allDocs
if (fach) filtered = filtered.filter(d => d.fach === fach)
if (jahr) filtered = filtered.filter(d => d.jahr === parseInt(jahr))
if (bundesland) filtered = filtered.filter(d => d.bundesland === bundesland)
if (niveau) filtered = filtered.filter(d => d.niveau === niveau)
if (typ) filtered = filtered.filter(d => d.typ === typ)
// Paginate
const start = (page - 1) * limit
const docs = filtered.slice(start, start + limit)
return {
documents: docs,
total: filtered.length,
page,
limit,
total_pages: Math.ceil(filtered.length / limit),
themes: extractThemes(docs)
}
}
function getMockDocumentsWithTheme(thema: string, searchParams: URLSearchParams) {
const limit = parseInt(searchParams.get('limit') || '20')
const allDocs = generateMockDocs()
// Simple theme matching (in production this would be semantic search)
const themaLower = thema.toLowerCase()
let filtered = allDocs
// Match theme to aufgabentyp keywords
if (themaLower.includes('gedicht')) {
filtered = filtered.filter(d => d.themes?.includes('gedichtanalyse'))
} else if (themaLower.includes('drama')) {
filtered = filtered.filter(d => d.themes?.includes('dramenanalyse'))
} else if (themaLower.includes('prosa') || themaLower.includes('roman')) {
filtered = filtered.filter(d => d.themes?.includes('prosaanalyse'))
} else if (themaLower.includes('eroerterung')) {
filtered = filtered.filter(d => d.themes?.includes('eroerterung'))
} else if (themaLower.includes('text') || themaLower.includes('analyse')) {
filtered = filtered.filter(d => d.themes?.includes('textanalyse'))
}
// Apply additional filters
const fach = searchParams.get('fach')
const jahr = searchParams.get('jahr')
if (fach) filtered = filtered.filter(d => d.fach === fach)
if (jahr) filtered = filtered.filter(d => d.jahr === parseInt(jahr))
return {
documents: filtered.slice(0, limit),
total: filtered.length,
page: 1,
limit,
total_pages: Math.ceil(filtered.length / limit),
search_query: thema,
themes: extractThemes(filtered)
}
}
function generateMockDocs() {
const faecher = ['deutsch', 'englisch']
const jahre = [2021, 2022, 2023, 2024, 2025]
const niveaus: Array<'eA' | 'gA'> = ['eA', 'gA']
const typen: Array<'aufgabe' | 'erwartungshorizont'> = ['aufgabe', 'erwartungshorizont']
const aufgabentypen = [
{ nummer: 'I', themes: ['textanalyse', 'sachtext'] },
{ nummer: 'II', themes: ['gedichtanalyse', 'lyrik'] },
{ nummer: 'III', themes: ['prosaanalyse', 'epik'] },
]
const docs = []
let id = 1
for (const jahr of jahre) {
for (const fach of faecher) {
for (const niveau of niveaus) {
for (const aufgabe of aufgabentypen) {
for (const typ of typen) {
const suffix = typ === 'erwartungshorizont' ? '_EWH' : ''
const dateiname = `${jahr}_${capitalize(fach)}_${niveau}_Aufgabe_${aufgabe.nummer}${suffix}.pdf`
docs.push({
id: `doc-${id++}`,
dateiname,
original_dateiname: dateiname,
bundesland: 'niedersachsen',
fach,
jahr,
niveau,
typ,
aufgaben_nummer: aufgabe.nummer,
themes: aufgabe.themes,
status: 'indexed' as const,
confidence: 0.92 + Math.random() * 0.08,
file_path: `/api/education/abitur-archiv/file/${dateiname}`,
file_size: Math.floor(Math.random() * 500000) + 100000,
indexed: true,
vector_ids: [`vec-${id}-1`, `vec-${id}-2`],
created_at: new Date(Date.now() - Math.random() * 365 * 24 * 60 * 60 * 1000).toISOString(),
updated_at: new Date().toISOString(),
})
}
}
}
}
}
return docs
}
function extractThemes(documents: any[]) {
const themeCounts = new Map<string, number>()
for (const doc of documents) {
const themes = doc.themes || []
for (const theme of themes) {
themeCounts.set(theme, (themeCounts.get(theme) || 0) + 1)
}
}
return Array.from(themeCounts.entries())
.map(([label, count]) => ({
label: capitalize(label),
count,
aufgabentyp: label,
}))
.sort((a, b) => b.count - a.count)
.slice(0, 10)
}
function capitalize(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1)
}

View File

@@ -0,0 +1,105 @@
import { NextRequest, NextResponse } from 'next/server'
/**
* Theme Suggestions API for Abitur-Archiv
* Returns autocomplete suggestions for semantic search
*/
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const query = searchParams.get('q') || ''
if (query.length < 2) {
return NextResponse.json({ suggestions: [], query })
}
// Try to get suggestions from backend
try {
const url = `${BACKEND_URL}/api/abitur-archiv/suggest?q=${encodeURIComponent(query)}`
const response = await fetch(url, {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
})
if (response.ok) {
const data = await response.json()
return NextResponse.json(data)
}
} catch (error) {
console.log('Backend suggest not available, using static suggestions')
}
// Fallback to static suggestions
return NextResponse.json({
suggestions: getStaticSuggestions(query),
query
})
} catch (error) {
console.error('Suggest error:', error)
return NextResponse.json({ suggestions: [], query: '' })
}
}
function getStaticSuggestions(query: string) {
const allSuggestions = [
// Textanalyse
{ label: 'Textanalyse', count: 45, aufgabentyp: 'textanalyse', kategorie: 'Analyse' },
{ label: 'Textanalyse Sachtext', count: 28, aufgabentyp: 'textanalyse_pragmatisch', kategorie: 'Analyse' },
{ label: 'Textanalyse Rede', count: 12, aufgabentyp: 'textanalyse_rede', kategorie: 'Analyse' },
{ label: 'Textanalyse Kommentar', count: 8, aufgabentyp: 'textanalyse_kommentar', kategorie: 'Analyse' },
// Gedichtanalyse
{ label: 'Gedichtanalyse', count: 38, aufgabentyp: 'gedichtanalyse', kategorie: 'Lyrik' },
{ label: 'Gedichtanalyse Romantik', count: 15, aufgabentyp: 'gedichtanalyse', zeitraum: 'Romantik', kategorie: 'Lyrik' },
{ label: 'Gedichtanalyse Expressionismus', count: 12, aufgabentyp: 'gedichtanalyse', zeitraum: 'Expressionismus', kategorie: 'Lyrik' },
{ label: 'Gedichtanalyse Barock', count: 8, aufgabentyp: 'gedichtanalyse', zeitraum: 'Barock', kategorie: 'Lyrik' },
{ label: 'Gedichtanalyse Klassik', count: 10, aufgabentyp: 'gedichtanalyse', zeitraum: 'Klassik', kategorie: 'Lyrik' },
{ label: 'Gedichtanalyse Moderne', count: 14, aufgabentyp: 'gedichtanalyse', zeitraum: 'Moderne', kategorie: 'Lyrik' },
{ label: 'Gedichtvergleich', count: 18, aufgabentyp: 'gedichtvergleich', kategorie: 'Lyrik' },
// Dramenanalyse
{ label: 'Dramenanalyse', count: 28, aufgabentyp: 'dramenanalyse', kategorie: 'Drama' },
{ label: 'Dramenanalyse Faust', count: 14, aufgabentyp: 'dramenanalyse', kategorie: 'Drama' },
{ label: 'Dramenanalyse Woyzeck', count: 8, aufgabentyp: 'dramenanalyse', kategorie: 'Drama' },
{ label: 'Episches Theater Brecht', count: 10, aufgabentyp: 'dramenanalyse', kategorie: 'Drama' },
{ label: 'Szenenanalyse', count: 22, aufgabentyp: 'szenenanalyse', kategorie: 'Drama' },
// Prosaanalyse
{ label: 'Prosaanalyse', count: 25, aufgabentyp: 'prosaanalyse', kategorie: 'Epik' },
{ label: 'Romananalyse', count: 18, aufgabentyp: 'prosaanalyse', kategorie: 'Epik' },
{ label: 'Kurzgeschichte', count: 20, aufgabentyp: 'prosaanalyse', kategorie: 'Epik' },
{ label: 'Novelle', count: 12, aufgabentyp: 'prosaanalyse', kategorie: 'Epik' },
{ label: 'Erzaehlung', count: 15, aufgabentyp: 'prosaanalyse', kategorie: 'Epik' },
// Eroerterung
{ label: 'Eroerterung', count: 32, aufgabentyp: 'eroerterung', kategorie: 'Argumentation' },
{ label: 'Eroerterung textgebunden', count: 18, aufgabentyp: 'eroerterung_textgebunden', kategorie: 'Argumentation' },
{ label: 'Eroerterung materialgestuetzt', count: 14, aufgabentyp: 'eroerterung_materialgestuetzt', kategorie: 'Argumentation' },
{ label: 'Stellungnahme', count: 10, aufgabentyp: 'stellungnahme', kategorie: 'Argumentation' },
// Sprachreflexion
{ label: 'Sprachreflexion', count: 15, aufgabentyp: 'sprachreflexion', kategorie: 'Sprache' },
{ label: 'Sprachwandel', count: 8, aufgabentyp: 'sprachreflexion', kategorie: 'Sprache' },
{ label: 'Sprachkritik', count: 6, aufgabentyp: 'sprachreflexion', kategorie: 'Sprache' },
{ label: 'Kommunikation', count: 10, aufgabentyp: 'kommunikation', kategorie: 'Sprache' },
// Vergleich
{ label: 'Vergleichende Analyse', count: 20, aufgabentyp: 'vergleich', kategorie: 'Vergleich' },
{ label: 'Epochenvergleich', count: 12, aufgabentyp: 'epochenvergleich', kategorie: 'Vergleich' },
]
const queryLower = query.toLowerCase()
// Filter suggestions based on query
return allSuggestions
.filter(s =>
s.label.toLowerCase().includes(queryLower) ||
s.aufgabentyp.toLowerCase().includes(queryLower) ||
(s.zeitraum && s.zeitraum.toLowerCase().includes(queryLower)) ||
s.kategorie.toLowerCase().includes(queryLower)
)
.slice(0, 8)
}

View File

@@ -0,0 +1,139 @@
import { NextRequest, NextResponse } from 'next/server'
/**
* Proxy to backend /api/abitur-docs
* Lists and manages Abitur documents (NiBiS, etc.)
*/
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
// Forward all query params to backend
const queryString = searchParams.toString()
const url = `${BACKEND_URL}/api/abitur-docs/${queryString ? `?${queryString}` : ''}`
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
})
if (!response.ok) {
// Return mock data for development if backend is not available
if (response.status === 404 || response.status === 502) {
return NextResponse.json(getMockDocuments(searchParams))
}
throw new Error(`Backend responded with ${response.status}`)
}
const data = await response.json()
// If backend returns empty array, use mock data for demo purposes
// (Backend uses in-memory storage which is lost on restart)
if (Array.isArray(data) && data.length === 0) {
console.log('Backend returned empty array, using mock data')
return NextResponse.json(getMockDocuments(searchParams))
}
// Handle paginated response with empty documents
if (data.documents && Array.isArray(data.documents) && data.documents.length === 0 && data.total === 0) {
console.log('Backend returned empty documents, using mock data')
return NextResponse.json(getMockDocuments(searchParams))
}
return NextResponse.json(data)
} catch (error) {
console.error('Abitur docs list error:', error)
// Return mock data for development
return NextResponse.json(getMockDocuments(new URL(request.url).searchParams))
}
}
function getMockDocuments(searchParams: URLSearchParams) {
const page = parseInt(searchParams.get('page') || '1')
const limit = parseInt(searchParams.get('limit') || '20')
const fach = searchParams.get('fach')
const jahr = searchParams.get('jahr')
const bundesland = searchParams.get('bundesland')
// Generate mock documents
const allDocs = generateMockDocs()
// Apply filters
let filtered = allDocs
if (fach) {
filtered = filtered.filter(d => d.fach === fach)
}
if (jahr) {
filtered = filtered.filter(d => d.jahr === parseInt(jahr))
}
if (bundesland) {
filtered = filtered.filter(d => d.bundesland === bundesland)
}
// Paginate
const start = (page - 1) * limit
const docs = filtered.slice(start, start + limit)
return {
documents: docs,
total: filtered.length,
page,
limit,
total_pages: Math.ceil(filtered.length / limit),
}
}
function generateMockDocs() {
const faecher = ['deutsch', 'mathematik', 'englisch', 'biologie', 'physik', 'chemie', 'geschichte']
const jahre = [2024, 2025]
const niveaus = ['eA', 'gA']
const typen = ['aufgabe', 'erwartungshorizont']
const nummern = ['I', 'II', 'III']
const docs = []
let id = 1
for (const jahr of jahre) {
for (const fach of faecher) {
for (const niveau of niveaus) {
for (const nummer of nummern) {
for (const typ of typen) {
const suffix = typ === 'erwartungshorizont' ? '_EWH' : ''
const dateiname = `${jahr}_${capitalize(fach)}_${niveau}_${nummer}${suffix}.pdf`
docs.push({
id: `doc-${id++}`,
dateiname,
original_dateiname: dateiname,
bundesland: 'niedersachsen',
fach,
jahr,
niveau,
typ,
aufgaben_nummer: nummer,
status: 'indexed',
confidence: 0.95,
file_path: `/tmp/abitur-docs/${dateiname}`,
file_size: Math.floor(Math.random() * 500000) + 100000,
indexed: true,
vector_ids: [`vec-${id}-1`, `vec-${id}-2`],
created_at: new Date(Date.now() - Math.random() * 30 * 24 * 60 * 60 * 1000).toISOString(),
updated_at: new Date().toISOString(),
})
}
}
}
}
}
return docs
}
function capitalize(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1)
}

View File

@@ -0,0 +1,395 @@
import { NextRequest, NextResponse } from 'next/server'
import type { ExtractedError, ErrorCategory, LogExtractionResponse } from '@/types/infrastructure-modules'
// Woodpecker API configuration
const WOODPECKER_URL = process.env.WOODPECKER_URL || 'http://woodpecker-server:8000'
const WOODPECKER_TOKEN = process.env.WOODPECKER_TOKEN || ''
// =============================================================================
// Error Pattern Matching
// =============================================================================
interface ErrorPattern {
pattern: RegExp
category: ErrorCategory
extractMessage?: (match: RegExpMatchArray, line: string) => string
}
/**
* Patterns fuer verschiedene Fehlertypen in CI/CD Logs
*/
const ERROR_PATTERNS: ErrorPattern[] = [
// Test Failures
{
pattern: /^(FAIL|FAILED|ERROR):?\s+(.+)$/i,
category: 'test_failure',
extractMessage: (match, line) => match[2] || line,
},
{
pattern: /^---\s+FAIL:\s+(.+)\s+\([\d.]+s\)$/,
category: 'test_failure',
extractMessage: (match) => `Test failed: ${match[1]}`,
},
{
pattern: /pytest.*FAILED\s+(.+)$/,
category: 'test_failure',
extractMessage: (match) => `pytest: ${match[1]}`,
},
{
pattern: /AssertionError:\s+(.+)$/,
category: 'test_failure',
extractMessage: (match) => `Assertion failed: ${match[1]}`,
},
{
pattern: /FAIL\s+[\w\/]+\s+\[build failed\]/,
category: 'build_error',
},
// Build Errors
{
pattern: /^(error|Error)\[[\w-]+\]:\s+(.+)$/,
category: 'build_error',
extractMessage: (match) => match[2],
},
{
pattern: /cannot find (module|package)\s+["'](.+)["']/i,
category: 'build_error',
extractMessage: (match) => `Missing ${match[1]}: ${match[2]}`,
},
{
pattern: /undefined:\s+(.+)$/,
category: 'build_error',
extractMessage: (match) => `Undefined: ${match[1]}`,
},
{
pattern: /compilation failed/i,
category: 'build_error',
},
{
pattern: /npm ERR!\s+(.+)$/,
category: 'build_error',
extractMessage: (match) => `npm error: ${match[1]}`,
},
{
pattern: /go:\s+(.+):\s+(.+)$/,
category: 'build_error',
extractMessage: (match) => `Go: ${match[1]}: ${match[2]}`,
},
// Security Warnings
{
pattern: /\[CRITICAL\]\s+(.+)$/i,
category: 'security_warning',
extractMessage: (match) => `Critical: ${match[1]}`,
},
{
pattern: /\[HIGH\]\s+(.+)$/i,
category: 'security_warning',
extractMessage: (match) => `High severity: ${match[1]}`,
},
{
pattern: /CVE-\d{4}-\d+/,
category: 'security_warning',
extractMessage: (match, line) => line.trim(),
},
{
pattern: /vulnerability found/i,
category: 'security_warning',
},
{
pattern: /secret.*detected/i,
category: 'security_warning',
},
{
pattern: /gitleaks.*found/i,
category: 'security_warning',
},
{
pattern: /semgrep.*finding/i,
category: 'security_warning',
},
// License Violations
{
pattern: /license.*violation/i,
category: 'license_violation',
},
{
pattern: /incompatible license/i,
category: 'license_violation',
},
{
pattern: /AGPL|GPL-3|SSPL/,
category: 'license_violation',
extractMessage: (match, line) => `Potentially problematic license found: ${match[0]}`,
},
// Dependency Issues
{
pattern: /dependency.*not found/i,
category: 'dependency_issue',
},
{
pattern: /outdated.*dependency/i,
category: 'dependency_issue',
},
{
pattern: /version conflict/i,
category: 'dependency_issue',
},
]
/**
* Patterns to extract file paths from error lines
*/
const FILE_PATH_PATTERNS = [
/([\/\w.-]+\.(go|py|ts|tsx|js|jsx|rs)):(\d+)/,
/File "([^"]+)", line (\d+)/,
/at ([\/\w.-]+):(\d+):\d+/,
]
/**
* Patterns to extract service names from log lines or paths
*/
const SERVICE_PATTERNS = [
/service[s]?\/([a-z-]+)/i,
/\/([a-z-]+-service)\//i,
/^([a-z-]+):\s/,
]
// =============================================================================
// Log Parsing Functions
// =============================================================================
interface LogLine {
pos: number
out: string
time: number
}
function extractFilePath(line: string): { path?: string; lineNumber?: number } {
for (const pattern of FILE_PATH_PATTERNS) {
const match = line.match(pattern)
if (match) {
return {
path: match[1],
lineNumber: parseInt(match[2] || match[3], 10) || undefined,
}
}
}
return {}
}
function extractService(line: string, filePath?: string): string | undefined {
// First try to extract from file path
if (filePath) {
for (const pattern of SERVICE_PATTERNS) {
const match = filePath.match(pattern)
if (match) return match[1]
}
}
// Then try from the line itself
for (const pattern of SERVICE_PATTERNS) {
const match = line.match(pattern)
if (match) return match[1]
}
return undefined
}
function parseLogLines(logs: LogLine[], stepName: string): ExtractedError[] {
const errors: ExtractedError[] = []
const seenMessages = new Set<string>()
for (const logLine of logs) {
const line = logLine.out.trim()
if (!line) continue
for (const errorPattern of ERROR_PATTERNS) {
const match = line.match(errorPattern.pattern)
if (match) {
const message = errorPattern.extractMessage
? errorPattern.extractMessage(match, line)
: line
// Deduplicate similar errors
const messageKey = `${errorPattern.category}:${message.substring(0, 100)}`
if (seenMessages.has(messageKey)) continue
seenMessages.add(messageKey)
const fileInfo = extractFilePath(line)
const service = extractService(line, fileInfo.path)
errors.push({
step: stepName,
line: logLine.pos,
message,
category: errorPattern.category,
file_path: fileInfo.path,
service,
})
break // Only match first pattern per line
}
}
}
return errors
}
// =============================================================================
// API Handler
// =============================================================================
/**
* POST /api/infrastructure/logs/extract
*
* Extrahiert Fehler aus Woodpecker Pipeline Logs.
*
* Request Body:
* - pipeline_number: number (required)
* - repo_id?: string (default: '1')
*
* Response:
* - errors: ExtractedError[]
* - pipeline_number: number
* - extracted_at: string
* - lines_parsed: number
*/
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { pipeline_number, repo_id = '1' } = body
if (!pipeline_number) {
return NextResponse.json(
{ error: 'pipeline_number ist erforderlich' },
{ status: 400 }
)
}
// 1. Fetch pipeline details to get step IDs
const pipelineResponse = await fetch(
`${WOODPECKER_URL}/api/repos/${repo_id}/pipelines/${pipeline_number}`,
{
headers: {
'Authorization': `Bearer ${WOODPECKER_TOKEN}`,
'Content-Type': 'application/json',
},
cache: 'no-store',
}
)
if (!pipelineResponse.ok) {
return NextResponse.json(
{ error: `Pipeline ${pipeline_number} nicht gefunden` },
{ status: 404 }
)
}
const pipeline = await pipelineResponse.json()
// 2. Extract step IDs from workflows
const failedSteps: { id: number; name: string }[] = []
if (pipeline.workflows) {
for (const workflow of pipeline.workflows) {
if (workflow.children) {
for (const child of workflow.children) {
if (child.state === 'failure' || child.state === 'error') {
failedSteps.push({
id: child.id,
name: child.name,
})
}
}
}
}
}
// 3. Fetch logs for each failed step
const allErrors: ExtractedError[] = []
let totalLinesParsed = 0
for (const step of failedSteps) {
try {
const logsResponse = await fetch(
`${WOODPECKER_URL}/api/repos/${repo_id}/pipelines/${pipeline_number}/logs/${step.id}`,
{
headers: {
'Authorization': `Bearer ${WOODPECKER_TOKEN}`,
'Content-Type': 'application/json',
},
}
)
if (logsResponse.ok) {
const logs: LogLine[] = await logsResponse.json()
totalLinesParsed += logs.length
const stepErrors = parseLogLines(logs, step.name)
allErrors.push(...stepErrors)
}
} catch (logError) {
console.error(`Failed to fetch logs for step ${step.name}:`, logError)
}
}
// 4. Sort errors by severity (security > license > build > test > dependency)
const categoryPriority: Record<ErrorCategory, number> = {
'security_warning': 1,
'license_violation': 2,
'build_error': 3,
'test_failure': 4,
'dependency_issue': 5,
}
allErrors.sort((a, b) => categoryPriority[a.category] - categoryPriority[b.category])
const response: LogExtractionResponse = {
errors: allErrors,
pipeline_number,
extracted_at: new Date().toISOString(),
lines_parsed: totalLinesParsed,
}
return NextResponse.json(response)
} catch (error) {
console.error('Log extraction error:', error)
return NextResponse.json(
{ error: 'Fehler bei der Log-Extraktion' },
{ status: 500 }
)
}
}
/**
* GET /api/infrastructure/logs/extract?pipeline_number=123
*
* Convenience method - calls POST internally
*/
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams
const pipeline_number = searchParams.get('pipeline_number')
const repo_id = searchParams.get('repo_id') || '1'
if (!pipeline_number) {
return NextResponse.json(
{ error: 'pipeline_number Query-Parameter ist erforderlich' },
{ status: 400 }
)
}
// Create a mock request with JSON body
const mockRequest = new NextRequest(request.url, {
method: 'POST',
body: JSON.stringify({ pipeline_number: parseInt(pipeline_number, 10), repo_id }),
headers: {
'Content-Type': 'application/json',
},
})
return POST(mockRequest)
}

View File

@@ -0,0 +1,180 @@
/**
* Legal Corpus API Proxy
*
* Proxies requests to klausur-service for RAG operations.
* This allows the client-side RAG page to call the API without CORS issues.
*/
import { NextRequest, NextResponse } from 'next/server'
const KLAUSUR_SERVICE_URL = process.env.KLAUSUR_SERVICE_URL || 'http://klausur-service:8086'
const QDRANT_URL = process.env.QDRANT_URL || 'http://qdrant:6333'
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const action = searchParams.get('action')
try {
let url = `${KLAUSUR_SERVICE_URL}/api/v1/admin/legal-corpus`
switch (action) {
case 'status': {
// Query Qdrant directly for collection stats
const qdrantRes = await fetch(`${QDRANT_URL}/collections/bp_legal_corpus`, {
cache: 'no-store',
})
if (!qdrantRes.ok) {
return NextResponse.json({ error: 'Qdrant not available' }, { status: 503 })
}
const qdrantData = await qdrantRes.json()
const result = qdrantData.result || {}
return NextResponse.json({
collection: 'bp_legal_corpus',
totalPoints: result.points_count || 0,
vectorSize: result.config?.params?.vectors?.size || 0,
status: result.status || 'unknown',
regulations: {},
})
}
case 'search':
const query = searchParams.get('query')
const topK = searchParams.get('top_k') || '5'
const regulations = searchParams.get('regulations')
url += `/search?query=${encodeURIComponent(query || '')}&top_k=${topK}`
if (regulations) {
url += `&regulations=${encodeURIComponent(regulations)}`
}
break
case 'ingestion-status':
url += '/ingestion-status'
break
case 'regulations':
url += '/regulations'
break
case 'custom-documents':
url += '/custom-documents'
break
case 'pipeline-checkpoints':
url = `${KLAUSUR_SERVICE_URL}/api/v1/admin/pipeline/checkpoints`
break
case 'pipeline-status':
url = `${KLAUSUR_SERVICE_URL}/api/v1/admin/pipeline/status`
break
case 'traceability': {
const chunkId = searchParams.get('chunk_id')
const regulation = searchParams.get('regulation')
url += `/traceability?chunk_id=${encodeURIComponent(chunkId || '')}&regulation=${encodeURIComponent(regulation || '')}`
break
}
default:
return NextResponse.json({ error: 'Unknown action' }, { status: 400 })
}
const res = await fetch(url, {
headers: {
'Content-Type': 'application/json',
},
cache: 'no-store',
})
const data = await res.json()
return NextResponse.json(data, { status: res.status })
} catch (error) {
console.error('Legal corpus proxy error:', error)
return NextResponse.json(
{ error: 'Failed to connect to klausur-service' },
{ status: 503 }
)
}
}
export async function POST(request: NextRequest) {
const { searchParams } = new URL(request.url)
const action = searchParams.get('action')
try {
let url = `${KLAUSUR_SERVICE_URL}/api/v1/admin/legal-corpus`
switch (action) {
case 'ingest': {
url += '/ingest'
const body = await request.json()
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
const data = await res.json()
return NextResponse.json(data, { status: res.status })
}
case 'add-link': {
url += '/add-link'
const body = await request.json()
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
const data = await res.json()
return NextResponse.json(data, { status: res.status })
}
case 'upload': {
url += '/upload'
// Forward FormData directly
const formData = await request.formData()
const res = await fetch(url, {
method: 'POST',
body: formData,
})
const data = await res.json()
return NextResponse.json(data, { status: res.status })
}
case 'start-pipeline': {
url = `${KLAUSUR_SERVICE_URL}/api/v1/admin/pipeline/start`
const body = await request.json()
const res = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body),
})
const data = await res.json()
return NextResponse.json(data, { status: res.status })
}
default:
return NextResponse.json({ error: 'Unknown action' }, { status: 400 })
}
} catch (error) {
console.error('Legal corpus proxy error:', error)
return NextResponse.json(
{ error: 'Failed to connect to klausur-service' },
{ status: 503 }
)
}
}
export async function DELETE(request: NextRequest) {
const { searchParams } = new URL(request.url)
const action = searchParams.get('action')
const docId = searchParams.get('docId')
try {
if (action === 'delete-document' && docId) {
const url = `${KLAUSUR_SERVICE_URL}/api/v1/admin/legal-corpus/custom-documents/${docId}`
const res = await fetch(url, { method: 'DELETE' })
const data = await res.json()
return NextResponse.json(data, { status: res.status })
}
return NextResponse.json({ error: 'Unknown action' }, { status: 400 })
} catch (error) {
console.error('Legal corpus proxy error:', error)
return NextResponse.json(
{ error: 'Failed to connect to klausur-service' },
{ status: 503 }
)
}
}

View File

@@ -0,0 +1,75 @@
/**
* Test Registry API Proxy
*
* Leitet Anfragen an das Python-Backend (Port 8000) weiter.
* Vermeidet CORS-Probleme und ermoeglicht Server-Side Rendering.
*/
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ path: string[] }> }
) {
const { path } = await params
const pathStr = path.join('/')
const url = new URL(request.url)
const queryString = url.search
try {
const response = await fetch(`${BACKEND_URL}/api/tests/${pathStr}${queryString}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
})
const data = await response.json()
return NextResponse.json(data, { status: response.status })
} catch (error) {
console.error('Test Registry API proxy error:', error)
return NextResponse.json(
{ error: 'Backend nicht erreichbar', demo: true },
{ status: 503 }
)
}
}
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ path: string[] }> }
) {
const { path } = await params
const pathStr = path.join('/')
try {
let body = null
const contentType = request.headers.get('content-type')
if (contentType?.includes('application/json')) {
try {
body = await request.json()
} catch {
// Empty body is OK for some endpoints
}
}
const response = await fetch(`${BACKEND_URL}/api/tests/${pathStr}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: body ? JSON.stringify(body) : undefined,
})
const data = await response.json()
return NextResponse.json(data, { status: response.status })
} catch (error) {
console.error('Test Registry API proxy error:', error)
return NextResponse.json(
{ error: 'Backend nicht erreichbar', demo: true },
{ status: 503 }
)
}
}

View File

@@ -0,0 +1,97 @@
/**
* Security API Proxy - Catch-all route
* Proxies all /api/v1/security/* requests to backend
*/
import { NextRequest, NextResponse } from 'next/server'
const BACKEND_URL = process.env.BACKEND_URL || 'http://localhost:8000'
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ path: string[] }> }
) {
const { path } = await params
const pathStr = path.join('/')
const searchParams = request.nextUrl.searchParams.toString()
const url = `${BACKEND_URL}/api/v1/security/${pathStr}${searchParams ? `?${searchParams}` : ''}`
try {
const response = await fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
},
signal: AbortSignal.timeout(30000)
})
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, details: errorText },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Security API proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum Backend fehlgeschlagen' },
{ status: 503 }
)
}
}
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ path: string[] }> }
) {
const { path } = await params
const pathStr = path.join('/')
const url = `${BACKEND_URL}/api/v1/security/${pathStr}`
try {
let body = null
const contentType = request.headers.get('content-type')
if (contentType?.includes('application/json')) {
// Try to parse JSON body, but handle empty body gracefully
try {
const text = await request.text()
if (text && text.trim()) {
body = JSON.parse(text)
}
} catch {
// Empty or invalid JSON body - continue without body
body = null
}
}
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: body ? JSON.stringify(body) : undefined,
signal: AbortSignal.timeout(120000) // 2 min for scans
})
if (!response.ok) {
const errorText = await response.text()
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, details: errorText },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Security API proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum Backend fehlgeschlagen' },
{ status: 503 }
)
}
}

View File

@@ -0,0 +1,273 @@
import { NextRequest, NextResponse } from 'next/server'
import type { WoodpeckerWebhookPayload, ExtractedError, BacklogSource } from '@/types/infrastructure-modules'
// =============================================================================
// Configuration
// =============================================================================
// Webhook secret for verification (optional but recommended)
const WEBHOOK_SECRET = process.env.WOODPECKER_WEBHOOK_SECRET || ''
// Internal API URL for log extraction
const LOG_EXTRACT_URL = process.env.NEXT_PUBLIC_APP_URL
? `${process.env.NEXT_PUBLIC_APP_URL}/api/infrastructure/log-extract/extract`
: 'http://localhost:3002/api/infrastructure/log-extract/extract'
// Test service API URL for backlog insertion
const TEST_SERVICE_URL = process.env.TEST_SERVICE_URL || 'http://localhost:8086'
// =============================================================================
// Helper Functions
// =============================================================================
/**
* Verify webhook signature (if secret is configured)
*/
function verifySignature(request: NextRequest, body: string): boolean {
if (!WEBHOOK_SECRET) return true // Skip verification if no secret configured
const signature = request.headers.get('X-Woodpecker-Signature')
if (!signature) return false
// Simple HMAC verification (Woodpecker uses SHA256)
const crypto = require('crypto')
const expectedSignature = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(body)
.digest('hex')
return signature === `sha256=${expectedSignature}`
}
/**
* Map error category to backlog priority
*/
function categoryToPriority(category: string): 'critical' | 'high' | 'medium' | 'low' {
switch (category) {
case 'security_warning':
return 'critical'
case 'build_error':
return 'high'
case 'license_violation':
return 'high'
case 'test_failure':
return 'medium'
case 'dependency_issue':
return 'low'
default:
return 'medium'
}
}
/**
* Map error category to error_type for backlog
*/
function categoryToErrorType(category: string): string {
switch (category) {
case 'security_warning':
return 'security'
case 'build_error':
return 'build'
case 'license_violation':
return 'license'
case 'test_failure':
return 'test'
case 'dependency_issue':
return 'dependency'
default:
return 'unknown'
}
}
/**
* Insert extracted errors into backlog
*/
async function insertIntoBacklog(
errors: ExtractedError[],
pipelineNumber: number,
source: BacklogSource
): Promise<{ inserted: number; failed: number }> {
let inserted = 0
let failed = 0
for (const error of errors) {
try {
// Create backlog item
const backlogItem = {
test_name: error.message.substring(0, 200), // Truncate long messages
test_file: error.file_path || null,
service: error.service || 'unknown',
framework: `ci_cd_pipeline_${pipelineNumber}`,
error_message: error.message,
error_type: categoryToErrorType(error.category),
status: 'open',
priority: categoryToPriority(error.category),
fix_suggestion: error.suggested_fix || null,
notes: `Auto-generated from pipeline #${pipelineNumber}, step: ${error.step}, line: ${error.line}`,
source, // Custom field to track origin
}
// Try to insert into test service backlog
const response = await fetch(`${TEST_SERVICE_URL}/api/v1/backlog`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(backlogItem),
})
if (response.ok) {
inserted++
} else {
console.warn(`Failed to insert backlog item: ${response.status}`)
failed++
}
} catch (insertError) {
console.error('Backlog insertion error:', insertError)
failed++
}
}
return { inserted, failed }
}
// =============================================================================
// API Handler
// =============================================================================
/**
* POST /api/webhooks/woodpecker
*
* Webhook endpoint fuer Woodpecker CI/CD Events.
*
* Bei Pipeline-Failure:
* 1. Extrahiert Logs mit /api/infrastructure/logs/extract
* 2. Parsed Fehler nach Kategorie
* 3. Traegt automatisch in Backlog ein
*
* Request Body (Woodpecker Webhook Format):
* - event: 'pipeline_success' | 'pipeline_failure' | 'pipeline_started'
* - repo_id: number
* - pipeline_number: number
* - branch?: string
* - commit?: string
* - author?: string
* - message?: string
*/
export async function POST(request: NextRequest) {
try {
const bodyText = await request.text()
// Verify webhook signature
if (!verifySignature(request, bodyText)) {
return NextResponse.json(
{ error: 'Invalid webhook signature' },
{ status: 401 }
)
}
const payload: WoodpeckerWebhookPayload = JSON.parse(bodyText)
// Log all events for debugging
console.log(`Woodpecker webhook: ${payload.event} for pipeline #${payload.pipeline_number}`)
// Only process pipeline_failure events
if (payload.event !== 'pipeline_failure') {
return NextResponse.json({
status: 'ignored',
message: `Event ${payload.event} wird nicht verarbeitet`,
pipeline_number: payload.pipeline_number,
})
}
// 1. Extract logs from failed pipeline
console.log(`Extracting logs for failed pipeline #${payload.pipeline_number}`)
const extractResponse = await fetch(LOG_EXTRACT_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
pipeline_number: payload.pipeline_number,
repo_id: String(payload.repo_id),
}),
})
if (!extractResponse.ok) {
const errorText = await extractResponse.text()
console.error('Log extraction failed:', errorText)
return NextResponse.json({
status: 'error',
message: 'Log-Extraktion fehlgeschlagen',
pipeline_number: payload.pipeline_number,
}, { status: 500 })
}
const extractionResult = await extractResponse.json()
const errors: ExtractedError[] = extractionResult.errors || []
console.log(`Extracted ${errors.length} errors from pipeline #${payload.pipeline_number}`)
// 2. Insert errors into backlog
if (errors.length > 0) {
const backlogResult = await insertIntoBacklog(
errors,
payload.pipeline_number,
'ci_cd'
)
console.log(`Backlog: ${backlogResult.inserted} inserted, ${backlogResult.failed} failed`)
return NextResponse.json({
status: 'processed',
pipeline_number: payload.pipeline_number,
branch: payload.branch,
commit: payload.commit,
errors_found: errors.length,
backlog_inserted: backlogResult.inserted,
backlog_failed: backlogResult.failed,
categories: {
test_failure: errors.filter(e => e.category === 'test_failure').length,
build_error: errors.filter(e => e.category === 'build_error').length,
security_warning: errors.filter(e => e.category === 'security_warning').length,
license_violation: errors.filter(e => e.category === 'license_violation').length,
dependency_issue: errors.filter(e => e.category === 'dependency_issue').length,
},
})
}
return NextResponse.json({
status: 'processed',
pipeline_number: payload.pipeline_number,
message: 'Keine Fehler extrahiert',
errors_found: 0,
})
} catch (error) {
console.error('Webhook processing error:', error)
return NextResponse.json(
{ error: 'Webhook-Verarbeitung fehlgeschlagen' },
{ status: 500 }
)
}
}
/**
* GET /api/webhooks/woodpecker
*
* Health check endpoint
*/
export async function GET() {
return NextResponse.json({
status: 'ready',
endpoint: '/api/webhooks/woodpecker',
events: ['pipeline_failure'],
description: 'Woodpecker CI/CD Webhook Handler',
configured: {
webhook_secret: WEBHOOK_SECRET ? 'yes' : 'no',
log_extract_url: LOG_EXTRACT_URL,
test_service_url: TEST_SERVICE_URL,
},
})
}

View File

@@ -0,0 +1,65 @@
/**
* Website Content API Route
*
* GET: Load current website content
* POST: Save changed content (Admin only)
*/
import { NextRequest, NextResponse } from 'next/server'
import { getContent, saveContent } from '@/lib/content'
import type { WebsiteContent } from '@/lib/content-types'
// GET - Load content
export async function GET() {
try {
const content = getContent()
return NextResponse.json(content)
} catch (error) {
console.error('Error loading content:', error)
return NextResponse.json(
{ error: 'Failed to load content' },
{ status: 500 }
)
}
}
// POST - Save content
export async function POST(request: NextRequest) {
try {
const adminKey = request.headers.get('x-admin-key')
const expectedKey = process.env.ADMIN_API_KEY || 'breakpilot-admin-2024'
if (adminKey !== expectedKey) {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 401 }
)
}
const content: WebsiteContent = await request.json()
if (!content.hero || !content.features || !content.faq || !content.pricing) {
return NextResponse.json(
{ error: 'Invalid content structure' },
{ status: 400 }
)
}
const result = saveContent(content)
if (result.success) {
return NextResponse.json({ success: true, message: 'Content saved' })
} else {
return NextResponse.json(
{ error: result.error || 'Failed to save content' },
{ status: 500 }
)
}
} catch (error) {
console.error('Error saving content:', error)
return NextResponse.json(
{ error: error instanceof Error ? error.message : 'Failed to save content' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,52 @@
/**
* Website Status API Route
*
* GET: Health-Check ob die Website (Port 3000) erreichbar ist
*/
import { NextResponse } from 'next/server'
const WEBSITE_URL = process.env.WEBSITE_URL || 'http://website:3000'
const WEBSITE_FALLBACK_URL = 'https://macmini:3000'
export async function GET() {
const start = Date.now()
try {
const controller = new AbortController()
const timeout = setTimeout(() => controller.abort(), 5000)
let response: Response | null = null
try {
response = await fetch(WEBSITE_URL, {
method: 'HEAD',
signal: controller.signal,
cache: 'no-store',
})
} catch {
// Docker-internal name failed, try fallback
response = await fetch(WEBSITE_FALLBACK_URL, {
method: 'HEAD',
signal: controller.signal,
cache: 'no-store',
})
} finally {
clearTimeout(timeout)
}
const responseTime = Date.now() - start
return NextResponse.json({
online: response.ok || response.status < 500,
responseTime,
statusCode: response.status,
})
} catch (error) {
const responseTime = Date.now() - start
return NextResponse.json({
online: false,
responseTime,
error: error instanceof Error ? error.message : 'Website nicht erreichbar',
})
}
}