@@ -4,7 +4,7 @@
* Screen Flow Visualization
*
* Visualisiert alle Screens aus:
* - Studio (Port 8000): Lehrer-Oberflä che
* - Studio (Port 8000): Lehrer-Oberflae che
* - Admin (Port 3000): Admin Panel
*/
@@ -17,310 +17,20 @@ import ReactFlow, {
MiniMap ,
useNodesState ,
useEdgesState ,
Connection ,
BackgroundVariant ,
MarkerType ,
Panel ,
} from 'reactflow'
import 'reactflow/dist/style.css'
import AdminLayout from '@/components/admin/AdminLayout'
// ============================================
// TYPES
// ============================================
interface ScreenDefinition {
id : string
name : string
description : string
category : string
icon : string
url? : string
}
interface ConnectionDef {
source : string
target : string
label? : string
}
type FlowType = 'studio' | 'admin'
// ============================================
// STUDIO SCREENS (Port 8000)
// ============================================
const STUDIO_SCREENS : ScreenDefinition [ ] = [
{ id : 'lehrer-dashboard' , name : 'Mein Dashboard' , description : 'Hauptübersicht mit Widgets' , category : 'navigation' , icon : '🏠' , url : '/app#lehrer-dashboard' } ,
{ id : 'lehrer-onboarding' , name : 'Erste Schritte' , description : 'Onboarding & Schnellstart' , category : 'navigation' , icon : '🚀' , url : '/app#lehrer-onboarding' } ,
{ id : 'hilfe' , name : 'Dokumentation' , description : 'Hilfe & Anleitungen' , category : 'navigation' , icon : '📚' , url : '/app#hilfe' } ,
{ id : 'worksheets' , name : 'Arbeitsblätter Studio' , description : 'Lernmaterialien erstellen' , category : 'content' , icon : '📝' , url : '/app#worksheets' } ,
{ id : 'content-creator' , name : 'Content Creator' , description : 'Inhalte erstellen' , category : 'content' , icon : '✨' , url : '/app#content-creator' } ,
{ id : 'content-feed' , name : 'Content Feed' , description : 'Inhalte durchsuchen' , category : 'content' , icon : '📰' , url : '/app#content-feed' } ,
{ id : 'unit-creator' , name : 'Unit Creator' , description : 'Lerneinheiten erstellen' , category : 'content' , icon : '📦' , url : '/app#unit-creator' } ,
{ id : 'letters' , name : 'Briefe & Vorlagen' , description : 'Brief-Generator' , category : 'content' , icon : '✉️' , url : '/app#letters' } ,
{ id : 'correction' , name : 'Korrektur' , description : 'Arbeiten korrigieren' , category : 'content' , icon : '✏️' , url : '/app#correction' } ,
{ id : 'klausur-korrektur' , name : 'Abiturklausuren' , description : 'KI-gestützte Klausurkorrektur' , category : 'content' , icon : '📋' , url : '/app#klausur-korrektur' } ,
{ id : 'jitsi' , name : 'Videokonferenz' , description : 'Jitsi Meet Integration' , category : 'communication' , icon : '🎥' , url : '/app#jitsi' } ,
{ id : 'messenger' , name : 'Messenger' , description : 'Matrix E2EE Chat' , category : 'communication' , icon : '💬' , url : '/app#messenger' } ,
{ id : 'mail' , name : 'Unified Inbox' , description : 'E-Mail Verwaltung' , category : 'communication' , icon : '📧' , url : '/app#mail' } ,
{ id : 'school-classes' , name : 'Klassen' , description : 'Klassenverwaltung' , category : 'school' , icon : '👥' , url : '/app#school-classes' } ,
{ id : 'school-exams' , name : 'Prüfungen' , description : 'Prüfungsverwaltung' , category : 'school' , icon : '📝' , url : '/app#school-exams' } ,
{ id : 'school-grades' , name : 'Noten' , description : 'Notenverwaltung' , category : 'school' , icon : '📊' , url : '/app#school-grades' } ,
{ id : 'school-gradebook' , name : 'Notenbuch' , description : 'Digitales Notenbuch' , category : 'school' , icon : '📖' , url : '/app#school-gradebook' } ,
{ id : 'school-certificates' , name : 'Zeugnisse' , description : 'Zeugniserstellung' , category : 'school' , icon : '🎓' , url : '/app#school-certificates' } ,
{ id : 'companion' , name : 'Begleiter & Stunde' , description : 'KI-Unterrichtsassistent' , category : 'ai' , icon : '🤖' , url : '/app#companion' } ,
{ id : 'alerts' , name : 'Alerts' , description : 'News & Benachrichtigungen' , category : 'ai' , icon : '🔔' , url : '/app#alerts' } ,
{ id : 'admin' , name : 'Einstellungen' , description : 'Systemeinstellungen' , category : 'admin' , icon : '⚙️' , url : '/app#admin' } ,
{ id : 'rbac-admin' , name : 'Rollen & Rechte' , description : 'Berechtigungsverwaltung' , category : 'admin' , icon : '🔐' , url : '/app#rbac-admin' } ,
{ id : 'abitur-docs-admin' , name : 'Abitur Dokumente' , description : 'Erwartungshorizonte' , category : 'admin' , icon : '📄' , url : '/app#abitur-docs-admin' } ,
{ id : 'system-info' , name : 'System Info' , description : 'Systeminformationen' , category : 'admin' , icon : '💻' , url : '/app#system-info' } ,
{ id : 'workflow' , name : 'Workflow' , description : 'Automatisierungen' , category : 'admin' , icon : '⚡' , url : '/app#workflow' } ,
]
const STUDIO_CONNECTIONS : ConnectionDef [ ] = [
{ source : 'lehrer-onboarding' , target : 'worksheets' , label : 'Arbeitsblätter' } ,
{ source : 'lehrer-onboarding' , target : 'klausur-korrektur' , label : 'Abiturklausuren' } ,
{ source : 'lehrer-onboarding' , target : 'correction' , label : 'Korrektur' } ,
{ source : 'lehrer-onboarding' , target : 'letters' , label : 'Briefe' } ,
{ source : 'lehrer-onboarding' , target : 'school-classes' , label : 'Klassen' } ,
{ source : 'lehrer-onboarding' , target : 'jitsi' , label : 'Meet' } ,
{ source : 'lehrer-onboarding' , target : 'hilfe' , label : 'Doku' } ,
{ source : 'lehrer-onboarding' , target : 'admin' , label : 'Settings' } ,
{ source : 'lehrer-dashboard' , target : 'worksheets' } ,
{ source : 'lehrer-dashboard' , target : 'correction' } ,
{ source : 'lehrer-dashboard' , target : 'jitsi' } ,
{ source : 'lehrer-dashboard' , target : 'letters' } ,
{ source : 'lehrer-dashboard' , target : 'messenger' } ,
{ source : 'lehrer-dashboard' , target : 'klausur-korrektur' } ,
{ source : 'lehrer-dashboard' , target : 'companion' } ,
{ source : 'lehrer-dashboard' , target : 'alerts' } ,
{ source : 'lehrer-dashboard' , target : 'mail' } ,
{ source : 'lehrer-dashboard' , target : 'school-classes' } ,
{ source : 'lehrer-dashboard' , target : 'lehrer-onboarding' , label : 'Sidebar' } ,
{ source : 'school-classes' , target : 'school-exams' } ,
{ source : 'school-classes' , target : 'school-grades' } ,
{ source : 'school-grades' , target : 'school-gradebook' } ,
{ source : 'school-gradebook' , target : 'school-certificates' } ,
{ source : 'worksheets' , target : 'content-creator' } ,
{ source : 'worksheets' , target : 'unit-creator' } ,
{ source : 'content-creator' , target : 'content-feed' } ,
{ source : 'klausur-korrektur' , target : 'abitur-docs-admin' } ,
{ source : 'admin' , target : 'rbac-admin' } ,
{ source : 'admin' , target : 'system-info' } ,
{ source : 'admin' , target : 'workflow' } ,
]
// ============================================
// ADMIN SCREENS (Port 3000)
// ============================================
const ADMIN_SCREENS : ScreenDefinition [ ] = [
{ id : 'admin-dashboard' , name : 'Dashboard' , description : 'Übersicht & Statistiken' , category : 'overview' , icon : '🏠' , url : '/admin' } ,
{ id : 'admin-onboarding' , name : 'Onboarding' , description : 'Lern-Wizards für alle Module' , category : 'overview' , icon : '📖' , url : '/admin/onboarding' } ,
{ id : 'admin-gpu' , name : 'GPU Infrastruktur' , description : 'vast.ai GPU Management' , category : 'infrastructure' , icon : '🖥️' , url : '/admin/gpu' } ,
{ id : 'admin-middleware' , name : 'Middleware' , description : 'Middleware Stack & Test' , category : 'infrastructure' , icon : '🔧' , url : '/admin/middleware' } ,
{ id : 'admin-mac-mini' , name : 'Mac Mini' , description : 'Headless Mac Mini Control' , category : 'infrastructure' , icon : '🍎' , url : '/admin/mac-mini' } ,
{ id : 'admin-consent' , name : 'Consent Verwaltung' , description : 'Rechtliche Dokumente' , category : 'compliance' , icon : '📄' , url : '/admin/consent' } ,
{ id : 'admin-dsr' , name : 'Datenschutzanfragen' , description : 'DSGVO Art. 15-21' , category : 'compliance' , icon : '🔒' , url : '/admin/dsr' } ,
{ id : 'admin-dsms' , name : 'DSMS' , description : 'Datenschutz-Management' , category : 'compliance' , icon : '🛡️' , url : '/admin/dsms' } ,
{ id : 'admin-compliance' , name : 'Compliance' , description : 'GRC & Audit' , category : 'compliance' , icon : '✅' , url : '/admin/compliance' } ,
{ id : 'admin-docs-audit' , name : 'DSGVO-Audit' , description : 'Audit-Dokumentation' , category : 'compliance' , icon : '📋' , url : '/admin/docs/audit' } ,
{ id : 'admin-rag' , name : 'Daten & RAG' , description : 'Training Data & RAG' , category : 'ai' , icon : '🗄️' , url : '/admin/rag' } ,
{ id : 'admin-ocr-labeling' , name : 'OCR-Labeling' , description : 'Handschrift-Training' , category : 'ai' , icon : '🏷️' , url : '/admin/ocr-labeling' } ,
{ id : 'admin-magic-help' , name : 'Magic Help (TrOCR)' , description : 'Handschrift-OCR' , category : 'ai' , icon : '✨' , url : '/admin/magic-help' } ,
{ id : 'admin-companion' , name : 'Companion Dev' , description : 'Lesson-Modus Entwicklung' , category : 'ai' , icon : '📚' , url : '/admin/companion' } ,
{ id : 'admin-communication' , name : 'Kommunikation' , description : 'Matrix & Jitsi Monitoring' , category : 'communication' , icon : '💬' , url : '/admin/communication' } ,
{ id : 'admin-alerts' , name : 'Alerts Monitoring' , description : 'Google Alerts & Feeds' , category : 'communication' , icon : '🔔' , url : '/admin/alerts' } ,
{ id : 'admin-mail' , name : 'Unified Inbox' , description : 'E-Mail & KI-Analyse' , category : 'communication' , icon : '📧' , url : '/admin/mail' } ,
{ id : 'admin-security' , name : 'Security' , description : 'DevSecOps Dashboard' , category : 'security' , icon : '🔐' , url : '/admin/security' } ,
{ id : 'admin-sbom' , name : 'SBOM' , description : 'Software Bill of Materials' , category : 'security' , icon : '📦' , url : '/admin/sbom' } ,
{ id : 'admin-screen-flow' , name : 'Screen Flow' , description : 'UI Verbindungen' , category : 'security' , icon : '🔀' , url : '/admin/screen-flow' } ,
{ id : 'admin-content' , name : 'Übersetzungen' , description : 'Website Content' , category : 'content' , icon : '🌍' , url : '/admin/content' } ,
{ id : 'admin-edu-search' , name : 'Education Search' , description : 'Bildungsquellen & Crawler' , category : 'content' , icon : '🔍' , url : '/admin/edu-search' } ,
{ id : 'admin-staff-search' , name : 'Personensuche' , description : 'Uni-Mitarbeiter' , category : 'content' , icon : '👤' , url : '/admin/staff-search' } ,
{ id : 'admin-uni-crawler' , name : 'Uni-Crawler' , description : 'Universitäts-Crawling' , category : 'content' , icon : '🕷️' , url : '/admin/uni-crawler' } ,
{ id : 'admin-game' , name : 'Breakpilot Drive' , description : 'Lernspiel Klasse 2-6' , category : 'game' , icon : '🎮' , url : '/admin/game' } ,
{ id : 'admin-unity-bridge' , name : 'Unity Bridge' , description : 'Unity Editor Steuerung' , category : 'game' , icon : '⚡' , url : '/admin/unity-bridge' } ,
{ id : 'admin-backlog' , name : 'Production Backlog' , description : 'Go-Live Checkliste' , category : 'misc' , icon : '📝' , url : '/admin/backlog' } ,
{ id : 'admin-brandbook' , name : 'Brandbook' , description : 'Corporate Design' , category : 'misc' , icon : '🎨' , url : '/admin/brandbook' } ,
{ id : 'admin-docs' , name : 'Developer Docs' , description : 'API & Architektur' , category : 'misc' , icon : '📖' , url : '/admin/docs' } ,
{ id : 'admin-pca-platform' , name : 'PCA Platform' , description : 'Bot-Erkennung' , category : 'misc' , icon : '💰' , url : '/admin/pca-platform' } ,
]
const ADMIN_CONNECTIONS : ConnectionDef [ ] = [
{ source : 'admin-dashboard' , target : 'admin-onboarding' } ,
{ source : 'admin-dashboard' , target : 'admin-security' } ,
{ source : 'admin-dashboard' , target : 'admin-compliance' } ,
{ source : 'admin-onboarding' , target : 'admin-gpu' } ,
{ source : 'admin-onboarding' , target : 'admin-consent' } ,
{ source : 'admin-consent' , target : 'admin-dsr' } ,
{ source : 'admin-dsr' , target : 'admin-dsms' } ,
{ source : 'admin-dsms' , target : 'admin-compliance' } ,
{ source : 'admin-compliance' , target : 'admin-docs-audit' } ,
{ source : 'admin-rag' , target : 'admin-ocr-labeling' } ,
{ source : 'admin-ocr-labeling' , target : 'admin-magic-help' } ,
{ source : 'admin-magic-help' , target : 'admin-companion' } ,
{ source : 'admin-security' , target : 'admin-sbom' } ,
{ source : 'admin-sbom' , target : 'admin-screen-flow' } ,
{ source : 'admin-communication' , target : 'admin-alerts' } ,
{ source : 'admin-alerts' , target : 'admin-mail' } ,
{ source : 'admin-gpu' , target : 'admin-middleware' } ,
{ source : 'admin-middleware' , target : 'admin-mac-mini' } ,
{ source : 'admin-game' , target : 'admin-unity-bridge' } ,
{ source : 'admin-edu-search' , target : 'admin-staff-search' } ,
{ source : 'admin-staff-search' , target : 'admin-uni-crawler' } ,
]
// ============================================
// CATEGORY COLORS
// ============================================
const STUDIO_COLORS : Record < string , { bg : string ; border : string ; text : string } > = {
navigation : { bg : '#dbeafe' , border : '#3b82f6' , text : '#1e40af' } ,
content : { bg : '#dcfce7' , border : '#22c55e' , text : '#166534' } ,
communication : { bg : '#fef3c7' , border : '#f59e0b' , text : '#92400e' } ,
school : { bg : '#fce7f3' , border : '#ec4899' , text : '#9d174d' } ,
admin : { bg : '#f3e8ff' , border : '#a855f7' , text : '#6b21a8' } ,
ai : { bg : '#cffafe' , border : '#06b6d4' , text : '#0e7490' } ,
}
const ADMIN_COLORS : Record < string , { bg : string ; border : string ; text : string } > = {
overview : { bg : '#dbeafe' , border : '#3b82f6' , text : '#1e40af' } ,
infrastructure : { bg : '#fef3c7' , border : '#f59e0b' , text : '#92400e' } ,
compliance : { bg : '#dcfce7' , border : '#22c55e' , text : '#166534' } ,
ai : { bg : '#cffafe' , border : '#06b6d4' , text : '#0e7490' } ,
communication : { bg : '#fce7f3' , border : '#ec4899' , text : '#9d174d' } ,
security : { bg : '#fee2e2' , border : '#ef4444' , text : '#991b1b' } ,
content : { bg : '#f3e8ff' , border : '#a855f7' , text : '#6b21a8' } ,
game : { bg : '#fef9c3' , border : '#eab308' , text : '#713f12' } ,
misc : { bg : '#f1f5f9' , border : '#64748b' , text : '#334155' } ,
}
const STUDIO_LABELS : Record < string , string > = {
navigation : 'Navigation' ,
content : 'Content & Tools' ,
communication : 'Kommunikation' ,
school : 'Schulverwaltung' ,
admin : 'Administration' ,
ai : 'KI & Assistent' ,
}
const ADMIN_LABELS : Record < string , string > = {
overview : 'Übersicht' ,
infrastructure : 'Infrastruktur' ,
compliance : 'DSGVO & Compliance' ,
ai : 'KI & LLM' ,
communication : 'Kommunikation' ,
security : 'Security & DevOps' ,
content : 'Content & Suche' ,
game : 'Game & Unity' ,
misc : 'Sonstiges' ,
}
// ============================================
// HELPER: Find all connected nodes (recursive)
// ============================================
function findConnectedNodes (
startNodeId : string ,
connections : ConnectionDef [ ] ,
direction : 'children' | 'parents' | 'both' = 'children'
) : Set < string > {
const connected = new Set < string > ( )
connected . add ( startNodeId )
const queue = [ startNodeId ]
while ( queue . length > 0 ) {
const current = queue . shift ( ) !
connections . forEach ( conn = > {
if ( ( direction === 'children' || direction === 'both' ) && conn . source === current ) {
if ( ! connected . has ( conn . target ) ) {
connected . add ( conn . target )
queue . push ( conn . target )
}
}
if ( ( direction === 'parents' || direction === 'both' ) && conn . target === current ) {
if ( ! connected . has ( conn . source ) ) {
connected . add ( conn . source )
queue . push ( conn . source )
}
}
} )
}
return connected
}
// ============================================
// HELPER: Construct embed URL
// ============================================
function constructEmbedUrl ( baseUrl : string , url : string | undefined ) : string | null {
if ( ! url ) return null
const hashIndex = url . indexOf ( '#' )
if ( hashIndex !== - 1 ) {
const basePart = url . substring ( 0 , hashIndex )
const hashPart = url . substring ( hashIndex )
const separator = basePart . includes ( '?' ) ? '&' : '?'
return ` ${ baseUrl } ${ basePart } ${ separator } embed=true ${ hashPart } `
} else {
const separator = url . includes ( '?' ) ? '&' : '?'
return ` ${ baseUrl } ${ url } ${ separator } embed=true `
}
}
// ============================================
// LAYOUT HELPERS
// ============================================
const getNodePosition = (
id : string ,
category : string ,
screens : ScreenDefinition [ ] ,
flowType : FlowType
) = > {
const studioPositions : Record < string , { x : number ; y : number } > = {
navigation : { x : 400 , y : 50 } ,
content : { x : 50 , y : 250 } ,
communication : { x : 750 , y : 250 } ,
school : { x : 50 , y : 500 } ,
admin : { x : 750 , y : 500 } ,
ai : { x : 400 , y : 380 } ,
}
const adminPositions : Record < string , { x : number ; y : number } > = {
overview : { x : 400 , y : 30 } ,
infrastructure : { x : 50 , y : 150 } ,
compliance : { x : 700 , y : 150 } ,
ai : { x : 50 , y : 350 } ,
communication : { x : 400 , y : 350 } ,
security : { x : 700 , y : 350 } ,
content : { x : 50 , y : 550 } ,
game : { x : 400 , y : 550 } ,
misc : { x : 700 , y : 550 } ,
}
const positions = flowType === 'studio' ? studioPositions : adminPositions
const base = positions [ category ] || { x : 400 , y : 300 }
const categoryScreens = screens . filter ( s = > s . category === category )
const categoryIndex = categoryScreens . findIndex ( s = > s . id === id )
const cols = Math . ceil ( Math . sqrt ( categoryScreens . length + 1 ) )
const row = Math . floor ( categoryIndex / cols )
const col = categoryIndex % cols
return {
x : base.x + col * 160 ,
y : base.y + row * 90 ,
}
}
// ============================================
// MAIN COMPONENT
// ============================================
import { ScreenDefinition , FlowType } from './_components/types'
import {
STUDIO_SCREENS , STUDIO_CONNECTIONS ,
ADMIN_SCREENS , ADMIN_CONNECTIONS ,
STUDIO_COLORS , ADMIN_COLORS ,
STUDIO_LABELS , ADMIN_LABELS ,
} from './_components/screen-data'
import { findConnectedNodes , constructEmbedUrl , getNodePosition } from './_components/flow-helpers'
export default function ScreenFlowPage() {
const [ flowType , setFlowType ] = useState < FlowType > ( 'studio' )
@@ -329,82 +39,44 @@ export default function ScreenFlowPage() {
const [ previewUrl , setPreviewUrl ] = useState < string | null > ( null )
const [ previewScreen , setPreviewScreen ] = useState < ScreenDefinition | null > ( null )
// Get data based on flow type
const screens = flowType === 'studio' ? STUDIO_SCREENS : ADMIN_SCREENS
const connections = flowType === 'studio' ? STUDIO_CONNECTIONS : ADMIN_CONNECTIONS
const colors = flowType === 'studio' ? STUDIO_COLORS : ADMIN_COLORS
const labels = flowType === 'studio' ? STUDIO_LABELS : ADMIN_LABELS
const baseUrl = flowType === 'studio' ? 'http://macmini:8000' : 'http://macmini:3000'
// Calculate connected nodes
const connectedNodes = useMemo ( ( ) = > {
if ( ! selectedNode ) return new Set < string > ( )
return findConnectedNodes ( selectedNode , connections , 'children' )
} , [ selectedNode , connections ] )
// Create nodes with useMemo
const initialNodes = useMemo ( ( ) : Node [ ] = > {
return screens . map ( ( screen ) = > {
const catColors = colors [ screen . category ] || { bg : '#f1f5f9' , border : '#94a3b8' , text : '#475569' }
const position = getNodePosition ( screen . id , screen . category , screens , flowType )
// Determine opacity
let opacity = 1
if ( selectedNode ) {
opacity = connectedNodes . has ( screen . id ) ? 1 : 0.2
} else if ( selectedCategory ) {
opacity = screen . category === selectedCategory ? 1 : 0.2
}
const isSelected = selectedNode === screen . id
return {
id : screen.id ,
type : 'default' ,
position ,
data : {
label : (
< div className = "text-center p-1" >
< div className = "text-lg mb-1" > { screen . icon } < / div >
< div className = "font-medium text-xs leading-tight" > { screen . name } < / div >
< / div >
) ,
} ,
style : {
background : isSelected ? catColors.border : catColors.bg ,
color : isSelected ? 'white' : catColors . text ,
border : ` 2px solid ${ catColors . border } ` ,
borderRadius : '12px' ,
padding : '6px' ,
minWidth : '110px' ,
opacity ,
cursor : 'pointer' ,
boxShadow : isSelected ? ` 0 0 20px ${ catColors . border } ` : 'none' ,
} ,
id : screen.id , type : 'default' , position ,
data : { label : ( < div className = "text-center p-1" > < div className = "text-lg mb-1" > { screen . icon } < / div > < div className = "font-medium text-xs leading-tight" > { screen . name } < / div > < / div > ) } ,
style : { background : isSelected ? catColors.border : catColors.bg , color : isSelected ? 'white' : catColors . text , border : ` 2px solid ${ catColors . border } ` , borderRadius : '12px' , padding : '6px' , minWidth : '110px' , opacity , cursor : 'pointer' , boxShadow : isSelected ? ` 0 0 20px ${ catColors . border } ` : 'none' } ,
}
} )
} , [ screens , colors , flowType , selectedCategory , selectedNode , connectedNodes ] )
// Create edges with useMemo
const initialEdges = useMemo ( ( ) : Edge [ ] = > {
return connections . map ( ( conn , index ) = > {
const isHighlighted = selectedNode && ( conn . source === selectedNode || conn . target === selectedNode )
const isInSubtree = selectedNode && connectedNodes . has ( conn . source ) && connectedNodes . has ( conn . target )
return {
id : ` e- ${ conn . source } - ${ conn . target } - ${ index } ` ,
source : conn.source ,
target : conn.target ,
label : conn.label ,
type : 'smoothstep' ,
animated : isHighlighted || false ,
style : {
stroke : isHighlighted ? '#3b82f6' : ( isInSubtree ? '#94a3b8' : '#e2e8f0' ) ,
strokeWidth : isHighlighted ? 3 : 1.5 ,
opacity : selectedNode ? ( isInSubtree ? 1 : 0.15 ) : 1 ,
} ,
labelStyle : { fontSize : 9 , fill : '#64748b' } ,
labelBgStyle : { fill : '#f8fafc' } ,
id : ` e- ${ conn . source } - ${ conn . target } - ${ index } ` , source : conn.source , target : conn.target , label : conn.label , type : 'smoothstep' , animated : isHighlighted || false ,
style : { stroke : isHighlighted ? '#3b82f6' : ( isInSubtree ? '#94a3b8' : '#e2e8f0' ) , strokeWidth : isHighlighted ? 3 : 1.5 , opacity : selectedNode ? ( isInSubtree ? 1 : 0.15 ) : 1 } ,
labelStyle : { fontSize : 9 , fill : '#64748b' } , labelBgStyle : { fill : '#f8fafc' } ,
markerEnd : { type : MarkerType . ArrowClosed , color : isHighlighted ? '#3b82f6' : '#94a3b8' , width : 15 , height : 15 } ,
}
} )
@@ -413,196 +85,74 @@ export default function ScreenFlowPage() {
const [ nodes , setNodes , onNodesChange ] = useNodesState ( [ ] )
const [ edges , setEdges , onEdgesChange ] = useEdgesState ( [ ] )
// Update nodes/edges when dependencies change
useEffect ( ( ) = > {
setNodes ( initialNodes )
setEdges ( initialEdges )
} , [ initialNodes , initialEdges ] )
useEffect ( ( ) = > { setNodes ( initialNodes ) ; setEdges ( initialEdges ) } , [ initialNodes , initialEdges ] )
// Reset when flow type changes
const handleFlowTypeChange = useCallback ( ( newType : FlowType ) = > {
setFlowType ( newType )
setSelectedNode ( null )
setSelectedCategory ( null )
setPreviewUrl ( null )
setPreviewScreen ( null )
setFlowType ( newType ) ; setSelectedNode ( null ) ; setSelectedCategory ( null ) ; setPreviewUrl ( null ) ; setPreviewScreen ( null )
} , [ ] )
// Handle node click
const onNodeClick = useCallback ( ( _event : React.MouseEvent , node : Node ) = > {
const screen = screens . find ( s = > s . id === node . id )
if ( selectedNode === node . id ) {
// Double-click: open in new tab
if ( screen ? . url ) {
window . open ( ` ${ baseUrl } ${ screen . url } ` , '_blank' )
}
return
}
setSelectedNode ( node . id )
setSelectedCategory ( null )
if ( screen ? . url ) {
const embedUrl = constructEmbedUrl ( baseUrl , screen . url )
setPreviewUrl ( embedUrl )
setPreviewScreen ( screen )
}
if ( selectedNode === node . id ) { if ( screen ? . url ) window . open ( ` ${ baseUrl } ${ screen . url } ` , '_blank' ) ; return }
setSelectedNode ( node . id ) ; setSelectedCategory ( null )
if ( screen ? . url ) { setPreviewUrl ( constructEmbedUrl ( baseUrl , screen . url ) ) ; setPreviewScreen ( screen ) }
} , [ screens , baseUrl , selectedNode ] )
// Handle background click - deselect
const onPaneClick = useCallback ( ( ) = > {
setSelectedNode ( null )
setPreviewUrl ( null )
setPreviewScreen ( null )
} , [ ] )
// Close preview
const closePreview = useCallback ( ( ) = > {
setPreviewUrl ( null )
setPreviewScreen ( null )
setSelectedNode ( null )
} , [ ] )
// Stats
const stats = {
totalScreens : screens.length ,
totalConnections : connections.length ,
connectedCount : connectedNodes.size ,
}
const onPaneClick = useCallback ( ( ) = > { setSelectedNode ( null ) ; setPreviewUrl ( null ) ; setPreviewScreen ( null ) } , [ ] )
const closePreview = useCallback ( ( ) = > { setPreviewUrl ( null ) ; setPreviewScreen ( null ) ; setSelectedNode ( null ) } , [ ] )
const categories = Object . keys ( labels )
// Connected screens list
const connectedScreens = selectedNode
? screens . filter ( s = > connectedNodes . has ( s . id ) )
: [ ]
const connectedScreens = selectedNode ? screens . filter ( s = > connectedNodes . has ( s . id ) ) : [ ]
return (
< AdminLayout
title = "Screen Flow"
description = "Visualisierung aller UI-Screens und ihrer Verbindungen"
>
< AdminLayout title = "Screen Flow" description = "Visualisierung aller UI-Screens und ihrer Verbindungen" >
{ /* Flow Type Selector */ }
< div className = "grid grid-cols-2 gap-4 mb-6" >
< button
onClick = { ( ) = > handleFlowTypeChange ( 'studio' ) }
className = { ` p-6 rounded-xl border-2 transition-all ${
flowType === 's tudio'
? 'border-green -500 bg-green-50 shadow-lg'
: 'border-slate-200 bg-white hover:border-slate-300'
} ` }
>
< div className = "flex items-center gap-4" >
< div className = { ` w-14 h-14 rounded-xl flex items-center justify-center text-2xl ${
flowType === 'studio' ? 'bg-green-500 text-white' : 'bg-slate-100'
} ` } >
🎓
< / div >
< div className = "text-left" >
< div className = "font-bold text-lg" > Studio ( Port 8000 ) < / div >
< div className = "text-sm text-slate-500" > Lehrer - Oberfläche < / div >
< div className = "text-xs text-slate-400 mt-1" > { STUDIO_SCREENS . length } Screens < / div >
< / div >
< / div >
< / button >
< button
onClick = { ( ) = > handleFlowTypeChange ( 'admin' ) }
className = { ` p-6 rounded-xl border-2 transition-all ${
flowType === 'admin'
? 'border-purple-500 bg-purple-50 shadow-lg'
: 'border-slate-200 bg-white hover:border-slate-300'
} ` }
>
< div className = "flex items-center gap-4" >
< div className = { ` w-14 h-14 rounded-xl flex items-center justify-center text-2xl ${
flowType === 'admin' ? 'bg-purple-500 text-white' : 'bg-slate-100'
} ` } >
⚙ ️
< / div >
< div className = "text-left" >
< div className = "font-bold text-lg" > Admin ( Port 3000 ) < / div >
< div className = "text-sm text-slate-500" > Admin Panel < / div >
< div className = "text-xs text-slate-400 mt-1" > { ADMIN_SCREENS . length } Screens < / div >
< / div >
< / div >
< / button >
{ ( [ 'studio' , 'admin' ] as const ) . map ( type = > {
const isActive = flowType === type
const cfg = type === 'studio'
? { icon : '🎓' , label : 'S tudio (Port 8000)' , sub : 'Lehrer-Oberflaeche' , count : STUDIO_SCREENS.length , activeColor : 'border-green-500 bg-green-50' , iconBg : 'bg-green-500 text-white' }
: { icon : '⚙️' , label : 'Admin (Port 3000)' , sub : 'Admin Panel' , count : ADMIN_SCREENS.length , activeColor : 'border-purple -500 bg-purple-50' , iconBg : 'bg-purple-500 text-white' }
return (
< button key = { type } onClick = { ( ) = > handleFlowTypeChange ( type ) } className = { ` p-6 rounded-xl border-2 transition-all ${ isActive ? ` ${ cfg . activeColor } shadow-lg ` : 'border-slate-200 bg-white hover:border-slate-300' } ` } >
< div className = "flex items-center gap-4" >
< div className = { ` w-14 h-14 rounded-xl flex items-center justify-center text-2xl ${ isActive ? cfg . iconBg : 'bg-slate-100' } ` } > { cfg . icon } < / div >
< div className = "text-left" >
< div className = "font-bold text-lg" > { cfg . label } < / div >
< div className = "text-sm text-slate-500" > { cfg . sub } < / div >
< div className = "text-xs text-slate-400 mt-1" > { cfg . count } Screens < / div >
< / div >
< / div >
< / button >
)
} ) }
< / div >
{ /* Stats & Selection Info */ }
{ /* Stats */ }
< div className = "grid grid-cols-4 gap-4 mb-6" >
< div className = "bg-white rounded-lg shadow p-4" >
< div className = "text-3xl font-bold text-slat e-8 00" > { stats . totalScreens } < / div >
< div className = "text-sm text-slate-500" > Screens < / div >
< / div >
< div className = "bg-white rounded-lg shadow p-4" >
< div className = "text-3xl font-bold text-blue-600" > { stats . totalConnections } < / div >
< div className = "text-sm text-slate-500" > Verbindungen < / div >
< / div >
< div className = "bg-white rounded-lg shadow p-4" > < div className = "text-3xl font-bold text-slate-800" > { screens . length } < / div > < div className = "text-sm text-slate-500" > Screens < / div > < / div >
< div className = "bg-white rounded-lg shadow p-4" > < div className = "text-3xl font-bold text-blu e-6 00" > { connections . length } < / div > < div className = "text-sm text-slate-500" > Verbindungen < / div > < / div >
< div className = "bg-white rounded-lg shadow p-4 col-span-2" >
{ selectedNode ? (
< div className = "flex items-center gap-3" >
< div className = "text-3xl" > { previewScreen ? . icon } < / div >
< div >
< div className = "font-bold text -slate-8 00" > { previewScreen ? . name } < / div >
< div className = "text-sm text-slate-500" >
{ stats . connectedCount } verbundene Screen { stats . connectedCount !== 1 ? 's' : '' }
< / div >
< / div >
< button
onClick = { closePreview }
className = "ml-auto px-3 py-1 text-sm bg-slate-100 hover:bg-slate-200 rounded-lg"
>
Zurücksetzen
< / button >
< div > < div className = "font-bold text-slate-800" > { previewScreen ? . name } < / div > < div className = "text-sm text-slate-500" > { connectedNodes . size } verbundene Screen { connectedNodes . size !== 1 ? 's' : '' } < / div > < / div >
< button onClick = { closePreview } className = "ml-auto px-3 py-1 text-sm bg-slate-100 hover:bg -slate-2 00 rounded-lg" > Zuruecksetzen < / button >
< / div >
) : (
< div className = "text-slate-500 text-sm" >
Klicke auf einen Screen um den Subtree und die Vorschau zu sehen
< / div >
) }
) : ( < div className = "text-slate-500 text-sm" > Klicke auf einen Screen um den Subtree und die Vorschau zu sehen < / div > ) }
< / div >
< / div >
{ /* Category Filter */ }
< div className = "bg-white rounded-lg shadow p-4 mb-6" >
< div className = "flex flex-wrap gap-2" >
< button
onClick = { ( ) = > {
setSelectedCategory ( null )
setSelectedNode ( null )
setPreviewUrl ( null )
setPreviewScreen ( null )
} }
className = { ` px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
selectedCategory === null && ! selectedNode
? 'bg-slate-800 text-white'
: 'bg-slate-100 text-slate-700 hover:bg-slate-200'
} ` }
>
Alle ( { screens . length } )
< / button >
< button onClick = { ( ) = > { setSelectedCategory ( null ) ; setSelectedNode ( null ) ; setPreviewUrl ( null ) ; setPreviewScreen ( null ) } } className = { ` px-4 py-2 rounded-lg text-sm font-medium transition-colors ${ selectedCategory === null && ! selectedNode ? 'bg-slate-800 text-white' : 'bg-slate-100 text-slate-700 hover:bg-slate-200' } ` } > Alle ( { screens . length } ) < / button >
{ categories . map ( ( key ) = > {
const count = screens . filter ( s = > s . category === key ) . length
const catColors = colors [ key ] || { bg : '#f1f5f9' , border : '#94a3b8' , text : '#475569' }
return (
< button
key = { key }
onClick = { ( ) = > {
setSelectedCategory ( selectedCategory === key ? null : key )
setSelectedNode ( null )
setPreviewUrl ( null )
setPreviewScreen ( null )
} }
className = "px-4 py-2 rounded-lg text-sm font-medium transition-all flex items-center gap-2"
style = { {
background : selectedCategory === key ? catColors.border : catColors.bg ,
color : selectedCategory === key ? 'white' : catColors . text ,
} }
>
< span className = "w-3 h-3 rounded-full" style = { { background : catColors.border } } / >
{ labels [ key ] } ( { count } )
< button key = { key } onClick = { ( ) = > { setSelectedCategory ( selectedCategory === key ? null : key ) ; setSelectedNode ( null ) ; setPreviewUrl ( null ) ; setPreviewScreen ( null ) } } className = "px-4 py-2 rounded-lg text-sm font-medium transition-all flex items-center gap-2" style = { { background : selectedCategory === key ? catColors.border : catColors.bg , color : selectedCategory === key ? 'white' : catColors . text } } >
< span className = "w-3 h-3 rounded-full" style = { { background : catColors.border } } / > { labels [ key ] } ( { count } )
< / button >
)
} ) }
@@ -618,23 +168,8 @@ export default function ScreenFlowPage() {
const catColors = colors [ screen . category ] || { bg : '#f1f5f9' , border : '#94a3b8' , text : '#475569' }
const isCurrentNode = screen . id === selectedNode
return (
< button
key = { screen . id }
onClick = { ( ) = > {
const embedUrl = constructEmbedUrl ( baseUrl , screen . url )
setPreviewUrl ( embedUrl )
setPreviewScreen ( screen )
} }
className = { ` px-3 py-2 rounded-lg text-sm font-medium transition-all flex items-center gap-2 ${
previewScreen ? . id === screen . id ? 'ring-2 ring-blue-500' : ''
} ` }
style = { {
background : isCurrentNode ? catColors.border : catColors.bg ,
color : isCurrentNode ? 'white' : catColors . text ,
} }
>
< span > { screen . icon } < / span >
{ screen . name }
< button key = { screen . id } onClick = { ( ) = > { setPreviewUrl ( constructEmbedUrl ( baseUrl , screen . url ) ) ; setPreviewScreen ( screen ) } } className = { ` px-3 py-2 rounded-lg text-sm font-medium transition-all flex items-center gap-2 ${ previewScreen ? . id === screen . id ? 'ring-2 ring-blue-500' : '' } ` } style = { { background : isCurrentNode ? catColors.border : catColors.bg , color : isCurrentNode ? 'white' : catColors . text } } >
< span > { screen . icon } < / span > { screen . name }
< / button >
)
} ) }
@@ -644,50 +179,12 @@ export default function ScreenFlowPage() {
{ /* Flow Diagram */ }
< div className = "bg-white rounded-lg shadow overflow-hidden" style = { { height : previewUrl ? '350px' : '500px' } } >
< ReactFlow
nodes = { nodes }
edges = { edges }
onNodesChange = { onNodesChange }
onEdgesChange = { onEdgesChange }
onNodeClick = { onNodeClick }
onPaneClick = { onPaneClick }
fitView
fitViewOptions = { { padding : 0.2 } }
attributionPosition = "bottom-left"
>
< Controls / >
< MiniMap
nodeColor = { ( node ) = > {
const screen = screens . find ( s = > s . id === node . id )
const catColors = screen ? colors [ screen . category ] : null
return catColors ? . border || '#94a3b8'
} }
maskColor = "rgba(0, 0, 0, 0.1)"
/ >
< Background variant = { BackgroundVariant . Dots } gap = { 12 } size = { 1 } / >
< ReactFlow nodes = { nodes } edges = { edges } onNodesChange = { onNodesChange } onEdgesChange = { onEdgesChange } onNodeClick = { onNodeClick } onPaneClick = { onPaneClick } fitView fitViewOptions = { { padding : 0.2 } } attributionPosition = "bottom-left" >
< Controls / > < MiniMap nodeColor = { ( node ) = > { const s = screens . find ( s = > s . id === node . id ) ; return s ? ( colors [ s . category ] ? . border || '#94a3b8' ) : '#94a3b8' } } maskColor = "rgba(0, 0, 0, 0.1)" / > < Background variant = { BackgroundVariant . Dots } gap = { 12 } size = { 1 } / >
< Panel position = "top-left" className = "bg-white/95 p-3 rounded-lg shadow-lg text-xs" >
< div className = "font-medium text-slate-700 mb-2" >
{ flowType === 'studio' ? '🎓 Studio' : '⚙️ Admin' }
< / div >
< div className = "space-y-1" >
{ categories . slice ( 0 , 4 ) . map ( ( key ) = > {
const catColors = colors [ key ] || { bg : '#f1f5f9' , border : '#94a3b8' }
return (
< div key = { key } className = "flex items-center gap-2" >
< span
className = "w-3 h-3 rounded"
style = { { background : catColors.bg , border : ` 1px solid ${ catColors . border } ` } }
/ >
< span className = "text-slate-600" > { labels [ key ] } < / span >
< / div >
)
} ) }
< / div >
< div className = "mt-2 pt-2 border-t text-slate-400" >
Klick = Subtree + Preview < br / >
Doppelklick = Ö ffnen
< / div >
< div className = "font-medium text-slate-700 mb-2" > { flowType === 'studio' ? '🎓 Studio' : '⚙️ Admin' } < / div >
< div className = "space-y-1" > { categories . slice ( 0 , 4 ) . map ( ( key ) = > { const catColors = colors [ key ] || { bg : '#f1f5f9' , border : '#94a3b8' } ; return ( < div key = { key } className = "flex items-center gap-2" > < span className = "w-3 h-3 rounded" style = { { background : catColors.bg , border : ` 1px solid ${ catColors . border } ` } } / > < span className = "text-slate-600" > { labels [ key ] } < / span > < / div > ) } ) } < / div >
< div className = "mt-2 pt-2 border-t text-slate-400" > Klick = Subtree + Preview < br / > Doppelklick = Oeffnen < / div >
< / Panel >
< / ReactFlow >
< / div >
@@ -696,92 +193,32 @@ export default function ScreenFlowPage() {
{ previewUrl && (
< div className = "mt-6 bg-white rounded-lg shadow overflow-hidden" >
< div className = "px-4 py-3 bg-slate-50 border-b flex items-center justify-between" >
< div className = "flex items-center gap-3" >
< span className = "text-xl" > { previewScreen ? . icon } < / span >
< div >
< h3 className = "font-medium text-slate-700" > { previewScreen ? . name } < / h3 >
< p className = "text-xs text-slate-500" > { previewScreen ? . description } < / p >
< / div >
< / div >
< div className = "flex items-center gap-3" > < span className = "text-xl" > { previewScreen ? . icon } < / span > < div > < h3 className = "font-medium text-slate-700" > { previewScreen ? . name } < / h3 > < p className = "text-xs text-slate-500" > { previewScreen ? . description } < / p > < / div > < / div >
< div className = "flex items-center gap-2" >
< span className = "text-xs text-slate-400 font-mono" > { previewScreen ? . url } < / span >
< a
href = { ` ${ baseUrl } ${ previewScreen ? . url } ` }
target = "_blank"
rel = "noopener noreferrer"
className = "px-3 py-1 text-sm bg-blue-500 text-white rounded-lg hover:bg-blue-600 flex items-center gap-1"
>
< svg className = "w-4 h-4" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
< path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = { 2 } d = "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" / >
< / svg >
Ö ffnen
< / a >
< button
onClick = { closePreview }
className = "px-3 py-1 text-sm bg-slate-200 text-slate-700 rounded-lg hover:bg-slate-300 flex items-center gap-1"
>
< svg className = "w-4 h-4" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" >
< path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = { 2 } d = "M6 18L18 6M6 6l12 12" / >
< / svg >
Schließen
< / button >
< a href = { ` ${ baseUrl } ${ previewScreen ? . url } ` } target = "_blank" rel = "noopener noreferrer" className = "px-3 py-1 text-sm bg-blue-500 text-white rounded-lg hover:bg-blue-600 flex items-center gap-1" > < svg className = "w-4 h-4" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" > < path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = { 2 } d = "M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" / > < / svg > Oeffnen < / a >
< button onClick = { closePreview } className = "px-3 py-1 text-sm bg-slate-200 text-slate-700 rounded-lg hover:bg-slate-300 flex items-center gap-1" > < svg className = "w-4 h-4" fill = "none" stroke = "currentColor" viewBox = "0 0 24 24" > < path strokeLinecap = "round" strokeLinejoin = "round" strokeWidth = { 2 } d = "M6 18L18 6M6 6l12 12" / > < / svg > Schliessen < / button >
< / div >
< / div >
< div className = "relative" style = { { height : '600px' } } >
< iframe
src = { previewUrl }
className = "w-full h-full border-0"
title = { ` Preview: ${ previewScreen ? . name } ` }
/ >
< / div >
< div className = "relative" style = { { height : '600px' } } > < iframe src = { previewUrl } className = "w-full h-full border-0" title = { ` Preview: ${ previewScreen ? . name } ` } / > < / div >
< / div >
) }
{ /* Screen List (when no preview) */ }
{ /* Screen List */ }
{ ! previewUrl && (
< div className = "mt-6 bg-white rounded-lg shadow overflow-hidden" >
< div className = "px-4 py-3 bg-slate-50 border-b flex items-center justify-between" >
< h3 className = "font-medium text-slate-700" >
Alle Screens ( { screens . length } )
< / h3 >
< span className = "text-xs text-slate-400" > { baseUrl } < / span >
< / div >
< div className = "px-4 py-3 bg-slate-50 border-b flex items-center justify-between" > < h3 className = "font-medium text-slate-700" > Alle Screens ( { screens . length } ) < / h3 > < span className = "text-xs text-slate-400" > { baseUrl } < / span > < / div >
< div className = "divide-y max-h-80 overflow-y-auto" >
{ screens
. filter ( s = > ! selectedCategory || s . category === selectedCategory )
. map ( ( screen ) = > {
const catColors = colors [ screen . category ] || { bg : '#f1f5f9' , border : '#94a3b8' , text : '#475569' }
return (
< button
key = { screen . id }
onClick = { ( ) = > {
setSelectedNode ( screen . id )
setSelectedCategory ( null )
const embedUrl = constructEmbedUrl ( baseUrl , screen . url )
setPreviewUrl ( embedUrl )
setPreviewScreen ( screen )
} }
className = "w-full flex items-center gap-4 p-3 hover:bg-slate-50 transition-colors text-left"
>
< span
className = "w-9 h-9 rounded-lg flex items-center justify-center text-lg"
style = { { background : catColors.bg } }
>
{ screen . icon }
< / span >
< div className = "flex-1 min-w-0" >
< div className = "font-medium text-slate-800 text-sm" > { screen . name } < / div >
< div className = "text-xs text-slate-500 truncate" > { screen . description } < / div >
< / div >
< span
className = "px-2 py-1 rounded text-xs font-medium shrink-0"
style = { { background : catColors.bg , color : catColors.text } }
>
{ labels [ screen . category ] }
< / span >
< / button >
)
} ) }
{ screens . filter ( s = > ! selectedCategory || s . category === selectedCategory ) . map ( ( screen ) = > {
const catColors = colors [ screen . category ] || { bg : '#f1f5f9' , border : '#94a3b8' , text : '#475569' }
return (
< button key = { screen . id } onClick = { ( ) = > { setSelectedNode ( screen . id ) ; setSelectedCategory ( null ) ; setPreviewUrl ( constructEmbedUrl ( baseUrl , screen . url ) ) ; setPreviewScreen ( screen ) } } className = "w-full flex items-center gap-4 p-3 hover:bg-slate-50 transition-colors text-left" >
< span className = "w-9 h-9 rounded-lg flex items-center justify-center text-lg" style = { { background : catColors.bg } } > { screen . icon } < / span >
< div className = "flex-1 min-w-0" > < div className = "font-medium text-slate-800 text-sm" > { screen . name } < / div > < div className = "text-xs text-slate-500 truncate" > { screen . description } < / div > < / div >
< span className = "px-2 py-1 rounded text-xs font-medium shrink-0" style = { { background : catColors.bg , color : catColors.text } } > { labels [ screen . category ] } < / span >
< / button >
)
} ) }
< / div >
< / div >
) }