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>
174 lines
5.5 KiB
TypeScript
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 }
|
|
)
|
|
}
|
|
}
|