[split-required] Split remaining 500-680 LOC files (final batch)
website (17 pages + 3 components): - multiplayer/wizard, middleware/wizard+test-wizard, communication - builds/wizard, staff-search, voice, sbom/wizard - foerderantrag, mail/tasks, tools/communication, sbom - compliance/evidence, uni-crawler, brandbook (already done) - CollectionsTab, IngestionTab, RiskHeatmap backend-lehrer (5 files): - letters_api (641 → 2), certificates_api (636 → 2) - alerts_agent/db/models (636 → 3) - llm_gateway/communication_service (614 → 2) - game/database already done in prior batch klausur-service (2 files): - hybrid_vocab_extractor (664 → 2) - klausur-service/frontend: api.ts (620 → 3), EHUploadWizard (591 → 2) voice-service (3 files): - bqas/rag_judge (618 → 3), runner (529 → 2) - enhanced_task_orchestrator (519 → 2) studio-v2 (6 files): - korrektur/[klausurId] (578 → 4), fairness (569 → 2) - AlertsWizard (552 → 2), OnboardingWizard (513 → 2) - korrektur/api.ts (506 → 3), geo-lernwelt (501 → 2) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
interface Evidence {
|
||||
id: string
|
||||
control_id: string
|
||||
evidence_type: string
|
||||
title: string
|
||||
description: string
|
||||
artifact_url: string | null
|
||||
file_size_bytes: number | null
|
||||
status: string
|
||||
collected_at: string
|
||||
}
|
||||
|
||||
const EVIDENCE_TYPE_ICONS: Record<string, string> = {
|
||||
scan_report: 'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z',
|
||||
policy_document: 'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z',
|
||||
config_snapshot: 'M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4',
|
||||
test_result: 'M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z',
|
||||
screenshot: 'M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z',
|
||||
external_link: 'M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14',
|
||||
manual_upload: 'M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12',
|
||||
}
|
||||
|
||||
const STATUS_STYLES: Record<string, string> = {
|
||||
valid: 'bg-green-100 text-green-700',
|
||||
expired: 'bg-red-100 text-red-700',
|
||||
pending: 'bg-yellow-100 text-yellow-700',
|
||||
failed: 'bg-red-100 text-red-700',
|
||||
}
|
||||
|
||||
function formatFileSize(bytes: number | null) {
|
||||
if (!bytes) return '-'
|
||||
if (bytes < 1024) return `${bytes} B`
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
||||
}
|
||||
|
||||
interface EvidenceCardProps {
|
||||
evidence: Evidence
|
||||
controlTitle: string
|
||||
}
|
||||
|
||||
export function EvidenceCard({ evidence: ev, controlTitle }: EvidenceCardProps) {
|
||||
const defaultIcon = 'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z'
|
||||
|
||||
return (
|
||||
<div className="bg-white rounded-xl shadow-sm border p-4 hover:border-primary-300 transition-colors">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-10 h-10 bg-slate-100 rounded-lg flex items-center justify-center">
|
||||
<svg className="w-5 h-5 text-slate-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={EVIDENCE_TYPE_ICONS[ev.evidence_type] || defaultIcon} />
|
||||
</svg>
|
||||
</div>
|
||||
<span className={`px-2 py-0.5 text-xs rounded-full ${STATUS_STYLES[ev.status] || 'bg-slate-100 text-slate-700'}`}>
|
||||
{ev.status}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-xs text-slate-500 font-mono">{controlTitle}</span>
|
||||
</div>
|
||||
<h4 className="font-medium text-slate-900 mb-1">{ev.title}</h4>
|
||||
{ev.description && <p className="text-sm text-slate-500 mb-3 line-clamp-2">{ev.description}</p>}
|
||||
<div className="flex items-center justify-between text-xs text-slate-500 pt-3 border-t">
|
||||
<span>{ev.evidence_type.replace('_', ' ')}</span>
|
||||
<span>{formatFileSize(ev.file_size_bytes)}</span>
|
||||
</div>
|
||||
{ev.artifact_url && (
|
||||
<a href={ev.artifact_url} target="_blank" rel="noopener noreferrer"
|
||||
className="mt-3 block text-sm text-primary-600 hover:text-primary-700 truncate">
|
||||
{ev.artifact_url}
|
||||
</a>
|
||||
)}
|
||||
<div className="mt-3 text-xs text-slate-400">
|
||||
Erfasst: {new Date(ev.collected_at).toLocaleDateString('de-DE')}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
import { useRef, useState } from 'react'
|
||||
|
||||
interface Control {
|
||||
id: string
|
||||
control_id: string
|
||||
title: string
|
||||
}
|
||||
|
||||
interface NewEvidenceData {
|
||||
control_id: string
|
||||
evidence_type: string
|
||||
title: string
|
||||
description: string
|
||||
artifact_url: string
|
||||
}
|
||||
|
||||
const EVIDENCE_TYPES = [
|
||||
{ value: 'scan_report', label: 'Scan Report' },
|
||||
{ value: 'policy_document', label: 'Policy Dokument' },
|
||||
{ value: 'config_snapshot', label: 'Config Snapshot' },
|
||||
{ value: 'test_result', label: 'Test Ergebnis' },
|
||||
{ value: 'screenshot', label: 'Screenshot' },
|
||||
{ value: 'external_link', label: 'Externer Link' },
|
||||
{ value: 'manual_upload', label: 'Manueller Upload' },
|
||||
]
|
||||
|
||||
function formatFileSize(bytes: number | null) {
|
||||
if (!bytes) return '-'
|
||||
if (bytes < 1024) return `${bytes} B`
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
||||
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
||||
}
|
||||
|
||||
interface UploadModalProps {
|
||||
controls: Control[]
|
||||
newEvidence: NewEvidenceData
|
||||
setNewEvidence: (data: NewEvidenceData) => void
|
||||
uploading: boolean
|
||||
onUpload: (file: File) => void
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export function UploadModal({
|
||||
controls,
|
||||
newEvidence,
|
||||
setNewEvidence,
|
||||
uploading,
|
||||
onUpload,
|
||||
onClose,
|
||||
}: UploadModalProps) {
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const [selectedFile, setSelectedFile] = useState<File | null>(null)
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-xl shadow-xl w-full max-w-lg p-6">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">Datei hochladen</h3>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Control *</label>
|
||||
<select
|
||||
value={newEvidence.control_id}
|
||||
onChange={(e) => setNewEvidence({ ...newEvidence, control_id: e.target.value })}
|
||||
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-primary-500"
|
||||
>
|
||||
<option value="">Control auswaehlen...</option>
|
||||
{controls.map((c) => (
|
||||
<option key={c.id} value={c.control_id}>{c.control_id} - {c.title}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Typ</label>
|
||||
<select
|
||||
value={newEvidence.evidence_type}
|
||||
onChange={(e) => setNewEvidence({ ...newEvidence, evidence_type: e.target.value })}
|
||||
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-primary-500"
|
||||
>
|
||||
{EVIDENCE_TYPES.filter((t) => t.value !== 'external_link').map((t) => (
|
||||
<option key={t.value} value={t.value}>{t.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Titel *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newEvidence.title}
|
||||
onChange={(e) => setNewEvidence({ ...newEvidence, title: e.target.value })}
|
||||
placeholder="z.B. Semgrep Scan Report 2026-01"
|
||||
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-primary-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Beschreibung</label>
|
||||
<textarea
|
||||
value={newEvidence.description}
|
||||
onChange={(e) => setNewEvidence({ ...newEvidence, description: e.target.value })}
|
||||
rows={2}
|
||||
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-primary-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Datei *</label>
|
||||
<input
|
||||
type="file"
|
||||
ref={fileInputRef}
|
||||
onChange={(e) => setSelectedFile(e.target.files?.[0] || null)}
|
||||
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-primary-500"
|
||||
/>
|
||||
{selectedFile && (
|
||||
<p className="mt-1 text-sm text-slate-500">
|
||||
{selectedFile.name} ({formatFileSize(selectedFile.size)})
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-3 mt-6">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 text-slate-600 hover:text-slate-800"
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button
|
||||
onClick={() => selectedFile && onUpload(selectedFile)}
|
||||
disabled={uploading || !selectedFile}
|
||||
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 disabled:opacity-50"
|
||||
>
|
||||
{uploading ? 'Hochladen...' : 'Hochladen'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
interface LinkModalProps {
|
||||
controls: Control[]
|
||||
newEvidence: NewEvidenceData
|
||||
setNewEvidence: (data: NewEvidenceData) => void
|
||||
uploading: boolean
|
||||
onSubmit: () => void
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export function LinkModal({
|
||||
controls,
|
||||
newEvidence,
|
||||
setNewEvidence,
|
||||
uploading,
|
||||
onSubmit,
|
||||
onClose,
|
||||
}: LinkModalProps) {
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-xl shadow-xl w-full max-w-lg p-6">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">Link/Quelle hinzufuegen</h3>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Control *</label>
|
||||
<select
|
||||
value={newEvidence.control_id}
|
||||
onChange={(e) => setNewEvidence({ ...newEvidence, control_id: e.target.value })}
|
||||
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-primary-500"
|
||||
>
|
||||
<option value="">Control auswaehlen...</option>
|
||||
{controls.map((c) => (
|
||||
<option key={c.id} value={c.control_id}>{c.control_id} - {c.title}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Titel *</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newEvidence.title}
|
||||
onChange={(e) => setNewEvidence({ ...newEvidence, title: e.target.value })}
|
||||
placeholder="z.B. GitHub Branch Protection Settings"
|
||||
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-primary-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">URL *</label>
|
||||
<input
|
||||
type="url"
|
||||
value={newEvidence.artifact_url}
|
||||
onChange={(e) => setNewEvidence({ ...newEvidence, artifact_url: e.target.value })}
|
||||
placeholder="https://github.com/..."
|
||||
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-primary-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Beschreibung</label>
|
||||
<textarea
|
||||
value={newEvidence.description}
|
||||
onChange={(e) => setNewEvidence({ ...newEvidence, description: e.target.value })}
|
||||
rows={2}
|
||||
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-primary-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-3 mt-6">
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="px-4 py-2 text-slate-600 hover:text-slate-800"
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button
|
||||
onClick={onSubmit}
|
||||
disabled={uploading}
|
||||
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 disabled:opacity-50"
|
||||
>
|
||||
{uploading ? 'Speichern...' : 'Hinzufuegen'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user