From cbba9392c01dfebb325ef7803438cb6b3453b013 Mon Sep 17 00:00:00 2001 From: ValueOn AG Date: Mon, 6 Apr 2026 09:59:56 +0200 Subject: [PATCH] cleanup --- README.md | 29 +- TOPICS.md | 4 +- d-guides/cursor-doc-sync.md | 32 + .../c-work/doc_workflow_editor_concept.md | 1219 +++++++++++++++++ 4 files changed, 1255 insertions(+), 29 deletions(-) create mode 100644 d-guides/cursor-doc-sync.md create mode 100644 z-archive/c-work/doc_workflow_editor_concept.md diff --git a/README.md b/README.md index d164dde..287d27c 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ - + # PowerOn PORTA -- Dokumentation @@ -72,32 +72,7 @@ b-reference/ --- -## Cursor-Regel: Doc-Sync - -Eine Cursor-Rule sorgt dafür, dass der AI-Assistent bei konzeptionellen Code-Änderungen automatisch an die Wiki-Pflege erinnert. - -**Datei:** `@poweron/.cursor/rules/doc-sync.mdc` (Quelle: `wiki/d-guides/doc-sync.mdc`) - -Falls die Regel lokal noch nicht existiert, kopiere sie: - -```powershell -# Prüfen und kopieren (PowerShell) -$src = "wiki\d-guides\doc-sync.mdc" -$dst = ".cursor\rules\doc-sync.mdc" -if (-not (Test-Path $dst)) { - New-Item -ItemType Directory -Path ".cursor\rules" -Force | Out-Null - Copy-Item $src $dst - Write-Host "doc-sync.mdc installiert" -} else { - Write-Host "doc-sync.mdc bereits vorhanden" -} -``` - -**Was die Regel tut:** -- Erinnert bei Änderungen an API, Domain-Logik, Security, RBAC, Navigation, Features, Billing -- Während Arbeit: `c-work//.md` pflegen -- Bei Abschluss: betroffene `b-reference/` Seite + `TOPICS.md` aktualisieren -- Kein Doku-Zwang bei reinen Refactors, Formatting oder Test-only Changes +**Cursor Doc-Sync:** Installationshinweise und Verhalten der Regel → [d-guides/cursor-doc-sync.md](d-guides/cursor-doc-sync.md) --- diff --git a/TOPICS.md b/TOPICS.md index f30790d..751a12c 100644 --- a/TOPICS.md +++ b/TOPICS.md @@ -1,5 +1,5 @@  - + # Themen-Index für AI-Kontext @@ -53,7 +53,7 @@ Lade immer zuerst diese Datei. Dann gezielt die passende(n) Referenz-Datei(en). | Secrets-Verschluesselung | d-guides/encrypt-env-secrets.md | Env-Dateien verschluesseln | | Google OAuth | d-guides/google-oauth-setup.md | OAuth Auth/Data Apps einrichten | | Security-Migration | d-guides/security-migration-guide.md | JWT Cookie Migration | -| Doc-Sync Cursor-Rule | d-guides/doc-sync.mdc | Cursor-Regel installieren, Doku-Workflow | +| Doc-Sync Cursor-Rule | d-guides/cursor-doc-sync.md | Installation, Regel-Quelle `doc-sync.mdc`, Doku-Workflow | ## Compliance & Sicherheit diff --git a/d-guides/cursor-doc-sync.md b/d-guides/cursor-doc-sync.md new file mode 100644 index 0000000..f3b130b --- /dev/null +++ b/d-guides/cursor-doc-sync.md @@ -0,0 +1,32 @@ + + + +# Cursor-Regel: Doc-Sync + +Eine Cursor-Rule erinnert den AI-Assistenten bei konzeptionellen Code-Änderungen an die Wiki-Pflege. + +**Regel-Datei (Quelle im Repo):** [doc-sync.mdc](./doc-sync.mdc) +**Ziel nach Installation:** `.cursor/rules/doc-sync.mdc` (im PowerOn-Repo-Root, nicht unter `wiki/`) + +## Installation (PowerShell) + +Aus dem PowerOn-Repo-Root ausführen: + +```powershell +$src = "wiki\d-guides\doc-sync.mdc" +$dst = ".cursor\rules\doc-sync.mdc" +if (-not (Test-Path $dst)) { + New-Item -ItemType Directory -Path ".cursor\rules" -Force | Out-Null + Copy-Item $src $dst + Write-Host "doc-sync.mdc installiert" +} else { + Write-Host "doc-sync.mdc bereits vorhanden" +} +``` + +## Was die Regel tut + +- Erinnert bei Änderungen an API, Domain-Logik, Security, RBAC, Navigation, Features, Billing +- Während Arbeit: `c-work//.md` pflegen +- Bei Abschluss: betroffene `b-reference/` Seite + `TOPICS.md` aktualisieren +- Kein Doku-Zwang bei reinen Refactors, Formatting oder Test-only Changes diff --git a/z-archive/c-work/doc_workflow_editor_concept.md b/z-archive/c-work/doc_workflow_editor_concept.md new file mode 100644 index 0000000..66c0df4 --- /dev/null +++ b/z-archive/c-work/doc_workflow_editor_concept.md @@ -0,0 +1,1219 @@ +# Graphischer Workflow-Editor Konzept + +## Übersicht + +Dieses Dokument beschreibt das Konzept für einen graphischen Workflow-Editor, der es ermöglicht, Workflow-Templates visuell zu modellieren. Der Editor wird für die Erstellung und Bearbeitung von Automation-Templates verwendet, die in `formAutomations.js` verwaltet werden. + +## Wichtige Terminologie: Workflow vs. WorkflowTemplate + +**WICHTIG**: Es gibt eine klare Trennung zwischen ausführbaren Workflows und Templates: + +### ChatWorkflow (ausführbares Objekt) +- **Klasse**: `ChatWorkflow` (in `datamodelChat.py`) +- **Zweck**: Ausführbare Workflow-Instanz +- **Eigenschaften**: + - Hat `id`, `status` (running/completed/stopped/error) + - Enthält `tasks` (Liste von TaskItem-Objekten) + - Enthält `messages`, `logs`, `stats` + - Hat `workflowMode` (automatic oder dynamic) +- **Ausführung**: + - Kann über Route mit `start` oder `stop` ausgeführt werden + - Wird automatisch (scheduled) oder dynamisch (on-demand) gestartet +- **Lebenszyklus**: Wird zur Laufzeit erstellt und ausgeführt + +### AutomationDefinition (Template-Objekt) +- **Klasse**: `AutomationDefinition` (in `datamodelChat.py`) +- **Zweck**: Vorlage für ChatWorkflows +- **Eigenschaften**: + - Hat `id`, `label`, `schedule` (Cron-Pattern) + - Enthält `template` (JSON-String mit Workflow-Struktur) + - Enthält `placeholders` (Dict mit Platzhalter-Werten) + - Hat `active` (ob Automation aktiviert ist) +- **Verwendung**: + - Wird im Workflow-Editor bearbeitet + - Wird verwendet, um ChatWorkflows zu erstellen + - Template wird mit Placeholders gefüllt → ChatWorkflow wird erstellt +- **Lebenszyklus**: Wird einmal erstellt und mehrfach verwendet + +### WorkflowTemplateModel (Editor-Modell) +- **Klasse**: `WorkflowTemplateModel` (nur für Editor, nicht in Datenbank) +- **Zweck**: Strukturiertes Modell für graphischen Editor +- **Verwendung**: + - Wird aus `AutomationDefinition.template` (JSON-String) geparst + - Wird im Editor bearbeitet + - Wird zurück zu `AutomationDefinition.template` konvertiert +- **Lebenszyklus**: Nur während Editor-Session vorhanden + +### Beziehung zwischen Objekten + +``` +AutomationDefinition (Template) + ↓ (wird verwendet, um zu erstellen) +ChatWorkflow (ausführbare Instanz) + ↓ (wird ausgeführt) +TaskItem → ActionItem → Ergebnisse +``` + +**Beispiel**: +1. `AutomationDefinition` mit `template: '{"tasks": [...]}'` und `placeholders: {"connectionName": "MyConn"}` +2. Template wird mit Placeholders gefüllt → `ChatWorkflow` wird erstellt +3. `ChatWorkflow` wird gestartet → Tasks werden ausgeführt → Actions werden ausgeführt + +## Zielsetzung + +1. **Visuelle Modellierung**: Workflows mit Tasks und Actions graphisch erstellen +2. **Parameter-Konfiguration**: Action-Parameter direkt im Editor setzen +3. **Dependency-Management**: Document-Dependencies zwischen Actions visuell darstellen +4. **Template-Generierung**: Automatische Generierung von JSON-Templates aus graphischem Modell + +## Datenstruktur-Analyse + +### Bestehende Datenmodelle + +#### ChatWorkflow (datamodelChat.py) - AUSFÜHRBARES OBJEKT +```python +class ChatWorkflow(BaseModel): + """Ausführbare Workflow-Instanz""" + id: str + mandateId: str + status: str # "running", "completed", "stopped", "error" + workflowMode: WorkflowModeEnum # "automatic" oder "dynamic" + tasks: list # Liste von TaskItem-Objekten (zur Laufzeit erstellt) + messages: List[ChatMessage] + logs: List[ChatLog] + stats: List[ChatStat] + # ... weitere Felder +``` + +**WICHTIG**: `ChatWorkflow` ist das **ausführbare Objekt**, das über Route mit `start`/`stop` gesteuert werden kann. + +#### TaskItem (datamodelChat.py) - Teil von ChatWorkflow +```python +class TaskItem(BaseModel): + """Task innerhalb eines ausführbaren ChatWorkflows""" + id: str + workflowId: str # Foreign Key zu ChatWorkflow + userInput: str + status: TaskStatus + actionList: List[ActionItem] # Liste von Actions + dependencies: List[str] # Task IDs, von denen dieser Task abhängt + # ... weitere Felder +``` + +**WICHTIG**: `TaskItem` existiert nur in ausführbaren `ChatWorkflow`-Instanzen, nicht in Templates. + +#### ActionItem (datamodelChat.py) - Teil von TaskItem +```python +class ActionItem(BaseModel): + """Action innerhalb eines TaskItems""" + id: str + execMethod: str # z.B. "outlook", "sharepoint", "ai" + execAction: str # z.B. "readEmails", "uploadDocument" + execParameters: Dict[str, Any] # Action-Parameter (mit konkreten Werten) + execResultLabel: Optional[str] # Label für resultierende Documents + # ... weitere Felder +``` + +**WICHTIG**: `ActionItem` existiert nur in ausführbaren `ChatWorkflow`-Instanzen, nicht in Templates. + +#### AutomationDefinition (datamodelChat.py) - TEMPLATE-OBJEKT +```python +class AutomationDefinition(BaseModel): + """Template/Vorlage für ChatWorkflows""" + id: str + mandateId: str + label: str # User-friendly name + template: str # JSON-String mit Workflow-Struktur (enthält Tasks und Actions als Template) + placeholders: Dict[str, str] # Platzhalter-Werte (z.B. {"connectionName": "MyConn"}) + schedule: str # Cron-Schedule für automatische Ausführung + active: bool # Ob Automation aktiviert ist + eventId: Optional[str] # Event ID für Event-Management + executionLogs: List[Dict[str, Any]] # Logs von Workflow-Ausführungen + # ... weitere Felder +``` + +**WICHTIG**: +- `AutomationDefinition` ist das **Template-Objekt**, das im Workflow-Editor bearbeitet wird +- `template` Feld enthält JSON-String mit Workflow-Struktur (Tasks und Actions als Vorlage) +- `placeholders` enthält Werte für Platzhalter im Template +- Wird verwendet, um `ChatWorkflow`-Instanzen zu erstellen + +### Document Dependencies + +Actions können `documentList` Parameter haben, die auf Ergebnisse von anderen Actions verweisen: + +**Formate**: +- `docList:label` - Referenz zu einem Document-Label aus einer vorherigen Action +- `docList:messageId:label` - Cross-Round Referenz mit Message-ID + +**Beispiel**: +```json +{ + "execMethod": "sharepoint", + "execAction": "readDocuments", + "execParameters": { + "connectionReference": "{{connectionName}}", + "documentList": ["docList:emails_found"] // Verweist auf execResultLabel "emails_found" + } +} +``` + +## Workflow-Editor UI-Konzept + +### Layout-Struktur + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Workflow Editor - [Template Name] │ +├─────────────────────────────────────────────────────────────┤ +│ [Toolbar] [Save] [Validate] [Preview] [Help] │ +├─────────────────────────────────────────────────────────────┤ +│ ┌──────────────────┐ ┌─────────────────────────────────┐ │ +│ │ Toolbox │ │ Canvas (Graph Editor) │ │ +│ │ │ │ │ │ +│ │ 📋 Tasks │ │ ┌─────────┐ │ │ +│ │ ┌─────────────┐ │ │ │ Task 1 │ │ │ +│ │ │ + Task │ │ │ └────┬───┘ │ │ +│ │ └─────────────┘ │ │ │ │ │ +│ │ │ │ ┌────▼───┐ │ │ +│ │ ⚙️ Actions │ │ │Action 1 │ │ │ +│ │ ┌─────────────┐ │ │ └────┬───┘ │ │ +│ │ │ outlook │ │ │ │ │ │ +│ │ │ sharepoint │ │ │ ┌────▼───┐ │ │ +│ │ │ ai │ │ │ │Action 2│ │ │ +│ │ │ ... │ │ │ └────────┘ │ │ +│ │ └─────────────┘ │ │ │ │ +│ │ │ │ ┌─────────┐ │ │ +│ │ 🔗 Connections │ │ │ Task 2 │ │ │ +│ │ (Drag to link) │ │ └─────────┘ │ │ +│ └──────────────────┘ └─────────────────────────────────┘ │ +├─────────────────────────────────────────────────────────────┤ +│ [Properties Panel] - Selected: Task 1 │ +│ ┌───────────────────────────────────────────────────────┐ │ +│ │ Task Properties: │ │ +│ │ ID: task_1 │ │ +│ │ User Input: {{userPrompt}} │ │ +│ │ Dependencies: [task_0] │ │ +│ │ │ │ +│ │ Actions: │ │ +│ │ [Action 1] [Action 2] [+ Add Action] │ │ +│ └───────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────┘ +``` + +### Komponenten + +#### 1. Toolbox (Links) +- **Tasks**: Drag & Drop für neue Tasks +- **Actions**: Gruppiert nach Methoden (outlook, sharepoint, ai, etc.) +- **Connections**: Visueller Hinweis für Dependency-Verbindungen + +#### 2. Canvas (Mitte) +- **Graph-Editor**: Visualisierung des Workflows +- **Nodes**: Tasks (große Boxen) und Actions (kleinere Boxen innerhalb von Tasks) +- **Edges**: Verbindungen zwischen Tasks (Dependencies) und Actions (Document-Flows) + +#### 3. Properties Panel (Unten) +- **Task Properties**: Wenn Task selektiert +- **Action Properties**: Wenn Action selektiert +- **Parameter Editor**: Dynamisches Formular basierend auf `WorkflowActionParameter` Definitionen + +## Graphische Darstellung + +### Task-Node +``` +┌─────────────────────────────────────┐ +│ 📋 Task 1 │ +│ ───────────────────────────────── │ +│ User Input: {{userPrompt}} │ +│ │ +│ ┌─────────────┐ ┌─────────────┐ │ +│ │ Action 1.1 │ │ Action 1.2 │ │ +│ │ outlook. │ │ sharepoint. │ │ +│ │ readEmails │ │ uploadDoc │ │ +│ └─────────────┘ └─────────────┘ │ +│ │ +│ Dependencies: [Task 0] │ +└─────────────────────────────────────┘ +``` + +### Action-Node +``` +┌─────────────────────────┐ +│ ⚙️ outlook.readEmails │ +│ ─────────────────────── │ +│ connectionRef: {{...}} │ +│ query: "unread" │ +│ limit: 50 │ +│ │ +│ Result Label: emails │ +│ ─────────────────────── │ +│ [Edit] [Delete] │ +└─────────────────────────┘ +``` + +### Dependency-Verbindungen + +**Task Dependencies** (gestrichelte Linie): +``` +Task 0 ──┐ + │ (depends on) + ▼ + Task 1 +``` + +**Document Flow** (durchgezogene Linie mit Label): +``` +Action 1.1 (emails) ──[docList:emails]──► Action 2.1 +``` + +## Datenmodell für Editor + +### WorkflowTemplateModel (neues Datenmodell) + +```python +class WorkflowTemplateModel(BaseModel): + """Template-Modell für graphischen Editor""" + + # Metadaten + name: str = Field(description="Template name") + description: Optional[str] = Field(None, description="Template description") + version: str = Field(default="1.0", description="Template version") + + # Graph-Struktur + tasks: List[TaskTemplateModel] = Field( + default_factory=list, + description="List of task templates" + ) + + # Placeholders (werden aus Template extrahiert) + placeholders: Dict[str, str] = Field( + default_factory=dict, + description="Placeholder definitions with default values" + ) + + # UI-Metadaten (für Editor) + uiMetadata: Optional[Dict[str, Any]] = Field( + None, + description="UI metadata (node positions, zoom level, etc.)" + ) + +class TaskTemplateModel(BaseModel): + """Task-Template für Editor""" + + id: str = Field(description="Task ID (unique within template)") + label: str = Field(description="Task label/name") + userInput: str = Field( + description="User input template (can contain placeholders like {{userPrompt}})" + ) + dependencies: List[str] = Field( + default_factory=list, + description="List of task IDs this task depends on" + ) + actions: List[ActionTemplateModel] = Field( + default_factory=list, + description="List of actions in this task" + ) + + # UI-Metadaten + position: Optional[Dict[str, float]] = Field( + None, + description="Node position in editor: {x: number, y: number}" + ) + +class ActionTemplateModel(BaseModel): + """Action-Template für Editor""" + + id: str = Field(description="Action ID (unique within task)") + method: str = Field(description="Method name (e.g., 'outlook', 'sharepoint')") + action: str = Field(description="Action name (e.g., 'readEmails', 'uploadDocument')") + parameters: Dict[str, Any] = Field( + default_factory=dict, + description="Action parameters (can contain placeholders and document references)" + ) + resultLabel: Optional[str] = Field( + None, + description="Result label for document output (used in documentList references)" + ) + + # Document Dependencies (automatisch aus parameters extrahiert) + documentDependencies: List[str] = Field( + default_factory=list, + description="List of result labels this action depends on (from documentList parameter)" + ) + + # UI-Metadaten + position: Optional[Dict[str, float]] = Field( + None, + description="Node position within task: {x: number, y: number}" + ) +``` + +## Konvertierung: Template ↔ Editor-Modell + +### AutomationDefinition.template → Editor-Modell + +```python +def parseTemplateToEditorModel(templateJson: str) -> WorkflowTemplateModel: + """ + Parse AutomationDefinition.template (JSON string) to WorkflowTemplateModel + + INPUT: AutomationDefinition.template (JSON-String) + OUTPUT: WorkflowTemplateModel (Editor-Modell) + + Template-Format (aktuell in AutomationDefinition.template): + { + "tasks": [ + { + "id": "task_1", + "userInput": "{{userPrompt}}", + "dependencies": [], + "actions": [ + { + "method": "outlook", + "action": "readEmails", + "parameters": { + "connectionReference": "{{connectionName}}", + "query": "unread", + "limit": 50 + }, + "resultLabel": "emails" + } + ] + } + ] + } + """ + templateData = json.loads(templateJson) + + tasks = [] + for taskData in templateData.get("tasks", []): + actions = [] + for actionData in taskData.get("actions", []): + # Extract document dependencies from documentList parameter + documentDeps = [] + if "documentList" in actionData.get("parameters", {}): + docList = actionData["parameters"]["documentList"] + if isinstance(docList, list): + for ref in docList: + if isinstance(ref, str) and ref.startswith("docList:"): + # Extract label: docList:label or docList:messageId:label + parts = ref.split(":") + if len(parts) >= 2: + label = parts[-1] # Last part is always the label + documentDeps.append(label) + + actions.append(ActionTemplateModel( + id=actionData.get("id", f"action_{len(actions)}"), + method=actionData["method"], + action=actionData["action"], + parameters=actionData.get("parameters", {}), + resultLabel=actionData.get("resultLabel"), + documentDependencies=documentDeps + )) + + tasks.append(TaskTemplateModel( + id=taskData["id"], + label=taskData.get("label", taskData["id"]), + userInput=taskData.get("userInput", ""), + dependencies=taskData.get("dependencies", []), + actions=actions + )) + + # Extract placeholders from template + placeholders = extractPlaceholders(templateJson) + + return WorkflowTemplateModel( + name="Template", + tasks=tasks, + placeholders=placeholders + ) +``` + +### Editor-Modell → AutomationDefinition.template + +```python +def convertEditorModelToTemplate( + editorModel: WorkflowTemplateModel, + placeholders: Dict[str, str] +) -> str: + """ + Convert WorkflowTemplateModel to AutomationDefinition.template JSON string + + INPUT: WorkflowTemplateModel (Editor-Modell) + placeholders Dict + OUTPUT: JSON-String für AutomationDefinition.template Feld + """ + templateData = { + "tasks": [] + } + + for task in editorModel.tasks: + taskData = { + "id": task.id, + "userInput": task.userInput, + "dependencies": task.dependencies, + "actions": [] + } + + for action in task.actions: + # Build documentList from documentDependencies + parameters = action.parameters.copy() + if action.documentDependencies: + documentList = [f"docList:{label}" for label in action.documentDependencies] + parameters["documentList"] = documentList + + actionData = { + "method": action.method, + "action": action.action, + "parameters": parameters + } + + if action.resultLabel: + actionData["resultLabel"] = action.resultLabel + + taskData["actions"].append(actionData) + + templateData["tasks"].append(taskData) + + return json.dumps(templateData, indent=2) +``` + +## UI-Implementierung + +### Technologie-Stack + +**Empfehlung**: +- **Graph-Library**: [Cytoscape.js](https://js.cytoscape.org/) oder [React Flow](https://reactflow.dev/) oder [JointJS](https://www.jointjs.com/) +- **Framework**: Vanilla JavaScript (konsistent mit bestehendem Frontend) oder React (falls Migration geplant) +- **Styling**: CSS mit bestehenden Styles aus `formGeneric.js` + +### HTML-Struktur + +```html + + + + Workflow Editor + + + +
+ +
+ + + +
+ + +
+ +
+
+

Tasks

+
+ Task +
+
+ +
+

Actions

+
+
Outlook
+
+ readEmails +
+
+ sendEmail +
+
+
+
SharePoint
+
+ readDocuments +
+ +
+
+
+ + +
+ +
+ + +
+
+

Properties

+
+
+

Select a node to edit properties

+
+
+
+ + +
+

Placeholders

+
+
+
+ + + + +``` + +### JavaScript-Architektur + +```javascript +// workflow-editor.js + +class WorkflowEditor { + constructor(containerId, initialTemplate = null) { + this.container = document.getElementById(containerId); + this.graph = null; // Graph-Library Instanz + this.model = null; // WorkflowTemplateModel + this.selectedNode = null; + + this.init(); + if (initialTemplate) { + this.loadTemplate(initialTemplate); + } + } + + init() { + this.initGraph(); + this.initToolbox(); + this.initPropertiesPanel(); + this.initEventHandlers(); + } + + initGraph() { + // Initialize graph library (z.B. Cytoscape.js) + this.graph = cytoscape({ + container: document.getElementById('editor-canvas'), + elements: [], + style: [ + { + selector: 'node[type="task"]', + style: { + 'shape': 'roundrectangle', + 'width': 200, + 'height': 150, + 'background-color': '#e8f4f8', + 'label': 'data(label)', + 'text-valign': 'top', + 'text-margin-y': 10 + } + }, + { + selector: 'node[type="action"]', + style: { + 'shape': 'roundrectangle', + 'width': 150, + 'height': 80, + 'background-color': '#fff4e6', + 'label': 'data(label)', + 'text-valign': 'center' + } + }, + { + selector: 'edge[type="task-dependency"]', + style: { + 'line-style': 'dashed', + 'target-arrow-shape': 'triangle', + 'curve-style': 'bezier' + } + }, + { + selector: 'edge[type="document-flow"]', + style: { + 'line-style': 'solid', + 'target-arrow-shape': 'triangle', + 'label': 'data(label)', + 'curve-style': 'bezier', + 'line-color': '#4a90e2' + } + } + ], + layout: { + name: 'dagre', + rankDir: 'TB', + spacingFactor: 1.5 + } + }); + + // Event handlers + this.graph.on('tap', 'node', (evt) => { + this.selectNode(evt.target); + }); + + this.graph.on('tap', (evt) => { + if (evt.target === this.graph) { + this.deselectNode(); + } + }); + } + + loadTemplate(templateJson) { + // Parse template to editor model + this.model = parseTemplateToEditorModel(templateJson); + + // Render graph + this.renderGraph(); + + // Update placeholders panel + this.updatePlaceholdersPanel(); + } + + renderGraph() { + const elements = []; + + // Add task nodes + this.model.tasks.forEach((task, taskIndex) => { + // Task node + elements.push({ + data: { + id: `task_${task.id}`, + type: 'task', + label: task.label || task.id, + taskId: task.id, + nodeType: 'task' + }, + position: task.position || { x: 100, y: taskIndex * 200 + 100 } + }); + + // Action nodes within task + task.actions.forEach((action, actionIndex) => { + const actionNodeId = `action_${task.id}_${action.id}`; + elements.push({ + data: { + id: actionNodeId, + type: 'action', + label: `${action.method}.${action.action}`, + taskId: task.id, + actionId: action.id, + nodeType: 'action' + }, + position: action.position || { + x: 100 + actionIndex * 180, + y: taskIndex * 200 + 150 + } + }); + + // Parent-child relationship (action belongs to task) + elements.push({ + data: { + id: `edge_${task.id}_${action.id}`, + source: `task_${task.id}`, + target: actionNodeId, + type: 'contains' + } + }); + }); + }); + + // Add task dependencies + this.model.tasks.forEach(task => { + task.dependencies.forEach(depId => { + elements.push({ + data: { + id: `dep_${depId}_${task.id}`, + source: `task_${depId}`, + target: `task_${task.id}`, + type: 'task-dependency' + } + }); + }); + }); + + // Add document flow edges + this.model.tasks.forEach(task => { + task.actions.forEach(action => { + action.documentDependencies.forEach(depLabel => { + // Find source action with matching resultLabel + const sourceAction = this.findActionByResultLabel(depLabel); + if (sourceAction) { + elements.push({ + data: { + id: `doc_${sourceAction.id}_${action.id}`, + source: `action_${sourceAction.taskId}_${sourceAction.id}`, + target: `action_${task.id}_${action.id}`, + type: 'document-flow', + label: depLabel + } + }); + } + }); + }); + }); + + this.graph.elements().remove(); + this.graph.add(elements); + this.graph.layout({ name: 'dagre' }).run(); + } + + selectNode(node) { + this.selectedNode = node; + const nodeData = node.data(); + + if (nodeData.nodeType === 'task') { + this.showTaskProperties(nodeData.taskId); + } else if (nodeData.nodeType === 'action') { + this.showActionProperties(nodeData.taskId, nodeData.actionId); + } + + // Highlight selected node + this.graph.elements().removeClass('selected'); + node.addClass('selected'); + } + + showActionProperties(taskId, actionId) { + const task = this.model.tasks.find(t => t.id === taskId); + const action = task?.actions.find(a => a.id === actionId); + + if (!action) return; + + // Load action schema from API + this.loadActionSchema(action.method, action.action).then(schema => { + const propertiesHtml = this.renderActionPropertiesForm(action, schema); + document.getElementById('properties-content').innerHTML = propertiesHtml; + + // Bind form handlers + this.bindActionFormHandlers(taskId, actionId, schema); + }); + } + + async loadActionSchema(method, actionName) { + // Fetch from /api/workflows/actions/{method}/{actionName} + const response = await fetch(`/api/workflows/actions/${method}/${actionName}`); + return await response.json(); + } + + renderActionPropertiesForm(action, schema) { + let html = ` +
+
${action.method}.${action.action}
+
+ + + Label for document output (used in documentList references) +
+
+ + `; + + // Render parameter fields based on schema + Object.entries(schema.parameters || {}).forEach(([paramName, paramDef]) => { + html += this.renderParameterField(paramName, paramDef, action.parameters[paramName]); + }); + + html += ` +
+
+ + +
+
+ `; + + return html; + } + + renderParameterField(paramName, paramDef, currentValue) { + const value = currentValue !== undefined ? currentValue : (paramDef.default || ''); + const valueStr = typeof value === 'object' ? JSON.stringify(value) : String(value); + + let html = `
`; + html += ``; + + switch (paramDef.frontendType) { + case 'text': + case 'textarea': + html += ``; + break; + case 'number': + html += ``; + break; + case 'select': + case 'userConnection': + case 'documentReference': + html += ``; + break; + case 'checkbox': + html += ``; + break; + default: + html += ``; + } + + html += `${paramDef.description || ''}`; + html += `
`; + + return html; + } + + saveActionProperties(actionId) { + const task = this.model.tasks.find(t => + t.actions.some(a => a.id === actionId) + ); + const action = task?.actions.find(a => a.id === actionId); + + if (!action) return; + + // Collect parameter values from form + const parameters = {}; + // ... collect from form fields ... + + // Update action + action.parameters = parameters; + action.resultLabel = document.getElementById('action-result-label').value; + + // Update document dependencies + action.documentDependencies = this.extractDocumentDependencies(parameters); + + // Re-render graph to show updated connections + this.renderGraph(); + } + + extractDocumentDependencies(parameters) { + const deps = []; + if (parameters.documentList && Array.isArray(parameters.documentList)) { + parameters.documentList.forEach(ref => { + if (typeof ref === 'string' && ref.startsWith('docList:')) { + const parts = ref.split(':'); + if (parts.length >= 2) { + deps.push(parts[parts.length - 1]); // Last part is label + } + } + }); + } + return deps; + } + + saveTemplate() { + // Convert editor model to template JSON + const templateJson = convertEditorModelToTemplate( + this.model, + this.getPlaceholders() + ); + + // Save via API to AutomationDefinition (Template-Objekt) + // WICHTIG: Dies speichert das Template, nicht das ausführbare ChatWorkflow + return this.saveToAutomationDefinition(templateJson); + } + + validateTemplate() { + const errors = []; + + // Check: All tasks have at least one action + this.model.tasks.forEach(task => { + if (task.actions.length === 0) { + errors.push(`Task "${task.id}" has no actions`); + } + }); + + // Check: All document dependencies resolve to valid result labels + this.model.tasks.forEach(task => { + task.actions.forEach(action => { + action.documentDependencies.forEach(depLabel => { + const sourceAction = this.findActionByResultLabel(depLabel); + if (!sourceAction) { + errors.push(`Action "${action.method}.${action.action}" depends on unknown result label "${depLabel}"`); + } + }); + }); + }); + + // Check: Circular dependencies + const circularDeps = this.detectCircularDependencies(); + if (circularDeps.length > 0) { + errors.push(`Circular dependencies detected: ${circularDeps.join(', ')}`); + } + + // Check: Required parameters are set + // ... weitere Validierungen ... + + return { + valid: errors.length === 0, + errors: errors + }; + } + + findActionByResultLabel(resultLabel) { + for (const task of this.model.tasks) { + for (const action of task.actions) { + if (action.resultLabel === resultLabel) { + return { ...action, taskId: task.id }; + } + } + } + return null; + } + + detectCircularDependencies() { + // Graph-based cycle detection + // ... Implementation ... + return []; + } +} + +// Initialize editor +let editor; +document.addEventListener('DOMContentLoaded', () => { + const templateJson = getInitialTemplate(); // From formAutomations.js + editor = new WorkflowEditor('workflow-editor', templateJson); +}); +``` + +## Integration mit formAutomations.js + +### Erweiterte Automation-Form + +```javascript +// In formAutomations.js + +// Add "Edit Template" button to automation form +function initAutomationForm() { + // ... existing code ... + + // Add workflow editor button + const templateField = form.querySelector('#entity-template'); + if (templateField) { + const editorButton = document.createElement('button'); + editorButton.type = 'button'; + editorButton.className = 'btn btn-secondary'; + editorButton.innerHTML = ' Open Workflow Editor'; + editorButton.addEventListener('click', () => { + openWorkflowEditor(templateField.value); + }); + templateField.parentNode.appendChild(editorButton); + } +} + +function openWorkflowEditor(templateJson) { + // Open workflow editor in modal or new tab + const modal = document.createElement('div'); + modal.className = 'workflow-editor-modal'; + modal.innerHTML = ` +
+
+

Workflow Editor

+ +
+
+
+
+ +
+ `; + + document.body.appendChild(modal); + + // Initialize editor + const editor = new WorkflowEditor('workflow-editor-container', templateJson); + + // Save handler + document.getElementById('workflow-editor-save').addEventListener('click', () => { + const updatedTemplate = editor.saveTemplate(); + // Update template field in form + document.querySelector('#entity-template').value = updatedTemplate; + modal.remove(); + }); + + // Cancel handler + document.getElementById('workflow-editor-cancel').addEventListener('click', () => { + modal.remove(); + }); +} +``` + +## API-Endpunkte + +### GET /api/workflows/actions +Liefert alle verfügbaren Actions (bereits im RBAC-Konzept definiert). + +### GET /api/workflows/actions/{method}/{action} +Liefert Action-Schema mit Parameter-Definitionen: + +```json +{ + "method": "outlook", + "action": "readEmails", + "actionId": "outlook.readEmails", + "description": "Read emails from Outlook mailbox", + "parameters": { + "connectionReference": { + "name": "connectionReference", + "type": "str", + "frontendType": "userConnection", + "frontendOptions": "user.connection", + "required": true, + "description": "Microsoft connection label" + }, + "query": { + "name": "query", + "type": "str", + "frontendType": "text", + "required": false, + "description": "Search query for emails" + } + } +} +``` + +### GET /api/automations/{automationId} +Liefert AutomationDefinition für Editor. + +**Zweck**: Lädt AutomationDefinition (Template) für Bearbeitung im Editor +**Rückgabe**: `AutomationDefinition` Objekt mit `template` (JSON-String) und `placeholders` + +### POST /api/automations/{automationId} +Speichert AutomationDefinition (Template). + +**Zweck**: Speichert bearbeitetes Template zurück in AutomationDefinition +**Input**: `AutomationDefinition` Objekt mit aktualisiertem `template` (JSON-String) und `placeholders` +**Hinweis**: Dies ist das Template-Objekt, nicht das ausführbare ChatWorkflow-Objekt + +## Validierung + +### Template-Validierung + +1. **Struktur-Validierung**: + - Alle Tasks haben mindestens eine Action + - Alle Task-Dependencies verweisen auf existierende Tasks + - Keine zirkulären Dependencies + +2. **Action-Validierung**: + - Alle Actions haben gültige method/action Kombinationen + - Alle required Parameters sind gesetzt + - Alle documentList Referenzen verweisen auf existierende resultLabels + +3. **Placeholder-Validierung**: + - Alle Placeholders im Template sind in placeholders-Dict definiert + - Placeholder-Format: `{{PLACEHOLDER_NAME}}` + +## Beispiel-Workflow + +### Visuelle Darstellung + +``` +┌─────────────────────────────────────┐ +│ 📋 Task 1: Read Emails │ +│ ───────────────────────────────── │ +│ User Input: {{userPrompt}} │ +│ │ +│ ┌───────────────────────────────┐ │ +│ │ ⚙️ outlook.readEmails │ │ +│ │ connectionRef: {{conn}} │ │ +│ │ query: "unread" │ │ +│ │ Result: emails │ │ +│ └───────────────┬───────────────┘ │ +└──────────────────┼─────────────────┘ + │ docList:emails + ▼ +┌─────────────────────────────────────┐ +│ 📋 Task 2: Process Documents │ +│ ───────────────────────────────── │ +│ Dependencies: [Task 1] │ +│ │ +│ ┌───────────────────────────────┐ │ +│ │ ⚙️ ai.process │ │ +│ │ documentList: [docList:emails]│ │ +│ │ aiPrompt: {{processPrompt}} │ │ +│ │ Result: processed │ │ +│ └───────────────────────────────┘ │ +└─────────────────────────────────────┘ +``` + +### Generiertes Template JSON + +```json +{ + "tasks": [ + { + "id": "task_1", + "userInput": "{{userPrompt}}", + "dependencies": [], + "actions": [ + { + "method": "outlook", + "action": "readEmails", + "parameters": { + "connectionReference": "{{conn}}", + "query": "unread", + "limit": 50 + }, + "resultLabel": "emails" + } + ] + }, + { + "id": "task_2", + "userInput": "Process emails", + "dependencies": ["task_1"], + "actions": [ + { + "method": "ai", + "action": "process", + "parameters": { + "documentList": ["docList:emails"], + "aiPrompt": "{{processPrompt}}", + "resultType": "json" + }, + "resultLabel": "processed" + } + ] + } + ] +} +``` + +## Nächste Schritte + +1. **Graph-Library auswählen**: Cytoscape.js vs. React Flow vs. JointJS +2. **Editor-Komponenten implementieren**: Toolbox, Canvas, Properties Panel +3. **API-Integration**: Action-Schemas laden, Templates speichern +4. **Validierung implementieren**: Dependency-Checking, Parameter-Validierung +5. **Placeholder-Management**: UI für Placeholder-Definition +6. **Testing**: Unit-Tests für Konvertierungs-Logik + +## Offene Fragen + +1. **Graph-Library**: Welche Library bevorzugt? (Cytoscape.js ist gut für komplexe Graphen, React Flow ist moderner) +2. **Positionierung**: Sollen Node-Positionen gespeichert werden oder automatisch layouted? +3. **Placeholder-UI**: Separate UI für Placeholder-Management oder integriert in Editor? +4. **Versionierung**: Sollen Templates versioniert werden? +