wiki/c-work/4-done/2026-04-generic-graph-editor.md
ValueOn AG 93edc94da3 upd
2026-04-10 22:44:27 +02:00

694 lines
31 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!-- status: done -->
<!-- lastReviewed: 2026-04-10 -->
# Typed Node Handover System — Spezifikation & Execution Plan
**Ansatz: Greenfield.** Keine Rückwärtskompatibilität, keine Migration, kein Deprecation.
---
## 1 Problemstellung
Der Graphical Editor hat keine formale Typisierung der Daten zwischen Nodes. Jede Node produziert ein freiformiges Dict; die Folge-Node extrahiert heuristisch. Kern-Symptome:
- `actionNodeExecutor.py` (~860 Zeilen, >50 % handover-Heuristik pro Node-Paar)
- `outputPreviewRegistry.ts` hartcodiert Beispiel-Schemas, die von Laufzeit-Outputs abweichen
- Keine `data.transform`, `data.filter`, `data.aggregate`, `flow.merge` implementiert
- Frontend baut pro Node-Typ eine eigene Config-Component (12+ Dateien)
- Keine System-Variablen (Timestamp, Datum, User, etc.)
Die Method/Action-Schicht hat bereits `WorkflowActionParameter` mit `FrontendType`, `_validateType`, `_validateParameters`. Dieses System wird für den Graphen übernommen und erweitert.
---
## 2 Architektur
### 2.1 Port-Schema (Kern-Datenmodell)
Jede Node deklariert **typisierte Ports** für Ein- und Ausgänge sowie **Parameter** für die Konfiguration:
```
┌──────────────────────────────────────────────────────────────────────┐
│ Node Definition (Beispiel: email.draftEmail) │
│ │
│ inputPorts: │
│ [0]: { accepts: [EmailDraft, AiResult] } │
│ │
│ outputPorts: │
│ [0]: { schema: ActionResult } │
│ │
│ parameters: │
│ connectionId: { type: str, frontendType: USER_CONNECTION } │
│ subject: { type: str, frontendType: TEXT } │
│ body: { type: str, frontendType: TEXTAREA } │
│ to: { type: List[str], frontendType: TEXT } │
└──────────────────────────────────────────────────────────────────────┘
```
### 2.2 Port-Schema Pydantic-Modelle
**Neue Datei:** `gateway/modules/features/graphicalEditor/portTypes.py`
```python
class PortField(BaseModel):
name: str
type: str # str, int, bool, List[str], List[Document], Dict[str,Any]
description: Dict[str, str] # {en, de, fr}
required: bool = True
class PortSchema(BaseModel):
name: str # z.B. "EmailDraft", "AiResult", "Transit"
fields: List[PortField]
class InputPortDef(BaseModel):
accepts: List[str] # Liste akzeptierter Schema-Namen
class OutputPortDef(BaseModel):
schema: str # Schema-Name aus dem Katalog
dynamic: bool = False # True = Schema wird aus Parametern abgeleitet
deriveFrom: Optional[str] = None # Parameter-Name für dynamische Ableitung
```
### 2.3 Port-Typen-Katalog
| Port-Typ | Felder | Produziert von | Konsumiert von |
|----------|--------|----------------|----------------|
| `DocumentList` | `documents: List[{name, data, mimeType, metadata}]` | sharepoint.readFile, sharepoint.downloadFile, ai.*, file.create, input.upload | ai.*, file.create, email.draftEmail, sharepoint.uploadFile |
| `FileList` | `files: List[{url, name, path, size}]` | sharepoint.listFiles, sharepoint.findFile | sharepoint.readFile, sharepoint.downloadFile, flow.loop |
| `EmailDraft` | `subject: str, body: str, to: List[str], cc?: List[str], attachments?: List[Document]` | ai.prompt (outputFormat=emailDraft) | email.draftEmail |
| `EmailList` | `emails: List[{subject, from, to, body, date, attachments}]` | email.checkEmail, email.searchEmail | ai.prompt, flow.loop |
| `TaskList` | `tasks: List[{id, name, status, url}]` | clickup.searchTasks, clickup.listTasks | flow.loop, clickup.updateTask |
| `TaskResult` | `success: bool, taskId: str, task: {id, name, status}` | clickup.createTask, clickup.updateTask | flow.ifElse |
| `FormPayload` | `payload: Dict[str, Any]` (dynamisch) | trigger.form, input.form | Alle (via Feld-Referenz) |
| `AiResult` | `prompt: str, response: str, responseData?: Dict, context: str, documents: List[Document]` | ai.* | email.draftEmail, file.create, sharepoint.uploadFile |
| `BoolResult` | `result: bool, reason?: str` | input.approval, input.confirmation | flow.ifElse |
| `TextResult` | `text: str` | input.comment | ai.*, file.create |
| `LoopItem` | `currentItem: Any, currentIndex: int, items: List[Any], count: int` | flow.loop (pro Iteration) | Loop-Body Nodes |
| `AggregateResult` | `items: List[Any], count: int` | data.aggregate | Alle |
| `MergeResult` | `inputs: Dict[int, Any], first: Any, merged: Dict` | flow.merge | Alle |
Jeder Port-Typ bekommt implizit zwei Meta-Felder: **`_success: bool`** und **`_error: str?`**. Wenn ein Node fehlschlägt, liefert der Normalizer `{ _success: false, _error: "...", ...restliche Felder leer/default }`.
### 2.4 Transit-Typ
Flow-Control-Nodes transformieren keine Daten — sie routen. Transit bedeutet: **Output-Schema = Input-Schema des Upstream-Produzenten**.
Transit-Nodes produzieren ein Envelope:
```python
{
"_transit": True,
"_meta": { "branch": 0, "conditionResult": True }, # Routing-Metadaten
"data": <upstream-output-unverändert>,
}
```
| Node | Output-Typ | _meta-Felder |
|------|-----------|-------------|
| `flow.ifElse` | Transit | `branch: int`, `conditionResult: bool` |
| `flow.switch` | Transit | `match: int`, `value: str` |
| `data.filter` | Transit | `originalCount: int`, `filteredCount: int` |
**Transit-Auflösung** (rekursiv): DataPicker und Engine folgen `_transit`-Kette bis zum echten Produzenten. Multi-Output-Ports (ifElse hat 2): Transit-Auflösung berücksichtigt `sourceOutput`-Index, beide Ports zeigen dasselbe Upstream-Schema.
`flow.loop` ist **kein** Transit — er transformiert eine Liste in `LoopItem` pro Iteration.
### 2.5 Dynamische Schemas
Nodes mit konfigurationsabhängigem Output (Formulare, data.transform):
```python
{
"id": "input.form",
"outputPorts": {
0: {"schema": "FormPayload", "dynamic": True, "deriveFrom": "fields"},
},
}
```
Schema-Ableitung: die Engine und das Frontend leiten aus dem Parameter `fields` die konkreten Output-Felder ab. Jedes Formular-Feld erscheint als wählbares Attribut im DataPicker.
---
## 3 Handover-Mechanismen
### 3.1 Prioritätsreihenfolge
Wenn ein Parameter-Wert aus mehreren Quellen kommen kann:
**DataRef/SystemVar/Static > Wire-Handover > Default**
1. Wire-Handover: Extraktor füllt Input-Felder aus dem Upstream-Output.
2. DataRef/System/Static: überschreiben Wire-Werte (= explizite User-Wahl gewinnt).
3. Defaults aus Parameter-Definition, falls noch leer.
### 3.2 Mechanismus 1: DataRef (Hauptmechanismus, ~80 %)
User wählt im DataPicker, welches Feld eines Vorgängers in welchen Parameter fließt:
```
email.draftEmail.parameters:
subject = DataRef { nodeId: "ai_1", path: ["responseData", "subject"] }
body = DataRef { nodeId: "ai_1", path: ["responseData", "body"] }
to = DataRef { nodeId: "form_1", path: ["payload", "recipient"] }
```
### 3.3 Mechanismus 2: Wire-Handover (Extraktoren)
Pro **Input-Port-Typ** ein Extraktor (~12 Funktionen, nicht N×M):
```python
INPUT_EXTRACTORS: Dict[str, Callable] = {
"EmailDraft": _extractEmailDraft,
"DocumentList": _extractDocuments,
"TextResult": _extractText,
...
}
```
Extraktor kennt das Upstream-Schema (aus `outputPorts`) und greift Felder direkt ab — kein Raten mit Fallbacks.
### 3.4 Mechanismus 3: data.transform
Expliziter Konverter-Node für Fälle, wo DataRef und Extraktor nicht reichen.
### 3.5 Mechanismus 4: System-Variablen
Dritter DynamicValue-Typ neben `ref` und `value`:
```python
SYSTEM_VARIABLES = {
"system.timestamp": { "type": "int", "description": "Unix timestamp (ms)" },
"system.date": { "type": "str", "description": "ISO date (YYYY-MM-DD)" },
"system.datetime": { "type": "str", "description": "ISO datetime" },
"system.time": { "type": "str", "description": "HH:MM:SS" },
"system.userId": { "type": "str", "description": "Aktueller User-ID" },
"system.userName": { "type": "str", "description": "Aktueller User-Name" },
"system.userEmail": { "type": "str", "description": "Aktueller User-Email" },
"system.workflowId": { "type": "str", "description": "Workflow-ID" },
"system.runId": { "type": "str", "description": "Run-ID" },
"system.instanceId": { "type": "str", "description": "Feature-Instanz-ID" },
"system.mandateId": { "type": "str", "description": "Mandant-ID" },
"system.loopIndex": { "type": "int", "description": "Aktueller Loop-Index (nur in Loop)" },
"system.loopCount": { "type": "int", "description": "Anzahl Loop-Items (nur in Loop)" },
"system.uuid": { "type": "str", "description": "Neue zufällige UUID" },
}
```
---
## 4 Generisches Frontend-Rendering
### 4.1 Prinzip: Jede UI-Darstellung ist ein FrontendType
Es gibt **keine** Node-spezifischen Config-Components. `NODE_CONFIG_REGISTRY` entfällt komplett.
### 4.2 FrontendType-Katalog
```python
class FrontendType(str, Enum):
# Standard Types
TEXT = "text"
TEXTAREA = "textarea"
NUMBER = "number"
CHECKBOX = "checkbox"
DATE = "date"
DATETIME = "datetime"
EMAIL = "email"
SELECT = "select"
MULTISELECT = "multiselect"
JSON = "json"
FILE = "file"
HIDDEN = "hidden"
# Picker Types (API-backed)
USER_CONNECTION = "userConnection"
SHAREPOINT_FOLDER = "sharepointFolder"
SHAREPOINT_FILE = "sharepointFile"
CLICKUP_LIST = "clickupList"
CLICKUP_TASK = "clickupTask"
# Complex Structure Types
CASE_LIST = "caseList"
FIELD_BUILDER = "fieldBuilder"
KEY_VALUE_ROWS = "keyValueRows"
CRON = "cron"
CONDITION = "condition"
MAPPING_TABLE = "mappingTable"
FILTER_EXPRESSION = "filterExpression"
```
### 4.3 Parameter-Abhängigkeiten
Picker-Parameter die von anderen Parametern desselben Nodes abhängen:
```python
{
"name": "path",
"type": "str",
"frontendType": "sharepointFolder",
"frontendOptions": {"dependsOn": "connectionId"},
}
```
Der generische Renderer übergibt den Wert des abhängigen Parameters. Der Picker rendert sich disabled, solange die Abhängigkeit nicht erfüllt ist.
### 4.4 Generischer Renderer
```typescript
const FRONTEND_TYPE_RENDERERS: Record<string, ComponentType<FieldRendererProps>> = {
text: TextInput,
textarea: TextareaInput,
number: NumberInput,
checkbox: CheckboxInput,
date: DatePicker,
select: SelectInput,
multiselect: MultiSelectInput,
json: JsonEditor,
userConnection: ConnectionPicker,
sharepointFolder: FolderPicker,
clickupList: ClickUpListPicker,
caseList: CaseListEditor,
fieldBuilder: FieldBuilderEditor,
keyValueRows: KeyValueRowsEditor,
cron: CronBuilder,
condition: ConditionBuilder,
mappingTable: MappingTableEditor,
filterExpression: FilterExpressionEditor,
};
function GenericNodeConfig({ node, nodeType }) {
return nodeType.parameters.map(param => {
const Renderer = FRONTEND_TYPE_RENDERERS[param.frontendType] ?? TextInput;
return <Renderer key={param.name} param={param} value={...} />;
});
}
```
---
## 5 Neue Nodes
### 5.1 data.aggregate
```python
{
"id": "data.aggregate",
"category": "data",
"label": {"en": "Aggregate", "de": "Sammeln", "fr": "Agréger"},
"parameters": [
{"name": "mode", "type": "str", "frontendType": "select",
"options": ["collect", "concat", "sum", "count"], "default": "collect"},
],
"inputPorts": {0: {"accepts": ["Transit"]}},
"outputPorts": {0: {"schema": "AggregateResult"}},
"executor": "data",
}
```
**Engine-Semantik:** Wenn data.aggregate im Loop-Body steht, wird ihr Output **nicht** pro Iteration überschrieben, sondern in `_aggregateAccumulators[nodeId]` gesammelt. Nach Loop-Ende: `nodeOutputs[nodeId] = { items: akkumulator, count: len }`.
### 5.2 data.transform
```python
{
"id": "data.transform",
"category": "data",
"label": {"en": "Transform", "de": "Umwandeln", "fr": "Transformer"},
"parameters": [
{"name": "mappings", "type": "json", "frontendType": "mappingTable"},
],
"inputPorts": {0: {"accepts": ["Transit"]}},
"outputPorts": {0: {"schema": "Dict", "dynamic": True, "deriveFrom": "mappings"}},
"executor": "data",
}
```
### 5.3 data.filter
```python
{
"id": "data.filter",
"category": "data",
"label": {"en": "Filter", "de": "Filtern", "fr": "Filtrer"},
"parameters": [
{"name": "condition", "type": "str", "frontendType": "filterExpression"},
],
"inputPorts": {0: {"accepts": ["AggregateResult", "FileList", "TaskList", "EmailList", "DocumentList"]}},
"outputPorts": {0: {"schema": "Transit"}},
"executor": "data",
}
```
**Filter-Expression-Sprache:** Vergleiche (`==`, `!=`, `<`, `>`, `<=`, `>=`), Logik (`and`, `or`, `not`), String (`contains`, `startsWith`), Null (`isEmpty`, `isNotEmpty`). Der `FILTER_EXPRESSION`-Renderer baut einen visuellen Condition-Builder (Dropdown Feld → Operator → Wert).
### 5.4 flow.merge
```python
{
"id": "flow.merge",
"category": "flow",
"label": {"en": "Merge", "de": "Zusammenführen", "fr": "Fusionner"},
"parameters": [
{"name": "mode", "type": "str", "frontendType": "select",
"options": ["first", "all", "append"], "default": "first"},
],
"inputs": 2,
"outputs": 1,
"inputPorts": {0: {"accepts": ["Transit"]}, 1: {"accepts": ["Transit"]}},
"outputPorts": {0: {"schema": "MergeResult"}},
"executor": "flow",
"meta": {"icon": "mdi-call-merge", "color": "#FF9800"},
}
```
**Engine-Semantik:** flow.merge wird erst ausgeführt, wenn alle verbundenen Vorgänger verarbeitet sind. Für inaktive Branches (nach ifElse): der übersprungene Vorgänger ist nicht in `nodeOutputs` → wird als „nicht verfügbar" markiert. mode=first nimmt den ersten verfügbaren, mode=all merged nur die verfügbaren.
---
## 6 Entscheidungen
| # | Frage | Entscheidung |
|---|-------|-------------|
| 1 | Typ-Mismatch bei Kante? | **Soft** — Warnung + visuelle Markierung, kein Hard-Block. |
| 2 | Transit statt Any für Flow-Nodes? | **Ja.** Transit-Envelope mit `_meta` und `data`. DataPicker resolved rekursiv. |
| 3 | Dynamische Schemas? | **Ja.** `deriveFrom`-Parameter → Schema-Ableitung. |
| 4 | Migration? | **Nein.** Greenfield. |
| 5 | data.aggregate separat? | **Ja.** Separater Node. |
| 6 | flow.merge? | **Ja, sofort.** Komplettes Set. |
| 7 | `_paramMap` Zukunft? | **Entfällt.** Node-Parameter = Action-Parameter-Namen. UI-Labels separat in `description`. |
| 8 | Parameter vs Wire Priorität? | **DataRef > Wire > Default.** |
| 9 | Fehler-Output? | Jeder Port-Typ hat `_success`/`_error` Meta-Felder. |
| 10 | AI Structured Output? | `AiResult.responseData: Dict` + Parameter `outputFormat` (text/json). |
| 11 | Pause/Resume? | Resume-Handler validiert User-Eingabe gegen Output-Schema der pausierten Node. |
---
## 7 Execution Plan — Backend
### 7.1 NEU: `gateway/modules/features/graphicalEditor/portTypes.py`
Erstellen. Inhalt:
- `PortField`, `PortSchema`, `InputPortDef`, `OutputPortDef` (Pydantic-Modelle, Abschnitt 2.2)
- `PORT_TYPE_CATALOG`: Dict aller PortSchemas (Abschnitt 2.3)
- `SYSTEM_VARIABLES`: Dict (Abschnitt 3.5)
- Output-Normalizer: `_normalizeToSchema(raw: Dict, schemaName: str) -> Dict` + pro Port-Typ eine Funktion
- Input-Extraktoren: `INPUT_EXTRACTORS: Dict[str, Callable]` (Abschnitt 3.3)
- Transit-Envelope-Helpers: `_wrapTransit(data, meta)`, `_unwrapTransit(output)`, `_resolveTransitChain(nodeId, nodeOutputs, connectionMap)`
- Schema-Ableitungsfunktionen: `_deriveFormPayloadSchema(node)`, `_deriveTransformSchema(node)` (Abschnitt 2.5)
### 7.2 ÄNDERN: `gateway/modules/shared/frontendTypes.py`
**Aktuell:** `FrontendType` Enum mit 14 Werten (Zeilen 1663).
**Änderung:** Erweitern um Complex Structure Types:
```
Hinzufügen:
SHAREPOINT_FILE = "sharepointFile"
CLICKUP_LIST = "clickupList"
CLICKUP_TASK = "clickupTask"
CASE_LIST = "caseList"
FIELD_BUILDER = "fieldBuilder"
KEY_VALUE_ROWS = "keyValueRows"
CRON = "cron"
CONDITION = "condition"
MAPPING_TABLE = "mappingTable"
FILTER_EXPRESSION = "filterExpression"
```
`CUSTOM_TYPE_OPTIONS_API` und `CUSTOM_TYPE_DESCRIPTIONS` entsprechend erweitern.
### 7.3 NEU SCHREIBEN: `gateway/modules/features/graphicalEditor/nodeDefinitions/*.py`
Alle 10 Dateien (`triggers.py`, `flow.py`, `input.py`, `ai.py`, `email.py`, `sharepoint.py`, `clickup.py`, `file.py`, `trustee.py`, `__init__.py`) **komplett neu**.
Jede Node-Definition bekommt:
- `inputPorts`: Dict mit `InputPortDef` pro Port-Index
- `outputPorts`: Dict mit `OutputPortDef` pro Port-Index
- `parameters`: Liste von Dicts im `WorkflowActionParameter`-Format (mit `frontendType`, `frontendOptions` inkl. `dependsOn`)
- **Kein** `_paramMap` mehr — Parameter-Namen = Action-Parameter-Namen
- `_method` und `_action` bleiben (für ActionExecutor-Mapping)
NEU: `data.py` mit `data.aggregate`, `data.transform`, `data.filter`.
`__init__.py`: `STATIC_NODE_TYPES` um `DATA_NODES` erweitern; `flow.py` um `flow.merge` erweitern.
### 7.4 ÄNDERN: `gateway/modules/features/graphicalEditor/nodeRegistry.py`
**Funktion `getNodeTypesForApi`** (Zeile 5475):
- `_localizeNode` anpassen: `inputPorts` und `outputPorts` in API-Response einschließen (nicht als `_`-Prefix, also nicht wegstrippen)
- Response erweitern um `systemVariables` aus `portTypes.SYSTEM_VARIABLES`
- Response erweitern um `portTypeCatalog` aus `portTypes.PORT_TYPE_CATALOG`
**Funktion `getNodeTypeToMethodAction`** (Zeile 7889): bleibt, `_method`/`_action` weiterhin verwendet.
### 7.5 ÄNDERN: `gateway/modules/workflows/automation2/graphUtils.py`
**Funktion `resolveParameterReferences`** (Zeile 185243):
- Neuen Case hinzufügen für `type: "system"`:
```python
if isinstance(value, dict) and value.get("type") == "system":
return _resolveSystemVariable(value["variable"], context)
```
- `_resolveSystemVariable(variable, context)` als neue Funktion (Abschnitt 3.5).
**Funktion `validateGraph`** (Zeile 86120): erweitern um Port-Kompatibilitäts-Check (soft — Warnings sammeln, nicht hart ablehnen).
### 7.6 NEU SCHREIBEN: `gateway/modules/workflows/automation2/executors/actionNodeExecutor.py`
**Komplett neu.** Die ~860 Zeilen mit heuristischen Merge-Funktionen (`_extractEmailContentFromUpstream`, `_getContextFromUpstream`, `_gatherAttachmentDocumentsFromUpstream`, `_formatEmailOutputAsContext`, `_unpackIncomingEmail`, `_getIncomingEmailFromUpstream`, `_buildActionParams`, `_paramMap`-Logik, etc.) werden **ersetzt** durch:
1. Node-Definition laden (`_getNodeDefinition`)
2. Parameter per `resolveParameterReferences` auflösen (DataRef, SystemVar, Static)
3. Wire-Handover: wenn `inputSources[nodeId][0]` existiert, Extraktor aus `INPUT_EXTRACTORS` aufrufen
4. Priorität anwenden (DataRef > Wire > Default)
5. `ActionExecutor.executeAction(method, action, params)` aufrufen
6. Ergebnis durch `_normalizeToSchema(result, outputSchema)` normalisieren
7. Fehler-Envelope: bei Exception `{ _success: false, _error: str, ...defaults }` zurückgeben
**Keine** node-type-spezifische Logik mehr im Executor.
### 7.7 NEU: `gateway/modules/workflows/automation2/executors/dataExecutor.py`
Neuer Executor für `data.aggregate`, `data.transform`, `data.filter`:
- `data.aggregate`: im Loop-Kontext akkumuliert Ergebnisse (Modus: collect, concat, sum, count)
- `data.transform`: wendet `mappings` an (jedes Mapping löst eine DataRef/Static-Referenz auf)
- `data.filter`: evaluiert `condition` pro Item der Input-Liste, gibt gefilterte Liste zurück
### 7.8 ÄNDERN: `gateway/modules/workflows/automation2/executors/flowExecutor.py`
**Funktion `_ifElse`** (Zeile 5565): Output ändern zu Transit-Envelope:
```python
# Heute:
return {"branch": 0 if ok else 1, "conditionResult": ok, "input": inp}
# Neu:
return {"_transit": True, "_meta": {"branch": 0 if ok else 1, "conditionResult": ok}, "data": inp}
```
**Funktion `_switch`** (Zeile 199207): analog Transit-Envelope.
**Funktion `_loop`** (Zeile 261272): bleibt wie bisher (kein Transit, produziert `LoopItem`).
**NEU: `_merge`** Funktion: sammelt Outputs aller verbundenen Input-Ports aus `nodeOutputs`, wendet Modus an (first/all/append), gibt `MergeResult` zurück.
### 7.9 ÄNDERN: `gateway/modules/workflows/automation2/executors/__init__.py`
`DataExecutor` importieren und exportieren.
### 7.10 ÄNDERN: `gateway/modules/workflows/automation2/executionEngine.py`
**Funktion `_getExecutor`** (Zeile 7185): Case für `data.*` hinzufügen → `DataExecutor`.
**Funktion `_is_node_on_active_path`** (Zeile 3968): Transit-Envelope berücksichtigen — `branch`/`match` jetzt in `out["_meta"]` statt direkt in `out`.
**Loop-Body-Verarbeitung** (Zeile 400460): Spezialbehandlung für `data.aggregate` Nodes: statt `nodeOutputs[bnid] = result` ein `_aggregateAccumulators[bnid].append(result)`. Nach Loop-Ende: `nodeOutputs[aggregateNodeId] = { items: akkumulator, count: len }`.
**Nach jedem Execute** (Zeile 390+): `_normalizeToSchema(result, outputSchema)` aufrufen bevor in `nodeOutputs` gespeichert.
**flow.merge Wartelogik:** merge-Nodes werden von `topoSort` automatisch nach ihren Vorgängern eingeordnet (BFS). Vor Ausführung: prüfe ob alle verbundenen Vorgänger in `nodeOutputs` sind oder übersprungen wurden (inaktiver Branch → `not in nodeOutputs` und Node wurde als skipped geloggt).
**Pause/Resume:** Resume-Handler (`initialNodeOutputs`) — Validierung der User-Eingabe gegen Output-Schema der pausierten Node (in `executeGraph` nach Empfang von `initialNodeOutputs`).
### 7.11 ÄNDERN: `gateway/modules/features/graphicalEditor/routeFeatureGraphicalEditor.py`
**Funktion `get_node_types`** (Zeile 146168): Response-Struktur erweitern:
```python
# Heute:
return {"nodeTypes": localized, "categories": categories}
# Neu:
return {
"nodeTypes": localized, # mit inputPorts, outputPorts
"categories": categories,
"portTypeCatalog": PORT_TYPE_CATALOG, # alle Schemas
"systemVariables": SYSTEM_VARIABLES,
}
```
---
## 8 Execution Plan — Frontend
### 8.1 ÄNDERN: `frontend_nyla/src/api/workflowApi.ts`
**Interface `NodeTypeParameter`** (Zeile 1420): erweitern um `frontendType`, `frontendOptions`, `options`, `validation`.
**Interface `NodeType`** (Zeile 2239): erweitern um `inputPorts`, `outputPorts`.
**Interface `NodeTypesResponse`** (Zeile 4649): erweitern um `portTypeCatalog`, `systemVariables`.
### 8.2 LÖSCHEN + NEU: `frontend_nyla/src/components/FlowEditor/nodes/configs/`
**Löschen:** `index.ts`, `AiNodeConfig.tsx`, `EmailNodeConfig.tsx`, `SharePointNodeConfig.tsx`, `ClickUpNodeConfig.tsx`, `ApprovalNodeConfig.tsx`, `UploadNodeConfig.tsx`, `CommentNodeConfig.tsx`, `ReviewNodeConfig.tsx`, `SelectionNodeConfig.tsx`, `ConfirmationNodeConfig.tsx`, `FileCreateNodeConfig.tsx`, `TrusteeNodeConfig.tsx`, `types.ts`.
**Neu:** `frontendTypeRenderers/` Ordner mit einem Renderer pro FrontendType (TextInput, TextareaInput, NumberInput, CheckboxInput, DatePicker, SelectInput, MultiSelectInput, JsonEditor, ConnectionPicker, FolderPicker, ClickUpListPicker, CaseListEditor, FieldBuilderEditor, KeyValueRowsEditor, CronBuilder, ConditionBuilder, MappingTableEditor, FilterExpressionEditor).
**Neu:** `frontendTypeRenderers/index.ts` mit `FRONTEND_TYPE_RENDERERS` Registry.
Bestehende Logik aus den gelöschten Config-Components (z.B. ConnectionPicker aus `EmailNodeConfig`, CaseEditor aus `SwitchNodeConfig`, FieldBuilder aus `FormNodeConfig`) wird in die entsprechenden FrontendType-Renderer extrahiert.
### 8.3 ÄNDERN: `frontend_nyla/src/components/FlowEditor/editor/NodeConfigPanel.tsx`
**Komplett neu.** Statt `NODE_CONFIG_REGISTRY`-Lookup:
```typescript
import { FRONTEND_TYPE_RENDERERS } from '../nodes/frontendTypeRenderers';
// Iteriere nodeType.parameters, rendere pro Parameter den passenden Renderer
nodeType.parameters.map(param => {
const Renderer = FRONTEND_TYPE_RENDERERS[param.frontendType] ?? TextInput;
return <Renderer param={param} value={...} onChange={...} />;
});
```
### 8.4 LÖSCHEN: `frontend_nyla/src/components/FlowEditor/nodes/shared/outputPreviewRegistry.ts`
Komplett entfernen. Ersetzen durch generische Funktion die aus dem `portTypeCatalog` + `outputPorts` Schema den Preview-Baum baut:
```typescript
function buildPreviewFromSchema(
node: CanvasNode,
nodeTypes: NodeType[],
portTypeCatalog: Record<string, PortSchema>
): Record<string, unknown> {
const nt = nodeTypes.find(t => t.id === node.type);
const outputSchema = nt?.outputPorts?.[0]?.schema;
if (outputSchema === 'Transit') return {}; // resolved dynamisch
const schema = portTypeCatalog[outputSchema];
// Generiere Beispielwerte pro Feld aus schema.fields
}
```
### 8.5 ÄNDERN: `frontend_nyla/src/components/FlowEditor/nodes/shared/dataRef.ts`
**`DynamicValue` Union** (Zeile 20): erweitern um `SystemVarRef`:
```typescript
interface SystemVarRef {
type: 'system';
variable: string;
}
type DynamicValue = DataRef | DataValue | SystemVarRef;
```
`isRef`, `isValue` etc. ergänzen um `isSystemVar` Type Guard.
### 8.6 ÄNDERN: `frontend_nyla/src/components/FlowEditor/nodes/shared/DataPicker.tsx`
- `buildPickablePaths` (Zeile 2040): **Schema-basiert** statt aus Preview-Daten. Erhält `portTypeCatalog` und baut Baum aus Schema-Feldern.
- Transit-Auflösung: wenn Output-Schema = Transit, folge connectionMap rückwärts bis zum echten Produzenten.
- Neue Sektion **„System"** mit allen Variablen aus `systemVariables`, gruppiert (Datum/Zeit, User, Workflow).
- Bei Pick eines System-Werts: `onPick` liefert `SystemVarRef` statt `DataRef`.
### 8.7 ÄNDERN: `frontend_nyla/src/components/FlowEditor/context/Automation2DataFlowContext.tsx`
- `Automation2DataFlowContextValue` (Zeile 1019): erweitern um `portTypeCatalog`, `systemVariables`.
- `nodeOutputsPreview` Berechnung: aus Schema statt aus `outputPreviewRegistry`.
### 8.8 ÄNDERN: `frontend_nyla/src/components/FlowEditor/nodes/shared/graphUtils.ts`
`fromApiGraph` / `toApiGraph`: `inputPorts` und `outputPorts` mitserialisieren in `CanvasNode`.
### 8.9 ÄNDERN: `frontend_nyla/src/components/FlowEditor/editor/FlowCanvas` (Verbindungs-Validierung)
Beim Verbinden zweier Ports: prüfe `sourceNode.outputPorts[outputIdx].schema` gegen `targetNode.inputPorts[inputIdx].accepts`. Bei Mismatch: Kante gelb/orange markieren (Soft-Warnung).
---
## 9 Dateien-Übersicht
### Neue Dateien
| Datei | Inhalt |
|-------|--------|
| `gateway/.../graphicalEditor/portTypes.py` | PortSchema, Katalog, Normalizer, Extraktoren, System-Variablen |
| `gateway/.../automation2/executors/dataExecutor.py` | Executor für data.aggregate, data.transform, data.filter |
| `gateway/.../nodeDefinitions/data.py` | Node-Definitionen: data.aggregate, data.transform, data.filter |
| `frontend_nyla/.../nodes/frontendTypeRenderers/index.ts` | FRONTEND_TYPE_RENDERERS Registry |
| `frontend_nyla/.../nodes/frontendTypeRenderers/*.tsx` | Ein Renderer pro FrontendType |
### Komplett neu geschriebene Dateien
| Datei | Grund |
|-------|-------|
| `gateway/.../automation2/executors/actionNodeExecutor.py` | Heuristische Merge-Logik → Normalizer + Extraktor |
| `gateway/.../nodeDefinitions/triggers.py` | + inputPorts, outputPorts, frontendType |
| `gateway/.../nodeDefinitions/flow.py` | + flow.merge, Transit-Ports |
| `gateway/.../nodeDefinitions/input.py` | + dynamische FormPayload-Schemas |
| `gateway/.../nodeDefinitions/ai.py` | + AiResult-Ports, outputFormat-Param |
| `gateway/.../nodeDefinitions/email.py` | + EmailDraft/EmailList-Ports |
| `gateway/.../nodeDefinitions/sharepoint.py` | + FileList/DocumentList-Ports |
| `gateway/.../nodeDefinitions/clickup.py` | + TaskList/TaskResult-Ports |
| `gateway/.../nodeDefinitions/file.py` | + DocumentList-Ports |
| `gateway/.../nodeDefinitions/trustee.py` | + Ports |
| `gateway/.../nodeDefinitions/__init__.py` | + DATA_NODES Import |
| `frontend_nyla/.../editor/NodeConfigPanel.tsx` | Generischer Renderer statt NODE_CONFIG_REGISTRY |
### Geänderte Dateien
| Datei | Änderung |
|-------|----------|
| `gateway/.../shared/frontendTypes.py` | FrontendType Enum erweitern (+10 Werte) |
| `gateway/.../automation2/graphUtils.py` | `resolveParameterReferences` + SystemVar, `validateGraph` + Port-Check |
| `gateway/.../automation2/executors/flowExecutor.py` | ifElse/switch → Transit-Envelope, + _merge |
| `gateway/.../automation2/executors/__init__.py` | + DataExecutor Export |
| `gateway/.../automation2/executionEngine.py` | + DataExecutor, Transit-_meta, aggregate-Akkumulator, merge-Wartelogik |
| `gateway/.../graphicalEditor/nodeRegistry.py` | API-Response + portTypeCatalog, systemVariables |
| `gateway/.../graphicalEditor/routeFeatureGraphicalEditor.py` | Response-Struktur erweitern |
| `frontend_nyla/.../api/workflowApi.ts` | Interfaces erweitern (inputPorts, outputPorts, frontendType) |
| `frontend_nyla/.../nodes/shared/dataRef.ts` | + SystemVarRef |
| `frontend_nyla/.../nodes/shared/DataPicker.tsx` | Schema-basiert, Transit-Auflösung, System-Sektion |
| `frontend_nyla/.../nodes/shared/graphUtils.ts` | inputPorts/outputPorts serialisieren |
| `frontend_nyla/.../context/Automation2DataFlowContext.tsx` | + portTypeCatalog, systemVariables |
| `frontend_nyla/.../editor/FlowCanvas` | Verbindungs-Validierung |
### Gelöschte Dateien
| Datei | Grund |
|-------|-------|
| `frontend_nyla/.../nodes/configs/index.ts` | Ersetzt durch FRONTEND_TYPE_RENDERERS |
| `frontend_nyla/.../nodes/configs/AiNodeConfig.tsx` | Generischer Renderer |
| `frontend_nyla/.../nodes/configs/EmailNodeConfig.tsx` | Generischer Renderer |
| `frontend_nyla/.../nodes/configs/SharePointNodeConfig.tsx` | Generischer Renderer |
| `frontend_nyla/.../nodes/configs/ClickUpNodeConfig.tsx` | Generischer Renderer |
| `frontend_nyla/.../nodes/configs/ApprovalNodeConfig.tsx` | Generischer Renderer |
| `frontend_nyla/.../nodes/configs/UploadNodeConfig.tsx` | Generischer Renderer |
| `frontend_nyla/.../nodes/configs/CommentNodeConfig.tsx` | Generischer Renderer |
| `frontend_nyla/.../nodes/configs/ReviewNodeConfig.tsx` | Generischer Renderer |
| `frontend_nyla/.../nodes/configs/SelectionNodeConfig.tsx` | Generischer Renderer |
| `frontend_nyla/.../nodes/configs/ConfirmationNodeConfig.tsx` | Generischer Renderer |
| `frontend_nyla/.../nodes/configs/FileCreateNodeConfig.tsx` | Generischer Renderer |
| `frontend_nyla/.../nodes/configs/TrusteeNodeConfig.tsx` | Generischer Renderer |
| `frontend_nyla/.../nodes/shared/outputPreviewRegistry.ts` | Schema-basierte Preview |