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:
1631
admin-v2/app/(admin)/infrastructure/ci-cd/page.tsx
Normal file
1631
admin-v2/app/(admin)/infrastructure/ci-cd/page.tsx
Normal file
File diff suppressed because it is too large
Load Diff
391
admin-v2/app/(admin)/infrastructure/gpu/page.tsx
Normal file
391
admin-v2/app/(admin)/infrastructure/gpu/page.tsx
Normal file
@@ -0,0 +1,391 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* GPU Infrastructure Admin Page
|
||||
*
|
||||
* vast.ai GPU Management for LLM Processing
|
||||
*/
|
||||
|
||||
import { useEffect, useState, useCallback } from 'react'
|
||||
import { PagePurpose } from '@/components/common/PagePurpose'
|
||||
|
||||
interface VastStatus {
|
||||
instance_id: number | null
|
||||
status: string
|
||||
gpu_name: string | null
|
||||
dph_total: number | null
|
||||
endpoint_base_url: string | null
|
||||
last_activity: string | null
|
||||
auto_shutdown_in_minutes: number | null
|
||||
total_runtime_hours: number | null
|
||||
total_cost_usd: number | null
|
||||
account_credit: number | null
|
||||
account_total_spend: number | null
|
||||
session_runtime_minutes: number | null
|
||||
session_cost_usd: number | null
|
||||
message: string | null
|
||||
error?: string
|
||||
}
|
||||
|
||||
export default function GPUInfrastructurePage() {
|
||||
const [status, setStatus] = useState<VastStatus | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [actionLoading, setActionLoading] = useState<string | null>(null)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [message, setMessage] = useState<string | null>(null)
|
||||
|
||||
const API_PROXY = '/api/admin/gpu'
|
||||
|
||||
const fetchStatus = useCallback(async () => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const response = await fetch(API_PROXY)
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || `HTTP ${response.status}`)
|
||||
}
|
||||
|
||||
setStatus(data)
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Verbindungsfehler')
|
||||
setStatus({
|
||||
instance_id: null,
|
||||
status: 'error',
|
||||
gpu_name: null,
|
||||
dph_total: null,
|
||||
endpoint_base_url: null,
|
||||
last_activity: null,
|
||||
auto_shutdown_in_minutes: null,
|
||||
total_runtime_hours: null,
|
||||
total_cost_usd: null,
|
||||
account_credit: null,
|
||||
account_total_spend: null,
|
||||
session_runtime_minutes: null,
|
||||
session_cost_usd: null,
|
||||
message: 'Verbindung fehlgeschlagen'
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
fetchStatus()
|
||||
}, [fetchStatus])
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(fetchStatus, 30000)
|
||||
return () => clearInterval(interval)
|
||||
}, [fetchStatus])
|
||||
|
||||
const powerOn = async () => {
|
||||
setActionLoading('on')
|
||||
setError(null)
|
||||
setMessage(null)
|
||||
|
||||
try {
|
||||
const response = await fetch(API_PROXY, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action: 'on' }),
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || data.detail || 'Aktion fehlgeschlagen')
|
||||
}
|
||||
|
||||
setMessage('Start angefordert')
|
||||
setTimeout(fetchStatus, 3000)
|
||||
setTimeout(fetchStatus, 10000)
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Fehler beim Starten')
|
||||
fetchStatus()
|
||||
} finally {
|
||||
setActionLoading(null)
|
||||
}
|
||||
}
|
||||
|
||||
const powerOff = async () => {
|
||||
setActionLoading('off')
|
||||
setError(null)
|
||||
setMessage(null)
|
||||
|
||||
try {
|
||||
const response = await fetch(API_PROXY, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action: 'off' }),
|
||||
})
|
||||
|
||||
const data = await response.json()
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.error || data.detail || 'Aktion fehlgeschlagen')
|
||||
}
|
||||
|
||||
setMessage('Stop angefordert')
|
||||
setTimeout(fetchStatus, 3000)
|
||||
setTimeout(fetchStatus, 10000)
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Fehler beim Stoppen')
|
||||
fetchStatus()
|
||||
} finally {
|
||||
setActionLoading(null)
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusBadge = (s: string) => {
|
||||
const baseClasses = 'px-3 py-1 rounded-full text-sm font-semibold uppercase'
|
||||
switch (s) {
|
||||
case 'running':
|
||||
return `${baseClasses} bg-green-100 text-green-800`
|
||||
case 'stopped':
|
||||
case 'exited':
|
||||
return `${baseClasses} bg-red-100 text-red-800`
|
||||
case 'loading':
|
||||
case 'scheduling':
|
||||
case 'creating':
|
||||
case 'starting...':
|
||||
case 'stopping...':
|
||||
return `${baseClasses} bg-yellow-100 text-yellow-800`
|
||||
default:
|
||||
return `${baseClasses} bg-slate-100 text-slate-600`
|
||||
}
|
||||
}
|
||||
|
||||
const getCreditColor = (credit: number | null) => {
|
||||
if (credit === null) return 'text-slate-500'
|
||||
if (credit < 5) return 'text-red-600'
|
||||
if (credit < 15) return 'text-yellow-600'
|
||||
return 'text-green-600'
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Page Purpose */}
|
||||
<PagePurpose
|
||||
title="GPU Infrastruktur"
|
||||
purpose="Verwalten Sie die vast.ai GPU-Instanzen fuer LLM-Verarbeitung und OCR. Starten/Stoppen Sie GPUs bei Bedarf und ueberwachen Sie Kosten in Echtzeit."
|
||||
audience={['DevOps', 'Entwickler', 'System-Admins']}
|
||||
architecture={{
|
||||
services: ['vast.ai API', 'Ollama', 'VLLM'],
|
||||
databases: ['PostgreSQL (Logs)'],
|
||||
}}
|
||||
relatedPages={[
|
||||
{ name: 'LLM Vergleich', href: '/ai/llm-compare', description: 'KI-Provider testen' },
|
||||
{ name: 'Security', href: '/infrastructure/security', description: 'DevSecOps Dashboard' },
|
||||
{ name: 'Builds', href: '/infrastructure/builds', description: 'CI/CD Pipeline' },
|
||||
]}
|
||||
collapsible={true}
|
||||
defaultCollapsed={true}
|
||||
/>
|
||||
|
||||
{/* Status Cards */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6 mb-6">
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-6">
|
||||
<div>
|
||||
<div className="text-sm text-slate-500 mb-2">Status</div>
|
||||
{loading ? (
|
||||
<span className="px-3 py-1 rounded-full text-sm font-semibold bg-slate-100 text-slate-600">
|
||||
Laden...
|
||||
</span>
|
||||
) : (
|
||||
<span className={getStatusBadge(
|
||||
actionLoading === 'on' ? 'starting...' :
|
||||
actionLoading === 'off' ? 'stopping...' :
|
||||
status?.status || 'unknown'
|
||||
)}>
|
||||
{actionLoading === 'on' ? 'starting...' :
|
||||
actionLoading === 'off' ? 'stopping...' :
|
||||
status?.status || 'unbekannt'}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-sm text-slate-500 mb-2">GPU</div>
|
||||
<div className="font-semibold text-slate-900">
|
||||
{status?.gpu_name || '-'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-sm text-slate-500 mb-2">Kosten/h</div>
|
||||
<div className="font-semibold text-slate-900">
|
||||
{status?.dph_total ? `$${status.dph_total.toFixed(3)}` : '-'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-sm text-slate-500 mb-2">Auto-Stop</div>
|
||||
<div className="font-semibold text-slate-900">
|
||||
{status && status.auto_shutdown_in_minutes !== null
|
||||
? `${status.auto_shutdown_in_minutes} min`
|
||||
: '-'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-sm text-slate-500 mb-2">Budget</div>
|
||||
<div className={`font-bold text-lg ${getCreditColor(status?.account_credit ?? null)}`}>
|
||||
{status && status.account_credit !== null
|
||||
? `$${status.account_credit.toFixed(2)}`
|
||||
: '-'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-sm text-slate-500 mb-2">Session</div>
|
||||
<div className="font-semibold text-slate-900">
|
||||
{status && status.session_runtime_minutes !== null && status.session_cost_usd !== null
|
||||
? `${Math.round(status.session_runtime_minutes)} min / $${status.session_cost_usd.toFixed(3)}`
|
||||
: '-'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Buttons */}
|
||||
<div className="flex items-center gap-4 mt-6 pt-6 border-t border-slate-200">
|
||||
<button
|
||||
onClick={powerOn}
|
||||
disabled={actionLoading !== null || status?.status === 'running'}
|
||||
className="px-6 py-2 bg-orange-600 text-white rounded-lg font-medium hover:bg-orange-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
Starten
|
||||
</button>
|
||||
<button
|
||||
onClick={powerOff}
|
||||
disabled={actionLoading !== null || status?.status !== 'running'}
|
||||
className="px-6 py-2 bg-red-600 text-white rounded-lg font-medium hover:bg-red-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
||||
>
|
||||
Stoppen
|
||||
</button>
|
||||
<button
|
||||
onClick={fetchStatus}
|
||||
disabled={loading}
|
||||
className="px-4 py-2 border border-slate-300 text-slate-700 rounded-lg font-medium hover:bg-slate-50 disabled:opacity-50 transition-colors"
|
||||
>
|
||||
{loading ? 'Aktualisiere...' : 'Aktualisieren'}
|
||||
</button>
|
||||
|
||||
{message && (
|
||||
<span className="ml-4 text-sm text-green-600 font-medium">{message}</span>
|
||||
)}
|
||||
{error && (
|
||||
<span className="ml-4 text-sm text-red-600 font-medium">{error}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Extended Stats */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6">
|
||||
<h3 className="font-semibold text-slate-900 mb-4">Kosten-Uebersicht</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-slate-600">Session Laufzeit</span>
|
||||
<span className="font-semibold">
|
||||
{status && status.session_runtime_minutes !== null
|
||||
? `${Math.round(status.session_runtime_minutes)} Minuten`
|
||||
: '-'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-slate-600">Session Kosten</span>
|
||||
<span className="font-semibold">
|
||||
{status && status.session_cost_usd !== null
|
||||
? `$${status.session_cost_usd.toFixed(4)}`
|
||||
: '-'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center pt-4 border-t border-slate-100">
|
||||
<span className="text-slate-600">Gesamtlaufzeit</span>
|
||||
<span className="font-semibold">
|
||||
{status && status.total_runtime_hours !== null
|
||||
? `${status.total_runtime_hours.toFixed(1)} Stunden`
|
||||
: '-'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-slate-600">Gesamtkosten</span>
|
||||
<span className="font-semibold">
|
||||
{status && status.total_cost_usd !== null
|
||||
? `$${status.total_cost_usd.toFixed(2)}`
|
||||
: '-'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-slate-600">vast.ai Ausgaben</span>
|
||||
<span className="font-semibold">
|
||||
{status && status.account_total_spend !== null
|
||||
? `$${status.account_total_spend.toFixed(2)}`
|
||||
: '-'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6">
|
||||
<h3 className="font-semibold text-slate-900 mb-4">Instanz-Details</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-slate-600">Instanz ID</span>
|
||||
<span className="font-mono text-sm">
|
||||
{status?.instance_id || '-'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-slate-600">GPU</span>
|
||||
<span className="font-semibold">
|
||||
{status?.gpu_name || '-'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-slate-600">Stundensatz</span>
|
||||
<span className="font-semibold">
|
||||
{status?.dph_total ? `$${status.dph_total.toFixed(4)}/h` : '-'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-slate-600">Letzte Aktivitaet</span>
|
||||
<span className="text-sm">
|
||||
{status?.last_activity
|
||||
? new Date(status.last_activity).toLocaleString('de-DE')
|
||||
: '-'}
|
||||
</span>
|
||||
</div>
|
||||
{status?.endpoint_base_url && status.status === 'running' && (
|
||||
<div className="pt-4 border-t border-slate-100">
|
||||
<div className="text-slate-600 text-sm mb-1">Endpoint</div>
|
||||
<code className="text-xs bg-slate-100 px-2 py-1 rounded block overflow-x-auto">
|
||||
{status.endpoint_base_url}
|
||||
</code>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Info */}
|
||||
<div className="bg-orange-50 border border-orange-200 rounded-xl p-4">
|
||||
<div className="flex gap-3">
|
||||
<svg className="w-5 h-5 text-orange-600 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<div>
|
||||
<h4 className="font-semibold text-orange-900">Auto-Shutdown</h4>
|
||||
<p className="text-sm text-orange-800 mt-1">
|
||||
Die GPU-Instanz wird automatisch gestoppt, wenn sie laengere Zeit inaktiv ist.
|
||||
Der Status wird alle 30 Sekunden automatisch aktualisiert.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
650
admin-v2/app/(admin)/infrastructure/middleware/page.tsx
Normal file
650
admin-v2/app/(admin)/infrastructure/middleware/page.tsx
Normal file
@@ -0,0 +1,650 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* Middleware Admin - Rate Limiting, IP Whitelist/Blacklist, Events
|
||||
*
|
||||
* Manage middleware configurations and monitor events
|
||||
* Migrated from old admin (/admin/middleware)
|
||||
*/
|
||||
|
||||
import { useEffect, useState, useCallback } from 'react'
|
||||
import { PagePurpose } from '@/components/common/PagePurpose'
|
||||
|
||||
interface MiddlewareConfig {
|
||||
id: string
|
||||
middleware_name: string
|
||||
enabled: boolean
|
||||
config: Record<string, unknown>
|
||||
updated_at: string | null
|
||||
}
|
||||
|
||||
interface RateLimitIP {
|
||||
id: string
|
||||
ip_address: string
|
||||
list_type: 'whitelist' | 'blacklist'
|
||||
reason: string | null
|
||||
expires_at: string | null
|
||||
created_at: string
|
||||
}
|
||||
|
||||
interface MiddlewareEvent {
|
||||
id: string
|
||||
middleware_name: string
|
||||
event_type: string
|
||||
ip_address: string | null
|
||||
user_id: string | null
|
||||
request_path: string | null
|
||||
request_method: string | null
|
||||
details: Record<string, unknown> | null
|
||||
created_at: string
|
||||
}
|
||||
|
||||
interface MiddlewareStats {
|
||||
middleware_name: string
|
||||
total_events: number
|
||||
events_last_hour: number
|
||||
events_last_24h: number
|
||||
top_event_types: Array<{ event_type: string; count: number }>
|
||||
top_ips: Array<{ ip_address: string; count: number }>
|
||||
}
|
||||
|
||||
export default function MiddlewareAdminPage() {
|
||||
const [configs, setConfigs] = useState<MiddlewareConfig[]>([])
|
||||
const [ipList, setIpList] = useState<RateLimitIP[]>([])
|
||||
const [events, setEvents] = useState<MiddlewareEvent[]>([])
|
||||
const [stats, setStats] = useState<MiddlewareStats[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [activeTab, setActiveTab] = useState<'overview' | 'config' | 'ip-list' | 'events' | 'stats'>('overview')
|
||||
const [actionLoading, setActionLoading] = useState<string | null>(null)
|
||||
|
||||
// IP Form
|
||||
const [newIP, setNewIP] = useState('')
|
||||
const [newIPType, setNewIPType] = useState<'whitelist' | 'blacklist'>('whitelist')
|
||||
const [newIPReason, setNewIPReason] = useState('')
|
||||
|
||||
const fetchData = useCallback(async () => {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const [configsRes, ipListRes, eventsRes, statsRes] = await Promise.all([
|
||||
fetch('/api/admin/middleware'),
|
||||
fetch('/api/admin/middleware/rate-limit/ip-list'),
|
||||
fetch('/api/admin/middleware/events?limit=50'),
|
||||
fetch('/api/admin/middleware/stats'),
|
||||
])
|
||||
|
||||
if (configsRes.ok) {
|
||||
setConfigs(await configsRes.json())
|
||||
}
|
||||
if (ipListRes.ok) {
|
||||
setIpList(await ipListRes.json())
|
||||
}
|
||||
if (eventsRes.ok) {
|
||||
setEvents(await eventsRes.json())
|
||||
}
|
||||
if (statsRes.ok) {
|
||||
setStats(await statsRes.json())
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Verbindung zum Backend fehlgeschlagen')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
fetchData()
|
||||
}, [fetchData])
|
||||
|
||||
useEffect(() => {
|
||||
const interval = setInterval(fetchData, 30000)
|
||||
return () => clearInterval(interval)
|
||||
}, [fetchData])
|
||||
|
||||
const toggleMiddleware = async (name: string, enabled: boolean) => {
|
||||
setActionLoading(name)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/admin/middleware/${name}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ enabled }),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Fehler beim Aktualisieren: ${response.statusText}`)
|
||||
}
|
||||
|
||||
// Update local state
|
||||
setConfigs(prev =>
|
||||
prev.map(c => (c.middleware_name === name ? { ...c, enabled } : c))
|
||||
)
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Aktualisierung fehlgeschlagen')
|
||||
} finally {
|
||||
setActionLoading(null)
|
||||
}
|
||||
}
|
||||
|
||||
const addIP = async (e: React.FormEvent) => {
|
||||
e.preventDefault()
|
||||
if (!newIP.trim()) return
|
||||
|
||||
setActionLoading('add-ip')
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/admin/middleware/rate-limit/ip-list', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
ip_address: newIP.trim(),
|
||||
list_type: newIPType,
|
||||
reason: newIPReason.trim() || null,
|
||||
}),
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const data = await response.json()
|
||||
throw new Error(data.detail || `Fehler: ${response.statusText}`)
|
||||
}
|
||||
|
||||
const newEntry = await response.json()
|
||||
setIpList(prev => [newEntry, ...prev])
|
||||
setNewIP('')
|
||||
setNewIPReason('')
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'IP konnte nicht hinzugefuegt werden')
|
||||
} finally {
|
||||
setActionLoading(null)
|
||||
}
|
||||
}
|
||||
|
||||
const removeIP = async (id: string) => {
|
||||
setActionLoading(`remove-${id}`)
|
||||
setError(null)
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/admin/middleware/rate-limit/ip-list/${id}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Fehler beim Loeschen: ${response.statusText}`)
|
||||
}
|
||||
|
||||
setIpList(prev => prev.filter(ip => ip.id !== id))
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'IP konnte nicht entfernt werden')
|
||||
} finally {
|
||||
setActionLoading(null)
|
||||
}
|
||||
}
|
||||
|
||||
const getMiddlewareDescription = (name: string): { icon: string; desc: string } => {
|
||||
const descriptions: Record<string, { icon: string; desc: string }> = {
|
||||
request_id: { icon: '🆔', desc: 'Generiert eindeutige Request-IDs fuer Tracing' },
|
||||
security_headers: { icon: '🛡️', desc: 'Fuegt Security-Header hinzu (CSP, HSTS, etc.)' },
|
||||
cors: { icon: '🌐', desc: 'Cross-Origin Resource Sharing Konfiguration' },
|
||||
rate_limiter: { icon: '⏱️', desc: 'Rate Limiting zum Schutz vor Missbrauch' },
|
||||
pii_redactor: { icon: '🔒', desc: 'Redaktiert personenbezogene Daten in Logs' },
|
||||
input_gate: { icon: '🚪', desc: 'Validiert und sanitisiert Eingaben' },
|
||||
}
|
||||
return descriptions[name] || { icon: '⚙️', desc: 'Middleware-Komponente' }
|
||||
}
|
||||
|
||||
const getEventTypeColor = (eventType: string) => {
|
||||
if (eventType.includes('error') || eventType.includes('blocked') || eventType.includes('blacklist')) {
|
||||
return 'bg-red-100 text-red-800'
|
||||
}
|
||||
if (eventType.includes('warning') || eventType.includes('rate_limit')) {
|
||||
return 'bg-yellow-100 text-yellow-800'
|
||||
}
|
||||
if (eventType.includes('success') || eventType.includes('whitelist')) {
|
||||
return 'bg-green-100 text-green-800'
|
||||
}
|
||||
return 'bg-slate-100 text-slate-800'
|
||||
}
|
||||
|
||||
const whitelistCount = ipList.filter(ip => ip.list_type === 'whitelist').length
|
||||
const blacklistCount = ipList.filter(ip => ip.list_type === 'blacklist').length
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PagePurpose
|
||||
title="Middleware Admin"
|
||||
purpose="Verwalten Sie die Middleware-Konfiguration, Rate Limiting und IP-Listen. Ueberwachen Sie Middleware-Events und Statistiken in Echtzeit."
|
||||
audience={['DevOps', 'Security', 'System-Admins']}
|
||||
gdprArticles={['Art. 32 (Sicherheit der Verarbeitung)']}
|
||||
architecture={{
|
||||
services: ['FastAPI Middleware Stack', 'PostgreSQL'],
|
||||
databases: ['middleware_config', 'rate_limit_ip_list', 'middleware_events'],
|
||||
}}
|
||||
relatedPages={[
|
||||
{ name: 'Security', href: '/infrastructure/security', description: 'DevSecOps Dashboard' },
|
||||
{ name: 'Mac Mini', href: '/infrastructure/mac-mini', description: 'Server-Monitoring' },
|
||||
{ name: 'Controls', href: '/compliance/controls', description: 'Security Controls' },
|
||||
]}
|
||||
collapsible={true}
|
||||
defaultCollapsed={true}
|
||||
/>
|
||||
|
||||
{/* Stats Overview */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6 mb-6">
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<h2 className="text-xl font-semibold text-slate-900">Middleware Status</h2>
|
||||
<button
|
||||
onClick={fetchData}
|
||||
disabled={loading}
|
||||
className="px-4 py-2 text-sm bg-orange-600 text-white rounded-lg hover:bg-orange-700 disabled:opacity-50 transition-colors"
|
||||
>
|
||||
{loading ? 'Laden...' : 'Aktualisieren'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div className="bg-slate-50 rounded-lg p-4 border border-slate-200">
|
||||
<div className="text-2xl font-bold text-slate-900">{configs.length}</div>
|
||||
<div className="text-sm text-slate-600">Middleware</div>
|
||||
</div>
|
||||
<div className="bg-green-50 rounded-lg p-4 border border-green-200">
|
||||
<div className="text-2xl font-bold text-green-600">{whitelistCount}</div>
|
||||
<div className="text-sm text-slate-600">Whitelist IPs</div>
|
||||
</div>
|
||||
<div className="bg-red-50 rounded-lg p-4 border border-red-200">
|
||||
<div className="text-2xl font-bold text-red-600">{blacklistCount}</div>
|
||||
<div className="text-sm text-slate-600">Blacklist IPs</div>
|
||||
</div>
|
||||
<div className="bg-blue-50 rounded-lg p-4 border border-blue-200">
|
||||
<div className="text-2xl font-bold text-blue-600">{events.length}</div>
|
||||
<div className="text-sm text-slate-600">Recent Events</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden mb-6">
|
||||
<div className="flex border-b border-slate-200 overflow-x-auto">
|
||||
{(['overview', 'config', 'ip-list', 'events', 'stats'] as const).map(tab => (
|
||||
<button
|
||||
key={tab}
|
||||
onClick={() => setActiveTab(tab)}
|
||||
className={`px-6 py-3 text-sm font-medium whitespace-nowrap transition-colors ${
|
||||
activeTab === tab
|
||||
? 'bg-orange-50 text-orange-700 border-b-2 border-orange-600'
|
||||
: 'text-slate-600 hover:bg-slate-50'
|
||||
}`}
|
||||
>
|
||||
{tab === 'overview' && 'Uebersicht'}
|
||||
{tab === 'config' && 'Konfiguration'}
|
||||
{tab === 'ip-list' && `IP-Listen (${ipList.length})`}
|
||||
{tab === 'events' && 'Events'}
|
||||
{tab === 'stats' && 'Statistiken'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="p-6">
|
||||
{error && (
|
||||
<div className="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg text-red-700">{error}</div>
|
||||
)}
|
||||
|
||||
{loading ? (
|
||||
<div className="flex justify-center py-12">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-orange-600" />
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Overview Tab */}
|
||||
{activeTab === 'overview' && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{configs.map(config => {
|
||||
const info = getMiddlewareDescription(config.middleware_name)
|
||||
return (
|
||||
<div
|
||||
key={config.id}
|
||||
className={`rounded-lg p-4 border ${
|
||||
config.enabled
|
||||
? 'bg-green-50 border-green-200'
|
||||
: 'bg-slate-50 border-slate-200'
|
||||
}`}
|
||||
>
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xl">{info.icon}</span>
|
||||
<span className="font-semibold text-slate-900 capitalize">
|
||||
{config.middleware_name.replace('_', ' ')}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => toggleMiddleware(config.middleware_name, !config.enabled)}
|
||||
disabled={actionLoading === config.middleware_name}
|
||||
className={`px-3 py-1 rounded-full text-xs font-semibold transition-colors ${
|
||||
config.enabled
|
||||
? 'bg-green-200 text-green-800 hover:bg-green-300'
|
||||
: 'bg-slate-200 text-slate-600 hover:bg-slate-300'
|
||||
}`}
|
||||
>
|
||||
{actionLoading === config.middleware_name
|
||||
? '...'
|
||||
: config.enabled
|
||||
? 'Aktiv'
|
||||
: 'Inaktiv'}
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-sm text-slate-600">{info.desc}</p>
|
||||
{config.updated_at && (
|
||||
<div className="mt-2 text-xs text-slate-400">
|
||||
Aktualisiert: {new Date(config.updated_at).toLocaleString('de-DE')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Config Tab */}
|
||||
{activeTab === 'config' && (
|
||||
<div className="space-y-4">
|
||||
{configs.map(config => {
|
||||
const info = getMiddlewareDescription(config.middleware_name)
|
||||
return (
|
||||
<div key={config.id} className="bg-slate-50 rounded-lg p-4 border border-slate-200">
|
||||
<div className="flex justify-between items-start mb-4">
|
||||
<div>
|
||||
<h3 className="font-semibold text-slate-900 flex items-center gap-2">
|
||||
<span>{info.icon}</span>
|
||||
<span className="capitalize">{config.middleware_name.replace('_', ' ')}</span>
|
||||
</h3>
|
||||
<p className="text-sm text-slate-600">{info.desc}</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span
|
||||
className={`px-3 py-1 rounded-full text-xs font-semibold ${
|
||||
config.enabled ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'
|
||||
}`}
|
||||
>
|
||||
{config.enabled ? 'Aktiviert' : 'Deaktiviert'}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => toggleMiddleware(config.middleware_name, !config.enabled)}
|
||||
disabled={actionLoading === config.middleware_name}
|
||||
className="px-4 py-1.5 text-sm border border-slate-300 rounded-lg hover:bg-slate-100 disabled:opacity-50 transition-colors"
|
||||
>
|
||||
{actionLoading === config.middleware_name
|
||||
? '...'
|
||||
: config.enabled
|
||||
? 'Deaktivieren'
|
||||
: 'Aktivieren'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{Object.keys(config.config).length > 0 && (
|
||||
<div className="mt-3 pt-3 border-t border-slate-200">
|
||||
<div className="text-xs font-semibold text-slate-500 uppercase mb-2">
|
||||
Konfiguration
|
||||
</div>
|
||||
<pre className="text-xs bg-white p-3 rounded border border-slate-200 overflow-x-auto">
|
||||
{JSON.stringify(config.config, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* IP List Tab */}
|
||||
{activeTab === 'ip-list' && (
|
||||
<div>
|
||||
{/* Add IP Form */}
|
||||
<form onSubmit={addIP} className="mb-6 p-4 bg-slate-50 rounded-lg border border-slate-200">
|
||||
<h3 className="font-semibold text-slate-900 mb-4">IP hinzufuegen</h3>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<input
|
||||
type="text"
|
||||
value={newIP}
|
||||
onChange={e => setNewIP(e.target.value)}
|
||||
placeholder="IP-Adresse (z.B. 192.168.1.1)"
|
||||
className="flex-1 min-w-[200px] px-4 py-2 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-500"
|
||||
/>
|
||||
<select
|
||||
value={newIPType}
|
||||
onChange={e => setNewIPType(e.target.value as 'whitelist' | 'blacklist')}
|
||||
className="px-4 py-2 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-500"
|
||||
>
|
||||
<option value="whitelist">Whitelist</option>
|
||||
<option value="blacklist">Blacklist</option>
|
||||
</select>
|
||||
<input
|
||||
type="text"
|
||||
value={newIPReason}
|
||||
onChange={e => setNewIPReason(e.target.value)}
|
||||
placeholder="Grund (optional)"
|
||||
className="flex-1 min-w-[150px] px-4 py-2 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-500"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
disabled={!newIP.trim() || actionLoading === 'add-ip'}
|
||||
className="px-6 py-2 bg-orange-600 text-white rounded-lg font-medium hover:bg-orange-700 disabled:opacity-50 transition-colors"
|
||||
>
|
||||
{actionLoading === 'add-ip' ? 'Hinzufuegen...' : 'Hinzufuegen'}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
{/* IP List Table */}
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-200">
|
||||
<th className="text-left py-3 px-4 text-xs font-semibold text-slate-500 uppercase">
|
||||
IP-Adresse
|
||||
</th>
|
||||
<th className="text-left py-3 px-4 text-xs font-semibold text-slate-500 uppercase">
|
||||
Typ
|
||||
</th>
|
||||
<th className="text-left py-3 px-4 text-xs font-semibold text-slate-500 uppercase">
|
||||
Grund
|
||||
</th>
|
||||
<th className="text-left py-3 px-4 text-xs font-semibold text-slate-500 uppercase">
|
||||
Hinzugefuegt
|
||||
</th>
|
||||
<th className="text-right py-3 px-4 text-xs font-semibold text-slate-500 uppercase">
|
||||
Aktion
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{ipList.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={5} className="text-center py-8 text-slate-500">
|
||||
Keine IP-Eintraege vorhanden.
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
ipList.map(ip => (
|
||||
<tr key={ip.id} className="border-b border-slate-100 hover:bg-slate-50">
|
||||
<td className="py-3 px-4 font-mono text-sm">{ip.ip_address}</td>
|
||||
<td className="py-3 px-4">
|
||||
<span
|
||||
className={`px-2 py-1 rounded text-xs font-semibold ${
|
||||
ip.list_type === 'whitelist'
|
||||
? 'bg-green-100 text-green-800'
|
||||
: 'bg-red-100 text-red-800'
|
||||
}`}
|
||||
>
|
||||
{ip.list_type === 'whitelist' ? 'Whitelist' : 'Blacklist'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-3 px-4 text-sm text-slate-600">{ip.reason || '-'}</td>
|
||||
<td className="py-3 px-4 text-sm text-slate-500">
|
||||
{new Date(ip.created_at).toLocaleString('de-DE')}
|
||||
</td>
|
||||
<td className="py-3 px-4 text-right">
|
||||
<button
|
||||
onClick={() => removeIP(ip.id)}
|
||||
disabled={actionLoading === `remove-${ip.id}`}
|
||||
className="px-3 py-1 text-sm text-red-600 hover:bg-red-50 rounded transition-colors disabled:opacity-50"
|
||||
>
|
||||
{actionLoading === `remove-${ip.id}` ? '...' : 'Entfernen'}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Events Tab */}
|
||||
{activeTab === 'events' && (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-200">
|
||||
<th className="text-left py-3 px-4 text-xs font-semibold text-slate-500 uppercase">
|
||||
Zeit
|
||||
</th>
|
||||
<th className="text-left py-3 px-4 text-xs font-semibold text-slate-500 uppercase">
|
||||
Middleware
|
||||
</th>
|
||||
<th className="text-left py-3 px-4 text-xs font-semibold text-slate-500 uppercase">
|
||||
Event
|
||||
</th>
|
||||
<th className="text-left py-3 px-4 text-xs font-semibold text-slate-500 uppercase">
|
||||
IP
|
||||
</th>
|
||||
<th className="text-left py-3 px-4 text-xs font-semibold text-slate-500 uppercase">
|
||||
Pfad
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{events.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={5} className="text-center py-8 text-slate-500">
|
||||
Keine Events vorhanden.
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
events.map(event => (
|
||||
<tr key={event.id} className="border-b border-slate-100 hover:bg-slate-50">
|
||||
<td className="py-3 px-4 text-sm text-slate-500">
|
||||
{new Date(event.created_at).toLocaleString('de-DE')}
|
||||
</td>
|
||||
<td className="py-3 px-4 text-sm capitalize">
|
||||
{event.middleware_name.replace('_', ' ')}
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
<span
|
||||
className={`px-2 py-1 rounded text-xs font-semibold ${getEventTypeColor(event.event_type)}`}
|
||||
>
|
||||
{event.event_type}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-3 px-4 text-sm font-mono text-slate-600">
|
||||
{event.ip_address || '-'}
|
||||
</td>
|
||||
<td className="py-3 px-4 text-sm text-slate-600 max-w-xs truncate">
|
||||
{event.request_method && event.request_path
|
||||
? `${event.request_method} ${event.request_path}`
|
||||
: '-'}
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Stats Tab */}
|
||||
{activeTab === 'stats' && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{stats.map(stat => {
|
||||
const info = getMiddlewareDescription(stat.middleware_name)
|
||||
return (
|
||||
<div key={stat.middleware_name} className="bg-slate-50 rounded-lg p-4 border border-slate-200">
|
||||
<h3 className="font-semibold text-slate-900 flex items-center gap-2 mb-4">
|
||||
<span>{info.icon}</span>
|
||||
<span className="capitalize">{stat.middleware_name.replace('_', ' ')}</span>
|
||||
</h3>
|
||||
<div className="grid grid-cols-3 gap-4 mb-4">
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-slate-900">{stat.total_events}</div>
|
||||
<div className="text-xs text-slate-500">Gesamt</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-blue-600">{stat.events_last_hour}</div>
|
||||
<div className="text-xs text-slate-500">Letzte Stunde</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-2xl font-bold text-orange-600">{stat.events_last_24h}</div>
|
||||
<div className="text-xs text-slate-500">24 Stunden</div>
|
||||
</div>
|
||||
</div>
|
||||
{stat.top_event_types.length > 0 && (
|
||||
<div className="mb-3">
|
||||
<div className="text-xs font-semibold text-slate-500 uppercase mb-2">
|
||||
Top Event-Typen
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{stat.top_event_types.slice(0, 3).map(et => (
|
||||
<span
|
||||
key={et.event_type}
|
||||
className={`px-2 py-1 rounded text-xs ${getEventTypeColor(et.event_type)}`}
|
||||
>
|
||||
{et.event_type} ({et.count})
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{stat.top_ips.length > 0 && (
|
||||
<div>
|
||||
<div className="text-xs font-semibold text-slate-500 uppercase mb-2">Top IPs</div>
|
||||
<div className="text-xs text-slate-600">
|
||||
{stat.top_ips
|
||||
.slice(0, 3)
|
||||
.map(ip => `${ip.ip_address} (${ip.count})`)
|
||||
.join(', ')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Info Box */}
|
||||
<div className="bg-orange-50 border border-orange-200 rounded-xl p-4">
|
||||
<div className="flex gap-3">
|
||||
<svg className="w-5 h-5 text-orange-600 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<div>
|
||||
<h4 className="font-semibold text-orange-900">Middleware Stack</h4>
|
||||
<p className="text-sm text-orange-800 mt-1">
|
||||
Der Middleware Stack verarbeitet alle API-Anfragen in der konfigurierten Reihenfolge.
|
||||
Aenderungen an der Konfiguration werden sofort wirksam.
|
||||
Verwenden Sie die Whitelist fuer vertrauenswuerdige IPs und die Blacklist fuer bekannte Angreifer.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
50
admin-v2/app/(admin)/infrastructure/page.tsx
Normal file
50
admin-v2/app/(admin)/infrastructure/page.tsx
Normal file
@@ -0,0 +1,50 @@
|
||||
'use client'
|
||||
|
||||
import { getCategoryById } from '@/lib/navigation'
|
||||
import { ModuleCard } from '@/components/common/ModuleCard'
|
||||
import { PagePurpose } from '@/components/common/PagePurpose'
|
||||
|
||||
export default function InfrastructurePage() {
|
||||
const category = getCategoryById('infrastructure')
|
||||
|
||||
if (!category) {
|
||||
return <div>Kategorie nicht gefunden</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Page Purpose */}
|
||||
<PagePurpose
|
||||
title={category.name}
|
||||
purpose="Diese Kategorie umfasst alle DevOps- und Infrastruktur-Tools. Hier verwalten Sie GPU-Ressourcen, ueberwachen Security-Scans, pruefen SBOM-Compliance und monitoren den Mac Mini Server."
|
||||
audience={['DevOps', 'System-Administratoren', 'Security']}
|
||||
architecture={{
|
||||
services: ['nginx (Reverse Proxy)', 'docker-compose', 'vault (Secrets)'],
|
||||
databases: ['PostgreSQL', 'Valkey (Cache)'],
|
||||
}}
|
||||
collapsible={true}
|
||||
defaultCollapsed={false}
|
||||
/>
|
||||
|
||||
{/* Modules Grid */}
|
||||
<h2 className="text-lg font-semibold text-slate-900 mb-4">Module</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{category.modules.map((module) => (
|
||||
<ModuleCard key={module.id} module={module} category={category} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Info Section */}
|
||||
<div className="mt-8 bg-orange-50 border border-orange-200 rounded-xl p-6">
|
||||
<h3 className="font-semibold text-orange-800 flex items-center gap-2">
|
||||
<span>🖥️</span>
|
||||
Mac Mini Server
|
||||
</h3>
|
||||
<p className="text-sm text-orange-700 mt-2">
|
||||
Der Mac Mini mit Apple Silicon dient als lokaler Server fuer alle Breakpilot-Services.
|
||||
GPU-intensive Workloads koennen bei Bedarf auf vast.ai ausgelagert werden.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
901
admin-v2/app/(admin)/infrastructure/sbom/page.tsx
Normal file
901
admin-v2/app/(admin)/infrastructure/sbom/page.tsx
Normal file
@@ -0,0 +1,901 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* SBOM (Software Bill of Materials) Admin Page
|
||||
*
|
||||
* Migriert von /admin/sbom (website) nach /infrastructure/sbom (admin-v2)
|
||||
*
|
||||
* Displays:
|
||||
* - All infrastructure components (Docker services)
|
||||
* - Python/Go dependencies
|
||||
* - Node.js packages
|
||||
* - License information
|
||||
* - Version tracking
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { PagePurpose } from '@/components/common/PagePurpose'
|
||||
|
||||
interface Component {
|
||||
type: string
|
||||
name: string
|
||||
version: string
|
||||
purl?: string
|
||||
licenses?: { license: { id: string } }[]
|
||||
category?: string
|
||||
port?: string
|
||||
description?: string
|
||||
license?: string
|
||||
sourceUrl?: string
|
||||
}
|
||||
|
||||
interface SBOMData {
|
||||
bomFormat?: string
|
||||
specVersion?: string
|
||||
version?: number
|
||||
metadata?: {
|
||||
timestamp?: string
|
||||
tools?: { vendor: string; name: string; version: string }[]
|
||||
component?: { type: string; name: string; version: string }
|
||||
}
|
||||
components?: Component[]
|
||||
}
|
||||
|
||||
type CategoryType = 'all' | 'infrastructure' | 'security-tools' | 'python' | 'go' | 'nodejs' | 'unity' | 'csharp'
|
||||
type InfoTabType = 'audit' | 'documentation'
|
||||
|
||||
// Infrastructure components from docker-compose.yml and project analysis
|
||||
const INFRASTRUCTURE_COMPONENTS: Component[] = [
|
||||
// ===== DATABASES =====
|
||||
{ type: 'service', name: 'PostgreSQL', version: '16-alpine', category: 'database', port: '5432', description: 'Hauptdatenbank', license: 'PostgreSQL', sourceUrl: 'https://github.com/postgres/postgres' },
|
||||
{ type: 'service', name: 'Synapse PostgreSQL', version: '16-alpine', category: 'database', port: '-', description: 'Matrix Datenbank', license: 'PostgreSQL', sourceUrl: 'https://github.com/postgres/postgres' },
|
||||
{ type: 'service', name: 'ERPNext MariaDB', version: '10.6', category: 'database', port: '-', description: 'ERPNext Datenbank', license: 'GPL-2.0', sourceUrl: 'https://github.com/MariaDB/server' },
|
||||
{ type: 'service', name: 'MongoDB', version: '7.0', category: 'database', port: '27017', description: 'LibreChat Datenbank', license: 'SSPL-1.0', sourceUrl: 'https://github.com/mongodb/mongo' },
|
||||
|
||||
// ===== CACHE & QUEUE =====
|
||||
{ type: 'service', name: 'Valkey', version: '8-alpine', category: 'cache', port: '6379', description: 'In-Memory Cache & Sessions (Redis OSS Fork)', license: 'BSD-3-Clause', sourceUrl: 'https://github.com/valkey-io/valkey' },
|
||||
{ type: 'service', name: 'ERPNext Valkey Queue', version: 'alpine', category: 'cache', port: '-', description: 'Job Queue', license: 'BSD-3-Clause', sourceUrl: 'https://github.com/valkey-io/valkey' },
|
||||
{ type: 'service', name: 'ERPNext Valkey Cache', version: 'alpine', category: 'cache', port: '-', description: 'Cache Layer', license: 'BSD-3-Clause', sourceUrl: 'https://github.com/valkey-io/valkey' },
|
||||
|
||||
// ===== SEARCH ENGINES =====
|
||||
{ type: 'service', name: 'Qdrant', version: '1.7.4', category: 'search', port: '6333', description: 'Vector Database (RAG/Embeddings)', license: 'Apache-2.0', sourceUrl: 'https://github.com/qdrant/qdrant' },
|
||||
{ type: 'service', name: 'OpenSearch', version: '2.x', category: 'search', port: '9200', description: 'Volltext-Suche (Elasticsearch Fork)', license: 'Apache-2.0', sourceUrl: 'https://github.com/opensearch-project/OpenSearch' },
|
||||
{ type: 'service', name: 'Meilisearch', version: 'latest', category: 'search', port: '7700', description: 'Instant Search Engine', license: 'MIT', sourceUrl: 'https://github.com/meilisearch/meilisearch' },
|
||||
|
||||
// ===== OBJECT STORAGE =====
|
||||
{ type: 'service', name: 'MinIO', version: 'latest', category: 'storage', port: '9000/9001', description: 'S3-kompatibel Object Storage', license: 'AGPL-3.0', sourceUrl: 'https://github.com/minio/minio' },
|
||||
{ type: 'service', name: 'IPFS (Kubo)', version: '0.24', category: 'storage', port: '5001', description: 'Dezentrales Speichersystem', license: 'MIT/Apache-2.0', sourceUrl: 'https://github.com/ipfs/kubo' },
|
||||
{ type: 'service', name: 'DSMS Gateway', version: '1.0', category: 'storage', port: '8082', description: 'IPFS REST API', license: 'Proprietary', sourceUrl: '-' },
|
||||
|
||||
// ===== SECURITY =====
|
||||
{ type: 'service', name: 'HashiCorp Vault', version: '1.15', category: 'security', port: '8200', description: 'Secrets Management', license: 'BUSL-1.1', sourceUrl: 'https://github.com/hashicorp/vault' },
|
||||
{ type: 'service', name: 'Keycloak', version: '23.0', category: 'security', port: '8180', description: 'Identity Provider (SSO/OIDC)', license: 'Apache-2.0', sourceUrl: 'https://github.com/keycloak/keycloak' },
|
||||
|
||||
// ===== COMMUNICATION =====
|
||||
{ type: 'service', name: 'Matrix Synapse', version: 'latest', category: 'communication', port: '8008', description: 'E2EE Messenger Server', license: 'AGPL-3.0', sourceUrl: 'https://github.com/element-hq/synapse' },
|
||||
{ type: 'service', name: 'Jitsi Web', version: 'stable-9823', category: 'communication', port: '8443', description: 'Videokonferenz UI', license: 'Apache-2.0', sourceUrl: 'https://github.com/jitsi/jitsi-meet' },
|
||||
{ type: 'service', name: 'Jitsi Prosody (XMPP)', version: 'stable-9823', category: 'communication', port: '-', description: 'XMPP Server', license: 'MIT', sourceUrl: 'https://github.com/bjc/prosody' },
|
||||
{ type: 'service', name: 'Jitsi Jicofo', version: 'stable-9823', category: 'communication', port: '-', description: 'Conference Focus Component', license: 'Apache-2.0', sourceUrl: 'https://github.com/jitsi/jicofo' },
|
||||
{ type: 'service', name: 'Jitsi JVB', version: 'stable-9823', category: 'communication', port: '10000/udp', description: 'Videobridge (WebRTC SFU)', license: 'Apache-2.0', sourceUrl: 'https://github.com/jitsi/jitsi-videobridge' },
|
||||
{ type: 'service', name: 'Jibri', version: 'stable-9823', category: 'communication', port: '-', description: 'Recording & Streaming Service', license: 'Apache-2.0', sourceUrl: 'https://github.com/jitsi/jibri' },
|
||||
|
||||
// ===== APPLICATION SERVICES (Python) =====
|
||||
{ type: 'service', name: 'Python Backend (FastAPI)', version: '3.12', category: 'application', port: '8000', description: 'Haupt-Backend API, Studio & Alerts Agent', license: 'Proprietary', sourceUrl: '-' },
|
||||
{ type: 'service', name: 'Klausur Service', version: '1.0', category: 'application', port: '8086', description: 'Abitur-Klausurkorrektur (BYOEH)', license: 'Proprietary', sourceUrl: '-' },
|
||||
{ type: 'service', name: 'Compliance Module', version: '2.0', category: 'application', port: '8000', description: 'GRC Framework (19 Regulations, 558 Requirements, AI)', license: 'Proprietary', sourceUrl: '-' },
|
||||
{ type: 'service', name: 'Transcription Worker', version: '1.0', category: 'application', port: '-', description: 'Whisper + pyannote Transkription', license: 'Proprietary', sourceUrl: '-' },
|
||||
|
||||
// ===== APPLICATION SERVICES (Go) =====
|
||||
{ type: 'service', name: 'Go Consent Service', version: '1.21', category: 'application', port: '8081', description: 'DSGVO Consent Management', license: 'Proprietary', sourceUrl: '-' },
|
||||
{ type: 'service', name: 'Go School Service', version: '1.21', category: 'application', port: '8084', description: 'Klausuren, Noten, Zeugnisse', license: 'Proprietary', sourceUrl: '-' },
|
||||
{ type: 'service', name: 'Go Billing Service', version: '1.21', category: 'application', port: '8083', description: 'Stripe Billing Integration', license: 'Proprietary', sourceUrl: '-' },
|
||||
|
||||
// ===== APPLICATION SERVICES (Node.js) =====
|
||||
{ type: 'service', name: 'Next.js Admin Frontend', version: '15.1', category: 'application', port: '3000', description: 'Admin Dashboard (React)', license: 'Proprietary', sourceUrl: '-' },
|
||||
{ type: 'service', name: 'H5P Content Service', version: 'latest', category: 'application', port: '8085', description: 'Interaktive Inhalte', license: 'MIT', sourceUrl: 'https://github.com/h5p/h5p-server' },
|
||||
{ type: 'service', name: 'Policy Vault (NestJS)', version: '1.0', category: 'application', port: '3001', description: 'Richtlinien-Verwaltung API', license: 'Proprietary', sourceUrl: '-' },
|
||||
{ type: 'service', name: 'Policy Vault (Angular)', version: '17', category: 'application', port: '4200', description: 'Richtlinien-Verwaltung UI', license: 'Proprietary', sourceUrl: '-' },
|
||||
|
||||
// ===== APPLICATION SERVICES (Vue) =====
|
||||
{ type: 'service', name: 'Creator Studio (Vue 3)', version: '3.4', category: 'application', port: '-', description: 'Content Creation UI', license: 'Proprietary', sourceUrl: '-' },
|
||||
|
||||
// ===== AI/LLM SERVICES =====
|
||||
{ type: 'service', name: 'LibreChat', version: 'latest', category: 'ai', port: '3080', description: 'Multi-LLM Chat Interface', license: 'MIT', sourceUrl: 'https://github.com/danny-avila/LibreChat' },
|
||||
{ type: 'service', name: 'RAGFlow', version: 'latest', category: 'ai', port: '9380', description: 'RAG Pipeline Service', license: 'Apache-2.0', sourceUrl: 'https://github.com/infiniflow/ragflow' },
|
||||
|
||||
// ===== ERP =====
|
||||
{ type: 'service', name: 'ERPNext', version: 'v15', category: 'erp', port: '8090', description: 'Open Source ERP System', license: 'GPL-3.0', sourceUrl: 'https://github.com/frappe/erpnext' },
|
||||
|
||||
// ===== CI/CD & VERSION CONTROL =====
|
||||
{ type: 'service', name: 'Woodpecker CI', version: '2.x', category: 'cicd', port: '8082', description: 'Self-hosted CI/CD Pipeline (Drone Fork)', license: 'Apache-2.0', sourceUrl: 'https://github.com/woodpecker-ci/woodpecker' },
|
||||
{ type: 'service', name: 'Gitea', version: '1.21', category: 'cicd', port: '3003', description: 'Self-hosted Git Service', license: 'MIT', sourceUrl: 'https://github.com/go-gitea/gitea' },
|
||||
|
||||
// ===== DEVELOPMENT =====
|
||||
{ type: 'service', name: 'Mailpit', version: 'latest', category: 'development', port: '8025/1025', description: 'E-Mail Testing (SMTP Catch-All)', license: 'MIT', sourceUrl: 'https://github.com/axllent/mailpit' },
|
||||
|
||||
// ===== GAME (Breakpilot Drive) =====
|
||||
{ type: 'service', name: 'Breakpilot Drive (Unity WebGL)', version: '6000.0', category: 'game', port: '3001', description: 'Lernspiel fuer Schueler (Klasse 2-6)', license: 'Proprietary', sourceUrl: '-' },
|
||||
|
||||
// ===== VOICE SERVICE =====
|
||||
{ type: 'service', name: 'Voice Service (FastAPI)', version: '1.0', category: 'voice', port: '8091', description: 'Voice-First Interface mit PersonaPlex-7B & TaskOrchestrator', license: 'Proprietary', sourceUrl: '-' },
|
||||
{ type: 'service', name: 'PersonaPlex-7B (NVIDIA)', version: '7B', category: 'voice', port: '8998', description: 'Full-Duplex Speech-to-Speech (Produktion)', license: 'MIT/NVIDIA Open Model', sourceUrl: 'https://developer.nvidia.com' },
|
||||
{ type: 'service', name: 'TaskOrchestrator', version: '1.0', category: 'voice', port: '-', description: 'Agent-Orchestrierung mit Task State Machine', license: 'Proprietary', sourceUrl: '-' },
|
||||
{ type: 'service', name: 'Mimi Audio Codec', version: '1.0', category: 'voice', port: '-', description: 'Audio Streaming (24kHz, 80ms Frames)', license: 'MIT', sourceUrl: '-' },
|
||||
|
||||
// ===== BQAS (Quality Assurance) =====
|
||||
{ type: 'service', name: 'BQAS Local Scheduler', version: '1.0', category: 'qa', port: '-', description: 'Lokale GitHub Actions Alternative (launchd)', license: 'Proprietary', sourceUrl: '-' },
|
||||
{ type: 'service', name: 'BQAS LLM Judge', version: '1.0', category: 'qa', port: '-', description: 'Qwen2.5-32B basierte Test-Bewertung', license: 'Proprietary', sourceUrl: '-' },
|
||||
{ type: 'service', name: 'BQAS RAG Judge', version: '1.0', category: 'qa', port: '-', description: 'RAG/Korrektur Evaluierung', license: 'Proprietary', sourceUrl: '-' },
|
||||
{ type: 'service', name: 'BQAS Notifier', version: '1.0', category: 'qa', port: '-', description: 'Desktop/Slack/Email Benachrichtigungen', license: 'Proprietary', sourceUrl: '-' },
|
||||
{ type: 'service', name: 'BQAS Regression Tracker', version: '1.0', category: 'qa', port: '-', description: 'Score-Historie und Regression-Erkennung', license: 'Proprietary', sourceUrl: '-' },
|
||||
]
|
||||
|
||||
// Security Tools discovered in project
|
||||
const SECURITY_TOOLS: Component[] = [
|
||||
{ type: 'tool', name: 'Trivy', version: 'latest', category: 'security-tool', description: 'Container Vulnerability Scanner', license: 'Apache-2.0', sourceUrl: 'https://github.com/aquasecurity/trivy' },
|
||||
{ type: 'tool', name: 'Grype', version: 'latest', category: 'security-tool', description: 'SBOM Vulnerability Scanner', license: 'Apache-2.0', sourceUrl: 'https://github.com/anchore/grype' },
|
||||
{ type: 'tool', name: 'Syft', version: 'latest', category: 'security-tool', description: 'SBOM Generator', license: 'Apache-2.0', sourceUrl: 'https://github.com/anchore/syft' },
|
||||
{ type: 'tool', name: 'Gitleaks', version: 'latest', category: 'security-tool', description: 'Secrets Detection in Git', license: 'MIT', sourceUrl: 'https://github.com/gitleaks/gitleaks' },
|
||||
{ type: 'tool', name: 'TruffleHog', version: '3.x', category: 'security-tool', description: 'Secrets Scanner (Regex/Entropy)', license: 'AGPL-3.0', sourceUrl: 'https://github.com/trufflesecurity/trufflehog' },
|
||||
{ type: 'tool', name: 'Semgrep', version: 'latest', category: 'security-tool', description: 'SAST - Static Analysis', license: 'LGPL-2.1', sourceUrl: 'https://github.com/semgrep/semgrep' },
|
||||
{ type: 'tool', name: 'Bandit', version: 'latest', category: 'security-tool', description: 'Python Security Linter', license: 'Apache-2.0', sourceUrl: 'https://github.com/PyCQA/bandit' },
|
||||
{ type: 'tool', name: 'Gosec', version: 'latest', category: 'security-tool', description: 'Go Security Scanner', license: 'Apache-2.0', sourceUrl: 'https://github.com/securego/gosec' },
|
||||
{ type: 'tool', name: 'govulncheck', version: 'latest', category: 'security-tool', description: 'Go Vulnerability Check', license: 'BSD-3-Clause', sourceUrl: 'https://pkg.go.dev/golang.org/x/vuln/cmd/govulncheck' },
|
||||
{ type: 'tool', name: 'golangci-lint', version: 'latest', category: 'security-tool', description: 'Go Linter (Security Rules)', license: 'GPL-3.0', sourceUrl: 'https://github.com/golangci/golangci-lint' },
|
||||
{ type: 'tool', name: 'npm audit', version: 'built-in', category: 'security-tool', description: 'Node.js Vulnerability Check', license: 'Artistic-2.0', sourceUrl: 'https://docs.npmjs.com/cli/commands/npm-audit' },
|
||||
{ type: 'tool', name: 'pip-audit', version: 'latest', category: 'security-tool', description: 'Python Dependency Audit', license: 'Apache-2.0', sourceUrl: 'https://github.com/pypa/pip-audit' },
|
||||
{ type: 'tool', name: 'safety', version: 'latest', category: 'security-tool', description: 'Python Safety Check', license: 'MIT', sourceUrl: 'https://github.com/pyupio/safety' },
|
||||
{ type: 'tool', name: 'CodeQL', version: 'latest', category: 'security-tool', description: 'GitHub Security Analysis', license: 'MIT', sourceUrl: 'https://github.com/github/codeql' },
|
||||
]
|
||||
|
||||
// Key Python packages (from requirements.txt)
|
||||
const PYTHON_PACKAGES: Component[] = [
|
||||
{ type: 'library', name: 'FastAPI', version: '0.109+', category: 'python', description: 'Web Framework', license: 'MIT', sourceUrl: 'https://github.com/tiangolo/fastapi' },
|
||||
{ type: 'library', name: 'Uvicorn', version: '0.38+', category: 'python', description: 'ASGI Server', license: 'BSD-3-Clause', sourceUrl: 'https://github.com/encode/uvicorn' },
|
||||
{ type: 'library', name: 'Starlette', version: '0.49+', category: 'python', description: 'ASGI Framework (FastAPI Basis)', license: 'BSD-3-Clause', sourceUrl: 'https://github.com/encode/starlette' },
|
||||
{ type: 'library', name: 'Pydantic', version: '2.x', category: 'python', description: 'Data Validation', license: 'MIT', sourceUrl: 'https://github.com/pydantic/pydantic' },
|
||||
{ type: 'library', name: 'SQLAlchemy', version: '2.x', category: 'python', description: 'ORM', license: 'MIT', sourceUrl: 'https://github.com/sqlalchemy/sqlalchemy' },
|
||||
{ type: 'library', name: 'Alembic', version: '1.14+', category: 'python', description: 'DB Migrations (Classroom, Feedback Tables)', license: 'MIT', sourceUrl: 'https://github.com/sqlalchemy/alembic' },
|
||||
{ type: 'library', name: 'psycopg2-binary', version: '2.9+', category: 'python', description: 'PostgreSQL Driver', license: 'LGPL-3.0', sourceUrl: 'https://github.com/psycopg/psycopg2' },
|
||||
{ type: 'library', name: 'httpx', version: 'latest', category: 'python', description: 'Async HTTP Client', license: 'BSD-3-Clause', sourceUrl: 'https://github.com/encode/httpx' },
|
||||
{ type: 'library', name: 'PyJWT', version: 'latest', category: 'python', description: 'JWT Handling', license: 'MIT', sourceUrl: 'https://github.com/jpadilla/pyjwt' },
|
||||
{ type: 'library', name: 'hvac', version: 'latest', category: 'python', description: 'Vault Client', license: 'Apache-2.0', sourceUrl: 'https://github.com/hvac/hvac' },
|
||||
{ type: 'library', name: 'python-multipart', version: 'latest', category: 'python', description: 'File Uploads', license: 'Apache-2.0', sourceUrl: 'https://github.com/andrew-d/python-multipart' },
|
||||
{ type: 'library', name: 'aiofiles', version: 'latest', category: 'python', description: 'Async File I/O', license: 'Apache-2.0', sourceUrl: 'https://github.com/Tinche/aiofiles' },
|
||||
{ type: 'library', name: 'openai', version: 'latest', category: 'python', description: 'OpenAI SDK', license: 'MIT', sourceUrl: 'https://github.com/openai/openai-python' },
|
||||
{ type: 'library', name: 'anthropic', version: 'latest', category: 'python', description: 'Anthropic Claude SDK', license: 'MIT', sourceUrl: 'https://github.com/anthropics/anthropic-sdk-python' },
|
||||
{ type: 'library', name: 'langchain', version: 'latest', category: 'python', description: 'LLM Framework', license: 'MIT', sourceUrl: 'https://github.com/langchain-ai/langchain' },
|
||||
{ type: 'library', name: 'aioimaplib', version: 'latest', category: 'python', description: 'Async IMAP Client (Unified Inbox)', license: 'MIT', sourceUrl: 'https://github.com/bamthomas/aioimaplib' },
|
||||
{ type: 'library', name: 'aiosmtplib', version: 'latest', category: 'python', description: 'Async SMTP Client (Mail Sending)', license: 'MIT', sourceUrl: 'https://github.com/cole/aiosmtplib' },
|
||||
{ type: 'library', name: 'email-validator', version: 'latest', category: 'python', description: 'Email Validation', license: 'CC0-1.0', sourceUrl: 'https://github.com/JoshData/python-email-validator' },
|
||||
{ type: 'library', name: 'cryptography', version: 'latest', category: 'python', description: 'Encryption (Mail Credentials)', license: 'BSD-3-Clause', sourceUrl: 'https://github.com/pyca/cryptography' },
|
||||
{ type: 'library', name: 'asyncpg', version: 'latest', category: 'python', description: 'Async PostgreSQL Driver', license: 'Apache-2.0', sourceUrl: 'https://github.com/MagicStack/asyncpg' },
|
||||
{ type: 'library', name: 'python-dateutil', version: 'latest', category: 'python', description: 'Date Parsing (Deadline Extraction)', license: 'Apache-2.0', sourceUrl: 'https://github.com/dateutil/dateutil' },
|
||||
{ type: 'library', name: 'faster-whisper', version: '1.0+', category: 'python', description: 'CTranslate2 Whisper (GPU-optimiert)', license: 'MIT', sourceUrl: 'https://github.com/SYSTRAN/faster-whisper' },
|
||||
{ type: 'library', name: 'pyannote.audio', version: '3.x', category: 'python', description: 'Speaker Diarization', license: 'MIT', sourceUrl: 'https://github.com/pyannote/pyannote-audio' },
|
||||
{ type: 'library', name: 'rq', version: '1.x', category: 'python', description: 'Redis Queue (Task Processing)', license: 'BSD-2-Clause', sourceUrl: 'https://github.com/rq/rq' },
|
||||
{ type: 'library', name: 'ffmpeg-python', version: '0.2+', category: 'python', description: 'FFmpeg Python Bindings', license: 'Apache-2.0', sourceUrl: 'https://github.com/kkroening/ffmpeg-python' },
|
||||
{ type: 'library', name: 'webvtt-py', version: '0.4+', category: 'python', description: 'WebVTT Subtitle Export', license: 'MIT', sourceUrl: 'https://github.com/glut23/webvtt-py' },
|
||||
{ type: 'library', name: 'minio', version: '7.x', category: 'python', description: 'MinIO S3 Client', license: 'Apache-2.0', sourceUrl: 'https://github.com/minio/minio-py' },
|
||||
{ type: 'library', name: 'structlog', version: '24.x', category: 'python', description: 'Structured Logging', license: 'Apache-2.0', sourceUrl: 'https://github.com/hynek/structlog' },
|
||||
{ type: 'library', name: 'feedparser', version: '6.x', category: 'python', description: 'RSS/Atom Feed Parser (Alerts Agent)', license: 'BSD-2-Clause', sourceUrl: 'https://github.com/kurtmckee/feedparser' },
|
||||
{ type: 'library', name: 'APScheduler', version: '3.x', category: 'python', description: 'AsyncIO Job Scheduler (Alerts Agent)', license: 'MIT', sourceUrl: 'https://github.com/agronholm/apscheduler' },
|
||||
{ type: 'library', name: 'beautifulsoup4', version: '4.x', category: 'python', description: 'HTML Parser (Email Parsing, Compliance Scraper)', license: 'MIT', sourceUrl: 'https://code.launchpad.net/beautifulsoup' },
|
||||
{ type: 'library', name: 'lxml', version: '5.x', category: 'python', description: 'XML/HTML Parser (EUR-Lex Scraping)', license: 'BSD-3-Clause', sourceUrl: 'https://github.com/lxml/lxml' },
|
||||
{ type: 'library', name: 'PyMuPDF', version: '1.24+', category: 'python', description: 'PDF Parser (BSI-TR Extraction)', license: 'AGPL-3.0', sourceUrl: 'https://github.com/pymupdf/PyMuPDF' },
|
||||
{ type: 'library', name: 'pdfplumber', version: '0.11+', category: 'python', description: 'PDF Table Extraction (Compliance Docs)', license: 'MIT', sourceUrl: 'https://github.com/jsvine/pdfplumber' },
|
||||
{ type: 'library', name: 'websockets', version: '14.x', category: 'python', description: 'WebSocket Support (Voice Streaming)', license: 'BSD-3-Clause', sourceUrl: 'https://github.com/python-websockets/websockets' },
|
||||
{ type: 'library', name: 'soundfile', version: '0.13+', category: 'python', description: 'Audio File Processing (Voice Service)', license: 'BSD-3-Clause', sourceUrl: 'https://github.com/bastibe/python-soundfile' },
|
||||
{ type: 'library', name: 'scipy', version: '1.14+', category: 'python', description: 'Signal Processing (Audio)', license: 'BSD-3-Clause', sourceUrl: 'https://github.com/scipy/scipy' },
|
||||
{ type: 'library', name: 'redis', version: '5.x', category: 'python', description: 'Valkey/Redis Client (Voice Sessions)', license: 'MIT', sourceUrl: 'https://github.com/redis/redis-py' },
|
||||
{ type: 'library', name: 'pydantic-settings', version: '2.x', category: 'python', description: 'Settings Management (Voice Config)', license: 'MIT', sourceUrl: 'https://github.com/pydantic/pydantic-settings' },
|
||||
]
|
||||
|
||||
// Key Go modules (from go.mod files)
|
||||
const GO_MODULES: Component[] = [
|
||||
{ type: 'library', name: 'gin-gonic/gin', version: '1.9+', category: 'go', description: 'Web Framework', license: 'MIT', sourceUrl: 'https://github.com/gin-gonic/gin' },
|
||||
{ type: 'library', name: 'gorm.io/gorm', version: '1.25+', category: 'go', description: 'ORM', license: 'MIT', sourceUrl: 'https://github.com/go-gorm/gorm' },
|
||||
{ type: 'library', name: 'golang-jwt/jwt', version: 'v5', category: 'go', description: 'JWT Library', license: 'MIT', sourceUrl: 'https://github.com/golang-jwt/jwt' },
|
||||
{ type: 'library', name: 'stripe/stripe-go', version: 'v76', category: 'go', description: 'Stripe SDK', license: 'MIT', sourceUrl: 'https://github.com/stripe/stripe-go' },
|
||||
{ type: 'library', name: 'spf13/viper', version: 'latest', category: 'go', description: 'Configuration', license: 'MIT', sourceUrl: 'https://github.com/spf13/viper' },
|
||||
{ type: 'library', name: 'uber-go/zap', version: 'latest', category: 'go', description: 'Structured Logging', license: 'MIT', sourceUrl: 'https://github.com/uber-go/zap' },
|
||||
{ type: 'library', name: 'swaggo/swag', version: 'latest', category: 'go', description: 'Swagger Docs', license: 'MIT', sourceUrl: 'https://github.com/swaggo/swag' },
|
||||
]
|
||||
|
||||
// Key Node.js packages (from package.json files)
|
||||
const NODE_PACKAGES: Component[] = [
|
||||
{ type: 'library', name: 'Next.js', version: '15.1', category: 'nodejs', description: 'React Framework', license: 'MIT', sourceUrl: 'https://github.com/vercel/next.js' },
|
||||
{ type: 'library', name: 'React', version: '19', category: 'nodejs', description: 'UI Library', license: 'MIT', sourceUrl: 'https://github.com/facebook/react' },
|
||||
{ type: 'library', name: 'Vue.js', version: '3.4', category: 'nodejs', description: 'UI Framework (Creator Studio)', license: 'MIT', sourceUrl: 'https://github.com/vuejs/core' },
|
||||
{ type: 'library', name: 'Angular', version: '17', category: 'nodejs', description: 'UI Framework (Policy Vault)', license: 'MIT', sourceUrl: 'https://github.com/angular/angular' },
|
||||
{ type: 'library', name: 'NestJS', version: '10', category: 'nodejs', description: 'Node.js Framework', license: 'MIT', sourceUrl: 'https://github.com/nestjs/nest' },
|
||||
{ type: 'library', name: 'TypeScript', version: '5.x', category: 'nodejs', description: 'Type System', license: 'Apache-2.0', sourceUrl: 'https://github.com/microsoft/TypeScript' },
|
||||
{ type: 'library', name: 'Tailwind CSS', version: '3.4', category: 'nodejs', description: 'Utility CSS', license: 'MIT', sourceUrl: 'https://github.com/tailwindlabs/tailwindcss' },
|
||||
{ type: 'library', name: 'Prisma', version: '5.x', category: 'nodejs', description: 'ORM (Policy Vault)', license: 'Apache-2.0', sourceUrl: 'https://github.com/prisma/prisma' },
|
||||
{ type: 'library', name: 'Material Design Icons', version: 'latest', category: 'nodejs', description: 'Icon-System (Companion UI, Studio)', license: 'Apache-2.0', sourceUrl: 'https://github.com/google/material-design-icons' },
|
||||
{ type: 'library', name: 'Recharts', version: '2.12', category: 'nodejs', description: 'React Charts (Compliance Dashboard)', license: 'MIT', sourceUrl: 'https://github.com/recharts/recharts' },
|
||||
{ type: 'library', name: 'React Flow', version: '11.x', category: 'nodejs', description: 'Node-basierte Flow-Diagramme (Screen Flow)', license: 'MIT', sourceUrl: 'https://github.com/xyflow/xyflow' },
|
||||
]
|
||||
|
||||
// Unity packages (Breakpilot Drive game engine)
|
||||
const UNITY_PACKAGES: Component[] = [
|
||||
{ type: 'library', name: 'Unity Engine', version: '6000.0 (Unity 6)', category: 'unity', description: 'Game Engine', license: 'Unity EULA', sourceUrl: 'https://unity.com' },
|
||||
{ type: 'library', name: 'Universal Render Pipeline (URP)', version: '17.x', category: 'unity', description: 'Render Pipeline', license: 'Unity Companion', sourceUrl: 'https://docs.unity3d.com/Packages/com.unity.render-pipelines.universal@17.0' },
|
||||
{ type: 'library', name: 'TextMeshPro', version: '3.2', category: 'unity', description: 'Advanced Text Rendering', license: 'Unity Companion', sourceUrl: 'https://docs.unity3d.com/Packages/com.unity.textmeshpro@3.2' },
|
||||
{ type: 'library', name: 'Unity Mathematics', version: '1.3', category: 'unity', description: 'Math Library (SIMD)', license: 'Unity Companion', sourceUrl: 'https://docs.unity3d.com/Packages/com.unity.mathematics@1.3' },
|
||||
{ type: 'library', name: 'Newtonsoft.Json (Unity)', version: '3.2', category: 'unity', description: 'JSON Serialization', license: 'MIT', sourceUrl: 'https://docs.unity3d.com/Packages/com.unity.nuget.newtonsoft-json@3.2' },
|
||||
{ type: 'library', name: 'Unity UI', version: '2.0', category: 'unity', description: 'UI System', license: 'Unity Companion', sourceUrl: 'https://docs.unity3d.com/Packages/com.unity.ugui@2.0' },
|
||||
{ type: 'library', name: 'Unity Input System', version: '1.8', category: 'unity', description: 'New Input System', license: 'Unity Companion', sourceUrl: 'https://docs.unity3d.com/Packages/com.unity.inputsystem@1.8' },
|
||||
]
|
||||
|
||||
// C# dependencies (Breakpilot Drive)
|
||||
const CSHARP_PACKAGES: Component[] = [
|
||||
{ type: 'library', name: '.NET Standard', version: '2.1', category: 'csharp', description: 'Runtime', license: 'MIT', sourceUrl: 'https://github.com/dotnet/standard' },
|
||||
{ type: 'library', name: 'UnityWebRequest', version: 'built-in', category: 'csharp', description: 'HTTP Client', license: 'Unity Companion', sourceUrl: '-' },
|
||||
{ type: 'library', name: 'System.Text.Json', version: 'built-in', category: 'csharp', description: 'JSON Parsing', license: 'MIT', sourceUrl: 'https://github.com/dotnet/runtime' },
|
||||
]
|
||||
|
||||
export default function SBOMPage() {
|
||||
const [sbomData, setSbomData] = useState<SBOMData | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [activeCategory, setActiveCategory] = useState<CategoryType>('all')
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
const [activeInfoTab, setActiveInfoTab] = useState<InfoTabType>('audit')
|
||||
const [showFullDocs, setShowFullDocs] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
loadSBOM()
|
||||
}, [])
|
||||
|
||||
const loadSBOM = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await fetch('/api/v1/security/sbom')
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
setSbomData(data)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load SBOM:', error)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const getAllComponents = (): Component[] => {
|
||||
const infraComponents = INFRASTRUCTURE_COMPONENTS.map(c => ({
|
||||
...c,
|
||||
category: c.category || 'infrastructure'
|
||||
}))
|
||||
|
||||
const securityToolsComponents = SECURITY_TOOLS.map(c => ({
|
||||
...c,
|
||||
category: c.category || 'security-tool'
|
||||
}))
|
||||
|
||||
const pythonComponents = PYTHON_PACKAGES.map(c => ({
|
||||
...c,
|
||||
category: 'python'
|
||||
}))
|
||||
|
||||
const goComponents = GO_MODULES.map(c => ({
|
||||
...c,
|
||||
category: 'go'
|
||||
}))
|
||||
|
||||
const nodeComponents = NODE_PACKAGES.map(c => ({
|
||||
...c,
|
||||
category: 'nodejs'
|
||||
}))
|
||||
|
||||
const unityComponents = UNITY_PACKAGES.map(c => ({
|
||||
...c,
|
||||
category: 'unity'
|
||||
}))
|
||||
|
||||
const csharpComponents = CSHARP_PACKAGES.map(c => ({
|
||||
...c,
|
||||
category: 'csharp'
|
||||
}))
|
||||
|
||||
// Add dynamic SBOM data from backend if available
|
||||
const dynamicPython = (sbomData?.components || []).map(c => ({
|
||||
...c,
|
||||
category: 'python'
|
||||
}))
|
||||
|
||||
return [...infraComponents, ...securityToolsComponents, ...pythonComponents, ...goComponents, ...nodeComponents, ...unityComponents, ...csharpComponents, ...dynamicPython]
|
||||
}
|
||||
|
||||
const getFilteredComponents = () => {
|
||||
let components = getAllComponents()
|
||||
|
||||
if (activeCategory !== 'all') {
|
||||
if (activeCategory === 'infrastructure') {
|
||||
components = INFRASTRUCTURE_COMPONENTS
|
||||
} else if (activeCategory === 'security-tools') {
|
||||
components = SECURITY_TOOLS
|
||||
} else if (activeCategory === 'python') {
|
||||
components = [...PYTHON_PACKAGES, ...(sbomData?.components || [])]
|
||||
} else if (activeCategory === 'go') {
|
||||
components = GO_MODULES
|
||||
} else if (activeCategory === 'nodejs') {
|
||||
components = NODE_PACKAGES
|
||||
} else if (activeCategory === 'unity') {
|
||||
components = UNITY_PACKAGES
|
||||
} else if (activeCategory === 'csharp') {
|
||||
components = CSHARP_PACKAGES
|
||||
}
|
||||
}
|
||||
|
||||
if (searchTerm) {
|
||||
components = components.filter(c =>
|
||||
c.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
c.version.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
(c.description?.toLowerCase().includes(searchTerm.toLowerCase()))
|
||||
)
|
||||
}
|
||||
|
||||
return components
|
||||
}
|
||||
|
||||
const getCategoryColor = (category?: string) => {
|
||||
switch (category) {
|
||||
case 'database': return 'bg-blue-100 text-blue-800'
|
||||
case 'security': return 'bg-purple-100 text-purple-800'
|
||||
case 'security-tool': return 'bg-red-100 text-red-800'
|
||||
case 'application': return 'bg-green-100 text-green-800'
|
||||
case 'communication': return 'bg-yellow-100 text-yellow-800'
|
||||
case 'storage': return 'bg-orange-100 text-orange-800'
|
||||
case 'search': return 'bg-pink-100 text-pink-800'
|
||||
case 'erp': return 'bg-indigo-100 text-indigo-800'
|
||||
case 'cache': return 'bg-cyan-100 text-cyan-800'
|
||||
case 'ai': return 'bg-violet-100 text-violet-800'
|
||||
case 'development': return 'bg-gray-100 text-gray-800'
|
||||
case 'cicd': return 'bg-orange-100 text-orange-800'
|
||||
case 'python': return 'bg-emerald-100 text-emerald-800'
|
||||
case 'go': return 'bg-sky-100 text-sky-800'
|
||||
case 'nodejs': return 'bg-lime-100 text-lime-800'
|
||||
case 'unity': return 'bg-amber-100 text-amber-800'
|
||||
case 'csharp': return 'bg-fuchsia-100 text-fuchsia-800'
|
||||
case 'game': return 'bg-rose-100 text-rose-800'
|
||||
case 'voice': return 'bg-teal-100 text-teal-800'
|
||||
case 'qa': return 'bg-blue-100 text-blue-800'
|
||||
default: return 'bg-slate-100 text-slate-800'
|
||||
}
|
||||
}
|
||||
|
||||
const getLicenseColor = (license?: string) => {
|
||||
if (!license) return 'bg-gray-100 text-gray-600'
|
||||
if (license.includes('MIT')) return 'bg-green-100 text-green-700'
|
||||
if (license.includes('Apache')) return 'bg-blue-100 text-blue-700'
|
||||
if (license.includes('BSD')) return 'bg-cyan-100 text-cyan-700'
|
||||
if (license.includes('GPL') || license.includes('LGPL')) return 'bg-orange-100 text-orange-700'
|
||||
return 'bg-gray-100 text-gray-600'
|
||||
}
|
||||
|
||||
const stats = {
|
||||
totalInfra: INFRASTRUCTURE_COMPONENTS.length,
|
||||
totalSecurityTools: SECURITY_TOOLS.length,
|
||||
totalPython: PYTHON_PACKAGES.length + (sbomData?.components?.length || 0),
|
||||
totalGo: GO_MODULES.length,
|
||||
totalNode: NODE_PACKAGES.length,
|
||||
totalUnity: UNITY_PACKAGES.length,
|
||||
totalCsharp: CSHARP_PACKAGES.length,
|
||||
totalAll: INFRASTRUCTURE_COMPONENTS.length + SECURITY_TOOLS.length + PYTHON_PACKAGES.length + GO_MODULES.length + NODE_PACKAGES.length + UNITY_PACKAGES.length + CSHARP_PACKAGES.length + (sbomData?.components?.length || 0),
|
||||
databases: INFRASTRUCTURE_COMPONENTS.filter(c => c.category === 'database').length,
|
||||
services: INFRASTRUCTURE_COMPONENTS.filter(c => c.category === 'application').length,
|
||||
communication: INFRASTRUCTURE_COMPONENTS.filter(c => c.category === 'communication').length,
|
||||
game: INFRASTRUCTURE_COMPONENTS.filter(c => c.category === 'game').length,
|
||||
}
|
||||
|
||||
const categories = [
|
||||
{ id: 'all', name: 'Alle', count: stats.totalAll },
|
||||
{ id: 'infrastructure', name: 'Infrastruktur', count: stats.totalInfra },
|
||||
{ id: 'security-tools', name: 'Security Tools', count: stats.totalSecurityTools },
|
||||
{ id: 'python', name: 'Python', count: stats.totalPython },
|
||||
{ id: 'go', name: 'Go', count: stats.totalGo },
|
||||
{ id: 'nodejs', name: 'Node.js', count: stats.totalNode },
|
||||
{ id: 'unity', name: 'Unity', count: stats.totalUnity },
|
||||
{ id: 'csharp', name: 'C#', count: stats.totalCsharp },
|
||||
]
|
||||
|
||||
const filteredComponents = getFilteredComponents()
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PagePurpose
|
||||
title="SBOM"
|
||||
purpose="Software Bill of Materials - Alle Komponenten & Abhaengigkeiten der Breakpilot-Plattform. Wichtig fuer Supply-Chain-Security, Compliance-Audits und Lizenz-Pruefung."
|
||||
audience={['DevOps', 'Compliance', 'Security', 'Auditoren']}
|
||||
gdprArticles={['Art. 32 (Sicherheit der Verarbeitung)']}
|
||||
architecture={{
|
||||
services: ['Syft (SBOM Generator)', 'Trivy (CVE Scanner)'],
|
||||
databases: ['CycloneDX JSON'],
|
||||
}}
|
||||
relatedPages={[
|
||||
{ name: 'Security', href: '/infrastructure/security', description: 'DevSecOps Dashboard' },
|
||||
{ name: 'Controls', href: '/compliance/controls', description: 'Security Controls' },
|
||||
]}
|
||||
collapsible={true}
|
||||
defaultCollapsed={true}
|
||||
/>
|
||||
|
||||
{/* Wizard Link */}
|
||||
<div className="mb-6 flex justify-end">
|
||||
<Link
|
||||
href="/infrastructure/sbom/wizard"
|
||||
className="flex items-center gap-2 px-4 py-2 rounded-lg font-medium bg-purple-100 text-purple-700 border border-purple-200 hover:bg-purple-200 transition-colors"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
|
||||
</svg>
|
||||
Lern-Wizard
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-5 lg:grid-cols-10 gap-4 mb-6">
|
||||
<div className="bg-white rounded-lg shadow p-4">
|
||||
<div className="text-3xl font-bold text-slate-800">{stats.totalAll}</div>
|
||||
<div className="text-sm text-slate-500">Komponenten Total</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg shadow p-4">
|
||||
<div className="text-3xl font-bold text-purple-600">{stats.totalInfra}</div>
|
||||
<div className="text-sm text-slate-500">Docker Services</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg shadow p-4">
|
||||
<div className="text-3xl font-bold text-red-600">{stats.totalSecurityTools}</div>
|
||||
<div className="text-sm text-slate-500">Security Tools</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg shadow p-4">
|
||||
<div className="text-3xl font-bold text-emerald-600">{stats.totalPython}</div>
|
||||
<div className="text-sm text-slate-500">Python</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg shadow p-4">
|
||||
<div className="text-3xl font-bold text-sky-600">{stats.totalGo}</div>
|
||||
<div className="text-sm text-slate-500">Go</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg shadow p-4">
|
||||
<div className="text-3xl font-bold text-lime-600">{stats.totalNode}</div>
|
||||
<div className="text-sm text-slate-500">Node.js</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg shadow p-4">
|
||||
<div className="text-3xl font-bold text-amber-600">{stats.totalUnity}</div>
|
||||
<div className="text-sm text-slate-500">Unity</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg shadow p-4">
|
||||
<div className="text-3xl font-bold text-fuchsia-600">{stats.totalCsharp}</div>
|
||||
<div className="text-sm text-slate-500">C#</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg shadow p-4">
|
||||
<div className="text-3xl font-bold text-blue-600">{stats.databases}</div>
|
||||
<div className="text-sm text-slate-500">Datenbanken</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg shadow p-4">
|
||||
<div className="text-3xl font-bold text-rose-600">{stats.game}</div>
|
||||
<div className="text-sm text-slate-500">Game</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<div className="bg-white rounded-lg shadow p-4 mb-6">
|
||||
<div className="flex flex-col md:flex-row gap-4 items-center justify-between">
|
||||
{/* Category Tabs */}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{categories.map((cat) => (
|
||||
<button
|
||||
key={cat.id}
|
||||
onClick={() => setActiveCategory(cat.id as CategoryType)}
|
||||
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||
activeCategory === cat.id
|
||||
? 'bg-orange-600 text-white'
|
||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
||||
}`}
|
||||
>
|
||||
{cat.name} ({cat.count})
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Search */}
|
||||
<div className="relative w-full md:w-64">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Suchen..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="w-full pl-10 pr-4 py-2 border border-gray-200 rounded-lg focus:ring-2 focus:ring-orange-500 focus:border-transparent"
|
||||
/>
|
||||
<svg className="absolute left-3 top-2.5 w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* SBOM Metadata */}
|
||||
{sbomData?.metadata && (
|
||||
<div className="bg-slate-50 rounded-lg p-4 mb-6 text-sm">
|
||||
<div className="flex flex-wrap gap-6">
|
||||
<div>
|
||||
<span className="text-slate-500">Format:</span>
|
||||
<span className="ml-2 font-medium">{sbomData.bomFormat} {sbomData.specVersion}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-slate-500">Generiert:</span>
|
||||
<span className="ml-2 font-medium">
|
||||
{sbomData.metadata.timestamp ? new Date(sbomData.metadata.timestamp).toLocaleString('de-DE') : '-'}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-slate-500">Anwendung:</span>
|
||||
<span className="ml-2 font-medium">
|
||||
{sbomData.metadata.component?.name} v{sbomData.metadata.component?.version}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Components Table */}
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-orange-600"></div>
|
||||
<span className="ml-3 text-gray-600">Lade SBOM...</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-white rounded-lg shadow overflow-hidden">
|
||||
<table className="min-w-full divide-y divide-gray-200">
|
||||
<thead className="bg-gray-50">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Komponente</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Version</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Kategorie</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider min-w-[300px]">Verwendungszweck</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Port</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Lizenz</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Source</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white divide-y divide-gray-200">
|
||||
{filteredComponents.map((component, idx) => {
|
||||
// Get license from either the new license field or the old licenses array
|
||||
const licenseId = component.license || component.licenses?.[0]?.license?.id
|
||||
|
||||
return (
|
||||
<tr key={idx} className="hover:bg-gray-50">
|
||||
<td className="px-4 py-4">
|
||||
<div className="text-sm font-medium text-gray-900">{component.name}</div>
|
||||
</td>
|
||||
<td className="px-4 py-4 whitespace-nowrap">
|
||||
<span className="text-sm font-mono text-gray-900">{component.version}</span>
|
||||
</td>
|
||||
<td className="px-4 py-4 whitespace-nowrap">
|
||||
<span className={`px-2 py-1 text-xs font-medium rounded ${getCategoryColor(component.category)}`}>
|
||||
{component.category || component.type}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-4">
|
||||
{component.description ? (
|
||||
<div className="text-sm text-gray-600 leading-relaxed">{component.description}</div>
|
||||
) : (
|
||||
<span className="text-sm text-gray-400 italic">Keine Beschreibung</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-4 whitespace-nowrap">
|
||||
{component.port ? (
|
||||
<span className="text-sm font-mono text-gray-600">{component.port}</span>
|
||||
) : (
|
||||
<span className="text-sm text-gray-400">-</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-4 whitespace-nowrap">
|
||||
{licenseId ? (
|
||||
<span className={`px-2 py-1 text-xs font-medium rounded ${getLicenseColor(licenseId)}`}>
|
||||
{licenseId}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-sm text-gray-400">-</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-4 whitespace-nowrap">
|
||||
{component.sourceUrl && component.sourceUrl !== '-' ? (
|
||||
<a
|
||||
href={component.sourceUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-orange-600 hover:text-orange-800 text-sm flex items-center gap-1"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
|
||||
</svg>
|
||||
GitHub
|
||||
</a>
|
||||
) : (
|
||||
<span className="text-sm text-gray-400">-</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{filteredComponents.length === 0 && (
|
||||
<div className="p-8 text-center text-gray-500">
|
||||
Keine Komponenten gefunden.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Export Button */}
|
||||
<div className="mt-6 flex justify-end">
|
||||
<button
|
||||
onClick={() => {
|
||||
const data = JSON.stringify({
|
||||
...sbomData,
|
||||
infrastructure: INFRASTRUCTURE_COMPONENTS
|
||||
}, null, 2)
|
||||
const blob = new Blob([data], { type: 'application/json' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `breakpilot-sbom-${new Date().toISOString().split('T')[0]}.json`
|
||||
a.click()
|
||||
}}
|
||||
className="px-4 py-2 bg-orange-600 text-white rounded-lg hover:bg-orange-700 transition-colors flex items-center gap-2"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
||||
</svg>
|
||||
SBOM exportieren (JSON)
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Info Tabs Section */}
|
||||
<div className="mt-8">
|
||||
<div className="bg-white rounded-lg shadow">
|
||||
{/* Tab Headers */}
|
||||
<div className="border-b border-gray-200">
|
||||
<nav className="flex -mb-px">
|
||||
<button
|
||||
onClick={() => setActiveInfoTab('audit')}
|
||||
className={`px-6 py-4 text-sm font-medium border-b-2 transition-colors ${
|
||||
activeInfoTab === 'audit'
|
||||
? 'border-orange-500 text-orange-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
Audit
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveInfoTab('documentation')}
|
||||
className={`px-6 py-4 text-sm font-medium border-b-2 transition-colors ${
|
||||
activeInfoTab === 'documentation'
|
||||
? 'border-orange-500 text-orange-600'
|
||||
: 'border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300'
|
||||
}`}
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
||||
</svg>
|
||||
Dokumentation
|
||||
</span>
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* Tab Content */}
|
||||
<div className="p-6">
|
||||
{/* Audit Tab */}
|
||||
{activeInfoTab === 'audit' && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* SBOM Status */}
|
||||
<div className="bg-slate-50 rounded-lg p-4">
|
||||
<h3 className="text-lg font-semibold text-slate-800 mb-4 flex items-center gap-2">
|
||||
<svg className="w-5 h-5 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
|
||||
</svg>
|
||||
SBOM Status
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-slate-600">Letzte Generierung</span>
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="w-2 h-2 rounded-full bg-green-500"></span>
|
||||
<span className="text-sm font-medium">CI/CD</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-slate-600">Format</span>
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="w-2 h-2 rounded-full bg-green-500"></span>
|
||||
<span className="text-sm font-medium">CycloneDX 1.5</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-slate-600">Komponenten</span>
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="w-2 h-2 rounded-full bg-green-500"></span>
|
||||
<span className="text-sm font-medium">Alle erfasst</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-slate-600">Transitive Deps</span>
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="w-2 h-2 rounded-full bg-green-500"></span>
|
||||
<span className="text-sm font-medium">Inkludiert</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* License Compliance */}
|
||||
<div className="bg-slate-50 rounded-lg p-4">
|
||||
<h3 className="text-lg font-semibold text-slate-800 mb-4 flex items-center gap-2">
|
||||
<svg className="w-5 h-5 text-purple-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 6l3 1m0 0l-3 9a5.002 5.002 0 006.001 0M6 7l3 9M6 7l6-2m6 2l3-1m-3 1l-3 9a5.002 5.002 0 006.001 0M18 7l3 9m-3-9l-6-2m0-2v2m0 16V5m0 16H9m3 0h3" />
|
||||
</svg>
|
||||
License Compliance
|
||||
</h3>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-slate-600">Erlaubte Lizenzen</span>
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="w-2 h-2 rounded-full bg-green-500"></span>
|
||||
<span className="text-sm font-medium">MIT, Apache, BSD</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-slate-600">Copyleft (GPL)</span>
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="w-2 h-2 rounded-full bg-green-500"></span>
|
||||
<span className="text-sm font-medium">0</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-slate-600">Unbekannte Lizenzen</span>
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="w-2 h-2 rounded-full bg-green-500"></span>
|
||||
<span className="text-sm font-medium">0</span>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-sm text-slate-600">Kommerzielle</span>
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="w-2 h-2 rounded-full bg-yellow-500"></span>
|
||||
<span className="text-sm font-medium">Review erforderlich</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Documentation Tab */}
|
||||
{activeInfoTab === 'documentation' && (
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h3 className="text-lg font-semibold text-slate-800">SBOM Dokumentation</h3>
|
||||
<button
|
||||
onClick={() => setShowFullDocs(!showFullDocs)}
|
||||
className="px-4 py-2 bg-slate-100 text-slate-700 rounded-lg hover:bg-slate-200 transition-colors flex items-center gap-2"
|
||||
>
|
||||
<svg className={`w-4 h-4 transition-transform ${showFullDocs ? 'rotate-180' : ''}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
{showFullDocs ? 'Weniger anzeigen' : 'Vollstaendige Dokumentation'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{!showFullDocs ? (
|
||||
<div className="prose prose-slate max-w-none">
|
||||
<p className="text-slate-600">
|
||||
Das SBOM-Modul generiert und analysiert die vollstaendige Komponentenliste aller Software-Abhaengigkeiten.
|
||||
Es dient der Compliance, Sicherheit und Supply-Chain-Transparenz.
|
||||
</p>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mt-4">
|
||||
<div className="bg-blue-50 p-4 rounded-lg">
|
||||
<h4 className="font-medium text-blue-800 mb-2">Generator</h4>
|
||||
<p className="text-sm text-blue-600">Syft (Primary), Trivy (Validation)</p>
|
||||
</div>
|
||||
<div className="bg-purple-50 p-4 rounded-lg">
|
||||
<h4 className="font-medium text-purple-800 mb-2">Format</h4>
|
||||
<p className="text-sm text-purple-600">CycloneDX 1.5, SPDX</p>
|
||||
</div>
|
||||
<div className="bg-green-50 p-4 rounded-lg">
|
||||
<h4 className="font-medium text-green-800 mb-2">Retention</h4>
|
||||
<p className="text-sm text-green-600">5 Jahre (Compliance)</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="prose prose-slate max-w-none bg-slate-50 p-6 rounded-lg overflow-auto max-h-[600px]">
|
||||
<h2>Software Bill of Materials (SBOM)</h2>
|
||||
|
||||
<h3>1. Uebersicht</h3>
|
||||
<p>Das SBOM-Modul generiert und analysiert die vollstaendige Komponentenliste aller Software-Abhaengigkeiten. Es dient der Compliance, Sicherheit und Supply-Chain-Transparenz.</p>
|
||||
|
||||
<h3>2. SBOM-Generierung</h3>
|
||||
<pre className="bg-slate-800 text-slate-100 p-4 rounded-lg overflow-x-auto text-sm">
|
||||
{`Source Code
|
||||
│
|
||||
v
|
||||
┌───────────────────────────────────────────────────────────────┐
|
||||
│ SBOM Generators │
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
|
||||
│ │ Syft │ │ Trivy │ │ Native Tooling │ │
|
||||
│ │ (Primary) │ │ (Validation)│ │ (npm, go mod, pip) │ │
|
||||
│ └──────┬──────┘ └──────┬──────┘ └──────────┬──────────┘ │
|
||||
└─────────┼────────────────┼────────────────────┼───────────────┘
|
||||
│ │ │
|
||||
└────────────────┴────────────────────┘
|
||||
│
|
||||
v
|
||||
┌────────────────┐
|
||||
│ CycloneDX │
|
||||
│ Format │
|
||||
└────────────────┘`}
|
||||
</pre>
|
||||
|
||||
<h3>3. Erfasste Komponenten</h3>
|
||||
<table className="min-w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="text-left">Typ</th>
|
||||
<th className="text-left">Quelle</th>
|
||||
<th className="text-left">Beispiele</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>npm packages</td><td>package-lock.json</td><td>react, next, tailwindcss</td></tr>
|
||||
<tr><td>Go modules</td><td>go.sum</td><td>gin, gorm, jwt-go</td></tr>
|
||||
<tr><td>Python packages</td><td>requirements.txt</td><td>fastapi, pydantic, httpx</td></tr>
|
||||
<tr><td>Container Images</td><td>Dockerfile</td><td>node:20-alpine, postgres:16</td></tr>
|
||||
<tr><td>OS Packages</td><td>apk, apt</td><td>openssl, libpq</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>4. License Compliance</h3>
|
||||
<table className="min-w-full">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="text-left">Kategorie</th>
|
||||
<th className="text-left">Lizenzen</th>
|
||||
<th className="text-left">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>Permissive (erlaubt)</td><td>MIT, Apache 2.0, BSD, ISC</td><td className="text-green-600">OK</td></tr>
|
||||
<tr><td>Weak Copyleft</td><td>LGPL, MPL</td><td className="text-yellow-600">Review</td></tr>
|
||||
<tr><td>Strong Copyleft</td><td>GPL, AGPL</td><td className="text-red-600">Nicht erlaubt</td></tr>
|
||||
<tr><td>Proprietaer</td><td>Commercial</td><td className="text-yellow-600">Genehmigung</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h3>5. Aufbewahrung & Compliance</h3>
|
||||
<ul>
|
||||
<li><strong>Retention:</strong> 5 Jahre (Compliance)</li>
|
||||
<li><strong>Format:</strong> JSON + PDF Report</li>
|
||||
<li><strong>Signierung:</strong> Digital signiert</li>
|
||||
<li><strong>Audit:</strong> Jederzeit abrufbar</li>
|
||||
</ul>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
558
admin-v2/app/(admin)/infrastructure/sbom/wizard/page.tsx
Normal file
558
admin-v2/app/(admin)/infrastructure/sbom/wizard/page.tsx
Normal file
@@ -0,0 +1,558 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* SBOM (Software Bill of Materials) - Lern-Wizard
|
||||
*
|
||||
* Migriert von /admin/sbom/wizard (website) nach /infrastructure/sbom/wizard (admin-v2)
|
||||
*
|
||||
* Interaktiver Wizard zum Verstehen der SBOM:
|
||||
* - Was ist eine SBOM?
|
||||
* - Warum ist sie wichtig?
|
||||
* - Kategorien erklaert
|
||||
* - Breakpilot Drive (Unity/C#/Game) Komponenten
|
||||
* - Lizenzen und Compliance
|
||||
*/
|
||||
|
||||
import { useState } from 'react'
|
||||
import Link from 'next/link'
|
||||
|
||||
// ==============================================
|
||||
// Types
|
||||
// ==============================================
|
||||
|
||||
type StepStatus = 'pending' | 'active' | 'completed'
|
||||
|
||||
interface WizardStep {
|
||||
id: string
|
||||
name: string
|
||||
icon: string
|
||||
status: StepStatus
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Constants
|
||||
// ==============================================
|
||||
|
||||
const STEPS: WizardStep[] = [
|
||||
{ id: 'welcome', name: 'Willkommen', icon: '📋', status: 'pending' },
|
||||
{ id: 'what-is-sbom', name: 'Was ist SBOM?', icon: '❓', status: 'pending' },
|
||||
{ id: 'why-important', name: 'Warum wichtig?', icon: '⚠️', status: 'pending' },
|
||||
{ id: 'categories', name: 'Kategorien', icon: '📁', status: 'pending' },
|
||||
{ id: 'infrastructure', name: 'Infrastruktur', icon: '🏗️', status: 'pending' },
|
||||
{ id: 'unity-game', name: 'Unity & Game', icon: '🎮', status: 'pending' },
|
||||
{ id: 'licenses', name: 'Lizenzen', icon: '📜', status: 'pending' },
|
||||
{ id: 'summary', name: 'Zusammenfassung', icon: '✅', status: 'pending' },
|
||||
]
|
||||
|
||||
const EDUCATION_CONTENT: Record<string, { title: string; content: string[]; tips?: string[] }> = {
|
||||
'welcome': {
|
||||
title: 'Willkommen zum SBOM-Wizard!',
|
||||
content: [
|
||||
'Eine **Software Bill of Materials (SBOM)** ist wie ein Zutaten-Etikett fuer Software.',
|
||||
'Sie listet alle Komponenten auf, aus denen eine Anwendung besteht:',
|
||||
'• Open-Source-Bibliotheken',
|
||||
'• Frameworks und Engines',
|
||||
'• Infrastruktur-Dienste',
|
||||
'• Entwicklungs-Tools',
|
||||
'In diesem Wizard lernst du, warum SBOMs wichtig sind und welche Komponenten BreakPilot verwendet - inklusive der neuen **Breakpilot Drive** (Unity) Komponenten.',
|
||||
],
|
||||
tips: [
|
||||
'SBOMs sind seit 2021 fuer US-Regierungsauftraege Pflicht',
|
||||
'Die EU plant aehnliche Vorschriften im Cyber Resilience Act',
|
||||
],
|
||||
},
|
||||
'what-is-sbom': {
|
||||
title: 'Was ist eine SBOM?',
|
||||
content: [
|
||||
'**SBOM = Software Bill of Materials**',
|
||||
'Eine SBOM ist eine vollstaendige Liste aller Software-Komponenten:',
|
||||
'**Enthaltene Informationen:**',
|
||||
'• Name der Komponente',
|
||||
'• Version',
|
||||
'• Lizenz (MIT, Apache, GPL, etc.)',
|
||||
'• Herkunft (Source URL)',
|
||||
'• Typ (Library, Service, Tool)',
|
||||
'**Formate:**',
|
||||
'• SPDX (Linux Foundation Standard)',
|
||||
'• CycloneDX (OWASP Standard)',
|
||||
'• SWID Tags (ISO Standard)',
|
||||
'BreakPilot verwendet eine eigene Darstellung im Admin-Panel, die alle relevanten Infos zeigt.',
|
||||
],
|
||||
tips: [
|
||||
'Eine SBOM ist wie ein Beipackzettel fuer Medikamente',
|
||||
'Sie ermoeglicht schnelle Reaktion bei Sicherheitsluecken',
|
||||
],
|
||||
},
|
||||
'why-important': {
|
||||
title: 'Warum sind SBOMs wichtig?',
|
||||
content: [
|
||||
'**1. Sicherheit (Security)**',
|
||||
'Wenn eine Sicherheitsluecke in einer Bibliothek entdeckt wird (z.B. Log4j), kannst du sofort pruefen ob du betroffen bist.',
|
||||
'**2. Compliance (Lizenz-Einhaltung)**',
|
||||
'Verschiedene Lizenzen haben verschiedene Anforderungen:',
|
||||
'• MIT: Fast keine Einschraenkungen',
|
||||
'• GPL: Copyleft - abgeleitete Werke muessen auch GPL sein',
|
||||
'• Proprietary: Kommerzielle Nutzung eingeschraenkt',
|
||||
'**3. Supply Chain Security**',
|
||||
'Moderne Software besteht aus hunderten Abhaengigkeiten. Eine SBOM macht diese Kette transparent.',
|
||||
'**4. Regulatorische Anforderungen**',
|
||||
'US Executive Order 14028 verlangt SBOMs fuer Regierungssoftware.',
|
||||
],
|
||||
tips: [
|
||||
'Log4Shell (2021) betraf Millionen von Systemen',
|
||||
'Mit SBOM: Betroffenheit in Minuten geprueft',
|
||||
],
|
||||
},
|
||||
'categories': {
|
||||
title: 'SBOM-Kategorien in BreakPilot',
|
||||
content: [
|
||||
'Die BreakPilot SBOM ist in Kategorien unterteilt:',
|
||||
'**infrastructure** (Blau)',
|
||||
'→ Kern-Infrastruktur: PostgreSQL, Valkey, Keycloak, Docker',
|
||||
'**security-tools** (Rot)',
|
||||
'→ Sicherheits-Tools: Trivy, Gitleaks, Semgrep',
|
||||
'**python** (Gelb)',
|
||||
'→ Python-Backend: FastAPI, Pydantic, httpx',
|
||||
'**go** (Cyan)',
|
||||
'→ Go-Services: Gin, GORM, JWT',
|
||||
'**nodejs** (Gruen)',
|
||||
'→ Frontend: Next.js, React, Tailwind',
|
||||
'**unity** (Amber) ← NEU!',
|
||||
'→ Game Engine: Unity 6, URP, TextMeshPro',
|
||||
'**csharp** (Fuchsia) ← NEU!',
|
||||
'→ C#/.NET: .NET Standard, UnityWebRequest',
|
||||
'**game** (Rose) ← NEU!',
|
||||
'→ Breakpilot Drive Service',
|
||||
],
|
||||
tips: [
|
||||
'Klicke auf eine Kategorie um zu filtern',
|
||||
'Die neuen Unity/Game-Kategorien wurden fuer Breakpilot Drive hinzugefuegt',
|
||||
],
|
||||
},
|
||||
'infrastructure': {
|
||||
title: 'Infrastruktur-Komponenten',
|
||||
content: [
|
||||
'BreakPilot basiert auf robuster Infrastruktur:',
|
||||
'**Datenbanken:**',
|
||||
'• PostgreSQL 16 - Relationale Datenbank',
|
||||
'• Valkey 8 - In-Memory Cache (Redis-Fork)',
|
||||
'• ChromaDB - Vector Store fuer RAG',
|
||||
'**Auth & Security:**',
|
||||
'• Keycloak 23 - Identity & Access Management',
|
||||
'• HashiCorp Vault - Secrets Management',
|
||||
'**Container & Orchestrierung:**',
|
||||
'• Docker - Container Runtime',
|
||||
'• Traefik - Reverse Proxy',
|
||||
'**Kommunikation:**',
|
||||
'• Matrix Synapse - Chat/Messaging',
|
||||
'• Jitsi Meet - Video-Konferenzen',
|
||||
],
|
||||
tips: [
|
||||
'Alle Services laufen in Docker-Containern',
|
||||
'Ports sind in docker-compose.yml definiert',
|
||||
],
|
||||
},
|
||||
'unity-game': {
|
||||
title: 'Unity & Breakpilot Drive',
|
||||
content: [
|
||||
'**Neu hinzugefuegt fuer Breakpilot Drive:**',
|
||||
'**Unity Engine (6000.0)**',
|
||||
'→ Die Game Engine fuer das Lernspiel',
|
||||
'→ Lizenz: Unity EULA (kostenlos bis 100k Revenue)',
|
||||
'**Universal Render Pipeline (17.x)**',
|
||||
'→ Optimierte Grafik-Pipeline fuer WebGL',
|
||||
'→ Lizenz: Unity Companion License',
|
||||
'**TextMeshPro (3.2)**',
|
||||
'→ Fortgeschrittenes Text-Rendering',
|
||||
'**Unity Mathematics (1.3)**',
|
||||
'→ SIMD-optimierte Mathe-Bibliothek',
|
||||
'**Newtonsoft.Json (3.2)**',
|
||||
'→ JSON-Serialisierung fuer API-Kommunikation',
|
||||
'**C# Abhaengigkeiten:**',
|
||||
'• .NET Standard 2.1',
|
||||
'• UnityWebRequest (HTTP Client)',
|
||||
'• System.Text.Json',
|
||||
],
|
||||
tips: [
|
||||
'Unity 6 ist die neueste LTS-Version',
|
||||
'WebGL-Builds sind ~30-50 MB gross',
|
||||
],
|
||||
},
|
||||
'licenses': {
|
||||
title: 'Lizenz-Compliance',
|
||||
content: [
|
||||
'**Lizenz-Typen in BreakPilot:**',
|
||||
'**Permissive (Unkompliziert):**',
|
||||
'• MIT - Die meisten JS/Python Libs',
|
||||
'• Apache 2.0 - FastAPI, Keycloak',
|
||||
'• BSD - PostgreSQL',
|
||||
'**Copyleft (Vorsicht bei Aenderungen):**',
|
||||
'• GPL - Wenige Komponenten',
|
||||
'• AGPL - Jitsi (Server-Side OK)',
|
||||
'**Proprietary:**',
|
||||
'• Unity EULA - Kostenlos bis 100k Revenue',
|
||||
'• Unity Companion - Packages an Engine gebunden',
|
||||
'**Wichtig:**',
|
||||
'Alle verwendeten Lizenzen sind mit kommerziellem Einsatz kompatibel. Bei Fragen: Rechtsabteilung konsultieren.',
|
||||
],
|
||||
tips: [
|
||||
'MIT und Apache 2.0 sind am unproblematischsten',
|
||||
'AGPL erfordert Source-Code-Freigabe bei Modifikation',
|
||||
],
|
||||
},
|
||||
'summary': {
|
||||
title: 'Zusammenfassung',
|
||||
content: [
|
||||
'Du hast die SBOM von BreakPilot kennengelernt:',
|
||||
'✅ Was eine SBOM ist und warum sie wichtig ist',
|
||||
'✅ Die verschiedenen Kategorien (8 Stueck)',
|
||||
'✅ Infrastruktur-Komponenten',
|
||||
'✅ Die neuen Unity/Game-Komponenten fuer Breakpilot Drive',
|
||||
'✅ Lizenz-Typen und Compliance',
|
||||
'**Im SBOM-Dashboard kannst du:**',
|
||||
'• Nach Kategorie filtern',
|
||||
'• Nach Namen suchen',
|
||||
'• Lizenzen pruefen',
|
||||
'• Komponenten-Details ansehen',
|
||||
'**180+ Komponenten** sind dokumentiert und nachverfolgbar.',
|
||||
],
|
||||
tips: [
|
||||
'Pruefe regelmaessig auf veraltete Komponenten',
|
||||
'Bei neuen Abhaengigkeiten: SBOM aktualisieren',
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Components
|
||||
// ==============================================
|
||||
|
||||
function WizardStepper({
|
||||
steps,
|
||||
currentStep,
|
||||
onStepClick
|
||||
}: {
|
||||
steps: WizardStep[]
|
||||
currentStep: number
|
||||
onStepClick: (index: number) => void
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-center justify-between mb-8 overflow-x-auto pb-4">
|
||||
{steps.map((step, index) => (
|
||||
<div key={step.id} className="flex items-center">
|
||||
<button
|
||||
onClick={() => onStepClick(index)}
|
||||
className={`flex flex-col items-center min-w-[80px] p-2 rounded-lg transition-colors ${
|
||||
index === currentStep
|
||||
? 'bg-orange-100 text-orange-700'
|
||||
: step.status === 'completed'
|
||||
? 'bg-green-100 text-green-700 cursor-pointer hover:bg-green-200'
|
||||
: 'text-slate-400 hover:bg-slate-100'
|
||||
}`}
|
||||
>
|
||||
<span className="text-2xl mb-1">{step.icon}</span>
|
||||
<span className="text-xs font-medium text-center">{step.name}</span>
|
||||
{step.status === 'completed' && <span className="text-xs text-green-600">✓</span>}
|
||||
</button>
|
||||
{index < steps.length - 1 && (
|
||||
<div className={`h-0.5 w-8 mx-1 ${
|
||||
index < currentStep ? 'bg-green-400' : 'bg-slate-200'
|
||||
}`} />
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function EducationCard({ stepId }: { stepId: string }) {
|
||||
const content = EDUCATION_CONTENT[stepId]
|
||||
if (!content) return null
|
||||
|
||||
return (
|
||||
<div className="bg-orange-50 border border-orange-200 rounded-lg p-6 mb-6">
|
||||
<h3 className="text-lg font-semibold text-orange-800 mb-4 flex items-center">
|
||||
<span className="mr-2">📖</span>
|
||||
{content.title}
|
||||
</h3>
|
||||
<div className="space-y-2 text-orange-900">
|
||||
{content.content.map((line, index) => (
|
||||
<p
|
||||
key={index}
|
||||
className={`${line.startsWith('•') ? 'ml-4' : ''} ${line.startsWith('**') ? 'font-semibold mt-3' : ''}`}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: line
|
||||
.replace(/\*\*(.*?)\*\*/g, '<strong>$1</strong>')
|
||||
.replace(/→/g, '<span class="text-orange-600">→</span>')
|
||||
.replace(/← NEU!/g, '<span class="bg-amber-200 text-amber-800 px-1 rounded text-sm font-bold">← NEU!</span>')
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{content.tips && content.tips.length > 0 && (
|
||||
<div className="mt-4 pt-4 border-t border-orange-200">
|
||||
<p className="text-sm font-semibold text-orange-700 mb-2">💡 Tipps:</p>
|
||||
{content.tips.map((tip, index) => (
|
||||
<p key={index} className="text-sm text-orange-700 ml-4">• {tip}</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function CategoryDemo({ stepId }: { stepId: string }) {
|
||||
if (stepId === 'categories') {
|
||||
const categories = [
|
||||
{ name: 'infrastructure', color: 'blue', count: 45 },
|
||||
{ name: 'security-tools', color: 'red', count: 12 },
|
||||
{ name: 'python', color: 'yellow', count: 35 },
|
||||
{ name: 'go', color: 'cyan', count: 18 },
|
||||
{ name: 'nodejs', color: 'green', count: 55 },
|
||||
{ name: 'unity', color: 'amber', count: 7, isNew: true },
|
||||
{ name: 'csharp', color: 'fuchsia', count: 3, isNew: true },
|
||||
{ name: 'game', color: 'rose', count: 1, isNew: true },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="bg-white border border-slate-200 rounded-lg p-4 mb-4">
|
||||
<h4 className="font-semibold text-slate-800 mb-3">Live-Vorschau: Kategorien</h4>
|
||||
<div className="grid grid-cols-4 gap-2">
|
||||
{categories.map((cat) => (
|
||||
<div
|
||||
key={cat.name}
|
||||
className={`bg-${cat.color}-100 text-${cat.color}-800 px-3 py-2 rounded-lg text-center text-sm relative`}
|
||||
>
|
||||
<p className="font-medium">{cat.name}</p>
|
||||
<p className="text-xs opacity-70">{cat.count} Komponenten</p>
|
||||
{cat.isNew && (
|
||||
<span className="absolute -top-1 -right-1 bg-amber-500 text-white text-xs px-1 rounded">NEU</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (stepId === 'unity-game') {
|
||||
const unityComponents = [
|
||||
{ name: 'Unity Engine', version: '6000.0', license: 'Unity EULA' },
|
||||
{ name: 'URP', version: '17.x', license: 'Unity Companion' },
|
||||
{ name: 'TextMeshPro', version: '3.2', license: 'Unity Companion' },
|
||||
{ name: 'Mathematics', version: '1.3', license: 'Unity Companion' },
|
||||
{ name: 'Newtonsoft.Json', version: '3.2', license: 'MIT' },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="bg-white border border-slate-200 rounded-lg p-4 mb-4">
|
||||
<h4 className="font-semibold text-slate-800 mb-3">Unity Packages (Breakpilot Drive)</h4>
|
||||
<div className="space-y-2">
|
||||
{unityComponents.map((comp) => (
|
||||
<div key={comp.name} className="flex items-center justify-between py-2 border-b border-slate-100 last:border-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="bg-amber-100 text-amber-800 text-xs px-2 py-0.5 rounded">unity</span>
|
||||
<span className="font-medium">{comp.name}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-sm text-slate-600">
|
||||
<span>{comp.version}</span>
|
||||
<span className="bg-slate-100 px-2 py-0.5 rounded text-xs">{comp.license}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (stepId === 'licenses') {
|
||||
const licenses = [
|
||||
{ name: 'MIT', count: 85, color: 'green', risk: 'Niedrig' },
|
||||
{ name: 'Apache 2.0', count: 45, color: 'green', risk: 'Niedrig' },
|
||||
{ name: 'BSD', count: 12, color: 'green', risk: 'Niedrig' },
|
||||
{ name: 'Unity EULA', count: 1, color: 'yellow', risk: 'Mittel' },
|
||||
{ name: 'Unity Companion', count: 6, color: 'yellow', risk: 'Mittel' },
|
||||
{ name: 'AGPL', count: 2, color: 'orange', risk: 'Hoch' },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="bg-white border border-slate-200 rounded-lg p-4 mb-4">
|
||||
<h4 className="font-semibold text-slate-800 mb-3">Lizenz-Uebersicht</h4>
|
||||
<div className="space-y-2">
|
||||
{licenses.map((lic) => (
|
||||
<div key={lic.name} className="flex items-center justify-between py-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium">{lic.name}</span>
|
||||
<span className="text-sm text-slate-500">({lic.count})</span>
|
||||
</div>
|
||||
<span className={`text-xs px-2 py-1 rounded ${
|
||||
lic.risk === 'Niedrig' ? 'bg-green-100 text-green-700' :
|
||||
lic.risk === 'Mittel' ? 'bg-yellow-100 text-yellow-700' :
|
||||
'bg-orange-100 text-orange-700'
|
||||
}`}>
|
||||
Risiko: {lic.risk}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// ==============================================
|
||||
// Main Component
|
||||
// ==============================================
|
||||
|
||||
export default function SBOMWizardPage() {
|
||||
const [currentStep, setCurrentStep] = useState(0)
|
||||
const [steps, setSteps] = useState<WizardStep[]>(STEPS)
|
||||
|
||||
const currentStepData = steps[currentStep]
|
||||
const isWelcome = currentStepData?.id === 'welcome'
|
||||
const isSummary = currentStepData?.id === 'summary'
|
||||
|
||||
const goToNext = () => {
|
||||
if (currentStep < steps.length - 1) {
|
||||
setSteps(prev => prev.map((step, idx) =>
|
||||
idx === currentStep && step.status === 'pending'
|
||||
? { ...step, status: 'completed' }
|
||||
: step
|
||||
))
|
||||
setCurrentStep(prev => prev + 1)
|
||||
}
|
||||
}
|
||||
|
||||
const goToPrev = () => {
|
||||
if (currentStep > 0) {
|
||||
setCurrentStep(prev => prev - 1)
|
||||
}
|
||||
}
|
||||
|
||||
const handleStepClick = (index: number) => {
|
||||
setCurrentStep(index)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-100 py-8">
|
||||
<div className="max-w-4xl mx-auto px-4">
|
||||
{/* Header */}
|
||||
<div className="bg-white rounded-lg shadow-lg p-6 mb-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-800">📋 SBOM Lern-Wizard</h1>
|
||||
<p className="text-slate-600 mt-1">
|
||||
Software Bill of Materials verstehen
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/infrastructure/sbom"
|
||||
className="text-orange-600 hover:text-orange-800 text-sm"
|
||||
>
|
||||
← Zurueck zur SBOM
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Stepper */}
|
||||
<div className="bg-white rounded-lg shadow-lg p-6 mb-6">
|
||||
<WizardStepper
|
||||
steps={steps}
|
||||
currentStep={currentStep}
|
||||
onStepClick={handleStepClick}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<div className="bg-white rounded-lg shadow-lg p-6">
|
||||
{/* Step Header */}
|
||||
<div className="flex items-center mb-6">
|
||||
<span className="text-4xl mr-4">{currentStepData?.icon}</span>
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-slate-800">
|
||||
Schritt {currentStep + 1}: {currentStepData?.name}
|
||||
</h2>
|
||||
<p className="text-slate-500 text-sm">
|
||||
{currentStep + 1} von {steps.length}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Education Card */}
|
||||
<EducationCard stepId={currentStepData?.id || ''} />
|
||||
|
||||
{/* Category Demo */}
|
||||
<CategoryDemo stepId={currentStepData?.id || ''} />
|
||||
|
||||
{/* Welcome Start Button */}
|
||||
{isWelcome && (
|
||||
<div className="text-center py-8">
|
||||
<button
|
||||
onClick={goToNext}
|
||||
className="bg-orange-600 text-white px-8 py-3 rounded-lg font-medium hover:bg-orange-700 transition-colors"
|
||||
>
|
||||
🚀 Lern-Tour starten
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Summary Actions */}
|
||||
{isSummary && (
|
||||
<div className="text-center py-6 space-y-4">
|
||||
<div className="flex justify-center gap-4">
|
||||
<Link
|
||||
href="/infrastructure/sbom"
|
||||
className="px-6 py-3 bg-orange-600 text-white rounded-lg font-medium hover:bg-orange-700 transition-colors"
|
||||
>
|
||||
📋 Zur SBOM
|
||||
</Link>
|
||||
<button
|
||||
onClick={() => {
|
||||
setCurrentStep(0)
|
||||
setSteps(STEPS.map(s => ({ ...s, status: 'pending' })))
|
||||
}}
|
||||
className="px-6 py-3 bg-slate-200 text-slate-700 rounded-lg font-medium hover:bg-slate-300 transition-colors"
|
||||
>
|
||||
🔄 Wizard neu starten
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Navigation */}
|
||||
{!isWelcome && (
|
||||
<div className="flex justify-between mt-8 pt-6 border-t">
|
||||
<button
|
||||
onClick={goToPrev}
|
||||
disabled={currentStep === 0}
|
||||
className={`px-6 py-2 rounded-lg transition-colors ${
|
||||
currentStep === 0
|
||||
? 'bg-slate-200 text-slate-400 cursor-not-allowed'
|
||||
: 'bg-slate-200 text-slate-700 hover:bg-slate-300'
|
||||
}`}
|
||||
>
|
||||
← Zurueck
|
||||
</button>
|
||||
|
||||
{!isSummary && (
|
||||
<button
|
||||
onClick={goToNext}
|
||||
className="bg-orange-600 text-white px-6 py-2 rounded-lg hover:bg-orange-700 transition-colors"
|
||||
>
|
||||
Weiter →
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Footer Info */}
|
||||
<div className="text-center text-slate-500 text-sm mt-6">
|
||||
BreakPilot SBOM - 180+ Komponenten dokumentiert
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
1133
admin-v2/app/(admin)/infrastructure/security/page.tsx
Normal file
1133
admin-v2/app/(admin)/infrastructure/security/page.tsx
Normal file
File diff suppressed because it is too large
Load Diff
1560
admin-v2/app/(admin)/infrastructure/tests/page.tsx
Normal file
1560
admin-v2/app/(admin)/infrastructure/tests/page.tsx
Normal file
File diff suppressed because it is too large
Load Diff
192
admin-v2/app/(admin)/infrastructure/tests/types.ts
Normal file
192
admin-v2/app/(admin)/infrastructure/tests/types.ts
Normal file
@@ -0,0 +1,192 @@
|
||||
/**
|
||||
* TypeScript Types for Test Dashboard
|
||||
*/
|
||||
|
||||
export type TestFramework =
|
||||
| 'go_test'
|
||||
| 'pytest'
|
||||
| 'jest'
|
||||
| 'playwright'
|
||||
| 'bqas_golden'
|
||||
| 'bqas_rag'
|
||||
| 'bqas_synthetic'
|
||||
|
||||
export type TestCategory = 'unit' | 'integration' | 'e2e' | 'bqas' | 'security' | 'performance'
|
||||
|
||||
export type TestStatus = 'pending' | 'running' | 'passed' | 'failed' | 'skipped' | 'error'
|
||||
|
||||
export type RunStatus = 'queued' | 'running' | 'completed' | 'failed' | 'cancelled'
|
||||
|
||||
export interface TestCase {
|
||||
id: string
|
||||
name: string
|
||||
file_path: string
|
||||
line_number?: number
|
||||
framework: TestFramework
|
||||
category: TestCategory
|
||||
duration_ms?: number
|
||||
status: TestStatus
|
||||
error_message?: string
|
||||
output?: string
|
||||
}
|
||||
|
||||
export interface ServiceTestInfo {
|
||||
service: string
|
||||
display_name: string
|
||||
port?: number
|
||||
language: string
|
||||
total_tests: number
|
||||
passed_tests: number
|
||||
failed_tests: number
|
||||
skipped_tests: number
|
||||
pass_rate: number
|
||||
coverage_percent?: number
|
||||
last_run?: string
|
||||
status: TestStatus
|
||||
}
|
||||
|
||||
export interface TestRun {
|
||||
id: string
|
||||
suite_id: string
|
||||
service: string
|
||||
started_at: string
|
||||
completed_at?: string
|
||||
status: RunStatus
|
||||
total_tests: number
|
||||
passed_tests: number
|
||||
failed_tests: number
|
||||
skipped_tests: number
|
||||
duration_seconds: number
|
||||
git_commit?: string
|
||||
git_branch?: string
|
||||
coverage_percent?: number
|
||||
triggered_by: string
|
||||
output?: string
|
||||
failed_test_ids: string[]
|
||||
}
|
||||
|
||||
export interface TestRegistryStats {
|
||||
total_tests: number
|
||||
total_passed: number
|
||||
total_failed: number
|
||||
total_skipped: number
|
||||
overall_pass_rate: number
|
||||
average_coverage?: number
|
||||
services_count: number
|
||||
last_full_run?: string
|
||||
by_category: Record<string, number>
|
||||
by_framework: Record<string, number>
|
||||
}
|
||||
|
||||
export interface RegistryResponse {
|
||||
services: ServiceTestInfo[]
|
||||
stats: TestRegistryStats
|
||||
last_updated: string
|
||||
}
|
||||
|
||||
export interface CoverageData {
|
||||
service: string
|
||||
display_name: string
|
||||
coverage_percent: number
|
||||
language: string
|
||||
}
|
||||
|
||||
export interface CoverageResponse {
|
||||
services: CoverageData[]
|
||||
average_coverage: number
|
||||
total_services: number
|
||||
}
|
||||
|
||||
export type TabType = 'overview' | 'unit' | 'integration' | 'bqas' | 'history' | 'backlog' | 'guide'
|
||||
|
||||
export type BacklogStatus = 'open' | 'in_progress' | 'fixed' | 'wont_fix' | 'flaky'
|
||||
export type BacklogPriority = 'critical' | 'high' | 'medium' | 'low'
|
||||
|
||||
export interface FailedTest {
|
||||
id: string
|
||||
name: string
|
||||
service: string
|
||||
file_path: string
|
||||
line_number?: number
|
||||
error_message: string
|
||||
error_type: string
|
||||
suggestion: string
|
||||
run_id: string
|
||||
last_failed: string
|
||||
status: BacklogStatus
|
||||
}
|
||||
|
||||
export interface FailedTestsResponse {
|
||||
total_failed: number
|
||||
by_service: Record<string, FailedTest[]>
|
||||
tests: FailedTest[]
|
||||
last_updated: string
|
||||
}
|
||||
|
||||
// Neue PostgreSQL-basierte Backlog-Typen
|
||||
export interface BacklogItem {
|
||||
id: number
|
||||
test_name: string
|
||||
test_file: string | null
|
||||
service: string
|
||||
framework: string | null
|
||||
error_message: string | null
|
||||
error_type: string | null
|
||||
first_failed_at: string
|
||||
last_failed_at: string
|
||||
failure_count: number
|
||||
status: BacklogStatus
|
||||
priority: BacklogPriority
|
||||
assigned_to: string | null
|
||||
fix_suggestion: string | null
|
||||
notes: string | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
fixes?: FixAttempt[]
|
||||
}
|
||||
|
||||
export interface FixAttempt {
|
||||
id: number
|
||||
backlog_id: number
|
||||
fix_type: 'manual' | 'auto_claude' | 'auto_script'
|
||||
fix_description: string | null
|
||||
commit_hash: string | null
|
||||
success: boolean
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface BacklogResponse {
|
||||
total: number
|
||||
items: BacklogItem[]
|
||||
by_service: Record<string, BacklogItem[]>
|
||||
filters: {
|
||||
status: string | null
|
||||
service: string | null
|
||||
priority: string | null
|
||||
}
|
||||
pagination: {
|
||||
limit: number
|
||||
offset: number
|
||||
}
|
||||
}
|
||||
|
||||
export interface TrendDataPoint {
|
||||
date: string
|
||||
total_tests: number
|
||||
passed: number
|
||||
failed: number
|
||||
runs: number
|
||||
pass_rate: number
|
||||
}
|
||||
|
||||
export interface TrendsResponse {
|
||||
trends: TrendDataPoint[]
|
||||
days: number
|
||||
service: string | null
|
||||
}
|
||||
|
||||
export interface Toast {
|
||||
id: number
|
||||
type: 'success' | 'error' | 'info' | 'loading'
|
||||
message: string
|
||||
}
|
||||
Reference in New Issue
Block a user