[split-required] Split website + studio-v2 monoliths (Phase 3 continued)
Website (14 monoliths split): - compliance/page.tsx (1,519 → 9), docs/audit (1,262 → 20) - quality (1,231 → 16), alerts (1,203 → 10), docs (1,202 → 11) - i18n.ts (1,173 → 8 language files) - unity-bridge (1,094 → 12), backlog (1,087 → 6) - training (1,066 → 8), rag (1,063 → 8) - Deleted index_original.ts (4,899 LOC dead backup) Studio-v2 (5 monoliths split): - meet/page.tsx (1,481 → 9), messages (1,166 → 9) - AlertsB2BContext.tsx (1,165 → 5 modules) - alerts-b2b/page.tsx (1,019 → 6), korrektur/archiv (1,001 → 6) All existing imports preserved. Zero new TypeScript errors. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
67
website/app/admin/docs/_components/ApiReferenceTab.tsx
Normal file
67
website/app/admin/docs/_components/ApiReferenceTab.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { services } from '../data'
|
||||
import { getServiceTypeColor, getMethodColor } from '../helpers'
|
||||
|
||||
export default function ApiReferenceTab() {
|
||||
const [copiedEndpoint, setCopiedEndpoint] = useState<string | null>(null)
|
||||
|
||||
const copyToClipboard = (text: string, id: string) => {
|
||||
navigator.clipboard.writeText(text)
|
||||
setCopiedEndpoint(id)
|
||||
setTimeout(() => setCopiedEndpoint(null), 2000)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{services.filter(s => s.endpoints.length > 0).map((service) => (
|
||||
<div key={service.id} className="bg-white rounded-xl border border-slate-200 overflow-hidden">
|
||||
<div className="px-6 py-4 bg-slate-50 border-b border-slate-200">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h3 className="font-semibold text-slate-900">{service.name}</h3>
|
||||
<div className="text-sm text-slate-500">Base URL: http://localhost:{service.port}</div>
|
||||
</div>
|
||||
<span className={`text-xs px-2 py-0.5 rounded-full ${getServiceTypeColor(service.type)}`}>
|
||||
{service.type}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="divide-y divide-slate-100">
|
||||
{service.endpoints.map((endpoint, idx) => {
|
||||
const endpointId = `${service.id}-${idx}`
|
||||
const curlCommand = `curl -X ${endpoint.method} http://localhost:${service.port}${endpoint.path}`
|
||||
|
||||
return (
|
||||
<div key={idx} className="px-6 py-3 hover:bg-slate-50 transition-colors">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className={`text-xs font-mono font-semibold px-2 py-1 rounded ${getMethodColor(endpoint.method)}`}>
|
||||
{endpoint.method}
|
||||
</span>
|
||||
<code className="text-sm font-mono text-slate-700 flex-1">{endpoint.path}</code>
|
||||
<button
|
||||
onClick={() => copyToClipboard(curlCommand, endpointId)}
|
||||
className="text-xs text-slate-400 hover:text-slate-600 transition-colors"
|
||||
title="Copy curl command"
|
||||
>
|
||||
{copiedEndpoint === endpointId ? (
|
||||
<span className="text-green-600">Copied!</span>
|
||||
) : (
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<div className="text-sm text-slate-500 mt-1 ml-14">{endpoint.description}</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
116
website/app/admin/docs/_components/DockerTab.tsx
Normal file
116
website/app/admin/docs/_components/DockerTab.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { services } from '../data'
|
||||
import { getServiceTypeColor } from '../helpers'
|
||||
|
||||
export default function DockerTab() {
|
||||
const [copiedEndpoint, setCopiedEndpoint] = useState<string | null>(null)
|
||||
|
||||
const copyToClipboard = (text: string, id: string) => {
|
||||
navigator.clipboard.writeText(text)
|
||||
setCopiedEndpoint(id)
|
||||
setTimeout(() => setCopiedEndpoint(null), 2000)
|
||||
}
|
||||
|
||||
const commonCommands = [
|
||||
{ label: 'Alle Services starten', cmd: 'docker compose up -d' },
|
||||
{ label: 'Logs anzeigen', cmd: 'docker compose logs -f [service]' },
|
||||
{ label: 'Service neu bauen', cmd: 'docker compose build [service] --no-cache' },
|
||||
{ label: 'Container Status', cmd: 'docker ps --format "table {{.Names}}\\t{{.Status}}\\t{{.Ports}}"' },
|
||||
{ label: 'In Container einloggen', cmd: 'docker exec -it [container] /bin/sh' },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6">
|
||||
<h2 className="text-lg font-semibold text-slate-900 mb-4">Docker Compose Services</h2>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-200">
|
||||
<th className="text-left py-3 px-4 font-medium text-slate-700">Container</th>
|
||||
<th className="text-left py-3 px-4 font-medium text-slate-700">Port</th>
|
||||
<th className="text-left py-3 px-4 font-medium text-slate-700">Type</th>
|
||||
<th className="text-left py-3 px-4 font-medium text-slate-700">Health Check</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-100">
|
||||
{services.map((service) => (
|
||||
<tr key={service.id} className="hover:bg-slate-50">
|
||||
<td className="py-3 px-4">
|
||||
<code className="text-sm bg-slate-100 px-2 py-0.5 rounded">{service.container}</code>
|
||||
</td>
|
||||
<td className="py-3 px-4 font-mono">{service.port}</td>
|
||||
<td className="py-3 px-4">
|
||||
<span className={`text-xs px-2 py-0.5 rounded-full ${getServiceTypeColor(service.type)}`}>
|
||||
{service.type}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
{service.healthEndpoint ? (
|
||||
<code className="text-xs bg-green-50 text-green-700 px-2 py-0.5 rounded">
|
||||
{service.healthEndpoint}
|
||||
</code>
|
||||
) : (
|
||||
<span className="text-slate-400">-</span>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Common Commands */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6">
|
||||
<h2 className="text-lg font-semibold text-slate-900 mb-4">Haeufige Befehle</h2>
|
||||
|
||||
<div className="space-y-4">
|
||||
{commonCommands.map((item, idx) => (
|
||||
<div key={idx} className="flex items-center gap-4 p-3 bg-slate-50 rounded-lg">
|
||||
<div className="text-sm text-slate-600 w-40">{item.label}</div>
|
||||
<code className="flex-1 text-sm font-mono bg-slate-900 text-green-400 px-3 py-2 rounded">
|
||||
{item.cmd}
|
||||
</code>
|
||||
<button
|
||||
onClick={() => copyToClipboard(item.cmd, `cmd-${idx}`)}
|
||||
className="text-slate-400 hover:text-slate-600"
|
||||
>
|
||||
{copiedEndpoint === `cmd-${idx}` ? (
|
||||
<span className="text-xs text-green-600">Copied!</span>
|
||||
) : (
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Environment Variables */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6">
|
||||
<h2 className="text-lg font-semibold text-slate-900 mb-4">Wichtige Umgebungsvariablen</h2>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{services.filter(s => s.envVars.length > 0).map((service) => (
|
||||
<div key={service.id} className="p-4 bg-slate-50 rounded-lg">
|
||||
<h4 className="font-medium text-slate-900 mb-2">{service.name}</h4>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{service.envVars.map((env) => (
|
||||
<code key={env} className="text-xs bg-slate-200 text-slate-700 px-2 py-1 rounded">
|
||||
{env}
|
||||
</code>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
229
website/app/admin/docs/_components/OverviewTab.tsx
Normal file
229
website/app/admin/docs/_components/OverviewTab.tsx
Normal file
@@ -0,0 +1,229 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import type { ServiceNode } from '../types'
|
||||
import { ARCHITECTURE_SERVICES, LAYERS, DATAFLOW_DIAGRAM } from '../data'
|
||||
import { getArchTypeColor, getArchTypeLabel } from '../helpers'
|
||||
import ServiceDetailPanel from './ServiceDetailPanel'
|
||||
|
||||
export default function OverviewTab() {
|
||||
const [selectedArchService, setSelectedArchService] = useState<ServiceNode | null>(null)
|
||||
const [activeLayer, setActiveLayer] = useState<string>('all')
|
||||
|
||||
const getServicesForLayer = (layer: typeof LAYERS[0]) => {
|
||||
return ARCHITECTURE_SERVICES.filter(s => layer.types.includes(s.type))
|
||||
}
|
||||
|
||||
const archStats = {
|
||||
total: ARCHITECTURE_SERVICES.length,
|
||||
frontends: ARCHITECTURE_SERVICES.filter(s => s.type === 'frontend').length,
|
||||
backends: ARCHITECTURE_SERVICES.filter(s => s.type === 'backend').length,
|
||||
databases: ARCHITECTURE_SERVICES.filter(s => s.type === 'database').length,
|
||||
infrastructure: ARCHITECTURE_SERVICES.filter(s => ['cache', 'search', 'storage', 'security', 'communication', 'ai', 'erp'].includes(s.type)).length,
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-4">
|
||||
<div className="bg-white rounded-lg shadow p-4">
|
||||
<div className="text-3xl font-bold text-slate-800">{archStats.total}</div>
|
||||
<div className="text-sm text-slate-500">Services Total</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg shadow p-4">
|
||||
<div className="text-3xl font-bold text-blue-600">{archStats.frontends}</div>
|
||||
<div className="text-sm text-slate-500">Frontends</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg shadow p-4">
|
||||
<div className="text-3xl font-bold text-green-600">{archStats.backends}</div>
|
||||
<div className="text-sm text-slate-500">Backends</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg shadow p-4">
|
||||
<div className="text-3xl font-bold text-purple-600">{archStats.databases}</div>
|
||||
<div className="text-sm text-slate-500">Datenbanken</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-lg shadow p-4">
|
||||
<div className="text-3xl font-bold text-orange-600">{archStats.infrastructure}</div>
|
||||
<div className="text-sm text-slate-500">Infrastruktur</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ASCII Architecture Diagram with Arrows */}
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<h2 className="text-xl font-bold text-slate-800 mb-4">Datenfluss-Diagramm</h2>
|
||||
<div className="bg-slate-900 rounded-lg p-6 overflow-x-auto">
|
||||
<pre className="text-green-400 font-mono text-xs whitespace-pre">{DATAFLOW_DIAGRAM}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Layer Filter */}
|
||||
<div className="bg-white rounded-lg shadow p-4">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
onClick={() => setActiveLayer('all')}
|
||||
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||
activeLayer === 'all'
|
||||
? 'bg-primary-600 text-white'
|
||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
||||
}`}
|
||||
>
|
||||
Alle Layer
|
||||
</button>
|
||||
{LAYERS.map((layer) => (
|
||||
<button
|
||||
key={layer.id}
|
||||
onClick={() => setActiveLayer(layer.id)}
|
||||
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||
activeLayer === layer.id
|
||||
? 'bg-primary-600 text-white'
|
||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
||||
}`}
|
||||
>
|
||||
{layer.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Architecture Diagram - Layered View */}
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<h2 className="text-xl font-bold text-slate-800 mb-6">System-Architektur Diagramm</h2>
|
||||
|
||||
<div className="space-y-6">
|
||||
{LAYERS.map((layer) => {
|
||||
const layerServices = getServicesForLayer(layer)
|
||||
if (activeLayer !== 'all' && activeLayer !== layer.id) return null
|
||||
|
||||
return (
|
||||
<div key={layer.id} className="border-2 border-dashed border-slate-200 rounded-xl p-4">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<h3 className="text-lg font-semibold text-slate-700">{layer.name}</h3>
|
||||
<span className="text-sm text-slate-500">- {layer.description}</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3">
|
||||
{layerServices.map((service) => {
|
||||
const colors = getArchTypeColor(service.type)
|
||||
const isSelected = selectedArchService?.id === service.id
|
||||
|
||||
return (
|
||||
<div
|
||||
key={service.id}
|
||||
onClick={() => setSelectedArchService(isSelected ? null : service)}
|
||||
className={`cursor-pointer rounded-lg border-2 p-3 transition-all ${
|
||||
isSelected
|
||||
? `${colors.border} ${colors.light} shadow-lg scale-105`
|
||||
: 'border-slate-200 bg-white hover:border-slate-300 hover:shadow'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<div className={`w-3 h-3 rounded-full ${colors.bg}`}></div>
|
||||
{service.port && service.port !== '-' && (
|
||||
<span className="text-xs font-mono text-slate-500">:{service.port}</span>
|
||||
)}
|
||||
</div>
|
||||
<h4 className="font-medium text-sm text-slate-800">{service.name}</h4>
|
||||
<p className="text-xs text-slate-500">{service.technology}</p>
|
||||
<p className="text-xs text-slate-600 mt-2 line-clamp-2">{service.description}</p>
|
||||
{service.connections && service.connections.length > 0 && (
|
||||
<div className="mt-2 flex items-center gap-1 text-xs text-slate-400">
|
||||
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
|
||||
</svg>
|
||||
{service.connections.length} Verbindung{service.connections.length > 1 ? 'en' : ''}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Service Detail Panel */}
|
||||
{selectedArchService && (
|
||||
<ServiceDetailPanel
|
||||
service={selectedArchService}
|
||||
onClose={() => setSelectedArchService(null)}
|
||||
onSelectService={setSelectedArchService}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Legend */}
|
||||
<div className="bg-white rounded-lg shadow p-4">
|
||||
<h3 className="text-sm font-medium text-slate-700 mb-3">Legende</h3>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
{(['frontend', 'backend', 'database', 'cache', 'search', 'storage', 'security', 'communication', 'ai', 'erp'] as const).map((type) => {
|
||||
const colors = getArchTypeColor(type)
|
||||
return (
|
||||
<div key={type} className="flex items-center gap-2">
|
||||
<div className={`w-3 h-3 rounded-full ${colors.bg}`}></div>
|
||||
<span className="text-sm text-slate-600">{getArchTypeLabel(type)}</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Technical Details Section */}
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<h2 className="text-xl font-bold text-slate-800 mb-4">Technische Details</h2>
|
||||
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
{/* Data Flow */}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-slate-700 mb-3">Datenfluss</h3>
|
||||
<div className="space-y-2 text-sm text-slate-600">
|
||||
<p><strong>1. Request:</strong> Browser → Next.js/FastAPI Frontend</p>
|
||||
<p><strong>2. API:</strong> Frontend → Python Backend / Go Microservices</p>
|
||||
<p><strong>3. Auth:</strong> Keycloak/Vault fuer SSO & Secrets</p>
|
||||
<p><strong>4. Data:</strong> PostgreSQL (ACID) / Redis (Cache)</p>
|
||||
<p><strong>5. Search:</strong> Qdrant (Vector) / Meilisearch (Fulltext)</p>
|
||||
<p><strong>6. Storage:</strong> MinIO (Files) / IPFS (Dezentral)</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Security */}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-slate-700 mb-3">Sicherheit</h3>
|
||||
<div className="space-y-2 text-sm text-slate-600">
|
||||
<p><strong>Auth:</strong> JWT + Keycloak OIDC</p>
|
||||
<p><strong>Secrets:</strong> HashiCorp Vault (encrypted)</p>
|
||||
<p><strong>Communication:</strong> Matrix E2EE, TLS everywhere</p>
|
||||
<p><strong>DSGVO:</strong> Consent Service fuer Einwilligungen</p>
|
||||
<p><strong>DevSecOps:</strong> Trivy, Gitleaks, Semgrep, Bandit</p>
|
||||
<p><strong>SBOM:</strong> CycloneDX fuer alle Komponenten</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Languages */}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-slate-700 mb-3">Programmiersprachen</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<span className="px-3 py-1 bg-emerald-100 text-emerald-700 rounded-full text-sm">Python 3.12</span>
|
||||
<span className="px-3 py-1 bg-sky-100 text-sky-700 rounded-full text-sm">Go 1.21</span>
|
||||
<span className="px-3 py-1 bg-yellow-100 text-yellow-700 rounded-full text-sm">TypeScript 5.x</span>
|
||||
<span className="px-3 py-1 bg-lime-100 text-lime-700 rounded-full text-sm">JavaScript ES2022</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Frameworks */}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold text-slate-700 mb-3">Frameworks</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<span className="px-3 py-1 bg-blue-100 text-blue-700 rounded-full text-sm">Next.js 15</span>
|
||||
<span className="px-3 py-1 bg-green-100 text-green-700 rounded-full text-sm">FastAPI</span>
|
||||
<span className="px-3 py-1 bg-cyan-100 text-cyan-700 rounded-full text-sm">Gin (Go)</span>
|
||||
<span className="px-3 py-1 bg-purple-100 text-purple-700 rounded-full text-sm">Vue 3</span>
|
||||
<span className="px-3 py-1 bg-red-100 text-red-700 rounded-full text-sm">Angular 17</span>
|
||||
<span className="px-3 py-1 bg-pink-100 text-pink-700 rounded-full text-sm">NestJS</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
79
website/app/admin/docs/_components/ServiceDetailPanel.tsx
Normal file
79
website/app/admin/docs/_components/ServiceDetailPanel.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
'use client'
|
||||
|
||||
import type { ServiceNode } from '../types'
|
||||
import { ARCHITECTURE_SERVICES } from '../data'
|
||||
import { getArchTypeColor, getArchTypeLabel } from '../helpers'
|
||||
|
||||
interface ServiceDetailPanelProps {
|
||||
service: ServiceNode
|
||||
onClose: () => void
|
||||
onSelectService: (service: ServiceNode) => void
|
||||
}
|
||||
|
||||
export default function ServiceDetailPanel({ service, onClose, onSelectService }: ServiceDetailPanelProps) {
|
||||
return (
|
||||
<div className="bg-white rounded-lg shadow p-6">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-slate-800">{service.name}</h2>
|
||||
<p className="text-slate-600">{service.description}</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="p-2 hover:bg-slate-100 rounded-lg"
|
||||
>
|
||||
<svg className="w-5 h-5 text-slate-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div className="bg-slate-50 rounded-lg p-3">
|
||||
<div className="text-xs text-slate-500 uppercase">Typ</div>
|
||||
<div className={`text-sm font-medium ${getArchTypeColor(service.type).text}`}>
|
||||
{getArchTypeLabel(service.type)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-slate-50 rounded-lg p-3">
|
||||
<div className="text-xs text-slate-500 uppercase">Technologie</div>
|
||||
<div className="text-sm font-medium text-slate-800">{service.technology}</div>
|
||||
</div>
|
||||
<div className="bg-slate-50 rounded-lg p-3">
|
||||
<div className="text-xs text-slate-500 uppercase">Port</div>
|
||||
<div className="text-sm font-mono font-medium text-slate-800">
|
||||
{service.port || '-'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-slate-50 rounded-lg p-3">
|
||||
<div className="text-xs text-slate-500 uppercase">Verbindungen</div>
|
||||
<div className="text-sm font-medium text-slate-800">
|
||||
{service.connections?.length || 0} Services
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{service.connections && service.connections.length > 0 && (
|
||||
<div className="mt-4">
|
||||
<h4 className="text-sm font-medium text-slate-700 mb-2">Verbunden mit:</h4>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{service.connections.map((connId) => {
|
||||
const connService = ARCHITECTURE_SERVICES.find(s => s.id === connId)
|
||||
if (!connService) return null
|
||||
const colors = getArchTypeColor(connService.type)
|
||||
return (
|
||||
<button
|
||||
key={connId}
|
||||
onClick={() => onSelectService(connService)}
|
||||
className={`px-3 py-1 rounded-full text-sm ${colors.light} ${colors.text} hover:opacity-80`}
|
||||
>
|
||||
{connService.name}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
78
website/app/admin/docs/_components/ServicesTab.tsx
Normal file
78
website/app/admin/docs/_components/ServicesTab.tsx
Normal file
@@ -0,0 +1,78 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { services, docPaths, PROJECT_BASE_PATH } from '../data'
|
||||
import { getServiceTypeColor } from '../helpers'
|
||||
|
||||
export default function ServicesTab() {
|
||||
const [selectedService, setSelectedService] = useState<string | null>(null)
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{services.map((service) => (
|
||||
<div
|
||||
key={service.id}
|
||||
className={`bg-white rounded-xl border border-slate-200 p-5 cursor-pointer transition-all hover:shadow-md ${
|
||||
selectedService === service.id ? 'ring-2 ring-primary-600' : ''
|
||||
}`}
|
||||
onClick={() => setSelectedService(selectedService === service.id ? null : service.id)}
|
||||
>
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div>
|
||||
<h3 className="font-semibold text-slate-900">{service.name}</h3>
|
||||
<span className={`text-xs px-2 py-0.5 rounded-full ${getServiceTypeColor(service.type)}`}>
|
||||
{service.type}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className="text-sm font-mono text-slate-600">:{service.port}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-slate-600 mb-3">{service.description}</p>
|
||||
|
||||
<div className="flex flex-wrap gap-1 mb-3">
|
||||
{service.tech.map((t) => (
|
||||
<span key={t} className="text-xs bg-slate-100 text-slate-600 px-2 py-0.5 rounded">
|
||||
{t}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{selectedService === service.id && (
|
||||
<div className="mt-4 pt-4 border-t border-slate-200 space-y-2">
|
||||
{/* Purpose/Warum dieser Service */}
|
||||
<div className="bg-primary-50 border border-primary-200 rounded-lg p-3 mb-3">
|
||||
<div className="text-xs font-medium text-primary-700 mb-1">Warum dieser Service?</div>
|
||||
<div className="text-sm text-primary-900">{service.purpose}</div>
|
||||
</div>
|
||||
<div className="text-xs text-slate-500">Container: {service.container}</div>
|
||||
{service.healthEndpoint && (
|
||||
<div className="text-xs text-slate-500">
|
||||
Health: <code className="bg-slate-100 px-1 rounded">localhost:{service.port}{service.healthEndpoint}</code>
|
||||
</div>
|
||||
)}
|
||||
<div className="text-xs text-slate-500">
|
||||
Endpoints: {service.endpoints.length}
|
||||
</div>
|
||||
|
||||
{/* VS Code Link */}
|
||||
{docPaths[service.id] && (
|
||||
<a
|
||||
href={`vscode://file/${PROJECT_BASE_PATH}/${docPaths[service.id]}`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="mt-3 flex items-center gap-2 text-xs bg-blue-50 text-blue-700 px-3 py-2 rounded-lg hover:bg-blue-100 transition-colors"
|
||||
>
|
||||
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M23.15 2.587L18.21.21a1.494 1.494 0 0 0-1.705.29l-9.46 8.63-4.12-3.128a.999.999 0 0 0-1.276.057L.327 7.261A1 1 0 0 0 .326 8.74L3.899 12 .326 15.26a1 1 0 0 0 .001 1.479L1.65 17.94a.999.999 0 0 0 1.276.057l4.12-3.128 9.46 8.63a1.492 1.492 0 0 0 1.704.29l4.942-2.377A1.5 1.5 0 0 0 24 20.06V3.939a1.5 1.5 0 0 0-.85-1.352zm-5.146 14.861L10.826 12l7.178-5.448v10.896z"/>
|
||||
</svg>
|
||||
In VS Code oeffnen
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
31
website/app/admin/docs/_components/TabNavigation.tsx
Normal file
31
website/app/admin/docs/_components/TabNavigation.tsx
Normal file
@@ -0,0 +1,31 @@
|
||||
'use client'
|
||||
|
||||
import type { TabType } from '../types'
|
||||
import { TAB_DEFINITIONS } from '../data'
|
||||
|
||||
interface TabNavigationProps {
|
||||
activeTab: TabType
|
||||
onTabChange: (tab: TabType) => void
|
||||
}
|
||||
|
||||
export default function TabNavigation({ activeTab, onTabChange }: TabNavigationProps) {
|
||||
return (
|
||||
<div className="border-b border-slate-200 mb-6">
|
||||
<nav className="flex gap-6">
|
||||
{TAB_DEFINITIONS.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => onTabChange(tab.id as TabType)}
|
||||
className={`pb-3 px-1 text-sm font-medium border-b-2 transition-colors ${
|
||||
activeTab === tab.id
|
||||
? 'border-primary-600 text-primary-600'
|
||||
: 'border-transparent text-slate-500 hover:text-slate-700'
|
||||
}`}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
171
website/app/admin/docs/_components/TestingTab.tsx
Normal file
171
website/app/admin/docs/_components/TestingTab.tsx
Normal file
@@ -0,0 +1,171 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { PROJECT_BASE_PATH } from '../data'
|
||||
|
||||
const testDocLinks = [
|
||||
{ file: 'docs/testing/README.md', label: 'Test-Uebersicht', desc: 'Teststrategie & Coverage-Ziele' },
|
||||
{ file: 'docs/testing/QUICKSTART.md', label: 'Quickstart', desc: 'Schnellstart fuer Tests' },
|
||||
{ file: 'docs/testing/INTEGRATION_TESTS.md', label: 'Integrationstests', desc: 'API & DB Tests' },
|
||||
]
|
||||
|
||||
const coverageTargets = [
|
||||
{ component: 'Go Consent Service', target: '80%', current: '~75%', color: 'green' },
|
||||
{ component: 'Python Backend', target: '70%', current: '~65%', color: 'yellow' },
|
||||
{ component: 'Critical Paths (Auth, OAuth)', target: '95%', current: '~90%', color: 'green' },
|
||||
]
|
||||
|
||||
const testCommands = [
|
||||
{ label: 'Go Tests (alle)', cmd: 'cd consent-service && go test -v ./...', lang: 'Go' },
|
||||
{ label: 'Go Tests mit Coverage', cmd: 'cd consent-service && go test -cover ./...', lang: 'Go' },
|
||||
{ label: 'Python Tests (alle)', cmd: 'cd backend && source venv/bin/activate && pytest -v', lang: 'Python' },
|
||||
{ label: 'Python Tests mit Coverage', cmd: 'cd backend && pytest --cov=. --cov-report=html', lang: 'Python' },
|
||||
]
|
||||
|
||||
export default function TestingTab() {
|
||||
const [copiedEndpoint, setCopiedEndpoint] = useState<string | null>(null)
|
||||
|
||||
const copyToClipboard = (text: string, id: string) => {
|
||||
navigator.clipboard.writeText(text)
|
||||
setCopiedEndpoint(id)
|
||||
setTimeout(() => setCopiedEndpoint(null), 2000)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Quick Links to Docs */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-semibold text-slate-900">Test-Dokumentation</h2>
|
||||
<a
|
||||
href={`vscode://file/${PROJECT_BASE_PATH}/docs/testing/README.md`}
|
||||
className="flex items-center gap-2 text-sm bg-blue-50 text-blue-700 px-3 py-2 rounded-lg hover:bg-blue-100 transition-colors"
|
||||
>
|
||||
<svg className="w-4 h-4" viewBox="0 0 24 24" fill="currentColor">
|
||||
<path d="M23.15 2.587L18.21.21a1.494 1.494 0 0 0-1.705.29l-9.46 8.63-4.12-3.128a.999.999 0 0 0-1.276.057L.327 7.261A1 1 0 0 0 .326 8.74L3.899 12 .326 15.26a1 1 0 0 0 .001 1.479L1.65 17.94a.999.999 0 0 0 1.276.057l4.12-3.128 9.46 8.63a1.492 1.492 0 0 0 1.704.29l4.942-2.377A1.5 1.5 0 0 0 24 20.06V3.939a1.5 1.5 0 0 0-.85-1.352zm-5.146 14.861L10.826 12l7.178-5.448v10.896z"/>
|
||||
</svg>
|
||||
Vollstaendige Docs in VS Code
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{testDocLinks.map((doc) => (
|
||||
<a
|
||||
key={doc.file}
|
||||
href={`vscode://file/${PROJECT_BASE_PATH}/${doc.file}`}
|
||||
className="p-4 bg-slate-50 rounded-lg hover:bg-slate-100 transition-colors"
|
||||
>
|
||||
<div className="font-medium text-slate-900">{doc.label}</div>
|
||||
<div className="text-sm text-slate-500">{doc.desc}</div>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Test Pyramid */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6">
|
||||
<h2 className="text-lg font-semibold text-slate-900 mb-4">Test-Pyramide</h2>
|
||||
<div className="bg-slate-900 rounded-lg p-6 text-center">
|
||||
<pre className="text-green-400 font-mono text-sm whitespace-pre inline-block text-left">{` /\\
|
||||
/ \\ E2E (10%)
|
||||
/----\\
|
||||
/ \\ Integration (20%)
|
||||
/--------\\
|
||||
/ \\ Unit Tests (70%)
|
||||
/--------------\\`}</pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Coverage Ziele */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6">
|
||||
<h2 className="text-lg font-semibold text-slate-900 mb-4">Coverage-Ziele</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{coverageTargets.map((item) => (
|
||||
<div key={item.component} className="p-4 bg-slate-50 rounded-lg">
|
||||
<div className="font-medium text-slate-900">{item.component}</div>
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<div className="flex-1 h-2 bg-slate-200 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full ${item.color === 'green' ? 'bg-green-500' : 'bg-yellow-500'}`}
|
||||
style={{ width: item.current.replace('~', '') }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm text-slate-600">{item.current}</span>
|
||||
</div>
|
||||
<div className="text-xs text-slate-500 mt-1">Ziel: {item.target}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Test Commands */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6">
|
||||
<h2 className="text-lg font-semibold text-slate-900 mb-4">Test-Befehle</h2>
|
||||
<div className="space-y-4">
|
||||
{testCommands.map((item, idx) => (
|
||||
<div key={idx} className="flex items-center gap-4 p-3 bg-slate-50 rounded-lg">
|
||||
<span className={`text-xs px-2 py-0.5 rounded-full ${item.lang === 'Go' ? 'bg-cyan-100 text-cyan-700' : 'bg-yellow-100 text-yellow-700'}`}>
|
||||
{item.lang}
|
||||
</span>
|
||||
<div className="text-sm text-slate-600 w-40">{item.label}</div>
|
||||
<code className="flex-1 text-sm font-mono bg-slate-900 text-green-400 px-3 py-2 rounded">
|
||||
{item.cmd}
|
||||
</code>
|
||||
<button
|
||||
onClick={() => copyToClipboard(item.cmd, `test-${idx}`)}
|
||||
className="text-slate-400 hover:text-slate-600"
|
||||
>
|
||||
{copiedEndpoint === `test-${idx}` ? (
|
||||
<span className="text-xs text-green-600">Copied!</span>
|
||||
) : (
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Test-Struktur */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6">
|
||||
<h2 className="text-lg font-semibold text-slate-900 mb-4">Test-Struktur</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Go Tests */}
|
||||
<div>
|
||||
<h3 className="font-medium text-slate-900 mb-2 flex items-center gap-2">
|
||||
<span className="text-xs px-2 py-0.5 rounded-full bg-cyan-100 text-cyan-700">Go</span>
|
||||
Consent Service
|
||||
</h3>
|
||||
<div className="bg-slate-900 rounded-lg p-4 font-mono text-sm text-green-400">
|
||||
<div>consent-service/</div>
|
||||
<div className="ml-4">internal/</div>
|
||||
<div className="ml-8">handlers/handlers_test.go</div>
|
||||
<div className="ml-8">services/auth_service_test.go</div>
|
||||
<div className="ml-8">services/oauth_service_test.go</div>
|
||||
<div className="ml-8">services/totp_service_test.go</div>
|
||||
<div className="ml-8">middleware/middleware_test.go</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Python Tests */}
|
||||
<div>
|
||||
<h3 className="font-medium text-slate-900 mb-2 flex items-center gap-2">
|
||||
<span className="text-xs px-2 py-0.5 rounded-full bg-yellow-100 text-yellow-700">Python</span>
|
||||
Backend
|
||||
</h3>
|
||||
<div className="bg-slate-900 rounded-lg p-4 font-mono text-sm text-green-400">
|
||||
<div>backend/</div>
|
||||
<div className="ml-4">tests/</div>
|
||||
<div className="ml-8">test_consent_client.py</div>
|
||||
<div className="ml-8">test_gdpr_api.py</div>
|
||||
<div className="ml-8">test_dsms_webui.py</div>
|
||||
<div className="ml-4">conftest.py</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user