fix(admin-v2): Restore complete admin-v2 application
The admin-v2 application was incomplete in the repository. This commit restores all missing components: - Admin pages (76 pages): dashboard, ai, compliance, dsgvo, education, infrastructure, communication, development, onboarding, rbac - SDK pages (45 pages): tom, dsfa, vvt, loeschfristen, einwilligungen, vendor-compliance, tom-generator, dsr, and more - Developer portal (25 pages): API docs, SDK guides, frameworks - All components, lib files, hooks, and types - Updated package.json with all dependencies The issue was caused by incomplete initial repository state - the full admin-v2 codebase existed in backend/admin-v2 and docs-src/admin-v2 but was never fully synced to the main admin-v2 directory. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
203
admin-v2/components/common/ServiceStatus.tsx
Normal file
203
admin-v2/components/common/ServiceStatus.tsx
Normal file
@@ -0,0 +1,203 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState, useCallback } from 'react'
|
||||
|
||||
interface ServiceHealth {
|
||||
name: string
|
||||
port: number
|
||||
status: 'online' | 'offline' | 'checking' | 'degraded'
|
||||
responseTime?: number
|
||||
details?: string
|
||||
category: 'core' | 'ai' | 'database' | 'storage'
|
||||
}
|
||||
|
||||
// Initial services list for loading state
|
||||
const INITIAL_SERVICES: Omit<ServiceHealth, 'status' | 'responseTime' | 'details'>[] = [
|
||||
{ name: 'Backend API', port: 8000, category: 'core' },
|
||||
{ name: 'Consent Service', port: 8081, category: 'core' },
|
||||
{ name: 'Voice Service', port: 8091, category: 'core' },
|
||||
{ name: 'Klausur Service', port: 8086, category: 'core' },
|
||||
{ name: 'Mail Service (Mailpit)', port: 8025, category: 'core' },
|
||||
{ name: 'Edu Search', port: 8088, category: 'core' },
|
||||
{ name: 'H5P Service', port: 8092, category: 'core' },
|
||||
{ name: 'Ollama/LLM', port: 11434, category: 'ai' },
|
||||
{ name: 'Embedding Service', port: 8087, category: 'ai' },
|
||||
{ name: 'PostgreSQL', port: 5432, category: 'database' },
|
||||
{ name: 'Qdrant (Vector DB)', port: 6333, category: 'database' },
|
||||
{ name: 'Valkey (Cache)', port: 6379, category: 'database' },
|
||||
{ name: 'MinIO (S3)', port: 9000, category: 'storage' },
|
||||
]
|
||||
|
||||
export function ServiceStatus() {
|
||||
const [services, setServices] = useState<ServiceHealth[]>(
|
||||
INITIAL_SERVICES.map(s => ({ ...s, status: 'checking' as const }))
|
||||
)
|
||||
const [lastChecked, setLastChecked] = useState<Date | null>(null)
|
||||
const [isRefreshing, setIsRefreshing] = useState(false)
|
||||
|
||||
const checkServices = useCallback(async () => {
|
||||
setIsRefreshing(true)
|
||||
|
||||
try {
|
||||
// Use server-side API route to avoid mixed-content issues
|
||||
const response = await fetch('/api/admin/health', {
|
||||
method: 'GET',
|
||||
signal: AbortSignal.timeout(15000),
|
||||
})
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
setServices(data.services.map((s: ServiceHealth) => ({
|
||||
...s,
|
||||
status: s.status as 'online' | 'offline' | 'degraded'
|
||||
})))
|
||||
} else {
|
||||
// If API fails, mark all as offline
|
||||
setServices(prev => prev.map(s => ({
|
||||
...s,
|
||||
status: 'offline' as const,
|
||||
details: 'Health-Check API nicht erreichbar'
|
||||
})))
|
||||
}
|
||||
} catch (error) {
|
||||
// Network error - mark all as offline
|
||||
setServices(prev => prev.map(s => ({
|
||||
...s,
|
||||
status: 'offline' as const,
|
||||
details: error instanceof Error ? error.message : 'Verbindungsfehler'
|
||||
})))
|
||||
}
|
||||
|
||||
setLastChecked(new Date())
|
||||
setIsRefreshing(false)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
checkServices()
|
||||
// Auto-refresh every 30 seconds
|
||||
const interval = setInterval(checkServices, 30000)
|
||||
return () => clearInterval(interval)
|
||||
}, [checkServices])
|
||||
|
||||
const getStatusColor = (status: ServiceHealth['status']) => {
|
||||
switch (status) {
|
||||
case 'online': return 'bg-green-500'
|
||||
case 'offline': return 'bg-red-500'
|
||||
case 'degraded': return 'bg-yellow-500'
|
||||
case 'checking': return 'bg-slate-300 animate-pulse'
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusText = (status: ServiceHealth['status']) => {
|
||||
switch (status) {
|
||||
case 'online': return 'Online'
|
||||
case 'offline': return 'Offline'
|
||||
case 'degraded': return 'Eingeschränkt'
|
||||
case 'checking': return 'Prüfe...'
|
||||
}
|
||||
}
|
||||
|
||||
const getCategoryIcon = (category: ServiceHealth['category']) => {
|
||||
switch (category) {
|
||||
case 'core': return '⚙️'
|
||||
case 'ai': return '🤖'
|
||||
case 'database': return '🗄️'
|
||||
case 'storage': return '📦'
|
||||
}
|
||||
}
|
||||
|
||||
const getCategoryLabel = (category: ServiceHealth['category']) => {
|
||||
switch (category) {
|
||||
case 'core': return 'Core Services'
|
||||
case 'ai': return 'AI / LLM'
|
||||
case 'database': return 'Datenbanken'
|
||||
case 'storage': return 'Storage'
|
||||
}
|
||||
}
|
||||
|
||||
const groupedServices = services.reduce((acc, service) => {
|
||||
if (!acc[service.category]) {
|
||||
acc[service.category] = []
|
||||
}
|
||||
acc[service.category].push(service)
|
||||
return acc
|
||||
}, {} as Record<string, ServiceHealth[]>)
|
||||
|
||||
const onlineCount = services.filter(s => s.status === 'online').length
|
||||
const totalCount = services.length
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-xl border border-slate-200 shadow-sm">
|
||||
<div className="px-4 py-3 border-b border-slate-200 flex items-center justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<h3 className="font-semibold text-slate-900">System Status</h3>
|
||||
<span className={`text-xs px-2 py-0.5 rounded-full ${
|
||||
onlineCount === totalCount
|
||||
? 'bg-green-100 text-green-700'
|
||||
: onlineCount > totalCount / 2
|
||||
? 'bg-yellow-100 text-yellow-700'
|
||||
: 'bg-red-100 text-red-700'
|
||||
}`}>
|
||||
{onlineCount}/{totalCount} online
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={checkServices}
|
||||
disabled={isRefreshing}
|
||||
className="text-sm text-primary-600 hover:text-primary-700 disabled:opacity-50 flex items-center gap-1"
|
||||
>
|
||||
<svg className={`w-4 h-4 ${isRefreshing ? 'animate-spin' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
||||
</svg>
|
||||
Aktualisieren
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="p-4 space-y-4">
|
||||
{(['ai', 'core', 'database', 'storage'] as const).map(category => (
|
||||
<div key={category}>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<span>{getCategoryIcon(category)}</span>
|
||||
<span className="text-xs font-medium text-slate-500 uppercase tracking-wider">
|
||||
{getCategoryLabel(category)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{groupedServices[category]?.map((service) => (
|
||||
<div key={service.name} className="flex items-center justify-between py-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className={`w-2 h-2 rounded-full ${getStatusColor(service.status)}`}></span>
|
||||
<span className="text-sm text-slate-700">{service.name}</span>
|
||||
<span className="text-xs text-slate-400">:{service.port}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{service.details && (
|
||||
<span className="text-xs text-slate-500">{service.details}</span>
|
||||
)}
|
||||
{service.responseTime !== undefined && service.status === 'online' && (
|
||||
<span className="text-xs text-slate-400">{service.responseTime}ms</span>
|
||||
)}
|
||||
<span className={`text-xs ${
|
||||
service.status === 'online' ? 'text-green-600' :
|
||||
service.status === 'offline' ? 'text-red-600' :
|
||||
service.status === 'degraded' ? 'text-yellow-600' :
|
||||
'text-slate-400'
|
||||
}`}>
|
||||
{getStatusText(service.status)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{lastChecked && (
|
||||
<div className="px-4 py-2 border-t border-slate-100 text-xs text-slate-400">
|
||||
Zuletzt geprüft: {lastChecked.toLocaleTimeString('de-DE')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user