'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 && (
)}
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: (
),
},
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 */}
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
)}
)}
)
})}
)
}