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>
123 lines
9.7 KiB
TypeScript
123 lines
9.7 KiB
TypeScript
'use client';
|
|
|
|
import { SERVICE_TYPE_CONFIG, CRITICALITY_CONFIG } from './_components/types';
|
|
import { useModulesPage } from './_components/useModulesPage';
|
|
import ModuleDetailPanel from './_components/ModuleDetailPanel';
|
|
|
|
export default function ModulesPage() {
|
|
const mp = useModulesPage();
|
|
|
|
return (
|
|
<div className="p-6 space-y-6">
|
|
{/* Header */}
|
|
<div className="flex justify-between items-center">
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-gray-900">Service Module Registry</h1>
|
|
<p className="text-gray-600 mt-1">Alle {mp.overview?.total_modules || 0} Breakpilot-Services mit Regulation-Mappings</p>
|
|
</div>
|
|
<div className="flex gap-2">
|
|
<button onClick={() => mp.seedModules(false)} className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition">Seed Modules</button>
|
|
<button onClick={() => mp.seedModules(true)} className="px-4 py-2 bg-gray-600 text-white rounded-lg hover:bg-gray-700 transition">Force Re-Seed</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Overview Stats */}
|
|
{mp.overview && (
|
|
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-4">
|
|
<div className="bg-white rounded-lg p-4 shadow border"><div className="text-3xl font-bold text-blue-600">{mp.overview.total_modules}</div><div className="text-sm text-gray-600">Services</div></div>
|
|
<div className="bg-white rounded-lg p-4 shadow border"><div className="text-3xl font-bold text-red-600">{mp.overview.modules_by_criticality?.critical || 0}</div><div className="text-sm text-gray-600">Critical</div></div>
|
|
<div className="bg-white rounded-lg p-4 shadow border"><div className="text-3xl font-bold text-purple-600">{mp.overview.modules_processing_pii}</div><div className="text-sm text-gray-600">PII-Processing</div></div>
|
|
<div className="bg-white rounded-lg p-4 shadow border"><div className="text-3xl font-bold text-pink-600">{mp.overview.modules_with_ai}</div><div className="text-sm text-gray-600">AI-Komponenten</div></div>
|
|
<div className="bg-white rounded-lg p-4 shadow border"><div className="text-3xl font-bold text-green-600">{Object.keys(mp.overview.regulations_coverage || {}).length}</div><div className="text-sm text-gray-600">Regulations</div></div>
|
|
<div className="bg-white rounded-lg p-4 shadow border"><div className="text-3xl font-bold text-cyan-600">{mp.overview.average_compliance_score !== null ? `${mp.overview.average_compliance_score}%` : 'N/A'}</div><div className="text-sm text-gray-600">Avg. Score</div></div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Filters */}
|
|
<div className="bg-white rounded-lg p-4 shadow border">
|
|
<div className="flex flex-wrap gap-4 items-center">
|
|
<div><label className="block text-xs text-gray-500 mb-1">Service Type</label><select value={mp.typeFilter} onChange={(e) => mp.setTypeFilter(e.target.value)} className="border rounded px-3 py-2 text-sm"><option value="all">Alle Typen</option>{['backend','database','ai','communication','storage','infrastructure','monitoring','security'].map(t => (<option key={t} value={t}>{t.charAt(0).toUpperCase()+t.slice(1)}</option>))}</select></div>
|
|
<div><label className="block text-xs text-gray-500 mb-1">Criticality</label><select value={mp.criticalityFilter} onChange={(e) => mp.setCriticalityFilter(e.target.value)} className="border rounded px-3 py-2 text-sm"><option value="all">Alle</option>{['critical','high','medium','low'].map(c => (<option key={c} value={c}>{c.charAt(0).toUpperCase()+c.slice(1)}</option>))}</select></div>
|
|
<div><label className="block text-xs text-gray-500 mb-1">PII</label><select value={mp.piiFilter === null ? 'all' : String(mp.piiFilter)} onChange={(e) => { const val = e.target.value; mp.setPiiFilter(val === 'all' ? null : val === 'true') }} className="border rounded px-3 py-2 text-sm"><option value="all">Alle</option><option value="true">Verarbeitet PII</option><option value="false">Keine PII</option></select></div>
|
|
<div><label className="block text-xs text-gray-500 mb-1">AI</label><select value={mp.aiFilter === null ? 'all' : String(mp.aiFilter)} onChange={(e) => { const val = e.target.value; mp.setAiFilter(val === 'all' ? null : val === 'true') }} className="border rounded px-3 py-2 text-sm"><option value="all">Alle</option><option value="true">Mit AI</option><option value="false">Ohne AI</option></select></div>
|
|
<div className="flex-1"><label className="block text-xs text-gray-500 mb-1">Suche</label><input type="text" placeholder="Service, Beschreibung, Technologie..." value={mp.searchTerm} onChange={(e) => mp.setSearchTerm(e.target.value)} className="border rounded px-3 py-2 text-sm w-full" /></div>
|
|
<div className="pt-5"><button onClick={mp.fetchModules} className="px-4 py-2 bg-gray-100 rounded hover:bg-gray-200 transition text-sm">Filter anwenden</button></div>
|
|
</div>
|
|
</div>
|
|
|
|
{mp.error && (<div className="bg-red-50 border border-red-200 text-red-700 p-4 rounded-lg">{mp.error}</div>)}
|
|
{mp.loading && (<div className="text-center py-12 text-gray-500">Lade Module...</div>)}
|
|
|
|
{/* Main Content */}
|
|
{!mp.loading && (
|
|
<div className="flex gap-6">
|
|
<div className="flex-1 space-y-4">
|
|
{Object.entries(mp.modulesByType).map(([type, typeModules]) => (
|
|
<div key={type} className="bg-white rounded-lg shadow border">
|
|
<div className={`px-4 py-2 border-b ${SERVICE_TYPE_CONFIG[type]?.bgColor || 'bg-gray-100'}`}>
|
|
<span className="text-lg mr-2">{SERVICE_TYPE_CONFIG[type]?.icon || '📁'}</span>
|
|
<span className={`font-semibold ${SERVICE_TYPE_CONFIG[type]?.color || 'text-gray-700'}`}>{type.charAt(0).toUpperCase() + type.slice(1)}</span>
|
|
<span className="text-gray-500 ml-2">({typeModules.length})</span>
|
|
</div>
|
|
<div className="divide-y">
|
|
{typeModules.map((mod) => (
|
|
<div key={mod.id} onClick={() => mp.fetchModuleDetail(mod.name)} className={`p-4 cursor-pointer hover:bg-gray-50 transition ${mp.selectedModule?.id === mod.id ? 'bg-blue-50' : ''}`}>
|
|
<div className="flex justify-between items-start">
|
|
<div className="flex-1">
|
|
<div className="flex items-center gap-2"><span className="font-medium text-gray-900">{mod.display_name}</span>{mod.port && (<span className="text-xs text-gray-400">:{mod.port}</span>)}</div>
|
|
<div className="text-sm text-gray-500 mt-1">{mod.name}</div>
|
|
{mod.description && (<div className="text-sm text-gray-600 mt-1 line-clamp-2">{mod.description}</div>)}
|
|
<div className="flex flex-wrap gap-1 mt-2">
|
|
{mod.technology_stack.slice(0, 4).map((tech, i) => (<span key={i} className="px-2 py-0.5 bg-gray-100 text-gray-600 text-xs rounded">{tech}</span>))}
|
|
{mod.technology_stack.length > 4 && (<span className="px-2 py-0.5 text-gray-400 text-xs">+{mod.technology_stack.length - 4}</span>)}
|
|
</div>
|
|
</div>
|
|
<div className="flex flex-col items-end gap-1">
|
|
<span className={`px-2 py-0.5 text-xs rounded ${CRITICALITY_CONFIG[mod.criticality]?.bgColor || 'bg-gray-100'} ${CRITICALITY_CONFIG[mod.criticality]?.color || 'text-gray-700'}`}>{mod.criticality}</span>
|
|
<div className="flex gap-1 mt-1">
|
|
{mod.processes_pii && (<span className="px-1.5 py-0.5 bg-purple-100 text-purple-700 text-xs rounded" title="Verarbeitet PII">PII</span>)}
|
|
{mod.ai_components && (<span className="px-1.5 py-0.5 bg-pink-100 text-pink-700 text-xs rounded" title="AI-Komponenten">AI</span>)}
|
|
</div>
|
|
<div className="text-xs text-gray-400 mt-1">{mod.regulation_count} Regulations</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
))}
|
|
{mp.filteredModules.length === 0 && !mp.loading && (
|
|
<div className="text-center py-12 text-gray-500 bg-white rounded-lg shadow border">Keine Module gefunden.<button onClick={() => mp.seedModules(false)} className="text-blue-600 hover:underline ml-1">Jetzt seeden?</button></div>
|
|
)}
|
|
</div>
|
|
|
|
{mp.selectedModule && (
|
|
<ModuleDetailPanel
|
|
module={mp.selectedModule}
|
|
loadingDetail={mp.loadingDetail}
|
|
loadingRisk={mp.loadingRisk}
|
|
showRiskPanel={mp.showRiskPanel}
|
|
riskAssessment={mp.riskAssessment}
|
|
onClose={() => mp.setSelectedModule(null)}
|
|
onAssessRisk={mp.assessModuleRisk}
|
|
onCloseRisk={() => mp.setShowRiskPanel(false)}
|
|
/>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Regulations Coverage */}
|
|
{mp.overview && mp.overview.regulations_coverage && Object.keys(mp.overview.regulations_coverage).length > 0 && (
|
|
<div className="bg-white rounded-lg shadow border p-4">
|
|
<h3 className="font-semibold text-gray-900 mb-4">Regulation Coverage</h3>
|
|
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-3">
|
|
{Object.entries(mp.overview.regulations_coverage).sort(([, a], [, b]) => b - a).map(([code, count]) => (
|
|
<div key={code} className="bg-gray-50 rounded p-3 text-center"><div className="text-2xl font-bold text-blue-600">{count}</div><div className="text-xs text-gray-600 truncate" title={code}>{code}</div></div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|