diff --git a/admin-compliance/app/sdk/architecture/_components/ArchCanvas.tsx b/admin-compliance/app/sdk/architecture/_components/ArchCanvas.tsx new file mode 100644 index 0000000..4ccc920 --- /dev/null +++ b/admin-compliance/app/sdk/architecture/_components/ArchCanvas.tsx @@ -0,0 +1,164 @@ +'use client' + +import React, { useCallback, useEffect } from 'react' +import ReactFlow, { + Node, + Controls, + Background, + MiniMap, + useNodesState, + useEdgesState, + BackgroundVariant, + Panel, +} from 'reactflow' +import 'reactflow/dist/style.css' +import { ARCH_SERVICES, LAYERS, type ArchService } from '../architecture-data' +import { LAYER_ORDER, type LayerFilter } from '../_layout' +import { useArchGraph } from '../_hooks/useArchGraph' +import DetailPanel from './DetailPanel' + +export default function ArchCanvas({ + layerFilter, + showDb, + showRag, + showApis, + selectedService, + setSelectedService, +}: { + layerFilter: LayerFilter + showDb: boolean + showRag: boolean + showApis: boolean + selectedService: ArchService | null + setSelectedService: React.Dispatch> +}) { + const { nodes: initialNodes, edges: initialEdges } = useArchGraph({ + layerFilter, + showDb, + showRag, + showApis, + selectedService, + }) + + 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)) + } + }, [setSelectedService]) + + const onPaneClick = useCallback(() => { + setSelectedService(null) + }, [setSelectedService]) + + return ( +
+ {/* 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)} + /> + )} +
+ ) +} diff --git a/admin-compliance/app/sdk/architecture/_components/ArchHeader.tsx b/admin-compliance/app/sdk/architecture/_components/ArchHeader.tsx new file mode 100644 index 0000000..d5f1d73 --- /dev/null +++ b/admin-compliance/app/sdk/architecture/_components/ArchHeader.tsx @@ -0,0 +1,33 @@ +'use client' + +export default function ArchHeader({ + stats, +}: { + stats: { + services: number + dbTables: number + ragCollections: number + edges: number + } +}) { + return ( +
+
+
+ + + +
+
+

+ Architektur-Uebersicht +

+

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

+
+
+
+ ) +} diff --git a/admin-compliance/app/sdk/architecture/_components/DetailPanel.tsx b/admin-compliance/app/sdk/architecture/_components/DetailPanel.tsx new file mode 100644 index 0000000..2128f4f --- /dev/null +++ b/admin-compliance/app/sdk/architecture/_components/DetailPanel.tsx @@ -0,0 +1,164 @@ +'use client' + +import { ARCH_SERVICES, LAYERS, type ArchService } from '../architecture-data' + +export default 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 && ( + + )} +
+
+ ) +} diff --git a/admin-compliance/app/sdk/architecture/_components/ServiceTable.tsx b/admin-compliance/app/sdk/architecture/_components/ServiceTable.tsx new file mode 100644 index 0000000..fb3ccc0 --- /dev/null +++ b/admin-compliance/app/sdk/architecture/_components/ServiceTable.tsx @@ -0,0 +1,253 @@ +'use client' + +import React from 'react' +import { ARCH_SERVICES, LAYERS, type ArchService } from '../architecture-data' +import { type LayerFilter } from '../_layout' + +export default function ServiceTable({ + layerFilter, + expandedServices, + onToggleExpanded, + onMarkInGraph, +}: { + layerFilter: LayerFilter + expandedServices: Set + onToggleExpanded: (id: string) => void + onMarkInGraph: (service: ArchService) => void +}) { + const filtered = ARCH_SERVICES.filter( + s => layerFilter === 'alle' || s.layer === layerFilter + ) + + return ( +
+
+

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

+
+
+ {filtered.map(service => { + const layer = LAYERS[service.layer] + const isExpanded = expandedServices.has(service.id) + + return ( +
+ {/* Row Header */} + + + {/* Expanded Detail */} + {isExpanded && } +
+ ) + })} +
+
+ ) +} + +function ExpandedRow({ + service, + onMarkInGraph, +}: { + service: ArchService + onMarkInGraph: (service: ArchService) => void +}) { + return ( +
+ {/* Beschreibung */} +

+ {service.description} +

+

+ {service.descriptionLong} +

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

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

+
+ {service.dbTables.map(table => ( +
+ {table} +
+ ))} +
+
+ )} + + {service.ragCollections.length > 0 && ( +
+

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

+
+ {service.ragCollections.map(rag => ( +
+ {rag} +
+ ))} +
+
+ )} + + {service.apiEndpoints.length > 0 && ( +
+

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

+
+ {service.apiEndpoints.map(ep => ( +
+ {ep} +
+ ))} +
+
+ )} + + {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 + + )} +
+
+ ) +} diff --git a/admin-compliance/app/sdk/architecture/_components/Toolbar.tsx b/admin-compliance/app/sdk/architecture/_components/Toolbar.tsx new file mode 100644 index 0000000..fad21ca --- /dev/null +++ b/admin-compliance/app/sdk/architecture/_components/Toolbar.tsx @@ -0,0 +1,98 @@ +'use client' + +import { ARCH_SERVICES, LAYERS } from '../architecture-data' +import { LAYER_ORDER, type LayerFilter } from '../_layout' + +export default function Toolbar({ + layerFilter, + showDb, + showRag, + showApis, + onLayerFilter, + onToggleDb, + onToggleRag, + onToggleApis, +}: { + layerFilter: LayerFilter + showDb: boolean + showRag: boolean + showApis: boolean + onLayerFilter: (f: LayerFilter) => void + onToggleDb: () => void + onToggleRag: () => void + onToggleApis: () => void +}) { + return ( +
+
+ {/* Layer Filter */} + + {LAYER_ORDER.map(layerId => { + const layer = LAYERS[layerId] + const count = ARCH_SERVICES.filter(s => s.layer === layerId).length + return ( + + ) + })} + + {/* Separator */} +
+ + {/* Toggles */} + + + +
+
+ ) +} diff --git a/admin-compliance/app/sdk/architecture/_hooks/useArchGraph.tsx b/admin-compliance/app/sdk/architecture/_hooks/useArchGraph.tsx new file mode 100644 index 0000000..50daf9d --- /dev/null +++ b/admin-compliance/app/sdk/architecture/_hooks/useArchGraph.tsx @@ -0,0 +1,251 @@ +'use client' + +import { useMemo } from 'react' +import { type Node, type Edge, MarkerType } from 'reactflow' +import { + ARCH_SERVICES, + ARCH_EDGES, + LAYERS, + type ArchService, +} from '../architecture-data' +import { + NODE_WIDTH, + LANE_Y_START, + getServicePosition, + type LayerFilter, +} from '../_layout' + +export function useArchGraph({ + layerFilter, + showDb, + showRag, + showApis, + selectedService, +}: { + layerFilter: LayerFilter + showDb: boolean + showRag: boolean + showApis: boolean + selectedService: ArchService | null +}) { + return 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]) +} diff --git a/admin-compliance/app/sdk/architecture/_layout.ts b/admin-compliance/app/sdk/architecture/_layout.ts new file mode 100644 index 0000000..deb5d71 --- /dev/null +++ b/admin-compliance/app/sdk/architecture/_layout.ts @@ -0,0 +1,30 @@ +import { ARCH_SERVICES, LAYERS, type ArchService, type ServiceLayer } from './architecture-data' + +// ============================================================================= +// TYPES +// ============================================================================= + +export type LayerFilter = 'alle' | ServiceLayer + +// ============================================================================= +// LAYOUT +// ============================================================================= + +export const NODE_WIDTH = 180 +export const NODE_HEIGHT = 70 +export const NODE_X_SPACING = 220 +export const LANE_Y_START = 80 +export const LANE_LABEL_HEIGHT = 40 + +export const LAYER_ORDER: ServiceLayer[] = ['frontend', 'backend', 'infrastructure', 'data-sovereignty'] + +export 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, + } +} diff --git a/admin-compliance/app/sdk/architecture/page.tsx b/admin-compliance/app/sdk/architecture/page.tsx index 290f075..cdc1278 100644 --- a/admin-compliance/app/sdk/architecture/page.tsx +++ b/admin-compliance/app/sdk/architecture/page.tsx @@ -8,228 +8,19 @@ * 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 { useCallback, useState, useMemo } from 'react' 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 -// ============================================================================= +import { type LayerFilter } from './_layout' +import ArchHeader from './_components/ArchHeader' +import Toolbar from './_components/Toolbar' +import ArchCanvas from './_components/ArchCanvas' +import ServiceTable from './_components/ServiceTable' export default function ArchitecturePage() { const [selectedService, setSelectedService] = useState(null) @@ -251,258 +42,6 @@ export default function ArchitecturePage() { 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, @@ -512,439 +51,41 @@ export default function ArchitecturePage() { } }, [allDbTables, allRagCollections]) - // ========================================================================= - // Render - // ========================================================================= + const handleLayerFilter = useCallback((f: LayerFilter) => { + setLayerFilter(f) + setSelectedService(null) + }, []) 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 ( - - ) - })} + setShowDb(v => !v)} + onToggleRag={() => setShowRag(v => !v)} + onToggleApis={() => setShowApis(v => !v)} + /> - {/* 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 - - )} -
-
- )} -
- ) - })} -
-
+
) }