@@ -670,186 +38,38 @@ export default function KorrekturWorkspacePage() {
return (
- {/* Top Navigation Bar */}
-
- {/* Back link */}
-
-
-
-
- Zurück
-
-
- {/* Student navigation */}
-
-
goToStudent('prev')}
- disabled={currentIndex <= 0}
- className="p-2 rounded-lg hover:bg-slate-100 disabled:opacity-50 disabled:cursor-not-allowed"
- >
-
-
-
-
-
- {currentIndex + 1} / {students.length}
-
-
goToStudent('next')}
- disabled={currentIndex >= students.length - 1}
- className="p-2 rounded-lg hover:bg-slate-100 disabled:opacity-50 disabled:cursor-not-allowed"
- >
-
-
-
-
-
-
- {/* Workflow status and role */}
-
- {/* Role badge */}
- {workflow && (
-
-
- {ROLE_LABELS[workflow.user_role]?.label || workflow.user_role}
-
-
- {/* Workflow status badge */}
-
- {WORKFLOW_STATUS_LABELS[workflow.workflow_status]?.label || workflow.workflow_status}
-
-
- {/* Visibility mode indicator for ZK */}
- {workflow.user_role === 'zk' && workflow.visibility_mode !== 'full' && (
-
- {workflow.visibility_mode === 'blind' ? 'Blind-Modus' : 'Semi-Blind'}
-
- )}
-
- )}
-
- {saving && (
-
-
- Speichern...
-
- )}
-
-
- {totals.gradePoints} Punkte
-
-
- Note: {GRADE_LABELS[totals.gradePoints] || '-'}
-
-
-
-
+
{/* Einigung Modal */}
- {showEinigungModal && workflow && (
-
-
-
Einigung erforderlich
-
- {/* Grade comparison */}
-
-
-
-
Erstkorrektor
-
- {workflow.first_result?.grade_points || '-'} P
-
-
-
-
Zweitkorrektor
-
- {workflow.second_result?.grade_points || '-'} P
-
-
-
-
- Differenz: {workflow.grade_difference} Punkte
-
-
-
- {/* Final grade selection */}
-
-
- Endnote festlegen
-
-
setEinigungGrade(parseInt(e.target.value))}
- className="w-full"
- />
-
- {einigungGrade} Punkte ({GRADE_LABELS[einigungGrade] || '-'})
-
-
-
- {/* Notes */}
-
-
- Begruendung
-
-
-
- {/* Actions */}
-
- submitEinigung('agreed')}
- disabled={submittingWorkflow || !einigungNotes}
- className="flex-1 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50"
- >
- Einigung bestaetigen
-
- submitEinigung('escalated')}
- disabled={submittingWorkflow}
- className="py-2 px-4 bg-red-100 text-red-700 rounded-lg hover:bg-red-200"
- >
- Eskalieren
-
- setShowEinigungModal(false)}
- className="py-2 px-4 bg-slate-100 text-slate-700 rounded-lg hover:bg-slate-200"
- >
- Abbrechen
-
-
-
-
+ {ws.showEinigungModal && ws.workflow && (
+
ws.setShowEinigungModal(false)}
+ />
)}
{/* Error display */}
- {error && (
+ {ws.error && (
-
{error}
-
setError(null)} className="ml-auto">
+ {ws.error}
+ ws.setError(null)} className="ml-auto">
@@ -859,461 +79,58 @@ export default function KorrekturWorkspacePage() {
{/* Main Layout: 2/3 - 1/3 */}
- {/* Left: Document Viewer (2/3) */}
-
- {/* Toolbar */}
-
+
{
+ ws.setSelectedAnnotation(ann)
+ ws.setActiveTab('annotationen')
+ }}
+ />
- {/* Document display with annotation overlay */}
-
- {documentUrl ? (
-
- {/* Document */}
- {student?.file_path?.endsWith('.pdf') ? (
-
- ) : (
-
-
{
- (e.target as HTMLImageElement).src = '/placeholder-document.png'
- }}
- />
- {/* Annotation Layer Overlay */}
-
ann.page === currentPage)}
- selectedTool={selectedTool}
- onCreateAnnotation={createAnnotation}
- onSelectAnnotation={(ann) => {
- setSelectedAnnotation(ann)
- setActiveTab('annotationen')
- }}
- selectedAnnotationId={selectedAnnotation?.id}
- />
-
- )}
-
- ) : (
-
- Kein Dokument verfuegbar
-
- )}
-
-
- {/* Page navigation (for multi-page documents) */}
-
-
setCurrentPage((p) => Math.max(1, p - 1))}
- disabled={currentPage <= 1}
- className="p-1 rounded hover:bg-slate-200 disabled:opacity-50"
- >
-
-
-
-
-
- Seite {currentPage} / {totalPages}
-
-
setCurrentPage((p) => Math.min(totalPages, p + 1))}
- disabled={currentPage >= totalPages}
- className="p-1 rounded hover:bg-slate-200 disabled:opacity-50"
- >
-
-
-
-
-
-
- {/* OCR Text (collapsible) */}
- {student?.ocr_text && (
-
-
- OCR-Text anzeigen
-
-
-
- )}
-
-
- {/* Right: Correction Panel (1/3) */}
-
- {/* Tabs */}
-
-
- {[
- { id: 'kriterien' as const, label: 'Kriterien' },
- { id: 'annotationen' as const, label: `Notizen (${annotations.length})` },
- { id: 'gutachten' as const, label: 'Gutachten' },
- { id: 'eh-vorschlaege' as const, label: 'EH' },
- ].map(tab => (
- setActiveTab(tab.id)}
- className={`flex-1 px-2 py-3 text-xs font-medium border-b-2 transition-colors ${
- activeTab === tab.id
- ? 'border-primary-500 text-primary-600'
- : 'border-transparent text-slate-500 hover:text-slate-700'
- }`}
- >
- {tab.label}
-
- ))}
-
-
-
- {/* Tab content */}
-
- {/* Kriterien Tab */}
- {activeTab === 'kriterien' && gradeInfo && (
-
- {Object.entries(gradeInfo.criteria || {}).map(([key, criterion]) => {
- const score = criteriaScores[key] || 0
- const linkedAnnotations = annotations.filter(
- (a) => a.linked_criterion === key || a.type === key
- )
- const errorCount = linkedAnnotations.length
- const severityCounts = {
- minor: linkedAnnotations.filter((a) => a.severity === 'minor').length,
- major: linkedAnnotations.filter((a) => a.severity === 'major').length,
- critical: linkedAnnotations.filter((a) => a.severity === 'critical').length,
- }
- const criterionColor = ANNOTATION_COLORS[key as AnnotationType] || '#6b7280'
-
- return (
-
-
-
-
-
{criterion.name}
-
({criterion.weight}%)
-
-
{score}%
-
-
- {/* Annotation count for this criterion */}
- {errorCount > 0 && (
-
- {errorCount} Markierungen:
- {severityCounts.minor > 0 && (
-
- {severityCounts.minor} leicht
-
- )}
- {severityCounts.major > 0 && (
-
- {severityCounts.major} mittel
-
- )}
- {severityCounts.critical > 0 && (
-
- {severityCounts.critical} schwer
-
- )}
-
- )}
-
- {/* Slider */}
-
handleCriteriaChange(key, parseInt(e.target.value))}
- className="w-full h-2 bg-slate-200 rounded-lg appearance-none cursor-pointer"
- style={{ accentColor: criterionColor }}
- />
-
- {/* Quick buttons */}
-
- {[0, 25, 50, 75, 100].map((val) => (
- handleCriteriaChange(key, val)}
- className={`flex-1 py-1 text-xs rounded transition-colors ${
- score === val
- ? 'text-white'
- : 'bg-slate-200 text-slate-600 hover:bg-slate-300'
- }`}
- style={score === val ? { backgroundColor: criterionColor } : undefined}
- >
- {val}%
-
- ))}
-
-
- {/* Quick add annotation button for RS/Grammatik */}
- {(key === 'rechtschreibung' || key === 'grammatik') && (
-
{
- setSelectedTool(key as AnnotationType)
- }}
- className="mt-2 w-full py-1 text-xs border rounded hover:bg-slate-100 flex items-center justify-center gap-1"
- style={{ borderColor: criterionColor, color: criterionColor }}
- >
-
-
-
- {key === 'rechtschreibung' ? 'RS-Fehler' : 'Grammatik-Fehler'} markieren
-
- )}
-
- )
- })}
-
- {/* Total and Generate button */}
-
-
-
Gesamtergebnis
-
-
- {totals.gradePoints} Punkte
-
-
- ({totals.weighted}%) - Note {GRADE_LABELS[totals.gradePoints]}
-
-
-
-
- {/* Workflow-aware action buttons */}
-
- {/* Generate Gutachten button */}
-
- {generatingGutachten ? (
- <>
-
- Generiere Gutachten...
- >
- ) : (
- <>
-
-
-
- Gutachten generieren
- >
- )}
-
-
- {/* Workflow action buttons based on status */}
- {(!workflow || workflow.workflow_status === 'not_started' || workflow.workflow_status === 'ek_in_progress') && (
-
- {submittingWorkflow ? (
- <>
-
- Wird abgeschlossen...
- >
- ) : (
- <>
-
-
-
- Erstkorrektur abschliessen
- >
- )}
-
- )}
-
- {workflow?.workflow_status === 'ek_completed' && workflow.user_role === 'ek' && (
-
{
- const zkId = prompt('Zweitkorrektor-ID eingeben:')
- if (zkId) startZweitkorrektur(zkId)
- }}
- disabled={submittingWorkflow}
- className="w-full py-3 bg-amber-600 text-white rounded-lg hover:bg-amber-700 disabled:opacity-50 flex items-center justify-center gap-2"
- >
-
-
-
- Zur Zweitkorrektur weiterleiten
-
- )}
-
- {(workflow?.workflow_status === 'zk_assigned' || workflow?.workflow_status === 'zk_in_progress') &&
- workflow?.user_role === 'zk' && (
-
- {submittingWorkflow ? (
- <>
-
- Wird abgeschlossen...
- >
- ) : (
- <>
-
-
-
- Zweitkorrektur abschliessen
- >
- )}
-
- )}
-
- {workflow?.workflow_status === 'einigung_required' && (
-
setShowEinigungModal(true)}
- className="w-full py-3 bg-orange-600 text-white rounded-lg hover:bg-orange-700 flex items-center justify-center gap-2"
- >
-
-
-
- Einigung starten
-
- )}
-
- {workflow?.workflow_status === 'completed' && (
-
-
- Endnote: {workflow.final_grade} Punkte
-
-
- ({GRADE_LABELS[workflow.final_grade || 0]}) - {workflow.consensus_type === 'auto' ? 'Auto-Konsens' : workflow.consensus_type === 'drittkorrektur' ? 'Drittkorrektur' : 'Einigung'}
-
-
- )}
-
- {/* Show EK/ZK comparison when both results exist */}
- {workflow?.first_result && workflow?.second_result && workflow?.workflow_status !== 'completed' && (
-
-
Notenvergleich
-
-
-
EK
-
{workflow.first_result.grade_points}P
-
-
-
ZK
-
{workflow.second_result.grade_points}P
-
-
-
Diff
-
= 4 ? 'text-red-600' : 'text-slate-700'}`}>
- {workflow.grade_difference}P
-
-
-
-
- )}
-
-
-
- )}
-
- {/* Annotationen Tab */}
- {activeTab === 'annotationen' && (
-
- )}
-
- {/* Gutachten Tab */}
- {activeTab === 'gutachten' && (
-
- )}
-
- {/* EH-Vorschlaege Tab */}
- {activeTab === 'eh-vorschlaege' && (
-
- {
- // Append suggestion to gutachten
- setGutachten((prev) =>
- prev
- ? `${prev}\n\n[${criterion.toUpperCase()}]: ${text}`
- : `[${criterion.toUpperCase()}]: ${text}`
- )
- setActiveTab('gutachten')
- }}
- />
-
- )}
-
-
+
ws.setShowEinigungModal(true)}
+ />
)
diff --git a/admin-lehrer/app/(admin)/education/klausur-korrektur/_components/DirektuploadTab.tsx b/admin-lehrer/app/(admin)/education/klausur-korrektur/_components/DirektuploadTab.tsx
new file mode 100644
index 0000000..b3d78a9
--- /dev/null
+++ b/admin-lehrer/app/(admin)/education/klausur-korrektur/_components/DirektuploadTab.tsx
@@ -0,0 +1,337 @@
+'use client'
+
+/**
+ * DirektuploadTab - 3-step direct upload wizard
+ */
+
+import type { TabId } from './constants'
+import type { DirektuploadForm } from './types'
+
+interface DirektuploadTabProps {
+ direktForm: DirektuploadForm
+ setDirektForm: React.Dispatch>
+ direktStep: 1 | 2 | 3
+ setDirektStep: React.Dispatch>
+ uploading: boolean
+ onUpload: () => void
+ onNavigate: (tab: TabId) => void
+}
+
+export function DirektuploadTab({
+ direktForm,
+ setDirektForm,
+ direktStep,
+ setDirektStep,
+ uploading,
+ onUpload,
+ onNavigate,
+}: DirektuploadTabProps) {
+ return (
+
+
+
onNavigate('willkommen')}
+ />
+
+
+ {direktStep === 1 && (
+ setDirektForm(prev => ({ ...prev, files }))}
+ onNext={() => setDirektStep(2)}
+ />
+ )}
+ {direktStep === 2 && (
+ setDirektForm(prev => ({ ...prev, aufgabentyp: v }))}
+ onEhTextChange={(v) => setDirektForm(prev => ({ ...prev, ehText: v }))}
+ onBack={() => setDirektStep(1)}
+ onNext={() => setDirektStep(3)}
+ />
+ )}
+ {direktStep === 3 && (
+ setDirektStep(2)}
+ onUpload={onUpload}
+ />
+ )}
+
+
+
+ )
+}
+
+function StepHeader({ currentStep, onCancel }: { currentStep: number; onCancel: () => void }) {
+ return (
+
+
+
Schnellstart - Direkt Korrigieren
+
+ Abbrechen
+
+
+
+ {[1, 2, 3].map((step) => (
+
+
= step ? 'bg-blue-600 text-white' : 'bg-slate-200 text-slate-500'
+ }`}>
+ {step}
+
+
= step ? 'text-slate-800' : 'text-slate-400'}`}>
+ {step === 1 ? 'Arbeiten' : step === 2 ? 'Erwartungshorizont' : 'Starten'}
+
+ {step < 3 &&
step ? 'bg-blue-600' : 'bg-slate-200'}`} />}
+
+ ))}
+
+
+ )
+}
+
+function FileUploadStep({
+ files,
+ onFilesChange,
+ onNext,
+}: {
+ files: File[]
+ onFilesChange: (files: File[]) => void
+ onNext: () => void
+}) {
+ return (
+
+
+
Schuelerarbeiten hochladen
+
+ Laden Sie die eingescannten Klausuren hoch. Unterstuetzte Formate: PDF, JPG, PNG.
+
+
+
0 ? 'border-green-300 bg-green-50' : 'border-slate-300 hover:border-blue-400 hover:bg-blue-50'
+ }`}
+ onDrop={(e) => {
+ e.preventDefault()
+ const dropped = Array.from(e.dataTransfer.files)
+ onFilesChange([...files, ...dropped])
+ }}
+ onDragOver={(e) => e.preventDefault()}
+ >
+
+
+
+
Dateien hier ablegen oder
+
+ Dateien auswaehlen
+ {
+ const selected = Array.from(e.target.files || [])
+ onFilesChange([...files, ...selected])
+ }}
+ />
+
+
+
+ {files.length > 0 && (
+
+
+ {files.length} Datei{files.length !== 1 ? 'en' : ''} ausgewaehlt
+ onFilesChange([])}
+ className="text-red-600 hover:text-red-700"
+ >
+ Alle entfernen
+
+
+
+ {files.map((file, idx) => (
+
+
{file.name}
+
onFilesChange(files.filter((_, i) => i !== idx))}
+ className="text-slate-400 hover:text-red-600"
+ >
+
+
+
+
+
+ ))}
+
+
+ )}
+
+
+
+
+ Weiter
+
+
+
+ )
+}
+
+function EHStep({
+ aufgabentyp,
+ ehText,
+ onAufgabentypChange,
+ onEhTextChange,
+ onBack,
+ onNext,
+}: {
+ aufgabentyp: string
+ ehText: string
+ onAufgabentypChange: (v: string) => void
+ onEhTextChange: (v: string) => void
+ onBack: () => void
+ onNext: () => void
+}) {
+ return (
+
+
+
Erwartungshorizont (optional)
+
+ Beschreiben Sie die Aufgabenstellung fuer bessere KI-Vorschlaege.
+
+
+
+ Aufgabentyp
+ onAufgabentypChange(e.target.value)}
+ className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
+ >
+ -- Waehlen Sie einen Aufgabentyp --
+ Textanalyse (Sachtexte)
+ Gedichtanalyse
+ Prosaanalyse
+ Dramenanalyse
+ Textgebundene Eroerterung
+
+
+
+
+
+ Aufgabenstellung / Erwartungshorizont
+
+
+
+
+
+
+ Zurueck
+
+
+ Weiter
+
+
+
+ )
+}
+
+function SummaryStep({
+ direktForm,
+ setDirektForm,
+ uploading,
+ onBack,
+ onUpload,
+}: {
+ direktForm: DirektuploadForm
+ setDirektForm: React.Dispatch
>
+ uploading: boolean
+ onBack: () => void
+ onUpload: () => void
+}) {
+ return (
+
+
+
Zusammenfassung
+
+
+
+ Titel
+ setDirektForm(prev => ({ ...prev, klausurTitle: e.target.value }))}
+ className="text-sm font-medium text-slate-800 bg-white border border-slate-200 rounded px-2 py-1 text-right"
+ />
+
+
+ Anzahl Arbeiten
+ {direktForm.files.length}
+
+
+ Aufgabentyp
+
+ {direktForm.aufgabentyp || 'Nicht angegeben'}
+
+
+
+
+
+
+
+
+
+
+
Was passiert jetzt?
+
+ Eine neue Klausur wird automatisch erstellt
+ Alle {direktForm.files.length} Arbeiten werden hochgeladen
+ OCR-Erkennung startet automatisch
+ Sie werden zur Korrektur-Ansicht weitergeleitet
+
+
+
+
+
+
+
+
+ Zurueck
+
+
+ {uploading ? (
+ <>
+
+ Wird hochgeladen...
+ >
+ ) : (
+ <>
+
+
+
+ Korrektur starten
+ >
+ )}
+
+
+
+ )
+}
diff --git a/admin-lehrer/app/(admin)/education/klausur-korrektur/_components/ErstellenTab.tsx b/admin-lehrer/app/(admin)/education/klausur-korrektur/_components/ErstellenTab.tsx
new file mode 100644
index 0000000..6330696
--- /dev/null
+++ b/admin-lehrer/app/(admin)/education/klausur-korrektur/_components/ErstellenTab.tsx
@@ -0,0 +1,227 @@
+'use client'
+
+/**
+ * ErstellenTab - Form to create a new Klausur
+ */
+
+import type { TabId } from './constants'
+import type { CreateKlausurForm, VorabiturEHForm, EHTemplate } from './types'
+
+interface ErstellenTabProps {
+ form: CreateKlausurForm
+ setForm: React.Dispatch>
+ ehForm: VorabiturEHForm
+ setEhForm: React.Dispatch>
+ templates: EHTemplate[]
+ loadingTemplates: boolean
+ creating: boolean
+ onSubmit: (e: React.FormEvent) => void
+ onNavigate: (tab: TabId) => void
+}
+
+export function ErstellenTab({
+ form,
+ setForm,
+ ehForm,
+ setEhForm,
+ templates,
+ loadingTemplates,
+ creating,
+ onSubmit,
+ onNavigate,
+}: ErstellenTabProps) {
+ return (
+
+
+
Neue Klausur erstellen
+
+
+
+
+ )
+}
+
+function VorabiturEHSection({
+ ehForm,
+ setEhForm,
+ templates,
+ loadingTemplates,
+}: {
+ ehForm: VorabiturEHForm
+ setEhForm: React.Dispatch>
+ templates: EHTemplate[]
+ loadingTemplates: boolean
+}) {
+ return (
+
+
+
+
+
+
+
Eigenen Erwartungshorizont erstellen
+
Waehlen Sie einen Aufgabentyp und beschreiben Sie die Aufgabenstellung.
+
+
+
+
+
Aufgabentyp *
+ {loadingTemplates ? (
+
+ ) : (
+
setEhForm(prev => ({ ...prev, aufgabentyp: e.target.value }))}
+ className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent bg-white"
+ >
+ -- Aufgabentyp waehlen --
+ {templates.map(t => (
+ {t.name}
+ ))}
+
+ )}
+
+
+
+
+
+ Aufgabenstellung *
+
+
+ )
+}
diff --git a/admin-lehrer/app/(admin)/education/klausur-korrektur/_components/KlausurenTab.tsx b/admin-lehrer/app/(admin)/education/klausur-korrektur/_components/KlausurenTab.tsx
new file mode 100644
index 0000000..3bbfc34
--- /dev/null
+++ b/admin-lehrer/app/(admin)/education/klausur-korrektur/_components/KlausurenTab.tsx
@@ -0,0 +1,138 @@
+'use client'
+
+/**
+ * KlausurenTab - List of all Klausuren with progress and actions
+ */
+
+import Link from 'next/link'
+import type { Klausur } from '../types'
+import type { TabId } from './constants'
+
+interface KlausurenTabProps {
+ klausuren: Klausur[]
+ loading: boolean
+ onDelete: (id: string) => void
+ onNavigate: (tab: TabId) => void
+}
+
+export function KlausurenTab({ klausuren, loading, onDelete, onNavigate }: KlausurenTabProps) {
+ return (
+
+
+
+
Alle Klausuren
+
{klausuren.length} Klausuren insgesamt
+
+
onNavigate('erstellen')}
+ className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 flex items-center gap-2"
+ >
+
+
+
+ Neue Klausur
+
+
+
+ {loading ? (
+
+ ) : klausuren.length === 0 ? (
+
+
+
+
+
Keine Klausuren
+
Erstellen Sie Ihre erste Klausur zum Korrigieren.
+
onNavigate('erstellen')}
+ className="mt-4 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700"
+ >
+ Klausur erstellen
+
+
+ ) : (
+
+ {klausuren.map((klausur) => (
+
+ ))}
+
+ )}
+
+ )
+}
+
+function KlausurCard({ klausur, onDelete }: { klausur: Klausur; onDelete: (id: string) => void }) {
+ const progressPct = (klausur.student_count || 0) > 0
+ ? ((klausur.completed_count || 0) / (klausur.student_count || 1)) * 100
+ : 0
+
+ return (
+
+
+
+
+
{klausur.title}
+
{klausur.subject} - {klausur.year}
+
+
+ {klausur.modus === 'abitur' ? 'Abitur' : 'Vorabitur'}
+
+
+
+
+
+
+
+
+
{klausur.student_count || 0} Arbeiten
+
+
+
+
+
+
{klausur.completed_count || 0} fertig
+
+
+
+ {(klausur.student_count || 0) > 0 && (
+
+
+ Fortschritt
+ {Math.round(progressPct)}%
+
+
+
+ )}
+
+
+
+ Korrigieren
+
+
onDelete(klausur.id)}
+ className="px-3 py-2 text-red-600 hover:bg-red-50 rounded-lg"
+ title="Loeschen"
+ >
+
+
+
+
+
+
+
+ )
+}
diff --git a/admin-lehrer/app/(admin)/education/klausur-korrektur/_components/StatistikenTab.tsx b/admin-lehrer/app/(admin)/education/klausur-korrektur/_components/StatistikenTab.tsx
new file mode 100644
index 0000000..8ab5fdc
--- /dev/null
+++ b/admin-lehrer/app/(admin)/education/klausur-korrektur/_components/StatistikenTab.tsx
@@ -0,0 +1,62 @@
+'use client'
+
+/**
+ * StatistikenTab - Correction statistics overview
+ */
+
+import type { Klausur, GradeInfo } from '../types'
+
+interface StatistikenTabProps {
+ klausuren: Klausur[]
+ gradeInfo: GradeInfo | null
+}
+
+export function StatistikenTab({ klausuren, gradeInfo }: StatistikenTabProps) {
+ const totalStudents = klausuren.reduce((sum, k) => sum + (k.student_count || 0), 0)
+ const totalCompleted = klausuren.reduce((sum, k) => sum + (k.completed_count || 0), 0)
+ const totalPending = totalStudents - totalCompleted
+
+ return (
+
+
Korrektur-Statistiken
+
+
+
+
+
+
+
+
+ {gradeInfo && (
+
+
Bewertungskriterien (Niedersachsen)
+
+ {Object.entries(gradeInfo.criteria || {}).map(([key, criterion]) => (
+
+
{criterion.weight}%
+
{criterion.name}
+
+ ))}
+
+
+ )}
+
+ )
+}
+
+function StatCard({
+ value,
+ label,
+ className,
+}: {
+ value: number
+ label: string
+ className?: string
+}) {
+ return (
+
+ )
+}
diff --git a/admin-lehrer/app/(admin)/education/klausur-korrektur/_components/WillkommenTab.tsx b/admin-lehrer/app/(admin)/education/klausur-korrektur/_components/WillkommenTab.tsx
new file mode 100644
index 0000000..22c4c69
--- /dev/null
+++ b/admin-lehrer/app/(admin)/education/klausur-korrektur/_components/WillkommenTab.tsx
@@ -0,0 +1,132 @@
+'use client'
+
+/**
+ * WillkommenTab - Welcome page with onboarding options
+ */
+
+import type { Klausur } from '../types'
+import type { TabId } from './constants'
+
+interface WillkommenTabProps {
+ klausuren: Klausur[]
+ onNavigate: (tab: TabId) => void
+ markAsVisited: () => void
+}
+
+export function WillkommenTab({ klausuren, onNavigate, markAsVisited }: WillkommenTabProps) {
+ return (
+
+
+
+
Willkommen zur Abiturklausur-Korrektur
+
+ KI-gestuetzte Korrektur fuer Deutsch-Abiturklausuren nach dem 15-Punkte-System.
+ Sparen Sie bis zu 80% Zeit bei der Erstkorrektur.
+
+
+
+
+
+
+
{ markAsVisited(); onNavigate('erstellen'); }}
+ >
+
+
+
+
Neue Klausur erstellen
+
+ Empfohlen fuer regelmaessige Nutzung. Erstellen Sie eine Klausur mit allen Metadaten.
+
+
+ Klausur erstellen
+
+
+
+
+
+
+
+
+
{ markAsVisited(); onNavigate('direktupload'); }}
+ >
+
+
+
+
Schnellstart - Direkt hochladen
+
+ Ideal wenn Sie sofort loslegen moechten. Drag & Drop Upload.
+
+
+
+
+
+
+
+ {klausuren.length > 0 && (
+
+
+
Sie haben {klausuren.length} Klausur{klausuren.length !== 1 ? 'en' : ''}
+
Setzen Sie Ihre Arbeit fort oder starten Sie eine neue Korrektur.
+
+
{ markAsVisited(); onNavigate('klausuren'); }}
+ className="px-4 py-2 bg-slate-800 text-white rounded-lg hover:bg-slate-700 text-sm"
+ >
+ Zu meinen Klausuren
+
+
+ )}
+
+ )
+}
+
+function HowItWorksSection() {
+ const steps = [
+ { step: 1, title: 'Arbeiten hochladen', desc: 'Scans der Schuelerarbeiten hochladen', emoji: '📤' },
+ { step: 2, title: 'EH bereitstellen', desc: 'Erwartungshorizont hochladen oder erstellen', emoji: '📋' },
+ { step: 3, title: 'KI korrigiert', desc: 'Automatische Bewertung erhalten', emoji: '🤖' },
+ { step: 4, title: 'Pruefen & Anpassen', desc: 'Vorschlaege pruefen und finalisieren', emoji: '✅' },
+ ]
+
+ return (
+
+
+
+
+
+ So funktioniert es
+
+
+ {steps.map(({ step, title, desc, emoji }) => (
+
+
{emoji}
+
Schritt {step}
+
{title}
+
{desc}
+
+ ))}
+
+
+ )
+}
diff --git a/admin-lehrer/app/(admin)/education/klausur-korrektur/_components/constants.tsx b/admin-lehrer/app/(admin)/education/klausur-korrektur/_components/constants.tsx
new file mode 100644
index 0000000..34d84a4
--- /dev/null
+++ b/admin-lehrer/app/(admin)/education/klausur-korrektur/_components/constants.tsx
@@ -0,0 +1,58 @@
+/**
+ * Constants for Klausur-Korrektur page
+ */
+
+// API Base URL for klausur-service (same-origin proxy to avoid CORS)
+export const API_BASE = '/klausur-api'
+
+// Tab definitions
+export type TabId = 'willkommen' | 'klausuren' | 'erstellen' | 'direktupload' | 'statistiken'
+
+export const tabs: { id: TabId; name: string; icon: JSX.Element; hidden?: boolean }[] = [
+ {
+ id: 'willkommen',
+ name: 'Start',
+ icon: (
+
+
+
+ ),
+ },
+ {
+ id: 'klausuren',
+ name: 'Klausuren',
+ icon: (
+
+
+
+ ),
+ },
+ {
+ id: 'erstellen',
+ name: 'Neue Klausur',
+ icon: (
+
+
+
+ ),
+ },
+ {
+ id: 'direktupload',
+ name: 'Schnellstart',
+ hidden: true,
+ icon: (
+
+
+
+ ),
+ },
+ {
+ id: 'statistiken',
+ name: 'Statistiken',
+ icon: (
+
+
+
+ ),
+ },
+]
diff --git a/admin-lehrer/app/(admin)/education/klausur-korrektur/_components/types.ts b/admin-lehrer/app/(admin)/education/klausur-korrektur/_components/types.ts
new file mode 100644
index 0000000..1820fe7
--- /dev/null
+++ b/admin-lehrer/app/(admin)/education/klausur-korrektur/_components/types.ts
@@ -0,0 +1,34 @@
+/**
+ * Local form types for Klausur-Korrektur page
+ */
+
+export interface CreateKlausurForm {
+ title: string
+ subject: string
+ year: number
+ semester: string
+ modus: 'abitur' | 'vorabitur'
+}
+
+export interface VorabiturEHForm {
+ aufgabentyp: string
+ titel: string
+ text_titel: string
+ text_autor: string
+ aufgabenstellung: string
+}
+
+export interface EHTemplate {
+ aufgabentyp: string
+ name: string
+ description: string
+ category: string
+}
+
+export interface DirektuploadForm {
+ files: File[]
+ ehFile: File | null
+ ehText: string
+ aufgabentyp: string
+ klausurTitle: string
+}
diff --git a/admin-lehrer/app/(admin)/education/klausur-korrektur/_components/useKlausurKorrektur.ts b/admin-lehrer/app/(admin)/education/klausur-korrektur/_components/useKlausurKorrektur.ts
new file mode 100644
index 0000000..825832c
--- /dev/null
+++ b/admin-lehrer/app/(admin)/education/klausur-korrektur/_components/useKlausurKorrektur.ts
@@ -0,0 +1,322 @@
+'use client'
+
+/**
+ * Custom hook for Klausur-Korrektur page state and handlers
+ */
+
+import { useState, useEffect, useCallback } from 'react'
+import type { Klausur, GradeInfo } from '../types'
+import type { TabId } from './constants'
+import type {
+ CreateKlausurForm,
+ VorabiturEHForm,
+ EHTemplate,
+ DirektuploadForm,
+} from './types'
+import { API_BASE } from './constants'
+
+export function useKlausurKorrektur() {
+ const [activeTab, setActiveTab] = useState(() => {
+ if (typeof window !== 'undefined') {
+ const hasVisited = localStorage.getItem('klausur_korrektur_visited')
+ return hasVisited ? 'klausuren' : 'willkommen'
+ }
+ return 'willkommen'
+ })
+ const [klausuren, setKlausuren] = useState([])
+ const [loading, setLoading] = useState(true)
+ const [error, setError] = useState(null)
+ const [creating, setCreating] = useState(false)
+ const [gradeInfo, setGradeInfo] = useState(null)
+ const [templates, setTemplates] = useState([])
+ const [loadingTemplates, setLoadingTemplates] = useState(false)
+
+ const [form, setForm] = useState({
+ title: '',
+ subject: 'Deutsch',
+ year: new Date().getFullYear(),
+ semester: 'Abitur',
+ modus: 'abitur',
+ })
+
+ const [ehForm, setEhForm] = useState({
+ aufgabentyp: '',
+ titel: '',
+ text_titel: '',
+ text_autor: '',
+ aufgabenstellung: '',
+ })
+
+ const [direktForm, setDirektForm] = useState({
+ files: [],
+ ehFile: null,
+ ehText: '',
+ aufgabentyp: '',
+ klausurTitle: `Schnellkorrektur ${new Date().toLocaleDateString('de-DE')}`,
+ })
+ const [direktStep, setDirektStep] = useState<1 | 2 | 3>(1)
+ const [uploading, setUploading] = useState(false)
+
+ const fetchKlausuren = useCallback(async () => {
+ try {
+ setLoading(true)
+ const res = await fetch(`${API_BASE}/api/v1/klausuren`)
+ if (res.ok) {
+ const data = await res.json()
+ setKlausuren(Array.isArray(data) ? data : data.klausuren || [])
+ setError(null)
+ } else {
+ setError(`Fehler beim Laden: ${res.status}`)
+ }
+ } catch (err) {
+ console.error('Failed to fetch klausuren:', err)
+ setError('Verbindung zum Klausur-Service fehlgeschlagen')
+ } finally {
+ setLoading(false)
+ }
+ }, [])
+
+ const fetchGradeInfo = useCallback(async () => {
+ try {
+ const res = await fetch(`${API_BASE}/api/v1/grade-info`)
+ if (res.ok) {
+ const data = await res.json()
+ setGradeInfo(data)
+ }
+ } catch (err) {
+ console.error('Failed to fetch grade info:', err)
+ }
+ }, [])
+
+ const fetchTemplates = useCallback(async () => {
+ try {
+ setLoadingTemplates(true)
+ const res = await fetch(`${API_BASE}/api/v1/vorabitur/templates`)
+ if (res.ok) {
+ const data = await res.json()
+ setTemplates(data.templates || [])
+ }
+ } catch (err) {
+ console.error('Failed to fetch templates:', err)
+ } finally {
+ setLoadingTemplates(false)
+ }
+ }, [])
+
+ useEffect(() => {
+ fetchKlausuren()
+ fetchGradeInfo()
+ }, [fetchKlausuren, fetchGradeInfo])
+
+ useEffect(() => {
+ if (form.modus === 'vorabitur' && templates.length === 0) {
+ fetchTemplates()
+ }
+ }, [form.modus, templates.length, fetchTemplates])
+
+ const handleCreateKlausur = async (e: React.FormEvent) => {
+ e.preventDefault()
+ if (!form.title.trim()) {
+ setError('Bitte einen Titel eingeben')
+ return
+ }
+
+ if (form.modus === 'vorabitur') {
+ if (!ehForm.aufgabentyp) {
+ setError('Bitte einen Aufgabentyp auswaehlen')
+ return
+ }
+ if (!ehForm.aufgabenstellung.trim()) {
+ setError('Bitte die Aufgabenstellung eingeben')
+ return
+ }
+ }
+
+ try {
+ setCreating(true)
+
+ const res = await fetch(`${API_BASE}/api/v1/klausuren`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(form),
+ })
+
+ if (!res.ok) {
+ const errorData = await res.json()
+ setError(errorData.detail || 'Fehler beim Erstellen')
+ return
+ }
+
+ const newKlausur = await res.json()
+
+ if (form.modus === 'vorabitur') {
+ const ehRes = await fetch(`${API_BASE}/api/v1/klausuren/${newKlausur.id}/vorabitur-eh`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ aufgabentyp: ehForm.aufgabentyp,
+ titel: ehForm.titel || `EH: ${form.title}`,
+ text_titel: ehForm.text_titel || null,
+ text_autor: ehForm.text_autor || null,
+ aufgabenstellung: ehForm.aufgabenstellung,
+ }),
+ })
+
+ if (!ehRes.ok) {
+ console.error('Failed to create EH:', await ehRes.text())
+ setError('Klausur erstellt, aber Erwartungshorizont konnte nicht erstellt werden.')
+ }
+ }
+
+ setKlausuren(prev => [newKlausur, ...prev])
+ setForm({
+ title: '',
+ subject: 'Deutsch',
+ year: new Date().getFullYear(),
+ semester: 'Abitur',
+ modus: 'abitur',
+ })
+ setEhForm({
+ aufgabentyp: '',
+ titel: '',
+ text_titel: '',
+ text_autor: '',
+ aufgabenstellung: '',
+ })
+ setActiveTab('klausuren')
+ if (!error) setError(null)
+ } catch (err) {
+ console.error('Failed to create klausur:', err)
+ setError('Fehler beim Erstellen der Klausur')
+ } finally {
+ setCreating(false)
+ }
+ }
+
+ const handleDeleteKlausur = async (id: string) => {
+ if (!confirm('Klausur wirklich loeschen? Alle Studentenarbeiten werden ebenfalls geloescht.')) {
+ return
+ }
+
+ try {
+ const res = await fetch(`${API_BASE}/api/v1/klausuren/${id}`, {
+ method: 'DELETE',
+ })
+
+ if (res.ok) {
+ setKlausuren(prev => prev.filter(k => k.id !== id))
+ } else {
+ setError('Fehler beim Loeschen')
+ }
+ } catch (err) {
+ console.error('Failed to delete klausur:', err)
+ setError('Fehler beim Loeschen der Klausur')
+ }
+ }
+
+ const markAsVisited = () => {
+ if (typeof window !== 'undefined') {
+ localStorage.setItem('klausur_korrektur_visited', 'true')
+ }
+ }
+
+ const handleDirektupload = async () => {
+ if (direktForm.files.length === 0) {
+ setError('Bitte mindestens eine Arbeit hochladen')
+ return
+ }
+
+ try {
+ setUploading(true)
+
+ const klausurRes = await fetch(`${API_BASE}/api/v1/klausuren`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ title: direktForm.klausurTitle,
+ subject: 'Deutsch',
+ year: new Date().getFullYear(),
+ semester: 'Vorabitur',
+ modus: 'vorabitur',
+ }),
+ })
+
+ if (!klausurRes.ok) {
+ const err = await klausurRes.json()
+ throw new Error(err.detail || 'Klausur erstellen fehlgeschlagen')
+ }
+
+ const newKlausur = await klausurRes.json()
+
+ if (direktForm.ehText.trim() || direktForm.aufgabentyp) {
+ const ehRes = await fetch(`${API_BASE}/api/v1/klausuren/${newKlausur.id}/vorabitur-eh`, {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ aufgabentyp: direktForm.aufgabentyp || 'textanalyse_pragmatisch',
+ titel: `EH: ${direktForm.klausurTitle}`,
+ aufgabenstellung: direktForm.ehText || 'Individuelle Aufgabenstellung',
+ }),
+ })
+
+ if (!ehRes.ok) {
+ console.error('EH creation failed, continuing with upload')
+ }
+ }
+
+ for (let i = 0; i < direktForm.files.length; i++) {
+ const file = direktForm.files[i]
+ const formData = new FormData()
+ formData.append('file', file)
+ formData.append('anonym_id', `Arbeit-${i + 1}`)
+
+ const uploadRes = await fetch(`${API_BASE}/api/v1/klausuren/${newKlausur.id}/students`, {
+ method: 'POST',
+ body: formData,
+ })
+
+ if (!uploadRes.ok) {
+ console.error(`Upload failed for file ${i + 1}:`, file.name)
+ }
+ }
+
+ setKlausuren(prev => [newKlausur, ...prev])
+ markAsVisited()
+ window.location.href = `/education/klausur-korrektur/${newKlausur.id}`
+
+ } catch (err) {
+ console.error('Direktupload failed:', err)
+ setError(err instanceof Error ? err.message : 'Fehler beim Direktupload')
+ } finally {
+ setUploading(false)
+ }
+ }
+
+ return {
+ // State
+ activeTab,
+ setActiveTab,
+ klausuren,
+ loading,
+ error,
+ setError,
+ creating,
+ gradeInfo,
+ templates,
+ loadingTemplates,
+ form,
+ setForm,
+ ehForm,
+ setEhForm,
+ direktForm,
+ setDirektForm,
+ direktStep,
+ setDirektStep,
+ uploading,
+ // Handlers
+ handleCreateKlausur,
+ handleDeleteKlausur,
+ handleDirektupload,
+ markAsVisited,
+ }
+}
diff --git a/admin-lehrer/app/(admin)/education/klausur-korrektur/page.tsx b/admin-lehrer/app/(admin)/education/klausur-korrektur/page.tsx
index a3829ea..3beaac5 100644
--- a/admin-lehrer/app/(admin)/education/klausur-korrektur/page.tsx
+++ b/admin-lehrer/app/(admin)/education/klausur-korrektur/page.tsx
@@ -7,1060 +7,40 @@
* Zeigt alle Klausuren und ermoeglicht das Erstellen neuer Klausuren.
*/
-import { useState, useEffect, useCallback } from 'react'
-import Link from 'next/link'
-import type { Klausur, GradeInfo } from './types'
-
-// API Base URL for klausur-service (same-origin proxy to avoid CORS)
-const API_BASE = '/klausur-api'
-
-// Tab definitions
-type TabId = 'willkommen' | 'klausuren' | 'erstellen' | 'direktupload' | 'statistiken'
-
-const tabs: { id: TabId; name: string; icon: JSX.Element; hidden?: boolean }[] = [
- {
- id: 'willkommen',
- name: 'Start',
- icon: (
-
-
-
- ),
- },
- {
- id: 'klausuren',
- name: 'Klausuren',
- icon: (
-
-
-
- ),
- },
- {
- id: 'erstellen',
- name: 'Neue Klausur',
- icon: (
-
-
-
- ),
- },
- {
- id: 'direktupload',
- name: 'Schnellstart',
- hidden: true,
- icon: (
-
-
-
- ),
- },
- {
- id: 'statistiken',
- name: 'Statistiken',
- icon: (
-
-
-
- ),
- },
-]
-
-interface CreateKlausurForm {
- title: string
- subject: string
- year: number
- semester: string
- modus: 'abitur' | 'vorabitur'
-}
-
-interface VorabiturEHForm {
- aufgabentyp: string
- titel: string
- text_titel: string
- text_autor: string
- aufgabenstellung: string
-}
-
-interface EHTemplate {
- aufgabentyp: string
- name: string
- description: string
- category: string
-}
-
-interface DirektuploadForm {
- files: File[]
- ehFile: File | null
- ehText: string
- aufgabentyp: string
- klausurTitle: string
-}
+import { tabs } from './_components/constants'
+import { useKlausurKorrektur } from './_components/useKlausurKorrektur'
+import { KlausurenTab } from './_components/KlausurenTab'
+import { ErstellenTab } from './_components/ErstellenTab'
+import { WillkommenTab } from './_components/WillkommenTab'
+import { DirektuploadTab } from './_components/DirektuploadTab'
+import { StatistikenTab } from './_components/StatistikenTab'
export default function KlausurKorrekturPage() {
- const [activeTab, setActiveTab] = useState(() => {
- if (typeof window !== 'undefined') {
- const hasVisited = localStorage.getItem('klausur_korrektur_visited')
- return hasVisited ? 'klausuren' : 'willkommen'
- }
- return 'willkommen'
- })
- const [klausuren, setKlausuren] = useState([])
- const [loading, setLoading] = useState(true)
- const [error, setError] = useState(null)
- const [creating, setCreating] = useState(false)
- const [gradeInfo, setGradeInfo] = useState(null)
- const [templates, setTemplates] = useState([])
- const [loadingTemplates, setLoadingTemplates] = useState(false)
-
- const [form, setForm] = useState({
- title: '',
- subject: 'Deutsch',
- year: new Date().getFullYear(),
- semester: 'Abitur',
- modus: 'abitur',
- })
-
- const [ehForm, setEhForm] = useState({
- aufgabentyp: '',
- titel: '',
- text_titel: '',
- text_autor: '',
- aufgabenstellung: '',
- })
-
- const [direktForm, setDirektForm] = useState({
- files: [],
- ehFile: null,
- ehText: '',
- aufgabentyp: '',
- klausurTitle: `Schnellkorrektur ${new Date().toLocaleDateString('de-DE')}`,
- })
- const [direktStep, setDirektStep] = useState<1 | 2 | 3>(1)
- const [uploading, setUploading] = useState(false)
-
- const fetchKlausuren = useCallback(async () => {
- try {
- setLoading(true)
- const res = await fetch(`${API_BASE}/api/v1/klausuren`)
- if (res.ok) {
- const data = await res.json()
- setKlausuren(Array.isArray(data) ? data : data.klausuren || [])
- setError(null)
- } else {
- setError(`Fehler beim Laden: ${res.status}`)
- }
- } catch (err) {
- console.error('Failed to fetch klausuren:', err)
- setError('Verbindung zum Klausur-Service fehlgeschlagen')
- } finally {
- setLoading(false)
- }
- }, [])
-
- const fetchGradeInfo = useCallback(async () => {
- try {
- const res = await fetch(`${API_BASE}/api/v1/grade-info`)
- if (res.ok) {
- const data = await res.json()
- setGradeInfo(data)
- }
- } catch (err) {
- console.error('Failed to fetch grade info:', err)
- }
- }, [])
-
- const fetchTemplates = useCallback(async () => {
- try {
- setLoadingTemplates(true)
- const res = await fetch(`${API_BASE}/api/v1/vorabitur/templates`)
- if (res.ok) {
- const data = await res.json()
- setTemplates(data.templates || [])
- }
- } catch (err) {
- console.error('Failed to fetch templates:', err)
- } finally {
- setLoadingTemplates(false)
- }
- }, [])
-
- useEffect(() => {
- fetchKlausuren()
- fetchGradeInfo()
- }, [fetchKlausuren, fetchGradeInfo])
-
- useEffect(() => {
- if (form.modus === 'vorabitur' && templates.length === 0) {
- fetchTemplates()
- }
- }, [form.modus, templates.length, fetchTemplates])
-
- const handleCreateKlausur = async (e: React.FormEvent) => {
- e.preventDefault()
- if (!form.title.trim()) {
- setError('Bitte einen Titel eingeben')
- return
- }
-
- if (form.modus === 'vorabitur') {
- if (!ehForm.aufgabentyp) {
- setError('Bitte einen Aufgabentyp auswaehlen')
- return
- }
- if (!ehForm.aufgabenstellung.trim()) {
- setError('Bitte die Aufgabenstellung eingeben')
- return
- }
- }
-
- try {
- setCreating(true)
-
- const res = await fetch(`${API_BASE}/api/v1/klausuren`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(form),
- })
-
- if (!res.ok) {
- const errorData = await res.json()
- setError(errorData.detail || 'Fehler beim Erstellen')
- return
- }
-
- const newKlausur = await res.json()
-
- if (form.modus === 'vorabitur') {
- const ehRes = await fetch(`${API_BASE}/api/v1/klausuren/${newKlausur.id}/vorabitur-eh`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- aufgabentyp: ehForm.aufgabentyp,
- titel: ehForm.titel || `EH: ${form.title}`,
- text_titel: ehForm.text_titel || null,
- text_autor: ehForm.text_autor || null,
- aufgabenstellung: ehForm.aufgabenstellung,
- }),
- })
-
- if (!ehRes.ok) {
- console.error('Failed to create EH:', await ehRes.text())
- setError('Klausur erstellt, aber Erwartungshorizont konnte nicht erstellt werden.')
- }
- }
-
- setKlausuren(prev => [newKlausur, ...prev])
- setForm({
- title: '',
- subject: 'Deutsch',
- year: new Date().getFullYear(),
- semester: 'Abitur',
- modus: 'abitur',
- })
- setEhForm({
- aufgabentyp: '',
- titel: '',
- text_titel: '',
- text_autor: '',
- aufgabenstellung: '',
- })
- setActiveTab('klausuren')
- if (!error) setError(null)
- } catch (err) {
- console.error('Failed to create klausur:', err)
- setError('Fehler beim Erstellen der Klausur')
- } finally {
- setCreating(false)
- }
- }
-
- const handleDeleteKlausur = async (id: string) => {
- if (!confirm('Klausur wirklich loeschen? Alle Studentenarbeiten werden ebenfalls geloescht.')) {
- return
- }
-
- try {
- const res = await fetch(`${API_BASE}/api/v1/klausuren/${id}`, {
- method: 'DELETE',
- })
-
- if (res.ok) {
- setKlausuren(prev => prev.filter(k => k.id !== id))
- } else {
- setError('Fehler beim Loeschen')
- }
- } catch (err) {
- console.error('Failed to delete klausur:', err)
- setError('Fehler beim Loeschen der Klausur')
- }
- }
-
- const markAsVisited = () => {
- if (typeof window !== 'undefined') {
- localStorage.setItem('klausur_korrektur_visited', 'true')
- }
- }
-
- const handleDirektupload = async () => {
- if (direktForm.files.length === 0) {
- setError('Bitte mindestens eine Arbeit hochladen')
- return
- }
-
- try {
- setUploading(true)
-
- const klausurRes = await fetch(`${API_BASE}/api/v1/klausuren`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- title: direktForm.klausurTitle,
- subject: 'Deutsch',
- year: new Date().getFullYear(),
- semester: 'Vorabitur',
- modus: 'vorabitur',
- }),
- })
-
- if (!klausurRes.ok) {
- const err = await klausurRes.json()
- throw new Error(err.detail || 'Klausur erstellen fehlgeschlagen')
- }
-
- const newKlausur = await klausurRes.json()
-
- if (direktForm.ehText.trim() || direktForm.aufgabentyp) {
- const ehRes = await fetch(`${API_BASE}/api/v1/klausuren/${newKlausur.id}/vorabitur-eh`, {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({
- aufgabentyp: direktForm.aufgabentyp || 'textanalyse_pragmatisch',
- titel: `EH: ${direktForm.klausurTitle}`,
- aufgabenstellung: direktForm.ehText || 'Individuelle Aufgabenstellung',
- }),
- })
-
- if (!ehRes.ok) {
- console.error('EH creation failed, continuing with upload')
- }
- }
-
- for (let i = 0; i < direktForm.files.length; i++) {
- const file = direktForm.files[i]
- const formData = new FormData()
- formData.append('file', file)
- formData.append('anonym_id', `Arbeit-${i + 1}`)
-
- const uploadRes = await fetch(`${API_BASE}/api/v1/klausuren/${newKlausur.id}/students`, {
- method: 'POST',
- body: formData,
- })
-
- if (!uploadRes.ok) {
- console.error(`Upload failed for file ${i + 1}:`, file.name)
- }
- }
-
- setKlausuren(prev => [newKlausur, ...prev])
- markAsVisited()
- window.location.href = `/education/klausur-korrektur/${newKlausur.id}`
-
- } catch (err) {
- console.error('Direktupload failed:', err)
- setError(err instanceof Error ? err.message : 'Fehler beim Direktupload')
- } finally {
- setUploading(false)
- }
- }
-
- const renderKlausurenTab = () => (
-
-
-
-
Alle Klausuren
-
{klausuren.length} Klausuren insgesamt
-
-
setActiveTab('erstellen')}
- className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 flex items-center gap-2"
- >
-
-
-
- Neue Klausur
-
-
-
- {loading ? (
-
- ) : klausuren.length === 0 ? (
-
-
-
-
-
Keine Klausuren
-
Erstellen Sie Ihre erste Klausur zum Korrigieren.
-
setActiveTab('erstellen')}
- className="mt-4 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700"
- >
- Klausur erstellen
-
-
- ) : (
-
- {klausuren.map((klausur) => (
-
-
-
-
-
{klausur.title}
-
{klausur.subject} - {klausur.year}
-
-
- {klausur.modus === 'abitur' ? 'Abitur' : 'Vorabitur'}
-
-
-
-
-
-
-
-
-
{klausur.student_count || 0} Arbeiten
-
-
-
-
-
-
{klausur.completed_count || 0} fertig
-
-
-
- {(klausur.student_count || 0) > 0 && (
-
-
- Fortschritt
- {Math.round(((klausur.completed_count || 0) / (klausur.student_count || 1)) * 100)}%
-
-
-
- )}
-
-
-
- Korrigieren
-
-
handleDeleteKlausur(klausur.id)}
- className="px-3 py-2 text-red-600 hover:bg-red-50 rounded-lg"
- title="Loeschen"
- >
-
-
-
-
-
-
-
- ))}
-
- )}
-
- )
-
- const renderErstellenTab = () => (
-
-
-
Neue Klausur erstellen
-
-
-
-
- )
-
- const renderWillkommenTab = () => (
-
-
-
-
Willkommen zur Abiturklausur-Korrektur
-
- KI-gestuetzte Korrektur fuer Deutsch-Abiturklausuren nach dem 15-Punkte-System.
- Sparen Sie bis zu 80% Zeit bei der Erstkorrektur.
-
-
-
-
-
-
-
-
- So funktioniert es
-
-
- {[
- { step: 1, title: 'Arbeiten hochladen', desc: 'Scans der Schuelerarbeiten hochladen' },
- { step: 2, title: 'EH bereitstellen', desc: 'Erwartungshorizont hochladen oder erstellen' },
- { step: 3, title: 'KI korrigiert', desc: 'Automatische Bewertung erhalten' },
- { step: 4, title: 'Pruefen & Anpassen', desc: 'Vorschlaege pruefen und finalisieren' },
- ].map(({ step, title, desc }) => (
-
-
{step === 1 ? '📤' : step === 2 ? '📋' : step === 3 ? '🤖' : '✅'}
-
Schritt {step}
-
{title}
-
{desc}
-
- ))}
-
-
-
-
-
{ markAsVisited(); setActiveTab('erstellen'); }}
- >
-
-
-
-
Neue Klausur erstellen
-
- Empfohlen fuer regelmaessige Nutzung. Erstellen Sie eine Klausur mit allen Metadaten.
-
-
- Klausur erstellen
-
-
-
-
-
-
-
-
-
{ markAsVisited(); setActiveTab('direktupload'); }}
- >
-
-
-
-
Schnellstart - Direkt hochladen
-
- Ideal wenn Sie sofort loslegen moechten. Drag & Drop Upload.
-
-
-
-
-
-
-
- {klausuren.length > 0 && (
-
-
-
Sie haben {klausuren.length} Klausur{klausuren.length !== 1 ? 'en' : ''}
-
Setzen Sie Ihre Arbeit fort oder starten Sie eine neue Korrektur.
-
-
{ markAsVisited(); setActiveTab('klausuren'); }}
- className="px-4 py-2 bg-slate-800 text-white rounded-lg hover:bg-slate-700 text-sm"
- >
- Zu meinen Klausuren
-
-
- )}
-
- )
-
- const renderDirektuploadTab = () => (
-
-
-
-
-
Schnellstart - Direkt Korrigieren
- setActiveTab('willkommen')}
- className="text-sm text-slate-500 hover:text-slate-700"
- >
- Abbrechen
-
-
-
- {[1, 2, 3].map((step) => (
-
-
= step ? 'bg-blue-600 text-white' : 'bg-slate-200 text-slate-500'
- }`}>
- {step}
-
-
= step ? 'text-slate-800' : 'text-slate-400'}`}>
- {step === 1 ? 'Arbeiten' : step === 2 ? 'Erwartungshorizont' : 'Starten'}
-
- {step < 3 &&
step ? 'bg-blue-600' : 'bg-slate-200'}`} />}
-
- ))}
-
-
-
-
- {direktStep === 1 && (
-
-
-
Schuelerarbeiten hochladen
-
- Laden Sie die eingescannten Klausuren hoch. Unterstuetzte Formate: PDF, JPG, PNG.
-
-
-
0 ? 'border-green-300 bg-green-50' : 'border-slate-300 hover:border-blue-400 hover:bg-blue-50'
- }`}
- onDrop={(e) => {
- e.preventDefault()
- const files = Array.from(e.dataTransfer.files)
- setDirektForm(prev => ({ ...prev, files: [...prev.files, ...files] }))
- }}
- onDragOver={(e) => e.preventDefault()}
- >
-
-
-
-
Dateien hier ablegen oder
-
- Dateien auswaehlen
- {
- const files = Array.from(e.target.files || [])
- setDirektForm(prev => ({ ...prev, files: [...prev.files, ...files] }))
- }}
- />
-
-
-
- {direktForm.files.length > 0 && (
-
-
- {direktForm.files.length} Datei{direktForm.files.length !== 1 ? 'en' : ''} ausgewaehlt
- setDirektForm(prev => ({ ...prev, files: [] }))}
- className="text-red-600 hover:text-red-700"
- >
- Alle entfernen
-
-
-
- {direktForm.files.map((file, idx) => (
-
-
{file.name}
-
setDirektForm(prev => ({
- ...prev,
- files: prev.files.filter((_, i) => i !== idx)
- }))}
- className="text-slate-400 hover:text-red-600"
- >
-
-
-
-
-
- ))}
-
-
- )}
-
-
-
- setDirektStep(2)}
- disabled={direktForm.files.length === 0}
- className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed"
- >
- Weiter
-
-
-
- )}
-
- {direktStep === 2 && (
-
-
-
Erwartungshorizont (optional)
-
- Beschreiben Sie die Aufgabenstellung fuer bessere KI-Vorschlaege.
-
-
-
- Aufgabentyp
- setDirektForm(prev => ({ ...prev, aufgabentyp: e.target.value }))}
- className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
- >
- -- Waehlen Sie einen Aufgabentyp --
- Textanalyse (Sachtexte)
- Gedichtanalyse
- Prosaanalyse
- Dramenanalyse
- Textgebundene Eroerterung
-
-
-
-
-
- Aufgabenstellung / Erwartungshorizont
-
- setDirektForm(prev => ({ ...prev, ehText: e.target.value }))}
- placeholder="Beschreiben Sie hier die Aufgabenstellung..."
- rows={6}
- className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent resize-none"
- />
-
-
-
-
- setDirektStep(1)}
- className="px-6 py-2 text-slate-600 hover:bg-slate-100 rounded-lg"
- >
- Zurueck
-
- setDirektStep(3)}
- className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
- >
- Weiter
-
-
-
- )}
-
- {direktStep === 3 && (
-
-
-
Zusammenfassung
-
-
-
- Titel
- setDirektForm(prev => ({ ...prev, klausurTitle: e.target.value }))}
- className="text-sm font-medium text-slate-800 bg-white border border-slate-200 rounded px-2 py-1 text-right"
- />
-
-
- Anzahl Arbeiten
- {direktForm.files.length}
-
-
- Aufgabentyp
-
- {direktForm.aufgabentyp || 'Nicht angegeben'}
-
-
-
-
-
-
-
-
-
-
-
Was passiert jetzt?
-
- Eine neue Klausur wird automatisch erstellt
- Alle {direktForm.files.length} Arbeiten werden hochgeladen
- OCR-Erkennung startet automatisch
- Sie werden zur Korrektur-Ansicht weitergeleitet
-
-
-
-
-
-
-
-
setDirektStep(2)}
- className="px-6 py-2 text-slate-600 hover:bg-slate-100 rounded-lg"
- >
- Zurueck
-
-
- {uploading ? (
- <>
-
- Wird hochgeladen...
- >
- ) : (
- <>
-
-
-
- Korrektur starten
- >
- )}
-
-
-
- )}
-
-
-
- )
-
- const renderStatistikenTab = () => (
-
-
Korrektur-Statistiken
-
-
-
-
{klausuren.length}
-
Klausuren
-
-
-
- {klausuren.reduce((sum, k) => sum + (k.student_count || 0), 0)}
-
-
Studentenarbeiten
-
-
-
- {klausuren.reduce((sum, k) => sum + (k.completed_count || 0), 0)}
-
-
Abgeschlossen
-
-
-
- {klausuren.reduce((sum, k) => sum + ((k.student_count || 0) - (k.completed_count || 0)), 0)}
-
-
Ausstehend
-
-
-
- {gradeInfo && (
-
-
Bewertungskriterien (Niedersachsen)
-
- {Object.entries(gradeInfo.criteria || {}).map(([key, criterion]) => (
-
-
{criterion.weight}%
-
{criterion.name}
-
- ))}
-
-
- )}
-
- )
+ const {
+ activeTab,
+ setActiveTab,
+ klausuren,
+ loading,
+ error,
+ setError,
+ creating,
+ gradeInfo,
+ templates,
+ loadingTemplates,
+ form,
+ setForm,
+ ehForm,
+ setEhForm,
+ direktForm,
+ setDirektForm,
+ direktStep,
+ setDirektStep,
+ uploading,
+ handleCreateKlausur,
+ handleDeleteKlausur,
+ handleDirektupload,
+ markAsVisited,
+ } = useKlausurKorrektur()
return (
@@ -1110,11 +90,48 @@ export default function KlausurKorrekturPage() {
{/* Tab content */}
- {activeTab === 'willkommen' && renderWillkommenTab()}
- {activeTab === 'klausuren' && renderKlausurenTab()}
- {activeTab === 'erstellen' && renderErstellenTab()}
- {activeTab === 'direktupload' && renderDirektuploadTab()}
- {activeTab === 'statistiken' && renderStatistikenTab()}
+ {activeTab === 'willkommen' && (
+
+ )}
+ {activeTab === 'klausuren' && (
+
+ )}
+ {activeTab === 'erstellen' && (
+
+ )}
+ {activeTab === 'direktupload' && (
+
+ )}
+ {activeTab === 'statistiken' && (
+
+ )}
)
diff --git a/admin-lehrer/app/(admin)/infrastructure/middleware/_components/ConfigTab.tsx b/admin-lehrer/app/(admin)/infrastructure/middleware/_components/ConfigTab.tsx
new file mode 100644
index 0000000..4fb0d74
--- /dev/null
+++ b/admin-lehrer/app/(admin)/infrastructure/middleware/_components/ConfigTab.tsx
@@ -0,0 +1,63 @@
+'use client'
+
+import type { MiddlewareConfig } from '../types'
+import { getMiddlewareDescription } from './helpers'
+
+interface ConfigTabProps {
+ configs: MiddlewareConfig[]
+ actionLoading: string | null
+ toggleMiddleware: (name: string, enabled: boolean) => void
+}
+
+export function ConfigTab({ configs, actionLoading, toggleMiddleware }: ConfigTabProps) {
+ return (
+
+ {configs.map(config => {
+ const info = getMiddlewareDescription(config.middleware_name)
+ return (
+
+
+
+
+ {info.icon}
+ {config.middleware_name.replace('_', ' ')}
+
+
{info.desc}
+
+
+
+ {config.enabled ? 'Aktiviert' : 'Deaktiviert'}
+
+ 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'}
+
+
+
+ {Object.keys(config.config).length > 0 && (
+
+
+ Konfiguration
+
+
+ {JSON.stringify(config.config, null, 2)}
+
+
+ )}
+
+ )
+ })}
+
+ )
+}
diff --git a/admin-lehrer/app/(admin)/infrastructure/middleware/_components/EventsTab.tsx b/admin-lehrer/app/(admin)/infrastructure/middleware/_components/EventsTab.tsx
new file mode 100644
index 0000000..171e8e9
--- /dev/null
+++ b/admin-lehrer/app/(admin)/infrastructure/middleware/_components/EventsTab.tsx
@@ -0,0 +1,71 @@
+'use client'
+
+import type { MiddlewareEvent } from '../types'
+import { getEventTypeColor } from './helpers'
+
+interface EventsTabProps {
+ events: MiddlewareEvent[]
+}
+
+export function EventsTab({ events }: EventsTabProps) {
+ return (
+
+
+
+
+
+ Zeit
+
+
+ Middleware
+
+
+ Event
+
+
+ IP
+
+
+ Pfad
+
+
+
+
+ {events.length === 0 ? (
+
+
+ Keine Events vorhanden.
+
+
+ ) : (
+ events.map(event => (
+
+
+ {new Date(event.created_at).toLocaleString('de-DE')}
+
+
+ {event.middleware_name.replace('_', ' ')}
+
+
+
+ {event.event_type}
+
+
+
+ {event.ip_address || '-'}
+
+
+ {event.request_method && event.request_path
+ ? `${event.request_method} ${event.request_path}`
+ : '-'}
+
+
+ ))
+ )}
+
+
+
+ )
+}
diff --git a/admin-lehrer/app/(admin)/infrastructure/middleware/_components/IpListTab.tsx b/admin-lehrer/app/(admin)/infrastructure/middleware/_components/IpListTab.tsx
new file mode 100644
index 0000000..7159f77
--- /dev/null
+++ b/admin-lehrer/app/(admin)/infrastructure/middleware/_components/IpListTab.tsx
@@ -0,0 +1,133 @@
+'use client'
+
+import type { RateLimitIP } from '../types'
+
+interface IpListTabProps {
+ ipList: RateLimitIP[]
+ actionLoading: string | null
+ newIP: string
+ setNewIP: (v: string) => void
+ newIPType: 'whitelist' | 'blacklist'
+ setNewIPType: (v: 'whitelist' | 'blacklist') => void
+ newIPReason: string
+ setNewIPReason: (v: string) => void
+ addIP: (e: React.FormEvent) => void
+ removeIP: (id: string) => void
+}
+
+export function IpListTab({
+ ipList,
+ actionLoading,
+ newIP,
+ setNewIP,
+ newIPType,
+ setNewIPType,
+ newIPReason,
+ setNewIPReason,
+ addIP,
+ removeIP,
+}: IpListTabProps) {
+ return (
+
+ {/* Add IP Form */}
+
+ IP hinzufuegen
+
+ 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"
+ />
+ 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"
+ >
+ Whitelist
+ Blacklist
+
+ 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"
+ />
+
+ {actionLoading === 'add-ip' ? 'Hinzufuegen...' : 'Hinzufuegen'}
+
+
+
+
+ {/* IP List Table */}
+
+
+
+
+
+ IP-Adresse
+
+
+ Typ
+
+
+ Grund
+
+
+ Hinzugefuegt
+
+
+ Aktion
+
+
+
+
+ {ipList.length === 0 ? (
+
+
+ Keine IP-Eintraege vorhanden.
+
+
+ ) : (
+ ipList.map(ip => (
+
+ {ip.ip_address}
+
+
+ {ip.list_type === 'whitelist' ? 'Whitelist' : 'Blacklist'}
+
+
+ {ip.reason || '-'}
+
+ {new Date(ip.created_at).toLocaleString('de-DE')}
+
+
+ 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'}
+
+
+
+ ))
+ )}
+
+
+
+
+ )
+}
diff --git a/admin-lehrer/app/(admin)/infrastructure/middleware/_components/OverviewTab.tsx b/admin-lehrer/app/(admin)/infrastructure/middleware/_components/OverviewTab.tsx
new file mode 100644
index 0000000..7b056e4
--- /dev/null
+++ b/admin-lehrer/app/(admin)/infrastructure/middleware/_components/OverviewTab.tsx
@@ -0,0 +1,60 @@
+'use client'
+
+import type { MiddlewareConfig } from '../types'
+import { getMiddlewareDescription } from './helpers'
+
+interface OverviewTabProps {
+ configs: MiddlewareConfig[]
+ actionLoading: string | null
+ toggleMiddleware: (name: string, enabled: boolean) => void
+}
+
+export function OverviewTab({ configs, actionLoading, toggleMiddleware }: OverviewTabProps) {
+ return (
+
+ {configs.map(config => {
+ const info = getMiddlewareDescription(config.middleware_name)
+ return (
+
+
+
+ {info.icon}
+
+ {config.middleware_name.replace('_', ' ')}
+
+
+
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'}
+
+
+
{info.desc}
+ {config.updated_at && (
+
+ Aktualisiert: {new Date(config.updated_at).toLocaleString('de-DE')}
+
+ )}
+
+ )
+ })}
+
+ )
+}
diff --git a/admin-lehrer/app/(admin)/infrastructure/middleware/_components/StatsTab.tsx b/admin-lehrer/app/(admin)/infrastructure/middleware/_components/StatsTab.tsx
new file mode 100644
index 0000000..0c5ca4c
--- /dev/null
+++ b/admin-lehrer/app/(admin)/infrastructure/middleware/_components/StatsTab.tsx
@@ -0,0 +1,68 @@
+'use client'
+
+import type { MiddlewareStats } from '../types'
+import { getMiddlewareDescription, getEventTypeColor } from './helpers'
+
+interface StatsTabProps {
+ stats: MiddlewareStats[]
+}
+
+export function StatsTab({ stats }: StatsTabProps) {
+ return (
+
+ {stats.map(stat => {
+ const info = getMiddlewareDescription(stat.middleware_name)
+ return (
+
+
+ {info.icon}
+ {stat.middleware_name.replace('_', ' ')}
+
+
+
+
{stat.total_events}
+
Gesamt
+
+
+
{stat.events_last_hour}
+
Letzte Stunde
+
+
+
{stat.events_last_24h}
+
24 Stunden
+
+
+ {stat.top_event_types.length > 0 && (
+
+
+ Top Event-Typen
+
+
+ {stat.top_event_types.slice(0, 3).map(et => (
+
+ {et.event_type} ({et.count})
+
+ ))}
+
+
+ )}
+ {stat.top_ips.length > 0 && (
+
+
Top IPs
+
+ {stat.top_ips
+ .slice(0, 3)
+ .map(ip => `${ip.ip_address} (${ip.count})`)
+ .join(', ')}
+
+
+ )}
+
+ )
+ })}
+
+ )
+}
diff --git a/admin-lehrer/app/(admin)/infrastructure/middleware/_components/helpers.ts b/admin-lehrer/app/(admin)/infrastructure/middleware/_components/helpers.ts
new file mode 100644
index 0000000..c3a61ac
--- /dev/null
+++ b/admin-lehrer/app/(admin)/infrastructure/middleware/_components/helpers.ts
@@ -0,0 +1,24 @@
+export function getMiddlewareDescription(name: string): { icon: string; desc: string } {
+ const descriptions: Record = {
+ request_id: { icon: '\u{1F194}', desc: 'Generiert eindeutige Request-IDs fuer Tracing' },
+ security_headers: { icon: '\u{1F6E1}\uFE0F', desc: 'Fuegt Security-Header hinzu (CSP, HSTS, etc.)' },
+ cors: { icon: '\u{1F310}', desc: 'Cross-Origin Resource Sharing Konfiguration' },
+ rate_limiter: { icon: '\u23F1\uFE0F', desc: 'Rate Limiting zum Schutz vor Missbrauch' },
+ pii_redactor: { icon: '\u{1F512}', desc: 'Redaktiert personenbezogene Daten in Logs' },
+ input_gate: { icon: '\u{1F6AA}', desc: 'Validiert und sanitisiert Eingaben' },
+ }
+ return descriptions[name] || { icon: '\u2699\uFE0F', desc: 'Middleware-Komponente' }
+}
+
+export function getEventTypeColor(eventType: string): 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'
+}
diff --git a/admin-lehrer/app/(admin)/infrastructure/middleware/page.tsx b/admin-lehrer/app/(admin)/infrastructure/middleware/page.tsx
index 7757a6f..c6cadfc 100644
--- a/admin-lehrer/app/(admin)/infrastructure/middleware/page.tsx
+++ b/admin-lehrer/app/(admin)/infrastructure/middleware/page.tsx
@@ -7,210 +7,25 @@
* Migrated from old admin (/admin/middleware)
*/
-import { useEffect, useState, useCallback } from 'react'
import { PagePurpose } from '@/components/common/PagePurpose'
+import { useMiddlewareAdmin } from './useMiddlewareAdmin'
+import { OverviewTab } from './_components/OverviewTab'
+import { ConfigTab } from './_components/ConfigTab'
+import { IpListTab } from './_components/IpListTab'
+import { EventsTab } from './_components/EventsTab'
+import { StatsTab } from './_components/StatsTab'
+import type { TabId } from './types'
-interface MiddlewareConfig {
- id: string
- middleware_name: string
- enabled: boolean
- config: Record
- 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 | 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 }>
+const TAB_LABELS: Record = {
+ overview: 'Uebersicht',
+ config: 'Konfiguration',
+ 'ip-list': 'IP-Listen',
+ events: 'Events',
+ stats: 'Statistiken',
}
export default function MiddlewareAdminPage() {
- const [configs, setConfigs] = useState([])
- const [ipList, setIpList] = useState([])
- const [events, setEvents] = useState([])
- const [stats, setStats] = useState([])
- const [loading, setLoading] = useState(true)
- const [error, setError] = useState(null)
- const [activeTab, setActiveTab] = useState<'overview' | 'config' | 'ip-list' | 'events' | 'stats'>('overview')
- const [actionLoading, setActionLoading] = useState(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 = {
- 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
+ const mw = useMiddlewareAdmin()
return (
@@ -237,29 +52,29 @@ export default function MiddlewareAdminPage() {
Middleware Status
- {loading ? 'Laden...' : 'Aktualisieren'}
+ {mw.loading ? 'Laden...' : 'Aktualisieren'}
-
{configs.length}
+
{mw.configs.length}
Middleware
-
{whitelistCount}
+
{mw.whitelistCount}
Whitelist IPs
-
{blacklistCount}
+
{mw.blacklistCount}
Blacklist IPs
-
{events.length}
+
{mw.events.length}
Recent Events
@@ -271,359 +86,59 @@ export default function MiddlewareAdminPage() {
{(['overview', 'config', 'ip-list', 'events', 'stats'] as const).map(tab => (
setActiveTab(tab)}
+ onClick={() => mw.setActiveTab(tab)}
className={`px-6 py-3 text-sm font-medium whitespace-nowrap transition-colors ${
- activeTab === tab
+ mw.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'}
+ {tab === 'ip-list' ? `${TAB_LABELS[tab]} (${mw.ipList.length})` : TAB_LABELS[tab]}
))}
- {error && (
-
{error}
+ {mw.error && (
+
{mw.error}
)}
- {loading ? (
+ {mw.loading ? (
) : (
<>
- {/* Overview Tab */}
- {activeTab === 'overview' && (
-
- {configs.map(config => {
- const info = getMiddlewareDescription(config.middleware_name)
- return (
-
-
-
- {info.icon}
-
- {config.middleware_name.replace('_', ' ')}
-
-
-
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'}
-
-
-
{info.desc}
- {config.updated_at && (
-
- Aktualisiert: {new Date(config.updated_at).toLocaleString('de-DE')}
-
- )}
-
- )
- })}
-
+ {mw.activeTab === 'overview' && (
+
)}
-
- {/* Config Tab */}
- {activeTab === 'config' && (
-
- {configs.map(config => {
- const info = getMiddlewareDescription(config.middleware_name)
- return (
-
-
-
-
- {info.icon}
- {config.middleware_name.replace('_', ' ')}
-
-
{info.desc}
-
-
-
- {config.enabled ? 'Aktiviert' : 'Deaktiviert'}
-
- 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'}
-
-
-
- {Object.keys(config.config).length > 0 && (
-
-
- Konfiguration
-
-
- {JSON.stringify(config.config, null, 2)}
-
-
- )}
-
- )
- })}
-
+ {mw.activeTab === 'config' && (
+
)}
-
- {/* IP List Tab */}
- {activeTab === 'ip-list' && (
-
- {/* Add IP Form */}
-
- IP hinzufuegen
-
- 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"
- />
- 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"
- >
- Whitelist
- Blacklist
-
- 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"
- />
-
- {actionLoading === 'add-ip' ? 'Hinzufuegen...' : 'Hinzufuegen'}
-
-
-
-
- {/* IP List Table */}
-
-
-
-
-
- IP-Adresse
-
-
- Typ
-
-
- Grund
-
-
- Hinzugefuegt
-
-
- Aktion
-
-
-
-
- {ipList.length === 0 ? (
-
-
- Keine IP-Eintraege vorhanden.
-
-
- ) : (
- ipList.map(ip => (
-
- {ip.ip_address}
-
-
- {ip.list_type === 'whitelist' ? 'Whitelist' : 'Blacklist'}
-
-
- {ip.reason || '-'}
-
- {new Date(ip.created_at).toLocaleString('de-DE')}
-
-
- 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'}
-
-
-
- ))
- )}
-
-
-
-
- )}
-
- {/* Events Tab */}
- {activeTab === 'events' && (
-
-
-
-
-
- Zeit
-
-
- Middleware
-
-
- Event
-
-
- IP
-
-
- Pfad
-
-
-
-
- {events.length === 0 ? (
-
-
- Keine Events vorhanden.
-
-
- ) : (
- events.map(event => (
-
-
- {new Date(event.created_at).toLocaleString('de-DE')}
-
-
- {event.middleware_name.replace('_', ' ')}
-
-
-
- {event.event_type}
-
-
-
- {event.ip_address || '-'}
-
-
- {event.request_method && event.request_path
- ? `${event.request_method} ${event.request_path}`
- : '-'}
-
-
- ))
- )}
-
-
-
- )}
-
- {/* Stats Tab */}
- {activeTab === 'stats' && (
-
- {stats.map(stat => {
- const info = getMiddlewareDescription(stat.middleware_name)
- return (
-
-
- {info.icon}
- {stat.middleware_name.replace('_', ' ')}
-
-
-
-
{stat.total_events}
-
Gesamt
-
-
-
{stat.events_last_hour}
-
Letzte Stunde
-
-
-
{stat.events_last_24h}
-
24 Stunden
-
-
- {stat.top_event_types.length > 0 && (
-
-
- Top Event-Typen
-
-
- {stat.top_event_types.slice(0, 3).map(et => (
-
- {et.event_type} ({et.count})
-
- ))}
-
-
- )}
- {stat.top_ips.length > 0 && (
-
-
Top IPs
-
- {stat.top_ips
- .slice(0, 3)
- .map(ip => `${ip.ip_address} (${ip.count})`)
- .join(', ')}
-
-
- )}
-
- )
- })}
-
+ {mw.activeTab === 'ip-list' && (
+
)}
+ {mw.activeTab === 'events' &&
}
+ {mw.activeTab === 'stats' &&
}
>
)}
diff --git a/admin-lehrer/app/(admin)/infrastructure/middleware/types.ts b/admin-lehrer/app/(admin)/infrastructure/middleware/types.ts
new file mode 100644
index 0000000..1dbc23d
--- /dev/null
+++ b/admin-lehrer/app/(admin)/infrastructure/middleware/types.ts
@@ -0,0 +1,39 @@
+export interface MiddlewareConfig {
+ id: string
+ middleware_name: string
+ enabled: boolean
+ config: Record
+ updated_at: string | null
+}
+
+export interface RateLimitIP {
+ id: string
+ ip_address: string
+ list_type: 'whitelist' | 'blacklist'
+ reason: string | null
+ expires_at: string | null
+ created_at: string
+}
+
+export 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 | null
+ created_at: string
+}
+
+export 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 type TabId = 'overview' | 'config' | 'ip-list' | 'events' | 'stats'
diff --git a/admin-lehrer/app/(admin)/infrastructure/middleware/useMiddlewareAdmin.ts b/admin-lehrer/app/(admin)/infrastructure/middleware/useMiddlewareAdmin.ts
new file mode 100644
index 0000000..a8f134a
--- /dev/null
+++ b/admin-lehrer/app/(admin)/infrastructure/middleware/useMiddlewareAdmin.ts
@@ -0,0 +1,167 @@
+'use client'
+
+import { useEffect, useState, useCallback } from 'react'
+import type { MiddlewareConfig, RateLimitIP, MiddlewareEvent, MiddlewareStats, TabId } from './types'
+
+export function useMiddlewareAdmin() {
+ const [configs, setConfigs] = useState([])
+ const [ipList, setIpList] = useState([])
+ const [events, setEvents] = useState([])
+ const [stats, setStats] = useState([])
+ const [loading, setLoading] = useState(true)
+ const [error, setError] = useState(null)
+ const [activeTab, setActiveTab] = useState('overview')
+ const [actionLoading, setActionLoading] = useState(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}`)
+ }
+
+ 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 whitelistCount = ipList.filter(ip => ip.list_type === 'whitelist').length
+ const blacklistCount = ipList.filter(ip => ip.list_type === 'blacklist').length
+
+ return {
+ configs,
+ ipList,
+ events,
+ stats,
+ loading,
+ error,
+ activeTab,
+ setActiveTab,
+ actionLoading,
+ newIP,
+ setNewIP,
+ newIPType,
+ setNewIPType,
+ newIPReason,
+ setNewIPReason,
+ fetchData,
+ toggleMiddleware,
+ addIP,
+ removeIP,
+ whitelistCount,
+ blacklistCount,
+ }
+}
diff --git a/admin-lehrer/app/(admin)/infrastructure/night-mode/_components/InfoBox.tsx b/admin-lehrer/app/(admin)/infrastructure/night-mode/_components/InfoBox.tsx
new file mode 100644
index 0000000..048a4d9
--- /dev/null
+++ b/admin-lehrer/app/(admin)/infrastructure/night-mode/_components/InfoBox.tsx
@@ -0,0 +1,20 @@
+export function InfoBox() {
+ return (
+
+
+
+
+
+
+
Hinweise zur Nachtabschaltung
+
+ Der night-scheduler und nginx bleiben immer aktiv
+ Services werden mit docker compose stop angehalten (Daten bleiben erhalten)
+ Bei manuellem Start/Stop wird die letzte Aktion gespeichert
+ Der Scheduler prueft jede Minute, ob eine Aktion faellig ist
+
+
+
+
+ )
+}
diff --git a/admin-lehrer/app/(admin)/infrastructure/night-mode/_components/MainControl.tsx b/admin-lehrer/app/(admin)/infrastructure/night-mode/_components/MainControl.tsx
new file mode 100644
index 0000000..2ee0db7
--- /dev/null
+++ b/admin-lehrer/app/(admin)/infrastructure/night-mode/_components/MainControl.tsx
@@ -0,0 +1,76 @@
+import type { NightModeConfig } from './types'
+
+interface MainControlProps {
+ editConfig: NightModeConfig | null
+ actionLoading: string | null
+ onToggle: () => void
+ onExecute: (action: 'start' | 'stop') => void
+}
+
+export function MainControl({ editConfig, actionLoading, onToggle, onExecute }: MainControlProps) {
+ return (
+
+
+ {/* Toggle */}
+
+
+
+
+
+
+ Nachtmodus: {editConfig?.enabled ? 'Aktiv' : 'Inaktiv'}
+
+
+ {editConfig?.enabled
+ ? `Abschaltung um ${editConfig.shutdown_time}, Start um ${editConfig.startup_time}`
+ : 'Zeitgesteuerte Abschaltung ist deaktiviert'}
+
+
+
+
+ {/* Manuelle Aktionen */}
+
+
onExecute('stop')}
+ disabled={actionLoading !== null}
+ className="flex items-center gap-2 px-5 py-2.5 bg-red-600 text-white rounded-lg font-medium hover:bg-red-700 disabled:opacity-50 transition-colors"
+ >
+ {actionLoading === 'stop' ? (
+ ◠
+ ) : (
+
+
+
+
+ )}
+ Jetzt abschalten
+
+
onExecute('start')}
+ disabled={actionLoading !== null}
+ className="flex items-center gap-2 px-5 py-2.5 bg-green-600 text-white rounded-lg font-medium hover:bg-green-700 disabled:opacity-50 transition-colors"
+ >
+ {actionLoading === 'start' ? (
+ ◠
+ ) : (
+
+
+
+
+ )}
+ Jetzt starten
+
+
+
+
+ )
+}
diff --git a/admin-lehrer/app/(admin)/infrastructure/night-mode/_components/ServiceList.tsx b/admin-lehrer/app/(admin)/infrastructure/night-mode/_components/ServiceList.tsx
new file mode 100644
index 0000000..bc643ad
--- /dev/null
+++ b/admin-lehrer/app/(admin)/infrastructure/night-mode/_components/ServiceList.tsx
@@ -0,0 +1,93 @@
+import type { NightModeStatus, ServicesInfo } from './types'
+
+interface ServiceListProps {
+ services: ServicesInfo | null
+ status: NightModeStatus | null
+}
+
+function getServiceStatusColor(state: string) {
+ const lower = state.toLowerCase()
+ if (lower === 'running' || lower.includes('up')) {
+ return 'bg-green-100 text-green-800'
+ }
+ if (lower === 'exited' || lower.includes('exit')) {
+ return 'bg-slate-100 text-slate-600'
+ }
+ if (lower === 'paused' || lower.includes('pause')) {
+ return 'bg-yellow-100 text-yellow-800'
+ }
+ return 'bg-slate-100 text-slate-600'
+}
+
+export function ServiceList({ services, status }: ServiceListProps) {
+ return (
+
+
+
Services
+
+ Gruen = wird verwaltet, Grau = ausgeschlossen (laeuft immer)
+
+
+
+
+
+ {services?.all_services.map(service => {
+ const serviceStatus = status?.services_status[service] || 'unknown'
+ const isExcluded = services.excluded_services.includes(service)
+
+ return (
+
+
+ {isExcluded ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+ {service}
+
+
+
+ {serviceStatus}
+
+
+ )
+ })}
+
+ {/* Auch excluded Services anzeigen, die nicht in all_services sind */}
+ {services?.excluded_services
+ .filter(s => !services.all_services.includes(s))
+ .map(service => (
+
+ ))}
+
+
+
+ )
+}
diff --git a/admin-lehrer/app/(admin)/infrastructure/night-mode/_components/StatusCards.tsx b/admin-lehrer/app/(admin)/infrastructure/night-mode/_components/StatusCards.tsx
new file mode 100644
index 0000000..e6f0ba1
--- /dev/null
+++ b/admin-lehrer/app/(admin)/infrastructure/night-mode/_components/StatusCards.tsx
@@ -0,0 +1,91 @@
+import type { NightModeStatus } from './types'
+
+interface StatusCardsProps {
+ status: NightModeStatus | null
+ runningCount: number
+ stoppedCount: number
+}
+
+export function StatusCards({ status, runningCount, stoppedCount }: StatusCardsProps) {
+ return (
+
+ {/* Current Time */}
+
+
+
+
+
{status?.current_time || '--:--'}
+
Aktuelle Zeit
+
+
+
+
+ {/* Next Action */}
+
+
+
+ {status?.next_action === 'shutdown' ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+
+
+ {status?.next_action_time || '--:--'}
+
+
+ {status?.next_action === 'shutdown' ? 'Naechste Abschaltung' : 'Naechster Start'}
+
+
+
+
+
+ {/* Countdown */}
+
+
+
+
+
+ {status?.time_until_next_action || '-'}
+
+
Countdown
+
+
+
+
+ {/* Running / Stopped */}
+
+
+
+
+
+ {runningCount}
+ /
+ {stoppedCount}
+
+
Aktiv / Gestoppt
+
+
+
+
+ )
+}
diff --git a/admin-lehrer/app/(admin)/infrastructure/night-mode/_components/TimeConfig.tsx b/admin-lehrer/app/(admin)/infrastructure/night-mode/_components/TimeConfig.tsx
new file mode 100644
index 0000000..8f7f9e1
--- /dev/null
+++ b/admin-lehrer/app/(admin)/infrastructure/night-mode/_components/TimeConfig.tsx
@@ -0,0 +1,114 @@
+import type { NightModeConfig, NightModeStatus } from './types'
+
+interface TimeConfigProps {
+ editMode: boolean
+ editConfig: NightModeConfig | null
+ actionLoading: string | null
+ status: NightModeStatus | null
+ onSetEditMode: (v: boolean) => void
+ onSetEditConfig: (fn: (prev: NightModeConfig | null) => NightModeConfig | null) => void
+ onSave: () => void
+ onCancel: () => void
+}
+
+export function TimeConfig({
+ editMode, editConfig, actionLoading, status,
+ onSetEditMode, onSetEditConfig, onSave, onCancel,
+}: TimeConfigProps) {
+ return (
+
+
+
Zeitkonfiguration
+ {editMode ? (
+
+
+ Abbrechen
+
+
+ {actionLoading === 'save' ? 'Speichern...' : 'Speichern'}
+
+
+ ) : (
+
onSetEditMode(true)}
+ className="px-4 py-2 text-sm border border-slate-300 rounded-lg hover:bg-slate-50 transition-colors"
+ >
+ Bearbeiten
+
+ )}
+
+
+
+
+
+
+
+
+
+ Abschaltung um
+
+
+ {editMode ? (
+
onSetEditConfig(prev => prev ? { ...prev, shutdown_time: e.target.value } : null)}
+ className="w-full px-4 py-3 text-xl font-mono border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-500"
+ />
+ ) : (
+
+ {editConfig?.shutdown_time || '22:00'}
+
+ )}
+
+
+
+
+
+
+
+
+ Start um
+
+
+ {editMode ? (
+
onSetEditConfig(prev => prev ? { ...prev, startup_time: e.target.value } : null)}
+ className="w-full px-4 py-3 text-xl font-mono border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-500"
+ />
+ ) : (
+
+ {editConfig?.startup_time || '06:00'}
+
+ )}
+
+
+
+ {/* Letzte Aktion */}
+ {status?.config.last_action && (
+
+
+ Letzte Aktion:{' '}
+
+ {status.config.last_action === 'startup' ? 'Gestartet' : 'Abgeschaltet'}
+
+ {status.config.last_action_time && (
+
+ am {new Date(status.config.last_action_time).toLocaleString('de-DE')}
+
+ )}
+
+
+ )}
+
+ )
+}
diff --git a/admin-lehrer/app/(admin)/infrastructure/night-mode/_components/types.ts b/admin-lehrer/app/(admin)/infrastructure/night-mode/_components/types.ts
new file mode 100644
index 0000000..b44751e
--- /dev/null
+++ b/admin-lehrer/app/(admin)/infrastructure/night-mode/_components/types.ts
@@ -0,0 +1,23 @@
+export interface NightModeConfig {
+ enabled: boolean
+ shutdown_time: string
+ startup_time: string
+ last_action: string | null
+ last_action_time: string | null
+ excluded_services: string[]
+}
+
+export interface NightModeStatus {
+ config: NightModeConfig
+ current_time: string
+ next_action: string | null
+ next_action_time: string | null
+ time_until_next_action: string | null
+ services_status: Record
+}
+
+export interface ServicesInfo {
+ all_services: string[]
+ excluded_services: string[]
+ status: Record
+}
diff --git a/admin-lehrer/app/(admin)/infrastructure/night-mode/_components/useNightMode.ts b/admin-lehrer/app/(admin)/infrastructure/night-mode/_components/useNightMode.ts
new file mode 100644
index 0000000..b350e60
--- /dev/null
+++ b/admin-lehrer/app/(admin)/infrastructure/night-mode/_components/useNightMode.ts
@@ -0,0 +1,174 @@
+import { useEffect, useState, useCallback } from 'react'
+import type { NightModeConfig, NightModeStatus, ServicesInfo } from './types'
+
+export function useNightMode() {
+ const [status, setStatus] = useState(null)
+ const [services, setServices] = useState(null)
+ const [loading, setLoading] = useState(true)
+ const [actionLoading, setActionLoading] = useState(null)
+ const [error, setError] = useState(null)
+ const [successMessage, setSuccessMessage] = useState(null)
+
+ const [editMode, setEditMode] = useState(false)
+ const [editConfig, setEditConfig] = useState(null)
+
+ const fetchData = useCallback(async () => {
+ setError(null)
+ try {
+ const [statusRes, servicesRes] = await Promise.all([
+ fetch('/api/admin/night-mode'),
+ fetch('/api/admin/night-mode/services'),
+ ])
+
+ if (statusRes.ok) {
+ const data = await statusRes.json()
+ setStatus(data)
+ if (!editMode) {
+ setEditConfig(data.config)
+ }
+ } else {
+ const errData = await statusRes.json()
+ setError(errData.error || 'Fehler beim Laden des Status')
+ }
+
+ if (servicesRes.ok) {
+ setServices(await servicesRes.json())
+ }
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Verbindung zum Night-Scheduler fehlgeschlagen')
+ } finally {
+ setLoading(false)
+ }
+ }, [editMode])
+
+ useEffect(() => {
+ fetchData()
+ }, [fetchData])
+
+ // Auto-Refresh alle 30 Sekunden
+ useEffect(() => {
+ const interval = setInterval(fetchData, 30000)
+ return () => clearInterval(interval)
+ }, [fetchData])
+
+ const saveConfig = async () => {
+ if (!editConfig) return
+
+ setActionLoading('save')
+ setError(null)
+ setSuccessMessage(null)
+
+ try {
+ const response = await fetch('/api/admin/night-mode', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(editConfig),
+ })
+
+ if (!response.ok) {
+ const errData = await response.json()
+ throw new Error(errData.error || 'Fehler beim Speichern')
+ }
+
+ setEditMode(false)
+ setSuccessMessage('Konfiguration gespeichert')
+ setTimeout(() => setSuccessMessage(null), 3000)
+ fetchData()
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Speichern fehlgeschlagen')
+ } finally {
+ setActionLoading(null)
+ }
+ }
+
+ const executeAction = async (action: 'start' | 'stop') => {
+ setActionLoading(action)
+ setError(null)
+ setSuccessMessage(null)
+
+ try {
+ const response = await fetch('/api/admin/night-mode/execute', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ action }),
+ })
+
+ if (!response.ok) {
+ const errData = await response.json()
+ throw new Error(errData.error || `Fehler bei ${action}`)
+ }
+
+ const data = await response.json()
+ setSuccessMessage(data.message || `${action === 'start' ? 'Gestartet' : 'Gestoppt'}`)
+ setTimeout(() => setSuccessMessage(null), 5000)
+ fetchData()
+ } catch (err) {
+ setError(err instanceof Error ? err.message : `${action} fehlgeschlagen`)
+ } finally {
+ setActionLoading(null)
+ }
+ }
+
+ const toggleEnabled = async () => {
+ if (!editConfig) return
+
+ const newConfig = { ...editConfig, enabled: !editConfig.enabled }
+ setEditConfig(newConfig)
+
+ setActionLoading('toggle')
+ setError(null)
+
+ try {
+ const response = await fetch('/api/admin/night-mode', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(newConfig),
+ })
+
+ if (!response.ok) {
+ throw new Error('Fehler beim Umschalten')
+ }
+
+ setSuccessMessage(newConfig.enabled ? 'Nachtmodus aktiviert' : 'Nachtmodus deaktiviert')
+ setTimeout(() => setSuccessMessage(null), 3000)
+ fetchData()
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Umschalten fehlgeschlagen')
+ // Zuruecksetzen bei Fehler
+ setEditConfig({ ...editConfig })
+ } finally {
+ setActionLoading(null)
+ }
+ }
+
+ const cancelEdit = () => {
+ setEditMode(false)
+ setEditConfig(status?.config || null)
+ }
+
+ const runningCount = Object.values(status?.services_status || {}).filter(
+ s => s.toLowerCase() === 'running' || s.toLowerCase().includes('up')
+ ).length
+ const stoppedCount = Object.values(status?.services_status || {}).filter(
+ s => s.toLowerCase() === 'exited' || s.toLowerCase().includes('exit')
+ ).length
+
+ return {
+ status,
+ services,
+ loading,
+ actionLoading,
+ error,
+ successMessage,
+ editMode,
+ setEditMode,
+ editConfig,
+ setEditConfig,
+ saveConfig,
+ executeAction,
+ toggleEnabled,
+ cancelEdit,
+ runningCount,
+ stoppedCount,
+ }
+}
diff --git a/admin-lehrer/app/(admin)/infrastructure/night-mode/page.tsx b/admin-lehrer/app/(admin)/infrastructure/night-mode/page.tsx
index 16b89a9..d9dc2c9 100644
--- a/admin-lehrer/app/(admin)/infrastructure/night-mode/page.tsx
+++ b/admin-lehrer/app/(admin)/infrastructure/night-mode/page.tsx
@@ -7,193 +7,32 @@
* nach Zeitplan. Manuelles Starten/Stoppen ebenfalls moeglich.
*/
-import { useEffect, useState, useCallback } from 'react'
-
-interface NightModeConfig {
- enabled: boolean
- shutdown_time: string
- startup_time: string
- last_action: string | null
- last_action_time: string | null
- excluded_services: string[]
-}
-
-interface NightModeStatus {
- config: NightModeConfig
- current_time: string
- next_action: string | null
- next_action_time: string | null
- time_until_next_action: string | null
- services_status: Record
-}
-
-interface ServicesInfo {
- all_services: string[]
- excluded_services: string[]
- status: Record
-}
+import { useNightMode } from './_components/useNightMode'
+import { MainControl } from './_components/MainControl'
+import { StatusCards } from './_components/StatusCards'
+import { TimeConfig } from './_components/TimeConfig'
+import { ServiceList } from './_components/ServiceList'
+import { InfoBox } from './_components/InfoBox'
export default function NightModePage() {
- const [status, setStatus] = useState(null)
- const [services, setServices] = useState(null)
- const [loading, setLoading] = useState(true)
- const [actionLoading, setActionLoading] = useState(null)
- const [error, setError] = useState(null)
- const [successMessage, setSuccessMessage] = useState(null)
-
- // Lokale Konfiguration fuer Bearbeitung
- const [editMode, setEditMode] = useState(false)
- const [editConfig, setEditConfig] = useState(null)
-
- const fetchData = useCallback(async () => {
- setError(null)
- try {
- const [statusRes, servicesRes] = await Promise.all([
- fetch('/api/admin/night-mode'),
- fetch('/api/admin/night-mode/services'),
- ])
-
- if (statusRes.ok) {
- const data = await statusRes.json()
- setStatus(data)
- if (!editMode) {
- setEditConfig(data.config)
- }
- } else {
- const errData = await statusRes.json()
- setError(errData.error || 'Fehler beim Laden des Status')
- }
-
- if (servicesRes.ok) {
- setServices(await servicesRes.json())
- }
- } catch (err) {
- setError(err instanceof Error ? err.message : 'Verbindung zum Night-Scheduler fehlgeschlagen')
- } finally {
- setLoading(false)
- }
- }, [editMode])
-
- useEffect(() => {
- fetchData()
- }, [fetchData])
-
- // Auto-Refresh alle 30 Sekunden
- useEffect(() => {
- const interval = setInterval(fetchData, 30000)
- return () => clearInterval(interval)
- }, [fetchData])
-
- const saveConfig = async () => {
- if (!editConfig) return
-
- setActionLoading('save')
- setError(null)
- setSuccessMessage(null)
-
- try {
- const response = await fetch('/api/admin/night-mode', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(editConfig),
- })
-
- if (!response.ok) {
- const errData = await response.json()
- throw new Error(errData.error || 'Fehler beim Speichern')
- }
-
- setEditMode(false)
- setSuccessMessage('Konfiguration gespeichert')
- setTimeout(() => setSuccessMessage(null), 3000)
- fetchData()
- } catch (err) {
- setError(err instanceof Error ? err.message : 'Speichern fehlgeschlagen')
- } finally {
- setActionLoading(null)
- }
- }
-
- const executeAction = async (action: 'start' | 'stop') => {
- setActionLoading(action)
- setError(null)
- setSuccessMessage(null)
-
- try {
- const response = await fetch('/api/admin/night-mode/execute', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify({ action }),
- })
-
- if (!response.ok) {
- const errData = await response.json()
- throw new Error(errData.error || `Fehler bei ${action}`)
- }
-
- const data = await response.json()
- setSuccessMessage(data.message || `${action === 'start' ? 'Gestartet' : 'Gestoppt'}`)
- setTimeout(() => setSuccessMessage(null), 5000)
- fetchData()
- } catch (err) {
- setError(err instanceof Error ? err.message : `${action} fehlgeschlagen`)
- } finally {
- setActionLoading(null)
- }
- }
-
- const toggleEnabled = async () => {
- if (!editConfig) return
-
- const newConfig = { ...editConfig, enabled: !editConfig.enabled }
- setEditConfig(newConfig)
-
- setActionLoading('toggle')
- setError(null)
-
- try {
- const response = await fetch('/api/admin/night-mode', {
- method: 'POST',
- headers: { 'Content-Type': 'application/json' },
- body: JSON.stringify(newConfig),
- })
-
- if (!response.ok) {
- throw new Error('Fehler beim Umschalten')
- }
-
- setSuccessMessage(newConfig.enabled ? 'Nachtmodus aktiviert' : 'Nachtmodus deaktiviert')
- setTimeout(() => setSuccessMessage(null), 3000)
- fetchData()
- } catch (err) {
- setError(err instanceof Error ? err.message : 'Umschalten fehlgeschlagen')
- // Zuruecksetzen bei Fehler
- setEditConfig({ ...editConfig })
- } finally {
- setActionLoading(null)
- }
- }
-
- const getServiceStatusColor = (state: string) => {
- const lower = state.toLowerCase()
- if (lower === 'running' || lower.includes('up')) {
- return 'bg-green-100 text-green-800'
- }
- if (lower === 'exited' || lower.includes('exit')) {
- return 'bg-slate-100 text-slate-600'
- }
- if (lower === 'paused' || lower.includes('pause')) {
- return 'bg-yellow-100 text-yellow-800'
- }
- return 'bg-slate-100 text-slate-600'
- }
-
- const runningCount = Object.values(status?.services_status || {}).filter(
- s => s.toLowerCase() === 'running' || s.toLowerCase().includes('up')
- ).length
- const stoppedCount = Object.values(status?.services_status || {}).filter(
- s => s.toLowerCase() === 'exited' || s.toLowerCase().includes('exit')
- ).length
+ const {
+ status,
+ services,
+ loading,
+ actionLoading,
+ error,
+ successMessage,
+ editMode,
+ setEditMode,
+ editConfig,
+ setEditConfig,
+ saveConfig,
+ executeAction,
+ toggleEnabled,
+ cancelEdit,
+ runningCount,
+ stoppedCount,
+ } = useNightMode()
return (
@@ -226,334 +65,33 @@ export default function NightModePage() {
) : (
<>
- {/* Haupt-Steuerung */}
-
-
- {/* Toggle */}
-
-
-
-
-
-
- Nachtmodus: {editConfig?.enabled ? 'Aktiv' : 'Inaktiv'}
-
-
- {editConfig?.enabled
- ? `Abschaltung um ${editConfig.shutdown_time}, Start um ${editConfig.startup_time}`
- : 'Zeitgesteuerte Abschaltung ist deaktiviert'}
-
-
-
+
- {/* Manuelle Aktionen */}
-
-
executeAction('stop')}
- disabled={actionLoading !== null}
- className="flex items-center gap-2 px-5 py-2.5 bg-red-600 text-white rounded-lg font-medium hover:bg-red-700 disabled:opacity-50 transition-colors"
- >
- {actionLoading === 'stop' ? (
- ◠
- ) : (
-
-
-
-
- )}
- Jetzt abschalten
-
-
executeAction('start')}
- disabled={actionLoading !== null}
- className="flex items-center gap-2 px-5 py-2.5 bg-green-600 text-white rounded-lg font-medium hover:bg-green-700 disabled:opacity-50 transition-colors"
- >
- {actionLoading === 'start' ? (
- ◠
- ) : (
-
-
-
-
- )}
- Jetzt starten
-
-
-
-
+
- {/* Status-Karten */}
-
-
-
-
-
-
{status?.current_time || '--:--'}
-
Aktuelle Zeit
-
-
-
+
-
-
-
- {status?.next_action === 'shutdown' ? (
-
-
-
- ) : (
-
-
-
- )}
-
-
-
- {status?.next_action_time || '--:--'}
-
-
- {status?.next_action === 'shutdown' ? 'Naechste Abschaltung' : 'Naechster Start'}
-
-
-
-
+
-
-
-
-
-
- {status?.time_until_next_action || '-'}
-
-
Countdown
-
-
-
-
-
-
-
-
-
- {runningCount}
- /
- {stoppedCount}
-
-
Aktiv / Gestoppt
-
-
-
-
-
- {/* Zeitkonfiguration */}
-
-
-
Zeitkonfiguration
- {editMode ? (
-
- {
- setEditMode(false)
- setEditConfig(status?.config || null)
- }}
- className="px-4 py-2 text-sm border border-slate-300 rounded-lg hover:bg-slate-50 transition-colors"
- >
- Abbrechen
-
-
- {actionLoading === 'save' ? 'Speichern...' : 'Speichern'}
-
-
- ) : (
-
setEditMode(true)}
- className="px-4 py-2 text-sm border border-slate-300 rounded-lg hover:bg-slate-50 transition-colors"
- >
- Bearbeiten
-
- )}
-
-
-
-
-
-
-
-
-
- Abschaltung um
-
-
- {editMode ? (
-
setEditConfig(prev => prev ? { ...prev, shutdown_time: e.target.value } : null)}
- className="w-full px-4 py-3 text-xl font-mono border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-500"
- />
- ) : (
-
- {editConfig?.shutdown_time || '22:00'}
-
- )}
-
-
-
-
-
-
-
-
- Start um
-
-
- {editMode ? (
-
setEditConfig(prev => prev ? { ...prev, startup_time: e.target.value } : null)}
- className="w-full px-4 py-3 text-xl font-mono border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-500"
- />
- ) : (
-
- {editConfig?.startup_time || '06:00'}
-
- )}
-
-
-
- {/* Letzte Aktion */}
- {status?.config.last_action && (
-
-
- Letzte Aktion:{' '}
-
- {status.config.last_action === 'startup' ? 'Gestartet' : 'Abgeschaltet'}
-
- {status.config.last_action_time && (
-
- am {new Date(status.config.last_action_time).toLocaleString('de-DE')}
-
- )}
-
-
- )}
-
-
- {/* Service-Liste */}
-
-
-
Services
-
- Gruen = wird verwaltet, Grau = ausgeschlossen (laeuft immer)
-
-
-
-
-
- {services?.all_services.map(service => {
- const serviceStatus = status?.services_status[service] || 'unknown'
- const isExcluded = services.excluded_services.includes(service)
-
- return (
-
-
- {isExcluded ? (
-
-
-
- ) : (
-
-
-
- )}
-
- {service}
-
-
-
- {serviceStatus}
-
-
- )
- })}
-
- {/* Auch excluded Services anzeigen, die nicht in all_services sind */}
- {services?.excluded_services
- .filter(s => !services.all_services.includes(s))
- .map(service => (
-
- ))}
-
-
-
-
- {/* Info Box */}
-
-
-
-
-
-
-
Hinweise zur Nachtabschaltung
-
- Der night-scheduler und nginx bleiben immer aktiv
- Services werden mit docker compose stop angehalten (Daten bleiben erhalten)
- Bei manuellem Start/Stop wird die letzte Aktion gespeichert
- Der Scheduler prueft jede Minute, ob eine Aktion faellig ist
-
-
-
-
+
>
)}
diff --git a/admin-lehrer/app/(admin)/infrastructure/sbom/_components/CategoryFilter.tsx b/admin-lehrer/app/(admin)/infrastructure/sbom/_components/CategoryFilter.tsx
new file mode 100644
index 0000000..876837f
--- /dev/null
+++ b/admin-lehrer/app/(admin)/infrastructure/sbom/_components/CategoryFilter.tsx
@@ -0,0 +1,54 @@
+import type { CategoryType, CategoryItem } from '../types'
+
+interface CategoryFilterProps {
+ categories: CategoryItem[]
+ activeCategory: CategoryType
+ setActiveCategory: (cat: CategoryType) => void
+ searchTerm: string
+ setSearchTerm: (term: string) => void
+}
+
+export function CategoryFilter({
+ categories,
+ activeCategory,
+ setActiveCategory,
+ searchTerm,
+ setSearchTerm,
+}: CategoryFilterProps) {
+ return (
+
+
+ {/* Category Tabs */}
+
+ {categories.map((cat) => (
+ 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})
+
+ ))}
+
+
+ {/* Search */}
+
+
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"
+ />
+
+
+
+
+
+
+ )
+}
diff --git a/admin-lehrer/app/(admin)/infrastructure/sbom/_components/ComponentsTable.tsx b/admin-lehrer/app/(admin)/infrastructure/sbom/_components/ComponentsTable.tsx
new file mode 100644
index 0000000..ecb3649
--- /dev/null
+++ b/admin-lehrer/app/(admin)/infrastructure/sbom/_components/ComponentsTable.tsx
@@ -0,0 +1,103 @@
+import type { Component } from '../types'
+import { getCategoryColor, getLicenseColor } from './utils'
+
+interface ComponentsTableProps {
+ components: Component[]
+ loading: boolean
+}
+
+export function ComponentsTable({ components, loading }: ComponentsTableProps) {
+ if (loading) {
+ return (
+
+ )
+ }
+
+ return (
+
+
+
+
+ Komponente
+ Version
+ Kategorie
+ Verwendungszweck
+ Port
+ Lizenz
+ Source
+
+
+
+ {components.map((component, idx) => {
+ const licenseId = component.license || component.licenses?.[0]?.license?.id
+
+ return (
+
+
+ {component.name}
+
+
+ {component.version}
+
+
+
+ {component.category || component.type}
+
+
+
+ {component.description ? (
+ {component.description}
+ ) : (
+ Keine Beschreibung
+ )}
+
+
+ {component.port ? (
+ {component.port}
+ ) : (
+ -
+ )}
+
+
+ {licenseId ? (
+
+ {licenseId}
+
+ ) : (
+ -
+ )}
+
+
+ {component.sourceUrl && component.sourceUrl !== '-' ? (
+
+
+
+
+ GitHub
+
+ ) : (
+ -
+ )}
+
+
+ )
+ })}
+
+
+
+ {components.length === 0 && (
+
+ Keine Komponenten gefunden.
+
+ )}
+
+ )
+}
diff --git a/admin-lehrer/app/(admin)/infrastructure/sbom/_components/ExportButton.tsx b/admin-lehrer/app/(admin)/infrastructure/sbom/_components/ExportButton.tsx
new file mode 100644
index 0000000..e80c2bf
--- /dev/null
+++ b/admin-lehrer/app/(admin)/infrastructure/sbom/_components/ExportButton.tsx
@@ -0,0 +1,19 @@
+interface ExportButtonProps {
+ onExport: () => void
+}
+
+export function ExportButton({ onExport }: ExportButtonProps) {
+ return (
+
+
+
+
+
+ SBOM exportieren (JSON)
+
+
+ )
+}
diff --git a/admin-lehrer/app/(admin)/infrastructure/sbom/_components/InfoTabsSection.tsx b/admin-lehrer/app/(admin)/infrastructure/sbom/_components/InfoTabsSection.tsx
new file mode 100644
index 0000000..e9853c8
--- /dev/null
+++ b/admin-lehrer/app/(admin)/infrastructure/sbom/_components/InfoTabsSection.tsx
@@ -0,0 +1,251 @@
+import type { InfoTabType } from '../types'
+
+interface InfoTabsSectionProps {
+ activeInfoTab: InfoTabType
+ setActiveInfoTab: (tab: InfoTabType) => void
+ showFullDocs: boolean
+ setShowFullDocs: (show: boolean) => void
+}
+
+export function InfoTabsSection({
+ activeInfoTab,
+ setActiveInfoTab,
+ showFullDocs,
+ setShowFullDocs,
+}: InfoTabsSectionProps) {
+ return (
+
+
+ {/* Tab Headers */}
+
+
+ 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'
+ }`}
+ >
+
+
+
+
+ Audit
+
+
+ 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'
+ }`}
+ >
+
+
+
+
+ Dokumentation
+
+
+
+
+
+ {/* Tab Content */}
+
+ {activeInfoTab === 'audit' &&
}
+ {activeInfoTab === 'documentation' && (
+
+ )}
+
+
+
+ )
+}
+
+function AuditTab() {
+ return (
+
+ {/* SBOM Status */}
+
+
+
+
+
+ SBOM Status
+
+
+ {[
+ { label: 'Letzte Generierung', value: 'CI/CD' },
+ { label: 'Format', value: 'CycloneDX 1.5' },
+ { label: 'Komponenten', value: 'Alle erfasst' },
+ { label: 'Transitive Deps', value: 'Inkludiert' },
+ ].map((item) => (
+
+ {item.label}
+
+
+ {item.value}
+
+
+ ))}
+
+
+
+ {/* License Compliance */}
+
+
+
+
+
+ License Compliance
+
+
+ {[
+ { label: 'Erlaubte Lizenzen', value: 'MIT, Apache, BSD', color: 'bg-green-500' },
+ { label: 'Copyleft (GPL)', value: '0', color: 'bg-green-500' },
+ { label: 'Unbekannte Lizenzen', value: '0', color: 'bg-green-500' },
+ { label: 'Kommerzielle', value: 'Review erforderlich', color: 'bg-yellow-500' },
+ ].map((item) => (
+
+ {item.label}
+
+
+ {item.value}
+
+
+ ))}
+
+
+
+ )
+}
+
+function DocumentationTab({
+ showFullDocs,
+ setShowFullDocs,
+}: {
+ showFullDocs: boolean
+ setShowFullDocs: (show: boolean) => void
+}) {
+ return (
+
+
+
SBOM Dokumentation
+
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"
+ >
+
+
+
+ {showFullDocs ? 'Weniger anzeigen' : 'Vollstaendige Dokumentation'}
+
+
+
+ {!showFullDocs ?
:
}
+
+ )
+}
+
+function DocsSummary() {
+ return (
+
+
+ Das SBOM-Modul generiert und analysiert die vollstaendige Komponentenliste aller Software-Abhaengigkeiten.
+ Es dient der Compliance, Sicherheit und Supply-Chain-Transparenz.
+
+
+
+
Generator
+
Syft (Primary), Trivy (Validation)
+
+
+
Format
+
CycloneDX 1.5, SPDX
+
+
+
Retention
+
5 Jahre (Compliance)
+
+
+
+ )
+}
+
+function DocsFullContent() {
+ return (
+
+
Software Bill of Materials (SBOM)
+
+
1. Uebersicht
+
Das SBOM-Modul generiert und analysiert die vollstaendige Komponentenliste aller Software-Abhaengigkeiten. Es dient der Compliance, Sicherheit und Supply-Chain-Transparenz.
+
+
2. SBOM-Generierung
+
+{`Source Code
+ |
+ v
++---------------------------------------------------------------+
+| SBOM Generators |
+| +-------------+ +-------------+ +---------------------+ |
+| | Syft | | Trivy | | Native Tooling | |
+| | (Primary) | | (Validation)| | (npm, go mod, pip) | |
+| +------+------+ +------+------+ +----------+----------+ |
++---------+----------------+--------------------+---------------+
+ | | |
+ +----------------+--------------------+
+ |
+ v
+ +----------------+
+ | CycloneDX |
+ | Format |
+ +----------------+`}
+
+
+
3. Erfasste Komponenten
+
+
+
+ Typ
+ Quelle
+ Beispiele
+
+
+
+ npm packages package-lock.json react, next, tailwindcss
+ Go modules go.sum gin, gorm, jwt-go
+ Python packages requirements.txt fastapi, pydantic, httpx
+ Container Images Dockerfile node:20-alpine, postgres:16
+ OS Packages apk, apt openssl, libpq
+
+
+
+
4. License Compliance
+
+
+
+ Kategorie
+ Lizenzen
+ Status
+
+
+
+ Permissive (erlaubt) MIT, Apache 2.0, BSD, ISC OK
+ Weak Copyleft LGPL, MPL Review
+ Strong Copyleft GPL, AGPL Nicht erlaubt
+ Proprietaer Commercial Genehmigung
+
+
+
+
5. Aufbewahrung & Compliance
+
+ Retention: 5 Jahre (Compliance)
+ Format: JSON + PDF Report
+ Signierung: Digital signiert
+ Audit: Jederzeit abrufbar
+
+
+ )
+}
diff --git a/admin-lehrer/app/(admin)/infrastructure/sbom/_components/SbomMetadata.tsx b/admin-lehrer/app/(admin)/infrastructure/sbom/_components/SbomMetadata.tsx
new file mode 100644
index 0000000..0ca7d3e
--- /dev/null
+++ b/admin-lehrer/app/(admin)/infrastructure/sbom/_components/SbomMetadata.tsx
@@ -0,0 +1,32 @@
+import type { SBOMData } from '../types'
+
+interface SbomMetadataProps {
+ sbomData: SBOMData
+}
+
+export function SbomMetadata({ sbomData }: SbomMetadataProps) {
+ if (!sbomData.metadata) return null
+
+ return (
+
+
+
+ Format:
+ {sbomData.bomFormat} {sbomData.specVersion}
+
+
+ Generiert:
+
+ {sbomData.metadata.timestamp ? new Date(sbomData.metadata.timestamp).toLocaleString('de-DE') : '-'}
+
+
+
+ Anwendung:
+
+ {sbomData.metadata.component?.name} v{sbomData.metadata.component?.version}
+
+
+
+
+ )
+}
diff --git a/admin-lehrer/app/(admin)/infrastructure/sbom/_components/StatsCards.tsx b/admin-lehrer/app/(admin)/infrastructure/sbom/_components/StatsCards.tsx
new file mode 100644
index 0000000..f83a568
--- /dev/null
+++ b/admin-lehrer/app/(admin)/infrastructure/sbom/_components/StatsCards.tsx
@@ -0,0 +1,31 @@
+import type { SbomStats } from '../types'
+
+interface StatsCardsProps {
+ stats: SbomStats
+}
+
+export function StatsCards({ stats }: StatsCardsProps) {
+ const items = [
+ { value: stats.totalAll, label: 'Komponenten Total', color: 'text-slate-800' },
+ { value: stats.totalInfra, label: 'Docker Services', color: 'text-purple-600' },
+ { value: stats.totalSecurityTools, label: 'Security Tools', color: 'text-red-600' },
+ { value: stats.totalPython, label: 'Python', color: 'text-emerald-600' },
+ { value: stats.totalGo, label: 'Go', color: 'text-sky-600' },
+ { value: stats.totalNode, label: 'Node.js', color: 'text-lime-600' },
+ { value: stats.totalUnity, label: 'Unity', color: 'text-amber-600' },
+ { value: stats.totalCsharp, label: 'C#', color: 'text-fuchsia-600' },
+ { value: stats.databases, label: 'Datenbanken', color: 'text-blue-600' },
+ { value: stats.game, label: 'Game', color: 'text-rose-600' },
+ ]
+
+ return (
+
+ {items.map((item) => (
+
+
{item.value}
+
{item.label}
+
+ ))}
+
+ )
+}
diff --git a/admin-lehrer/app/(admin)/infrastructure/sbom/_components/index.ts b/admin-lehrer/app/(admin)/infrastructure/sbom/_components/index.ts
new file mode 100644
index 0000000..31e2b41
--- /dev/null
+++ b/admin-lehrer/app/(admin)/infrastructure/sbom/_components/index.ts
@@ -0,0 +1,6 @@
+export { StatsCards } from './StatsCards'
+export { CategoryFilter } from './CategoryFilter'
+export { ComponentsTable } from './ComponentsTable'
+export { ExportButton } from './ExportButton'
+export { SbomMetadata } from './SbomMetadata'
+export { InfoTabsSection } from './InfoTabsSection'
diff --git a/admin-lehrer/app/(admin)/infrastructure/sbom/_components/utils.ts b/admin-lehrer/app/(admin)/infrastructure/sbom/_components/utils.ts
new file mode 100644
index 0000000..d9bba40
--- /dev/null
+++ b/admin-lehrer/app/(admin)/infrastructure/sbom/_components/utils.ts
@@ -0,0 +1,32 @@
+export function getCategoryColor(category?: string): 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 'cache': return 'bg-cyan-100 text-cyan-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'
+ }
+}
+
+export function getLicenseColor(license?: string): 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'
+}
diff --git a/admin-lehrer/app/(admin)/infrastructure/sbom/data.ts b/admin-lehrer/app/(admin)/infrastructure/sbom/data.ts
new file mode 100644
index 0000000..7298fff
--- /dev/null
+++ b/admin-lehrer/app/(admin)/infrastructure/sbom/data.ts
@@ -0,0 +1,169 @@
+import type { Component } from './types'
+
+// Infrastructure components from docker-compose.yml and project analysis
+export 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' },
+
+ // ===== 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' },
+
+ // ===== 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' },
+
+ // ===== 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' },
+ { type: 'service', name: 'NetBird', version: '0.64.5', category: 'security', port: '-', description: 'Zero-Trust Mesh VPN (WireGuard-basiert)', license: 'BSD-3-Clause', sourceUrl: 'https://github.com/netbirdio/netbird' },
+
+ // ===== 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: 'Lehrer Backend API (Klausuren, E-Mail, Alerts)', 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: 'Transcription Worker', version: '1.0', category: 'application', port: '-', description: 'Whisper + pyannote Transkription', license: 'Proprietary', sourceUrl: '-' },
+
+ // ===== APPLICATION SERVICES (Go) =====
+ { type: 'service', name: 'Go School Service', version: '1.21', category: 'application', port: '8084', description: 'Klausuren, Noten, Zeugnisse', license: 'Proprietary', sourceUrl: '-' },
+
+ // ===== APPLICATION SERVICES (Node.js) =====
+ { type: 'service', name: 'Next.js Admin Frontend', version: '15.1', category: 'application', port: '3002', description: 'Admin Lehrer Dashboard (React)', license: 'Proprietary', sourceUrl: '-' },
+
+ // ===== 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
+export 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)
+export 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)', license: 'MIT', sourceUrl: 'https://code.launchpad.net/beautifulsoup' },
+ { 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)
+export 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: 'opensearch-project/opensearch-go', version: '4.x', category: 'go', description: 'OpenSearch Client (edu-search-service)', license: 'Apache-2.0', sourceUrl: 'https://github.com/opensearch-project/opensearch-go' },
+ { type: 'library', name: 'lib/pq', version: '1.10+', category: 'go', description: 'PostgreSQL Driver (school-service)', license: 'MIT', sourceUrl: 'https://github.com/lib/pq' },
+ { 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)
+export 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: '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: '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 (Admin Dashboard)', license: 'MIT', sourceUrl: 'https://github.com/recharts/recharts' },
+ { type: 'library', name: 'Playwright', version: '1.50', category: 'nodejs', description: 'E2E Testing Framework (SDK Tests)', license: 'Apache-2.0', sourceUrl: 'https://github.com/microsoft/playwright' },
+ { type: 'library', name: 'Vitest', version: '4.x', category: 'nodejs', description: 'Unit Testing Framework', license: 'MIT', sourceUrl: 'https://github.com/vitest-dev/vitest' },
+ { type: 'library', name: 'jsPDF', version: '4.x', category: 'nodejs', description: 'PDF Generation (SDK Export)', license: 'MIT', sourceUrl: 'https://github.com/parallax/jsPDF' },
+ { type: 'library', name: 'JSZip', version: '3.x', category: 'nodejs', description: 'ZIP File Creation (SDK Export)', license: 'MIT/GPL-3.0', sourceUrl: 'https://github.com/Stuk/jszip' },
+ { type: 'library', name: 'Lucide React', version: '0.468', category: 'nodejs', description: 'Icon Library', license: 'ISC', sourceUrl: 'https://github.com/lucide-icons/lucide' },
+]
+
+// Unity packages (Breakpilot Drive game engine)
+export 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)
+export 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' },
+]
diff --git a/admin-lehrer/app/(admin)/infrastructure/sbom/page.tsx b/admin-lehrer/app/(admin)/infrastructure/sbom/page.tsx
index db12765..a50de6e 100644
--- a/admin-lehrer/app/(admin)/infrastructure/sbom/page.tsx
+++ b/admin-lehrer/app/(admin)/infrastructure/sbom/page.tsx
@@ -13,371 +13,36 @@
* - Version tracking
*/
-import { useState, useEffect } from 'react'
import Link from 'next/link'
import { PagePurpose } from '@/components/common/PagePurpose'
import { DevOpsPipelineSidebarResponsive } from '@/components/infrastructure/DevOpsPipelineSidebar'
-
-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' },
-
- // ===== 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' },
-
- // ===== 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' },
-
- // ===== 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' },
- { type: 'service', name: 'NetBird', version: '0.64.5', category: 'security', port: '-', description: 'Zero-Trust Mesh VPN (WireGuard-basiert)', license: 'BSD-3-Clause', sourceUrl: 'https://github.com/netbirdio/netbird' },
-
- // ===== 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: 'Lehrer Backend API (Klausuren, E-Mail, Alerts)', 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: 'Transcription Worker', version: '1.0', category: 'application', port: '-', description: 'Whisper + pyannote Transkription', license: 'Proprietary', sourceUrl: '-' },
-
- // ===== APPLICATION SERVICES (Go) =====
- { type: 'service', name: 'Go School Service', version: '1.21', category: 'application', port: '8084', description: 'Klausuren, Noten, Zeugnisse', license: 'Proprietary', sourceUrl: '-' },
-
- // ===== APPLICATION SERVICES (Node.js) =====
- { type: 'service', name: 'Next.js Admin Frontend', version: '15.1', category: 'application', port: '3002', description: 'Admin Lehrer Dashboard (React)', license: 'Proprietary', sourceUrl: '-' },
-
- // ===== 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)', license: 'MIT', sourceUrl: 'https://code.launchpad.net/beautifulsoup' },
- { 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: 'opensearch-project/opensearch-go', version: '4.x', category: 'go', description: 'OpenSearch Client (edu-search-service)', license: 'Apache-2.0', sourceUrl: 'https://github.com/opensearch-project/opensearch-go' },
- { type: 'library', name: 'lib/pq', version: '1.10+', category: 'go', description: 'PostgreSQL Driver (school-service)', license: 'MIT', sourceUrl: 'https://github.com/lib/pq' },
- { 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: '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: '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 (Admin Dashboard)', license: 'MIT', sourceUrl: 'https://github.com/recharts/recharts' },
- { type: 'library', name: 'Playwright', version: '1.50', category: 'nodejs', description: 'E2E Testing Framework (SDK Tests)', license: 'Apache-2.0', sourceUrl: 'https://github.com/microsoft/playwright' },
- { type: 'library', name: 'Vitest', version: '4.x', category: 'nodejs', description: 'Unit Testing Framework', license: 'MIT', sourceUrl: 'https://github.com/vitest-dev/vitest' },
- { type: 'library', name: 'jsPDF', version: '4.x', category: 'nodejs', description: 'PDF Generation (SDK Export)', license: 'MIT', sourceUrl: 'https://github.com/parallax/jsPDF' },
- { type: 'library', name: 'JSZip', version: '3.x', category: 'nodejs', description: 'ZIP File Creation (SDK Export)', license: 'MIT/GPL-3.0', sourceUrl: 'https://github.com/Stuk/jszip' },
- { type: 'library', name: 'Lucide React', version: '0.468', category: 'nodejs', description: 'Icon Library', license: 'ISC', sourceUrl: 'https://github.com/lucide-icons/lucide' },
-]
-
-// 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' },
-]
+import {
+ StatsCards,
+ CategoryFilter,
+ ComponentsTable,
+ ExportButton,
+ SbomMetadata,
+ InfoTabsSection,
+} from './_components'
+import { useSbomData } from './useSbomData'
export default function SBOMPage() {
- const [sbomData, setSbomData] = useState
(null)
- const [loading, setLoading] = useState(true)
- const [activeCategory, setActiveCategory] = useState('all')
- const [searchTerm, setSearchTerm] = useState('')
- const [activeInfoTab, setActiveInfoTab] = useState('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 'cache': return 'bg-cyan-100 text-cyan-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()
+ const {
+ sbomData,
+ loading,
+ activeCategory,
+ setActiveCategory,
+ searchTerm,
+ setSearchTerm,
+ activeInfoTab,
+ setActiveInfoTab,
+ showFullDocs,
+ setShowFullDocs,
+ filteredComponents,
+ stats,
+ categories,
+ handleExport,
+ } = useSbomData()
return (
@@ -398,7 +63,6 @@ export default function SBOMPage() {
defaultCollapsed={true}
/>
- {/* DevOps Pipeline Sidebar */}
{/* Wizard Link */}
@@ -414,467 +78,28 @@ export default function SBOMPage() {
- {/* Stats Cards */}
-
-
-
{stats.totalAll}
-
Komponenten Total
-
-
-
{stats.totalInfra}
-
Docker Services
-
-
-
{stats.totalSecurityTools}
-
Security Tools
-
-
-
{stats.totalPython}
-
Python
-
-
-
-
{stats.totalNode}
-
Node.js
-
-
-
{stats.totalUnity}
-
Unity
-
-
-
{stats.totalCsharp}
-
C#
-
-
-
{stats.databases}
-
Datenbanken
-
-
-
+
- {/* Filters */}
-
-
- {/* Category Tabs */}
-
- {categories.map((cat) => (
- 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})
-
- ))}
-
+
- {/* Search */}
-
-
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"
- />
-
-
-
-
-
-
+ {sbomData && }
- {/* SBOM Metadata */}
- {sbomData?.metadata && (
-
-
-
- Format:
- {sbomData.bomFormat} {sbomData.specVersion}
-
-
- Generiert:
-
- {sbomData.metadata.timestamp ? new Date(sbomData.metadata.timestamp).toLocaleString('de-DE') : '-'}
-
-
-
- Anwendung:
-
- {sbomData.metadata.component?.name} v{sbomData.metadata.component?.version}
-
-
-
-
- )}
+
- {/* Components Table */}
- {loading ? (
-
- ) : (
-
-
-
-
- Komponente
- Version
- Kategorie
- Verwendungszweck
- Port
- Lizenz
- Source
-
-
-
- {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 (
-
-
- {component.name}
-
-
- {component.version}
-
-
-
- {component.category || component.type}
-
-
-
- {component.description ? (
- {component.description}
- ) : (
- Keine Beschreibung
- )}
-
-
- {component.port ? (
- {component.port}
- ) : (
- -
- )}
-
-
- {licenseId ? (
-
- {licenseId}
-
- ) : (
- -
- )}
-
-
- {component.sourceUrl && component.sourceUrl !== '-' ? (
-
-
-
-
- GitHub
-
- ) : (
- -
- )}
-
-
- )
- })}
-
-
-
- {filteredComponents.length === 0 && (
-
- Keine Komponenten gefunden.
-
- )}
-
- )}
-
- {/* Export Button */}
-
-
{
- 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-lehrer-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"
- >
-
-
-
- SBOM exportieren (JSON)
-
-
-
- {/* Info Tabs Section */}
-
-
- {/* Tab Headers */}
-
-
- 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'
- }`}
- >
-
-
-
-
- Audit
-
-
- 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'
- }`}
- >
-
-
-
-
- Dokumentation
-
-
-
-
-
- {/* Tab Content */}
-
- {/* Audit Tab */}
- {activeInfoTab === 'audit' && (
-
- {/* SBOM Status */}
-
-
-
-
-
- SBOM Status
-
-
-
- Letzte Generierung
-
-
- CI/CD
-
-
-
- Format
-
-
- CycloneDX 1.5
-
-
-
- Komponenten
-
-
- Alle erfasst
-
-
-
- Transitive Deps
-
-
- Inkludiert
-
-
-
-
-
- {/* License Compliance */}
-
-
-
-
-
- License Compliance
-
-
-
- Erlaubte Lizenzen
-
-
- MIT, Apache, BSD
-
-
-
- Copyleft (GPL)
-
-
- 0
-
-
-
- Unbekannte Lizenzen
-
-
- 0
-
-
-
- Kommerzielle
-
-
- Review erforderlich
-
-
-
-
-
- )}
-
- {/* Documentation Tab */}
- {activeInfoTab === 'documentation' && (
-
-
-
SBOM Dokumentation
-
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"
- >
-
-
-
- {showFullDocs ? 'Weniger anzeigen' : 'Vollstaendige Dokumentation'}
-
-
-
- {!showFullDocs ? (
-
-
- Das SBOM-Modul generiert und analysiert die vollstaendige Komponentenliste aller Software-Abhaengigkeiten.
- Es dient der Compliance, Sicherheit und Supply-Chain-Transparenz.
-
-
-
-
Generator
-
Syft (Primary), Trivy (Validation)
-
-
-
Format
-
CycloneDX 1.5, SPDX
-
-
-
Retention
-
5 Jahre (Compliance)
-
-
-
- ) : (
-
-
Software Bill of Materials (SBOM)
-
-
1. Uebersicht
-
Das SBOM-Modul generiert und analysiert die vollstaendige Komponentenliste aller Software-Abhaengigkeiten. Es dient der Compliance, Sicherheit und Supply-Chain-Transparenz.
-
-
2. SBOM-Generierung
-
-{`Source Code
- │
- v
-┌───────────────────────────────────────────────────────────────┐
-│ SBOM Generators │
-│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
-│ │ Syft │ │ Trivy │ │ Native Tooling │ │
-│ │ (Primary) │ │ (Validation)│ │ (npm, go mod, pip) │ │
-│ └──────┬──────┘ └──────┬──────┘ └──────────┬──────────┘ │
-└─────────┼────────────────┼────────────────────┼───────────────┘
- │ │ │
- └────────────────┴────────────────────┘
- │
- v
- ┌────────────────┐
- │ CycloneDX │
- │ Format │
- └────────────────┘`}
-
-
-
3. Erfasste Komponenten
-
-
-
- Typ
- Quelle
- Beispiele
-
-
-
- npm packages package-lock.json react, next, tailwindcss
- Go modules go.sum gin, gorm, jwt-go
- Python packages requirements.txt fastapi, pydantic, httpx
- Container Images Dockerfile node:20-alpine, postgres:16
- OS Packages apk, apt openssl, libpq
-
-
-
-
4. License Compliance
-
-
-
- Kategorie
- Lizenzen
- Status
-
-
-
- Permissive (erlaubt) MIT, Apache 2.0, BSD, ISC OK
- Weak Copyleft LGPL, MPL Review
- Strong Copyleft GPL, AGPL Nicht erlaubt
- Proprietaer Commercial Genehmigung
-
-
-
-
5. Aufbewahrung & Compliance
-
- Retention: 5 Jahre (Compliance)
- Format: JSON + PDF Report
- Signierung: Digital signiert
- Audit: Jederzeit abrufbar
-
-
- )}
-
- )}
-
-
-
+
)
}
diff --git a/admin-lehrer/app/(admin)/infrastructure/sbom/types.ts b/admin-lehrer/app/(admin)/infrastructure/sbom/types.ts
new file mode 100644
index 0000000..4dca401
--- /dev/null
+++ b/admin-lehrer/app/(admin)/infrastructure/sbom/types.ts
@@ -0,0 +1,49 @@
+export interface Component {
+ type: string
+ name: string
+ version: string
+ purl?: string
+ licenses?: { license: { id: string } }[]
+ category?: string
+ port?: string
+ description?: string
+ license?: string
+ sourceUrl?: string
+}
+
+export 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[]
+}
+
+export type CategoryType = 'all' | 'infrastructure' | 'security-tools' | 'python' | 'go' | 'nodejs' | 'unity' | 'csharp'
+
+export type InfoTabType = 'audit' | 'documentation'
+
+export interface SbomStats {
+ totalInfra: number
+ totalSecurityTools: number
+ totalPython: number
+ totalGo: number
+ totalNode: number
+ totalUnity: number
+ totalCsharp: number
+ totalAll: number
+ databases: number
+ services: number
+ communication: number
+ game: number
+}
+
+export interface CategoryItem {
+ id: string
+ name: string
+ count: number
+}
diff --git a/admin-lehrer/app/(admin)/infrastructure/sbom/useSbomData.ts b/admin-lehrer/app/(admin)/infrastructure/sbom/useSbomData.ts
new file mode 100644
index 0000000..9964ce6
--- /dev/null
+++ b/admin-lehrer/app/(admin)/infrastructure/sbom/useSbomData.ts
@@ -0,0 +1,150 @@
+'use client'
+
+import { useState, useEffect, useMemo } from 'react'
+import type { SBOMData, CategoryType, InfoTabType, SbomStats, CategoryItem, Component } from './types'
+import {
+ INFRASTRUCTURE_COMPONENTS,
+ SECURITY_TOOLS,
+ PYTHON_PACKAGES,
+ GO_MODULES,
+ NODE_PACKAGES,
+ UNITY_PACKAGES,
+ CSHARP_PACKAGES,
+} from './data'
+
+export function useSbomData() {
+ const [sbomData, setSbomData] = useState