Files
breakpilot-lehrer/admin-lehrer/app/api/admin/health/route.ts
Benjamin Boenisch 5a31f52310 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>
2026-02-11 23:47:26 +01:00

174 lines
5.5 KiB
TypeScript

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 }
)
}
}