Files
breakpilot-lehrer/website/app/admin/sbom/page.tsx
Benjamin Admin 451365a312 [split-required] Split remaining 500-680 LOC files (final batch)
website (17 pages + 3 components):
- multiplayer/wizard, middleware/wizard+test-wizard, communication
- builds/wizard, staff-search, voice, sbom/wizard
- foerderantrag, mail/tasks, tools/communication, sbom
- compliance/evidence, uni-crawler, brandbook (already done)
- CollectionsTab, IngestionTab, RiskHeatmap

backend-lehrer (5 files):
- letters_api (641 → 2), certificates_api (636 → 2)
- alerts_agent/db/models (636 → 3)
- llm_gateway/communication_service (614 → 2)
- game/database already done in prior batch

klausur-service (2 files):
- hybrid_vocab_extractor (664 → 2)
- klausur-service/frontend: api.ts (620 → 3), EHUploadWizard (591 → 2)

voice-service (3 files):
- bqas/rag_judge (618 → 3), runner (529 → 2)
- enhanced_task_orchestrator (519 → 2)

studio-v2 (6 files):
- korrektur/[klausurId] (578 → 4), fairness (569 → 2)
- AlertsWizard (552 → 2), OnboardingWizard (513 → 2)
- korrektur/api.ts (506 → 3), geo-lernwelt (501 → 2)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-25 08:56:45 +02:00

155 lines
7.4 KiB
TypeScript

'use client'
/**
* SBOM (Software Bill of Materials) Admin Page
*/
import { useState, useEffect } from 'react'
import AdminLayout from '@/components/admin/AdminLayout'
import {
Component,
SBOMData,
CategoryType,
INFRASTRUCTURE_COMPONENTS,
SECURITY_TOOLS,
PYTHON_PACKAGES,
GO_MODULES,
NODE_PACKAGES,
} from './_components/sbom-data'
import { SBOMTable } from './_components/SBOMTable'
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 BACKEND_URL = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8000'
useEffect(() => {
const loadSBOM = async () => {
setLoading(true)
try { const res = await fetch(`${BACKEND_URL}/api/v1/security/sbom`); if (res.ok) setSbomData(await res.json()) }
catch (error) { console.error('Failed to load SBOM:', error) }
finally { setLoading(false) }
}
loadSBOM()
}, [])
const getFilteredComponents = () => {
let components: Component[]
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 {
components = [
...INFRASTRUCTURE_COMPONENTS.map(c => ({ ...c, category: c.category || 'infrastructure' })),
...SECURITY_TOOLS.map(c => ({ ...c, category: c.category || 'security-tool' })),
...PYTHON_PACKAGES.map(c => ({ ...c, category: 'python' })),
...GO_MODULES.map(c => ({ ...c, category: 'go' })),
...NODE_PACKAGES.map(c => ({ ...c, category: 'nodejs' })),
...(sbomData?.components || []).map(c => ({ ...c, category: 'python' })),
]
}
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 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,
totalAll: INFRASTRUCTURE_COMPONENTS.length + SECURITY_TOOLS.length + PYTHON_PACKAGES.length + GO_MODULES.length + NODE_PACKAGES.length + (sbomData?.components?.length || 0),
databases: INFRASTRUCTURE_COMPONENTS.filter(c => c.category === 'database').length,
services: INFRASTRUCTURE_COMPONENTS.filter(c => c.category === 'application').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 },
]
return (
<AdminLayout title="SBOM" description="Software Bill of Materials - Alle Komponenten & Abhaengigkeiten">
{/* Stats Cards */}
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-8 gap-4 mb-6">
{[
{ v: stats.totalAll, l: 'Komponenten Total', c: 'text-slate-800' },
{ v: stats.totalInfra, l: 'Docker Services', c: 'text-purple-600' },
{ v: stats.totalSecurityTools, l: 'Security Tools', c: 'text-red-600' },
{ v: stats.totalPython, l: 'Python', c: 'text-emerald-600' },
{ v: stats.totalGo, l: 'Go', c: 'text-sky-600' },
{ v: stats.totalNode, l: 'Node.js', c: 'text-lime-600' },
{ v: stats.databases, l: 'Datenbanken', c: 'text-blue-600' },
{ v: stats.services, l: 'App Services', c: 'text-green-600' },
].map((s, i) => (
<div key={i} className="bg-white rounded-lg shadow p-4">
<div className={`text-3xl font-bold ${s.c}`}>{s.v}</div>
<div className="text-sm text-slate-500">{s.l}</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">
<div className="flex 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-primary-600 text-white' : 'bg-gray-100 text-gray-700 hover:bg-gray-200'}`}>
{cat.name} ({cat.count})
</button>
))}
</div>
<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-primary-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>
)}
<SBOMTable components={getFilteredComponents()} loading={loading} />
{/* 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-primary-600 text-white rounded-lg hover:bg-primary-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>
</AdminLayout>
)
}