'use client' /** * Architecture Overview — Interaktiver Service-Graph * * ReactFlow-Visualisierung der 13 Compliance-Services in 4 Schwimmbahnen: * Frontend, Backend APIs, Infrastructure, Data Sovereignty. * Analog zum SDK-Flow, aber fuer die Service-Topologie. */ import { useCallback, useState, useMemo, useEffect } from 'react' import ReactFlow, { Node, Edge, Controls, Background, MiniMap, useNodesState, useEdgesState, BackgroundVariant, MarkerType, Panel, } from 'reactflow' import 'reactflow/dist/style.css' import { ARCH_SERVICES, ARCH_EDGES, LAYERS, getAllDbTables, getAllRagCollections, type ArchService, type ServiceLayer, } from './architecture-data' // ============================================================================= // TYPES // ============================================================================= type LayerFilter = 'alle' | ServiceLayer // ============================================================================= // LAYOUT // ============================================================================= const NODE_WIDTH = 180 const NODE_HEIGHT = 70 const NODE_X_SPACING = 220 const LANE_Y_START = 80 const LANE_LABEL_HEIGHT = 40 const LAYER_ORDER: ServiceLayer[] = ['frontend', 'backend', 'infrastructure', 'data-sovereignty'] function getServicePosition(service: ArchService): { x: number; y: number } { const layer = LAYERS[service.layer] const layerServices = ARCH_SERVICES.filter(s => s.layer === service.layer) const idx = layerServices.findIndex(s => s.id === service.id) return { x: 80 + idx * NODE_X_SPACING, y: LANE_Y_START + LANE_LABEL_HEIGHT + layer.y, } } // ============================================================================= // DETAIL PANEL // ============================================================================= function DetailPanel({ service, onClose, }: { service: ArchService onClose: () => void }) { const layer = LAYERS[service.layer] return (

{service.name}

{layer.name} {service.tech}
{/* Beschreibung */}

{service.description}

{service.descriptionLong}

{/* Tech + Port + Container */}
Tech {service.tech}
{service.port && (
Port {service.port}
)} {service.url && (
URL {service.url}
)}
Container {service.container}
{/* DB Tables */} {service.dbTables.length > 0 && (

DB-Tabellen ({service.dbTables.length})

{service.dbTables.map(table => (
{table}
))}
)} {/* RAG Collections */} {service.ragCollections.length > 0 && (

RAG-Collections ({service.ragCollections.length})

{service.ragCollections.map(rag => (
{rag}
))}
)} {/* API Endpoints */} {service.apiEndpoints.length > 0 && (

API-Endpunkte ({service.apiEndpoints.length})

{service.apiEndpoints.map(ep => (
{ep}
))}
)} {/* Dependencies */} {service.dependsOn.length > 0 && (

Abhaengigkeiten

{service.dependsOn.map(depId => { const dep = ARCH_SERVICES.find(s => s.id === depId) return (
{dep?.name || depId}
) })}
)} {/* Open URL */} {service.url && ( )}
) } // ============================================================================= // MAIN COMPONENT // ============================================================================= export default function ArchitecturePage() { const [selectedService, setSelectedService] = useState(null) const [layerFilter, setLayerFilter] = useState('alle') const [showDb, setShowDb] = useState(false) const [showRag, setShowRag] = useState(false) const [showApis, setShowApis] = useState(false) const [expandedServices, setExpandedServices] = useState>(new Set()) const toggleExpanded = useCallback((id: string) => { setExpandedServices(prev => { const next = new Set(prev) if (next.has(id)) next.delete(id) else next.add(id) return next }) }, []) const allDbTables = useMemo(() => getAllDbTables(), []) const allRagCollections = useMemo(() => getAllRagCollections(), []) // ========================================================================= // Build Nodes + Edges // ========================================================================= const { nodes: initialNodes, edges: initialEdges } = useMemo(() => { const nodes: Node[] = [] const edges: Edge[] = [] const visibleServices = layerFilter === 'alle' ? ARCH_SERVICES : ARCH_SERVICES.filter(s => s.layer === layerFilter) const visibleIds = new Set(visibleServices.map(s => s.id)) // ── Service Nodes ────────────────────────────────────────────────────── visibleServices.forEach(service => { const layer = LAYERS[service.layer] const pos = getServicePosition(service) const isSelected = selectedService?.id === service.id nodes.push({ id: service.id, type: 'default', position: pos, data: { label: (
{service.nameShort}
{service.tech}
{service.port && (
:{service.port}
)}
), }, style: { background: isSelected ? layer.colorBorder : layer.colorBg, color: isSelected ? 'white' : layer.colorText, border: `2px solid ${layer.colorBorder}`, borderRadius: '10px', padding: '8px 4px', minWidth: `${NODE_WIDTH}px`, maxWidth: `${NODE_WIDTH}px`, cursor: 'pointer', boxShadow: isSelected ? `0 0 16px ${layer.colorBorder}` : '0 1px 3px rgba(0,0,0,0.08)', }, }) }) // ── Connection Edges ─────────────────────────────────────────────────── ARCH_EDGES.forEach(archEdge => { if (visibleIds.has(archEdge.source) && visibleIds.has(archEdge.target)) { const isHighlighted = selectedService?.id === archEdge.source || selectedService?.id === archEdge.target edges.push({ id: `e-${archEdge.source}-${archEdge.target}`, source: archEdge.source, target: archEdge.target, type: 'smoothstep', animated: isHighlighted, label: archEdge.label, labelStyle: { fontSize: 9, fill: isHighlighted ? '#7c3aed' : '#94a3b8' }, style: { stroke: isHighlighted ? '#7c3aed' : '#94a3b8', strokeWidth: isHighlighted ? 2.5 : 1.5, }, markerEnd: { type: MarkerType.ArrowClosed, color: isHighlighted ? '#7c3aed' : '#94a3b8', width: 14, height: 14, }, }) } }) // ── DB Table Nodes ───────────────────────────────────────────────────── if (showDb) { const dbTablesInUse = new Set() visibleServices.forEach(s => s.dbTables.forEach(t => dbTablesInUse.add(t))) let dbIdx = 0 dbTablesInUse.forEach(table => { const nodeId = `db-${table}` nodes.push({ id: nodeId, type: 'default', position: { x: -250, y: LANE_Y_START + dbIdx * 60 }, data: { label: (
{table}
), }, style: { background: '#f1f5f9', color: '#475569', border: '1px solid #94a3b8', borderRadius: '6px', padding: '4px 6px', fontSize: '10px', minWidth: '140px', }, }) visibleServices .filter(s => s.dbTables.includes(table)) .forEach(svc => { edges.push({ id: `e-db-${table}-${svc.id}`, source: nodeId, target: svc.id, type: 'straight', style: { stroke: '#94a3b8', strokeWidth: 1, strokeDasharray: '6 3' }, }) }) dbIdx++ }) } // ── RAG Collection Nodes ─────────────────────────────────────────────── if (showRag) { const ragInUse = new Set() visibleServices.forEach(s => s.ragCollections.forEach(r => ragInUse.add(r))) let ragIdx = 0 ragInUse.forEach(collection => { const nodeId = `rag-${collection}` const rightX = 1200 nodes.push({ id: nodeId, type: 'default', position: { x: rightX, y: LANE_Y_START + ragIdx * 60 }, data: { label: (
{collection.replace('bp_', '')}
), }, style: { background: '#dcfce7', color: '#166534', border: '1px solid #22c55e', borderRadius: '6px', padding: '4px 6px', fontSize: '10px', minWidth: '130px', }, }) visibleServices .filter(s => s.ragCollections.includes(collection)) .forEach(svc => { edges.push({ id: `e-rag-${collection}-${svc.id}`, source: nodeId, target: svc.id, type: 'straight', style: { stroke: '#22c55e', strokeWidth: 1, strokeDasharray: '6 3' }, }) }) ragIdx++ }) } // ── API Endpoint Nodes ───────────────────────────────────────────────── if (showApis) { visibleServices.forEach(svc => { if (svc.apiEndpoints.length === 0) return const svcPos = getServicePosition(svc) svc.apiEndpoints.forEach((ep, idx) => { const nodeId = `api-${svc.id}-${idx}` nodes.push({ id: nodeId, type: 'default', position: { x: svcPos.x + NODE_WIDTH + 30, y: svcPos.y + idx * 32 }, data: { label: (
{ep}
), }, style: { background: '#faf5ff', color: '#7c3aed', border: '1px solid #c4b5fd', borderRadius: '4px', padding: '2px 6px', fontSize: '9px', minWidth: '160px', }, }) edges.push({ id: `e-api-${svc.id}-${idx}`, source: svc.id, target: nodeId, type: 'straight', style: { stroke: '#c4b5fd', strokeWidth: 1 }, }) }) }) } return { nodes, edges } }, [layerFilter, showDb, showRag, showApis, selectedService]) // ========================================================================= // React Flow State // ========================================================================= const [nodes, setNodes, onNodesChange] = useNodesState([]) const [edges, setEdges, onEdgesChange] = useEdgesState([]) useEffect(() => { setNodes(initialNodes) setEdges(initialEdges) }, [initialNodes, initialEdges, setNodes, setEdges]) const onNodeClick = useCallback((_event: React.MouseEvent, node: Node) => { const service = ARCH_SERVICES.find(s => s.id === node.id) if (service) { setSelectedService(prev => (prev?.id === service.id ? null : service)) } }, []) const onPaneClick = useCallback(() => { setSelectedService(null) }, []) // ========================================================================= // Stats // ========================================================================= const stats = useMemo(() => { return { services: ARCH_SERVICES.length, dbTables: allDbTables.length, ragCollections: allRagCollections.length, edges: ARCH_EDGES.length, } }, [allDbTables, allRagCollections]) // ========================================================================= // Render // ========================================================================= return (
{/* Header */}

Architektur-Uebersicht

{stats.services} Services | {stats.dbTables} DB-Tabellen | {stats.ragCollections} RAG-Collections | {stats.edges} Verbindungen

{/* Toolbar */}
{/* Layer Filter */} {LAYER_ORDER.map(layerId => { const layer = LAYERS[layerId] const count = ARCH_SERVICES.filter(s => s.layer === layerId).length return ( ) })} {/* Separator */}
{/* Toggles */}
{/* Flow Canvas + Detail Panel */}
{/* Canvas */}
{ if (node.id.startsWith('db-')) return '#94a3b8' if (node.id.startsWith('rag-')) return '#22c55e' if (node.id.startsWith('api-')) return '#c4b5fd' const svc = ARCH_SERVICES.find(s => s.id === node.id) return svc ? LAYERS[svc.layer].colorBorder : '#94a3b8' }} maskColor="rgba(0,0,0,0.08)" /> {/* Legende */}
Legende
{LAYER_ORDER.map(layerId => { const layer = LAYERS[layerId] return (
{layer.name}
) })}
DB-Tabelle
RAG-Collection
API-Endpunkt
{/* Swim-Lane Labels */} {layerFilter === 'alle' && (
{LAYER_ORDER.map(layerId => { const layer = LAYERS[layerId] return (
{layer.name}
) })}
)}
{/* Detail Panel */} {selectedService && ( setSelectedService(null)} /> )}
{/* Service Table (aufklappbar) */}

Alle Services ({ layerFilter === 'alle' ? ARCH_SERVICES.length : `${ARCH_SERVICES.filter(s => s.layer === layerFilter).length} / ${ARCH_SERVICES.length}` })

{ARCH_SERVICES.filter( s => layerFilter === 'alle' || s.layer === layerFilter ).map(service => { const layer = LAYERS[service.layer] const isExpanded = expandedServices.has(service.id) return (
{/* Row Header */} {/* Expanded Detail */} {isExpanded && (
{/* Beschreibung */}

{service.description}

{service.descriptionLong}

{/* Info Grid */}
Tech
{service.tech}
Port
{service.port ? `:${service.port}` : 'Intern'}
Container
{service.container}
{service.url && ( )}
{/* Sections */}
{/* DB Tables */} {service.dbTables.length > 0 && (

DB-Tabellen ({service.dbTables.length})

{service.dbTables.map(table => (
{table}
))}
)} {/* RAG Collections */} {service.ragCollections.length > 0 && (

RAG-Collections ({service.ragCollections.length})

{service.ragCollections.map(rag => (
{rag}
))}
)} {/* API Endpoints */} {service.apiEndpoints.length > 0 && (

API-Endpunkte ({service.apiEndpoints.length})

{service.apiEndpoints.map(ep => (
{ep}
))}
)} {/* Dependencies */} {service.dependsOn.length > 0 && (

Abhaengigkeiten ({service.dependsOn.length})

{service.dependsOn.map(depId => { const dep = ARCH_SERVICES.find(s => s.id === depId) const depLayer = dep ? LAYERS[dep.layer] : null return (
{depLayer && ( )} {dep?.name || depId}
) })}
)}
{/* Open in Graph + URL */}
{service.url && ( Service oeffnen )}
)}
) })}
) }