Add admin-core frontend (Port 3008)
Next.js admin frontend for Core with 3 categories (Communication, Infrastructure, Development), 13 modules, 2 roles (developer, ops), and 11 API proxy routes. Includes docker-compose service and nginx SSL config. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
173
admin-core/app/(admin)/dashboard/page.tsx
Normal file
173
admin-core/app/(admin)/dashboard/page.tsx
Normal file
@@ -0,0 +1,173 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState } from 'react'
|
||||
import { navigation } from '@/lib/navigation'
|
||||
import { getStoredRole, isCategoryVisibleForRole, RoleId } from '@/lib/roles'
|
||||
import Link from 'next/link'
|
||||
|
||||
interface ServiceHealth {
|
||||
name: string
|
||||
status: 'healthy' | 'unhealthy' | 'unknown'
|
||||
url?: string
|
||||
}
|
||||
|
||||
export default function DashboardPage() {
|
||||
const [currentRole, setCurrentRole] = useState<RoleId | null>(null)
|
||||
const [services, setServices] = useState<ServiceHealth[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
const role = getStoredRole()
|
||||
setCurrentRole(role)
|
||||
|
||||
// Check service health
|
||||
const checkServices = async () => {
|
||||
const serviceList: ServiceHealth[] = [
|
||||
{ name: 'Matrix Synapse', status: 'unknown' },
|
||||
{ name: 'Jitsi Meet', status: 'unknown' },
|
||||
{ name: 'Mailpit', status: 'unknown' },
|
||||
{ name: 'Gitea', status: 'unknown' },
|
||||
{ name: 'Woodpecker CI', status: 'unknown' },
|
||||
{ name: 'Backend Core', status: 'unknown' },
|
||||
]
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/admin/health')
|
||||
if (response.ok) {
|
||||
const data = await response.json()
|
||||
if (data.services) {
|
||||
serviceList.forEach(s => {
|
||||
const key = s.name.toLowerCase().replace(/\s+/g, '-')
|
||||
if (data.services[key]) {
|
||||
s.status = data.services[key].healthy ? 'healthy' : 'unhealthy'
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// Services stay as unknown
|
||||
}
|
||||
|
||||
setServices(serviceList)
|
||||
setLoading(false)
|
||||
}
|
||||
|
||||
checkServices()
|
||||
}, [])
|
||||
|
||||
const visibleCategories = currentRole
|
||||
? navigation.filter(cat => isCategoryVisibleForRole(cat.id, currentRole))
|
||||
: navigation
|
||||
|
||||
const stats = [
|
||||
{ label: 'Kategorien', value: visibleCategories.length, color: 'text-primary-600' },
|
||||
{ label: 'Module', value: visibleCategories.reduce((acc, cat) => acc + cat.modules.length, 0), color: 'text-blue-600' },
|
||||
{ label: 'Services', value: services.filter(s => s.status === 'healthy').length, color: 'text-green-600' },
|
||||
{ label: 'Warnungen', value: services.filter(s => s.status === 'unhealthy').length, color: services.some(s => s.status === 'unhealthy') ? 'text-orange-600' : 'text-slate-600' },
|
||||
]
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
|
||||
{stats.map((stat) => (
|
||||
<div key={stat.label} className="bg-white rounded-xl border border-slate-200 p-4 shadow-sm">
|
||||
<div className={`text-3xl font-bold ${stat.color}`}>
|
||||
{loading ? '-' : stat.value}
|
||||
</div>
|
||||
<div className="text-sm text-slate-500 mt-1">{stat.label}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Categories */}
|
||||
<h2 className="text-lg font-semibold text-slate-900 mb-4">Bereiche</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
|
||||
{visibleCategories.map((category) => (
|
||||
<Link
|
||||
key={category.id}
|
||||
href={`/${category.id}`}
|
||||
className="bg-white rounded-xl border border-slate-200 p-5 hover:border-primary-300 hover:shadow-md transition-all group"
|
||||
>
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<div
|
||||
className="w-10 h-10 rounded-lg flex items-center justify-center"
|
||||
style={{ backgroundColor: category.color + '20', color: category.color }}
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
{category.icon === 'message-circle' && (
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
|
||||
)}
|
||||
{category.icon === 'server' && (
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01" />
|
||||
)}
|
||||
{category.icon === 'code' && (
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
|
||||
)}
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-slate-900 group-hover:text-primary-600 transition-colors">
|
||||
{category.name}
|
||||
</h3>
|
||||
<p className="text-xs text-slate-500">{category.modules.length} Module</p>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-slate-500">{category.description}</p>
|
||||
<div className="mt-3 flex flex-wrap gap-1">
|
||||
{category.modules.slice(0, 4).map((module) => (
|
||||
<span
|
||||
key={module.id}
|
||||
className="px-2 py-0.5 rounded text-xs"
|
||||
style={{ backgroundColor: category.color + '15', color: category.color }}
|
||||
>
|
||||
{module.name}
|
||||
</span>
|
||||
))}
|
||||
{category.modules.length > 4 && (
|
||||
<span className="px-2 py-0.5 rounded text-xs bg-slate-100 text-slate-500">
|
||||
+{category.modules.length - 4}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Service Status */}
|
||||
<h2 className="text-lg font-semibold text-slate-900 mb-4">Service Status</h2>
|
||||
<div className="bg-white rounded-xl border border-slate-200 shadow-sm">
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 divide-x divide-slate-200">
|
||||
{services.map((service) => (
|
||||
<div key={service.name} className="p-4 text-center">
|
||||
<div className={`inline-block w-3 h-3 rounded-full mb-2 ${
|
||||
service.status === 'healthy' ? 'bg-green-500' :
|
||||
service.status === 'unhealthy' ? 'bg-red-500' :
|
||||
'bg-slate-300'
|
||||
}`} />
|
||||
<div className="text-sm font-medium text-slate-900">{service.name}</div>
|
||||
<div className="text-xs text-slate-500 capitalize">{service.status}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Links */}
|
||||
<h2 className="text-lg font-semibold text-slate-900 mb-4 mt-8">Schnellzugriff</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{visibleCategories.flatMap(cat => cat.modules).slice(0, 8).map((module) => (
|
||||
<Link
|
||||
key={module.id}
|
||||
href={module.href}
|
||||
className="flex items-center gap-3 p-4 bg-white rounded-xl border border-slate-200 hover:border-primary-300 hover:shadow-md transition-all"
|
||||
>
|
||||
<div>
|
||||
<h3 className="font-medium text-slate-900">{module.name}</h3>
|
||||
<p className="text-sm text-slate-500">{module.description}</p>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user