[split-required] Split 700-870 LOC files across all services
backend-lehrer (11 files): - llm_gateway/routes/schools.py (867 → 5), recording_api.py (848 → 6) - messenger_api.py (840 → 5), print_generator.py (824 → 5) - unit_analytics_api.py (751 → 5), classroom/routes/context.py (726 → 4) - llm_gateway/routes/edu_search_seeds.py (710 → 4) klausur-service (12 files): - ocr_labeling_api.py (845 → 4), metrics_db.py (833 → 4) - legal_corpus_api.py (790 → 4), page_crop.py (758 → 3) - mail/ai_service.py (747 → 4), github_crawler.py (767 → 3) - trocr_service.py (730 → 4), full_compliance_pipeline.py (723 → 4) - dsfa_rag_api.py (715 → 4), ocr_pipeline_auto.py (705 → 4) website (6 pages): - audit-checklist (867 → 8), content (806 → 6) - screen-flow (790 → 4), scraper (789 → 5) - zeugnisse (776 → 5), modules (745 → 4) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,91 @@
|
||||
'use client'
|
||||
|
||||
import {
|
||||
ServiceModule, RiskAssessment,
|
||||
SERVICE_TYPE_CONFIG, CRITICALITY_CONFIG, RELEVANCE_CONFIG,
|
||||
} from './types'
|
||||
|
||||
interface ModuleDetailPanelProps {
|
||||
module: ServiceModule;
|
||||
loadingDetail: boolean;
|
||||
loadingRisk: boolean;
|
||||
showRiskPanel: boolean;
|
||||
riskAssessment: RiskAssessment | null;
|
||||
onClose: () => void;
|
||||
onAssessRisk: (moduleId: string) => void;
|
||||
onCloseRisk: () => void;
|
||||
}
|
||||
|
||||
export default function ModuleDetailPanel({
|
||||
module, loadingDetail, loadingRisk, showRiskPanel, riskAssessment,
|
||||
onClose, onAssessRisk, onCloseRisk,
|
||||
}: ModuleDetailPanelProps) {
|
||||
return (
|
||||
<div className="w-96 bg-white rounded-lg shadow border sticky top-6 h-fit">
|
||||
<div className={`px-4 py-3 border-b ${SERVICE_TYPE_CONFIG[module.service_type]?.bgColor || 'bg-gray-100'}`}>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-lg">{SERVICE_TYPE_CONFIG[module.service_type]?.icon || '📁'}</span>
|
||||
<button onClick={onClose} className="text-gray-400 hover:text-gray-600">✕</button>
|
||||
</div>
|
||||
<h3 className="font-bold text-lg mt-2">{module.display_name}</h3>
|
||||
<div className="text-sm text-gray-600">{module.name}</div>
|
||||
</div>
|
||||
|
||||
{loadingDetail ? (
|
||||
<div className="p-4 text-center text-gray-500">Lade Details...</div>
|
||||
) : (
|
||||
<div className="p-4 space-y-4">
|
||||
{module.description && (<div><div className="text-xs text-gray-500 uppercase mb-1">Beschreibung</div><div className="text-sm text-gray-700">{module.description}</div></div>)}
|
||||
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||
{module.port && (<div><span className="text-gray-500">Port:</span><span className="ml-1 font-mono">{module.port}</span></div>)}
|
||||
<div><span className="text-gray-500">Criticality:</span><span className={`ml-1 px-1.5 py-0.5 rounded text-xs ${CRITICALITY_CONFIG[module.criticality]?.bgColor || ''} ${CRITICALITY_CONFIG[module.criticality]?.color || ''}`}>{module.criticality}</span></div>
|
||||
</div>
|
||||
<div><div className="text-xs text-gray-500 uppercase mb-1">Tech Stack</div><div className="flex flex-wrap gap-1">{module.technology_stack.map((tech, i) => (<span key={i} className="px-2 py-0.5 bg-gray-100 text-gray-700 text-xs rounded">{tech}</span>))}</div></div>
|
||||
{module.data_categories.length > 0 && (<div><div className="text-xs text-gray-500 uppercase mb-1">Daten-Kategorien</div><div className="flex flex-wrap gap-1">{module.data_categories.map((cat, i) => (<span key={i} className="px-2 py-0.5 bg-blue-50 text-blue-700 text-xs rounded">{cat}</span>))}</div></div>)}
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{module.processes_pii && (<span className="px-2 py-1 bg-purple-100 text-purple-700 text-xs rounded">Verarbeitet PII</span>)}
|
||||
{module.ai_components && (<span className="px-2 py-1 bg-pink-100 text-pink-700 text-xs rounded">AI-Komponenten</span>)}
|
||||
{module.processes_health_data && (<span className="px-2 py-1 bg-red-100 text-red-700 text-xs rounded">Gesundheitsdaten</span>)}
|
||||
</div>
|
||||
{module.regulations && module.regulations.length > 0 && (
|
||||
<div>
|
||||
<div className="text-xs text-gray-500 uppercase mb-2">Applicable Regulations ({module.regulations.length})</div>
|
||||
<div className="space-y-2">
|
||||
{module.regulations.map((reg, i) => (
|
||||
<div key={i} className="p-2 bg-gray-50 rounded text-sm">
|
||||
<div className="flex justify-between items-start"><span className="font-medium">{reg.code}</span><span className={`px-1.5 py-0.5 rounded text-xs ${RELEVANCE_CONFIG[reg.relevance_level]?.bgColor || 'bg-gray-100'} ${RELEVANCE_CONFIG[reg.relevance_level]?.color || 'text-gray-700'}`}>{reg.relevance_level}</span></div>
|
||||
<div className="text-gray-500 text-xs">{reg.name}</div>
|
||||
{reg.notes && (<div className="text-gray-600 text-xs mt-1 italic">{reg.notes}</div>)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{module.owner_team && (<div><div className="text-xs text-gray-500 uppercase mb-1">Owner</div><div className="text-sm text-gray-700">{module.owner_team}</div></div>)}
|
||||
{module.repository_path && (<div><div className="text-xs text-gray-500 uppercase mb-1">Repository</div><code className="text-xs bg-gray-100 px-2 py-1 rounded block">{module.repository_path}</code></div>)}
|
||||
<div className="pt-2 border-t">
|
||||
<button onClick={() => onAssessRisk(module.id)} disabled={loadingRisk} className="w-full px-4 py-2 bg-gradient-to-r from-purple-600 to-pink-600 text-white rounded-lg hover:from-purple-700 hover:to-pink-700 transition disabled:opacity-50 flex items-center justify-center gap-2">
|
||||
{loadingRisk ? (<><span className="animate-spin">⚙️</span>AI analysiert...</>) : (<><span>🤖</span>AI Risikobewertung</>)}
|
||||
</button>
|
||||
</div>
|
||||
{showRiskPanel && (
|
||||
<div className="mt-4 p-4 bg-gradient-to-br from-purple-50 to-pink-50 rounded-lg border border-purple-200">
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<h4 className="font-semibold text-purple-900 flex items-center gap-2"><span>🤖</span> AI Risikobewertung</h4>
|
||||
<button onClick={onCloseRisk} className="text-purple-400 hover:text-purple-600">✕</button>
|
||||
</div>
|
||||
{loadingRisk ? (<div className="text-center py-4 text-purple-600"><div className="animate-pulse">Analysiere Compliance-Risiken...</div></div>) : riskAssessment ? (
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-center gap-2"><span className="text-sm text-gray-600">Gesamtrisiko:</span><span className={`px-2 py-1 rounded text-sm font-medium ${riskAssessment.overall_risk === 'critical' ? 'bg-red-100 text-red-700' : riskAssessment.overall_risk === 'high' ? 'bg-orange-100 text-orange-700' : riskAssessment.overall_risk === 'medium' ? 'bg-yellow-100 text-yellow-700' : 'bg-green-100 text-green-700'}`}>{riskAssessment.overall_risk.toUpperCase()}</span><span className="text-xs text-gray-400">({Math.round(riskAssessment.confidence_score * 100)}% Konfidenz)</span></div>
|
||||
{riskAssessment.risk_factors.length > 0 && (<div><div className="text-xs text-gray-500 uppercase mb-1">Risikofaktoren</div><div className="space-y-1">{riskAssessment.risk_factors.map((factor, i) => (<div key={i} className="flex items-center justify-between text-sm bg-white/50 rounded px-2 py-1"><span className="text-gray-700">{factor.factor}</span><span className={`text-xs px-1.5 py-0.5 rounded ${factor.severity === 'critical' || factor.severity === 'high' ? 'bg-red-100 text-red-600' : 'bg-yellow-100 text-yellow-600'}`}>{factor.severity}</span></div>))}</div></div>)}
|
||||
{riskAssessment.compliance_gaps.length > 0 && (<div><div className="text-xs text-gray-500 uppercase mb-1">Compliance-Luecken</div><ul className="text-sm text-gray-700 space-y-1">{riskAssessment.compliance_gaps.map((gap, i) => (<li key={i} className="flex items-start gap-1"><span className="text-red-500">⚠</span><span>{gap}</span></li>))}</ul></div>)}
|
||||
{riskAssessment.recommendations.length > 0 && (<div><div className="text-xs text-gray-500 uppercase mb-1">Empfehlungen</div><ul className="text-sm text-gray-700 space-y-1">{riskAssessment.recommendations.map((rec, i) => (<li key={i} className="flex items-start gap-1"><span className="text-green-500">→</span><span>{rec}</span></li>))}</ul></div>)}
|
||||
</div>
|
||||
) : (<div className="text-center py-4 text-gray-500 text-sm">Klicken Sie auf "AI Risikobewertung" um eine Analyse zu starten.</div>)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
71
website/app/admin/compliance/modules/_components/types.ts
Normal file
71
website/app/admin/compliance/modules/_components/types.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
export interface ServiceModule {
|
||||
id: string;
|
||||
name: string;
|
||||
display_name: string;
|
||||
description: string | null;
|
||||
service_type: string;
|
||||
port: number | null;
|
||||
technology_stack: string[];
|
||||
repository_path: string | null;
|
||||
docker_image: string | null;
|
||||
data_categories: string[];
|
||||
processes_pii: boolean;
|
||||
processes_health_data: boolean;
|
||||
ai_components: boolean;
|
||||
criticality: string;
|
||||
owner_team: string | null;
|
||||
is_active: boolean;
|
||||
compliance_score: number | null;
|
||||
regulation_count: number;
|
||||
risk_count: number;
|
||||
created_at: string;
|
||||
regulations?: Array<{
|
||||
code: string;
|
||||
name: string;
|
||||
relevance_level: string;
|
||||
notes: string | null;
|
||||
}>;
|
||||
}
|
||||
|
||||
export interface ModulesOverview {
|
||||
total_modules: number;
|
||||
modules_by_type: Record<string, number>;
|
||||
modules_by_criticality: Record<string, number>;
|
||||
modules_processing_pii: number;
|
||||
modules_with_ai: number;
|
||||
average_compliance_score: number | null;
|
||||
regulations_coverage: Record<string, number>;
|
||||
}
|
||||
|
||||
export interface RiskAssessment {
|
||||
overall_risk: string;
|
||||
risk_factors: Array<{ factor: string; severity: string; likelihood: string }>;
|
||||
recommendations: string[];
|
||||
compliance_gaps: string[];
|
||||
confidence_score: number;
|
||||
}
|
||||
|
||||
export const SERVICE_TYPE_CONFIG: Record<string, { icon: string; color: string; bgColor: string }> = {
|
||||
backend: { icon: '⚙️', color: 'text-blue-700', bgColor: 'bg-blue-100' },
|
||||
database: { icon: '🗄️', color: 'text-purple-700', bgColor: 'bg-purple-100' },
|
||||
ai: { icon: '🤖', color: 'text-pink-700', bgColor: 'bg-pink-100' },
|
||||
communication: { icon: '💬', color: 'text-green-700', bgColor: 'bg-green-100' },
|
||||
storage: { icon: '📦', color: 'text-orange-700', bgColor: 'bg-orange-100' },
|
||||
infrastructure: { icon: '🌐', color: 'text-gray-700', bgColor: 'bg-gray-100' },
|
||||
monitoring: { icon: '📊', color: 'text-cyan-700', bgColor: 'bg-cyan-100' },
|
||||
security: { icon: '🔒', color: 'text-red-700', bgColor: 'bg-red-100' },
|
||||
};
|
||||
|
||||
export const CRITICALITY_CONFIG: Record<string, { color: string; bgColor: string }> = {
|
||||
critical: { color: 'text-red-700', bgColor: 'bg-red-100' },
|
||||
high: { color: 'text-orange-700', bgColor: 'bg-orange-100' },
|
||||
medium: { color: 'text-yellow-700', bgColor: 'bg-yellow-100' },
|
||||
low: { color: 'text-green-700', bgColor: 'bg-green-100' },
|
||||
};
|
||||
|
||||
export const RELEVANCE_CONFIG: Record<string, { color: string; bgColor: string }> = {
|
||||
critical: { color: 'text-red-700', bgColor: 'bg-red-100' },
|
||||
high: { color: 'text-orange-700', bgColor: 'bg-orange-100' },
|
||||
medium: { color: 'text-yellow-700', bgColor: 'bg-yellow-100' },
|
||||
low: { color: 'text-green-700', bgColor: 'bg-green-100' },
|
||||
};
|
||||
@@ -0,0 +1,110 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { ServiceModule, ModulesOverview, RiskAssessment } from './types'
|
||||
|
||||
const BACKEND_URL = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8000'
|
||||
const API_BASE = `${BACKEND_URL}/api/v1/compliance`
|
||||
|
||||
export function useModulesPage() {
|
||||
const [modules, setModules] = useState<ServiceModule[]>([])
|
||||
const [overview, setOverview] = useState<ModulesOverview | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const [typeFilter, setTypeFilter] = useState<string>('all')
|
||||
const [criticalityFilter, setCriticalityFilter] = useState<string>('all')
|
||||
const [piiFilter, setPiiFilter] = useState<boolean | null>(null)
|
||||
const [aiFilter, setAiFilter] = useState<boolean | null>(null)
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
|
||||
const [selectedModule, setSelectedModule] = useState<ServiceModule | null>(null)
|
||||
const [loadingDetail, setLoadingDetail] = useState(false)
|
||||
|
||||
const [riskAssessment, setRiskAssessment] = useState<RiskAssessment | null>(null)
|
||||
const [loadingRisk, setLoadingRisk] = useState(false)
|
||||
const [showRiskPanel, setShowRiskPanel] = useState(false)
|
||||
|
||||
useEffect(() => { fetchModules(); fetchOverview() }, [])
|
||||
|
||||
const fetchModules = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const params = new URLSearchParams()
|
||||
if (typeFilter !== 'all') params.append('service_type', typeFilter)
|
||||
if (criticalityFilter !== 'all') params.append('criticality', criticalityFilter)
|
||||
if (piiFilter !== null) params.append('processes_pii', String(piiFilter))
|
||||
if (aiFilter !== null) params.append('ai_components', String(aiFilter))
|
||||
const url = `${API_BASE}/modules${params.toString() ? '?' + params.toString() : ''}`
|
||||
const res = await fetch(url)
|
||||
if (!res.ok) throw new Error('Failed to fetch modules')
|
||||
const data = await res.json()
|
||||
setModules(data.modules || [])
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Unknown error')
|
||||
} finally { setLoading(false) }
|
||||
}
|
||||
|
||||
const fetchOverview = async () => {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/modules/overview`)
|
||||
if (!res.ok) throw new Error('Failed to fetch overview')
|
||||
const data = await res.json()
|
||||
setOverview(data)
|
||||
} catch (err) { console.error('Failed to fetch overview:', err) }
|
||||
}
|
||||
|
||||
const fetchModuleDetail = async (moduleId: string) => {
|
||||
try {
|
||||
setLoadingDetail(true)
|
||||
const res = await fetch(`${API_BASE}/modules/${moduleId}`)
|
||||
if (!res.ok) throw new Error('Failed to fetch module details')
|
||||
const data = await res.json()
|
||||
setSelectedModule(data)
|
||||
} catch (err) { console.error('Failed to fetch module details:', err) }
|
||||
finally { setLoadingDetail(false) }
|
||||
}
|
||||
|
||||
const seedModules = async (force: boolean = false) => {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/modules/seed`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ force }) })
|
||||
if (!res.ok) throw new Error('Failed to seed modules')
|
||||
const data = await res.json()
|
||||
alert(`Seeded ${data.modules_created} modules with ${data.mappings_created} regulation mappings`)
|
||||
fetchModules(); fetchOverview()
|
||||
} catch (err) { alert('Failed to seed modules: ' + (err instanceof Error ? err.message : 'Unknown error')) }
|
||||
}
|
||||
|
||||
const assessModuleRisk = async (moduleId: string) => {
|
||||
setLoadingRisk(true); setShowRiskPanel(true); setRiskAssessment(null)
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/ai/assess-risk`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ module_id: moduleId }) })
|
||||
if (res.ok) { const data = await res.json(); setRiskAssessment(data) }
|
||||
else { alert('AI-Risikobewertung fehlgeschlagen') }
|
||||
} catch (err) { alert('Netzwerkfehler bei AI-Risikobewertung') }
|
||||
finally { setLoadingRisk(false) }
|
||||
}
|
||||
|
||||
const filteredModules = modules.filter(m => {
|
||||
if (!searchTerm) return true
|
||||
const term = searchTerm.toLowerCase()
|
||||
return m.name.toLowerCase().includes(term) || m.display_name.toLowerCase().includes(term) || (m.description && m.description.toLowerCase().includes(term)) || m.technology_stack.some(t => t.toLowerCase().includes(term))
|
||||
})
|
||||
|
||||
const modulesByType = filteredModules.reduce((acc, m) => {
|
||||
const type = m.service_type || 'unknown'
|
||||
if (!acc[type]) acc[type] = []
|
||||
acc[type].push(m)
|
||||
return acc
|
||||
}, {} as Record<string, ServiceModule[]>)
|
||||
|
||||
return {
|
||||
modules, overview, loading, error,
|
||||
typeFilter, setTypeFilter, criticalityFilter, setCriticalityFilter,
|
||||
piiFilter, setPiiFilter, aiFilter, setAiFilter, searchTerm, setSearchTerm,
|
||||
selectedModule, setSelectedModule, loadingDetail,
|
||||
riskAssessment, loadingRisk, showRiskPanel, setShowRiskPanel,
|
||||
filteredModules, modulesByType,
|
||||
fetchModules, fetchModuleDetail, seedModules, assessModuleRisk,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user